diff --git a/.github/workflows/ci-kotlin.yml b/.github/workflows/ci-kotlin.yml index 59c96a7ecf..49196d458e 100644 --- a/.github/workflows/ci-kotlin.yml +++ b/.github/workflows/ci-kotlin.yml @@ -12,7 +12,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: - go-version: '1.21.3' + go-version: '1.23.2' - name: install ./... run: go install ./... - uses: actions/checkout@v4 diff --git a/.github/workflows/ci-python.yml b/.github/workflows/ci-python.yml index a0e34ce03a..80a57ff503 100644 --- a/.github/workflows/ci-python.yml +++ b/.github/workflows/ci-python.yml @@ -12,7 +12,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: - go-version: '1.21.3' + go-version: '1.23.2' - name: install ./... run: go install ./... - uses: actions/checkout@v4 diff --git a/.github/workflows/ci-typescript.yml b/.github/workflows/ci-typescript.yml index f4b94e6c10..d347d58a0b 100644 --- a/.github/workflows/ci-typescript.yml +++ b/.github/workflows/ci-typescript.yml @@ -12,7 +12,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: - go-version: '1.21.3' + go-version: '1.23.2' - name: install ./... run: go install ./... - uses: actions/checkout@v4 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7b4b87de10..7f2743aee6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,7 +30,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: - go-version: '1.22.8' + go-version: '1.23.2' - name: install gotestsum run: go install gotest.tools/gotestsum@latest @@ -78,6 +78,6 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: - go-version: '1.22.8' + go-version: '1.23.2' - run: go install golang.org/x/vuln/cmd/govulncheck@latest - run: govulncheck ./... diff --git a/docs/overview/install.md b/docs/overview/install.md index e592888cc5..e4b39e6e63 100644 --- a/docs/overview/install.md +++ b/docs/overview/install.md @@ -16,7 +16,7 @@ sudo snap install sqlc ## go install -Installing recent versions of sqlc requires Go 1.21+. +Installing recent versions of sqlc requires Go 1.23+. ``` go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest diff --git a/examples/authors/mysql/query.sql b/examples/authors/mysql/query.sql index c3b5866149..b4f0252d17 100644 --- a/examples/authors/mysql/query.sql +++ b/examples/authors/mysql/query.sql @@ -6,6 +6,10 @@ WHERE id = ? LIMIT 1; SELECT * FROM authors ORDER BY name; +/* name: IterAuthors :iter */ +SELECT * FROM authors +ORDER BY name; + /* name: CreateAuthor :execresult */ INSERT INTO authors ( name, bio diff --git a/examples/authors/mysql/query.sql.go b/examples/authors/mysql/query.sql.go index 4a499ef2fa..3d44f8a01c 100644 --- a/examples/authors/mysql/query.sql.go +++ b/examples/authors/mysql/query.sql.go @@ -8,6 +8,7 @@ package authors import ( "context" "database/sql" + "iter" ) const createAuthor = `-- name: CreateAuthor :execresult @@ -49,6 +50,59 @@ func (q *Queries) GetAuthor(ctx context.Context, id int64) (Author, error) { return i, err } +const iterAuthors = `-- name: IterAuthors :iter +SELECT id, name, bio FROM authors +ORDER BY name +` + +func (q *Queries) IterAuthors(ctx context.Context) IterAuthorsRows { + rows, err := q.db.QueryContext(ctx, iterAuthors) + if err != nil { + return IterAuthorsRows{err: err} + } + return IterAuthorsRows{rows: rows} +} + +type IterAuthorsRows struct { + rows *sql.Rows + err error +} + +func (r *IterAuthorsRows) Iterate() iter.Seq[Author] { + if r.rows == nil { + return func(yield func(Author) bool) {} + } + + return func(yield func(Author) bool) { + defer r.rows.Close() + + for r.rows.Next() { + var i Author + err := r.rows.Scan(&i.ID, &i.Name, &i.Bio) + if err != nil { + r.err = err + return + } + + if !yield(i) { + r.err = r.rows.Close() + return + } + } + } +} + +func (r *IterAuthorsRows) Close() error { + return r.rows.Close() +} + +func (r *IterAuthorsRows) Err() error { + if r.err != nil { + return r.err + } + return r.rows.Err() +} + const listAuthors = `-- name: ListAuthors :many SELECT id, name, bio FROM authors ORDER BY name diff --git a/examples/authors/postgresql/query.sql b/examples/authors/postgresql/query.sql index 75e38b2caf..04e1ac2619 100644 --- a/examples/authors/postgresql/query.sql +++ b/examples/authors/postgresql/query.sql @@ -6,6 +6,10 @@ WHERE id = $1 LIMIT 1; SELECT * FROM authors ORDER BY name; +-- name: IterAuthors :iter +SELECT * FROM authors +ORDER BY name; + -- name: CreateAuthor :one INSERT INTO authors ( name, bio diff --git a/examples/authors/postgresql/query.sql.go b/examples/authors/postgresql/query.sql.go index eadab01177..36f4a9c544 100644 --- a/examples/authors/postgresql/query.sql.go +++ b/examples/authors/postgresql/query.sql.go @@ -7,7 +7,9 @@ package authors import ( "context" + "iter" + "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgtype" ) @@ -54,6 +56,58 @@ func (q *Queries) GetAuthor(ctx context.Context, id int64) (Author, error) { return i, err } +const iterAuthors = `-- name: IterAuthors :iter +SELECT id, name, bio FROM authors +ORDER BY name +` + +func (q *Queries) IterAuthors(ctx context.Context) IterAuthorsRows { + rows, err := q.db.Query(ctx, iterAuthors) + if err != nil { + return IterAuthorsRows{err: err} + } + return IterAuthorsRows{rows: rows} +} + +type IterAuthorsRows struct { + rows pgx.Rows + err error +} + +func (r *IterAuthorsRows) Iterate() iter.Seq[Author] { + if r.rows == nil { + return func(yield func(Author) bool) {} + } + + return func(yield func(Author) bool) { + defer r.rows.Close() + + for r.rows.Next() { + var i Author + err := r.rows.Scan(&i.ID, &i.Name, &i.Bio) + if err != nil { + r.err = err + return + } + + if !yield(i) { + return + } + } + } +} + +func (r *IterAuthorsRows) Close() { + r.rows.Close() +} + +func (r *IterAuthorsRows) Err() error { + if r.err != nil { + return r.err + } + return r.rows.Err() +} + const listAuthors = `-- name: ListAuthors :many SELECT id, name, bio FROM authors ORDER BY name diff --git a/examples/authors/sqlite/query.sql b/examples/authors/sqlite/query.sql index c3b5866149..b4f0252d17 100644 --- a/examples/authors/sqlite/query.sql +++ b/examples/authors/sqlite/query.sql @@ -6,6 +6,10 @@ WHERE id = ? LIMIT 1; SELECT * FROM authors ORDER BY name; +/* name: IterAuthors :iter */ +SELECT * FROM authors +ORDER BY name; + /* name: CreateAuthor :execresult */ INSERT INTO authors ( name, bio diff --git a/examples/authors/sqlite/query.sql.go b/examples/authors/sqlite/query.sql.go index 4a499ef2fa..3d44f8a01c 100644 --- a/examples/authors/sqlite/query.sql.go +++ b/examples/authors/sqlite/query.sql.go @@ -8,6 +8,7 @@ package authors import ( "context" "database/sql" + "iter" ) const createAuthor = `-- name: CreateAuthor :execresult @@ -49,6 +50,59 @@ func (q *Queries) GetAuthor(ctx context.Context, id int64) (Author, error) { return i, err } +const iterAuthors = `-- name: IterAuthors :iter +SELECT id, name, bio FROM authors +ORDER BY name +` + +func (q *Queries) IterAuthors(ctx context.Context) IterAuthorsRows { + rows, err := q.db.QueryContext(ctx, iterAuthors) + if err != nil { + return IterAuthorsRows{err: err} + } + return IterAuthorsRows{rows: rows} +} + +type IterAuthorsRows struct { + rows *sql.Rows + err error +} + +func (r *IterAuthorsRows) Iterate() iter.Seq[Author] { + if r.rows == nil { + return func(yield func(Author) bool) {} + } + + return func(yield func(Author) bool) { + defer r.rows.Close() + + for r.rows.Next() { + var i Author + err := r.rows.Scan(&i.ID, &i.Name, &i.Bio) + if err != nil { + r.err = err + return + } + + if !yield(i) { + r.err = r.rows.Close() + return + } + } + } +} + +func (r *IterAuthorsRows) Close() error { + return r.rows.Close() +} + +func (r *IterAuthorsRows) Err() error { + if r.err != nil { + return r.err + } + return r.rows.Err() +} + const listAuthors = `-- name: ListAuthors :many SELECT id, name, bio FROM authors ORDER BY name diff --git a/examples/booktest/mysql/db_test.go b/examples/booktest/mysql/db_test.go index 6f97178c94..cffbc09fd2 100644 --- a/examples/booktest/mysql/db_test.go +++ b/examples/booktest/mysql/db_test.go @@ -5,6 +5,7 @@ package booktest import ( "context" "database/sql" + "slices" "testing" "time" @@ -150,6 +151,25 @@ func TestBooks(t *testing.T) { t.Logf("Book %d author: %s\n", book.BookID, author.Name) } + // retrieve first book + rows := dq.IterBooksByTitleYear(ctx, IterBooksByTitleYearParams{ + Title: "my book title", + Yr: 2016, + }) + + // We need to collect the iterator elements to avoid panic when we make other db queries in the loop. + for _, book := range slices.Collect(rows.Iterate()) { + t.Logf("Book %d (%s): %s available: %s\n", book.BookID, book.BookType, book.Title, book.Available.Format(time.RFC822Z)) + author, err := dq.GetAuthor(ctx, book.AuthorID) + if err != nil { + t.Fatal(err) + } + t.Logf("Book %d author: %s\n", book.BookID, author.Name) + } + if err = rows.Err(); err != nil { + t.Fatal(err) + } + // find a book with either "cool" or "other" tag t.Logf("---------\nTag search results:\n") res, err := dq.BooksByTags(ctx, "cool") diff --git a/examples/booktest/mysql/query.sql b/examples/booktest/mysql/query.sql index a455ad4370..88fc349991 100644 --- a/examples/booktest/mysql/query.sql +++ b/examples/booktest/mysql/query.sql @@ -14,6 +14,10 @@ WHERE book_id = ?; SELECT * FROM books WHERE title = ? AND yr = ?; +/* name: IterBooksByTitleYear :iter */ +SELECT * FROM books +WHERE title = ? AND yr = ?; + /* name: BooksByTags :many */ SELECT book_id, diff --git a/examples/booktest/mysql/query.sql.go b/examples/booktest/mysql/query.sql.go index d1083eca86..4a2bc740e2 100644 --- a/examples/booktest/mysql/query.sql.go +++ b/examples/booktest/mysql/query.sql.go @@ -8,6 +8,7 @@ package booktest import ( "context" "database/sql" + "iter" "time" ) @@ -210,6 +211,73 @@ func (q *Queries) GetBook(ctx context.Context, bookID int32) (Book, error) { return i, err } +const iterBooksByTitleYear = `-- name: IterBooksByTitleYear :iter +SELECT book_id, author_id, isbn, book_type, title, yr, available, tags FROM books +WHERE title = ? AND yr = ? +` + +type IterBooksByTitleYearParams struct { + Title string + Yr int32 +} + +func (q *Queries) IterBooksByTitleYear(ctx context.Context, arg IterBooksByTitleYearParams) IterBooksByTitleYearRows { + rows, err := q.db.QueryContext(ctx, iterBooksByTitleYear, arg.Title, arg.Yr) + if err != nil { + return IterBooksByTitleYearRows{err: err} + } + return IterBooksByTitleYearRows{rows: rows} +} + +type IterBooksByTitleYearRows struct { + rows *sql.Rows + err error +} + +func (r *IterBooksByTitleYearRows) Iterate() iter.Seq[Book] { + if r.rows == nil { + return func(yield func(Book) bool) {} + } + + return func(yield func(Book) bool) { + defer r.rows.Close() + + for r.rows.Next() { + var i Book + err := r.rows.Scan( + &i.BookID, + &i.AuthorID, + &i.Isbn, + &i.BookType, + &i.Title, + &i.Yr, + &i.Available, + &i.Tags, + ) + if err != nil { + r.err = err + return + } + + if !yield(i) { + r.err = r.rows.Close() + return + } + } + } +} + +func (r *IterBooksByTitleYearRows) Close() error { + return r.rows.Close() +} + +func (r *IterBooksByTitleYearRows) Err() error { + if r.err != nil { + return r.err + } + return r.rows.Err() +} + const updateBook = `-- name: UpdateBook :exec UPDATE books SET title = ?, tags = ? diff --git a/examples/booktest/postgresql/db_test.go b/examples/booktest/postgresql/db_test.go index ae71593852..3c66a44c6c 100644 --- a/examples/booktest/postgresql/db_test.go +++ b/examples/booktest/postgresql/db_test.go @@ -4,6 +4,7 @@ package booktest import ( "context" + "slices" "testing" "time" @@ -139,6 +140,25 @@ func TestBooks(t *testing.T) { t.Logf("Book %d author: %s\n", book.BookID, author.Name) } + // retrieve first book + rows := dq.IterBooksByTitleYear(ctx, IterBooksByTitleYearParams{ + Title: "my book title", + Year: 2016, + }) + + // We need to collect the iterator elements to avoid panic when we make other db queries in the loop. + for _, book := range slices.Collect(rows.Iterate()) { + t.Logf("Book %d (%s): %s available: %s\n", book.BookID, book.BookType, book.Title, book.Available.Time.Format(time.RFC822Z)) + author, err := dq.GetAuthor(ctx, book.AuthorID) + if err != nil { + t.Fatal(err) + } + t.Logf("Book %d author: %s\n", book.BookID, author.Name) + } + if err = rows.Err(); err != nil { + t.Fatal(err) + } + // find a book with either "cool" or "other" tag t.Logf("---------\nTag search results:\n") res, err := dq.BooksByTags(ctx, []string{"cool", "other", "someother"}) diff --git a/examples/booktest/postgresql/query.sql b/examples/booktest/postgresql/query.sql index 75b8cd3b6d..d9fa209794 100644 --- a/examples/booktest/postgresql/query.sql +++ b/examples/booktest/postgresql/query.sql @@ -14,6 +14,10 @@ WHERE book_id = $1; SELECT * FROM books WHERE title = $1 AND year = $2; +-- name: IterBooksByTitleYear :iter +SELECT * FROM books +WHERE title = $1 AND year = $2; + -- name: BooksByTags :many SELECT book_id, diff --git a/examples/booktest/postgresql/query.sql.go b/examples/booktest/postgresql/query.sql.go index 800e56abff..17d279da37 100644 --- a/examples/booktest/postgresql/query.sql.go +++ b/examples/booktest/postgresql/query.sql.go @@ -7,7 +7,9 @@ package booktest import ( "context" + "iter" + "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgtype" ) @@ -206,6 +208,72 @@ func (q *Queries) GetBook(ctx context.Context, bookID int32) (Book, error) { return i, err } +const iterBooksByTitleYear = `-- name: IterBooksByTitleYear :iter +SELECT book_id, author_id, isbn, book_type, title, year, available, tags FROM books +WHERE title = $1 AND year = $2 +` + +type IterBooksByTitleYearParams struct { + Title string + Year int32 +} + +func (q *Queries) IterBooksByTitleYear(ctx context.Context, arg IterBooksByTitleYearParams) IterBooksByTitleYearRows { + rows, err := q.db.Query(ctx, iterBooksByTitleYear, arg.Title, arg.Year) + if err != nil { + return IterBooksByTitleYearRows{err: err} + } + return IterBooksByTitleYearRows{rows: rows} +} + +type IterBooksByTitleYearRows struct { + rows pgx.Rows + err error +} + +func (r *IterBooksByTitleYearRows) Iterate() iter.Seq[Book] { + if r.rows == nil { + return func(yield func(Book) bool) {} + } + + return func(yield func(Book) bool) { + defer r.rows.Close() + + for r.rows.Next() { + var i Book + err := r.rows.Scan( + &i.BookID, + &i.AuthorID, + &i.Isbn, + &i.BookType, + &i.Title, + &i.Year, + &i.Available, + &i.Tags, + ) + if err != nil { + r.err = err + return + } + + if !yield(i) { + return + } + } + } +} + +func (r *IterBooksByTitleYearRows) Close() { + r.rows.Close() +} + +func (r *IterBooksByTitleYearRows) Err() error { + if r.err != nil { + return r.err + } + return r.rows.Err() +} + const sayHello = `-- name: SayHello :one select say_hello from say_hello($1) ` diff --git a/examples/booktest/sqlite/db_test.go b/examples/booktest/sqlite/db_test.go index b9490245b3..3dccab70f0 100644 --- a/examples/booktest/sqlite/db_test.go +++ b/examples/booktest/sqlite/db_test.go @@ -4,6 +4,7 @@ package booktest import ( "context" + "slices" "testing" "time" @@ -138,6 +139,25 @@ func TestBooks(t *testing.T) { t.Logf("Book %d author: %s\n", book.BookID, author.Name) } + // retrieve first book + rows := dq.IterBooksByTitleYear(ctx, IterBooksByTitleYearParams{ + Title: "my book title", + Yr: 2016, + }) + + // We need to collect the iterator elements to avoid panic when we make other db queries in the loop. + for _, book := range slices.Collect(rows.Iterate()) { + t.Logf("Book %d (%s): %s available: %s\n", book.BookID, book.BookType, book.Title, book.Available.Format(time.RFC822Z)) + author, err := dq.GetAuthor(ctx, book.AuthorID) + if err != nil { + t.Fatal(err) + } + t.Logf("Book %d author: %s\n", book.BookID, author.Name) + } + if err = rows.Err(); err != nil { + t.Fatal(err) + } + // find a book with either "cool" or "other" or "someother" tag t.Logf("---------\nTag search results:\n") res, err := dq.BooksByTags(ctx, []string{"cool", "other", "someother"}) diff --git a/examples/booktest/sqlite/query.sql b/examples/booktest/sqlite/query.sql index 4022c7ea53..bdc3e07198 100644 --- a/examples/booktest/sqlite/query.sql +++ b/examples/booktest/sqlite/query.sql @@ -14,6 +14,10 @@ WHERE book_id = ?; SELECT * FROM books WHERE title = ? AND yr = ?; +/* name: IterBooksByTitleYear :iter */ +SELECT * FROM books +WHERE title = ? AND yr = ?; + /* name: BooksByTags :many */ SELECT book_id, diff --git a/examples/booktest/sqlite/query.sql.go b/examples/booktest/sqlite/query.sql.go index cc5fa4a8fe..91dd4cfd95 100644 --- a/examples/booktest/sqlite/query.sql.go +++ b/examples/booktest/sqlite/query.sql.go @@ -8,6 +8,7 @@ package booktest import ( "context" "database/sql" + "iter" "strings" "time" ) @@ -238,6 +239,73 @@ func (q *Queries) GetBook(ctx context.Context, bookID int64) (Book, error) { return i, err } +const iterBooksByTitleYear = `-- name: IterBooksByTitleYear :iter +SELECT book_id, author_id, isbn, book_type, title, yr, available, tag FROM books +WHERE title = ? AND yr = ? +` + +type IterBooksByTitleYearParams struct { + Title string + Yr int64 +} + +func (q *Queries) IterBooksByTitleYear(ctx context.Context, arg IterBooksByTitleYearParams) IterBooksByTitleYearRows { + rows, err := q.db.QueryContext(ctx, iterBooksByTitleYear, arg.Title, arg.Yr) + if err != nil { + return IterBooksByTitleYearRows{err: err} + } + return IterBooksByTitleYearRows{rows: rows} +} + +type IterBooksByTitleYearRows struct { + rows *sql.Rows + err error +} + +func (r *IterBooksByTitleYearRows) Iterate() iter.Seq[Book] { + if r.rows == nil { + return func(yield func(Book) bool) {} + } + + return func(yield func(Book) bool) { + defer r.rows.Close() + + for r.rows.Next() { + var i Book + err := r.rows.Scan( + &i.BookID, + &i.AuthorID, + &i.Isbn, + &i.BookType, + &i.Title, + &i.Yr, + &i.Available, + &i.Tag, + ) + if err != nil { + r.err = err + return + } + + if !yield(i) { + r.err = r.rows.Close() + return + } + } + } +} + +func (r *IterBooksByTitleYearRows) Close() error { + return r.rows.Close() +} + +func (r *IterBooksByTitleYearRows) Err() error { + if r.err != nil { + return r.err + } + return r.rows.Err() +} + const updateBook = `-- name: UpdateBook :exec UPDATE books SET title = ?1, tag = ?2 diff --git a/examples/jets/postgresql/query-building.sql b/examples/jets/postgresql/query-building.sql index ede8952367..8890959dd9 100644 --- a/examples/jets/postgresql/query-building.sql +++ b/examples/jets/postgresql/query-building.sql @@ -4,5 +4,8 @@ SELECT COUNT(*) FROM pilots; -- name: ListPilots :many SELECT * FROM pilots LIMIT 5; +-- name: IterPilots :iter +SELECT * FROM pilots; + -- name: DeletePilot :exec DELETE FROM pilots WHERE id = $1; diff --git a/examples/jets/postgresql/query-building.sql.go b/examples/jets/postgresql/query-building.sql.go index 7322905379..471955f2a8 100644 --- a/examples/jets/postgresql/query-building.sql.go +++ b/examples/jets/postgresql/query-building.sql.go @@ -7,6 +7,9 @@ package jets import ( "context" + "iter" + + "github.com/jackc/pgx/v5" ) const countPilots = `-- name: CountPilots :one @@ -29,6 +32,57 @@ func (q *Queries) DeletePilot(ctx context.Context, id int32) error { return err } +const iterPilots = `-- name: IterPilots :iter +SELECT id, name FROM pilots +` + +func (q *Queries) IterPilots(ctx context.Context) IterPilotsRows { + rows, err := q.db.Query(ctx, iterPilots) + if err != nil { + return IterPilotsRows{err: err} + } + return IterPilotsRows{rows: rows} +} + +type IterPilotsRows struct { + rows pgx.Rows + err error +} + +func (r *IterPilotsRows) Iterate() iter.Seq[Pilot] { + if r.rows == nil { + return func(yield func(Pilot) bool) {} + } + + return func(yield func(Pilot) bool) { + defer r.rows.Close() + + for r.rows.Next() { + var i Pilot + err := r.rows.Scan(&i.ID, &i.Name) + if err != nil { + r.err = err + return + } + + if !yield(i) { + return + } + } + } +} + +func (r *IterPilotsRows) Close() { + r.rows.Close() +} + +func (r *IterPilotsRows) Err() error { + if r.err != nil { + return r.err + } + return r.rows.Err() +} + const listPilots = `-- name: ListPilots :many SELECT id, name FROM pilots LIMIT 5 ` diff --git a/examples/ondeck/mysql/city.sql.go b/examples/ondeck/mysql/city.sql.go index ee90b3b559..b4ac1cbc9e 100644 --- a/examples/ondeck/mysql/city.sql.go +++ b/examples/ondeck/mysql/city.sql.go @@ -7,6 +7,8 @@ package ondeck import ( "context" + "database/sql" + "iter" ) const createCity = `-- name: CreateCity :exec @@ -42,6 +44,60 @@ func (q *Queries) GetCity(ctx context.Context, slug string) (City, error) { return i, err } +const iterCities = `-- name: IterCities :iter +SELECT slug, name +FROM city +ORDER BY name +` + +func (q *Queries) IterCities(ctx context.Context) IterCitiesRows { + rows, err := q.query(ctx, q.iterCitiesStmt, iterCities) + if err != nil { + return IterCitiesRows{err: err} + } + return IterCitiesRows{rows: rows} +} + +type IterCitiesRows struct { + rows *sql.Rows + err error +} + +func (r *IterCitiesRows) Iterate() iter.Seq[City] { + if r.rows == nil { + return func(yield func(City) bool) {} + } + + return func(yield func(City) bool) { + defer r.rows.Close() + + for r.rows.Next() { + var i City + err := r.rows.Scan(&i.Slug, &i.Name) + if err != nil { + r.err = err + return + } + + if !yield(i) { + r.err = r.rows.Close() + return + } + } + } +} + +func (r *IterCitiesRows) Close() error { + return r.rows.Close() +} + +func (r *IterCitiesRows) Err() error { + if r.err != nil { + return r.err + } + return r.rows.Err() +} + const listCities = `-- name: ListCities :many SELECT slug, name FROM city diff --git a/examples/ondeck/mysql/db.go b/examples/ondeck/mysql/db.go index 09429da435..563dc857fb 100644 --- a/examples/ondeck/mysql/db.go +++ b/examples/ondeck/mysql/db.go @@ -39,6 +39,12 @@ func Prepare(ctx context.Context, db DBTX) (*Queries, error) { if q.getVenueStmt, err = db.PrepareContext(ctx, getVenue); err != nil { return nil, fmt.Errorf("error preparing query GetVenue: %w", err) } + if q.iterCitiesStmt, err = db.PrepareContext(ctx, iterCities); err != nil { + return nil, fmt.Errorf("error preparing query IterCities: %w", err) + } + if q.iterVenuesStmt, err = db.PrepareContext(ctx, iterVenues); err != nil { + return nil, fmt.Errorf("error preparing query IterVenues: %w", err) + } if q.listCitiesStmt, err = db.PrepareContext(ctx, listCities); err != nil { return nil, fmt.Errorf("error preparing query ListCities: %w", err) } @@ -84,6 +90,16 @@ func (q *Queries) Close() error { err = fmt.Errorf("error closing getVenueStmt: %w", cerr) } } + if q.iterCitiesStmt != nil { + if cerr := q.iterCitiesStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing iterCitiesStmt: %w", cerr) + } + } + if q.iterVenuesStmt != nil { + if cerr := q.iterVenuesStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing iterVenuesStmt: %w", cerr) + } + } if q.listCitiesStmt != nil { if cerr := q.listCitiesStmt.Close(); cerr != nil { err = fmt.Errorf("error closing listCitiesStmt: %w", cerr) @@ -153,6 +169,8 @@ type Queries struct { deleteVenueStmt *sql.Stmt getCityStmt *sql.Stmt getVenueStmt *sql.Stmt + iterCitiesStmt *sql.Stmt + iterVenuesStmt *sql.Stmt listCitiesStmt *sql.Stmt listVenuesStmt *sql.Stmt updateCityNameStmt *sql.Stmt @@ -169,6 +187,8 @@ func (q *Queries) WithTx(tx *sql.Tx) *Queries { deleteVenueStmt: q.deleteVenueStmt, getCityStmt: q.getCityStmt, getVenueStmt: q.getVenueStmt, + iterCitiesStmt: q.iterCitiesStmt, + iterVenuesStmt: q.iterVenuesStmt, listCitiesStmt: q.listCitiesStmt, listVenuesStmt: q.listVenuesStmt, updateCityNameStmt: q.updateCityNameStmt, diff --git a/examples/ondeck/mysql/db_test.go b/examples/ondeck/mysql/db_test.go index 6ac70aaf06..b41509ab0f 100644 --- a/examples/ondeck/mysql/db_test.go +++ b/examples/ondeck/mysql/db_test.go @@ -91,6 +91,20 @@ func runOnDeckQueries(t *testing.T, q *Queries) { } } + { + var actualCities []City + rows := q.IterCities(ctx) + for actualCity := range rows.Iterate() { + actualCities = append(actualCities, actualCity) + } + if err = rows.Err(); err != nil { + t.Error(err) + } + if diff := cmp.Diff(actualCities, []City{city}); diff != "" { + t.Errorf("iter city mismatch:\n%s", diff) + } + } + { actual, err := q.ListVenues(ctx, city.Slug) if err != nil { @@ -101,6 +115,20 @@ func runOnDeckQueries(t *testing.T, q *Queries) { } } + { + var actualVenues []Venue + rows := q.IterVenues(ctx, city.Slug) + for actualVenue := range rows.Iterate() { + actualVenues = append(actualVenues, actualVenue) + } + if err = rows.Err(); err != nil { + t.Error(err) + } + if diff := cmp.Diff(actualVenues, []Venue{venue}); diff != "" { + t.Errorf("iter venue mismatch:\n%s", diff) + } + } + { err := q.UpdateCityName(ctx, UpdateCityNameParams{ Slug: city.Slug, diff --git a/examples/ondeck/mysql/querier.go b/examples/ondeck/mysql/querier.go index 7f615fc2be..99e6483392 100644 --- a/examples/ondeck/mysql/querier.go +++ b/examples/ondeck/mysql/querier.go @@ -15,6 +15,8 @@ type Querier interface { DeleteVenue(ctx context.Context, arg DeleteVenueParams) error GetCity(ctx context.Context, slug string) (City, error) GetVenue(ctx context.Context, arg GetVenueParams) (Venue, error) + IterCities(ctx context.Context) IterCitiesRows + IterVenues(ctx context.Context, city string) IterVenuesRows ListCities(ctx context.Context) ([]City, error) ListVenues(ctx context.Context, city string) ([]Venue, error) UpdateCityName(ctx context.Context, arg UpdateCityNameParams) error diff --git a/examples/ondeck/mysql/query/city.sql b/examples/ondeck/mysql/query/city.sql index c387e9d000..714593adbd 100644 --- a/examples/ondeck/mysql/query/city.sql +++ b/examples/ondeck/mysql/query/city.sql @@ -3,6 +3,11 @@ SELECT * FROM city ORDER BY name; +/* name: IterCities :iter */ +SELECT * +FROM city +ORDER BY name; + /* name: GetCity :one */ SELECT * FROM city diff --git a/examples/ondeck/mysql/query/venue.sql b/examples/ondeck/mysql/query/venue.sql index a1dd7a1633..9243c584ed 100644 --- a/examples/ondeck/mysql/query/venue.sql +++ b/examples/ondeck/mysql/query/venue.sql @@ -4,6 +4,12 @@ FROM venue WHERE city = ? ORDER BY name; +/* name: IterVenues :iter */ +SELECT * +FROM venue +WHERE city = ? +ORDER BY name; + /* name: DeleteVenue :exec */ DELETE FROM venue WHERE slug = ? AND slug = ?; diff --git a/examples/ondeck/mysql/venue.sql.go b/examples/ondeck/mysql/venue.sql.go index 3f16fca9ec..67a1e88f2f 100644 --- a/examples/ondeck/mysql/venue.sql.go +++ b/examples/ondeck/mysql/venue.sql.go @@ -8,6 +8,7 @@ package ondeck import ( "context" "database/sql" + "iter" ) const createVenue = `-- name: CreateVenue :execresult @@ -98,6 +99,72 @@ func (q *Queries) GetVenue(ctx context.Context, arg GetVenueParams) (Venue, erro return i, err } +const iterVenues = `-- name: IterVenues :iter +SELECT id, status, statuses, slug, name, city, spotify_playlist, songkick_id, tags, created_at +FROM venue +WHERE city = ? +ORDER BY name +` + +func (q *Queries) IterVenues(ctx context.Context, city string) IterVenuesRows { + rows, err := q.query(ctx, q.iterVenuesStmt, iterVenues, city) + if err != nil { + return IterVenuesRows{err: err} + } + return IterVenuesRows{rows: rows} +} + +type IterVenuesRows struct { + rows *sql.Rows + err error +} + +func (r *IterVenuesRows) Iterate() iter.Seq[Venue] { + if r.rows == nil { + return func(yield func(Venue) bool) {} + } + + return func(yield func(Venue) bool) { + defer r.rows.Close() + + for r.rows.Next() { + var i Venue + err := r.rows.Scan( + &i.ID, + &i.Status, + &i.Statuses, + &i.Slug, + &i.Name, + &i.City, + &i.SpotifyPlaylist, + &i.SongkickID, + &i.Tags, + &i.CreatedAt, + ) + if err != nil { + r.err = err + return + } + + if !yield(i) { + r.err = r.rows.Close() + return + } + } + } +} + +func (r *IterVenuesRows) Close() error { + return r.rows.Close() +} + +func (r *IterVenuesRows) Err() error { + if r.err != nil { + return r.err + } + return r.rows.Err() +} + const listVenues = `-- name: ListVenues :many SELECT id, status, statuses, slug, name, city, spotify_playlist, songkick_id, tags, created_at FROM venue diff --git a/examples/ondeck/postgresql/city.sql.go b/examples/ondeck/postgresql/city.sql.go index 528ef10f3e..d6258d4fb4 100644 --- a/examples/ondeck/postgresql/city.sql.go +++ b/examples/ondeck/postgresql/city.sql.go @@ -7,6 +7,8 @@ package ondeck import ( "context" + "database/sql" + "iter" ) const createCity = `-- name: CreateCity :one @@ -47,6 +49,60 @@ func (q *Queries) GetCity(ctx context.Context, slug string) (City, error) { return i, err } +const iterCities = `-- name: IterCities :iter +SELECT slug, name +FROM city +ORDER BY name +` + +func (q *Queries) IterCities(ctx context.Context) IterCitiesRows { + rows, err := q.query(ctx, q.iterCitiesStmt, iterCities) + if err != nil { + return IterCitiesRows{err: err} + } + return IterCitiesRows{rows: rows} +} + +type IterCitiesRows struct { + rows *sql.Rows + err error +} + +func (r *IterCitiesRows) Iterate() iter.Seq[City] { + if r.rows == nil { + return func(yield func(City) bool) {} + } + + return func(yield func(City) bool) { + defer r.rows.Close() + + for r.rows.Next() { + var i City + err := r.rows.Scan(&i.Slug, &i.Name) + if err != nil { + r.err = err + return + } + + if !yield(i) { + r.err = r.rows.Close() + return + } + } + } +} + +func (r *IterCitiesRows) Close() error { + return r.rows.Close() +} + +func (r *IterCitiesRows) Err() error { + if r.err != nil { + return r.err + } + return r.rows.Err() +} + const listCities = `-- name: ListCities :many SELECT slug, name FROM city diff --git a/examples/ondeck/postgresql/db.go b/examples/ondeck/postgresql/db.go index 09429da435..563dc857fb 100644 --- a/examples/ondeck/postgresql/db.go +++ b/examples/ondeck/postgresql/db.go @@ -39,6 +39,12 @@ func Prepare(ctx context.Context, db DBTX) (*Queries, error) { if q.getVenueStmt, err = db.PrepareContext(ctx, getVenue); err != nil { return nil, fmt.Errorf("error preparing query GetVenue: %w", err) } + if q.iterCitiesStmt, err = db.PrepareContext(ctx, iterCities); err != nil { + return nil, fmt.Errorf("error preparing query IterCities: %w", err) + } + if q.iterVenuesStmt, err = db.PrepareContext(ctx, iterVenues); err != nil { + return nil, fmt.Errorf("error preparing query IterVenues: %w", err) + } if q.listCitiesStmt, err = db.PrepareContext(ctx, listCities); err != nil { return nil, fmt.Errorf("error preparing query ListCities: %w", err) } @@ -84,6 +90,16 @@ func (q *Queries) Close() error { err = fmt.Errorf("error closing getVenueStmt: %w", cerr) } } + if q.iterCitiesStmt != nil { + if cerr := q.iterCitiesStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing iterCitiesStmt: %w", cerr) + } + } + if q.iterVenuesStmt != nil { + if cerr := q.iterVenuesStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing iterVenuesStmt: %w", cerr) + } + } if q.listCitiesStmt != nil { if cerr := q.listCitiesStmt.Close(); cerr != nil { err = fmt.Errorf("error closing listCitiesStmt: %w", cerr) @@ -153,6 +169,8 @@ type Queries struct { deleteVenueStmt *sql.Stmt getCityStmt *sql.Stmt getVenueStmt *sql.Stmt + iterCitiesStmt *sql.Stmt + iterVenuesStmt *sql.Stmt listCitiesStmt *sql.Stmt listVenuesStmt *sql.Stmt updateCityNameStmt *sql.Stmt @@ -169,6 +187,8 @@ func (q *Queries) WithTx(tx *sql.Tx) *Queries { deleteVenueStmt: q.deleteVenueStmt, getCityStmt: q.getCityStmt, getVenueStmt: q.getVenueStmt, + iterCitiesStmt: q.iterCitiesStmt, + iterVenuesStmt: q.iterVenuesStmt, listCitiesStmt: q.listCitiesStmt, listVenuesStmt: q.listVenuesStmt, updateCityNameStmt: q.updateCityNameStmt, diff --git a/examples/ondeck/postgresql/db_test.go b/examples/ondeck/postgresql/db_test.go index 6d2fba6714..026fa2bb75 100644 --- a/examples/ondeck/postgresql/db_test.go +++ b/examples/ondeck/postgresql/db_test.go @@ -81,6 +81,20 @@ func runOnDeckQueries(t *testing.T, q *Queries) { } } + { + var actualCities []City + rows := q.IterCities(ctx) + for actualCity := range rows.Iterate() { + actualCities = append(actualCities, actualCity) + } + if err = rows.Err(); err != nil { + t.Error(err) + } + if diff := cmp.Diff(actualCities, []City{city}); diff != "" { + t.Errorf("iter city mismatch:\n%s", diff) + } + } + { actual, err := q.ListVenues(ctx, city.Slug) if err != nil { @@ -91,6 +105,20 @@ func runOnDeckQueries(t *testing.T, q *Queries) { } } + { + var actualVenues []Venue + rows := q.IterVenues(ctx, city.Slug) + for actualVenue := range rows.Iterate() { + actualVenues = append(actualVenues, actualVenue) + } + if err = rows.Err(); err != nil { + t.Error(err) + } + if diff := cmp.Diff(actualVenues, []Venue{venue}); diff != "" { + t.Errorf("iter venue mismatch:\n%s", diff) + } + } + { err := q.UpdateCityName(ctx, UpdateCityNameParams{ Slug: city.Slug, diff --git a/examples/ondeck/postgresql/querier.go b/examples/ondeck/postgresql/querier.go index 30ddca54ca..2e13ac5fe1 100644 --- a/examples/ondeck/postgresql/querier.go +++ b/examples/ondeck/postgresql/querier.go @@ -17,6 +17,8 @@ type Querier interface { DeleteVenue(ctx context.Context, slug string) error GetCity(ctx context.Context, slug string) (City, error) GetVenue(ctx context.Context, arg GetVenueParams) (Venue, error) + IterCities(ctx context.Context) IterCitiesRows + IterVenues(ctx context.Context, city string) IterVenuesRows ListCities(ctx context.Context) ([]City, error) ListVenues(ctx context.Context, city string) ([]Venue, error) UpdateCityName(ctx context.Context, arg UpdateCityNameParams) error diff --git a/examples/ondeck/postgresql/query/city.sql b/examples/ondeck/postgresql/query/city.sql index f34dc9961e..a60ad2d5fc 100644 --- a/examples/ondeck/postgresql/query/city.sql +++ b/examples/ondeck/postgresql/query/city.sql @@ -3,6 +3,11 @@ SELECT * FROM city ORDER BY name; +-- name: IterCities :iter +SELECT * +FROM city +ORDER BY name; + -- name: GetCity :one SELECT * FROM city diff --git a/examples/ondeck/postgresql/query/venue.sql b/examples/ondeck/postgresql/query/venue.sql index 8c6bd02664..79ba73a2f6 100644 --- a/examples/ondeck/postgresql/query/venue.sql +++ b/examples/ondeck/postgresql/query/venue.sql @@ -4,6 +4,12 @@ FROM venue WHERE city = $1 ORDER BY name; +-- name: IterVenues :iter +SELECT * +FROM venue +WHERE city = $1 +ORDER BY name; + -- name: DeleteVenue :exec DELETE FROM venue WHERE slug = $1 AND slug = $1; diff --git a/examples/ondeck/postgresql/venue.sql.go b/examples/ondeck/postgresql/venue.sql.go index dbb9a7028d..f8899973d9 100644 --- a/examples/ondeck/postgresql/venue.sql.go +++ b/examples/ondeck/postgresql/venue.sql.go @@ -7,6 +7,8 @@ package ondeck import ( "context" + "database/sql" + "iter" "github.com/lib/pq" ) @@ -97,6 +99,72 @@ func (q *Queries) GetVenue(ctx context.Context, arg GetVenueParams) (Venue, erro return i, err } +const iterVenues = `-- name: IterVenues :iter +SELECT id, status, statuses, slug, name, city, spotify_playlist, songkick_id, tags, created_at +FROM venue +WHERE city = $1 +ORDER BY name +` + +func (q *Queries) IterVenues(ctx context.Context, city string) IterVenuesRows { + rows, err := q.query(ctx, q.iterVenuesStmt, iterVenues, city) + if err != nil { + return IterVenuesRows{err: err} + } + return IterVenuesRows{rows: rows} +} + +type IterVenuesRows struct { + rows *sql.Rows + err error +} + +func (r *IterVenuesRows) Iterate() iter.Seq[Venue] { + if r.rows == nil { + return func(yield func(Venue) bool) {} + } + + return func(yield func(Venue) bool) { + defer r.rows.Close() + + for r.rows.Next() { + var i Venue + err := r.rows.Scan( + &i.ID, + &i.Status, + pq.Array(&i.Statuses), + &i.Slug, + &i.Name, + &i.City, + &i.SpotifyPlaylist, + &i.SongkickID, + pq.Array(&i.Tags), + &i.CreatedAt, + ) + if err != nil { + r.err = err + return + } + + if !yield(i) { + r.err = r.rows.Close() + return + } + } + } +} + +func (r *IterVenuesRows) Close() error { + return r.rows.Close() +} + +func (r *IterVenuesRows) Err() error { + if r.err != nil { + return r.err + } + return r.rows.Err() +} + const listVenues = `-- name: ListVenues :many SELECT id, status, statuses, slug, name, city, spotify_playlist, songkick_id, tags, created_at FROM venue diff --git a/examples/ondeck/sqlite/city.sql.go b/examples/ondeck/sqlite/city.sql.go index ee90b3b559..b4ac1cbc9e 100644 --- a/examples/ondeck/sqlite/city.sql.go +++ b/examples/ondeck/sqlite/city.sql.go @@ -7,6 +7,8 @@ package ondeck import ( "context" + "database/sql" + "iter" ) const createCity = `-- name: CreateCity :exec @@ -42,6 +44,60 @@ func (q *Queries) GetCity(ctx context.Context, slug string) (City, error) { return i, err } +const iterCities = `-- name: IterCities :iter +SELECT slug, name +FROM city +ORDER BY name +` + +func (q *Queries) IterCities(ctx context.Context) IterCitiesRows { + rows, err := q.query(ctx, q.iterCitiesStmt, iterCities) + if err != nil { + return IterCitiesRows{err: err} + } + return IterCitiesRows{rows: rows} +} + +type IterCitiesRows struct { + rows *sql.Rows + err error +} + +func (r *IterCitiesRows) Iterate() iter.Seq[City] { + if r.rows == nil { + return func(yield func(City) bool) {} + } + + return func(yield func(City) bool) { + defer r.rows.Close() + + for r.rows.Next() { + var i City + err := r.rows.Scan(&i.Slug, &i.Name) + if err != nil { + r.err = err + return + } + + if !yield(i) { + r.err = r.rows.Close() + return + } + } + } +} + +func (r *IterCitiesRows) Close() error { + return r.rows.Close() +} + +func (r *IterCitiesRows) Err() error { + if r.err != nil { + return r.err + } + return r.rows.Err() +} + const listCities = `-- name: ListCities :many SELECT slug, name FROM city diff --git a/examples/ondeck/sqlite/db.go b/examples/ondeck/sqlite/db.go index 09429da435..563dc857fb 100644 --- a/examples/ondeck/sqlite/db.go +++ b/examples/ondeck/sqlite/db.go @@ -39,6 +39,12 @@ func Prepare(ctx context.Context, db DBTX) (*Queries, error) { if q.getVenueStmt, err = db.PrepareContext(ctx, getVenue); err != nil { return nil, fmt.Errorf("error preparing query GetVenue: %w", err) } + if q.iterCitiesStmt, err = db.PrepareContext(ctx, iterCities); err != nil { + return nil, fmt.Errorf("error preparing query IterCities: %w", err) + } + if q.iterVenuesStmt, err = db.PrepareContext(ctx, iterVenues); err != nil { + return nil, fmt.Errorf("error preparing query IterVenues: %w", err) + } if q.listCitiesStmt, err = db.PrepareContext(ctx, listCities); err != nil { return nil, fmt.Errorf("error preparing query ListCities: %w", err) } @@ -84,6 +90,16 @@ func (q *Queries) Close() error { err = fmt.Errorf("error closing getVenueStmt: %w", cerr) } } + if q.iterCitiesStmt != nil { + if cerr := q.iterCitiesStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing iterCitiesStmt: %w", cerr) + } + } + if q.iterVenuesStmt != nil { + if cerr := q.iterVenuesStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing iterVenuesStmt: %w", cerr) + } + } if q.listCitiesStmt != nil { if cerr := q.listCitiesStmt.Close(); cerr != nil { err = fmt.Errorf("error closing listCitiesStmt: %w", cerr) @@ -153,6 +169,8 @@ type Queries struct { deleteVenueStmt *sql.Stmt getCityStmt *sql.Stmt getVenueStmt *sql.Stmt + iterCitiesStmt *sql.Stmt + iterVenuesStmt *sql.Stmt listCitiesStmt *sql.Stmt listVenuesStmt *sql.Stmt updateCityNameStmt *sql.Stmt @@ -169,6 +187,8 @@ func (q *Queries) WithTx(tx *sql.Tx) *Queries { deleteVenueStmt: q.deleteVenueStmt, getCityStmt: q.getCityStmt, getVenueStmt: q.getVenueStmt, + iterCitiesStmt: q.iterCitiesStmt, + iterVenuesStmt: q.iterVenuesStmt, listCitiesStmt: q.listCitiesStmt, listVenuesStmt: q.listVenuesStmt, updateCityNameStmt: q.updateCityNameStmt, diff --git a/examples/ondeck/sqlite/db_test.go b/examples/ondeck/sqlite/db_test.go index 39264c26d4..92141b0d34 100644 --- a/examples/ondeck/sqlite/db_test.go +++ b/examples/ondeck/sqlite/db_test.go @@ -96,6 +96,20 @@ func runOnDeckQueries(t *testing.T, q *Queries) { } } + { + var actualCities []City + rows := q.IterCities(ctx) + for actualCity := range rows.Iterate() { + actualCities = append(actualCities, actualCity) + } + if err = rows.Err(); err != nil { + t.Error(err) + } + if diff := cmp.Diff(actualCities, []City{city}); diff != "" { + t.Errorf("iter city mismatch:\n%s", diff) + } + } + { actual, err := q.ListVenues(ctx, city.Slug) if err != nil { @@ -106,6 +120,20 @@ func runOnDeckQueries(t *testing.T, q *Queries) { } } + { + var actualVenues []Venue + rows := q.IterVenues(ctx, city.Slug) + for actualVenue := range rows.Iterate() { + actualVenues = append(actualVenues, actualVenue) + } + if err = rows.Err(); err != nil { + t.Error(err) + } + if diff := cmp.Diff(actualVenues, []Venue{venue}); diff != "" { + t.Errorf("iter venue mismatch:\n%s", diff) + } + } + { err := q.UpdateCityName(ctx, UpdateCityNameParams{ Slug: city.Slug, diff --git a/examples/ondeck/sqlite/querier.go b/examples/ondeck/sqlite/querier.go index 7f615fc2be..99e6483392 100644 --- a/examples/ondeck/sqlite/querier.go +++ b/examples/ondeck/sqlite/querier.go @@ -15,6 +15,8 @@ type Querier interface { DeleteVenue(ctx context.Context, arg DeleteVenueParams) error GetCity(ctx context.Context, slug string) (City, error) GetVenue(ctx context.Context, arg GetVenueParams) (Venue, error) + IterCities(ctx context.Context) IterCitiesRows + IterVenues(ctx context.Context, city string) IterVenuesRows ListCities(ctx context.Context) ([]City, error) ListVenues(ctx context.Context, city string) ([]Venue, error) UpdateCityName(ctx context.Context, arg UpdateCityNameParams) error diff --git a/examples/ondeck/sqlite/query/city.sql b/examples/ondeck/sqlite/query/city.sql index c387e9d000..714593adbd 100644 --- a/examples/ondeck/sqlite/query/city.sql +++ b/examples/ondeck/sqlite/query/city.sql @@ -3,6 +3,11 @@ SELECT * FROM city ORDER BY name; +/* name: IterCities :iter */ +SELECT * +FROM city +ORDER BY name; + /* name: GetCity :one */ SELECT * FROM city diff --git a/examples/ondeck/sqlite/query/venue.sql b/examples/ondeck/sqlite/query/venue.sql index b4f5fd4071..f234e2ee3b 100644 --- a/examples/ondeck/sqlite/query/venue.sql +++ b/examples/ondeck/sqlite/query/venue.sql @@ -4,6 +4,12 @@ FROM venue WHERE city = ? ORDER BY name; +/* name: IterVenues :iter */ +SELECT * +FROM venue +WHERE city = ? +ORDER BY name; + /* name: DeleteVenue :exec */ DELETE FROM venue WHERE slug = ? AND slug = ?; diff --git a/examples/ondeck/sqlite/venue.sql.go b/examples/ondeck/sqlite/venue.sql.go index b6c063e03c..f682e0e5c8 100644 --- a/examples/ondeck/sqlite/venue.sql.go +++ b/examples/ondeck/sqlite/venue.sql.go @@ -8,6 +8,7 @@ package ondeck import ( "context" "database/sql" + "iter" ) const createVenue = `-- name: CreateVenue :execresult @@ -98,6 +99,72 @@ func (q *Queries) GetVenue(ctx context.Context, arg GetVenueParams) (Venue, erro return i, err } +const iterVenues = `-- name: IterVenues :iter +SELECT id, status, statuses, slug, name, city, spotify_playlist, songkick_id, tags, created_at +FROM venue +WHERE city = ? +ORDER BY name +` + +func (q *Queries) IterVenues(ctx context.Context, city string) IterVenuesRows { + rows, err := q.query(ctx, q.iterVenuesStmt, iterVenues, city) + if err != nil { + return IterVenuesRows{err: err} + } + return IterVenuesRows{rows: rows} +} + +type IterVenuesRows struct { + rows *sql.Rows + err error +} + +func (r *IterVenuesRows) Iterate() iter.Seq[Venue] { + if r.rows == nil { + return func(yield func(Venue) bool) {} + } + + return func(yield func(Venue) bool) { + defer r.rows.Close() + + for r.rows.Next() { + var i Venue + err := r.rows.Scan( + &i.ID, + &i.Status, + &i.Statuses, + &i.Slug, + &i.Name, + &i.City, + &i.SpotifyPlaylist, + &i.SongkickID, + &i.Tags, + &i.CreatedAt, + ) + if err != nil { + r.err = err + return + } + + if !yield(i) { + r.err = r.rows.Close() + return + } + } + } +} + +func (r *IterVenuesRows) Close() error { + return r.rows.Close() +} + +func (r *IterVenuesRows) Err() error { + if r.err != nil { + return r.err + } + return r.rows.Err() +} + const listVenues = `-- name: ListVenues :many SELECT id, status, statuses, slug, name, city, spotify_playlist, songkick_id, tags, created_at FROM venue diff --git a/go.mod b/go.mod index 74a07017ee..f6b4224e14 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,6 @@ module github.com/sqlc-dev/sqlc -go 1.22 - -toolchain go1.22.8 +go 1.23 require ( github.com/antlr4-go/antlr/v4 v4.13.1 diff --git a/internal/codegen/golang/gen.go b/internal/codegen/golang/gen.go index 5b7977f500..0c5eccb870 100644 --- a/internal/codegen/golang/gen.go +++ b/internal/codegen/golang/gen.go @@ -73,7 +73,7 @@ func (t *tmplCtx) codegenQueryMethod(q Query) string { } return db + ".QueryRowContext" - case ":many": + case ":many", ":iter": if t.EmitPreparedQueries { return "q.query" } @@ -91,7 +91,7 @@ func (t *tmplCtx) codegenQueryRetval(q Query) (string, error) { switch q.Cmd { case ":one": return "row :=", nil - case ":many": + case ":many", ":iter": return "rows, err :=", nil case ":exec": return "_, err :=", nil diff --git a/internal/codegen/golang/imports.go b/internal/codegen/golang/imports.go index 9e7819e4b1..5e4f6636be 100644 --- a/internal/codegen/golang/imports.go +++ b/internal/codegen/golang/imports.go @@ -299,6 +299,7 @@ func sortedImports(std map[string]struct{}, pkg map[ImportSpec]struct{}) fileImp func (i *importer) queryImports(filename string) fileImports { var gq []Query anyNonCopyFrom := false + containIter := false for _, query := range i.Queries { if usesBatch([]Query{query}) { continue @@ -308,6 +309,9 @@ func (i *importer) queryImports(filename string) fileImports { if query.Cmd != metadata.CmdCopyFrom { anyNonCopyFrom = true } + if query.Cmd == metadata.CmdIter { + containIter = true + } } } @@ -402,6 +406,18 @@ func (i *importer) queryImports(filename string) fileImports { pkg[ImportSpec{Path: "github.com/lib/pq"}] = struct{}{} } + if containIter { + std["iter"] = struct{}{} + switch sqlpkg { + case opts.SQLDriverPGXV4: + pkg[ImportSpec{Path: "github.com/jackc/pgx/v4"}] = struct{}{} + case opts.SQLDriverPGXV5: + pkg[ImportSpec{Path: "github.com/jackc/pgx/v5"}] = struct{}{} + default: + std["database/sql"] = struct{}{} + } + } + return sortedImports(std, pkg) } diff --git a/internal/codegen/golang/query.go b/internal/codegen/golang/query.go index 3b4fb2fa1a..3672f6a1f1 100644 --- a/internal/codegen/golang/query.go +++ b/internal/codegen/golang/query.go @@ -270,7 +270,7 @@ type Query struct { } func (q Query) hasRetType() bool { - scanned := q.Cmd == metadata.CmdOne || q.Cmd == metadata.CmdMany || + scanned := q.Cmd == metadata.CmdOne || q.Cmd == metadata.CmdMany || q.Cmd == metadata.CmdIter || q.Cmd == metadata.CmdBatchMany || q.Cmd == metadata.CmdBatchOne return scanned && !q.Ret.isEmpty() } diff --git a/internal/codegen/golang/result.go b/internal/codegen/golang/result.go index 515d0a654f..f97a029773 100644 --- a/internal/codegen/golang/result.go +++ b/internal/codegen/golang/result.go @@ -335,6 +335,7 @@ var cmdReturnsData = map[string]struct{}{ metadata.CmdBatchMany: {}, metadata.CmdBatchOne: {}, metadata.CmdMany: {}, + metadata.CmdIter: {}, metadata.CmdOne: {}, } diff --git a/internal/codegen/golang/result_test.go b/internal/codegen/golang/result_test.go index 0c58525ec3..d751771b0a 100644 --- a/internal/codegen/golang/result_test.go +++ b/internal/codegen/golang/result_test.go @@ -32,6 +32,10 @@ func TestPutOutColumns_ForZeroColumns(t *testing.T) { cmd: metadata.CmdMany, want: true, }, + { + cmd: metadata.CmdIter, + want: true, + }, { cmd: metadata.CmdOne, want: true, diff --git a/internal/codegen/golang/templates/pgx/interfaceCode.tmpl b/internal/codegen/golang/templates/pgx/interfaceCode.tmpl index cf7cd36cb9..ac4d77a711 100644 --- a/internal/codegen/golang/templates/pgx/interfaceCode.tmpl +++ b/internal/codegen/golang/templates/pgx/interfaceCode.tmpl @@ -20,6 +20,15 @@ {{end -}} {{.MethodName}}(ctx context.Context, {{.Arg.Pair}}) ([]{{.Ret.DefineType}}, error) {{- end}} + {{- if and (eq .Cmd ":iter") ($dbtxParam) }} + {{range .Comments}}//{{.}} + {{end -}} + {{.MethodName}}(ctx context.Context, db DBTX, {{.Arg.Pair}}) {{.MethodName}}Rows + {{- else if eq .Cmd ":iter"}} + {{range .Comments}}//{{.}} + {{end -}} + {{.MethodName}}(ctx context.Context, {{.Arg.Pair}}) {{.MethodName}}Rows + {{- end}} {{- if and (eq .Cmd ":exec") ($dbtxParam) }} {{range .Comments}}//{{.}} {{end -}} diff --git a/internal/codegen/golang/templates/pgx/queryCode.tmpl b/internal/codegen/golang/templates/pgx/queryCode.tmpl index 18de5db2ba..9ddcba8e5f 100644 --- a/internal/codegen/golang/templates/pgx/queryCode.tmpl +++ b/internal/codegen/golang/templates/pgx/queryCode.tmpl @@ -74,6 +74,62 @@ func (q *Queries) {{.MethodName}}(ctx context.Context, {{.Arg.Pair}}) ([]{{.Ret. } {{end}} +{{if eq .Cmd ":iter"}} +{{range .Comments}}//{{.}} +{{end -}} +{{- if $.EmitMethodsWithDBArgument -}} +func (q *Queries) {{.MethodName}}(ctx context.Context, db DBTX, {{.Arg.Pair}}) {{.MethodName}}Rows { + rows, err := db.Query(ctx, {{.ConstantName}}, {{.Arg.Params}}) +{{- else -}} +func (q *Queries) {{.MethodName}}(ctx context.Context, {{.Arg.Pair}}) {{.MethodName}}Rows { + rows, err := q.db.Query(ctx, {{.ConstantName}}, {{.Arg.Params}}) +{{- end}} + if err != nil { + return {{.MethodName}}Rows{err: err} + } + return {{.MethodName}}Rows{rows: rows} +} + +type {{.MethodName}}Rows struct{ + rows pgx.Rows + err error +} + +func (r *{{.MethodName}}Rows) Iterate() iter.Seq[{{.Ret.DefineType}}] { + if r.rows == nil { + return func(yield func({{.Ret.DefineType}}) bool) {} + } + + return func(yield func({{.Ret.DefineType}}) bool) { + defer r.rows.Close() + + for r.rows.Next() { + var {{.Ret.Name}} {{.Ret.Type}} + err := r.rows.Scan({{.Ret.Scan}}) + if err != nil { + r.err = err + return + } + + if !yield({{.Ret.ReturnName}}) { + return + } + } + } +} + +func (r *{{.MethodName}}Rows) Close() { + r.rows.Close() +} + +func (r *{{.MethodName}}Rows) Err() error { + if r.err != nil { + return r.err + } + return r.rows.Err() +} +{{end}} + {{if eq .Cmd ":exec"}} {{range .Comments}}//{{.}} {{end -}} diff --git a/internal/codegen/golang/templates/stdlib/interfaceCode.tmpl b/internal/codegen/golang/templates/stdlib/interfaceCode.tmpl index 3cbefe6df4..c41bdd0e9e 100644 --- a/internal/codegen/golang/templates/stdlib/interfaceCode.tmpl +++ b/internal/codegen/golang/templates/stdlib/interfaceCode.tmpl @@ -20,6 +20,15 @@ {{end -}} {{.MethodName}}(ctx context.Context, {{.Arg.Pair}}) ([]{{.Ret.DefineType}}, error) {{- end}} + {{- if and (eq .Cmd ":iter") ($dbtxParam) }} + {{range .Comments}}//{{.}} + {{end -}} + {{.MethodName}}(ctx context.Context, db DBTX, {{.Arg.Pair}}) {{.MethodName}}Rows + {{- else if eq .Cmd ":iter"}} + {{range .Comments}}//{{.}} + {{end -}} + {{.MethodName}}(ctx context.Context, {{.Arg.Pair}}) {{.MethodName}}Rows + {{- end}} {{- if and (eq .Cmd ":exec") ($dbtxParam) }} {{range .Comments}}//{{.}} {{end -}} diff --git a/internal/codegen/golang/templates/stdlib/queryCode.tmpl b/internal/codegen/golang/templates/stdlib/queryCode.tmpl index cf56000ec6..933b655f9e 100644 --- a/internal/codegen/golang/templates/stdlib/queryCode.tmpl +++ b/internal/codegen/golang/templates/stdlib/queryCode.tmpl @@ -63,6 +63,58 @@ func (q *Queries) {{.MethodName}}(ctx context.Context, {{ dbarg }} {{.Arg.Pair}} } {{end}} +{{if eq .Cmd ":iter"}} +{{range .Comments}}//{{.}} +{{end -}} +func (q *Queries) {{.MethodName}}(ctx context.Context, {{ dbarg }} {{.Arg.Pair}}) {{.MethodName}}Rows { + {{- template "queryCodeStdExec" . }} + if err != nil { + return {{.MethodName}}Rows{err: err} + } + return {{.MethodName}}Rows{rows: rows} +} + +type {{.MethodName}}Rows struct{ + rows *sql.Rows + err error +} + +func (r *{{.MethodName}}Rows) Iterate() iter.Seq[{{.Ret.DefineType}}] { + if r.rows == nil { + return func(yield func({{.Ret.DefineType}}) bool) {} + } + + return func(yield func({{.Ret.DefineType}}) bool) { + defer r.rows.Close() + + for r.rows.Next() { + var {{.Ret.Name}} {{.Ret.Type}} + err := r.rows.Scan({{.Ret.Scan}}) + if err != nil { + r.err = err + return + } + + if !yield({{.Ret.ReturnName}}) { + r.err = r.rows.Close() + return + } + } + } +} + +func (r *{{.MethodName}}Rows) Close() error { + return r.rows.Close() +} + +func (r *{{.MethodName}}Rows) Err() error { + if r.err != nil { + return r.err + } + return r.rows.Err() +} +{{end}} + {{if eq .Cmd ":exec"}} {{range .Comments}}//{{.}} {{end -}} diff --git a/internal/endtoend/testdata/iter/mysql/go/db.go b/internal/endtoend/testdata/iter/mysql/go/db.go new file mode 100644 index 0000000000..0ea90b328a --- /dev/null +++ b/internal/endtoend/testdata/iter/mysql/go/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.27.0 + +package querytest + +import ( + "context" + "database/sql" +) + +type DBTX interface { + ExecContext(context.Context, string, ...interface{}) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...interface{}) *sql.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/internal/endtoend/testdata/iter/mysql/go/models.go b/internal/endtoend/testdata/iter/mysql/go/models.go new file mode 100644 index 0000000000..52b17d356d --- /dev/null +++ b/internal/endtoend/testdata/iter/mysql/go/models.go @@ -0,0 +1,14 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.27.0 + +package querytest + +import ( + "database/sql" +) + +type Foo struct { + A sql.NullString + B sql.NullInt32 +} diff --git a/internal/endtoend/testdata/iter/mysql/go/query.sql.go b/internal/endtoend/testdata/iter/mysql/go/query.sql.go new file mode 100644 index 0000000000..5cf2437968 --- /dev/null +++ b/internal/endtoend/testdata/iter/mysql/go/query.sql.go @@ -0,0 +1,66 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.27.0 +// source: query.sql + +package querytest + +import ( + "context" + "database/sql" + "iter" +) + +const iterValues = `-- name: IterValues :iter +SELECT a, b +FROM foo +WHERE b = ? +` + +func (q *Queries) IterValues(ctx context.Context, b sql.NullInt32) IterValuesRows { + rows, err := q.db.QueryContext(ctx, iterValues, b) + if err != nil { + return IterValuesRows{err: err} + } + return IterValuesRows{rows: rows} +} + +type IterValuesRows struct { + rows *sql.Rows + err error +} + +func (r *IterValuesRows) Iterate() iter.Seq[Foo] { + if r.rows == nil { + return func(yield func(Foo) bool) {} + } + + return func(yield func(Foo) bool) { + defer r.rows.Close() + + for r.rows.Next() { + var i Foo + err := r.rows.Scan(&i.A, &i.B) + if err != nil { + r.err = err + return + } + + if !yield(i) { + r.err = r.rows.Close() + return + } + } + } +} + +func (r *IterValuesRows) Close() error { + return r.rows.Close() +} + +func (r *IterValuesRows) Err() error { + if r.err != nil { + return r.err + } + return r.rows.Err() +} diff --git a/internal/endtoend/testdata/iter/mysql/query.sql b/internal/endtoend/testdata/iter/mysql/query.sql new file mode 100644 index 0000000000..7688e5fe2e --- /dev/null +++ b/internal/endtoend/testdata/iter/mysql/query.sql @@ -0,0 +1,4 @@ +-- name: IterValues :iter +SELECT * +FROM foo +WHERE b = ?; diff --git a/internal/endtoend/testdata/iter/mysql/schema.sql b/internal/endtoend/testdata/iter/mysql/schema.sql new file mode 100644 index 0000000000..ca4d437be0 --- /dev/null +++ b/internal/endtoend/testdata/iter/mysql/schema.sql @@ -0,0 +1,2 @@ +CREATE TABLE foo (a text, b integer); + diff --git a/internal/endtoend/testdata/iter/mysql/sqlc.json b/internal/endtoend/testdata/iter/mysql/sqlc.json new file mode 100644 index 0000000000..e41c39e8b3 --- /dev/null +++ b/internal/endtoend/testdata/iter/mysql/sqlc.json @@ -0,0 +1,12 @@ +{ + "version": "1", + "packages": [ + { + "path": "go", + "engine": "mysql", + "name": "querytest", + "schema": "schema.sql", + "queries": "query.sql" + } + ] +} diff --git a/internal/endtoend/testdata/iter/postgresql/pgx/v4/go/db.go b/internal/endtoend/testdata/iter/postgresql/pgx/v4/go/db.go new file mode 100644 index 0000000000..58779be81d --- /dev/null +++ b/internal/endtoend/testdata/iter/postgresql/pgx/v4/go/db.go @@ -0,0 +1,32 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.27.0 + +package querytest + +import ( + "context" + + "github.com/jackc/pgconn" + "github.com/jackc/pgx/v4" +) + +type DBTX interface { + Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error) + Query(context.Context, string, ...interface{}) (pgx.Rows, error) + QueryRow(context.Context, string, ...interface{}) pgx.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx pgx.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/internal/endtoend/testdata/iter/postgresql/pgx/v4/go/models.go b/internal/endtoend/testdata/iter/postgresql/pgx/v4/go/models.go new file mode 100644 index 0000000000..4962ee1aa0 --- /dev/null +++ b/internal/endtoend/testdata/iter/postgresql/pgx/v4/go/models.go @@ -0,0 +1,14 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.27.0 + +package querytest + +import ( + "database/sql" +) + +type MyschemaFoo struct { + A sql.NullString + B sql.NullInt32 +} diff --git a/internal/endtoend/testdata/iter/postgresql/pgx/v4/go/query.sql.go b/internal/endtoend/testdata/iter/postgresql/pgx/v4/go/query.sql.go new file mode 100644 index 0000000000..2c9fa540ba --- /dev/null +++ b/internal/endtoend/testdata/iter/postgresql/pgx/v4/go/query.sql.go @@ -0,0 +1,67 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.27.0 +// source: query.sql + +package querytest + +import ( + "context" + "database/sql" + "iter" + + "github.com/jackc/pgx/v4" +) + +const iterValues = `-- name: IterValues :iter +SELECT a, b +FROM myschema.foo +WHERE b = $1 +` + +func (q *Queries) IterValues(ctx context.Context, b sql.NullInt32) IterValuesRows { + rows, err := q.db.Query(ctx, iterValues, b) + if err != nil { + return IterValuesRows{err: err} + } + return IterValuesRows{rows: rows} +} + +type IterValuesRows struct { + rows pgx.Rows + err error +} + +func (r *IterValuesRows) Iterate() iter.Seq[MyschemaFoo] { + if r.rows == nil { + return func(yield func(MyschemaFoo) bool) {} + } + + return func(yield func(MyschemaFoo) bool) { + defer r.rows.Close() + + for r.rows.Next() { + var i MyschemaFoo + err := r.rows.Scan(&i.A, &i.B) + if err != nil { + r.err = err + return + } + + if !yield(i) { + return + } + } + } +} + +func (r *IterValuesRows) Close() { + r.rows.Close() +} + +func (r *IterValuesRows) Err() error { + if r.err != nil { + return r.err + } + return r.rows.Err() +} diff --git a/internal/endtoend/testdata/iter/postgresql/pgx/v4/query.sql b/internal/endtoend/testdata/iter/postgresql/pgx/v4/query.sql new file mode 100644 index 0000000000..e245a05a7a --- /dev/null +++ b/internal/endtoend/testdata/iter/postgresql/pgx/v4/query.sql @@ -0,0 +1,4 @@ +-- name: IterValues :iter +SELECT * +FROM myschema.foo +WHERE b = $1; diff --git a/internal/endtoend/testdata/iter/postgresql/pgx/v4/schema.sql b/internal/endtoend/testdata/iter/postgresql/pgx/v4/schema.sql new file mode 100644 index 0000000000..e53abe57ad --- /dev/null +++ b/internal/endtoend/testdata/iter/postgresql/pgx/v4/schema.sql @@ -0,0 +1,3 @@ +CREATE SCHEMA myschema; +CREATE TABLE myschema.foo (a text, b integer); + diff --git a/internal/endtoend/testdata/iter/postgresql/pgx/v4/sqlc.json b/internal/endtoend/testdata/iter/postgresql/pgx/v4/sqlc.json new file mode 100644 index 0000000000..d1244c9e7a --- /dev/null +++ b/internal/endtoend/testdata/iter/postgresql/pgx/v4/sqlc.json @@ -0,0 +1,13 @@ +{ + "version": "1", + "packages": [ + { + "path": "go", + "engine": "postgresql", + "sql_package": "pgx/v4", + "name": "querytest", + "schema": "schema.sql", + "queries": "query.sql" + } + ] +} diff --git a/internal/endtoend/testdata/iter/postgresql/pgx/v5/go/db.go b/internal/endtoend/testdata/iter/postgresql/pgx/v5/go/db.go new file mode 100644 index 0000000000..69ef001548 --- /dev/null +++ b/internal/endtoend/testdata/iter/postgresql/pgx/v5/go/db.go @@ -0,0 +1,32 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.27.0 + +package querytest + +import ( + "context" + + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" +) + +type DBTX interface { + Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error) + Query(context.Context, string, ...interface{}) (pgx.Rows, error) + QueryRow(context.Context, string, ...interface{}) pgx.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx pgx.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/internal/endtoend/testdata/iter/postgresql/pgx/v5/go/models.go b/internal/endtoend/testdata/iter/postgresql/pgx/v5/go/models.go new file mode 100644 index 0000000000..12d0af5048 --- /dev/null +++ b/internal/endtoend/testdata/iter/postgresql/pgx/v5/go/models.go @@ -0,0 +1,14 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.27.0 + +package querytest + +import ( + "github.com/jackc/pgx/v5/pgtype" +) + +type MyschemaFoo struct { + A pgtype.Text + B pgtype.Int4 +} diff --git a/internal/endtoend/testdata/iter/postgresql/pgx/v5/go/query.sql.go b/internal/endtoend/testdata/iter/postgresql/pgx/v5/go/query.sql.go new file mode 100644 index 0000000000..885c8493c9 --- /dev/null +++ b/internal/endtoend/testdata/iter/postgresql/pgx/v5/go/query.sql.go @@ -0,0 +1,67 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.27.0 +// source: query.sql + +package querytest + +import ( + "context" + "iter" + + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgtype" +) + +const iterValues = `-- name: IterValues :iter +SELECT a, b +FROM myschema.foo +WHERE b = $1 +` + +func (q *Queries) IterValues(ctx context.Context, b pgtype.Int4) IterValuesRows { + rows, err := q.db.Query(ctx, iterValues, b) + if err != nil { + return IterValuesRows{err: err} + } + return IterValuesRows{rows: rows} +} + +type IterValuesRows struct { + rows pgx.Rows + err error +} + +func (r *IterValuesRows) Iterate() iter.Seq[MyschemaFoo] { + if r.rows == nil { + return func(yield func(MyschemaFoo) bool) {} + } + + return func(yield func(MyschemaFoo) bool) { + defer r.rows.Close() + + for r.rows.Next() { + var i MyschemaFoo + err := r.rows.Scan(&i.A, &i.B) + if err != nil { + r.err = err + return + } + + if !yield(i) { + return + } + } + } +} + +func (r *IterValuesRows) Close() { + r.rows.Close() +} + +func (r *IterValuesRows) Err() error { + if r.err != nil { + return r.err + } + return r.rows.Err() +} diff --git a/internal/endtoend/testdata/iter/postgresql/pgx/v5/query.sql b/internal/endtoend/testdata/iter/postgresql/pgx/v5/query.sql new file mode 100644 index 0000000000..e245a05a7a --- /dev/null +++ b/internal/endtoend/testdata/iter/postgresql/pgx/v5/query.sql @@ -0,0 +1,4 @@ +-- name: IterValues :iter +SELECT * +FROM myschema.foo +WHERE b = $1; diff --git a/internal/endtoend/testdata/iter/postgresql/pgx/v5/schema.sql b/internal/endtoend/testdata/iter/postgresql/pgx/v5/schema.sql new file mode 100644 index 0000000000..e53abe57ad --- /dev/null +++ b/internal/endtoend/testdata/iter/postgresql/pgx/v5/schema.sql @@ -0,0 +1,3 @@ +CREATE SCHEMA myschema; +CREATE TABLE myschema.foo (a text, b integer); + diff --git a/internal/endtoend/testdata/iter/postgresql/pgx/v5/sqlc.json b/internal/endtoend/testdata/iter/postgresql/pgx/v5/sqlc.json new file mode 100644 index 0000000000..32ede07158 --- /dev/null +++ b/internal/endtoend/testdata/iter/postgresql/pgx/v5/sqlc.json @@ -0,0 +1,13 @@ +{ + "version": "1", + "packages": [ + { + "path": "go", + "engine": "postgresql", + "sql_package": "pgx/v5", + "name": "querytest", + "schema": "schema.sql", + "queries": "query.sql" + } + ] +} diff --git a/internal/endtoend/testdata/iter/postgresql/stdlib/go/db.go b/internal/endtoend/testdata/iter/postgresql/stdlib/go/db.go new file mode 100644 index 0000000000..0ea90b328a --- /dev/null +++ b/internal/endtoend/testdata/iter/postgresql/stdlib/go/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.27.0 + +package querytest + +import ( + "context" + "database/sql" +) + +type DBTX interface { + ExecContext(context.Context, string, ...interface{}) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...interface{}) *sql.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/internal/endtoend/testdata/iter/postgresql/stdlib/go/models.go b/internal/endtoend/testdata/iter/postgresql/stdlib/go/models.go new file mode 100644 index 0000000000..4962ee1aa0 --- /dev/null +++ b/internal/endtoend/testdata/iter/postgresql/stdlib/go/models.go @@ -0,0 +1,14 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.27.0 + +package querytest + +import ( + "database/sql" +) + +type MyschemaFoo struct { + A sql.NullString + B sql.NullInt32 +} diff --git a/internal/endtoend/testdata/iter/postgresql/stdlib/go/query.sql.go b/internal/endtoend/testdata/iter/postgresql/stdlib/go/query.sql.go new file mode 100644 index 0000000000..2fbee5f60d --- /dev/null +++ b/internal/endtoend/testdata/iter/postgresql/stdlib/go/query.sql.go @@ -0,0 +1,66 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.27.0 +// source: query.sql + +package querytest + +import ( + "context" + "database/sql" + "iter" +) + +const iterValues = `-- name: IterValues :iter +SELECT a, b +FROM myschema.foo +WHERE b = $1 +` + +func (q *Queries) IterValues(ctx context.Context, b sql.NullInt32) IterValuesRows { + rows, err := q.db.QueryContext(ctx, iterValues, b) + if err != nil { + return IterValuesRows{err: err} + } + return IterValuesRows{rows: rows} +} + +type IterValuesRows struct { + rows *sql.Rows + err error +} + +func (r *IterValuesRows) Iterate() iter.Seq[MyschemaFoo] { + if r.rows == nil { + return func(yield func(MyschemaFoo) bool) {} + } + + return func(yield func(MyschemaFoo) bool) { + defer r.rows.Close() + + for r.rows.Next() { + var i MyschemaFoo + err := r.rows.Scan(&i.A, &i.B) + if err != nil { + r.err = err + return + } + + if !yield(i) { + r.err = r.rows.Close() + return + } + } + } +} + +func (r *IterValuesRows) Close() error { + return r.rows.Close() +} + +func (r *IterValuesRows) Err() error { + if r.err != nil { + return r.err + } + return r.rows.Err() +} diff --git a/internal/endtoend/testdata/iter/postgresql/stdlib/query.sql b/internal/endtoend/testdata/iter/postgresql/stdlib/query.sql new file mode 100644 index 0000000000..e245a05a7a --- /dev/null +++ b/internal/endtoend/testdata/iter/postgresql/stdlib/query.sql @@ -0,0 +1,4 @@ +-- name: IterValues :iter +SELECT * +FROM myschema.foo +WHERE b = $1; diff --git a/internal/endtoend/testdata/iter/postgresql/stdlib/schema.sql b/internal/endtoend/testdata/iter/postgresql/stdlib/schema.sql new file mode 100644 index 0000000000..e53abe57ad --- /dev/null +++ b/internal/endtoend/testdata/iter/postgresql/stdlib/schema.sql @@ -0,0 +1,3 @@ +CREATE SCHEMA myschema; +CREATE TABLE myschema.foo (a text, b integer); + diff --git a/internal/endtoend/testdata/iter/postgresql/stdlib/sqlc.json b/internal/endtoend/testdata/iter/postgresql/stdlib/sqlc.json new file mode 100644 index 0000000000..f717ca2e66 --- /dev/null +++ b/internal/endtoend/testdata/iter/postgresql/stdlib/sqlc.json @@ -0,0 +1,12 @@ +{ + "version": "1", + "packages": [ + { + "path": "go", + "engine": "postgresql", + "name": "querytest", + "schema": "schema.sql", + "queries": "query.sql" + } + ] +} diff --git a/internal/endtoend/testdata/iter/sqlite/go/db.go b/internal/endtoend/testdata/iter/sqlite/go/db.go new file mode 100644 index 0000000000..0ea90b328a --- /dev/null +++ b/internal/endtoend/testdata/iter/sqlite/go/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.27.0 + +package querytest + +import ( + "context" + "database/sql" +) + +type DBTX interface { + ExecContext(context.Context, string, ...interface{}) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...interface{}) *sql.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/internal/endtoend/testdata/iter/sqlite/go/models.go b/internal/endtoend/testdata/iter/sqlite/go/models.go new file mode 100644 index 0000000000..c3eb40fe2e --- /dev/null +++ b/internal/endtoend/testdata/iter/sqlite/go/models.go @@ -0,0 +1,14 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.27.0 + +package querytest + +import ( + "database/sql" +) + +type Foo struct { + A sql.NullString + B sql.NullInt64 +} diff --git a/internal/endtoend/testdata/iter/sqlite/go/query.sql.go b/internal/endtoend/testdata/iter/sqlite/go/query.sql.go new file mode 100644 index 0000000000..2a02892246 --- /dev/null +++ b/internal/endtoend/testdata/iter/sqlite/go/query.sql.go @@ -0,0 +1,66 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.27.0 +// source: query.sql + +package querytest + +import ( + "context" + "database/sql" + "iter" +) + +const iterValues = `-- name: IterValues :iter +SELECT a, b +FROM foo +WHERE b = ? +` + +func (q *Queries) IterValues(ctx context.Context, b sql.NullInt64) IterValuesRows { + rows, err := q.db.QueryContext(ctx, iterValues, b) + if err != nil { + return IterValuesRows{err: err} + } + return IterValuesRows{rows: rows} +} + +type IterValuesRows struct { + rows *sql.Rows + err error +} + +func (r *IterValuesRows) Iterate() iter.Seq[Foo] { + if r.rows == nil { + return func(yield func(Foo) bool) {} + } + + return func(yield func(Foo) bool) { + defer r.rows.Close() + + for r.rows.Next() { + var i Foo + err := r.rows.Scan(&i.A, &i.B) + if err != nil { + r.err = err + return + } + + if !yield(i) { + r.err = r.rows.Close() + return + } + } + } +} + +func (r *IterValuesRows) Close() error { + return r.rows.Close() +} + +func (r *IterValuesRows) Err() error { + if r.err != nil { + return r.err + } + return r.rows.Err() +} diff --git a/internal/endtoend/testdata/iter/sqlite/query.sql b/internal/endtoend/testdata/iter/sqlite/query.sql new file mode 100644 index 0000000000..7688e5fe2e --- /dev/null +++ b/internal/endtoend/testdata/iter/sqlite/query.sql @@ -0,0 +1,4 @@ +-- name: IterValues :iter +SELECT * +FROM foo +WHERE b = ?; diff --git a/internal/endtoend/testdata/iter/sqlite/schema.sql b/internal/endtoend/testdata/iter/sqlite/schema.sql new file mode 100644 index 0000000000..ca4d437be0 --- /dev/null +++ b/internal/endtoend/testdata/iter/sqlite/schema.sql @@ -0,0 +1,2 @@ +CREATE TABLE foo (a text, b integer); + diff --git a/internal/endtoend/testdata/iter/sqlite/sqlc.json b/internal/endtoend/testdata/iter/sqlite/sqlc.json new file mode 100644 index 0000000000..cd66df063b --- /dev/null +++ b/internal/endtoend/testdata/iter/sqlite/sqlc.json @@ -0,0 +1,12 @@ +{ + "version": "1", + "packages": [ + { + "path": "go", + "engine": "sqlite", + "name": "querytest", + "schema": "schema.sql", + "queries": "query.sql" + } + ] +} diff --git a/internal/metadata/meta.go b/internal/metadata/meta.go index 97ff36dbd2..57cf741973 100644 --- a/internal/metadata/meta.go +++ b/internal/metadata/meta.go @@ -27,6 +27,7 @@ const ( CmdExecRows = ":execrows" CmdExecLastId = ":execlastid" CmdMany = ":many" + CmdIter = ":iter" CmdOne = ":one" CmdCopyFrom = ":copyfrom" CmdBatchExec = ":batchexec" @@ -101,7 +102,7 @@ func ParseQueryNameAndType(t string, commentStyle CommentSyntax) (string, string queryName := part[2] queryType := strings.TrimSpace(part[3]) switch queryType { - case CmdOne, CmdMany, CmdExec, CmdExecResult, CmdExecRows, CmdExecLastId, CmdCopyFrom, CmdBatchExec, CmdBatchMany, CmdBatchOne: + case CmdOne, CmdMany, CmdIter, CmdExec, CmdExecResult, CmdExecRows, CmdExecLastId, CmdCopyFrom, CmdBatchExec, CmdBatchMany, CmdBatchOne: default: return "", "", fmt.Errorf("invalid query type: %s", queryType) } diff --git a/internal/sql/validate/cmd.go b/internal/sql/validate/cmd.go index 66e849de6c..f24bcf9b72 100644 --- a/internal/sql/validate/cmd.go +++ b/internal/sql/validate/cmd.go @@ -71,7 +71,7 @@ func Cmd(n ast.Node, name, cmd string) error { return err } } - if !(cmd == metadata.CmdMany || cmd == metadata.CmdOne || cmd == metadata.CmdBatchMany || cmd == metadata.CmdBatchOne) { + if !(cmd == metadata.CmdMany || cmd == metadata.CmdIter || cmd == metadata.CmdOne || cmd == metadata.CmdBatchMany || cmd == metadata.CmdBatchOne) { return nil } var list *ast.List