diff --git a/docs/reference/query-annotations.md b/docs/reference/query-annotations.md index 4fabe05aae..4abdbb58c6 100644 --- a/docs/reference/query-annotations.md +++ b/docs/reference/query-annotations.md @@ -77,6 +77,49 @@ func (q *Queries) InsertAuthor(ctx context.Context, name string) (int64, error) } ``` +## `:iter` + +The generated method will return an [iter.Seq](https://pkg.go.dev/iter#Seq) that +streams records as they are scanned, together with a function that eventually +reports the query's error. This needs Go 1.23 or later. + +```sql +-- name: ListAuthors :iter +SELECT * FROM authors +ORDER BY name; +``` + +```go +func (q *Queries) ListAuthors(ctx context.Context) (iter.Seq[Author], func() error) { + // ... +} +``` + +To consume, you can range over the `iter.Seq`: + +```go +authors, ferr := q.ListAuthors(ctx) +for author := range authors { + // ... +} +if err := ferr(); err != nil { + // ... +} +``` + +The query starts only when the iterator is ranged over, not when the method is +called. When `q` runs on a single connection or transaction, it must not be +used inside the `for` loop, as the query is still reading from it. Once the +loop ends, by running out of rows or by `break`, the rows are closed and the +connection is released. + +The function returned alongside the iterator reports any error from running the +query or scanning its rows, so call it after the loop. + +The iterator is single-use: it runs its query once, which matters for a +`DELETE ... RETURNING` or `INSERT ... RETURNING`. Ranging over it a second +time yields nothing, and the returned function then reports an error. + ## `:many` The generated method will return a slice of records via diff --git a/examples/authors/postgresql/db_test.go b/examples/authors/postgresql/db_test.go index 0fdd8824be..c2403c8a6d 100644 --- a/examples/authors/postgresql/db_test.go +++ b/examples/authors/postgresql/db_test.go @@ -4,6 +4,8 @@ package authors import ( "context" + "errors" + "slices" "testing" "github.com/jackc/pgx/v5" @@ -47,3 +49,71 @@ func TestAuthors(t *testing.T) { } t.Log(fetchedAuthor) } + +func TestIterAuthors(t *testing.T) { + ctx := context.Background() + uri := local.PostgreSQL(t, []string{"schema.sql"}) + db, err := pgx.Connect(ctx, uri) + if err != nil { + t.Fatal(err) + } + defer db.Close(ctx) + + q := New(db) + + var last Author + for _, name := range []string{"Rob Pike", "Brian Kernighan", "Ken Thompson"} { + if last, err = q.CreateAuthor(ctx, CreateAuthorParams{Name: name}); err != nil { + t.Fatal(err) + } + } + + // the query only runs once the iterator is ranged over, so the + // connection is still free for other queries until then + authors, ferr := q.IterAuthors(ctx) + if _, err := q.GetAuthor(ctx, last.ID); err != nil { + t.Fatalf("query before ranging: %v", err) + } + var names []string + for author := range authors { + names = append(names, author.Name) + } + if err := ferr(); err != nil { + t.Fatal(err) + } + if want := []string{"Brian Kernighan", "Ken Thompson", "Rob Pike"}; !slices.Equal(names, want) { + t.Fatalf("got %v, want %v", names, want) + } + + // the iterator is single-use: ranging over it again yields nothing and + // reports an error + for author := range authors { + t.Errorf("second range yielded %v", author) + } + if err := ferr(); err == nil { + t.Error("ranging twice reported no error") + } + + // breaking out of the loop early releases the connection + authors, ferr = q.IterAuthors(ctx) + for range authors { + break + } + if err := ferr(); err != nil { + t.Fatal(err) + } + if _, err := q.GetAuthor(ctx, last.ID); err != nil { + t.Errorf("query after breaking out: %v", err) + } + + // a query that fails reports its error through the returned func + cctx, cancel := context.WithCancel(ctx) + cancel() + authors, ferr = q.IterAuthors(cctx) + for author := range authors { + t.Errorf("cancelled query yielded %v", author) + } + if err := ferr(); !errors.Is(err, context.Canceled) { + t.Errorf("got error %v, want context.Canceled", err) + } +} diff --git a/examples/authors/postgresql/query.sql b/examples/authors/postgresql/query.sql index 75e38b2caf..142019de96 100644 --- a/examples/authors/postgresql/query.sql +++ b/examples/authors/postgresql/query.sql @@ -17,3 +17,7 @@ RETURNING *; -- name: DeleteAuthor :exec DELETE FROM authors WHERE id = $1; + +-- name: IterAuthors :iter +SELECT * FROM authors +ORDER BY name; diff --git a/examples/authors/postgresql/query.sql.go b/examples/authors/postgresql/query.sql.go index 2910a809f3..493606dad7 100644 --- a/examples/authors/postgresql/query.sql.go +++ b/examples/authors/postgresql/query.sql.go @@ -7,6 +7,8 @@ package authors import ( "context" + "errors" + "iter" "github.com/jackc/pgx/v5/pgtype" ) @@ -54,6 +56,46 @@ 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 +` + +// IterAuthors returns a single-use iterator. The query runs when the +// iterator is ranged over, and the returned func reports its error +// after the loop. +func (q *Queries) IterAuthors(ctx context.Context) (iter.Seq[Author], func() error) { + var _err error + var _used bool + return func(_yield func(Author) bool) { + if _used { + _err = errors.New("query IterAuthors: iterator already used") + return + } + _used = true + rows, err := q.db.Query(ctx, iterAuthors) + if err != nil { + _err = err + return + } + defer rows.Close() + for rows.Next() { + var i Author + if err := rows.Scan(&i.ID, &i.Name, &i.Bio); err != nil { + _err = err + return + } + if !_yield(i) { + break + } + } + rows.Close() + if err := rows.Err(); err != nil { + _err = err + } + }, func() error { return _err } +} + const listAuthors = `-- name: ListAuthors :many SELECT id, name, bio FROM authors ORDER BY name diff --git a/examples/authors/sqlite/db_test.go b/examples/authors/sqlite/db_test.go index e10ce46069..ff3b96c129 100644 --- a/examples/authors/sqlite/db_test.go +++ b/examples/authors/sqlite/db_test.go @@ -5,6 +5,8 @@ package authors import ( "context" "database/sql" + "errors" + "slices" "testing" "github.com/sqlc-dev/sqlc/internal/sqltest" @@ -46,3 +48,66 @@ func TestAuthors(t *testing.T) { } t.Log(fetchedAuthor) } + +func TestIterAuthors(t *testing.T) { + sdb, cleanup := sqltest.SQLite(t, []string{"schema.sql"}) + defer sdb.Close() + defer cleanup() + + ctx := context.Background() + db := New(sdb) + + for _, name := range []string{"Rob Pike", "Brian Kernighan", "Ken Thompson"} { + if _, err := db.CreateAuthor(ctx, CreateAuthorParams{Name: name}); err != nil { + t.Fatal(err) + } + } + + // the query only runs once the iterator is ranged over + authors, ferr := db.IterAuthors(ctx) + if inUse := sdb.Stats().InUse; inUse != 0 { + t.Fatalf("%d connections in use before ranging, want 0", inUse) + } + var names []string + for author := range authors { + names = append(names, author.Name) + } + if err := ferr(); err != nil { + t.Fatal(err) + } + if want := []string{"Brian Kernighan", "Ken Thompson", "Rob Pike"}; !slices.Equal(names, want) { + t.Fatalf("got %v, want %v", names, want) + } + + // the iterator is single-use: ranging over it again yields nothing and + // reports an error + for author := range authors { + t.Errorf("second range yielded %v", author) + } + if err := ferr(); err == nil { + t.Error("ranging twice reported no error") + } + + // breaking out of the loop early releases the connection + authors, ferr = db.IterAuthors(ctx) + for range authors { + break + } + if err := ferr(); err != nil { + t.Fatal(err) + } + if inUse := sdb.Stats().InUse; inUse != 0 { + t.Errorf("%d connections in use after breaking out, want 0", inUse) + } + + // a query that fails reports its error through the returned func + cctx, cancel := context.WithCancel(ctx) + cancel() + authors, ferr = db.IterAuthors(cctx) + for author := range authors { + t.Errorf("cancelled query yielded %v", author) + } + if err := ferr(); !errors.Is(err, context.Canceled) { + t.Errorf("got error %v, want context.Canceled", err) + } +} diff --git a/examples/authors/sqlite/query.sql b/examples/authors/sqlite/query.sql index c3b5866149..f58df832db 100644 --- a/examples/authors/sqlite/query.sql +++ b/examples/authors/sqlite/query.sql @@ -16,3 +16,7 @@ INSERT INTO authors ( /* name: DeleteAuthor :exec */ DELETE FROM authors WHERE id = ?; + +/* name: IterAuthors :iter */ +SELECT * FROM authors +ORDER BY name; diff --git a/examples/authors/sqlite/query.sql.go b/examples/authors/sqlite/query.sql.go index 0d66af757f..0005e9e78b 100644 --- a/examples/authors/sqlite/query.sql.go +++ b/examples/authors/sqlite/query.sql.go @@ -8,6 +8,8 @@ package authors import ( "context" "database/sql" + "errors" + "iter" ) const createAuthor = `-- name: CreateAuthor :execresult @@ -49,6 +51,49 @@ 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 +` + +// IterAuthors returns a single-use iterator. The query runs when the +// iterator is ranged over, and the returned func reports its error +// after the loop. +func (q *Queries) IterAuthors(ctx context.Context) (iter.Seq[Author], func() error) { + var _err error + var _used bool + return func(_yield func(Author) bool) { + if _used { + _err = errors.New("query IterAuthors: iterator already used") + return + } + _used = true + rows, err := q.db.QueryContext(ctx, iterAuthors) + if err != nil { + _err = err + return + } + defer rows.Close() + for rows.Next() { + var i Author + if err := rows.Scan(&i.ID, &i.Name, &i.Bio); err != nil { + _err = err + return + } + if !_yield(i) { + break + } + } + if err := rows.Close(); err != nil { + _err = err + return + } + if err := rows.Err(); err != nil { + _err = err + } + }, func() error { return _err } +} + const listAuthors = `-- name: ListAuthors :many SELECT id, name, bio FROM authors ORDER BY name diff --git a/internal/codegen/golang/gen.go b/internal/codegen/golang/gen.go index 725f5970cf..2f0e276742 100644 --- a/internal/codegen/golang/gen.go +++ b/internal/codegen/golang/gen.go @@ -76,7 +76,7 @@ func (t *tmplCtx) codegenQueryMethod(q Query) string { } return db + ".QueryRowContext" - case ":many": + case ":many", ":iter": if t.EmitPreparedQueries { return "q.query" } @@ -94,7 +94,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 e1370e48eb..d63275d49a 100644 --- a/internal/codegen/golang/imports.go +++ b/internal/codegen/golang/imports.go @@ -206,6 +206,9 @@ func buildImports(options *opts.Options, engine string, queries []Query, uses fu std["database/sql"] = struct{}{} } } + if q.Cmd == metadata.CmdIter { + std["iter"] = struct{}{} + } } for typeName, pkg := range stdlibTypes { @@ -446,6 +449,13 @@ func (i *importer) queryImports(filename string) fileImports { } } + // An :iter method reports a second range over its iterator as an error. + for _, q := range gq { + if q.Cmd == metadata.CmdIter { + std["errors"] = struct{}{} + } + } + if anyNonCopyFrom { std["context"] = struct{}{} } diff --git a/internal/codegen/golang/query.go b/internal/codegen/golang/query.go index 6bd9b70e25..b1ffeef709 100644 --- a/internal/codegen/golang/query.go +++ b/internal/codegen/golang/query.go @@ -304,7 +304,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 82059c54b7..36c5dfb015 100644 --- a/internal/codegen/golang/result.go +++ b/internal/codegen/golang/result.go @@ -205,6 +205,16 @@ func buildQueries(req *plugin.GenerateRequest, options *opts.Options, enums []En } comments := query.Comments + if query.Cmd == metadata.CmdIter { + if len(comments) > 0 { + comments = append(comments, "") + } + comments = append(comments, + " "+query.Name+" returns a single-use iterator. The query runs when the", + " iterator is ranged over, and the returned func reports its error", + " after the loop.", + ) + } if options.EmitSqlAsComment { if len(comments) == 0 { comments = append(comments, query.Name) @@ -370,6 +380,7 @@ func buildQueries(req *plugin.GenerateRequest, options *opts.Options, enums []En var cmdReturnsData = map[string]struct{}{ metadata.CmdBatchMany: {}, metadata.CmdBatchOne: {}, + metadata.CmdIter: {}, metadata.CmdMany: {}, metadata.CmdOne: {}, } diff --git a/internal/codegen/golang/templates/pgx/interfaceCode.tmpl b/internal/codegen/golang/templates/pgx/interfaceCode.tmpl index cf7cd36cb9..cd57a63418 100644 --- a/internal/codegen/golang/templates/pgx/interfaceCode.tmpl +++ b/internal/codegen/golang/templates/pgx/interfaceCode.tmpl @@ -20,6 +20,11 @@ {{end -}} {{.MethodName}}(ctx context.Context, {{.Arg.Pair}}) ([]{{.Ret.DefineType}}, error) {{- end}} + {{- if eq .Cmd ":iter" }} + {{range .Comments}}//{{.}} + {{end -}} + {{.MethodName}}(ctx context.Context, {{if $dbtxParam}}db DBTX, {{end}}{{.Arg.Pair}}) (iter.Seq[{{.Ret.DefineType}}], func() error) + {{- 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 59a88c880a..1dbdce8062 100644 --- a/internal/codegen/golang/templates/pgx/queryCode.tmpl +++ b/internal/codegen/golang/templates/pgx/queryCode.tmpl @@ -79,6 +79,42 @@ func (q *Queries) {{.MethodName}}(ctx context.Context, {{.Arg.Pair}}) ([]{{.Ret. } {{end}} +{{if eq .Cmd ":iter"}} +{{range .Comments}}//{{.}} +{{end -}} +func (q *Queries) {{.MethodName}}(ctx context.Context, {{if $.EmitMethodsWithDBArgument}}db DBTX, {{end}}{{.Arg.Pair}}) (iter.Seq[{{.Ret.DefineType}}], func() error) { + var _err error + var _used bool + return func(_yield func({{.Ret.DefineType}}) bool) { + if _used { + _err = errors.New("query {{.MethodName}}: iterator already used") + return + } + _used = true + rows, err := {{if $.EmitMethodsWithDBArgument}}db{{else}}q.db{{end}}.Query(ctx, {{.ConstantName}}, {{.Arg.Params}}) + if err != nil { + _err = {{if $.WrapErrors}}fmt.Errorf("query {{.MethodName}}: %w", err){{else}}err{{end}} + return + } + defer rows.Close() + for rows.Next() { + var {{.Ret.Name}} {{.Ret.Type}} + if err := rows.Scan({{.Ret.Scan}}); err != nil { + _err = {{if $.WrapErrors}}fmt.Errorf("query {{.MethodName}}: %w", err){{else}}err{{end}} + return + } + if !_yield({{.Ret.ReturnName}}) { + break + } + } + rows.Close() + if err := rows.Err(); err != nil { + _err = {{if $.WrapErrors}}fmt.Errorf("query {{.MethodName}}: %w", err){{else}}err{{end}} + } + }, func() error { return _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..52d49d6112 100644 --- a/internal/codegen/golang/templates/stdlib/interfaceCode.tmpl +++ b/internal/codegen/golang/templates/stdlib/interfaceCode.tmpl @@ -20,6 +20,11 @@ {{end -}} {{.MethodName}}(ctx context.Context, {{.Arg.Pair}}) ([]{{.Ret.DefineType}}, error) {{- end}} + {{- if eq .Cmd ":iter" }} + {{range .Comments}}//{{.}} + {{end -}} + {{.MethodName}}(ctx context.Context, {{if $dbtxParam}}db DBTX, {{end}}{{.Arg.Pair}}) (iter.Seq[{{.Ret.DefineType}}], func() error) + {{- 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 8fd67f1567..4820a15fa7 100644 --- a/internal/codegen/golang/templates/stdlib/queryCode.tmpl +++ b/internal/codegen/golang/templates/stdlib/queryCode.tmpl @@ -68,6 +68,45 @@ 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}}) (iter.Seq[{{.Ret.DefineType}}], func() error) { + var _err error + var _used bool + return func(_yield func({{.Ret.DefineType}}) bool) { + if _used { + _err = errors.New("query {{.MethodName}}: iterator already used") + return + } + _used = true + {{ template "queryCodeStdExec" . }} + if err != nil { + _err = {{if $.WrapErrors}}fmt.Errorf("query {{.MethodName}}: %w", err){{else}}err{{end}} + return + } + defer rows.Close() + for rows.Next() { + var {{.Ret.Name}} {{.Ret.Type}} + if err := rows.Scan({{.Ret.Scan}}); err != nil { + _err = {{if $.WrapErrors}}fmt.Errorf("query {{.MethodName}}: %w", err){{else}}err{{end}} + return + } + if !_yield({{.Ret.ReturnName}}) { + break + } + } + if err := rows.Close(); err != nil { + _err = {{if $.WrapErrors}}fmt.Errorf("query {{.MethodName}}: %w", err){{else}}err{{end}} + return + } + if err := rows.Err(); err != nil { + _err = {{if $.WrapErrors}}fmt.Errorf("query {{.MethodName}}: %w", err){{else}}err{{end}} + } + }, func() error { return _err } +} +{{end}} + {{if eq .Cmd ":exec"}} {{range .Comments}}//{{.}} {{end -}} @@ -125,7 +164,7 @@ func (q *Queries) {{.MethodName}}(ctx context.Context, {{ dbarg }} {{.Arg.Pair}} {{end}} {{define "queryCodeStdExec"}} - {{- if .Arg.HasSqlcSlices }} + {{- if .Arg.HasSqlcSlices -}} query := {{.ConstantName}} var queryParams []any {{- if .Arg.Struct }} diff --git a/internal/endtoend/testdata/comment_godoc_db_argument/postgresql/pgx/v4/go/query.sql.go b/internal/endtoend/testdata/comment_godoc_db_argument/postgresql/pgx/v4/go/query.sql.go index 7512f5a29a..a60e34fbcc 100644 --- a/internal/endtoend/testdata/comment_godoc_db_argument/postgresql/pgx/v4/go/query.sql.go +++ b/internal/endtoend/testdata/comment_godoc_db_argument/postgresql/pgx/v4/go/query.sql.go @@ -8,6 +8,8 @@ package querytest import ( "context" "database/sql" + "errors" + "iter" "github.com/jackc/pgconn" ) @@ -44,6 +46,47 @@ func (q *Queries) ExecRowFoo(ctx context.Context, db DBTX) (int64, error) { return result.RowsAffected(), nil } +const iterFoo = `-- name: IterFoo :iter +SELECT bar FROM foo +` + +// This function returns an iterator over Foos +// +// IterFoo returns a single-use iterator. The query runs when the +// iterator is ranged over, and the returned func reports its error +// after the loop. +func (q *Queries) IterFoo(ctx context.Context, db DBTX) (iter.Seq[sql.NullString], func() error) { + var _err error + var _used bool + return func(_yield func(sql.NullString) bool) { + if _used { + _err = errors.New("query IterFoo: iterator already used") + return + } + _used = true + rows, err := db.Query(ctx, iterFoo) + if err != nil { + _err = err + return + } + defer rows.Close() + for rows.Next() { + var bar sql.NullString + if err := rows.Scan(&bar); err != nil { + _err = err + return + } + if !_yield(bar) { + break + } + } + rows.Close() + if err := rows.Err(); err != nil { + _err = err + } + }, func() error { return _err } +} + const manyFoo = `-- name: ManyFoo :many SELECT bar FROM foo ` diff --git a/internal/endtoend/testdata/comment_godoc_db_argument/postgresql/pgx/v4/query.sql b/internal/endtoend/testdata/comment_godoc_db_argument/postgresql/pgx/v4/query.sql index b5fab38de7..eb926edb69 100644 --- a/internal/endtoend/testdata/comment_godoc_db_argument/postgresql/pgx/v4/query.sql +++ b/internal/endtoend/testdata/comment_godoc_db_argument/postgresql/pgx/v4/query.sql @@ -2,6 +2,10 @@ -- This function returns a list of Foos SELECT * FROM foo; +-- name: IterFoo :iter +-- This function returns an iterator over Foos +SELECT * FROM foo; + -- name: OneFoo :one -- This function returns one Foo SELECT * FROM foo; diff --git a/internal/endtoend/testdata/comment_godoc_db_argument/postgresql/pgx/v5/go/query.sql.go b/internal/endtoend/testdata/comment_godoc_db_argument/postgresql/pgx/v5/go/query.sql.go index d8d62964a4..b5c6b4572f 100644 --- a/internal/endtoend/testdata/comment_godoc_db_argument/postgresql/pgx/v5/go/query.sql.go +++ b/internal/endtoend/testdata/comment_godoc_db_argument/postgresql/pgx/v5/go/query.sql.go @@ -7,6 +7,8 @@ package querytest import ( "context" + "errors" + "iter" "github.com/jackc/pgx/v5/pgconn" "github.com/jackc/pgx/v5/pgtype" @@ -44,6 +46,47 @@ func (q *Queries) ExecRowFoo(ctx context.Context, db DBTX) (int64, error) { return result.RowsAffected(), nil } +const iterFoo = `-- name: IterFoo :iter +SELECT bar FROM foo +` + +// This function returns an iterator over Foos +// +// IterFoo returns a single-use iterator. The query runs when the +// iterator is ranged over, and the returned func reports its error +// after the loop. +func (q *Queries) IterFoo(ctx context.Context, db DBTX) (iter.Seq[pgtype.Text], func() error) { + var _err error + var _used bool + return func(_yield func(pgtype.Text) bool) { + if _used { + _err = errors.New("query IterFoo: iterator already used") + return + } + _used = true + rows, err := db.Query(ctx, iterFoo) + if err != nil { + _err = err + return + } + defer rows.Close() + for rows.Next() { + var bar pgtype.Text + if err := rows.Scan(&bar); err != nil { + _err = err + return + } + if !_yield(bar) { + break + } + } + rows.Close() + if err := rows.Err(); err != nil { + _err = err + } + }, func() error { return _err } +} + const manyFoo = `-- name: ManyFoo :many SELECT bar FROM foo ` diff --git a/internal/endtoend/testdata/comment_godoc_db_argument/postgresql/pgx/v5/query.sql b/internal/endtoend/testdata/comment_godoc_db_argument/postgresql/pgx/v5/query.sql index b5fab38de7..eb926edb69 100644 --- a/internal/endtoend/testdata/comment_godoc_db_argument/postgresql/pgx/v5/query.sql +++ b/internal/endtoend/testdata/comment_godoc_db_argument/postgresql/pgx/v5/query.sql @@ -2,6 +2,10 @@ -- This function returns a list of Foos SELECT * FROM foo; +-- name: IterFoo :iter +-- This function returns an iterator over Foos +SELECT * FROM foo; + -- name: OneFoo :one -- This function returns one Foo SELECT * FROM foo; diff --git a/internal/endtoend/testdata/invalid_queries_foo/pgx/v4/query.sql b/internal/endtoend/testdata/invalid_queries_foo/pgx/v4/query.sql index 06644a2cd3..12663a73bd 100644 --- a/internal/endtoend/testdata/invalid_queries_foo/pgx/v4/query.sql +++ b/internal/endtoend/testdata/invalid_queries_foo/pgx/v4/query.sql @@ -18,3 +18,6 @@ INSERT INTO foo (id) VALUES ($1); -- name: InsertFoo :batchone INSERT INTO foo (id) VALUES ($1); + +-- name: DeleteFooIter :iter +DELETE FROM foo WHERE id = $1; diff --git a/internal/endtoend/testdata/invalid_queries_foo/pgx/v4/stderr.txt b/internal/endtoend/testdata/invalid_queries_foo/pgx/v4/stderr.txt index 8c745b7e3b..34e859d0d6 100644 --- a/internal/endtoend/testdata/invalid_queries_foo/pgx/v4/stderr.txt +++ b/internal/endtoend/testdata/invalid_queries_foo/pgx/v4/stderr.txt @@ -1,8 +1,9 @@ # package querytest -query.sql:1:1: missing query type [':one', ':many', ':exec', ':execrows', ':execlastid', ':execresult', ':copyfrom', 'batchexec', 'batchmany', 'batchone']: -- name: ListFoos +query.sql:1:1: missing query type [':one', ':many', ':iter', ':exec', ':execrows', ':execlastid', ':execresult', ':copyfrom', 'batchexec', 'batchmany', 'batchone']: -- name: ListFoos query.sql:5:1: invalid query comment: -- name: ListFoos :one :many query.sql:8:1: invalid query type: :two query.sql:11:1: query "DeleteFoo" specifies parameter ":one" without containing a RETURNING clause query.sql:14:1: query "UpdateFoo" specifies parameter ":one" without containing a RETURNING clause query.sql:17:1: query "InsertFoo" specifies parameter ":one" without containing a RETURNING clause query.sql:20:1: query "InsertFoo" specifies parameter ":batchone" without containing a RETURNING clause +query.sql:23:1: query "DeleteFooIter" specifies parameter ":iter" without containing a RETURNING clause diff --git a/internal/endtoend/testdata/invalid_queries_foo/pgx/v5/query.sql b/internal/endtoend/testdata/invalid_queries_foo/pgx/v5/query.sql index 06644a2cd3..12663a73bd 100644 --- a/internal/endtoend/testdata/invalid_queries_foo/pgx/v5/query.sql +++ b/internal/endtoend/testdata/invalid_queries_foo/pgx/v5/query.sql @@ -18,3 +18,6 @@ INSERT INTO foo (id) VALUES ($1); -- name: InsertFoo :batchone INSERT INTO foo (id) VALUES ($1); + +-- name: DeleteFooIter :iter +DELETE FROM foo WHERE id = $1; diff --git a/internal/endtoend/testdata/invalid_queries_foo/pgx/v5/stderr.txt b/internal/endtoend/testdata/invalid_queries_foo/pgx/v5/stderr.txt index 8c745b7e3b..34e859d0d6 100644 --- a/internal/endtoend/testdata/invalid_queries_foo/pgx/v5/stderr.txt +++ b/internal/endtoend/testdata/invalid_queries_foo/pgx/v5/stderr.txt @@ -1,8 +1,9 @@ # package querytest -query.sql:1:1: missing query type [':one', ':many', ':exec', ':execrows', ':execlastid', ':execresult', ':copyfrom', 'batchexec', 'batchmany', 'batchone']: -- name: ListFoos +query.sql:1:1: missing query type [':one', ':many', ':iter', ':exec', ':execrows', ':execlastid', ':execresult', ':copyfrom', 'batchexec', 'batchmany', 'batchone']: -- name: ListFoos query.sql:5:1: invalid query comment: -- name: ListFoos :one :many query.sql:8:1: invalid query type: :two query.sql:11:1: query "DeleteFoo" specifies parameter ":one" without containing a RETURNING clause query.sql:14:1: query "UpdateFoo" specifies parameter ":one" without containing a RETURNING clause query.sql:17:1: query "InsertFoo" specifies parameter ":one" without containing a RETURNING clause query.sql:20:1: query "InsertFoo" specifies parameter ":batchone" without containing a RETURNING clause +query.sql:23:1: query "DeleteFooIter" specifies parameter ":iter" without containing a RETURNING clause diff --git a/internal/endtoend/testdata/invalid_queries_foo/stdlib/query.sql b/internal/endtoend/testdata/invalid_queries_foo/stdlib/query.sql index 0e98fb622d..165d93421d 100644 --- a/internal/endtoend/testdata/invalid_queries_foo/stdlib/query.sql +++ b/internal/endtoend/testdata/invalid_queries_foo/stdlib/query.sql @@ -15,3 +15,6 @@ UPDATE foo SET id = $2 WHERE id = $1; -- name: InsertFoo :one INSERT INTO foo (id) VALUES ($1); + +-- name: DeleteFooIter :iter +DELETE FROM foo WHERE id = $1; diff --git a/internal/endtoend/testdata/invalid_queries_foo/stdlib/stderr.txt b/internal/endtoend/testdata/invalid_queries_foo/stdlib/stderr.txt index 06ec54327f..cc3e1cb733 100644 --- a/internal/endtoend/testdata/invalid_queries_foo/stdlib/stderr.txt +++ b/internal/endtoend/testdata/invalid_queries_foo/stdlib/stderr.txt @@ -1,7 +1,8 @@ # package querytest -query.sql:1:1: missing query type [':one', ':many', ':exec', ':execrows', ':execlastid', ':execresult', ':copyfrom', 'batchexec', 'batchmany', 'batchone']: -- name: ListFoos +query.sql:1:1: missing query type [':one', ':many', ':iter', ':exec', ':execrows', ':execlastid', ':execresult', ':copyfrom', 'batchexec', 'batchmany', 'batchone']: -- name: ListFoos query.sql:5:1: invalid query comment: -- name: ListFoos :one :many query.sql:8:1: invalid query type: :two query.sql:11:1: query "DeleteFoo" specifies parameter ":one" without containing a RETURNING clause query.sql:14:1: query "UpdateFoo" specifies parameter ":one" without containing a RETURNING clause -query.sql:17:1: query "InsertFoo" specifies parameter ":one" without containing a RETURNING clause \ No newline at end of file +query.sql:17:1: query "InsertFoo" specifies parameter ":one" without containing a RETURNING clause +query.sql:20:1: query "DeleteFooIter" specifies parameter ":iter" without containing a RETURNING clause \ No newline at end of file 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..2666c1c05d --- /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.31.1 + +package querytest + +import ( + "context" + + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" +) + +type DBTX interface { + Exec(context.Context, string, ...any) (pgconn.CommandTag, error) + Query(context.Context, string, ...any) (pgx.Rows, error) + QueryRow(context.Context, string, ...any) 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..4e97f66a99 --- /dev/null +++ b/internal/endtoend/testdata/iter/postgresql/pgx/v5/go/models.go @@ -0,0 +1,15 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 + +package querytest + +import ( + "github.com/jackc/pgx/v5/pgtype" +) + +type Author struct { + ID int64 + Name string + Bio pgtype.Text +} diff --git a/internal/endtoend/testdata/iter/postgresql/pgx/v5/go/querier.go b/internal/endtoend/testdata/iter/postgresql/pgx/v5/go/querier.go new file mode 100644 index 0000000000..748a6892a3 --- /dev/null +++ b/internal/endtoend/testdata/iter/postgresql/pgx/v5/go/querier.go @@ -0,0 +1,29 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 + +package querytest + +import ( + "context" + "iter" +) + +type Querier interface { + // DeleteAuthorsByName returns a single-use iterator. The query runs when the + // iterator is ranged over, and the returned func reports its error + // after the loop. + DeleteAuthorsByName(ctx context.Context, name string) (iter.Seq[DeleteAuthorsByNameRow], func() error) + // ListAuthorNamesAfter returns a single-use iterator. The query runs when the + // iterator is ranged over, and the returned func reports its error + // after the loop. + ListAuthorNamesAfter(ctx context.Context, id int64) (iter.Seq[string], func() error) + // Lists every author by name. + // + // ListAuthors returns a single-use iterator. The query runs when the + // iterator is ranged over, and the returned func reports its error + // after the loop. + ListAuthors(ctx context.Context) (iter.Seq[Author], func() error) +} + +var _ Querier = (*Queries)(nil) 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..87fa69e2e8 --- /dev/null +++ b/internal/endtoend/testdata/iter/postgresql/pgx/v5/go/query.sql.go @@ -0,0 +1,143 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 +// source: query.sql + +package querytest + +import ( + "context" + "errors" + "iter" + + "github.com/jackc/pgx/v5/pgtype" +) + +const deleteAuthorsByName = `-- name: DeleteAuthorsByName :iter +DELETE FROM authors +WHERE name = $1 +RETURNING id, bio +` + +type DeleteAuthorsByNameRow struct { + ID int64 + Bio pgtype.Text +} + +// DeleteAuthorsByName returns a single-use iterator. The query runs when the +// iterator is ranged over, and the returned func reports its error +// after the loop. +func (q *Queries) DeleteAuthorsByName(ctx context.Context, name string) (iter.Seq[DeleteAuthorsByNameRow], func() error) { + var _err error + var _used bool + return func(_yield func(DeleteAuthorsByNameRow) bool) { + if _used { + _err = errors.New("query DeleteAuthorsByName: iterator already used") + return + } + _used = true + rows, err := q.db.Query(ctx, deleteAuthorsByName, name) + if err != nil { + _err = err + return + } + defer rows.Close() + for rows.Next() { + var i DeleteAuthorsByNameRow + if err := rows.Scan(&i.ID, &i.Bio); err != nil { + _err = err + return + } + if !_yield(i) { + break + } + } + rows.Close() + if err := rows.Err(); err != nil { + _err = err + } + }, func() error { return _err } +} + +const listAuthorNamesAfter = `-- name: ListAuthorNamesAfter :iter +SELECT name FROM authors +WHERE id > $1 +ORDER BY id +` + +// ListAuthorNamesAfter returns a single-use iterator. The query runs when the +// iterator is ranged over, and the returned func reports its error +// after the loop. +func (q *Queries) ListAuthorNamesAfter(ctx context.Context, id int64) (iter.Seq[string], func() error) { + var _err error + var _used bool + return func(_yield func(string) bool) { + if _used { + _err = errors.New("query ListAuthorNamesAfter: iterator already used") + return + } + _used = true + rows, err := q.db.Query(ctx, listAuthorNamesAfter, id) + if err != nil { + _err = err + return + } + defer rows.Close() + for rows.Next() { + var name string + if err := rows.Scan(&name); err != nil { + _err = err + return + } + if !_yield(name) { + break + } + } + rows.Close() + if err := rows.Err(); err != nil { + _err = err + } + }, func() error { return _err } +} + +const listAuthors = `-- name: ListAuthors :iter +SELECT id, name, bio FROM authors +ORDER BY name +` + +// Lists every author by name. +// +// ListAuthors returns a single-use iterator. The query runs when the +// iterator is ranged over, and the returned func reports its error +// after the loop. +func (q *Queries) ListAuthors(ctx context.Context) (iter.Seq[Author], func() error) { + var _err error + var _used bool + return func(_yield func(Author) bool) { + if _used { + _err = errors.New("query ListAuthors: iterator already used") + return + } + _used = true + rows, err := q.db.Query(ctx, listAuthors) + if err != nil { + _err = err + return + } + defer rows.Close() + for rows.Next() { + var i Author + if err := rows.Scan(&i.ID, &i.Name, &i.Bio); err != nil { + _err = err + return + } + if !_yield(i) { + break + } + } + rows.Close() + if err := rows.Err(); err != nil { + _err = err + } + }, func() error { return _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..dea0e0384f --- /dev/null +++ b/internal/endtoend/testdata/iter/postgresql/pgx/v5/query.sql @@ -0,0 +1,14 @@ +-- name: ListAuthors :iter +-- Lists every author by name. +SELECT * FROM authors +ORDER BY name; + +-- name: ListAuthorNamesAfter :iter +SELECT name FROM authors +WHERE id > $1 +ORDER BY id; + +-- name: DeleteAuthorsByName :iter +DELETE FROM authors +WHERE name = $1 +RETURNING id, bio; 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..69b607d902 --- /dev/null +++ b/internal/endtoend/testdata/iter/postgresql/pgx/v5/schema.sql @@ -0,0 +1,5 @@ +CREATE TABLE authors ( + id BIGSERIAL PRIMARY KEY, + name text NOT NULL, + bio text +); 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..007fb8530f --- /dev/null +++ b/internal/endtoend/testdata/iter/postgresql/pgx/v5/sqlc.json @@ -0,0 +1,14 @@ +{ + "version": "1", + "packages": [ + { + "path": "go", + "engine": "postgresql", + "sql_package": "pgx/v5", + "name": "querytest", + "schema": "schema.sql", + "queries": "query.sql", + "emit_interface": true + } + ] +} 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..32faa9f2b8 --- /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.31.1 + +package querytest + +import ( + "context" + "database/sql" +) + +type DBTX interface { + ExecContext(context.Context, string, ...any) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...any) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...any) *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..2208139112 --- /dev/null +++ b/internal/endtoend/testdata/iter/postgresql/stdlib/go/models.go @@ -0,0 +1,15 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 + +package querytest + +import ( + "database/sql" +) + +type Author struct { + ID int64 + Name string + Bio sql.NullString +} diff --git a/internal/endtoend/testdata/iter/postgresql/stdlib/go/querier.go b/internal/endtoend/testdata/iter/postgresql/stdlib/go/querier.go new file mode 100644 index 0000000000..748a6892a3 --- /dev/null +++ b/internal/endtoend/testdata/iter/postgresql/stdlib/go/querier.go @@ -0,0 +1,29 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 + +package querytest + +import ( + "context" + "iter" +) + +type Querier interface { + // DeleteAuthorsByName returns a single-use iterator. The query runs when the + // iterator is ranged over, and the returned func reports its error + // after the loop. + DeleteAuthorsByName(ctx context.Context, name string) (iter.Seq[DeleteAuthorsByNameRow], func() error) + // ListAuthorNamesAfter returns a single-use iterator. The query runs when the + // iterator is ranged over, and the returned func reports its error + // after the loop. + ListAuthorNamesAfter(ctx context.Context, id int64) (iter.Seq[string], func() error) + // Lists every author by name. + // + // ListAuthors returns a single-use iterator. The query runs when the + // iterator is ranged over, and the returned func reports its error + // after the loop. + ListAuthors(ctx context.Context) (iter.Seq[Author], func() error) +} + +var _ Querier = (*Queries)(nil) 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..11729ac688 --- /dev/null +++ b/internal/endtoend/testdata/iter/postgresql/stdlib/go/query.sql.go @@ -0,0 +1,151 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 +// source: query.sql + +package querytest + +import ( + "context" + "database/sql" + "errors" + "iter" +) + +const deleteAuthorsByName = `-- name: DeleteAuthorsByName :iter +DELETE FROM authors +WHERE name = $1 +RETURNING id, bio +` + +type DeleteAuthorsByNameRow struct { + ID int64 + Bio sql.NullString +} + +// DeleteAuthorsByName returns a single-use iterator. The query runs when the +// iterator is ranged over, and the returned func reports its error +// after the loop. +func (q *Queries) DeleteAuthorsByName(ctx context.Context, name string) (iter.Seq[DeleteAuthorsByNameRow], func() error) { + var _err error + var _used bool + return func(_yield func(DeleteAuthorsByNameRow) bool) { + if _used { + _err = errors.New("query DeleteAuthorsByName: iterator already used") + return + } + _used = true + rows, err := q.db.QueryContext(ctx, deleteAuthorsByName, name) + if err != nil { + _err = err + return + } + defer rows.Close() + for rows.Next() { + var i DeleteAuthorsByNameRow + if err := rows.Scan(&i.ID, &i.Bio); err != nil { + _err = err + return + } + if !_yield(i) { + break + } + } + if err := rows.Close(); err != nil { + _err = err + return + } + if err := rows.Err(); err != nil { + _err = err + } + }, func() error { return _err } +} + +const listAuthorNamesAfter = `-- name: ListAuthorNamesAfter :iter +SELECT name FROM authors +WHERE id > $1 +ORDER BY id +` + +// ListAuthorNamesAfter returns a single-use iterator. The query runs when the +// iterator is ranged over, and the returned func reports its error +// after the loop. +func (q *Queries) ListAuthorNamesAfter(ctx context.Context, id int64) (iter.Seq[string], func() error) { + var _err error + var _used bool + return func(_yield func(string) bool) { + if _used { + _err = errors.New("query ListAuthorNamesAfter: iterator already used") + return + } + _used = true + rows, err := q.db.QueryContext(ctx, listAuthorNamesAfter, id) + if err != nil { + _err = err + return + } + defer rows.Close() + for rows.Next() { + var name string + if err := rows.Scan(&name); err != nil { + _err = err + return + } + if !_yield(name) { + break + } + } + if err := rows.Close(); err != nil { + _err = err + return + } + if err := rows.Err(); err != nil { + _err = err + } + }, func() error { return _err } +} + +const listAuthors = `-- name: ListAuthors :iter +SELECT id, name, bio FROM authors +ORDER BY name +` + +// Lists every author by name. +// +// ListAuthors returns a single-use iterator. The query runs when the +// iterator is ranged over, and the returned func reports its error +// after the loop. +func (q *Queries) ListAuthors(ctx context.Context) (iter.Seq[Author], func() error) { + var _err error + var _used bool + return func(_yield func(Author) bool) { + if _used { + _err = errors.New("query ListAuthors: iterator already used") + return + } + _used = true + rows, err := q.db.QueryContext(ctx, listAuthors) + if err != nil { + _err = err + return + } + defer rows.Close() + for rows.Next() { + var i Author + if err := rows.Scan(&i.ID, &i.Name, &i.Bio); err != nil { + _err = err + return + } + if !_yield(i) { + break + } + } + if err := rows.Close(); err != nil { + _err = err + return + } + if err := rows.Err(); err != nil { + _err = err + } + }, func() error { return _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..dea0e0384f --- /dev/null +++ b/internal/endtoend/testdata/iter/postgresql/stdlib/query.sql @@ -0,0 +1,14 @@ +-- name: ListAuthors :iter +-- Lists every author by name. +SELECT * FROM authors +ORDER BY name; + +-- name: ListAuthorNamesAfter :iter +SELECT name FROM authors +WHERE id > $1 +ORDER BY id; + +-- name: DeleteAuthorsByName :iter +DELETE FROM authors +WHERE name = $1 +RETURNING id, bio; 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..69b607d902 --- /dev/null +++ b/internal/endtoend/testdata/iter/postgresql/stdlib/schema.sql @@ -0,0 +1,5 @@ +CREATE TABLE authors ( + id BIGSERIAL PRIMARY KEY, + name text NOT NULL, + bio text +); 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..36f98f9072 --- /dev/null +++ b/internal/endtoend/testdata/iter/postgresql/stdlib/sqlc.json @@ -0,0 +1,13 @@ +{ + "version": "1", + "packages": [ + { + "path": "go", + "engine": "postgresql", + "name": "querytest", + "schema": "schema.sql", + "queries": "query.sql", + "emit_interface": true + } + ] +} diff --git a/internal/endtoend/testdata/prepared_queries/mysql/go/db.go b/internal/endtoend/testdata/prepared_queries/mysql/go/db.go index 9c72c93be7..9a78b01542 100644 --- a/internal/endtoend/testdata/prepared_queries/mysql/go/db.go +++ b/internal/endtoend/testdata/prepared_queries/mysql/go/db.go @@ -36,6 +36,9 @@ func Prepare(ctx context.Context, db DBTX) (*Queries, error) { if q.insertNewUserWithResultStmt, err = db.PrepareContext(ctx, insertNewUserWithResult); err != nil { return nil, fmt.Errorf("error preparing query InsertNewUserWithResult: %w", err) } + if q.iterUsersStmt, err = db.PrepareContext(ctx, iterUsers); err != nil { + return nil, fmt.Errorf("error preparing query IterUsers: %w", err) + } if q.listUsersStmt, err = db.PrepareContext(ctx, listUsers); err != nil { return nil, fmt.Errorf("error preparing query ListUsers: %w", err) } @@ -64,6 +67,11 @@ func (q *Queries) Close() error { err = fmt.Errorf("error closing insertNewUserWithResultStmt: %w", cerr) } } + if q.iterUsersStmt != nil { + if cerr := q.iterUsersStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing iterUsersStmt: %w", cerr) + } + } if q.listUsersStmt != nil { if cerr := q.listUsersStmt.Close(); cerr != nil { err = fmt.Errorf("error closing listUsersStmt: %w", cerr) @@ -112,6 +120,7 @@ type Queries struct { getUserByIDStmt *sql.Stmt insertNewUserStmt *sql.Stmt insertNewUserWithResultStmt *sql.Stmt + iterUsersStmt *sql.Stmt listUsersStmt *sql.Stmt } @@ -123,6 +132,7 @@ func (q *Queries) WithTx(tx *sql.Tx) *Queries { getUserByIDStmt: q.getUserByIDStmt, insertNewUserStmt: q.insertNewUserStmt, insertNewUserWithResultStmt: q.insertNewUserWithResultStmt, + iterUsersStmt: q.iterUsersStmt, listUsersStmt: q.listUsersStmt, } } diff --git a/internal/endtoend/testdata/prepared_queries/mysql/go/query.sql.go b/internal/endtoend/testdata/prepared_queries/mysql/go/query.sql.go index f2021cb4fb..823a57c1f6 100644 --- a/internal/endtoend/testdata/prepared_queries/mysql/go/query.sql.go +++ b/internal/endtoend/testdata/prepared_queries/mysql/go/query.sql.go @@ -8,6 +8,8 @@ package querytest import ( "context" "database/sql" + "errors" + "iter" ) const deleteUsersByName = `-- name: DeleteUsersByName :execrows @@ -71,6 +73,53 @@ func (q *Queries) InsertNewUserWithResult(ctx context.Context, arg InsertNewUser return q.exec(ctx, q.insertNewUserWithResultStmt, insertNewUserWithResult, arg.FirstName, arg.LastName) } +const iterUsers = `-- name: IterUsers :iter +SELECT first_name, last_name FROM users +` + +type IterUsersRow struct { + FirstName string + LastName sql.NullString +} + +// IterUsers returns a single-use iterator. The query runs when the +// iterator is ranged over, and the returned func reports its error +// after the loop. +func (q *Queries) IterUsers(ctx context.Context) (iter.Seq[IterUsersRow], func() error) { + var _err error + var _used bool + return func(_yield func(IterUsersRow) bool) { + if _used { + _err = errors.New("query IterUsers: iterator already used") + return + } + _used = true + rows, err := q.query(ctx, q.iterUsersStmt, iterUsers) + if err != nil { + _err = err + return + } + defer rows.Close() + for rows.Next() { + var i IterUsersRow + if err := rows.Scan(&i.FirstName, &i.LastName); err != nil { + _err = err + return + } + if !_yield(i) { + break + } + } + if err := rows.Close(); err != nil { + _err = err + return + } + if err := rows.Err(); err != nil { + _err = err + } + }, func() error { return _err } +} + const listUsers = `-- name: ListUsers :many SELECT first_name, last_name FROM users ` diff --git a/internal/endtoend/testdata/prepared_queries/mysql/query.sql b/internal/endtoend/testdata/prepared_queries/mysql/query.sql index b743aee060..eaf78dc7c0 100644 --- a/internal/endtoend/testdata/prepared_queries/mysql/query.sql +++ b/internal/endtoend/testdata/prepared_queries/mysql/query.sql @@ -4,6 +4,9 @@ SELECT first_name, id, last_name FROM users WHERE id = sqlc.arg('target_id'); /* name: ListUsers :many */ SELECT first_name, last_name FROM users; +/* name: IterUsers :iter */ +SELECT first_name, last_name FROM users; + /* name: InsertNewUser :exec */ INSERT INTO users (first_name, last_name) VALUES (?, ?); diff --git a/internal/endtoend/testdata/prepared_queries/postgresql/stdlib/go/db.go b/internal/endtoend/testdata/prepared_queries/postgresql/stdlib/go/db.go index 9c72c93be7..9a78b01542 100644 --- a/internal/endtoend/testdata/prepared_queries/postgresql/stdlib/go/db.go +++ b/internal/endtoend/testdata/prepared_queries/postgresql/stdlib/go/db.go @@ -36,6 +36,9 @@ func Prepare(ctx context.Context, db DBTX) (*Queries, error) { if q.insertNewUserWithResultStmt, err = db.PrepareContext(ctx, insertNewUserWithResult); err != nil { return nil, fmt.Errorf("error preparing query InsertNewUserWithResult: %w", err) } + if q.iterUsersStmt, err = db.PrepareContext(ctx, iterUsers); err != nil { + return nil, fmt.Errorf("error preparing query IterUsers: %w", err) + } if q.listUsersStmt, err = db.PrepareContext(ctx, listUsers); err != nil { return nil, fmt.Errorf("error preparing query ListUsers: %w", err) } @@ -64,6 +67,11 @@ func (q *Queries) Close() error { err = fmt.Errorf("error closing insertNewUserWithResultStmt: %w", cerr) } } + if q.iterUsersStmt != nil { + if cerr := q.iterUsersStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing iterUsersStmt: %w", cerr) + } + } if q.listUsersStmt != nil { if cerr := q.listUsersStmt.Close(); cerr != nil { err = fmt.Errorf("error closing listUsersStmt: %w", cerr) @@ -112,6 +120,7 @@ type Queries struct { getUserByIDStmt *sql.Stmt insertNewUserStmt *sql.Stmt insertNewUserWithResultStmt *sql.Stmt + iterUsersStmt *sql.Stmt listUsersStmt *sql.Stmt } @@ -123,6 +132,7 @@ func (q *Queries) WithTx(tx *sql.Tx) *Queries { getUserByIDStmt: q.getUserByIDStmt, insertNewUserStmt: q.insertNewUserStmt, insertNewUserWithResultStmt: q.insertNewUserWithResultStmt, + iterUsersStmt: q.iterUsersStmt, listUsersStmt: q.listUsersStmt, } } diff --git a/internal/endtoend/testdata/prepared_queries/postgresql/stdlib/go/query.sql.go b/internal/endtoend/testdata/prepared_queries/postgresql/stdlib/go/query.sql.go index f4c96a4b70..c35eba8006 100644 --- a/internal/endtoend/testdata/prepared_queries/postgresql/stdlib/go/query.sql.go +++ b/internal/endtoend/testdata/prepared_queries/postgresql/stdlib/go/query.sql.go @@ -8,6 +8,8 @@ package querytest import ( "context" "database/sql" + "errors" + "iter" ) const deleteUsersByName = `-- name: DeleteUsersByName :execrows @@ -71,6 +73,53 @@ func (q *Queries) InsertNewUserWithResult(ctx context.Context, arg InsertNewUser return q.exec(ctx, q.insertNewUserWithResultStmt, insertNewUserWithResult, arg.FirstName, arg.LastName) } +const iterUsers = `-- name: IterUsers :iter +SELECT first_name, last_name FROM users +` + +type IterUsersRow struct { + FirstName string + LastName sql.NullString +} + +// IterUsers returns a single-use iterator. The query runs when the +// iterator is ranged over, and the returned func reports its error +// after the loop. +func (q *Queries) IterUsers(ctx context.Context) (iter.Seq[IterUsersRow], func() error) { + var _err error + var _used bool + return func(_yield func(IterUsersRow) bool) { + if _used { + _err = errors.New("query IterUsers: iterator already used") + return + } + _used = true + rows, err := q.query(ctx, q.iterUsersStmt, iterUsers) + if err != nil { + _err = err + return + } + defer rows.Close() + for rows.Next() { + var i IterUsersRow + if err := rows.Scan(&i.FirstName, &i.LastName); err != nil { + _err = err + return + } + if !_yield(i) { + break + } + } + if err := rows.Close(); err != nil { + _err = err + return + } + if err := rows.Err(); err != nil { + _err = err + } + }, func() error { return _err } +} + const listUsers = `-- name: ListUsers :many SELECT first_name, last_name FROM users ` diff --git a/internal/endtoend/testdata/prepared_queries/postgresql/stdlib/query.sql b/internal/endtoend/testdata/prepared_queries/postgresql/stdlib/query.sql index d2f54f2037..3ef4f83ccb 100644 --- a/internal/endtoend/testdata/prepared_queries/postgresql/stdlib/query.sql +++ b/internal/endtoend/testdata/prepared_queries/postgresql/stdlib/query.sql @@ -4,6 +4,9 @@ SELECT first_name, id, last_name FROM users WHERE id = sqlc.arg('target_id'); /* name: ListUsers :many */ SELECT first_name, last_name FROM users; +/* name: IterUsers :iter */ +SELECT first_name, last_name FROM users; + /* name: InsertNewUser :exec */ INSERT INTO users (first_name, last_name) VALUES ($1, $2); diff --git a/internal/endtoend/testdata/sqlc_slice/mysql/go/query.sql.go b/internal/endtoend/testdata/sqlc_slice/mysql/go/query.sql.go index 0d213c5db6..a2de4d54a3 100644 --- a/internal/endtoend/testdata/sqlc_slice/mysql/go/query.sql.go +++ b/internal/endtoend/testdata/sqlc_slice/mysql/go/query.sql.go @@ -8,6 +8,8 @@ package querytest import ( "context" "database/sql" + "errors" + "iter" "strings" "github.com/sqlc-dev/sqlc-testdata/mysql" @@ -134,6 +136,66 @@ func (q *Queries) FuncParamIdent(ctx context.Context, arg FuncParamIdentParams) return items, nil } +const funcParamIdentIter = `-- name: FuncParamIdentIter :iter +SELECT name FROM foo +WHERE name = ? + AND id IN (/*SLICE:favourites*/?) +` + +type FuncParamIdentIterParams struct { + Slug string + Favourites []int32 +} + +// FuncParamIdentIter returns a single-use iterator. The query runs when the +// iterator is ranged over, and the returned func reports its error +// after the loop. +func (q *Queries) FuncParamIdentIter(ctx context.Context, arg FuncParamIdentIterParams) (iter.Seq[string], func() error) { + var _err error + var _used bool + return func(_yield func(string) bool) { + if _used { + _err = errors.New("query FuncParamIdentIter: iterator already used") + return + } + _used = true + query := funcParamIdentIter + var queryParams []any + queryParams = append(queryParams, arg.Slug) + if len(arg.Favourites) > 0 { + for _, v := range arg.Favourites { + queryParams = append(queryParams, v) + } + query = strings.Replace(query, "/*SLICE:favourites*/?", strings.Repeat(",?", len(arg.Favourites))[1:], 1) + } else { + query = strings.Replace(query, "/*SLICE:favourites*/?", "NULL", 1) + } + rows, err := q.db.QueryContext(ctx, query, queryParams...) + if err != nil { + _err = err + return + } + defer rows.Close() + for rows.Next() { + var name string + if err := rows.Scan(&name); err != nil { + _err = err + return + } + if !_yield(name) { + break + } + } + if err := rows.Close(); err != nil { + _err = err + return + } + if err := rows.Err(); err != nil { + _err = err + } + }, func() error { return _err } +} + const funcParamSoloArg = `-- name: FuncParamSoloArg :many SELECT name FROM foo WHERE id IN (/*SLICE:favourites*/?) diff --git a/internal/endtoend/testdata/sqlc_slice/mysql/query.sql b/internal/endtoend/testdata/sqlc_slice/mysql/query.sql index 6cf377d00d..21133a7df6 100644 --- a/internal/endtoend/testdata/sqlc_slice/mysql/query.sql +++ b/internal/endtoend/testdata/sqlc_slice/mysql/query.sql @@ -3,6 +3,11 @@ SELECT name FROM foo WHERE name = sqlc.arg(slug) AND id IN (sqlc.slice(favourites)); +/* name: FuncParamIdentIter :iter */ +SELECT name FROM foo +WHERE name = sqlc.arg(slug) + AND id IN (sqlc.slice(favourites)); + /* name: FuncParamString :many */ SELECT name FROM foo WHERE name = sqlc.arg('slug') diff --git a/internal/endtoend/testdata/wrap_errors/mysql/db/query.sql.go b/internal/endtoend/testdata/wrap_errors/mysql/db/query.sql.go index cd9e3a04c9..b07468bcb4 100644 --- a/internal/endtoend/testdata/wrap_errors/mysql/db/query.sql.go +++ b/internal/endtoend/testdata/wrap_errors/mysql/db/query.sql.go @@ -8,7 +8,9 @@ package authors import ( "context" "database/sql" + "errors" "fmt" + "iter" ) const createAuthor = `-- name: CreateAuthor :execlastid @@ -94,6 +96,49 @@ func (q *Queries) GetAuthor(ctx context.Context) (Author, error) { return i, err } +const iterAuthors = `-- name: IterAuthors :iter +SELECT id, name, bio FROM authors +ORDER BY name +` + +// IterAuthors returns a single-use iterator. The query runs when the +// iterator is ranged over, and the returned func reports its error +// after the loop. +func (q *Queries) IterAuthors(ctx context.Context) (iter.Seq[Author], func() error) { + var _err error + var _used bool + return func(_yield func(Author) bool) { + if _used { + _err = errors.New("query IterAuthors: iterator already used") + return + } + _used = true + rows, err := q.db.QueryContext(ctx, iterAuthors) + if err != nil { + _err = fmt.Errorf("query IterAuthors: %w", err) + return + } + defer rows.Close() + for rows.Next() { + var i Author + if err := rows.Scan(&i.ID, &i.Name, &i.Bio); err != nil { + _err = fmt.Errorf("query IterAuthors: %w", err) + return + } + if !_yield(i) { + break + } + } + if err := rows.Close(); err != nil { + _err = fmt.Errorf("query IterAuthors: %w", err) + return + } + if err := rows.Err(); err != nil { + _err = fmt.Errorf("query IterAuthors: %w", err) + } + }, func() error { return _err } +} + const listAuthors = `-- name: ListAuthors :many SELECT id, name, bio FROM authors ORDER BY name diff --git a/internal/endtoend/testdata/wrap_errors/mysql/query.sql b/internal/endtoend/testdata/wrap_errors/mysql/query.sql index 3f9971d942..6393759196 100644 --- a/internal/endtoend/testdata/wrap_errors/mysql/query.sql +++ b/internal/endtoend/testdata/wrap_errors/mysql/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 :execlastid INSERT INTO authors ( name, bio diff --git a/internal/endtoend/testdata/wrap_errors/postgresql/pgx/db/query.sql.go b/internal/endtoend/testdata/wrap_errors/postgresql/pgx/db/query.sql.go index e11566eb17..f4c2926958 100644 --- a/internal/endtoend/testdata/wrap_errors/postgresql/pgx/db/query.sql.go +++ b/internal/endtoend/testdata/wrap_errors/postgresql/pgx/db/query.sql.go @@ -7,7 +7,9 @@ package authors import ( "context" + "errors" "fmt" + "iter" "github.com/jackc/pgx/v5/pgconn" "github.com/jackc/pgx/v5/pgtype" @@ -96,6 +98,46 @@ 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 +` + +// IterAuthors returns a single-use iterator. The query runs when the +// iterator is ranged over, and the returned func reports its error +// after the loop. +func (q *Queries) IterAuthors(ctx context.Context) (iter.Seq[Author], func() error) { + var _err error + var _used bool + return func(_yield func(Author) bool) { + if _used { + _err = errors.New("query IterAuthors: iterator already used") + return + } + _used = true + rows, err := q.db.Query(ctx, iterAuthors) + if err != nil { + _err = fmt.Errorf("query IterAuthors: %w", err) + return + } + defer rows.Close() + for rows.Next() { + var i Author + if err := rows.Scan(&i.ID, &i.Name, &i.Bio); err != nil { + _err = fmt.Errorf("query IterAuthors: %w", err) + return + } + if !_yield(i) { + break + } + } + rows.Close() + if err := rows.Err(); err != nil { + _err = fmt.Errorf("query IterAuthors: %w", err) + } + }, func() error { return _err } +} + const listAuthors = `-- name: ListAuthors :many SELECT id, name, bio FROM authors ORDER BY name diff --git a/internal/endtoend/testdata/wrap_errors/postgresql/pgx/query.sql b/internal/endtoend/testdata/wrap_errors/postgresql/pgx/query.sql index e0310a2257..d4794e675e 100644 --- a/internal/endtoend/testdata/wrap_errors/postgresql/pgx/query.sql +++ b/internal/endtoend/testdata/wrap_errors/postgresql/pgx/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/internal/endtoend/testdata/wrap_errors/postgresql/stdlib/db/query.sql.go b/internal/endtoend/testdata/wrap_errors/postgresql/stdlib/db/query.sql.go index b954a613a8..b345c2ff86 100644 --- a/internal/endtoend/testdata/wrap_errors/postgresql/stdlib/db/query.sql.go +++ b/internal/endtoend/testdata/wrap_errors/postgresql/stdlib/db/query.sql.go @@ -8,7 +8,9 @@ package authors import ( "context" "database/sql" + "errors" "fmt" + "iter" ) const createAuthor = `-- name: CreateAuthor :one @@ -102,6 +104,49 @@ 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 +` + +// IterAuthors returns a single-use iterator. The query runs when the +// iterator is ranged over, and the returned func reports its error +// after the loop. +func (q *Queries) IterAuthors(ctx context.Context) (iter.Seq[Author], func() error) { + var _err error + var _used bool + return func(_yield func(Author) bool) { + if _used { + _err = errors.New("query IterAuthors: iterator already used") + return + } + _used = true + rows, err := q.db.QueryContext(ctx, iterAuthors) + if err != nil { + _err = fmt.Errorf("query IterAuthors: %w", err) + return + } + defer rows.Close() + for rows.Next() { + var i Author + if err := rows.Scan(&i.ID, &i.Name, &i.Bio); err != nil { + _err = fmt.Errorf("query IterAuthors: %w", err) + return + } + if !_yield(i) { + break + } + } + if err := rows.Close(); err != nil { + _err = fmt.Errorf("query IterAuthors: %w", err) + return + } + if err := rows.Err(); err != nil { + _err = fmt.Errorf("query IterAuthors: %w", err) + } + }, func() error { return _err } +} + const listAuthors = `-- name: ListAuthors :many SELECT id, name, bio FROM authors ORDER BY name diff --git a/internal/endtoend/testdata/wrap_errors/postgresql/stdlib/query.sql b/internal/endtoend/testdata/wrap_errors/postgresql/stdlib/query.sql index e0310a2257..d4794e675e 100644 --- a/internal/endtoend/testdata/wrap_errors/postgresql/stdlib/query.sql +++ b/internal/endtoend/testdata/wrap_errors/postgresql/stdlib/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/internal/endtoend/testdata/wrap_errors/sqlite/db/query.sql.go b/internal/endtoend/testdata/wrap_errors/sqlite/db/query.sql.go index 11b72d8491..7a214bc1ad 100644 --- a/internal/endtoend/testdata/wrap_errors/sqlite/db/query.sql.go +++ b/internal/endtoend/testdata/wrap_errors/sqlite/db/query.sql.go @@ -8,7 +8,9 @@ package authors import ( "context" "database/sql" + "errors" "fmt" + "iter" ) const createAuthor = `-- name: CreateAuthor :execlastid @@ -99,6 +101,49 @@ 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 +` + +// IterAuthors returns a single-use iterator. The query runs when the +// iterator is ranged over, and the returned func reports its error +// after the loop. +func (q *Queries) IterAuthors(ctx context.Context) (iter.Seq[Author], func() error) { + var _err error + var _used bool + return func(_yield func(Author) bool) { + if _used { + _err = errors.New("query IterAuthors: iterator already used") + return + } + _used = true + rows, err := q.db.QueryContext(ctx, iterAuthors) + if err != nil { + _err = fmt.Errorf("query IterAuthors: %w", err) + return + } + defer rows.Close() + for rows.Next() { + var i Author + if err := rows.Scan(&i.ID, &i.Name, &i.Bio); err != nil { + _err = fmt.Errorf("query IterAuthors: %w", err) + return + } + if !_yield(i) { + break + } + } + if err := rows.Close(); err != nil { + _err = fmt.Errorf("query IterAuthors: %w", err) + return + } + if err := rows.Err(); err != nil { + _err = fmt.Errorf("query IterAuthors: %w", err) + } + }, func() error { return _err } +} + const listAuthors = `-- name: ListAuthors :many SELECT id, name, bio FROM authors ORDER BY name diff --git a/internal/endtoend/testdata/wrap_errors/sqlite/query.sql b/internal/endtoend/testdata/wrap_errors/sqlite/query.sql index 2db6403c8b..602852c4c9 100644 --- a/internal/endtoend/testdata/wrap_errors/sqlite/query.sql +++ b/internal/endtoend/testdata/wrap_errors/sqlite/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 :execlastid INSERT INTO authors ( name, bio diff --git a/internal/metadata/meta.go b/internal/metadata/meta.go index 76ee992a7a..15c3a14d38 100644 --- a/internal/metadata/meta.go +++ b/internal/metadata/meta.go @@ -32,6 +32,7 @@ const ( CmdExecRows = ":execrows" CmdExecLastId = ":execlastid" CmdMany = ":many" + CmdIter = ":iter" CmdOne = ":one" CmdCopyFrom = ":copyfrom" CmdBatchExec = ":batchexec" @@ -98,7 +99,7 @@ func ParseQueryNameAndType(t string, commentStyle CommentSyntax) (string, string part = part[:len(part)-1] // removes the trailing "*/" element } if len(part) == 3 { - return "", "", fmt.Errorf("missing query type [':one', ':many', ':exec', ':execrows', ':execlastid', ':execresult', ':copyfrom', 'batchexec', 'batchmany', 'batchone']: %s", line) + return "", "", fmt.Errorf("missing query type [':one', ':many', ':iter', ':exec', ':execrows', ':execlastid', ':execresult', ':copyfrom', 'batchexec', 'batchmany', 'batchone']: %s", line) } if len(part) != 4 { return "", "", fmt.Errorf("invalid query comment: %s", line) @@ -106,7 +107,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 e9d9dd03cb..702379683c 100644 --- a/internal/sql/validate/cmd.go +++ b/internal/sql/validate/cmd.go @@ -65,7 +65,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