Skip to content

Commit 77154c2

Browse files
committed
docs: tighten README; add transactions, errors, and caveats sections
- Compresses the three-part design manifesto into one short Design paragraph and merges the duplicated helper/piecemeal sections. - New sections: transactions (via *sql.Tx satisfying Execer), error handling (ErrInvalidInsert + errors.Is), caveats (trusted identifiers, driver bind-parameter limits). - Notes zero dependencies and the verified version range: the test suite passes on every Go release from 1.18 through 1.27.
1 parent 13b2d82 commit 77154c2

1 file changed

Lines changed: 93 additions & 117 deletions

File tree

‎README.md‎

Lines changed: 93 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -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
7978
ins := sqlinsert.Insert{Table: `candy`, Data: []CandyInsert{rec1, rec2, rec3}}
8079
result, 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
92104
ins := sqlinsert.Insert{
93105
Table: `candy`,
94106
Data: []CandyInsert{rec1, rec2},
95-
TokenType: sqlinsert.OrdinalNumberTokenType,
107+
TokenType: sqlinsert.OrdinalNumberTokenType, // PostgreSQL
96108
}
97109
query, 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()
110111
args, err := ins.Args()
112+
// []any{"c0600afd-...", "Gougat", ..., "Nussnicht", ...}
111113
result, 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

212187
Behavior 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

Comments
 (0)