Skip to content

Commit

Permalink
Add example for storage.Table[V]
Browse files Browse the repository at this point in the history
  • Loading branch information
vearutop committed May 24, 2024
1 parent 42741c1 commit e641f48
Show file tree
Hide file tree
Showing 4 changed files with 274 additions and 0 deletions.
106 changes: 106 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ Field tags (`db` by default) act as a source of truth for column names to allow
`Storage` is a high level service that provides query building, query executing and result fetching facilities
as easy to use facades.

`StorageOf[V any]` typed query builder and scanner for specific table(s).

`Mapper` is a lower level tool that focuses on managing `squirrel` query builder with row structures.

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

// Or if you already have an *sql.DB or *sqlx.DB instances, you can use them:
// db, _ := sql.Open("postgres", "postgres://pqgotest:password@localhost/pqgotest?sslmode=disable")
// s := sqluct.NewStorage(sqlx.NewDb(db, "postgres"))

ctx := context.TODO()

const tableName = "products"
Expand Down Expand Up @@ -162,6 +168,106 @@ fmt.Println(args)
// [John]
```

## Typed Storage

`sqluct.Table[RowType](storageInstance, tableName)` creates a type-safe storage accessor to a table with `RowType`.
This accessor can help to retrieve or store data. Columns from multiple tables can be joined using field pointers.

Please check features overview in an example below.

```go
var (
st = sqluct.NewStorage(sqlx.NewDb(sql.OpenDB(dumpConnector{}), "postgres"))
ctx = context.Background()
)

st.IdentifierQuoter = sqluct.QuoteANSI

type User struct {
ID int `db:"id"`
RoleID int `db:"role_id"`
Name string `db:"name"`
}

// Users repository.
ur := sqluct.Table[User](st, "users")

// Pointer to row, that can be used to reference columns via struct fields.
_ = ur.R

// Single user record can be inserted, last insert id (if available) and error are returned.
fmt.Println("Insert single user.")
_, _ = ur.InsertRow(ctx, User{Name: "John Doe", ID: 123})

// Multiple user records can be inserted with sql.Result and error returned.
fmt.Println("Insert two users.")
_, _ = ur.InsertRows(ctx, []User{{Name: "Jane Doe", ID: 124}, {Name: "Richard Roe", ID: 125}})

// Update statement for a single user with condition.
fmt.Println("Update a user with new name.")
_, _ = ur.UpdateStmt(User{Name: "John Doe, Jr.", ID: 123}).Where(ur.Eq(&ur.R.ID, 123)).ExecContext(ctx)

// Delete statement for a condition.
fmt.Println("Delete a user with id 123.")
_, _ = ur.DeleteStmt().Where(ur.Eq(&ur.R.ID, 123)).ExecContext(ctx)

fmt.Println("Get single user with id = 123.")
user, _ := ur.Get(ctx, ur.SelectStmt().Where(ur.Eq(&ur.R.ID, 123)))

// Squirrel expression can be formatted with %s reference(s) to column pointer.
fmt.Println("Get multiple users with names starting with 'John '.")
users, _ := ur.List(ctx, ur.SelectStmt().Where(ur.Fmt("%s LIKE ?", &ur.R.Name), "John %"))

// Squirrel expressions can be applied.
fmt.Println("Get multiple users with id != 123.")
users, _ = ur.List(ctx, ur.SelectStmt().Where(squirrel.NotEq(ur.Eq(&ur.R.ID, 123))))

fmt.Println("Get all users.")
users, _ = ur.List(ctx, ur.SelectStmt())

// More complex statements can be made with references to other tables.

type Role struct {
ID int `db:"id"`
Name string `db:"name"`
}

// Roles repository.
rr := sqluct.Table[Role](st, "roles")

// To be able to resolve "roles" columns, we need to attach roles repo to users repo.
ur.AddTableAlias(rr.R, "roles")

fmt.Println("Get users with role 'admin'.")
users, _ = ur.List(ctx, ur.SelectStmt().
LeftJoin(ur.Fmt("%s ON %s = %s", rr.R, &rr.R.ID, &ur.R.RoleID)).
Where(ur.Fmt("%s = ?", &rr.R.Name), "admin"),
)

