first commit
This commit is contained in:
28
.gitignore
vendored
Normal file
28
.gitignore
vendored
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# ---> Go
|
||||||
|
# If you prefer the allow list template instead of the deny list, see community template:
|
||||||
|
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
|
||||||
|
#
|
||||||
|
# Binaries for programs and plugins
|
||||||
|
*.exe
|
||||||
|
*.exe~
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
|
||||||
|
# Test binary, built with `go test -c`
|
||||||
|
*.test
|
||||||
|
|
||||||
|
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||||
|
*.out
|
||||||
|
|
||||||
|
# Dependency directories (remove the comment below to include it)
|
||||||
|
# vendor/
|
||||||
|
|
||||||
|
# Go workspace file
|
||||||
|
go.work
|
||||||
|
go.work.sum
|
||||||
|
|
||||||
|
# env file
|
||||||
|
.env
|
||||||
|
|
||||||
|
example/local_*
|
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2025 Patial Tech
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
4
Makefile
Normal file
4
Makefile
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
run:
|
||||||
|
go run ./cmd -o ./example/db ./example/schema.sql
|
||||||
|
bench-select:
|
||||||
|
go test ./example -bench BenchmarkSelect -memprofile memprofile.out -cpuprofile profile.out
|
5
README.md
Normal file
5
README.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# pgm (Postgres Mapper)
|
||||||
|
|
||||||
|
Simple query builder to work with Go:PG apps.
|
||||||
|
|
||||||
|
Will work along side with [dbmate](https://github.com/amacneil/dbmate), will consume schema.sql file created by dbmate
|
214
cmd/generate.go
Normal file
214
cmd/generate.go
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
// Patial Tech.
|
||||||
|
// Author, Ankit Patial
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"go/format"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/text/cases"
|
||||||
|
"golang.org/x/text/language"
|
||||||
|
)
|
||||||
|
|
||||||
|
func generate(scheamPath, outDir string) error {
|
||||||
|
// read schame.sql
|
||||||
|
f, err := os.ReadFile(scheamPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse scheam.sql
|
||||||
|
tbls, err := parse(f)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Output dir, create if not exists.
|
||||||
|
if _, err := os.Stat(outDir); os.IsNotExist(err) {
|
||||||
|
if err := os.MkdirAll(outDir, 0740); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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(fmt.Sprintf("package %s \n", filepath.Base(outDir)))
|
||||||
|
sb.WriteString(`
|
||||||
|
import "code.patial.tech/go/pgm"
|
||||||
|
|
||||||
|
var (
|
||||||
|
`)
|
||||||
|
caser := cases.Title(language.English)
|
||||||
|
var (
|
||||||
|
modalDir string
|
||||||
|
fieldCount int
|
||||||
|
)
|
||||||
|
for _, t := range tbls {
|
||||||
|
// skip schema_migrations
|
||||||
|
if t.Table == "schema_migrations" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
name := pluralToSingular(t.Table)
|
||||||
|
|
||||||
|
name = strings.ReplaceAll(name, "_", " ")
|
||||||
|
name = caser.String(name)
|
||||||
|
name = strings.ReplaceAll(name, " ", "")
|
||||||
|
fieldCount = len(t.Columns)
|
||||||
|
sb.WriteString(fmt.Sprintf(
|
||||||
|
" %s = pgm.Table{Name: %q, FieldCount: %d", name, t.Table, fieldCount,
|
||||||
|
))
|
||||||
|
if len(t.PrimaryKey) > 0 {
|
||||||
|
sb.WriteString(", PK: []string{}")
|
||||||
|
sb.WriteString("}\n")
|
||||||
|
} else {
|
||||||
|
sb.WriteString("}\n")
|
||||||
|
}
|
||||||
|
modalDir = strings.ToLower(name)
|
||||||
|
os.Mkdir(filepath.Join(outDir, modalDir), 0740)
|
||||||
|
|
||||||
|
if err = writeColFile(t.Table, t.Columns, filepath.Join(outDir, modalDir), caser); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sb.WriteString(")")
|
||||||
|
|
||||||
|
// Format code before saving
|
||||||
|
code, err := formatGoCode(sb.String())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save file to disk
|
||||||
|
os.WriteFile(filepath.Join(outDir, "schema.go"), code, 0640)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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(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 (")
|
||||||
|
var name string
|
||||||
|
for _, c := range cols {
|
||||||
|
name = strings.ReplaceAll(c.Name, "_", " ")
|
||||||
|
name = caser.String(name)
|
||||||
|
name = strings.ReplaceAll(name, " ", "")
|
||||||
|
|
||||||
|
if strings.HasSuffix(name, "Id") {
|
||||||
|
name = name[0:len(name)-2] + "ID"
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.WriteString(fmt.Sprintf("\n // %s field has db type %q", name, c.Type))
|
||||||
|
sb.WriteString(fmt.Sprintf("\n %s pgm.Field = %q", name, tblName+"."+c.Name))
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.WriteString("\n)")
|
||||||
|
|
||||||
|
// Format code before saving
|
||||||
|
code, err := formatGoCode(sb.String())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Save file to disk.
|
||||||
|
return os.WriteFile(filepath.Join(outDir, tblName+".go"), code, 0640)
|
||||||
|
}
|
||||||
|
|
||||||
|
// pluralToSingular converts plural table names to singular forms
|
||||||
|
func pluralToSingular(plural string) string {
|
||||||
|
// Handle special irregular plurals
|
||||||
|
irregulars := map[string]string{
|
||||||
|
"people": "person",
|
||||||
|
"children": "child",
|
||||||
|
"men": "man",
|
||||||
|
"women": "woman",
|
||||||
|
"feet": "foot",
|
||||||
|
"teeth": "tooth",
|
||||||
|
"mice": "mouse",
|
||||||
|
"geese": "goose",
|
||||||
|
"oxen": "ox",
|
||||||
|
"data": "datum",
|
||||||
|
"criteria": "criterion",
|
||||||
|
"phenomena": "phenomenon",
|
||||||
|
}
|
||||||
|
|
||||||
|
lower := strings.ToLower(plural)
|
||||||
|
if singular, exists := irregulars[lower]; exists {
|
||||||
|
return singular
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle words ending in -ies (cities -> city, companies -> company)
|
||||||
|
if strings.HasSuffix(lower, "ies") && len(plural) > 3 {
|
||||||
|
return plural[:len(plural)-3] + "y"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle words ending in -ves (lives -> life, wives -> wife)
|
||||||
|
if strings.HasSuffix(lower, "ves") && len(plural) > 3 {
|
||||||
|
return plural[:len(plural)-3] + "fe"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle words ending in -ses (classes -> class, addresses -> address)
|
||||||
|
if strings.HasSuffix(lower, "ses") && len(plural) > 3 {
|
||||||
|
return plural[:len(plural)-2]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle words ending in -xes (boxes -> box, taxes -> tax)
|
||||||
|
if strings.HasSuffix(lower, "xes") && len(plural) > 3 {
|
||||||
|
return plural[:len(plural)-2]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle words ending in -ches (watches -> watch, benches -> bench)
|
||||||
|
if strings.HasSuffix(lower, "ches") && len(plural) > 4 {
|
||||||
|
return plural[:len(plural)-2]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle words ending in -shes (dishes -> dish, bushes -> bush)
|
||||||
|
if strings.HasSuffix(lower, "shes") && len(plural) > 4 {
|
||||||
|
return plural[:len(plural)-2]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle words ending in -oes (heroes -> hero, potatoes -> potato)
|
||||||
|
if strings.HasSuffix(lower, "oes") && len(plural) > 3 {
|
||||||
|
return plural[:len(plural)-2]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle general -es endings
|
||||||
|
if strings.HasSuffix(lower, "es") && len(plural) > 2 {
|
||||||
|
withoutEs := plural[:len(plural)-2]
|
||||||
|
if len(withoutEs) > 0 {
|
||||||
|
lastChar := strings.ToLower(string(withoutEs[len(withoutEs)-1]))
|
||||||
|
// Only remove -es (not just -s) if the word ends in s, x, ch, sh, o (those need the -es)
|
||||||
|
if lastChar == "s" || lastChar == "x" || lastChar == "o" ||
|
||||||
|
strings.HasSuffix(strings.ToLower(withoutEs), "ch") ||
|
||||||
|
strings.HasSuffix(strings.ToLower(withoutEs), "sh") {
|
||||||
|
return withoutEs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// For words like "references", "preferences" - just remove the -s
|
||||||
|
return plural[:len(plural)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle simple -s endings (users -> user, books -> book)
|
||||||
|
if strings.HasSuffix(lower, "s") && len(plural) > 1 {
|
||||||
|
return plural[:len(plural)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no rules match, return as-is (might already be singular)
|
||||||
|
return plural
|
||||||
|
}
|
||||||
|
|
||||||
|
// formatGoCode formats the input Go code using go/format.
|
||||||
|
func formatGoCode(str string) ([]byte, error) {
|
||||||
|
formatted, err := format.Source([]byte(str))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("formatting code: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return formatted, nil
|
||||||
|
}
|
37
cmd/main.go
Normal file
37
cmd/main.go
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
// Patial Tech.
|
||||||
|
// Author, Ankit Patial
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
const usageTxt = `Please provide output director and input schema.
|
||||||
|
Example:
|
||||||
|
pgm/cmd -o ./db ./db/schema.sql
|
||||||
|
|
||||||
|
`
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var outDir string
|
||||||
|
flag.StringVar(&outDir, "o", "", "-o as output directory path")
|
||||||
|
flag.Parse()
|
||||||
|
if len(os.Args) < 4 {
|
||||||
|
fmt.Print(usageTxt)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if outDir == "" {
|
||||||
|
println("missing, -o output directory path")
|
||||||
|
os.Exit(1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := generate(os.Args[3], outDir); err != nil {
|
||||||
|
println(err.Error())
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
139
cmd/parse.go
Normal file
139
cmd/parse.go
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
// Patial Tech.
|
||||||
|
// Author, Ankit Patial
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
TableInfo struct {
|
||||||
|
Schema string
|
||||||
|
Table string
|
||||||
|
Columns []*Column
|
||||||
|
PrimaryKey []string
|
||||||
|
}
|
||||||
|
|
||||||
|
Column struct {
|
||||||
|
// Name of column
|
||||||
|
Name string
|
||||||
|
// Type of column
|
||||||
|
Type string
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func parse(scheam []byte) ([]*TableInfo, error) {
|
||||||
|
var (
|
||||||
|
t = false
|
||||||
|
n = bytes.Count(scheam, []byte("CREATE TABLE"))
|
||||||
|
tables = make([]*TableInfo, n)
|
||||||
|
i = 0
|
||||||
|
sb strings.Builder
|
||||||
|
)
|
||||||
|
|
||||||
|
for l := range bytes.SplitSeq(scheam, []byte("\n")) {
|
||||||
|
if strings.HasPrefix(string(l), "CREATE TABLE") {
|
||||||
|
t = true
|
||||||
|
sb.Write(l)
|
||||||
|
} else if t {
|
||||||
|
sb.Write(l)
|
||||||
|
if strings.Contains(string(l), ";") {
|
||||||
|
t = false
|
||||||
|
t, err := parseTableStmt(sb.String())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
tables[i] = t
|
||||||
|
i++
|
||||||
|
sb.Reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tables, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseTableStmt(sqlStatement string) (*TableInfo, error) {
|
||||||
|
// Regex to match CREATE TABLE statement and extract table name and column definitions
|
||||||
|
tableRegex := regexp.MustCompile(`(?i)CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?(?:` + "`" + `?(\w+)` + "`" + `?\.)?` + "`" + `?(\w+)` + "`" + `?\s*\(([\s\S]*?)\)(?:\s*ENGINE.*?)?;`)
|
||||||
|
matches := tableRegex.FindStringSubmatch(sqlStatement)
|
||||||
|
if matches == nil {
|
||||||
|
return nil, fmt.Errorf("no CREATE TABLE statement found")
|
||||||
|
}
|
||||||
|
|
||||||
|
schema := matches[1]
|
||||||
|
tbl := matches[2]
|
||||||
|
cols := matches[3]
|
||||||
|
|
||||||
|
// Parse column definitions by splitting first on commas
|
||||||
|
// This is a simplistic approach - a real SQL parser would be more robust
|
||||||
|
columns := parseColumns(cols)
|
||||||
|
|
||||||
|
return &TableInfo{
|
||||||
|
Schema: schema,
|
||||||
|
Table: tbl,
|
||||||
|
Columns: columns,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseColumns(colsStr string) []*Column {
|
||||||
|
var columns []*Column
|
||||||
|
var currentDef strings.Builder
|
||||||
|
parenthesesCount := 0
|
||||||
|
inQuote := false
|
||||||
|
|
||||||
|
// First, intelligently split the column definitions
|
||||||
|
var columnDefs []string
|
||||||
|
for _, char := range colsStr {
|
||||||
|
if char == '(' {
|
||||||
|
parenthesesCount++
|
||||||
|
currentDef.WriteRune(char)
|
||||||
|
} else if char == ')' {
|
||||||
|
parenthesesCount--
|
||||||
|
currentDef.WriteRune(char)
|
||||||
|
} else if char == '\'' || char == '"' {
|
||||||
|
inQuote = !inQuote
|
||||||
|
currentDef.WriteRune(char)
|
||||||
|
} else if char == ',' && parenthesesCount == 0 && !inQuote {
|
||||||
|
// End of a column definition
|
||||||
|
columnDefs = append(columnDefs, strings.TrimSpace(currentDef.String()))
|
||||||
|
currentDef.Reset()
|
||||||
|
} else {
|
||||||
|
currentDef.WriteRune(char)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the last column definition if there's anything left
|
||||||
|
if currentDef.Len() > 0 {
|
||||||
|
columnDefs = append(columnDefs, strings.TrimSpace(currentDef.String()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now parse each column definition
|
||||||
|
for _, colDef := range columnDefs {
|
||||||
|
// Skip constraints and keys that don't define columns
|
||||||
|
if strings.HasPrefix(strings.ToUpper(colDef), "PRIMARY KEY") ||
|
||||||
|
strings.HasPrefix(strings.ToUpper(colDef), "UNIQUE KEY") ||
|
||||||
|
strings.HasPrefix(strings.ToUpper(colDef), "FOREIGN KEY") ||
|
||||||
|
strings.HasPrefix(strings.ToUpper(colDef), "KEY") ||
|
||||||
|
strings.HasPrefix(strings.ToUpper(colDef), "INDEX") ||
|
||||||
|
strings.HasPrefix(strings.ToUpper(colDef), "CONSTRAINT") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
colNameRegex := regexp.MustCompile(`^` + "`" + `?(\w+)` + "`" + `?\s+(.+)$`)
|
||||||
|
matches := colNameRegex.FindStringSubmatch(colDef)
|
||||||
|
if matches != nil {
|
||||||
|
columns = append(columns, &Column{
|
||||||
|
Name: matches[1],
|
||||||
|
Type: strings.TrimSpace(matches[2]),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return columns
|
||||||
|
}
|
18
example/db/branchuser/branch_users.go
Normal file
18
example/db/branchuser/branch_users.go
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
// Code generated by db-gen. DO NOT EDIT.
|
||||||
|
|
||||||
|
package branchuser
|
||||||
|
|
||||||
|
import "code.patial.tech/go/pgm"
|
||||||
|
|
||||||
|
const (
|
||||||
|
// BranchID field has db type "bigint NOT NULL"
|
||||||
|
BranchID pgm.Field = "branch_users.branch_id"
|
||||||
|
// UserID field has db type "bigint NOT NULL"
|
||||||
|
UserID pgm.Field = "branch_users.user_id"
|
||||||
|
// RoleID field has db type "smallint NOT NULL"
|
||||||
|
RoleID pgm.Field = "branch_users.role_id"
|
||||||
|
// CreatedAt field has db type "timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL"
|
||||||
|
CreatedAt pgm.Field = "branch_users.created_at"
|
||||||
|
// UpdatedAt field has db type "timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL"
|
||||||
|
UpdatedAt pgm.Field = "branch_users.updated_at"
|
||||||
|
)
|
18
example/db/comment/comments.go
Normal file
18
example/db/comment/comments.go
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
// Code generated by db-gen. DO NOT EDIT.
|
||||||
|
|
||||||
|
package comment
|
||||||
|
|
||||||
|
import "code.patial.tech/go/pgm"
|
||||||
|
|
||||||
|
const (
|
||||||
|
// ID field has db type "integer NOT NULL"
|
||||||
|
ID pgm.Field = "comments.id"
|
||||||
|
// PostID field has db type "integer NOT NULL"
|
||||||
|
PostID pgm.Field = "comments.post_id"
|
||||||
|
// UserID field has db type "integer NOT NULL"
|
||||||
|
UserID pgm.Field = "comments.user_id"
|
||||||
|
// Content field has db type "text NOT NULL"
|
||||||
|
Content pgm.Field = "comments.content"
|
||||||
|
// CreatedAt field has db type "timestamp without time zone NOT NULL DEFAULT CURRENT_TIMESTAMP"
|
||||||
|
CreatedAt pgm.Field = "comments.created_at"
|
||||||
|
)
|
18
example/db/employee/employees.go
Normal file
18
example/db/employee/employees.go
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
// Code generated by db-gen. DO NOT EDIT.
|
||||||
|
|
||||||
|
package employee
|
||||||
|
|
||||||
|
import "code.patial.tech/go/pgm"
|
||||||
|
|
||||||
|
const (
|
||||||
|
// ID field has db type "integer NOT NULL"
|
||||||
|
ID pgm.Field = "employees.id"
|
||||||
|
// Name field has db type "var NOT NULL"
|
||||||
|
Name pgm.Field = "employees.name"
|
||||||
|
// Department field has db type "var NOT NULL"
|
||||||
|
Department pgm.Field = "employees.department"
|
||||||
|
// Salary field has db type "decimal(10,2)"
|
||||||
|
Salary pgm.Field = "employees.salary"
|
||||||
|
// CreatedAt field has db type "timestamp without time zone NOT NULL DEFAULT CURRENT_TIMESTAMP updated_at timestamp without time zone NOT NULL DEFAULT CURRENT_TIMESTAMP"
|
||||||
|
CreatedAt pgm.Field = "employees.created_at"
|
||||||
|
)
|
18
example/db/post/posts.go
Normal file
18
example/db/post/posts.go
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
// Code generated by db-gen. DO NOT EDIT.
|
||||||
|
|
||||||
|
package post
|
||||||
|
|
||||||
|
import "code.patial.tech/go/pgm"
|
||||||
|
|
||||||
|
const (
|
||||||
|
// ID field has db type "integer NOT NULL"
|
||||||
|
ID pgm.Field = "posts.id"
|
||||||
|
// UserID field has db type "integer NOT NULL"
|
||||||
|
UserID pgm.Field = "posts.user_id"
|
||||||
|
// Title field has db type "character varying(255) NOT NULL"
|
||||||
|
Title pgm.Field = "posts.title"
|
||||||
|
// Content field has db type "text"
|
||||||
|
Content pgm.Field = "posts.content"
|
||||||
|
// CreatedAt field has db type "timestamp without time zone NOT NULL DEFAULT CURRENT_TIMESTAMP"
|
||||||
|
CreatedAt pgm.Field = "posts.created_at"
|
||||||
|
)
|
15
example/db/schema.go
Normal file
15
example/db/schema.go
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
// Code generated by code.patial.tech/go/pgm/cmd
|
||||||
|
// DO NOT EDIT.
|
||||||
|
|
||||||
|
package db
|
||||||
|
|
||||||
|
import "code.patial.tech/go/pgm"
|
||||||
|
|
||||||
|
var (
|
||||||
|
User = pgm.Table{Name: "users", FieldCount: 11}
|
||||||
|
UserSession = pgm.Table{Name: "user_sessions", FieldCount: 8}
|
||||||
|
BranchUser = pgm.Table{Name: "branch_users", FieldCount: 5}
|
||||||
|
Post = pgm.Table{Name: "posts", FieldCount: 5}
|
||||||
|
Comment = pgm.Table{Name: "comments", FieldCount: 5}
|
||||||
|
Employee = pgm.Table{Name: "employees", FieldCount: 5}
|
||||||
|
)
|
30
example/db/user/users.go
Normal file
30
example/db/user/users.go
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
// Code generated by db-gen. DO NOT EDIT.
|
||||||
|
|
||||||
|
package user
|
||||||
|
|
||||||
|
import "code.patial.tech/go/pgm"
|
||||||
|
|
||||||
|
const (
|
||||||
|
// ID field has db type "integer NOT NULL"
|
||||||
|
ID pgm.Field = "users.id"
|
||||||
|
// Name field has db type "character varying(255) NOT NULL"
|
||||||
|
Name pgm.Field = "users.name"
|
||||||
|
// Email field has db type "character varying(255) NOT NULL"
|
||||||
|
Email pgm.Field = "users.email"
|
||||||
|
// Phone field has db type "character varying(20)"
|
||||||
|
Phone pgm.Field = "users.phone"
|
||||||
|
// FirstName field has db type "character varying(50) NOT NULL"
|
||||||
|
FirstName pgm.Field = "users.first_name"
|
||||||
|
// MiddleName field has db type "character varying(50)"
|
||||||
|
MiddleName pgm.Field = "users.middle_name"
|
||||||
|
// LastName field has db type "character varying(50) NOT NULL"
|
||||||
|
LastName pgm.Field = "users.last_name"
|
||||||
|
// StatusID field has db type "smallint"
|
||||||
|
StatusID pgm.Field = "users.status_id"
|
||||||
|
// MfaKind field has db type "character varying(50) DEFAULT 'None'::character varying"
|
||||||
|
MfaKind pgm.Field = "users.mfa_kind"
|
||||||
|
// CreatedAt field has db type "timestamp without time zone NOT NULL DEFAULT CURRENT_TIMESTAMP"
|
||||||
|
CreatedAt pgm.Field = "users.created_at"
|
||||||
|
// UpdatedAt field has db type "timestamp without time zone NOT NULL DEFAULT CURRENT_TIMESTAMP"
|
||||||
|
UpdatedAt pgm.Field = "users.updated_at"
|
||||||
|
)
|
24
example/db/usersession/user_sessions.go
Normal file
24
example/db/usersession/user_sessions.go
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
// Code generated by db-gen. DO NOT EDIT.
|
||||||
|
|
||||||
|
package usersession
|
||||||
|
|
||||||
|
import "code.patial.tech/go/pgm"
|
||||||
|
|
||||||
|
const (
|
||||||
|
// 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"
|
||||||
|
CreatedAt pgm.Field = "user_sessions.created_at"
|
||||||
|
// UpdatedAt field has db type "timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL"
|
||||||
|
UpdatedAt pgm.Field = "user_sessions.updated_at"
|
||||||
|
// ExpiresAt field has db type "timestamp with time zone NOT NULL"
|
||||||
|
ExpiresAt pgm.Field = "user_sessions.expires_at"
|
||||||
|
// BranchID field has db type "bigint"
|
||||||
|
BranchID pgm.Field = "user_sessions.branch_id"
|
||||||
|
// UserID field has db type "bigint NOT NULL"
|
||||||
|
UserID pgm.Field = "user_sessions.user_id"
|
||||||
|
// RoleID field has db type "smallint"
|
||||||
|
RoleID pgm.Field = "user_sessions.role_id"
|
||||||
|
// LoginIp field has db type "character varying NOT NULL"
|
||||||
|
LoginIp pgm.Field = "user_sessions.login_ip"
|
||||||
|
)
|
3
example/generate.go
Normal file
3
example/generate.go
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
//go:generate go run code.patial.tech/go/pgm/cmd -o ./db ./schema.sql
|
||||||
|
|
||||||
|
package example
|
18
example/qry_delete_test.go
Normal file
18
example/qry_delete_test.go
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
package example
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"code.patial.tech/go/pgm/example/db"
|
||||||
|
"code.patial.tech/go/pgm/example/db/user"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDelete(t *testing.T) {
|
||||||
|
expected := "DELETE FROM users WHERE users.id = $1 AND users.status_id NOT IN($2)"
|
||||||
|
got := db.User.Delete().
|
||||||
|
Where(user.ID.Eq(1), user.StatusID.NotIn(1, 2, 3)).
|
||||||
|
String()
|
||||||
|
if got != expected {
|
||||||
|
t.Errorf("got %q, want %q", got, expected)
|
||||||
|
}
|
||||||
|
}
|
84
example/qry_insert_test.go
Normal file
84
example/qry_insert_test.go
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
package example
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"code.patial.tech/go/pgm"
|
||||||
|
"code.patial.tech/go/pgm/example/db"
|
||||||
|
"code.patial.tech/go/pgm/example/db/user"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestInsertQuery(t *testing.T) {
|
||||||
|
got := db.User.Insert().
|
||||||
|
Set(user.Email, "aa@aa.com").
|
||||||
|
Set(user.Phone, 8889991234).
|
||||||
|
Set(user.FirstName, "fname").
|
||||||
|
Set(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 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").
|
||||||
|
Set(user.Phone, 8889991234).
|
||||||
|
OnConflict(user.ID).
|
||||||
|
DoNothing().
|
||||||
|
String()
|
||||||
|
expected := "INSERT INTO users(email, phone) VALUES($1, $2) ON CONFLICT (id) DO NOTHING"
|
||||||
|
if got != expected {
|
||||||
|
t.Errorf("\nexpected: %q\ngot: %q", expected, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BenchmarkInsertQuery-12 1952412 605.3 ns/op 1144 B/op 18 allocs/op
|
||||||
|
func BenchmarkInsertQuery(b *testing.B) {
|
||||||
|
for b.Loop() {
|
||||||
|
_ = db.User.Insert().
|
||||||
|
Set(user.Email, "aa@aa.com").
|
||||||
|
Set(user.Phone, 8889991234).
|
||||||
|
Set(user.FirstName, "fname").
|
||||||
|
Set(user.LastName, "lname").
|
||||||
|
Returning(user.ID).
|
||||||
|
String()
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BenchmarkInsertSetMap-12 1534039 777.1 ns/op 1480 B/op 20 allocs/op
|
||||||
|
func BenchmarkInsertSetMap(b *testing.B) {
|
||||||
|
for b.Loop() {
|
||||||
|
_ = 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()
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
90
example/qry_select_test.go
Normal file
90
example/qry_select_test.go
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
package example
|
||||||
|
|
||||||
|
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"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestQryBuilder2(t *testing.T) {
|
||||||
|
got := db.User.Debug().Select(user.Email, user.FirstName).
|
||||||
|
Join(db.UserSession, user.ID, usersession.UserID).
|
||||||
|
Join(db.BranchUser, user.ID, branchuser.UserID).
|
||||||
|
Where(
|
||||||
|
user.ID.Eq(1),
|
||||||
|
pgm.Or(
|
||||||
|
user.StatusID.Eq(2),
|
||||||
|
user.UpdatedAt.Eq(3),
|
||||||
|
),
|
||||||
|
user.MfaKind.Eq(4),
|
||||||
|
pgm.Or(
|
||||||
|
user.FirstName.Eq(5),
|
||||||
|
user.MiddleName.Eq(6),
|
||||||
|
),
|
||||||
|
).
|
||||||
|
Where(
|
||||||
|
user.LastName.NEq(7),
|
||||||
|
user.Phone.Like("%123%"),
|
||||||
|
user.Email.NotInSubQuery(db.User.Select(user.ID).Where(user.ID.Eq(123))),
|
||||||
|
).
|
||||||
|
Limit(10).
|
||||||
|
Offset(100).
|
||||||
|
String()
|
||||||
|
|
||||||
|
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"
|
||||||
|
if expected != got {
|
||||||
|
t.Errorf("\nexpected: %q\ngot: %q", expected, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSelectWithHaving(t *testing.T) {
|
||||||
|
expected := "SELECT employees.department, AVG(employees.salary), COUNT(employees.id)" +
|
||||||
|
" FROM employees GROUP BY employees.department HAVING AVG(employees.salary) > $1 AND COUNT(employees.id) > $2"
|
||||||
|
got := db.Employee.
|
||||||
|
Select(employee.Department, employee.Salary.Avg(), employee.ID.Count()).
|
||||||
|
GroupBy(employee.Department).
|
||||||
|
Having(employee.Salary.Avg().Gt(50000), employee.ID.Count().Gt(5)).
|
||||||
|
String()
|
||||||
|
|
||||||
|
if expected != got {
|
||||||
|
t.Errorf("\nexpected: %q\ngot: %q", expected, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BenchmarkSelect-12 668817 1753 ns/op 4442 B/op 59 allocs/op
|
||||||
|
// BenchmarkSelect-12 638901 1860 ns/op 4266 B/op 61 allocs/op
|
||||||
|
func BenchmarkSelect(b *testing.B) {
|
||||||
|
for b.Loop() {
|
||||||
|
_ = db.User.Select(user.Email, user.FirstName).
|
||||||
|
Join(db.UserSession, user.ID, usersession.UserID).
|
||||||
|
Join(db.BranchUser, user.ID, branchuser.UserID).
|
||||||
|
Where(
|
||||||
|
user.ID.Eq(1),
|
||||||
|
pgm.Or(
|
||||||
|
user.StatusID.Eq(2),
|
||||||
|
user.UpdatedAt.Eq(3),
|
||||||
|
),
|
||||||
|
user.MfaKind.Eq(4),
|
||||||
|
pgm.Or(
|
||||||
|
user.FirstName.Eq(5),
|
||||||
|
user.MiddleName.Eq(6),
|
||||||
|
),
|
||||||
|
).
|
||||||
|
Where(
|
||||||
|
user.LastName.NEq(7),
|
||||||
|
user.Phone.Like("%123%"),
|
||||||
|
user.Email.NotInSubQuery(db.User.Select(user.ID).Where(user.ID.Eq(123))),
|
||||||
|
).
|
||||||
|
Limit(10).
|
||||||
|
Offset(100).
|
||||||
|
String()
|
||||||
|
}
|
||||||
|
}
|
66
example/qry_update_test.go
Normal file
66
example/qry_update_test.go
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
package example
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"code.patial.tech/go/pgm"
|
||||||
|
"code.patial.tech/go/pgm/example/db"
|
||||||
|
"code.patial.tech/go/pgm/example/db/user"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestUpdateQuery(t *testing.T) {
|
||||||
|
got := db.User.Update().
|
||||||
|
Set(user.FirstName, "ankit").
|
||||||
|
Set(user.MiddleName, "singh").
|
||||||
|
Set(user.LastName, "patial").
|
||||||
|
Where(
|
||||||
|
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),
|
||||||
|
).
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BenchmarkUpdateQuery-12 2004985 592.2 ns/op 1176 B/op 20 allocs/op
|
||||||
|
func BenchmarkUpdateQuery(b *testing.B) {
|
||||||
|
for b.Loop() {
|
||||||
|
_ = db.User.Update().
|
||||||
|
Set(user.FirstName, "ankit").
|
||||||
|
Set(user.MiddleName, "singh").
|
||||||
|
Set(user.LastName, "patial").
|
||||||
|
Where(
|
||||||
|
user.Email.Eq("aa@aa.com"),
|
||||||
|
).
|
||||||
|
Where(
|
||||||
|
user.StatusID.NEq(1),
|
||||||
|
).
|
||||||
|
String()
|
||||||
|
}
|
||||||
|
}
|
306
example/schema.sql
Normal file
306
example/schema.sql
Normal file
@@ -0,0 +1,306 @@
|
|||||||
|
--
|
||||||
|
-- PostgreSQL database dump
|
||||||
|
--
|
||||||
|
|
||||||
|
-- Dumped from database version 15.3
|
||||||
|
-- Dumped by pg_dump version 15.3
|
||||||
|
|
||||||
|
SET statement_timeout = 0;
|
||||||
|
SET lock_timeout = 0;
|
||||||
|
SET idle_in_transaction_session_timeout = 0;
|
||||||
|
SET client_encoding = 'UTF8';
|
||||||
|
SET standard_conforming_strings = on;
|
||||||
|
SELECT pg_catalog.set_config('search_path', '', false);
|
||||||
|
SET check_function_bodies = false;
|
||||||
|
SET xmloption = content;
|
||||||
|
SET client_min_messages = warning;
|
||||||
|
SET row_security = off;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: myapp_development; Type: DATABASE; Schema: -; Owner: postgres
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE DATABASE myapp_development WITH TEMPLATE = template0 ENCODING = 'UTF8' LOCALE = 'en_US.UTF-8';
|
||||||
|
|
||||||
|
ALTER DATABASE myapp_development OWNER TO postgres;
|
||||||
|
|
||||||
|
\connect myapp_development
|
||||||
|
|
||||||
|
SET statement_timeout = 0;
|
||||||
|
SET lock_timeout = 0;
|
||||||
|
SET idle_in_transaction_session_timeout = 0;
|
||||||
|
SET client_encoding = 'UTF8';
|
||||||
|
SET standard_conforming_strings = on;
|
||||||
|
SELECT pg_catalog.set_config('search_path', '', false);
|
||||||
|
SET check_function_bodies = false;
|
||||||
|
SET xmloption = content;
|
||||||
|
SET client_min_messages = warning;
|
||||||
|
SET row_security = off;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: schema_migrations; Type: TABLE; Schema: public; Owner: postgres
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE public.schema_migrations (
|
||||||
|
version character varying(255) NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE public.schema_migrations OWNER TO postgres;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: users; Type: TABLE; Schema: public; Owner: postgres
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE public.users (
|
||||||
|
id integer NOT NULL,
|
||||||
|
name character varying(255) NOT NULL,
|
||||||
|
email character varying(255) NOT NULL,
|
||||||
|
phone character varying(20),
|
||||||
|
first_name character varying(50) NOT NULL,
|
||||||
|
middle_name character varying(50),
|
||||||
|
last_name character varying(50) NOT NULL,
|
||||||
|
status_id smallint,
|
||||||
|
mfa_kind character varying(50) DEFAULT 'None'::character varying,
|
||||||
|
created_at timestamp without time zone NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at timestamp without time zone NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE public.users OWNER TO postgres;
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE public.user_sessions (
|
||||||
|
id character varying NOT NULL,
|
||||||
|
created_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
updated_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
expires_at timestamp with time zone NOT NULL,
|
||||||
|
branch_id bigint,
|
||||||
|
user_id bigint NOT NULL,
|
||||||
|
role_id smallint,
|
||||||
|
login_ip character varying NOT NULL,
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE public.branch_users (
|
||||||
|
branch_id bigint NOT NULL,
|
||||||
|
user_id bigint NOT NULL,
|
||||||
|
role_id smallint NOT NULL,
|
||||||
|
created_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
updated_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: users_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE SEQUENCE public.users_id_seq
|
||||||
|
AS integer
|
||||||
|
START WITH 1
|
||||||
|
INCREMENT BY 1
|
||||||
|
NO MINVALUE
|
||||||
|
NO MAXVALUE
|
||||||
|
CACHE 1;
|
||||||
|
|
||||||
|
ALTER TABLE public.users_id_seq OWNER TO postgres;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: users_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER SEQUENCE public.users_id_seq OWNED BY public.users.id;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: posts; Type: TABLE; Schema: public; Owner: postgres
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE public.posts (
|
||||||
|
id integer NOT NULL,
|
||||||
|
user_id integer NOT NULL,
|
||||||
|
title character varying(255) NOT NULL,
|
||||||
|
content text,
|
||||||
|
created_at timestamp without time zone NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE public.posts OWNER TO postgres;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: posts_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE SEQUENCE public.posts_id_seq
|
||||||
|
AS integer
|
||||||
|
START WITH 1
|
||||||
|
INCREMENT BY 1
|
||||||
|
NO MINVALUE
|
||||||
|
NO MAXVALUE
|
||||||
|
CACHE 1;
|
||||||
|
|
||||||
|
ALTER TABLE public.posts_id_seq OWNER TO postgres;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: posts_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER SEQUENCE public.posts_id_seq OWNED BY public.posts.id;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: comments; Type: TABLE; Schema: public; Owner: postgres
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE public.comments (
|
||||||
|
id integer NOT NULL,
|
||||||
|
post_id integer NOT NULL,
|
||||||
|
user_id integer NOT NULL,
|
||||||
|
content text NOT NULL,
|
||||||
|
created_at timestamp without time zone NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE public.comments OWNER TO postgres;
|
||||||
|
|
||||||
|
CREATE TABLE public.employees (
|
||||||
|
id integer NOT NULL,
|
||||||
|
name var NOT NULL,
|
||||||
|
department var NOT NULL,
|
||||||
|
salary decimal(10,2) ,
|
||||||
|
created_at timestamp without time zone NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
updated_at timestamp without time zone NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
--
|
||||||
|
-- Name: comments_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE SEQUENCE public.comments_id_seq
|
||||||
|
AS integer
|
||||||
|
START WITH 1
|
||||||
|
INCREMENT BY 1
|
||||||
|
NO MINVALUE
|
||||||
|
NO MAXVALUE
|
||||||
|
CACHE 1;
|
||||||
|
|
||||||
|
ALTER TABLE public.comments_id_seq OWNER TO postgres;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: comments_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER SEQUENCE public.comments_id_seq OWNED BY public.comments.id;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: users id; Type: DEFAULT; Schema: public; Owner: postgres
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.users ALTER COLUMN id SET DEFAULT nextval('public.users_id_seq'::regclass);
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: posts id; Type: DEFAULT; Schema: public; Owner: postgres
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.posts ALTER COLUMN id SET DEFAULT nextval('public.posts_id_seq'::regclass);
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: comments id; Type: DEFAULT; Schema: public; Owner: postgres
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.comments ALTER COLUMN id SET DEFAULT nextval('public.comments_id_seq'::regclass);
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: schema_migrations schema_migrations_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.schema_migrations
|
||||||
|
ADD CONSTRAINT schema_migrations_pkey PRIMARY KEY (version);
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: users users_email_key; Type: CONSTRAINT; Schema: public; Owner: postgres
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.users
|
||||||
|
ADD CONSTRAINT users_email_key UNIQUE (email);
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: users users_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.users
|
||||||
|
ADD CONSTRAINT users_pkey PRIMARY KEY (id);
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: posts posts_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.posts
|
||||||
|
ADD CONSTRAINT posts_pkey PRIMARY KEY (id);
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: comments comments_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.comments
|
||||||
|
ADD CONSTRAINT comments_pkey PRIMARY KEY (id);
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: idx_comments_post_id; Type: INDEX; Schema: public; Owner: postgres
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE INDEX idx_comments_post_id ON public.comments USING btree (post_id);
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: idx_comments_user_id; Type: INDEX; Schema: public; Owner: postgres
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE INDEX idx_comments_user_id ON public.comments USING btree (user_id);
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: idx_posts_user_id; Type: INDEX; Schema: public; Owner: postgres
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE INDEX idx_posts_user_id ON public.posts USING btree (user_id);
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: posts posts_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: postgres
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.posts
|
||||||
|
ADD CONSTRAINT posts_user_id_fkey FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: comments comments_post_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: postgres
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.comments
|
||||||
|
ADD CONSTRAINT comments_post_id_fkey FOREIGN KEY (post_id) REFERENCES public.posts(id) ON DELETE CASCADE;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: comments comments_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: postgres
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.comments
|
||||||
|
ADD CONSTRAINT comments_user_id_fkey FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Data for Name: schema_migrations; Type: TABLE DATA; Schema: public; Owner: postgres
|
||||||
|
--
|
||||||
|
|
||||||
|
COPY public.schema_migrations (version) FROM stdin;
|
||||||
|
20200227231541
|
||||||
|
20200227231542
|
||||||
|
20200227231543
|
||||||
|
\.
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Data for Name: users; Type: TABLE DATA; Schema: public; Owner: postgres
|
||||||
|
--
|
||||||
|
|
||||||
|
COPY public.users (id, name, email, created_at) FROM stdin;
|
||||||
|
\.
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Data for Name: posts; Type: TABLE DATA; Schema: public; Owner: postgres
|
||||||
|
--
|
||||||
|
|
||||||
|
COPY public.posts (id, user_id, title, content, created_at) FROM stdin;
|
||||||
|
\.
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Data for Name: comments; Type: TABLE DATA; Schema: public; Owner: postgres
|
||||||
|
--
|
||||||
|
|
||||||
|
COPY public.comments (id, post_id, user_id, content, created_at)
|
21
go.mod
Normal file
21
go.mod
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
module code.patial.tech/go/pgm
|
||||||
|
|
||||||
|
go 1.24.5
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/jackc/pgx v3.6.2+incompatible
|
||||||
|
golang.org/x/text v0.27.0
|
||||||
|
)
|
||||||
|
|
||||||
|
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/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
|
||||||
|
)
|
25
go.sum
Normal file
25
go.sum
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
github.com/davecgh/go-spew v1.1.0/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/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=
|
||||||
|
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=
|
||||||
|
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||||
|
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=
|
134
pgm.go
Normal file
134
pgm.go
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
// Patial Tech.
|
||||||
|
// Author, Ankit Patial
|
||||||
|
|
||||||
|
package pgm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Table in database
|
||||||
|
type Table struct {
|
||||||
|
Name string
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Field ==>
|
||||||
|
//
|
||||||
|
|
||||||
|
// 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 fn wrapping of field
|
||||||
|
func (f Field) Count() Field {
|
||||||
|
return Field("COUNT(" + f.String() + ")")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 + "')"
|
||||||
|
}
|
98
pool.go
Normal file
98
pool.go
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
// 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
|
||||||
|
}
|
240
qry.go
Normal file
240
qry.go
Normal file
@@ -0,0 +1,240 @@
|
|||||||
|
// Patial Tech.
|
||||||
|
// Author, Ankit Patial
|
||||||
|
|
||||||
|
package pgm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
Clause interface {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
Execute interface {
|
||||||
|
Exec(ctx context.Context) error
|
||||||
|
ExecTx(ctx context.Context, tx pgx.Tx) error
|
||||||
|
Stringer
|
||||||
|
}
|
||||||
|
|
||||||
|
Stringer interface {
|
||||||
|
String() string
|
||||||
|
}
|
||||||
|
|
||||||
|
RowScanner interface {
|
||||||
|
Scan(dest ...any) error
|
||||||
|
}
|
||||||
|
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func And(cond ...Conditioner) Conditioner {
|
||||||
|
return &CondGroup{op: " AND ", cond: cond}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Or(cond ...Conditioner) Conditioner {
|
||||||
|
return &CondGroup{op: " OR ", cond: cond}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cv *Cond) Condition(args *[]any, argIdx int) string {
|
||||||
|
// 1. condition with subquery
|
||||||
|
if cv.action == CondActionSubQuery {
|
||||||
|
qStr, newArgs := cv.Val.(SelectClause).raw(*args)
|
||||||
|
*args = newArgs
|
||||||
|
return cv.Field + strings.Replace(cv.op, "$", qStr, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. normal condition
|
||||||
|
*args = append(*args, cv.Val)
|
||||||
|
var op string
|
||||||
|
if strings.HasSuffix(cv.op, "$") {
|
||||||
|
op = cv.op + strconv.Itoa(argIdx+1)
|
||||||
|
} else {
|
||||||
|
op = strings.Replace(cv.op, "$", "$"+strconv.Itoa(argIdx+1), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cv.action == CondActionNeedToClose {
|
||||||
|
return cv.Field + op + ")"
|
||||||
|
}
|
||||||
|
return cv.Field + op
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CondGroup) Condition(args *[]any, argIdx int) string {
|
||||||
|
sb := getSB()
|
||||||
|
defer putSB(sb)
|
||||||
|
|
||||||
|
sb.WriteString("(")
|
||||||
|
for i, cond := range c.cond {
|
||||||
|
if i == 0 {
|
||||||
|
sb.WriteString(cond.Condition(args, argIdx+i))
|
||||||
|
} else {
|
||||||
|
sb.WriteString(c.op)
|
||||||
|
sb.WriteString(cond.Condition(args, argIdx+i))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sb.WriteString(")")
|
||||||
|
return sb.String()
|
||||||
|
}
|
74
qry_delete.go
Normal file
74
qry_delete.go
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
package pgm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
deleteQry struct {
|
||||||
|
table string
|
||||||
|
condition []Conditioner
|
||||||
|
args []any
|
||||||
|
debug bool
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *deleteQry) Exec(ctx context.Context) error {
|
||||||
|
_, err := poolPGX.Load().Exec(ctx, q.String(), q.args...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *deleteQry) ExecTx(ctx context.Context, tx pgx.Tx) error {
|
||||||
|
_, err := tx.Exec(ctx, q.String(), q.args...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *deleteQry) String() string {
|
||||||
|
sb := getSB()
|
||||||
|
defer putSB(sb)
|
||||||
|
|
||||||
|
n := len("DELETE FROM ") + len(q.table) + 20
|
||||||
|
sb.Grow(n)
|
||||||
|
|
||||||
|
// DELETE
|
||||||
|
sb.WriteString("DELETE FROM ")
|
||||||
|
sb.WriteString(q.table)
|
||||||
|
|
||||||
|
// WHERE
|
||||||
|
if len(q.condition) > 0 {
|
||||||
|
sb.WriteString(" WHERE ")
|
||||||
|
var argIdx int
|
||||||
|
for i, c := range q.condition {
|
||||||
|
argIdx = len(q.args)
|
||||||
|
if i > 0 {
|
||||||
|
sb.WriteString(" AND ")
|
||||||
|
}
|
||||||
|
sb.WriteString(c.Condition(&q.args, argIdx))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.String()
|
||||||
|
}
|
150
qry_insert.go
Normal file
150
qry_insert.go
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
// Patial Tech.
|
||||||
|
// Author, Ankit Patial
|
||||||
|
|
||||||
|
package pgm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"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,
|
||||||
|
}
|
||||||
|
return qb
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *insertQry) Set(field Field, val any) InsertClause {
|
||||||
|
q.fields = append(q.fields, field.Name())
|
||||||
|
q.vals = append(q.vals, "$"+strconv.Itoa(len(q.args)+1))
|
||||||
|
q.args = append(q.args, val)
|
||||||
|
return q
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *insertQry) SetMap(cols map[Field]any) InsertClause {
|
||||||
|
for k, v := range cols {
|
||||||
|
q.Set(k, v)
|
||||||
|
}
|
||||||
|
return q
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *insertQry) Returning(field Field) First {
|
||||||
|
col := field.Name()
|
||||||
|
q.returing = &col
|
||||||
|
return q
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *insertQry) OnConflict(fields ...Field) Do {
|
||||||
|
if len(fields) > 0 {
|
||||||
|
sb := getSB()
|
||||||
|
defer putSB(sb)
|
||||||
|
|
||||||
|
for i, f := range fields {
|
||||||
|
if i == 0 {
|
||||||
|
sb.WriteString(f.Name())
|
||||||
|
} else {
|
||||||
|
sb.WriteString(", " + f.Name())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c := sb.String()
|
||||||
|
q.onConflict = &c
|
||||||
|
}
|
||||||
|
return q
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *insertQry) DoNothing() Execute {
|
||||||
|
q.conflictAction = "DO NOTHING"
|
||||||
|
return q
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *insertQry) DoUpdate(fields ...Field) Execute {
|
||||||
|
var sb strings.Builder
|
||||||
|
for i, f := range fields {
|
||||||
|
col := f.Name()
|
||||||
|
if i == 0 {
|
||||||
|
fmt.Fprintf(&sb, "%s = EXCLUDED.%s", col, col)
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(&sb, ", %s = EXCLUDED.%s", col, col)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
q.conflictAction = "DO UPDATE SET " + sb.String()
|
||||||
|
return q
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *insertQry) Exec(ctx context.Context) error {
|
||||||
|
_, err := poolPGX.Load().Exec(ctx, q.String(), q.args...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *insertQry) ExecTx(ctx context.Context, tx pgx.Tx) error {
|
||||||
|
_, err := tx.Exec(ctx, q.String(), q.args...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *insertQry) First(ctx context.Context, dest ...any) error {
|
||||||
|
return poolPGX.Load().QueryRow(ctx, q.String(), q.args...).Scan(dest...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *insertQry) FirstTx(ctx context.Context, tx pgx.Tx, dest ...any) error {
|
||||||
|
return tx.QueryRow(ctx, q.String(), q.args...).Scan(dest...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// build query string
|
||||||
|
func (q *insertQry) String() string {
|
||||||
|
sb := getSB()
|
||||||
|
defer putSB(sb)
|
||||||
|
|
||||||
|
n := 12 + len(q.table) + 10
|
||||||
|
for i, c := range q.fields {
|
||||||
|
n += len(c) + len(" =$,"+strconv.Itoa(i))
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.Grow(n)
|
||||||
|
sb.WriteString("INSERT INTO ")
|
||||||
|
sb.WriteString(q.table)
|
||||||
|
sb.WriteString("(")
|
||||||
|
sb.WriteString(strings.Join(q.fields, ", "))
|
||||||
|
sb.WriteString(") VALUES(")
|
||||||
|
sb.WriteString(strings.Join(q.vals, ", "))
|
||||||
|
sb.WriteString(")")
|
||||||
|
|
||||||
|
if q.onConflict != nil {
|
||||||
|
sb.WriteString(" ON CONFLICT (" + *q.onConflict + ") " + q.conflictAction)
|
||||||
|
}
|
||||||
|
|
||||||
|
if q.returing != nil {
|
||||||
|
sb.WriteString(" RETURNING " + *q.returing)
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.String()
|
||||||
|
}
|
286
qry_select.go
Normal file
286
qry_select.go
Normal file
@@ -0,0 +1,286 @@
|
|||||||
|
// Patial Tech.
|
||||||
|
// Author, Ankit Patial
|
||||||
|
|
||||||
|
package pgm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
selectQry struct {
|
||||||
|
table string
|
||||||
|
fields []Field
|
||||||
|
args []any
|
||||||
|
join []string
|
||||||
|
where []Conditioner
|
||||||
|
groupBy []Field
|
||||||
|
having []Conditioner
|
||||||
|
orderBy []Field
|
||||||
|
limit int
|
||||||
|
offset int
|
||||||
|
debug bool
|
||||||
|
}
|
||||||
|
|
||||||
|
CondAction uint8
|
||||||
|
|
||||||
|
Cond struct {
|
||||||
|
Val any
|
||||||
|
op string
|
||||||
|
Field string
|
||||||
|
len int
|
||||||
|
action CondAction
|
||||||
|
}
|
||||||
|
|
||||||
|
CondGroup struct {
|
||||||
|
op string
|
||||||
|
cond []Conditioner
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Contdition actions
|
||||||
|
const (
|
||||||
|
CondActionNothing CondAction = iota
|
||||||
|
CondActionNeedToClose
|
||||||
|
CondActionSubQuery
|
||||||
|
)
|
||||||
|
|
||||||
|
// Select clause
|
||||||
|
func (t Table) Select(field ...Field) SelectClause {
|
||||||
|
qb := &selectQry{
|
||||||
|
table: t.Name,
|
||||||
|
debug: t.debug,
|
||||||
|
fields: field,
|
||||||
|
}
|
||||||
|
|
||||||
|
return qb
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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())
|
||||||
|
return q
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *selectQry) CrossJoin(t Table) SelectClause {
|
||||||
|
q.join = append(q.join, "CROSS JOIN "+t.Name)
|
||||||
|
return q
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *selectQry) Where(cond ...Conditioner) AfterWhere {
|
||||||
|
q.where = append(q.where, cond...)
|
||||||
|
return q
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *selectQry) OrderBy(fields ...Field) AfterOrderBy {
|
||||||
|
q.orderBy = fields
|
||||||
|
return q
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *selectQry) GroupBy(fields ...Field) AfterGroupBy {
|
||||||
|
q.groupBy = fields
|
||||||
|
return q
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *selectQry) Having(cond ...Conditioner) AfterHaving {
|
||||||
|
q.having = append(q.having, cond...)
|
||||||
|
return q
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *selectQry) Limit(v int) AfterLimit {
|
||||||
|
q.limit = v
|
||||||
|
return q
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *selectQry) Offset(v int) AfterOffset {
|
||||||
|
q.offset = v
|
||||||
|
return q
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *selectQry) First(ctx context.Context, dest ...any) error {
|
||||||
|
return poolPGX.Load().QueryRow(ctx, q.String(), q.args...).Scan(dest...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *selectQry) FirstTx(ctx context.Context, tx pgx.Tx, dest ...any) error {
|
||||||
|
return tx.QueryRow(ctx, q.String(), q.args...).Scan(dest...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *selectQry) All(ctx context.Context, row RowsCb) error {
|
||||||
|
rows, err := poolPGX.Load().Query(ctx, q.String(), q.args...)
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return ErrNoRows
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
if err := row(rows); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *selectQry) AllTx(ctx context.Context, tx pgx.Tx, row RowsCb) error {
|
||||||
|
rows, err := tx.Query(ctx, q.String(), q.args...)
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return ErrNoRows
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
if err := row(rows); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *selectQry) raw(prefixArgs []any) (string, []any) {
|
||||||
|
q.args = append(prefixArgs, q.args...)
|
||||||
|
return q.String(), q.args
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *selectQry) String() string {
|
||||||
|
sb := getSB()
|
||||||
|
defer putSB(sb)
|
||||||
|
|
||||||
|
sb.Grow(q.averageLen())
|
||||||
|
|
||||||
|
// SELECT
|
||||||
|
sb.WriteString("SELECT ")
|
||||||
|
sb.WriteString(joinFileds(q.fields))
|
||||||
|
sb.WriteString(" FROM " + q.table)
|
||||||
|
|
||||||
|
// JOIN
|
||||||
|
if len(q.join) > 0 {
|
||||||
|
sb.WriteString(" " + strings.Join(q.join, " "))
|
||||||
|
}
|
||||||
|
|
||||||
|
// WHERE
|
||||||
|
if len(q.where) > 0 {
|
||||||
|
sb.WriteString(" WHERE ")
|
||||||
|
var argIdx int
|
||||||
|
for i, c := range q.where {
|
||||||
|
argIdx = len(q.args)
|
||||||
|
if i > 0 {
|
||||||
|
sb.WriteString(" AND ")
|
||||||
|
}
|
||||||
|
sb.WriteString(c.Condition(&q.args, argIdx))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GROUP BY
|
||||||
|
if len(q.groupBy) > 0 {
|
||||||
|
sb.WriteString(" GROUP BY ")
|
||||||
|
sb.WriteString(joinFileds(q.groupBy))
|
||||||
|
}
|
||||||
|
|
||||||
|
// HAVING
|
||||||
|
if len(q.having) > 0 {
|
||||||
|
sb.WriteString(" HAVING ")
|
||||||
|
var argIdx int
|
||||||
|
for i, c := range q.having {
|
||||||
|
argIdx = len(q.args)
|
||||||
|
if i > 0 {
|
||||||
|
sb.WriteString(" AND ")
|
||||||
|
}
|
||||||
|
sb.WriteString(c.Condition(&q.args, argIdx))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ORDER BY
|
||||||
|
if len(q.orderBy) > 0 {
|
||||||
|
sb.WriteString(" ORDER BY ")
|
||||||
|
sb.WriteString(joinFileds(q.orderBy))
|
||||||
|
}
|
||||||
|
|
||||||
|
// LIMIT
|
||||||
|
if q.limit > 0 {
|
||||||
|
sb.WriteString(" LIMIT ")
|
||||||
|
sb.WriteString(strconv.Itoa(q.limit))
|
||||||
|
}
|
||||||
|
|
||||||
|
// OFFSET
|
||||||
|
if q.offset > 0 {
|
||||||
|
sb.WriteString(" OFFSET ")
|
||||||
|
sb.WriteString(strconv.Itoa(q.offset))
|
||||||
|
}
|
||||||
|
|
||||||
|
qry := sb.String()
|
||||||
|
if q.debug {
|
||||||
|
fmt.Println("***")
|
||||||
|
fmt.Println(qry)
|
||||||
|
fmt.Printf("%+v\n", q.args)
|
||||||
|
fmt.Println("***")
|
||||||
|
}
|
||||||
|
return qry
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *selectQry) averageLen() int {
|
||||||
|
n := 12 + len(q.table) // SELECT FROM <tbl_name>
|
||||||
|
for _, c := range q.fields {
|
||||||
|
n += (len(c) + 2) * len(q.table) // columns with tablename.columnname
|
||||||
|
}
|
||||||
|
|
||||||
|
// JOIN
|
||||||
|
if len(q.join) > 0 {
|
||||||
|
for _, c := range q.join {
|
||||||
|
n += len(c) + 2 // with whitespace
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WHERE
|
||||||
|
if len(q.where) > 0 {
|
||||||
|
n += 7 + len(q.args)*5 // WHERE with 2 sapces and each args roughly with min of 5 char
|
||||||
|
}
|
||||||
|
|
||||||
|
// GROUP BY
|
||||||
|
if len(q.groupBy) > 0 {
|
||||||
|
n += 10 // GROUP BY
|
||||||
|
for _, c := range q.groupBy {
|
||||||
|
n += len(c) + 2 // one command and a whitespace
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ORDER BY
|
||||||
|
if len(q.orderBy) > 0 {
|
||||||
|
n += 10 // ORDER BY
|
||||||
|
for _, c := range q.orderBy {
|
||||||
|
n += len(c) + 2 // one command and a whitespace
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LIMIT
|
||||||
|
if q.limit > 0 {
|
||||||
|
n += 10 // LIMIT
|
||||||
|
}
|
||||||
|
|
||||||
|
// OFFSET
|
||||||
|
if q.offset > 0 {
|
||||||
|
n += 10 // OFFSET
|
||||||
|
}
|
||||||
|
|
||||||
|
return n
|
||||||
|
}
|
100
qry_update.go
Normal file
100
qry_update.go
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
// Patial Tech.
|
||||||
|
// Author, Ankit Patial
|
||||||
|
|
||||||
|
package pgm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"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),
|
||||||
|
}
|
||||||
|
return qb
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *updateQry) Set(field Field, val any) UpdateClause {
|
||||||
|
col := field.Name()
|
||||||
|
q.cols = append(q.cols, col+"=$"+strconv.Itoa(len(q.args)+1))
|
||||||
|
q.args = append(q.args, val)
|
||||||
|
return q
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *updateQry) SetMap(cols map[Field]any) UpdateClause {
|
||||||
|
for k, v := range cols {
|
||||||
|
q.Set(k, v)
|
||||||
|
}
|
||||||
|
return q
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *updateQry) Where(cond ...Conditioner) WhereOrExec {
|
||||||
|
q.condition = append(q.condition, cond...)
|
||||||
|
return q
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *updateQry) Exec(ctx context.Context) error {
|
||||||
|
_, err := poolPGX.Load().Exec(ctx, q.String(), q.args...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *updateQry) ExecTx(ctx context.Context, tx pgx.Tx) error {
|
||||||
|
_, err := tx.Exec(ctx, q.String(), q.args...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *updateQry) String() string {
|
||||||
|
sb := getSB()
|
||||||
|
defer putSB(sb)
|
||||||
|
|
||||||
|
n := 7 + len(q.table) + 5 // "UPDATE q.table SET
|
||||||
|
for _, col := range q.cols {
|
||||||
|
n += len(col) + 5
|
||||||
|
}
|
||||||
|
if len(q.condition) > 0 {
|
||||||
|
n += 7 + len(q.condition)*5 // WHERE with condition, 5 is just avg small min val
|
||||||
|
}
|
||||||
|
sb.Grow(n)
|
||||||
|
|
||||||
|
// UPDATE
|
||||||
|
sb.WriteString("UPDATE " + q.table + " SET ")
|
||||||
|
sb.WriteString(strings.Join(q.cols, ", "))
|
||||||
|
|
||||||
|
// WHERE
|
||||||
|
if len(q.condition) > 0 {
|
||||||
|
sb.WriteString(" WHERE ")
|
||||||
|
var argIdx int
|
||||||
|
for i, c := range q.condition {
|
||||||
|
argIdx = len(q.args)
|
||||||
|
if i > 0 {
|
||||||
|
sb.WriteString(" AND ")
|
||||||
|
}
|
||||||
|
sb.WriteString(c.Condition(&q.args, argIdx))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.String()
|
||||||
|
}
|
Reference in New Issue
Block a user