@@ -3,17 +3,16 @@ Generate a SQL INSERT statement with bind parameters directly from a Go struct.
33[ ![ Go Reference] ( https://pkg.go.dev/badge/github.com/zachvictor/sqlinsert/v2.svg )] ( https://pkg.go.dev/github.com/zachvictor/sqlinsert/v2 )
44
55## Features
6- * Define column names in struct tags.
7- * Struct values become bind arguments.
8- * Single-row and multi-row inserts, from a struct, struct pointer, or slice of either.
9- * Use the SQL string and args slice piecemeal, or use ` Exec() ` /` ExecContext() ` with a
10- ` sql.DB ` , ` sql.Tx ` , or ` sql.Conn ` to execute the INSERT directly.
11- * Works seamlessly with the Go standard library [ database/sql] ( https://pkg.go.dev/database/sql ) package.
12- * Bind-parameter token types for MySQL/MariaDB/SQLite/SingleStore (` ? ` ), PostgreSQL/CockroachDB (` $1 ` ),
13- SQL Server (` @p1 ` ), and named parameters (` @name ` , ` :name ` ).
14- * Per-` Insert ` configuration — safe for concurrent use, including different databases in one process.
15- * Errors instead of panics for invalid input.
16- * Requires Go 1.18+.
6+ * Column names come from struct tags; struct values become bind args.
7+ * Single-row and multi-row inserts from a struct, struct pointer, or slice of either.
8+ * Take the SQL string and args and use them yourself, or run the INSERT directly with
9+ ` Exec() ` /` ExecContext() ` on a ` sql.DB ` , ` sql.Tx ` , or ` sql.Conn ` .
10+ * Bind-parameter formats for MySQL/MariaDB/SQLite/SingleStore (` ? ` ),
11+ PostgreSQL/CockroachDB (` $1 ` ), SQL Server (` @p1 ` ), and named parameters (` @name ` , ` :name ` ).
12+ * Configuration is per ` Insert ` value — safe for concurrent use, even with different
13+ databases in one process.
14+ * Errors instead of panics, all matchable with ` errors.Is ` .
15+ * Zero dependencies. Requires Go 1.18+ (test suite passes on every release from 1.18 through 1.27).
1716
1817## Install
1918```
@@ -73,142 +72,119 @@ result, err := ins.Exec(db)
7372```
7473
7574` Exec ` returns the ` sql.Result ` , so ` LastInsertId() ` and ` RowsAffected() ` work as usual.
75+ A slice makes it a multi-row insert:
7676
77- ### Multi-row insert
7877``` go
7978ins := sqlinsert.Insert {Table: ` candy` , Data : []CandyInsert {rec1, rec2, rec3}}
8079result , err := ins.Exec (db)
8180```
8281
83- ### I want to see the SQL
84- Question-mark VALUES-tokens are the default:
82+ ### Transactions
83+ ` *sql.Tx ` satisfies the same interface as ` *sql.DB ` , so inserts join a transaction
84+ by passing the transaction:
85+
8586``` go
86- query , err := ins.SQL ()
87- // INSERT INTO candy (id,candy_name,form_factor,description,manufacturer,weight_grams,ts) VALUES (?,?,?,?,?,?,?)
87+ tx , err := db.BeginTx (ctx, nil )
88+ if err != nil { ... }
89+ defer tx.Rollback ()
90+
91+ if _, err = (sqlinsert.Insert {Table: ` candy` , Data : candies}).ExecContext (ctx, tx); err != nil {
92+ return err
93+ }
94+ if _, err = (sqlinsert.Insert {Table: ` wrappers` , Data : wrappers}).ExecContext (ctx, tx); err != nil {
95+ return err
96+ }
97+ return tx.Commit ()
8898```
8999
90- Set the token type per Insert. For example, for PostgreSQL:
100+ ### Inspecting the SQL and args
101+ Question-mark tokens are the default; set ` TokenType ` per Insert for other dialects:
102+
91103``` go
92104ins := sqlinsert.Insert {
93105 Table : ` candy` ,
94106 Data : []CandyInsert {rec1, rec2},
95- TokenType : sqlinsert.OrdinalNumberTokenType ,
107+ TokenType : sqlinsert.OrdinalNumberTokenType , // PostgreSQL
96108}
97109query , err := ins.SQL ()
98110// INSERT INTO candy (id,candy_name,...,ts) VALUES ($1,$2,...,$7),($8,$9,...,$14)
99- ```
100-
101- ### I want to see the args
102- ``` go
103- args , err := ins.Args ()
104- // []any{"c0600afd-78a7-4a1a-87c5-1bc48cafd14e", "Gougat", "Package", ...}
105- ```
106-
107- ### I want to use database/sql apparatus
108- ``` go
109- query , err := ins.SQL ()
110111args , err := ins.Args ()
112+ // []any{"c0600afd-...", "Gougat", ..., "Nussnicht", ...}
111113result , err := db.Exec (query, args...)
112114```
113115
116+ Every piece is available on its own: ` Columns() ` for the column list, ` Params() ` for the
117+ token rows, ` Args() ` for the bind args. That is also the route to anything ` Exec ` doesn't
118+ do — SQL functions in the VALUES clause, ` RETURNING ` via ` QueryRow ` , or preparing a
119+ statement once and executing it many times with your own ` sql.Stmt ` .
120+
114121## Token types
115122
116- | TokenType | VALUES tokens | Multi-row | For |
117- | --------------------------| --------------------| -----------| -------------------------------------- |
118- | ` QuestionMarkTokenType ` | ` ?,?, ... ? ` | yes | MySQL, MariaDB, SQLite, SingleStore |
119- | ` OrdinalNumberTokenType ` | ` $1,$2, ... $n ` | yes | PostgreSQL, CockroachDB |
120- | ` AtOrdinalTokenType ` | ` @p1,@p2, ... @pn ` | yes | SQL Server (T-SQL) |
121- | ` AtColumnNameTokenType ` | ` @foo,@bar ` | no | named parameters |
122- | ` ColonTokenType ` | ` :foo,:bar ` | no | Oracle |
123+ | TokenType | VALUES tokens | Multi-row | For |
124+ | --------------------------| --------------------| -----------| -------------------------------------|
125+ | ` QuestionMarkTokenType ` | ` ?,?, ... ? ` | yes | MySQL, MariaDB, SQLite, SingleStore |
126+ | ` OrdinalNumberTokenType ` | ` $1,$2, ... $n ` | yes | PostgreSQL, CockroachDB |
127+ | ` AtOrdinalTokenType ` | ` @p1,@p2, ... @pn ` | yes | SQL Server (T-SQL) |
128+ | ` AtColumnNameTokenType ` | ` @foo,@bar ` | no | named parameters |
129+ | ` ColonTokenType ` | ` :foo,:bar ` | no | Oracle |
123130
124131` QuestionMarkTokenType ` is the zero value, so it applies when ` TokenType ` is unset.
125- Ordinal tokens number continuously across the rows of a multi-row insert.
126- The named-parameter token types repeat the same name in every row, so they are restricted
127- to single-row inserts; a multi-row ` Insert ` with one of them returns an error.
132+ Ordinal tokens number continuously across the rows of a multi-row insert. The
133+ named-parameter token types repeat the same name in every row, so a multi-row ` Insert `
134+ with one of them returns an error.
128135
129- ## Field mapping rules
136+ ## Field mapping
130137
131138* Exported fields with a ` col ` tag become columns and bind args, in field order.
132- * Unexported fields and untagged fields are skipped. ` col:"-" ` skips explicitly.
133- * A ` col ` tag on an unexported field is an error (the tag states an intent the
134- package cannot honor).
135- * Untagged embedded structs are flattened into their parent; a * tagged* embedded
136- struct is treated as a single column.
137- * Use ` StructTag ` to read a different tag key:
138- ``` go
139- ins := sqlinsert.Insert {Table: ` t` , Data : rec, StructTag : ` db` }
140- ```
141-
142- ** Security note:** the table name and tag-derived column names are interpolated into the
143- SQL verbatim (identifiers cannot be bind parameters). They must come from trusted code —
144- never build them from user input. Field * values* are always passed as bind args, never
145- interpolated.
146-
147- ** Large batches:** drivers cap bind parameters per statement (PostgreSQL 65,535;
148- SQL Server 2,100). Chunk your slice accordingly.
149-
150- ## This is a helper
151-
152- ` sqlinsert ` is fundamentally a helper for [ database/sql] ( https://pkg.go.dev/database/sql ) .
153- It simply maps struct fields to INSERT elements:
154- * struct tags ⇒ SQL columns and tokens ` string ` ⇒ [ Exec] ( https://pkg.go.dev/database/sql#DB.Exec ) ` query string `
155- * struct values ⇒ bind args ` []any ` ⇒ [ Exec] ( https://pkg.go.dev/database/sql#DB.Exec ) ` args ...any `
156-
157- ### Use only what you need
158- All aspects of SQL INSERT remain in your control:
159- * _ I just want the column names for my SQL._ ` Insert.Columns() `
160- * _ I just want the parameter-tokens for my SQL._ ` Insert.Params() `
161- * _ I just want the bind args for my Exec() call._ ` Insert.Args() `
162- * _ I just want to run the INSERT._ ` Insert.Exec() ` / ` Insert.ExecContext() `
163-
164- ## This is not an ORM
165-
166- ### Hide nothing
167- Unlike ORMs, ` sqlinsert ` does ** not** create an abstraction layer over SQL relations, nor does it
168- restructure SQL functions. The aim is to keep it simple and hide nothing.
169-
170- ### Let SQL be great
171- SQL’s INSERT is already as close to functionally pure as possible. Why would we change that?
172- Its simplicity and directness are its power.
173-
174- ### Let database/sql be great
175- Some database vendors support collection types for bind parameters, some don’t.
176- Some database drivers support slices for bind args, some don’t.
177- The complexity of this reality is met admirably by [ database/sql] ( https://pkg.go.dev/database/sql )
178- with the _ necessary_ amount of flexibility and abstraction:
179- _ flexibility_ in open-ended SQL;
180- _ abstraction_ in the variadic ` args ...any ` for bind args.
181-
182- ### Let Go be great
183- Go structs support ordered fields, strong types, and field metadata via [ tags] ( https://go.dev/ref/spec#Tag )
184- and [ reflection] ( https://pkg.go.dev/reflect#StructTag ) . In these respects, the Go struct can encapsulate
185- the information of a SQL INSERT-row perfectly and completely. ` sqlinsert ` uses these features of Go
186- structs to make your SQL INSERT experience more Go-idiomatic.
187-
188- ## Limitations of Exec/ExecContext
189- ` Insert.Exec ` and ` Insert.ExecContext ` are for simple binding _ only._
190- In the spirit of “hide nothing,” they do _ not_ support SQL operations in the ` VALUES ` clause.
191- If you require, say—
192- ``` sql
193- INSERT INTO foo (bar, baz, oof) VALUES (some_function(?), REPLACE(?, ' oink' , ' moo' ), ? + ?);
139+ * Unexported fields and untagged fields are skipped; ` col:"-" ` skips explicitly.
140+ * A ` col ` tag on an unexported field is an error — the tag states an intent the
141+ package cannot honor.
142+ * Untagged embedded structs are flattened into their parent; a tagged embedded
143+ struct is a single column.
144+ * Read a different tag key with ` StructTag ` :
145+ ` sqlinsert.Insert{Table: "t", Data: rec, StructTag: "db"} `
146+
147+ ## Errors
148+ Everything sqlinsert rejects — nil or non-struct data, empty slices, bad tags, a
149+ named-parameter token type on a multi-row insert — wraps ` ErrInvalidInsert ` :
150+
151+ ``` go
152+ if _ , err := ins.Exec (db); errors.Is (err, sqlinsert.ErrInvalidInsert ) {
153+ // a bug on the caller's side, not a database failure
154+ }
194155```
195- —then use the ` sqlinsert.Insert ` methods piecemeal: ` Insert.Columns ` to build the column list
196- and ` Insert.Args ` to marshal the args for your own ` Exec ` /` ExecContext ` /` QueryRow ` call
197- (which is also the route to ` RETURNING ` clauses).
156+
157+ Errors from the database pass through unwrapped.
158+
159+ ## Caveats
160+ * ** Identifiers are trusted input.** The table name and tag-derived column names are
161+ interpolated into the SQL verbatim (identifiers cannot be bind parameters). Never build
162+ them from user input. Field * values* are always bind args, never interpolated.
163+ * ** Batch limits.** Drivers cap bind parameters per statement (PostgreSQL 65,535;
164+ SQL Server 2,100). Chunk large slices accordingly.
165+
166+ ## Design
167+ ` sqlinsert ` is a helper for [ database/sql] ( https://pkg.go.dev/database/sql ) , not an ORM.
168+ It maps struct tags to the column list and struct values to the bind args — the two
169+ mechanical, error-prone parts of writing an INSERT — and hides nothing else. SQL's
170+ INSERT is already simple and direct; ` database/sql ` already absorbs the vagaries of
171+ drivers and bind parameters; Go structs already carry ordered, typed, tagged fields.
172+ This package just connects them.
198173
199174## Migrating from v1
200175
201- | v1 | v2 |
202- | -------------------------------------------| ---------------------------------------------------|
203- | ` import ".../sqlinsert" ` | ` import ".../sqlinsert/v2" ` |
204- | ` sqlinsert.UseTokenType = X ` (global) | ` Insert{TokenType: X} ` (per Insert) |
205- | ` sqlinsert.UseStructTag = "db" ` (global) | ` Insert{StructTag: "db"} ` (per Insert) |
206- | ` ins.Insert(db) ` → closed ` *sql.Stmt ` | ` ins.Exec(db) ` → ` sql.Result ` |
207- | ` ins.InsertContext(ctx, db) ` | ` ins.ExecContext(ctx, db) ` (works with ` sql.Conn ` )|
208- | ` ins.SQL() ` → ` string ` | ` ins.SQL() ` → ` (string, error) ` |
209- | ` Tokenize(...) ` , ` ColumnNameTokenType ` | removed (use ` Columns() ` ) |
210- | panics on bad input | errors on bad input |
176+ | v1 | v2 |
177+ | -------------------------------------------| ---------------------------------------------------- |
178+ | ` import ".../sqlinsert" ` | ` import ".../sqlinsert/v2" ` |
179+ | ` sqlinsert.UseTokenType = X ` (global) | ` Insert{TokenType: X} ` (per Insert) |
180+ | ` sqlinsert.UseStructTag = "db" ` (global) | ` Insert{StructTag: "db"} ` (per Insert) |
181+ | ` ins.Insert(db) ` → closed ` *sql.Stmt ` | ` ins.Exec(db) ` → ` sql.Result ` |
182+ | ` ins.InsertContext(ctx, db) ` | ` ins.ExecContext(ctx, db) ` (works with ` sql.Conn ` ) |
183+ | ` ins.SQL() ` → ` string ` | ` ins.SQL() ` → ` (string, error) ` |
184+ | ` Tokenize(...) ` , ` ColumnNameTokenType ` | removed (use ` Columns() ` ) |
185+ | panics on bad input | errors wrapping ` ErrInvalidInsert ` |
211186
212187Behavior fixes to be aware of: multi-row ordinal parameters now number continuously
213- (` ($1,$2),($3,$4) ` — v1 emitted invalid repeated ` ($1,$2),($1,$2) ` ), and multi-row named-token
214- inserts now return an error instead of silently generating driver-dependent SQL.
188+ (` ($1,$2),($3,$4) ` — v1 emitted invalid repeated ` ($1,$2),($1,$2) ` ), and multi-row
189+ named-token inserts now return an error instead of silently generating
190+ driver-dependent SQL.
0 commit comments