From a96c3177958f9ba1335f2f2be08e3ef11595edaa Mon Sep 17 00:00:00 2001 From: Dmitry Anderson <4nd3r5z0n@gmail.com> Date: Tue, 12 Nov 2024 09:45:45 +0100 Subject: [PATCH] Select and Tx functions added --- .idea/.gitignore | 8 ++++ .idea/modules.xml | 8 ++++ .idea/nettools.iml | 9 ++++ .idea/vcs.xml | 6 +++ go.mod | 8 ++++ go.sum | 17 ++++++++ pg/pg.go | 85 ++++++++++++++++++++++++++++++++++++++ reflect/extract_by_tags.go | 28 +++++++++++++ 8 files changed, 169 insertions(+) create mode 100644 .idea/.gitignore create mode 100644 .idea/modules.xml create mode 100644 .idea/nettools.iml create mode 100644 .idea/vcs.xml create mode 100644 go.sum create mode 100644 pg/pg.go create mode 100644 reflect/extract_by_tags.go diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..ba1285f --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/nettools.iml b/.idea/nettools.iml new file mode 100644 index 0000000..5e764c4 --- /dev/null +++ b/.idea/nettools.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/go.mod b/go.mod index ccd4439..c480c02 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,11 @@ module nettools go 1.23.1 + +require ( + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/pgx/v5 v5.7.1 // indirect + golang.org/x/crypto v0.27.0 // indirect + golang.org/x/text v0.18.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e091b4f --- /dev/null +++ b/go.sum @@ -0,0 +1,17 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.7.1 h1:x7SYsPBYDkHDksogeSmZZ5xzThcTgRz++I5E+ePFUcs= +github.com/jackc/pgx/v5 v5.7.1/go.mod h1:e7O26IywZZ+naJtWWos6i6fvWK+29etgITqrqHLfoZA= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= +golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= +golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= +golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pg/pg.go b/pg/pg.go new file mode 100644 index 0000000..65967d8 --- /dev/null +++ b/pg/pg.go @@ -0,0 +1,85 @@ +package pgUtils + +import ( + "context" + "errors" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgxpool" + reflectUtils "nettools/reflect" +) + +var ( + ErrEntityNotFound = errors.New("entity not found") + ErrTooManyRows = errors.New("too many rows") + ErrEntityAlreadyExists = errors.New("entity already exists") + ErrNoDstFields = errors.New("no destination fields") +) + +type PgxQuerier interface { + Query(ctx context.Context, sql string, args ...any) (pgx.Rows, error) +} + +// Select executes query on a provided querier and tries to parse db response into antit +// Works only with objects +// +// Usage: +// +// type User struct { +// id int +// name string +// } +// +// db := pgx.Connect(context.Background(), "") +// users, err := pgUtils.Select[User](context.Background(), db, "SELECT * FROM users") +func Select[T any](ctx context.Context, db PgxQuerier, query string, args ...any) (out []*T, err error) { + out = []*T{} + rows, err := db.Query(ctx, query, args) + if err != nil { + switch { + case errors.Is(err, pgx.ErrNoRows): + err = ErrEntityNotFound + } // TODO: extend cases + return nil, err + } + + // Get column names + columns := make([]string, len(rows.FieldDescriptions())) + for i, fd := range rows.FieldDescriptions() { + columns[i] = fd.Name + } + itemFieldPtrs := make([]interface{}, len(columns)) + + defer rows.Close() + for rows.Next() { + item := new(T) + dstItemPtrsMap, err := reflectUtils.GetEntityPtrs(item, "db") + if err != nil { + return nil, err + } + for i, columnName := range columns { + itemFieldPtrs[i] = dstItemPtrsMap[columnName] + } + if len(itemFieldPtrs) == 0 { + return nil, ErrNoDstFields + } + if err = rows.Scan(itemFieldPtrs...); err != nil { + return out, err + } + out = append(out, item) + } + return out, err +} + +// Tx creates new transaction. Cancels it if returned not nil err +func Tx(ctx context.Context, db *pgxpool.Pool, exec func(ctx context.Context, tx pgx.Tx) error) error { + tx, err := db.Begin(ctx) + if err != nil { + return err + } + err = exec(ctx, tx) + if err != nil { + _ = tx.Rollback(ctx) + return err + } + return tx.Commit(ctx) +} diff --git a/reflect/extract_by_tags.go b/reflect/extract_by_tags.go new file mode 100644 index 0000000..97aa10e --- /dev/null +++ b/reflect/extract_by_tags.go @@ -0,0 +1,28 @@ +package reflectUtils + +import ( + "errors" + "reflect" +) + +var ErrNilDst = errors.New("destination is nil") + +func GetEntityPtrs(dstItem any, tag string) (dstItemPtrsMap map[string]any, err error) { + if dstItem == nil { + return nil, errors.Join(errors.New("error parsing destination"), ErrNilDst) + } + v := reflect.ValueOf(dstItem).Elem() + t := v.Type() + + dstItemPtrsMap = make(map[string]any) + for i := 0; i < t.NumField(); i++ { + field := t.Field(i) + dbTag := field.Tag.Get(tag) + if dbTag != "" { + valueField := v.Field(i).Addr().Interface() + dstItemPtrsMap[dbTag] = &valueField + } + } + + return dstItemPtrsMap, nil +}