Skip to content

Commit 1f7d6a9

Browse files
authoredMay 24, 2024··
Merge pull request #19 from bool64/storage-of-example
Storage table example
2 parents ca130b3 + e641f48 commit 1f7d6a9

8 files changed

+293
-14
lines changed
 

‎.golangci.yml

+3
Original file line numberDiff line numberDiff line change
@@ -67,5 +67,8 @@ issues:
6767
- linters:
6868
- errcheck # Error checking omitted for brevity.
6969
- gosec
70+
- wsl
71+
- ineffassign
72+
- wastedassign
7073
path: "example_"
7174

‎README.md

+106
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ Field tags (`db` by default) act as a source of truth for column names to allow
2323
`Storage` is a high level service that provides query building, query executing and result fetching facilities
2424
as easy to use facades.
2525

26+
`StorageOf[V any]` typed query builder and scanner for specific table(s).
27+
2628
`Mapper` is a lower level tool that focuses on managing `squirrel` query builder with row structures.
2729

2830
`Referencer` helps to build complex statements by providing fully qualified and properly escaped names for
@@ -37,6 +39,10 @@ s, _ := sqluct.Open(
3739
"postgres://pqgotest:password@localhost/pqgotest?sslmode=disable",
3840
)
3941

42+
// Or if you already have an *sql.DB or *sqlx.DB instances, you can use them:
43+
// db, _ := sql.Open("postgres", "postgres://pqgotest:password@localhost/pqgotest?sslmode=disable")
44+
// s := sqluct.NewStorage(sqlx.NewDb(db, "postgres"))
45+
4046
ctx := context.TODO()
4147

