Compare commits
2 Commits
Author | SHA1 | Date | |
---|---|---|---|
f700f3e891 | |||
f5350292fc |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -25,4 +25,4 @@ go.work.sum
|
||||
# env file
|
||||
.env
|
||||
|
||||
example/local_*
|
||||
playground/local_*
|
||||
|
75
README.md
75
README.md
@@ -1,5 +1,74 @@
|
||||
# pgm (Postgres Mapper)
|
||||
# pgm - PostgreSQL query mapper
|
||||
A simple ORM that will work on top of [jackc/pgx](https://github.com/jackc/pgx) db connection pool.
|
||||
|
||||
Simple query builder to work with Go:PG apps.
|
||||
|
||||
Will work along side with [dbmate](https://github.com/amacneil/dbmate), will consume schema.sql file created by dbmate
|
||||
## ORMs in go eco system tha I like
|
||||
- [ent](https://github.com/ent/ent)
|
||||
- [sqlc](https://github.com/sqlc-dev/sqlc)
|
||||
|
||||
## Why not to use ent?
|
||||
ent is feature loadedm, you can easily define complex schemas, it has nice integration with graphQL.
|
||||
Very nice auto migration feature. Basically all the things you need in an ORM.
|
||||
|
||||
But to me its overkill for simple apps, I have seen ent related code is taking significatn space in app binary(feels bloated).
|
||||
|
||||
|
||||
## Why not to use sqlc?
|
||||
sqlc is nice as well, but using it will start feeling that now your DB layer has its own Models and you need map you apps model with it or have no choice but to sart using DB layer models, which to me is not a good thing.
|
||||
|
||||
## Things that I am not happy with
|
||||
|
||||
- Auto migrations, at many points you will see ORM is either not providing or have a complex way to do the simple db schema related changes. Its like obscure db schema. you can simply tell and test it in sql query editor.
|
||||
|
||||
DB must be a in form sql statement that you see, fine tune and event run/test in sql query editor.
|
||||
Thers is lots of mature tools available in Go ecosystem, [dbmate](https://github.com/amacneil/dbmate) is one of them, its a nice tool to manage migrations, can be use in code or in cli
|
||||
|
||||
- To much extra code is generated for various condition and scenarios that you may not use
|
||||
|
||||
- Auto genearted models for select queries that will now force you to either use them or to map them to required model.
|
||||
|
||||
## Okay, what to do now, plain old sql queries?
|
||||
Yes and No may hybrid. Plain old sql queries are not bad, just have have few concerns
|
||||
|
||||
- Change in schema are not detected well.
|
||||
- Sql in jection issue if not using parameterized queries.
|
||||
|
||||
We can address these issues using pgm, pgm cli will help to cream light weight DB schema go files that will help in writing sql queries this can help keeping eye on schema changes, goal is not to hard code table and table.column names
|
||||
|
||||
## Generate pgm schema files
|
||||
run following command to generate pgm schema files
|
||||
```bash
|
||||
go run code.patial.tech/go/pgm/cmd -o ./db ./schema.sql
|
||||
```
|
||||
It will create bunch of files and filders under `./db` directory
|
||||
|
||||
|
||||
```go
|
||||
package main
|
||||
import "code.patial.tech/go/pgm"
|
||||
|
||||
type MyModel struct {
|
||||
ID string
|
||||
Email string
|
||||
}
|
||||
|
||||
func main() {
|
||||
println("init pgx connection pool")
|
||||
pgm.InitPool(pgm.Config{
|
||||
ConnString: url,
|
||||
})
|
||||
|
||||
// Select query with first record to scan
|
||||
// it is assuming that schema is already created and is in "db" package and has User table init
|
||||
var v MyModel
|
||||
err := db.User.Select(user.ID, user.Email).
|
||||
Where(user.Email.Like("anki%")).
|
||||
First(context.TODO(), &v.Email, &v.ID)
|
||||
if err != nil {
|
||||
println("error, ", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
println("user email", v.Email)
|
||||
}
|
||||
```
|
||||
|
@@ -36,7 +36,7 @@ func generate(scheamPath, outDir string) error {
|
||||
|
||||
// schema.go will hold all tables info
|
||||
var sb strings.Builder
|
||||
sb.WriteString("// Code generated by code.patial.tech/go/pgm/cmd\n// DO NOT EDIT.\n\n")
|
||||
sb.WriteString("// Code generated by code.patial.tech/go/pgm/cmd DO NOT EDIT.\n\n")
|
||||
sb.WriteString(fmt.Sprintf("package %s \n", filepath.Base(outDir)))
|
||||
sb.WriteString(`
|
||||
import "code.patial.tech/go/pgm"
|
||||
@@ -91,7 +91,7 @@ func generate(scheamPath, outDir string) error {
|
||||
|
||||
func writeColFile(tblName string, cols []*Column, outDir string, caser cases.Caser) error {
|
||||
var sb strings.Builder
|
||||
sb.WriteString("// Code generated by db-gen. DO NOT EDIT.\n\n")
|
||||
sb.WriteString("// Code generated by code.patial.tech/go/pgm/cmd DO NOT EDIT.\n\n")
|
||||
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 (")
|
||||
|
9
go.mod
9
go.mod
@@ -3,7 +3,7 @@ module code.patial.tech/go/pgm
|
||||
go 1.24.5
|
||||
|
||||
require (
|
||||
github.com/jackc/pgx v3.6.2+incompatible
|
||||
github.com/jackc/pgx/v5 v5.7.5
|
||||
golang.org/x/text v0.27.0
|
||||
)
|
||||
|
||||
@@ -11,11 +11,6 @@ require (
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
golang.org/x/crypto v0.40.0 // indirect
|
||||
golang.org/x/sync v0.16.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/jackc/pgx/v5 v5.7.5
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
golang.org/x/crypto v0.40.0 // indirect
|
||||
)
|
||||
|
11
go.sum
11
go.sum
@@ -1,20 +1,21 @@
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/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 v3.6.2+incompatible h1:2zP5OD7kiyR3xzRYMhOcXVvkDZsImVXfj+yIyTQf3/o=
|
||||
github.com/jackc/pgx v3.6.2+incompatible/go.mod h1:0ZGrqGqkRlliWnWB4zKnWtjbSWbGkVEFm4TeybAXq+I=
|
||||
github.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs=
|
||||
github.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
|
||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
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=
|
||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
|
||||
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
|
||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||
@@ -23,3 +24,5 @@ golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
|
||||
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
|
||||
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=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
@@ -1,3 +1,3 @@
|
||||
//go:generate go run code.patial.tech/go/pgm/cmd -o ./db ./schema.sql
|
||||
|
||||
package example
|
||||
package playground
|
@@ -1,10 +1,10 @@
|
||||
package example
|
||||
package playground
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.patial.tech/go/pgm/example/db"
|
||||
"code.patial.tech/go/pgm/example/db/user"
|
||||
"code.patial.tech/go/pgm/playground/db"
|
||||
"code.patial.tech/go/pgm/playground/db/user"
|
||||
)
|
||||
|
||||
func TestDelete(t *testing.T) {
|
@@ -1,11 +1,11 @@
|
||||
package example
|
||||
package playground
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.patial.tech/go/pgm"
|
||||
"code.patial.tech/go/pgm/example/db"
|
||||
"code.patial.tech/go/pgm/example/db/user"
|
||||
"code.patial.tech/go/pgm/playground/db"
|
||||
"code.patial.tech/go/pgm/playground/db/user"
|
||||
)
|
||||
|
||||
func TestInsertQuery(t *testing.T) {
|
@@ -1,14 +1,14 @@
|
||||
package example
|
||||
package playground
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.patial.tech/go/pgm"
|
||||
"code.patial.tech/go/pgm/example/db"
|
||||
"code.patial.tech/go/pgm/example/db/branchuser"
|
||||
"code.patial.tech/go/pgm/example/db/employee"
|
||||
"code.patial.tech/go/pgm/example/db/user"
|
||||
"code.patial.tech/go/pgm/example/db/usersession"
|
||||
"code.patial.tech/go/pgm/playground/db"
|
||||
"code.patial.tech/go/pgm/playground/db/branchuser"
|
||||
"code.patial.tech/go/pgm/playground/db/employee"
|
||||
"code.patial.tech/go/pgm/playground/db/user"
|
||||
"code.patial.tech/go/pgm/playground/db/usersession"
|
||||
)
|
||||
|
||||
func TestQryBuilder2(t *testing.T) {
|
@@ -1,11 +1,11 @@
|
||||
package example
|
||||
package playground
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.patial.tech/go/pgm"
|
||||
"code.patial.tech/go/pgm/example/db"
|
||||
"code.patial.tech/go/pgm/example/db/user"
|
||||
"code.patial.tech/go/pgm/playground/db"
|
||||
"code.patial.tech/go/pgm/playground/db/user"
|
||||
)
|
||||
|
||||
func TestUpdateQuery(t *testing.T) {
|
63
pool.go
63
pool.go
@@ -24,40 +24,45 @@ var (
|
||||
},
|
||||
}
|
||||
|
||||
ErrInitTX = errors.New("failed to init db.tx")
|
||||
ErrCommitTX = errors.New("failed to commit db.tx")
|
||||
ErrNoRows = errors.New("no data found")
|
||||
ErrConnStringMissing = errors.New("connection string is empty")
|
||||
ErrInitTX = errors.New("failed to init db.tx")
|
||||
ErrCommitTX = errors.New("failed to commit db.tx")
|
||||
ErrNoRows = errors.New("no data found")
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
ConnString string
|
||||
MaxConns int32
|
||||
MinConns int32
|
||||
MaxConnLifetime time.Duration
|
||||
MaxConnIdleTime time.Duration
|
||||
}
|
||||
|
||||
func Init(connString string, conf *Config) {
|
||||
cfg, err := pgxpool.ParseConfig(connString)
|
||||
// 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 != nil {
|
||||
if conf.MaxConns > 0 {
|
||||
cfg.MaxConns = conf.MaxConns // 100
|
||||
}
|
||||
if conf.MaxConns > 0 {
|
||||
cfg.MaxConns = conf.MaxConns // 100
|
||||
}
|
||||
|
||||
if conf.MinConns > 0 {
|
||||
cfg.MinConns = conf.MaxConns // 5
|
||||
}
|
||||
if conf.MinConns > 0 {
|
||||
cfg.MinConns = conf.MaxConns // 5
|
||||
}
|
||||
|
||||
if conf.MaxConnLifetime > 0 {
|
||||
cfg.MaxConnLifetime = conf.MaxConnLifetime // time.Minute * 10
|
||||
}
|
||||
if conf.MaxConnLifetime > 0 {
|
||||
cfg.MaxConnLifetime = conf.MaxConnLifetime // time.Minute * 10
|
||||
}
|
||||
|
||||
if conf.MaxConnIdleTime > 0 {
|
||||
cfg.MaxConnIdleTime = conf.MaxConnIdleTime // time.Minute * 5
|
||||
}
|
||||
if conf.MaxConnIdleTime > 0 {
|
||||
cfg.MaxConnIdleTime = conf.MaxConnIdleTime // time.Minute * 5
|
||||
}
|
||||
|
||||
p, err := pgxpool.NewWithConfig(context.Background(), cfg)
|
||||
@@ -72,10 +77,22 @@ func Init(connString string, conf *Config) {
|
||||
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
|
||||
}
|
||||
|
||||
// get string builder from pool
|
||||
func getSB() *strings.Builder {
|
||||
return poolStringBuilder.Get().(*strings.Builder)
|
||||
@@ -86,13 +103,3 @@ func putSB(sb *strings.Builder) {
|
||||
sb.Reset()
|
||||
poolStringBuilder.Put(sb)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
Reference in New Issue
Block a user