├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── command ├── dump_db.go ├── gen_code.go ├── gen_migration.go └── template │ └── sqlc.tmpl ├── config ├── code.go ├── code_test.go ├── field.go ├── field_test.go ├── index.go ├── index_test.go ├── schema.go └── schema_test.go ├── examples ├── json-schema-spec.md └── schemas │ ├── document.json │ ├── example.json │ ├── school.json │ └── user.json ├── generator ├── code.go └── code_test.go ├── go.mod ├── go.sum ├── main.go ├── sqlgen ├── alter_table_generator.go ├── alter_table_generator_test.go ├── create_index_generator.go ├── create_index_generator_test.go ├── create_table_generator.go ├── create_table_generator_test.go ├── dialect │ └── dialect_option.go ├── diff │ ├── schema.go │ └── schema_test.go ├── dir │ └── dir.go ├── drop_index_generator.go ├── drop_index_generator_test.go ├── drop_table_generator.go ├── drop_table_generator_test.go ├── exp │ ├── expression_sql_generator.go │ └── expression_sql_generator_test.go ├── flag.go ├── flag_test.go ├── generator.go ├── generator_test.go ├── json │ ├── schema.go │ └── schema_test.go ├── mocks │ └── schema │ │ └── schema.go ├── sb │ └── sql_builder.go ├── schema │ ├── postgres.go │ ├── postgres_test.go │ ├── schema.go │ └── schema_test.go └── step │ ├── alter_column.go │ ├── alter_schema.go │ └── migration_planner.go ├── types ├── field_option │ ├── type.go │ └── type_test.go └── field_type │ ├── type.go │ └── type_test.go └── version.go /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | coverage.* 3 | db 4 | .idea -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright © 2022 GovTechID 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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: build 2 | build: 3 | go build -o ${GOPATH}/bin/dbgen . 4 | 5 | .PHONY: test.cleancache 6 | test.cleancache: 7 | go clean -testcache 8 | 9 | .PHONY: test.unit 10 | test.unit: test.cleancache 11 | go test -v -race ./... 12 | 13 | .PHONY: test.cover 14 | test.cover: test.cleancache 15 | go test -v -race ./... -coverprofile=coverage.out 16 | go tool cover -html=coverage.out -o coverage.html 17 | go tool cover -func coverage.out 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Go Database Code Generator 2 | This tool is to help you generate schema migrations and CRUD code in Golang from an entity definition in form of JSON. 3 | 4 | ## Why 5 | This tool is inspired by an amazing video titled [Design Microservice Architectures the Right Way](https://www.youtube.com/watch?v=j6ow-UemzBc). 6 | Plenty of reasons why this tool is built can be found there. One of the example, according to that video, is when things 7 | are slow because some queries don't have index, the hero is not the person who fix it by adding index. However, the unsung 8 | hero is the one who prevents it from ever happened in the first place. One way to do that is to provide a tool to generate high 9 | quality database access code. This dbcodegen is aimed to be such tool. 10 | 11 | ## Supported Command 12 | ## gen:migration 13 | Generate database migration file 14 | 15 | Command: 16 | ``` 17 | dbgen gen:migration -c {connection_string} -d {output directory} -o {output filename} input file(s)/folder(s) 18 | ``` 19 | Example: 20 | ``` 21 | dbgen gen:migration -c postgresql://postgres:postgres@localhost:5432/playground -d db/migration -o users_registrations db/schemas 22 | ``` 23 | 24 | ## gen:code 25 | Generate schemas and queries into code 26 | 27 | Command: 28 | ``` 29 | dbgen gen:code -i {input folder} 30 | ``` 31 | 32 | Example: 33 | ``` 34 | dbgen gen:code -i examples/schemas 35 | ``` 36 | 37 | Notes: 38 | gen:code also has some optional fields: 39 | 1. packageName: package name for database in Go 40 | 2. packagePath: output path for database in Go 41 | 3. sqlPackage: SQL Package to specify which library to use 42 | 43 | ## dump:db 44 | Dump current schemas into JSON files 45 | 46 | Command: 47 | ``` 48 | dbgen dump:db -c {connection} -o {output path} 49 | ``` 50 | 51 | Example: 52 | ``` 53 | dump:db -c postgresql://postgres:postgres@localhost:5432/playground -o examples/schemas 54 | ``` 55 | 56 | ## Input file Example 57 | The input is JSON file containing structures of an entity. Complete schema spec can be found [here](https://github.com/telkomdev/go-dbcodegen/blob/main/examples/schemas/json-schema-spec.md) 58 | ``` 59 | { 60 | "name": "example", 61 | "fields": [ 62 | { 63 | "name": "id", 64 | "type": "bigserial", 65 | "options": [ 66 | "primary key" 67 | ] 68 | }, 69 | { 70 | "name": "name", 71 | "type": "varchar", 72 | "limit": 50, 73 | "options": [ 74 | "not null" 75 | ] 76 | } 77 | ], 78 | "indexes": [ 79 | { 80 | "name": "index_example_on_name", 81 | "fields": [ 82 | { 83 | "column": "name" 84 | } 85 | ], 86 | "unique": true 87 | } 88 | ] 89 | } 90 | ``` 91 | Other examples can be found [here](https://github.com/telkomdev/go-dbcodegen/tree/main/examples/schemas) 92 | 93 | -------------------------------------------------------------------------------- /command/dump_db.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/fatih/color" 8 | "github.com/spf13/cobra" 9 | "gitlab.com/wartek-id/core/tools/dbgen/sqlgen/dir" 10 | "gitlab.com/wartek-id/core/tools/dbgen/sqlgen/json" 11 | "gitlab.com/wartek-id/core/tools/dbgen/sqlgen/schema" 12 | ) 13 | 14 | var ( 15 | outputDir string 16 | DumpDbCmd = &cobra.Command{ 17 | Use: "dump:db", 18 | Short: "Dump db", 19 | Long: "This command is used to dump your current db and save it to your JSON schemas", 20 | Run: DumpDb, 21 | Example: "dump:db -c postgresql://postgres:postgres@localhost:5432/playground -o examples/schemas", // pragma: allowlist secret 22 | } 23 | ) 24 | 25 | func init() { 26 | DumpDbCmd.Flags().StringVarP(&migrationConnString, "connection", "c", "", "(Required) Set connection string") 27 | DumpDbCmd.Flags().StringVarP(&outputDir, "output", "o", "", "Output schemas path") 28 | DumpDbCmd.MarkFlagRequired("connection") 29 | DumpDbCmd.MarkFlagRequired("output") 30 | } 31 | 32 | func DumpDb(cmd *cobra.Command, args []string) { 33 | fmt.Println("🚀 Dumping database") 34 | crawler, err := schema.NewSchema(migrationConnString) 35 | if err != nil { 36 | fmt.Println(color.RedString("Failed to connect to datasource")) 37 | fmt.Println("Please see error details below:") 38 | fmt.Printf("\t%s\n", err) 39 | os.Exit(1) 40 | } 41 | 42 | gen := json.NewSchemasGenerator(crawler) 43 | override, err := dir.CheckDirExists(outputDir) 44 | if err != nil { 45 | fmt.Println(color.RedString("Check output path failed")) 46 | fmt.Println("Please see error details below:") 47 | fmt.Printf("\t%s\n", err) 48 | os.Exit(1) 49 | } 50 | 51 | if override { 52 | err = gen.GenerateBySchemas(outputDir) 53 | if err != nil { 54 | fmt.Println(color.RedString("Failed to generate schemas to JSON")) 55 | fmt.Println("Please see error details below:") 56 | fmt.Printf("\t%s\n", err) 57 | os.Exit(1) 58 | } 59 | } 60 | fmt.Println("Succeed dumping database") 61 | } 62 | -------------------------------------------------------------------------------- /command/gen_code.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "bytes" 5 | "embed" 6 | _ "embed" 7 | "fmt" 8 | "html/template" 9 | "os" 10 | "os/exec" 11 | "path/filepath" 12 | 13 | "github.com/fatih/color" 14 | "github.com/spf13/cobra" 15 | "gitlab.com/wartek-id/core/tools/dbgen/config" 16 | "gitlab.com/wartek-id/core/tools/dbgen/generator" 17 | ) 18 | 19 | const ( 20 | SqlcTemplatePath = "template/sqlc.tmpl" 21 | DefaultPackageName = "db" 22 | DefaultPackagePath = "internal/db" 23 | DefaultSqlPackage = "pgx/v4" 24 | SqlcPath = "./sqlc.yaml" 25 | TemporaryPath = "./temp" 26 | OutputQueriesPath = TemporaryPath + "/query/" 27 | FullSchemaMigrationPath = TemporaryPath + "/fullschema/" 28 | ) 29 | 30 | type SqlcConfig struct { 31 | PackageName string 32 | PackagePath string 33 | SqlPackage string 34 | } 35 | 36 | var ( 37 | packageName string 38 | packagePath string 39 | sqlPackage string 40 | inputPath string 41 | GenCode = &cobra.Command{ 42 | Use: "gen:code", 43 | Short: "Generate code", 44 | Long: "This command is to generate the CRUD code", 45 | Run: GenerateCode, 46 | Example: "gen:code -i examples/schemas", 47 | } 48 | //go:embed template/* 49 | sqlcTemplate embed.FS 50 | ) 51 | 52 | func init() { 53 | GenCode.Flags().StringVar(&packageName, "packageName", DefaultPackageName, "(Optional) Package name for database in Go") 54 | GenCode.Flags().StringVar(&packagePath, "packagePath", DefaultPackagePath, "(Optional) Output path for database in Go") 55 | GenCode.Flags().StringVar(&sqlPackage, "sqlPackage", DefaultSqlPackage, "(Optional) SQL Package to specify which library to use") 56 | GenCode.Flags().StringVarP(&inputPath, "inputPath", "i", "", "(Required) Path to JSON schemas") 57 | GenCode.MarkFlagRequired("inputPath") 58 | } 59 | 60 | func GenerateCode(cmd *cobra.Command, args []string) { 61 | CreateSqlcConfigFile() 62 | schemas, err := config.Parse(inputPath) 63 | function := generator.GenerateQueries(schemas) 64 | err = SaveQueriesToFile(function, OutputQueriesPath) 65 | if err != nil { 66 | fmt.Println(err) 67 | os.Exit(1) 68 | } 69 | 70 | err = ExecuteSqlc() 71 | if err != nil { 72 | fmt.Println(err) 73 | os.Exit(1) 74 | } 75 | 76 | err = RemoveUnusedFiles() 77 | if err != nil { 78 | fmt.Println(err) 79 | os.Exit(1) 80 | } 81 | } 82 | 83 | func ExecuteSqlc() error { 84 | fmt.Println("🚀 Generating code") 85 | _, err := exec.LookPath("sqlc") 86 | if err != nil { 87 | DownloadSqlc() 88 | } 89 | cmd := exec.Command("sqlc", "generate") 90 | cmd.Stdout = os.Stdout 91 | cmd.Stderr = os.Stdout 92 | if err = cmd.Run(); err != nil { 93 | fmt.Println(color.RedString("Generate code failed")) 94 | return err 95 | } 96 | fmt.Println(color.GreenString("Succeeded")) 97 | return nil 98 | } 99 | 100 | func DownloadSqlc() { 101 | cmd := exec.Command("go", "install", "github.com/kyleconroy/sqlc/cmd/sqlc@v1.13.0") 102 | cmd.Stdout = os.Stdout 103 | cmd.Stderr = os.Stdout 104 | if err := cmd.Run(); err != nil { 105 | fmt.Println("Download sqlc failed") 106 | panic(err) 107 | } 108 | } 109 | 110 | func CreateSqlcConfigFile() error { 111 | sqlcConfig := &SqlcConfig{ 112 | PackageName: packageName, 113 | PackagePath: packagePath, 114 | SqlPackage: sqlPackage, 115 | } 116 | 117 | template, err := template.ParseFS(sqlcTemplate, SqlcTemplatePath) 118 | if err != nil { 119 | return err 120 | } 121 | 122 | f, err := os.Create(SqlcPath) 123 | if err != nil { 124 | return err 125 | } 126 | err = template.Execute(f, sqlcConfig) 127 | if err != nil { 128 | return err 129 | } 130 | f.Close() 131 | 132 | return nil 133 | } 134 | 135 | func SaveQueriesToFile(function []*config.Function, outputPath string) error { 136 | var buf bytes.Buffer 137 | curFilename := "" 138 | curTable := "" 139 | err := os.MkdirAll(outputPath, 0755) 140 | if err != nil { 141 | return err 142 | } 143 | 144 | for _, f := range function { 145 | if curTable != f.TableName { 146 | if curTable != "" { 147 | os.WriteFile(curFilename, buf.Bytes(), 0644) 148 | buf = *&bytes.Buffer{} 149 | fmt.Printf(color.GreenString("Succeed generate query, target file: %s\n"), color.HiBlueString(curFilename)) 150 | } 151 | curTable = f.TableName 152 | curFilename = filepath.Join(outputPath, curTable+".sql") 153 | fmt.Printf("\n🚀 Generating query to file for table: %s\n", f.TableName) 154 | } 155 | buf.WriteString("-- name: " + f.Name + " " + f.SqlcType + "\n") 156 | buf.WriteString(f.Query + ";\n\n") 157 | } 158 | os.WriteFile(curFilename, buf.Bytes(), 0644) 159 | fmt.Printf(color.GreenString("Succeed generate query, target file: %s\n"), color.HiBlueString(curFilename)) 160 | return nil 161 | } 162 | 163 | func RemoveUnusedFiles() error { 164 | err := os.RemoveAll(SqlcPath) 165 | if err != nil { 166 | return err 167 | } 168 | 169 | err = os.RemoveAll(TemporaryPath) 170 | if err != nil { 171 | return err 172 | } 173 | 174 | return nil 175 | } 176 | -------------------------------------------------------------------------------- /command/gen_migration.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/fatih/color" 8 | "github.com/spf13/cobra" 9 | "gitlab.com/wartek-id/core/tools/dbgen/config" 10 | "gitlab.com/wartek-id/core/tools/dbgen/sqlgen" 11 | "gitlab.com/wartek-id/core/tools/dbgen/sqlgen/schema" 12 | ) 13 | 14 | const ( 15 | DefaultConnectionString = "" 16 | DefaultOutputDirectory = "db/migration" 17 | DefaultOutputName = "" 18 | DefaultSkipTable = false 19 | ) 20 | 21 | var GenMigration = &cobra.Command{ 22 | Use: "gen:migration [flags] file(s)/folder(s)...", 23 | Short: "Generate SQL migration", 24 | Long: "This command is used to generate new migrations and the full schema", 25 | Run: GenerateMigration, 26 | Example: "gen:migration -c postgresql://postgres:postgres@localhost:5432/playground -o migration user.json logging.json", // pragma: allowlist secret 27 | } 28 | 29 | var ( 30 | migrationConnString, 31 | migrationDir, 32 | migrationOutput string 33 | 34 | skipDropTable bool 35 | ) 36 | 37 | func init() { 38 | GenMigration.Flags().StringVarP(&migrationConnString, "connection", "c", DefaultConnectionString, "set connection string") 39 | GenMigration.Flags().StringVarP(&migrationDir, "dir", "d", DefaultOutputDirectory, "set migration directory") 40 | GenMigration.Flags().StringVarP(&migrationOutput, "output", "o", DefaultOutputName, "set output name") 41 | GenMigration.Flags().BoolVar(&skipDropTable, "skip-drop-table", DefaultSkipTable, "skip drop table generation query") 42 | } 43 | 44 | func GenerateMigration(cmd *cobra.Command, args []string) { 45 | if len(args) < 1 { 46 | fmt.Print(color.RedString("Command Failed ")) 47 | fmt.Println("Please specify schema file(s)/folder(s)") 48 | os.Exit(1) 49 | } 50 | 51 | schemas, err := config.Parse(args...) 52 | if err != nil { 53 | fmt.Println(color.RedString("Database Generation Failed")) 54 | fmt.Println("Please see error details below:") 55 | fmt.Printf("\t%s\n", err) 56 | os.Exit(1) 57 | } 58 | 59 | crawler, err := schema.NewSchema(migrationConnString) 60 | if err != nil { 61 | fmt.Println(color.RedString("Failed to connect to datasource")) 62 | fmt.Println("Please see error details below:") 63 | fmt.Printf("\t%s\n", err) 64 | os.Exit(1) 65 | } 66 | 67 | flag, err := sqlgen.NewFlag(migrationDir, migrationOutput, skipDropTable) 68 | if err != nil { 69 | fmt.Println(err) 70 | os.Exit(1) 71 | } 72 | 73 | gen := sqlgen.NewGenerator(crawler, schemas, flag) 74 | err = gen.Generate() 75 | if err != nil { 76 | fmt.Println(err) 77 | os.Exit(1) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /command/template/sqlc.tmpl: -------------------------------------------------------------------------------- 1 | version: "1" 2 | packages: 3 | - name: {{.PackageName}} 4 | emit_json_tags: true 5 | emit_prepared_queries: false 6 | emit_interface: true 7 | path: {{.PackagePath}} 8 | queries: "./temp/query/" 9 | schema: "./temp/fullschema/" 10 | sql_package: {{.SqlPackage}} 11 | -------------------------------------------------------------------------------- /config/code.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | type Function struct { 4 | Name string 5 | Query string 6 | TableName string 7 | SqlcType string 8 | } 9 | 10 | func ParseFunction(name string, query string, tableName string, sqlcType string) *Function { 11 | return &Function{ 12 | Name: name, 13 | Query: query, 14 | TableName: tableName, 15 | SqlcType: sqlcType, 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /config/code_test.go: -------------------------------------------------------------------------------- 1 | package config_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "gitlab.com/wartek-id/core/tools/dbgen/config" 8 | ) 9 | 10 | func TestParseFunction(t *testing.T) { 11 | function := config.Function{ 12 | Name: "Name", 13 | Query: "Query", 14 | TableName: "TableName", 15 | SqlcType: "one", 16 | } 17 | result := config.ParseFunction("Name", "Query", "TableName", "one") 18 | assert.Equal(t, result.Name, function.Name) 19 | assert.Equal(t, result.Query, function.Query) 20 | assert.Equal(t, result.TableName, function.TableName) 21 | assert.Equal(t, result.SqlcType, function.SqlcType) 22 | } 23 | -------------------------------------------------------------------------------- /config/field.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "gitlab.com/wartek-id/core/tools/dbgen/types/field_option" 5 | "gitlab.com/wartek-id/core/tools/dbgen/types/field_type" 6 | ) 7 | 8 | type Field struct { 9 | Name string `json:"name"` 10 | Type field_type.FieldType `json:"type"` 11 | Scale int `json:"scale"` 12 | Limit int `json:"limit"` 13 | Default interface{} `json:"default"` 14 | Options []field_option.FieldOption `json:"options"` 15 | } 16 | 17 | func (f *Field) GetName() string { 18 | return f.Name 19 | } 20 | 21 | func (f *Field) IsNotNull() bool { 22 | for _, opt := range f.Options { 23 | switch opt { 24 | case field_option.NotNull, field_option.PrimaryKey: 25 | return true 26 | case field_option.Nullable: 27 | return false 28 | } 29 | } 30 | 31 | return false 32 | } 33 | -------------------------------------------------------------------------------- /config/field_test.go: -------------------------------------------------------------------------------- 1 | package config_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "gitlab.com/wartek-id/core/tools/dbgen/config" 8 | "gitlab.com/wartek-id/core/tools/dbgen/types/field_option" 9 | ) 10 | 11 | func TestField_GetName(t *testing.T) { 12 | field := config.Field{ 13 | Name: "users", 14 | } 15 | 16 | assert.Equal(t, "users", field.GetName()) 17 | } 18 | 19 | func TestField_IsNotNull(t *testing.T) { 20 | field := config.Field{ 21 | Name: "users", 22 | Options: []field_option.FieldOption{ 23 | field_option.NotNull, 24 | }, 25 | } 26 | 27 | assert.True(t, field.IsNotNull()) 28 | 29 | field = config.Field{ 30 | Name: "users", 31 | Options: []field_option.FieldOption{ 32 | field_option.Nullable, 33 | }, 34 | } 35 | 36 | assert.False(t, field.IsNotNull()) 37 | 38 | field = config.Field{ 39 | Name: "users", 40 | } 41 | 42 | assert.False(t, field.IsNotNull()) 43 | } 44 | -------------------------------------------------------------------------------- /config/index.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import "encoding/json" 4 | 5 | type Index struct { 6 | Name string `json:"name"` 7 | Fields []*IndexField `json:"fields"` 8 | Unique bool `json:"unique"` 9 | } 10 | 11 | type IndexField struct { 12 | Column string `json:"column"` 13 | Order string `json:"order"` 14 | } 15 | 16 | func (f *IndexField) UnmarshalJSON(data []byte) error { 17 | type fieldAlias IndexField 18 | field := fieldAlias{ 19 | Order: "ASC", 20 | } 21 | err := json.Unmarshal(data, &field) 22 | if err != nil { 23 | return err 24 | } 25 | *f = IndexField(field) 26 | return nil 27 | } 28 | 29 | func (i *Index) GetName() string { 30 | return i.Name 31 | } 32 | 33 | func (i *Index) GetColumns() []string { 34 | cols := []string{} 35 | for _, field := range i.Fields { 36 | cols = append(cols, field.Column) 37 | } 38 | 39 | return cols 40 | } 41 | -------------------------------------------------------------------------------- /config/index_test.go: -------------------------------------------------------------------------------- 1 | package config_test 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "gitlab.com/wartek-id/core/tools/dbgen/config" 9 | ) 10 | 11 | func TestIndex_GetName(t *testing.T) { 12 | index := config.Index{ 13 | Name: "users", 14 | } 15 | 16 | assert.Equal(t, "users", index.GetName()) 17 | } 18 | 19 | func TestIndex_UnmarshallJSON(t *testing.T) { 20 | testCases := []struct { 21 | input []byte 22 | result *config.IndexField 23 | }{ 24 | { 25 | input: []byte(`{"column": "name"}`), 26 | result: &config.IndexField{ 27 | Column: "name", 28 | Order: "ASC", 29 | }, 30 | }, 31 | { 32 | input: []byte(`{"column": "name", "order": "ASC"}`), 33 | result: &config.IndexField{ 34 | Column: "name", 35 | Order: "ASC", 36 | }, 37 | }, 38 | { 39 | input: []byte(`{"column": "name", "order": "DESC"}`), 40 | result: &config.IndexField{ 41 | Column: "name", 42 | Order: "DESC", 43 | }, 44 | }, 45 | } 46 | 47 | for _, tc := range testCases { 48 | var result config.IndexField 49 | err := json.Unmarshal(tc.input, &result) 50 | 51 | assert.Nil(t, err) 52 | assert.Equal(t, tc.result, &result) 53 | } 54 | } 55 | 56 | func TestIndex_GetColumns(t *testing.T) { 57 | index := config.Index{ 58 | Name: "index_users_on_name_and_email", 59 | Fields: []*config.IndexField{ 60 | { 61 | Column: "name", 62 | }, 63 | { 64 | Column: "email", 65 | }, 66 | }, 67 | } 68 | 69 | assert.Equal(t, []string{"name", "email"}, index.GetColumns()) 70 | } 71 | -------------------------------------------------------------------------------- /config/schema.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "encoding/json" 5 | "io/fs" 6 | "os" 7 | "path/filepath" 8 | 9 | "github.com/pkg/errors" 10 | ) 11 | 12 | type Schema struct { 13 | Name string `json:"name"` 14 | Fields []*Field `json:"fields"` 15 | Index []*Index `json:"indexes"` 16 | } 17 | 18 | func (s *Schema) GetName() string { 19 | return s.Name 20 | } 21 | 22 | func ParseSchema(path string) (*Schema, error) { 23 | var schema Schema 24 | b, err := os.ReadFile(path) 25 | if err != nil { 26 | return nil, err 27 | } 28 | 29 | err = json.Unmarshal(b, &schema) 30 | if err != nil { 31 | return nil, err 32 | } 33 | return &schema, nil 34 | } 35 | 36 | func ParseDir(rootPath string) ([]*Schema, error) { 37 | schemas := make([]*Schema, 0) 38 | 39 | rootPath, err := filepath.Abs(rootPath) 40 | if err != nil { 41 | return schemas, err 42 | } 43 | 44 | wd, _ := os.Getwd() 45 | err = filepath.WalkDir(rootPath, func(path string, d fs.DirEntry, _ error) error { 46 | if d.IsDir() { 47 | return nil 48 | } 49 | 50 | if filepath.Ext(path) != ".json" { 51 | return nil 52 | } 53 | schema, err := ParseSchema(path) 54 | if err != nil { 55 | filename, _ := filepath.Rel(wd, path) 56 | return errors.Wrap(err, filename) 57 | } 58 | 59 | schemas = append(schemas, schema) 60 | return nil 61 | }) 62 | 63 | if err != nil { 64 | return nil, err 65 | } 66 | 67 | return schemas, nil 68 | } 69 | 70 | func Parse(paths ...string) ([]*Schema, error) { 71 | var schemes []*Schema 72 | for _, path := range paths { 73 | path, err := filepath.Abs(path) 74 | if err != nil { 75 | return nil, err 76 | } 77 | 78 | stat, err := os.Stat(path) 79 | if os.IsNotExist(err) { 80 | return nil, err 81 | } 82 | 83 | if stat.IsDir() { 84 | pathSchemas, err := ParseDir(path) 85 | if err != nil { 86 | return nil, err 87 | } 88 | schemes = append(schemes, pathSchemas...) 89 | } else { 90 | scheme, err := ParseSchema(path) 91 | if err != nil { 92 | return nil, err 93 | } 94 | schemes = append(schemes, scheme) 95 | } 96 | } 97 | return schemes, nil 98 | } 99 | -------------------------------------------------------------------------------- /config/schema_test.go: -------------------------------------------------------------------------------- 1 | package config_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "gitlab.com/wartek-id/core/tools/dbgen/config" 8 | ) 9 | 10 | func TestParseSchema(t *testing.T) { 11 | schema, err := config.ParseSchema("../examples/schemas/example.json") 12 | assert.NoError(t, err) 13 | assert.Equal(t, "example", schema.Name) 14 | assert.Len(t, schema.Fields, 2) 15 | } 16 | 17 | func TestParseDir(t *testing.T) { 18 | schemas, err := config.ParseDir("../examples/schemas") 19 | assert.NoError(t, err) 20 | assert.Len(t, schemas, 4) 21 | } 22 | 23 | func TestParse(t *testing.T) { 24 | // Parse Dir 25 | schemas, err := config.Parse("../examples/schemas") 26 | assert.NoError(t, err) 27 | assert.Len(t, schemas, 4) 28 | 29 | // Parse File 30 | schemas, err = config.Parse("../examples/schemas/example.json") 31 | assert.NoError(t, err) 32 | assert.Len(t, schemas, 1) 33 | 34 | // DirNotExists 35 | schemas, err = config.Parse("invalid_dir") 36 | assert.NotNil(t, err) 37 | assert.Empty(t, schemas) 38 | } 39 | 40 | func TestSchema_GetName(t *testing.T) { 41 | schema := config.Schema{ 42 | Name: "users", 43 | } 44 | 45 | assert.Equal(t, "users", schema.GetName()) 46 | } 47 | -------------------------------------------------------------------------------- /examples/json-schema-spec.md: -------------------------------------------------------------------------------- 1 | # Json Schema Spec 2 | ``` 3 | { 4 | "$schema": "http://json-schema.org/draft-04/schema#", 5 | "type": "object", 6 | "properties": { 7 | "name": { 8 | "type": "string" 9 | }, 10 | "fields": { 11 | "type": "array", 12 | "items": [ 13 | { 14 | "type": "object", 15 | "properties": { 16 | "name": { 17 | "type": "string" 18 | }, 19 | "type": { 20 | "type": "string" 21 | }, 22 | "options": { 23 | "type": "array", 24 | "items": [ 25 | { 26 | "type": "string" 27 | } 28 | ] 29 | } 30 | }, 31 | "required": [ 32 | "name", 33 | "type", 34 | "options" 35 | ] 36 | } 37 | ] 38 | }, 39 | "indexes": { 40 | "type": "array", 41 | "items": [ 42 | { 43 | "type": "object", 44 | "properties": { 45 | "name": { 46 | "type": "string" 47 | }, 48 | "fields": { 49 | "type": "array", 50 | "items": [ 51 | { 52 | "type": "object", 53 | "properties": { 54 | "column": { 55 | "type": "string" 56 | }, 57 | "order": { 58 | "type": "string" 59 | } 60 | }, 61 | "required": [ 62 | "column" 63 | ] 64 | } 65 | ] 66 | }, 67 | "unique": { 68 | "type": "boolean" 69 | } 70 | }, 71 | "required": [ 72 | "name", 73 | "fields", 74 | "unique" 75 | ] 76 | } 77 | ] 78 | } 79 | }, 80 | "required": [ 81 | "name", 82 | "fields", 83 | "indexes" 84 | ] 85 | } 86 | ``` -------------------------------------------------------------------------------- /examples/schemas/document.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "document", 3 | "fields": [ 4 | { 5 | "name": "id", 6 | "type": "bigserial", 7 | "options": [ 8 | "primary key" 9 | ] 10 | }, 11 | { 12 | "name": "document_mpid", 13 | "type": "bigint", 14 | "options": [ 15 | "not null" 16 | ] 17 | }, 18 | { 19 | "name": "merchant_id", 20 | "type": "bigint" 21 | }, 22 | { 23 | "name": "transaction_id", 24 | "type": "bigint" 25 | }, 26 | { 27 | "name": "marketplace_id", 28 | "type": "smallint", 29 | "options": [ 30 | "not null" 31 | ] 32 | }, 33 | { 34 | "name": "number", 35 | "type": "varchar", 36 | "limit": 200, 37 | "options": [ 38 | "not null" 39 | ] 40 | }, 41 | { 42 | "name": "type", 43 | "type": "varchar", 44 | "limit": 50, 45 | "options": [ 46 | "not null" 47 | ] 48 | }, 49 | { 50 | "name": "created_at", 51 | "type": "timestamp", 52 | "options": [ 53 | "not null" 54 | ] 55 | }, 56 | { 57 | "name": "updated_at", 58 | "type": "timestamp", 59 | "options": [ 60 | "not null" 61 | ] 62 | }, 63 | { 64 | "name": "deleted_at", 65 | "type": "timestamp" 66 | } 67 | ] 68 | } 69 | -------------------------------------------------------------------------------- /examples/schemas/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "fields": [ 4 | { 5 | "name": "id", 6 | "type": "bigserial", 7 | "options": [ 8 | "primary key" 9 | ] 10 | }, 11 | { 12 | "name": "name", 13 | "type": "varchar", 14 | "limit": 50, 15 | "options": [ 16 | "not null" 17 | ] 18 | } 19 | ], 20 | "indexes": [ 21 | { 22 | "name": "index_example_on_name", 23 | "fields": [ 24 | { 25 | "column": "name" 26 | } 27 | ], 28 | "unique": true 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /examples/schemas/school.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "school", 3 | "fields": [ 4 | { 5 | "name": "id", 6 | "type": "bigserial", 7 | "options": [ 8 | "primary key" 9 | ] 10 | }, 11 | { 12 | "name": "marketplace_id", 13 | "type": "smallint", 14 | "options": [ 15 | "not null" 16 | ] 17 | }, 18 | { 19 | "name": "is_suspended", 20 | "type": "bool", 21 | "options": [ 22 | "not null" 23 | ] 24 | }, 25 | { 26 | "name": "ip_address", 27 | "type": "varchar", 28 | "limit": 45, 29 | "options": [ 30 | "NOT NULL" 31 | ] 32 | }, 33 | { 34 | "name": "user_agent", 35 | "type": "varchar", 36 | "limit": 255, 37 | "options": [ 38 | "not null" 39 | ] 40 | }, 41 | { 42 | "name": "device_platform", 43 | "type": "varchar", 44 | "limit": 255, 45 | "options": [ 46 | "not null" 47 | ] 48 | }, 49 | { 50 | "name": "device_model", 51 | "type": "varchar", 52 | "limit": 255, 53 | "options": [ 54 | "not null" 55 | ] 56 | }, 57 | { 58 | "name": "device_browser", 59 | "type": "varchar", 60 | "limit": 255, 61 | "options": [ 62 | "not null" 63 | ] 64 | }, 65 | { 66 | "name": "created_at", 67 | "type": "timestamp", 68 | "options": [ 69 | "not null" 70 | ] 71 | }, 72 | { 73 | "name": "updated_at", 74 | "type": "timestamp", 75 | "options": [ 76 | "not null" 77 | ] 78 | }, 79 | { 80 | "name": "deleted_at", 81 | "type": "timestamp" 82 | } 83 | ], 84 | "indexes": [ 85 | { 86 | "name": "index_school_on_marketplace_id", 87 | "fields": [ 88 | { 89 | "column": "marketplace_id" 90 | } 91 | ] 92 | } 93 | ] 94 | } 95 | -------------------------------------------------------------------------------- /examples/schemas/user.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "user", 3 | "fields": [ 4 | { 5 | "name": "id", 6 | "type": "bigserial", 7 | "options": [ 8 | "primary key" 9 | ] 10 | }, 11 | { 12 | "name": "email", 13 | "type": "varchar", 14 | "limit": 200, 15 | "options": [ 16 | "not null" 17 | ] 18 | }, 19 | { 20 | "name": "name", 21 | "type": "varchar", 22 | "limit": 200, 23 | "options": [ 24 | "not null" 25 | ] 26 | }, 27 | { 28 | "name": "created_at", 29 | "type": "timestamp", 30 | "options": [ 31 | "nullable" 32 | ] 33 | }, 34 | { 35 | "name": "updated_at", 36 | "type": "timestamp" 37 | }, 38 | { 39 | "name": "deleted_at", 40 | "type": "timestamp" 41 | }, 42 | { 43 | "name": "location", 44 | "type": "varchar", 45 | "limit": 200, 46 | "options": [ 47 | "not null" 48 | ] 49 | } 50 | ], 51 | "indexes": [ 52 | { 53 | "name": "index_user_on_email", 54 | "fields": [ 55 | { 56 | "column": "email" 57 | } 58 | ], 59 | "unique": true, 60 | "order": "ASC" 61 | }, 62 | { 63 | "name": "index_user_on_name_and_email", 64 | "fields": [ 65 | { 66 | "column": "name", 67 | "order": "ASC" 68 | }, 69 | { 70 | "column": "email", 71 | "order": "ASC" 72 | }, 73 | { 74 | "column": "location", 75 | "order": "ASC" 76 | } 77 | ] 78 | } 79 | ] 80 | } 81 | -------------------------------------------------------------------------------- /generator/code.go: -------------------------------------------------------------------------------- 1 | package generator 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | 7 | "gitlab.com/wartek-id/core/tools/dbgen/config" 8 | 9 | goqu "github.com/doug-martin/goqu/v9" 10 | _ "github.com/doug-martin/goqu/v9/dialect/postgres" 11 | "github.com/fatih/color" 12 | "github.com/iancoleman/strcase" 13 | ) 14 | 15 | func GenerateQueries(schemas []*config.Schema) []*config.Function { 16 | dialect := goqu.Dialect("postgres") 17 | var function []*config.Function 18 | for _, element := range schemas { 19 | fmt.Printf("🚀 Generating query for table: %s\n", element.Name) 20 | for _, f := range GenerateSelectQueryByIndex(dialect, element) { 21 | function = append(function, f) 22 | } 23 | function = append(function, GenerateUpdateQuery(dialect, element)) 24 | function = append(function, GenerateInsertQuery(dialect, element)) 25 | sql := GenerateDeleteQuery(dialect, element) 26 | if sql != nil { 27 | function = append(function, sql) 28 | } 29 | 30 | sql = GenerateRestoreQuery(dialect, element) 31 | if sql != nil { 32 | function = append(function, sql) 33 | } 34 | 35 | sql, err := GenerateDestroyQuery(dialect, element) 36 | if err != nil { 37 | fmt.Println(color.RedString("Fail creating query for table: %s\n", element.Name)) 38 | panic(err) 39 | } 40 | function = append(function, sql) 41 | fmt.Println(color.GreenString("Succeeded")) 42 | } 43 | return function 44 | } 45 | 46 | func GenerateSelectQuery(dialect goqu.DialectWrapper, element *config.Schema, index goqu.Ex) (string, error) { 47 | ds := dialect.From(element.Name).Where(index) 48 | sql, _, err := ds.Prepared(true).ToSQL() 49 | if err != nil { 50 | return "", err 51 | } 52 | 53 | return sql, nil 54 | } 55 | 56 | func GenerateInsertQuery(dialect goqu.DialectWrapper, element *config.Schema) *config.Function { 57 | var cols []interface{} 58 | var vals []interface{} 59 | for i, e := range element.Fields { 60 | if e.Name == "id" || e.Name == "deleted_at" || e.Name == "updated_at" { 61 | continue 62 | } 63 | cols = append(cols, e.Name) 64 | vals = append(vals, "$"+strconv.Itoa(i+1)) 65 | } 66 | 67 | ds := dialect.Insert(element.Name).Cols(cols...).Vals(vals) 68 | sql, _, _ := ds.Prepared(true).ToSQL() 69 | functionName := GenerateFunctionName("create_"+element.Name, "") 70 | return &config.Function{ 71 | Name: functionName, 72 | Query: sql, 73 | TableName: element.Name, 74 | SqlcType: ":exec", 75 | } 76 | } 77 | 78 | func GenerateUpdateQuery(dialect goqu.DialectWrapper, element *config.Schema) *config.Function { 79 | filter := goqu.And(goqu.Ex{"id": "1"}) 80 | cols := goqu.Record{} 81 | for _, e := range element.Fields { 82 | if e.Name == "created_at" || e.Name == "id" { 83 | continue 84 | } else if e.Name == "deleted_at" { 85 | filter = filter.Append(goqu.I("deleted_at").IsNull()) 86 | continue 87 | } 88 | cols[e.Name] = e.Name 89 | } 90 | 91 | ds := dialect.Update(element.Name).Where(filter).Set(cols) 92 | sql, _, _ := ds.Prepared(true).ToSQL() 93 | functionName := GenerateFunctionName("update_"+element.Name, "") 94 | return &config.Function{ 95 | Name: functionName, 96 | Query: sql, 97 | TableName: element.Name, 98 | SqlcType: ":exec", 99 | } 100 | } 101 | 102 | func GenerateDestroyQuery(dialect goqu.DialectWrapper, element *config.Schema) (*config.Function, error) { 103 | ds := dialect.Delete(element.Name).Where(goqu.Ex{"id": "$1"}) 104 | sql, _, err := ds.Prepared(true).ToSQL() 105 | if err != nil { 106 | return nil, err 107 | } 108 | 109 | functionName := GenerateFunctionName("destroy_"+element.Name, "") 110 | return &config.Function{ 111 | Name: functionName, 112 | Query: sql, 113 | TableName: element.Name, 114 | SqlcType: ":exec", 115 | }, nil 116 | } 117 | 118 | func GenerateDeleteQuery(dialect goqu.DialectWrapper, element *config.Schema) *config.Function { 119 | filter := goqu.And(goqu.Ex{"id": "1"}) 120 | for _, f := range element.Fields { 121 | if f.Name == "deleted_at" { 122 | filter = filter.Append(goqu.I("deleted_at").IsNull()) 123 | ds := dialect.Update(element.Name).Where(filter).Set(goqu.Record{"deleted_at": goqu.L("NOW()")}) 124 | sql, _, _ := ds.Prepared(true).ToSQL() 125 | 126 | functionName := GenerateFunctionName("delete_"+element.Name, "") 127 | return &config.Function{ 128 | Name: functionName, 129 | Query: sql, 130 | TableName: element.Name, 131 | SqlcType: ":exec", 132 | } 133 | } 134 | } 135 | return nil 136 | } 137 | 138 | func GenerateRestoreQuery(dialect goqu.DialectWrapper, element *config.Schema) *config.Function { 139 | filter := goqu.And(goqu.Ex{"id": "1"}) 140 | for _, f := range element.Fields { 141 | if f.Name == "deleted_at" { 142 | filter = filter.Append(goqu.I("deleted_at").IsNotNull()) 143 | ds := dialect.Update(element.Name).Where(filter).Set(goqu.Record{"deleted_at": goqu.L("NULL")}) 144 | sql, _, _ := ds.Prepared(true).ToSQL() 145 | 146 | functionName := GenerateFunctionName("restore_"+element.Name, "") 147 | return &config.Function{ 148 | Name: functionName, 149 | Query: sql, 150 | TableName: element.Name, 151 | SqlcType: ":exec", 152 | } 153 | } 154 | } 155 | return nil 156 | } 157 | 158 | func GenerateSelectQueryByIndex(dialect goqu.DialectWrapper, element *config.Schema) []*config.Function { 159 | // Initialize FindById 160 | index := goqu.Ex{"id": "1"} 161 | var function []*config.Function 162 | sql, err := GenerateSelectQuery(dialect, element, index) 163 | if err != nil { 164 | panic(err) 165 | } 166 | functionName := GenerateFunctionName("find_"+element.Name, "_id") 167 | function = append(function, config.ParseFunction(functionName, sql, element.Name, ":one")) 168 | 169 | // Initialize Find with indexes 170 | for _, e := range element.Index { 171 | m := goqu.Ex{} 172 | cnt := len(e.Fields) 173 | fields := "" 174 | for i, field := range e.Fields { 175 | m[field.Column] = "random" 176 | fields += "_" + field.Column 177 | if i < cnt-1 { 178 | fields += "_and" 179 | } 180 | } 181 | index = goqu.Ex(m) 182 | sql, err = GenerateSelectQuery(dialect, element, index) 183 | if err != nil { 184 | panic(err) 185 | } 186 | functionName := GenerateFunctionName("find_"+element.Name, fields) 187 | 188 | function = append(function, config.ParseFunction(functionName, sql, element.Name, ":many")) 189 | } 190 | return function 191 | } 192 | 193 | func GenerateFunctionName(actionName string, fields string) string { 194 | if fields != "" { 195 | actionName += "_by" + fields 196 | } 197 | return strcase.ToCamel(actionName) 198 | } 199 | -------------------------------------------------------------------------------- /generator/code_test.go: -------------------------------------------------------------------------------- 1 | package generator_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/doug-martin/goqu/v9" 7 | "github.com/stretchr/testify/assert" 8 | "gitlab.com/wartek-id/core/tools/dbgen/config" 9 | "gitlab.com/wartek-id/core/tools/dbgen/generator" 10 | ) 11 | 12 | func TestGenerateSelectQuery(t *testing.T) { 13 | dialect := goqu.Dialect("postgres") 14 | element := &config.Schema{ 15 | Name: "user", 16 | } 17 | idx := goqu.Ex{"id": "1"} 18 | ds := dialect.From(element.Name).Where(idx) 19 | sql, _, _ := ds.Prepared(true).ToSQL() 20 | 21 | res, err := generator.GenerateSelectQuery(dialect, element, idx) 22 | assert.Nil(t, err) 23 | assert.Equal(t, res, sql) 24 | } 25 | 26 | func TestGenerateInsertQuery(t *testing.T) { 27 | dialect := goqu.Dialect("postgres") 28 | element := &config.Schema{ 29 | Name: "user", 30 | Fields: []*config.Field{ 31 | { 32 | Name: "name", 33 | }, { 34 | Name: "id", 35 | }, 36 | }, 37 | } 38 | tc := &config.Function{ 39 | Name: "CreateUser", 40 | Query: "INSERT INTO \"user\" (\"name\") VALUES ($1)", 41 | TableName: "user", 42 | SqlcType: ":exec", 43 | } 44 | res := generator.GenerateInsertQuery(dialect, element) 45 | assert.Equal(t, res, tc) 46 | } 47 | 48 | func TestGenerateUpdateQuery(t *testing.T) { 49 | dialect := goqu.Dialect("postgres") 50 | element := &config.Schema{ 51 | Name: "user", 52 | Fields: []*config.Field{ 53 | { 54 | Name: "name", 55 | }, 56 | { 57 | Name: "deleted_at", 58 | }, 59 | { 60 | Name: "id", 61 | }, 62 | }, 63 | } 64 | tc := &config.Function{ 65 | Name: "UpdateUser", 66 | Query: "UPDATE \"user\" SET \"name\"=$1 WHERE ((\"id\" = $2) AND (\"deleted_at\" IS NULL))", 67 | TableName: "user", 68 | SqlcType: ":exec", 69 | } 70 | res := generator.GenerateUpdateQuery(dialect, element) 71 | assert.Equal(t, res, tc) 72 | } 73 | 74 | func TestGenerateDestroyQuery(t *testing.T) { 75 | dialect := goqu.Dialect("postgres") 76 | element := &config.Schema{ 77 | Name: "user", 78 | Fields: []*config.Field{ 79 | { 80 | Name: "name", 81 | }, 82 | }, 83 | } 84 | tc := &config.Function{ 85 | Name: "DestroyUser", 86 | Query: "DELETE FROM \"user\" WHERE (\"id\" = $1)", 87 | TableName: "user", 88 | SqlcType: ":exec", 89 | } 90 | res, err := generator.GenerateDestroyQuery(dialect, element) 91 | assert.Nil(t, err) 92 | assert.Equal(t, res, tc) 93 | } 94 | 95 | func TestGenerateDeleteQuery(t *testing.T) { 96 | dialect := goqu.Dialect("postgres") 97 | element := &config.Schema{ 98 | Name: "user", 99 | Fields: []*config.Field{ 100 | { 101 | Name: "name", 102 | }, 103 | { 104 | Name: "deleted_at", 105 | }, 106 | }, 107 | } 108 | tc := &config.Function{ 109 | Name: "DeleteUser", 110 | Query: "UPDATE \"user\" SET \"deleted_at\"=NOW() WHERE ((\"id\" = $1) AND (\"deleted_at\" IS NULL))", 111 | TableName: "user", 112 | SqlcType: ":exec", 113 | } 114 | res := generator.GenerateDeleteQuery(dialect, element) 115 | assert.Equal(t, res, tc) 116 | } 117 | 118 | func TestGenerateDeleteQuery_ReturnNil(t *testing.T) { 119 | dialect := goqu.Dialect("postgres") 120 | element := &config.Schema{ 121 | Name: "user", 122 | Fields: []*config.Field{ 123 | { 124 | Name: "name", 125 | }, 126 | }, 127 | } 128 | res := generator.GenerateDeleteQuery(dialect, element) 129 | assert.Nil(t, res) 130 | } 131 | 132 | func TestGenerateSelectQueryByIndex(t *testing.T) { 133 | dialect := goqu.Dialect("postgres") 134 | element := &config.Schema{ 135 | Name: "user", 136 | Fields: []*config.Field{ 137 | { 138 | Name: "name", 139 | }, 140 | }, 141 | Index: []*config.Index{ 142 | { 143 | Name: "idx_name_and_email", 144 | Fields: []*config.IndexField{ 145 | { 146 | Column: "name", 147 | Order: "ASC", 148 | }, 149 | { 150 | Column: "email", 151 | Order: "ASC", 152 | }, 153 | }, 154 | }, 155 | }, 156 | } 157 | tc := []*config.Function{ 158 | { 159 | Name: "FindUserById", 160 | Query: "SELECT * FROM \"user\" WHERE (\"id\" = $1)", 161 | TableName: "user", 162 | SqlcType: ":one", 163 | }, 164 | { 165 | Name: "FindUserByNameAndEmail", 166 | Query: "SELECT * FROM \"user\" WHERE ((\"email\" = $1) AND (\"name\" = $2))", 167 | TableName: "user", 168 | SqlcType: ":many", 169 | }, 170 | } 171 | res := generator.GenerateSelectQueryByIndex(dialect, element) 172 | assert.Equal(t, res, tc) 173 | } 174 | 175 | func TestGenerateFunctionName(t *testing.T) { 176 | tc := "FindById" 177 | res := generator.GenerateFunctionName("find", "_id") 178 | assert.Equal(t, res, tc) 179 | } 180 | 181 | func TestGenerateQueries(t *testing.T) { 182 | element := []*config.Schema{ 183 | { 184 | Name: "user", 185 | Fields: []*config.Field{ 186 | { 187 | Name: "name", 188 | }, 189 | { 190 | Name: "deleted_at", 191 | }, 192 | }, 193 | Index: []*config.Index{ 194 | { 195 | Name: "idx_name_and_email", 196 | Fields: []*config.IndexField{ 197 | { 198 | Column: "name", 199 | Order: "ASC", 200 | }, 201 | { 202 | Column: "email", 203 | Order: "ASC", 204 | }, 205 | }, 206 | }, 207 | }, 208 | }, 209 | } 210 | tc := []*config.Function{ 211 | { 212 | Name: "FindUserById", 213 | Query: "SELECT * FROM \"user\" WHERE (\"id\" = $1)", 214 | TableName: "user", 215 | SqlcType: ":one", 216 | }, 217 | { 218 | Name: "FindUserByNameAndEmail", 219 | Query: "SELECT * FROM \"user\" WHERE ((\"email\" = $1) AND (\"name\" = $2))", 220 | TableName: "user", 221 | SqlcType: ":many", 222 | }, 223 | { 224 | Name: "UpdateUser", 225 | Query: "UPDATE \"user\" SET \"name\"=$1 WHERE ((\"id\" = $2) AND (\"deleted_at\" IS NULL))", 226 | TableName: "user", 227 | SqlcType: ":exec", 228 | }, 229 | { 230 | Name: "CreateUser", 231 | Query: "INSERT INTO \"user\" (\"name\") VALUES ($1)", 232 | TableName: "user", 233 | SqlcType: ":exec", 234 | }, 235 | { 236 | Name: "DeleteUser", 237 | Query: "UPDATE \"user\" SET \"deleted_at\"=NOW() WHERE ((\"id\" = $1) AND (\"deleted_at\" IS NULL))", 238 | TableName: "user", 239 | SqlcType: ":exec", 240 | }, 241 | { 242 | Name: "RestoreUser", 243 | Query: "UPDATE \"user\" SET \"deleted_at\"=NULL WHERE ((\"id\" = $1) AND (\"deleted_at\" IS NOT NULL))", 244 | TableName: "user", 245 | SqlcType: ":exec", 246 | }, 247 | { 248 | Name: "DestroyUser", 249 | Query: "DELETE FROM \"user\" WHERE (\"id\" = $1)", 250 | TableName: "user", 251 | SqlcType: ":exec", 252 | }, 253 | } 254 | res := generator.GenerateQueries(element) 255 | assert.Equal(t, res, tc) 256 | } 257 | 258 | func TestGenerateRestoreQuery(t *testing.T) { 259 | dialect := goqu.Dialect("postgres") 260 | element := &config.Schema{ 261 | Name: "user", 262 | Fields: []*config.Field{ 263 | { 264 | Name: "name", 265 | }, 266 | { 267 | Name: "deleted_at", 268 | }, 269 | }, 270 | } 271 | tc := &config.Function{ 272 | Name: "RestoreUser", 273 | Query: "UPDATE \"user\" SET \"deleted_at\"=NULL WHERE ((\"id\" = $1) AND (\"deleted_at\" IS NOT NULL))", 274 | TableName: "user", 275 | SqlcType: ":exec", 276 | } 277 | res := generator.GenerateRestoreQuery(dialect, element) 278 | assert.Equal(t, res, tc) 279 | } 280 | 281 | func TestGenerateRestoreQuery_ReturnNil(t *testing.T) { 282 | dialect := goqu.Dialect("postgres") 283 | element := &config.Schema{ 284 | Name: "user", 285 | Fields: []*config.Field{ 286 | { 287 | Name: "name", 288 | }, 289 | }, 290 | } 291 | res := generator.GenerateRestoreQuery(dialect, element) 292 | assert.Nil(t, res) 293 | } 294 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module gitlab.com/wartek-id/core/tools/dbgen 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/AlecAivazis/survey/v2 v2.3.4 7 | github.com/doug-martin/goqu/v9 v9.18.0 8 | github.com/fatih/color v1.13.0 9 | github.com/golang/mock v1.6.0 10 | github.com/google/go-cmp v0.5.7 11 | github.com/iancoleman/strcase v0.2.0 12 | github.com/jackc/pgconn v1.12.0 13 | github.com/jackc/pgx/v4 v4.16.0 14 | github.com/pashagolub/pgxmock v1.4.4 15 | github.com/pganalyze/pg_query_go/v2 v2.1.0 16 | github.com/pkg/errors v0.9.1 17 | github.com/spf13/cobra v1.4.0 18 | github.com/stretchr/testify v1.7.1 19 | ) 20 | 21 | require ( 22 | github.com/davecgh/go-spew v1.1.1 // indirect 23 | github.com/golang/protobuf v1.5.2 // indirect 24 | github.com/inconshreveable/mousetrap v1.0.0 // indirect 25 | github.com/jackc/chunkreader/v2 v2.0.1 // indirect 26 | github.com/jackc/pgio v1.0.0 // indirect 27 | github.com/jackc/pgpassfile v1.0.0 // indirect 28 | github.com/jackc/pgproto3/v2 v2.3.0 // indirect 29 | github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect 30 | github.com/jackc/pgtype v1.11.0 // indirect 31 | github.com/jackc/puddle v1.2.1 // indirect 32 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect 33 | github.com/kr/pretty v0.2.1 // indirect 34 | github.com/lib/pq v1.10.4 // indirect 35 | github.com/mattn/go-colorable v0.1.12 // indirect 36 | github.com/mattn/go-isatty v0.0.14 // indirect 37 | github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect 38 | github.com/pmezard/go-difflib v1.0.0 // indirect 39 | github.com/spf13/pflag v1.0.5 // indirect 40 | golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 // indirect 41 | golang.org/x/sys v0.0.0-20220422013727-9388b58f7150 // indirect 42 | golang.org/x/term v0.0.0-20210503060354-a79de5458b56 // indirect 43 | golang.org/x/text v0.3.7 // indirect 44 | google.golang.org/protobuf v1.28.0 // indirect 45 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect 46 | ) 47 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/AlecAivazis/survey/v2 v2.3.4 h1:pchTU9rsLUSvWEl2Aq9Pv3k0IE2fkqtGxazskAMd9Ng= 2 | github.com/AlecAivazis/survey/v2 v2.3.4/go.mod h1:hrV6Y/kQCLhIZXGcriDCUBtB3wnN7156gMXJ3+b23xM= 3 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 4 | github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60= 5 | github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= 6 | github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= 7 | github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s= 8 | github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w= 9 | github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= 10 | github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= 11 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 12 | github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 13 | github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 14 | github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= 15 | github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI= 16 | github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= 17 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 18 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 19 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 20 | github.com/denisenkom/go-mssqldb v0.10.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= 21 | github.com/doug-martin/goqu/v9 v9.18.0 h1:/6bcuEtAe6nsSMVK/M+fOiXUNfyFF3yYtE07DBPFMYY= 22 | github.com/doug-martin/goqu/v9 v9.18.0/go.mod h1:nf0Wc2/hV3gYK9LiyqIrzBEVGlI8qW3GuDCEobC4wBQ= 23 | github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= 24 | github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= 25 | github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= 26 | github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= 27 | github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= 28 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 29 | github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw= 30 | github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= 31 | github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= 32 | github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= 33 | github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= 34 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 35 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 36 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 37 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 38 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 39 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 40 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 41 | github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= 42 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 43 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 44 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 45 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 46 | github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 47 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 48 | github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o= 49 | github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= 50 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 51 | github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog= 52 | github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68= 53 | github.com/iancoleman/strcase v0.2.0 h1:05I4QRnGpI0m37iZQRuskXh+w77mr6Z41lwQzuHLwW0= 54 | github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= 55 | github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= 56 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 57 | github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= 58 | github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= 59 | github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= 60 | github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= 61 | github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= 62 | github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= 63 | github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= 64 | github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= 65 | github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY= 66 | github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= 67 | github.com/jackc/pgconn v1.12.0 h1:/RvQ24k3TnNdfBSW0ou9EOi5jx2cX7zfE8n2nLKuiP0= 68 | github.com/jackc/pgconn v1.12.0/go.mod h1:ZkhRC59Llhrq3oSfrikvwQ5NaxYExr6twkdkMLaKono= 69 | github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= 70 | github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= 71 | github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= 72 | github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c= 73 | github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc= 74 | github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak= 75 | github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= 76 | github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= 77 | github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= 78 | github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= 79 | github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= 80 | github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= 81 | github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= 82 | github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= 83 | github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= 84 | github.com/jackc/pgproto3/v2 v2.3.0 h1:brH0pCGBDkBW07HWlN/oSBXrmo3WB0UvZd1pIuDcL8Y= 85 | github.com/jackc/pgproto3/v2 v2.3.0/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= 86 | github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg= 87 | github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= 88 | github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= 89 | github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= 90 | github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= 91 | github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM= 92 | github.com/jackc/pgtype v1.11.0 h1:u4uiGPz/1hryuXzyaBhSk6dnIyyG2683olG2OV+UUgs= 93 | github.com/jackc/pgtype v1.11.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= 94 | github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= 95 | github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= 96 | github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= 97 | github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= 98 | github.com/jackc/pgx/v4 v4.16.0 h1:4k1tROTJctHotannFYzu77dY3bgtMRymQP7tXQjqpPk= 99 | github.com/jackc/pgx/v4 v4.16.0/go.mod h1:N0A9sFdWzkw/Jy1lwoiB64F2+ugFZi987zRxcPez/wI= 100 | github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 101 | github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 102 | github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 103 | github.com/jackc/puddle v1.2.1 h1:gI8os0wpRXFd4FiAY2dWiqRK037tjj3t7rKFeO4X5iw= 104 | github.com/jackc/puddle v1.2.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 105 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= 106 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= 107 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 108 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 109 | github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 110 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 111 | github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= 112 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 113 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 114 | github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= 115 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 116 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 117 | github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 118 | github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 119 | github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 120 | github.com/lib/pq v1.10.1/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 121 | github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 122 | github.com/lib/pq v1.10.4 h1:SO9z7FRPzA03QhHKJrH5BXA6HU1rS4V2nIVrrNC1iYk= 123 | github.com/lib/pq v1.10.4/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 124 | github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= 125 | github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 126 | github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 127 | github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 128 | github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= 129 | github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= 130 | github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 131 | github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 132 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 133 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 134 | github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= 135 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 136 | github.com/mattn/go-sqlite3 v1.14.7/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= 137 | github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4= 138 | github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= 139 | github.com/pashagolub/pgxmock v1.4.4 h1:g9d6q9YK95I0QQYq6x0j2sibVct5rpJKSdO2IQVg3gc= 140 | github.com/pashagolub/pgxmock v1.4.4/go.mod h1:D9PsCahVzAfYtaWRR3rHmXfXcCWd0ypOS/uMmt3DVZs= 141 | github.com/pganalyze/pg_query_go/v2 v2.1.0 h1:donwPZ4G/X+kMs7j5eYtKjdziqyOLVp3pkUrzb9lDl8= 142 | github.com/pganalyze/pg_query_go/v2 v2.1.0/go.mod h1:XAxmVqz1tEGqizcQ3YSdN90vCOHBWjJi8URL1er5+cA= 143 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 144 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 145 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 146 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 147 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 148 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 149 | github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= 150 | github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= 151 | github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= 152 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 153 | github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= 154 | github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= 155 | github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= 156 | github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= 157 | github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= 158 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 159 | github.com/spf13/cobra v1.4.0 h1:y+wJpx64xcgO1V+RcnwW0LEHxTKRi2ZDPSBjWnrg88Q= 160 | github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g= 161 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 162 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 163 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 164 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 165 | github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48= 166 | github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= 167 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 168 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 169 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 170 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 171 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 172 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 173 | github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= 174 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 175 | github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 176 | github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= 177 | go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 178 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 179 | go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 180 | go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 181 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 182 | go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= 183 | go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= 184 | go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= 185 | go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 186 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 187 | go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= 188 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 189 | golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 190 | golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= 191 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 192 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 193 | golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 194 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 195 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 196 | golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= 197 | golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 198 | golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 199 | golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 h1:kUhD7nTDoI3fVd9G4ORWrbV5NY0liEs/Jg2pv5f+bBA= 200 | golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 201 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 202 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 203 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 204 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 205 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 206 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 207 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 208 | golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 209 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 210 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 211 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 212 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 213 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 214 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 215 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 216 | golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 217 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 218 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 219 | golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 220 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 221 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 222 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 223 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 224 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 225 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 226 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 227 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 228 | golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 229 | golang.org/x/sys v0.0.0-20220422013727-9388b58f7150 h1:xHms4gcpe1YE7A3yIllJXP16CMAGuqwO2lX1mTyyRRc= 230 | golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 231 | golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= 232 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 233 | golang.org/x/term v0.0.0-20210503060354-a79de5458b56 h1:b8jxX3zqjpqb2LklXPzKSGJhzyxCOZSz8ncv8Nv+y7w= 234 | golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY= 235 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 236 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 237 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 238 | golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 239 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 240 | golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= 241 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 242 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 243 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 244 | golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 245 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 246 | golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 247 | golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 248 | golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 249 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 250 | golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 251 | golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 252 | golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 253 | golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 254 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 255 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 256 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 257 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 258 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 259 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 260 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 261 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 262 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 263 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 264 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 265 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 266 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 267 | google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= 268 | google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 269 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 270 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 271 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 272 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 273 | gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= 274 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 275 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 276 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 277 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= 278 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 279 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 280 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/spf13/cobra" 7 | "gitlab.com/wartek-id/core/tools/dbgen/command" 8 | ) 9 | 10 | var ( 11 | packageName string 12 | rootCmd = &cobra.Command{ 13 | Use: "dbgen", 14 | Short: "Wartool database generator", 15 | Version: release, 16 | } 17 | ) 18 | 19 | func init() { 20 | rootCmd.AddCommand(command.GenCode) 21 | rootCmd.AddCommand(command.GenMigration) 22 | rootCmd.AddCommand(command.DumpDbCmd) 23 | } 24 | 25 | func main() { 26 | if err := rootCmd.Execute(); err != nil { 27 | log.Fatal(err) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /sqlgen/alter_table_generator.go: -------------------------------------------------------------------------------- 1 | package sqlgen 2 | 3 | import ( 4 | "bytes" 5 | 6 | "gitlab.com/wartek-id/core/tools/dbgen/config" 7 | "gitlab.com/wartek-id/core/tools/dbgen/sqlgen/dialect" 8 | "gitlab.com/wartek-id/core/tools/dbgen/sqlgen/exp" 9 | "gitlab.com/wartek-id/core/tools/dbgen/sqlgen/sb" 10 | "gitlab.com/wartek-id/core/tools/dbgen/sqlgen/step" 11 | ) 12 | 13 | type AlterTableGenerator interface { 14 | Dialect() string 15 | DialectOptions() *dialect.DialectOption 16 | ExpressionSQLGenerator() exp.ExpressionSQLGenerator 17 | Generate(b sb.SQLBuilder, at *step.AlterSchema) error 18 | Rollback(b sb.SQLBuilder, at *step.AlterSchema) error 19 | } 20 | 21 | type alterTableGenerator struct { 22 | dialect string 23 | esg exp.ExpressionSQLGenerator 24 | dialectOptions *dialect.DialectOption 25 | } 26 | 27 | func NewAlterTableGenerator(dialect string, do *dialect.DialectOption) AlterTableGenerator { 28 | return &alterTableGenerator{ 29 | dialect: dialect, 30 | dialectOptions: do, 31 | esg: exp.NewExpressionSQLGenerator(dialect, do), 32 | } 33 | } 34 | 35 | func (atg *alterTableGenerator) Dialect() string { 36 | return atg.dialect 37 | } 38 | 39 | func (atg *alterTableGenerator) DialectOptions() *dialect.DialectOption { 40 | return atg.dialectOptions 41 | } 42 | 43 | func (atg *alterTableGenerator) ExpressionSQLGenerator() exp.ExpressionSQLGenerator { 44 | return atg.esg 45 | } 46 | 47 | func (atg *alterTableGenerator) Generate(b sb.SQLBuilder, at *step.AlterSchema) error { 48 | if !at.FieldChanged() { 49 | return nil 50 | } 51 | 52 | atg.alterTableTemplate(b, at.Name) 53 | queries := make([][]byte, 0) 54 | 55 | if at.IsColumnsAdded() { 56 | buf := sb.NewSQLBuilder() 57 | atg.generateColumns(buf, at.AddedColumns) 58 | queries = append(queries, buf.Bytes()) 59 | } 60 | 61 | if at.IsColumnsDropped() { 62 | buf := sb.NewSQLBuilder() 63 | atg.dropColumns(buf, at.DroppedColumns) 64 | queries = append(queries, buf.Bytes()) 65 | } 66 | 67 | if at.IsColumnsAltered() { 68 | buf := sb.NewSQLBuilder() 69 | atg.alterColumns(buf, at.AlteredColumns) 70 | queries = append(queries, buf.Bytes()) 71 | } 72 | 73 | b.Write(bytes.Join(queries, atg.dialectOptions.CommaNewLineFragment)) 74 | b.WriteRunes(atg.dialectOptions.SemiColonRune) 75 | return nil 76 | } 77 | 78 | func (atg *alterTableGenerator) generateColumns(b sb.SQLBuilder, fields []*config.Field) { 79 | for i, field := range fields { 80 | b.WriteRunes(atg.dialectOptions.TabRune) 81 | b.Write(atg.dialectOptions.AddColumnTemplate()) 82 | atg.ExpressionSQLGenerator().LiteralExpression(b, field.Name) 83 | b.WriteRunes(atg.dialectOptions.SpaceRune) 84 | b.Write(atg.ExpressionSQLGenerator().GetTypeFragment(field)) 85 | b.Write(atg.ExpressionSQLGenerator().GetOptionsFragment(field)) 86 | 87 | if i != len(fields)-1 { 88 | b.WriteRunes(atg.dialectOptions.CommaRune) 89 | b.WriteRunes(atg.dialectOptions.NewLineRune) 90 | } 91 | } 92 | } 93 | 94 | func (atg *alterTableGenerator) dropColumns(b sb.SQLBuilder, fields []*config.Field) { 95 | for i, field := range fields { 96 | b.WriteRunes(atg.dialectOptions.TabRune) 97 | b.Write(atg.dialectOptions.DropColumnTemplate()) 98 | atg.ExpressionSQLGenerator().LiteralExpression(b, field.Name) 99 | 100 | if i != len(fields)-1 { 101 | b.WriteRunes(atg.dialectOptions.CommaRune) 102 | b.WriteRunes(atg.dialectOptions.NewLineRune) 103 | } 104 | } 105 | } 106 | 107 | func (atg *alterTableGenerator) alterColumns(b sb.SQLBuilder, fields []*step.AlterColumn) { 108 | for i, field := range fields { 109 | atg.alterColumn(b, field) 110 | if i != len(fields)-1 { 111 | b.Write(atg.dialectOptions.CommaNewLineFragment) 112 | } 113 | } 114 | } 115 | 116 | func (atg *alterTableGenerator) alterColumn(b sb.SQLBuilder, field *step.AlterColumn) { 117 | changes := [][]byte{} 118 | if field.ChangedType { 119 | buf := sb.NewSQLBuilder() 120 | atg.changeColumnType(buf, field.Field) 121 | changes = append(changes, buf.Bytes()) 122 | } 123 | 124 | if field.IsOptionsChanged() { 125 | buf := sb.NewSQLBuilder() 126 | atg.ChangeColumnOptions(buf, field) 127 | changes = append(changes, buf.Bytes()) 128 | } 129 | 130 | if field.ChangedDefaultValue { 131 | buf := sb.NewSQLBuilder() 132 | atg.changeColumnDefault(buf, field.Field) 133 | changes = append(changes, buf.Bytes()) 134 | } 135 | 136 | b.Write(bytes.Join(changes, atg.dialectOptions.CommaNewLineFragment)) 137 | } 138 | 139 | func (atg *alterTableGenerator) changeColumnType(b sb.SQLBuilder, field *config.Field) { 140 | atg.alterColumnTemplate(b, field.Name) 141 | b.Write(atg.dialectOptions.SetFragment) 142 | b.Write(atg.dialectOptions.DataTypeFragment) 143 | b.Write(atg.ExpressionSQLGenerator().GetTypeFragment(field)) 144 | } 145 | 146 | func (atg *alterTableGenerator) changeColumnDefault(b sb.SQLBuilder, field *config.Field) { 147 | atg.alterColumnTemplate(b, field.Name) 148 | b.Write(atg.dialectOptions.SetFragment) 149 | b.Write(atg.dialectOptions.DefaultFragment) 150 | b.Write(atg.ExpressionSQLGenerator().GetDefaultValue(field.Default)) 151 | } 152 | 153 | func (atg *alterTableGenerator) ChangeColumnOptions(b sb.SQLBuilder, field *step.AlterColumn) { 154 | for _, option := range field.ChangedOptions { 155 | switch option { 156 | case step.SetNotNull: 157 | atg.setNotNull(b, field.Name) 158 | case step.DropNotNull: 159 | atg.dropNotNull(b, field.Name) 160 | } 161 | } 162 | } 163 | 164 | func (atg *alterTableGenerator) Rollback(b sb.SQLBuilder, at *step.AlterSchema) error { 165 | atg.alterTableTemplate(b, at.Name) 166 | queries := make([][]byte, 0) 167 | 168 | if at.IsColumnsAdded() { 169 | buf := sb.NewSQLBuilder() 170 | atg.dropColumns(buf, at.AddedColumns) 171 | queries = append(queries, buf.Bytes()) 172 | } 173 | 174 | if at.IsColumnsDropped() { 175 | buf := sb.NewSQLBuilder() 176 | atg.generateColumns(buf, at.DroppedColumns) 177 | queries = append(queries, buf.Bytes()) 178 | } 179 | 180 | if at.IsColumnsAltered() { 181 | buf := sb.NewSQLBuilder() 182 | atg.rollbackAlterColumns(buf, at.AlteredColumns) 183 | queries = append(queries, buf.Bytes()) 184 | } 185 | 186 | b.Write(bytes.Join(queries, atg.dialectOptions.CommaNewLineFragment)) 187 | b.WriteRunes(atg.dialectOptions.SemiColonRune) 188 | return nil 189 | } 190 | 191 | func (atg *alterTableGenerator) rollbackAlterColumns(b sb.SQLBuilder, fields []*step.AlterColumn) { 192 | for i, field := range fields { 193 | atg.rollbackAlterColumn(b, field) 194 | if i != len(fields)-1 { 195 | b.Write(atg.dialectOptions.CommaNewLineFragment) 196 | } 197 | } 198 | } 199 | 200 | func (atg *alterTableGenerator) rollbackAlterColumn(b sb.SQLBuilder, field *step.AlterColumn) { 201 | changes := [][]byte{} 202 | if field.ChangedType { 203 | buf := sb.NewSQLBuilder() 204 | atg.changeColumnType(buf, field.LastField) 205 | changes = append(changes, buf.Bytes()) 206 | } 207 | 208 | if field.IsOptionsChanged() { 209 | buf := sb.NewSQLBuilder() 210 | atg.rollbackChangeColumnOptions(buf, field) 211 | changes = append(changes, buf.Bytes()) 212 | } 213 | 214 | if field.ChangedDefaultValue { 215 | buf := sb.NewSQLBuilder() 216 | atg.changeColumnDefault(buf, field.LastField) 217 | changes = append(changes, buf.Bytes()) 218 | } 219 | 220 | b.Write(bytes.Join(changes, atg.dialectOptions.CommaNewLineFragment)) 221 | } 222 | 223 | func (atg *alterTableGenerator) rollbackChangeColumnOptions(b sb.SQLBuilder, field *step.AlterColumn) { 224 | for _, option := range field.ChangedOptions { 225 | switch option { 226 | case step.SetNotNull: 227 | atg.dropNotNull(b, field.Name) 228 | case step.DropNotNull: 229 | atg.setNotNull(b, field.Name) 230 | } 231 | } 232 | } 233 | 234 | func (atg *alterTableGenerator) dropNotNull(b sb.SQLBuilder, name string) { 235 | atg.alterColumnTemplate(b, name) 236 | b.Write(atg.dialectOptions.DropFragment) 237 | b.Write(atg.dialectOptions.NotNullFragment) 238 | } 239 | 240 | func (atg *alterTableGenerator) setNotNull(b sb.SQLBuilder, name string) { 241 | atg.alterColumnTemplate(b, name) 242 | b.Write(atg.dialectOptions.SetFragment) 243 | b.Write(atg.dialectOptions.NotNullFragment) 244 | } 245 | 246 | func (atg *alterTableGenerator) alterColumnTemplate(b sb.SQLBuilder, name string) { 247 | b.WriteRunes(atg.dialectOptions.TabRune) 248 | b.Write(atg.dialectOptions.AlterColumnTemplate()) 249 | atg.ExpressionSQLGenerator().LiteralExpression(b, name) 250 | b.WriteRunes(atg.dialectOptions.SpaceRune) 251 | } 252 | 253 | func (atg *alterTableGenerator) alterTableTemplate(b sb.SQLBuilder, name string) { 254 | b.Write(atg.dialectOptions.AlterClause) 255 | b.Write(atg.dialectOptions.TableFragment) 256 | b.Write(atg.dialectOptions.IfExistsFragment) 257 | atg.ExpressionSQLGenerator().LiteralExpression(b, name) 258 | b.WriteRunes(atg.dialectOptions.NewLineRune) 259 | } 260 | -------------------------------------------------------------------------------- /sqlgen/alter_table_generator_test.go: -------------------------------------------------------------------------------- 1 | package sqlgen_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "gitlab.com/wartek-id/core/tools/dbgen/config" 9 | "gitlab.com/wartek-id/core/tools/dbgen/sqlgen" 10 | "gitlab.com/wartek-id/core/tools/dbgen/sqlgen/dialect" 11 | "gitlab.com/wartek-id/core/tools/dbgen/sqlgen/sb" 12 | "gitlab.com/wartek-id/core/tools/dbgen/sqlgen/step" 13 | "gitlab.com/wartek-id/core/tools/dbgen/types/field_option" 14 | "gitlab.com/wartek-id/core/tools/dbgen/types/field_type" 15 | ) 16 | 17 | func TestAlterTableGenerator_Dialect(t *testing.T) { 18 | dial := "postgres" 19 | do := dialect.DefaultDialectOption() 20 | 21 | sqlGen := sqlgen.NewAlterTableGenerator(dial, do) 22 | assert.Equal(t, dial, sqlGen.Dialect()) 23 | } 24 | 25 | func TestAlterTableGenerator_DialectOptions(t *testing.T) { 26 | dial := "postgres" 27 | do := dialect.DefaultDialectOption() 28 | 29 | sqlGen := sqlgen.NewAlterTableGenerator(dial, do) 30 | assert.Equal(t, do, sqlGen.DialectOptions()) 31 | } 32 | 33 | func TestAlterTableGenerator_ExpressionSQLGenerator(t *testing.T) { 34 | dial := "postgres" 35 | do := dialect.DefaultDialectOption() 36 | 37 | sqlGen := sqlgen.NewAlterTableGenerator(dial, do) 38 | assert.NotNil(t, sqlGen.ExpressionSQLGenerator()) 39 | } 40 | 41 | func TestAlterSchemaGenerator_Generate(t *testing.T) { 42 | alterStep := step.AlterSchema{ 43 | Name: "users", 44 | AddedColumns: []*config.Field{ 45 | { 46 | Name: "location", 47 | Type: field_type.Varchar, 48 | Limit: 50, 49 | }, 50 | { 51 | Name: "address", 52 | Type: field_type.Varchar, 53 | Limit: 100, 54 | Options: []field_option.FieldOption{field_option.NotNull}, 55 | }, 56 | }, 57 | DroppedColumns: []*config.Field{ 58 | { 59 | Name: "stock", 60 | }, 61 | { 62 | Name: "author", 63 | }, 64 | }, 65 | AlteredColumns: []*step.AlterColumn{ 66 | { 67 | Name: "name", 68 | Field: &config.Field{ 69 | Name: "name", 70 | Type: field_type.Varchar, 71 | Limit: 200, 72 | Default: "Alfred", 73 | Options: []field_option.FieldOption{ 74 | field_option.NotNull, 75 | }, 76 | }, 77 | ChangedType: true, 78 | ChangedDefaultValue: true, 79 | ChangedOptions: []step.OptionAction{ 80 | step.SetNotNull, 81 | }, 82 | }, 83 | { 84 | Name: "price", 85 | Field: &config.Field{ 86 | Name: "price", 87 | Type: field_type.BigInt, 88 | Default: 10000, 89 | Options: []field_option.FieldOption{ 90 | field_option.Nullable, 91 | }, 92 | }, 93 | ChangedType: true, 94 | ChangedDefaultValue: true, 95 | ChangedOptions: []step.OptionAction{ 96 | step.DropNotNull, 97 | }, 98 | }, 99 | }, 100 | } 101 | 102 | gen := sqlgen.NewAlterTableGenerator("postgres", dialect.DefaultDialectOption()) 103 | buf := sb.NewSQLBuilder() 104 | gen.Generate(buf, &alterStep) 105 | result := fmt.Sprintf("%s\n%s,\n%s,\n%s,\n%s,\n%s,\n%s;", 106 | "ALTER TABLE IF EXISTS \"users\"", 107 | "\tADD COLUMN \"location\" VARCHAR(50)", 108 | "\tADD COLUMN \"address\" VARCHAR(100) NOT NULL", 109 | "\tDROP COLUMN \"stock\"", 110 | "\tDROP COLUMN \"author\"", 111 | "\tALTER COLUMN \"name\" SET DATA TYPE VARCHAR(200),\n\tALTER COLUMN \"name\" SET NOT NULL,\n\tALTER COLUMN \"name\" SET DEFAULT 'Alfred'", 112 | "\tALTER COLUMN \"price\" SET DATA TYPE BIGINT,\n\tALTER COLUMN \"price\" DROP NOT NULL,\n\tALTER COLUMN \"price\" SET DEFAULT 10000", 113 | ) 114 | assert.Equal(t, result, buf.String()) 115 | } 116 | 117 | func TestAlterSchemaGenerator_Rollback(t *testing.T) { 118 | alterStep := step.AlterSchema{ 119 | Name: "users", 120 | AddedColumns: []*config.Field{ 121 | { 122 | Name: "location", 123 | Type: field_type.Varchar, 124 | Limit: 50, 125 | }, 126 | { 127 | Name: "address", 128 | Type: field_type.Varchar, 129 | Limit: 100, 130 | Options: []field_option.FieldOption{field_option.NotNull}, 131 | }, 132 | }, 133 | DroppedColumns: []*config.Field{ 134 | { 135 | Name: "stock", 136 | Type: field_type.Decimal, 137 | Limit: 2, 138 | Scale: 10, 139 | }, 140 | { 141 | Name: "author", 142 | Type: field_type.Varchar, 143 | Limit: 20, 144 | }, 145 | }, 146 | AlteredColumns: []*step.AlterColumn{ 147 | { 148 | Name: "name", 149 | Field: &config.Field{ 150 | Name: "name", 151 | Type: field_type.Varchar, 152 | Limit: 200, 153 | Default: "Alfred", 154 | Options: []field_option.FieldOption{ 155 | field_option.NotNull, 156 | }, 157 | }, 158 | LastField: &config.Field{ 159 | Name: "name", 160 | Type: field_type.Varchar, 161 | Limit: 100, 162 | Default: "Tejo", 163 | Options: []field_option.FieldOption{ 164 | field_option.Nullable, 165 | }, 166 | }, 167 | ChangedType: true, 168 | ChangedDefaultValue: true, 169 | ChangedOptions: []step.OptionAction{ 170 | step.SetNotNull, 171 | }, 172 | }, 173 | { 174 | Name: "price", 175 | Field: &config.Field{ 176 | Name: "price", 177 | Type: field_type.BigInt, 178 | Default: 10000, 179 | Options: []field_option.FieldOption{ 180 | field_option.Nullable, 181 | }, 182 | }, 183 | LastField: &config.Field{ 184 | Name: "price", 185 | Type: field_type.Decimal, 186 | Limit: 2, 187 | Scale: 10, 188 | Default: 50000, 189 | Options: []field_option.FieldOption{ 190 | field_option.NotNull, 191 | }, 192 | }, 193 | ChangedType: true, 194 | ChangedDefaultValue: true, 195 | ChangedOptions: []step.OptionAction{ 196 | step.DropNotNull, 197 | }, 198 | }, 199 | }, 200 | } 201 | 202 | gen := sqlgen.NewAlterTableGenerator("postgres", dialect.DefaultDialectOption()) 203 | buf := sb.NewSQLBuilder() 204 | gen.Rollback(buf, &alterStep) 205 | result := fmt.Sprintf("%s\n%s,\n%s,\n%s,\n%s,\n%s,\n%s;", 206 | "ALTER TABLE IF EXISTS \"users\"", 207 | "\tDROP COLUMN \"location\"", 208 | "\tDROP COLUMN \"address\"", 209 | "\tADD COLUMN \"stock\" DECIMAL(2, 10)", 210 | "\tADD COLUMN \"author\" VARCHAR(20)", 211 | "\tALTER COLUMN \"name\" SET DATA TYPE VARCHAR(100),\n\tALTER COLUMN \"name\" DROP NOT NULL,\n\tALTER COLUMN \"name\" SET DEFAULT 'Tejo'", 212 | "\tALTER COLUMN \"price\" SET DATA TYPE DECIMAL(2, 10),\n\tALTER COLUMN \"price\" SET NOT NULL,\n\tALTER COLUMN \"price\" SET DEFAULT 50000", 213 | ) 214 | assert.Equal(t, result, buf.String()) 215 | } 216 | -------------------------------------------------------------------------------- /sqlgen/create_index_generator.go: -------------------------------------------------------------------------------- 1 | package sqlgen 2 | 3 | import ( 4 | "gitlab.com/wartek-id/core/tools/dbgen/config" 5 | "gitlab.com/wartek-id/core/tools/dbgen/sqlgen/dialect" 6 | "gitlab.com/wartek-id/core/tools/dbgen/sqlgen/exp" 7 | "gitlab.com/wartek-id/core/tools/dbgen/sqlgen/sb" 8 | ) 9 | 10 | type CreateIndexGenerator interface { 11 | Dialect() string 12 | DialectOptions() *dialect.DialectOption 13 | ExpressionSQLGenerator() exp.ExpressionSQLGenerator 14 | Generate(sb.SQLBuilder, string, *config.Index) 15 | } 16 | 17 | type createIndexGenerator struct { 18 | dialect string 19 | esg exp.ExpressionSQLGenerator 20 | dialectOptions *dialect.DialectOption 21 | } 22 | 23 | func NewCreateIndexGenerator(dialect string, do *dialect.DialectOption) CreateIndexGenerator { 24 | return &createIndexGenerator{ 25 | dialect: dialect, 26 | dialectOptions: do, 27 | esg: exp.NewExpressionSQLGenerator(dialect, do), 28 | } 29 | } 30 | 31 | func (cig *createIndexGenerator) Dialect() string { 32 | return cig.dialect 33 | } 34 | 35 | func (cig *createIndexGenerator) DialectOptions() *dialect.DialectOption { 36 | return cig.dialectOptions 37 | } 38 | 39 | func (cig *createIndexGenerator) ExpressionSQLGenerator() exp.ExpressionSQLGenerator { 40 | return cig.esg 41 | } 42 | 43 | func (cig *createIndexGenerator) Generate(b sb.SQLBuilder, tblName string, idx *config.Index) { 44 | b.Write(cig.dialectOptions.CreateClause) 45 | if idx.Unique { 46 | b.Write(cig.dialectOptions.UniqueFragment). 47 | WriteRunes(cig.dialectOptions.SpaceRune) 48 | } 49 | b.Write(cig.dialectOptions.IndexFragment) 50 | 51 | if cig.dialectOptions.SupportConcurrently { 52 | b.Write(cig.dialectOptions.ConcurrentlyFragment). 53 | WriteRunes(cig.dialectOptions.SpaceRune) 54 | } 55 | 56 | b.Write(cig.dialectOptions.IfNotExistsFragment) 57 | cig.ExpressionSQLGenerator().LiteralExpression(b, idx.Name) 58 | b.Write(cig.dialectOptions.OnFragment) 59 | cig.ExpressionSQLGenerator().LiteralExpression(b, tblName) 60 | b.WriteRunes(cig.dialectOptions.LeftParenRune) 61 | cig.FieldSQL(b, idx.Fields) 62 | b.WriteRunes(cig.dialectOptions.RightParenRune) 63 | b.WriteRunes(cig.dialectOptions.SemiColonRune) 64 | } 65 | 66 | func (cig *createIndexGenerator) FieldSQL(b sb.SQLBuilder, fields []*config.IndexField) { 67 | for i, field := range fields { 68 | cig.ExpressionSQLGenerator().LiteralExpression(b, field.Column) 69 | if field.Order != "" { 70 | b.WriteRunes(cig.dialectOptions.SpaceRune) 71 | b.WriteString(field.Order) 72 | } 73 | if i != len(fields)-1 { 74 | b.WriteRunes(cig.dialectOptions.CommaRune) 75 | b.WriteRunes(cig.dialectOptions.SpaceRune) 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /sqlgen/create_index_generator_test.go: -------------------------------------------------------------------------------- 1 | package sqlgen_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "gitlab.com/wartek-id/core/tools/dbgen/config" 8 | "gitlab.com/wartek-id/core/tools/dbgen/sqlgen" 9 | "gitlab.com/wartek-id/core/tools/dbgen/sqlgen/dialect" 10 | "gitlab.com/wartek-id/core/tools/dbgen/sqlgen/sb" 11 | ) 12 | 13 | func TestCreateIndexGenerator_Dialect(t *testing.T) { 14 | dial := "postgres" 15 | do := dialect.DefaultDialectOption() 16 | 17 | sqlGen := sqlgen.NewCreateIndexGenerator(dial, do) 18 | assert.Equal(t, dial, sqlGen.Dialect()) 19 | } 20 | 21 | func TestCreateIndexGenerator_DialectOptions(t *testing.T) { 22 | dial := "postgres" 23 | do := dialect.DefaultDialectOption() 24 | 25 | sqlGen := sqlgen.NewCreateIndexGenerator(dial, do) 26 | assert.Equal(t, do, sqlGen.DialectOptions()) 27 | } 28 | 29 | func TestCreateIndexGenerator_ExpressionSQLGenerator(t *testing.T) { 30 | dial := "postgres" 31 | do := dialect.DefaultDialectOption() 32 | 33 | sqlGen := sqlgen.NewCreateIndexGenerator(dial, do) 34 | assert.NotNil(t, sqlGen.ExpressionSQLGenerator()) 35 | } 36 | 37 | func TestCreateIndexGenerator_Generate(t *testing.T) { 38 | doConcurrent := dialect.DefaultDialectOption() 39 | doConcurrent.SupportConcurrently = true 40 | 41 | testCases := []struct { 42 | dialect *dialect.DialectOption 43 | input *config.Index 44 | result string 45 | }{ 46 | { 47 | dialect: dialect.DefaultDialectOption(), 48 | input: &config.Index{ 49 | Name: "idx_name", 50 | Fields: []*config.IndexField{ 51 | { 52 | Column: "name", 53 | Order: "ASC", 54 | }, 55 | }, 56 | Unique: true, 57 | }, 58 | result: `CREATE UNIQUE INDEX IF NOT EXISTS "idx_name" ON "user"("name" ASC);`, 59 | }, 60 | { 61 | dialect: dialect.DefaultDialectOption(), 62 | input: &config.Index{ 63 | Name: "idx_name", 64 | Fields: []*config.IndexField{ 65 | { 66 | Column: "name", 67 | Order: "ASC", 68 | }, 69 | }, 70 | }, 71 | result: `CREATE INDEX IF NOT EXISTS "idx_name" ON "user"("name" ASC);`, 72 | }, 73 | { 74 | dialect: dialect.DefaultDialectOption(), 75 | input: &config.Index{ 76 | Name: "idx_name", 77 | Fields: []*config.IndexField{ 78 | { 79 | Column: "name", 80 | Order: "ASC", 81 | }, 82 | { 83 | Column: "age", 84 | Order: "ASC", 85 | }, 86 | }, 87 | }, 88 | result: `CREATE INDEX IF NOT EXISTS "idx_name" ON "user"("name" ASC, "age" ASC);`, 89 | }, 90 | { 91 | dialect: doConcurrent, 92 | input: &config.Index{ 93 | Name: "idx_name", 94 | Fields: []*config.IndexField{ 95 | { 96 | Column: "name", 97 | Order: "ASC", 98 | }, 99 | { 100 | Column: "age", 101 | }, 102 | }, 103 | }, 104 | result: `CREATE INDEX CONCURRENTLY IF NOT EXISTS "idx_name" ON "user"("name" ASC, "age");`, 105 | }, 106 | } 107 | 108 | for _, tc := range testCases { 109 | buf := sb.NewSQLBuilder() 110 | sqlGen := sqlgen.NewCreateIndexGenerator("postgres", tc.dialect) 111 | sqlGen.Generate(buf, "user", tc.input) 112 | result, err := buf.ToSQL() 113 | assert.Nil(t, err) 114 | assert.Equal(t, tc.result, result) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /sqlgen/create_table_generator.go: -------------------------------------------------------------------------------- 1 | package sqlgen 2 | 3 | import ( 4 | "gitlab.com/wartek-id/core/tools/dbgen/config" 5 | "gitlab.com/wartek-id/core/tools/dbgen/sqlgen/dialect" 6 | "gitlab.com/wartek-id/core/tools/dbgen/sqlgen/exp" 7 | "gitlab.com/wartek-id/core/tools/dbgen/sqlgen/sb" 8 | ) 9 | 10 | type CreateTableGenerator interface { 11 | Dialect() string 12 | DialectOptions() *dialect.DialectOption 13 | ExpressionSQLGenerator() exp.ExpressionSQLGenerator 14 | Generate(sb.SQLBuilder, *config.Schema) 15 | } 16 | 17 | type createTableGenerator struct { 18 | dialect string 19 | esg exp.ExpressionSQLGenerator 20 | dialectOptions *dialect.DialectOption 21 | } 22 | 23 | func NewCreateTableGenerator(dialect string, do *dialect.DialectOption) CreateTableGenerator { 24 | return &createTableGenerator{ 25 | dialect: dialect, 26 | dialectOptions: do, 27 | esg: exp.NewExpressionSQLGenerator(dialect, do), 28 | } 29 | } 30 | 31 | func (ctg *createTableGenerator) Dialect() string { 32 | return ctg.dialect 33 | } 34 | 35 | func (ctg *createTableGenerator) DialectOptions() *dialect.DialectOption { 36 | return ctg.dialectOptions 37 | } 38 | 39 | func (ctg *createTableGenerator) Generate(b sb.SQLBuilder, schema *config.Schema) { 40 | b.Write(ctg.dialectOptions.CreateClause). 41 | Write(ctg.dialectOptions.TableFragment). 42 | Write(ctg.dialectOptions.IfNotExistsFragment) 43 | 44 | ctg.ExpressionSQLGenerator().LiteralExpression(b, schema.Name) 45 | 46 | b.WriteRunes(ctg.dialectOptions.SpaceRune) 47 | b.WriteRunes(ctg.dialectOptions.LeftParenRune) 48 | b.WriteRunes(ctg.dialectOptions.NewLineRune) 49 | ctg.FieldSQL(b, schema.Fields) 50 | b.WriteRunes(ctg.dialectOptions.NewLineRune) 51 | b.WriteRunes(ctg.dialectOptions.RightParenRune) 52 | b.WriteRunes(ctg.dialectOptions.SemiColonRune) 53 | } 54 | 55 | func (ctg *createTableGenerator) FieldSQL(b sb.SQLBuilder, fields []*config.Field) { 56 | for i, field := range fields { 57 | b.WriteRunes(ctg.dialectOptions.TabRune) 58 | ctg.ExpressionSQLGenerator().LiteralExpression(b, field.Name) 59 | b.WriteRunes(ctg.dialectOptions.SpaceRune) 60 | b.Write(ctg.esg.GetTypeFragment(field)) 61 | b.Write(ctg.esg.GetOptionsFragment(field)) 62 | 63 | if i != len(fields)-1 { 64 | b.Write(ctg.dialectOptions.CommaNewLineFragment) 65 | } 66 | } 67 | } 68 | 69 | func (ctg *createTableGenerator) ExpressionSQLGenerator() exp.ExpressionSQLGenerator { 70 | return ctg.esg 71 | } 72 | -------------------------------------------------------------------------------- /sqlgen/create_table_generator_test.go: -------------------------------------------------------------------------------- 1 | package sqlgen_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "gitlab.com/wartek-id/core/tools/dbgen/config" 8 | "gitlab.com/wartek-id/core/tools/dbgen/sqlgen" 9 | "gitlab.com/wartek-id/core/tools/dbgen/sqlgen/dialect" 10 | "gitlab.com/wartek-id/core/tools/dbgen/sqlgen/sb" 11 | "gitlab.com/wartek-id/core/tools/dbgen/types/field_option" 12 | ) 13 | 14 | func TestCreateTableGenerator_Dialect(t *testing.T) { 15 | dial := "postgres" 16 | do := dialect.DefaultDialectOption() 17 | 18 | sqlGen := sqlgen.NewCreateTableGenerator(dial, do) 19 | assert.Equal(t, dial, sqlGen.Dialect()) 20 | } 21 | 22 | func TestCreateTableGenerator_DialectOptions(t *testing.T) { 23 | dial := "postgres" 24 | do := dialect.DefaultDialectOption() 25 | 26 | sqlGen := sqlgen.NewCreateTableGenerator(dial, do) 27 | assert.Equal(t, do, sqlGen.DialectOptions()) 28 | } 29 | 30 | func TestCreateTableGenerator_ExpressionSQLGenerator(t *testing.T) { 31 | dial := "postgres" 32 | do := dialect.DefaultDialectOption() 33 | 34 | sqlGen := sqlgen.NewCreateTableGenerator(dial, do) 35 | assert.NotNil(t, sqlGen.ExpressionSQLGenerator()) 36 | } 37 | 38 | func TestCreateTableGenerator_Generate(t *testing.T) { 39 | testCases := []struct { 40 | dialect *dialect.DialectOption 41 | input *config.Schema 42 | result string 43 | }{ 44 | { 45 | dialect: dialect.DefaultDialectOption(), 46 | input: &config.Schema{ 47 | Name: "user", 48 | Fields: []*config.Field{ 49 | { 50 | Name: "id", 51 | Type: "bigserial", 52 | Options: []field_option.FieldOption{ 53 | field_option.NotNull, 54 | }, 55 | }, 56 | { 57 | Name: "name", 58 | Type: "varchar", 59 | Limit: 255, 60 | Options: []field_option.FieldOption{ 61 | field_option.NotNull, 62 | }, 63 | }, 64 | { 65 | Name: "school", 66 | Type: "varchar", 67 | Limit: 100, 68 | Options: []field_option.FieldOption{ 69 | field_option.Nullable, 70 | }, 71 | }, 72 | { 73 | Name: "salary", 74 | Type: "decimal", 75 | Limit: 5, 76 | Scale: 2, 77 | }, 78 | }, 79 | }, 80 | result: "CREATE TABLE IF NOT EXISTS \"user\" (\n\t\"id\" BIGSERIAL NOT NULL,\n\t\"name\" VARCHAR(255) NOT NULL,\n\t\"school\" VARCHAR(100) NULL,\n\t\"salary\" DECIMAL(5, 2)\n);", 81 | }, 82 | } 83 | 84 | for _, tc := range testCases { 85 | buf := sb.NewSQLBuilder() 86 | sqlGen := sqlgen.NewCreateTableGenerator("postgres", tc.dialect) 87 | sqlGen.Generate(buf, tc.input) 88 | result, err := buf.ToSQL() 89 | assert.Nil(t, err) 90 | assert.Equal(t, tc.result, result) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /sqlgen/dialect/dialect_option.go: -------------------------------------------------------------------------------- 1 | package dialect 2 | 3 | import ( 4 | "bytes" 5 | 6 | "gitlab.com/wartek-id/core/tools/dbgen/types/field_option" 7 | "gitlab.com/wartek-id/core/tools/dbgen/types/field_type" 8 | ) 9 | 10 | type DialectOption struct { 11 | CreateClause []byte 12 | DropClause []byte 13 | AlterClause []byte 14 | BeginClause []byte 15 | CommitClause []byte 16 | 17 | IndexFragment []byte 18 | TableFragment []byte 19 | 20 | AlterFragment []byte 21 | DropFragment []byte 22 | AddFragment []byte 23 | ColumnFragment []byte 24 | SetFragment []byte 25 | DefaultFragment []byte 26 | DataTypeFragment []byte 27 | 28 | BooleanFragment []byte 29 | VarcharFragment []byte 30 | SmallIntFragment []byte 31 | IntFragment []byte 32 | BigIntFragment []byte 33 | JsonFragment []byte 34 | JsonbFragment []byte 35 | FloatFragment []byte 36 | DecimalFragment []byte 37 | TimestampFragment []byte 38 | TimestamptzFragment []byte 39 | 40 | SmallSerialFragment []byte 41 | SerialFragment []byte 42 | BigSerialFragment []byte 43 | 44 | PrimaryKeyFragment []byte 45 | NullableFragment []byte 46 | NotNullFragment []byte 47 | UniqueFragment []byte 48 | 49 | ConcurrentlyFragment []byte 50 | IfNotExistsFragment []byte 51 | IfExistsFragment []byte 52 | AutoIncrementFragment []byte 53 | 54 | EmptyFragment []byte 55 | OnFragment []byte 56 | CommaNewLineFragment []byte 57 | SupportConcurrently bool 58 | SupportTransaction bool 59 | 60 | LeftParenRune rune 61 | RightParenRune rune 62 | CommaRune rune 63 | SemiColonRune rune 64 | SpaceRune rune 65 | QuoteRune rune 66 | StringQuoteRune rune 67 | NewLineRune rune 68 | TabRune rune 69 | 70 | DataTypesLookup map[field_type.FieldType][]byte 71 | FieldOptionsLookup map[field_option.FieldOption][]byte 72 | } 73 | 74 | func DefaultDialectOption() *DialectOption { 75 | do := &DialectOption{ 76 | CreateClause: []byte("CREATE "), 77 | DropClause: []byte("DROP "), 78 | AlterClause: []byte("ALTER "), 79 | BeginClause: []byte("BEGIN;"), 80 | CommitClause: []byte("COMMIT;"), 81 | 82 | IndexFragment: []byte("INDEX "), 83 | TableFragment: []byte("TABLE "), 84 | 85 | AlterFragment: []byte("ALTER "), 86 | DropFragment: []byte("DROP "), 87 | AddFragment: []byte("ADD "), 88 | ColumnFragment: []byte("COLUMN "), 89 | SetFragment: []byte("SET "), 90 | DefaultFragment: []byte("DEFAULT "), 91 | DataTypeFragment: []byte("DATA TYPE "), 92 | 93 | BooleanFragment: []byte("BOOLEAN"), 94 | VarcharFragment: []byte("VARCHAR"), 95 | SmallIntFragment: []byte("SMALLINT"), 96 | IntFragment: []byte("INT"), 97 | BigIntFragment: []byte("BIGINT"), 98 | JsonFragment: []byte("JSON"), 99 | JsonbFragment: []byte("JSONB"), 100 | FloatFragment: []byte("FLOAT"), 101 | DecimalFragment: []byte("DECIMAL"), 102 | TimestampFragment: []byte("TIMESTAMP"), 103 | TimestamptzFragment: []byte("TIMESTAMPTZ"), 104 | 105 | SmallSerialFragment: []byte("SMALLSERIAL"), 106 | SerialFragment: []byte("SERIAL"), 107 | BigSerialFragment: []byte("BIGSERIAL"), 108 | 109 | PrimaryKeyFragment: []byte("PRIMARY KEY"), 110 | NullableFragment: []byte("NULL"), 111 | NotNullFragment: []byte("NOT NULL"), 112 | UniqueFragment: []byte("UNIQUE"), 113 | 114 | ConcurrentlyFragment: []byte("CONCURRENTLY"), 115 | IfNotExistsFragment: []byte("IF NOT EXISTS "), 116 | IfExistsFragment: []byte("IF EXISTS "), 117 | AutoIncrementFragment: []byte("AUTO INCREMENT"), 118 | 119 | LeftParenRune: '(', 120 | RightParenRune: ')', 121 | CommaRune: ',', 122 | SemiColonRune: ';', 123 | SpaceRune: ' ', 124 | QuoteRune: '"', 125 | StringQuoteRune: '\'', 126 | NewLineRune: '\n', 127 | TabRune: '\t', 128 | 129 | CommaNewLineFragment: []byte(",\n"), 130 | OnFragment: []byte(" ON "), 131 | SupportConcurrently: false, 132 | SupportTransaction: true, 133 | } 134 | 135 | do.DataTypesLookup = map[field_type.FieldType][]byte{ 136 | field_type.Boolean: do.BooleanFragment, 137 | field_type.Varchar: do.VarcharFragment, 138 | field_type.SmallInt: do.SmallIntFragment, 139 | field_type.Int: do.IntFragment, 140 | field_type.BigInt: do.BigIntFragment, 141 | field_type.Json: do.JsonFragment, 142 | field_type.Jsonb: do.JsonbFragment, 143 | field_type.Float: do.FloatFragment, 144 | field_type.Decimal: do.DecimalFragment, 145 | field_type.Timestamp: do.TimestampFragment, 146 | field_type.Timestamptz: do.TimestamptzFragment, 147 | field_type.BigSerial: do.BigSerialFragment, 148 | field_type.Serial: do.SerialFragment, 149 | field_type.SmallSerial: do.SmallSerialFragment, 150 | } 151 | 152 | do.FieldOptionsLookup = map[field_option.FieldOption][]byte{ 153 | field_option.Nullable: do.NullableFragment, 154 | field_option.NotNull: do.NotNullFragment, 155 | field_option.AutoIncrement: do.AutoIncrementFragment, 156 | field_option.Unique: do.UniqueFragment, 157 | field_option.PrimaryKey: do.PrimaryKeyFragment, 158 | } 159 | 160 | return do 161 | } 162 | 163 | func (do *DialectOption) CreateTableTemplate() []byte { 164 | buf := bytes.Buffer{} 165 | buf.Write(do.CreateClause) 166 | buf.Write(do.TableFragment) 167 | return buf.Bytes() 168 | } 169 | 170 | func (do *DialectOption) AlterColumnTemplate() []byte { 171 | buf := bytes.Buffer{} 172 | buf.Write(do.AlterClause) 173 | buf.Write(do.ColumnFragment) 174 | return buf.Bytes() 175 | } 176 | 177 | func (do *DialectOption) AddColumnTemplate() []byte { 178 | buf := bytes.Buffer{} 179 | buf.Write(do.AddFragment) 180 | buf.Write(do.ColumnFragment) 181 | return buf.Bytes() 182 | } 183 | 184 | func (do *DialectOption) DropColumnTemplate() []byte { 185 | buf := bytes.Buffer{} 186 | buf.Write(do.DropClause) 187 | buf.Write(do.ColumnFragment) 188 | return buf.Bytes() 189 | } 190 | -------------------------------------------------------------------------------- /sqlgen/diff/schema.go: -------------------------------------------------------------------------------- 1 | package diff 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/google/go-cmp/cmp" 7 | "gitlab.com/wartek-id/core/tools/dbgen/config" 8 | "gitlab.com/wartek-id/core/tools/dbgen/sqlgen/step" 9 | "gitlab.com/wartek-id/core/tools/dbgen/types/field_type" 10 | ) 11 | 12 | var ( 13 | ErrMissingCurrentTable = errors.New("current table is not exists") 14 | ErrMissingTargetTable = errors.New("missing target table") 15 | ) 16 | 17 | type Nameable interface { 18 | GetName() string 19 | } 20 | 21 | type diffSchema struct { 22 | name string 23 | schema *config.Schema 24 | fields map[string]*config.Field 25 | indexes map[string]*config.Index 26 | } 27 | 28 | type Schema struct { 29 | from map[string]*diffSchema 30 | target map[string]*diffSchema 31 | } 32 | 33 | func NewSchema(from, target []*config.Schema) *Schema { 34 | return &Schema{ 35 | from: buildSchema(from), 36 | target: buildSchema(target), 37 | } 38 | } 39 | 40 | func (diff *Schema) GeneratePlan() (*step.MigrationPlanner, error) { 41 | planner := step.NewMigrationPlanner() 42 | planner.CreateTable = diff.CreatedTable() 43 | planner.DropTable = diff.DroppedTable() 44 | 45 | for name := range diff.target { 46 | existingTable := diff.from[name] 47 | if existingTable == nil { 48 | continue 49 | } 50 | alterTable, err := diff.AlteredSchema(name) 51 | if err != nil { 52 | return nil, err 53 | } 54 | 55 | if alterTable.HasChanges() { 56 | planner.AlterSchema[name] = alterTable 57 | } 58 | } 59 | 60 | return planner, nil 61 | } 62 | 63 | func (diff *Schema) CreatedTable() []*config.Schema { 64 | createdTable := make([]*config.Schema, 0) 65 | for name, diffSchema := range diff.target { 66 | if diff.from[name] == nil { 67 | createdTable = append(createdTable, diffSchema.schema) 68 | } 69 | } 70 | return createdTable 71 | } 72 | 73 | func (diff *Schema) DroppedTable() []*config.Schema { 74 | droppedTable := make([]*config.Schema, 0) 75 | for name, diffSchema := range diff.from { 76 | if diff.target[name] == nil { 77 | droppedTable = append(droppedTable, diffSchema.schema) 78 | } 79 | } 80 | return droppedTable 81 | } 82 | 83 | func (diff *Schema) AlteredIndexes(existing, target map[string]*config.Index, planner *step.AlterSchema) { 84 | for name, index := range existing { 85 | if target[name] == nil { 86 | planner.DroppedIndices = append(planner.DroppedIndices, index) 87 | } 88 | } 89 | 90 | for name, targetIndex := range target { 91 | existingIndex := existing[name] 92 | if existingIndex == nil { 93 | planner.AddedIndices = append(planner.AddedIndices, targetIndex) 94 | continue 95 | } 96 | 97 | if !cmp.Equal(existingIndex, targetIndex) { 98 | planner.DroppedIndices = append(planner.DroppedIndices, existingIndex) 99 | planner.AddedIndices = append(planner.AddedIndices, targetIndex) 100 | } 101 | } 102 | } 103 | 104 | func (diff *Schema) AlteredSchema(table string) (*step.AlterSchema, error) { 105 | tableFrom := diff.from[table] 106 | if tableFrom == nil { 107 | return nil, ErrMissingCurrentTable 108 | } 109 | tableTarget := diff.target[table] 110 | if tableTarget == nil { 111 | return nil, ErrMissingTargetTable 112 | } 113 | 114 | existingFields := tableFrom.fields 115 | targetFields := tableTarget.fields 116 | migrationSteps := step.NewAlterSchema(table) 117 | 118 | for name, field := range targetFields { 119 | existingField := existingFields[name] 120 | if existingField == nil { 121 | migrationSteps.AddedColumns = append(migrationSteps.AddedColumns, field) 122 | continue 123 | } 124 | 125 | alteredColumn := diff.alteredColumn(existingField, field) 126 | if alteredColumn.HasChanges() { 127 | migrationSteps.AlteredColumns = append(migrationSteps.AlteredColumns, alteredColumn) 128 | } 129 | } 130 | 131 | for name, field := range existingFields { 132 | if targetFields[name] == nil { 133 | migrationSteps.DroppedColumns = append(migrationSteps.DroppedColumns, field) 134 | continue 135 | } 136 | } 137 | 138 | diff.AlteredIndexes(tableFrom.indexes, tableTarget.indexes, migrationSteps) 139 | return migrationSteps, nil 140 | } 141 | 142 | func (diff *Schema) alteredColumn(from, target *config.Field) *step.AlterColumn { 143 | alterColumn := step.AlterColumn{ 144 | Name: target.Name, 145 | Field: target, 146 | LastField: from, 147 | ChangedType: !diff.isSameFieldType(from, target), 148 | ChangedDefaultValue: diff.changedDefaultValue(from, target), 149 | ChangedOptions: diff.changedOptions(from, target), 150 | } 151 | 152 | return &alterColumn 153 | } 154 | 155 | func (diff *Schema) isSameFieldType(from, target *config.Field) bool { 156 | if from.Type != target.Type { 157 | return false 158 | } 159 | 160 | switch target.Type { 161 | case field_type.Varchar, field_type.Decimal: 162 | return from.Limit == target.Limit && 163 | from.Scale == target.Scale 164 | } 165 | 166 | return true 167 | } 168 | 169 | func (diff *Schema) changedOptions(from, target *config.Field) []step.OptionAction { 170 | options := make([]step.OptionAction, 0) 171 | if from.IsNotNull() != target.IsNotNull() { 172 | if target.IsNotNull() { 173 | options = append(options, step.SetNotNull) 174 | } else { 175 | options = append(options, step.DropNotNull) 176 | } 177 | } 178 | 179 | return options 180 | } 181 | 182 | func (diff *Schema) changedDefaultValue(from, target *config.Field) bool { 183 | return from.Default != target.Default 184 | } 185 | 186 | func buildSchema(schemas []*config.Schema) map[string]*diffSchema { 187 | cmpSchema := make(map[string]*diffSchema) 188 | for _, sc := range schemas { 189 | cmpSchema[sc.Name] = &diffSchema{ 190 | name: sc.Name, 191 | schema: sc, 192 | fields: nameableMapper(sc.Fields), 193 | indexes: nameableMapper(sc.Index), 194 | } 195 | } 196 | return cmpSchema 197 | } 198 | 199 | func nameableMapper[T Nameable](elements []T) map[string]T { 200 | result := make(map[string]T) 201 | for _, element := range elements { 202 | result[element.GetName()] = element 203 | } 204 | 205 | return result 206 | } 207 | -------------------------------------------------------------------------------- /sqlgen/diff/schema_test.go: -------------------------------------------------------------------------------- 1 | package diff_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "gitlab.com/wartek-id/core/tools/dbgen/config" 8 | "gitlab.com/wartek-id/core/tools/dbgen/sqlgen/diff" 9 | "gitlab.com/wartek-id/core/tools/dbgen/sqlgen/step" 10 | "gitlab.com/wartek-id/core/tools/dbgen/types/field_option" 11 | ) 12 | 13 | func TestCreatedTable(t *testing.T) { 14 | from := []*config.Schema{ 15 | { 16 | Name: "user", 17 | }, 18 | { 19 | Name: "module", 20 | }, 21 | } 22 | 23 | target := []*config.Schema{ 24 | { 25 | Name: "user", 26 | }, 27 | { 28 | Name: "module", 29 | }, 30 | { 31 | Name: "toggle", 32 | }, 33 | } 34 | 35 | diffSchema := diff.NewSchema(from, target) 36 | result := diffSchema.CreatedTable() 37 | 38 | assert.Equal(t, []*config.Schema{{Name: "toggle"}}, result) 39 | } 40 | 41 | func TestDroppedTable(t *testing.T) { 42 | from := []*config.Schema{ 43 | { 44 | Name: "user", 45 | }, 46 | { 47 | Name: "module", 48 | }, 49 | { 50 | Name: "toggle", 51 | }, 52 | } 53 | 54 | target := []*config.Schema{ 55 | { 56 | Name: "user", 57 | }, 58 | { 59 | Name: "module", 60 | }, 61 | } 62 | 63 | diffSchema := diff.NewSchema(from, target) 64 | result := diffSchema.DroppedTable() 65 | 66 | assert.Equal(t, []*config.Schema{{Name: "toggle"}}, result) 67 | } 68 | 69 | func TestAlterSchema(t *testing.T) { 70 | testCases := []struct { 71 | existing []*config.Schema 72 | target []*config.Schema 73 | input string 74 | result *step.AlterSchema 75 | err error 76 | }{ 77 | { 78 | existing: []*config.Schema{ 79 | { 80 | Name: "users", 81 | Fields: []*config.Field{ 82 | { 83 | Name: "id", 84 | Type: "bigserial", 85 | Options: []field_option.FieldOption{ 86 | field_option.NotNull, 87 | }, 88 | }, 89 | { 90 | Name: "name", 91 | Type: "varchar", 92 | Limit: 20, 93 | }, 94 | { 95 | Name: "address", 96 | Type: "varchar", 97 | Limit: 50, 98 | }, 99 | { 100 | Name: "school", 101 | Type: "varchar", 102 | Limit: 50, 103 | }, 104 | { 105 | Name: "age", 106 | Type: "int", 107 | Options: []field_option.FieldOption{ 108 | field_option.NotNull, 109 | }, 110 | }, 111 | }, 112 | }, 113 | }, 114 | target: []*config.Schema{ 115 | { 116 | Name: "users", 117 | Fields: []*config.Field{ 118 | { 119 | Name: "id", 120 | Type: "bigserial", 121 | Options: []field_option.FieldOption{ 122 | field_option.NotNull, 123 | }, 124 | }, 125 | { 126 | Name: "name", 127 | Type: "varchar", 128 | Limit: 20, 129 | Options: []field_option.FieldOption{ 130 | field_option.NotNull, 131 | }, 132 | }, 133 | { 134 | Name: "address", 135 | Type: "varchar", 136 | Limit: 150, 137 | }, 138 | { 139 | Name: "school", 140 | Type: "varchar", 141 | Limit: 100, 142 | Options: []field_option.FieldOption{ 143 | field_option.NotNull, 144 | }, 145 | }, 146 | { 147 | Name: "created_at", 148 | Type: "timestamp", 149 | Options: []field_option.FieldOption{ 150 | field_option.NotNull, 151 | }, 152 | }, 153 | }, 154 | }, 155 | }, 156 | input: "users", 157 | result: &step.AlterSchema{ 158 | Name: "users", 159 | AddedColumns: []*config.Field{ 160 | { 161 | Name: "created_at", 162 | Type: "timestamp", 163 | Options: []field_option.FieldOption{ 164 | field_option.NotNull, 165 | }, 166 | }, 167 | }, 168 | AlteredColumns: []*step.AlterColumn{ 169 | { 170 | Name: "name", 171 | Field: &config.Field{ 172 | Name: "name", 173 | Type: "varchar", 174 | Limit: 20, 175 | Options: []field_option.FieldOption{ 176 | field_option.NotNull, 177 | }, 178 | }, 179 | LastField: &config.Field{ 180 | Name: "name", 181 | Type: "varchar", 182 | Limit: 20, 183 | }, 184 | ChangedOptions: []step.OptionAction{step.SetNotNull}, 185 | }, 186 | { 187 | Name: "address", 188 | Field: &config.Field{ 189 | Name: "address", 190 | Type: "varchar", 191 | Limit: 150, 192 | }, 193 | LastField: &config.Field{ 194 | Name: "address", 195 | Type: "varchar", 196 | Limit: 50, 197 | }, 198 | ChangedType: true, 199 | ChangedOptions: []step.OptionAction{}, 200 | }, 201 | { 202 | Name: "school", 203 | Field: &config.Field{ 204 | Name: "school", 205 | Type: "varchar", 206 | Limit: 100, 207 | Options: []field_option.FieldOption{ 208 | field_option.NotNull, 209 | }, 210 | }, 211 | LastField: &config.Field{ 212 | Name: "school", 213 | Type: "varchar", 214 | Limit: 50, 215 | }, 216 | ChangedType: true, 217 | ChangedOptions: []step.OptionAction{step.SetNotNull}, 218 | }, 219 | }, 220 | DroppedColumns: []*config.Field{ 221 | { 222 | Name: "age", 223 | Type: "int", 224 | Options: []field_option.FieldOption{ 225 | field_option.NotNull, 226 | }, 227 | }, 228 | }, 229 | }, 230 | }, 231 | { 232 | existing: []*config.Schema{ 233 | { 234 | Name: "users", 235 | }, 236 | }, 237 | target: []*config.Schema{}, 238 | input: "users", 239 | err: diff.ErrMissingTargetTable, 240 | }, 241 | { 242 | target: []*config.Schema{ 243 | { 244 | Name: "users", 245 | }, 246 | }, 247 | existing: []*config.Schema{}, 248 | input: "users", 249 | err: diff.ErrMissingCurrentTable, 250 | }, 251 | { 252 | existing: []*config.Schema{ 253 | { 254 | Name: "users", 255 | Index: []*config.Index{ 256 | { 257 | Name: "index_on_name", 258 | Fields: []*config.IndexField{ 259 | { 260 | Column: "name", 261 | }, 262 | }, 263 | }, 264 | { 265 | Name: "index_on_name_age", 266 | Fields: []*config.IndexField{ 267 | { 268 | Column: "age", 269 | }, 270 | { 271 | Column: "name", 272 | Order: "ASC", 273 | }, 274 | }, 275 | }, 276 | { 277 | Name: "index_on_dashboard", 278 | Fields: []*config.IndexField{ 279 | { 280 | Column: "name", 281 | }, 282 | { 283 | Column: "email", 284 | }, 285 | }, 286 | }, 287 | { 288 | Name: "index_on_location", 289 | Fields: []*config.IndexField{ 290 | { 291 | Column: "location", 292 | }, 293 | }, 294 | }, 295 | }, 296 | }, 297 | }, 298 | target: []*config.Schema{ 299 | { 300 | Name: "users", 301 | Index: []*config.Index{ 302 | { 303 | Name: "index_on_name", 304 | Fields: []*config.IndexField{ 305 | { 306 | Column: "name", 307 | }, 308 | }, 309 | }, 310 | { 311 | Name: "index_on_name_age", 312 | Fields: []*config.IndexField{ 313 | { 314 | Column: "name", 315 | Order: "ASC", 316 | }, 317 | { 318 | Column: "age", 319 | }, 320 | }, 321 | }, 322 | { 323 | Name: "index_on_dashboard", 324 | Fields: []*config.IndexField{ 325 | { 326 | Column: "name", 327 | }, 328 | { 329 | Column: "email", 330 | }, 331 | { 332 | Column: "location", 333 | }, 334 | }, 335 | }, 336 | { 337 | Name: "index_on_email", 338 | Fields: []*config.IndexField{ 339 | { 340 | Column: "email", 341 | }, 342 | }, 343 | }, 344 | }, 345 | }, 346 | }, 347 | input: "users", 348 | result: &step.AlterSchema{ 349 | Name: "users", 350 | DroppedIndices: []*config.Index{ 351 | { 352 | Name: "index_on_location", 353 | Fields: []*config.IndexField{ 354 | { 355 | Column: "location", 356 | }, 357 | }, 358 | }, 359 | { 360 | Name: "index_on_dashboard", 361 | Fields: []*config.IndexField{ 362 | { 363 | Column: "name", 364 | }, 365 | { 366 | Column: "email", 367 | }, 368 | }, 369 | }, 370 | }, 371 | AddedIndices: []*config.Index{ 372 | { 373 | Name: "index_on_email", 374 | Fields: []*config.IndexField{ 375 | { 376 | Column: "email", 377 | }, 378 | }, 379 | }, 380 | { 381 | Name: "index_on_dashboard", 382 | Fields: []*config.IndexField{ 383 | { 384 | Column: "name", 385 | }, 386 | { 387 | Column: "email", 388 | }, 389 | { 390 | Column: "location", 391 | }, 392 | }, 393 | }, 394 | }, 395 | }, 396 | }, 397 | } 398 | 399 | for _, tc := range testCases { 400 | diffSchema := diff.NewSchema(tc.existing, tc.target) 401 | result, err := diffSchema.AlteredSchema(tc.input) 402 | assert.Equal(t, tc.err, err) 403 | if tc.result != nil { 404 | assert.Equal(t, tc.result.Name, result.Name) 405 | assert.ElementsMatch(t, tc.result.AddedColumns, result.AddedColumns) 406 | assert.ElementsMatch(t, tc.result.AlteredColumns, result.AlteredColumns) 407 | assert.ElementsMatch(t, tc.result.DroppedColumns, result.DroppedColumns) 408 | } 409 | } 410 | } 411 | 412 | func TestGeneratePlan(t *testing.T) { 413 | testCases := []struct { 414 | existingSchema []*config.Schema 415 | targetSchema []*config.Schema 416 | result *step.MigrationPlanner 417 | err error 418 | }{ 419 | { 420 | existingSchema: []*config.Schema{ 421 | { 422 | Name: "users", 423 | Fields: []*config.Field{ 424 | { 425 | Name: "id", 426 | Type: "bigserial", 427 | Options: []field_option.FieldOption{ 428 | field_option.NotNull, 429 | }, 430 | }, 431 | { 432 | Name: "name", 433 | Type: "varchar", 434 | Limit: 20, 435 | }, 436 | { 437 | Name: "address", 438 | Type: "varchar", 439 | Limit: 50, 440 | }, 441 | { 442 | Name: "school", 443 | Type: "varchar", 444 | Limit: 50, 445 | }, 446 | { 447 | Name: "age", 448 | Type: "int", 449 | Options: []field_option.FieldOption{ 450 | field_option.NotNull, 451 | }, 452 | }, 453 | }, 454 | }, 455 | { 456 | Name: "example", 457 | Fields: []*config.Field{ 458 | { 459 | Name: "id", 460 | Type: "bigserial", 461 | }, 462 | { 463 | Name: "name", 464 | Type: "varchar", 465 | Limit: 20, 466 | }, 467 | }, 468 | }, 469 | }, 470 | targetSchema: []*config.Schema{ 471 | { 472 | Name: "users", 473 | Fields: []*config.Field{ 474 | { 475 | Name: "id", 476 | Type: "bigserial", 477 | Options: []field_option.FieldOption{ 478 | field_option.NotNull, 479 | }, 480 | }, 481 | { 482 | Name: "name", 483 | Type: "varchar", 484 | Limit: 20, 485 | Options: []field_option.FieldOption{ 486 | field_option.NotNull, 487 | }, 488 | }, 489 | { 490 | Name: "address", 491 | Type: "varchar", 492 | Limit: 150, 493 | }, 494 | { 495 | Name: "school", 496 | Type: "varchar", 497 | Limit: 100, 498 | Options: []field_option.FieldOption{ 499 | field_option.NotNull, 500 | }, 501 | }, 502 | { 503 | Name: "created_at", 504 | Type: "timestamp", 505 | Options: []field_option.FieldOption{ 506 | field_option.NotNull, 507 | }, 508 | }, 509 | }, 510 | }, 511 | { 512 | Name: "documents", 513 | Fields: []*config.Field{ 514 | { 515 | Name: "id", 516 | Type: "bigserial", 517 | }, 518 | { 519 | Name: "name", 520 | Type: "varchar", 521 | Limit: 20, 522 | }, 523 | }, 524 | }, 525 | }, 526 | result: &step.MigrationPlanner{ 527 | CreateTable: []*config.Schema{ 528 | { 529 | Name: "documents", 530 | Fields: []*config.Field{ 531 | { 532 | Name: "id", 533 | Type: "bigserial", 534 | }, 535 | { 536 | Name: "name", 537 | Type: "varchar", 538 | Limit: 20, 539 | }, 540 | }, 541 | }, 542 | }, 543 | DropTable: []*config.Schema{ 544 | { 545 | Name: "example", 546 | Fields: []*config.Field{ 547 | { 548 | Name: "id", 549 | Type: "bigserial", 550 | }, 551 | { 552 | Name: "name", 553 | Type: "varchar", 554 | Limit: 20, 555 | }, 556 | }, 557 | }, 558 | }, 559 | AlterSchema: map[string]*step.AlterSchema{ 560 | "users": { 561 | Name: "users", 562 | AddedColumns: []*config.Field{ 563 | { 564 | Name: "created_at", 565 | Type: "timestamp", 566 | Options: []field_option.FieldOption{ 567 | field_option.NotNull, 568 | }, 569 | }, 570 | }, 571 | AlteredColumns: []*step.AlterColumn{ 572 | { 573 | Name: "name", 574 | Field: &config.Field{ 575 | Name: "name", 576 | Type: "varchar", 577 | Limit: 20, 578 | Options: []field_option.FieldOption{ 579 | field_option.NotNull, 580 | }, 581 | }, 582 | LastField: &config.Field{ 583 | Name: "name", 584 | Type: "varchar", 585 | Limit: 20, 586 | }, 587 | ChangedOptions: []step.OptionAction{step.SetNotNull}, 588 | }, 589 | { 590 | Name: "address", 591 | Field: &config.Field{ 592 | Name: "address", 593 | Type: "varchar", 594 | Limit: 150, 595 | }, 596 | LastField: &config.Field{ 597 | Name: "address", 598 | Type: "varchar", 599 | Limit: 50, 600 | }, 601 | ChangedType: true, 602 | ChangedOptions: []step.OptionAction{}, 603 | }, 604 | { 605 | Name: "school", 606 | Field: &config.Field{ 607 | Name: "school", 608 | Type: "varchar", 609 | Limit: 100, 610 | Options: []field_option.FieldOption{ 611 | field_option.NotNull, 612 | }, 613 | }, 614 | LastField: &config.Field{ 615 | Name: "school", 616 | Type: "varchar", 617 | Limit: 50, 618 | }, 619 | ChangedType: true, 620 | ChangedOptions: []step.OptionAction{step.SetNotNull}, 621 | }, 622 | }, 623 | DroppedColumns: []*config.Field{ 624 | { 625 | Name: "age", 626 | Type: "int", 627 | Options: []field_option.FieldOption{ 628 | field_option.NotNull, 629 | }, 630 | }, 631 | }, 632 | }, 633 | }, 634 | }, 635 | }, 636 | } 637 | 638 | for _, tc := range testCases { 639 | schemaDiff := diff.NewSchema(tc.existingSchema, tc.targetSchema) 640 | result, err := schemaDiff.GeneratePlan() 641 | assert.Equal(t, tc.err, err) 642 | assert.ElementsMatch(t, tc.result.CreateTable, result.CreateTable) 643 | assert.ElementsMatch(t, tc.result.DropTable, result.DropTable) 644 | for name, alter := range tc.result.AlterSchema { 645 | res := result.AlterSchema[name] 646 | assert.Equal(t, alter.Name, res.Name) 647 | assert.ElementsMatch(t, alter.AddedColumns, res.AddedColumns) 648 | assert.ElementsMatch(t, alter.DroppedColumns, res.DroppedColumns) 649 | assert.ElementsMatch(t, alter.AlteredColumns, res.AlteredColumns) 650 | } 651 | } 652 | } 653 | -------------------------------------------------------------------------------- /sqlgen/dir/dir.go: -------------------------------------------------------------------------------- 1 | package dir 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/AlecAivazis/survey/v2" 8 | ) 9 | 10 | func CheckDirExists(outputDir string) (bool, error) { 11 | override := true 12 | if _, err := os.Stat(outputDir); !os.IsNotExist(err) { 13 | prompt := survey.Confirm{ 14 | Message: fmt.Sprintf("%s folder exists, existing files will be replaced. continue?", outputDir), 15 | } 16 | e := survey.AskOne(&prompt, &override) 17 | 18 | if e != nil { 19 | return false, e 20 | } 21 | } 22 | return override, nil 23 | } 24 | -------------------------------------------------------------------------------- /sqlgen/drop_index_generator.go: -------------------------------------------------------------------------------- 1 | package sqlgen 2 | 3 | import ( 4 | "gitlab.com/wartek-id/core/tools/dbgen/config" 5 | "gitlab.com/wartek-id/core/tools/dbgen/sqlgen/dialect" 6 | "gitlab.com/wartek-id/core/tools/dbgen/sqlgen/exp" 7 | "gitlab.com/wartek-id/core/tools/dbgen/sqlgen/sb" 8 | ) 9 | 10 | type DropIndexGenerator interface { 11 | Dialect() string 12 | DialectOptions() *dialect.DialectOption 13 | ExpressionSQLGenerator() exp.ExpressionSQLGenerator 14 | Generate(sb.SQLBuilder, *config.Index) 15 | } 16 | 17 | type dropIndexGenerator struct { 18 | dialect string 19 | esg exp.ExpressionSQLGenerator 20 | dialectOptions *dialect.DialectOption 21 | } 22 | 23 | func NewDropIndexGenerator(dialect string, do *dialect.DialectOption) DropIndexGenerator { 24 | return &dropIndexGenerator{ 25 | dialect: dialect, 26 | dialectOptions: do, 27 | esg: exp.NewExpressionSQLGenerator(dialect, do), 28 | } 29 | } 30 | 31 | func (dig *dropIndexGenerator) Dialect() string { 32 | return dig.dialect 33 | } 34 | 35 | func (dig *dropIndexGenerator) DialectOptions() *dialect.DialectOption { 36 | return dig.dialectOptions 37 | } 38 | 39 | func (dig *dropIndexGenerator) ExpressionSQLGenerator() exp.ExpressionSQLGenerator { 40 | return dig.esg 41 | } 42 | 43 | func (dig *dropIndexGenerator) Generate(b sb.SQLBuilder, index *config.Index) { 44 | b.Write(dig.dialectOptions.DropClause). 45 | Write(dig.dialectOptions.IndexFragment). 46 | Write(dig.dialectOptions.IfExistsFragment) 47 | 48 | dig.ExpressionSQLGenerator().LiteralExpression(b, index.Name) 49 | b.WriteRunes(dig.dialectOptions.SemiColonRune) 50 | } 51 | -------------------------------------------------------------------------------- /sqlgen/drop_index_generator_test.go: -------------------------------------------------------------------------------- 1 | package sqlgen_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "gitlab.com/wartek-id/core/tools/dbgen/config" 8 | "gitlab.com/wartek-id/core/tools/dbgen/sqlgen" 9 | "gitlab.com/wartek-id/core/tools/dbgen/sqlgen/dialect" 10 | "gitlab.com/wartek-id/core/tools/dbgen/sqlgen/sb" 11 | ) 12 | 13 | func TestDropIndexGenerator_Dialect(t *testing.T) { 14 | dial := "postgres" 15 | do := dialect.DefaultDialectOption() 16 | 17 | sqlGen := sqlgen.NewDropIndexGenerator(dial, do) 18 | assert.Equal(t, dial, sqlGen.Dialect()) 19 | } 20 | 21 | func TestDropIndexGenerator_DialectOptions(t *testing.T) { 22 | dial := "postgres" 23 | do := dialect.DefaultDialectOption() 24 | 25 | sqlGen := sqlgen.NewDropIndexGenerator(dial, do) 26 | assert.Equal(t, do, sqlGen.DialectOptions()) 27 | } 28 | 29 | func TestDropIndexGenerator_ExpressionSQLGenerator(t *testing.T) { 30 | dial := "postgres" 31 | do := dialect.DefaultDialectOption() 32 | 33 | sqlGen := sqlgen.NewDropIndexGenerator(dial, do) 34 | assert.NotNil(t, sqlGen.ExpressionSQLGenerator()) 35 | } 36 | 37 | func TestDropIndexGenerator_Generate(t *testing.T) { 38 | testCases := []struct { 39 | dialect *dialect.DialectOption 40 | input *config.Index 41 | result string 42 | }{ 43 | { 44 | dialect: dialect.DefaultDialectOption(), 45 | input: &config.Index{ 46 | Name: "index_on_name", 47 | }, 48 | result: `DROP INDEX IF EXISTS "index_on_name";`, 49 | }, 50 | { 51 | dialect: dialect.DefaultDialectOption(), 52 | input: &config.Index{ 53 | Name: "index_on_school", 54 | }, 55 | result: `DROP INDEX IF EXISTS "index_on_school";`, 56 | }, 57 | } 58 | 59 | for _, tc := range testCases { 60 | buf := sb.NewSQLBuilder() 61 | sqlGen := sqlgen.NewDropIndexGenerator("postgres", tc.dialect) 62 | sqlGen.Generate(buf, tc.input) 63 | result, err := buf.ToSQL() 64 | assert.Nil(t, err) 65 | assert.Equal(t, tc.result, result) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /sqlgen/drop_table_generator.go: -------------------------------------------------------------------------------- 1 | package sqlgen 2 | 3 | import ( 4 | "gitlab.com/wartek-id/core/tools/dbgen/config" 5 | "gitlab.com/wartek-id/core/tools/dbgen/sqlgen/dialect" 6 | "gitlab.com/wartek-id/core/tools/dbgen/sqlgen/exp" 7 | "gitlab.com/wartek-id/core/tools/dbgen/sqlgen/sb" 8 | ) 9 | 10 | type DropTableGenerator interface { 11 | Dialect() string 12 | DialectOptions() *dialect.DialectOption 13 | ExpressionSQLGenerator() exp.ExpressionSQLGenerator 14 | Generate(sb.SQLBuilder, *config.Schema) 15 | } 16 | 17 | type dropTableGenerator struct { 18 | dialect string 19 | esg exp.ExpressionSQLGenerator 20 | dialectOptions *dialect.DialectOption 21 | } 22 | 23 | func NewDropTableGenerator(dialect string, do *dialect.DialectOption) DropTableGenerator { 24 | return &dropTableGenerator{ 25 | dialect: dialect, 26 | dialectOptions: do, 27 | esg: exp.NewExpressionSQLGenerator(dialect, do), 28 | } 29 | } 30 | 31 | func (dtg *dropTableGenerator) Dialect() string { 32 | return dtg.dialect 33 | } 34 | 35 | func (dtg *dropTableGenerator) DialectOptions() *dialect.DialectOption { 36 | return dtg.dialectOptions 37 | } 38 | 39 | func (dtg *dropTableGenerator) ExpressionSQLGenerator() exp.ExpressionSQLGenerator { 40 | return dtg.esg 41 | } 42 | 43 | func (dtg *dropTableGenerator) Generate(b sb.SQLBuilder, schema *config.Schema) { 44 | b.Write(dtg.dialectOptions.DropClause). 45 | Write(dtg.dialectOptions.TableFragment). 46 | Write(dtg.dialectOptions.IfExistsFragment) 47 | 48 | dtg.ExpressionSQLGenerator().LiteralExpression(b, schema.Name) 49 | b.WriteRunes(dtg.dialectOptions.SemiColonRune) 50 | } 51 | -------------------------------------------------------------------------------- /sqlgen/drop_table_generator_test.go: -------------------------------------------------------------------------------- 1 | package sqlgen_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "gitlab.com/wartek-id/core/tools/dbgen/config" 8 | "gitlab.com/wartek-id/core/tools/dbgen/sqlgen" 9 | "gitlab.com/wartek-id/core/tools/dbgen/sqlgen/dialect" 10 | "gitlab.com/wartek-id/core/tools/dbgen/sqlgen/sb" 11 | ) 12 | 13 | func TestDropTableGenerator_Dialect(t *testing.T) { 14 | dial := "postgres" 15 | do := dialect.DefaultDialectOption() 16 | 17 | sqlGen := sqlgen.NewDropTableGenerator(dial, do) 18 | assert.Equal(t, dial, sqlGen.Dialect()) 19 | } 20 | 21 | func TestDropTableGenerator_DialectOptions(t *testing.T) { 22 | dial := "postgres" 23 | do := dialect.DefaultDialectOption() 24 | 25 | sqlGen := sqlgen.NewDropTableGenerator(dial, do) 26 | assert.Equal(t, do, sqlGen.DialectOptions()) 27 | } 28 | 29 | func TestDropTableGenerator_ExpressionSQLGenerator(t *testing.T) { 30 | dial := "postgres" 31 | do := dialect.DefaultDialectOption() 32 | 33 | sqlGen := sqlgen.NewDropTableGenerator(dial, do) 34 | assert.NotNil(t, sqlGen.ExpressionSQLGenerator()) 35 | } 36 | 37 | func TestDropTableGenerator_Generate(t *testing.T) { 38 | testCases := []struct { 39 | dialect *dialect.DialectOption 40 | input *config.Schema 41 | result string 42 | }{ 43 | { 44 | dialect: dialect.DefaultDialectOption(), 45 | input: &config.Schema{ 46 | Name: "user", 47 | }, 48 | result: `DROP TABLE IF EXISTS "user";`, 49 | }, 50 | { 51 | dialect: dialect.DefaultDialectOption(), 52 | input: &config.Schema{ 53 | Name: "school", 54 | }, 55 | result: `DROP TABLE IF EXISTS "school";`, 56 | }, 57 | } 58 | 59 | for _, tc := range testCases { 60 | buf := sb.NewSQLBuilder() 61 | sqlGen := sqlgen.NewDropTableGenerator("postgres", tc.dialect) 62 | sqlGen.Generate(buf, tc.input) 63 | result, err := buf.ToSQL() 64 | assert.Nil(t, err) 65 | assert.Equal(t, tc.result, result) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /sqlgen/exp/expression_sql_generator.go: -------------------------------------------------------------------------------- 1 | package exp 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | 7 | "gitlab.com/wartek-id/core/tools/dbgen/config" 8 | "gitlab.com/wartek-id/core/tools/dbgen/sqlgen/dialect" 9 | "gitlab.com/wartek-id/core/tools/dbgen/sqlgen/sb" 10 | ) 11 | 12 | type ExpressionSQLGenerator interface { 13 | GetTypeFragment(field *config.Field) []byte 14 | GetOptionsFragment(field *config.Field) []byte 15 | LiteralExpression(buf sb.SQLBuilder, value string) 16 | GetDefaultValue(value interface{}) []byte 17 | } 18 | 19 | type expressionSQLGenerator struct { 20 | dialect string 21 | dialectOptions *dialect.DialectOption 22 | } 23 | 24 | func NewExpressionSQLGenerator(dialect string, do *dialect.DialectOption) ExpressionSQLGenerator { 25 | return &expressionSQLGenerator{ 26 | dialect: dialect, 27 | dialectOptions: do, 28 | } 29 | } 30 | 31 | func (ex *expressionSQLGenerator) GetTypeFragment(field *config.Field) []byte { 32 | buf := sb.NewSQLBuilder() 33 | buf.Write(ex.dialectOptions.DataTypesLookup[field.Type]) 34 | if field.Limit == 0 && field.Scale == 0 || !field.Type.HasLimit() { 35 | return buf.Bytes() 36 | } 37 | 38 | buf.WriteRunes(ex.dialectOptions.LeftParenRune). 39 | WriteString(fmt.Sprint(field.Limit)) 40 | if field.Scale != 0 && field.Type.HasScale() { 41 | buf.WriteRunes(ex.dialectOptions.CommaRune, ex.dialectOptions.SpaceRune). 42 | WriteString(fmt.Sprint(field.Scale)) 43 | } 44 | buf.WriteRunes(ex.dialectOptions.RightParenRune) 45 | return buf.Bytes() 46 | } 47 | 48 | func (ex *expressionSQLGenerator) GetOptionsFragment(field *config.Field) []byte { 49 | if len(field.Options) <= 0 { 50 | return []byte{} 51 | } 52 | options := make([][]byte, 0, len(field.Options)) 53 | options = append(options, ex.dialectOptions.EmptyFragment) 54 | 55 | for _, opts := range field.Options { 56 | options = append(options, ex.dialectOptions.FieldOptionsLookup[opts]) 57 | } 58 | 59 | return bytes.Join(options, []byte(string(ex.dialectOptions.SpaceRune))) 60 | } 61 | 62 | func (ex *expressionSQLGenerator) LiteralExpression(buf sb.SQLBuilder, value string) { 63 | buf.WriteRunes(ex.dialectOptions.QuoteRune) 64 | buf.WriteString(value) 65 | buf.WriteRunes(ex.dialectOptions.QuoteRune) 66 | } 67 | 68 | func (ex *expressionSQLGenerator) GetDefaultValue(value interface{}) []byte { 69 | switch v := value.(type) { 70 | case string: 71 | buf := bytes.Buffer{} 72 | buf.WriteRune(ex.dialectOptions.StringQuoteRune) 73 | buf.WriteString(v) 74 | buf.WriteRune(ex.dialectOptions.StringQuoteRune) 75 | return buf.Bytes() 76 | } 77 | 78 | return []byte(fmt.Sprint(value)) 79 | } 80 | -------------------------------------------------------------------------------- /sqlgen/exp/expression_sql_generator_test.go: -------------------------------------------------------------------------------- 1 | package exp_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "gitlab.com/wartek-id/core/tools/dbgen/config" 8 | "gitlab.com/wartek-id/core/tools/dbgen/sqlgen/dialect" 9 | "gitlab.com/wartek-id/core/tools/dbgen/sqlgen/exp" 10 | "gitlab.com/wartek-id/core/tools/dbgen/sqlgen/sb" 11 | "gitlab.com/wartek-id/core/tools/dbgen/types/field_option" 12 | ) 13 | 14 | func TestGetTypeFragment(t *testing.T) { 15 | ex := exp.NewExpressionSQLGenerator("", dialect.DefaultDialectOption()) 16 | testCases := []struct { 17 | input config.Field 18 | result string 19 | }{ 20 | { 21 | input: config.Field{ 22 | Type: "decimal", 23 | Limit: 50, 24 | Scale: 2, 25 | }, 26 | result: "DECIMAL(50, 2)", 27 | }, 28 | { 29 | input: config.Field{ 30 | Type: "varchar", 31 | Limit: 255, 32 | }, 33 | result: "VARCHAR(255)", 34 | }, 35 | { 36 | input: config.Field{ 37 | Type: "int", 38 | }, 39 | result: "INT", 40 | }, 41 | } 42 | 43 | for _, tc := range testCases { 44 | result := ex.GetTypeFragment(&tc.input) 45 | assert.Equal(t, tc.result, string(result)) 46 | } 47 | } 48 | 49 | func TestGetOptionsFragment(t *testing.T) { 50 | ex := exp.NewExpressionSQLGenerator("", dialect.DefaultDialectOption()) 51 | testCases := []struct { 52 | input config.Field 53 | result string 54 | }{ 55 | { 56 | input: config.Field{ 57 | Options: []field_option.FieldOption{ 58 | "not null", 59 | "auto increment", 60 | }, 61 | }, 62 | result: " NOT NULL AUTO INCREMENT", 63 | }, 64 | { 65 | input: config.Field{ 66 | Options: []field_option.FieldOption{ 67 | "nullable", 68 | }, 69 | }, 70 | result: " NULL", 71 | }, 72 | { 73 | input: config.Field{}, 74 | result: "", 75 | }, 76 | } 77 | 78 | for _, tc := range testCases { 79 | result := ex.GetOptionsFragment(&tc.input) 80 | assert.Equal(t, tc.result, string(result)) 81 | } 82 | } 83 | 84 | func TestLiteralExpression(t *testing.T) { 85 | ex := exp.NewExpressionSQLGenerator("", dialect.DefaultDialectOption()) 86 | b := sb.NewSQLBuilder() 87 | 88 | ex.LiteralExpression(b, "user") 89 | assert.Equal(t, []byte("\"user\""), b.Bytes()) 90 | } 91 | 92 | func TestGetDefaultValue(t *testing.T) { 93 | ex := exp.NewExpressionSQLGenerator("", dialect.DefaultDialectOption()) 94 | 95 | result := ex.GetDefaultValue("user") 96 | assert.Equal(t, []byte("'user'"), result) 97 | 98 | result = ex.GetDefaultValue(1) 99 | assert.Equal(t, []byte("1"), result) 100 | } 101 | -------------------------------------------------------------------------------- /sqlgen/flag.go: -------------------------------------------------------------------------------- 1 | package sqlgen 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strings" 7 | "time" 8 | ) 9 | 10 | type Flag struct { 11 | OutputDirectory string 12 | OutputTarget string 13 | SkipDropTable bool 14 | } 15 | 16 | func NewFlag(dir, target string, skipDrop bool) (*Flag, error) { 17 | if strings.Contains(target, "/") { 18 | return nil, errors.New("output target cannot contain \"\\\" character") 19 | } 20 | 21 | t := time.Now() 22 | target = fmt.Sprintf("%s_%s", t.Format("20060102150405"), target) 23 | flag := Flag{ 24 | OutputDirectory: dir, 25 | OutputTarget: target, 26 | SkipDropTable: skipDrop, 27 | } 28 | return &flag, nil 29 | } 30 | -------------------------------------------------------------------------------- /sqlgen/flag_test.go: -------------------------------------------------------------------------------- 1 | package sqlgen_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "gitlab.com/wartek-id/core/tools/dbgen/sqlgen" 8 | ) 9 | 10 | func TestNewFlag(t *testing.T) { 11 | flag, err := sqlgen.NewFlag("db/migration", "migration", false) 12 | assert.NoError(t, err) 13 | assert.NotNil(t, flag) 14 | 15 | flag, err = sqlgen.NewFlag("db/migration", "migration/001", false) 16 | assert.Error(t, err, "output target cannot contain \"\\\" character") 17 | assert.Nil(t, flag) 18 | } 19 | -------------------------------------------------------------------------------- /sqlgen/generator.go: -------------------------------------------------------------------------------- 1 | package sqlgen 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | 10 | "github.com/fatih/color" 11 | "gitlab.com/wartek-id/core/tools/dbgen/config" 12 | "gitlab.com/wartek-id/core/tools/dbgen/sqlgen/dialect" 13 | "gitlab.com/wartek-id/core/tools/dbgen/sqlgen/diff" 14 | "gitlab.com/wartek-id/core/tools/dbgen/sqlgen/sb" 15 | "gitlab.com/wartek-id/core/tools/dbgen/sqlgen/schema" 16 | "gitlab.com/wartek-id/core/tools/dbgen/sqlgen/step" 17 | ) 18 | 19 | const ( 20 | DefaultMigrationExt = ".sql" 21 | DefaultDialect = "postgres" 22 | FullSchemaMigrationFilename = "temp/fullschema/migration.sql" 23 | ) 24 | 25 | var SectionSeparator = []byte("\n\n") 26 | 27 | type SqlGenerator struct { 28 | dbUpFilename string 29 | dbDownFilename string 30 | flag *Flag 31 | generators *generators 32 | schemas []*config.Schema 33 | crawler schema.Schema 34 | dialect string 35 | dialectOption *dialect.DialectOption 36 | } 37 | 38 | type generators struct { 39 | ctg CreateTableGenerator 40 | cig CreateIndexGenerator 41 | atg AlterTableGenerator 42 | dig DropIndexGenerator 43 | dtg DropTableGenerator 44 | } 45 | 46 | func NewGenerator(crawler schema.Schema, schemas []*config.Schema, flag *Flag) *SqlGenerator { 47 | dbUpFilename, dbDownFilename := getTargetPath(flag.OutputDirectory, flag.OutputTarget) 48 | 49 | return &SqlGenerator{ 50 | dbUpFilename: dbUpFilename, 51 | dbDownFilename: dbDownFilename, 52 | schemas: schemas, 53 | flag: flag, 54 | generators: initGenerator(DefaultDialect, dialect.DefaultDialectOption()), 55 | crawler: crawler, 56 | dialect: DefaultDialect, 57 | dialectOption: dialect.DefaultDialectOption(), 58 | } 59 | } 60 | 61 | func initGenerator(dialect string, do *dialect.DialectOption) *generators { 62 | return &generators{ 63 | ctg: NewCreateTableGenerator(dialect, do), 64 | cig: NewCreateIndexGenerator(dialect, do), 65 | atg: NewAlterTableGenerator(dialect, do), 66 | dig: NewDropIndexGenerator(dialect, do), 67 | dtg: NewDropTableGenerator(dialect, do), 68 | } 69 | } 70 | 71 | func getTargetPath(dir, target string) (string, string) { 72 | ext := filepath.Ext(target) 73 | if ext != "" { 74 | target = strings.Replace(target, ext, "", -1) 75 | } else { 76 | ext = DefaultMigrationExt 77 | } 78 | 79 | upFilename := fmt.Sprintf("%s/%s.up%s", dir, target, ext) 80 | downFilename := fmt.Sprintf("%s/%s.down%s", dir, target, ext) 81 | return upFilename, downFilename 82 | } 83 | 84 | func (gen *SqlGenerator) CreateTableGenerator() CreateTableGenerator { 85 | return gen.generators.ctg 86 | } 87 | 88 | func (gen *SqlGenerator) CreateIndexGenerator() CreateIndexGenerator { 89 | return gen.generators.cig 90 | } 91 | 92 | func (gen *SqlGenerator) AlterTableGenerator() AlterTableGenerator { 93 | return gen.generators.atg 94 | } 95 | 96 | func (gen *SqlGenerator) DropIndexGenerator() DropIndexGenerator { 97 | return gen.generators.dig 98 | } 99 | 100 | func (gen *SqlGenerator) DropTableGenerator() DropTableGenerator { 101 | return gen.generators.dtg 102 | } 103 | 104 | func (gen *SqlGenerator) Generate() error { 105 | currentSchemas, err := gen.crawler.GetSchemas() 106 | if err != nil { 107 | return err 108 | } 109 | 110 | planner := diff.NewSchema(currentSchemas, gen.schemas) 111 | migrationPlanner, err := planner.GeneratePlan() 112 | if err != nil { 113 | return err 114 | } 115 | 116 | err = gen.UpMigration(migrationPlanner) 117 | if err != nil { 118 | return err 119 | } 120 | 121 | fmt.Println() 122 | err = gen.DownMigration(migrationPlanner) 123 | if err != nil { 124 | return err 125 | } 126 | 127 | fmt.Println() 128 | err = gen.FullSchemaMigration() 129 | if err != nil { 130 | return err 131 | } 132 | 133 | fmt.Println("\nDatabase Migration Generation Completed.") 134 | return nil 135 | } 136 | 137 | func (gen *SqlGenerator) FullSchemaMigration() error { 138 | fmt.Println("🚀 Generating up full schema migration file") 139 | 140 | createTables := gen.GenerateCreateTables(gen.schemas) 141 | err := gen.Writer(FullSchemaMigrationFilename, getContents(createTables)) 142 | if err != nil { 143 | fmt.Println(color.RedString("Failed")) 144 | return err 145 | } 146 | fmt.Println(color.GreenString("Succeeded")) 147 | return nil 148 | } 149 | 150 | func (gen *SqlGenerator) UpMigration(plan *step.MigrationPlanner) error { 151 | fmt.Println("🚀 Generating up database migration files") 152 | fmt.Printf("Target file: %s\n", color.HiBlueString(gen.dbUpFilename)) 153 | 154 | createTables := gen.GenerateCreateTables(plan.CreateTable) 155 | alterTables := gen.AlterTableUp(plan.AlterSchema) 156 | dropTables := []byte{} 157 | if !gen.flag.SkipDropTable { 158 | dropTables = gen.GenerateDropTables(plan.DropTable) 159 | } 160 | 161 | content := getContents(createTables, dropTables, alterTables) 162 | if len(bytes.TrimSpace(content)) == 0 { 163 | fmt.Println(color.YellowString("No changes being detected, skipping...")) 164 | return nil 165 | } 166 | 167 | if gen.dialectOption.SupportTransaction { 168 | content = getContents( 169 | gen.dialectOption.BeginClause, content, gen.dialectOption.CommitClause, 170 | ) 171 | } 172 | err := gen.Writer(gen.dbUpFilename, content) 173 | if err != nil { 174 | fmt.Println(color.RedString("Failed")) 175 | return err 176 | } 177 | fmt.Println(color.GreenString("Succeeded")) 178 | return nil 179 | } 180 | 181 | func (gen *SqlGenerator) AlterTableUp(alterSchemas map[string]*step.AlterSchema) []byte { 182 | contents := make([][]byte, 0) 183 | for _, as := range alterSchemas { 184 | diBuf := sb.NewSQLBuilder() 185 | for _, idx := range as.DroppedIndices { 186 | gen.DropIndexGenerator().Generate(diBuf, idx) 187 | diBuf.WriteNewLine() 188 | } 189 | 190 | atBuf := sb.NewSQLBuilder() 191 | gen.AlterTableGenerator().Generate(atBuf, as) 192 | 193 | aiBuf := sb.NewSQLBuilder() 194 | for _, idx := range as.AddedIndices { 195 | gen.CreateIndexGenerator().Generate(aiBuf, as.Name, idx) 196 | aiBuf.WriteNewLine() 197 | } 198 | 199 | contents = append(contents, getContents(atBuf.Bytes(), diBuf.Bytes(), aiBuf.Bytes())) 200 | } 201 | return bytes.Join(contents, SectionSeparator) 202 | } 203 | 204 | func (gen *SqlGenerator) AlterTableDown(alterSchemas map[string]*step.AlterSchema) []byte { 205 | contents := make([][]byte, 0) 206 | for _, as := range alterSchemas { 207 | aiBuf := sb.NewSQLBuilder() 208 | for _, idx := range as.AddedIndices { 209 | gen.DropIndexGenerator().Generate(aiBuf, idx) 210 | aiBuf.WriteNewLine() 211 | } 212 | 213 | atBuf := sb.NewSQLBuilder() 214 | gen.AlterTableGenerator().Rollback(atBuf, as) 215 | 216 | diBuf := sb.NewSQLBuilder() 217 | for _, idx := range as.DroppedIndices { 218 | gen.CreateIndexGenerator().Generate(diBuf, as.Name, idx) 219 | diBuf.WriteNewLine() 220 | } 221 | 222 | contents = append(contents, getContents(atBuf.Bytes(), diBuf.Bytes(), aiBuf.Bytes())) 223 | } 224 | return bytes.Join(contents, SectionSeparator) 225 | } 226 | 227 | func (gen *SqlGenerator) DownMigration(plan *step.MigrationPlanner) error { 228 | fmt.Println("🚀 Generating down database migration files") 229 | fmt.Printf("Target file: %s\n", color.HiBlueString(gen.dbDownFilename)) 230 | 231 | createTableDown := gen.GenerateDropTables(plan.CreateTable) 232 | alterTables := gen.AlterTableDown(plan.AlterSchema) 233 | dropTableDown := []byte{} 234 | if !gen.flag.SkipDropTable { 235 | dropTableDown = gen.GenerateCreateTables(plan.DropTable) 236 | } 237 | 238 | content := getContents(createTableDown, dropTableDown, alterTables) 239 | if len(bytes.TrimSpace(content)) == 0 { 240 | fmt.Println(color.YellowString("No changes being detected, skipping...")) 241 | return nil 242 | } 243 | 244 | if gen.dialectOption.SupportTransaction { 245 | content = getContents( 246 | gen.dialectOption.BeginClause, content, gen.dialectOption.CommitClause, 247 | ) 248 | } 249 | err := gen.Writer(gen.dbDownFilename, content) 250 | if err != nil { 251 | fmt.Println(color.RedString("Failed")) 252 | return err 253 | } 254 | fmt.Println(color.GreenString("Succeeded")) 255 | return nil 256 | } 257 | 258 | func (gen *SqlGenerator) Writer(filename string, content []byte) error { 259 | err := os.MkdirAll(filepath.Dir(filename), 0755) 260 | if err != nil { 261 | return err 262 | } 263 | 264 | err = os.WriteFile(filename, content, 0644) 265 | if err != nil { 266 | return err 267 | } 268 | 269 | return nil 270 | } 271 | 272 | func (gen *SqlGenerator) GenerateCreateTables(schemas []*config.Schema) []byte { 273 | sb := sb.NewSQLBuilder() 274 | for _, schema := range schemas { 275 | gen.CreateTableGenerator().Generate(sb, schema) 276 | sb.WriteNewLine() 277 | sb.WriteNewLine() 278 | 279 | for _, idx := range schema.Index { 280 | gen.CreateIndexGenerator().Generate(sb, schema.Name, idx) 281 | sb.WriteNewLine() 282 | } 283 | if len(schema.Index) > 0 { 284 | sb.WriteNewLine() 285 | } 286 | } 287 | 288 | return bytes.TrimSpace(sb.Bytes()) 289 | } 290 | 291 | func (gen *SqlGenerator) GenerateDropTables(schemas []*config.Schema) []byte { 292 | sb := sb.NewSQLBuilder() 293 | for _, schema := range schemas { 294 | gen.DropTableGenerator().Generate(sb, schema) 295 | sb.WriteNewLine() 296 | sb.WriteNewLine() 297 | } 298 | 299 | return bytes.TrimSpace(sb.Bytes()) 300 | } 301 | 302 | func getContents(contents ...[]byte) []byte { 303 | container := make([][]byte, 0) 304 | 305 | for _, content := range contents { 306 | content = bytes.TrimSpace(content) 307 | if len(content) > 0 { 308 | container = append(container, content) 309 | } 310 | } 311 | 312 | return bytes.TrimSpace(bytes.Join(container, SectionSeparator)) 313 | } 314 | -------------------------------------------------------------------------------- /sqlgen/generator_test.go: -------------------------------------------------------------------------------- 1 | package sqlgen_test 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/golang/mock/gomock" 11 | "github.com/stretchr/testify/assert" 12 | "gitlab.com/wartek-id/core/tools/dbgen/config" 13 | "gitlab.com/wartek-id/core/tools/dbgen/sqlgen" 14 | mock_schema "gitlab.com/wartek-id/core/tools/dbgen/sqlgen/mocks/schema" 15 | "gitlab.com/wartek-id/core/tools/dbgen/types/field_option" 16 | ) 17 | 18 | func TestSqlGenerator_Generate(t *testing.T) { 19 | ctrl := gomock.NewController(t) 20 | defer ctrl.Finish() 21 | 22 | target := filepath.Join(t.TempDir(), "generator") 23 | upTarget := fmt.Sprintf("%s.up.sql", target) 24 | downTarget := fmt.Sprintf("%s.down.sql", target) 25 | mockCrawler := mock_schema.NewMockSchema(ctrl) 26 | mockCrawler.EXPECT().GetSchemas().Return([]*config.Schema{ 27 | { 28 | Name: "documents", 29 | Fields: []*config.Field{ 30 | { 31 | Name: "id", 32 | Type: "bigserial", 33 | Options: []field_option.FieldOption{ 34 | field_option.PrimaryKey, 35 | }, 36 | }, 37 | { 38 | Name: "name", 39 | Type: "varchar", 40 | Limit: 200, 41 | }, 42 | { 43 | Name: "release_date", 44 | Type: "timestamp", 45 | }, 46 | { 47 | Name: "created_at", 48 | Type: "timestamp", 49 | }, 50 | }, 51 | Index: []*config.Index{ 52 | { 53 | Name: "index_document_on_name", 54 | Fields: []*config.IndexField{ 55 | { 56 | Column: "name", 57 | }, 58 | }, 59 | Unique: true, 60 | }, 61 | }, 62 | }, 63 | { 64 | Name: "example", 65 | Fields: []*config.Field{ 66 | { 67 | Name: "id", 68 | Type: "bigserial", 69 | Options: []field_option.FieldOption{ 70 | field_option.PrimaryKey, 71 | }, 72 | }, 73 | { 74 | Name: "name", 75 | Type: "varchar", 76 | Limit: 100, 77 | }, 78 | }, 79 | }, 80 | }, nil).AnyTimes() 81 | 82 | gen := sqlgen.NewGenerator(mockCrawler, []*config.Schema{ 83 | { 84 | Name: "user", 85 | Fields: []*config.Field{ 86 | { 87 | Name: "id", 88 | Type: "bigserial", 89 | }, 90 | { 91 | Name: "name", 92 | Type: "varchar", 93 | Limit: 200, 94 | }, 95 | { 96 | Name: "created_at", 97 | Type: "timestamp", 98 | }, 99 | }, 100 | Index: []*config.Index{ 101 | { 102 | Name: "index_user_on_name", 103 | Fields: []*config.IndexField{ 104 | { 105 | Column: "name", 106 | }, 107 | }, 108 | Unique: true, 109 | }, 110 | }, 111 | }, 112 | { 113 | Name: "documents", 114 | Fields: []*config.Field{ 115 | { 116 | Name: "id", 117 | Type: "bigserial", 118 | Options: []field_option.FieldOption{ 119 | field_option.PrimaryKey, 120 | }, 121 | }, 122 | { 123 | Name: "name", 124 | Type: "varchar", 125 | Limit: 50, 126 | Options: []field_option.FieldOption{ 127 | field_option.NotNull, 128 | }, 129 | }, 130 | { 131 | Name: "approval", 132 | Type: "varchar", 133 | Limit: 50, 134 | }, 135 | { 136 | Name: "created_at", 137 | Type: "timestamp", 138 | Options: []field_option.FieldOption{ 139 | field_option.NotNull, 140 | }, 141 | }, 142 | { 143 | Name: "updated_at", 144 | Type: "timestamp", 145 | Options: []field_option.FieldOption{ 146 | field_option.NotNull, 147 | }, 148 | }, 149 | }, 150 | Index: []*config.Index{ 151 | { 152 | Name: "index_document_on_name_approval", 153 | Fields: []*config.IndexField{ 154 | { 155 | Column: "name", 156 | Order: "ASC", 157 | }, 158 | { 159 | Column: "approval", 160 | Order: "ASC", 161 | }, 162 | }, 163 | Unique: true, 164 | }, 165 | }, 166 | }, 167 | }, &sqlgen.Flag{OutputTarget: target, SkipDropTable: false}) 168 | err := gen.Generate() 169 | assert.NoError(t, err) 170 | 171 | upMigration, err := os.ReadFile(upTarget) 172 | comLen := strings.Split(string(upMigration), ";") 173 | assert.NoError(t, err) 174 | assert.Len(t, comLen, 9) 175 | assert.Contains(t, string(upMigration), "BEGIN;") 176 | assert.Contains(t, string(upMigration), "CREATE TABLE IF NOT EXISTS \"user\" (\n\t\"id\" BIGSERIAL,\n\t\"name\" VARCHAR(200),\n\t\"created_at\" TIMESTAMP\n);") 177 | assert.Contains(t, string(upMigration), "CREATE UNIQUE INDEX IF NOT EXISTS \"index_user_on_name\" ON \"user\"(\"name\");") 178 | assert.Contains(t, string(upMigration), "ALTER TABLE IF EXISTS \"documents\"\n") 179 | assert.Contains(t, string(upMigration), "\tADD COLUMN \"updated_at\" TIMESTAMP NOT NULL") 180 | assert.Contains(t, string(upMigration), "\tADD COLUMN \"approval\" VARCHAR(50)") 181 | assert.Contains(t, string(upMigration), "\tDROP COLUMN \"release_date\"") 182 | assert.Contains(t, string(upMigration), "\tALTER COLUMN \"created_at\" SET NOT NULL") 183 | assert.Contains(t, string(upMigration), "\tALTER COLUMN \"name\" SET DATA TYPE VARCHAR(50)") 184 | assert.Contains(t, string(upMigration), "\tALTER COLUMN \"name\" SET NOT NULL") 185 | assert.Contains(t, string(upMigration), "DROP INDEX IF EXISTS \"index_document_on_name\";") 186 | assert.Contains(t, string(upMigration), "CREATE UNIQUE INDEX IF NOT EXISTS \"index_document_on_name_approval\" ON \"documents\"(\"name\" ASC, \"approval\" ASC);") 187 | assert.Contains(t, string(upMigration), "DROP TABLE IF EXISTS \"example\";") 188 | assert.Contains(t, string(upMigration), "COMMIT;") 189 | 190 | downMigration, err := os.ReadFile(downTarget) 191 | comLen = strings.Split(string(downMigration), ";") 192 | assert.NoError(t, err) 193 | assert.Len(t, comLen, 8) 194 | assert.Contains(t, string(downMigration), "BEGIN;") 195 | assert.Contains(t, string(downMigration), "DROP TABLE IF EXISTS \"user\";") 196 | assert.Contains(t, string(downMigration), "ALTER TABLE IF EXISTS \"documents\"\n") 197 | assert.Contains(t, string(downMigration), "\tDROP COLUMN \"approval\"") 198 | assert.Contains(t, string(downMigration), "\tDROP COLUMN \"updated_at\"") 199 | assert.Contains(t, string(downMigration), "\tADD COLUMN \"release_date\" TIMESTAMP") 200 | assert.Contains(t, string(downMigration), "\tALTER COLUMN \"name\" DROP NOT NULL") 201 | assert.Contains(t, string(downMigration), "\tALTER COLUMN \"name\" SET DATA TYPE VARCHAR(200)") 202 | assert.Contains(t, string(downMigration), "\tALTER COLUMN \"created_at\" DROP NOT NULL") 203 | assert.Contains(t, string(downMigration), "CREATE UNIQUE INDEX IF NOT EXISTS \"index_document_on_name\" ON \"documents\"(\"name\");") 204 | assert.Contains(t, string(downMigration), "DROP INDEX IF EXISTS \"index_document_on_name_approval\";") 205 | assert.Contains(t, string(downMigration), "CREATE TABLE IF NOT EXISTS \"example\" (\n\t\"id\" BIGSERIAL PRIMARY KEY,\n\t\"name\" VARCHAR(100)\n);") 206 | assert.Contains(t, string(downMigration), "COMMIT;") 207 | } 208 | 209 | func TestSqlGenerator_GenerateNoDrop(t *testing.T) { 210 | ctrl := gomock.NewController(t) 211 | defer ctrl.Finish() 212 | 213 | target := filepath.Join(t.TempDir(), "generator") 214 | upTarget := fmt.Sprintf("%s.up.sql", target) 215 | downTarget := fmt.Sprintf("%s.down.sql", target) 216 | mockCrawler := mock_schema.NewMockSchema(ctrl) 217 | mockCrawler.EXPECT().GetSchemas().Return([]*config.Schema{ 218 | { 219 | Name: "documents", 220 | Fields: []*config.Field{ 221 | { 222 | Name: "id", 223 | Type: "bigserial", 224 | Options: []field_option.FieldOption{ 225 | field_option.PrimaryKey, 226 | }, 227 | }, 228 | { 229 | Name: "name", 230 | Type: "varchar", 231 | Limit: 200, 232 | }, 233 | { 234 | Name: "release_date", 235 | Type: "timestamp", 236 | }, 237 | { 238 | Name: "created_at", 239 | Type: "timestamp", 240 | }, 241 | }, 242 | Index: []*config.Index{ 243 | { 244 | Name: "index_document_on_name", 245 | Fields: []*config.IndexField{ 246 | { 247 | Column: "name", 248 | }, 249 | }, 250 | Unique: true, 251 | }, 252 | }, 253 | }, 254 | { 255 | Name: "example", 256 | Fields: []*config.Field{ 257 | { 258 | Name: "id", 259 | Type: "bigserial", 260 | Options: []field_option.FieldOption{ 261 | field_option.PrimaryKey, 262 | }, 263 | }, 264 | { 265 | Name: "name", 266 | Type: "varchar", 267 | Limit: 100, 268 | }, 269 | }, 270 | }, 271 | }, nil).AnyTimes() 272 | 273 | gen := sqlgen.NewGenerator(mockCrawler, []*config.Schema{ 274 | { 275 | Name: "user", 276 | Fields: []*config.Field{ 277 | { 278 | Name: "id", 279 | Type: "bigserial", 280 | }, 281 | { 282 | Name: "name", 283 | Type: "varchar", 284 | Limit: 200, 285 | }, 286 | { 287 | Name: "created_at", 288 | Type: "timestamp", 289 | }, 290 | }, 291 | Index: []*config.Index{ 292 | { 293 | Name: "index_user_on_name", 294 | Fields: []*config.IndexField{ 295 | { 296 | Column: "name", 297 | }, 298 | }, 299 | Unique: true, 300 | }, 301 | }, 302 | }, 303 | { 304 | Name: "documents", 305 | Fields: []*config.Field{ 306 | { 307 | Name: "id", 308 | Type: "bigserial", 309 | Options: []field_option.FieldOption{ 310 | field_option.PrimaryKey, 311 | }, 312 | }, 313 | { 314 | Name: "name", 315 | Type: "varchar", 316 | Limit: 50, 317 | Options: []field_option.FieldOption{ 318 | field_option.NotNull, 319 | }, 320 | }, 321 | { 322 | Name: "approval", 323 | Type: "varchar", 324 | Limit: 50, 325 | }, 326 | { 327 | Name: "created_at", 328 | Type: "timestamp", 329 | Options: []field_option.FieldOption{ 330 | field_option.NotNull, 331 | }, 332 | }, 333 | { 334 | Name: "updated_at", 335 | Type: "timestamp", 336 | Options: []field_option.FieldOption{ 337 | field_option.NotNull, 338 | }, 339 | }, 340 | }, 341 | Index: []*config.Index{ 342 | { 343 | Name: "index_document_on_name_approval", 344 | Fields: []*config.IndexField{ 345 | { 346 | Column: "name", 347 | Order: "ASC", 348 | }, 349 | { 350 | Column: "approval", 351 | Order: "ASC", 352 | }, 353 | }, 354 | Unique: true, 355 | }, 356 | }, 357 | }, 358 | }, &sqlgen.Flag{OutputTarget: target, SkipDropTable: true}) 359 | err := gen.Generate() 360 | assert.NoError(t, err) 361 | 362 | upMigration, err := os.ReadFile(upTarget) 363 | comLen := strings.Split(string(upMigration), ";") 364 | assert.NoError(t, err) 365 | assert.Len(t, comLen, 8) 366 | assert.Contains(t, string(upMigration), "BEGIN;") 367 | assert.Contains(t, string(upMigration), "CREATE TABLE IF NOT EXISTS \"user\" (\n\t\"id\" BIGSERIAL,\n\t\"name\" VARCHAR(200),\n\t\"created_at\" TIMESTAMP\n);") 368 | assert.Contains(t, string(upMigration), "CREATE UNIQUE INDEX IF NOT EXISTS \"index_user_on_name\" ON \"user\"(\"name\");") 369 | assert.Contains(t, string(upMigration), "ALTER TABLE IF EXISTS \"documents\"\n") 370 | assert.Contains(t, string(upMigration), "\tADD COLUMN \"updated_at\" TIMESTAMP NOT NULL") 371 | assert.Contains(t, string(upMigration), "\tADD COLUMN \"approval\" VARCHAR(50)") 372 | assert.Contains(t, string(upMigration), "\tDROP COLUMN \"release_date\"") 373 | assert.Contains(t, string(upMigration), "\tALTER COLUMN \"created_at\" SET NOT NULL") 374 | assert.Contains(t, string(upMigration), "\tALTER COLUMN \"name\" SET DATA TYPE VARCHAR(50)") 375 | assert.Contains(t, string(upMigration), "\tALTER COLUMN \"name\" SET NOT NULL") 376 | assert.Contains(t, string(upMigration), "DROP INDEX IF EXISTS \"index_document_on_name\";") 377 | assert.Contains(t, string(upMigration), "CREATE UNIQUE INDEX IF NOT EXISTS \"index_document_on_name_approval\" ON \"documents\"(\"name\" ASC, \"approval\" ASC);") 378 | assert.NotContains(t, string(upMigration), "DROP TABLE IF EXISTS \"example\";") 379 | assert.Contains(t, string(upMigration), "COMMIT;") 380 | 381 | downMigration, err := os.ReadFile(downTarget) 382 | comLen = strings.Split(string(downMigration), ";") 383 | assert.NoError(t, err) 384 | assert.Len(t, comLen, 7) 385 | assert.Contains(t, string(downMigration), "BEGIN;") 386 | assert.Contains(t, string(downMigration), "DROP TABLE IF EXISTS \"user\";") 387 | assert.Contains(t, string(downMigration), "ALTER TABLE IF EXISTS \"documents\"\n") 388 | assert.Contains(t, string(downMigration), "\tDROP COLUMN \"approval\"") 389 | assert.Contains(t, string(downMigration), "\tDROP COLUMN \"updated_at\"") 390 | assert.Contains(t, string(downMigration), "\tADD COLUMN \"release_date\" TIMESTAMP") 391 | assert.Contains(t, string(downMigration), "\tALTER COLUMN \"name\" DROP NOT NULL") 392 | assert.Contains(t, string(downMigration), "\tALTER COLUMN \"name\" SET DATA TYPE VARCHAR(200)") 393 | assert.Contains(t, string(downMigration), "\tALTER COLUMN \"created_at\" DROP NOT NULL") 394 | assert.Contains(t, string(downMigration), "CREATE UNIQUE INDEX IF NOT EXISTS \"index_document_on_name\" ON \"documents\"(\"name\");") 395 | assert.Contains(t, string(downMigration), "DROP INDEX IF EXISTS \"index_document_on_name_approval\";") 396 | assert.NotContains(t, string(downMigration), "CREATE TABLE IF NOT EXISTS \"example\" (\n\t\"id\" BIGSERIAL PRIMARY KEY,\n\t\"name\" VARCHAR(100)\n);") 397 | assert.Contains(t, string(downMigration), "COMMIT;") 398 | } 399 | 400 | func TestSqlGenerator_GenerateNoChanges(t *testing.T) { 401 | ctrl := gomock.NewController(t) 402 | defer ctrl.Finish() 403 | 404 | target := filepath.Join(t.TempDir(), "generator") 405 | upTarget := fmt.Sprintf("%s.up.sql", target) 406 | downTarget := fmt.Sprintf("%s.down.sql", target) 407 | mockCrawler := mock_schema.NewMockSchema(ctrl) 408 | mockCrawler.EXPECT().GetSchemas().Return([]*config.Schema{ 409 | { 410 | Name: "documents", 411 | Fields: []*config.Field{ 412 | { 413 | Name: "id", 414 | Type: "bigserial", 415 | Options: []field_option.FieldOption{ 416 | field_option.PrimaryKey, 417 | }, 418 | }, 419 | { 420 | Name: "name", 421 | Type: "varchar", 422 | Limit: 200, 423 | }, 424 | { 425 | Name: "release_date", 426 | Type: "timestamp", 427 | }, 428 | { 429 | Name: "created_at", 430 | Type: "timestamp", 431 | }, 432 | }, 433 | Index: []*config.Index{ 434 | { 435 | Name: "index_document_on_name", 436 | Fields: []*config.IndexField{ 437 | { 438 | Column: "name", 439 | }, 440 | }, 441 | Unique: true, 442 | }, 443 | }, 444 | }, 445 | { 446 | Name: "example", 447 | Fields: []*config.Field{ 448 | { 449 | Name: "id", 450 | Type: "bigserial", 451 | Options: []field_option.FieldOption{ 452 | field_option.PrimaryKey, 453 | }, 454 | }, 455 | { 456 | Name: "name", 457 | Type: "varchar", 458 | Limit: 100, 459 | }, 460 | }, 461 | }, 462 | }, nil).AnyTimes() 463 | 464 | gen := sqlgen.NewGenerator(mockCrawler, []*config.Schema{ 465 | { 466 | Name: "documents", 467 | Fields: []*config.Field{ 468 | { 469 | Name: "id", 470 | Type: "bigserial", 471 | Options: []field_option.FieldOption{ 472 | field_option.PrimaryKey, 473 | }, 474 | }, 475 | { 476 | Name: "name", 477 | Type: "varchar", 478 | Limit: 200, 479 | }, 480 | { 481 | Name: "release_date", 482 | Type: "timestamp", 483 | }, 484 | { 485 | Name: "created_at", 486 | Type: "timestamp", 487 | }, 488 | }, 489 | Index: []*config.Index{ 490 | { 491 | Name: "index_document_on_name", 492 | Fields: []*config.IndexField{ 493 | { 494 | Column: "name", 495 | }, 496 | }, 497 | Unique: true, 498 | }, 499 | }, 500 | }, 501 | { 502 | Name: "example", 503 | Fields: []*config.Field{ 504 | { 505 | Name: "id", 506 | Type: "bigserial", 507 | Options: []field_option.FieldOption{ 508 | field_option.PrimaryKey, 509 | }, 510 | }, 511 | { 512 | Name: "name", 513 | Type: "varchar", 514 | Limit: 100, 515 | }, 516 | }, 517 | }, 518 | }, &sqlgen.Flag{OutputTarget: target, SkipDropTable: false}) 519 | err := gen.Generate() 520 | assert.NoError(t, err) 521 | 522 | upMig, err := os.ReadFile(upTarget) 523 | assert.Error(t, err, "not found") 524 | assert.Nil(t, upMig) 525 | 526 | downMig, err := os.ReadFile(downTarget) 527 | assert.Error(t, err, "not found") 528 | assert.Nil(t, downMig) 529 | } 530 | 531 | func TestSqlGenerator_CreateTableGenerator(t *testing.T) { 532 | gen := sqlgen.NewGenerator(nil, []*config.Schema{}, &sqlgen.Flag{OutputTarget: "target"}) 533 | assert.NotNil(t, gen.CreateTableGenerator()) 534 | } 535 | 536 | func TestSqlGenerator_CreateIndexGenerator(t *testing.T) { 537 | gen := sqlgen.NewGenerator(nil, []*config.Schema{}, &sqlgen.Flag{OutputTarget: "target"}) 538 | assert.NotNil(t, gen.CreateIndexGenerator()) 539 | } 540 | 541 | func TestSqlGenerator_AlterTableGenerator(t *testing.T) { 542 | gen := sqlgen.NewGenerator(nil, []*config.Schema{}, &sqlgen.Flag{OutputTarget: "target"}) 543 | assert.NotNil(t, gen.AlterTableGenerator()) 544 | } 545 | 546 | func TestSqlGenerator_DropIndexGenerator(t *testing.T) { 547 | gen := sqlgen.NewGenerator(nil, []*config.Schema{}, &sqlgen.Flag{OutputTarget: "target"}) 548 | assert.NotNil(t, gen.DropIndexGenerator()) 549 | } 550 | 551 | func TestSqlGenerator_DropTableGenerator(t *testing.T) { 552 | gen := sqlgen.NewGenerator(nil, []*config.Schema{}, &sqlgen.Flag{OutputTarget: "target"}) 553 | assert.NotNil(t, gen.DropTableGenerator()) 554 | } 555 | -------------------------------------------------------------------------------- /sqlgen/json/schema.go: -------------------------------------------------------------------------------- 1 | package json 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "os" 8 | "path/filepath" 9 | 10 | "github.com/fatih/color" 11 | "gitlab.com/wartek-id/core/tools/dbgen/sqlgen/schema" 12 | ) 13 | 14 | type JsonSchemasGenerator struct { 15 | schemas schema.Schema 16 | } 17 | 18 | func NewSchemasGenerator(schemas schema.Schema) *JsonSchemasGenerator { 19 | return &JsonSchemasGenerator{ 20 | schemas: schemas, 21 | } 22 | } 23 | 24 | func (s *JsonSchemasGenerator) GenerateBySchemas(outputDir string) error { 25 | err := os.MkdirAll(outputDir, 0755) 26 | if err != nil { 27 | return err 28 | } 29 | 30 | currentSchemas, err := s.schemas.GetSchemas() 31 | if err != nil { 32 | return err 33 | } 34 | 35 | for _, s := range currentSchemas { 36 | fmt.Println("\nDumping db: " + s.Name) 37 | filename := filepath.Join(outputDir, s.Name+".json") 38 | file, err := json.MarshalIndent(s, "", " ") 39 | if err != nil { 40 | return err 41 | } 42 | 43 | err = ioutil.WriteFile(filename, file, 0644) 44 | if err != nil { 45 | return err 46 | } 47 | fmt.Printf(color.GreenString("Succeed dumping db: %s, target file: %s\n"), color.HiBlueString(s.Name), color.HiBlueString(filename)) 48 | } 49 | return nil 50 | } 51 | -------------------------------------------------------------------------------- /sqlgen/json/schema_test.go: -------------------------------------------------------------------------------- 1 | package json_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/golang/mock/gomock" 7 | "github.com/stretchr/testify/assert" 8 | "gitlab.com/wartek-id/core/tools/dbgen/config" 9 | "gitlab.com/wartek-id/core/tools/dbgen/sqlgen/json" 10 | mock_schema "gitlab.com/wartek-id/core/tools/dbgen/sqlgen/mocks/schema" 11 | "gitlab.com/wartek-id/core/tools/dbgen/types/field_option" 12 | ) 13 | 14 | func TestGenerateBySchemas(t *testing.T) { 15 | ctrl := gomock.NewController(t) 16 | defer ctrl.Finish() 17 | 18 | outputDir := t.TempDir() 19 | mockCrawler := mock_schema.NewMockSchema(ctrl) 20 | mockCrawler.EXPECT().GetSchemas().Return([]*config.Schema{ 21 | { 22 | Name: "example", 23 | Fields: []*config.Field{ 24 | { 25 | Name: "id", 26 | Type: "bigserial", 27 | Options: []field_option.FieldOption{ 28 | field_option.PrimaryKey, 29 | }, 30 | }, 31 | { 32 | Name: "name", 33 | Type: "varchar", 34 | Limit: 100, 35 | }, 36 | }, 37 | }, 38 | }, nil).AnyTimes() 39 | gen := json.NewSchemasGenerator(mockCrawler) 40 | err := gen.GenerateBySchemas(outputDir) 41 | 42 | assert.NoError(t, err) 43 | } 44 | -------------------------------------------------------------------------------- /sqlgen/mocks/schema/schema.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: gitlab.com/wartek-id/core/tools/dbgen/sqlgen/schema (interfaces: Schema) 3 | 4 | // Package mock_schema is a generated GoMock package. 5 | package mock_schema 6 | 7 | import ( 8 | reflect "reflect" 9 | 10 | gomock "github.com/golang/mock/gomock" 11 | config "gitlab.com/wartek-id/core/tools/dbgen/config" 12 | schema "gitlab.com/wartek-id/core/tools/dbgen/sqlgen/schema" 13 | ) 14 | 15 | // MockSchema is a mock of Schema interface. 16 | type MockSchema struct { 17 | ctrl *gomock.Controller 18 | recorder *MockSchemaMockRecorder 19 | } 20 | 21 | // MockSchemaMockRecorder is the mock recorder for MockSchema. 22 | type MockSchemaMockRecorder struct { 23 | mock *MockSchema 24 | } 25 | 26 | // NewMockSchema creates a new mock instance. 27 | func NewMockSchema(ctrl *gomock.Controller) *MockSchema { 28 | mock := &MockSchema{ctrl: ctrl} 29 | mock.recorder = &MockSchemaMockRecorder{mock} 30 | return mock 31 | } 32 | 33 | // EXPECT returns an object that allows the caller to indicate expected use. 34 | func (m *MockSchema) EXPECT() *MockSchemaMockRecorder { 35 | return m.recorder 36 | } 37 | 38 | // GetFields mocks base method. 39 | func (m *MockSchema) GetFields(arg0 string) ([]*config.Field, error) { 40 | m.ctrl.T.Helper() 41 | ret := m.ctrl.Call(m, "GetFields", arg0) 42 | ret0, _ := ret[0].([]*config.Field) 43 | ret1, _ := ret[1].(error) 44 | return ret0, ret1 45 | } 46 | 47 | // GetFields indicates an expected call of GetFields. 48 | func (mr *MockSchemaMockRecorder) GetFields(arg0 interface{}) *gomock.Call { 49 | mr.mock.ctrl.T.Helper() 50 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFields", reflect.TypeOf((*MockSchema)(nil).GetFields), arg0) 51 | } 52 | 53 | // GetIndices mocks base method. 54 | func (m *MockSchema) GetIndices() (map[string]*schema.Indices, error) { 55 | m.ctrl.T.Helper() 56 | ret := m.ctrl.Call(m, "GetIndices") 57 | ret0, _ := ret[0].(map[string]*schema.Indices) 58 | ret1, _ := ret[1].(error) 59 | return ret0, ret1 60 | } 61 | 62 | // GetIndices indicates an expected call of GetIndices. 63 | func (mr *MockSchemaMockRecorder) GetIndices() *gomock.Call { 64 | mr.mock.ctrl.T.Helper() 65 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetIndices", reflect.TypeOf((*MockSchema)(nil).GetIndices)) 66 | } 67 | 68 | // GetPrimaryKeys mocks base method. 69 | func (m *MockSchema) GetPrimaryKeys() (map[string]*schema.PrimaryKey, error) { 70 | m.ctrl.T.Helper() 71 | ret := m.ctrl.Call(m, "GetPrimaryKeys") 72 | ret0, _ := ret[0].(map[string]*schema.PrimaryKey) 73 | ret1, _ := ret[1].(error) 74 | return ret0, ret1 75 | } 76 | 77 | // GetPrimaryKeys indicates an expected call of GetPrimaryKeys. 78 | func (mr *MockSchemaMockRecorder) GetPrimaryKeys() *gomock.Call { 79 | mr.mock.ctrl.T.Helper() 80 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPrimaryKeys", reflect.TypeOf((*MockSchema)(nil).GetPrimaryKeys)) 81 | } 82 | 83 | // GetSchemas mocks base method. 84 | func (m *MockSchema) GetSchemas() ([]*config.Schema, error) { 85 | m.ctrl.T.Helper() 86 | ret := m.ctrl.Call(m, "GetSchemas") 87 | ret0, _ := ret[0].([]*config.Schema) 88 | ret1, _ := ret[1].(error) 89 | return ret0, ret1 90 | } 91 | 92 | // GetSchemas indicates an expected call of GetSchemas. 93 | func (mr *MockSchemaMockRecorder) GetSchemas() *gomock.Call { 94 | mr.mock.ctrl.T.Helper() 95 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSchemas", reflect.TypeOf((*MockSchema)(nil).GetSchemas)) 96 | } 97 | 98 | // GetTables mocks base method. 99 | func (m *MockSchema) GetTables() ([]string, error) { 100 | m.ctrl.T.Helper() 101 | ret := m.ctrl.Call(m, "GetTables") 102 | ret0, _ := ret[0].([]string) 103 | ret1, _ := ret[1].(error) 104 | return ret0, ret1 105 | } 106 | 107 | // GetTables indicates an expected call of GetTables. 108 | func (mr *MockSchemaMockRecorder) GetTables() *gomock.Call { 109 | mr.mock.ctrl.T.Helper() 110 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTables", reflect.TypeOf((*MockSchema)(nil).GetTables)) 111 | } 112 | -------------------------------------------------------------------------------- /sqlgen/sb/sql_builder.go: -------------------------------------------------------------------------------- 1 | package sb 2 | 3 | import "bytes" 4 | 5 | type SQLBuilder interface { 6 | Write([]byte) SQLBuilder 7 | WriteString(...string) SQLBuilder 8 | WriteRunes(...rune) SQLBuilder 9 | WriteNewLine() SQLBuilder 10 | ToSQL() (string, error) 11 | Bytes() []byte 12 | String() string 13 | } 14 | 15 | type sqlBuilder struct { 16 | buf *bytes.Buffer 17 | err error 18 | } 19 | 20 | func NewSQLBuilder() SQLBuilder { 21 | return &sqlBuilder{ 22 | buf: &bytes.Buffer{}, 23 | } 24 | } 25 | 26 | func (b *sqlBuilder) Write(bs []byte) SQLBuilder { 27 | if b.err == nil { 28 | _, _ = b.buf.Write(bs) 29 | } 30 | 31 | return b 32 | } 33 | 34 | func (b *sqlBuilder) WriteString(ss ...string) SQLBuilder { 35 | if b.err == nil { 36 | for _, s := range ss { 37 | _, _ = b.buf.WriteString(s) 38 | } 39 | } 40 | return b 41 | } 42 | 43 | func (b *sqlBuilder) WriteRunes(rs ...rune) SQLBuilder { 44 | if b.err == nil { 45 | for _, r := range rs { 46 | _, _ = b.buf.WriteRune(r) 47 | } 48 | } 49 | return b 50 | } 51 | 52 | func (b *sqlBuilder) ToSQL() (string, error) { 53 | if b.err != nil { 54 | return "", b.err 55 | } 56 | 57 | return b.buf.String(), nil 58 | } 59 | 60 | func (b *sqlBuilder) Bytes() []byte { 61 | return b.buf.Bytes() 62 | } 63 | 64 | func (b *sqlBuilder) String() string { 65 | return b.buf.String() 66 | } 67 | 68 | func (b *sqlBuilder) WriteNewLine() SQLBuilder { 69 | return b.WriteString("\n") 70 | } 71 | -------------------------------------------------------------------------------- /sqlgen/schema/postgres.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "regexp" 7 | "strconv" 8 | "strings" 9 | 10 | "github.com/doug-martin/goqu/v9" 11 | "github.com/jackc/pgx/v4" 12 | pg_query "github.com/pganalyze/pg_query_go/v2" 13 | "gitlab.com/wartek-id/core/tools/dbgen/config" 14 | "gitlab.com/wartek-id/core/tools/dbgen/types/field_option" 15 | "gitlab.com/wartek-id/core/tools/dbgen/types/field_type" 16 | 17 | _ "github.com/doug-martin/goqu/v9/dialect/postgres" 18 | ) 19 | 20 | const ( 21 | SchemaMigrationTable = "schema_migrations" 22 | ) 23 | 24 | type PgInterface interface { 25 | Query(ctx context.Context, sql string, args ...interface{}) (pgx.Rows, error) 26 | } 27 | 28 | var OrderMapper = map[pg_query.SortByDir]string{ 29 | pg_query.SortByDir_SORTBY_ASC: "ASC", 30 | pg_query.SortByDir_SORTBY_DEFAULT: "ASC", 31 | pg_query.SortByDir_SORTBY_DESC: "DESC", 32 | } 33 | 34 | var FieldTypeMapper = map[string]field_type.FieldType{ 35 | "timestamp without time zone": field_type.Timestamp, 36 | "timestamp with time zone": field_type.Timestamptz, 37 | "character varying": field_type.Varchar, 38 | "numeric": field_type.Decimal, 39 | "double precision": field_type.Float, 40 | "integer": field_type.Int, 41 | "boolean": field_type.Boolean, 42 | } 43 | 44 | const ( 45 | RegexAutoIncrement = `nextval\(\'[^']+'::regclass\)` 46 | DefaultSchema = "public" 47 | ) 48 | 49 | type postgresSchema struct { 50 | pool PgInterface 51 | schema string 52 | 53 | indicesLoaded bool 54 | indices map[string]*Indices 55 | 56 | primaryKeysLoaded bool 57 | primaryKeys map[string]*PrimaryKey 58 | } 59 | 60 | func NewPostgresSchema(pool PgInterface) *postgresSchema { 61 | return &postgresSchema{ 62 | pool: pool, 63 | schema: DefaultSchema, 64 | } 65 | } 66 | 67 | func (s *postgresSchema) GetSchemas() ([]*config.Schema, error) { 68 | schemas := make([]*config.Schema, 0) 69 | 70 | tables, err := s.GetTables() 71 | if err != nil { 72 | return nil, err 73 | } 74 | 75 | for _, table := range tables { 76 | fields, err := s.GetFields(table) 77 | if err != nil { 78 | return nil, err 79 | } 80 | 81 | indices, err := s.GetTablesIndices(table) 82 | if err != nil { 83 | return nil, err 84 | } 85 | schema := &config.Schema{ 86 | Name: table, 87 | Fields: fields, 88 | Index: indices, 89 | } 90 | 91 | schemas = append(schemas, schema) 92 | } 93 | 94 | return schemas, nil 95 | } 96 | 97 | func (s *postgresSchema) GetTables() ([]string, error) { 98 | query, _, err := goqu.Dialect("postgres").From("information_schema.tables"). 99 | Where( 100 | goqu.C("table_schema").Eq(s.schema), 101 | goqu.C("table_type").Eq("BASE TABLE"), 102 | ).Select("table_name").ToSQL() 103 | if err != nil { 104 | return nil, err 105 | } 106 | 107 | tables := make([]string, 0) 108 | rows, err := s.pool.Query(context.Background(), query) 109 | if err != nil { 110 | return nil, err 111 | } 112 | 113 | for rows.Next() { 114 | var table string 115 | err := rows.Scan(&table) 116 | if err != nil { 117 | return nil, err 118 | } 119 | 120 | if table == SchemaMigrationTable { 121 | continue 122 | } 123 | 124 | tables = append(tables, table) 125 | } 126 | 127 | return tables, nil 128 | } 129 | 130 | func (s *postgresSchema) GetFields(name string) ([]*config.Field, error) { 131 | query, _, err := goqu.Dialect("postgres").From("information_schema.columns"). 132 | Where( 133 | goqu.C("table_schema").Eq(s.schema), 134 | goqu.C("table_name").Eq(name), 135 | ).Select( 136 | "column_name", "column_default", "is_nullable", 137 | "data_type", "character_maximum_length", "numeric_precision", 138 | "numeric_scale").ToSQL() 139 | if err != nil { 140 | return nil, err 141 | } 142 | rows, err := s.pool.Query(context.Background(), query) 143 | if err != nil { 144 | return nil, err 145 | } 146 | 147 | fields := make([]*config.Field, 0) 148 | for rows.Next() { 149 | table := TableStructure{} 150 | err := rows.Scan(&table.ColumnName, &table.ColumnDefault, &table.IsNullable, 151 | &table.DataType, &table.CharMaxLen, &table.NumPrecision, &table.NumScale) 152 | if err != nil { 153 | return nil, err 154 | } 155 | 156 | fields = append(fields, s.getField(name, &table)) 157 | } 158 | return fields, nil 159 | } 160 | 161 | func (s *postgresSchema) getField(name string, table *TableStructure) *config.Field { 162 | ft := FieldTypeMapper[table.DataType] 163 | if ft == "" { 164 | ft = field_type.ParseString(table.DataType) 165 | } 166 | 167 | if s.isAutoIncrement(table.ColumnDefault.String) { 168 | switch ft { 169 | case field_type.BigInt: 170 | ft = field_type.BigSerial 171 | case field_type.Int: 172 | ft = field_type.Serial 173 | case field_type.SmallInt: 174 | ft = field_type.SmallSerial 175 | } 176 | } 177 | 178 | field := &config.Field{ 179 | Name: table.ColumnName, 180 | Type: ft, 181 | Default: s.ParseDefaultValue(table.ColumnDefault.String), 182 | Options: s.GetOptions(name, table), 183 | } 184 | 185 | switch ft.Type() { 186 | case field_type.FieldTypeString: 187 | if table.CharMaxLen.Valid { 188 | field.Limit = int(table.CharMaxLen.Int32) 189 | } 190 | case field_type.FieldTypeNumeric: 191 | if table.NumPrecision.Valid { 192 | field.Limit = int(table.NumPrecision.Int32) 193 | } 194 | if table.NumScale.Valid { 195 | field.Scale = int(table.NumScale.Int32) 196 | } 197 | } 198 | 199 | return field 200 | } 201 | 202 | func (s *postgresSchema) isAutoIncrement(defaultValue string) bool { 203 | match, err := regexp.MatchString(RegexAutoIncrement, defaultValue) 204 | if err != nil { 205 | return false 206 | } 207 | return match 208 | } 209 | 210 | func (s *postgresSchema) ParseDefaultValue(value string) interface{} { 211 | if value == "" || s.isAutoIncrement(value) { 212 | return nil 213 | } 214 | 215 | valSplit := strings.Split(value, "::") 216 | if len(valSplit) > 1 { 217 | valValue, valType := valSplit[0], valSplit[1] 218 | 219 | switch valType { 220 | case "character varying", "text", "timestamp without time zone", "timestamp with time zone": 221 | // trim ' prefix and suffix 222 | trimVal := strings.TrimPrefix(strings.TrimSuffix(valValue, "'"), "'") 223 | // replace escaped '' with ' 224 | return strings.Replace(trimVal, "''", "'", -1) 225 | } 226 | return valValue 227 | } 228 | 229 | valInt, err := strconv.ParseInt(value, 10, 64) 230 | if err == nil { 231 | return int(valInt) 232 | } 233 | 234 | valFloat, err := strconv.ParseFloat(value, 64) 235 | if err == nil { 236 | return valFloat 237 | } 238 | return nil 239 | } 240 | 241 | func (s *postgresSchema) GetOptions(name string, table *TableStructure) []field_option.FieldOption { 242 | options := make([]field_option.FieldOption, 0) 243 | pk, _ := s.GetPrimaryKey(name) 244 | if pk != nil && pk.Column == table.ColumnName { 245 | options = append(options, field_option.PrimaryKey) 246 | } 247 | 248 | if table.IsNullable == "NO" { 249 | options = append(options, field_option.NotNull) 250 | } 251 | 252 | return options 253 | } 254 | 255 | func (s *postgresSchema) GetPrimaryKeys() (map[string]*PrimaryKey, error) { 256 | err := s.LoadPrimaryKeys() 257 | if err != nil { 258 | return nil, err 259 | } 260 | return s.primaryKeys, nil 261 | } 262 | 263 | func (s *postgresSchema) GetPrimaryKey(table string) (*PrimaryKey, error) { 264 | primaryKeys, err := s.GetPrimaryKeys() 265 | if err != nil { 266 | return nil, err 267 | } 268 | return primaryKeys[table], nil 269 | } 270 | 271 | func (s *postgresSchema) LoadPrimaryKeys() error { 272 | if s.primaryKeysLoaded { 273 | return nil 274 | } 275 | 276 | indices, err := s.GetIndices() 277 | if err != nil { 278 | return err 279 | } 280 | 281 | pkConstraint, err := s.GetPKConstraint() 282 | if err != nil { 283 | return err 284 | } 285 | 286 | primaryKeys := make(map[string]*PrimaryKey) 287 | for tablename, constraintname := range pkConstraint { 288 | tableIndices := indices[tablename] 289 | if indices == nil { 290 | continue 291 | } 292 | 293 | constraint, err := tableIndices.GetByConstraintName(constraintname) 294 | if err != nil { 295 | continue 296 | } 297 | 298 | primaryKeys[tablename] = &PrimaryKey{ 299 | Table: tablename, 300 | Name: constraintname, 301 | Column: constraint.GetColumns()[0], 302 | } 303 | } 304 | 305 | s.primaryKeys = primaryKeys 306 | s.primaryKeysLoaded = true 307 | return nil 308 | } 309 | 310 | func (s *postgresSchema) GetPKConstraint() (map[string]string, error) { 311 | query, _, err := goqu.Dialect("postgres"). 312 | From("information_schema.table_constraints"). 313 | Where( 314 | goqu.C("constraint_schema").Eq(s.schema), 315 | goqu.C("constraint_type").Eq("PRIMARY KEY"), 316 | ).Select("table_name", "constraint_name").ToSQL() 317 | 318 | if err != nil { 319 | return nil, err 320 | } 321 | 322 | constraints := make(map[string]string) 323 | rows, err := s.pool.Query(context.Background(), query) 324 | if err != nil { 325 | return nil, err 326 | } 327 | 328 | for rows.Next() { 329 | var tablename, constraint string 330 | err := rows.Scan(&tablename, &constraint) 331 | if err != nil { 332 | return nil, err 333 | } 334 | 335 | constraints[tablename] = constraint 336 | } 337 | 338 | return constraints, nil 339 | } 340 | 341 | func (s *postgresSchema) GetTablesIndices(name string) ([]*config.Index, error) { 342 | indices, err := s.GetIndices() 343 | if err != nil { 344 | return nil, err 345 | } 346 | 347 | tablePk, err := s.GetPrimaryKey(name) 348 | if err != nil { 349 | return nil, err 350 | } 351 | 352 | result := make([]*config.Index, 0) 353 | tableIndices := indices[name] 354 | if tableIndices == nil { 355 | return result, nil 356 | } 357 | 358 | for name, index := range tableIndices.Indices { 359 | if tablePk == nil || name != tablePk.Name { 360 | result = append(result, index) 361 | } 362 | } 363 | 364 | return result, nil 365 | } 366 | 367 | func (s *postgresSchema) GetIndices() (map[string]*Indices, error) { 368 | err := s.LoadIndices() 369 | if err != nil { 370 | return nil, err 371 | } 372 | return s.indices, nil 373 | } 374 | 375 | func (s *postgresSchema) LoadIndices() error { 376 | if s.indicesLoaded { 377 | return nil 378 | } 379 | 380 | query, _, err := goqu.Dialect("postgres"). 381 | From("pg_catalog.pg_indexes"). 382 | Where(goqu.C("schemaname").Eq(s.schema)). 383 | Select("tablename", "indexname", "indexdef"). 384 | ToSQL() 385 | if err != nil { 386 | return err 387 | } 388 | rows, err := s.pool.Query(context.Background(), query) 389 | if err != nil { 390 | return err 391 | } 392 | 393 | indices := make(map[string]*Indices) 394 | for rows.Next() { 395 | var tablename, indexname, indexdef string 396 | err := rows.Scan(&tablename, &indexname, &indexdef) 397 | if err != nil { 398 | return err 399 | } 400 | 401 | idxdef, err := pg_query.Parse(indexdef) 402 | if err != nil { 403 | return err 404 | } 405 | 406 | if len(idxdef.Stmts) == 0 { 407 | return errors.New("invalid statement") 408 | } 409 | 410 | if indices[tablename] == nil { 411 | indices[tablename] = &Indices{ 412 | Table: tablename, 413 | Indices: make(map[string]*config.Index), 414 | } 415 | } 416 | 417 | container := indices[tablename].Indices 418 | idxStmt := idxdef.Stmts[0].GetStmt().GetIndexStmt() 419 | index := config.Index{ 420 | Name: idxStmt.GetIdxname(), 421 | Fields: []*config.IndexField{}, 422 | Unique: idxStmt.Unique, 423 | } 424 | 425 | for _, field := range idxStmt.GetIndexParams() { 426 | ordering := field.GetIndexElem().Ordering 427 | index.Fields = append(index.Fields, &config.IndexField{ 428 | Column: field.GetIndexElem().GetName(), 429 | Order: OrderMapper[ordering], 430 | }) 431 | } 432 | 433 | container[indexname] = &index 434 | indices[tablename].Indices = container 435 | } 436 | 437 | s.indices = indices 438 | s.indicesLoaded = true 439 | 440 | return nil 441 | } 442 | -------------------------------------------------------------------------------- /sqlgen/schema/postgres_test.go: -------------------------------------------------------------------------------- 1 | package schema_test 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "testing" 7 | 8 | "github.com/pashagolub/pgxmock" 9 | "github.com/stretchr/testify/assert" 10 | "gitlab.com/wartek-id/core/tools/dbgen/config" 11 | "gitlab.com/wartek-id/core/tools/dbgen/sqlgen/schema" 12 | "gitlab.com/wartek-id/core/tools/dbgen/types/field_option" 13 | ) 14 | 15 | func TestPostgres_GetSchemas(t *testing.T) { 16 | testCases := map[string]struct { 17 | tableResult *pgxmock.Rows 18 | tableErr error 19 | fieldResult *pgxmock.Rows 20 | fieldErr error 21 | indexResult *pgxmock.Rows 22 | indexErr error 23 | constResult *pgxmock.Rows 24 | result []*config.Schema 25 | err error 26 | }{ 27 | "success": { 28 | tableResult: pgxmock.NewRows([]string{ 29 | "table_name", 30 | }).AddRow("example"), 31 | fieldResult: pgxmock.NewRows([]string{ 32 | "column_name", "column_default", "is_nullable", "data_type", "character_maximum_length", 33 | "numeric_precision", "numeric_scale", 34 | }).AddRow( 35 | "id", "nextval('some_id_sec'::regclass)", "NO", "bigint", nil, 64, nil, 36 | ).AddRow( 37 | "name", "'Alfred'::character varying", "NO", "character varying", "200", nil, nil, 38 | ).AddRow( 39 | "price", "100.5", "YES", "numeric", nil, 64, 2, 40 | ), 41 | indexResult: pgxmock.NewRows([]string{ 42 | "tablename", "indexname", "indexdef", 43 | }).AddRow( 44 | "example", "example_pkey", "CREATE UNIQUE INDEX example_pkey ON public.example USING btree (id)", 45 | ), 46 | constResult: pgxmock.NewRows([]string{ 47 | "table_name", "constraint_name", 48 | }).AddRow("example", "example_pkey"), 49 | result: []*config.Schema{ 50 | { 51 | Name: "example", 52 | Fields: []*config.Field{ 53 | { 54 | Name: "id", 55 | Type: "bigserial", 56 | Limit: 64, 57 | Options: []field_option.FieldOption{ 58 | field_option.PrimaryKey, 59 | field_option.NotNull, 60 | }, 61 | }, 62 | { 63 | Name: "name", 64 | Type: "varchar", 65 | Limit: 200, 66 | Default: "Alfred", 67 | Options: []field_option.FieldOption{field_option.NotNull}, 68 | }, 69 | { 70 | Name: "price", 71 | Type: "decimal", 72 | Scale: 2, 73 | Limit: 64, 74 | Default: 100.5, 75 | Options: []field_option.FieldOption{}, 76 | }, 77 | }, 78 | Index: []*config.Index{}, 79 | }, 80 | }, 81 | }, 82 | "error get table": { 83 | tableErr: errors.New("error get table"), 84 | err: errors.New("error get table"), 85 | }, 86 | "error get field": { 87 | tableResult: pgxmock.NewRows([]string{ 88 | "table_name", 89 | }).AddRow("example"), 90 | fieldErr: errors.New("error get field"), 91 | err: errors.New("error get field"), 92 | }, 93 | "error get index": { 94 | tableResult: pgxmock.NewRows([]string{ 95 | "table_name", 96 | }).AddRow("example"), 97 | fieldResult: pgxmock.NewRows([]string{ 98 | "column_name", "column_default", "is_nullable", "data_type", "character_maximum_length", 99 | "numeric_precision", "numeric_scale", 100 | }), 101 | indexErr: errors.New("error get index"), 102 | err: errors.New("error get index"), 103 | }, 104 | } 105 | 106 | for name, tc := range testCases { 107 | t.Run(name, func(t *testing.T) { 108 | mock, err := pgxmock.NewConn() 109 | assert.Nil(t, err) 110 | defer mock.Close(context.Background()) 111 | 112 | if tc.tableResult != nil { 113 | mock.ExpectQuery("SELECT [^(FROM)]+FROM \"information_schema\".\"tables\""). 114 | WillReturnRows(tc.tableResult) 115 | } 116 | if tc.tableErr != nil { 117 | mock.ExpectQuery("SELECT [^(FROM)]+FROM \"information_schema\".\"tables\""). 118 | WillReturnError(tc.tableErr) 119 | } 120 | if tc.fieldResult != nil { 121 | mock.ExpectQuery("SELECT [^(FROM)]+FROM \"information_schema\".\"columns\""). 122 | WillReturnRows(tc.fieldResult) 123 | } 124 | if tc.fieldErr != nil { 125 | mock.ExpectQuery("SELECT [^(FROM)]+FROM \"information_schema\".\"columns\""). 126 | WillReturnError(tc.fieldErr) 127 | } 128 | if tc.indexResult != nil { 129 | mock.ExpectQuery("SELECT [^(FROM)]+FROM \"pg_catalog\".\"pg_indexes\""). 130 | WillReturnRows(tc.indexResult) 131 | } 132 | if tc.indexErr != nil { 133 | mock.ExpectQuery("SELECT [^(FROM)]+FROM \"pg_catalog\".\"pg_indexes\""). 134 | WillReturnError(tc.indexErr) 135 | } 136 | if tc.constResult != nil { 137 | mock.ExpectQuery("SELECT [^(FROM)]+FROM \"information_schema\".\"table_constraints\""). 138 | WillReturnRows(tc.constResult) 139 | } 140 | 141 | sc := schema.NewPostgresSchema(mock) 142 | result, err := sc.GetSchemas() 143 | assert.Equal(t, tc.err, err) 144 | assert.ElementsMatch(t, tc.result, result) 145 | }) 146 | } 147 | } 148 | 149 | func TestPostgres_GetField(t *testing.T) { 150 | mock, err := pgxmock.NewConn() 151 | assert.Nil(t, err) 152 | defer mock.Close(context.Background()) 153 | 154 | fieldsResults := pgxmock.NewRows([]string{ 155 | "column_name", "column_default", "is_nullable", "data_type", "character_maximum_length", 156 | "numeric_precision", "numeric_scale", 157 | }).AddRow( 158 | "id", "nextval('some_id_sec'::regclass)", "NO", "bigint", nil, 64, nil, 159 | ).AddRow( 160 | "name", "'Alfred'::character varying", "NO", "character varying", "200", nil, nil, 161 | ).AddRow( 162 | "price", "100.5", "YES", "numeric", nil, 64, 2, 163 | ) 164 | mock.ExpectQuery("SELECT [^(FROM)]+FROM \"information_schema\".\"columns\""). 165 | WillReturnRows(fieldsResults) 166 | 167 | indicesResults := pgxmock.NewRows([]string{ 168 | "tablename", "indexname", "indexdef", 169 | }).AddRow( 170 | "example", "example_pkey", "CREATE UNIQUE INDEX example_pkey ON public.example USING btree (id)", 171 | ) 172 | mock.ExpectQuery("SELECT [^(FROM)]+FROM \"pg_catalog\".\"pg_indexes\""). 173 | WillReturnRows(indicesResults) 174 | 175 | constraintResult := pgxmock.NewRows([]string{ 176 | "table_name", "constraint_name", 177 | }).AddRow("example", "example_pkey") 178 | mock.ExpectQuery("SELECT [^(FROM)]+FROM \"information_schema\".\"table_constraints\""). 179 | WillReturnRows(constraintResult) 180 | 181 | sc := schema.NewPostgresSchema(mock) 182 | result, err := sc.GetFields("example") 183 | assert.Nil(t, err) 184 | assert.ElementsMatch(t, []*config.Field{ 185 | { 186 | Name: "id", 187 | Type: "bigserial", 188 | Limit: 64, 189 | Options: []field_option.FieldOption{ 190 | field_option.PrimaryKey, 191 | field_option.NotNull, 192 | }, 193 | }, 194 | { 195 | Name: "name", 196 | Type: "varchar", 197 | Limit: 200, 198 | Default: "Alfred", 199 | Options: []field_option.FieldOption{field_option.NotNull}, 200 | }, 201 | { 202 | Name: "price", 203 | Type: "decimal", 204 | Scale: 2, 205 | Limit: 64, 206 | Default: 100.5, 207 | Options: []field_option.FieldOption{}, 208 | }, 209 | }, result) 210 | } 211 | 212 | func TestPostgres_GetTables(t *testing.T) { 213 | mock, err := pgxmock.NewConn() 214 | assert.Nil(t, err) 215 | defer mock.Close(context.Background()) 216 | 217 | mock.ExpectQuery("SELECT *"). 218 | WillReturnRows(pgxmock.NewRows([]string{"table_name"}). 219 | AddRow("users"). 220 | AddRow("histories"). 221 | AddRow("schema_migrations")) 222 | sc := schema.NewPostgresSchema(mock) 223 | result, err := sc.GetTables() 224 | assert.Nil(t, err) 225 | assert.ElementsMatch(t, []string{"users", "histories"}, result) 226 | 227 | if err := mock.ExpectationsWereMet(); err != nil { 228 | t.Errorf("there were unfulfilled expectations: %s", err) 229 | } 230 | } 231 | 232 | func TestPostgres_GetIndices(t *testing.T) { 233 | mock, err := pgxmock.NewConn() 234 | assert.Nil(t, err) 235 | defer mock.Close(context.Background()) 236 | 237 | indicesResults := pgxmock.NewRows([]string{ 238 | "tablename", "indexname", "indexdef", 239 | }).AddRow( 240 | "example", "example_pkey", "CREATE UNIQUE INDEX example_pkey ON public.example USING btree (id)", 241 | ).AddRow( 242 | "example", "index_example_on_name_email", "CREATE INDEX index_example_on_name_email ON public.example(name ASC, email DESC)", 243 | ) 244 | mock.ExpectQuery("SELECT [^(FROM)]+FROM \"pg_catalog\".\"pg_indexes\""). 245 | WillReturnRows(indicesResults) 246 | 247 | expectedResult := map[string]*schema.Indices{ 248 | "example": { 249 | Table: "example", 250 | Indices: map[string]*config.Index{ 251 | "example_pkey": { 252 | Name: "example_pkey", 253 | Fields: []*config.IndexField{ 254 | {Column: "id", Order: "ASC"}, 255 | }, 256 | Unique: true, 257 | }, 258 | "index_example_on_name_email": { 259 | Name: "index_example_on_name_email", 260 | Fields: []*config.IndexField{ 261 | {Column: "name", Order: "ASC"}, 262 | {Column: "email", Order: "DESC"}, 263 | }, 264 | }, 265 | }, 266 | }, 267 | } 268 | sc := schema.NewPostgresSchema(mock) 269 | result, err := sc.GetIndices() 270 | assert.Nil(t, err) 271 | assert.Equal(t, expectedResult, result) 272 | } 273 | 274 | func TestPostgres_GetPrimaryKeys(t *testing.T) { 275 | mock, err := pgxmock.NewConn() 276 | assert.Nil(t, err) 277 | defer mock.Close(context.Background()) 278 | 279 | indicesResults := pgxmock.NewRows([]string{ 280 | "tablename", "indexname", "indexdef", 281 | }).AddRow( 282 | "example", "example_pkey", "CREATE UNIQUE INDEX example_pkey ON public.example USING btree (id)", 283 | ).AddRow( 284 | "example", "index_example_on_name_email", "CREATE INDEX index_example_on_name_email ON public.example(name ASC, email DESC)", 285 | ) 286 | mock.ExpectQuery("SELECT [^(FROM)]+FROM \"pg_catalog\".\"pg_indexes\""). 287 | WillReturnRows(indicesResults) 288 | constraintResult := pgxmock.NewRows([]string{ 289 | "table_name", "constraint_name", 290 | }).AddRow("example", "example_pkey") 291 | mock.ExpectQuery("SELECT [^(FROM)]+FROM \"information_schema\".\"table_constraints\""). 292 | WillReturnRows(constraintResult) 293 | 294 | expectedResult := map[string]*schema.PrimaryKey{ 295 | "example": { 296 | Table: "example", 297 | Name: "example_pkey", 298 | Column: "id", 299 | }, 300 | } 301 | sc := schema.NewPostgresSchema(mock) 302 | result, err := sc.GetPrimaryKeys() 303 | assert.Nil(t, err) 304 | assert.Equal(t, expectedResult, result) 305 | } 306 | 307 | func TestPostgres_ParseDefaultValue(t *testing.T) { 308 | testCases := map[string]struct { 309 | input string 310 | result interface{} 311 | }{ 312 | "int": { 313 | input: "100", 314 | result: 100, 315 | }, 316 | "float int": { 317 | input: "100.00", 318 | result: 100.00, 319 | }, 320 | "float": { 321 | input: "100.50", 322 | result: 100.50, 323 | }, 324 | "pk": { 325 | input: "nextval('reg_seq'::regclass)", 326 | result: nil, 327 | }, 328 | "varchar": { 329 | input: "'100.50'::character varying", 330 | result: "100.50", 331 | }, 332 | "text": { 333 | input: "'100.50'::text", 334 | result: "100.50", 335 | }, 336 | } 337 | 338 | sc := schema.NewPostgresSchema(nil) 339 | for name, tc := range testCases { 340 | t.Run(name, func(t *testing.T) { 341 | result := sc.ParseDefaultValue(tc.input) 342 | assert.Equal(t, tc.result, result) 343 | }) 344 | } 345 | } 346 | -------------------------------------------------------------------------------- /sqlgen/schema/schema.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "errors" 7 | "strings" 8 | 9 | "github.com/jackc/pgx/v4/pgxpool" 10 | "gitlab.com/wartek-id/core/tools/dbgen/config" 11 | ) 12 | 13 | var ( 14 | ErrConstraintNotExists = errors.New("constraint is not exists") 15 | ErrConstraintHasNoFields = errors.New("constraint has no fields") 16 | ErrUnsupportedDriver = errors.New("unsupported driver") 17 | ) 18 | 19 | type Schema interface { 20 | GetSchemas() ([]*config.Schema, error) 21 | GetTables() ([]string, error) 22 | GetIndices() (map[string]*Indices, error) 23 | GetFields(tblName string) ([]*config.Field, error) 24 | GetPrimaryKeys() (map[string]*PrimaryKey, error) 25 | } 26 | 27 | func NewSchema(connString string) (Schema, error) { 28 | driver := strings.Split(connString, "://") 29 | switch driver[0] { 30 | case "postgresql": 31 | pool, err := pgxpool.Connect(context.Background(), connString) 32 | if err != nil { 33 | return nil, err 34 | } 35 | 36 | return NewPostgresSchema(pool), nil 37 | } 38 | 39 | return nil, ErrUnsupportedDriver 40 | } 41 | 42 | type PrimaryKey struct { 43 | Table string 44 | Name string 45 | Column string 46 | } 47 | 48 | type Indices struct { 49 | Table string 50 | Indices map[string]*config.Index 51 | } 52 | 53 | type TableStructure struct { 54 | ColumnName string 55 | ColumnDefault sql.NullString 56 | IsNullable string 57 | DataType string 58 | CharMaxLen sql.NullInt32 59 | NumPrecision sql.NullInt32 60 | NumScale sql.NullInt32 61 | } 62 | 63 | func (i *Indices) GetByConstraintName(name string) (*config.Index, error) { 64 | if i.Indices[name] == nil { 65 | return nil, ErrConstraintNotExists 66 | } 67 | 68 | idx := i.Indices[name] 69 | if len(idx.Fields) == 0 { 70 | return nil, ErrConstraintHasNoFields 71 | } 72 | 73 | return i.Indices[name], nil 74 | } 75 | -------------------------------------------------------------------------------- /sqlgen/schema/schema_test.go: -------------------------------------------------------------------------------- 1 | package schema_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "gitlab.com/wartek-id/core/tools/dbgen/config" 8 | "gitlab.com/wartek-id/core/tools/dbgen/sqlgen/schema" 9 | ) 10 | 11 | func TestNewSchema(t *testing.T) { 12 | sc, err := schema.NewSchema("mysql://root:root@localhost:3306/playground") 13 | assert.Equal(t, schema.ErrUnsupportedDriver, err) 14 | assert.Nil(t, sc) 15 | } 16 | 17 | func TestIndices_GetByConstraintName(t *testing.T) { 18 | pKeys := &config.Index{ 19 | Name: "example_pkeys", 20 | Fields: []*config.IndexField{ 21 | {Column: "id"}, 22 | }, 23 | Unique: true, 24 | } 25 | indices := schema.Indices{ 26 | Table: "example", 27 | Indices: map[string]*config.Index{ 28 | "example_pkeys": pKeys, 29 | "empty": { 30 | Name: "empty", 31 | }, 32 | }, 33 | } 34 | 35 | result, err := indices.GetByConstraintName("example_pkeys") 36 | assert.Nil(t, err) 37 | assert.Equal(t, pKeys, result) 38 | 39 | result, err = indices.GetByConstraintName("something") 40 | assert.Equal(t, schema.ErrConstraintNotExists, err) 41 | assert.Nil(t, result) 42 | 43 | result, err = indices.GetByConstraintName("empty") 44 | assert.Equal(t, schema.ErrConstraintHasNoFields, err) 45 | assert.Nil(t, result) 46 | } 47 | -------------------------------------------------------------------------------- /sqlgen/step/alter_column.go: -------------------------------------------------------------------------------- 1 | package step 2 | 3 | import ( 4 | "gitlab.com/wartek-id/core/tools/dbgen/config" 5 | ) 6 | 7 | type OptionAction int 8 | 9 | const ( 10 | DropNotNull OptionAction = iota 11 | SetNotNull 12 | ) 13 | 14 | type AlterColumn struct { 15 | Name string 16 | Field *config.Field 17 | LastField *config.Field 18 | ChangedType bool 19 | ChangedDefaultValue bool 20 | ChangedOptions []OptionAction 21 | } 22 | 23 | func (c *AlterColumn) HasChanges() bool { 24 | return c.ChangedType || c.IsOptionsChanged() 25 | } 26 | 27 | func (c *AlterColumn) IsOptionsChanged() bool { 28 | return len(c.ChangedOptions) != 0 29 | } 30 | -------------------------------------------------------------------------------- /sqlgen/step/alter_schema.go: -------------------------------------------------------------------------------- 1 | package step 2 | 3 | import ( 4 | "gitlab.com/wartek-id/core/tools/dbgen/config" 5 | ) 6 | 7 | type AlterSchema struct { 8 | Name string 9 | AddedColumns []*config.Field 10 | AlteredColumns []*AlterColumn 11 | DroppedColumns []*config.Field 12 | 13 | AddedIndices []*config.Index 14 | DroppedIndices []*config.Index 15 | } 16 | 17 | func NewAlterSchema(name string) *AlterSchema { 18 | return &AlterSchema{ 19 | Name: name, 20 | AddedColumns: make([]*config.Field, 0), 21 | AlteredColumns: make([]*AlterColumn, 0), 22 | DroppedColumns: make([]*config.Field, 0), 23 | } 24 | } 25 | 26 | func (s *AlterSchema) HasChanges() bool { 27 | return s.FieldChanged() || s.IndicesChanged() 28 | } 29 | 30 | func (s *AlterSchema) FieldChanged() bool { 31 | return s.IsColumnsAdded() || 32 | s.IsColumnsAltered() || 33 | s.IsColumnsDropped() 34 | } 35 | 36 | func (s *AlterSchema) IndicesChanged() bool { 37 | return s.IsIndicesAdded() || 38 | s.IsIndicesDropped() 39 | } 40 | 41 | func (s *AlterSchema) IsColumnsAdded() bool { 42 | return len(s.AddedColumns) != 0 43 | } 44 | 45 | func (s *AlterSchema) IsColumnsAltered() bool { 46 | return len(s.AlteredColumns) != 0 47 | } 48 | 49 | func (s *AlterSchema) IsColumnsDropped() bool { 50 | return len(s.DroppedColumns) != 0 51 | } 52 | 53 | func (s *AlterSchema) IsIndicesAdded() bool { 54 | return len(s.AddedIndices) != 0 55 | } 56 | 57 | func (s *AlterSchema) IsIndicesDropped() bool { 58 | return len(s.DroppedIndices) != 0 59 | } 60 | -------------------------------------------------------------------------------- /sqlgen/step/migration_planner.go: -------------------------------------------------------------------------------- 1 | package step 2 | 3 | import "gitlab.com/wartek-id/core/tools/dbgen/config" 4 | 5 | type MigrationPlanner struct { 6 | CreateTable []*config.Schema 7 | DropTable []*config.Schema 8 | AlterSchema map[string]*AlterSchema 9 | } 10 | 11 | func NewMigrationPlanner() *MigrationPlanner { 12 | return &MigrationPlanner{ 13 | CreateTable: make([]*config.Schema, 0), 14 | DropTable: make([]*config.Schema, 0), 15 | AlterSchema: make(map[string]*AlterSchema), 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /types/field_option/type.go: -------------------------------------------------------------------------------- 1 | package field_option 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "strings" 7 | ) 8 | 9 | type FieldOption string 10 | 11 | const ( 12 | Nullable FieldOption = "nullable" 13 | NotNull FieldOption = "not null" 14 | AutoIncrement FieldOption = "auto increment" 15 | Unique FieldOption = "unique" 16 | PrimaryKey FieldOption = "primary key" 17 | ) 18 | 19 | var SupportedOption = []FieldOption{ 20 | Nullable, 21 | NotNull, 22 | AutoIncrement, 23 | Unique, 24 | PrimaryKey, 25 | } 26 | 27 | func (o *FieldOption) UnmarshalJSON(data []byte) error { 28 | var strOpt string 29 | err := json.Unmarshal(data, &strOpt) 30 | if err != nil { 31 | return err 32 | } 33 | 34 | fo := FieldOption(strings.ToLower(strOpt)) 35 | for _, opt := range SupportedOption { 36 | if fo == opt { 37 | *o = fo 38 | return nil 39 | } 40 | } 41 | return fmt.Errorf("invalid \"%s\" as field option", strOpt) 42 | } 43 | -------------------------------------------------------------------------------- /types/field_option/type_test.go: -------------------------------------------------------------------------------- 1 | package field_option_test 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "gitlab.com/wartek-id/core/tools/dbgen/types/field_option" 10 | ) 11 | 12 | func TestFieldOption_UnmarshallJSON(t *testing.T) { 13 | testCases := map[string]struct { 14 | input []byte 15 | wantErr error 16 | result field_option.FieldOption 17 | }{ 18 | "success": { 19 | input: []byte("\"not null\""), 20 | result: "not null", 21 | }, 22 | "invalid type": { 23 | input: []byte("\"binary\""), 24 | wantErr: fmt.Errorf("invalid \"binary\" as field option"), 25 | }, 26 | } 27 | 28 | for name, tc := range testCases { 29 | t.Run(name, func(t *testing.T) { 30 | var result field_option.FieldOption 31 | err := json.Unmarshal(tc.input, &result) 32 | assert.Equal(t, tc.wantErr, err) 33 | assert.Equal(t, tc.result, result) 34 | }) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /types/field_type/type.go: -------------------------------------------------------------------------------- 1 | package field_type 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "strings" 7 | ) 8 | 9 | type FieldType string 10 | 11 | const ( 12 | Boolean FieldType = "bool" 13 | Varchar FieldType = "varchar" 14 | Text FieldType = "text" 15 | 16 | SmallInt FieldType = "smallint" 17 | Int FieldType = "int" 18 | BigInt FieldType = "bigint" 19 | 20 | Json FieldType = "json" 21 | Jsonb FieldType = "jsonb" 22 | 23 | Float FieldType = "float" 24 | Decimal FieldType = "decimal" 25 | 26 | Timestamp FieldType = "timestamp" 27 | Timestamptz FieldType = "timestamptz" 28 | 29 | BigSerial FieldType = "bigserial" 30 | Serial FieldType = "serial" 31 | SmallSerial FieldType = "smallserial" 32 | 33 | FieldTypeString = "string" 34 | FieldTypeNumeric = "numeric" 35 | FieldTypeBinary = "binary" 36 | ) 37 | 38 | var SupportedFieldType = []FieldType{ 39 | Boolean, 40 | Varchar, 41 | Text, 42 | SmallInt, 43 | Int, 44 | BigInt, 45 | Json, 46 | Jsonb, 47 | Decimal, 48 | Float, 49 | Timestamp, 50 | Timestamptz, 51 | BigSerial, 52 | Serial, 53 | SmallSerial, 54 | } 55 | 56 | func (t *FieldType) UnmarshalJSON(data []byte) error { 57 | var strType string 58 | err := json.Unmarshal(data, &strType) 59 | if err != nil { 60 | return err 61 | } 62 | 63 | ft := FieldType(strings.ToLower(strType)) 64 | for _, typ := range SupportedFieldType { 65 | if ft == typ { 66 | *t = ft 67 | return nil 68 | } 69 | } 70 | 71 | return fmt.Errorf("invalid \"%s\" as field type", strType) 72 | } 73 | 74 | func (t FieldType) Type() string { 75 | switch t { 76 | case Varchar, Text, Json: 77 | return FieldTypeString 78 | case Jsonb: 79 | return FieldTypeBinary 80 | } 81 | return FieldTypeNumeric 82 | } 83 | 84 | func (t FieldType) HasLimit() bool { 85 | return t == Varchar || t == Decimal 86 | } 87 | 88 | func (t FieldType) HasScale() bool { 89 | return t == Decimal 90 | } 91 | 92 | func ParseString(ft string) FieldType { 93 | return FieldType(ft) 94 | } 95 | -------------------------------------------------------------------------------- /types/field_type/type_test.go: -------------------------------------------------------------------------------- 1 | package field_type_test 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "gitlab.com/wartek-id/core/tools/dbgen/types/field_type" 10 | ) 11 | 12 | func TestFieldType_UnmarshallJSON(t *testing.T) { 13 | testCases := map[string]struct { 14 | input []byte 15 | wantErr error 16 | result field_type.FieldType 17 | }{ 18 | "success": { 19 | input: []byte("\"bool\""), 20 | result: "bool", 21 | }, 22 | "invalid type": { 23 | input: []byte("\"binary\""), 24 | wantErr: fmt.Errorf("invalid \"binary\" as field type"), 25 | }, 26 | } 27 | 28 | for name, tc := range testCases { 29 | t.Run(name, func(t *testing.T) { 30 | var result field_type.FieldType 31 | err := json.Unmarshal(tc.input, &result) 32 | assert.Equal(t, tc.wantErr, err) 33 | assert.Equal(t, tc.result, result) 34 | }) 35 | } 36 | } 37 | 38 | func TestFieldType_Type(t *testing.T) { 39 | testCases := []struct { 40 | input field_type.FieldType 41 | result string 42 | }{ 43 | { 44 | field_type.Varchar, 45 | field_type.FieldTypeString, 46 | }, 47 | { 48 | field_type.Text, 49 | field_type.FieldTypeString, 50 | }, 51 | { 52 | field_type.Json, 53 | field_type.FieldTypeString, 54 | }, 55 | { 56 | field_type.Jsonb, 57 | field_type.FieldTypeBinary, 58 | }, 59 | { 60 | field_type.Int, 61 | field_type.FieldTypeNumeric, 62 | }, 63 | { 64 | field_type.Float, 65 | field_type.FieldTypeNumeric, 66 | }, 67 | } 68 | 69 | for _, tc := range testCases { 70 | assert.Equal(t, tc.result, tc.input.Type()) 71 | } 72 | } 73 | 74 | func TestFieldType_HasLimit(t *testing.T) { 75 | assert.True(t, field_type.Varchar.HasLimit()) 76 | assert.True(t, field_type.Decimal.HasLimit()) 77 | assert.False(t, field_type.BigInt.HasLimit()) 78 | } 79 | 80 | func TestFieldType_HasScale(t *testing.T) { 81 | assert.False(t, field_type.Varchar.HasScale()) 82 | assert.True(t, field_type.Decimal.HasScale()) 83 | assert.False(t, field_type.BigInt.HasScale()) 84 | } 85 | 86 | func TestParseString(t *testing.T) { 87 | ft := field_type.ParseString("bigint") 88 | assert.Equal(t, field_type.FieldType("bigint"), ft) 89 | } 90 | -------------------------------------------------------------------------------- /version.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | const release = "v0.0.1" 4 | --------------------------------------------------------------------------------