From 7aed11b4612b0d268532638b9167f0ef0fbc7785 Mon Sep 17 00:00:00 2001 From: Gianmarco Fantinuoli Date: Tue, 4 Nov 2025 17:09:06 +0100 Subject: [PATCH 1/2] Fix: add missing `unsafe` in `Tx.Stmtx` The `Tx` type can wrap a statement, the returned statement was missing the `unsafe` flag. --- sqlx.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sqlx.go b/sqlx.go index 8259a4f..2422336 100644 --- a/sqlx.go +++ b/sqlx.go @@ -483,7 +483,7 @@ func (tx *Tx) Stmtx(stmt interface{}) *Stmt { default: panic(fmt.Sprintf("non-statement type %v passed to Stmtx", reflect.ValueOf(stmt).Type())) } - return &Stmt{Stmt: tx.Stmt(s), Mapper: tx.Mapper} + return &Stmt{Stmt: tx.Stmt(s), unsafe: tx.unsafe, Mapper: tx.Mapper} } // NamedStmt returns a version of the prepared statement which runs within a transaction. From 1e29ff799fb2b0459b57e21ea0fd52be59b3ab6e Mon Sep 17 00:00:00 2001 From: August Vi Date: Sat, 10 Oct 2026 21:09:51 -0700 Subject: [PATCH 2/2] Keep unsafe in Tx.StmtxContext as well and inherit it from the wrapped statement. --- sqlx.go | 5 +++- sqlx_context.go | 5 +++- tx_stmtx_test.go | 64 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 72 insertions(+), 2 deletions(-) create mode 100644 tx_stmtx_test.go diff --git a/sqlx.go b/sqlx.go index 0e02611..7646a10 100644 --- a/sqlx.go +++ b/sqlx.go @@ -473,17 +473,20 @@ func (tx *Tx) Preparex(query string) (*Stmt, error) { // stmt can be either *sql.Stmt or *sqlx.Stmt. func (tx *Tx) Stmtx(stmt interface{}) *Stmt { var s *sql.Stmt + unsafe := tx.unsafe switch v := stmt.(type) { case Stmt: s = v.Stmt + unsafe = unsafe || v.unsafe case *Stmt: s = v.Stmt + unsafe = unsafe || v.unsafe case *sql.Stmt: s = v default: panic(fmt.Sprintf("non-statement type %v passed to Stmtx", reflect.ValueOf(stmt).Type())) } - return &Stmt{Stmt: tx.Stmt(s), unsafe: tx.unsafe, Mapper: tx.Mapper} + return &Stmt{Stmt: tx.Stmt(s), unsafe: unsafe, Mapper: tx.Mapper} } // NamedStmt returns a version of the prepared statement which runs within a transaction. diff --git a/sqlx_context.go b/sqlx_context.go index dc90bc6..39d86d1 100644 --- a/sqlx_context.go +++ b/sqlx_context.go @@ -281,17 +281,20 @@ func (c *Conn) Rebind(query string) string { // transaction. Provided stmt can be either *sql.Stmt or *sqlx.Stmt. func (tx *Tx) StmtxContext(ctx context.Context, stmt interface{}) *Stmt { var s *sql.Stmt + unsafe := tx.unsafe switch v := stmt.(type) { case Stmt: s = v.Stmt + unsafe = unsafe || v.unsafe case *Stmt: s = v.Stmt + unsafe = unsafe || v.unsafe case *sql.Stmt: s = v default: panic(fmt.Sprintf("non-statement type %v passed to Stmtx", reflect.ValueOf(stmt).Type())) } - return &Stmt{Stmt: tx.StmtContext(ctx, s), Mapper: tx.Mapper} + return &Stmt{Stmt: tx.StmtContext(ctx, s), unsafe: unsafe, Mapper: tx.Mapper} } // NamedStmtContext returns a version of the prepared statement which runs diff --git a/tx_stmtx_test.go b/tx_stmtx_test.go new file mode 100644 index 0000000..ed62a18 --- /dev/null +++ b/tx_stmtx_test.go @@ -0,0 +1,64 @@ +package sqlx + +import ( + "context" + "testing" +) + +// Tx.Stmtx and Tx.StmtxContext keep the transaction's unsafe flag, and keep +// the wrapped statement's own unsafe flag (upstream #979). +func TestTxStmtxKeepsUnsafe(t *testing.T) { + RunWithSchema(defaultSchema, t, func(db *DB, t *testing.T, now string) { + loadDefaultFixture(db, t) + ctx := context.Background() + stmt, err := db.Preparex(db.Rebind("SELECT * FROM person WHERE first_name=?")) + if err != nil { + t.Fatal(err) + } + defer stmt.Close() + + var partial struct { + FirstName string `db:"first_name"` + } + + tx, err := db.Unsafe().Beginx() + if err != nil { + t.Fatal(err) + } + defer tx.Rollback() + if !tx.unsafe { + t.Fatal("precondition: tx from db.Unsafe() should be unsafe") + } + ts := tx.Stmtx(stmt) + if !ts.unsafe { + t.Error("Stmtx: unsafe Tx returned a safe Stmt") + } + if err := ts.Get(&partial, "Jason"); err != nil { + t.Errorf("Stmtx: Get into partial struct: %v", err) + } + tsc := tx.StmtxContext(ctx, stmt) + if !tsc.unsafe { + t.Error("StmtxContext: unsafe Tx returned a safe Stmt") + } + if err := tsc.GetContext(ctx, &partial, "Jason"); err != nil { + t.Errorf("StmtxContext: Get into partial struct: %v", err) + } + + // An unsafe statement wrapped in a safe transaction stays unsafe. + safeTx, err := db.Beginx() + if err != nil { + t.Fatal(err) + } + defer safeTx.Rollback() + us := safeTx.Stmtx(stmt.Unsafe()) + if !us.unsafe { + t.Error("Stmtx: unsafe Stmt wrapped in a safe Tx became safe") + } + if err := us.Get(&partial, "Jason"); err != nil { + t.Errorf("Stmtx: Get through unsafe Stmt in safe Tx: %v", err) + } + if ss := safeTx.StmtxContext(ctx, stmt); ss.unsafe { + t.Error("StmtxContext: safe Stmt in safe Tx became unsafe") + } + }) +}