Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2551e07b3e | |||
| 9837fb1e37 | |||
| 12d6fface6 | |||
| 325103e8ef | |||
| 6f5748d3d3 | |||
| b25f9367ed | |||
| 8750f3ad95 | |||
| ad1faf2056 | |||
| 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
|
||||
|
||||
example/local_*
|
||||
playground/local_*
|
||||
|
||||
7
Makefile
7
Makefile
@@ -1,4 +1,9 @@
|
||||
.PHONY: run bench-select test
|
||||
|
||||
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
|
||||
|
||||
test:
|
||||
go test ./playground
|
||||
|
||||
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
|
||||
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,10 +91,12 @@ 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 (")
|
||||
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, "_", " ")
|
||||
|
||||
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=
|
||||
|
||||
184
pgm.go
184
pgm.go
@@ -1,112 +1,103 @@
|
||||
// Patial Tech.
|
||||
// Author, Ankit Patial
|
||||
// pgm
|
||||
//
|
||||
// A simple PG string query builder
|
||||
//
|
||||
// Author: Ankit Patial
|
||||
|
||||
package pgm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"log/slog"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
// Table in database
|
||||
type Table struct {
|
||||
Name string
|
||||
PK []string
|
||||
FieldCount uint16
|
||||
debug bool
|
||||
var (
|
||||
poolPGX atomic.Pointer[pgxpool.Pool]
|
||||
ErrConnStringMissing = errors.New("connection string is empty")
|
||||
)
|
||||
|
||||
// Errors
|
||||
var (
|
||||
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
|
||||
}
|
||||
|
||||
// Debug when set true will print generated query string in stdout
|
||||
func (t Table) Debug() Clause {
|
||||
t.debug = true
|
||||
return t
|
||||
// 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)
|
||||
}
|
||||
|
||||
//
|
||||
// Field ==>
|
||||
//
|
||||
|
||||
// Field related to a table
|
||||
type Field string
|
||||
|
||||
func (f Field) Name() string {
|
||||
return strings.Split(string(f), ".")[1]
|
||||
// GetPool instance
|
||||
func GetPool() *pgxpool.Pool {
|
||||
return poolPGX.Load()
|
||||
}
|
||||
|
||||
func (f Field) String() string {
|
||||
return string(f)
|
||||
// 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
|
||||
}
|
||||
|
||||
// Count fn wrapping of field
|
||||
func (f Field) Count() Field {
|
||||
return Field("COUNT(" + f.String() + ")")
|
||||
// IsNotFound error check
|
||||
func IsNotFound(err error) bool {
|
||||
return errors.Is(err, pgx.ErrNoRows)
|
||||
}
|
||||
|
||||
// Avg fn wrapping of field
|
||||
func (f Field) Avg() Field {
|
||||
return Field("AVG(" + f.String() + ")")
|
||||
}
|
||||
|
||||
func (f Field) Eq(val any) Conditioner {
|
||||
col := f.String()
|
||||
return &Cond{Field: col, Val: val, op: " = $", len: len(col) + 5}
|
||||
}
|
||||
|
||||
// EqualFold will user LOWER() for comparision
|
||||
func (f Field) EqFold(val any) Conditioner {
|
||||
col := f.String()
|
||||
return &Cond{Field: "LOWER(" + col + ")", Val: val, op: " = LOWER($", action: CondActionNeedToClose, len: len(col) + 5}
|
||||
}
|
||||
|
||||
func (f Field) NEq(val any) Conditioner {
|
||||
col := f.String()
|
||||
return &Cond{Field: col, Val: val, op: " != $", len: len(col) + 5}
|
||||
}
|
||||
|
||||
func (f Field) Gt(val any) Conditioner {
|
||||
col := f.String()
|
||||
return &Cond{Field: col, Val: val, op: " > $", len: len(col) + 5}
|
||||
}
|
||||
|
||||
func (f Field) Gte(val any) Conditioner {
|
||||
col := f.String()
|
||||
return &Cond{Field: col, Val: val, op: " >= $", len: len(col) + 5}
|
||||
}
|
||||
|
||||
func (f Field) Like(val string) Conditioner {
|
||||
col := f.String()
|
||||
return &Cond{Field: col, Val: val, op: " LIKE $", len: len(f.String()) + 5}
|
||||
}
|
||||
|
||||
func (f Field) LikeFold(val string) Conditioner {
|
||||
col := f.String()
|
||||
return &Cond{Field: "LOWER(" + col + ")", Val: val, op: " LIKE LOWER($", action: CondActionNeedToClose, len: len(col) + 5}
|
||||
}
|
||||
|
||||
// ILIKE is case-insensitive
|
||||
func (f Field) ILike(val string) Conditioner {
|
||||
col := f.String()
|
||||
return &Cond{Field: col, Val: val, op: " ILIKE $", len: len(col) + 5}
|
||||
}
|
||||
|
||||
func (f Field) NotIn(val ...any) Conditioner {
|
||||
col := f.String()
|
||||
return &Cond{Field: col, Val: val, op: " NOT IN($", action: CondActionNeedToClose, len: len(col) + 5}
|
||||
}
|
||||
|
||||
func (f Field) NotInSubQuery(qry WhereClause) Conditioner {
|
||||
col := f.String()
|
||||
return &Cond{Field: col, Val: qry, op: " NOT IN($)", action: CondActionSubQuery}
|
||||
}
|
||||
|
||||
//
|
||||
// Helper func ==>
|
||||
//
|
||||
|
||||
// PgTime as in UTC
|
||||
func PgTime(t time.Time) pgtype.Timestamptz {
|
||||
return pgtype.Timestamptz{Time: t, Valid: true}
|
||||
@@ -115,20 +106,3 @@ func PgTime(t time.Time) pgtype.Timestamptz {
|
||||
func PgTimeNow() pgtype.Timestamptz {
|
||||
return pgtype.Timestamptz{Time: time.Now(), Valid: true}
|
||||
}
|
||||
|
||||
// IsNotFound error check
|
||||
func IsNotFound(err error) bool {
|
||||
return errors.Is(err, pgx.ErrNoRows)
|
||||
}
|
||||
|
||||
func ConcatWs(sep string, fields ...Field) string {
|
||||
return "concat_ws('" + sep + "'," + joinFileds(fields) + ")"
|
||||
}
|
||||
|
||||
func StringAgg(exp, sep string) string {
|
||||
return "string_agg(" + exp + ",'" + sep + "')"
|
||||
}
|
||||
|
||||
func StringAggCast(exp, sep string) string {
|
||||
return "string_agg(cast(" + exp + " as varchar),'" + sep + "')"
|
||||
}
|
||||
|
||||
238
pgm_field.go
Normal file
238
pgm_field.go
Normal file
@@ -0,0 +1,238 @@
|
||||
package pgm
|
||||
|
||||
import "strings"
|
||||
|
||||
// Field related to a table
|
||||
type Field string
|
||||
|
||||
func (f Field) Name() string {
|
||||
return strings.Split(string(f), ".")[1]
|
||||
}
|
||||
|
||||
func (f Field) String() string {
|
||||
return string(f)
|
||||
}
|
||||
|
||||
// Count function wrapped field
|
||||
func (f Field) Count() Field {
|
||||
return Field("COUNT(" + f.String() + ")")
|
||||
}
|
||||
|
||||
// StringEscape will wrap field with:
|
||||
//
|
||||
// COALESCE(field, ”)
|
||||
func (f Field) StringEscape() Field {
|
||||
return Field("COALESCE(" + f.String() + ", '')")
|
||||
}
|
||||
|
||||
// NumberEscape will wrap field with:
|
||||
//
|
||||
// COALESCE(field, 0)
|
||||
func (f Field) NumberEscape() Field {
|
||||
return Field("COALESCE(" + f.String() + ", 0)")
|
||||
}
|
||||
|
||||
// BooleanEscape will wrap field with:
|
||||
//
|
||||
// COALESCE(field, FALSE)
|
||||
func (f Field) BooleanEscape() Field {
|
||||
return Field("COALESCE(" + f.String() + ", FALSE)")
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
|
||||
func (f Field) RowNumber(as string) Field {
|
||||
return rowNumber(&f, nil, true, as)
|
||||
}
|
||||
|
||||
func (f Field) RowNumberDesc(as string) Field {
|
||||
return rowNumber(&f, nil, true, as)
|
||||
}
|
||||
|
||||
// RowNumberPartionBy in ascending order
|
||||
func (f Field) RowNumberPartionBy(partition Field, as string) Field {
|
||||
return rowNumber(&f, &partition, true, as)
|
||||
}
|
||||
|
||||
func (f Field) RowNumberDescPartionBy(partition Field, as string) Field {
|
||||
return rowNumber(&f, &partition, false, as)
|
||||
}
|
||||
|
||||
func rowNumber(f, partition *Field, isAsc bool, as string) Field {
|
||||
var orderBy string
|
||||
if isAsc {
|
||||
orderBy = " ASC"
|
||||
} else {
|
||||
orderBy = " DESC"
|
||||
}
|
||||
|
||||
if as == "" {
|
||||
as = "row_number"
|
||||
}
|
||||
|
||||
col := f.String()
|
||||
if partition != nil {
|
||||
return Field("ROW_NUMBER() OVER (PARTITION BY " + partition.String() + " ORDER BY " + col + orderBy + ") AS " + as)
|
||||
}
|
||||
|
||||
return Field("ROW_NUMBER() OVER (ORDER BY " + col + orderBy + ") AS " + as)
|
||||
}
|
||||
|
||||
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}
|
||||
}
|
||||
|
||||
// DateTrunc will truncate date or timestamp to specified level of precision
|
||||
//
|
||||
// Level values:
|
||||
// - microseconds, milliseconds, second, minute, hour
|
||||
// - day, week (Monday start), month, quarter, year
|
||||
// - decade, century, millennium
|
||||
func (f Field) DateTrunc(level, as string) Field {
|
||||
return Field("DATE_TRUNC('" + level + "', " + f.String() + ") AS " + as)
|
||||
}
|
||||
|
||||
// EqualFold will use LOWER(column_name) = LOWER(val) for comparision
|
||||
func (f Field) EqFold(val string) Conditioner {
|
||||
col := f.String()
|
||||
return &Cond{Field: "LOWER(" + col + ")", Val: val, op: " = LOWER($", action: CondActionNeedToClose, len: len(col) + 5}
|
||||
}
|
||||
|
||||
// Eq is equal
|
||||
func (f Field) Eq(val any) Conditioner {
|
||||
col := f.String()
|
||||
return &Cond{Field: col, Val: val, op: " = $", len: len(col) + 5}
|
||||
}
|
||||
|
||||
func (f Field) NotEq(val any) Conditioner {
|
||||
col := f.String()
|
||||
return &Cond{Field: col, Val: val, op: " != $", len: len(col) + 5}
|
||||
}
|
||||
|
||||
func (f Field) Gt(val any) Conditioner {
|
||||
col := f.String()
|
||||
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 {
|
||||
col := f.String()
|
||||
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 {
|
||||
col := f.String()
|
||||
return &Cond{Field: col, Val: val, op: " LIKE $", len: len(f.String()) + 5}
|
||||
}
|
||||
|
||||
func (f Field) LikeFold(val string) Conditioner {
|
||||
col := f.String()
|
||||
return &Cond{Field: "LOWER(" + col + ")", Val: val, op: " LIKE LOWER($", action: CondActionNeedToClose, len: len(col) + 5}
|
||||
}
|
||||
|
||||
// ILIKE is case-insensitive
|
||||
func (f Field) ILike(val string) Conditioner {
|
||||
col := f.String()
|
||||
return &Cond{Field: col, Val: val, op: " ILIKE $", len: len(col) + 5}
|
||||
}
|
||||
|
||||
func (f Field) Any(val ...any) Conditioner {
|
||||
col := f.String()
|
||||
return &Cond{Field: col, Val: val, op: " = ANY($", action: CondActionNeedToClose, len: len(col) + 5}
|
||||
}
|
||||
|
||||
func (f Field) NotAny(val ...any) Conditioner {
|
||||
col := f.String()
|
||||
return &Cond{Field: col, Val: val, op: " != ANY($", action: CondActionNeedToClose, len: len(col) + 5}
|
||||
}
|
||||
|
||||
// NotInSubQuery using ANY
|
||||
func (f Field) NotInSubQuery(qry WhereClause) Conditioner {
|
||||
col := f.String()
|
||||
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)
|
||||
for i, f := range fields {
|
||||
if i == 0 {
|
||||
sb.WriteString(f.String())
|
||||
} else {
|
||||
sb.WriteString(", ")
|
||||
sb.WriteString(f.String())
|
||||
}
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
71
pgm_table.go
Normal file
71
pgm_table.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package pgm
|
||||
|
||||
// Table in database
|
||||
type Table struct {
|
||||
Name string
|
||||
DerivedTable Query
|
||||
PK []string
|
||||
FieldCount uint16
|
||||
debug bool
|
||||
}
|
||||
|
||||
// Debug when set true will print generated query string in stdout
|
||||
func (t *Table) Debug() Clause {
|
||||
t.debug = true
|
||||
return t
|
||||
}
|
||||
|
||||
func (t *Table) Field(f string) Field {
|
||||
return Field(t.Name + "." + f)
|
||||
}
|
||||
|
||||
// Insert table statement
|
||||
func (t *Table) Insert() InsertClause {
|
||||
qb := &insertQry{
|
||||
debug: t.debug,
|
||||
table: t.Name,
|
||||
fields: make([]string, 0, t.FieldCount),
|
||||
vals: make([]string, 0, t.FieldCount),
|
||||
args: make([]any, 0, t.FieldCount),
|
||||
}
|
||||
return qb
|
||||
}
|
||||
|
||||
// Select table statement
|
||||
func (t *Table) Select(field ...Field) SelectClause {
|
||||
qb := &selectQry{
|
||||
debug: t.debug,
|
||||
fields: field,
|
||||
}
|
||||
|
||||
if t.DerivedTable != nil {
|
||||
tName, args := t.DerivedTable.Build(true)
|
||||
qb.table = "(" + tName + ") AS " + t.Name
|
||||
qb.args = args
|
||||
} else {
|
||||
qb.table = t.Name
|
||||
}
|
||||
|
||||
return qb
|
||||
}
|
||||
|
||||
// Update table statement
|
||||
func (t *Table) Update() UpdateClause {
|
||||
qb := &updateQry{
|
||||
debug: t.debug,
|
||||
table: t.Name,
|
||||
cols: make([]string, 0, t.FieldCount),
|
||||
args: make([]any, 0, t.FieldCount),
|
||||
}
|
||||
return qb
|
||||
}
|
||||
|
||||
// Detlete table statement
|
||||
func (t *Table) Delete() DeleteCluase {
|
||||
qb := &deleteQry{
|
||||
debug: t.debug,
|
||||
table: t.Name,
|
||||
}
|
||||
|
||||
return qb
|
||||
}
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
11
playground/db/derived.go
Normal file
11
playground/db/derived.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package db
|
||||
|
||||
import "code.patial.tech/go/pgm"
|
||||
|
||||
func DerivedTable(tblName string, fromQry pgm.Query) pgm.Table {
|
||||
t := pgm.Table{
|
||||
Name: tblName,
|
||||
DerivedTable: fromQry,
|
||||
}
|
||||
return t
|
||||
}
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -1,3 +1,3 @@
|
||||
//go:generate go run code.patial.tech/go/pgm/cmd -o ./db ./schema.sql
|
||||
|
||||
package example
|
||||
package playground
|
||||
@@ -1,16 +1,16 @@
|
||||
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) {
|
||||
expected := "DELETE FROM users WHERE users.id = $1 AND users.status_id NOT IN($2)"
|
||||
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)
|
||||
@@ -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) {
|
||||
@@ -23,23 +23,6 @@ func TestInsertQuery(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestInsertSetMap(t *testing.T) {
|
||||
got := db.User.Insert().
|
||||
SetMap(map[pgm.Field]any{
|
||||
user.Email: "aa@aa.com",
|
||||
user.Phone: 8889991234,
|
||||
user.FirstName: "fname",
|
||||
user.LastName: "lname",
|
||||
}).
|
||||
Returning(user.ID).
|
||||
String()
|
||||
|
||||
expected := "INSERT INTO users(email, phone, first_name, last_name) VALUES($1, $2, $3, $4) RETURNING id"
|
||||
if got != expected {
|
||||
t.Errorf("\nexpected: %q\ngot: %q", expected, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInsertQuery2(t *testing.T) {
|
||||
got := db.User.Insert().
|
||||
Set(user.Email, "aa@aa.com").
|
||||
@@ -53,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().
|
||||
@@ -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) {
|
||||
@@ -28,8 +28,9 @@ func TestQryBuilder2(t *testing.T) {
|
||||
),
|
||||
).
|
||||
Where(
|
||||
user.LastName.NEq(7),
|
||||
user.LastName.NotEq(7),
|
||||
user.Phone.Like("%123%"),
|
||||
user.UpdatedAt.IsNotNull(),
|
||||
user.Email.NotInSubQuery(db.User.Select(user.ID).Where(user.ID.Eq(123))),
|
||||
).
|
||||
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" +
|
||||
" 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" +
|
||||
" 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 != ANY(SELECT users.id FROM users WHERE users.id = $9) LIMIT 10 OFFSET 100"
|
||||
if expected != got {
|
||||
t.Errorf("\nexpected: %q\ngot: %q", expected, got)
|
||||
}
|
||||
@@ -59,7 +60,52 @@ func TestSelectWithHaving(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkSelect-12 668817 1753 ns/op 4442 B/op 59 allocs/op
|
||||
func TestSelectDerived(t *testing.T) {
|
||||
expected := "SELECT t.* FROM (SELECT users.*, ROW_NUMBER() OVER (PARTITION BY users.status_id ORDER BY users.created_at DESC) AS rn" +
|
||||
" FROM users WHERE users.status_id = $1) AS t WHERE t.rn <= $2" +
|
||||
" ORDER BY t.status_id, t.created_at DESC"
|
||||
|
||||
qry := db.User.
|
||||
Select(user.All, user.CreatedAt.RowNumberDescPartionBy(user.StatusID, "rn")).
|
||||
Where(user.StatusID.Eq(1))
|
||||
|
||||
tbl := db.DerivedTable("t", qry)
|
||||
got := tbl.
|
||||
Select(tbl.Field("*")).
|
||||
Where(tbl.Field("rn").Lte(5)).
|
||||
OrderBy(tbl.Field("status_id"), tbl.Field("created_at").Desc()).
|
||||
String()
|
||||
|
||||
if expected != got {
|
||||
t.Errorf("\nexpected: %q\n\ngot: %q", expected, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectWithJoin(t *testing.T) {
|
||||
got := db.User.Select(user.Email, user.FirstName).
|
||||
Join(db.UserSession, user.ID, usersession.UserID).
|
||||
LeftJoin(db.BranchUser, user.ID, branchuser.UserID, pgm.Or(branchuser.RoleID.Eq("1"), branchuser.RoleID.Eq("2"))).
|
||||
Where(
|
||||
user.ID.Eq(3),
|
||||
pgm.Or(
|
||||
user.StatusID.Eq(4),
|
||||
user.UpdatedAt.Eq(5),
|
||||
),
|
||||
).
|
||||
Limit(10).
|
||||
Offset(100).
|
||||
String()
|
||||
|
||||
expected := "SELECT users.email, users.first_name " +
|
||||
"FROM users JOIN user_sessions ON users.id = user_sessions.user_id " +
|
||||
"LEFT JOIN branch_users ON users.id = branch_users.user_id AND (branch_users.role_id = $1 OR branch_users.role_id = $2) " +
|
||||
"WHERE users.id = $3 AND (users.status_id = $4 OR users.updated_at = $5) " +
|
||||
"LIMIT 10 OFFSET 100"
|
||||
if expected != got {
|
||||
t.Errorf("\nexpected: %q\ngot: %q", expected, got)
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkSelect-12 638901 1860 ns/op 4266 B/op 61 allocs/op
|
||||
func BenchmarkSelect(b *testing.B) {
|
||||
for b.Loop() {
|
||||
@@ -79,7 +125,7 @@ func BenchmarkSelect(b *testing.B) {
|
||||
),
|
||||
).
|
||||
Where(
|
||||
user.LastName.NEq(7),
|
||||
user.LastName.NotEq(7),
|
||||
user.Phone.Like("%123%"),
|
||||
user.Email.NotInSubQuery(db.User.Select(user.ID).Where(user.ID.Eq(123))),
|
||||
).
|
||||
@@ -1,11 +1,10 @@
|
||||
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) {
|
||||
@@ -17,28 +16,7 @@ func TestUpdateQuery(t *testing.T) {
|
||||
user.Email.Eq("aa@aa.com"),
|
||||
).
|
||||
Where(
|
||||
user.StatusID.NEq(1),
|
||||
).
|
||||
String()
|
||||
|
||||
expected := "UPDATE users SET first_name=$1, middle_name=$2, last_name=$3 WHERE users.email = $4 AND users.status_id != $5"
|
||||
if got != expected {
|
||||
t.Errorf("\nexpected: %q\ngot: %q", expected, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateSetMap(t *testing.T) {
|
||||
got := db.User.Update().
|
||||
SetMap(map[pgm.Field]any{
|
||||
user.FirstName: "ankit",
|
||||
user.MiddleName: "singh",
|
||||
user.LastName: "patial",
|
||||
}).
|
||||
Where(
|
||||
user.Email.Eq("aa@aa.com"),
|
||||
).
|
||||
Where(
|
||||
user.StatusID.NEq(1),
|
||||
user.StatusID.NotEq(1),
|
||||
).
|
||||
String()
|
||||
|
||||
@@ -59,7 +37,7 @@ func BenchmarkUpdateQuery(b *testing.B) {
|
||||
user.Email.Eq("aa@aa.com"),
|
||||
).
|
||||
Where(
|
||||
user.StatusID.NEq(1),
|
||||
user.StatusID.NotEq(1),
|
||||
).
|
||||
String()
|
||||
}
|
||||
98
pool.go
98
pool.go
@@ -1,98 +0,0 @@
|
||||
// Patial Tech.
|
||||
// Author, Ankit Patial
|
||||
|
||||
package pgm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"log/slog"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
var (
|
||||
poolPGX atomic.Pointer[pgxpool.Pool]
|
||||
poolStringBuilder = sync.Pool{
|
||||
New: func() any {
|
||||
return new(strings.Builder)
|
||||
},
|
||||
}
|
||||
|
||||
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 {
|
||||
MaxConns int32
|
||||
MinConns int32
|
||||
MaxConnLifetime time.Duration
|
||||
MaxConnIdleTime time.Duration
|
||||
}
|
||||
|
||||
func Init(connString string, conf *Config) {
|
||||
cfg, err := pgxpool.ParseConfig(connString)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if conf != nil {
|
||||
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)
|
||||
}
|
||||
|
||||
func GetPool() *pgxpool.Pool {
|
||||
return poolPGX.Load()
|
||||
}
|
||||
|
||||
// get string builder from pool
|
||||
func getSB() *strings.Builder {
|
||||
return poolStringBuilder.Get().(*strings.Builder)
|
||||
}
|
||||
|
||||
// put string builder back to pool
|
||||
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
|
||||
}
|
||||
191
qry.go
191
qry.go
@@ -7,156 +7,17 @@ import (
|
||||
"context"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
type (
|
||||
Clause interface {
|
||||
Insert() InsertClause
|
||||
Select(fields ...Field) SelectClause
|
||||
// Insert() InsertSet
|
||||
// Update() UpdateSet
|
||||
// Delete() WhereOrExec
|
||||
}
|
||||
|
||||
SelectClause interface {
|
||||
// Join and Inner Join are same
|
||||
Join(m Table, t1Field, t2Field Field) SelectClause
|
||||
LeftJoin(m Table, t1Field, t2Field Field) SelectClause
|
||||
RightJoin(m Table, t1Field, t2Field Field) SelectClause
|
||||
FullJoin(m Table, t1Field, t2Field Field) SelectClause
|
||||
CrossJoin(m Table) SelectClause
|
||||
WhereClause
|
||||
OrderByClause
|
||||
GroupByClause
|
||||
LimitClause
|
||||
OffsetClause
|
||||
Query
|
||||
raw(prefixArgs []any) (string, []any)
|
||||
}
|
||||
|
||||
WhereClause interface {
|
||||
Where(cond ...Conditioner) AfterWhere
|
||||
}
|
||||
|
||||
AfterWhere interface {
|
||||
WhereClause
|
||||
GroupByClause
|
||||
OrderByClause
|
||||
LimitClause
|
||||
OffsetClause
|
||||
Query
|
||||
}
|
||||
|
||||
GroupByClause interface {
|
||||
GroupBy(fields ...Field) AfterGroupBy
|
||||
}
|
||||
|
||||
AfterGroupBy interface {
|
||||
HavinClause
|
||||
OrderByClause
|
||||
LimitClause
|
||||
OffsetClause
|
||||
Query
|
||||
}
|
||||
|
||||
HavinClause interface {
|
||||
Having(cond ...Conditioner) AfterHaving
|
||||
}
|
||||
|
||||
AfterHaving interface {
|
||||
OrderByClause
|
||||
LimitClause
|
||||
OffsetClause
|
||||
Query
|
||||
}
|
||||
|
||||
OrderByClause interface {
|
||||
OrderBy(fields ...Field) AfterOrderBy
|
||||
}
|
||||
|
||||
AfterOrderBy interface {
|
||||
LimitClause
|
||||
OffsetClause
|
||||
Query
|
||||
}
|
||||
|
||||
LimitClause interface {
|
||||
Limit(v int) AfterLimit
|
||||
}
|
||||
|
||||
AfterLimit interface {
|
||||
OffsetClause
|
||||
Query
|
||||
}
|
||||
|
||||
OffsetClause interface {
|
||||
Offset(v int) AfterOffset
|
||||
}
|
||||
|
||||
AfterOffset interface {
|
||||
LimitClause
|
||||
Query
|
||||
}
|
||||
|
||||
Conditioner interface {
|
||||
Condition(args *[]any, idx int) string
|
||||
}
|
||||
|
||||
Insert interface {
|
||||
Set(field Field, val any) InsertClause
|
||||
SetMap(fields map[Field]any) InsertClause
|
||||
}
|
||||
|
||||
InsertClause interface {
|
||||
Insert
|
||||
Returning(field Field) First
|
||||
OnConflict(fields ...Field) Do
|
||||
Execute
|
||||
Stringer
|
||||
}
|
||||
|
||||
Do interface {
|
||||
DoNothing() Execute
|
||||
DoUpdate(fields ...Field) Execute
|
||||
}
|
||||
|
||||
Update interface {
|
||||
Set(field Field, val any) UpdateClause
|
||||
SetMap(fields map[Field]any) UpdateClause
|
||||
}
|
||||
|
||||
UpdateClause interface {
|
||||
Update
|
||||
Where(cond ...Conditioner) WhereOrExec
|
||||
}
|
||||
|
||||
WhereOrExec interface {
|
||||
Where(cond ...Conditioner) WhereOrExec
|
||||
Execute
|
||||
}
|
||||
|
||||
Query interface {
|
||||
First
|
||||
All
|
||||
Stringer
|
||||
}
|
||||
|
||||
First interface {
|
||||
First(ctx context.Context, dest ...any) error
|
||||
FirstTx(ctx context.Context, tx pgx.Tx, dest ...any) error
|
||||
Stringer
|
||||
}
|
||||
|
||||
All interface {
|
||||
// Query rows
|
||||
//
|
||||
// don't forget to close() rows
|
||||
All(ctx context.Context, rows RowsCb) error
|
||||
// Query rows
|
||||
//
|
||||
// don't forget to close() rows
|
||||
AllTx(ctx context.Context, tx pgx.Tx, rows RowsCb) error
|
||||
Update() UpdateClause
|
||||
Delete() DeleteCluase
|
||||
}
|
||||
|
||||
Execute interface {
|
||||
@@ -169,26 +30,26 @@ type (
|
||||
String() string
|
||||
}
|
||||
|
||||
RowScanner interface {
|
||||
Scan(dest ...any) error
|
||||
Conditioner interface {
|
||||
Condition(args *[]any, idx int) string
|
||||
}
|
||||
|
||||
RowsCb func(row RowScanner) error
|
||||
)
|
||||
|
||||
func joinFileds(fields []Field) string {
|
||||
sb := getSB()
|
||||
defer putSB(sb)
|
||||
for i, f := range fields {
|
||||
if i == 0 {
|
||||
sb.WriteString(f.String())
|
||||
} else {
|
||||
sb.WriteString(", ")
|
||||
sb.WriteString(f.String())
|
||||
}
|
||||
}
|
||||
var sbPool = sync.Pool{
|
||||
New: func() any {
|
||||
return new(strings.Builder)
|
||||
},
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
// 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 {
|
||||
@@ -208,12 +69,16 @@ func (cv *Cond) Condition(args *[]any, argIdx int) string {
|
||||
}
|
||||
|
||||
// 2. normal condition
|
||||
*args = append(*args, cv.Val)
|
||||
var op string
|
||||
if strings.HasSuffix(cv.op, "$") {
|
||||
op = cv.op + strconv.Itoa(argIdx+1)
|
||||
if cv.Val != nil {
|
||||
*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 {
|
||||
op = strings.Replace(cv.op, "$", "$"+strconv.Itoa(argIdx+1), 1)
|
||||
op = cv.op
|
||||
}
|
||||
|
||||
if cv.action == CondActionNeedToClose {
|
||||
|
||||
@@ -7,6 +7,10 @@ import (
|
||||
)
|
||||
|
||||
type (
|
||||
DeleteCluase interface {
|
||||
WhereOrExec
|
||||
}
|
||||
|
||||
deleteQry struct {
|
||||
table string
|
||||
condition []Conditioner
|
||||
@@ -15,15 +19,6 @@ type (
|
||||
}
|
||||
)
|
||||
|
||||
func (t *Table) Delete() WhereOrExec {
|
||||
qb := &deleteQry{
|
||||
table: t.Name,
|
||||
debug: t.debug,
|
||||
}
|
||||
|
||||
return qb
|
||||
}
|
||||
|
||||
func (q *deleteQry) Where(cond ...Conditioner) WhereOrExec {
|
||||
q.condition = append(q.condition, cond...)
|
||||
return q
|
||||
|
||||
@@ -12,29 +12,33 @@ import (
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
type insertQry struct {
|
||||
returing *string
|
||||
onConflict *string
|
||||
|
||||
table string
|
||||
conflictAction string
|
||||
|
||||
fields []string
|
||||
vals []string
|
||||
args []any
|
||||
debug bool
|
||||
}
|
||||
|
||||
func (t *Table) Insert() Insert {
|
||||
qb := &insertQry{
|
||||
table: t.Name,
|
||||
fields: make([]string, 0, t.FieldCount),
|
||||
vals: make([]string, 0, t.FieldCount),
|
||||
args: make([]any, 0, t.FieldCount),
|
||||
debug: t.debug,
|
||||
type (
|
||||
InsertClause interface {
|
||||
Insert
|
||||
Returning(field Field) First
|
||||
OnConflict(fields ...Field) Do
|
||||
Execute
|
||||
Stringer
|
||||
}
|
||||
return qb
|
||||
}
|
||||
|
||||
Insert interface {
|
||||
Set(field Field, val any) InsertClause
|
||||
SetMap(fields map[Field]any) InsertClause
|
||||
}
|
||||
|
||||
insertQry struct {
|
||||
returing *string
|
||||
onConflict *string
|
||||
|
||||
table string
|
||||
conflictAction string
|
||||
|
||||
fields []string
|
||||
vals []string
|
||||
args []any
|
||||
debug bool
|
||||
}
|
||||
)
|
||||
|
||||
func (q *insertQry) Set(field Field, val any) InsertClause {
|
||||
q.fields = append(q.fields, field.Name())
|
||||
|
||||
206
qry_select.go
206
qry_select.go
@@ -7,6 +7,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@@ -14,6 +15,125 @@ import (
|
||||
)
|
||||
|
||||
type (
|
||||
SelectClause interface {
|
||||
// Join and Inner Join are same
|
||||
Join(m Table, t1Field, t2Field Field, cond ...Conditioner) SelectClause
|
||||
LeftJoin(m Table, t1Field, t2Field Field, cond ...Conditioner) SelectClause
|
||||
RightJoin(m Table, t1Field, t2Field Field, cond ...Conditioner) SelectClause
|
||||
FullJoin(m Table, t1Field, t2Field Field, cond ...Conditioner) SelectClause
|
||||
CrossJoin(m Table) SelectClause
|
||||
WhereClause
|
||||
OrderByClause
|
||||
GroupByClause
|
||||
LimitClause
|
||||
OffsetClause
|
||||
Query
|
||||
raw(prefixArgs []any) (string, []any)
|
||||
}
|
||||
|
||||
WhereClause interface {
|
||||
Where(cond ...Conditioner) AfterWhere
|
||||
}
|
||||
|
||||
AfterWhere interface {
|
||||
WhereClause
|
||||
GroupByClause
|
||||
OrderByClause
|
||||
LimitClause
|
||||
OffsetClause
|
||||
Query
|
||||
}
|
||||
|
||||
GroupByClause interface {
|
||||
GroupBy(fields ...Field) AfterGroupBy
|
||||
}
|
||||
|
||||
AfterGroupBy interface {
|
||||
HavinClause
|
||||
OrderByClause
|
||||
LimitClause
|
||||
OffsetClause
|
||||
Query
|
||||
}
|
||||
|
||||
HavinClause interface {
|
||||
Having(cond ...Conditioner) AfterHaving
|
||||
}
|
||||
|
||||
AfterHaving interface {
|
||||
OrderByClause
|
||||
LimitClause
|
||||
OffsetClause
|
||||
Query
|
||||
}
|
||||
|
||||
OrderByClause interface {
|
||||
OrderBy(fields ...Field) AfterOrderBy
|
||||
}
|
||||
|
||||
AfterOrderBy interface {
|
||||
LimitClause
|
||||
OffsetClause
|
||||
Query
|
||||
}
|
||||
|
||||
LimitClause interface {
|
||||
Limit(v int) AfterLimit
|
||||
}
|
||||
|
||||
AfterLimit interface {
|
||||
OffsetClause
|
||||
Query
|
||||
}
|
||||
|
||||
OffsetClause interface {
|
||||
Offset(v int) AfterOffset
|
||||
}
|
||||
|
||||
AfterOffset interface {
|
||||
LimitClause
|
||||
Query
|
||||
}
|
||||
|
||||
Do interface {
|
||||
DoNothing() Execute
|
||||
DoUpdate(fields ...Field) Execute
|
||||
}
|
||||
|
||||
Query interface {
|
||||
First
|
||||
All
|
||||
Stringer
|
||||
Bulder
|
||||
}
|
||||
|
||||
RowScanner interface {
|
||||
Scan(dest ...any) error
|
||||
}
|
||||
|
||||
RowsCb func(row RowScanner) error
|
||||
|
||||
First interface {
|
||||
First(ctx context.Context, dest ...any) error
|
||||
FirstTx(ctx context.Context, tx pgx.Tx, dest ...any) error
|
||||
Stringer
|
||||
}
|
||||
|
||||
All interface {
|
||||
// Query rows
|
||||
//
|
||||
// don't forget to close() rows
|
||||
All(ctx context.Context, rows RowsCb) error
|
||||
// Query rows
|
||||
//
|
||||
// don't forget to close() rows
|
||||
AllTx(ctx context.Context, tx pgx.Tx, rows RowsCb) error
|
||||
}
|
||||
|
||||
Bulder interface {
|
||||
Build(needArgs bool) (qry string, args []any)
|
||||
}
|
||||
|
||||
selectQry struct {
|
||||
table string
|
||||
fields []Field
|
||||
@@ -23,9 +143,11 @@ type (
|
||||
groupBy []Field
|
||||
having []Conditioner
|
||||
orderBy []Field
|
||||
limit int
|
||||
offset int
|
||||
debug bool
|
||||
|
||||
limit int
|
||||
offset int
|
||||
|
||||
debug bool
|
||||
}
|
||||
|
||||
CondAction uint8
|
||||
@@ -51,34 +173,46 @@ const (
|
||||
CondActionSubQuery
|
||||
)
|
||||
|
||||
// Select clause
|
||||
func (t Table) Select(field ...Field) SelectClause {
|
||||
qb := &selectQry{
|
||||
table: t.Name,
|
||||
debug: t.debug,
|
||||
fields: field,
|
||||
func (q *selectQry) Join(t Table, t1Field, t2Field Field, cond ...Conditioner) SelectClause {
|
||||
return q.buildJoin(t, "JOIN", t1Field, t2Field, cond...)
|
||||
}
|
||||
|
||||
func (q *selectQry) LeftJoin(t Table, t1Field, t2Field Field, cond ...Conditioner) SelectClause {
|
||||
return q.buildJoin(t, "LEFT JOIN", t1Field, t2Field, cond...)
|
||||
}
|
||||
|
||||
func (q *selectQry) RightJoin(t Table, t1Field, t2Field Field, cond ...Conditioner) SelectClause {
|
||||
return q.buildJoin(t, "RIGHT JOIN", t1Field, t2Field, cond...)
|
||||
}
|
||||
|
||||
func (q *selectQry) FullJoin(t Table, t1Field, t2Field Field, cond ...Conditioner) SelectClause {
|
||||
return q.buildJoin(t, "FULL JOIN", t1Field, t2Field, cond...)
|
||||
}
|
||||
|
||||
func (q *selectQry) buildJoin(t Table, joinKW string, t1Field, t2Field Field, cond ...Conditioner) SelectClause {
|
||||
str := joinKW + " " + t.Name + " ON " + t1Field.String() + " = " + t2Field.String()
|
||||
if len(cond) == 0 { // Join with no condition
|
||||
q.join = append(q.join, str)
|
||||
return q
|
||||
}
|
||||
|
||||
return qb
|
||||
}
|
||||
// Join has condition(s)
|
||||
sb := getSB()
|
||||
defer putSB(sb)
|
||||
sb.Grow(len(str) * 2)
|
||||
|
||||
func (q *selectQry) Join(t Table, t1Field, t2Field Field) SelectClause {
|
||||
q.join = append(q.join, "JOIN "+t.Name+" ON "+t1Field.String()+" = "+t2Field.String())
|
||||
return q
|
||||
}
|
||||
sb.WriteString(str + " AND ")
|
||||
|
||||
func (q *selectQry) LeftJoin(t Table, t1Field, t2Field Field) SelectClause {
|
||||
q.join = append(q.join, "LEFT JOIN "+t.Name+" ON "+t1Field.String()+" = "+t2Field.String())
|
||||
return q
|
||||
}
|
||||
var argIdx int
|
||||
for i, c := range cond {
|
||||
argIdx = len(q.args)
|
||||
if i > 0 {
|
||||
sb.WriteString(" AND ")
|
||||
}
|
||||
sb.WriteString(c.Condition(&q.args, argIdx))
|
||||
}
|
||||
|
||||
func (q *selectQry) RightJoin(t Table, t1Field, t2Field Field) SelectClause {
|
||||
q.join = append(q.join, "RIGHT JOIN "+t.Name+" ON "+t1Field.String()+" = "+t2Field.String())
|
||||
return q
|
||||
}
|
||||
|
||||
func (q *selectQry) FullJoin(t Table, t1Field, t2Field Field) SelectClause {
|
||||
q.join = append(q.join, "FULL JOIN "+t.Name+" ON "+t1Field.String()+" = "+t2Field.String())
|
||||
q.join = append(q.join, sb.String())
|
||||
return q
|
||||
}
|
||||
|
||||
@@ -163,6 +297,17 @@ func (q *selectQry) raw(prefixArgs []any) (string, []any) {
|
||||
}
|
||||
|
||||
func (q *selectQry) String() string {
|
||||
qry, _ := q.Build(false)
|
||||
if q.debug {
|
||||
fmt.Println("***")
|
||||
fmt.Println(qry)
|
||||
fmt.Printf("%+v\n", q.args)
|
||||
fmt.Println("***")
|
||||
}
|
||||
return qry
|
||||
}
|
||||
|
||||
func (q *selectQry) Build(needArgs bool) (qry string, args []any) {
|
||||
sb := getSB()
|
||||
defer putSB(sb)
|
||||
|
||||
@@ -228,14 +373,19 @@ func (q *selectQry) String() string {
|
||||
sb.WriteString(strconv.Itoa(q.offset))
|
||||
}
|
||||
|
||||
qry := sb.String()
|
||||
qry = sb.String()
|
||||
if q.debug {
|
||||
fmt.Println("***")
|
||||
fmt.Println(qry)
|
||||
fmt.Printf("%+v\n", q.args)
|
||||
fmt.Println("***")
|
||||
}
|
||||
return qry
|
||||
|
||||
if needArgs {
|
||||
args = slices.Clone(q.args)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (q *selectQry) averageLen() int {
|
||||
|
||||
@@ -11,23 +11,30 @@ import (
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
type updateQry struct {
|
||||
table string
|
||||
cols []string
|
||||
condition []Conditioner
|
||||
args []any
|
||||
debug bool
|
||||
}
|
||||
|
||||
func (t *Table) Update() Update {
|
||||
qb := &updateQry{
|
||||
table: t.Name,
|
||||
debug: t.debug,
|
||||
cols: make([]string, 0, t.FieldCount),
|
||||
args: make([]any, 0, t.FieldCount),
|
||||
type (
|
||||
Update interface {
|
||||
Set(field Field, val any) UpdateClause
|
||||
SetMap(fields map[Field]any) UpdateClause
|
||||
}
|
||||
return qb
|
||||
}
|
||||
|
||||
UpdateClause interface {
|
||||
Update
|
||||
Where(cond ...Conditioner) WhereOrExec
|
||||
}
|
||||
|
||||
WhereOrExec interface {
|
||||
Where(cond ...Conditioner) WhereOrExec
|
||||
Execute
|
||||
}
|
||||
|
||||
updateQry struct {
|
||||
table string
|
||||
cols []string
|
||||
condition []Conditioner
|
||||
args []any
|
||||
debug bool
|
||||
}
|
||||
)
|
||||
|
||||
func (q *updateQry) Set(field Field, val any) UpdateClause {
|
||||
col := field.Name()
|
||||
|
||||
Reference in New Issue
Block a user