diff --git a/README.md b/README.md index 512304b..ac362ac 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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" @@ -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. diff --git a/example_storageof_test.go b/example_storageof_test.go new file mode 100644 index 0000000..b3cb8e1 --- /dev/null +++ b/example_storageof_test.go @@ -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] +} diff --git a/storage_go1.18.go b/storage_go1.18.go index ca7a26c..c092148 100644 --- a/storage_go1.18.go +++ b/storage_go1.18.go @@ -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...) } diff --git a/storage_test.go b/storage_test.go index e4d36c9..d0611c6 100644 --- a/storage_test.go +++ b/storage_test.go @@ -2,7 +2,9 @@ package sqluct_test import ( "context" + "database/sql/driver" "errors" + "fmt" "testing" sqlmock "github.com/DATA-DOG/go-sqlmock" @@ -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)