diff --git a/Makefile b/Makefile index b2167a0..aa53152 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ run: - go run ./cmd -o ./example/db ./example/schema.sql + go run ./cmd -o ./playground/db ./playground/schema.sql bench-select: go test ./example -bench BenchmarkSelect -memprofile memprofile.out -cpuprofile profile.out diff --git a/cmd/generate.go b/cmd/generate.go index b58e1fd..6545d16 100644 --- a/cmd/generate.go +++ b/cmd/generate.go @@ -95,6 +95,8 @@ func writeColFile(tblName string, cols []*Column, outDir string, caser cases.Cas sb.WriteString(fmt.Sprintf("package %s\n\n", filepath.Base(outDir))) sb.WriteString(fmt.Sprintf("import %q\n\n", "code.patial.tech/go/pgm")) sb.WriteString("const (") + sb.WriteString("\n // All fields in table " + tblName) + sb.WriteString(fmt.Sprintf("\n All pgm.Field = %q", tblName+".*")) var name string for _, c := range cols { name = strings.ReplaceAll(c.Name, "_", " ") diff --git a/pgm.go b/pgm.go index 2b7d6db..7664343 100644 --- a/pgm.go +++ b/pgm.go @@ -1,25 +1,27 @@ // pgm // -// A simple PG query builder +// A simple PG string query builder // // Author: Ankit Patial package pgm import ( + "context" "errors" - "strings" - "sync" + "log/slog" + "sync/atomic" "time" + "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgtype" + "github.com/jackc/pgx/v5/pgxpool" ) -var poolStringBuilder = sync.Pool{ - New: func() any { - return new(strings.Builder) - }, -} +var ( + poolPGX atomic.Pointer[pgxpool.Pool] + ErrConnStringMissing = errors.New("connection string is empty") +) // Errors var ( @@ -28,15 +30,72 @@ var ( ErrNoRows = errors.New("no data found") ) -// get string builder from pool -func getSB() *strings.Builder { - return poolStringBuilder.Get().(*strings.Builder) +type Config struct { + ConnString string + MaxConns int32 + MinConns int32 + MaxConnLifetime time.Duration + MaxConnIdleTime time.Duration } -// put string builder back to pool -func putSB(sb *strings.Builder) { - sb.Reset() - poolStringBuilder.Put(sb) +// InitPool will create new pgxpool.Pool and will keep it for its working +func InitPool(conf Config) { + if conf.ConnString == "" { + panic(ErrConnStringMissing) + } + + cfg, err := pgxpool.ParseConfig(conf.ConnString) + if err != nil { + panic(err) + } + + if conf.MaxConns > 0 { + cfg.MaxConns = conf.MaxConns // 100 + } + + if conf.MinConns > 0 { + cfg.MinConns = conf.MaxConns // 5 + } + + if conf.MaxConnLifetime > 0 { + cfg.MaxConnLifetime = conf.MaxConnLifetime // time.Minute * 10 + } + + if conf.MaxConnIdleTime > 0 { + cfg.MaxConnIdleTime = conf.MaxConnIdleTime // time.Minute * 5 + } + + p, err := pgxpool.NewWithConfig(context.Background(), cfg) + if err != nil { + panic(err) + } + + if err = p.Ping(context.Background()); err != nil { + panic(err) + } + + poolPGX.Store(p) +} + +// GetPool instance +func GetPool() *pgxpool.Pool { + return poolPGX.Load() +} + +// BeginTx begins a pgx poll transaction +func BeginTx(ctx context.Context) (pgx.Tx, error) { + tx, err := poolPGX.Load().Begin(ctx) + if err != nil { + slog.Error(err.Error()) + return nil, errors.New("failed to open db tx") + } + + return tx, err +} + +// IsNotFound error check +func IsNotFound(err error) bool { + return errors.Is(err, pgx.ErrNoRows) } // PgTime as in UTC @@ -47,15 +106,3 @@ func PgTime(t time.Time) pgtype.Timestamptz { func PgTimeNow() pgtype.Timestamptz { return pgtype.Timestamptz{Time: time.Now(), Valid: true} } - -func ConcatWs(sep string, fields ...Field) Field { - return Field("concat_ws('" + sep + "'," + joinFileds(fields) + ")") -} - -func StringAgg(exp, sep string) Field { - return Field("string_agg(" + exp + ",'" + sep + "')") -} - -func StringAggCast(exp, sep string) Field { - return Field("string_agg(cast(" + exp + " as varchar),'" + sep + "')") -} diff --git a/pgm_field.go b/pgm_field.go index 82ccf58..a439629 100644 --- a/pgm_field.go +++ b/pgm_field.go @@ -13,59 +13,73 @@ func (f Field) String() string { return string(f) } -// Count fn wrapping of field +// Count function wrapped field func (f Field) Count() Field { return Field("COUNT(" + f.String() + ")") } -// StringEscape will return a empty string for null value +// StringEscape will wrap field with: +// +// COALESCE(field, ”) func (f Field) StringEscape() Field { return Field("COALESCE(" + f.String() + ", '')") } -// NumberEscape will return a zero string for null value +// NumberEscape will wrap field with: +// +// COALESCE(field, 0) func (f Field) NumberEscape() Field { return Field("COALESCE(" + f.String() + ", 0)") } -// BooleanEscape will return a false for null value +// BooleanEscape will wrap field with: +// +// COALESCE(field, FALSE) func (f Field) BooleanEscape() Field { return Field("COALESCE(" + f.String() + ", FALSE)") } -// Avg fn wrapping of field +// Avg function wrapped field func (f Field) Avg() Field { return Field("AVG(" + f.String() + ")") } +// Sum function wrapped field func (f Field) Sum() Field { return Field("SUM(" + f.String() + ")") } +// Max function wrapped field func (f Field) Max() Field { return Field("MAX(" + f.String() + ")") } +// Min function wrapped field func (f Field) Min() Field { return Field("Min(" + f.String() + ")") } +// Lower function wrapped field func (f Field) Lower() Field { return Field("LOWER(" + f.String() + ")") } +// Upper function wrapped field func (f Field) Upper() Field { return Field("UPPER(" + f.String() + ")") } +// Trim function wrapped field func (f Field) Trim() Field { return Field("TRIM(" + f.String() + ")") } +// Asc suffixed field, supposed to be used with order by func (f Field) Asc() Field { return Field(f.String() + " ASC") } +// Desc suffixed field, supposed to be used with order by func (f Field) Desc() Field { return Field(f.String() + " DESC") } @@ -133,14 +147,12 @@ func (f Field) ILike(val string) Conditioner { return &Cond{Field: col, Val: val, op: " ILIKE $", len: len(col) + 5} } -// In using ANY -func (f Field) In(val ...any) Conditioner { +func (f Field) Any(val ...any) Conditioner { col := f.String() return &Cond{Field: col, Val: val, op: " = ANY($", action: CondActionNeedToClose, len: len(col) + 5} } -// NotIn using ANY -func (f Field) NotIn(val ...any) Conditioner { +func (f Field) NotAny(val ...any) Conditioner { col := f.String() return &Cond{Field: col, Val: val, op: " != ANY($", action: CondActionNeedToClose, len: len(col) + 5} } @@ -151,6 +163,18 @@ func (f Field) NotInSubQuery(qry WhereClause) Conditioner { return &Cond{Field: col, Val: qry, op: " != ANY($)", action: CondActionSubQuery} } +func ConcatWs(sep string, fields ...Field) Field { + return Field("concat_ws('" + sep + "'," + joinFileds(fields) + ")") +} + +func StringAgg(exp, sep string) Field { + return Field("string_agg(" + exp + ",'" + sep + "')") +} + +func StringAggCast(exp, sep string) Field { + return Field("string_agg(cast(" + exp + " as varchar),'" + sep + "')") +} + func joinFileds(fields []Field) string { sb := getSB() defer putSB(sb) diff --git a/playground/db/branchuser/branch_users.go b/playground/db/branchuser/branch_users.go index bc6335e..39351eb 100644 --- a/playground/db/branchuser/branch_users.go +++ b/playground/db/branchuser/branch_users.go @@ -1,10 +1,12 @@ -// Code generated by db-gen. DO NOT EDIT. +// Code generated by code.patial.tech/go/pgm/cmd DO NOT EDIT. package branchuser import "code.patial.tech/go/pgm" const ( + // All fields in table branch_users + All pgm.Field = "branch_users.*" // BranchID field has db type "bigint NOT NULL" BranchID pgm.Field = "branch_users.branch_id" // UserID field has db type "bigint NOT NULL" diff --git a/playground/db/comment/comments.go b/playground/db/comment/comments.go index e14c83b..3de969d 100644 --- a/playground/db/comment/comments.go +++ b/playground/db/comment/comments.go @@ -1,10 +1,12 @@ -// Code generated by db-gen. DO NOT EDIT. +// Code generated by code.patial.tech/go/pgm/cmd DO NOT EDIT. package comment import "code.patial.tech/go/pgm" const ( + // All fields in table comments + All pgm.Field = "comments.*" // ID field has db type "integer NOT NULL" ID pgm.Field = "comments.id" // PostID field has db type "integer NOT NULL" diff --git a/playground/db/employee/employees.go b/playground/db/employee/employees.go index 66ccbe3..8a97535 100644 --- a/playground/db/employee/employees.go +++ b/playground/db/employee/employees.go @@ -1,10 +1,12 @@ -// Code generated by db-gen. DO NOT EDIT. +// Code generated by code.patial.tech/go/pgm/cmd DO NOT EDIT. package employee import "code.patial.tech/go/pgm" const ( + // All fields in table employees + All pgm.Field = "employees.*" // ID field has db type "integer NOT NULL" ID pgm.Field = "employees.id" // Name field has db type "var NOT NULL" diff --git a/playground/db/post/posts.go b/playground/db/post/posts.go index c0920a2..89d612a 100644 --- a/playground/db/post/posts.go +++ b/playground/db/post/posts.go @@ -1,10 +1,12 @@ -// Code generated by db-gen. DO NOT EDIT. +// Code generated by code.patial.tech/go/pgm/cmd DO NOT EDIT. package post import "code.patial.tech/go/pgm" const ( + // All fields in table posts + All pgm.Field = "posts.*" // ID field has db type "integer NOT NULL" ID pgm.Field = "posts.id" // UserID field has db type "integer NOT NULL" diff --git a/playground/db/schema.go b/playground/db/schema.go index 03fa8c8..7e641e2 100644 --- a/playground/db/schema.go +++ b/playground/db/schema.go @@ -1,5 +1,4 @@ -// Code generated by code.patial.tech/go/pgm/cmd -// DO NOT EDIT. +// Code generated by code.patial.tech/go/pgm/cmd DO NOT EDIT. package db diff --git a/playground/db/user/users.go b/playground/db/user/users.go index 7f751b9..cbd86a0 100644 --- a/playground/db/user/users.go +++ b/playground/db/user/users.go @@ -1,10 +1,12 @@ -// Code generated by db-gen. DO NOT EDIT. +// Code generated by code.patial.tech/go/pgm/cmd DO NOT EDIT. package user import "code.patial.tech/go/pgm" const ( + // All fields in table users + All pgm.Field = "users.*" // ID field has db type "integer NOT NULL" ID pgm.Field = "users.id" // Name field has db type "character varying(255) NOT NULL" diff --git a/playground/db/usersession/user_sessions.go b/playground/db/usersession/user_sessions.go index 266373c..527139a 100644 --- a/playground/db/usersession/user_sessions.go +++ b/playground/db/usersession/user_sessions.go @@ -1,10 +1,12 @@ -// Code generated by db-gen. DO NOT EDIT. +// Code generated by code.patial.tech/go/pgm/cmd DO NOT EDIT. package usersession import "code.patial.tech/go/pgm" const ( + // All fields in table user_sessions + All pgm.Field = "user_sessions.*" // ID field has db type "character varying NOT NULL" ID pgm.Field = "user_sessions.id" // CreatedAt field has db type "timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL" diff --git a/playground/qry_delete_test.go b/playground/qry_delete_test.go index 8e5da97..d511679 100644 --- a/playground/qry_delete_test.go +++ b/playground/qry_delete_test.go @@ -10,7 +10,7 @@ import ( func TestDelete(t *testing.T) { expected := "DELETE FROM users WHERE users.id = $1 AND users.status_id != ANY($2)" got := db.User.Delete(). - Where(user.ID.Eq(1), user.StatusID.NotIn(1, 2, 3)). + Where(user.ID.Eq(1), user.StatusID.NotAny(1, 2, 3)). String() if got != expected { t.Errorf("got %q, want %q", got, expected) diff --git a/playground/qry_insert_test.go b/playground/qry_insert_test.go index 60a8965..9277c5b 100644 --- a/playground/qry_insert_test.go +++ b/playground/qry_insert_test.go @@ -36,7 +36,7 @@ func TestInsertQuery2(t *testing.T) { } } -// BenchmarkInsertQuery-12 1952412 605.3 ns/op 1144 B/op 18 allocs/op +// BenchmarkInsertQuery-12 2014519 584.0 ns/op 1144 B/op 18 allocs/op func BenchmarkInsertQuery(b *testing.B) { for b.Loop() { _ = db.User.Insert(). @@ -51,7 +51,6 @@ func BenchmarkInsertQuery(b *testing.B) { } // BenchmarkInsertSetMap-12 1534039 777.1 ns/op 1480 B/op 20 allocs/op -// BenchmarkInsertSetMap-12 1361275 879.2 ns/op 1480 B/op 20 allocs/op func BenchmarkInsertSetMap(b *testing.B) { for b.Loop() { _ = db.User.Insert(). diff --git a/playground/qry_update_test.go b/playground/qry_update_test.go index e7ebae8..9560c85 100644 --- a/playground/qry_update_test.go +++ b/playground/qry_update_test.go @@ -27,7 +27,6 @@ func TestUpdateQuery(t *testing.T) { } // BenchmarkUpdateQuery-12 2004985 592.2 ns/op 1176 B/op 20 allocs/op -// BenchmarkUpdateQuery-12 1792483 670.7 ns/op 1176 B/op 20 allocs/op func BenchmarkUpdateQuery(b *testing.B) { for b.Loop() { _ = db.User.Update(). diff --git a/pool.go b/pool.go deleted file mode 100644 index 6f4dab7..0000000 --- a/pool.go +++ /dev/null @@ -1,88 +0,0 @@ -// Patial Tech. -// Author, Ankit Patial - -package pgm - -import ( - "context" - "errors" - "log/slog" - "sync/atomic" - "time" - - "github.com/jackc/pgx/v5" - "github.com/jackc/pgx/v5/pgxpool" -) - -var ( - poolPGX atomic.Pointer[pgxpool.Pool] - ErrConnStringMissing = errors.New("connection string is empty") -) - -type Config struct { - ConnString string - MaxConns int32 - MinConns int32 - MaxConnLifetime time.Duration - MaxConnIdleTime time.Duration -} - -// InitPool will create new pgxpool.Pool and will keep it for its working -func InitPool(conf Config) { - if conf.ConnString == "" { - panic(ErrConnStringMissing) - } - - cfg, err := pgxpool.ParseConfig(conf.ConnString) - if err != nil { - panic(err) - } - - if conf.MaxConns > 0 { - cfg.MaxConns = conf.MaxConns // 100 - } - - if conf.MinConns > 0 { - cfg.MinConns = conf.MaxConns // 5 - } - - if conf.MaxConnLifetime > 0 { - cfg.MaxConnLifetime = conf.MaxConnLifetime // time.Minute * 10 - } - - if conf.MaxConnIdleTime > 0 { - cfg.MaxConnIdleTime = conf.MaxConnIdleTime // time.Minute * 5 - } - - p, err := pgxpool.NewWithConfig(context.Background(), cfg) - if err != nil { - panic(err) - } - - if err = p.Ping(context.Background()); err != nil { - panic(err) - } - - poolPGX.Store(p) -} - -// GetPool instance -func GetPool() *pgxpool.Pool { - return poolPGX.Load() -} - -// BeginTx begins a pgx poll transaction -func BeginTx(ctx context.Context) (pgx.Tx, error) { - tx, err := poolPGX.Load().Begin(ctx) - if err != nil { - slog.Error(err.Error()) - return nil, errors.New("failed to open db tx") - } - - return tx, err -} - -// IsNotFound error check -func IsNotFound(err error) bool { - return errors.Is(err, pgx.ErrNoRows) -} diff --git a/qry.go b/qry.go index 64adbb6..dc3a89a 100644 --- a/qry.go +++ b/qry.go @@ -7,6 +7,7 @@ import ( "context" "strconv" "strings" + "sync" "github.com/jackc/pgx/v5" ) @@ -34,6 +35,23 @@ type ( } ) +var sbPool = sync.Pool{ + New: func() any { + return new(strings.Builder) + }, +} + +// get string builder from pool +func getSB() *strings.Builder { + return sbPool.Get().(*strings.Builder) +} + +// put string builder back to pool +func putSB(sb *strings.Builder) { + sb.Reset() + sbPool.Put(sb) +} + func And(cond ...Conditioner) Conditioner { return &CondGroup{op: " AND ", cond: cond} }