4248
const tableName = "products"
@@ -162,6 +168,106 @@ fmt.Println(args)
162168
// [John]
163169
```
164170

171+
## Typed Storage
172+
173+
`sqluct.Table[RowType](storageInstance, tableName)` creates a type-safe storage accessor to a table with `RowType`.
174+
This accessor can help to retrieve or store data. Columns from multiple tables can be joined using field pointers.
175+
176+
Please check features overview in an example below.
177+
178+
```go
179+
var (
180+
st = sqluct.NewStorage(sqlx.NewDb(sql.OpenDB(dumpConnector{}), "postgres"))
181+
ctx = context.Background()
182+
)
183+
184+
st.IdentifierQuoter = sqluct.QuoteANSI
185+
186+
type User struct {
187+
ID int `db:"id"`
188+
RoleID int `db:"role_id"`
189+
Name string `db:"name"`
190+
}
191+
192+
// Users repository.
193+
ur := sqluct.Table[User](st, "users")
194+
195+
// Pointer to row, that can be used to reference columns via struct fields.
196+
_ = ur.R
197+
198+
// Single user record can be inserted, last insert id (if available) and error are returned.
199+
fmt.Println("Insert single user.")
200+
_, _ = ur.InsertRow(ctx, User{Name: "John Doe", ID: 123})
201+
202+
// Multiple user records can be inserted with sql.Result and error returned.
203+
fmt.Println("Insert two users.")
204+
_, _ = ur.InsertRows(ctx, []User{{Name: "Jane Doe", ID: 124}, {Name: "Richard Roe", ID: 125}})
205+
206+
// Update statement for a single user with condition.
207+
fmt.Println("Update a user with new name.")
208+
_, _ = ur.UpdateStmt(User{Name: "John Doe, Jr.", ID: 123}).Where(ur.Eq(&ur.R.ID, 123)).ExecContext(ctx)
209+
210+
// Delete statement for a condition.
211+
fmt.Println("Delete a user with id 123.")
212+
_, _ = ur.DeleteStmt().Where(ur.Eq(&ur.R.ID, 123)).ExecContext(ctx)
213+
214+
fmt.Println("Get single user with id = 123.")
215+
user, _ := ur.Get(ctx, ur.SelectStmt().Where(ur.Eq(&ur.R.ID, 123)))
216+
217+
// Squirrel expression can be formatted with %s reference(s) to column pointer.
218+
fmt.Println("Get multiple users with names starting with 'John '.")
219+
users, _ := ur.List(ctx, ur.SelectStmt().Where(ur.Fmt("%s LIKE ?", &ur.R.Name), "John %"))
220+
221+
// Squirrel expressions can be applied.
222+
fmt.Println("Get multiple users with id != 123.")
223+
users, _ = ur.List(ctx, ur.SelectStmt().Where(squirrel.NotEq(ur.Eq(&ur.R.ID, 123))))
224+
225+
fmt.Println("Get all users.")
226+
users, _ = ur.List(ctx, ur.SelectStmt())
227+
228+
// More complex statements can be made with references to other tables.
229+
230+
type Role struct {
231+
ID int `db:"id"`
232+
Name string `db:"name"`
233+
}
234+
235+
// Roles repository.
236+
rr := sqluct.Table[Role](st, "roles")
237+
238+
// To be able to resolve "roles" columns, we need to attach roles repo to users repo.
239+
ur.AddTableAlias(rr.R, "roles")
240+
241+
fmt.Println("Get users with role 'admin'.")
242+
users, _ = ur.List(ctx, ur.SelectStmt().
243+
LeftJoin(ur.Fmt("%s ON %s = %s", rr.R, &rr.R.ID, &ur.R.RoleID)).
244+
Where(ur.Fmt("%s = ?", &rr.R.Name), "admin"),
245+
)
246+
247+
_ = user
248+
_ = users
249+
250+
// Output:
251+
// Insert single user.
252+
// exec INSERT INTO "users" ("id","role_id","name") VALUES ($1,$2,$3) [123 0 John Doe]
253+
// Insert two users.
254+
// exec INSERT INTO "users" ("id","role_id","name") VALUES ($1,$2,$3),($4,$5,$6) [124 0 Jane Doe 125 0 Richard Roe]
255+
// Update a user with new name.
256+
// exec UPDATE "users" SET "id" = $1, "role_id" = $2, "name" = $3 WHERE "users"."id" = $4 [123 0 John Doe, Jr. 123]
257+
// Delete a user with id 123.
258+
// exec DELETE FROM "users" WHERE "users"."id" = $1 [123]
259+
// Get single user with id = 123.
260+
// query SELECT "users"."id", "users"."role_id", "users"."name" FROM "users" WHERE "users"."id" = $1 [123]
261+
// Get multiple users with names starting with 'John '.
262+
// query SELECT "users"."id", "users"."role_id", "users"."name" FROM "users" WHERE "users"."name" LIKE $1 [John %]
263+
// Get multiple users with id != 123.
264+
// query SELECT "users"."id", "users"."role_id", "users"."name" FROM "users" WHERE "users"."id" <> $1 [123]
265+
// Get all users.
266+
// query SELECT "users"."id", "users"."role_id", "users"."name" FROM "users" []
267+
// Get users with role 'admin'.
268+
// query SELECT "users"."id", "users"."role_id", "users"."name" FROM "users" LEFT JOIN "roles" ON "roles"."id" = "users"."role_id" WHERE "roles"."name" = $1 [admin]
269+
```
270+
165271
## Omitting Zero Values
166272

167273
When building `WHERE` conditions from row structure it is often needed skip empty fields from condition.

‎example_storageof_test.go

+107
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
//go:build go1.18
2+
// +build go1.18
3+
4+
package sqluct_test
5+
6+
import (
7+
"context"
8+
"database/sql"
9+
"fmt"
10+
11+
"github.com/Masterminds/squirrel"
12+
"github.com/bool64/sqluct"
13+
"github.com/jmoiron/sqlx"
14+
)
15+
16+
func ExampleTable() {
17+
var (
18+
st = sqluct.NewStorage(sqlx.NewDb(sql.OpenDB(dumpConnector{}), "postgres"))
19+
ctx = context.Background()
20+
)
21+
22+
st.IdentifierQuoter = sqluct.QuoteANSI
23+
24+
type User struct {
25+
ID int `db:"id"`
26+
RoleID int `db:"role_id"`
27+
Name string `db:"name"`
28+
}
29+
30+
// Users repository.
31+
ur := sqluct.Table[User](st, "users")
32+
33+
// Pointer to row, that can be used to reference columns via struct fields.
34+
_ = ur.R
35+
36+
// Single user record can be inserted, last insert id (if available) and error are returned.
37+
fmt.Println("Insert single user.")
38+
_, _ = ur.InsertRow(ctx, User{Name: "John Doe", ID: 123})
39+
40+
// Multiple user records can be inserted with sql.Result and error returned.
41+
fmt.Println("Insert two users.")
42+
_, _ = ur.InsertRows(ctx, []User{{Name: "Jane Doe", ID: 124}, {Name: "Richard Roe", ID: 125}})
43+
44+
// Update statement for a single user with condition.
45+
fmt.Println("Update a user with new name.")
46+
_, _ = ur.UpdateStmt(User{Name: "John Doe, Jr.", ID: 123}).Where(ur.Eq(&ur.R.ID, 123)).ExecContext(ctx)
47+
48+
// Delete statement for a condition.
49+
fmt.Println("Delete a user with id 123.")
50+
_, _ = ur.DeleteStmt().Where(ur.Eq(&ur.R.ID, 123)).ExecContext(ctx)
51+
52+
fmt.Println("Get single user with id = 123.")
53+
user, _ := ur.Get(ctx, ur.SelectStmt().Where(ur.Eq(&ur.R.ID, 123)))
54+
55+
// Squirrel expression can be formatted with %s reference(s) to column pointer.
56+
fmt.Println("Get multiple users with names starting with 'John '.")
57+
users, _ := ur.List(ctx, ur.SelectStmt().Where(ur.Fmt("%s LIKE ?", &ur.R.Name), "John %"))
58+
59+
// Squirrel expressions can be applied.
60+
fmt.Println("Get multiple users with id != 123.")
61+
users, _ = ur.List(ctx, ur.SelectStmt().Where(squirrel.NotEq(ur.Eq(&ur.R.ID, 123))))
62+
63+
fmt.Println("Get all users.")
64+
users, _ = ur.List(ctx, ur.SelectStmt())
65+
66+
// More complex statements can be made with references to other tables.
67+
68+
type Role struct {
69+
ID int `db:"id"`
70+
Name string `db:"name"`
71+
}
72+
73+
// Roles repository.
74+
rr := sqluct.Table[Role](st, "roles")
75+
76+
// To be able to resolve "roles" columns, we need to attach roles repo to users repo.
77+
ur.AddTableAlias(rr.R, "roles")
78+
79+
fmt.Println("Get users with role 'admin'.")
80+
users, _ = ur.List(ctx, ur.SelectStmt().
81+
LeftJoin(ur.Fmt("%s ON %s = %s", rr.R, &rr.R.ID, &ur.R.RoleID)).
82+
Where(ur.Fmt("%s = ?", &rr.R.Name), "admin"),
83+
)
84+
85+
_ = user
86+
_ = users
87+
88+
// Output:
89+
// Insert single user.
90+
// exec INSERT INTO "users" ("id","role_id","name") VALUES ($1,$2,$3) [123 0 John Doe]
91+
// Insert two users.
92+
// exec INSERT INTO "users" ("id","role_id","name") VALUES ($1,$2,$3),($4,$5,$6) [124 0 Jane Doe 125 0 Richard Roe]
93+
// Update a user with new name.
94+
// exec UPDATE "users" SET "id" = $1, "role_id" = $2, "name" = $3 WHERE "users"."id" = $4 [123 0 John Doe, Jr. 123]
95+
// Delete a user with id 123.
96+
// exec DELETE FROM "users" WHERE "users"."id" = $1 [123]
97+
// Get single user with id = 123.
98+
// query SELECT "users"."id", "users"."role_id", "users"."name" FROM "users" WHERE "users"."id" = $1 [123]
99+
// Get multiple users with names starting with 'John '.
100+
// query SELECT "users"."id", "users"."role_id", "users"."name" FROM "users" WHERE "users"."name" LIKE $1 [John %]
101+
// Get multiple users with id != 123.
102+
// query SELECT "users"."id", "users"."role_id", "users"."name" FROM "users" WHERE "users"."id" <> $1 [123]
103+
// Get all users.
104+
// query SELECT "users"."id", "users"."role_id", "users"."name" FROM "users" []
105+
// Get users with role 'admin'.
106+
// query SELECT "users"."id", "users"."role_id", "users"."name" FROM "users" LEFT JOIN "roles" ON "roles"."id" = "users"."role_id" WHERE "roles"."name" = $1 [admin]
107+
}

‎go.mod

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ require (
77
github.com/Masterminds/squirrel v1.5.4
88
github.com/bool64/ctxd v1.2.1
99
github.com/bool64/dev v0.2.34
10-
github.com/jmoiron/sqlx v1.3.5
10+
github.com/jmoiron/sqlx v1.4.0
1111
github.com/stretchr/testify v1.8.2
1212
)
1313

‎go.sum

+10-8
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
2+
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
13
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
24
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
35
github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM=
@@ -9,19 +11,19 @@ github.com/bool64/dev v0.2.34/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8
911
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
1012
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
1113
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
12-
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
13-
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
14-
github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
15-
github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
14+
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
15+
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
16+
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
17+
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
1618
github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE=
1719
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw=
1820
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o=
1921
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk=
2022
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw=
21-
github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0=
22-
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
23-
github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg=
24-
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
23+
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
24+
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
25+
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
26+
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
2527
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
2628
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
2729
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=

‎referencer.go

+5-5
Original file line numberDiff line numberDiff line change
@@ -70,25 +70,25 @@ type Referencer struct {
7070
//
7171
// Argument is either a structure pointer or string alias.
7272
func (r *Referencer) ColumnsOf(rowStructPtr interface{}) func(o *Options) {
73-
var table string
73+
var table Quoted
7474

7575
switch v := rowStructPtr.(type) {
7676
case string:
77-
table = v
77+
table = r.Q(v)
7878
case Quoted:
79-
table = string(v)
79+
table = v
8080
default:
8181
t, found := r.refs[rowStructPtr]
8282
if !found {
8383
panic("row structure pointer needs to be added first with AddTableAlias")
8484
}
8585

86-
table = string(t)
86+
table = t
8787
}
8888

8989
return func(o *Options) {
9090
o.PrepareColumn = func(col string) string {
91-
return string(r.Q(table, col))
91+
return string(table + "." + r.Q(col))
9292
}
9393
}
9494
}

‎storage_go1.18.go

+6
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,12 @@ func (s *StorageOf[V]) Get(ctx context.Context, qb ToSQL) (V, error) {
9393

9494
// SelectStmt creates query statement with table name and row columns.
9595
func (s *StorageOf[V]) SelectStmt(options ...func(*Options)) squirrel.SelectBuilder {
96+
if len(options) == 0 {
97+
options = []func(*Options){
98+
s.ColumnsOf(s.R),
99+
}
100+
}
101+
96102
return s.s.SelectStmt(s.tableName, s.R, options...)
97103
}
98104

‎storage_test.go

+55
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ package sqluct_test
22

33
import (
44
"context"
5+
"database/sql/driver"
56
"errors"
7+
"fmt"
68
"testing"
79

810
sqlmock "github.com/DATA-DOG/go-sqlmock"
@@ -13,6 +15,59 @@ import (
1315
"github.com/stretchr/testify/require"
1416
)
1517

18+
type (
19+
dumpConnector struct{}
20+
dumpDriver struct{}
21+
dumpConn struct{}
22+
dumpStmt struct{ query string }
23+
)
24+
25+
func (d dumpStmt) Close() error {
26+
return nil
27+
}
28+
29+
func (d dumpStmt) NumInput() int {
30+
return -1
31+
}
32+
33+
func (d dumpStmt) Exec(args []driver.Value) (driver.Result, error) {
34+
fmt.Println("exec", d.query, args)
35+
36+
return nil, errors.New("skip")
37+
}
38+
39+
func (d dumpStmt) Query(args []driver.Value) (driver.Rows, error) {
40+
fmt.Println("query", d.query, args)
41+
42+
return nil, errors.New("skip")
43+
}
44+
45+
func (d dumpConn) Prepare(query string) (driver.Stmt, error) {
46+
return dumpStmt{query: query}, nil
47+
}
48+
49+
func (d dumpConn) Close() error {
50+
return nil
51+
}
52+
53+
func (d dumpConn) Begin() (driver.Tx, error) {
54+
return nil, nil
55+
}
56+
57+
func (d dumpDriver) Open(name string) (driver.Conn, error) {
58+
fmt.Println("open", name)
59+
60+
return dumpConn{}, nil
61+
}
62+
63+
func (d dumpConnector) Connect(_ context.Context) (driver.Conn, error) {
64+
return dumpConn{}, nil
65+
}
66+
67+
func (d dumpConnector) Driver() driver.Driver {
68+
return dumpDriver{}
69+
}
70+
1671
func TestStorage_InTx_FailToStart(t *testing.T) {
1772
db, mock, err := sqlmock.New()
1873
require.NoError(t, err)

0 commit comments

Comments
 (0)
Please sign in to comment.