From f71a55c4bac78d679902ab02dd3db746692b1ca0 Mon Sep 17 00:00:00 2001 From: Yury Nasretdinov Date: Tue, 9 Jun 2026 15:54:31 +0200 Subject: [PATCH] Select() using iterator, rudimentary implementation --- go.mod | 10 +++++---- go.sum | 8 +++++++ sqlx.go | 63 ++++++++++++++++++++++++++++++++++++++++++++++++++++ sqlx_test.go | 36 ++++++++++++++++++++++++++++++ 4 files changed, 113 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index 9b164a1c1..5605cc9e5 100644 --- a/go.mod +++ b/go.mod @@ -1,9 +1,11 @@ module github.com/jmoiron/sqlx -go 1.10 +go 1.27 require ( - github.com/go-sql-driver/mysql v1.8.1 - github.com/lib/pq v1.10.9 - github.com/mattn/go-sqlite3 v1.14.22 + github.com/go-sql-driver/mysql v1.10.0 + github.com/lib/pq v1.12.3 + github.com/mattn/go-sqlite3 v1.14.45 ) + +require filippo.io/edwards25519 v1.2.0 // indirect diff --git a/go.sum b/go.sum index 31d5abac8..69a1f1e69 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,16 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= +filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +github.com/go-sql-driver/mysql v1.10.0 h1:Q+1LV8DkHJvSYAdR83XzuhDaTykuDx0l6fkXxoWCWfw= +github.com/go-sql-driver/mysql v1.10.0/go.mod h1:M+cqaI7+xxXGG9swrdeUIoPG3Y3KCkF0pZej+SK+nWk= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ= +github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/mattn/go-sqlite3 v1.14.45 h1:6KA/spDguL3KV8rnybG7ezSaE4SeMR3KC9VbUoAQaIk= +github.com/mattn/go-sqlite3 v1.14.45/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ= diff --git a/sqlx.go b/sqlx.go index 8259a4feb..c5ceac0d0 100644 --- a/sqlx.go +++ b/sqlx.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "io/ioutil" + "iter" "path/filepath" "reflect" "strings" @@ -320,6 +321,10 @@ func (db *DB) Select(dest interface{}, query string, args ...interface{}) error return Select(db, dest, query, args...) } +func (db *DB) SelectIter[T any](query string, args ...interface{}) iter.Seq2[*T, error] { + return SelectIter[T](db, query, args...) +} + // Get using this DB. // Any placeholder parameters are replaced with supplied args. // An error is returned if the result set is empty. @@ -434,6 +439,12 @@ func (tx *Tx) Select(dest interface{}, query string, args ...interface{}) error return Select(tx, dest, query, args...) } +// SelectIter within a transaction. +// Any placeholder parameters are replaced with supplied args. +func (tx *Tx) SelectIter[T any](query string, args ...interface{}) iter.Seq2[*T, error] { + return SelectIter[T](tx, query, args...) +} + // Queryx within a transaction. // Any placeholder parameters are replaced with supplied args. func (tx *Tx) Queryx(query string, args ...interface{}) (*Rows, error) { @@ -519,6 +530,12 @@ func (s *Stmt) Select(dest interface{}, args ...interface{}) error { return Select(&qStmt{s}, dest, "", args...) } +// SelectIter using the prepared statement. +// Any placeholder parameters are replaced with supplied args. +func (s *Stmt) SelectIter[T any](query string, args ...interface{}) iter.Seq2[*T, error] { + return SelectIter[T](&qStmt{s}, query, args...) +} + // Get using the prepared statement. // Any placeholder parameters are replaced with supplied args. // An error is returned if the result set is empty. @@ -683,6 +700,52 @@ func Select(q Queryer, dest interface{}, query string, args ...interface{}) erro return scanAll(rows, dest, false) } +// SelectIter returns an iterator that executes the query and then starts +// returning rows of the specific type one by one. When an error is returned +// iteration stops. +// +// Example: +// +// for u, err := range SelectIter[User](db, `SELECT * FROM users ORDER BY id`) { +// if err != nil { ... } +// ... +// } +func SelectIter[T any](q Queryer, query string, args ...interface{}) iter.Seq2[*T, error] { + return func(yield func(*T, error) bool) { + rows, err := q.Queryx(query, args...) + if err != nil { + yield(nil, err) + return + } + // We cannot use yield() inside defer, so we Close() the rows manually. + + for rows.Next() { + var row T + if err := rows.StructScan(&row); err != nil { + yield(nil, err) + + // We've already returned a single error, so ignoring Close() + // error here. + _ = rows.Close() + return + } + + if !yield(&row, nil) { + // Cannot yield again after the iteration has stopped, so ignoring + // Close() error here. + _ = rows.Close() + return + } + } + + // It's a common mistake to ignore rows.Close() error value, however + // it can still happen, so we need to handle it. + if err := rows.Close(); err != nil { + yield(nil, err) + } + } +} + // Get does a QueryRow using the provided Queryer, and scans the resulting row // to dest. If dest is scannable, the result must only have one column. Otherwise, // StructScan is used. Get will return sql.ErrNoRows like row.Scan would. diff --git a/sqlx_test.go b/sqlx_test.go index 9fac2cd4f..6bd087b35 100644 --- a/sqlx_test.go +++ b/sqlx_test.go @@ -1923,3 +1923,39 @@ func TestSelectReset(t *testing.T) { } }) } + +func TestSelectIter(t *testing.T) { + RunWithSchema(defaultSchema, t, func(db *DB, t *testing.T, now string) { + loadDefaultFixture(db, t) + + var people []*Person + const query = "SELECT * FROM person ORDER BY first_name" + + err := db.Select(&people, query) + if err != nil { + t.Fatal(err) + } + if want := 2; len(people) != want { + t.Errorf("Expected %d first names, got %d", want, len(people)) + } + + var peopleIter []*Person + for p, err := range db.SelectIter[Person](query) { + if err != nil { + t.Fatalf("Iteration failed for %s: %v", query, err) + } + + peopleIter = append(peopleIter, p) + } + + if len(people) != len(peopleIter) { + t.Fatalf("SelectIter() returned %d rows, want %d", len(peopleIter), len(people)) + } + + for i, p := range people { + if !reflect.DeepEqual(p, peopleIter[i]) { + t.Errorf("SelectIter() %d result is wrong, got %+v, want %+v", i, peopleIter[i], p) + } + } + }) +}