Skip to content

Commit 97a6136

Browse files
authored
Add generics and All functions (#10)
1 parent dd2e9ee commit 97a6136

31 files changed

Lines changed: 1756 additions & 268 deletions

.github/workflows/ci.yaml

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,19 @@ jobs:
1313
runs-on: ubuntu-latest
1414
strategy:
1515
matrix:
16-
go-version: [1.22.12, 1.23.7, 1.24.1]
16+
go-version: [1.23.8, 1.24.2]
17+
services:
18+
postgres:
19+
image: postgres:17.4
20+
ports:
21+
- 5444:5432
22+
options: >-
23+
--health-cmd pg_isready
24+
--health-interval 10s
25+
--health-timeout 5s
26+
--health-retries 5
27+
env:
28+
POSTGRES_PASSWORD: password
1729
steps:
1830
- uses: actions/checkout@v3
1931
- name: Set up Go
@@ -34,6 +46,7 @@ jobs:
3446
mkdir -p /tmp/test-reports
3547
# gotestsum hash is version version v1.12.1
3648
go run gotest.tools/gotestsum@3f7ff0ec4aeb6f95f5d67c998b71f272aa8a8b41 --junitfile /tmp/test-reports/unit-tests.xml
49+
make test-examples
3750
- uses: actions/upload-artifact@v4
3851
name: Upload test results
3952
with:
@@ -46,14 +59,14 @@ jobs:
4659
chart: true
4760
amend: true
4861
if: |
49-
matrix.go-version == '1.23.7'
62+
matrix.go-version == '1.24.2'
5063
continue-on-error: true
5164

5265
test-race:
5366
runs-on: ubuntu-latest
5467
strategy:
5568
matrix:
56-
go-version: [1.22.12, 1.23.7, 1.24.1]
69+
go-version: [1.23.8, 1.24.2]
5770
steps:
5871
- uses: actions/checkout@v3
5972
- name: Set up Go
@@ -76,7 +89,7 @@ jobs:
7689
runs-on: ubuntu-latest
7790
strategy:
7891
matrix:
79-
go-version: [1.22.12, 1.23.7, 1.24.1]
92+
go-version: [1.23.8, 1.24.2]
8093
steps:
8194
- uses: actions/checkout@v3
8295
- name: Set up Go
@@ -103,7 +116,7 @@ jobs:
103116
runs-on: ubuntu-latest
104117
strategy:
105118
matrix:
106-
go-version: [1.22.12, 1.23.7, 1.24.1]
119+
go-version: [1.23.8, 1.24.2]
107120
steps:
108121
- uses: actions/checkout@v3
109122
- name: Set up Go

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
examples/**/main
2+
13
# Compiled Object files, Static and Dynamic libs (Shared Objects)
24
*.o
35
*.a

Makefile

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,11 @@ test-race:
2828
update-dependencies:
2929
go get -u -t -v ./...
3030
go mod tidy
31+
32+
test-examples:
33+
for dir in $$(find examples -type d); do \
34+
if [ -f $$dir/main.go ]; then \
35+
echo "Building and running $$dir/main.go"; \
36+
go build -o $$dir/main $$dir/main.go && $$dir/main; \
37+
fi \
38+
done

README.md

Lines changed: 44 additions & 136 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,27 @@ Major additional concepts are:
1919
* Marshal rows into structs (with embedded struct support), maps, and slices
2020
* Named parameter support including prepared statements
2121
* `Get` and `Select` to go quickly from query to struct/slice
22+
* Bulk insert SQL builder
23+
* Generic functions `One` and `List`
24+
* Range function `AllRows`, or for prepared statements use `All`
2225

2326
In addition to the [godoc API documentation](http://godoc.org/github.com/jmoiron/sqlx),
2427
there is also some [user documentation](http://jmoiron.github.io/sqlx/) that
2528
explains how to use `database/sql` along with sqlx.
2629

2730
## Recent Changes
2831

32+
* Set Go version to 1.23.
33+
34+
* Added `AllRows` package function and `All` on statement types to
35+
range over rows and automatically close the result set.
36+
37+
* Added functions `One` and `List` for querying with
38+
generics. Modifications to types Stmt and NamedStmt to support
39+
generics may cause some small compatibility changes that should be
40+
quick to resolve with a search and replace. Both are now type
41+
aliases to attempt to minimize the impact of this change.
42+
2943
* fix for sql in-list parsing that will properly ignore question marks
3044
in comments and strings. This frequently caused a confusing error
3145
("number of bindVars exceeds arguments").
@@ -73,170 +87,64 @@ new changes. Compatibility beyond that is not guaranteed.
7387
Versioning is done with Go modules. Breaking changes (eg. removing deprecated API)
7488
will get major version number bumps.
7589

76-
## install
90+
## Install
7791

7892
go get github.com/vinovest/sqlx
7993

80-
## issues
81-
82-
Row headers can be ambiguous (`SELECT 1 AS a, 2 AS a`), and the result of
83-
`Columns()` does not fully qualify column names in queries like:
84-
85-
```sql
86-
SELECT a.id, a.name, b.id, b.name FROM foos AS a JOIN foos AS b ON a.parent = b.id;
87-
```
88-
89-
making a struct or map destination ambiguous. Use `AS` in your queries
90-
to give columns distinct names, `rows.Scan` to scan them manually, or
91-
`SliceScan` to get a slice of results.
94+
## Usage
9295

93-
## usage
96+
There are several standalone examples in the examples directory that demonstrate various features of sqlx.
9497

95-
Below is an example which shows some common use cases for sqlx. Check
96-
[sqlx_test.go](https://github.com/vinovest/sqlx/blob/master/sqlx_test.go) for more
97-
usage.
98+
* [Generics](./examples/generics/main.go) - query with generics
99+
* [Bulk inserts](./examples/bulk/main.go) - efficient bulk inserts
100+
* [Named queries](./examples/named/main.go) - named parameter queries
101+
* [SQL in-lists](./examples/inlist/main.go) - using `In` to generate SQL `IN` clauses
102+
* [Prepared statements](./examples/preparedstatements/main.go) - prepared statements offer better performance
98103

104+
The simplified form of querying using sqlx looks like the below. Given a struct `Person`, querying all of the rows
105+
in the `person` table is as simple as:
99106

100107
```go
101108
package main
102109

103110
import (
104-
"database/sql"
105111
"fmt"
106-
"log"
107112

108113
_ "github.com/lib/pq"
109114
"github.com/vinovest/sqlx"
110115
)
111116

112-
var schema = `
113-
CREATE TABLE person (
114-
first_name text,
115-
last_name text,
116-
email text
117-
);
118-
119-
CREATE TABLE place (
120-
country text,
121-
city text NULL,
122-
telcode integer
123-
)`
124-
125117
type Person struct {
126118
FirstName string `db:"first_name"`
127119
LastName string `db:"last_name"`
128120
Email string
129121
}
130122

131-
type Place struct {
132-
Country string
133-
City sql.NullString
134-
TelCode int
135-
}
136-
137123
func main() {
138-
// this Pings the database trying to connect
139-
// use sqlx.Open() for sql.Open() semantics
140-
db, err := sqlx.Connect("postgres", "user=foo dbname=bar sslmode=disable")
141-
if err != nil {
142-
log.Fatalln(err)
143-
}
124+
db, _ := sqlx.Connect("postgres", "user=foo dbname=bar sslmode=disable")
125+
people, _ := sqlx.List[Person](db, "SELECT * FROM person")
126+
fmt.Printf("People %#v\n", people)
144127

145-
// exec the schema or fail; multi-statement Exec behavior varies between
146-
// database drivers; pq will exec them all, sqlite3 won't, ymmv
147-
db.MustExec(schema)
148-
149-
tx := db.MustBegin()
150-
tx.MustExec("INSERT INTO person (first_name, last_name, email) VALUES ($1, $2, $3)", "Jason", "Moiron", "jmoiron@jmoiron.net")
151-
tx.MustExec("INSERT INTO person (first_name, last_name, email) VALUES ($1, $2, $3)", "John", "Doe", "johndoeDNE@gmail.net")
152-
tx.MustExec("INSERT INTO place (country, city, telcode) VALUES ($1, $2, $3)", "United States", "New York", "1")
153-
tx.MustExec("INSERT INTO place (country, telcode) VALUES ($1, $2)", "Hong Kong", "852")
154-
tx.MustExec("INSERT INTO place (country, telcode) VALUES ($1, $2)", "Singapore", "65")
155-
// Named queries can use structs, so if you have an existing struct (i.e. person := &Person{}) that you have populated, you can pass it in as &person
156-
tx.NamedExec("INSERT INTO person (first_name, last_name, email) VALUES (:first_name, :last_name, :email)", &Person{"Jane", "Citizen", "jane.citzen@example.com"})
157-
tx.Commit()
158-
159-
// Query the database, storing results in a []Person (wrapped in []interface{})
160-
people := []Person{}
161-
db.Select(&people, "SELECT * FROM person ORDER BY first_name ASC")
162-
jason, john := people[0], people[1]
163-
164-
fmt.Printf("%#v\n%#v", jason, john)
165-
// Person{FirstName:"Jason", LastName:"Moiron", Email:"jmoiron@jmoiron.net"}
166-
// Person{FirstName:"John", LastName:"Doe", Email:"johndoeDNE@gmail.net"}
167-
168-
// You can also get a single result, a la QueryRow
169-
jason = Person{}
170-
err = db.Get(&jason, "SELECT * FROM person WHERE first_name=$1", "Jason")
171-
fmt.Printf("%#v\n", jason)
172-
// Person{FirstName:"Jason", LastName:"Moiron", Email:"jmoiron@jmoiron.net"}
173-
174-
// if you have null fields and use SELECT *, you must use sql.Null* in your struct
175-
places := []Place{}
176-
err = db.Select(&places, "SELECT * FROM place ORDER BY telcode ASC")
177-
if err != nil {
178-
fmt.Println(err)
179-
return
180-
}
181-
usa, singsing, honkers := places[0], places[1], places[2]
182-
183-
fmt.Printf("%#v\n%#v\n%#v\n", usa, singsing, honkers)
184-
// Place{Country:"United States", City:sql.NullString{String:"New York", Valid:true}, TelCode:1}
185-
// Place{Country:"Singapore", City:sql.NullString{String:"", Valid:false}, TelCode:65}
186-
// Place{Country:"Hong Kong", City:sql.NullString{String:"", Valid:false}, TelCode:852}
187-
188-
// Loop through rows using only one struct
189-
place := Place{}
190-
rows, err := db.Queryx("SELECT * FROM place")
191-
for rows.Next() {
192-
err := rows.StructScan(&place)
128+
// alternatively, range over rows
129+
rows, _ := db.Queryx("select * from person")
130+
for person, err := range sqlx.AllRows[Person](rows) {
193131
if err != nil {
194-
log.Fatalln(err)
195-
}
196-
fmt.Printf("%#v\n", place)
197-
}
198-
// Place{Country:"United States", City:sql.NullString{String:"New York", Valid:true}, TelCode:1}
199-
// Place{Country:"Hong Kong", City:sql.NullString{String:"", Valid:false}, TelCode:852}
200-
// Place{Country:"Singapore", City:sql.NullString{String:"", Valid:false}, TelCode:65}
201-
202-
// Named queries, using `:name` as the bindvar. Automatic bindvar support
203-
// which takes into account the dbtype based on the driverName on sqlx.Open/Connect
204-
_, err = db.NamedExec(`INSERT INTO person (first_name,last_name,email) VALUES (:first,:last,:email)`,
205-
map[string]interface{}{
206-
"first": "Bin",
207-
"last": "Smuth",
208-
"email": "bensmith@allblacks.nz",
209-
})
210-
211-
// Selects Mr. Smith from the database
212-
rows, err = db.NamedQuery(`SELECT * FROM person WHERE first_name=:fn`, map[string]interface{}{"fn": "Bin"})
213-
214-
// Named queries can also use structs. Their bind names follow the same rules
215-
// as the name -> db mapping, so struct fields are lowercased and the `db` tag
216-
// is taken into consideration.
217-
rows, err = db.NamedQuery(`SELECT * FROM person WHERE first_name=:first_name`, jason)
218-
219-
220-
// batch insert
221-
222-
// batch insert with structs
223-
personStructs := []Person{
224-
{FirstName: "Ardie", LastName: "Savea", Email: "asavea@ab.co.nz"},
225-
{FirstName: "Sonny Bill", LastName: "Williams", Email: "sbw@ab.co.nz"},
226-
{FirstName: "Ngani", LastName: "Laumape", Email: "nlaumape@ab.co.nz"},
132+
panic(err)
133+
}
134+
fmt.Printf("%#v\n", person)
227135
}
136+
}
137+
```
228138

229-
_, err = db.NamedExec(`INSERT INTO person (first_name, last_name, email)
230-
VALUES (:first_name, :last_name, :email)`, personStructs)
139+
## Issues
231140

232-
// batch insert with maps
233-
personMaps := []map[string]interface{}{
234-
{"first_name": "Ardie", "last_name": "Savea", "email": "asavea@ab.co.nz"},
235-
{"first_name": "Sonny Bill", "last_name": "Williams", "email": "sbw@ab.co.nz"},
236-
{"first_name": "Ngani", "last_name": "Laumape", "email": "nlaumape@ab.co.nz"},
237-
}
141+
Row headers can be ambiguous (`SELECT 1 AS a, 2 AS a`), and the result of
142+
`Columns()` does not fully qualify column names in queries like:
238143

239-
_, err = db.NamedExec(`INSERT INTO person (first_name, last_name, email)
240-
VALUES (:first_name, :last_name, :email)`, personMaps)
241-
}
144+
```sql
145+
SELECT a.id, a.name, b.id, b.name FROM foos AS a JOIN foos AS b ON a.parent = b.id;
242146
```
147+
148+
making a struct or map destination ambiguous. Use `AS` in your queries
149+
to give columns distinct names, `rows.Scan` to scan them manually, or
150+
`SliceScan` to get a slice of results.

bind.go

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package sqlx
22

33
import (
4-
"bytes"
54
"database/sql/driver"
65
"errors"
76
"reflect"
@@ -106,31 +105,6 @@ func Rebind(bindType int, query string) string {
106105
return string(rqb)
107106
}
108107

109-
// Experimental implementation of Rebind which uses a bytes.Buffer. The code is
110-
// much simpler and should be more resistant to odd unicode, but it is twice as
111-
// slow. Kept here for benchmarking purposes and to possibly replace Rebind if
112-
// problems arise with its somewhat naive handling of unicode.
113-
func rebindBuff(bindType int, query string) string {
114-
if bindType != DOLLAR {
115-
return query
116-
}
117-
118-
b := make([]byte, 0, len(query))
119-
rqb := bytes.NewBuffer(b)
120-
j := 1
121-
for _, r := range query {
122-
if r == '?' {
123-
rqb.WriteRune('$')
124-
rqb.WriteString(strconv.Itoa(j))
125-
j++
126-
} else {
127-
rqb.WriteRune(r)
128-
}
129-
}
130-
131-
return rqb.String()
132-
}
133-
134108
func asSliceForIn(i interface{}) (v reflect.Value, ok bool) {
135109
if i == nil {
136110
return reflect.Value{}, false

examples/bulk/go.mod

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
module bulk
2+
3+
go 1.23
4+
5+
require (
6+
github.com/lib/pq v1.10.9
7+
github.com/vinovest/sqlx v1.5.2
8+
)
9+
10+
require github.com/muir/sqltoken v0.1.0 // indirect
11+
12+
replace github.com/vinovest/sqlx => ../../

examples/bulk/go.sum

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
2+
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
3+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
4+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
5+
github.com/go-sql-driver/mysql v1.9.0 h1:Y0zIbQXhQKmQgTp44Y1dp3wTXcn804QoTptLZT1vtvo=
6+
github.com/go-sql-driver/mysql v1.9.0/go.mod h1:pDetrLJeA3oMujJuvXc8RJoasr589B6A9fwzD3QMrqw=
7+
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
8+
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
9+
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
10+
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
11+
github.com/muir/sqltoken v0.1.0 h1:edosEGsOClOZNfgGQNQSgxR9O6LiVefm2rDRqp2InuI=
12+
github.com/muir/sqltoken v0.1.0/go.mod h1:lgOIORnKekMsuc/ZwdPOfwz/PtWLPCke43cEbT3uDuY=
13+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
14+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
15+
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
16+
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
17+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
18+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

0 commit comments

Comments
 (0)