_ = user
_ = users

// Output:
// Insert single user.
// exec INSERT INTO "users" ("id","role_id","name") VALUES ($1,$2,$3) [123 0 John Doe]
// Insert two users.
// exec INSERT INTO "users" ("id","role_id","name") VALUES ($1,$2,$3),($4,$5,$6) [124 0 Jane Doe 125 0 Richard Roe]
// Update a user with new name.
// exec UPDATE "users" SET "id" = $1, "role_id" = $2, "name" = $3 WHERE "users"."id" = $4 [123 0 John Doe, Jr. 123]
// Delete a user with id 123.
// exec DELETE FROM "users" WHERE "users"."id" = $1 [123]
// Get single user with id = 123.
// query SELECT "users"."id", "users"."role_id", "users"."name" FROM "users" WHERE "users"."id" = $1 [123]
// Get multiple users with names starting with 'John '.
// query SELECT "users"."id", "users"."role_id", "users"."name" FROM "users" WHERE "users"."name" LIKE $1 [John %]
// Get multiple users with id != 123.
// query SELECT "users"."id", "users"."role_id", "users"."name" FROM "users" WHERE "users"."id" <> $1 [123]
// Get all users.
// query SELECT "users"."id", "users"."role_id", "users"."name" FROM "users" []
// Get users with role 'admin'.
// 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]
```

## Omitting Zero Values

When building `WHERE` conditions from row structure it is often needed skip empty fields from condition.
Expand Down
107 changes: 107 additions & 0 deletions example_storageof_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
//go:build go1.18
// +build go1.18

package sqluct_test

import (
"context"
"database/sql"
"fmt"

"github.com/Masterminds/squirrel"
"github.com/bool64/sqluct"
"github.com/jmoiron/sqlx"
)

func ExampleTable() {
var (
st = sqluct.NewStorage(sqlx.NewDb(sql.OpenDB(dumpConnector{}), "postgres"))
ctx = context.Background()
)

st.IdentifierQuoter = sqluct.QuoteANSI

type User struct {
ID int `db:"id"`
RoleID int `db:"role_id"`
Name string `db:"name"`
}

// Users repository.
ur := sqluct.Table[User](st, "users")

// Pointer to row, that can be used to reference columns via struct fields.
_ = ur.R

// Single user record can be inserted, last insert id (if available) and error are returned.
fmt.Println("Insert single user.")
_, _ = ur.InsertRow(ctx, User{Name: "John Doe", ID: 123})

// Multiple user records can be inserted with sql.Result and error returned.
fmt.Println("Insert two users.")
_, _ = ur.InsertRows(ctx, []User{{Name: "Jane Doe", ID: 124}, {Name: "Richard Roe", ID: 125}})

// Update statement for a single user with condition.
fmt.Println("Update a user with new name.")
_, _ = ur.UpdateStmt(User{Name: "John Doe, Jr.", ID: 123}).Where(ur.Eq(&ur.R.ID, 123)).ExecContext(ctx)

// Delete statement for a condition.
fmt.Println("Delete a user with id 123.")
_, _ = ur.DeleteStmt().Where(ur.Eq(&ur.R.ID, 123)).ExecContext(ctx)

fmt.Println("Get single user with id = 123.")
user, _ := ur.Get(ctx, ur.SelectStmt().Where(ur.Eq(&ur.R.ID, 123)))

// Squirrel expression can be formatted with %s reference(s) to column pointer.
fmt.Println("Get multiple users with names starting with 'John '.")
users, _ := ur.List(ctx, ur.SelectStmt().Where(ur.Fmt("%s LIKE ?", &ur.R.Name), "John %"))

// Squirrel expressions can be applied.
fmt.Println("Get multiple users with id != 123.")
users, _ = ur.List(ctx, ur.SelectStmt().Where(squirrel.NotEq(ur.Eq(&ur.R.ID, 123))))

fmt.Println("Get all users.")
users, _ = ur.List(ctx, ur.SelectStmt())

// More complex statements can be made with references to other tables.

type Role struct {
ID int `db:"id"`
Name string `db:"name"`
}

// Roles repository.
rr := sqluct.Table[Role](st, "roles")

// To be able to resolve "roles" columns, we need to attach roles repo to users repo.
ur.AddTableAlias(rr.R, "roles")

fmt.Println("Get users with role 'admin'.")
users, _ = ur.List(ctx, ur.SelectStmt().
LeftJoin(ur.Fmt("%s ON %s = %s", rr.R, &rr.R.ID, &ur.R.RoleID)).
Where(ur.Fmt("%s = ?", &rr.R.Name), "admin"),
)

_ = user
_ = users

// Output:
// Insert single user.
// exec INSERT INTO "users" ("id","role_id","name") VALUES ($1,$2,$3) [123 0 John Doe]
// Insert two users.
// exec INSERT INTO "users" ("id","role_id","name") VALUES ($1,$2,$3),($4,$5,$6) [124 0 Jane Doe 125 0 Richard Roe]
// Update a user with new name.
// exec UPDATE "users" SET "id" = $1, "role_id" = $2, "name" = $3 WHERE "users"."id" = $4 [123 0 John Doe, Jr. 123]
// Delete a user with id 123.
// exec DELETE FROM "users" WHERE "users"."id" = $1 [123]
// Get single user with id = 123.
// query SELECT "users"."id", "users"."role_id", "users"."name" FROM "users" WHERE "users"."id" = $1 [123]
// Get multiple users with names starting with 'John '.
// query SELECT "users"."id", "users"."role_id", "users"."name" FROM "users" WHERE "users"."name" LIKE $1 [John %]
// Get multiple users with id != 123.
// query SELECT "users"."id", "users"."role_id", "users"."name" FROM "users" WHERE "users"."id" <> $1 [123]
// Get all users.
// query SELECT "users"."id", "users"."role_id", "users"."name" FROM "users" []
// Get users with role 'admin'.
// 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]
}
6 changes: 6 additions & 0 deletions storage_go1.18.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,12 @@ func (s *StorageOf[V]) Get(ctx context.Context, qb ToSQL) (V, error) {

// SelectStmt creates query statement with table name and row columns.
func (s *StorageOf[V]) SelectStmt(options ...func(*Options)) squirrel.SelectBuilder {
if len(options) == 0 {
options = []func(*Options){
s.ColumnsOf(s.R),
}
}

return s.s.SelectStmt(s.tableName, s.R, options...)
}

Expand Down
55 changes: 55 additions & 0 deletions storage_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package sqluct_test

import (
"context"
"database/sql/driver"
"errors"
"fmt"
"testing"

sqlmock "github.com/DATA-DOG/go-sqlmock"
Expand All @@ -13,6 +15,59 @@ import (
"github.com/stretchr/testify/require"
)

type (
dumpConnector struct{}
dumpDriver struct{}
dumpConn struct{}
dumpStmt struct{ query string }
)

func (d dumpStmt) Close() error {
return nil
}

func (d dumpStmt) NumInput() int {
return -1
}

func (d dumpStmt) Exec(args []driver.Value) (driver.Result, error) {
fmt.Println("exec", d.query, args)

return nil, errors.New("skip")
}

func (d dumpStmt) Query(args []driver.Value) (driver.Rows, error) {
fmt.Println("query", d.query, args)

return nil, errors.New("skip")
}

func (d dumpConn) Prepare(query string) (driver.Stmt, error) {
return dumpStmt{query: query}, nil
}

func (d dumpConn) Close() error {
return nil
}

func (d dumpConn) Begin() (driver.Tx, error) {
return nil, nil
}

func (d dumpDriver) Open(name string) (driver.Conn, error) {
fmt.Println("open", name)

return dumpConn{}, nil
}

func (d dumpConnector) Connect(_ context.Context) (driver.Conn, error) {
return dumpConn{}, nil
}

func (d dumpConnector) Driver() driver.Driver {
return dumpDriver{}
}

func TestStorage_InTx_FailToStart(t *testing.T) {
db, mock, err := sqlmock.New()
require.NoError(t, err)
Expand Down

0 comments on commit e641f48

Please sign in to comment.