21 Commits

Author SHA1 Message Date
2551e07b3e Field, date truncate method 2025-10-21 16:45:43 +05:30
9837fb1e37 derived table and row number addition 2025-10-21 00:27:00 +05:30
12d6fface6 table all field 2025-10-18 14:43:42 +05:30
325103e8ef restructuring of files. added 2 new methods(Asc, Desc) to field 2025-10-18 12:31:46 +05:30
6f5748d3d3 fix 2025-08-11 23:45:36 +05:30
b25f9367ed using any for In/NotIn 2025-08-11 23:42:58 +05:30
8750f3ad95 method "In" 2025-08-11 22:37:25 +05:30
ad1faf2056 join with conditions 2025-08-10 20:26:09 +05:30
525c64e678 removed toLower 2025-08-10 12:54:29 +05:30
5f0fdadb8b undo, strings.ToLower 2025-08-10 12:24:26 +05:30
68263895f7 EqFold to do value lower case 2025-08-10 11:59:01 +05:30
ee6cb445ab remove unwanted method args 2025-08-03 22:21:24 +05:30
d07c25fe01 COALESCE related methods 2025-08-03 22:17:58 +05:30
096480a3eb field IsNull, IsNotNull methods 2025-08-03 21:43:46 +05:30
6c14441591 added in few field wraping functions 2025-08-03 21:43:46 +05:30
d95eea6636 code example correction 2025-07-27 06:12:11 +00:00
63b71692b5 go example 2025-07-27 06:10:24 +00:00
36e4145365 few corrections 2025-07-27 06:08:27 +00:00
2ec328059f func return type change 2025-07-26 21:45:09 +05:30
f700f3e891 refactor, generator comment change 2025-07-26 20:22:16 +05:30
f5350292fc renamed exaples to playground, edited README 2025-07-26 20:16:50 +05:30
29 changed files with 847 additions and 531 deletions

2
.gitignore vendored
View File

@@ -25,4 +25,4 @@ go.work.sum
# env file # env file
.env .env
example/local_* playground/local_*

View File

@@ -1,4 +1,9 @@
.PHONY: run bench-select test
run: run:
go run ./cmd -o ./example/db ./example/schema.sql go run ./cmd -o ./playground/db ./playground/schema.sql
bench-select: bench-select:
go test ./example -bench BenchmarkSelect -memprofile memprofile.out -cpuprofile profile.out go test ./example -bench BenchmarkSelect -memprofile memprofile.out -cpuprofile profile.out
test:
go test ./playground

View File

@@ -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 applications models to these database models or use the database models directly, which may not align with your applications 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 applications 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

View File

