├── .dockerignore ├── glide.yaml ├── _templates └── model.go.tmpl ├── docker-compose.yml ├── testdata ├── models │ ├── project.go │ ├── post_comment.go │ ├── user.go │ └── preference.go └── testdata.sql ├── script ├── test.sh └── ping_db.sh ├── Dockerfile.test ├── glide.lock ├── .travis.yml ├── generate_test.go ├── Makefile ├── LICENSE ├── .gitignore ├── main.go ├── README.md ├── postgres.go └── generate.go /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | README.md 3 | LICENSE 4 | vendor 5 | out 6 | testdata/db.dump 7 | bindata.go 8 | -------------------------------------------------------------------------------- /glide.yaml: -------------------------------------------------------------------------------- 1 | package: github.com/wantedly/pq2gorm 2 | import: 3 | - package: github.com/gedex/inflector 4 | - package: github.com/lib/pq 5 | - package: github.com/serenize/snaker 6 | -------------------------------------------------------------------------------- /_templates/model.go.tmpl: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | {{ if .NeedTimePackage }} import "time" 4 | {{ end -}} 5 | 6 | type {{ .Name }} struct { 7 | {{ range .Fields -}} 8 | {{ .Name }} {{ .Type }} `{{ .Tag }}` {{ if (ne .Comment "") }}// {{ .Comment }}{{ end }} 9 | {{ end }} 10 | } 11 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | db: 4 | image: postgres:9.6 5 | environment: 6 | - POSTGRES_USER=postgres 7 | - POSTGRES_PASSWORD=password 8 | - POSTGRES_DB=test 9 | volumes: 10 | - $PWD/testdata:/testdata 11 | pq2gorm: 12 | build: 13 | context: . 14 | dockerfile: Dockerfile.test 15 | -------------------------------------------------------------------------------- /testdata/models/project.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type Project struct { 4 | ID uint `json:"id"` 5 | CompanyID uint `json:"company_id"` 6 | Title string `json:"title"` 7 | Description string `json:"description"` 8 | Location string `json:"location"` 9 | Latitude float32 `json:"latitude"` 10 | Longitude float32 `json:"longitude"` 11 | } 12 | -------------------------------------------------------------------------------- /script/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | bin/pq2gorm 'postgres://postgres:password@db:5432/test?sslmode=disable' -d out 4 | 5 | for f in `ls testdata/models`; do 6 | diff -u out/$f testdata/models/$f 7 | 8 | if [[ $? -gt 0 ]]; then 9 | echo "" 10 | echo "FAILED: $f does not match." 11 | echo "" 12 | exit 1 13 | fi 14 | done 15 | 16 | echo "" 17 | echo "SUCCESS!" 18 | echo "" 19 | -------------------------------------------------------------------------------- /Dockerfile.test: -------------------------------------------------------------------------------- 1 | FROM golang:1.7.0-alpine 2 | 3 | ENV GOPATH /go 4 | WORKDIR /go/src/github.com/wantedly/pq2gorm 5 | 6 | RUN apk add --no-cache --update bash curl git make 7 | 8 | COPY Makefile /go/src/github.com/wantedly/pq2gorm/ 9 | COPY glide.yaml /go/src/github.com/wantedly/pq2gorm/ 10 | COPY glide.lock /go/src/github.com/wantedly/pq2gorm/ 11 | 12 | RUN make deps 13 | 14 | COPY . /go/src/github.com/wantedly/pq2gorm 15 | 16 | RUN make 17 | -------------------------------------------------------------------------------- /testdata/models/post_comment.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "time" 4 | 5 | type PostComment struct { 6 | ID uint `json:"id"` 7 | UserID uint `json:"user_id"` 8 | User *User `json:"user"` // This line is infered from column name "user_id". 9 | PostID uint `json:"post_id"` 10 | Content string `json:"content"` 11 | CreatedAt *time.Time `json:"created_at"` 12 | UpdatedAt *time.Time `json:"updated_at"` 13 | } 14 | -------------------------------------------------------------------------------- /glide.lock: -------------------------------------------------------------------------------- 1 | hash: a56e962010d68ac952dd59abffeb2b8d50f79a4d464d4a5f84c4d386cbe5974c 2 | updated: 2016-09-07T15:26:25.189713017+09:00 3 | imports: 4 | - name: github.com/gedex/inflector 5 | version: 91797f1712fd58f224781be1080b8613d096187e 6 | - name: github.com/lib/pq 7 | version: 50761b0867bd1d9d069276790bcd4a3bccf2324a 8 | subpackages: 9 | - oid 10 | - name: github.com/serenize/snaker 11 | version: 8824b61eca66d308fcb2d515287d3d7a28dba8d6 12 | testImports: [] 13 | -------------------------------------------------------------------------------- /testdata/models/user.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "time" 4 | 5 | type User struct { 6 | ID uint `json:"id"` 7 | CreatedAt *time.Time `json:"created_at"` 8 | UpdatedAt *time.Time `json:"updated_at"` 9 | DeletedAt *time.Time `json:"deleted_at"` 10 | Preferences []*Preference `json:"preferences"` // This line is infered from other tables. 11 | PostComments []*PostComment `json:"post_comments"` // This line is infered from other tables. 12 | 13 | } 14 | -------------------------------------------------------------------------------- /script/ping_db.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | docker-compose exec db psql -U postgres -d test -c 'select 1;' 2>&1 > /dev/null 4 | 5 | if [[ $? -eq 0 ]]; then 6 | echo "Connection established." 7 | exit 0 8 | fi 9 | 10 | for i in `seq 1 5`; do 11 | echo "Wait for 5 seconds..." 12 | sleep 5 13 | 14 | docker-compose exec db psql -U postgres -d test -c 'select 1;' 2>&1 > /dev/null 15 | 16 | if [[ $? -eq 0 ]]; then 17 | echo "Connection established." 18 | exit 0 19 | fi 20 | done 21 | 22 | echo "Failed to connect to database." 23 | exit 1 24 | -------------------------------------------------------------------------------- /testdata/models/preference.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "time" 4 | 5 | type Preference struct { 6 | ID uint `json:"id"` 7 | UserID uint `json:"user_id"` 8 | User *User `json:"user"` // This line is infered from column name "user_id". 9 | CreatedAt *time.Time `json:"created_at"` 10 | UpdatedAt *time.Time `json:"updated_at"` 11 | Locale string `json:"locale" sql:"DEFAULT:'ja'::character varying"` 12 | DeletedAt *time.Time `json:"deleted_at"` 13 | Birthday *time.Time `json:"birthday"` 14 | EmailSubscriptions string `json:"email_subscriptions"` 15 | Searchable bool `json:"searchable" sql:"DEFAULT:true"` 16 | } 17 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | services: 3 | - docker 4 | language: go 5 | go: 6 | - '1.6' 7 | - '1.7' 8 | install: 9 | - make deps 10 | script: 11 | - make test 12 | - make generate-test 13 | notifications: 14 | slack: 15 | secure: I6P4Vq4BFZ8pE7zb7ZxITR9g5stu1dfZxdZe5tb47FWYYTLBQoCSIjolQEQ1GqRickwhLsJFZlsk3TK8kki0Ult/95i21LBCI1TOXCy7PtV/SwQVYvHM8WezIeqOKnYek1YTBX/ksHUtsMCT2f+5AjJySNNkp7V3xCszvYtiq6iCQcCal8dw8S2I3Z27y6HqLzoNzkudCHIEP8f6zMnYrVK+kbl3DdPItRGNprrjbIIdr8yAkiOVGN4YDj4VghOztv+9JZ+h++PZMmnnz5tZLrsJSUGdF9B0F5w9wanoPe/bMCCiyGLmV2P5Zpr7eqxVNLimQIZRe5BMYvEDxweb9IFT0BfhhpNtRIXBzpJ1SnvasyTCETZhkstsD8gJO8y69TTJirZsJXAUbLt7xfihuHueRrHyc57ZEwem/VwsYtvvOfruhuTlobd8oNVXWJUQkEzG633PxeHQCDIy0nzofLc+U47ZpMtifiLC3dRtEjuagJuMpM8UM1k5S+B0lZSNlIT5hX20hnSfEdpS5Iq3LOOoIoQpLg0WwznBM5sQlKIvydkbD4bwJgPmwJVtJslSpVVfVJkLDPwk6652xjSO2scq4tYPVdht7pYfWgnr/rkocQFvXpIYB36i4vP/767aFtr0qo3rFa8nVRNmG33cLZR1AOm9Ip0PCrza4y7D50I= 16 | -------------------------------------------------------------------------------- /generate_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestGormTableName(t *testing.T) { 8 | var testdata = []struct { 9 | in string 10 | out string 11 | }{ 12 | {"events", "Event"}, 13 | {"post_comments", "PostComment"}, 14 | } 15 | 16 | for _, td := range testdata { 17 | s := gormTableName(td.in) 18 | 19 | if s != td.out { 20 | t.Fatalf("Table name does not match. expect: %s, actual: %s", td.out, s) 21 | } 22 | } 23 | } 24 | 25 | func TestGormColumnName(t *testing.T) { 26 | var testdata = []struct { 27 | in string 28 | out string 29 | }{ 30 | {"description", "Description"}, 31 | {"user_id", "UserID"}, 32 | {"facebook_uid", "FacebookUID"}, 33 | {"candidacy", "Candidacy"}, 34 | {"video_id", "VideoID"}, 35 | {"image_url", "ImageURL"}, 36 | {"curl_name", "CurlName"}, 37 | } 38 | 39 | for _, td := range testdata { 40 | s := gormColumnName(td.in) 41 | 42 | if s != td.out { 43 | t.Fatalf("Field name does not match. expect: %s, actual: %s", td.out, s) 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | NAME := pq2gorm 2 | LDFLAGS := -ldflags="-s -w" 3 | 4 | .DEFAULT_GOAL := bin/$(NAME) 5 | 6 | bin/$(NAME): deps 7 | go generate 8 | go build $(LDFLAGS) -o bin/$(NAME) 9 | 10 | .PHONY: clean 11 | clean: 12 | rm -rf bin/* 13 | rm -rf vendor/* 14 | 15 | .PHONY: deps 16 | deps: glide 17 | go get github.com/jteeuwen/go-bindata/... 18 | glide install 19 | 20 | .PHONY: generate-test 21 | generate-test: 22 | @docker-compose stop > /dev/null 23 | @docker-compose rm -f > /dev/null 24 | docker-compose up -d db 25 | script/ping_db.sh 26 | docker-compose exec db psql -U postgres -d test -f /testdata/testdata.sql 27 | docker-compose build pq2gorm 28 | docker-compose run --rm pq2gorm script/test.sh 29 | @docker-compose stop > /dev/null 30 | @docker-compose rm -f > /dev/null 31 | 32 | .PHONY: glide 33 | glide: 34 | ifeq ($(shell command -v glide 2> /dev/null),) 35 | curl https://glide.sh/get | sh 36 | endif 37 | 38 | .PHONY: install 39 | install: 40 | go generate 41 | go install $(LDFLAGS) 42 | 43 | .PHONY: test 44 | test: 45 | go generate 46 | go test -v 47 | 48 | .PHONY: update-deps 49 | update-deps: glide 50 | glide update 51 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Wantedly, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/go 3 | 4 | ### Go ### 5 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 6 | *.o 7 | *.a 8 | *.so 9 | 10 | # Folders 11 | _obj 12 | _test 13 | 14 | # Architecture specific extensions/prefixes 15 | *.[568vq] 16 | [568vq].out 17 | 18 | *.cgo1.go 19 | *.cgo2.c 20 | _cgo_defun.c 21 | _cgo_gotypes.go 22 | _cgo_export.* 23 | 24 | _testmain.go 25 | 26 | *.exe 27 | *.test 28 | !Dockerfile.test 29 | *.prof 30 | 31 | # Output of the go coverage tool, specifically when used with LiteIDE 32 | *.out 33 | 34 | # Created by https://www.gitignore.io/api/macos 35 | 36 | ### macOS ### 37 | *.DS_Store 38 | .AppleDouble 39 | .LSOverride 40 | 41 | # Icon must end with two \r 42 | Icon 43 | 44 | # Thumbnails 45 | ._* 46 | 47 | # Files that might appear in the root of a volume 48 | .DocumentRevisions-V100 49 | .fseventsd 50 | .Spotlight-V100 51 | .TemporaryItems 52 | .Trashes 53 | .VolumeIcon.icns 54 | .com.apple.timemachine.donotpresent 55 | 56 | # Directories potentially created on remote AFP share 57 | .AppleDB 58 | .AppleDesktop 59 | Network Trash Folder 60 | Temporary Items 61 | .apdisk 62 | 63 | /vendor 64 | /bin 65 | /pq2gorm 66 | 67 | /out 68 | 69 | /bindata.go 70 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | "strings" 8 | ) 9 | 10 | //go:generate go-bindata _templates/ 11 | 12 | func main() { 13 | var ( 14 | dir string 15 | ts string 16 | ) 17 | 18 | f := flag.NewFlagSet(os.Args[0], flag.ExitOnError) 19 | 20 | f.Usage = func() { 21 | fmt.Fprintf(os.Stderr, `Usage of %s: 22 | %s [] 23 | 24 | Options: 25 | `, os.Args[0], os.Args[0]) 26 | f.PrintDefaults() // Print usage of options 27 | } 28 | f.StringVar(&dir, "dir", "./", "Set output path") 29 | f.StringVar(&dir, "d", "./", "Set output path") 30 | f.StringVar(&ts, "tables", "", "Target tables (table1,table2,...) (default: all tables)") 31 | f.StringVar(&ts, "t", "", "Target tables (table1,table2,...) (default: all tables)") 32 | 33 | f.Parse(os.Args[1:]) 34 | 35 | var url string 36 | 37 | for 0 < f.NArg() { 38 | url = f.Args()[0] 39 | f.Parse(f.Args()[1:]) 40 | } 41 | 42 | if url == "" { 43 | f.Usage() 44 | os.Exit(1) 45 | } 46 | 47 | if err := os.MkdirAll(dir, 0777); err != nil { 48 | fmt.Fprintln(os.Stderr, err) 49 | os.Exit(1) 50 | } 51 | 52 | fmt.Println("Connecting to database...") 53 | 54 | postgres, err := NewPostgres(url) 55 | if err != nil { 56 | fmt.Fprintln(os.Stderr, err) 57 | os.Exit(1) 58 | } 59 | defer postgres.DB.Close() 60 | 61 | var targets []string 62 | 63 | for _, t := range strings.Split(ts, ",") { 64 | if t != "" { 65 | targets = append(targets, t) 66 | } 67 | } 68 | 69 | tables, err := postgres.RetrieveTables(targets) 70 | if err != nil { 71 | fmt.Fprintln(os.Stderr, err) 72 | os.Exit(1) 73 | } 74 | 75 | modelParams := map[string]*TemplateParams{} 76 | 77 | for _, table := range tables { 78 | fmt.Println("Table name: " + table) 79 | 80 | pkeys, err := postgres.RetrievePrimaryKeys(table) 81 | if err != nil { 82 | fmt.Fprintln(os.Stderr, err) 83 | os.Exit(1) 84 | } 85 | 86 | fields, err := postgres.RetrieveFields(table) 87 | if err != nil { 88 | fmt.Fprintln(os.Stderr, err) 89 | os.Exit(1) 90 | } 91 | 92 | modelParams[table] = GenerateModel(table, pkeys, fields, tables) 93 | } 94 | 95 | for table, param := range modelParams { 96 | fmt.Println("Add relation for Table name: " + table) 97 | 98 | AddHasMany(param) 99 | 100 | if err := SaveModel(table, param, dir); err != nil { 101 | fmt.Fprintln(os.Stderr, err) 102 | os.Exit(1) 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pq2gorm - Generate [gorm](https://github.com/jinzhu/gorm) model structs from PostgreSQL database schema 2 | 3 | [![Build Status](https://travis-ci.org/wantedly/pq2gorm.svg?branch=master)](https://travis-ci.org/wantedly/pq2gorm) 4 | 5 | pq2gorm is a generator of [gorm](https://github.com/jinzhu/gorm) model structs from a PostgresSQL database. 6 | 7 | * Input: Connection URI of a PostgresSQL database. 8 | * Output: Model definitions based on [gorm](https://github.com/jinzhu/gorm) annotated struct. 9 | 10 | ## How to build and install 11 | 12 | Prepare Go 1.6 or higher. 13 | Go 1.5 is acceptable, but `GO15VENDOREXPERIMENT=1` must be set. 14 | 15 | After installing required version of Go, you can build and install `pq2gorm` by 16 | 17 | ```bash 18 | $ go get -d -u github.com/wantedly/pq2gorm 19 | $ cd $GOPATH/src/github.com/wantedly/pq2gorm 20 | $ make 21 | $ make install 22 | ``` 23 | 24 | `make` generates a binary into `bin/pq2gorm`. 25 | `make install` put it to `$GOPATH/bin`. 26 | 27 | ## How to use 28 | 29 | Run `pq2gorm` with Connection URI of a PostgresSQL database. 30 | Connection URI is necessary for running. 31 | 32 | ### Usage 33 | 34 | ```bash 35 | $ pq2gorm 36 | Usage: Generate gorm model structs from PostgreSQL database schema. 37 | -d string 38 | Set output path (default "./") 39 | -dir string 40 | Set output path (default "./") 41 | -t string 42 | Target tables (table1,table2,...) (default: all tables) 43 | -tables string 44 | Target tables (table1,table2,...) (default: all tables) 45 | ``` 46 | 47 | **Example 1:** Generate gorm model files of all tables in current directory. 48 | 49 | ```bash 50 | $ pq2gorm "postgresql://user:password@host:port/dbname?sslmode=disable" 51 | ``` 52 | 53 | For example, user model user.go as shown below will be generated: 54 | 55 | ```go 56 | type User struct { 57 | ID uint `json:"id"` 58 | ... 59 | } 60 | ``` 61 | 62 | **Example 2:** Generate gorm model files of all tables in `./out` directory. 63 | 64 | ```bash 65 | $ pq2gorm "postgresql://user:password@host:port/dbname?sslmode=disable" -d ./out 66 | ``` 67 | 68 | **Example 3:** Generate gorm model files of `profiles` and `users` tables. 69 | 70 | ```bash 71 | $ pq2gorm "postgresql://user:password@host:port/dbname?sslmode=disable" -d ./out -t profiles,users 72 | ``` 73 | 74 | If the directory `./out` does not exist, `pq2gorm` creates `./out` directory with output files. 75 | 76 | ## License 77 | [![MIT License](http://img.shields.io/badge/license-MIT-blue.svg?style=flat)](LICENSE) 78 | -------------------------------------------------------------------------------- /postgres.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "database/sql" 5 | "strconv" 6 | "strings" 7 | 8 | _ "github.com/lib/pq" 9 | ) 10 | 11 | type Postgres struct { 12 | DB *sql.DB 13 | } 14 | 15 | type Field struct { 16 | Name string 17 | Type string 18 | Default string 19 | Nullable bool 20 | } 21 | 22 | func NewPostgres(url string) (*Postgres, error) { 23 | db, err := sql.Open("postgres", url) 24 | if err != nil { 25 | return nil, err 26 | } 27 | 28 | return &Postgres{ 29 | DB: db, 30 | }, nil 31 | } 32 | 33 | func (p *Postgres) retrieveAllTables() (*sql.Rows, error) { 34 | return p.DB.Query(`select relname as TABLE_NAME from pg_stat_user_tables`) 35 | } 36 | 37 | func (p *Postgres) retrieveSelectedTables(targets []string) (*sql.Rows, error) { 38 | qs := []string{} 39 | params := []interface{}{} 40 | 41 | for i, t := range targets { 42 | qs = append(qs, "$"+strconv.Itoa(i+1)) 43 | params = append(params, t) 44 | } 45 | 46 | return p.DB.Query(`select relname as TABLE_NAME from pg_stat_user_tables where relname in (`+strings.Join(qs, ", ")+`)`, params...) 47 | } 48 | 49 | func (p *Postgres) RetrieveFields(table string) ([]*Field, error) { 50 | query := 51 | ` 52 | select column_name, data_type, COALESCE(column_default, '') as column_default, is_nullable 53 | from information_schema.columns 54 | where 55 | table_name='` + table + `' 56 | order by 57 | ordinal_position; 58 | ` 59 | 60 | rows, err := p.DB.Query(query) 61 | if err != nil { 62 | return nil, err 63 | } 64 | 65 | var ( 66 | columnName string 67 | columnType string 68 | columnDefault string 69 | columnIsNullable string 70 | ) 71 | 72 | var nullable bool 73 | 74 | fields := []*Field{} 75 | 76 | for rows.Next() { 77 | err = rows.Scan(&columnName, &columnType, &columnDefault, &columnIsNullable) 78 | if err != nil { 79 | return nil, err 80 | } 81 | 82 | if columnIsNullable == "YES" { 83 | nullable = true 84 | } else { 85 | nullable = false 86 | } 87 | 88 | field := &Field{ 89 | Name: columnName, 90 | Type: columnType, 91 | Default: columnDefault, 92 | Nullable: nullable, 93 | } 94 | fields = append(fields, field) 95 | } 96 | 97 | return fields, nil 98 | } 99 | 100 | func (p *Postgres) RetrieveTables(targets []string) ([]string, error) { 101 | var ( 102 | rows *sql.Rows 103 | err error 104 | ) 105 | 106 | if len(targets) == 0 { 107 | rows, err = p.retrieveAllTables() 108 | if err != nil { 109 | return nil, err 110 | } 111 | } else { 112 | rows, err = p.retrieveSelectedTables(targets) 113 | if err != nil { 114 | return nil, err 115 | } 116 | } 117 | 118 | tables := []string{} 119 | var table string 120 | 121 | for rows.Next() { 122 | err = rows.Scan(&table) 123 | if err != nil { 124 | return nil, err 125 | } 126 | 127 | tables = append(tables, table) 128 | } 129 | 130 | return tables, nil 131 | } 132 | 133 | func (p *Postgres) RetrievePrimaryKeys(table string) (map[string]bool, error) { 134 | query := 135 | ` 136 | select 137 | ccu.column_name as COLUMN_NAME 138 | from 139 | information_schema.table_constraints tc 140 | ,information_schema.constraint_column_usage ccu 141 | where 142 | tc.table_name='` + table + `' 143 | and 144 | tc.constraint_type='PRIMARY KEY' 145 | and 146 | tc.table_catalog=ccu.table_catalog 147 | and 148 | tc.table_schema=ccu.table_schema 149 | and 150 | tc.table_name=ccu.table_name 151 | and 152 | tc.constraint_name=ccu.constraint_name 153 | ` 154 | 155 | rows, err := p.DB.Query(query) 156 | if err != nil { 157 | return nil, err 158 | } 159 | 160 | var column string 161 | pkeys := map[string]bool{} 162 | 163 | for rows.Next() { 164 | err = rows.Scan(&column) 165 | if err != nil { 166 | return nil, err 167 | } 168 | 169 | pkeys[column] = true 170 | } 171 | 172 | return pkeys, nil 173 | } 174 | -------------------------------------------------------------------------------- /testdata/testdata.sql: -------------------------------------------------------------------------------- 1 | -- 2 | -- PostgreSQL database dump 3 | -- 4 | 5 | -- Dumped from database version 9.5.2 6 | -- Dumped by pg_dump version 9.5.2 7 | 8 | SET statement_timeout = 0; 9 | SET lock_timeout = 0; 10 | SET client_encoding = 'UTF8'; 11 | SET standard_conforming_strings = on; 12 | SET check_function_bodies = false; 13 | SET client_min_messages = warning; 14 | SET row_security = off; 15 | 16 | SET search_path = public, pg_catalog; 17 | 18 | SET default_tablespace = ''; 19 | 20 | SET default_with_oids = false; 21 | 22 | -- 23 | -- Name: preferences; Type: TABLE; Schema: public; Owner: postgres 24 | -- 25 | 26 | CREATE TABLE preferences ( 27 | id integer NOT NULL, 28 | user_id integer, 29 | created_at timestamp without time zone, 30 | updated_at timestamp without time zone, 31 | locale character varying(255) DEFAULT 'ja'::character varying, 32 | deleted_at timestamp without time zone, 33 | birthday date, 34 | email_subscriptions text, 35 | searchable boolean DEFAULT true NOT NULL 36 | ); 37 | 38 | 39 | ALTER TABLE preferences OWNER TO postgres; 40 | 41 | -- 42 | -- Name: preferences_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres 43 | -- 44 | 45 | CREATE SEQUENCE preferences_id_seq 46 | START WITH 1 47 | INCREMENT BY 1 48 | NO MINVALUE 49 | NO MAXVALUE 50 | CACHE 1; 51 | 52 | 53 | ALTER TABLE preferences_id_seq OWNER TO postgres; 54 | 55 | -- 56 | -- Name: preferences_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres 57 | -- 58 | 59 | ALTER SEQUENCE preferences_id_seq OWNED BY preferences.id; 60 | 61 | 62 | -- 63 | -- Name: projects; Type: TABLE; Schema: public; Owner: postgres 64 | -- 65 | 66 | CREATE TABLE projects ( 67 | id integer NOT NULL, 68 | company_id integer, 69 | title character varying(255), 70 | description text, 71 | location text, 72 | latitude double precision, 73 | longitude double precision 74 | ); 75 | 76 | 77 | ALTER TABLE projects OWNER TO postgres; 78 | 79 | -- 80 | -- Name: projects_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres 81 | -- 82 | 83 | CREATE SEQUENCE projects_id_seq 84 | START WITH 1 85 | INCREMENT BY 1 86 | NO MINVALUE 87 | NO MAXVALUE 88 | CACHE 1; 89 | 90 | 91 | ALTER TABLE projects_id_seq OWNER TO postgres; 92 | 93 | -- 94 | -- Name: projects_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres 95 | -- 96 | 97 | ALTER SEQUENCE projects_id_seq OWNED BY projects.id; 98 | 99 | -- 100 | -- Name: post_comments; Type: TABLE; Schema: public; Owner: postgres 101 | -- 102 | 103 | CREATE TABLE post_comments ( 104 | id integer NOT NULL, 105 | user_id integer, 106 | post_id integer, 107 | content text, 108 | created_at timestamp without time zone, 109 | updated_at timestamp without time zone 110 | ); 111 | 112 | 113 | ALTER TABLE post_comments OWNER TO postgres; 114 | 115 | -- 116 | -- Name: post_comments_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres 117 | -- 118 | 119 | CREATE SEQUENCE post_comments_id_seq 120 | START WITH 1 121 | INCREMENT BY 1 122 | NO MINVALUE 123 | NO MAXVALUE 124 | CACHE 1; 125 | 126 | 127 | ALTER TABLE post_comments_id_seq OWNER TO postgres; 128 | 129 | -- 130 | -- Name: post_comments_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres 131 | -- 132 | 133 | ALTER SEQUENCE post_comments_id_seq OWNED BY post_comments.id; 134 | 135 | -- 136 | -- Name: users; Type: TABLE; Schema: public; Owner: postgres 137 | -- 138 | 139 | CREATE TABLE users ( 140 | id integer NOT NULL, 141 | created_at timestamp without time zone, 142 | updated_at timestamp without time zone, 143 | deleted_at timestamp without time zone 144 | ); 145 | 146 | 147 | ALTER TABLE users OWNER TO postgres; 148 | 149 | -- 150 | -- Name: users_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres 151 | -- 152 | 153 | CREATE SEQUENCE users_id_seq 154 | START WITH 1 155 | INCREMENT BY 1 156 | NO MINVALUE 157 | NO MAXVALUE 158 | CACHE 1; 159 | 160 | 161 | ALTER TABLE users_id_seq OWNER TO postgres; 162 | 163 | -- 164 | -- Name: users_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres 165 | -- 166 | 167 | ALTER SEQUENCE users_id_seq OWNED BY users.id; 168 | 169 | 170 | -- 171 | -- Name: id; Type: DEFAULT; Schema: public; Owner: postgres 172 | -- 173 | 174 | ALTER TABLE ONLY preferences ALTER COLUMN id SET DEFAULT nextval('preferences_id_seq'::regclass); 175 | 176 | 177 | -- 178 | -- Name: id; Type: DEFAULT; Schema: public; Owner: postgres 179 | -- 180 | 181 | ALTER TABLE ONLY projects ALTER COLUMN id SET DEFAULT nextval('projects_id_seq'::regclass); 182 | 183 | -- 184 | -- Name: id; Type: DEFAULT; Schema: public; Owner: postgres 185 | -- 186 | 187 | ALTER TABLE ONLY post_comments ALTER COLUMN id SET DEFAULT nextval('post_comments_id_seq'::regclass); 188 | 189 | -- 190 | -- Name: id; Type: DEFAULT; Schema: public; Owner: postgres 191 | -- 192 | 193 | ALTER TABLE ONLY users ALTER COLUMN id SET DEFAULT nextval('users_id_seq'::regclass); 194 | 195 | -- 196 | -- PostgreSQL database dump complete 197 | -- 198 | -------------------------------------------------------------------------------- /generate.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "go/format" 6 | "io/ioutil" 7 | "path/filepath" 8 | "strings" 9 | "text/template" 10 | 11 | "github.com/gedex/inflector" 12 | "github.com/serenize/snaker" 13 | ) 14 | 15 | type TemplateField struct { 16 | Name string 17 | Type string 18 | Tag string 19 | Comment string 20 | } 21 | 22 | type TemplateParams struct { 23 | Name string 24 | Fields []*TemplateField 25 | NeedTimePackage bool 26 | } 27 | 28 | var hasMany = make(map[string][]string) 29 | 30 | func GenerateModel(table string, pkeys map[string]bool, fields []*Field, tables []string) *TemplateParams { 31 | var needTimePackage bool 32 | 33 | templateFields := []*TemplateField{} 34 | 35 | for _, field := range fields { 36 | fieldType := gormDataType(field.Type) 37 | 38 | if fieldType == "time.Time" || fieldType == "*time.Time" { 39 | needTimePackage = true 40 | 41 | if field.Nullable { 42 | fieldType = "*time.Time" 43 | } else { 44 | fieldType = "time.Time" 45 | } 46 | } 47 | 48 | if fieldType == "double precision" { 49 | fieldType = "float32" 50 | } 51 | 52 | templateFields = append(templateFields, &TemplateField{ 53 | Name: gormColumnName(field.Name), 54 | Type: fieldType, 55 | Tag: genJSON(field.Name, field.Default, pkeys), 56 | }) 57 | 58 | isInfered, infColName := inferORM(field.Name, tables) 59 | 60 | colName := gormColumnName(infColName) 61 | 62 | // Add belongs_to relation 63 | if isInfered { 64 | templateFields = append(templateFields, &TemplateField{ 65 | Name: colName, 66 | Type: "*" + colName, 67 | Tag: genJSON(strings.ToLower(infColName), "", nil), 68 | Comment: "This line is infered from column name \"" + field.Name + "\".", 69 | }) 70 | 71 | // Add has_many relation 72 | hasMany[colName] = append(hasMany[colName], table) 73 | } 74 | } 75 | 76 | params := &TemplateParams{ 77 | Name: gormTableName(table), 78 | Fields: templateFields, 79 | NeedTimePackage: needTimePackage, 80 | } 81 | 82 | return params 83 | } 84 | 85 | func AddHasMany(params *TemplateParams) { 86 | if _, ok := hasMany[params.Name]; ok { 87 | for _, infColName := range hasMany[params.Name] { 88 | params.Fields = append(params.Fields, &TemplateField{ 89 | Name: gormColumnName(infColName), 90 | Type: "[]*" + gormTableName(infColName), 91 | Tag: genJSON(strings.ToLower(infColName), "", nil), 92 | Comment: "This line is infered from other tables.", 93 | }) 94 | } 95 | } 96 | } 97 | 98 | func SaveModel(table string, params *TemplateParams, outPath string) error { 99 | body, err := Asset("_templates/model.go.tmpl") 100 | if err != nil { 101 | return err 102 | } 103 | 104 | tmpl, err := template.New("").Parse(string(body)) 105 | if err != nil { 106 | return err 107 | } 108 | 109 | var buf bytes.Buffer 110 | 111 | if err := tmpl.Execute(&buf, params); err != nil { 112 | return err 113 | } 114 | 115 | src, err := format.Source(buf.Bytes()) 116 | if err != nil { 117 | return err 118 | } 119 | 120 | modelFile := filepath.Join(outPath, inflector.Singularize(table)+".go") 121 | 122 | if err := ioutil.WriteFile(modelFile, src, 0644); err != nil { 123 | return err 124 | } 125 | 126 | return nil 127 | } 128 | 129 | // Infer belongs_to Relation from column's name 130 | func inferORM(s string, tables []string) (bool, string) { 131 | s = strings.ToLower(s) 132 | ss := strings.Split(s, "_") 133 | 134 | newSS := []string{} 135 | var containsID bool = false 136 | for _, word := range ss { 137 | if word == "id" { 138 | containsID = true 139 | continue 140 | } 141 | 142 | newSS = append(newSS, word) 143 | } 144 | 145 | if containsID == false || len(newSS) == 0 { 146 | return false, "" 147 | } 148 | 149 | infColName := strings.Join(newSS, "_") 150 | 151 | // Check the table is existed or not 152 | tableName := snaker.CamelToSnake(infColName) 153 | tableName = inflector.Pluralize(tableName) 154 | 155 | exist := false 156 | for _, table := range tables { 157 | if table == tableName { 158 | exist = true 159 | } 160 | } 161 | 162 | if !exist { 163 | return false, "" 164 | } 165 | 166 | return true, infColName 167 | } 168 | 169 | // Generate json 170 | func genJSON(columnName, columnDefault string, primaryKeys map[string]bool) (json string) { 171 | json = "json:\"" + columnName + "\"" 172 | 173 | if primaryKeys[columnName] { 174 | p := "gorm:\"primary_key;AUTO_INCREMENT\" " 175 | json = p + json 176 | } 177 | 178 | if columnDefault != "" && !strings.Contains(columnDefault, "nextval") { 179 | d := " sql:\"DEFAULT:" + columnDefault + "\"" 180 | json += d 181 | } 182 | 183 | return 184 | } 185 | 186 | // Singlarlize table name and upper initial character 187 | func gormTableName(s string) string { 188 | var tableName string 189 | 190 | tableName = strings.ToLower(s) 191 | tableName = inflector.Singularize(tableName) 192 | tableName = snaker.SnakeToCamel(tableName) 193 | 194 | return strings.Title(tableName) 195 | } 196 | 197 | // Ex: facebook_uid → FacebookUID 198 | func gormColumnName(s string) string { 199 | s = strings.ToLower(s) 200 | ss := strings.Split(s, "_") 201 | 202 | for i, word := range ss { 203 | if word == "id" || word == "uid" || word == "url" { 204 | word = strings.ToUpper(word) 205 | } 206 | 207 | ss[i] = strings.Title(word) 208 | } 209 | 210 | return strings.Join(ss, "") 211 | } 212 | 213 | func gormDataType(s string) string { 214 | switch s { 215 | case "integer": 216 | return "uint" 217 | case "numeric": 218 | return "float64" 219 | case "character varying", "text": 220 | return "string" 221 | case "boolean": 222 | return "bool" 223 | case "timestamp with time zone", "timestamp without time zone": 224 | return "time.Time" 225 | case "date": 226 | return "*time.Time" 227 | default: 228 | return s 229 | } 230 | } 231 | --------------------------------------------------------------------------------