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