Compare commits
13 Commits
Author | SHA1 | Date | |
---|---|---|---|
525c64e678 | |||
5f0fdadb8b | |||
68263895f7 | |||
ee6cb445ab | |||
d07c25fe01 | |||
096480a3eb | |||
6c14441591 | |||
d95eea6636 | |||
63b71692b5 | |||
36e4145365 | |||
2ec328059f | |||
f700f3e891 | |||
f5350292fc |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -25,4 +25,4 @@ go.work.sum
|
|||||||
# env file
|
# env file
|
||||||
.env
|
.env
|
||||||
|
|
||||||
example/local_*
|
playground/local_*
|
||||||
|
82
README.md
82
README.md
@@ -1,5 +1,81 @@
|
|||||||
# pgm (Postgres Mapper)
|
# pgm - PostgreSQL Query Mapper
|
||||||
|
|
||||||
Simple query builder to work with Go:PG apps.
|
A lightweight ORM built on top of [jackc/pgx](https://github.com/jackc/pgx) database connection pool.
|
||||||
|
|
||||||
|
## ORMs I Like in the Go Ecosystem
|
||||||
|
|
||||||
|
- [ent](https://github.com/ent/ent)
|
||||||
|
- [sqlc](https://github.com/sqlc-dev/sqlc)
|
||||||
|
|
||||||
|
## Why Not Use `ent`?
|
||||||
|
|
||||||
|
`ent` is a feature-rich ORM with schema definition, automatic migrations, integration with `gqlgen` (GraphQL server), and more. It provides nearly everything you could want in an ORM.
|
||||||
|
|
||||||
|
However, it can be overkill. The generated code supports a wide range of features, many of which you may not use, significantly increasing the compiled binary size.
|
||||||
|
|
||||||
|
## Why Not Use `sqlc`?
|
||||||
|
|
||||||
|
`sqlc` is a great tool, but it often feels like the database layer introduces its own models. This forces you to either map your application’s models to these database models or use the database models directly, which may not align with your application’s design.
|
||||||
|
|
||||||
|
## Issues with Existing ORMs
|
||||||
|
|
||||||
|
Here are some common pain points with ORMs:
|
||||||
|
|
||||||
|
- **Auto Migrations**: Many ORMs either lack robust migration support or implement complex methods for simple schema changes. This can obscure the database schema, making it harder to understand and maintain. A database schema should be defined in clear SQL statements that can be tested in a SQL query editor. Tools like [dbmate](https://github.com/amacneil/dbmate) provide a mature solution for managing migrations, usable via CLI or in code.
|
||||||
|
|
||||||
|
- **Excessive Code Generation**: ORMs often generate excessive code for various conditions and scenarios, much of which goes unused.
|
||||||
|
|
||||||
|
- **Generated Models for Queries**: Auto-generated models for `SELECT` queries force you to either adopt them or map them to your application’s models, adding complexity.
|
||||||
|
|
||||||
|
## A Hybrid Approach: Plain SQL Queries with `pgm`
|
||||||
|
|
||||||
|
Plain SQL queries are not inherently bad but come with challenges:
|
||||||
|
|
||||||
|
- **Schema Change Detection**: Changes in the database schema are not easily detected.
|
||||||
|
- **SQL Injection Risks**: Without parameterized queries, SQL injection becomes a concern.
|
||||||
|
|
||||||
|
`pgm` addresses these issues by providing a lightweight CLI tool that generates Go files for your database schema. These files help you write SQL queries while keeping track of schema changes, avoiding hardcoded table and column names.
|
||||||
|
|
||||||
|
## Generating `pgm` Schema Files
|
||||||
|
|
||||||
|
Run the following command to generate schema files:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go run code.partial.tech/go/pgm/cmd -o ./db ./schema.sql
|
||||||
|
```
|
||||||
|
once you have the schama files created you can use `pgm` as
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"code.partial.tech/go/pgm"
|
||||||
|
"myapp/db/user" // scham create by pgm/cmd
|
||||||
|
)
|
||||||
|
|
||||||
|
type MyModel struct {
|
||||||
|
ID string
|
||||||
|
Email string
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
println("Initializing pgx connection pool")
|
||||||
|
pgm.InitPool(pgm.Config{
|
||||||
|
ConnString: url,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Select query to fetch the first record
|
||||||
|
// Assumes the schema is defined in the "db" package with a User table
|
||||||
|
var v MyModel
|
||||||
|
err := db.User.Select(user.ID, user.Email).
|
||||||
|
Where(user.Email.Like("anki%")).
|
||||||
|
First(context.TODO(), &v.ID, &v.Email)
|
||||||
|
if err != nil {
|
||||||
|
println("Error:", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
println("User email:", v.Email)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
Will work along side with [dbmate](https://github.com/amacneil/dbmate), will consume schema.sql file created by dbmate
|
|
||||||
|
@@ -36,7 +36,7 @@ func generate(scheamPath, outDir string) error {
|
|||||||
|
|
||||||
// schema.go will hold all tables info
|
// schema.go will hold all tables info
|
||||||
var sb strings.Builder
|
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(fmt.Sprintf("package %s \n", filepath.Base(outDir)))
|
||||||
sb.WriteString(`
|
sb.WriteString(`
|
||||||
import "code.patial.tech/go/pgm"
|
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 {
|
func writeColFile(tblName string, cols []*Column, outDir string, caser cases.Caser) error {
|
||||||
var sb strings.Builder
|
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("package %s\n\n", filepath.Base(outDir)))
|
||||||
sb.WriteString(fmt.Sprintf("import %q\n\n", "code.patial.tech/go/pgm"))
|
sb.WriteString(fmt.Sprintf("import %q\n\n", "code.patial.tech/go/pgm"))
|
||||||
sb.WriteString("const (")
|
sb.WriteString("const (")
|
||||||
|
9
go.mod
9
go.mod
@@ -3,7 +3,7 @@ module code.patial.tech/go/pgm
|
|||||||
go 1.24.5
|
go 1.24.5
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/jackc/pgx v3.6.2+incompatible
|
github.com/jackc/pgx/v5 v5.7.5
|
||||||
golang.org/x/text v0.27.0
|
golang.org/x/text v0.27.0
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -11,11 +11,6 @@ require (
|
|||||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||||
github.com/jackc/puddle/v2 v2.2.2 // 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
|
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.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 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
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 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
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 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs=
|
||||||
github.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
|
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 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
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/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.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.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 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
|
||||||
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
|
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
|
||||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
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=
|
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/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.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=
|
||||||
|
83
pgm.go
83
pgm.go
@@ -4,11 +4,9 @@
|
|||||||
package pgm
|
package pgm
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/jackc/pgx/v5"
|
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -46,18 +44,68 @@ func (f Field) Count() Field {
|
|||||||
return Field("COUNT(" + f.String() + ")")
|
return Field("COUNT(" + f.String() + ")")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// StringEscape will return a empty string for null value
|
||||||
|
func (f Field) StringEscape() Field {
|
||||||
|
return Field("COALESCE(" + f.String() + ", '')")
|
||||||
|
}
|
||||||
|
|
||||||
|
// NumberEscape will return a zero string for null value
|
||||||
|
func (f Field) NumberEscape() Field {
|
||||||
|
return Field("COALESCE(" + f.String() + ", 0)")
|
||||||
|
}
|
||||||
|
|
||||||
|
// BooleanEscape will return a false for null value
|
||||||
|
func (f Field) BooleanEscape() Field {
|
||||||
|
return Field("COALESCE(" + f.String() + ", FALSE)")
|
||||||
|
}
|
||||||
|
|
||||||
// Avg fn wrapping of field
|
// Avg fn wrapping of field
|
||||||
func (f Field) Avg() Field {
|
func (f Field) Avg() Field {
|
||||||
return Field("AVG(" + f.String() + ")")
|
return Field("AVG(" + f.String() + ")")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f Field) Sum() Field {
|
||||||
|
return Field("SUM(" + f.String() + ")")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f Field) Max() Field {
|
||||||
|
return Field("MAX(" + f.String() + ")")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f Field) Min() Field {
|
||||||
|
return Field("Min(" + f.String() + ")")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f Field) Lower() Field {
|
||||||
|
return Field("LOWER(" + f.String() + ")")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f Field) Upper() Field {
|
||||||
|
return Field("UPPER(" + f.String() + ")")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f Field) Trim() Field {
|
||||||
|
return Field("TRIM(" + f.String() + ")")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f Field) IsNull() Conditioner {
|
||||||
|
col := f.String()
|
||||||
|
return &Cond{Field: col, op: " IS NULL", len: len(col) + 8}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f Field) IsNotNull() Conditioner {
|
||||||
|
col := f.String()
|
||||||
|
return &Cond{Field: col, op: " IS NOT NULL", len: len(col) + 12}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Eq is equal
|
||||||
func (f Field) Eq(val any) Conditioner {
|
func (f Field) Eq(val any) Conditioner {
|
||||||
col := f.String()
|
col := f.String()
|
||||||
return &Cond{Field: col, Val: val, op: " = $", len: len(col) + 5}
|
return &Cond{Field: col, Val: val, op: " = $", len: len(col) + 5}
|
||||||
}
|
}
|
||||||
|
|
||||||
// EqualFold will user LOWER() for comparision
|
// EqualFold will use LOWER(column_name) = LOWER(val) for comparision
|
||||||
func (f Field) EqFold(val any) Conditioner {
|
func (f Field) EqFold(val string) Conditioner {
|
||||||
col := f.String()
|
col := f.String()
|
||||||
return &Cond{Field: "LOWER(" + col + ")", Val: val, op: " = LOWER($", action: CondActionNeedToClose, len: len(col) + 5}
|
return &Cond{Field: "LOWER(" + col + ")", Val: val, op: " = LOWER($", action: CondActionNeedToClose, len: len(col) + 5}
|
||||||
}
|
}
|
||||||
@@ -72,11 +120,21 @@ func (f Field) Gt(val any) Conditioner {
|
|||||||
return &Cond{Field: col, Val: val, op: " > $", len: len(col) + 5}
|
return &Cond{Field: col, Val: val, op: " > $", len: len(col) + 5}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f Field) Lt(val any) Conditioner {
|
||||||
|
col := f.String()
|
||||||
|
return &Cond{Field: col, Val: val, op: " < $", len: len(col) + 5}
|
||||||
|
}
|
||||||
|
|
||||||
func (f Field) Gte(val any) Conditioner {
|
func (f Field) Gte(val any) Conditioner {
|
||||||
col := f.String()
|
col := f.String()
|
||||||
return &Cond{Field: col, Val: val, op: " >= $", len: len(col) + 5}
|
return &Cond{Field: col, Val: val, op: " >= $", len: len(col) + 5}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f Field) Lte(val any) Conditioner {
|
||||||
|
col := f.String()
|
||||||
|
return &Cond{Field: col, Val: val, op: " <= $", len: len(col) + 5}
|
||||||
|
}
|
||||||
|
|
||||||
func (f Field) Like(val string) Conditioner {
|
func (f Field) Like(val string) Conditioner {
|
||||||
col := f.String()
|
col := f.String()
|
||||||
return &Cond{Field: col, Val: val, op: " LIKE $", len: len(f.String()) + 5}
|
return &Cond{Field: col, Val: val, op: " LIKE $", len: len(f.String()) + 5}
|
||||||
@@ -116,19 +174,14 @@ func PgTimeNow() pgtype.Timestamptz {
|
|||||||
return pgtype.Timestamptz{Time: time.Now(), Valid: true}
|
return pgtype.Timestamptz{Time: time.Now(), Valid: true}
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsNotFound error check
|
func ConcatWs(sep string, fields ...Field) Field {
|
||||||
func IsNotFound(err error) bool {
|
return Field("concat_ws('" + sep + "'," + joinFileds(fields) + ")")
|
||||||
return errors.Is(err, pgx.ErrNoRows)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func ConcatWs(sep string, fields ...Field) string {
|
func StringAgg(exp, sep string) Field {
|
||||||
return "concat_ws('" + sep + "'," + joinFileds(fields) + ")"
|
return Field("string_agg(" + exp + ",'" + sep + "')")
|
||||||
}
|
}
|
||||||
|
|
||||||
func StringAgg(exp, sep string) string {
|
func StringAggCast(exp, sep string) Field {
|
||||||
return "string_agg(" + exp + ",'" + sep + "')"
|
return Field("string_agg(cast(" + exp + " as varchar),'" + sep + "')")
|
||||||
}
|
|
||||||
|
|
||||||
func StringAggCast(exp, sep string) string {
|
|
||||||
return "string_agg(cast(" + exp + " as varchar),'" + sep + "')"
|
|
||||||
}
|
}
|
||||||
|
@@ -1,3 +1,3 @@
|
|||||||
//go:generate go run code.patial.tech/go/pgm/cmd -o ./db ./schema.sql
|
//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 (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"code.patial.tech/go/pgm/example/db"
|
"code.patial.tech/go/pgm/playground/db"
|
||||||
"code.patial.tech/go/pgm/example/db/user"
|
"code.patial.tech/go/pgm/playground/db/user"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestDelete(t *testing.T) {
|
func TestDelete(t *testing.T) {
|
@@ -1,11 +1,11 @@
|
|||||||
package example
|
package playground
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"code.patial.tech/go/pgm"
|
"code.patial.tech/go/pgm"
|
||||||
"code.patial.tech/go/pgm/example/db"
|
"code.patial.tech/go/pgm/playground/db"
|
||||||
"code.patial.tech/go/pgm/example/db/user"
|
"code.patial.tech/go/pgm/playground/db/user"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestInsertQuery(t *testing.T) {
|
func TestInsertQuery(t *testing.T) {
|
@@ -1,14 +1,14 @@
|
|||||||
package example
|
package playground
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"code.patial.tech/go/pgm"
|
"code.patial.tech/go/pgm"
|
||||||
"code.patial.tech/go/pgm/example/db"
|
"code.patial.tech/go/pgm/playground/db"
|
||||||
"code.patial.tech/go/pgm/example/db/branchuser"
|
"code.patial.tech/go/pgm/playground/db/branchuser"
|
||||||
"code.patial.tech/go/pgm/example/db/employee"
|
"code.patial.tech/go/pgm/playground/db/employee"
|
||||||
"code.patial.tech/go/pgm/example/db/user"
|
"code.patial.tech/go/pgm/playground/db/user"
|
||||||
"code.patial.tech/go/pgm/example/db/usersession"
|
"code.patial.tech/go/pgm/playground/db/usersession"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestQryBuilder2(t *testing.T) {
|
func TestQryBuilder2(t *testing.T) {
|
||||||
@@ -30,6 +30,7 @@ func TestQryBuilder2(t *testing.T) {
|
|||||||
Where(
|
Where(
|
||||||
user.LastName.NEq(7),
|
user.LastName.NEq(7),
|
||||||
user.Phone.Like("%123%"),
|
user.Phone.Like("%123%"),
|
||||||
|
user.UpdatedAt.IsNotNull(),
|
||||||
user.Email.NotInSubQuery(db.User.Select(user.ID).Where(user.ID.Eq(123))),
|
user.Email.NotInSubQuery(db.User.Select(user.ID).Where(user.ID.Eq(123))),
|
||||||
).
|
).
|
||||||
Limit(10).
|
Limit(10).
|
||||||
@@ -39,7 +40,7 @@ func TestQryBuilder2(t *testing.T) {
|
|||||||
expected := "SELECT users.email, users.first_name FROM users JOIN user_sessions ON users.id = user_sessions.user_id" +
|
expected := "SELECT users.email, users.first_name FROM users JOIN user_sessions ON users.id = user_sessions.user_id" +
|
||||||
" JOIN branch_users ON users.id = branch_users.user_id WHERE users.id = $1 AND (users.status_id = $2 OR users.updated_at = $3)" +
|
" JOIN branch_users ON users.id = branch_users.user_id WHERE users.id = $1 AND (users.status_id = $2 OR users.updated_at = $3)" +
|
||||||
" AND users.mfa_kind = $4 AND (users.first_name = $5 OR users.middle_name = $6) AND users.last_name != $7 AND users.phone" +
|
" AND users.mfa_kind = $4 AND (users.first_name = $5 OR users.middle_name = $6) AND users.last_name != $7 AND users.phone" +
|
||||||
" LIKE $8 AND users.email NOT IN(SELECT users.id FROM users WHERE users.id = $9) LIMIT 10 OFFSET 100"
|
" LIKE $8 AND users.updated_at IS NOT NULL AND users.email NOT IN(SELECT users.id FROM users WHERE users.id = $9) LIMIT 10 OFFSET 100"
|
||||||
if expected != got {
|
if expected != got {
|
||||||
t.Errorf("\nexpected: %q\ngot: %q", expected, got)
|
t.Errorf("\nexpected: %q\ngot: %q", expected, got)
|
||||||
}
|
}
|
@@ -1,11 +1,11 @@
|
|||||||
package example
|
package playground
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"code.patial.tech/go/pgm"
|
"code.patial.tech/go/pgm"
|
||||||
"code.patial.tech/go/pgm/example/db"
|
"code.patial.tech/go/pgm/playground/db"
|
||||||
"code.patial.tech/go/pgm/example/db/user"
|
"code.patial.tech/go/pgm/playground/db/user"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestUpdateQuery(t *testing.T) {
|
func TestUpdateQuery(t *testing.T) {
|
56
pool.go
56
pool.go
@@ -24,40 +24,45 @@ var (
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
ErrInitTX = errors.New("failed to init db.tx")
|
ErrConnStringMissing = errors.New("connection string is empty")
|
||||||
ErrCommitTX = errors.New("failed to commit db.tx")
|
ErrInitTX = errors.New("failed to init db.tx")
|
||||||
ErrNoRows = errors.New("no data found")
|
ErrCommitTX = errors.New("failed to commit db.tx")
|
||||||
|
ErrNoRows = errors.New("no data found")
|
||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
|
ConnString string
|
||||||
MaxConns int32
|
MaxConns int32
|
||||||
MinConns int32
|
MinConns int32
|
||||||
MaxConnLifetime time.Duration
|
MaxConnLifetime time.Duration
|
||||||
MaxConnIdleTime time.Duration
|
MaxConnIdleTime time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
func Init(connString string, conf *Config) {
|
// InitPool will create new pgxpool.Pool and will keep it for its working
|
||||||
cfg, err := pgxpool.ParseConfig(connString)
|
func InitPool(conf Config) {
|
||||||
|
if conf.ConnString == "" {
|
||||||
|
panic(ErrConnStringMissing)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg, err := pgxpool.ParseConfig(conf.ConnString)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if conf != nil {
|
if conf.MaxConns > 0 {
|
||||||
if conf.MaxConns > 0 {
|
cfg.MaxConns = conf.MaxConns // 100
|
||||||
cfg.MaxConns = conf.MaxConns // 100
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if conf.MinConns > 0 {
|
if conf.MinConns > 0 {
|
||||||
cfg.MinConns = conf.MaxConns // 5
|
cfg.MinConns = conf.MaxConns // 5
|
||||||
}
|
}
|
||||||
|
|
||||||
if conf.MaxConnLifetime > 0 {
|
if conf.MaxConnLifetime > 0 {
|
||||||
cfg.MaxConnLifetime = conf.MaxConnLifetime // time.Minute * 10
|
cfg.MaxConnLifetime = conf.MaxConnLifetime // time.Minute * 10
|
||||||
}
|
}
|
||||||
|
|
||||||
if conf.MaxConnIdleTime > 0 {
|
if conf.MaxConnIdleTime > 0 {
|
||||||
cfg.MaxConnIdleTime = conf.MaxConnIdleTime // time.Minute * 5
|
cfg.MaxConnIdleTime = conf.MaxConnIdleTime // time.Minute * 5
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
p, err := pgxpool.NewWithConfig(context.Background(), cfg)
|
p, err := pgxpool.NewWithConfig(context.Background(), cfg)
|
||||||
@@ -72,10 +77,6 @@ func Init(connString string, conf *Config) {
|
|||||||
poolPGX.Store(p)
|
poolPGX.Store(p)
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetPool() *pgxpool.Pool {
|
|
||||||
return poolPGX.Load()
|
|
||||||
}
|
|
||||||
|
|
||||||
// get string builder from pool
|
// get string builder from pool
|
||||||
func getSB() *strings.Builder {
|
func getSB() *strings.Builder {
|
||||||
return poolStringBuilder.Get().(*strings.Builder)
|
return poolStringBuilder.Get().(*strings.Builder)
|
||||||
@@ -87,6 +88,12 @@ func putSB(sb *strings.Builder) {
|
|||||||
poolStringBuilder.Put(sb)
|
poolStringBuilder.Put(sb)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetPool instance
|
||||||
|
func GetPool() *pgxpool.Pool {
|
||||||
|
return poolPGX.Load()
|
||||||
|
}
|
||||||
|
|
||||||
|
// BeginTx begins a pgx poll transaction
|
||||||
func BeginTx(ctx context.Context) (pgx.Tx, error) {
|
func BeginTx(ctx context.Context) (pgx.Tx, error) {
|
||||||
tx, err := poolPGX.Load().Begin(ctx)
|
tx, err := poolPGX.Load().Begin(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -96,3 +103,8 @@ func BeginTx(ctx context.Context) (pgx.Tx, error) {
|
|||||||
|
|
||||||
return tx, err
|
return tx, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsNotFound error check
|
||||||
|
func IsNotFound(err error) bool {
|
||||||
|
return errors.Is(err, pgx.ErrNoRows)
|
||||||
|
}
|
||||||
|
12
qry.go
12
qry.go
@@ -208,12 +208,16 @@ func (cv *Cond) Condition(args *[]any, argIdx int) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 2. normal condition
|
// 2. normal condition
|
||||||
*args = append(*args, cv.Val)
|
|
||||||
var op string
|
var op string
|
||||||
if strings.HasSuffix(cv.op, "$") {
|
if cv.Val != nil {
|
||||||
op = cv.op + strconv.Itoa(argIdx+1)
|
*args = append(*args, cv.Val)
|
||||||
|
if strings.HasSuffix(cv.op, "$") {
|
||||||
|
op = cv.op + strconv.Itoa(argIdx+1)
|
||||||
|
} else {
|
||||||
|
op = strings.Replace(cv.op, "$", "$"+strconv.Itoa(argIdx+1), 1)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
op = strings.Replace(cv.op, "$", "$"+strconv.Itoa(argIdx+1), 1)
|
op = cv.op
|
||||||
}
|
}
|
||||||
|
|
||||||
if cv.action == CondActionNeedToClose {
|
if cv.action == CondActionNeedToClose {
|
||||||
|
Reference in New Issue
Block a user