From a817d1d7c45bad4c90187de968c2d3fe71194ec7 Mon Sep 17 00:00:00 2001 From: Kamil Akhmetov Date: Wed, 12 Apr 2023 17:26:20 +0300 Subject: [PATCH 1/2] Invalidate cache on Rows.NextResultSet call When reading multiple result sets from sqlx.Rows using StructScan, some information obtained usCCCing costly reflect operations is cached for performance optimisation. However, different result sets in a single sqlx.Rows object might have different structures, so cache should be invalidated and rebuild after each NextResultSet call. Now this method is not overwritten, so old cache interferes with new structures after first result set scan. Relates #857 --- sqlx.go | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/sqlx.go b/sqlx.go index f7b2876..b4589c1 100644 --- a/sqlx.go +++ b/sqlx.go @@ -5,7 +5,6 @@ import ( "database/sql/driver" "errors" "fmt" - "io/ioutil" "path/filepath" "reflect" @@ -51,9 +50,9 @@ func mapper() *reflectx.Mapper { // isScannable takes the reflect.Type and the actual dest value and returns // whether or not it's Scannable. Something is scannable if: -// * it is not a struct -// * it implements sql.Scanner -// * it has no exported fields +// - it is not a struct +// - it implements sql.Scanner +// - it has no exported fields func isScannable(t reflect.Type) bool { if reflect.PtrTo(t).Implements(_scannerInterface) { return true @@ -633,6 +632,17 @@ func (r *Rows) StructScan(dest interface{}) error { return r.Err() } +// NextResultSet behaves like sql.NextResultSet, but cleans cache of previous result set. +func (r *Rows) NextResultSet() bool { + if !r.Rows.NextResultSet() { + return false + } + r.started = false + r.fields = nil + r.values = nil + return true +} + // Connect to a database and verify with a ping. func Connect(driverName, dataSourceName string) (*DB, error) { db, err := Open(driverName, dataSourceName) @@ -884,9 +894,9 @@ func structOnlyError(t reflect.Type) error { // then each row must only have one column which can scan into that type. This // allows you to do something like: // -// rows, _ := db.Query("select id from people;") -// var ids []int -// scanAll(rows, &ids, false) +// rows, _ := db.Query("select id from people;") +// var ids []int +// scanAll(rows, &ids, false) // // and ids will be a list of the id results. I realize that this is a desirable // interface to expose to users, but for now it will only be exposed via changes From 3e4e644cccd902721b1cca3445fa063d31391708 Mon Sep 17 00:00:00 2001 From: August Vi Date: Sat, 10 Oct 2026 21:09:55 -0700 Subject: [PATCH 2/2] Add the multi-result-set test from upstream #747. --- multi_result_set_test.go | 111 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 multi_result_set_test.go diff --git a/multi_result_set_test.go b/multi_result_set_test.go new file mode 100644 index 0000000..efeb31e --- /dev/null +++ b/multi_result_set_test.go @@ -0,0 +1,111 @@ +package sqlx + +import "testing" + +// Scanning a second result set with a different column layout must not reuse +// the first set's field cache (upstream #380, #857; test from upstream #747). +func TestMultiResultSet(t *testing.T) { + RunWithSchema(defaultSchema, t, func(db *DB, t *testing.T, now string) { + var sp, sql string + + loadDefaultFixture(db, t) + + switch db.DriverName() { + case "mysql": + sp = ` + CREATE PROCEDURE multiQuery() + BEGIN + SELECT first_name, last_name, email FROM person LIMIT 1; + SELECT country, telcode FROM place LIMIT 1; + END + ` + sql = "CALL multiQuery();" + case "postgres": + sql = ` + SELECT first_name, last_name, email FROM person LIMIT 1; + SELECT country, telcode FROM place LIMIT 1; + ` + default: + // sqlite3 does not support multiple result sets. + return + } + + type Person struct { + FirstName string `db:"first_name"` + LastName string `db:"last_name"` + Email string `db:"email"` + } + type Place struct { + Country string `db:"country"` + TelCode string `db:"telcode"` + } + + if sp != "" { + if _, err := db.Exec("DROP PROCEDURE IF EXISTS multiQuery"); err != nil { + t.Fatal(err) + } + + if _, err := db.Exec(sp); err != nil { + t.Fatal(err) + } + } + + rows, err := db.Queryx(sql) + if err != nil { + t.Fatal(err) + } + defer rows.Close() + + var ( + person Person + place Place + ) + + var i int + for rows.Next() { + i++ + if err = rows.StructScan(&person); err != nil { + t.Fatal(err) + } + } + if i < 1 { + t.Errorf("[%s] Expected at least one record, got none", db.DriverName()) + } + + if !rows.NextResultSet() { + t.Fatalf("[%s] Expected a second recordset, got one", db.DriverName()) + } + + i = 0 + for rows.Next() { + i++ + if err = rows.StructScan(&place); err != nil { + t.Fatal(err) + } + } + if i < 1 { + t.Errorf("Expected at least one records, got none") + } + + // nest no next + rows2, err := db.Queryx("SELECT first_name, last_name, email FROM person LIMIT 2;") + if err != nil { + t.Fatal(err) + } + defer rows2.Close() + + i = 0 + for rows2.Next() { + i++ + if err = rows2.StructScan(&person); err != nil { + t.Fatal(err) + } + } + if i < 2 { + t.Errorf("[%s] Expected at least 2 records, got %d", db.DriverName(), i) + } + if rows.NextResultSet() { + t.Fatalf("[%s] Did not expected a second recordset", db.DriverName()) + } + }) +}