first commit
This commit is contained in:
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
|
||||
}
|
Reference in New Issue
Block a user