@@ -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,10 +91,12 @@ 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 (")
sb.WriteString("\n // All fields in table " + tblName)
sb.WriteString(fmt.Sprintf("\n All pgm.Field = %q", tblName+".*"))
var name string var name string
for _, c := range cols { for _, c := range cols {
name = strings.ReplaceAll(c.Name, "_", " ") name = strings.ReplaceAll(c.Name, "_", " ")

9
go.mod
View File

@@ -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
View File

@@ -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=

184
pgm.go
View File

@@ -1,112 +1,103 @@
// Patial Tech. // pgm
// Author, Ankit Patial //
// A simple PG string query builder
//
// Author: Ankit Patial
package pgm package pgm
import ( import (
"context"
"errors" "errors"
"strings" "log/slog"
"sync/atomic"
"time" "time"
"github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgtype" "github.com/jackc/pgx/v5/pgtype"
"github.com/jackc/pgx/v5/pgxpool"
) )
// Table in database var (
type Table struct { poolPGX atomic.Pointer[pgxpool.Pool]
Name string ErrConnStringMissing = errors.New("connection string is empty")
PK []string )
FieldCount uint16
debug bool // 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 // InitPool will create new pgxpool.Pool and will keep it for its working
func (t Table) Debug() Clause { func InitPool(conf Config) {
t.debug = true if conf.ConnString == "" {
return t panic(ErrConnStringMissing)
}
cfg, err := pgxpool.ParseConfig(conf.ConnString)
if err != nil {
panic(err)
}
if conf.MaxConns > 0 {
cfg.MaxConns = conf.MaxConns // 100
}
if conf.MinConns > 0 {
cfg.MinConns = conf.MaxConns // 5
}
if conf.MaxConnLifetime > 0 {
cfg.MaxConnLifetime = conf.MaxConnLifetime // time.Minute * 10
}
if conf.MaxConnIdleTime > 0 {
cfg.MaxConnIdleTime = conf.MaxConnIdleTime // time.Minute * 5
}
p, err := pgxpool.NewWithConfig(context.Background(), cfg)
if err != nil {
panic(err)
}
if err = p.Ping(context.Background()); err != nil {
panic(err)
}
poolPGX.Store(p)
} }
// // GetPool instance
// Field ==> func GetPool() *pgxpool.Pool {
// return poolPGX.Load()
// Field related to a table
type Field string
func (f Field) Name() string {
return strings.Split(string(f), ".")[1]
} }
func (f Field) String() string { // BeginTx begins a pgx poll transaction
return string(f) 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 // IsNotFound error check
func (f Field) Count() Field { func IsNotFound(err error) bool {
return Field("COUNT(" + f.String() + ")") 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 // PgTime as in UTC
func PgTime(t time.Time) pgtype.Timestamptz { func PgTime(t time.Time) pgtype.Timestamptz {
return pgtype.Timestamptz{Time: t, Valid: true} return pgtype.Timestamptz{Time: t, Valid: true}
@@ -115,20 +106,3 @@ func PgTime(t time.Time) pgtype.Timestamptz {
func PgTimeNow() pgtype.Timestamptz { func PgTimeNow() pgtype.Timestamptz {
return pgtype.Timestamptz{Time: time.Now(), Valid: true} 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
View 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
View 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
}

View File

@@ -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 package branchuser
import "code.patial.tech/go/pgm" import "code.patial.tech/go/pgm"
const ( const (
// All fields in table branch_users
All pgm.Field = "branch_users.*"
// BranchID field has db type "bigint NOT NULL" // BranchID field has db type "bigint NOT NULL"
BranchID pgm.Field = "branch_users.branch_id" BranchID pgm.Field = "branch_users.branch_id"
// UserID field has db type "bigint NOT NULL" // UserID field has db type "bigint NOT NULL"

View File

@@ -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 package comment
import "code.patial.tech/go/pgm" import "code.patial.tech/go/pgm"
const ( const (
// All fields in table comments
All pgm.Field = "comments.*"
// ID field has db type "integer NOT NULL" // ID field has db type "integer NOT NULL"
ID pgm.Field = "comments.id" ID pgm.Field = "comments.id"
// PostID field has db type "integer NOT NULL" // PostID field has db type "integer NOT NULL"

11
playground/db/derived.go Normal file
View 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
}

View File

@@ -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 package employee
import "code.patial.tech/go/pgm" import "code.patial.tech/go/pgm"
const ( const (
// All fields in table employees
All pgm.Field = "employees.*"
// ID field has db type "integer NOT NULL" // ID field has db type "integer NOT NULL"
ID pgm.Field = "employees.id" ID pgm.Field = "employees.id"
// Name field has db type "var NOT NULL" // Name field has db type "var NOT NULL"

View File

@@ -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 package post
import "code.patial.tech/go/pgm" import "code.patial.tech/go/pgm"
const ( const (
// All fields in table posts
All pgm.Field = "posts.*"
// ID field has db type "integer NOT NULL" // ID field has db type "integer NOT NULL"
ID pgm.Field = "posts.id" ID pgm.Field = "posts.id"
// UserID field has db type "integer NOT NULL" // UserID field has db type "integer NOT NULL"

View File

@@ -1,5 +1,4 @@
// Code generated by code.patial.tech/go/pgm/cmd // Code generated by code.patial.tech/go/pgm/cmd DO NOT EDIT.
// DO NOT EDIT.
package db package db

View File

@@ -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 package user
import "code.patial.tech/go/pgm" import "code.patial.tech/go/pgm"
const ( const (
// All fields in table users
All pgm.Field = "users.*"
// ID field has db type "integer NOT NULL" // ID field has db type "integer NOT NULL"
ID pgm.Field = "users.id" ID pgm.Field = "users.id"
// Name field has db type "character varying(255) NOT NULL" // Name field has db type "character varying(255) NOT NULL"

View File

@@ -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 package usersession
import "code.patial.tech/go/pgm" import "code.patial.tech/go/pgm"
const ( const (
// All fields in table user_sessions
All pgm.Field = "user_sessions.*"
// ID field has db type "character varying NOT NULL" // ID field has db type "character varying NOT NULL"
ID pgm.Field = "user_sessions.id" ID pgm.Field = "user_sessions.id"
// CreatedAt field has db type "timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL" // CreatedAt field has db type "timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL"

View File

@@ -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

View File

@@ -1,16 +1,16 @@
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) {
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(). 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() String()
if got != expected { if got != expected {
t.Errorf("got %q, want %q", got, expected) t.Errorf("got %q, want %q", got, expected)

View File

@@ -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) {
@@ -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) { func TestInsertQuery2(t *testing.T) {
got := db.User.Insert(). got := db.User.Insert().
Set(user.Email, "aa@aa.com"). 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) { func BenchmarkInsertQuery(b *testing.B) {
for b.Loop() { for b.Loop() {
_ = db.User.Insert(). _ = db.User.Insert().

View File

@@ -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) {
@@ -28,8 +28,9 @@ func TestQryBuilder2(t *testing.T) {
), ),
). ).
Where( Where(
user.LastName.NEq(7), user.LastName.NotEq(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 != ANY(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)
} }
@@ -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 // BenchmarkSelect-12 638901 1860 ns/op 4266 B/op 61 allocs/op
func BenchmarkSelect(b *testing.B) { func BenchmarkSelect(b *testing.B) {
for b.Loop() { for b.Loop() {
@@ -79,7 +125,7 @@ func BenchmarkSelect(b *testing.B) {
), ),
). ).
Where( Where(
user.LastName.NEq(7), user.LastName.NotEq(7),
user.Phone.Like("%123%"), user.Phone.Like("%123%"),
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))),
). ).

View File

@@ -1,11 +1,10 @@
package example package playground
import ( import (
"testing" "testing"
"code.patial.tech/go/pgm" "code.patial.tech/go/pgm/playground/db"
"code.patial.tech/go/pgm/example/db" "code.patial.tech/go/pgm/playground/db/user"
"code.patial.tech/go/pgm/example/db/user"
) )
func TestUpdateQuery(t *testing.T) { func TestUpdateQuery(t *testing.T) {
@@ -17,28 +16,7 @@ func TestUpdateQuery(t *testing.T) {
user.Email.Eq("aa@aa.com"), user.Email.Eq("aa@aa.com"),
). ).
Where( Where(
user.StatusID.NEq(1), user.StatusID.NotEq(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),
). ).
String() String()
@@ -59,7 +37,7 @@ func BenchmarkUpdateQuery(b *testing.B) {
user.Email.Eq("aa@aa.com"), user.Email.Eq("aa@aa.com"),
). ).
Where( Where(
user.StatusID.NEq(1), user.StatusID.NotEq(1),
). ).
String() String()
} }

98
pool.go
View File

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

185
qry.go
View File

@@ -7,156 +7,17 @@ import (
"context" "context"
"strconv" "strconv"
"strings" "strings"
"sync"
"github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5"
) )
type ( type (
Clause interface { Clause interface {
Insert() InsertClause
Select(fields ...Field) SelectClause Select(fields ...Field) SelectClause
// Insert() InsertSet Update() UpdateClause
// Update() UpdateSet Delete() DeleteCluase
// 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
} }
Execute interface { Execute interface {
@@ -169,26 +30,26 @@ type (
String() string String() string
} }
RowScanner interface { Conditioner interface {
Scan(dest ...any) error Condition(args *[]any, idx int) string
} }
RowsCb func(row RowScanner) error
) )
func joinFileds(fields []Field) string { var sbPool = sync.Pool{
sb := getSB() New: func() any {
defer putSB(sb) return new(strings.Builder)
for i, f := range fields { },
if i == 0 { }
sb.WriteString(f.String())
} else {
sb.WriteString(", ")
sb.WriteString(f.String())
}
}
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 { func And(cond ...Conditioner) Conditioner {
@@ -208,13 +69,17 @@ 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 cv.Val != nil {
*args = append(*args, cv.Val)
if strings.HasSuffix(cv.op, "$") { if strings.HasSuffix(cv.op, "$") {
op = cv.op + strconv.Itoa(argIdx+1) op = cv.op + strconv.Itoa(argIdx+1)
} else { } else {
op = strings.Replace(cv.op, "$", "$"+strconv.Itoa(argIdx+1), 1) op = strings.Replace(cv.op, "$", "$"+strconv.Itoa(argIdx+1), 1)
} }
} else {
op = cv.op
}
if cv.action == CondActionNeedToClose { if cv.action == CondActionNeedToClose {
return cv.Field + op + ")" return cv.Field + op + ")"

View File

@@ -7,6 +7,10 @@ import (
) )
type ( type (
DeleteCluase interface {
WhereOrExec
}
deleteQry struct { deleteQry struct {
table string table string
condition []Conditioner 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 { func (q *deleteQry) Where(cond ...Conditioner) WhereOrExec {
q.condition = append(q.condition, cond...) q.condition = append(q.condition, cond...)
return q return q

View File

@@ -12,7 +12,21 @@ import (
"github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5"
) )
type insertQry struct { type (
InsertClause interface {
Insert
Returning(field Field) First
OnConflict(fields ...Field) Do
Execute
Stringer
}
Insert interface {
Set(field Field, val any) InsertClause
SetMap(fields map[Field]any) InsertClause
}
insertQry struct {
returing *string returing *string
onConflict *string onConflict *string
@@ -23,18 +37,8 @@ type insertQry struct {
vals []string vals []string
args []any args []any
debug bool 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,
} }
return qb )
}
func (q *insertQry) Set(field Field, val any) InsertClause { func (q *insertQry) Set(field Field, val any) InsertClause {
q.fields = append(q.fields, field.Name()) q.fields = append(q.fields, field.Name())

View File

@@ -7,6 +7,7 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"slices"
"strconv" "strconv"
"strings" "strings"
@@ -14,6 +15,125 @@ import (
) )
type ( 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 { selectQry struct {
table string table string
fields []Field fields []Field
@@ -23,8 +143,10 @@ type (
groupBy []Field groupBy []Field
having []Conditioner having []Conditioner
orderBy []Field orderBy []Field
limit int limit int
offset int offset int
debug bool debug bool
} }
@@ -51,34 +173,46 @@ const (
CondActionSubQuery CondActionSubQuery
) )
// Select clause func (q *selectQry) Join(t Table, t1Field, t2Field Field, cond ...Conditioner) SelectClause {
func (t Table) Select(field ...Field) SelectClause { return q.buildJoin(t, "JOIN", t1Field, t2Field, cond...)
qb := &selectQry{ }
table: t.Name,
debug: t.debug, func (q *selectQry) LeftJoin(t Table, t1Field, t2Field Field, cond ...Conditioner) SelectClause {
fields: field, 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 { sb.WriteString(str + " AND ")
q.join = append(q.join, "JOIN "+t.Name+" ON "+t1Field.String()+" = "+t2Field.String())
return q
}
func (q *selectQry) LeftJoin(t Table, t1Field, t2Field Field) SelectClause { var argIdx int
q.join = append(q.join, "LEFT JOIN "+t.Name+" ON "+t1Field.String()+" = "+t2Field.String()) for i, c := range cond {
return q 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, sb.String())
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())
return q return q
} }
@@ -163,6 +297,17 @@ func (q *selectQry) raw(prefixArgs []any) (string, []any) {
} }
func (q *selectQry) String() string { 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() sb := getSB()
defer putSB(sb) defer putSB(sb)
@@ -228,14 +373,19 @@ func (q *selectQry) String() string {
sb.WriteString(strconv.Itoa(q.offset)) sb.WriteString(strconv.Itoa(q.offset))
} }
qry := sb.String() qry = sb.String()
if q.debug { if q.debug {
fmt.Println("***") fmt.Println("***")
fmt.Println(qry) fmt.Println(qry)
fmt.Printf("%+v\n", q.args) fmt.Printf("%+v\n", q.args)
fmt.Println("***") fmt.Println("***")
} }
return qry
if needArgs {
args = slices.Clone(q.args)
}
return
} }
func (q *selectQry) averageLen() int { func (q *selectQry) averageLen() int {

View File

@@ -11,23 +11,30 @@ import (
"github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5"
) )
type updateQry struct { type (
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
}
updateQry struct {
table string table string
cols []string cols []string
condition []Conditioner condition []Conditioner
args []any args []any
debug bool 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),
} }
return qb )
}
func (q *updateQry) Set(field Field, val any) UpdateClause { func (q *updateQry) Set(field Field, val any) UpdateClause {
col := field.Name() col := field.Name()