From 2324522dddecac3e86c52ed0ed2ef5523d7800ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20V=C4=83n=20Tu=E1=BA=A5n?= Date: Sun, 7 Jun 2026 20:35:54 +0700 Subject: [PATCH 1/8] docs: update --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d415acab2..95b8c9177 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ plugins: - name: golang wasm: url: https://github.com/vtuanjs/sqlc-gen-go/releases/download/v2.4.1/sqlc-gen-go.wasm - sha256: sha256:sha256:4b90887b709c3b531add02d8cfcc342f01d4ca50c55549df6ee101efc33cd340 + sha256: 4b90887b709c3b531add02d8cfcc342f01d4ca50c55549df6ee101efc33cd340 sql: - schema: schema.sql queries: query.sql @@ -63,7 +63,7 @@ plugins: - name: golang wasm: url: https://github.com/vtuanjs/sqlc-gen-go/releases/download/v2.4.1/sqlc-gen-go.wasm - sha256: sha256:sha256:4b90887b709c3b531add02d8cfcc342f01d4ca50c55549df6ee101efc33cd340 + sha256: 4b90887b709c3b531add02d8cfcc342f01d4ca50c55549df6ee101efc33cd340 sql: - engine: postgresql codegen: From 0dd52abc07b3f741a575eec1b20ab7ed1efefbc0 Mon Sep 17 00:00:00 2001 From: vtuanjs Date: Thu, 11 Jun 2026 11:12:29 +0700 Subject: [PATCH 2/8] feat: add disable_result_slice_pointers --- README.md | 19 +++++++ example/db/mock/orders.sql.go | 4 +- example/db/mock/product.sql.go | 4 +- example/db/mock/search.sql.go | 28 +++++----- example/db/mock/users.sql.go | 4 +- example/db/orders.sql.go | 8 +-- example/db/product.sql.go | 8 +-- example/db/search.sql.go | 56 ++++++++++---------- example/db/users.sql.go | 8 +-- example/sqlc.yaml | 3 +- internal/opts/options.go | 4 ++ internal/opts/options_test.go | 16 ++++++ internal/query.go | 31 +++++++++++ internal/query_test.go | 45 ++++++++++++++++ internal/result.go | 2 + internal/templates/pgx/batchCode.tmpl | 8 +-- internal/templates/pgx/interfaceCode.tmpl | 4 +- internal/templates/pgx/queryCode.tmpl | 10 ++-- internal/templates/stdlib/interfaceCode.tmpl | 4 +- internal/templates/stdlib/queryCode.tmpl | 8 +-- mise.toml | 3 ++ 21 files changed, 199 insertions(+), 78 deletions(-) create mode 100644 mise.toml diff --git a/README.md b/README.md index d415acab2..a841bf3ea 100644 --- a/README.md +++ b/README.md @@ -228,6 +228,25 @@ After filtering, remaining `$N` placeholders are renumbered sequentially and the --- +### `disable_result_slice_pointers` + +When `emit_result_struct_pointers: true` is set, `:many` queries return `[]*T` by default. Setting `disable_result_slice_pointers: true` keeps `:one` results as `*T` while changing `:many` results back to `[]T`. + +Requires `emit_result_struct_pointers: true`. + +```yaml +options: + emit_result_struct_pointers: true + disable_result_slice_pointers: true +``` + +| Query command | `emit_result_struct_pointers` only | + `disable_result_slice_pointers` | +|---|---|---| +| `:one` | `*MyRow` | `*MyRow` | +| `:many` | `[]*MyRow` | `[]MyRow` | + +--- + ### `go_generate_mock` Adds a `//go:generate` directive for mock generation. `$GOFILE` expands to the current filename at generate time. diff --git a/example/db/mock/orders.sql.go b/example/db/mock/orders.sql.go index 069b63633..4c7eeea2b 100644 --- a/example/db/mock/orders.sql.go +++ b/example/db/mock/orders.sql.go @@ -87,10 +87,10 @@ func (mr *MockOrdersQuerierMockRecorder) GetUserOrderSummary(ctx, arg1, arg any) } // ListOrdersByUser mocks base method. -func (m *MockOrdersQuerier) ListOrdersByUser(ctx context.Context, arg1 db.DBTX, arg db.ListOrdersByUserParams) ([]*db.Order, error) { +func (m *MockOrdersQuerier) ListOrdersByUser(ctx context.Context, arg1 db.DBTX, arg db.ListOrdersByUserParams) ([]db.Order, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ListOrdersByUser", ctx, arg1, arg) - ret0, _ := ret[0].([]*db.Order) + ret0, _ := ret[0].([]db.Order) ret1, _ := ret[1].(error) return ret0, ret1 } diff --git a/example/db/mock/product.sql.go b/example/db/mock/product.sql.go index 67568a407..9ea6fa21b 100644 --- a/example/db/mock/product.sql.go +++ b/example/db/mock/product.sql.go @@ -117,10 +117,10 @@ func (mr *MockProductQuerierMockRecorder) GetProductsInStock(ctx, arg1 any) *gom } // ListProducts mocks base method. -func (m *MockProductQuerier) ListProducts(ctx context.Context, arg1 db.DBTX) ([]*db.Product, error) { +func (m *MockProductQuerier) ListProducts(ctx context.Context, arg1 db.DBTX) ([]db.Product, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ListProducts", ctx, arg1) - ret0, _ := ret[0].([]*db.Product) + ret0, _ := ret[0].([]db.Product) ret1, _ := ret[1].(error) return ret0, ret1 } diff --git a/example/db/mock/search.sql.go b/example/db/mock/search.sql.go index fae4d1e59..1f4557742 100644 --- a/example/db/mock/search.sql.go +++ b/example/db/mock/search.sql.go @@ -42,10 +42,10 @@ func (m *MockSearchQuerier) EXPECT() *MockSearchQuerierMockRecorder { } // SearchUsers mocks base method. -func (m *MockSearchQuerier) SearchUsers(ctx context.Context, arg1 db.DBTX, arg db.SearchUsersParams) ([]*db.User, error) { +func (m *MockSearchQuerier) SearchUsers(ctx context.Context, arg1 db.DBTX, arg db.SearchUsersParams) ([]db.User, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "SearchUsers", ctx, arg1, arg) - ret0, _ := ret[0].([]*db.User) + ret0, _ := ret[0].([]db.User) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -57,10 +57,10 @@ func (mr *MockSearchQuerierMockRecorder) SearchUsers(ctx, arg1, arg any) *gomock } // SearchUsersByContact mocks base method. -func (m *MockSearchQuerier) SearchUsersByContact(ctx context.Context, arg1 db.DBTX, arg db.SearchUsersByContactParams) ([]*db.User, error) { +func (m *MockSearchQuerier) SearchUsersByContact(ctx context.Context, arg1 db.DBTX, arg db.SearchUsersByContactParams) ([]db.User, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "SearchUsersByContact", ctx, arg1, arg) - ret0, _ := ret[0].([]*db.User) + ret0, _ := ret[0].([]db.User) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -72,10 +72,10 @@ func (mr *MockSearchQuerierMockRecorder) SearchUsersByContact(ctx, arg1, arg any } // SearchUsersOrdered mocks base method. -func (m *MockSearchQuerier) SearchUsersOrdered(ctx context.Context, arg1 db.DBTX, arg db.SearchUsersOrderedParams) ([]*db.User, error) { +func (m *MockSearchQuerier) SearchUsersOrdered(ctx context.Context, arg1 db.DBTX, arg db.SearchUsersOrderedParams) ([]db.User, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "SearchUsersOrdered", ctx, arg1, arg) - ret0, _ := ret[0].([]*db.User) + ret0, _ := ret[0].([]db.User) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -87,10 +87,10 @@ func (mr *MockSearchQuerierMockRecorder) SearchUsersOrdered(ctx, arg1, arg any) } // SearchUsersOrderedByID mocks base method. -func (m *MockSearchQuerier) SearchUsersOrderedByID(ctx context.Context, arg1 db.DBTX, arg db.SearchUsersOrderedByIDParams) ([]*db.User, error) { +func (m *MockSearchQuerier) SearchUsersOrderedByID(ctx context.Context, arg1 db.DBTX, arg db.SearchUsersOrderedByIDParams) ([]db.User, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "SearchUsersOrderedByID", ctx, arg1, arg) - ret0, _ := ret[0].([]*db.User) + ret0, _ := ret[0].([]db.User) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -102,10 +102,10 @@ func (mr *MockSearchQuerierMockRecorder) SearchUsersOrderedByID(ctx, arg1, arg a } // SearchUsersWithBlock mocks base method. -func (m *MockSearchQuerier) SearchUsersWithBlock(ctx context.Context, arg1 db.DBTX, arg db.SearchUsersWithBlockParams) ([]*db.User, error) { +func (m *MockSearchQuerier) SearchUsersWithBlock(ctx context.Context, arg1 db.DBTX, arg db.SearchUsersWithBlockParams) ([]db.User, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "SearchUsersWithBlock", ctx, arg1, arg) - ret0, _ := ret[0].([]*db.User) + ret0, _ := ret[0].([]db.User) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -117,10 +117,10 @@ func (mr *MockSearchQuerierMockRecorder) SearchUsersWithBlock(ctx, arg1, arg any } // SearchUsersWithSameNameAndEmail mocks base method. -func (m *MockSearchQuerier) SearchUsersWithSameNameAndEmail(ctx context.Context, arg1 db.DBTX, arg db.SearchUsersWithSameNameAndEmailParams) ([]*db.User, error) { +func (m *MockSearchQuerier) SearchUsersWithSameNameAndEmail(ctx context.Context, arg1 db.DBTX, arg db.SearchUsersWithSameNameAndEmailParams) ([]db.User, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "SearchUsersWithSameNameAndEmail", ctx, arg1, arg) - ret0, _ := ret[0].([]*db.User) + ret0, _ := ret[0].([]db.User) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -132,10 +132,10 @@ func (mr *MockSearchQuerierMockRecorder) SearchUsersWithSameNameAndEmail(ctx, ar } // SearchUsersWithTopStyle mocks base method. -func (m *MockSearchQuerier) SearchUsersWithTopStyle(ctx context.Context, arg1 db.DBTX, arg db.SearchUsersWithTopStyleParams) ([]*db.User, error) { +func (m *MockSearchQuerier) SearchUsersWithTopStyle(ctx context.Context, arg1 db.DBTX, arg db.SearchUsersWithTopStyleParams) ([]db.User, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "SearchUsersWithTopStyle", ctx, arg1, arg) - ret0, _ := ret[0].([]*db.User) + ret0, _ := ret[0].([]db.User) ret1, _ := ret[1].(error) return ret0, ret1 } diff --git a/example/db/mock/users.sql.go b/example/db/mock/users.sql.go index 21f8cf46d..278e50dec 100644 --- a/example/db/mock/users.sql.go +++ b/example/db/mock/users.sql.go @@ -86,10 +86,10 @@ func (mr *MockUsersQuerierMockRecorder) GetUser(ctx, arg1, arg any) *gomock.Call } // ListUsers mocks base method. -func (m *MockUsersQuerier) ListUsers(ctx context.Context, arg1 db.DBTX, arg db.ListUsersParams) ([]*db.User, error) { +func (m *MockUsersQuerier) ListUsers(ctx context.Context, arg1 db.DBTX, arg db.ListUsersParams) ([]db.User, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ListUsers", ctx, arg1, arg) - ret0, _ := ret[0].([]*db.User) + ret0, _ := ret[0].([]db.User) ret1, _ := ret[1].(error) return ret0, ret1 } diff --git a/example/db/orders.sql.go b/example/db/orders.sql.go index 560de20ea..831c0e816 100644 --- a/example/db/orders.sql.go +++ b/example/db/orders.sql.go @@ -112,7 +112,7 @@ type ListOrdersByUserParams struct { UserID int64 } -func (q *OrdersQueries) ListOrdersByUser(ctx context.Context, db DBTX, arg ListOrdersByUserParams) ([]*Order, error) { +func (q *OrdersQueries) ListOrdersByUser(ctx context.Context, db DBTX, arg ListOrdersByUserParams) ([]Order, error) { ctx, tracer := tracing.StartTracing(ctx, "OrdersQueries.ListOrdersByUser") defer tracer.End() rows, err := db.Query(ctx, ListOrdersByUser, arg.UserID) @@ -120,7 +120,7 @@ func (q *OrdersQueries) ListOrdersByUser(ctx context.Context, db DBTX, arg ListO return nil, err } defer rows.Close() - items := []*Order{} + var items []Order for rows.Next() { var i Order if err := rows.Scan( @@ -132,7 +132,7 @@ func (q *OrdersQueries) ListOrdersByUser(ctx context.Context, db DBTX, arg ListO ); err != nil { return nil, err } - items = append(items, &i) + items = append(items, i) } if err := rows.Err(); err != nil { return nil, err @@ -160,7 +160,7 @@ type OrdersQuerier interface { CreateOrder(ctx context.Context, db DBTX, arg CreateOrderParams) (*Order, error) GetOrder(ctx context.Context, db DBTX, arg GetOrderParams) (*Order, error) GetUserOrderSummary(ctx context.Context, db DBTX, arg GetUserOrderSummaryParams) (*GetUserOrderSummaryRow, error) - ListOrdersByUser(ctx context.Context, db DBTX, arg ListOrdersByUserParams) ([]*Order, error) + ListOrdersByUser(ctx context.Context, db DBTX, arg ListOrdersByUserParams) ([]Order, error) UpdateOrderStatus(ctx context.Context, db DBTX, arg UpdateOrderStatusParams) error } diff --git a/example/db/product.sql.go b/example/db/product.sql.go index 39864a769..cb1ee24a7 100644 --- a/example/db/product.sql.go +++ b/example/db/product.sql.go @@ -129,7 +129,7 @@ const ListProducts = `-- name: ListProducts :many SELECT id, name, price, stock, created_at FROM products ORDER BY created_at DESC ` -func (q *ProductQueries) ListProducts(ctx context.Context, db DBTX) ([]*Product, error) { +func (q *ProductQueries) ListProducts(ctx context.Context, db DBTX) ([]Product, error) { ctx, tracer := tracing.StartTracing(ctx, "ProductQueries.ListProducts") defer tracer.End() rows, err := db.Query(ctx, ListProducts) @@ -137,7 +137,7 @@ func (q *ProductQueries) ListProducts(ctx context.Context, db DBTX) ([]*Product, return nil, err } defer rows.Close() - items := []*Product{} + var items []Product for rows.Next() { var i Product if err := rows.Scan( @@ -149,7 +149,7 @@ func (q *ProductQueries) ListProducts(ctx context.Context, db DBTX) ([]*Product, ); err != nil { return nil, err } - items = append(items, &i) + items = append(items, i) } if err := rows.Err(); err != nil { return nil, err @@ -179,7 +179,7 @@ type ProductQuerier interface { GetProduct(ctx context.Context, db DBTX, arg GetProductParams) (*Product, error) GetProductPrice(ctx context.Context, db DBTX, arg GetProductPriceParams) (decimal.Decimal, error) GetProductsInStock(ctx context.Context, db DBTX) (*int32, error) - ListProducts(ctx context.Context, db DBTX) ([]*Product, error) + ListProducts(ctx context.Context, db DBTX) ([]Product, error) UpdateProductStock(ctx context.Context, db DBTX, arg UpdateProductStockParams) error } diff --git a/example/db/search.sql.go b/example/db/search.sql.go index b85e32d66..ffaf51a27 100644 --- a/example/db/search.sql.go +++ b/example/db/search.sql.go @@ -43,7 +43,7 @@ type SearchUsersParams struct { HasOrders bool } -func (q *SearchQueries) SearchUsers(ctx context.Context, db DBTX, arg SearchUsersParams) ([]*User, error) { +func (q *SearchQueries) SearchUsers(ctx context.Context, db DBTX, arg SearchUsersParams) ([]User, error) { ctx, tracer := tracing.StartTracing(ctx, "SearchQueries.SearchUsers") defer tracer.End() dynQuery, dynArgs := _searchUsersDynQ.Build([]any{arg.Name, arg.Email, arg.Phone, arg.OrdersSince, arg.HasOrders}) @@ -52,7 +52,7 @@ func (q *SearchQueries) SearchUsers(ctx context.Context, db DBTX, arg SearchUser return nil, err } defer rows.Close() - items := []*User{} + var items []User for rows.Next() { var i User if err := rows.Scan( @@ -64,7 +64,7 @@ func (q *SearchQueries) SearchUsers(ctx context.Context, db DBTX, arg SearchUser ); err != nil { return nil, err } - items = append(items, &i) + items = append(items, i) } if err := rows.Err(); err != nil { return nil, err @@ -88,7 +88,7 @@ type SearchUsersByContactParams struct { } // Include the combined contact filter only when BOTH email AND phone are provided. -func (q *SearchQueries) SearchUsersByContact(ctx context.Context, db DBTX, arg SearchUsersByContactParams) ([]*User, error) { +func (q *SearchQueries) SearchUsersByContact(ctx context.Context, db DBTX, arg SearchUsersByContactParams) ([]User, error) { ctx, tracer := tracing.StartTracing(ctx, "SearchQueries.SearchUsersByContact") defer tracer.End() dynQuery, dynArgs := _searchUsersByContactDynQ.Build([]any{arg.Name, arg.Email, arg.Phone}) @@ -97,7 +97,7 @@ func (q *SearchQueries) SearchUsersByContact(ctx context.Context, db DBTX, arg S return nil, err } defer rows.Close() - items := []*User{} + var items []User for rows.Next() { var i User if err := rows.Scan( @@ -109,7 +109,7 @@ func (q *SearchQueries) SearchUsersByContact(ctx context.Context, db DBTX, arg S ); err != nil { return nil, err } - items = append(items, &i) + items = append(items, i) } if err := rows.Err(); err != nil { return nil, err @@ -136,7 +136,7 @@ type SearchUsersOrderedParams struct { OrderNameAsc bool } -func (q *SearchQueries) SearchUsersOrdered(ctx context.Context, db DBTX, arg SearchUsersOrderedParams) ([]*User, error) { +func (q *SearchQueries) SearchUsersOrdered(ctx context.Context, db DBTX, arg SearchUsersOrderedParams) ([]User, error) { ctx, tracer := tracing.StartTracing(ctx, "SearchQueries.SearchUsersOrdered") defer tracer.End() dynQuery, dynArgs := _searchUsersOrderedDynQ.Build([]any{arg.Name, arg.Email, arg.OrderCreatedAtDesc, arg.OrderNameAsc}) @@ -145,7 +145,7 @@ func (q *SearchQueries) SearchUsersOrdered(ctx context.Context, db DBTX, arg Sea return nil, err } defer rows.Close() - items := []*User{} + var items []User for rows.Next() { var i User if err := rows.Scan( @@ -157,7 +157,7 @@ func (q *SearchQueries) SearchUsersOrdered(ctx context.Context, db DBTX, arg Sea ); err != nil { return nil, err } - items = append(items, &i) + items = append(items, i) } if err := rows.Err(); err != nil { return nil, err @@ -183,7 +183,7 @@ type SearchUsersOrderedByIDParams struct { IdDesc bool } -func (q *SearchQueries) SearchUsersOrderedByID(ctx context.Context, db DBTX, arg SearchUsersOrderedByIDParams) ([]*User, error) { +func (q *SearchQueries) SearchUsersOrderedByID(ctx context.Context, db DBTX, arg SearchUsersOrderedByIDParams) ([]User, error) { ctx, tracer := tracing.StartTracing(ctx, "SearchQueries.SearchUsersOrderedByID") defer tracer.End() dynQuery, dynArgs := _searchUsersOrderedByIDDynQ.Build([]any{arg.Name, arg.Email, arg.IdAsc, arg.IdDesc}) @@ -192,7 +192,7 @@ func (q *SearchQueries) SearchUsersOrderedByID(ctx context.Context, db DBTX, arg return nil, err } defer rows.Close() - items := []*User{} + var items []User for rows.Next() { var i User if err := rows.Scan( @@ -204,7 +204,7 @@ func (q *SearchQueries) SearchUsersOrderedByID(ctx context.Context, db DBTX, arg ); err != nil { return nil, err } - items = append(items, &i) + items = append(items, i) } if err := rows.Err(); err != nil { return nil, err @@ -228,7 +228,7 @@ type SearchUsersWithBlockParams struct { Name *string } -func (q *SearchQueries) SearchUsersWithBlock(ctx context.Context, db DBTX, arg SearchUsersWithBlockParams) ([]*User, error) { +func (q *SearchQueries) SearchUsersWithBlock(ctx context.Context, db DBTX, arg SearchUsersWithBlockParams) ([]User, error) { ctx, tracer := tracing.StartTracing(ctx, "SearchQueries.SearchUsersWithBlock") defer tracer.End() dynQuery, dynArgs := _searchUsersWithBlockDynQ.Build([]any{arg.Name}) @@ -237,7 +237,7 @@ func (q *SearchQueries) SearchUsersWithBlock(ctx context.Context, db DBTX, arg S return nil, err } defer rows.Close() - items := []*User{} + var items []User for rows.Next() { var i User if err := rows.Scan( @@ -249,7 +249,7 @@ func (q *SearchQueries) SearchUsersWithBlock(ctx context.Context, db DBTX, arg S ); err != nil { return nil, err } - items = append(items, &i) + items = append(items, i) } if err := rows.Err(); err != nil { return nil, err @@ -271,7 +271,7 @@ type SearchUsersWithSameNameAndEmailParams struct { Name *string } -func (q *SearchQueries) SearchUsersWithSameNameAndEmail(ctx context.Context, db DBTX, arg SearchUsersWithSameNameAndEmailParams) ([]*User, error) { +func (q *SearchQueries) SearchUsersWithSameNameAndEmail(ctx context.Context, db DBTX, arg SearchUsersWithSameNameAndEmailParams) ([]User, error) { ctx, tracer := tracing.StartTracing(ctx, "SearchQueries.SearchUsersWithSameNameAndEmail") defer tracer.End() dynQuery, dynArgs := _searchUsersWithSameNameAndEmailDynQ.Build([]any{arg.Name}) @@ -280,7 +280,7 @@ func (q *SearchQueries) SearchUsersWithSameNameAndEmail(ctx context.Context, db return nil, err } defer rows.Close() - items := []*User{} + var items []User for rows.Next() { var i User if err := rows.Scan( @@ -292,7 +292,7 @@ func (q *SearchQueries) SearchUsersWithSameNameAndEmail(ctx context.Context, db ); err != nil { return nil, err } - items = append(items, &i) + items = append(items, i) } if err := rows.Err(); err != nil { return nil, err @@ -317,7 +317,7 @@ type SearchUsersWithTopStyleParams struct { Name *string } -func (q *SearchQueries) SearchUsersWithTopStyle(ctx context.Context, db DBTX, arg SearchUsersWithTopStyleParams) ([]*User, error) { +func (q *SearchQueries) SearchUsersWithTopStyle(ctx context.Context, db DBTX, arg SearchUsersWithTopStyleParams) ([]User, error) { ctx, tracer := tracing.StartTracing(ctx, "SearchQueries.SearchUsersWithTopStyle") defer tracer.End() dynQuery, dynArgs := _searchUsersWithTopStyleDynQ.Build([]any{arg.Name}) @@ -326,7 +326,7 @@ func (q *SearchQueries) SearchUsersWithTopStyle(ctx context.Context, db DBTX, ar return nil, err } defer rows.Close() - items := []*User{} + var items []User for rows.Next() { var i User if err := rows.Scan( @@ -338,7 +338,7 @@ func (q *SearchQueries) SearchUsersWithTopStyle(ctx context.Context, db DBTX, ar ); err != nil { return nil, err } - items = append(items, &i) + items = append(items, i) } if err := rows.Err(); err != nil { return nil, err @@ -347,14 +347,14 @@ func (q *SearchQueries) SearchUsersWithTopStyle(ctx context.Context, db DBTX, ar } type SearchQuerier interface { - SearchUsers(ctx context.Context, db DBTX, arg SearchUsersParams) ([]*User, error) + SearchUsers(ctx context.Context, db DBTX, arg SearchUsersParams) ([]User, error) // Include the combined contact filter only when BOTH email AND phone are provided. - SearchUsersByContact(ctx context.Context, db DBTX, arg SearchUsersByContactParams) ([]*User, error) - SearchUsersOrdered(ctx context.Context, db DBTX, arg SearchUsersOrderedParams) ([]*User, error) - SearchUsersOrderedByID(ctx context.Context, db DBTX, arg SearchUsersOrderedByIDParams) ([]*User, error) - SearchUsersWithBlock(ctx context.Context, db DBTX, arg SearchUsersWithBlockParams) ([]*User, error) - SearchUsersWithSameNameAndEmail(ctx context.Context, db DBTX, arg SearchUsersWithSameNameAndEmailParams) ([]*User, error) - SearchUsersWithTopStyle(ctx context.Context, db DBTX, arg SearchUsersWithTopStyleParams) ([]*User, error) + SearchUsersByContact(ctx context.Context, db DBTX, arg SearchUsersByContactParams) ([]User, error) + SearchUsersOrdered(ctx context.Context, db DBTX, arg SearchUsersOrderedParams) ([]User, error) + SearchUsersOrderedByID(ctx context.Context, db DBTX, arg SearchUsersOrderedByIDParams) ([]User, error) + SearchUsersWithBlock(ctx context.Context, db DBTX, arg SearchUsersWithBlockParams) ([]User, error) + SearchUsersWithSameNameAndEmail(ctx context.Context, db DBTX, arg SearchUsersWithSameNameAndEmailParams) ([]User, error) + SearchUsersWithTopStyle(ctx context.Context, db DBTX, arg SearchUsersWithTopStyleParams) ([]User, error) } var _ SearchQuerier = (*SearchQueries)(nil) diff --git a/example/db/users.sql.go b/example/db/users.sql.go index c590122f8..2bdd8f174 100644 --- a/example/db/users.sql.go +++ b/example/db/users.sql.go @@ -95,7 +95,7 @@ type ListUsersParams struct { Names []string } -func (q *UsersQueries) ListUsers(ctx context.Context, db DBTX, arg ListUsersParams) ([]*User, error) { +func (q *UsersQueries) ListUsers(ctx context.Context, db DBTX, arg ListUsersParams) ([]User, error) { ctx, tracer := tracing.StartTracing(ctx, "UsersQueries.ListUsers") defer tracer.End() rows, err := db.Query(ctx, ListUsers, arg.Names) @@ -103,7 +103,7 @@ func (q *UsersQueries) ListUsers(ctx context.Context, db DBTX, arg ListUsersPara return nil, err } defer rows.Close() - items := []*User{} + var items []User for rows.Next() { var i User if err := rows.Scan( @@ -115,7 +115,7 @@ func (q *UsersQueries) ListUsers(ctx context.Context, db DBTX, arg ListUsersPara ); err != nil { return nil, err } - items = append(items, &i) + items = append(items, i) } if err := rows.Err(); err != nil { return nil, err @@ -155,7 +155,7 @@ type UsersQuerier interface { CreateUser(ctx context.Context, db DBTX, arg CreateUserParams) (*User, error) DeleteUser(ctx context.Context, db DBTX, arg DeleteUserParams) error GetUser(ctx context.Context, db DBTX, arg GetUserParams) (*User, error) - ListUsers(ctx context.Context, db DBTX, arg ListUsersParams) ([]*User, error) + ListUsers(ctx context.Context, db DBTX, arg ListUsersParams) ([]User, error) UpdateUser(ctx context.Context, db DBTX, arg UpdateUserParams) (*User, error) } diff --git a/example/sqlc.yaml b/example/sqlc.yaml index 87c2fb1bc..d818b5c20 100644 --- a/example/sqlc.yaml +++ b/example/sqlc.yaml @@ -87,7 +87,7 @@ sql: package: db sql_package: "pgx/v5" emit_pointers_for_null_types: true - emit_empty_slices: true + emit_empty_slices: false emit_interface: true emit_result_struct_pointers: true emit_enum_valid_method: false @@ -107,4 +107,5 @@ sql: omit_sqlc_version: true emit_dynamic_filter: true emit_exported_queries: true + disable_result_slice_pointers: true diff --git a/internal/opts/options.go b/internal/opts/options.go index d1f2d05e9..55e9f1330 100644 --- a/internal/opts/options.go +++ b/internal/opts/options.go @@ -19,6 +19,7 @@ type Options struct { EmitEmptySlices bool `json:"emit_empty_slices,omitempty" yaml:"emit_empty_slices"` EmitExportedQueries bool `json:"emit_exported_queries" yaml:"emit_exported_queries"` EmitResultStructPointers bool `json:"emit_result_struct_pointers" yaml:"emit_result_struct_pointers"` + DisableResultSlicePointers bool `json:"disable_result_slice_pointers,omitempty" yaml:"disable_result_slice_pointers"` EmitParamsStructPointers bool `json:"emit_params_struct_pointers" yaml:"emit_params_struct_pointers"` EmitMethodsWithDbArgument bool `json:"emit_methods_with_db_argument,omitempty" yaml:"emit_methods_with_db_argument"` EmitPointersForNullTypes bool `json:"emit_pointers_for_null_types" yaml:"emit_pointers_for_null_types"` @@ -167,6 +168,9 @@ func ValidateOpts(opts *Options) error { if opts.EmitDynamicFilter && opts.EmitPreparedQueries { return fmt.Errorf("invalid options: emit_dynamic_filter and emit_prepared_queries options are mutually exclusive") } + if opts.DisableResultSlicePointers && !opts.EmitResultStructPointers { + return fmt.Errorf("invalid options: disable_result_slice_pointers requires emit_result_struct_pointers to be enabled") + } if *opts.QueryParameterLimit < 0 { return fmt.Errorf("invalid options: query parameter limit must not be negative") } diff --git a/internal/opts/options_test.go b/internal/opts/options_test.go index 097ab1c0b..0ca0cb054 100644 --- a/internal/opts/options_test.go +++ b/internal/opts/options_test.go @@ -133,6 +133,10 @@ func TestValidateOpts_MutuallyExclusive(t *testing.T) { "emit_dynamic_filter + emit_prepared_queries", &Options{EmitDynamicFilter: true, EmitPreparedQueries: true, QueryParameterLimit: &limit}, }, + { + "disable_result_slice_pointers without emit_result_struct_pointers", + &Options{DisableResultSlicePointers: true, QueryParameterLimit: &limit}, + }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { @@ -158,3 +162,15 @@ func TestValidateOpts_Valid(t *testing.T) { t.Errorf("unexpected error: %v", err) } } + +func TestValidateOpts_DisableResultSlicePointersWithEmit(t *testing.T) { + limit := int32(1) + err := ValidateOpts(&Options{ + EmitResultStructPointers: true, + DisableResultSlicePointers: true, + QueryParameterLimit: &limit, + }) + if err != nil { + t.Errorf("unexpected error: %v", err) + } +} diff --git a/internal/query.go b/internal/query.go index 4da7f01d4..3e975125d 100644 --- a/internal/query.go +++ b/internal/query.go @@ -18,6 +18,11 @@ type QueryValue struct { Typ string SQLDriver opts.SQLDriver + // DisableSlicePointer, when true, keeps the element type of returned + // slices (:many / :batchmany) a value type even though EmitPointer is set. + // It does not affect single-row (:one / :batchone) results. + DisableSlicePointer bool + // Column is kept so late in the generation process around to differentiate // between mysql slices and pg arrays Column *plugin.Column @@ -108,6 +113,32 @@ func (v *QueryValue) ReturnName() string { return escape(v.Name) } +// IsSlicePointer reports whether the element type of a returned slice +// (:many / :batchmany) should be a pointer. It mirrors IsPointer unless +// DisableSlicePointer turns slice pointers off. +func (v QueryValue) IsSlicePointer() bool { + return v.IsPointer() && !v.DisableSlicePointer +} + +// SliceType returns the element type used inside returned slices. It is like +// DefineType but honors DisableSlicePointer. +func (v *QueryValue) SliceType() string { + t := v.Type() + if v.IsSlicePointer() { + return "*" + t + } + return t +} + +// SliceReturnName returns the value appended to a result slice. It is like +// ReturnName but honors DisableSlicePointer. +func (v *QueryValue) SliceReturnName() string { + if v.IsSlicePointer() { + return "&" + escape(v.Name) + } + return escape(v.Name) +} + func (v QueryValue) UniqueFields() []Field { seen := map[string]struct{}{} fields := make([]Field, 0, len(v.Struct.Fields)) diff --git a/internal/query_test.go b/internal/query_test.go index 0a2f397c5..2bdd69fe0 100644 --- a/internal/query_test.go +++ b/internal/query_test.go @@ -159,6 +159,51 @@ func TestQueryValue_DefineType(t *testing.T) { } } +func TestQueryValue_SlicePointer(t *testing.T) { + s := &Struct{Name: "Item"} + + // EmitPointer without DisableSlicePointer: slice elements are pointers, + // matching DefineType/ReturnName. + ptr := QueryValue{EmitPointer: true, Struct: s, Name: "i"} + if !ptr.IsSlicePointer() { + t.Error("expected IsSlicePointer() = true when EmitPointer set and slice pointers not disabled") + } + if got := ptr.SliceType(); got != "*Item" { + t.Errorf("expected *Item, got %q", got) + } + if got := ptr.SliceReturnName(); got != "&i" { + t.Errorf("expected &i, got %q", got) + } + + // DisableSlicePointer keeps slice elements as values, while the single-row + // DefineType/ReturnName stay pointers. + noSlice := QueryValue{EmitPointer: true, DisableSlicePointer: true, Struct: s, Name: "i"} + if noSlice.IsSlicePointer() { + t.Error("expected IsSlicePointer() = false when DisableSlicePointer set") + } + if got := noSlice.SliceType(); got != "Item" { + t.Errorf("expected Item, got %q", got) + } + if got := noSlice.SliceReturnName(); got != "i" { + t.Errorf("expected i, got %q", got) + } + if got := noSlice.DefineType(); got != "*Item" { + t.Errorf("expected DefineType to stay *Item, got %q", got) + } + if got := noSlice.ReturnName(); got != "&i" { + t.Errorf("expected ReturnName to stay &i, got %q", got) + } + + // Without EmitPointer, DisableSlicePointer is a no-op. + plain := QueryValue{DisableSlicePointer: true, Struct: s, Name: "i"} + if plain.IsSlicePointer() { + t.Error("expected IsSlicePointer() = false when EmitPointer unset") + } + if got := plain.SliceType(); got != "Item" { + t.Errorf("expected Item, got %q", got) + } +} + func TestQueryValue_ColumnNamesAsGoSlice(t *testing.T) { // nil struct: uses DBName v := QueryValue{DBName: "user_id"} diff --git a/internal/result.go b/internal/result.go index 4d748d4ca..48697c8f7 100644 --- a/internal/result.go +++ b/internal/result.go @@ -338,6 +338,8 @@ func buildQueries(req *plugin.GenerateRequest, options *opts.Options, structs [] Struct: gs, SQLDriver: sqlpkg, EmitPointer: options.EmitResultStructPointers, + + DisableSlicePointer: options.DisableResultSlicePointers, } } diff --git a/internal/templates/pgx/batchCode.tmpl b/internal/templates/pgx/batchCode.tmpl index 36153f52f..2c27d8e32 100644 --- a/internal/templates/pgx/batchCode.tmpl +++ b/internal/templates/pgx/batchCode.tmpl @@ -69,13 +69,13 @@ func (b *{{.MethodName}}BatchResults) Exec(f func(int, error)) { {{end}} {{if eq .Cmd ":batchmany"}} -func (b *{{.MethodName}}BatchResults) Query(f func(int, []{{.Ret.DefineType}}, error)) { +func (b *{{.MethodName}}BatchResults) Query(f func(int, []{{.Ret.SliceType}}, error)) { defer b.br.Close() for t := 0; t < b.tot; t++ { {{- if $.EmitEmptySlices}} - items := []{{.Ret.DefineType}}{} + items := []{{.Ret.SliceType}}{} {{else}} - var items []{{.Ret.DefineType}} + var items []{{.Ret.SliceType}} {{end -}} if b.closed { if f != nil { @@ -94,7 +94,7 @@ func (b *{{.MethodName}}BatchResults) Query(f func(int, []{{.Ret.DefineType}}, e if err := rows.Scan({{.Ret.Scan}}); err != nil { return err } - items = append(items, {{.Ret.ReturnName}}) + items = append(items, {{.Ret.SliceReturnName}}) } return rows.Err() }() diff --git a/internal/templates/pgx/interfaceCode.tmpl b/internal/templates/pgx/interfaceCode.tmpl index 9dd1527c5..c951ed8b4 100644 --- a/internal/templates/pgx/interfaceCode.tmpl +++ b/internal/templates/pgx/interfaceCode.tmpl @@ -15,11 +15,11 @@ {{- if and (eq .Cmd ":many") ($dbtxParam) }} {{range .Comments}}//{{.}} {{end -}} - {{.MethodName}}(ctx context.Context, db DBTX, {{.Arg.Pair}}) ([]{{.Ret.DefineType}}, error) + {{.MethodName}}(ctx context.Context, db DBTX, {{.Arg.Pair}}) ([]{{.Ret.SliceType}}, error) {{- else if eq .Cmd ":many" }} {{range .Comments}}//{{.}} {{end -}} - {{.MethodName}}(ctx context.Context, {{.Arg.Pair}}) ([]{{.Ret.DefineType}}, error) + {{.MethodName}}(ctx context.Context, {{.Arg.Pair}}) ([]{{.Ret.SliceType}}, error) {{- end}} {{- if and (eq .Cmd ":exec") ($dbtxParam) }} {{range .Comments}}//{{.}} diff --git a/internal/templates/pgx/queryCode.tmpl b/internal/templates/pgx/queryCode.tmpl index 5ca8a336a..b8bb7733a 100644 --- a/internal/templates/pgx/queryCode.tmpl +++ b/internal/templates/pgx/queryCode.tmpl @@ -88,13 +88,13 @@ func (q *{{$.StructName}}) {{.MethodName}}(ctx context.Context, {{.Arg.Pair}}) ( {{range .Comments}}//{{.}} {{end -}} {{- if $.EmitMethodsWithDBArgument -}} -func (q *{{$.StructName}}) {{.MethodName}}(ctx context.Context, db DBTX, {{.Arg.Pair}}) ([]{{.Ret.DefineType}}, error) { +func (q *{{$.StructName}}) {{.MethodName}}(ctx context.Context, db DBTX, {{.Arg.Pair}}) ([]{{.Ret.SliceType}}, error) { {{tracingCode .MethodName}}{{- if .HasDynFilter}} dynQuery, dynArgs := {{.DynQueryVar}}.Build([]any{ {{.DynFilterArgs}} }) rows, err := db.Query(ctx, dynQuery, dynArgs...) {{- else}} rows, err := db.Query(ctx, {{.ConstantName}}, {{.Arg.Params}}) {{- end}} {{- else -}} -func (q *{{$.StructName}}) {{.MethodName}}(ctx context.Context, {{.Arg.Pair}}) ([]{{.Ret.DefineType}}, error) { +func (q *{{$.StructName}}) {{.MethodName}}(ctx context.Context, {{.Arg.Pair}}) ([]{{.Ret.SliceType}}, error) { {{tracingCode .MethodName}}{{- if .HasDynFilter}} dynQuery, dynArgs := {{.DynQueryVar}}.Build([]any{ {{.DynFilterArgs}} }) rows, err := q.db.Query(ctx, dynQuery, dynArgs...) {{- else}} rows, err := q.db.Query(ctx, {{.ConstantName}}, {{.Arg.Params}}) @@ -105,16 +105,16 @@ func (q *{{$.StructName}}) {{.MethodName}}(ctx context.Context, {{.Arg.Pair}}) ( } defer rows.Close() {{- if $.EmitEmptySlices}} - items := []{{.Ret.DefineType}}{} + items := []{{.Ret.SliceType}}{} {{else}} - var items []{{.Ret.DefineType}} + var items []{{.Ret.SliceType}} {{end -}} for rows.Next() { var {{.Ret.Name}} {{.Ret.Type}} if err := rows.Scan({{.Ret.Scan}}); err != nil { return nil, {{if $.WrapErrors}}fmt.Errorf("query {{.MethodName}}: %w", err){{else}}err{{end}} } - items = append(items, {{.Ret.ReturnName}}) + items = append(items, {{.Ret.SliceReturnName}}) } if err := rows.Err(); err != nil { return nil, {{if $.WrapErrors}}fmt.Errorf("query {{.MethodName}}: %w", err){{else}}err{{end}} diff --git a/internal/templates/stdlib/interfaceCode.tmpl b/internal/templates/stdlib/interfaceCode.tmpl index 4db2356c4..0a2c620e4 100644 --- a/internal/templates/stdlib/interfaceCode.tmpl +++ b/internal/templates/stdlib/interfaceCode.tmpl @@ -15,11 +15,11 @@ {{- if and (eq .Cmd ":many") ($dbtxParam) }} {{range .Comments}}//{{.}} {{end -}} - {{.MethodName}}(ctx context.Context, db DBTX, {{.Arg.Pair}}) ([]{{.Ret.DefineType}}, error) + {{.MethodName}}(ctx context.Context, db DBTX, {{.Arg.Pair}}) ([]{{.Ret.SliceType}}, error) {{- else if eq .Cmd ":many"}} {{range .Comments}}//{{.}} {{end -}} - {{.MethodName}}(ctx context.Context, {{.Arg.Pair}}) ([]{{.Ret.DefineType}}, error) + {{.MethodName}}(ctx context.Context, {{.Arg.Pair}}) ([]{{.Ret.SliceType}}, error) {{- end}} {{- if and (eq .Cmd ":exec") ($dbtxParam) }} {{range .Comments}}//{{.}} diff --git a/internal/templates/stdlib/queryCode.tmpl b/internal/templates/stdlib/queryCode.tmpl index 83ae78f54..5d98fb111 100644 --- a/internal/templates/stdlib/queryCode.tmpl +++ b/internal/templates/stdlib/queryCode.tmpl @@ -72,23 +72,23 @@ func (q *{{$.StructName}}) {{.MethodName}}(ctx context.Context, {{ dbarg }} {{.A {{if eq .Cmd ":many"}} {{range .Comments}}//{{.}} {{end -}} -func (q *{{$.StructName}}) {{.MethodName}}(ctx context.Context, {{ dbarg }} {{.Arg.Pair}}) ([]{{.Ret.DefineType}}, error) { +func (q *{{$.StructName}}) {{.MethodName}}(ctx context.Context, {{ dbarg }} {{.Arg.Pair}}) ([]{{.Ret.SliceType}}, error) { {{tracingCode .MethodName}} {{- template "queryCodeStdExec" . }} if err != nil { return nil, {{if $.WrapErrors}}fmt.Errorf("query {{.MethodName}}: %w", err){{else}}err{{end}} } defer rows.Close() {{- if $.EmitEmptySlices}} - items := []{{.Ret.DefineType}}{} + items := []{{.Ret.SliceType}}{} {{else}} - var items []{{.Ret.DefineType}} + var items []{{.Ret.SliceType}} {{end -}} for rows.Next() { var {{.Ret.Name}} {{.Ret.Type}} if err := rows.Scan({{.Ret.Scan}}); err != nil { return nil, {{if $.WrapErrors}}fmt.Errorf("query {{.MethodName}}: %w", err){{else}}err{{end}} } - items = append(items, {{.Ret.ReturnName}}) + items = append(items, {{.Ret.SliceReturnName}}) } if err := rows.Close(); err != nil { return nil, {{if $.WrapErrors}}fmt.Errorf("query {{.MethodName}}: %w", err){{else}}err{{end}} diff --git a/mise.toml b/mise.toml new file mode 100644 index 000000000..2b3d211f8 --- /dev/null +++ b/mise.toml @@ -0,0 +1,3 @@ +[tools] +go = "1.25.10" +sqlc = "1.31.1" From 3e714c10c1d45aac2ecb9a138a2a1471f09ce57c Mon Sep 17 00:00:00 2001 From: vtuanjs Date: Thu, 11 Jun 2026 12:27:22 +0700 Subject: [PATCH 3/8] bump: v2.5.0 --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 423f09d0b..4be0ec8c9 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,8 @@ version: '2' plugins: - name: golang wasm: - url: https://github.com/vtuanjs/sqlc-gen-go/releases/download/v2.4.1/sqlc-gen-go.wasm - sha256: 4b90887b709c3b531add02d8cfcc342f01d4ca50c55549df6ee101efc33cd340 + url: https://github.com/vtuanjs/sqlc-gen-go/releases/download/v2.5.0/sqlc-gen-go.wasm + sha256: 22ff409d72b41992d9dd9bb1ff132b9132a6c1b41c9a0a10afbaacab1f94445b sql: - schema: schema.sql queries: query.sql @@ -62,8 +62,8 @@ sql: plugins: - name: golang wasm: - url: https://github.com/vtuanjs/sqlc-gen-go/releases/download/v2.4.1/sqlc-gen-go.wasm - sha256: 4b90887b709c3b531add02d8cfcc342f01d4ca50c55549df6ee101efc33cd340 + url: https://github.com/vtuanjs/sqlc-gen-go/releases/download/v2.5.0/sqlc-gen-go.wasm + sha256: 22ff409d72b41992d9dd9bb1ff132b9132a6c1b41c9a0a10afbaacab1f94445b sql: - engine: postgresql codegen: From b3c01e8a4c806dc44108b42616ce15ad9d6b3997 Mon Sep 17 00:00:00 2001 From: vtuanjs Date: Mon, 15 Jun 2026 18:00:16 +0700 Subject: [PATCH 4/8] fix: not return pointer in slice params --- example/db/dynfilter.go | 2 +- example/db/search.sql.go | 48 ++++++++++ example/e2e/search_test.go | 78 ++++++++++++++++ example/queries/search.sql | 8 ++ example/sqlc.yaml | 2 +- internal/dynfilter_gen_test.go | 125 ++++++++++++++++++++++++++ internal/result.go | 7 +- internal/templates/dynfilterCode.tmpl | 2 +- 8 files changed, 266 insertions(+), 6 deletions(-) diff --git a/example/db/dynfilter.go b/example/db/dynfilter.go index a8a7cc49e..aba7539ed 100644 --- a/example/db/dynfilter.go +++ b/example/db/dynfilter.go @@ -371,7 +371,7 @@ func dynArgActive(arg any) bool { } v := reflect.ValueOf(arg) switch v.Kind() { - case reflect.Ptr, reflect.Interface: + case reflect.Ptr, reflect.Interface, reflect.Slice: return !v.IsNil() default: return true diff --git a/example/db/search.sql.go b/example/db/search.sql.go index ffaf51a27..e55f5b938 100644 --- a/example/db/search.sql.go +++ b/example/db/search.sql.go @@ -117,6 +117,51 @@ func (q *SearchQueries) SearchUsersByContact(ctx context.Context, db DBTX, arg S return items, nil } +const SearchUsersByIDs = `-- name: SearchUsersByIDs :many +SELECT id, name, email, created_at, phone FROM users +WHERE name = $1 + AND id = ANY($2::bigint[]) -- :if $2 +ORDER BY id ASC +` + +var _searchUsersByIDsDynQ = dynCompile(SearchUsersByIDs) + +type SearchUsersByIDsParams struct { + Name string + Ids []int64 +} + +// Filter by a list of IDs. When ids is nil the condition is skipped and all +// users matching the name are returned (nil slice = inactive filter). +func (q *SearchQueries) SearchUsersByIDs(ctx context.Context, db DBTX, arg SearchUsersByIDsParams) ([]User, error) { + ctx, tracer := tracing.StartTracing(ctx, "SearchQueries.SearchUsersByIDs") + defer tracer.End() + dynQuery, dynArgs := _searchUsersByIDsDynQ.Build([]any{arg.Name, arg.Ids}) + rows, err := db.Query(ctx, dynQuery, dynArgs...) + if err != nil { + return nil, err + } + defer rows.Close() + var items []User + for rows.Next() { + var i User + if err := rows.Scan( + &i.ID, + &i.Name, + &i.Email, + &i.CreatedAt, + &i.Phone, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const SearchUsersOrdered = `-- name: SearchUsersOrdered :many SELECT id, name, email, created_at, phone FROM users WHERE name = $1 @@ -350,6 +395,9 @@ type SearchQuerier interface { SearchUsers(ctx context.Context, db DBTX, arg SearchUsersParams) ([]User, error) // Include the combined contact filter only when BOTH email AND phone are provided. SearchUsersByContact(ctx context.Context, db DBTX, arg SearchUsersByContactParams) ([]User, error) + // Filter by a list of IDs. When ids is nil the condition is skipped and all + // users matching the name are returned (nil slice = inactive filter). + SearchUsersByIDs(ctx context.Context, db DBTX, arg SearchUsersByIDsParams) ([]User, error) SearchUsersOrdered(ctx context.Context, db DBTX, arg SearchUsersOrderedParams) ([]User, error) SearchUsersOrderedByID(ctx context.Context, db DBTX, arg SearchUsersOrderedByIDParams) ([]User, error) SearchUsersWithBlock(ctx context.Context, db DBTX, arg SearchUsersWithBlockParams) ([]User, error) diff --git a/example/e2e/search_test.go b/example/e2e/search_test.go index 99847afc0..cff3c79cd 100644 --- a/example/e2e/search_test.go +++ b/example/e2e/search_test.go @@ -320,6 +320,84 @@ func TestSearchUsersOrderedByID(t *testing.T) { }) } +func TestSearchUsersByIDs(t *testing.T) { + conn := setup.NewDB(t, "../schema.sql") + ctx := context.Background() + q := db.NewSearchQueries() + + alice := setup.InsertUser(t, conn, "alice", "alice.ids@example.com", nil) + bob := setup.InsertUser(t, conn, "alice", "bob.ids@example.com", nil) // same name, different id + + t.Run("NilIDs_ReturnsAll", func(t *testing.T) { + // nil slice → condition skipped → both users returned + users, err := q.SearchUsersByIDs(ctx, conn, db.SearchUsersByIDsParams{Name: "alice"}) + if err != nil { + t.Fatal(err) + } + ids := make(map[int64]bool, len(users)) + for _, u := range users { + ids[u.ID] = true + } + if !ids[alice.ID] || !ids[bob.ID] { + t.Errorf("expected both alice and bob when IDs is nil, got %v", users) + } + }) + + t.Run("SpecificIDs_OnlyAlice", func(t *testing.T) { + // non-nil slice → condition active → only alice matches + users, err := q.SearchUsersByIDs(ctx, conn, db.SearchUsersByIDsParams{ + Name: "alice", + Ids: []int64{alice.ID}, + }) + if err != nil { + t.Fatal(err) + } + if len(users) != 1 || users[0].ID != alice.ID { + t.Errorf("got %v, want only alice (%d)", users, alice.ID) + } + }) + + t.Run("MultipleIDs_BothMatch", func(t *testing.T) { + users, err := q.SearchUsersByIDs(ctx, conn, db.SearchUsersByIDsParams{ + Name: "alice", + Ids: []int64{alice.ID, bob.ID}, + }) + if err != nil { + t.Fatal(err) + } + if len(users) != 2 { + t.Errorf("got %d users, want 2", len(users)) + } + }) + + t.Run("EmptySlice_NoMatch", func(t *testing.T) { + // empty (non-nil) slice → condition active with empty set → no rows + users, err := q.SearchUsersByIDs(ctx, conn, db.SearchUsersByIDsParams{ + Name: "alice", + Ids: []int64{}, + }) + if err != nil { + t.Fatal(err) + } + if len(users) != 0 { + t.Errorf("got %d users, want 0 for empty id list", len(users)) + } + }) + + t.Run("IDNotInList_NoMatch", func(t *testing.T) { + users, err := q.SearchUsersByIDs(ctx, conn, db.SearchUsersByIDsParams{ + Name: "alice", + Ids: []int64{-1}, + }) + if err != nil { + t.Fatal(err) + } + if len(users) != 0 { + t.Errorf("got %d users, want 0", len(users)) + } + }) +} + func TestSearchUsersWithSameNameAndEmail(t *testing.T) { conn := setup.NewDB(t, "../schema.sql") ctx := context.Background() diff --git a/example/queries/search.sql b/example/queries/search.sql index 8c7377728..e6b00e9e2 100644 --- a/example/queries/search.sql +++ b/example/queries/search.sql @@ -60,4 +60,12 @@ WHERE 1 = 1 name = @name AND email = @name ) +ORDER BY id ASC; + +-- name: SearchUsersByIDs :many +-- Filter by a list of IDs. When ids is nil the condition is skipped and all +-- users matching the name are returned (nil slice = inactive filter). +SELECT * FROM users +WHERE name = @name + AND id = ANY(@ids::bigint[]) -- :if @ids ORDER BY id ASC; \ No newline at end of file diff --git a/example/sqlc.yaml b/example/sqlc.yaml index d818b5c20..95e40d2ee 100644 --- a/example/sqlc.yaml +++ b/example/sqlc.yaml @@ -4,7 +4,7 @@ plugins: - name: golang wasm: url: file://../bin/sqlc-gen-go.wasm - sha256: "" + sha256: "a652e2c2c25d2b0332b4d8a8d0476e6674cda615f74090a8ead91aacba9704f3" options: golang: diff --git a/internal/dynfilter_gen_test.go b/internal/dynfilter_gen_test.go index 223428cff..257cd7d87 100644 --- a/internal/dynfilter_gen_test.go +++ b/internal/dynfilter_gen_test.go @@ -277,3 +277,128 @@ func TestGenerateDynamicFilter_BlockAnnotation(t *testing.T) { t.Logf("Generated:\n%s", queryFile) } +// TestGenerateDynamicFilter_ArrayParam verifies that a conditional param whose +// Go type is a slice ([]T) is NOT wrapped in a pointer (*[]T). Slices are +// already nil-able, so nil means "skip this condition" without the extra level +// of indirection. +func TestGenerateDynamicFilter_ArrayParam(t *testing.T) { + req := &plugin.GenerateRequest{ + SqlcVersion: "v1.0.0", + Settings: &plugin.Settings{Engine: "postgresql"}, + Catalog: &plugin.Catalog{ + DefaultSchema: "public", + Schemas: []*plugin.Schema{{ + Name: "public", + Tables: []*plugin.Table{{ + Rel: &plugin.Identifier{Schema: "public", Name: "items"}, + Columns: []*plugin.Column{ + {Name: "id", NotNull: true, Type: &plugin.Identifier{Name: "bigint"}}, + {Name: "name", NotNull: true, Type: &plugin.Identifier{Name: "text"}}, + }, + }}, + }}, + }, + Queries: []*plugin.Query{{ + Name: "SearchItems", + Cmd: ":many", + Filename: "query.sql", + Text: "SELECT id, name FROM items\nWHERE name = $1\n AND id = ANY($2) -- :if @ids", + Params: []*plugin.Parameter{ + {Number: 1, Column: &plugin.Column{Name: "name", NotNull: true, Type: &plugin.Identifier{Name: "text"}}}, + {Number: 2, Column: &plugin.Column{Name: "ids", IsArray: true, ArrayDims: 1, NotNull: true, Type: &plugin.Identifier{Name: "bigint"}}}, + }, + Columns: []*plugin.Column{ + {Name: "id", NotNull: true, Type: &plugin.Identifier{Name: "bigint"}}, + {Name: "name", NotNull: true, Type: &plugin.Identifier{Name: "text"}}, + }, + }}, + PluginOptions: []byte(`{"package":"testpkg","sql_package":"pgx/v5","emit_dynamic_filter":true}`), + GlobalOptions: []byte(`{}`), + } + + resp, err := Generate(context.Background(), req) + if err != nil { + t.Fatal(err) + } + + var queryFile string + for _, f := range resp.Files { + if f.Name == "query.sql.go" { + queryFile = string(f.Contents) + } + } + if queryFile == "" { + t.Fatal("query.sql.go not generated") + } + + // The slice type must NOT be wrapped in a pointer. + if strings.Contains(queryFile, "*[]int64") { + t.Logf("query file:\n%s", queryFile) + t.Error("array param should not be wrapped in a pointer: got *[]int64, want []int64") + } + // The field must still be present as a plain slice. + if !strings.Contains(queryFile, "[]int64") { + t.Logf("query file:\n%s", queryFile) + t.Error("expected []int64 field for array param") + } + t.Logf("Generated:\n%s", queryFile) +} + +// TestGenerateDynamicFilter_ArrayParam_Single verifies the same nil-slice logic +// for a query with a single conditional array param (no struct wrapping). +func TestGenerateDynamicFilter_ArrayParam_Single(t *testing.T) { + req := &plugin.GenerateRequest{ + SqlcVersion: "v1.0.0", + Settings: &plugin.Settings{Engine: "postgresql"}, + Catalog: &plugin.Catalog{ + DefaultSchema: "public", + Schemas: []*plugin.Schema{{ + Name: "public", + Tables: []*plugin.Table{{ + Rel: &plugin.Identifier{Schema: "public", Name: "items"}, + Columns: []*plugin.Column{{Name: "id", NotNull: true, Type: &plugin.Identifier{Name: "bigint"}}}, + }}, + }}, + }, + Queries: []*plugin.Query{{ + Name: "FilterByIDs", + Cmd: ":many", + Filename: "query.sql", + Text: "SELECT id FROM items\nWHERE id = ANY($1) -- :if @ids", + Params: []*plugin.Parameter{ + {Number: 1, Column: &plugin.Column{Name: "ids", IsArray: true, ArrayDims: 1, NotNull: true, Type: &plugin.Identifier{Name: "bigint"}}}, + }, + Columns: []*plugin.Column{ + {Name: "id", NotNull: true, Type: &plugin.Identifier{Name: "bigint"}}, + }, + }}, + PluginOptions: []byte(`{"package":"testpkg","sql_package":"pgx/v5","emit_dynamic_filter":true}`), + GlobalOptions: []byte(`{}`), + } + + resp, err := Generate(context.Background(), req) + if err != nil { + t.Fatal(err) + } + + var queryFile string + for _, f := range resp.Files { + if f.Name == "query.sql.go" { + queryFile = string(f.Contents) + } + } + if queryFile == "" { + t.Fatal("query.sql.go not generated") + } + + if strings.Contains(queryFile, "*[]int64") { + t.Logf("query file:\n%s", queryFile) + t.Error("single array param should not be wrapped in a pointer: got *[]int64, want []int64") + } + if !strings.Contains(queryFile, "[]int64") { + t.Logf("query file:\n%s", queryFile) + t.Error("expected []int64 type for single array param") + } + t.Logf("Generated:\n%s", queryFile) +} + diff --git a/internal/result.go b/internal/result.go index 48697c8f7..5ac14dfda 100644 --- a/internal/result.go +++ b/internal/result.go @@ -493,7 +493,7 @@ func applyDynFilter(_ *plugin.GenerateRequest, options *opts.Options, gq *Query, // Find the param number for this field for _, p := range sqlParams { if p.Column.Name == f.DBName && conditionalNums[int(p.Number)] { - if !strings.HasPrefix(f.Type, "*") { + if !strings.HasPrefix(f.Type, "*") && !strings.HasPrefix(f.Type, "[]") { gq.Arg.Struct.Fields[i].Type = "*" + f.Type } break @@ -511,8 +511,9 @@ func applyDynFilter(_ *plugin.GenerateRequest, options *opts.Options, gq *Query, // Force emission of the struct gq.Arg.Emit = true } else if !gq.Arg.isEmpty() { - // Single param (not a struct) that is conditional: make it a pointer - if conditionalNums[1] && !strings.HasPrefix(gq.Arg.Typ, "*") { + // Single param (not a struct) that is conditional: make it a pointer. + // Slices are already nil-able, so skip wrapping for []T types. + if conditionalNums[1] && !strings.HasPrefix(gq.Arg.Typ, "*") && !strings.HasPrefix(gq.Arg.Typ, "[]") { gq.Arg.Typ = "*" + gq.Arg.Typ } // If there are flag params, we need to upgrade to a struct diff --git a/internal/templates/dynfilterCode.tmpl b/internal/templates/dynfilterCode.tmpl index 7de04ce3d..bc7204d43 100644 --- a/internal/templates/dynfilterCode.tmpl +++ b/internal/templates/dynfilterCode.tmpl @@ -363,7 +363,7 @@ func dynArgActive(arg any) bool { } v := reflect.ValueOf(arg) switch v.Kind() { - case reflect.Ptr, reflect.Interface: + case reflect.Ptr, reflect.Interface, reflect.Slice: return !v.IsNil() default: return true From 8bad9c6cc0c9c6182b2dd2894ca24abbf5209454 Mon Sep 17 00:00:00 2001 From: vtuanjs Date: Mon, 15 Jun 2026 18:04:33 +0700 Subject: [PATCH 5/8] bump: v3.0.0 --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 4be0ec8c9..3565546dc 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,8 @@ version: '2' plugins: - name: golang wasm: - url: https://github.com/vtuanjs/sqlc-gen-go/releases/download/v2.5.0/sqlc-gen-go.wasm - sha256: 22ff409d72b41992d9dd9bb1ff132b9132a6c1b41c9a0a10afbaacab1f94445b + url: https://github.com/vtuanjs/sqlc-gen-go/releases/download/v3.0.0/sqlc-gen-go.wasm + sha256: a652e2c2c25d2b0332b4d8a8d0476e6674cda615f74090a8ead91aacba9704f3 sql: - schema: schema.sql queries: query.sql @@ -62,8 +62,8 @@ sql: plugins: - name: golang wasm: - url: https://github.com/vtuanjs/sqlc-gen-go/releases/download/v2.5.0/sqlc-gen-go.wasm - sha256: 22ff409d72b41992d9dd9bb1ff132b9132a6c1b41c9a0a10afbaacab1f94445b + url: https://github.com/vtuanjs/sqlc-gen-go/releases/download/v3.0.0/sqlc-gen-go.wasm + sha256: a652e2c2c25d2b0332b4d8a8d0476e6674cda615f74090a8ead91aacba9704f3 sql: - engine: postgresql codegen: From 4c264122ac68f0a72ef6811630284605d758c89f Mon Sep 17 00:00:00 2001 From: vtuanjs Date: Mon, 15 Jun 2026 18:18:23 +0700 Subject: [PATCH 6/8] bump: v3.0.0 --- example/db/mock/search.sql.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/example/db/mock/search.sql.go b/example/db/mock/search.sql.go index 1f4557742..c50d41335 100644 --- a/example/db/mock/search.sql.go +++ b/example/db/mock/search.sql.go @@ -71,6 +71,21 @@ func (mr *MockSearchQuerierMockRecorder) SearchUsersByContact(ctx, arg1, arg any return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SearchUsersByContact", reflect.TypeOf((*MockSearchQuerier)(nil).SearchUsersByContact), ctx, arg1, arg) } +// SearchUsersByIDs mocks base method. +func (m *MockSearchQuerier) SearchUsersByIDs(ctx context.Context, arg1 db.DBTX, arg db.SearchUsersByIDsParams) ([]db.User, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SearchUsersByIDs", ctx, arg1, arg) + ret0, _ := ret[0].([]db.User) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// SearchUsersByIDs indicates an expected call of SearchUsersByIDs. +func (mr *MockSearchQuerierMockRecorder) SearchUsersByIDs(ctx, arg1, arg any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SearchUsersByIDs", reflect.TypeOf((*MockSearchQuerier)(nil).SearchUsersByIDs), ctx, arg1, arg) +} + // SearchUsersOrdered mocks base method. func (m *MockSearchQuerier) SearchUsersOrdered(ctx context.Context, arg1 db.DBTX, arg db.SearchUsersOrderedParams) ([]db.User, error) { m.ctrl.T.Helper() From 1f0c37ce8bf184dab2cb3af9b6bfa12a599b9c4e Mon Sep 17 00:00:00 2001 From: vtuanjs Date: Mon, 15 Jun 2026 18:23:17 +0700 Subject: [PATCH 7/8] fix: generate example --- example/sqlc.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/sqlc.yaml b/example/sqlc.yaml index 95e40d2ee..922e75d74 100644 --- a/example/sqlc.yaml +++ b/example/sqlc.yaml @@ -4,7 +4,7 @@ plugins: - name: golang wasm: url: file://../bin/sqlc-gen-go.wasm - sha256: "a652e2c2c25d2b0332b4d8a8d0476e6674cda615f74090a8ead91aacba9704f3" + sha256: a652e2c2c25d2b0332b4d8a8d0476e6674cda615f74090a8ead91aacba9704f3 options: golang: From 5c9fe7f784d4e4915709c3189774c583e9ac4774 Mon Sep 17 00:00:00 2001 From: vtuanjs Date: Mon, 15 Jun 2026 18:26:12 +0700 Subject: [PATCH 8/8] fix: generate example --- example/sqlc.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/sqlc.yaml b/example/sqlc.yaml index 922e75d74..d818b5c20 100644 --- a/example/sqlc.yaml +++ b/example/sqlc.yaml @@ -4,7 +4,7 @@ plugins: - name: golang wasm: url: file://../bin/sqlc-gen-go.wasm - sha256: a652e2c2c25d2b0332b4d8a8d0476e6674cda615f74090a8ead91aacba9704f3 + sha256: "" options: golang: