{"id":51236585,"url":"https://github.com/go-sqlex/sqlex","last_synced_at":"2026-07-04T11:00:45.061Z","repository":{"id":365247526,"uuid":"1271221356","full_name":"go-sqlex/sqlex","owner":"go-sqlex","description":"A drop-in modernization of jmoiron/sqlx that fixes lexer bugs, automates IN expansion, and adds pluggable hooks — built for Go 1.21+.","archived":false,"fork":false,"pushed_at":"2026-06-23T13:10:55.000Z","size":412,"stargazers_count":20,"open_issues_count":1,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-06-23T15:13:23.613Z","etag":null,"topics":["databases","mysql","postgresql","sql","sqlserver","sqlx"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/go-sqlex.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-16T12:59:26.000Z","updated_at":"2026-06-23T13:11:29.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/go-sqlex/sqlex","commit_stats":null,"previous_names":["go-sqlex/sqlex"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/go-sqlex/sqlex","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/go-sqlex%2Fsqlex","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/go-sqlex%2Fsqlex/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/go-sqlex%2Fsqlex/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/go-sqlex%2Fsqlex/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/go-sqlex","download_url":"https://codeload.github.com/go-sqlex/sqlex/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/go-sqlex%2Fsqlex/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35118971,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-04T02:00:05.987Z","response_time":113,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["databases","mysql","postgresql","sql","sqlserver","sqlx"],"created_at":"2026-06-28T21:00:25.575Z","updated_at":"2026-07-04T11:00:45.048Z","avatar_url":"https://github.com/go-sqlex.png","language":"Go","funding_links":[],"categories":["Utilities"],"sub_categories":["Utility/Miscellaneous"],"readme":"**English** | [中文](README_zh.md)\n\n[![CI](https://github.com/go-sqlex/sqlex/actions/workflows/ci.yml/badge.svg)](https://github.com/go-sqlex/sqlex/actions/workflows/ci.yml)\n[![Go Report Card](https://goreportcard.com/badge/github.com/go-sqlex/sqlex)](https://goreportcard.com/report/github.com/go-sqlex/sqlex)\n[![GoDoc](https://pkg.go.dev/badge/github.com/go-sqlex/sqlex)](https://pkg.go.dev/github.com/go-sqlex/sqlex)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)\n[![Mentioned in Awesome Go](https://awesome.re/mentioned-badge.svg)](https://github.com/avelino/awesome-go)\n\n# sqlex\n\n\u003e A **drop-in replacement** for [jmoiron/sqlx](https://github.com/jmoiron/sqlx) — 100% API-compatible, with added Hook aspects, generic JSON types, bug fixes, and more.\n\n**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.\n\n```diff\n- import \"github.com/jmoiron/sqlx\"\n+ import \"github.com/go-sqlex/sqlex\"\n```\n\nWhat you get for free after migrating:\n\n- 🚀 **Auto-Rebind** — write `?` everywhere, works on PostgreSQL (`$1`), MySQL (`?`), SQLite (`?`), SQL Server (`@p1`). No more manual `db.Rebind()`. Including `Preparex`.\n- 🐛 **SQL parsing fixes** — colons in strings, `::` type casts, `?` in comments are correctly handled. Silent bugs from sqlx are gone.\n- 🎯 **Unified interfaces** — `Ext` / `ExtContext` / `NamedExt` / `BindExt` / `Preparer` / `PreparerContext` with compile-time checks. Write `func f(ext NamedExt)` and pass DB, Tx, or Conn.\n- 🔀 **Auto IN expansion** — slices in `IN (?)` detected and expanded automatically on all methods.\n- 🪝 **Hook system** — pluggable SQL interceptors for logging, tracing, metrics (onion model).\n- 📦 **JSONValue[T]** — generic JSON column type with auto serialize/deserialize.\n- 🛡️ **StrictMode** — lenient by default (matching sqlx `Unsafe()`), optionally strict for debugging.\n- 🛠️ **20+ bug fixes** — data corruption, panics, silent data loss, and cross-database failures from jmoiron/sqlx, all fixed. See [Critical Bug Fixes](#critical-bug-fixes-from-sqlx).\n\n→ [Migration Guide](#migration-from-jmoironsqlx)\n\n## Installation\n\n```bash\ngo get github.com/go-sqlex/sqlex\n```\n\nRequires Go 1.21 or later.\n\n## Migration from jmoiron/sqlx\n\n**30 seconds, 3 steps:**\n\n**1. Change import path:**\n\n```go\n// old\nimport \"github.com/jmoiron/sqlx\"\n\n// new\nimport \"github.com/go-sqlex/sqlex\"\n```\n\n**2. Change package references:**\n\n```go\n// old\ndb, err := sqlx.Connect(\"postgres\", dsn)\n\n// new\ndb, err := sqlex.Connect(\"postgres\", dsn)\n```\n\n**3. Update go.mod:**\n\n```bash\ngo get github.com/go-sqlex/sqlex\n```\n\n**Done.** All your existing sqlx code works without changes.\n\n\u003e **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)`.\n\n### Gradual adoption\n\nNew features are optional — adopt at your own pace:\n\n| Step | Action | Time |\n|------|--------|------|\n| 1 | Replace import path | 30s |\n| 2 | Switch transactions to `CloseWithErr` pattern | per-use |\n| 3 | Use `NamedGet`/`NamedSelect` instead of `NamedQuery` + manual scan | per-use |\n| 4 | Register custom Hooks (logging, tracing, metrics) | as needed |\n\n## Quick Start\n\n```go\npackage main\n\nimport (\n    \"fmt\"\n    \"log\"\n\n    \"github.com/go-sqlex/sqlex\"\n    _ \"github.com/mattn/go-sqlite3\"\n)\n\ntype User struct {\n    ID    int    `db:\"id\"`\n    Name  string `db:\"name\"`\n    Email string `db:\"email\"`\n}\n\nfunc main() {\n    // Connect to database\n    db, err := sqlex.Connect(\"sqlite3\", \":memory:\")\n    if err != nil {\n        log.Fatal(err)\n    }\n    defer db.Close()\n\n    // Create table\n    db.MustExec(`CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)`)\n    db.MustExec(`INSERT INTO users (name, email) VALUES ('Alice', 'alice@example.com')`)\n    db.MustExec(`INSERT INTO users (name, email) VALUES ('Bob', 'bob@example.com')`)\n\n    // Query single row\n    var user User\n    err = db.Get(\u0026user, \"SELECT * FROM users WHERE id = ?\", 1)\n    fmt.Printf(\"User: %+v\\n\", user)\n\n    // Query multiple rows\n    var users []User\n    err = db.Select(\u0026users, \"SELECT * FROM users\")\n    fmt.Printf(\"Users: %+v\\n\", users)\n}\n```\n\n## New Features\n\nsqlex preserves all sqlx APIs and adds the following capabilities:\n\n| Feature | Description |\n|---------|-------------|\n| **Hook aspects** | `AddHook` — pluggable SQL execution interceptors (onion model) |\n| **JSONValue[T]** | `types.JSONValue[T]` — generic JSON column type |\n| **NamedGet/NamedSelect** | Named parameter convenience methods on DB/Tx (built-in IN expansion) |\n| **CloseWithErr** | Auto Commit/Rollback based on error |\n| **Unified interfaces** | `Ext` / `ExtContext` / `NamedExt` / `BindExt` / `Preparer` / `PreparerContext` — DB, Tx, and Conn share identical method signatures with compile-time checks |\n| **Auto IN expansion** | All methods auto-detect slice args and expand IN clauses |\n| **Auto Rebind** | All query methods auto-convert `?` to target database placeholders |\n| **StrictMode** | Optional strict struct-field matching for debugging (off by default) |\n| **Cross-database out of the box** | Write SQL with `?` everywhere — works on PostgreSQL, MySQL, SQLite, SQL Server |\n\n## Critical Bug Fixes from sqlx\n\nsqlex fixes **20+ known bugs** from jmoiron/sqlx — including data corruption, panics, and silent failures:\n\n| Bug | Impact | sqlx Issue |\n|-----|--------|------------|\n| `Select` + `sql.RawBytes` | **Data corruption** — driver buffer reuse across rows silently overwrites scanned data | [#931](https://github.com/jmoiron/sqlx/issues/931) |\n| `In` panic on nil `driver.Valuer` | **Panic** — nil pointer Valuers crash instead of returning NULL | [#952](https://github.com/jmoiron/sqlx/issues/952) |\n| `fixBound` VALUES drops rows | **Silent data loss** — batch INSERT/UPDATE with `VALUES (...)` silently skips rows | [#898](https://github.com/jmoiron/sqlx/issues/898) |\n| `NextResultSet` cache stale | **Data corruption** — multi-result-set scans with different columns produce wrong data | [#857](https://github.com/jmoiron/sqlx/issues/857) |\n| `Rebind` replaces `?` in strings | **Wrong SQL** — `?` inside string literals, comments, identifiers replaced with bind vars | — |\n| Named query colons in strings | **Wrong SQL** — IPv6 addresses, URLs, time formats misidentified as named parameters | [#947](https://github.com/jmoiron/sqlx/issues/947) |\n| `ConnectContext` connection leak | **Resource leak** — connection not closed on Ping failure | — |\n| PostgreSQL `::` type cast | **Wrong SQL** — `::int` misidentified as named parameter | [#428](https://github.com/jmoiron/sqlx/issues/428) |\n| Named queries fail on PostgreSQL | **Cross-DB broken** — Named methods don't Rebind, fail on `$N` databases | — |\n| `IN(?)` not expanded on `Exec`/`Queryx` | **Runtime error** — slice args not expanded on some methods | — |\n| Unified SQL lexer | **Root cause** — original has duplicated, inconsistent skip logic in `Rebind`/`In`/`compileNamedQuery`. sqlex uses shared `scanSkipSegment` | — |\n\n\u003e Additional fixes: escaped `??`/`\\?` in Rebind, `db:\"-\"` skip in Named, missing field strict mode checks, `NamedStmt.Exec` return type, named parameter fallback tolerance ([#892](https://github.com/jmoiron/sqlx/issues/892)), and more.\n\n## Usage Examples\n\n### Basic CRUD\n\n```go\n// Use ? placeholders universally — the framework auto-converts\n// to target database bindvar format ($N, :argN, @pN)\n\n// Insert\nresult, err := db.Exec(\"INSERT INTO users (name, email) VALUES (?, ?)\", \"Alice\", \"alice@example.com\")\n\n// Query single row → struct\nvar user User\nerr = db.Get(\u0026user, \"SELECT * FROM users WHERE id = ?\", 1)\n\n// Query multiple rows → slice\nvar users []User\nerr = db.Select(\u0026users, \"SELECT * FROM users WHERE age \u003e ?\", 18)\n\n// Update\n_, err = db.Exec(\"UPDATE users SET name = ? WHERE id = ?\", \"Alice Updated\", 1)\n\n// Delete\n_, err = db.Exec(\"DELETE FROM users WHERE id = ?\", 1)\n```\n\n### Named Parameter Queries\n\n```go\n// Using struct as parameter\nuser := User{Name: \"Alice\", Email: \"alice@example.com\"}\n_, err = db.NamedExec(`INSERT INTO users (name, email) VALUES (:name, :email)`, user)\n\n// Using map as parameter\nparams := map[string]any{\"name\": \"Alice\"}\n\n// NamedGet — query single row\nvar result User\nerr = db.NamedGet(\u0026result, `SELECT * FROM users WHERE name = :name`, params)\n\n// NamedSelect — query multiple rows\nvar results []User\nerr = db.NamedSelect(\u0026results, `SELECT * FROM users WHERE name = :name`, params)\n\n// NamedQuery — return *Rows for manual iteration\nrows, err := db.NamedQuery(`SELECT * FROM users WHERE name = :name`, params)\ndefer rows.Close()\nfor rows.Next() {\n    var u User\n    rows.StructScan(\u0026u)\n}\n```\n\n### IN Queries\n\n```go\nids := []int{1, 2, 3, 4, 5}\n\n// Positional: auto-detects slice and expands IN\nvar users []User\nerr = db.Select(\u0026users, \"SELECT * FROM users WHERE id IN (?)\", ids)\n\n// Named: built-in IN expansion\nerr = db.NamedSelect(\u0026users,\n    `SELECT * FROM users WHERE id IN (:ids) AND status = :status`,\n    map[string]any{\"ids\": ids, \"status\": \"active\"})\n```\n\n\u003e 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.\n\n#### Slice argument handling (IN list context recognition)\n\nsqlex 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:\n\n1. **Strict `(?)` form**: only one `?` and optional ASCII whitespace (space/Tab/newline/CR) between `(` and `)`\n2. **The complete identifier immediately before `(` is the `IN` keyword** (case-insensitive); `NOT IN (?)` also matches\n\nOther `(?)` contexts (`ANY(?)` / `ALL(?)` / `VALUES (?)` / `func(?)` / scalar subquery `= (?)` etc.) are treated as single values — **no need for `AsValue` escape hatch**.\n\n**Detection rules**:\n\n| SQL pattern | Argument | Behavior | Notes |\n|---|---|---|---|\n| `WHERE id IN (?)` | `[]int{1,2,3}` | Expand | Preceded by IN |\n| `WHERE id NOT IN (?)` | `[]int{1,2,3}` | Expand | NOT IN still matches |\n| `WHERE id IN (\\n  ?\\n)` | `[]int{1,2,3}` | Expand | Multi-line IN (?) |\n| `WHERE x = ANY(?)` | `[]int{1,2,3}` | No expand | Preceded by ANY, not IN |\n| `INSERT ... VALUES (?)` | `[]int{1,2,3}` | No expand | Preceded by VALUES, not IN |\n| `SELECT func(?)` | `[]int{1,2,3}` | No expand | Preceded by function name |\n| `WHERE x = (?)` | `[]int{1,2,3}` | No expand | Preceded by `=`, not IN |\n| `WHERE col_in (?)` | `[]int{1,2,3}` | No expand | Full token is `col_in`, not IN |\n| `IN (?, ?, ?)` | `1, 2, 3` scalars | No expand | Multiple `?` → user already expanded |\n| `WHERE x = ?` | `[]int{1,2,3}` | No expand | `?` not in `(?)` form |\n\n**Escape hatch APIs**:\n\n```go\nimport \"github.com/go-sqlex/sqlex\"\n\n// ① sqlex.AsValue(v) — force no expansion (even in IN (?) context)\ndb.Select(\u0026rows, `SELECT * FROM t WHERE id IN (?)`,\n    sqlex.AsValue([]int{1, 2, 3})) // entire slice as single value to driver\n\n// ② sqlex.AsList(slice) — force expansion (even outside IN (?) context)\ndb.Select(\u0026rows, `SELECT * FROM t WHERE id = ANY(?)`,\n    sqlex.AsList([]int{1, 2, 3})) // force expand to ?, ?, ?\n\n// ③ Other native approaches still work\ndb.Exec(`INSERT INTO users (tags) VALUES (?)`, pq.Array([]int{1, 2, 3})) // driver.Valuer\ndata, _ := json.Marshal([]int{1, 2, 3})\ndb.Exec(`INSERT INTO t (json_col) VALUES (?)`, data) // []byte is a standard driver type\n```\n\n\u003e Note: `ANY(?)` / `VALUES (?)` etc. now default to **no expansion** — just pass the slice directly or wrap with `pq.Array`. No `AsValue` needed.\n\n**Priority** (high to low):\n\n1. `sqlex.AsValue(v)` / `sqlex.AsList(s)` — explicit declaration, highest priority\n2. `driver.Valuer` interface (including `pq.Array`) — treated as single value\n3. `[]byte` — standard driver type, treated as single value\n4. `IN (?)` context match + slice — auto-expand\n5. Other positions + slice — no expansion, passed as single value (driver will likely error)\n\n**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.\n\n**Empty slice handling** (context-sensitive):\n\n| Scenario | Behavior |\n|---|---|\n| `IN (?)` context + `[]int{}` | Error `sqlex: empty slice cannot be expanded into IN ()` (IN () is invalid SQL) |\n| Non-IN context (`WHERE x = ?` / `VALUES (?)`) + `[]int{}` | OK, entire slice passed to driver |\n| `sqlex.AsValue([]int{})` | OK (already single-value semantics) |\n| `sqlex.AsList([]int{})` | Error `sqlex.AsList: empty slice` (expanding to nothing is meaningless) |\n\n#### Named parameter name rules \u0026 lexical context\n\nNamed parameter `:name` rule: `[A-Za-z_][A-Za-z0-9_.]*` (letter/underscore start, digits/underscore/dot allowed; dots for nested fields like `:user.name`).\n\n| Pattern | Recognized? | Notes |\n|---|---|---|\n| `:name` / `:user_id` / `:arg1` | ✅ | Standard named parameter |\n| `:user.name` | ✅ | Dot-nested field |\n| `:123` / `:1` | ❌ preserved as literal | Digit-start rejected (avoids Oracle `:N` / SQLite `?NNN` conflicts) |\n| `:名字` (Unicode) | ❌ preserved as literal | ASCII-only param names (matches `db` tag / map key convention) |\n| `::int` (PG type cast) | ❌ preserved as literal | `::` recognized as type cast, not parameter |\n| `:=` (assignment) | ❌ preserved as literal | Output as-is |\n\n**Lexical scanning**: `:name` / `?` inside these regions are skipped (shared `lexer.go` scanner):\n- Single-quoted strings `'...'` (with `''` escapes), double-quoted identifiers `\"...\"`, backtick identifiers `` `...` ``\n- Dollar-quoted strings `$$...$$` / `$tag$...$tag$`\n- Line comments `-- ...`, block comments `/* ... */`\n\n\u003e 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.\n\n### Prepared Statements\n\n```go\n// Preparex auto-Rebinds — use ? uniformly across all databases\nstmt, err := db.Preparex(\"SELECT * FROM users WHERE name = ?\")\ndefer stmt.Close() // Stmt must be Closed to avoid resource leaks\nvar user User\nerr = stmt.Get(\u0026user, \"Alice\")\n\n// Also works within transactions\ntx, _ := db.Beginx()\nstmt, err = tx.Preparex(\"SELECT * FROM users WHERE age \u003e ?\")\ndefer stmt.Close()\nvar users []User\nerr = stmt.Select(\u0026users, 18)\n\n// PreparexContext — context-aware version\nctx := context.Background()\nstmt, err = db.PreparexContext(ctx, \"SELECT * FROM users WHERE name = ?\")\ndefer stmt.Close()\nvar user2 User\nerr = stmt.Get(\u0026user2, \"Alice\")\n\n// PrepareNamed — named prepared statement (use :name uniformly, framework handles binding)\nnstmt, err := db.PrepareNamed(\"SELECT * FROM users WHERE name = :name\")\ndefer nstmt.Close()\nerr = nstmt.Get(\u0026user, map[string]any{\"name\": \"Alice\"})\n\n// PreparerContext — write generic prepare functions accepting DB/Tx/Conn\nfunc prepareQuery(p sqlex.PreparerContext) (*sqlex.Stmt, error) {\n    return sqlex.PreparexContext(context.Background(), p, \"SELECT * FROM users WHERE name = ?\")\n}\n```\n\n\u003e **Unified experience**: `Preparex`/`PreparexContext` auto-Rebind like all other query methods, using `?` uniformly.\n\u003e `PrepareNamed`/`PrepareNamedContext` use named parameters `:name`; the framework handles binding internally.\n\u003e\n\u003e **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`.\n\u003e\n\u003e **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()`.\n\u003e\n\u003e **Hook coverage**: `Stmt`/`NamedStmt` `Exec`/`Query` methods also fire Hooks. Hooks are auto-propagated from the parent DB/Tx/Conn to the Stmt.\n\n### Transaction Management\n\n```go\n// Recommended pattern: CloseWithErr auto-management\nfunc createUserWithProfile(db *sqlex.DB, user User, profile Profile) (err error) {\n    tx, err := db.Beginx()\n    if err != nil {\n        return err\n    }\n    defer func() { tx.CloseWithErr(err) }() // auto Commit or Rollback\n\n    _, err = tx.NamedExec(`INSERT INTO users (name) VALUES (:name)`, user)\n    if err != nil {\n        return err // CloseWithErr detects err != nil, auto Rollback\n    }\n\n    _, err = tx.NamedExec(`INSERT INTO profiles (user_name, bio) VALUES (:user_name, :bio)`, profile)\n    return nil // CloseWithErr detects err == nil, auto Commit\n}\n```\n\n### JSONValue[T]\n\n```go\nimport \"github.com/go-sqlex/sqlex/types\"\n\ntype Article struct {\n    ID       int                            `db:\"id\"`\n    Title    string                         `db:\"title\"`\n    Metadata types.JSONValue[ArticleMeta]   `db:\"metadata\"`\n}\n\ntype ArticleMeta struct {\n    Tags      []string `json:\"tags\"`\n    ViewCount int      `json:\"view_count\"`\n}\n\n// Write — auto-serializes to JSON\narticle := Article{\n    Title:    \"Hello World\",\n    Metadata: types.NewJSONValue(ArticleMeta{\n        Tags:      []string{\"go\", \"sql\"},\n        ViewCount: 0,\n    }),\n}\ndb.NamedExec(`INSERT INTO articles (title, metadata) VALUES (:title, :metadata)`, article)\n\n// Read — auto-deserializes\nvar a Article\ndb.Get(\u0026a, \"SELECT * FROM articles WHERE id = ?\", 1)\nif a.Metadata.Valid {\n    fmt.Println(a.Metadata.Val.Tags) // [\"go\", \"sql\"]\n}\n// Val is zero value when !Valid (guaranteed by Scan/zero-value init)\n// Marshal/Unmarshal (implements json.Marshaler/Unmarshaler)\ndata, _ := json.Marshal(a.Metadata)\njson.Unmarshal(data, \u0026a.Metadata)\n```\n\n### Hook Aspects\n\n```go\n// Custom Hook — e.g., OpenTelemetry tracing\ntype TracingHook struct{}\n\nfunc (h *TracingHook) BeforeQuery(ctx context.Context, event *sqlex.QueryEvent) context.Context {\n    ctx, span := tracer.Start(ctx, \"sql.\"+event.OperationType)\n    span.SetAttributes(attribute.String(\"db.statement\", event.Query))\n    return ctx\n}\n\nfunc (h *TracingHook) AfterQuery(ctx context.Context, event *sqlex.QueryEvent) {\n    span := trace.SpanFromContext(ctx)\n    if event.Error != nil {\n        span.RecordError(event.Error)\n    }\n    span.End()\n}\n\ndb.AddHook(\u0026TracingHook{})\n\n// Hook covers the full lifecycle: query/exec/begin/commit/rollback\n// Transaction operations (Begin/Commit/Rollback) also fire Hooks, regardless of success or failure\ntx, _ := db.Beginx()       // → Hook(OpBegin)\n// tx queries also fire Hooks\ntx.CloseWithErr(nil)       // → Hook(OpCommit) or Hook(OpRollback)\n```\n\n#### QueryEvent Fields\n\n```go\ntype QueryEvent struct {\n    Query         string        // SQL statement\n    Args          []any         // execution parameters\n    Duration      time.Duration // total elapsed time (includes Hook chain overhead)\n    Error         error         // execution error (available in AfterQuery phase)\n    OperationType OpType        // operation type: OpQuery/OpExec/OpBegin/OpCommit/OpRollback\n    RowsAffected  int64         // rows affected (only for exec operations)\n    LastInsertID  int64         // last inserted auto-increment ID (only for exec)\n}\n```\n\n#### Conditional Hook Filtering\n\nsqlex does not ship a built-in filter; use the decorator pattern to compose your own:\n\n```go\n// Only fire on slow queries\nfunc SlowOnly(h sqlex.Hook, threshold time.Duration) sqlex.Hook {\n    return \u0026slowHook{hook: h, threshold: threshold}\n}\ntype slowHook struct {\n    hook      sqlex.Hook\n    threshold time.Duration\n}\nfunc (h *slowHook) BeforeQuery(ctx context.Context, e *sqlex.QueryEvent) context.Context {\n    return h.hook.BeforeQuery(ctx, e)\n}\nfunc (h *slowHook) AfterQuery(ctx context.Context, e *sqlex.QueryEvent) {\n    if e.Duration \u003e= h.threshold {\n        h.hook.AfterQuery(ctx, e)\n    }\n}\n\n// Only fire on errors\nfunc OnError(h sqlex.Hook) sqlex.Hook { /* BeforeQuery passthrough, AfterQuery checks e.Error != nil */ }\n\ndb.AddHook(SlowOnly(\u0026AlertHook{}, 500*time.Millisecond))\n```\n\n### StrictMode\n\n```go\n// Default: lenient mode (strict=false), silently ignores extra columns\ndb, _ := sqlex.Connect(\"postgres\", dsn)\nfmt.Println(db.IsStrict()) // false\n\n// Enable strict mode: returns detailed error on field mismatch\ndb.SetStrict(true)\nerr = db.Select(\u0026users, \"SELECT * FROM users\")\n// err: missing destination name email (index 2), age (index 3) in UserPartial\n\n// strict auto-propagates to Tx/Conn\ntx, _ := db.Beginx()    // inherits DB's strict setting\nconn, _ := db.Connx(ctx) // inherits DB's strict setting\n```\n\n### Unified Interfaces\n\nDB, 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\":\n\n| Interface | Methods | Purpose |\n|-----------|---------|---------|\n| `Ext` | `Exec`, `Queryx`, `QueryRowx` | Basic query/execution |\n| `ExtContext` | `ExecContext`, `QueryxContext`, `QueryRowxContext` | Context-aware variants |\n| `NamedExt` | `NamedExec`, `NamedQuery`, `NamedGet`, `NamedSelect` | Named parameter queries |\n| `BindExt` | `BindNamed`, `Get`, `Select`, `Rebind`, `DriverName` | Positional parameter queries |\n| `Preparer` | `Preparex`, `PrepareNamed` | Prepared statement creation |\n| `PreparerContext` | `PreparexContext`, `PrepareNamedContext` | Context-aware preparation |\n\n```go\n// Accept DB, Tx, or Conn via NamedExt\nfunc getUserByName(ext sqlex.NamedExt, name string) (*User, error) {\n    var user User\n    err := ext.NamedGet(\u0026user, `SELECT * FROM users WHERE name = :name`,\n        map[string]any{\"name\": name})\n    return \u0026user, err\n}\n\nuser, err := getUserByName(db, \"Alice\")\ntx, _ := db.Beginx()\nuser, err = getUserByName(tx, \"Bob\")\nconn, _ := db.Connx(ctx)\nuser, err = getUserByName(conn, \"Charlie\")\n```\n\n## Comparison with jmoiron/sqlx\n\n| Feature | jmoiron/sqlx | sqlex |\n|---------|-------------|-------|\n| Go version | 1.10+ | 1.21+ |\n| Struct scanning | ✅ | ✅ |\n| Named queries | ✅ | ✅ |\n| Bindvar conversion | ✅ | ✅ (enhanced: supports `\\?` and `??` escape, skips string literals, identifiers, comments, PG dollar quoting) |\n| IN clause expansion | ✅ `In()` | ✅ Auto-IN across all DB/Tx/Conn × Exec/Query/Select/Get/Named* paths |\n| Cross-database placeholders | ❌ Manual Rebind | ✅ All methods auto-Rebind, use `?` uniformly (including `Preparex`) |\n| Field matching | `unsafe` (default strict) | `StrictMode` (default lenient, more intuitive) |\n| Hook aspects | ❌ | ✅ `AddHook` pluggable SQL interceptors |\n| JSONValue[T] | ❌ | ✅ `types.JSONValue[T]` |\n| NamedGet/NamedSelect | ❌ | ✅ DB/Tx convenience methods |\n| CloseWithErr | ❌ | ✅ Auto transaction management |\n| Unified interfaces | ❌ DB/Tx methods overlap but no shared interface | ✅ `Ext` / `ExtContext` / `NamedExt` / `BindExt` / `Preparer` / `PreparerContext` — DB/Tx/Conn unified with compile-time checks |\n| Unicode named parameters | ⚠️ Unreliable | ❌ Not supported (ASCII only; Unicode elsewhere is safe) |\n| PostgreSQL `::` | ❌ Misidentified | ✅ Correctly handled |\n| Named query string literals | ❌ Colons misidentified | ✅ Skips colons in strings/comments |\n| Named parameter fallback | ❌ Errors on misidentification | ✅ Missing params preserved as `:name` literals |\n\n## Testing\n\n```bash\n# 1) Main package unit tests (no DB dependency, fastest)\ngo test -count=1 -timeout=120s .\n\n# 2) cross_db MySQL only (isolate PG/SQLite for debugging)\nSQLX_POSTGRES_DSN=skip SQLX_SQLITE_DSN=skip \\\n  go test -count=1 -timeout=300s ./tests/cross_db/\n\n# 3) cross_db PostgreSQL only\nSQLX_MYSQL_DSN=skip SQLX_SQLITE_DSN=skip \\\n  go test -count=1 -timeout=300s ./tests/cross_db/\n\n# 4) cross_db SQLite only (no external dependencies)\nSQLX_MYSQL_DSN=skip SQLX_POSTGRES_DSN=skip \\\n  go test -count=1 -timeout=120s ./tests/cross_db/\n\n# 5) cross_db all drivers (CI recommended)\ngo test -count=1 -timeout=300s ./tests/cross_db/\n\n# 6) integration tests\ngo test -count=1 -timeout=120s ./tests/integration/\n\n# 7) pg-specific tests (PostgreSQL unique features)\ngo test -count=1 -timeout=120s ./tests/pg/\n\n# 8) types / reflectx subpackages\ngo test -count=1 -timeout=60s ./types/ ./reflectx/\n```\n\n**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.\n\n**DSN configuration**: Write complete DSNs in `.env.test` using the `SQLX_*_DSN` namespace. Set to `skip` to skip that driver. SQLite defaults to `:memory:`.\n\n| Env var | Value | Behavior |\n|---------|-------|----------|\n| `SQLX_MYSQL_DSN` | Full DSN | Uses this DSN |\n| `SQLX_MYSQL_DSN` | `skip` or empty | Skips MySQL tests |\n| `SQLX_POSTGRES_DSN` | Same | |\n| `SQLX_SQLITE_DSN` | Same (default `:memory:`) | |\n\n### Single test debugging\n\n```bash\n# Run a single test function\ngo test -count=1 -timeout=60s -run \"TestNextPlaceholder\" -v .\n\n# Run a single sub-test\ngo test -count=1 -timeout=60s -run \"TestNextPlaceholder/multiline_IN\" -v .\n\n# Race detection\ngo test -count=1 -race -timeout=180s .\n\n# Coverage\ngo test -count=1 -cover -coverprofile=cover.out -timeout=120s .\ngo tool cover -html=cover.out\n\n# Benchmarks\ngo test -bench=. -benchmem -run=NoSuch -benchtime=2s .\n```\n\n## Performance\n\n- **Prepared statements**: `Preparex`/`PreparexContext` auto-Rebinds, unifying `?` placeholders regardless of database driver\n- **Zero-overhead principle**: No Hook overhead when unregistered; auto-Rebind is a no-op for `QUESTION`-type drivers (MySQL/SQLite)\n- **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\n- **Slice arg detection**: `needsInRewrite` uses reflection type checks (nanosecond-level for non-slice args)\n- **Mapper caching**: Field mapping results cached after first use\n- **Hook execution**: Hooks run synchronously; use lightweight operations or async for heavy ones\n\n### About NameMapper\n\n`NameMapper` is a global variable that controls field-name-to-column-name mapping, defaulting to `strings.ToLower`.\n\n\u003e **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()`.\n\n## License\n\n[MIT License](LICENSE)\n\nBased on [jmoiron/sqlx](https://github.com/jmoiron/sqlx) — thanks to Jason Moiron for the excellent work.\n\nPlease read our [Contributing Guide](CONTRIBUTING.md) before submitting a pull request.  \nSee [CHANGELOG.md](CHANGELOG.md) for version history.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgo-sqlex%2Fsqlex","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fgo-sqlex%2Fsqlex","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgo-sqlex%2Fsqlex/lists"}