├── examples ├── DefinitionFiles │ ├── crudGenAlt.txt │ ├── crudGen.txt │ └── structs.txt └── UsesGeneratedCode │ ├── main.go │ └── models │ └── user.go ├── models_test.go ├── string_fns_test.go ├── LICENSE.md ├── string_fns.go ├── models.go ├── db_gen.go ├── README.md ├── file_gen.go └── street_crud.go /examples/DefinitionFiles/crudGenAlt.txt: -------------------------------------------------------------------------------- 1 | StreetCRUD 2 | [Server] localhost 3 | [User] tut 4 | [Group] sqlgroupname 5 | [Password] secret 6 | [Database] db_name 7 | [schema] public 8 | [ssl] false 9 | [Underscore] true 10 | [package] models 11 | 12 | [alter table] user 13 | [copy cols] 14 | login_id [to] LogID 15 | name [to] UserName 16 | email [to] Email 17 | [add Struct] 18 | [table] 19 | [File name] 20 | [prepared] false 21 | type UserA struct { 22 | LogID int `json:"loginid"` [primary] 23 | UserName string `json:"userName"` [index][patch][size:255] 24 | Phone string `json:"phone"` [nulls] 25 | Email string `json:"email"` [nulls] 26 | } -------------------------------------------------------------------------------- /examples/DefinitionFiles/crudGen.txt: -------------------------------------------------------------------------------- 1 | StreetCRUD 2 | [Server] localhost 3 | [User] dan 4 | [Group] sqlgroupname 5 | [Password] secret 6 | [Database] db_name 7 | [schema] public 8 | [ssl] false 9 | [Underscore] true 10 | [package]models 11 | 12 | [Add struct] 13 | [table] 14 | [File name] 15 | [prepared] true 16 | type User struct { 17 | LoginID int `json:"loginid"` [primary] 18 | Name string `json:"name,omitempty"` [index] [patch][size:255] 19 | Email string `json:"email"`[index] [nulls] 20 | Password string `json:"password" out:"false"` 21 | DeletedUser bool `json:"deleted"` [nulls][deleted] 22 | DelOn time.Time `json:"deletedon"`[deletedOn] [nulls] 23 | } -------------------------------------------------------------------------------- /models_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "testing" 4 | 5 | func TestCheckStructForDeletes(t *testing.T) { 6 | tests := []struct { 7 | name string 8 | cols []*column 9 | want bool 10 | }{ 11 | {"none", []*column{}, true}, 12 | {"deleted only", []*column{{deleted: true}}, false}, 13 | {"deletedOn only", []*column{{deletedOn: true}}, false}, 14 | {"both", []*column{{deleted: true}, {deletedOn: true}}, true}, 15 | {"multiple deleted", []*column{{deleted: true}, {deleted: true}, {deletedOn: true}}, false}, 16 | {"multiple deletedOn", []*column{{deleted: true}, {deletedOn: true}, {deletedOn: true}}, false}, 17 | {"two each", []*column{{deleted: true}, {deletedOn: true}, {deleted: true}, {deletedOn: true}}, false}, 18 | } 19 | for _, tt := range tests { 20 | s := &structToCreate{cols: tt.cols} 21 | if got := s.CheckStructForDeletes(); got != tt.want { 22 | t.Errorf("%s: got %v, want %v", tt.name, got, tt.want) 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /string_fns_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "testing" 4 | 5 | func TestConvertToUnderscore(t *testing.T) { 6 | tests := []struct { 7 | in string 8 | want string 9 | }{ 10 | {"CamelCaseID", "camel_case_id"}, 11 | {"Simple", "simple"}, 12 | {"HTTPServer", "http_server"}, 13 | {"Camel_Case", "camel_case"}, 14 | } 15 | for _, tt := range tests { 16 | got, err := ConvertToUnderscore(tt.in) 17 | if err != nil { 18 | t.Fatalf("ConvertToUnderscore(%q) returned error: %v", tt.in, err) 19 | } 20 | if got != tt.want { 21 | t.Errorf("ConvertToUnderscore(%q) = %q; want %q", tt.in, got, tt.want) 22 | } 23 | } 24 | } 25 | 26 | func TestTrimInnerSpacesToOne(t *testing.T) { 27 | tests := []struct { 28 | in string 29 | want string 30 | }{ 31 | {" Hello world ", "Hello world"}, 32 | {"a\t\tb c", "a b c"}, 33 | {" ", ""}, 34 | } 35 | for _, tt := range tests { 36 | if got := TrimInnerSpacesToOne(tt.in); got != tt.want { 37 | t.Errorf("TrimInnerSpacesToOne(%q) = %q; want %q", tt.in, got, tt.want) 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2025 Daniel Isted 2 | 3 | The MIT License (MIT) 4 | 5 | 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: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | 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. 10 | -------------------------------------------------------------------------------- /examples/DefinitionFiles/structs.txt: -------------------------------------------------------------------------------- 1 | StreetCRUD 2 | [Server] localhost 3 | [User] dan 4 | [Group] sqlgroupname 5 | [Password] secret 6 | [Database] db_name 7 | [schema] public 8 | [ssl] false 9 | [Underscore] true 10 | [package]models 11 | 12 | [Add struct] 13 | [table] 14 | [File name] XUsers 15 | [prepared] true 16 | type User struct { 17 | LoginID int `json:"loginid"` [primary] 18 | Name string `json:"name, omitempty"` [index] [patch][size:255] 19 | Email string `json:"email"` 20 | Password string `json:"password" out:"false"` 21 | Deleted bool `json:”deleted”` [deleted] 22 | DelOn time.Time `json:”deletedon”` [deletedOn][nulls] 23 | } 24 | 25 | [add struct] 26 | [table] blog 27 | [File name] XUsers 28 | [prepared] false 29 | type Blog struct { 30 | BlogID int `json:”blogid"` [primary] 31 | Title string `json:”title, omitempty"` [index] [patch] [size:255] 32 | Body string `json:”body”` [nulls] 33 | CategoryID int `json:”catID”` 34 | Object T [Ignore] 35 | Deleted bool `json:”deleted”` [deleted] 36 | DeletedOn time.Time `json:”deletedon”` [deletedOn][nulls] 37 | 38 | } 39 | 40 | 41 | [alter table] user 42 | [copy cols] 43 | login_id [to] LogID 44 | name [to] UserName 45 | email [to] Email 46 | [add Struct] 47 | [table] tbl_new_name 48 | [File name] x9.go 49 | [prepared] false 50 | type UserNew struct { 51 | LogID int `json:"loginid"` [primary] 52 | UserName string `json:”userName”` [index][patch][size:255] 53 | Phone string `json:”phone”` [nulls] 54 | Email string `json:”email”` [nulls] 55 | Password string `json:"password" out:"false"` 56 | } 57 | -------------------------------------------------------------------------------- /string_fns.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "unicode" 7 | "unicode/utf8" 8 | ) 9 | 10 | func ConvertToUnderscore(camel string) (string, error) { 11 | if camel == "" { 12 | return "", nil 13 | } 14 | 15 | runes := []rune(camel) 16 | if !unicode.IsLetter(runes[0]) { 17 | return "", fmt.Errorf("Table and column names can't start with a character other than a letter.") 18 | } 19 | 20 | var underscore []rune 21 | underscore = append(underscore, unicode.ToLower(runes[0])) 22 | 23 | for i := 1; i < len(runes); i++ { 24 | r := runes[i] 25 | 26 | if !(r == '_' || unicode.IsLetter(r) || unicode.IsDigit(r)) { 27 | return "", fmt.Errorf("Table and column names can't contain non-alphanumeric characters.") 28 | } 29 | 30 | prev := runes[i-1] 31 | 32 | if unicode.IsUpper(r) { 33 | nextIsLower := i+1 < len(runes) && unicode.IsLower(runes[i+1]) 34 | if prev != '_' && (unicode.IsLower(prev) || (unicode.IsUpper(prev) && nextIsLower)) { 35 | underscore = append(underscore, '_') 36 | } 37 | underscore = append(underscore, unicode.ToLower(r)) 38 | } else { 39 | underscore = append(underscore, r) 40 | } 41 | } 42 | 43 | return string(underscore), nil 44 | } 45 | 46 | func UpperCaseFirstChar(word string) string { 47 | runes := []rune(word) 48 | if len(runes) > 0 { 49 | if unicode.IsLower(runes[0]) { 50 | runes[0] = unicode.ToUpper(runes[0]) 51 | } 52 | } 53 | return string(runes) 54 | } 55 | 56 | func LowerCaseFirstChar(word string) string { 57 | runes := []rune(word) 58 | if len(runes) > 0 { 59 | if unicode.IsUpper(runes[0]) { 60 | runes[0] = unicode.ToLower(runes[0]) 61 | } 62 | } 63 | return string(runes) 64 | } 65 | 66 | func AddQuotesIfAnyUpperCase(dbOrSchema string) string { 67 | for _, letter := range dbOrSchema { 68 | if unicode.IsUpper(letter) { 69 | dbOrSchema = "\"" + dbOrSchema + "\"" 70 | break 71 | } 72 | } 73 | return dbOrSchema 74 | } 75 | 76 | func TrimInnerSpacesToOne(spacey string) string { 77 | 78 | if strings.TrimSpace(spacey) == "" { 79 | return "" 80 | } 81 | var runeSlice []rune 82 | var isAtStart bool = true 83 | var isWord bool = false 84 | for _, runeChar := range spacey { 85 | if runeChar != ' ' && runeChar != '\t' && isAtStart { 86 | runeSlice = append(runeSlice, runeChar) 87 | isAtStart = false 88 | isWord = true 89 | } else if isWord { 90 | if runeChar != ' ' && runeChar != '\t' { 91 | runeSlice = append(runeSlice, runeChar) 92 | } else { 93 | runeSlice = append(runeSlice, ' ') 94 | isWord = false 95 | } 96 | } else if !isWord { 97 | if runeChar != ' ' && runeChar != '\t' { 98 | runeSlice = append(runeSlice, runeChar) 99 | isWord = true 100 | } 101 | } 102 | } 103 | if runeSlice[len(runeSlice)-1] == ' ' { 104 | return fmt.Sprint(string(runeSlice[:len(runeSlice)-1])) 105 | } else { 106 | return fmt.Sprint(string(runeSlice)) 107 | } 108 | 109 | } 110 | 111 | func ChangeCaseForRange(changeMe string, startIndex int, endIndex int) string { 112 | if changeMe == "" || utf8.RuneCountInString(changeMe) < endIndex+1 || startIndex > endIndex || startIndex < 0 { 113 | return changeMe 114 | } 115 | newWord := []rune(changeMe) 116 | for ; startIndex <= endIndex; startIndex++ { 117 | newWord[startIndex] = unicode.ToLower(newWord[startIndex]) 118 | } 119 | return string(newWord) 120 | } 121 | -------------------------------------------------------------------------------- /models.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "unicode" 7 | ) 8 | 9 | type structToCreate struct { 10 | cols []*column 11 | oldAltCols []string 12 | newAltCols []string 13 | oldColPrim string 14 | actionType string 15 | structName string 16 | tableName string 17 | database string 18 | schema string 19 | filePath string 20 | fileName string 21 | hasKey bool 22 | nullsPkg bool 23 | prepared bool 24 | } 25 | 26 | type column struct { 27 | colName string 28 | varName string 29 | structLine string 30 | goType string 31 | dbType string 32 | primary bool 33 | index bool 34 | patch bool 35 | size string // "" if not varchar w/ size 36 | deleted bool 37 | deletedOn bool 38 | nulls bool 39 | } 40 | 41 | func (struc *structToCreate) CheckStructForDeletes() bool { 42 | var delCnt, delOnCnt int 43 | for _, col := range struc.cols { 44 | if col.deleted { 45 | delCnt++ 46 | } 47 | if col.deletedOn { 48 | delOnCnt++ 49 | } 50 | } 51 | if delCnt != delOnCnt || delCnt > 1 || delOnCnt > 1 { 52 | return false 53 | } 54 | return true 55 | } 56 | 57 | func (col *column) MapGoTypeToDBTypes() (bool, string) { 58 | switch strings.ToLower(col.goType) { 59 | case "int", "int8", "int16", "int32", "uint", "uint8", "uint16", "uint32", "uintptr", "byte": 60 | col.dbType = "integer" 61 | case "int64", "uint64": 62 | col.dbType = "bigint" 63 | case "float32": 64 | col.dbType = "real" 65 | case "float64": 66 | col.dbType = "double precision" 67 | case "bool": 68 | col.dbType = "boolean" 69 | case "time.time": 70 | col.dbType = "timestamp without time zone" 71 | case "string": 72 | if col.size == "" { 73 | col.dbType = "character varying" 74 | } else { 75 | col.dbType = "character varying(" + col.size + ")" 76 | } 77 | case "rune": 78 | col.dbType = "character varying" 79 | case "[]byte": 80 | col.dbType = "bytea" 81 | 82 | default: 83 | return false, "A non-supported data type (" + col.goType + ") was provided. The [ignore] option can be added to the end of a struct variable allowing it to be ignored for code generation." 84 | } 85 | return true, "" 86 | } 87 | 88 | func (col *column) MapNullTypes() error { 89 | switch strings.ToLower(col.goType) { 90 | case "int": 91 | col.goType = "nulls.Int" 92 | case "int32": 93 | col.goType = "nulls.Int32" 94 | case "int64": 95 | col.goType = "nulls.Int64" 96 | case "uint32": 97 | col.goType = "nulls.UInt32" 98 | case "float32": 99 | col.goType = "nulls.Float32" 100 | case "float64": 101 | col.goType = "nulls.Float64" 102 | case "bool": 103 | col.goType = "nulls.Bool" 104 | case "time.time": 105 | col.goType = "nulls.Time" 106 | case "string": 107 | col.goType = "nulls.String" 108 | case "[]byte": 109 | col.goType = "nulls.ByteSlice" 110 | default: 111 | return fmt.Errorf("A non-supported data type (" + col.goType + ") was provided as a nullable column. Types must be int64, uint32, int32, int, float64, float32, string, bool, time.Time, or []byte.") 112 | } 113 | return nil 114 | } 115 | 116 | func CheckColAndTblNames(name string) error { 117 | runes := []rune(name) 118 | if len(runes) < 1 { 119 | return fmt.Errorf("The name was left empty.") 120 | } 121 | if !unicode.IsLetter(runes[0]) { 122 | return fmt.Errorf("The first character of the name must start w/ a letter.") 123 | } 124 | for i := 1; i < len(runes); i++ { 125 | if !unicode.IsLetter(runes[i]) && !unicode.IsDigit(runes[i]) && runes[i] != '_' { 126 | return fmt.Errorf("At least one character in the name was either not a letter, number, or underscore.") 127 | } 128 | } 129 | return nil 130 | } 131 | -------------------------------------------------------------------------------- /examples/UsesGeneratedCode/main.go: -------------------------------------------------------------------------------- 1 | //The following is example code that shows 2 | //the basics of how to interact with the 3 | //generated struct file user.go. Process the crudGen.txt 4 | //file to create the table that corresponds with user.go 5 | package main 6 | 7 | import ( 8 | "./models" 9 | "database/sql" 10 | "fmt" 11 | _ "github.com/lib/pq" 12 | "github.com/markbates/going/nulls" 13 | "strconv" 14 | "strings" 15 | "time" 16 | ) 17 | 18 | //IMPORTANT NOTE: 19 | //if user.go was generated w/o prepared statements, the 20 | //models.InitUserDataLayer() and models.CloseUserStmts() 21 | //shouldn't be called. Instead just pass a DB connection 22 | //pointer to your generated file's global DB variable. 23 | //e.g. models.UserDB = userDB before calling any struct 24 | //methods such as Insert(), GetByID(), etc. 25 | //Many of the functions and methods return errors, but I don't use 26 | //them much below since this is just from demonstration purposes 27 | 28 | func main() { 29 | 30 | //Open local DB connection 31 | userDB, _ := sql.Open("postgres", "postgres://userName:Password@localhost/dbName?sslmode=disable") 32 | 33 | //Prepare statments and assign DB 34 | models.InitUserDataLayer(userDB) 35 | 36 | //Fill User struct and then insert it into the DB 37 | var user *models.User = &models.User{} 38 | user.Name = "Viki" 39 | user.Email = nulls.NewString("Viki@demo.com") //nulls.String{"Viki@demo.com", true} 40 | user.Password = "pass" 41 | user.DeletedUser = false 42 | err := user.Insert() 43 | if err != nil { 44 | fmt.Printf("Insert Error: %s", err.Error()) 45 | } 46 | fmt.Printf("%s Inserted with an ID of %d\n", user.Name, user.LoginID) 47 | 48 | //Lookup newly added User by LoginID 49 | user.GetByID(user.LoginID, models.ALL) 50 | fmt.Printf("%s just retrieved from the DB\n", user.Name) 51 | 52 | //Print JSON representation of User object 53 | message, _ := user.ToJSON() 54 | fmt.Printf("%s as JSON: %s\n", user.Name, string(message)) 55 | 56 | //Get a new User object from the DB using an existing LoginID 57 | user2, _ := models.NewUser(user.LoginID, models.EXISTS) 58 | user2.Name = "Victoria" 59 | 60 | //Update Name change in DB 61 | user2.Update() 62 | fmt.Printf("Updated name %s to %s in the DB\n", user.Name, user2.Name) 63 | 64 | //Insert two more rows for the demo 65 | user3 := models.User{Name: "Sam", Email: nulls.NewNullString("Sam@Sam.com"), Password: "secret"} 66 | user3.Insert() 67 | user3.Email = nulls.NewString("ChangeLocal") 68 | user3.Password = "newSam" 69 | user3.Insert() 70 | 71 | //Get function gets rows by an index (Name in this case) 72 | var loginIDs []string 73 | users, _ := models.GetUsersByName("Sam", models.ALL) 74 | fmt.Println("The following LoginID's have the Name Sam: ") 75 | for _, usr := range users { 76 | loginIDs = append(loginIDs, strconv.Itoa(usr.LoginID)) 77 | } 78 | fmt.Println(strings.Join(loginIDs, ", ")) 79 | 80 | //Convert the returned users slice to JSON 81 | baUsers, _ := models.UsersToJSON(users) 82 | fmt.Println("Convert and print users named Sam to JSON: ") 83 | fmt.Println(string(baUsers)) 84 | 85 | //Convert JSON to a User struct 86 | jsonUser := []byte(`{"loginID":15,"name":"Rachel","email":"r@r.com","password":"pw","deleted":false, "deletedOn":null}`) 87 | user4, e0 := models.UserFromJSON(jsonUser) 88 | if e0 != nil { 89 | fmt.Println(e0.Error()) 90 | } 91 | fmt.Printf("%s was converted from JSON to a User struct.", user4.Name) 92 | 93 | //Insert 94 | user4.Insert() 95 | fmt.Printf("\n%s was inserted in the DB. Her LoginID is %d.", user4.Name, user4.LoginID) 96 | 97 | //Mark Rachel as a deleted user 98 | user4.MarkDeleted(true, nulls.Time{time.Now(), true}) 99 | fmt.Printf("\n%s marked as deleted at %v.\n", user4.Name, user4.DelOn.Time) 100 | 101 | //Delete 102 | user3.Delete() 103 | fmt.Printf("%s (LoginID: %d) was permanently deleted.\n", user3.Name, user3.LoginID) 104 | 105 | //Patch Name 106 | fmt.Printf("%s's name has been changed to ", user4.Name) 107 | user4.PatchName("Shallan") 108 | fmt.Printf("%s.\n", user4.Name) 109 | fmt.Println("Only the name column was updated(patched).\n") 110 | 111 | //Close the User prepared SQL statements 112 | models.CloseUserStmts() 113 | 114 | //Close local DB connection 115 | userDB.Close() 116 | } 117 | -------------------------------------------------------------------------------- /examples/UsesGeneratedCode/models/user.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "database/sql" 5 | "encoding/json" 6 | _ "github.com/lib/pq" 7 | "github.com/markbates/going/nulls" 8 | "log" 9 | ) 10 | 11 | //Global Data Layer 12 | var userSQL UserDataLayer 13 | 14 | const ( 15 | EXISTS = iota 16 | DELETED = iota 17 | ALL = iota 18 | ) 19 | 20 | type User struct { 21 | LoginID int `json:"loginid"` 22 | Name string `json:"name,omitempty"` 23 | Email nulls.String `json:"email"` 24 | Password string `json:"password" out:"false"` 25 | DeletedUser bool `json:"deleted"` 26 | DelOn nulls.Time `json:"deletedon"` 27 | } 28 | 29 | //Initialize and fill a User object from the DB 30 | func NewUser(loginID int, delFilter int) (*User, error) { 31 | user := new(User) 32 | deleted1 := false 33 | deleted2 := false 34 | switch delFilter { 35 | case DELETED: 36 | deleted1 = true 37 | deleted2 = true 38 | case ALL: 39 | deleted2 = true 40 | } 41 | row := userSQL.GetByID.QueryRow(loginID, deleted1, deleted2) 42 | err := row.Scan(&user.LoginID, &user.Name, &user.Email, &user.Password, &user.DeletedUser, &user.DelOn) 43 | if err != nil { 44 | log.Println(err.Error()) 45 | return nil, err 46 | } 47 | return user, nil 48 | } 49 | 50 | //Transform JSON into a User object 51 | func UserFromJSON(userJSON []byte) (*User, error) { 52 | user := new(User) 53 | err := json.Unmarshal(userJSON, user) 54 | if err != nil { 55 | log.Println(err.Error()) 56 | return nil, err 57 | } 58 | return user, nil 59 | } 60 | 61 | //Convert a User object to JSON 62 | func (user *User) ToJSON() ([]byte, error) { 63 | userJSON, err := json.Marshal(user) 64 | return userJSON, err 65 | } 66 | 67 | //Convert multiple User objects to JSON 68 | func UsersToJSON(users []*User) ([]byte, error) { 69 | usersJSON, err := json.Marshal(users) 70 | return usersJSON, err 71 | } 72 | 73 | //Fill User object with data from DB 74 | func (user *User) GetByID(loginID int, delFilter int) error { 75 | deleted1 := false 76 | deleted2 := false 77 | switch delFilter { 78 | case DELETED: 79 | deleted1 = true 80 | deleted2 = true 81 | case ALL: 82 | deleted2 = true 83 | } 84 | row := userSQL.GetByID.QueryRow(loginID, deleted1, deleted2) 85 | err := row.Scan(&user.LoginID, &user.Name, &user.Email, &user.Password, &user.DeletedUser, &user.DelOn) 86 | if err != nil { 87 | log.Println(err.Error()) 88 | return err 89 | } 90 | return nil 91 | } 92 | 93 | //Insert User object to DB 94 | func (user *User) Insert() error { 95 | var id int 96 | row := userSQL.Insert.QueryRow(user.Name, user.Email, user.Password, user.DeletedUser, user.DelOn) 97 | err := row.Scan(&id) 98 | if err != nil { 99 | log.Println(err.Error()) 100 | return err 101 | } 102 | user.LoginID = id 103 | return nil 104 | } 105 | 106 | //Update User object in DB 107 | func (user *User) Update() error { 108 | _, err := userSQL.Update.Exec(user.Name, user.Email, user.Password, user.DeletedUser, user.DelOn, user.LoginID) 109 | if err != nil { 110 | log.Println(err.Error()) 111 | return err 112 | } 113 | return nil 114 | } 115 | 116 | //Mark a row as deleted and at time.Time 117 | func (user *User) MarkDeleted(del bool, when nulls.Time) error { 118 | _, err := userSQL.MarkDel.Exec(del, when, user.LoginID) 119 | if err != nil { 120 | log.Println(err.Error()) 121 | return err 122 | } 123 | user.DeletedUser = del 124 | user.DelOn = when 125 | return nil 126 | } 127 | 128 | //Delete will remove the matching row from the DB 129 | func (user *User) Delete() error { 130 | _, err := userSQL.Delete.Exec(user.LoginID) 131 | if err != nil { 132 | log.Println(err.Error()) 133 | return err 134 | } 135 | return nil 136 | } 137 | 138 | //Get Users by name 139 | func GetUsersByName(name string, delFilter int) ([]*User, error) { 140 | deleted1 := false 141 | deleted2 := false 142 | switch delFilter { 143 | case DELETED: 144 | deleted1 = true 145 | deleted2 = true 146 | case ALL: 147 | deleted2 = true 148 | } 149 | rows, err := userSQL.GetByName.Query(name, deleted1, deleted2) 150 | if err != nil { 151 | rows.Close() 152 | log.Println(err.Error()) 153 | return nil, err 154 | } 155 | users := []*User{} 156 | for rows.Next() { 157 | user := new(User) 158 | if err = rows.Scan(&user.LoginID, &user.Name, &user.Email, &user.Password, &user.DeletedUser, &user.DelOn); err != nil { 159 | log.Println(err.Error()) 160 | rows.Close() 161 | return users, err 162 | } 163 | users = append(users, user) 164 | } 165 | 166 | rows.Close() 167 | return users, nil 168 | } 169 | 170 | //Get Users by email 171 | func GetUsersByEmail(email nulls.String, delFilter int) ([]*User, error) { 172 | deleted1 := false 173 | deleted2 := false 174 | switch delFilter { 175 | case DELETED: 176 | deleted1 = true 177 | deleted2 = true 178 | case ALL: 179 | deleted2 = true 180 | } 181 | rows, err := userSQL.GetByEmail.Query(email, deleted1, deleted2) 182 | if err != nil { 183 | rows.Close() 184 | log.Println(err.Error()) 185 | return nil, err 186 | } 187 | users := []*User{} 188 | for rows.Next() { 189 | user := new(User) 190 | if err = rows.Scan(&user.LoginID, &user.Name, &user.Email, &user.Password, &user.DeletedUser, &user.DelOn); err != nil { 191 | log.Println(err.Error()) 192 | rows.Close() 193 | return users, err 194 | } 195 | users = append(users, user) 196 | } 197 | 198 | rows.Close() 199 | return users, nil 200 | } 201 | 202 | //Update name only 203 | func (user *User) PatchName(name string) error { 204 | _, err := userSQL.PatchName.Exec(name, user.LoginID) 205 | if err != nil { 206 | log.Println(err.Error()) 207 | return err 208 | } 209 | user.Name = name 210 | return nil 211 | } 212 | 213 | //DataLayer is used to store prepared SQL statements 214 | type UserDataLayer struct { 215 | DB *sql.DB 216 | GetByID *sql.Stmt 217 | Update *sql.Stmt 218 | Insert *sql.Stmt 219 | Delete *sql.Stmt 220 | MarkDel *sql.Stmt 221 | GetByName *sql.Stmt 222 | GetByEmail *sql.Stmt 223 | PatchName *sql.Stmt 224 | Init bool 225 | } 226 | 227 | //InitUserDataLayer prepares SQL statements and assigns the passed in DB pointer 228 | func InitUserDataLayer(db *sql.DB) error { 229 | var err error 230 | if !userSQL.Init { 231 | userSQL.GetByID, err = db.Prepare("SELECT login_id, name, email, password, deleted_user, del_on FROM vikiblog.public.user WHERE login_id = $1 and (deleted_user = $2 or deleted_user = $3)") 232 | userSQL.Update, err = db.Prepare("UPDATE vikiblog.public.user SET name = $1, email = $2, password = $3, deleted_user = $4, del_on = $5 WHERE login_id = $6") 233 | userSQL.Insert, err = db.Prepare("INSERT INTO vikiblog.public.user (name, email, password, deleted_user, del_on) VALUES ($1, $2, $3, $4, $5) RETURNING login_id") 234 | userSQL.MarkDel, err = db.Prepare("UPDATE vikiblog.public.user SET deleted_user = $1, del_on = $2 WHERE login_id = $3") 235 | userSQL.Delete, err = db.Prepare("DELETE from vikiblog.public.user WHERE login_id = $1") 236 | userSQL.GetByName, err = db.Prepare("SELECT login_id, name, email, password, deleted_user, del_on FROM vikiblog.public.user WHERE name = $1 and (deleted_user = $2 or deleted_user = $3) ORDER BY login_id") 237 | userSQL.GetByEmail, err = db.Prepare("SELECT login_id, name, email, password, deleted_user, del_on FROM vikiblog.public.user WHERE email = $1 and (deleted_user = $2 or deleted_user = $3) ORDER BY login_id") 238 | userSQL.PatchName, err = db.Prepare("UPDATE vikiblog.public.user SET name = $1 WHERE login_id = $2") 239 | userSQL.Init = true 240 | userSQL.DB = db 241 | } 242 | return err 243 | } 244 | 245 | //CloseUserStmts should be called when prepared SQL statements aren't needed anymore 246 | func CloseUserStmts() { 247 | if userSQL.Init { 248 | userSQL.GetByID.Close() 249 | userSQL.Update.Close() 250 | userSQL.Insert.Close() 251 | userSQL.Delete.Close() 252 | userSQL.MarkDel.Close() 253 | userSQL.GetByName.Close() 254 | userSQL.GetByEmail.Close() 255 | userSQL.PatchName.Close() 256 | userSQL.Init = false 257 | } 258 | } 259 | -------------------------------------------------------------------------------- /db_gen.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "database/sql" 6 | "fmt" 7 | _ "github.com/lib/pq" 8 | "log" 9 | "strconv" 10 | "strings" 11 | ) 12 | 13 | // BuildConnString builds the connection string from file 14 | func BuildConnString(dbUser string, password string, dbName string, server string, useSSL bool) string { 15 | 16 | var buffer bytes.Buffer 17 | 18 | buffer.WriteString("user=") 19 | buffer.WriteString(dbUser) 20 | buffer.WriteString(" dbname=") 21 | buffer.WriteString(dbName) 22 | buffer.WriteString(" host=") 23 | buffer.WriteString(server) 24 | buffer.WriteString(" password=") 25 | buffer.WriteString(password) 26 | buffer.WriteString(" sslmode=") 27 | if !useSSL { 28 | buffer.WriteString("disable") 29 | } else { 30 | // lib/pq expects "require" to enable SSL 31 | buffer.WriteString("require") 32 | } 33 | 34 | return buffer.String() 35 | } 36 | 37 | // CreateOrAlterTables creates and alters tables based on the struct definition file 38 | func CreateOrAlterTables(structObj *structToCreate, db *sql.DB, group string) { 39 | var tablePathName string = fmt.Sprintf("%s.%s.%s", AddQuotesIfAnyUpperCase(structObj.database), AddQuotesIfAnyUpperCase(structObj.schema), structObj.tableName) 40 | var oldTableName string 41 | var row *sql.Row 42 | var err error 43 | var indexes []string 44 | var indexNames []string 45 | var primCol string 46 | 47 | //find values for needed variables 48 | for _, col := range structObj.cols { 49 | if col.primary { 50 | primCol = col.colName 51 | } 52 | if col.index { 53 | indexNames = append(indexNames, fmt.Sprintf("ix_%s_%s", structObj.tableName, col.colName)) 54 | indexes = append(indexes, fmt.Sprintf("CREATE INDEX ix_%s_%s ON %s USING btree (%s);", structObj.tableName, col.colName, tablePathName, col.colName)) 55 | } 56 | } 57 | 58 | //Check if a table exists when [alter table] is in the input file 59 | checkTable := "SELECT EXISTS(SELECT * FROM information_schema.tables WHERE table_name = $1 and table_schema = $2)" 60 | if len(structObj.newAltCols) > 0 { 61 | //copy data from oldAltCols to newAltCols 62 | var exists bool 63 | row = db.QueryRow(checkTable, structObj.actionType, structObj.schema) 64 | err = row.Scan(&exists) 65 | if err != nil { 66 | log.Println("\nAn error occurred checking for the table's existence :" + err.Error() + "\n") 67 | return 68 | } 69 | if !exists { 70 | fmt.Println(fmt.Sprintf("The table %s to be altered doesn't exist in the database. Please make sure the table name matches the name in your Street CRUD file.", structObj.actionType)) 71 | return 72 | } 73 | } 74 | 75 | loop := true 76 | renameTable := structObj.tableName 77 | //rename old table if needed, store new name if copying of data is needed 78 | for i := 1; loop; i++ { 79 | row = db.QueryRow(checkTable, renameTable, structObj.schema) 80 | err = row.Scan(&loop) 81 | if err != nil { 82 | log.Println("\nAn error occurred checking for the table's existence :" + err.Error() + "\n") 83 | return 84 | } 85 | if loop { 86 | //name exists, store name and check again 87 | renameTable = structObj.tableName + strconv.Itoa(i) 88 | } 89 | } 90 | //rename old table if it exists 91 | if renameTable != structObj.tableName { 92 | alterTable := "ALTER TABLE IF EXISTS %s RENAME TO %s;" 93 | _, err = db.Exec(fmt.Sprintf(alterTable, tablePathName, fmt.Sprintf("%s", renameTable))) 94 | if err != nil { 95 | log.Println("\nThere was an issue changing the existing table's name: " + err.Error() + "\n") 96 | return 97 | } 98 | } 99 | 100 | //Determine the table name for copying from 101 | if structObj.actionType != "Add" { 102 | if structObj.tableName == structObj.actionType { 103 | //use rename 104 | oldTableName = fmt.Sprintf("%s.%s.%s", AddQuotesIfAnyUpperCase(structObj.database), AddQuotesIfAnyUpperCase(structObj.schema), renameTable) 105 | } else { 106 | //structObj.actionType 107 | oldTableName = fmt.Sprintf("%s.%s.%s", AddQuotesIfAnyUpperCase(structObj.database), AddQuotesIfAnyUpperCase(structObj.schema), structObj.actionType) 108 | } 109 | } 110 | 111 | //Check and rename old primary key constraint if needed 112 | pgClassStmt := "SELECT EXISTS(SELECT relname FROM pg_class WHERE relname = $1)" 113 | loop = true 114 | pkConstraint := fmt.Sprintf("pk_%s_%s", structObj.tableName, primCol) 115 | pkRename := pkConstraint 116 | for i := 1; loop; i++ { 117 | row = db.QueryRow(pgClassStmt, pkRename) 118 | err = row.Scan(&loop) 119 | if err != nil { 120 | log.Println("\nAn error occurred checking for the primary key constraint's name:" + err.Error() + "\n") 121 | return 122 | } 123 | if loop { 124 | //name exists, store name and check again 125 | pkRename = pkConstraint + strconv.Itoa(i) 126 | } 127 | } 128 | //rename old pk if it exists 129 | if pkConstraint != pkRename { 130 | _, err = db.Exec(fmt.Sprintf("ALTER INDEX %s RENAME TO %s;", pkConstraint, pkRename)) 131 | if err != nil { 132 | log.Println("\nThere was an issue changing the existing pk constraint's name: " + err.Error() + "\n") 133 | return 134 | } 135 | } 136 | 137 | //Check if sequence exists, then rename it if needed 138 | loop = true 139 | seqName := fmt.Sprintf("%s_%s_seq", structObj.tableName, primCol) 140 | seqRename := seqName 141 | for i := 1; loop; i++ { 142 | row = db.QueryRow(pgClassStmt, seqRename) 143 | err = row.Scan(&loop) 144 | if err != nil { 145 | log.Println("An error occurred checking for the sequences' name:" + err.Error() + "\n") 146 | return 147 | } 148 | if loop { 149 | seqRename = seqName + strconv.Itoa(i) 150 | } 151 | } 152 | //rename old seq if it exists 153 | if seqName != seqRename { 154 | _, err = db.Exec(fmt.Sprintf("ALTER SEQUENCE %s RENAME TO %s;", seqName, seqRename)) 155 | if err != nil { 156 | log.Println("\nThere was an issue changing the existing sequences' name: " + err.Error() + "\n") 157 | return 158 | } 159 | } 160 | seqName = fmt.Sprintf("%s.%s", AddQuotesIfAnyUpperCase(structObj.schema), seqName) 161 | 162 | //Check if indexes exist, then rename if needed 163 | var indexRename string 164 | for _, index := range indexNames { 165 | indexRename = index 166 | loop = true 167 | for i := 1; loop; i++ { 168 | row = db.QueryRow(pgClassStmt, indexRename) 169 | err = row.Scan(&loop) 170 | if err != nil { 171 | log.Println("\nAn error occurred checking for an indexes' name:" + err.Error() + "\n") 172 | return 173 | } 174 | if loop { 175 | indexRename = index + strconv.Itoa(i) 176 | } 177 | } 178 | if index != indexRename { 179 | _, err = db.Exec(fmt.Sprintf("ALTER INDEX %s RENAME TO %s;", index, indexRename)) 180 | if err != nil { 181 | log.Println("\nThere was an issue changing the existing indexes' name: " + err.Error() + "\n") 182 | return 183 | } 184 | } 185 | } 186 | 187 | //Create new table 188 | var buffer bytes.Buffer 189 | buffer.WriteString(fmt.Sprintf("CREATE TABLE IF NOT EXISTS %s (", tablePathName)) 190 | for i, col := range structObj.cols { 191 | buffer.WriteString(fmt.Sprintf("%s %s ", col.colName, col.dbType)) 192 | if !col.nulls || col.primary { 193 | buffer.WriteString("NOT NULL") 194 | } 195 | if col.deleted { 196 | buffer.WriteString(" DEFAULT false") 197 | } 198 | if i < len(structObj.cols)-1 { 199 | buffer.WriteString(", ") 200 | } 201 | } 202 | buffer.WriteString(" ) WITH (OIDS=FALSE);") 203 | _, err = db.Exec(buffer.String()) 204 | if err != nil { 205 | log.Println("Issue creating table: " + err.Error() + "\n") 206 | return 207 | } 208 | 209 | //Alter permissions 210 | buffer.Reset() 211 | buffer.WriteString(fmt.Sprintf("ALTER TABLE %s OWNER to %s; GRANT ALL ON TABLE %s TO %s;", tablePathName, group, tablePathName, group)) 212 | _, err = db.Exec(buffer.String()) 213 | if err != nil { 214 | log.Println("\nIssue assigning permissions: " + err.Error() + "\n") 215 | return 216 | } 217 | 218 | //Copy data from old table to new table if [alter table] 219 | lastSequence := 1 220 | copyData := true 221 | if oldTableName != "" { 222 | selectFromOld := fmt.Sprintf("SELECT %s FROM %s", strings.Join(structObj.oldAltCols, ", "), oldTableName) 223 | insertToNew := fmt.Sprintf("INSERT INTO %s (%s) (%s)", tablePathName, strings.Join(structObj.newAltCols, ", "), selectFromOld) 224 | _, err = db.Exec(insertToNew) 225 | if err != nil { 226 | log.Printf("\nIssue copying data from %s to %s: %s\n", oldTableName, tablePathName, err.Error()) 227 | copyData = false 228 | } 229 | if structObj.oldColPrim != "" && copyData { 230 | //make sure old table has rows. 231 | numRows := 1 232 | row = db.QueryRow(fmt.Sprintf("SELECT COUNT(*) FROM %s", oldTableName)) 233 | err = row.Scan(&numRows) 234 | if err != nil { 235 | log.Println("\nIssue checking table being altered" + err.Error() + "\n") 236 | return 237 | } 238 | if numRows > 0 { 239 | //Get last value for primary key 240 | row = db.QueryRow(fmt.Sprintf("Select MAX(%s) from %s", structObj.oldColPrim, oldTableName)) 241 | err = row.Scan(&lastSequence) 242 | if err != nil { 243 | log.Println("\nIssue reading table's primary key :" + err.Error() + "\n") 244 | return 245 | } 246 | lastSequence = lastSequence + 1 247 | } 248 | } 249 | } 250 | 251 | //Add Primary Key 252 | _, err = db.Exec(fmt.Sprintf("ALTER TABLE %s ADD CONSTRAINT %s PRIMARY KEY (%s);", tablePathName, pkConstraint, primCol)) 253 | if err != nil { 254 | log.Println("\nCreating the primary key constraint failed: " + err.Error() + "\n") 255 | return 256 | } 257 | 258 | //Create and add sequence to primary key 259 | _, err = db.Exec(fmt.Sprintf("CREATE SEQUENCE %s INCREMENT 1 MINVALUE 1 MAXVALUE 9223372036854775807 START %d CACHE 1; ALTER TABLE %s OWNER to %s; GRANT ALL ON TABLE %s TO %s;", seqName, lastSequence, seqName, group, seqName, group)) 260 | if err != nil { 261 | log.Println("\nCreating the primary key sequence failed: " + err.Error() + "\n") 262 | return 263 | } 264 | 265 | //Bind sequence to primary key column as its defualt value 266 | _, err = db.Exec(fmt.Sprintf("ALTER TABLE %s ALTER COLUMN %s SET DEFAULT nextval('%s'::regclass);", tablePathName, primCol, seqName)) 267 | if err != nil { 268 | log.Println("\nBinding the default primary key sequence failed: " + err.Error() + "\n") 269 | return 270 | } 271 | 272 | //Loop and add indexes if needed 273 | for _, stmt := range indexes { 274 | _, err = db.Exec(stmt) 275 | if err != nil { 276 | log.Println("\nCreating an index failed: " + err.Error() + "\n") 277 | return 278 | } 279 | } 280 | 281 | } 282 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # StreetCRUD 2 | 3 | #### Videos: 4 | * A three minute [overview](http://youtu.be/R_FQdpizIDQ "StreetCRUD Demo") that demonstrates StreetCRUD. 5 | * A longer, more detailed [video](http://youtu.be/t3CCy1zSGNw "StreetCRUD Tutorial"). 6 | 7 | #### Introduction 8 | 9 | StreetCRUD is a code and table generation command-line utility for people who aren't fans of ORMs, but appreciate a kick start creating struct methods, tables, and queries for CRUD functionality. You only have to supply your structs, database connection info, and a few keywords in a text file that StreetCRUD will process. Tables, queries, and struct methods will be created or altered automatically. This allows the programmer to add methods and queries at a later date without having to wrestle with an ORM. You keep all the power, but don't have to start at level one. At this time, StreetCRUD supports PostgreSQL, JSON, and Go. 10 | 11 | As a nice side benefit StreetCRUD can be used to easily reorder columns in Postgres tables. The documentation below may look daunting, but it's just thorough. StreetCRUD is actually quite simple and straightforward to use. 12 | 13 | **Features Include**: 14 | * Table creation (if it doesn't exist), table alteration (if it exists) 15 | * Generates Get, Insert, Update, Patch (optional), GetByIndex (optional), and Delete struct methods (with corresponding queries) 16 | * StreetCRUD can be rerun to alter methods and queries if there is a struct change 17 | * Table data is safely copied via a map if a struct/table is altered 18 | * Methods return and receive JSON 19 | * Reordering of table columns when a struct is altered (something pgAdmin doesn't support) 20 | * Queries can be prepared for optimal performance 21 | * Null columns are supported for most data types 22 | * Great for new gophers learning how Go uses databases. Also useful for understanding data types, structs, and struct methods 23 | 24 | **Installation (OS X)**: 25 | 26 | ~~~ 27 | $ go get github.com/isted/StreetCRUD 28 | ~~~ 29 | 30 | If GOBIN isn't set, you can run the following before you install StreetCRUD: 31 | ~~~ 32 | $ export GOBIN=/Users/userName/go/bin 33 | ~~~ 34 | 35 | Install: 36 | ~~~ 37 | $ go install github.com/isted/StreetCRUD 38 | ~~~ 39 | Run: 40 | ~~~ 41 | $ cd ~/go/bin/StreetCRUD && ./StreetCRUD 42 | ~~~ 43 | 44 | ## Getting Started 45 | 46 | The user will need to create a text file that defines information about his or her database connection, structs (data models), and keywords. This file will be processed by StreetCRUD and used for generating go code, SQL queries, tables, and columns. Below is an example file that would add two new structs/tables and alter a third struct/table. Inconsistent spacing and capitalization are used on purpose to demonstrate that they aren't an issue for file processing. Keywords are defined below the example file. 47 | 48 | ~~~ 49 | StreetCRUD 50 | [Server] localhost 51 | [User] dan 52 | [Group] sqlgroupname 53 | [Password] secret 54 | [Database] db_name 55 | [schema] public 56 | [ssl] false 57 | [Underscore] true 58 | [package]models 59 | 60 | [Add struct] 61 | [table] 62 | [File name] XUsers 63 | [prepared] true 64 | type User struct { 65 | LoginID int `json:"loginid"` [primary] 66 | Name string `json:"name, omitempty"` [index] [patch] [size:255] 67 | Email string `json:"email"` 68 | Password string `json:"password" out:"false"` 69 | Deleted bool `json:”deleted”` [deleted] 70 | DelOn time.Time `json:”deletedon”` [deletedOn][nulls] 71 | } 72 | 73 | [add struct] 74 | [table] blog 75 | [File name] 76 | [prepared] false 77 | type Blog struct { 78 | BlogID int `json:”blogid"` [primary] 79 | Title string `json:”title, omitempty"` [index][patch][size:255] 80 | Body string `json:”body”` [nulls] 81 | CategoryID int `json:”catID”` 82 | Object T [Ignore] 83 | Deleted bool `json:”deleted”` [deleted] 84 | DeletedOn time.Time `json:”deletedon”` [deletedOn][nulls] 85 | 86 | } 87 | 88 | [alter table] user 89 | [copy cols] 90 | login_id [to] LogID 91 | name [to] UserName 92 | email [to] Email 93 | [add Struct] 94 | [table] tbl_new_name 95 | [File name] x9.go 96 | [prepared] false 97 | type UserNew struct { 98 | LogID int `json:"loginid"` [primary] 99 | UserName string `json:”userName”` [index][patch] [size:255] 100 | Phone string `json:”phone”` [nulls] 101 | Email string `json:”email”` [nulls] 102 | Password string `json:"password" out:"false"` 103 | } 104 | ~~~ 105 | 106 | #### Keywords and Definitions: 107 | 108 | The Following keywords need to appear at the top of the user created text file before structs are defined (one key-value pair per line). A value needs to be typed next to the keyword. For instance, [Server] localhost indicates that the name of the database server is localhost. The keywords defined below will let StreetCRUD connect and make changes to your database server. Some of the other key-values pairs will indicate package and table naming options. Keywords and their values are not case sensitive, also spaces after the "]" symbol are ignored. StreetCRUD includes an example struct files for you to test out and process. You will need to create the database, database user, and enter all relevant server information at the top of the example struct files so that they can be processed properly. 109 | 110 | - **[Server]**: Name of the database server where tables should be created (for many: localhost) 111 | - **[User]**: User name used to login to the database (must already exist and needs to have database creation rights) 112 | - **[Group]**: Group role in Postgres. If left blank, the user name will be used. Postgres prefers a group to be given security rights instead of a user. Users should be assigned to groups (must already exist on the database server). 113 | - **[Password]**: Password for above User 114 | - **[Database]**: Name of the database where the tables need to be created (must already exist) 115 | - **[Schema]**: The name of the schema where the table should be created. PostgreSQL uses public by default. If no value is given for [Schema], public will be used by default. **Important**: The schema must already exist in the database. 116 | - **[SSL]**: A value of true or false will indicate if the connection uses ssl. 117 | - **[Underscore]**: A value of true or false will indicate whether table and column names will be formatted with underscores. Since Postgres doesn't support camel cased names without quotes, all table and column names will be converted to lower case whether or not underscores are used. 118 | - **[Package]**: Name of the package for the generated code. If more than one package is required, two separate files will need to be processed by StreetCRUD, each with a different value for [Package]. 119 | 120 | The next areas of the text file consist of structs used for code and table generation. The structs are in Go syntax with a few modifications to allow StreetCRUD to generate the proper tables and functions. four keywords appear above the struct to indicate action, table name, file name, and to use prepared SQL statements. Table name and file name can be left blank, causing default names to be used based on the struct name. Additional keywords are used at the end of each line of a struct variable. They indicate what type of column should be created in the database (e.g. [primary] to signal that that column is the primary key). Some of these keywords also cause additional methods to be generated. 121 | 122 | #### Keywords Above a Struct for Generating New Code/Table 123 | - **[add struct]**: Indicates that a new struct needs to be processed and added to the database. No text is required next to [add struct]. 124 | - **[table]**: A table name can be added next to this or it can be left blank to allow for default naming (e.g., tbl_structName). Table names will be converted to lower case and named using underscores if [Underscore] is set to true. For example, "[table] tblName" will create a new table named tblname or tbl_name depending on the [Underscore setting. If there is only empty space after "[table]" and the name of the defined struct is User, the table name will be tbl_user or tbluser. 125 | - **[file name]**: A file name such as user.go can be added to the right of [file name] which will cause the code-generation file to be named user.go. If no name is given, default naming based on struct name will be used. If multiple struct definitions use the same value for [file name], code will be generated to the same file and not generated in separate files. 126 | 127 | #### Keywords Above a Struct for Dealing With an Altered Table/Struct 128 | 129 | - **[alter table]**: The name of the table to be altered should appear after the keyword (e.g., [alter table] old_table). This command should be used after changes occur to a previously generated struct. These can include the deletion of a struct variable (dropping a column), altering the order of columns (new struct variable order will mirror new column order), or adding a new struct variable (new column). [alter table] will trigger StreetCRUD to generate new code and a new table. Data will be copied from a previously generated table to the new table. The new table will mirror the new struct, and the data to be copied to the new table will be defined by the user in a series of lines mapping the old column name to the new struct variable name. The order of keywords and struct definition required to follow an [alter table] command can be seen in the example file above, and below they are defined. 130 | - **[copy cols]**: Must appear on the line following [alter table] and before the lines that define how columns are copied from the old table to the new table. This keyword is only used to help improve readability of the user created text file. The mapping of the old column names to the newly defined struct variables should follow this line. 131 | - **OldColName [to] NewStructVarName**: These lines will let StreetCRUD know how data should be copied from the original table to the newly created table (altered table). OldColName is the column name in the existing database table. NewStructVarName is the struct variable name that appears in the new struct. Data from OldColName will be copied to the column that will be created based on the struct variable name. If an OldColName is not mapped, then its data will not be copied to the new table. 132 | - **[add struct]**: Needs to appear before the new struct definition. 133 | - **[table]**: Same as previously defined. 134 | - **[file name]**: Same as above previously defined. 135 | - **[prepared]**: Can be set to true or false. If set to true, then generated code will have prepared sql statements. If false, generated code will have string value sql statements. 136 | 137 | #### Struct Keywords 138 | The following keywords can be added to the end of a line that defines a struct variable. There can be 0 to many keywords at the end of each line. These will alter how columns are defined and what methods should be created. Below is an example taken out of a struct definition 139 | ~~~ 140 | Title string `json:”title"` [index][patch][size:255] 141 | ~~~ 142 | The three keywords are [index], [patch], and [size:255] all of which are optional and are defined below. 143 | - **[primary]**: This is required and can only appear on one variable. The variable must be one of the variety of int types. This will cause the column to be created with a Postgres sequence. The primary key will auto-increment on insert. 144 | - **[index]**: When used, the column will have an index created which will improve SQL search speeds. I have found that when an index is created, it is usually because a search will be performed using the indexed column. Because of this, an additional method is created that will get all rows where the column value equals a passed in value. 145 | - **[patch]**: Causes a patch (update) method to be created where only the column is updated instead of the entire object. At this time, patch methods generated only support the update of one column, but later, patch-groups will be added to allow patch methods to be created that update more than one column at a time. No keyword is needed for the creation of whole-object updates since those are created by default. 146 | - **[size:n]**: n should be an integer value such as 255. This keyword can be used for string variables to let StreetCRUD know the size of the Postgres "character varying" variable to be created. If [size:n] isn't used, then the database column type will be "character varying" with no size, which is the same as the "text" type. 147 | - **[ignore]**: Used when the variable is of non-basic type, such as struct type. StreetCRUD does not yet support nested non-basic types. A variable column marked with [ignore] will not be added to the database and struct methods. 148 | - **[deleted] and [deletedOn]**: When [deleted] is used, the variable type must be bool. When [deletedOn] is used, the variable type must be time.Time. [deleted] and [deletedOn] can only appear on a single variable in a struct, and they can't be on the same variable. Also, the keywords must appear as a pair. A method will be created that sets the [deleted] column to true and sets the [deletedOn] column to the current date and time. 149 | - **[nulls]**: When used, the column will be set to allow null values. The generated variable will use the "github.com/markbates/going/nulls" package null types because they automatically marshal to and from JSON properly. Supported types are string, int64, float64, bool, []byte, float32, int, int32, uint32, and time.Time. Make sure to run the "go get github.com/markbates/going/nulls" command if this keyword is used. Columns marked as both [deleted] and [nulls] will just be marked as [deleted]. 150 | 151 | ## Table and File Creation Handling 152 | The generated code file(s) will not be formatted, but thanks to goFMT, the code will be perfectly formatted after a save in your text editor of choice is performed. 153 | 154 | If a file exists with the same name as a newly generated file, the old file will not be renamed. The generated file will have an incremented number appended. 155 | 156 | Along the same lines, if an [alter table] command is performed, the newly generated Go code will not be appended to a previously generated file, but will be added to a new file. This new code may have to be copied and pasted to the previously generated file because it is safe to assume that custom code may have been added to the originally generated file. 157 | 158 | If a new table is added and there already exists a table with the same name, the old table will be renamed with an incremented number appended. Tables that are altered will not result in the old table being dropped, but, as stated, they will be renamed. Data will be copied from the old table to the new table according to the column mapping provided by the user if an [alter table] command was executed. 159 | 160 | ## Gotchas 161 | - At this time, the only type of primary key possible is an auto-incrementing variable of some type of integer. 162 | - Support for nested objects has not yet been added. If your struct has a struct for a variable, use the keyword [ignore] after the variable/column for it to be ignored. 163 | - Fully qualified names for anything database related should not be used. StreetCRUD combines partial elements such as database name, schema, etc., for you. 164 | - StreetCRUD does not make sure that struct variables and columns are unique. Entering identical names in the file to be processed will cause an error. This issue will be addressed in the future. 165 | 166 | ### Licensed Under the MIT License (see included LICENSE.md file) -------------------------------------------------------------------------------- /file_gen.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "fmt" 7 | "os" 8 | "strconv" 9 | "strings" 10 | ) 11 | 12 | func readFileMakeSlice(filePath string) ([]string, error) { 13 | 14 | file, err := os.Open(filePath) 15 | if err != nil { 16 | return nil, err 17 | } 18 | defer file.Close() 19 | 20 | var fileLines []string 21 | fileScanner := bufio.NewScanner(file) 22 | for fileScanner.Scan() { 23 | 24 | fileLines = append(fileLines, fileScanner.Text()) 25 | } 26 | 27 | return fileLines, nil 28 | } 29 | 30 | func GetSafePathForSave(filePath string) string { 31 | _, err := os.Stat(filePath) 32 | if os.IsNotExist(err) { 33 | //A file doesn't exist at the given path 34 | return filePath 35 | } else { 36 | //Need to rename file (check if rename name is taken too) 37 | var newName string 38 | tryInt := 1 39 | for tryInt > 0 { 40 | //filepath.Ext 41 | newName = strings.Replace(filePath, ".go", "_gen_"+strconv.Itoa(tryInt)+".go", -1) 42 | if _, err := os.Stat(newName); os.IsNotExist(err) { 43 | //File doesn't exist here, good to go 44 | tryInt = 0 45 | } else { 46 | //File exists, try another name 47 | tryInt++ 48 | } 49 | } 50 | return newName 51 | } 52 | } 53 | 54 | func BuildStringForFileWrite(structFromFile *structToCreate, isNew bool, packageName string) string { 55 | 56 | var buffer bytes.Buffer 57 | var primColName string 58 | var primVarName string 59 | var primVarType string 60 | var delColName string 61 | var delOnColName string 62 | var delColType string 63 | var delOnColType string 64 | var delVarName string 65 | var delOnVarName string 66 | var tablePathName string = fmt.Sprintf("%s.%s.%s", AddQuotesIfAnyUpperCase(structFromFile.database), AddQuotesIfAnyUpperCase(structFromFile.schema), structFromFile.tableName) 67 | structObject := LowerCaseFirstChar(structFromFile.structName) 68 | 69 | //Write package and imports 70 | if isNew { 71 | //discover if the time package needs to be included 72 | time := "\n" 73 | for _, col := range structFromFile.cols { 74 | if (col.deletedOn && !col.nulls) || col.goType == "time.Time" { 75 | time = "\n\"time\"\n" 76 | } 77 | } 78 | buffer.WriteString("package ") 79 | buffer.WriteString(packageName) 80 | buffer.WriteString("\n\n") 81 | buffer.WriteString("import (\n") 82 | buffer.WriteString("\"database/sql\"\n//DB Driver\n_ \"github.com/lib/pq\"\n\"encoding/json\"\n\"log\"") 83 | buffer.WriteString(time) 84 | if structFromFile.nullsPkg { 85 | buffer.WriteString("\"github.com/markbates/going/nulls\"") 86 | } 87 | buffer.WriteString("\n)\n") 88 | } 89 | 90 | //Write global variable if generated code will be using prepared stmts 91 | var dataLayerVar string = LowerCaseFirstChar(structFromFile.structName) + "SQL" 92 | if structFromFile.prepared { 93 | buffer.WriteString("\n//Global Data Layer\n") 94 | buffer.WriteString(fmt.Sprintf("var %s %sDataLayer\n", dataLayerVar, structFromFile.structName)) 95 | } else { 96 | buffer.WriteString("\n//Global DB Pointer\n") 97 | buffer.WriteString(fmt.Sprintf("var %sDB *sql.DB\n", structFromFile.structName)) 98 | } 99 | 100 | //Get name of primary column and deleted column 101 | for _, col := range structFromFile.cols { 102 | if col.primary { 103 | primColName = col.colName 104 | primVarName = col.varName 105 | primVarType = col.goType 106 | } else if col.deleted { 107 | //ignore [nulls] if a column is marked as [deleted] 108 | if col.nulls { 109 | col.dbType = "boolean" 110 | col.goType = "bool" 111 | col.structLine = strings.Replace(col.structLine, "nulls.Bool", "bool", 1) 112 | col.nulls = false 113 | } 114 | delColName = col.colName 115 | delColType = col.goType 116 | delVarName = col.varName 117 | } else if col.deletedOn { 118 | delOnColName = col.colName 119 | delOnColType = "time.Time" 120 | if col.nulls { 121 | delOnColType = "nulls.Time" 122 | } 123 | delOnVarName = col.varName 124 | } 125 | } 126 | 127 | //Create query statements 128 | var indexMethods [][]string 129 | var patchMethods [][]string 130 | var updateSet []string 131 | var insertSet []string 132 | var insertVals []string 133 | var selectVals []string 134 | var objectVars []string 135 | var updateVars []string 136 | var insertVars []string 137 | var sqlVarFinal string 138 | i := 0 139 | for _, col := range structFromFile.cols { 140 | //build slices for insert and update statements 141 | if !col.primary { 142 | i += 1 143 | updateSet = append(updateSet, col.colName+" = $"+strconv.Itoa(i)) 144 | insertSet = append(insertSet, col.colName) 145 | insertVals = append(insertVals, "$"+strconv.Itoa(i)) 146 | insertVars = append(insertVars, structObject+"."+col.varName) 147 | updateVars = append(updateVars, structObject+"."+col.varName) 148 | } 149 | selectVals = append(selectVals, col.colName) 150 | objectVars = append(objectVars, "&"+structObject+"."+col.varName) 151 | } 152 | sqlVarFinal = "$" + strconv.Itoa(len(structFromFile.cols)) 153 | updateVars = append(updateVars, structObject+"."+primVarName) 154 | 155 | for _, col := range structFromFile.cols { 156 | if col.index { 157 | indexMethods = append(indexMethods, []string{fmt.Sprintf("Get%ssBy%s", structFromFile.structName, UpperCaseFirstChar(col.varName)), fmt.Sprintf("SELECT %s FROM %s WHERE %s = $1 ORDER BY %s", strings.Join(selectVals, ", "), tablePathName, col.colName, primColName), LowerCaseFirstChar(col.varName), col.goType, fmt.Sprintf("GetBy%s", UpperCaseFirstChar(col.varName))}) 158 | if delColName != "" { 159 | indexMethods[len(indexMethods)-1][1] = fmt.Sprintf("SELECT %s FROM %s WHERE %s = $1 and (%s = $2 or %s = $3) ORDER BY %s", strings.Join(selectVals, ", "), tablePathName, col.colName, delColName, delColName, primColName) 160 | } 161 | } 162 | if col.patch { 163 | patchMethods = append(patchMethods, []string{"Patch" + UpperCaseFirstChar(col.varName), fmt.Sprintf("UPDATE %s SET %s = $1 WHERE %s = $2", tablePathName, col.colName, primColName), LowerCaseFirstChar(col.varName), col.goType, fmt.Sprintf("Patch%s", UpperCaseFirstChar(col.varName)), col.varName}) 164 | } 165 | } 166 | 167 | selectStmt := fmt.Sprintf("SELECT %s FROM %s WHERE %s = $1", strings.Join(selectVals, ", "), tablePathName, primColName) 168 | if delColName != "" { 169 | selectStmt = fmt.Sprintf("%s and (%s = $2 or %s = $3)", selectStmt, delColName, delColName) 170 | } 171 | updateStmt := fmt.Sprintf("UPDATE %s SET %s WHERE %s = %s", tablePathName, strings.Join(updateSet, ", "), primColName, sqlVarFinal) 172 | insertStmt := fmt.Sprintf("INSERT INTO %s (%s) VALUES (%s) RETURNING %s", tablePathName, strings.Join(insertSet, ", "), strings.Join(insertVals, ", "), primColName) 173 | markDelStmt := fmt.Sprintf("UPDATE %s SET %s = $1, %s = $2 WHERE %s = $3", tablePathName, delColName, delOnColName, primColName) 174 | delStmt := fmt.Sprintf("DELETE from %s WHERE %s = $1", tablePathName, primColName) 175 | constStmt := fmt.Sprintf("\n//Constants used to alter Get queries (for rows marked as deleted)\nconst (\nEXISTS%s = iota\nDELETED%s = iota\nALL%s = iota\n)\n", strings.ToUpper(structFromFile.structName), strings.ToUpper(structFromFile.structName), strings.ToUpper(structFromFile.structName)) 176 | //End Create query statements 177 | 178 | //Write constants used to alter Get queries 179 | if delColName != "" { 180 | buffer.WriteString(constStmt) 181 | } 182 | 183 | //Write struct 184 | buffer.WriteString("\ntype ") 185 | buffer.WriteString(structFromFile.structName) 186 | buffer.WriteString(" struct {\n") 187 | for _, col := range structFromFile.cols { 188 | buffer.WriteString(col.structLine) 189 | buffer.WriteString("\n") 190 | } 191 | buffer.WriteString("}\n\n") 192 | 193 | //Write New() 194 | delFilter := "" 195 | if delColName != "" { 196 | delFilter = ", delFilter int" 197 | } 198 | buffer.WriteString(fmt.Sprintf("//Initialize and fill a %s object from the DB\nfunc New%s(%s %s%s) (*%s, error) {\n", structFromFile.structName, structFromFile.structName, LowerCaseFirstChar(primVarName), primVarType, delFilter, structFromFile.structName)) 199 | buffer.WriteString(fmt.Sprintf("%s := new(%s)\n", structObject, structFromFile.structName)) 200 | delFilter = "" 201 | if delColName != "" { 202 | delFilter = ", deleted1, deleted2" 203 | buffer.WriteString("deleted1 := false\ndeleted2 := false\nswitch delFilter {\ncase DELETED" + strings.ToUpper(structFromFile.structName) + ":\ndeleted1 = true\ndeleted2 = true\ncase ALL" + strings.ToUpper(structFromFile.structName) + ":\ndeleted2 = true\n}\n") 204 | } 205 | if structFromFile.prepared { 206 | buffer.WriteString(fmt.Sprintf("row := %s.GetByID.QueryRow(%s%s)\n", dataLayerVar, LowerCaseFirstChar(primVarName), delFilter)) 207 | } else { 208 | buffer.WriteString(fmt.Sprintf("row := %sDB.QueryRow(\"%s\", %s%s)\n", structFromFile.structName, selectStmt, LowerCaseFirstChar(primVarName), delFilter)) 209 | } 210 | buffer.WriteString(fmt.Sprintf("err := row.Scan(%s)\n", strings.Join(objectVars, ", "))) 211 | buffer.WriteString(fmt.Sprintf("if err != nil {\nlog.Println(err.Error())\nreturn nil, err\n}\nreturn %s, nil\n}\n\n", structObject)) 212 | 213 | //Write UserFromJSON() 214 | buffer.WriteString(fmt.Sprintf("//Transform JSON into a %s object\nfunc %sFromJSON(%sJSON []byte) (*%s, error) {\n", structFromFile.structName, structFromFile.structName, structObject, structFromFile.structName)) 215 | buffer.WriteString(fmt.Sprintf("%s := new(%s)\nerr := json.Unmarshal(%sJSON, %s)\n", structObject, structFromFile.structName, structObject, structObject)) 216 | buffer.WriteString(fmt.Sprintf("if err != nil{\nlog.Println(err.Error())\nreturn nil, err\n}\nreturn %s, nil\n}\n\n", structObject)) 217 | 218 | //Write ToJSON() 219 | buffer.WriteString(fmt.Sprintf("//Convert a %s object to JSON\nfunc(%s *%s) ToJSON() ([]byte, error) {\n", structFromFile.structName, structObject, structFromFile.structName)) 220 | buffer.WriteString(fmt.Sprintf("%sJSON, err := json.Marshal(%s)\nreturn %sJSON, err\n}\n\n", structObject, structObject, structObject)) 221 | 222 | //Write ObjectsToJSON() 223 | buffer.WriteString(fmt.Sprintf("//Convert multiple %s objects to JSON\nfunc %ssToJSON(%ss []*%s) ([]byte, error) {\n", structFromFile.structName, UpperCaseFirstChar(structObject), structObject, structFromFile.structName)) 224 | buffer.WriteString(fmt.Sprintf("%ssJSON, err := json.Marshal(%ss)\nreturn %ssJSON, err\n}\n\n", structObject, structObject, structObject)) 225 | 226 | //Write GetBy() 227 | delFilter = "" 228 | if delColName != "" { 229 | delFilter = ", delFilter int" 230 | } 231 | buffer.WriteString(fmt.Sprintf("//Fill %s object with data from DB\nfunc (%s *%s) GetByID(%s %s%s) error {\n", structFromFile.structName, structObject, structFromFile.structName, LowerCaseFirstChar(primVarName), primVarType, delFilter)) 232 | delFilter = "" 233 | if delColName != "" { 234 | delFilter = ", deleted1, deleted2" 235 | buffer.WriteString("deleted1 := false\ndeleted2 := false\nswitch delFilter {\ncase DELETED" + strings.ToUpper(structFromFile.structName) + ":\ndeleted1 = true\ndeleted2 = true\ncase ALL" + strings.ToUpper(structFromFile.structName) + ":\ndeleted2 = true\n}\n") 236 | } 237 | if structFromFile.prepared { 238 | buffer.WriteString(fmt.Sprintf("row := %s.GetByID.QueryRow(%s%s)\n", dataLayerVar, LowerCaseFirstChar(primVarName), delFilter)) 239 | } else { 240 | buffer.WriteString(fmt.Sprintf("row := %sDB.QueryRow(\"%s\", %s%s)\n", structFromFile.structName, selectStmt, LowerCaseFirstChar(primVarName), delFilter)) 241 | } 242 | buffer.WriteString(fmt.Sprintf("err := row.Scan(%s)\n", strings.Join(objectVars, ", "))) 243 | buffer.WriteString("if err != nil {\nlog.Println(err.Error())\nreturn err\n}\nreturn nil\n}\n\n") 244 | 245 | //Write Insert() 246 | buffer.WriteString(fmt.Sprintf("//Insert %s object to DB\nfunc (%s *%s) Insert() error {\n", structFromFile.structName, structObject, structFromFile.structName)) 247 | if structFromFile.prepared { 248 | buffer.WriteString(fmt.Sprintf("var id int\n row := %s.Insert.QueryRow(%s)\n", dataLayerVar, strings.Join(insertVars, ", "))) 249 | } else { 250 | buffer.WriteString(fmt.Sprintf("var id int\n row := %sDB.QueryRow(\"%s\", %s)\n", structFromFile.structName, insertStmt, strings.Join(insertVars, ", "))) 251 | } 252 | buffer.WriteString(fmt.Sprintf("err := row.Scan(&id)\nif err != nil {\nlog.Println(err.Error())\nreturn err\n}\n%s.%s = id\nreturn nil\n}\n\n", structObject, primVarName)) 253 | 254 | //Write Update() 255 | buffer.WriteString(fmt.Sprintf("//Update %s object in DB\nfunc (%s *%s) Update() error {\n", structFromFile.structName, structObject, structFromFile.structName)) 256 | if structFromFile.prepared { 257 | buffer.WriteString(fmt.Sprintf("_, err := %s.Update.Exec(%s)\n", dataLayerVar, strings.Join(updateVars, ", "))) 258 | } else { 259 | buffer.WriteString(fmt.Sprintf("_, err := %sDB.Exec(\"%s\", %s)\n", structFromFile.structName, updateStmt, strings.Join(updateVars, ", "))) 260 | } 261 | buffer.WriteString("if err != nil {\nlog.Println(err.Error())\nreturn err\n}\nreturn nil\n}\n\n") 262 | 263 | //Write MarkDeleted() if needed 264 | if delColName != "" { 265 | buffer.WriteString(fmt.Sprintf("//Mark a row as deleted at a specific time\nfunc (%s *%s) MarkDeleted(del ", structObject, structFromFile.structName)) 266 | buffer.WriteString(fmt.Sprintf("%s, when %s) error {\n", delColType, delOnColType)) 267 | if structFromFile.prepared { 268 | buffer.WriteString(fmt.Sprintf("_, err := %s.MarkDel.Exec(del, when, %s.%s)\n", dataLayerVar, structObject, primVarName)) 269 | } else { 270 | buffer.WriteString(fmt.Sprintf("_, err := %sDB.Exec(\"%s\", del, when, %s.%s)\n", structFromFile.structName, markDelStmt, structObject, primVarName)) 271 | } 272 | buffer.WriteString("if err != nil {\nlog.Println(err.Error())\nreturn err\n}\n") 273 | buffer.WriteString(fmt.Sprintf("%s.%s = del\n%s.%s = when\n", structObject, delVarName, structObject, delOnVarName)) 274 | buffer.WriteString("return nil\n}\n\n") 275 | } 276 | 277 | //Write Delete() 278 | buffer.WriteString(fmt.Sprintf("//Delete will remove the matching row from the DB")) 279 | buffer.WriteString(fmt.Sprintf("\nfunc (%s *%s) Delete() error {\n", structObject, structFromFile.structName)) 280 | if structFromFile.prepared { 281 | buffer.WriteString(fmt.Sprintf("_, err := %s.Delete.Exec(%s.%s)\n", dataLayerVar, structObject, primVarName)) 282 | } else { 283 | buffer.WriteString(fmt.Sprintf("_, err := %sDB.Exec(\"%s\", %s.%s)\n", structFromFile.structName, delStmt, structObject, primVarName)) 284 | } 285 | buffer.WriteString("if err != nil {\nlog.Println(err.Error())\nreturn err\n}\nreturn nil\n}\n\n") 286 | 287 | //Write GetObjectsByColumn 288 | for _, method := range indexMethods { 289 | buffer.WriteString(fmt.Sprintf("//Get %ss by %s\n", structFromFile.structName, method[2])) 290 | delFilter = "" 291 | if delColName != "" { 292 | delFilter = ", delFilter int" 293 | } 294 | buffer.WriteString(fmt.Sprintf("func %s(%s %s%s) ([]*%s, error) {\n", method[0], method[2], method[3], delFilter, structFromFile.structName)) 295 | delFilter = "" 296 | if delColName != "" { 297 | delFilter = ", deleted1, deleted2" 298 | buffer.WriteString("deleted1 := false\ndeleted2 := false\nswitch delFilter {\ncase DELETED" + strings.ToUpper(structFromFile.structName) + ":\ndeleted1 = true\ndeleted2 = true\ncase ALL" + strings.ToUpper(structFromFile.structName) + ":\ndeleted2 = true\n}\n") 299 | } 300 | if structFromFile.prepared { 301 | buffer.WriteString(fmt.Sprintf("rows, err := %s.%s.Query(%s%s)\n", dataLayerVar, method[4], method[2], delFilter)) 302 | } else { 303 | buffer.WriteString(fmt.Sprintf("rows, err := %sDB.Query(\"%s\", %s%s)\n", structFromFile.structName, method[1], method[2], delFilter)) 304 | } 305 | buffer.WriteString("if err != nil {\nrows.Close()\nlog.Println(err.Error())\nreturn nil, err\n}\n") 306 | buffer.WriteString(fmt.Sprintf("%ss := []*%s{}\nfor rows.Next() {\n%s := new(%s)\nif err = rows.Scan(%s); err != nil {\n", structObject, structFromFile.structName, structObject, structFromFile.structName, strings.Join(objectVars, ", "))) 307 | buffer.WriteString("log.Println(err.Error())\nrows.Close()\nreturn") 308 | buffer.WriteString(fmt.Sprintf(" %ss, err\n}\n%ss = append(%ss, %s)\n}\n\nrows.Close()\nreturn %ss, nil\n}\n\n", structObject, structObject, structObject, structObject, structObject)) 309 | } 310 | 311 | //Write PatchVar 312 | for _, method := range patchMethods { 313 | buffer.WriteString(fmt.Sprintf("//Update %s only\n", method[2])) 314 | buffer.WriteString(fmt.Sprintf("func (%s *%s) %s(%s %s) error {\n", structObject, structFromFile.structName, method[0], method[2], method[3])) 315 | if structFromFile.prepared { 316 | buffer.WriteString(fmt.Sprintf("_, err := %s.%s.Exec(%s, %s.%s)\n", dataLayerVar, method[4], method[2], structObject, primVarName)) 317 | } else { 318 | buffer.WriteString(fmt.Sprintf("_, err := %sDB.Exec(\"%s\", %s, %s.%s)\n", structFromFile.structName, method[1], method[2], structObject, primVarName)) 319 | } 320 | buffer.WriteString("if err != nil {\nlog.Println(err.Error())\nreturn err\n}\n") 321 | buffer.WriteString(fmt.Sprintf("%s.%s = %s\n", structObject, method[5], method[2])) 322 | buffer.WriteString("return nil\n}\n\n") 323 | } 324 | 325 | //Create DataLayer section if prepared statements are being used 326 | if structFromFile.prepared { 327 | buffer.WriteString(fmt.Sprintf("//DataLayer is used to store prepared SQL statements\ntype %sDataLayer struct {\n", structFromFile.structName)) 328 | buffer.WriteString("DB *sql.DB\nGetByID *sql.Stmt\nUpdate *sql.Stmt\nInsert *sql.Stmt\nDelete *sql.Stmt\n") 329 | if delColName != "" { 330 | buffer.WriteString("MarkDel *sql.Stmt\n") 331 | } 332 | for _, methodSlc := range indexMethods { 333 | buffer.WriteString(fmt.Sprintf("%s *sql.Stmt\n", methodSlc[4])) 334 | } 335 | for _, methodSlc := range patchMethods { 336 | buffer.WriteString(fmt.Sprintf("%s *sql.Stmt\n", methodSlc[4])) 337 | } 338 | buffer.WriteString("Init bool\n}\n") 339 | 340 | //Write InitDataLayer f() and prepared SQL statements 341 | buffer.WriteString(fmt.Sprintf("\n//Init%sDataLayer prepares SQL statements and assigns the passed in DB pointer\nfunc Init%sDataLayer(db *sql.DB) error {\nvar err error\nif !%s.Init {\n", structFromFile.structName, structFromFile.structName, dataLayerVar)) 342 | buffer.WriteString(fmt.Sprintf("%s.GetByID, err = db.Prepare(\"%s\")\n", dataLayerVar, selectStmt)) 343 | buffer.WriteString(fmt.Sprintf("%s.Update, err = db.Prepare(\"%s\")\n", dataLayerVar, updateStmt)) 344 | buffer.WriteString(fmt.Sprintf("%s.Insert, err = db.Prepare(\"%s\")\n", dataLayerVar, insertStmt)) 345 | if delColName != "" { 346 | buffer.WriteString(fmt.Sprintf("%s.MarkDel, err = db.Prepare(\"%s\")\n", dataLayerVar, markDelStmt)) 347 | } 348 | buffer.WriteString(fmt.Sprintf("%s.Delete, err = db.Prepare(\"%s\")\n", dataLayerVar, delStmt)) 349 | //Write patch and index methods if they exist 350 | for _, method := range indexMethods { 351 | buffer.WriteString(fmt.Sprintf("%s.%s, err = db.Prepare(\"%s\")\n", dataLayerVar, method[4], method[1])) 352 | } 353 | for _, method := range patchMethods { 354 | buffer.WriteString(fmt.Sprintf("%s.%s, err = db.Prepare(\"%s\")\n", dataLayerVar, method[4], method[1])) 355 | } 356 | buffer.WriteString(fmt.Sprintf("%s.Init = true\n%s.DB = db\n}\nreturn err\n}\n", dataLayerVar, dataLayerVar)) 357 | //Write CloseStmts f() 358 | buffer.WriteString(fmt.Sprintf("\n//Close%sStmts should be called when prepared SQL statements aren't needed anymore\nfunc Close%sStmts() {\n", structFromFile.structName, structFromFile.structName)) 359 | buffer.WriteString(fmt.Sprintf("if %s.Init {\n%s.GetByID.Close()\n%s.Update.Close()\n%s.Insert.Close()\n%s.Delete.Close()\n", dataLayerVar, dataLayerVar, dataLayerVar, dataLayerVar, dataLayerVar)) 360 | if delColName != "" { 361 | buffer.WriteString(fmt.Sprintf("%s.MarkDel.Close()\n", dataLayerVar)) 362 | } 363 | for _, method := range indexMethods { 364 | buffer.WriteString(fmt.Sprintf("%s.%s.Close()\n", dataLayerVar, method[4])) 365 | } 366 | for _, method := range patchMethods { 367 | buffer.WriteString(fmt.Sprintf("%s.%s.Close()\n", dataLayerVar, method[4])) 368 | } 369 | buffer.WriteString(fmt.Sprintf("%s.Init = false\n}\n}\n", dataLayerVar)) 370 | } 371 | 372 | return buffer.String() 373 | } 374 | -------------------------------------------------------------------------------- /street_crud.go: -------------------------------------------------------------------------------- 1 | package main //StreetCRUD by Daniel Isted (c) 2014 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | _ "github.com/lib/pq" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | "unicode/utf8" 11 | ) 12 | 13 | // The start of the main program 14 | func main() { 15 | const reqBaseVarsC = 9 16 | var server string 17 | var dbUser string 18 | var dbGroup string 19 | var password string 20 | var dbName string 21 | var schemaName string 22 | var useSSL bool 23 | var useUnderscore bool 24 | var packageName string 25 | var reqVarCount uint8 26 | var structsToAdd []*structToCreate 27 | var structFromFile *structToCreate 28 | 29 | var filePath string 30 | var isFileFound bool 31 | var processFail string = "\nThe file could not be processed. " 32 | 33 | //db, err := sql.Open("postgres", "") 34 | //if err == nil { 35 | // CheckForTables(db) 36 | //} 37 | fmt.Println("") 38 | fmt.Println("////////////////////////////////////////////////////") 39 | fmt.Println(" __ ___ __ ___ ___ ___ __ __ __ ") 40 | fmt.Println(" /__` | |__) |__ |__ | / ` |__) | | | \\ ") 41 | fmt.Println(" .__/ | | \\ |___ |___ | \\__, | \\ \\__/ |__/ ") 42 | fmt.Println("") 43 | fmt.Println(" __ ") 44 | fmt.Println(" |__) \\ / ") 45 | fmt.Println(" |__) | ") 46 | fmt.Println("") 47 | fmt.Println(" __ ___ __ ___ ___ __ ") 48 | fmt.Println(" | \\ /\\ |\\ | | |__ | | /__` | |__ | \\ ") 49 | fmt.Println(" |__/ /~~\\ | \\| | |___ |___ | .__/ | |___ |__/ ") 50 | fmt.Println("") 51 | fmt.Println("/////////////////////Ver. 1.0///////////////////////") 52 | fmt.Println("") 53 | fmt.Printf("Please see github.com/isted/StreetCRUD for instructions.\n") 54 | fmt.Printf("Press return at any time to quit.\n") 55 | //uiLoop: 56 | for { 57 | fmt.Printf("\nEnter file path for StreetCRUD struct file: ") 58 | _, err := fmt.Scanf("%s", &filePath) 59 | if err != nil || filePath == "" { 60 | fmt.Print("StreetCRUD Closed\n\n") 61 | return 62 | } 63 | isFileFound = true 64 | if _, err := os.Stat(filePath); os.IsNotExist(err) { 65 | fmt.Printf("The file %s does not exist.", filePath) 66 | isFileFound = false 67 | } 68 | //build go file and generate SQL 69 | if isFileFound { 70 | //read in file 71 | lineSlices, e := readFileMakeSlice(filePath) 72 | if lineSlices == nil || e != nil { 73 | fmt.Printf("The file is empty or missing key elements.\n") 74 | continue //uiLoop 75 | } 76 | //gather directory path for writing generated go files later 77 | absPath, _ := filepath.Abs(filePath) 78 | absPath, _ = filepath.Split(absPath) 79 | var inCollectState bool = false 80 | var inAddStructState bool = false 81 | var inCollectStructState bool = false 82 | var inCollectNamesState bool = false 83 | var inAlterStructState bool = false 84 | LineParsed: 85 | for _, sLine := range lineSlices { 86 | var bracks []rune 87 | //Loop through characters since some whitespace is needed for keywords, structure, etc. 88 | for letterIndex, cLetter := range sLine { 89 | if (cLetter == '[' || inCollectState) && (!inAddStructState) && (!inAlterStructState) { 90 | inCollectState = true 91 | bracks = append(bracks, cLetter) 92 | 93 | if cLetter == ']' { 94 | inCollectState = false 95 | 96 | switch strings.ToLower(string(bracks)) { 97 | 98 | case "[server]": 99 | 100 | if utf8.RuneCountInString(sLine) <= letterIndex+1 { 101 | fmt.Print(processFail + "No Server was specified.\n") 102 | return 103 | } 104 | server = strings.TrimSpace(string(sLine[letterIndex+1:])) 105 | //Check to see if there is only an empty string left after the whitespace was trimmed 106 | if server == "" { 107 | fmt.Print(processFail + "[Server] consists of whitespace.\n") 108 | return 109 | } 110 | reqVarCount = reqVarCount + 1 111 | continue LineParsed 112 | 113 | case "[user]": 114 | if utf8.RuneCountInString(sLine) <= letterIndex+1 { 115 | fmt.Print(processFail + "No User was specified.\n") 116 | return 117 | } 118 | dbUser = strings.TrimSpace(string(sLine[letterIndex+1:])) 119 | //Check to see if there is only an empty string left after the whitespace was trimmed 120 | if dbUser == "" { 121 | fmt.Print(processFail + "[User] consists of whitespace.\n") 122 | return 123 | } 124 | reqVarCount = reqVarCount + 1 125 | continue LineParsed 126 | 127 | case "[group]": 128 | if utf8.RuneCountInString(sLine) > letterIndex+1 { 129 | dbGroup = strings.TrimSpace(string(sLine[letterIndex+1:])) 130 | } 131 | reqVarCount = reqVarCount + 1 132 | continue LineParsed 133 | 134 | case "[password]": 135 | if utf8.RuneCountInString(sLine) <= letterIndex+1 { 136 | fmt.Print(processFail + "No [Password] was specified.\n") 137 | return 138 | } 139 | password = strings.TrimSpace(string(sLine[letterIndex+1:])) 140 | if password == "" { 141 | fmt.Print(processFail + "[Password] consists of whitespace.\n") 142 | return 143 | } 144 | reqVarCount = reqVarCount + 1 145 | continue LineParsed 146 | case "[database]": 147 | if utf8.RuneCountInString(sLine) <= letterIndex+1 { 148 | fmt.Print(processFail + "No Database was specified.\n") 149 | return 150 | } 151 | dbName = strings.TrimSpace(string(sLine[letterIndex+1:])) 152 | if dbName == "" { 153 | fmt.Print(processFail + "[Database] consists of whitespace.\n") 154 | return 155 | } 156 | reqVarCount = reqVarCount + 1 157 | continue LineParsed 158 | case "[schema]": 159 | if utf8.RuneCountInString(sLine) > letterIndex+1 { 160 | schemaName = strings.TrimSpace(string(sLine[letterIndex+1:])) 161 | } 162 | if schemaName == "" { 163 | schemaName = "public" 164 | } 165 | reqVarCount = reqVarCount + 1 166 | continue LineParsed 167 | case "[ssl]": 168 | if utf8.RuneCountInString(sLine) <= letterIndex+1 { 169 | fmt.Print(processFail + "No SSL option was specified.\n") 170 | return 171 | } 172 | if strings.TrimSpace(string(sLine[letterIndex+1:])) == "true" { 173 | useSSL = true 174 | } else { 175 | useSSL = false 176 | } 177 | reqVarCount = reqVarCount + 1 178 | continue LineParsed 179 | case "[underscore]": 180 | if utf8.RuneCountInString(sLine) <= letterIndex+1 { 181 | fmt.Print(processFail + "No Underscore option was specified.\n") 182 | return 183 | } 184 | if strings.TrimSpace(string(sLine[letterIndex+1:])) == "true" { 185 | useUnderscore = true 186 | } else { 187 | useUnderscore = false 188 | } 189 | reqVarCount = reqVarCount + 1 190 | continue LineParsed 191 | case "[package]": 192 | if utf8.RuneCountInString(sLine) <= letterIndex+1 { 193 | fmt.Print(processFail + "No Package was specified.\n") 194 | return 195 | } 196 | packageName = strings.TrimSpace(string(sLine[letterIndex+1:])) 197 | if packageName == "" { 198 | fmt.Print(processFail + "[Package] consists of whitespace.\n") 199 | return 200 | } 201 | reqVarCount = reqVarCount + 1 202 | continue LineParsed 203 | case "[add struct]": 204 | //enter addStruct state 205 | inAddStructState = true 206 | inCollectState = false 207 | structFromFile = new(structToCreate) 208 | structFromFile.actionType = "Add" 209 | structFromFile.prepared = true 210 | continue LineParsed 211 | case "[alter table]": 212 | inAlterStructState = true 213 | inCollectState = false 214 | //collect name of struct to alter 215 | if utf8.RuneCountInString(sLine) <= letterIndex+1 { 216 | fmt.Print(processFail + "The table name from which to copy data must exist after the [alter table] statement.\n") 217 | return 218 | } 219 | tblToAlter := strings.TrimSpace(string(sLine[letterIndex+1:])) 220 | if tblToAlter == "" { 221 | fmt.Print(processFail + "The table name from which to copy data must exist after the [alter table] statement.\n") 222 | return 223 | } 224 | structFromFile = new(structToCreate) 225 | structFromFile.actionType = tblToAlter 226 | structFromFile.prepared = true 227 | continue LineParsed 228 | } //switch 229 | } 230 | 231 | } else if inAddStructState { 232 | //checks to make sure all of the base variables were collected 233 | if reqVarCount < reqBaseVarsC { 234 | fmt.Print(processFail + "At least one of the following was not specified: [Server], [User], [Password], [Database], [Schema], [SSL], [Underscore], and/or [Package].\n") 235 | return 236 | } 237 | 238 | if cLetter == '[' || inCollectNamesState { 239 | inCollectNamesState = true 240 | bracks = append(bracks, cLetter) 241 | if cLetter == ']' { 242 | inCollectNamesState = false 243 | switch strings.ToLower(string(bracks)) { 244 | case "[table]": 245 | if utf8.RuneCountInString(sLine) <= letterIndex+1 { 246 | //No data, use default table naming once struct name is known 247 | structFromFile.tableName = "" 248 | } else { 249 | userTbl := strings.TrimSpace(string(sLine[letterIndex+1:])) 250 | if userTbl != "" { 251 | if errNaming := CheckColAndTblNames(userTbl); errNaming != nil { 252 | fmt.Println(processFail + "[Table] issue: " + errNaming.Error()) 253 | return 254 | } 255 | } 256 | structFromFile.tableName = userTbl 257 | } 258 | continue LineParsed 259 | case "[file name]": 260 | if utf8.RuneCountInString(sLine) <= letterIndex+1 { 261 | //No data, use default file naming 262 | structFromFile.fileName = "" 263 | } else { 264 | var fileName = strings.TrimSpace(string(sLine[letterIndex+1:])) 265 | if fileName != "" { 266 | if strings.Contains(strings.ToLower(fileName), ".go") { 267 | fileName = ChangeCaseForRange(fileName, utf8.RuneCountInString(fileName)-2, utf8.RuneCountInString(fileName)-1) 268 | structFromFile.fileName = absPath + fileName 269 | structFromFile.filePath = absPath + fileName 270 | } else { 271 | structFromFile.fileName = absPath + fileName + ".go" 272 | } 273 | } else { 274 | structFromFile.fileName = "" 275 | } 276 | } 277 | continue LineParsed 278 | case "[prepared]": 279 | if utf8.RuneCountInString(sLine) <= letterIndex+1 { 280 | //No data, use prepared statments 281 | structFromFile.prepared = true 282 | } else { 283 | usePrepared := strings.TrimSpace(string(sLine[letterIndex+1:])) 284 | if strings.ToLower(usePrepared) == "false" || strings.ToLower(usePrepared) == "f" { 285 | structFromFile.prepared = false 286 | } 287 | } 288 | continue LineParsed 289 | } //switch 290 | } 291 | } else if cLetter == 't' || cLetter == 'T' { 292 | //Read in stuct name from file 293 | lineStructDef := strings.Split(sLine, " ") 294 | if len(lineStructDef) > 1 { 295 | structFromFile.structName = UpperCaseFirstChar(lineStructDef[1]) 296 | } else { 297 | fmt.Print(processFail + "No struct name was given.\n") 298 | return 299 | } 300 | //Finish naming Table 301 | var err error 302 | var tblName string 303 | if structFromFile.tableName == "" { 304 | if errNaming := CheckColAndTblNames(structFromFile.structName); errNaming != nil { 305 | fmt.Println(processFail + errNaming.Error() + "\n") 306 | return 307 | } 308 | if useUnderscore { 309 | tblName, err = ConvertToUnderscore(structFromFile.structName) 310 | structFromFile.tableName = tblName 311 | } else { 312 | structFromFile.tableName = strings.ToLower(structFromFile.structName) 313 | } 314 | 315 | } else { 316 | if useUnderscore { 317 | structFromFile.tableName, err = ConvertToUnderscore(structFromFile.tableName) 318 | } else { 319 | structFromFile.tableName = strings.ToLower(structFromFile.tableName) 320 | } 321 | } 322 | if err != nil { 323 | fmt.Println(processFail + err.Error() + "\n") 324 | return 325 | } 326 | 327 | //Finish naming File if needed 328 | if structFromFile.fileName == "" { 329 | structFromFile.fileName = absPath + strings.ToLower(structFromFile.structName) + ".go" 330 | } 331 | inCollectStructState = true 332 | continue LineParsed 333 | 334 | } else if inCollectStructState { 335 | //end of struct logic 336 | if cLetter == '}' { 337 | if !structFromFile.CheckStructForDeletes() { 338 | fmt.Println(processFail + "If a column has a [deleted] option, then another column must be marked as [deletedOn] and vice versa.") 339 | return 340 | } 341 | //columns are finished being read, end Add states 342 | inAddStructState = false 343 | inCollectStructState = false 344 | if !structFromFile.hasKey { 345 | fmt.Println(processFail + "At least one column of type integer must be marked with the keyword [Primary].") 346 | return 347 | } 348 | structFromFile.database = dbName 349 | structFromFile.schema = schemaName 350 | structsToAdd = append(structsToAdd, structFromFile) 351 | continue LineParsed 352 | } 353 | //Collect column, type, json, bracks 354 | if strings.TrimSpace(sLine) != "" { 355 | if lineColumn := strings.Split(TrimInnerSpacesToOne(sLine), " "); len(lineColumn) > 1 { 356 | var err error 357 | err = CheckColAndTblNames(lineColumn[0]) 358 | if err != nil { 359 | fmt.Println(processFail + err.Error() + "\n") 360 | return 361 | } 362 | col := new(column) 363 | col.varName = lineColumn[0] 364 | if useUnderscore { 365 | col.colName, err = ConvertToUnderscore(lineColumn[0]) 366 | if err != nil { 367 | fmt.Println(processFail + err.Error() + "\n") 368 | return 369 | } 370 | } else { 371 | col.colName = strings.ToLower(lineColumn[0]) 372 | } 373 | 374 | col.goType = lineColumn[1] 375 | //Handle meta data contained w/in ` ` 376 | strucOptsColumn := strings.Split(sLine, "`") 377 | if len(strucOptsColumn) > 1 { 378 | col.structLine = lineColumn[0] + " " + col.goType + " `" + strucOptsColumn[1] + "`" 379 | } else { 380 | col.structLine = lineColumn[0] + " " + col.goType 381 | } 382 | 383 | //Handle column options 384 | scOptsColumn := strings.Split(sLine, "[") 385 | var userOptions string 386 | var wasTypeAssigned bool 387 | for i := 1; i < len(scOptsColumn); i++ { 388 | userOptions = strings.TrimSpace(strings.ToLower(scOptsColumn[i])) 389 | wasTypeAssigned = false 390 | switch { 391 | case userOptions == "primary]": 392 | if !structFromFile.hasKey { 393 | switch strings.ToLower(col.goType) { 394 | case "int", "int8", "int16", "int32", "uint", "uint8", "uint16", "uint32", "uintptr": 395 | col.dbType = "integer" 396 | case "int64", "uint64": 397 | col.dbType = "bigint" 398 | default: 399 | fmt.Println(processFail + "Not a known primary key type. StreetCRUD only supports auto incrementing integers at this point.") 400 | return 401 | } 402 | col.primary = true 403 | wasTypeAssigned = true 404 | structFromFile.hasKey = true 405 | //Find and store the primary key column from the old table 406 | for i, newCol := range structFromFile.newAltCols { 407 | if newCol == col.colName { 408 | structFromFile.oldColPrim = structFromFile.oldAltCols[i] 409 | } 410 | } 411 | } else { 412 | fmt.Println(processFail + "The [primary] keyword can only be used on one column per struct definition.") 413 | return 414 | } 415 | case strings.Contains(userOptions, "size:"): 416 | if col.goType != "string" { 417 | fmt.Println(processFail + "[size] can only be used with type string.") 418 | return 419 | } 420 | col.size = userOptions[5:strings.IndexRune(userOptions, ']')] 421 | case userOptions == "index]": 422 | col.index = true 423 | case userOptions == "patch]": 424 | col.patch = true 425 | case userOptions == "deleted]": 426 | if strings.ToLower(col.goType) != "bool" { 427 | fmt.Println(processFail + "A column marked as [deleted] must have the type bool.") 428 | return 429 | } 430 | col.dbType = "boolean" 431 | col.deleted = true 432 | wasTypeAssigned = true 433 | case userOptions == "deletedon]": 434 | if strings.ToLower(col.goType) == "time.time" { 435 | col.dbType = "timestamp without time zone" 436 | } else { 437 | fmt.Println(processFail + "A column marked as [deletedOn] must have the type time.Time.") 438 | return 439 | } 440 | col.deletedOn = true 441 | wasTypeAssigned = true 442 | case userOptions == "ignore]": 443 | //ignore this line of the input struct 444 | col = nil 445 | continue LineParsed 446 | case userOptions == "nulls]": 447 | col.nulls = true 448 | structFromFile.nullsPkg = true 449 | } 450 | 451 | } //for i < len(scOptsColumn) 452 | 453 | if !wasTypeAssigned { 454 | //map goType to dbType if a dbType wasn't assigned above 455 | if check, msg := col.MapGoTypeToDBTypes(); !check { 456 | fmt.Println(processFail + msg) 457 | return 458 | } 459 | } 460 | 461 | if col.nulls { 462 | if err := col.MapNullTypes(); err != nil { 463 | fmt.Println(processFail + err.Error() + "\n") 464 | return 465 | } 466 | //Handle meta data contained w/in ` ` 467 | strucOptsColumn := strings.Split(sLine, "`") 468 | if len(strucOptsColumn) > 1 { 469 | col.structLine = lineColumn[0] + " " + col.goType + " `" + strucOptsColumn[1] + "`" 470 | } else { 471 | col.structLine = lineColumn[0] + " " + col.goType 472 | } 473 | } 474 | 475 | //add the built struct to the slice of structs to use later for code gen 476 | structFromFile.cols = append(structFromFile.cols, col) 477 | 478 | } else { 479 | fmt.Println(processFail + "Struct variable data was missing.") 480 | return 481 | } 482 | } 483 | continue LineParsed 484 | 485 | } 486 | 487 | } else if inAlterStructState { 488 | lineMap := strings.Split(TrimInnerSpacesToOne(sLine), "[") 489 | if len(lineMap) > 1 { 490 | //collect column mapping data 491 | if strings.ToLower(lineMap[1]) == "add struct]" { 492 | inAlterStructState = false 493 | inAddStructState = true 494 | } else { 495 | errorCheck := strings.ToLower(TrimInnerSpacesToOne(sLine)) 496 | if strings.Contains(strings.ToLower(errorCheck), "[to]") { 497 | if strings.Index(errorCheck, "[") == 0 || utf8.RuneCountInString(errorCheck) <= strings.LastIndex(errorCheck, "]")+1 { 498 | fmt.Println(processFail + "The old column name and/or the new struct name were not included in one of the [alter table] [to] sections.") 499 | return 500 | } 501 | } else { 502 | if errorCheck != "[copy cols]" { 503 | fmt.Println(processFail + "[to] was missing from OldColumnName [to] NewStructVar.") 504 | return 505 | } 506 | continue LineParsed 507 | } 508 | //The line appears to be formatted properly 509 | structFromFile.oldAltCols = append(structFromFile.oldAltCols, strings.TrimSpace(lineMap[0])) 510 | if useUnderscore { 511 | under, err := ConvertToUnderscore(strings.TrimSpace(lineMap[1][strings.Index(lineMap[1], "]")+1:])) 512 | if err != nil { 513 | fmt.Println(processFail + err.Error() + "\n") 514 | return 515 | } 516 | structFromFile.newAltCols = append(structFromFile.newAltCols, under) 517 | } else { 518 | structFromFile.newAltCols = append(structFromFile.newAltCols, strings.ToLower(strings.TrimSpace(lineMap[1][strings.Index(lineMap[1], "]")+1:]))) 519 | } 520 | } 521 | } else { 522 | fmt.Println(processFail + "Problem mapping columns to structs in [alter table] section.") 523 | return 524 | } 525 | continue LineParsed 526 | } 527 | 528 | } //for range sLine 529 | } //for lineSlices 530 | 531 | //Assign user to group if group wasn't defined in the file 532 | if dbGroup == "" { 533 | dbGroup = dbUser 534 | } 535 | 536 | //Cycle through structsToAdd 537 | fileOpen := make(map[string]*os.File) 538 | pathChanged := make(map[string]string) 539 | connString := BuildConnString(dbUser, password, dbName, server, useSSL) 540 | dbConnected := false 541 | var db *sql.DB 542 | for _, structObj := range structsToAdd { 543 | if pathChanged[structObj.fileName] == "" { 544 | //New path, check to make sure it doesn't already exist 545 | pathChanged[structObj.fileName] = GetSafePathForSave(structObj.fileName) 546 | } 547 | if fileOpen[pathChanged[structObj.fileName]] == nil { 548 | //file is new so don't append 549 | fileOpen[pathChanged[structObj.fileName]], err = os.Create(pathChanged[structObj.fileName]) 550 | if err != nil { 551 | fmt.Println("\nThere was a problem generating a new go file. " + err.Error() + "\n") 552 | return 553 | } 554 | 555 | //BuildStringForFileWrite(structObj, true, packageName) 556 | fileOpen[pathChanged[structObj.fileName]].WriteString(BuildStringForFileWrite(structObj, true, packageName)) 557 | } else { 558 | //file exists so append 559 | fileOpen[pathChanged[structObj.fileName]].WriteString(BuildStringForFileWrite(structObj, false, packageName)) 560 | 561 | } 562 | fileOpen[pathChanged[structObj.fileName]].Sync() 563 | 564 | //Check to see if user wants to generate or alter tables 565 | var yesOrNo string 566 | fmt.Printf("\nFile %s generated.", structObj.fileName) 567 | fmt.Printf("\n\nDo you want to create/alter the table %s (y or n): ", structObj.tableName) 568 | _, err := fmt.Scanf("%s", &yesOrNo) 569 | if err != nil { 570 | fmt.Println("An error occurred, exiting Street CRUD.\n") 571 | return 572 | } 573 | if strings.ToLower(yesOrNo) == "y" || strings.ToLower(yesOrNo) == "yes" { 574 | if dbConnected == false { 575 | dbConnected = true 576 | var err error 577 | db, err = sql.Open("postgres", connString) 578 | if err != nil { 579 | fmt.Printf("\nThere was a problem opening the database: %s", err.Error()+"\n") 580 | return 581 | } 582 | if err := db.Ping(); err != nil { 583 | fmt.Printf("\nDB connection issue: %s", err.Error()+"\n") 584 | return 585 | } 586 | } 587 | CreateOrAlterTables(structObj, db, dbGroup) 588 | } 589 | 590 | } //end range structsToAdd 591 | if dbConnected { 592 | db.Close() 593 | } 594 | //Close files manually since the defer.Close() doesn't get called until the program exits 595 | for _, value := range fileOpen { 596 | value.Close() 597 | } 598 | 599 | } 600 | 601 | //reinitialize variables after a file is processed 602 | dbUser = "" 603 | password = "" 604 | dbName = "" 605 | useSSL = false 606 | useUnderscore = false 607 | packageName = "" 608 | reqVarCount = 0 609 | structsToAdd = nil 610 | structFromFile = nil 611 | filePath = "" 612 | isFileFound = false 613 | } 614 | } 615 | --------------------------------------------------------------------------------