Skip to content

Commit 2c10512

Browse files
committed
initial commit
1 parent 4fba896 commit 2c10512

File tree

9 files changed

+582
-3
lines changed

9 files changed

+582
-3
lines changed

.gitignore

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,5 +28,5 @@ go.work.sum
2828
.env
2929

3030
# Editor/IDE
31-
# .idea/
32-
# .vscode/
31+
.idea/
32+
.vscode/

README.md

Lines changed: 143 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,144 @@
11
# sqlc-plugin-bulk-go
2-
Plugin to generate bulk insert function by sqlc
2+
3+
A [sqlc](https://github.com/sqlc-dev/sqlc) plugin that automatically generates bulk insert functions for your existing sqlc-generated BULK INSERT queries.
4+
5+
## Overview
6+
7+
This plugin analyzes your sqlc-generated BULK INSERT queries and creates corresponding bulk insert functions that can efficiently insert multiple rows in a single database operation. It works by:
8+
9+
1. Identifying all INSERT queries in your sqlc configuration
10+
2. Generating a bulk version of each INSERT function that accepts a slice of parameters
11+
3. Creating helper functions to build the SQL query and extract values from the parameter structs
12+
13+
## Features
14+
15+
- Automatically generates bulk insert functions for all INSERT queries
16+
- Handles parameter extraction from struct fields
17+
- Builds proper SQL queries with placeholders for multiple rows
18+
- Maintains type safety with Go generics
19+
20+
## Options
21+
22+
The plugin supports the following configuration options:
23+
24+
| Option | Type | Required | Description |
25+
|--------|------|----------|-------------|
26+
| `package` | string | Yes | The package name for the generated code |
27+
28+
## Usage
29+
30+
### 1. Configure sqlc.yaml
31+
32+
Add the plugin to your sqlc configuration:
33+
34+
```yaml
35+
version: "2"
36+
plugins:
37+
- name: bulkinsert
38+
process:
39+
cmd: "go run github.com/tomtwinkle/sqlc-plugin-bulk-go"
40+
options:
41+
package: "db" # Replace with your database package name
42+
43+
sql:
44+
- schema: "path/to/schema.sql"
45+
queries: "path/to/query.sql"
46+
engine: "postgresql" # or "mysql"
47+
codegen:
48+
- plugin: bulkinsert
49+
out: "path/to/output"
50+
```
51+
52+
### 2. Generate code
53+
54+
Run sqlc to generate your code:
55+
56+
```bash
57+
sqlc generate
58+
```
59+
60+
### 3. Use the generated bulk insert functions
61+
62+
For each INSERT query in your sqlc configuration, a corresponding bulk insert function will be generated. For example, if you have a query named `CreateUser`, a `BulkCreateUser` function will be generated.
63+
64+
```go
65+
// In your db/bulk.sql.go file (generated by this plugin)
66+
package db
67+
68+
// Generated by this plugin
69+
type BulkCreateUserParams []CreateUserParams
70+
71+
func (q *Queries) BulkCreateUser(ctx context.Context, args BulkCreateUserParams) error {
72+
// Implementation generated by this plugin
73+
return nil
74+
}
75+
```
76+
77+
```go
78+
// Example code showing how the generated code would look like
79+
80+
// In your db/models.go file (generated by sqlc)
81+
package db
82+
83+
import (
84+
"context"
85+
"database/sql"
86+
)
87+
88+
// Original sqlc query
89+
const createUser = `
90+
INSERT INTO users (id, name, email)
91+
VALUES ($1, $2, $3)
92+
`
93+
94+
// Generated by sqlc
95+
type CreateUserParams struct {
96+
ID int64
97+
Name string
98+
Email string
99+
}
100+
101+
type Queries struct {
102+
db *sql.DB
103+
}
104+
105+
func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) error {
106+
// Implementation generated by sqlc
107+
return nil
108+
}
109+
```
110+
111+
```go
112+
// In your application code
113+
func ExampleUsage() {
114+
// Create a database connection
115+
db, err := sql.Open("postgres", "postgresql://user:password@localhost:5432/mydb?sslmode=disable")
116+
if err != nil {
117+
panic(err)
118+
}
119+
defer db.Close()
120+
121+
// Create a queries object
122+
queries := &Queries{db: db}
123+
124+
// Create a context
125+
ctx := context.Background()
126+
127+
// Prepare data for bulk insert
128+
users := BulkCreateUserParams{
129+
{ID: 1, Name: "User 1", Email: "[email protected]"},
130+
{ID: 2, Name: "User 2", Email: "[email protected]"},
131+
{ID: 3, Name: "User 3", Email: "[email protected]"},
132+
}
133+
134+
// Execute bulk insert
135+
err = queries.BulkCreateUser(ctx, users)
136+
if err != nil {
137+
panic(err)
138+
}
139+
}
140+
```
141+
142+
## License
143+
144+
[MIT License](LICENSE)

bulkinsert.go

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package main
2+
3+
import (
4+
"strings"
5+
"unicode"
6+
7+
"github.com/sqlc-dev/plugin-sdk-go/plugin"
8+
)
9+
10+
type BulkInsert struct {
11+
// QueryName is the name of the SQL query, corresponding to the Go function name generated by sqlc
12+
QueryName string
13+
// Go field names corresponding to the INSERT column order
14+
ParamFieldNames []string
15+
// Original SQL query string (for placeholder generation)
16+
OriginalQuery string
17+
}
18+
19+
type BulkInserts []BulkInsert
20+
21+
func buildBulkInsert(
22+
req *plugin.GenerateRequest, _ *Options,
23+
) BulkInserts {
24+
slices := make([]BulkInsert, 0)
25+
for _, query := range req.GetQueries() {
26+
// For queries that are INSERT statements and of the type where sqlc generates a parameter structure
27+
// If query.GetCmd() is an empty string, it may be different from something like a simple :exec
28+
// Assumes parameters are defined in the INSERT statement
29+
if !strings.HasPrefix(strings.ToUpper(query.GetText()), "INSERT INTO") || len(query.GetParams()) == 0 {
30+
continue
31+
}
32+
33+
paramFieldNames := make([]string, 0, len(query.GetParams()))
34+
for _, p := range query.GetParams() {
35+
if p.GetColumn() == nil {
36+
continue
37+
}
38+
nameFromPlugin := p.GetColumn().GetName()
39+
goFieldName := snakeToPascalCase(nameFromPlugin)
40+
paramFieldNames = append(paramFieldNames, goFieldName)
41+
}
42+
43+
// INSERT statements that fail to get any parameters are skipped (usually len(query.GetParams()) == 0)
44+
if len(paramFieldNames) == 0 {
45+
continue
46+
}
47+
48+
slices = append(slices, BulkInsert{
49+
QueryName: query.GetName(),
50+
ParamFieldNames: paramFieldNames,
51+
OriginalQuery: query.GetText(),
52+
})
53+
}
54+
return slices
55+
}
56+
57+
// snakeToPascalCase converts a snake case string to a Pascal case.
58+
// certain words such as "id" are treated as uppercase, as in "ID".
59+
// Example: "user_id" -> "UserID", "email" -> "Email"
60+
func snakeToPascalCase(snakeStr string) string {
61+
if snakeStr == "" {
62+
return ""
63+
}
64+
65+
// map a specific word to the expected capitalization.
66+
// This map can be extended as needed.
67+
// (Keys are written in lowercase and are also converted to lowercase for comparison and matching)
68+
commonInitialisms := map[string]string{
69+
"id": "ID",
70+
"url": "URL",
71+
"api": "API",
72+
"json": "JSON",
73+
"xml": "XML",
74+
"html": "HTML",
75+
}
76+
77+
var result strings.Builder
78+
parts := strings.Split(snakeStr, "_")
79+
80+
for _, part := range parts {
81+
if part == "" {
82+
continue
83+
}
84+
85+
// Check if the whole part matches the lower case key of the common acronym
86+
// (e.g., match as "id" even if part is "id", "Id", "iD", or "ID")
87+
if initialism, ok := commonInitialisms[strings.ToLower(part)]; ok {
88+
result.WriteString(initialism)
89+
} else {
90+
runes := []rune(part)
91+
if len(runes) > 0 {
92+
result.WriteRune(unicode.ToUpper(runes[0]))
93+
if len(runes) > 1 {
94+
result.WriteString(string(runes[1:]))
95+
}
96+
}
97+
}
98+
}
99+
return result.String()
100+
}

go.mod

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
module github.com/tomtwinkle/sqlc-plugin-bulk-go
2+
3+
go 1.24
4+
5+
require github.com/sqlc-dev/plugin-sdk-go v1.23.0
6+
7+
require (
8+
golang.org/x/net v0.40.0 // indirect
9+
golang.org/x/sys v0.33.0 // indirect
10+
golang.org/x/text v0.25.0 // indirect
11+
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a // indirect
12+
google.golang.org/grpc v1.72.2 // indirect
13+
google.golang.org/protobuf v1.36.6 // indirect
14+
)

go.sum

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
2+
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
3+
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
4+
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
5+
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
6+
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
7+
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
8+
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
9+
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
10+
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
11+
github.com/sqlc-dev/plugin-sdk-go v1.23.0 h1:iSeJhnXPlbDXlbzUEebw/DxsGzE9rdDJArl8Hvt0RMM=
12+
github.com/sqlc-dev/plugin-sdk-go v1.23.0/go.mod h1:I1r4THOfyETD+LI2gogN2LX8wCjwUZrgy/NU4In3llA=
13+
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
14+
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
15+
go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
16+
go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
17+
go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ=
18+
go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
19+
go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
20+
go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
21+
go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk=
22+
go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
23+
go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
24+
go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
25+
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
26+
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
27+
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
28+
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
29+
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
30+
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
31+
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a h1:v2PbRU4K3llS09c7zodFpNePeamkAwG3mPrAery9VeE=
32+
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
33+
google.golang.org/grpc v1.72.2 h1:TdbGzwb82ty4OusHWepvFWGLgIbNo1/SUynEN0ssqv8=
34+
google.golang.org/grpc v1.72.2/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
35+
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
36+
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=

main.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/sqlc-dev/plugin-sdk-go/codegen"
8+
"github.com/sqlc-dev/plugin-sdk-go/plugin"
9+
)
10+
11+
const generateFileName = "bulk.sql.go"
12+
13+
func main() {
14+
codegen.Run(Generate)
15+
}
16+
17+
func Generate(ctx context.Context, req *plugin.GenerateRequest) (*plugin.GenerateResponse, error) {
18+
opts, err := ParseOptions(req)
19+
if err != nil {
20+
return nil, err
21+
}
22+
if err := ValidateOptions(opts); err != nil {
23+
return nil, err
24+
}
25+
26+
bulkInserts := buildBulkInsert(req, opts)
27+
28+
if len(bulkInserts) == 0 {
29+
// Returns an empty response if nothing is generated
30+
return &plugin.GenerateResponse{}, nil
31+
}
32+
33+
// Return the response with the generated code
34+
return generate(ctx, req, opts, bulkInserts)
35+
}
36+
37+
func generate(
38+
ctx context.Context, req *plugin.GenerateRequest, opts *Options, structs BulkInserts,
39+
) (*plugin.GenerateResponse, error) {
40+
tmpl := struct {
41+
Package string
42+
SqlcVersion string
43+
BulkInsert []BulkInsert
44+
}{
45+
Package: opts.Package,
46+
SqlcVersion: req.GetSqlcVersion(),
47+
BulkInsert: structs,
48+
}
49+
50+
code, err := executeTemplate(ctx, "bulkInsertFile", tmpl)
51+
if err != nil {
52+
return nil, fmt.Errorf("failed to execute template: %w", err)
53+
}
54+
return &plugin.GenerateResponse{
55+
Files: []*plugin.File{
56+
{
57+
Name: generateFileName,
58+
Contents: code,
59+
},
60+
},
61+
}, nil
62+
}

options.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package main
2+
3+
import (
4+
"encoding/json"
5+
"errors"
6+
7+
"github.com/sqlc-dev/plugin-sdk-go/plugin"
8+
)
9+
10+
type Options struct {
11+
Package string `json:"package"`
12+
}
13+
14+
func ParseOptions(req *plugin.GenerateRequest) (*Options, error) {
15+
var options Options
16+
if err := json.Unmarshal(req.GetPluginOptions(), &options); err != nil {
17+
return nil, err
18+
}
19+
return &options, nil
20+
}
21+
22+
func ValidateOptions(opts *Options) error {
23+
if opts.Package == "" {
24+
return errors.New(`options: "package" is required`)
25+
}
26+
return nil
27+
}

0 commit comments

Comments
 (0)