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
+}