├── .github └── workflows │ └── go.yml ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── README_zh.md ├── element ├── column.go ├── foreign_key.go ├── index.go ├── migration.go ├── node.go └── table.go ├── export ├── avro │ ├── builder.go │ └── schema.go └── mermaidjs │ └── builder.go ├── go.mod ├── go.sum ├── options.go ├── sql-builder ├── builder.go └── options.go ├── sql-parser ├── mysql.go ├── parser.go ├── postgresql.go └── sqlite.go ├── sql-templates ├── ddl.go ├── option.go └── type.go ├── sqlize.go ├── sqlize_test.go ├── test ├── README.md └── sqlite │ ├── common.go │ ├── migration_test.go │ ├── parser_test.go │ └── testdata │ ├── schema_one_table.sql │ └── schema_two_tables.sql └── utils ├── file.go ├── slc.go └── str.go /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Build pipeline 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | 16 | - name: Set up Go 17 | uses: actions/setup-go@v5 18 | with: 19 | go-version-file: go.mod 20 | 21 | - name: Build 22 | run: go build -v ./... 23 | 24 | - name: Test 25 | run: go test -v ./... 26 | 27 | - name: Run coverage 28 | run: go test -race -coverprofile=coverage.txt -covermode=atomic 29 | - name: Upload coverage to Codecov 30 | run: bash <(curl -s https://codecov.io/bash) 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | /vendor 9 | 10 | # Test binary, build with `go test -caching` 11 | *.test 12 | 13 | # Output of the go coverage tool, specifically when used with LiteIDE 14 | *.out 15 | 16 | .idea 17 | .vscode 18 | .DS_Store 19 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.13.x 5 | - 1.14.x 6 | 7 | env: GO111MODULE=on 8 | 9 | go_import_path: github.com/sunary/sqlize 10 | 11 | script: 12 | - go test -v ./... -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright [2021] [Nhat Vo Van (sunary)] 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SQLize 2 | 3 | ![github action](https://github.com/sunary/sqlize/actions/workflows/go.yml/badge.svg) 4 | 5 | English | [中文](README_zh.md) 6 | 7 | SQLize is a powerful migration generation tool that detects differences between two SQL state sources. It simplifies migration creation by comparing an existing SQL schema with Go models, ensuring seamless database updates. 8 | 9 | Designed for flexibility, SQLize supports `MySQL`, `PostgreSQL`, and `SQLite` and integrates well with popular Go ORM and migration tools like `gorm` (gorm tag), `golang-migrate/migrate` (migration version), and more. 10 | 11 | Additionally, SQLize offers advanced features, including `Avro Schema` export (MySQL only) and `ERD` diagram generation (`MermaidJS`). 12 | 13 | ## Conventions 14 | 15 | ### Default Behaviors 16 | 17 | - Database: `mysql` (use `sql_builder.WithPostgresql()` for PostgreSQL, etc.) 18 | - SQL syntax: Uppercase (e.g., `"SELECT * FROM user WHERE id = ?"`) 19 | - For lowercase, use `sql_builder.WithSqlLowercase()` 20 | - Table naming: Singular 21 | - For plural (adding 's'), use `sql_builder.WithPluralTableName()` 22 | - Comment generation: Use `sql_builder.WithCommentGenerate()` 23 | 24 | ### SQL Tag Options 25 | 26 | - Format: Supports both `snake_case` and `camelCase` (e.g., `sql:"primary_key"` equals `sql:"primaryKey"`) 27 | - Custom column: `sql:"column:column_name"` 28 | - Primary key: `sql:"primary_key"` 29 | - Foreign key: `sql:"foreign_key:user_id;references:user_id"` 30 | - Auto increment: `sql:"auto_increment"` 31 | - Default value: `sql:"default:CURRENT_TIMESTAMP"` 32 | - Override datatype: `sql:"type:VARCHAR(64)"` 33 | - Ignore field: `sql:"-"` 34 | 35 | ### Indexing 36 | 37 | - Basic index: `sql:"index"` 38 | - Custom index name: `sql:"index:idx_col_name"` 39 | - Unique index: `sql:"unique"` 40 | - Custom unique index: `sql:"unique:idx_name"` 41 | - Composite index: `sql:"index_columns:col1,col2"` (includes unique index and primary key) 42 | - Index type: `sql:"index_type:btree"` 43 | 44 | ### Embedded Structs 45 | 46 | - Use `sql:"embedded"` or `sql:"squash"` 47 | - Cannot be a pointer 48 | - Supports prefix: `sql:"embedded_prefix:base_"` 49 | - Fields have lowest order, except for primary key (always first) 50 | 51 | ### Data Types 52 | 53 | - MySQL data types are implicitly changed: 54 | 55 | ```sql 56 | TINYINT => tinyint(4) 57 | INT => int(11) 58 | BIGINT => bigint(20) 59 | ``` 60 | 61 | - Pointer values must be declared in the struct or predefined data types. 62 | 63 | ```golang 64 | // your struct 65 | type Record struct { 66 | ID int 67 | DeletedAt *time.Time 68 | } 69 | 70 | // => 71 | // the struct is declared with a value 72 | now := time.Now() 73 | Record{DeletedAt: &now} 74 | 75 | // or predefined data type 76 | type Record struct { 77 | ID int 78 | DeletedAt *time.Time `sql:"type:DATETIME"` 79 | } 80 | 81 | // or using struct supported by "database/sql" 82 | type Record struct { 83 | ID int 84 | DeletedAt sql.NullTime 85 | } 86 | ``` 87 | 88 | ## Usage 89 | 90 | - Add the following code to your project as a command. 91 | - Implement `YourModels()` to return the Go models affected by the migration. 92 | - Run the command whenever you need to generate a migration. 93 | 94 | ```golang 95 | package main 96 | 97 | import ( 98 | "fmt" 99 | "log" 100 | "os" 101 | 102 | "github.com/sunary/sqlize" 103 | ) 104 | 105 | func main() { 106 | migrationFolder := "migrations/" 107 | sqlLatest := sqlize.NewSqlize(sqlize.WithSqlTag("sql"), 108 | sqlize.WithMigrationFolder(migrationFolder), 109 | sqlize.WithCommentGenerate()) 110 | 111 | ms := YourModels() // TODO: implement YourModels() function 112 | err := sqlLatest.FromObjects(ms...) 113 | if err != nil { 114 | log.Fatal("sqlize FromObjects", err) 115 | } 116 | sqlVersion := sqlLatest.HashValue() 117 | 118 | sqlMigrated := sqlize.NewSqlize(sqlize.WithMigrationFolder(migrationFolder)) 119 | err = sqlMigrated.FromMigrationFolder() 120 | if err != nil { 121 | log.Fatal("sqlize FromMigrationFolder", err) 122 | } 123 | 124 | sqlLatest.Diff(*sqlMigrated) 125 | 126 | fmt.Println("sql version", sqlVersion) 127 | 128 | fmt.Println("\n\n### migration up") 129 | migrationUp := sqlLatest.StringUp() 130 | fmt.Println(migrationUp) 131 | 132 | fmt.Println("\n\n### migration down") 133 | fmt.Println(sqlLatest.StringDown()) 134 | 135 | initVersion := false 136 | if initVersion { 137 | log.Println("write to init version") 138 | err = sqlLatest.WriteFilesVersion("new version", 0, false) 139 | if err != nil { 140 | log.Fatal("sqlize WriteFilesVersion", err) 141 | } 142 | } 143 | 144 | if len(os.Args) > 1 { 145 | log.Println("write to file", os.Args[1]) 146 | err = sqlLatest.WriteFilesWithVersion(os.Args[1], sqlVersion, false) 147 | if err != nil { 148 | log.Fatal("sqlize WriteFilesWithVersion", err) 149 | } 150 | } 151 | } 152 | ``` -------------------------------------------------------------------------------- /README_zh.md: -------------------------------------------------------------------------------- 1 | ### SQLize 2 | 3 | ![github action](https://github.com/sunary/sqlize/actions/workflows/go.yml/badge.svg) 4 | 5 | [English](README.md) | 中文 6 | 7 | SQLize 是一款强大的迁移生成工具,可检测两个 SQL 状态源之间的差异。它通过对比现有 SQL 模式与 Go 模型,简化迁移创建过程,确保数据库平滑更新。 8 | 9 | SQLize 设计灵活,支持 `MySQL`、`PostgreSQL` 和 `SQLite`,并能与流行的 Go ORM 和迁移工具(如 `gorm`(gorm 标签)、`golang-migrate/migrate`(迁移版本)等)良好集成。 10 | 11 | 此外,SQLize 还提供高级功能,包括 `Avro Schema` 导出(仅支持 MySQL)和 `ERD` 关系图生成(`MermaidJS`)。 12 | 13 | ## 约定 14 | 15 | ### 默认行为 16 | 17 | - 数据库:默认为 `mysql`(使用 `sql_builder.WithPostgresql()` 可切换到 PostgreSQL 等) 18 | - SQL 语法:默认大写(例如:`"SELECT * FROM user WHERE id = ?"`) 19 | - 使用 `sql_builder.WithSqlLowercase()` 可切换为小写 20 | - 表名:默认单数 21 | - 使用 `sql_builder.WithPluralTableName()` 可自动添加 's' 实现复数命名 22 | - 注释生成:使用 `sql_builder.WithCommentGenerate()` 选项 23 | 24 | ### SQL 标签选项 25 | 26 | - 格式:支持 `snake_case` 和 `camelCase`(例如:`sql:"primary_key"` 等同于 `sql:"primaryKey"`) 27 | - 自定义列名:`sql:"column:column_name"` 28 | - 主键:`sql:"primary_key"` 29 | - 外键:`sql:"foreign_key:user_id;references:user_id"` 30 | - 自增:`sql:"auto_increment"` 31 | - 默认值:`sql:"default:CURRENT_TIMESTAMP"` 32 | - 覆盖数据类型:`sql:"type:VARCHAR(64)"` 33 | - 忽略字段:`sql:"-"` 34 | 35 | ### 索引 36 | 37 | - 基本索引:`sql:"index"` 38 | - 自定义索引名:`sql:"index:idx_col_name"` 39 | - 唯一索引:`sql:"unique"` 40 | - 自定义唯一索引名:`sql:"unique:idx_name"` 41 | - 复合索引:`sql:"index_columns:col1,col2"`(包括唯一索引和主键) 42 | - 索引类型:`sql:"index_type:btree"` 43 | 44 | ### 嵌入式结构体 45 | 46 | - 使用 `sql:"embedded"` 或 `sql:"squash"` 47 | - 不能是指针 48 | - 支持前缀:`sql:"embedded_prefix:base_"` 49 | - 字段具有最低顺序,除了主键(始终在首位) 50 | 51 | ### 数据类型 52 | 53 | - MySQL 数据类型会被隐式更改: 54 | 55 | ```sql 56 | TINYINT => tinyint(4) 57 | INT => int(11) 58 | BIGINT => bigint(20) 59 | ``` 60 | 61 | - 指针值必须在结构体或预定义数据类型中声明: 62 | 63 | ```golang 64 | // your struct 65 | type Record struct { 66 | ID int 67 | DeletedAt *time.Time 68 | } 69 | 70 | // => 71 | // the struct is declared with a value 72 | now := time.Now() 73 | Record{DeletedAt: &now} 74 | 75 | // or predefined data type 76 | type Record struct { 77 | ID int 78 | DeletedAt *time.Time `sql:"type:DATETIME"` 79 | } 80 | 81 | // or using struct supported by "database/sql" 82 | type Record struct { 83 | ID int 84 | DeletedAt sql.NullTime 85 | } 86 | ``` 87 | 88 | ### 使用方法 89 | 90 | - 将以下代码添加到您的项目中作为命令。 91 | - 实现 YourModels(),返回受迁移影响的 Go 模型。 92 | - 需要生成迁移时,运行该命令。 93 | 94 | ```golang 95 | package main 96 | 97 | import ( 98 | "fmt" 99 | "log" 100 | "os" 101 | 102 | "github.com/sunary/sqlize" 103 | ) 104 | 105 | func main() { 106 | migrationFolder := "migrations/" 107 | sqlLatest := sqlize.NewSqlize(sqlize.WithSqlTag("sql"), 108 | sqlize.WithMigrationFolder(migrationFolder), 109 | sqlize.WithCommentGenerate()) 110 | 111 | ms := YourModels() // TODO: implement YourModels() function 112 | err := sqlLatest.FromObjects(ms...) 113 | if err != nil { 114 | log.Fatal("sqlize FromObjects", err) 115 | } 116 | sqlVersion := sqlLatest.HashValue() 117 | 118 | sqlMigrated := sqlize.NewSqlize(sqlize.WithMigrationFolder(migrationFolder)) 119 | err = sqlMigrated.FromMigrationFolder() 120 | if err != nil { 121 | log.Fatal("sqlize FromMigrationFolder", err) 122 | } 123 | 124 | sqlLatest.Diff(*sqlMigrated) 125 | 126 | fmt.Println("sql version", sqlVersion) 127 | 128 | fmt.Println("\n\n### migration up") 129 | migrationUp := sqlLatest.StringUp() 130 | fmt.Println(migrationUp) 131 | 132 | fmt.Println("\n\n### migration down") 133 | fmt.Println(sqlLatest.StringDown()) 134 | 135 | initVersion := false 136 | if initVersion { 137 | log.Println("write to init version") 138 | err = sqlLatest.WriteFilesVersion("new version", 0, false) 139 | if err != nil { 140 | log.Fatal("sqlize WriteFilesVersion", err) 141 | } 142 | } 143 | 144 | if len(os.Args) > 1 { 145 | log.Println("write to file", os.Args[1]) 146 | err = sqlLatest.WriteFilesWithVersion(os.Args[1], sqlVersion, false) 147 | if err != nil { 148 | log.Fatal("sqlize WriteFilesWithVersion", err) 149 | } 150 | } 151 | } 152 | ``` -------------------------------------------------------------------------------- /element/column.go: -------------------------------------------------------------------------------- 1 | package element 2 | 3 | import ( 4 | "bytes" 5 | "crypto/md5" 6 | "encoding/hex" 7 | "fmt" 8 | "strconv" 9 | "strings" 10 | 11 | ptypes "github.com/auxten/postgresql-parser/pkg/sql/types" 12 | "github.com/pingcap/parser/ast" 13 | "github.com/pingcap/parser/format" 14 | "github.com/pingcap/parser/types" 15 | sqlite "github.com/rqlite/sql" 16 | sql_templates "github.com/sunary/sqlize/sql-templates" 17 | ) 18 | 19 | const ( 20 | // UppercaseRestoreFlag ... 21 | UppercaseRestoreFlag = format.RestoreStringSingleQuotes | format.RestoreKeyWordUppercase | format.RestoreNameUppercase | format.RestoreNameBackQuotes 22 | // LowerRestoreFlag ... 23 | LowerRestoreFlag = format.RestoreStringSingleQuotes | format.RestoreKeyWordLowercase | format.RestoreNameLowercase | format.RestoreNameBackQuotes 24 | ) 25 | 26 | type SqlAttr struct { 27 | MysqlType *types.FieldType 28 | PgType *ptypes.T 29 | LiteType *sqlite.Type 30 | Options []*ast.ColumnOption 31 | Comment string 32 | } 33 | 34 | // Column ... 35 | type Column struct { 36 | Node 37 | 38 | CurrentAttr SqlAttr 39 | PreviousAttr SqlAttr 40 | } 41 | 42 | // GetType ... 43 | func (c Column) GetType() byte { 44 | if c.CurrentAttr.MysqlType != nil { 45 | return c.CurrentAttr.MysqlType.Tp 46 | } 47 | 48 | return 0 49 | } 50 | 51 | // DataType ... 52 | func (c Column) DataType() string { 53 | return c.typeDefinition(false) 54 | } 55 | 56 | // Constraint ... 57 | func (c Column) Constraint() string { 58 | for _, opt := range c.CurrentAttr.Options { 59 | switch opt.Tp { 60 | case ast.ColumnOptionPrimaryKey: 61 | return "PK" 62 | case ast.ColumnOptionReference: 63 | return "FK" 64 | } 65 | } 66 | 67 | return "" 68 | } 69 | 70 | // HasDefaultValue ... 71 | func (c Column) HasDefaultValue() bool { 72 | for _, opt := range c.CurrentAttr.Options { 73 | if opt.Tp == ast.ColumnOptionDefaultValue { 74 | return true 75 | } 76 | } 77 | 78 | return false 79 | } 80 | 81 | func (c Column) hashValue() string { 82 | strHash := sql.EscapeSqlName(c.Name) 83 | strHash += " " + c.typeDefinition(false) 84 | hash := md5.Sum([]byte(strHash)) 85 | return hex.EncodeToString(hash[:]) 86 | } 87 | 88 | func (c Column) migrationUp(tbName, after string, ident int) []string { 89 | switch c.Action { 90 | case MigrateNoAction: 91 | return nil 92 | 93 | case MigrateAddAction: 94 | strSql := sql.EscapeSqlName(c.Name) 95 | 96 | if ident > len(c.Name) { 97 | strSql += strings.Repeat(" ", ident-len(c.Name)) 98 | } 99 | 100 | strSql += c.definition(false) 101 | 102 | if ident < 0 { 103 | if after != "" { 104 | return []string{fmt.Sprintf(sql.AlterTableAddColumnAfterStm(), sql.EscapeSqlName(tbName), strSql, sql.EscapeSqlName(after))} 105 | } 106 | return []string{fmt.Sprintf(sql.AlterTableAddColumnFirstStm(), sql.EscapeSqlName(tbName), strSql)} 107 | } 108 | 109 | return append([]string{strSql}, c.migrationCommentUp(tbName)...) 110 | 111 | case MigrateRemoveAction: 112 | if sql.IsSqlite() { 113 | return nil 114 | } 115 | 116 | return []string{fmt.Sprintf(sql.AlterTableDropColumnStm(), sql.EscapeSqlName(tbName), sql.EscapeSqlName(c.Name))} 117 | 118 | case MigrateModifyAction: 119 | def, isPk := c.pkDefinition(false) 120 | if isPk { 121 | if _, isPrevPk := c.pkDefinition(true); isPrevPk { 122 | // avoid repeat define primary key 123 | def = strings.Replace(def, " "+sql.PrimaryOption(), "", 1) 124 | } 125 | } 126 | 127 | return []string{fmt.Sprintf(sql.AlterTableModifyColumnStm(), sql.EscapeSqlName(tbName), sql.EscapeSqlName(c.Name)+def)} 128 | 129 | case MigrateRevertAction: 130 | prevDef, isPrevPk := c.pkDefinition(true) 131 | if isPrevPk { 132 | if _, isPk := c.pkDefinition(false); isPk { 133 | // avoid repeat define primary key 134 | prevDef = strings.Replace(prevDef, " "+sql.PrimaryOption(), "", 1) 135 | } 136 | } 137 | 138 | return []string{fmt.Sprintf(sql.AlterTableModifyColumnStm(), sql.EscapeSqlName(tbName), sql.EscapeSqlName(c.Name)+prevDef)} 139 | 140 | case MigrateRenameAction: 141 | return []string{fmt.Sprintf(sql.AlterTableRenameColumnStm(), sql.EscapeSqlName(tbName), sql.EscapeSqlName(c.OldName), sql.EscapeSqlName(c.Name))} 142 | 143 | default: 144 | return nil 145 | } 146 | } 147 | 148 | func (c Column) migrationCommentUp(tbName string) []string { 149 | if c.CurrentAttr.Comment == "" || sql.GetDialect() != sql_templates.PostgresDialect { 150 | return nil 151 | } 152 | 153 | // apply for postgres only 154 | return []string{fmt.Sprintf(sql.ColumnComment(), tbName, c.Name, c.CurrentAttr.Comment)} 155 | } 156 | 157 | func (c Column) migrationDown(tbName, after string) []string { 158 | switch c.Action { 159 | case MigrateNoAction: 160 | return nil 161 | 162 | case MigrateAddAction: 163 | c.Action = MigrateRemoveAction 164 | 165 | case MigrateRemoveAction: 166 | c.Action = MigrateAddAction 167 | 168 | case MigrateModifyAction: 169 | c.Action = MigrateRevertAction 170 | 171 | case MigrateRenameAction: 172 | c.Name, c.OldName = c.OldName, c.Name 173 | 174 | default: 175 | return nil 176 | } 177 | 178 | return c.migrationUp(tbName, after, -1) 179 | } 180 | 181 | func (c Column) pkDefinition(isPrev bool) (string, bool) { 182 | attr := c.CurrentAttr 183 | if isPrev { 184 | attr = c.PreviousAttr 185 | } 186 | strSql := " " + c.typeDefinition(isPrev) 187 | 188 | isPrimaryKey := false 189 | for _, opt := range attr.Options { 190 | if opt.Tp == ast.ColumnOptionPrimaryKey { 191 | isPrimaryKey = true 192 | } 193 | 194 | b := bytes.NewBufferString("") 195 | var ctx *format.RestoreCtx 196 | 197 | if sql.IsLowercase() { 198 | ctx = format.NewRestoreCtx(LowerRestoreFlag, b) 199 | } else { 200 | ctx = format.NewRestoreCtx(UppercaseRestoreFlag, b) 201 | } 202 | 203 | if sql.IsPostgres() && opt.Tp == ast.ColumnOptionDefaultValue { 204 | strSql += " " + opt.StrValue 205 | continue 206 | } 207 | 208 | if sql.IsSqlite() { 209 | // SQLite overrides, that pingcap doesn't support 210 | if opt.Tp == ast.ColumnOptionDefaultValue { 211 | // Parsed StrValue may be quoted in single quotes, which breaks SQL expression. 212 | // We need to unquote it and, if it's a TEXT column. quote it again with double quotes. 213 | expression, err := strconv.Unquote(opt.StrValue) 214 | if err != nil { 215 | expression = opt.StrValue 216 | } 217 | if len(expression) >= 2 && expression[0] == '\'' && expression[len(expression)-1] == '\'' { 218 | // remove single quotes. strconv may not detect it 219 | expression = expression[1 : len(expression)-1] 220 | } 221 | if c.typeDefinition(isPrev) == "TEXT" { 222 | expression = strconv.Quote(expression) 223 | } 224 | strSql += " DEFAULT " + expression 225 | continue 226 | } 227 | 228 | if opt.Tp == ast.ColumnOptionAutoIncrement { 229 | strSql += " AUTOINCREMENT" 230 | continue 231 | } 232 | if opt.Tp == ast.ColumnOptionUniqKey { 233 | strSql += " UNIQUE" 234 | continue 235 | } 236 | if opt.Tp == ast.ColumnOptionCheck { 237 | strSql += " CHECK (" + opt.StrValue + ")" 238 | continue 239 | } 240 | } 241 | 242 | if opt.Tp == ast.ColumnOptionReference && opt.Refer == nil { // manual add 243 | continue 244 | } 245 | 246 | _ = opt.Restore(ctx) 247 | strSql += " " + b.String() 248 | } 249 | 250 | return strSql, isPrimaryKey 251 | } 252 | 253 | func (c Column) definition(isPrev bool) string { 254 | def, _ := c.pkDefinition(isPrev) 255 | return def 256 | } 257 | 258 | func (c Column) typeDefinition(isPrev bool) string { 259 | attr := c.CurrentAttr 260 | if isPrev { 261 | attr = c.PreviousAttr 262 | } 263 | 264 | switch { 265 | case sql.IsPostgres() && attr.PgType != nil: 266 | return attr.PgType.SQLString() 267 | case sql.IsSqlite() && attr.LiteType != nil: 268 | return attr.LiteType.Name.Name 269 | case attr.MysqlType != nil: 270 | return attr.MysqlType.String() 271 | } 272 | 273 | return "" // column type is empty 274 | } 275 | -------------------------------------------------------------------------------- /element/foreign_key.go: -------------------------------------------------------------------------------- 1 | package element 2 | 3 | import ( 4 | "crypto/md5" 5 | "encoding/hex" 6 | "fmt" 7 | "strings" 8 | ) 9 | 10 | // ForeignKey ... 11 | type ForeignKey struct { 12 | Node 13 | Table string 14 | Column string 15 | RefTable string 16 | RefColumn string 17 | Constraint string 18 | } 19 | 20 | func (fk ForeignKey) hashValue() string { 21 | strHash := strings.Join(fk.migrationUp(fk.Name), ";") 22 | hash := md5.Sum([]byte(strHash)) 23 | return hex.EncodeToString(hash[:]) 24 | } 25 | 26 | func (fk ForeignKey) migrationUp(tbName string) []string { 27 | switch fk.Action { 28 | case MigrateNoAction: 29 | return nil 30 | 31 | case MigrateAddAction: 32 | return []string{fmt.Sprintf(sql.CreateForeignKeyStm(), 33 | sql.EscapeSqlName(tbName), sql.EscapeSqlName(fk.Name), sql.EscapeSqlName(fk.Column), 34 | sql.EscapeSqlName(fk.RefTable), sql.EscapeSqlName(fk.RefColumn))} 35 | 36 | case MigrateRemoveAction: 37 | return []string{fmt.Sprintf(sql.DropForeignKeyStm(), 38 | sql.EscapeSqlName(tbName), sql.EscapeSqlName(fk.Name))} 39 | 40 | case MigrateModifyAction: 41 | return nil 42 | 43 | case MigrateRenameAction: 44 | return nil 45 | 46 | default: 47 | return nil 48 | } 49 | } 50 | 51 | func (fk ForeignKey) migrationDown(tbName string) []string { 52 | switch fk.Action { 53 | case MigrateNoAction: 54 | return nil 55 | 56 | case MigrateAddAction: 57 | fk.Action = MigrateRemoveAction 58 | 59 | case MigrateRemoveAction: 60 | fk.Action = MigrateAddAction 61 | 62 | case MigrateModifyAction: 63 | 64 | case MigrateRenameAction: 65 | 66 | default: 67 | return nil 68 | } 69 | 70 | return fk.migrationUp(tbName) 71 | } 72 | -------------------------------------------------------------------------------- /element/index.go: -------------------------------------------------------------------------------- 1 | package element 2 | 3 | import ( 4 | "crypto/md5" 5 | "encoding/hex" 6 | "fmt" 7 | "strings" 8 | 9 | "github.com/pingcap/parser/ast" 10 | "github.com/pingcap/parser/model" 11 | ) 12 | 13 | // Index ... 14 | type Index struct { 15 | Node 16 | Typ ast.IndexKeyType 17 | IndexType model.IndexType 18 | CnsTyp ast.ConstraintType 19 | Columns []string 20 | } 21 | 22 | func (i Index) hashValue() string { 23 | strHash := strings.Join(i.migrationUp(""), ";") 24 | hash := md5.Sum([]byte(strHash)) 25 | return hex.EncodeToString(hash[:]) 26 | } 27 | 28 | func (i Index) migrationUp(tbName string) []string { 29 | switch i.Action { 30 | case MigrateNoAction: 31 | return nil 32 | 33 | case MigrateAddAction: 34 | if i.CnsTyp == ast.ConstraintPrimaryKey { 35 | return []string{fmt.Sprintf(sql.CreatePrimaryKeyStm(), 36 | sql.EscapeSqlName(tbName), 37 | strings.Join(sql.EscapeSqlNames(i.Columns), ", "))} 38 | } 39 | 40 | switch i.Typ { 41 | case ast.IndexKeyTypeNone: 42 | return []string{fmt.Sprintf(sql.CreateIndexStm(i.IndexType.String()), 43 | sql.EscapeSqlName(i.Name), sql.EscapeSqlName(tbName), 44 | strings.Join(sql.EscapeSqlNames(i.Columns), ", "))} 45 | 46 | case ast.IndexKeyTypeUnique: 47 | return []string{fmt.Sprintf(sql.CreateUniqueIndexStm(i.IndexType.String()), 48 | sql.EscapeSqlName(i.Name), sql.EscapeSqlName(tbName), 49 | strings.Join(sql.EscapeSqlNames(i.Columns), ", "))} 50 | 51 | default: 52 | return nil 53 | } 54 | 55 | case MigrateRemoveAction: 56 | if i.CnsTyp == ast.ConstraintPrimaryKey { 57 | return []string{fmt.Sprintf(sql.DropPrimaryKeyStm(), 58 | sql.EscapeSqlName(tbName))} 59 | } 60 | 61 | if sql.IsSqlite() { 62 | return []string{fmt.Sprintf(sql.DropIndexStm(), 63 | sql.EscapeSqlName(i.Name))} 64 | } 65 | 66 | return []string{fmt.Sprintf(sql.DropIndexStm(), 67 | sql.EscapeSqlName(i.Name), 68 | sql.EscapeSqlName(tbName))} 69 | 70 | case MigrateModifyAction: 71 | strRems := make([]string, 2) 72 | i.Action = MigrateRemoveAction 73 | strRems[0] = i.migrationUp(tbName)[0] 74 | i.Action = MigrateAddAction 75 | strRems[1] = i.migrationUp(tbName)[0] 76 | return strRems 77 | 78 | case MigrateRenameAction: 79 | return []string{fmt.Sprintf(sql.AlterTableRenameIndexStm(), 80 | sql.EscapeSqlName(tbName), 81 | sql.EscapeSqlName(i.OldName), 82 | sql.EscapeSqlName(i.Name))} 83 | 84 | default: 85 | return nil 86 | } 87 | } 88 | 89 | func (i Index) migrationDown(tbName string) []string { 90 | switch i.Action { 91 | case MigrateNoAction: 92 | return nil 93 | 94 | case MigrateAddAction: 95 | i.Action = MigrateRemoveAction 96 | 97 | case MigrateRemoveAction: 98 | i.Action = MigrateAddAction 99 | 100 | case MigrateModifyAction: 101 | 102 | case MigrateRenameAction: 103 | i.Name, i.OldName = i.OldName, i.Name 104 | 105 | default: 106 | return nil 107 | } 108 | 109 | return i.migrationUp(tbName) 110 | } 111 | -------------------------------------------------------------------------------- /element/migration.go: -------------------------------------------------------------------------------- 1 | package element 2 | 3 | import ( 4 | "crypto/md5" 5 | "encoding/binary" 6 | "strings" 7 | 8 | "github.com/pingcap/parser/ast" 9 | sql_templates "github.com/sunary/sqlize/sql-templates" 10 | "github.com/sunary/sqlize/utils" 11 | ) 12 | 13 | var ( 14 | sql *sql_templates.Sql 15 | ignoreFieldOrder bool 16 | ) 17 | 18 | // Migration ... 19 | type Migration struct { 20 | currentTable string 21 | Tables []Table 22 | tableIndexes map[string]int 23 | } 24 | 25 | // NewMigration ... 26 | func NewMigration(dialect sql_templates.SqlDialect, lowercase, ignoreOrder bool) Migration { 27 | sql = sql_templates.NewSql(dialect, lowercase) 28 | ignoreFieldOrder = ignoreOrder 29 | 30 | return Migration{ 31 | Tables: []Table{}, 32 | tableIndexes: map[string]int{}, 33 | } 34 | } 35 | 36 | // Using set current table 37 | func (m *Migration) Using(tbName string) { 38 | if tbName != "" { 39 | m.currentTable = tbName 40 | } 41 | } 42 | 43 | // AddTable ... 44 | func (m *Migration) AddTable(tb Table) { 45 | id := m.getIndexTable(tb.Name) 46 | if id == -1 { 47 | m.Tables = append(m.Tables, tb) 48 | m.tableIndexes[tb.Name] = len(m.Tables) - 1 49 | return 50 | } 51 | 52 | m.Tables[id] = tb 53 | } 54 | 55 | // RemoveTable ... 56 | func (m *Migration) RemoveTable(tbName string) { 57 | id := m.getIndexTable(tbName) 58 | if id == -1 { 59 | tb := NewTableWithAction(tbName, MigrateRemoveAction) 60 | m.Tables = append(m.Tables, *tb) 61 | m.tableIndexes[tb.Name] = len(m.Tables) - 1 62 | return 63 | } 64 | 65 | if m.Tables[id].Action == MigrateAddAction { 66 | m.Tables[id].Action = MigrateNoAction 67 | } else { 68 | m.Tables[id].Action = MigrateRemoveAction 69 | } 70 | } 71 | 72 | // RenameTable ... 73 | func (m *Migration) RenameTable(oldName, newName string) { 74 | if id := m.getIndexTable(oldName); id >= 0 { 75 | m.Tables[id].Action = MigrateRemoveAction 76 | m.Tables[id].OldName = oldName 77 | m.Tables[id].Name = newName 78 | m.tableIndexes[newName] = id 79 | delete(m.tableIndexes, oldName) 80 | } 81 | } 82 | 83 | // AddColumn ... 84 | func (m *Migration) AddColumn(tbName string, col Column) { 85 | if tbName == "" { 86 | tbName = m.currentTable 87 | } 88 | 89 | id := m.getIndexTable(tbName) 90 | if id == -1 { 91 | tb := NewTableWithAction(tbName, MigrateModifyAction) 92 | m.AddTable(*tb) 93 | id = len(m.Tables) - 1 94 | } 95 | 96 | m.Tables[id].AddColumn(col) 97 | } 98 | 99 | // SetColumnPosition ... 100 | func (m *Migration) SetColumnPosition(tbName string, pos *ast.ColumnPosition) { 101 | if tbName == "" { 102 | tbName = m.currentTable 103 | } 104 | 105 | if id := m.getIndexTable(tbName); id >= 0 { 106 | m.Tables[id].columnPosition = pos 107 | } 108 | } 109 | 110 | // RemoveColumn ... 111 | func (m *Migration) RemoveColumn(tbName, colName string) { 112 | if tbName == "" { 113 | tbName = m.currentTable 114 | } 115 | 116 | id := m.getIndexTable(tbName) 117 | if id == -1 { 118 | tb := NewTableWithAction(tbName, MigrateModifyAction) 119 | m.AddTable(*tb) 120 | id = len(m.Tables) - 1 121 | } 122 | 123 | m.Tables[id].removeColumn(colName) 124 | } 125 | 126 | // RenameColumn ... 127 | func (m *Migration) RenameColumn(tbName, oldName, newName string) { 128 | if tbName == "" { 129 | tbName = m.currentTable 130 | } 131 | 132 | if id := m.getIndexTable(tbName); id >= 0 { 133 | m.Tables[id].RenameColumn(oldName, newName) 134 | } 135 | } 136 | 137 | // AddIndex ... 138 | func (m *Migration) AddComment(tbName, colName, comment string) { 139 | if tbName == "" { 140 | tbName = m.currentTable 141 | } 142 | 143 | id := m.getIndexTable(tbName) 144 | if id == -1 { 145 | return 146 | } 147 | 148 | colIdx := m.Tables[id].getIndexColumn(colName) 149 | if colIdx == -1 { 150 | return 151 | } 152 | 153 | m.Tables[id].Columns[colIdx].CurrentAttr.Comment = comment 154 | } 155 | 156 | // AddIndex ... 157 | func (m *Migration) AddIndex(tbName string, idx Index) { 158 | if tbName == "" { 159 | tbName = m.currentTable 160 | } 161 | 162 | id := m.getIndexTable(tbName) 163 | if id == -1 { 164 | tb := NewTableWithAction(tbName, MigrateModifyAction) 165 | m.AddTable(*tb) 166 | id = len(m.Tables) - 1 167 | } 168 | 169 | m.Tables[id].AddIndex(idx) 170 | } 171 | 172 | // RemoveIndex ... 173 | func (m *Migration) RemoveIndex(tbName, idxName string) { 174 | if tbName == "" { 175 | tbName = m.currentTable 176 | } 177 | 178 | id := m.getIndexTable(tbName) 179 | if id == -1 { 180 | tb := NewTableWithAction(tbName, MigrateModifyAction) 181 | m.AddTable(*tb) 182 | id = len(m.Tables) - 1 183 | } 184 | 185 | m.Tables[id].RemoveIndex(idxName) 186 | } 187 | 188 | // RenameIndex ... 189 | func (m *Migration) RenameIndex(tbName, oldName, newName string) { 190 | if tbName == "" { 191 | tbName = m.currentTable 192 | } 193 | 194 | if id := m.getIndexTable(tbName); id >= 0 { 195 | m.Tables[id].RenameIndex(oldName, newName) 196 | } 197 | } 198 | 199 | // AddForeignKey ... 200 | func (m *Migration) AddForeignKey(tbName string, fk ForeignKey) { 201 | if tbName == "" { 202 | tbName = m.currentTable 203 | } 204 | if fk.Table == "" { 205 | fk.Table = tbName 206 | } 207 | 208 | id := m.getIndexTable(tbName) 209 | if id == -1 { 210 | tb := NewTableWithAction(tbName, MigrateModifyAction) 211 | m.AddTable(*tb) 212 | id = len(m.Tables) - 1 213 | } 214 | 215 | m.Tables[id].AddForeignKey(fk) 216 | } 217 | 218 | // RemoveForeignKey ... 219 | func (m *Migration) RemoveForeignKey(tbName, fkName string) { 220 | if tbName == "" { 221 | tbName = m.currentTable 222 | } 223 | 224 | id := m.getIndexTable(tbName) 225 | if id == -1 { 226 | tb := NewTableWithAction(tbName, MigrateModifyAction) 227 | m.AddTable(*tb) 228 | id = len(m.Tables) - 1 229 | } 230 | 231 | m.Tables[id].RemoveForeignKey(fkName) 232 | } 233 | 234 | func (m Migration) getIndexTable(tableName string) int { 235 | if v, ok := m.tableIndexes[tableName]; ok { 236 | return v 237 | } 238 | 239 | return -1 240 | } 241 | 242 | // Diff differ between 2 migrations 243 | func (m *Migration) Diff(old Migration) { 244 | for i := range m.Tables { 245 | if j := old.getIndexTable(m.Tables[i].Name); j >= 0 { 246 | m.Tables[i].Diff(old.Tables[j]) 247 | m.Tables[i].Action = MigrateNoAction 248 | } 249 | } 250 | 251 | for j := range old.Tables { 252 | if i := m.getIndexTable(old.Tables[j].Name); i == -1 { 253 | old.Tables[j].Action = MigrateRemoveAction 254 | m.AddTable(old.Tables[j]) 255 | } 256 | } 257 | } 258 | 259 | // HashValue ... 260 | func (m Migration) HashValue() int64 { 261 | if len(m.Tables) == 0 { 262 | return 0 263 | } 264 | 265 | tbs := make([]string, len(m.Tables)) 266 | for i := range m.Tables { 267 | tbs[i] = m.Tables[i].hashValue() 268 | } 269 | 270 | strHash := strings.Join(tbs, ";") 271 | hash := md5.Sum([]byte(strHash)) 272 | return int64(binary.BigEndian.Uint64(hash[:])) 273 | } 274 | 275 | // MigrationUp ... 276 | func (m Migration) MigrationUp() string { 277 | strTables := make([]string, 0) 278 | for i := range m.Tables { 279 | if m.Tables[i].Name == utils.DefaultMigrationTable { 280 | continue 281 | } 282 | 283 | m.Tables[i].Arrange() 284 | 285 | strTb := make([]string, 0) 286 | mCols, dropCols := m.Tables[i].MigrationColumnUp() 287 | if len(mCols) > 0 { 288 | strTb = append(strTb, strings.Join(mCols, "\n")) 289 | } 290 | if mIdxs := m.Tables[i].MigrationIndexUp(dropCols); len(mIdxs) > 0 { 291 | strTb = append(strTb, strings.Join(mIdxs, "\n")) 292 | } 293 | if mFks := m.Tables[i].MigrationForeignKeyUp(dropCols); len(mFks) > 0 { 294 | strTb = append(strTb, strings.Join(mFks, "\n")) 295 | } 296 | 297 | if len(strTb) > 0 { 298 | strTables = append(strTables, strings.Join(strTb, "\n")) 299 | } 300 | } 301 | 302 | return strings.Join(strTables, "\n\n") 303 | } 304 | 305 | // MigrationDown ... 306 | func (m Migration) MigrationDown() string { 307 | strTables := make([]string, 0) 308 | for i := range m.Tables { 309 | if m.Tables[i].Name == utils.DefaultMigrationTable { 310 | continue 311 | } 312 | 313 | m.Tables[i].Arrange() 314 | 315 | strTb := make([]string, 0) 316 | mCols, dropCols := m.Tables[i].MigrationColumnDown() 317 | if len(mCols) > 0 { 318 | strTb = append(strTb, strings.Join(mCols, "\n")) 319 | } 320 | if mIdxs := m.Tables[i].MigrationIndexDown(dropCols); len(mIdxs) > 0 { 321 | strTb = append(strTb, strings.Join(mIdxs, "\n")) 322 | } 323 | if mFks := m.Tables[i].MigrationForeignKeyDown(dropCols); len(mFks) > 0 { 324 | strTb = append(strTb, strings.Join(mFks, "\n")) 325 | } 326 | 327 | if len(strTb) > 0 { 328 | strTables = append(strTables, strings.Join(strTb, "\n")) 329 | } 330 | } 331 | 332 | return strings.Join(strTables, "\n\n") 333 | } 334 | -------------------------------------------------------------------------------- /element/node.go: -------------------------------------------------------------------------------- 1 | package element 2 | 3 | // MigrateAction ... 4 | type MigrateAction int8 5 | 6 | const ( 7 | // MigrateNoAction ... 8 | MigrateNoAction MigrateAction = iota 9 | // MigrateAddAction ... 10 | MigrateAddAction 11 | // MigrateRemoveAction ... 12 | MigrateRemoveAction 13 | // MigrateModifyAction ... 14 | MigrateModifyAction 15 | // MigrateRevertAction ... 16 | MigrateRevertAction 17 | // MigrateRenameAction ... 18 | MigrateRenameAction 19 | ) 20 | 21 | // Node primitive element 22 | type Node struct { 23 | Name string 24 | OldName string 25 | Action MigrateAction 26 | } 27 | -------------------------------------------------------------------------------- /element/table.go: -------------------------------------------------------------------------------- 1 | package element 2 | 3 | import ( 4 | "crypto/md5" 5 | "encoding/hex" 6 | "fmt" 7 | "sort" 8 | "strings" 9 | 10 | ptypes "github.com/auxten/postgresql-parser/pkg/sql/types" 11 | "github.com/pingcap/parser/ast" 12 | "github.com/pingcap/parser/types" 13 | "github.com/sunary/sqlize/utils" 14 | ) 15 | 16 | // Table ... 17 | type Table struct { 18 | Node 19 | 20 | Columns []Column 21 | columnIndexes map[string]int 22 | columnPosition *ast.ColumnPosition 23 | 24 | Indexes []Index 25 | indexIndexes map[string]int 26 | 27 | ForeignKeys []ForeignKey 28 | indexForeignKeys map[string]int 29 | } 30 | 31 | // NewTable ... 32 | func NewTable(name string) *Table { 33 | return NewTableWithAction(name, MigrateAddAction) 34 | } 35 | 36 | // NewTableWithAction ... 37 | func NewTableWithAction(name string, action MigrateAction) *Table { 38 | return &Table{ 39 | Node: Node{ 40 | Name: name, 41 | Action: action, 42 | }, 43 | Columns: []Column{}, 44 | columnIndexes: map[string]int{}, 45 | Indexes: []Index{}, 46 | indexIndexes: map[string]int{}, 47 | ForeignKeys: []ForeignKey{}, 48 | indexForeignKeys: map[string]int{}, 49 | } 50 | } 51 | 52 | // AddColumn ... 53 | func (t *Table) AddColumn(col Column) { 54 | id := t.getIndexColumn(col.Name) 55 | switch { 56 | case id == -1: 57 | t.Columns = append(t.Columns, col) 58 | t.columnIndexes[col.Name] = len(t.Columns) - 1 59 | id = len(t.Columns) - 1 60 | 61 | case t.Columns[id].Action != MigrateAddAction: 62 | t.Columns[id] = col 63 | 64 | default: 65 | t.Columns[id].CurrentAttr.Options = append(t.Columns[id].CurrentAttr.Options, col.CurrentAttr.Options...) 66 | 67 | if size := len(t.Columns[id].CurrentAttr.Options); size > 0 { 68 | for i := range t.Columns[id].CurrentAttr.Options[:size-1] { 69 | if t.Columns[id].CurrentAttr.Options[i].Tp == ast.ColumnOptionPrimaryKey { 70 | t.Columns[id].CurrentAttr.Options[i], t.Columns[id].CurrentAttr.Options[size-1] = t.Columns[id].CurrentAttr.Options[size-1], t.Columns[id].CurrentAttr.Options[i] 71 | break 72 | } 73 | } 74 | } 75 | 76 | t.Columns[id].CurrentAttr.MysqlType = col.CurrentAttr.MysqlType 77 | return 78 | } 79 | 80 | if t.columnPosition != nil { 81 | defer func() { 82 | t.columnPosition = nil 83 | }() 84 | 85 | switch t.columnPosition.Tp { 86 | case ast.ColumnPositionFirst: 87 | t.swapOrder(col.Name, id, 0) 88 | 89 | case ast.ColumnPositionAfter: 90 | if afterID := t.getIndexColumn(t.columnPosition.RelativeColumn.Name.O); afterID >= 0 { 91 | t.swapOrder(col.Name, id, afterID+1) 92 | } 93 | } 94 | } 95 | } 96 | 97 | func (t *Table) swapOrder(colName string, oldID, newID int) { 98 | if oldID == newID { 99 | return 100 | } 101 | 102 | if newID == len(t.Columns)-1 { 103 | t.Columns = append(t.Columns, t.Columns[oldID]) 104 | } else { 105 | oldCol := t.Columns[oldID] 106 | t.Columns = append(t.Columns[:newID+1], t.Columns[newID:]...) 107 | t.Columns[newID] = oldCol 108 | } 109 | 110 | switch { 111 | case oldID > newID: 112 | for k := range t.columnIndexes { 113 | if t.columnIndexes[k] >= newID && t.columnIndexes[k] < oldID { 114 | t.columnIndexes[k]++ 115 | } 116 | } 117 | 118 | if oldID == len(t.Columns)-2 { 119 | t.Columns = t.Columns[:len(t.Columns)-1] 120 | } else { 121 | t.Columns = append(t.Columns[:oldID], t.Columns[oldID+1:]...) 122 | } 123 | 124 | case oldID < newID: 125 | for k := range t.columnIndexes { 126 | if t.columnIndexes[k] > oldID && t.columnIndexes[k] <= newID { 127 | t.columnIndexes[k]-- 128 | } 129 | } 130 | 131 | t.Columns = append(t.Columns[:oldID-1], t.Columns[oldID:]...) 132 | } 133 | 134 | t.columnIndexes[colName] = newID 135 | } 136 | 137 | func (t *Table) removeColumn(colName string) { 138 | id := t.getIndexColumn(colName) 139 | switch { 140 | case id == -1: 141 | col := Column{Node: Node{Name: colName, Action: MigrateRemoveAction}} 142 | t.Columns = append(t.Columns, col) 143 | t.columnIndexes[colName] = len(t.Columns) - 1 144 | 145 | case t.Columns[id].Action == MigrateAddAction: 146 | t.Columns[id].Action = MigrateNoAction 147 | 148 | default: 149 | t.Columns[id].Action = MigrateRemoveAction 150 | } 151 | } 152 | 153 | // RenameColumn ... 154 | func (t *Table) RenameColumn(oldName, newName string) { 155 | if id := t.getIndexColumn(oldName); id >= 0 { 156 | t.Columns[id].Action = MigrateRenameAction 157 | t.Columns[id].OldName = oldName 158 | t.Columns[id].Name = newName 159 | t.columnIndexes[newName] = id 160 | delete(t.columnIndexes, oldName) 161 | } 162 | } 163 | 164 | // AddIndex ... 165 | func (t *Table) AddIndex(idx Index) { 166 | id := t.getIndexIndex(idx.Name) 167 | if id == -1 { 168 | t.Indexes = append(t.Indexes, idx) 169 | t.indexIndexes[idx.Name] = len(t.Indexes) - 1 170 | return 171 | } 172 | 173 | t.Indexes[id] = idx 174 | } 175 | 176 | // RemoveIndex ... 177 | func (t *Table) RemoveIndex(idxName string) { 178 | id := t.getIndexIndex(idxName) 179 | if id == -1 { 180 | idx := Index{Node: Node{Name: idxName, Action: MigrateRemoveAction}} 181 | t.Indexes = append(t.Indexes, idx) 182 | t.indexIndexes[idxName] = len(t.Indexes) - 1 183 | return 184 | } 185 | 186 | if t.Indexes[id].Action == MigrateAddAction { 187 | t.Indexes[id].Action = MigrateNoAction 188 | } else { 189 | t.Indexes[id].Action = MigrateRemoveAction 190 | } 191 | } 192 | 193 | // RenameIndex ... 194 | func (t *Table) RenameIndex(oldName, newName string) { 195 | if id := t.getIndexIndex(oldName); id >= 0 { 196 | t.Indexes[id].Action = MigrateRenameAction 197 | t.Indexes[id].OldName = oldName 198 | t.Indexes[id].Name = newName 199 | t.indexIndexes[newName] = id 200 | delete(t.indexIndexes, oldName) 201 | } 202 | } 203 | 204 | // AddForeignKey ... 205 | func (t *Table) AddForeignKey(fk ForeignKey) { 206 | id := t.getIndexForeignKey(fk.Name) 207 | if id == -1 { 208 | t.ForeignKeys = append(t.ForeignKeys, fk) 209 | t.indexForeignKeys[fk.Name] = len(t.ForeignKeys) - 1 210 | } else { 211 | t.ForeignKeys[id] = fk 212 | } 213 | 214 | for i := range t.Columns { 215 | if t.Columns[i].Name == fk.Column { 216 | t.Columns[i].CurrentAttr.Options = append(t.Columns[i].CurrentAttr.Options, &ast.ColumnOption{ 217 | Tp: ast.ColumnOptionReference, 218 | }) 219 | } 220 | } 221 | } 222 | 223 | // RemoveForeignKey ... 224 | func (t *Table) RemoveForeignKey(fkName string) { 225 | id := t.getIndexForeignKey(fkName) 226 | if id == -1 { 227 | fk := ForeignKey{Node: Node{Name: fkName, Action: MigrateRemoveAction}} 228 | t.ForeignKeys = append(t.ForeignKeys, fk) 229 | t.indexForeignKeys[fkName] = len(t.ForeignKeys) - 1 230 | return 231 | } 232 | 233 | if t.ForeignKeys[id].Action == MigrateAddAction { 234 | t.ForeignKeys[id].Action = MigrateNoAction 235 | } else { 236 | t.ForeignKeys[id].Action = MigrateRemoveAction 237 | } 238 | } 239 | 240 | func (t Table) getIndexColumn(colName string) int { 241 | if v, ok := t.columnIndexes[colName]; ok { 242 | return v 243 | } 244 | 245 | return -1 246 | } 247 | 248 | func (t Table) getIndexIndex(idxName string) int { 249 | if v, ok := t.indexIndexes[idxName]; ok { 250 | return v 251 | } 252 | 253 | return -1 254 | } 255 | 256 | func (t Table) getIndexForeignKey(fkName string) int { 257 | if v, ok := t.indexForeignKeys[fkName]; ok { 258 | return v 259 | } 260 | 261 | return -1 262 | } 263 | 264 | func hasChangedMysqlOptions(new, old []*ast.ColumnOption) bool { 265 | if len(new) != len(old) { 266 | return true 267 | } 268 | 269 | mNew := map[ast.ColumnOptionType]int{} 270 | for i := range old { 271 | mNew[old[i].Tp] += 1 272 | } 273 | 274 | mOld := map[ast.ColumnOptionType]int{} 275 | for i := range old { 276 | mOld[old[i].Tp] += 1 277 | } 278 | 279 | for k, v := range mOld { 280 | if mNew[k] != v { 281 | return true 282 | } 283 | } 284 | 285 | return false 286 | } 287 | 288 | func hasChangedMysqlType(new, old *types.FieldType) bool { 289 | return new != nil && new.String() != old.String() 290 | } 291 | 292 | func hasChangePostgresType(new, old *ptypes.T) bool { 293 | return new != nil && new.SQLString() != old.SQLString() 294 | } 295 | 296 | // Diff differ between 2 migrations 297 | func (t *Table) Diff(old Table) { 298 | for i := range t.Columns { 299 | if j := old.getIndexColumn(t.Columns[i].Name); t.Columns[i].Action == MigrateAddAction && 300 | j >= 0 && old.Columns[j].Action != MigrateNoAction { 301 | if hasChangedMysqlOptions(t.Columns[i].CurrentAttr.Options, old.Columns[j].CurrentAttr.Options) || 302 | hasChangedMysqlType(t.Columns[i].CurrentAttr.MysqlType, old.Columns[j].CurrentAttr.MysqlType) || 303 | hasChangePostgresType(t.Columns[i].CurrentAttr.PgType, old.Columns[j].CurrentAttr.PgType) { 304 | t.Columns[i].Action = MigrateModifyAction 305 | t.Columns[i].PreviousAttr = old.Columns[j].CurrentAttr 306 | } else { 307 | t.Columns[i].Action = MigrateNoAction 308 | } 309 | } 310 | } 311 | 312 | for j := range old.Columns { 313 | if old.Columns[j].Action == MigrateAddAction && t.getIndexColumn(old.Columns[j].Name) == -1 { 314 | old.Columns[j].Action = MigrateRemoveAction 315 | t.AddColumn(old.Columns[j]) 316 | 317 | if j == 0 { 318 | t.swapOrder(old.Columns[j].Name, len(t.Columns)-1, 0) 319 | } else if newID, ok := old.columnIndexes[old.Columns[j-1].Name]; ok { 320 | t.swapOrder(old.Columns[j].Name, len(t.Columns)-1, newID+1) 321 | } 322 | } 323 | } 324 | 325 | for i := range t.Indexes { 326 | if j := old.getIndexIndex(t.Indexes[i].Name); t.Indexes[i].Action == MigrateAddAction && 327 | j >= 0 && old.Indexes[j].Action != MigrateNoAction { 328 | if t.Indexes[i].Typ == old.Indexes[j].Typ && utils.SlideStrEqual(t.Indexes[i].Columns, old.Indexes[j].Columns) { 329 | t.Indexes[i].Action = MigrateNoAction 330 | } else { 331 | t.Indexes[i] = old.Indexes[j] 332 | t.Indexes[i].Action = MigrateModifyAction 333 | } 334 | } 335 | } 336 | 337 | for j := range old.Indexes { 338 | if old.Indexes[j].Action == MigrateAddAction && t.getIndexIndex(old.Indexes[j].Name) == -1 { 339 | old.Indexes[j].Action = MigrateRemoveAction 340 | t.AddIndex(old.Indexes[j]) 341 | } 342 | } 343 | 344 | for i := range t.ForeignKeys { 345 | if j := old.getIndexForeignKey(t.ForeignKeys[i].Name); t.ForeignKeys[i].Action == MigrateAddAction && 346 | j >= 0 && old.ForeignKeys[j].Action != MigrateNoAction { 347 | t.ForeignKeys[i] = old.ForeignKeys[j] 348 | t.ForeignKeys[i].Action = MigrateModifyAction 349 | } 350 | } 351 | 352 | for j := range old.ForeignKeys { 353 | if old.ForeignKeys[j].Action == MigrateAddAction && t.getIndexForeignKey(old.ForeignKeys[j].Name) == -1 { 354 | old.ForeignKeys[j].Action = MigrateRemoveAction 355 | t.AddForeignKey(old.ForeignKeys[j]) 356 | } 357 | } 358 | } 359 | 360 | type mOrder struct { 361 | k string 362 | v int 363 | } 364 | 365 | // Arrange correct column order 366 | func (t *Table) Arrange() { 367 | orders := make([]mOrder, 0, len(t.columnIndexes)) 368 | for k, v := range t.columnIndexes { 369 | orders = append(orders, mOrder{ 370 | k: k, 371 | v: v, 372 | }) 373 | } 374 | sort.Slice(orders, func(i, j int) bool { 375 | return orders[i].v < orders[j].v 376 | }) 377 | 378 | for i := range orders { 379 | for j := range t.Columns { 380 | if orders[i].k == t.Columns[j].Name { 381 | t.Columns[i], t.Columns[j] = t.Columns[j], t.Columns[i] 382 | break 383 | } 384 | } 385 | } 386 | } 387 | 388 | func (t Table) hashValue() string { 389 | cols, idxs := make([]string, len(t.Columns)), make([]string, len(t.Indexes)) 390 | for i := range t.Columns { 391 | cols[i] = t.Columns[i].hashValue() 392 | } 393 | sort.Slice(cols, func(i, j int) bool { 394 | return cols[i] < cols[j] 395 | }) 396 | 397 | for i := range t.Indexes { 398 | idxs[i] = t.Indexes[i].hashValue() 399 | } 400 | sort.Slice(idxs, func(i, j int) bool { 401 | return idxs[i] < idxs[j] 402 | }) 403 | 404 | strHash := strings.Join(append(cols, idxs...), ";") 405 | hash := md5.Sum([]byte(strHash)) 406 | return hex.EncodeToString(hash[:]) 407 | } 408 | 409 | // MigrationColumnUp ... 410 | func (t Table) MigrationColumnUp() ([]string, map[string]struct{}) { 411 | switch t.Action { 412 | case MigrateNoAction: 413 | strSqls := make([]string, 0) 414 | dropCols := make(map[string]struct{}) 415 | for i := range t.Columns { 416 | if t.Columns[i].Action != MigrateNoAction { 417 | after := "" 418 | if t.Columns[i].Action == MigrateAddAction { 419 | for j := i - 1; j >= 0; j-- { 420 | if t.Columns[j].Action != MigrateRemoveAction { 421 | after = t.Columns[j].Name 422 | break 423 | } 424 | } 425 | } else if t.Columns[i].Action == MigrateRemoveAction { 426 | dropCols[t.Columns[i].Name] = struct{}{} 427 | } 428 | 429 | if ignoreFieldOrder || after == "" { 430 | strSqls = append(strSqls, t.Columns[i].migrationUp(t.Name, "", -1)...) 431 | } else { 432 | strSqls = append(strSqls, t.Columns[i].migrationUp(t.Name, after, -1)...) 433 | } 434 | } 435 | } 436 | 437 | return strSqls, dropCols 438 | 439 | case MigrateAddAction: 440 | maxIdent := len(t.Columns[0].Name) 441 | for i := range t.Columns { 442 | if t.Columns[i].Action == MigrateAddAction || t.Columns[i].Action == MigrateModifyAction || t.Columns[i].Action == MigrateRenameAction { 443 | if len(t.Columns[i].Name) > maxIdent { 444 | maxIdent = len(t.Columns[i].Name) 445 | } 446 | } 447 | } 448 | 449 | strCols := make([]string, 0) 450 | commentCols := make([]string, 0) 451 | for i := range t.Columns { 452 | if t.Columns[i].Action == MigrateAddAction { 453 | strCols = append(strCols, " "+t.Columns[i].migrationUp("", "", maxIdent)[0]) 454 | } else if t.Columns[i].Action == MigrateModifyAction || t.Columns[i].Action == MigrateRenameAction { 455 | nCol := t.Columns[i] 456 | nCol.Action = MigrateAddAction 457 | strCols = append(strCols, " "+nCol.migrationUp("", "", maxIdent)[0]) 458 | } 459 | 460 | commentCols = append(commentCols, t.Columns[i].migrationCommentUp(t.Name)...) 461 | } 462 | 463 | return append([]string{fmt.Sprintf(sql.CreateTableStm(), sql.EscapeSqlName(t.Name), strings.Join(strCols, ",\n"), "")}, commentCols...), nil 464 | 465 | case MigrateRemoveAction: 466 | return []string{fmt.Sprintf(sql.DropTableStm(), sql.EscapeSqlName(t.Name))}, nil 467 | 468 | case MigrateModifyAction: 469 | return nil, nil 470 | 471 | default: 472 | return nil, nil 473 | } 474 | } 475 | 476 | // MigrationIndexUp ... 477 | func (t Table) MigrationIndexUp(dropCols map[string]struct{}) []string { 478 | switch t.Action { 479 | case MigrateNoAction: 480 | strSqls := make([]string, 0) 481 | for i := range t.Indexes { 482 | if _, ok := dropCols[t.Indexes[i].Columns[0]]; t.Indexes[i].Action != MigrateNoAction && 483 | (t.Indexes[i].Action != MigrateRemoveAction || !ok) { 484 | strSqls = append(strSqls, t.Indexes[i].migrationUp(t.Name)...) 485 | } 486 | } 487 | return strSqls 488 | 489 | case MigrateAddAction: 490 | strSqls := make([]string, 0) 491 | for i := range t.Indexes { 492 | if t.Indexes[i].Action == MigrateAddAction { 493 | strSqls = append(strSqls, t.Indexes[i].migrationUp(t.Name)...) 494 | } 495 | } 496 | return strSqls 497 | 498 | case MigrateRemoveAction: 499 | return nil 500 | 501 | case MigrateModifyAction: 502 | return nil 503 | 504 | default: 505 | return nil 506 | } 507 | } 508 | 509 | // MigrationForeignKeyUp ... 510 | func (t Table) MigrationForeignKeyUp(dropCols map[string]struct{}) []string { 511 | switch t.Action { 512 | case MigrateNoAction: 513 | strSqls := make([]string, 0) 514 | for i := range t.ForeignKeys { 515 | if _, ok := dropCols[t.ForeignKeys[i].Column]; t.ForeignKeys[i].Action != MigrateNoAction && 516 | (t.ForeignKeys[i].Action != MigrateRemoveAction || !ok) { 517 | strSqls = append(strSqls, t.ForeignKeys[i].migrationUp(t.Name)...) 518 | } 519 | } 520 | return strSqls 521 | 522 | case MigrateAddAction: 523 | strSqls := make([]string, 0) 524 | for i := range t.ForeignKeys { 525 | if t.ForeignKeys[i].Action == MigrateAddAction { 526 | strSqls = append(strSqls, t.ForeignKeys[i].migrationUp(t.Name)...) 527 | } 528 | } 529 | return strSqls 530 | 531 | case MigrateRemoveAction: 532 | return nil 533 | 534 | case MigrateModifyAction: 535 | return nil 536 | 537 | default: 538 | return nil 539 | } 540 | } 541 | 542 | // MigrationColumnDown ... 543 | func (t Table) MigrationColumnDown() ([]string, map[string]struct{}) { 544 | switch t.Action { 545 | case MigrateNoAction: 546 | strSqls := make([]string, 0) 547 | dropCols := make(map[string]struct{}) 548 | for i := range t.Columns { 549 | if t.Columns[i].Action != MigrateNoAction { 550 | after := "" 551 | if t.Columns[i].Action == MigrateRemoveAction { 552 | for j := i - 1; j >= 0; j-- { 553 | if t.Columns[j].Action != MigrateAddAction { 554 | after = t.Columns[j].Name 555 | break 556 | } 557 | } 558 | } else if t.Columns[i].Action == MigrateAddAction { 559 | dropCols[t.Columns[i].Name] = struct{}{} 560 | } 561 | 562 | if ignoreFieldOrder || after == "" { 563 | strSqls = append(strSqls, t.Columns[i].migrationDown(t.Name, "")...) 564 | } else { 565 | strSqls = append(strSqls, t.Columns[i].migrationDown(t.Name, after)...) 566 | } 567 | } 568 | } 569 | 570 | return strSqls, dropCols 571 | 572 | case MigrateAddAction: 573 | t.Action = MigrateRemoveAction 574 | return t.MigrationColumnUp() 575 | 576 | case MigrateRemoveAction: 577 | t.Action = MigrateAddAction 578 | return t.MigrationColumnUp() 579 | 580 | case MigrateModifyAction: 581 | return nil, nil 582 | 583 | default: 584 | return nil, nil 585 | } 586 | } 587 | 588 | // MigrationIndexDown ... 589 | func (t Table) MigrationIndexDown(dropCols map[string]struct{}) []string { 590 | switch t.Action { 591 | case MigrateNoAction: 592 | strSqls := make([]string, 0) 593 | for i := range t.Indexes { 594 | if _, ok := dropCols[t.Indexes[i].Columns[0]]; t.Indexes[i].Action != MigrateNoAction && 595 | (t.Indexes[i].Action != MigrateRemoveAction || !ok) { 596 | strSqls = append(strSqls, t.Indexes[i].migrationDown(t.Name)...) 597 | } 598 | } 599 | return strSqls 600 | 601 | case MigrateAddAction: 602 | t.Action = MigrateRemoveAction 603 | return t.MigrationIndexUp(dropCols) 604 | 605 | case MigrateRemoveAction: 606 | t.Action = MigrateAddAction 607 | return t.MigrationIndexUp(dropCols) 608 | 609 | case MigrateModifyAction: 610 | return nil 611 | 612 | default: 613 | return nil 614 | } 615 | } 616 | 617 | // MigrationForeignKeyDown ... 618 | func (t Table) MigrationForeignKeyDown(dropCols map[string]struct{}) []string { 619 | switch t.Action { 620 | case MigrateNoAction: 621 | strSqls := make([]string, 0) 622 | for i := range t.ForeignKeys { 623 | if _, ok := dropCols[t.ForeignKeys[i].Column]; t.ForeignKeys[i].Action != MigrateNoAction && 624 | (t.ForeignKeys[i].Action != MigrateRemoveAction || !ok) { 625 | strSqls = append(strSqls, t.ForeignKeys[i].migrationDown(t.Name)...) 626 | } 627 | } 628 | return strSqls 629 | 630 | case MigrateAddAction: 631 | t.Action = MigrateRemoveAction 632 | return t.MigrationForeignKeyUp(dropCols) 633 | 634 | case MigrateRemoveAction: 635 | t.Action = MigrateAddAction 636 | return t.MigrationForeignKeyUp(dropCols) 637 | 638 | case MigrateModifyAction: 639 | return nil 640 | 641 | default: 642 | return nil 643 | } 644 | } 645 | -------------------------------------------------------------------------------- /export/avro/builder.go: -------------------------------------------------------------------------------- 1 | package avro 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | 7 | "github.com/pingcap/parser/mysql" 8 | "github.com/pingcap/parser/types" 9 | "github.com/sunary/sqlize/element" 10 | ) 11 | 12 | // NewArvoSchema ... 13 | func NewArvoSchema(table element.Table) *RecordSchema { 14 | fields := buildFieldsFromTable(table) 15 | record := newRecordSchema(table.Name, table.Name) 16 | record.Name = table.Name 17 | //set payload , on before field 18 | record.Fields[0].Type = []interface{}{ 19 | "null", 20 | RecordSchema{ 21 | Name: "Value", 22 | Type: "record", 23 | Fields: fields, 24 | }, 25 | } 26 | 27 | return record 28 | } 29 | 30 | func buildFieldsFromTable(table element.Table) []Field { 31 | fields := []Field{} 32 | 33 | for _, col := range table.Columns { 34 | field := Field{ 35 | Name: col.Name, 36 | } 37 | if col.HasDefaultValue() { 38 | field.Type = []interface{}{"null", getAvroType(col)} 39 | field.Default = nil 40 | } else { 41 | field.Type = getAvroType(col) 42 | } 43 | fields = append(fields, field) 44 | } 45 | 46 | return fields 47 | } 48 | 49 | func getAvroType(col element.Column) interface{} { 50 | switch col.GetType() { 51 | //case mysql.TypeBit, mysql.TypeBlob, mysql.TypeTinyBlob, mysql.TypeMediumBlob, mysql.TypeLongBlob: 52 | // return "[]byte" 53 | 54 | case mysql.TypeTiny: 55 | return "bool" 56 | 57 | case mysql.TypeEnum: 58 | return map[string]interface{}{ 59 | "type": "string", 60 | "connect.version": 1, 61 | "connect.parameters": map[string]string{ 62 | "allowed": strings.Join(col.CurrentAttr.MysqlType.Elems, ","), 63 | }, 64 | "connect.default": "init", 65 | "connect.name": "io.debezium.data.Enum", 66 | } 67 | } 68 | 69 | switch col.CurrentAttr.MysqlType.EvalType() { 70 | case types.ETInt: 71 | return "int" 72 | 73 | case types.ETDecimal: 74 | displayFlen, displayDecimal := col.CurrentAttr.MysqlType.Flen, col.CurrentAttr.MysqlType.Decimal 75 | return map[string]interface{}{ 76 | "type": "bytes", 77 | "scale": displayDecimal, 78 | "precision": displayFlen, 79 | "connect.version": 1, 80 | "connect.parameters": map[string]string{ 81 | "scale": strconv.Itoa(displayDecimal), 82 | "connect.decimal.precision": strconv.Itoa(displayFlen), 83 | }, 84 | "connect.name": "org.apache.kafka.connect.data.Decimal", 85 | "logicalType": "decimal", 86 | } 87 | 88 | case types.ETReal: 89 | return "float64" 90 | 91 | case types.ETDatetime, types.ETTimestamp: 92 | return map[string]interface{}{ 93 | "type": "string", 94 | "connect.version": 1, 95 | "connect.default": "1970-01-01T00:00:00Z", 96 | "connect.name": "io.debezium.time.ZonedTimestamp", 97 | } 98 | 99 | case types.ETJson: 100 | return map[string]interface{}{ 101 | "type": "string", 102 | "connect.version": 1, 103 | "connect.name": "io.debezium.data.Json", 104 | } 105 | 106 | case types.ETString: 107 | return "string" 108 | 109 | default: 110 | return "string" 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /export/avro/schema.go: -------------------------------------------------------------------------------- 1 | package avro 2 | 3 | // Field ... 4 | type Field struct { 5 | Name string `json:"name"` 6 | Type interface{} `json:"type"` 7 | Default interface{} `json:"default,omitempty"` 8 | } 9 | 10 | // RecordSchema ... 11 | type RecordSchema struct { 12 | Type string `json:"type"` 13 | Name string `json:"name"` 14 | Namespace string `json:"namespace"` 15 | //shouldn't set directly to this field, only read and for json export 16 | Fields []Field `json:"fields"` 17 | ConnectName string `json:"connect.name"` 18 | } 19 | 20 | func newRecordSchema(namespace, connectName string) *RecordSchema { 21 | return &RecordSchema{ 22 | Type: "record", 23 | Name: "envelop", 24 | Namespace: namespace, 25 | ConnectName: connectName, 26 | Fields: []Field{ 27 | { 28 | Name: "before", 29 | Default: nil, 30 | }, 31 | { 32 | Name: "after", 33 | Type: []string{ 34 | "null", 35 | "Value", 36 | }, 37 | Default: nil, 38 | }, 39 | { 40 | Name: "op", 41 | Type: "string", 42 | }, 43 | { 44 | Name: "ts_ms", 45 | Type: []interface{}{ 46 | "null", 47 | "long", 48 | }, 49 | Default: nil, 50 | }, 51 | { 52 | Name: "transaction", 53 | Type: []interface{}{ 54 | "null", 55 | RecordSchema{ 56 | Type: "record", 57 | Name: "ConnectDefault", 58 | Namespace: "io.confluent.connect.avro", 59 | Fields: []Field{ 60 | { 61 | Name: "id", 62 | Type: "string", 63 | }, 64 | { 65 | Name: "total_order", 66 | Type: "long", 67 | }, 68 | { 69 | Name: "data_collection_order", 70 | Type: "long", 71 | }, 72 | }, 73 | }, 74 | }, 75 | Default: nil, 76 | }, 77 | }, 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /export/mermaidjs/builder.go: -------------------------------------------------------------------------------- 1 | package mermaidjs 2 | 3 | import ( 4 | "encoding/base64" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/sunary/sqlize/element" 9 | ) 10 | 11 | const ( 12 | erdTag = "erDiagram" 13 | liveUrl = "https://mermaid.ink/img/" 14 | defaultRelationType = "}o--||" // n-1 15 | ) 16 | 17 | type MermaidJs struct { 18 | entities []string 19 | relations []string 20 | } 21 | 22 | func NewMermaidJs(tables []element.Table) *MermaidJs { 23 | mm := &MermaidJs{} 24 | for i := range tables { 25 | mm.AddTable(tables[i]) 26 | } 27 | 28 | return mm 29 | } 30 | 31 | func (m *MermaidJs) AddTable(table element.Table) { 32 | normEntityName := func(s string) string { 33 | return strings.ToUpper(s) 34 | } 35 | 36 | tab := " " 37 | ent := []string{tab + normEntityName(table.Name) + " {"} 38 | 39 | tab = " " 40 | for _, c := range table.Columns { 41 | dataType := c.DataType() 42 | if strings.HasPrefix(strings.ToLower(dataType), "enum") { 43 | dataType = dataType[:4] 44 | } 45 | 46 | constraint := c.Constraint() 47 | 48 | cmt := c.CurrentAttr.Comment 49 | if cmt != "" { 50 | cmt = "\"" + cmt + "\"" 51 | } 52 | 53 | ent = append(ent, fmt.Sprintf("%s%s %s %s %s", tab, dataType, c.Name, constraint, cmt)) 54 | } 55 | 56 | tab = " " 57 | ent = append(ent, tab+"}") 58 | m.entities = append(m.entities, strings.Join(ent, "\n")) 59 | 60 | uniRel := map[string]bool{} 61 | for _, rel := range table.ForeignKeys { 62 | if _, ok := uniRel[rel.RefTable+"-"+rel.Table]; ok { 63 | continue 64 | } 65 | 66 | relType := defaultRelationType 67 | m.relations = append(m.relations, fmt.Sprintf("%s%s %s %s: %s", tab, normEntityName(rel.Table), relType, normEntityName(rel.RefTable), rel.Column)) 68 | uniRel[rel.Table+"-"+rel.RefTable] = true 69 | } 70 | } 71 | 72 | func (m MermaidJs) String() string { 73 | return erdTag + "\n" + strings.Join(m.entities, "\n") + "\n" + strings.Join(m.relations, "\n") 74 | } 75 | 76 | func (m MermaidJs) Live() string { 77 | mmParam := base64.URLEncoding.EncodeToString([]byte(m.String())) 78 | return liveUrl + string(mmParam) 79 | } 80 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/sunary/sqlize 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/auxten/postgresql-parser v1.0.1 7 | github.com/pingcap/parser v0.0.0-20200623164729-3a18f1e5dceb 8 | github.com/pkg/errors v0.9.1 9 | github.com/rqlite/sql v0.0.0-20241111133259-a4122fabb196 10 | ) 11 | 12 | require ( 13 | github.com/certifi/gocertifi v0.0.0-20210507211836-431795d63e8d // indirect 14 | github.com/cockroachdb/apd v1.1.1-0.20181017181144-bced77f817b4 // indirect 15 | github.com/cockroachdb/errors v1.11.3 // indirect 16 | github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b // indirect 17 | github.com/cockroachdb/redact v1.1.5 // indirect 18 | github.com/dustin/go-humanize v1.0.1 // indirect 19 | github.com/getsentry/raven-go v0.2.0 // indirect 20 | github.com/getsentry/sentry-go v0.28.1 // indirect 21 | github.com/gogo/protobuf v1.3.2 // indirect 22 | github.com/golang/protobuf v1.5.4 // indirect 23 | github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect 24 | github.com/kr/pretty v0.3.1 // indirect 25 | github.com/kr/text v0.2.0 // indirect 26 | github.com/lib/pq v1.10.9 // indirect 27 | github.com/pingcap/errors v0.11.4 // indirect 28 | github.com/pingcap/log v1.1.0 // indirect 29 | github.com/rogpeppe/go-internal v1.12.0 // indirect 30 | github.com/sirupsen/logrus v1.9.3 // indirect 31 | github.com/spf13/pflag v1.0.5 // indirect 32 | go.uber.org/multierr v1.11.0 // indirect 33 | go.uber.org/zap v1.27.0 // indirect 34 | golang.org/x/sync v0.7.0 // indirect 35 | golang.org/x/sys v0.21.0 // indirect 36 | golang.org/x/text v0.16.0 // indirect 37 | google.golang.org/genproto v0.0.0-20240617180043-68d350f18fd4 // indirect 38 | google.golang.org/genproto/googleapis/api v0.0.0-20240617180043-68d350f18fd4 // indirect 39 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240617180043-68d350f18fd4 // indirect 40 | google.golang.org/grpc v1.64.0 // indirect 41 | google.golang.org/protobuf v1.34.2 // indirect 42 | gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect 43 | ) 44 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= 4 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 5 | github.com/CloudyKit/fastprinter v0.0.0-20170127035650-74b38d55f37a/go.mod h1:EFZQ978U7x8IRnstaskI3IysnWY5Ao3QgZUKOXlsAdw= 6 | github.com/CloudyKit/jet v2.1.3-0.20180809161101-62edd43e4f88+incompatible/go.mod h1:HPYO+50pSWkPoj9Q/eq0aRGByCL6ScRlUmiEX5Zgm+w= 7 | github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY= 8 | github.com/Joker/jade v1.0.1-0.20190614124447-d475f43051e7/go.mod h1:6E6s8o2AE4KhCrqr6GRJjdC/gNfTdxkIXvuGZZda2VM= 9 | github.com/Shopify/goreferrer v0.0.0-20181106222321-ec9c9a553398/go.mod h1:a1uqRtAwp2Xwc6WNPJEufxJ7fx3npB4UV/JOLmbu5I0= 10 | github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= 11 | github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= 12 | github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= 13 | github.com/auxten/postgresql-parser v1.0.1 h1:x+qiEHAe2cH55Kly64dWh4tGvUKEQwMmJgma7a1kbj4= 14 | github.com/auxten/postgresql-parser v1.0.1/go.mod h1:Nf27dtv8EU1C+xNkoLD3zEwfgJfDDVi8Zl86gznxPvI= 15 | github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g= 16 | github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= 17 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 18 | github.com/certifi/gocertifi v0.0.0-20200922220541-2c3bb06c6054/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA= 19 | github.com/certifi/gocertifi v0.0.0-20210507211836-431795d63e8d h1:S2NE3iHSwP0XV47EEXL8mWmRdEfGscSJ+7EgePNgt0s= 20 | github.com/certifi/gocertifi v0.0.0-20210507211836-431795d63e8d/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA= 21 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 22 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 23 | github.com/cockroachdb/apd v1.1.1-0.20181017181144-bced77f817b4 h1:XWEdfNxDkZI3DXXlpo0hZJ1xdaH/f3CKuZpk93pS/Y0= 24 | github.com/cockroachdb/apd v1.1.1-0.20181017181144-bced77f817b4/go.mod h1:mdGz2CnkJrefFtlLevmE7JpL2zB9tKofya/6w7wWzNA= 25 | github.com/cockroachdb/datadriven v1.0.0/go.mod h1:5Ib8Meh+jk1RlHIXej6Pzevx/NLlNvQB9pmSBZErGA4= 26 | github.com/cockroachdb/errors v1.6.1/go.mod h1:tm6FTP5G81vwJ5lC0SizQo374JNCOPrHyXGitRJoDqM= 27 | github.com/cockroachdb/errors v1.8.2/go.mod h1:qGwQn6JmZ+oMjuLwjWzUNqblqk0xl4CVV3SQbGwK7Ac= 28 | github.com/cockroachdb/errors v1.11.3 h1:5bA+k2Y6r+oz/6Z/RFlNeVCesGARKuC6YymtcDrbC/I= 29 | github.com/cockroachdb/errors v1.11.3/go.mod h1:m4UIW4CDjx+R5cybPsNrRbreomiFqt8o1h1wUVazSd8= 30 | github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f/go.mod h1:i/u985jwjWRlyHXQbwatDASoW0RMlZ/3i9yJHE2xLkI= 31 | github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b h1:r6VH0faHjZeQy818SGhaone5OnYfxFR/+AzdY3sf5aE= 32 | github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs= 33 | github.com/cockroachdb/redact v1.0.8/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg= 34 | github.com/cockroachdb/redact v1.1.5 h1:u1PMllDkdFfPWaNGMyLD1+so+aq3uUItthCFqzwPJ30= 35 | github.com/cockroachdb/redact v1.1.5/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg= 36 | github.com/cockroachdb/sentry-go v0.6.1-cockroachdb.2/go.mod h1:8BT+cPK6xvFOcRlk0R8eg+OTkcqI6baNH4xAkpiYVvQ= 37 | github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM= 38 | github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= 39 | github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= 40 | github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 41 | github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= 42 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 43 | github.com/cznic/golex v0.0.0-20181122101858-9c343928389c/go.mod h1:+bmmJDNmKlhWNG+gwWCkaBoTy39Fs+bzRxVBzoTQbIc= 44 | github.com/cznic/mathutil v0.0.0-20181122101859-297441e03548/go.mod h1:e6NPNENfs9mPDVNRekM7lKScauxd5kXTr1Mfyig6TDM= 45 | github.com/cznic/parser v0.0.0-20160622100904-31edd927e5b1/go.mod h1:2B43mz36vGZNZEwkWi8ayRSSUXLfjL8OkbzwW4NcPMM= 46 | github.com/cznic/sortutil v0.0.0-20181122101858-f5f958428db8/go.mod h1:q2w6Bg5jeox1B+QkJ6Wp/+Vn0G/bo3f1uY7Fn3vivIQ= 47 | github.com/cznic/strutil v0.0.0-20171016134553-529a34b1c186/go.mod h1:AHHPPPXTw0h6pVabbcbyGRK1DckRn7r/STdZEeIDzZc= 48 | github.com/cznic/y v0.0.0-20170802143616-045f81c6662a/go.mod h1:1rk5VM7oSnA4vjp+hrLQ3HWHa+Y4yPCa3/CsJrcNnvs= 49 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 50 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 51 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 52 | github.com/dgraph-io/badger v1.6.0/go.mod h1:zwt7syl517jmP8s94KqSxTlM6IMsdhYy6psNgSztDR4= 53 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 54 | github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= 55 | github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= 56 | github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= 57 | github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= 58 | github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM= 59 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 60 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 61 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 62 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 63 | github.com/etcd-io/bbolt v1.3.3/go.mod h1:ZF2nL25h33cCyBtcyWeZ2/I3HQOfTP+0PIEvHjkjCrw= 64 | github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8= 65 | github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= 66 | github.com/flosch/pongo2 v0.0.0-20190707114632-bbf5a6c351f4/go.mod h1:T9YF2M40nIgbVgp3rreNmTged+9HrbNTIQf1PsaIiTA= 67 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 68 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 69 | github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc= 70 | github.com/getsentry/raven-go v0.2.0 h1:no+xWJRb5ZI7eE8TWgIq1jLulQiIoLG0IfYxv5JYMGs= 71 | github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= 72 | github.com/getsentry/sentry-go v0.28.1 h1:zzaSm/vHmGllRM6Tpx1492r0YDzauArdBfkJRtY6P5k= 73 | github.com/getsentry/sentry-go v0.28.1/go.mod h1:1fQZ+7l7eeJ3wYi82q5Hg8GqAPgefRq+FP/QhafYVgg= 74 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 75 | github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= 76 | github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM= 77 | github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98= 78 | github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= 79 | github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= 80 | github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8= 81 | github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= 82 | github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= 83 | github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= 84 | github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= 85 | github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= 86 | github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= 87 | github.com/gogo/googleapis v0.0.0-20180223154316-0cd9801be74a/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= 88 | github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 89 | github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= 90 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 91 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 92 | github.com/gogo/status v1.1.0/go.mod h1:BFv9nrluPLmrS0EmGVvLaPNmRosr9KapBYd5/hpY1WM= 93 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 94 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 95 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 96 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 97 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 98 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 99 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 100 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 101 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 102 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 103 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 104 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 105 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 106 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 107 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 108 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 109 | github.com/gomodule/redigo v1.7.1-0.20190724094224-574c33c3df38/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= 110 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 111 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 112 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 113 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 114 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 115 | github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 116 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 117 | github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= 118 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 119 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 120 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 121 | github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= 122 | github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= 123 | github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= 124 | github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= 125 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 126 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 127 | github.com/hydrogen18/memlistener v0.0.0-20141126152155-54553eb933fb/go.mod h1:qEIFzExnS6016fRpRfxrExeVn2gbClQA99gQhnIcdhE= 128 | github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= 129 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 130 | github.com/iris-contrib/blackfriday v2.0.0+incompatible/go.mod h1:UzZ2bDEoaSGPbkg6SAB4att1aAwTmVIx/5gCVqeyUdI= 131 | github.com/iris-contrib/go.uuid v2.0.0+incompatible/go.mod h1:iz2lgM/1UnEf1kP0L/+fafWORmlnuysV2EMP8MW+qe0= 132 | github.com/iris-contrib/i18n v0.0.0-20171121225848-987a633949d0/go.mod h1:pMCz62A0xJL6I+umB2YTlFRwWXaDFA0jy+5HzGiJjqI= 133 | github.com/iris-contrib/schema v0.0.1/go.mod h1:urYA3uvUNG1TIIjOSCzHr9/LmbQo8LrOcOqfqxa4hXw= 134 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 135 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 136 | github.com/juju/errors v0.0.0-20181118221551-089d3ea4e4d5/go.mod h1:W54LbzXuIE0boCoNJfwqpmkKJ1O4TCTZMetAt6jGk7Q= 137 | github.com/juju/loggo v0.0.0-20180524022052-584905176618/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U= 138 | github.com/juju/testing v0.0.0-20180920084828-472a3e8b2073/go.mod h1:63prj8cnj0tU0S9OHjGJn+b1h0ZghCndfnbQolrYTwA= 139 | github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k= 140 | github.com/kataras/golog v0.0.9/go.mod h1:12HJgwBIZFNGL0EJnMRhmvGA0PQGx8VFwrZtM4CqbAk= 141 | github.com/kataras/iris/v12 v12.0.1/go.mod h1:udK4vLQKkdDqMGJJVd/msuMtN6hpYJhg/lSzuxjhO+U= 142 | github.com/kataras/neffos v0.0.10/go.mod h1:ZYmJC07hQPW67eKuzlfY7SO3bC0mw83A3j6im82hfqw= 143 | github.com/kataras/pio v0.0.0-20190103105442-ea782b38602d/go.mod h1:NV88laa9UiiDuX9AhMbDPkGYSPugBOV6yTZB1l2K9Z0= 144 | github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= 145 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 146 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 147 | github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= 148 | github.com/klauspost/compress v1.9.0/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= 149 | github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= 150 | github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 151 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 152 | github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 153 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 154 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 155 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 156 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 157 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 158 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 159 | github.com/labstack/echo/v4 v4.1.11/go.mod h1:i541M3Fj6f76NZtHSj7TXnyM8n2gaodfvfxNnFqi74g= 160 | github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= 161 | github.com/lib/pq v1.9.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 162 | github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= 163 | github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 164 | github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 165 | github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 166 | github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 167 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 168 | github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= 169 | github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw= 170 | github.com/mediocregopher/mediocre-go-lib v0.0.0-20181029021733-cb65787f37ed/go.mod h1:dSsfyI2zABAdhcbvkXqgxOxrCsbYeHCPgrZkku60dSg= 171 | github.com/mediocregopher/radix/v3 v3.3.0/go.mod h1:EmfVyvspXz1uZEyPBMyGK+kjWiKQGvsUt6O3Pj+LDCQ= 172 | github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc= 173 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 174 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 175 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 176 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 177 | github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ= 178 | github.com/nats-io/nats.go v1.8.1/go.mod h1:BrFz9vVn0fU3AcH9Vn4Kd7W0NpJ651tD5omQ3M8LwxM= 179 | github.com/nats-io/nkeys v0.0.2/go.mod h1:dab7URMsZm6Z/jp9Z5UGa87Uutgc2mVpXLC4B7TDb/4= 180 | github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= 181 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 182 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 183 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 184 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 185 | github.com/onsi/ginkgo v1.13.0/go.mod h1:+REjRxOmWfHCjfv9TTWB1jD1Frx4XydAD3zm1lskyM0= 186 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 187 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 188 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= 189 | github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 h1:q2e307iGHPdTGp0hoxKjt1H5pDo6utceo3dQVK3I5XQ= 190 | github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5/go.mod h1:jvVRKCrJTQWu0XVbaOlby/2lO20uSCHEMzzplHXte1o= 191 | github.com/pingcap/check v0.0.0-20190102082844-67f458068fc8 h1:USx2/E1bX46VG32FIw034Au6seQ2fY9NEILmNh/UlQg= 192 | github.com/pingcap/check v0.0.0-20190102082844-67f458068fc8/go.mod h1:B1+S9LNcuMyLH/4HMTViQOJevkGiik3wW2AN9zb2fNQ= 193 | github.com/pingcap/errors v0.11.0/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= 194 | github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= 195 | github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= 196 | github.com/pingcap/log v0.0.0-20191012051959-b742a5d432e9/go.mod h1:4rbK1p9ILyIfb6hU7OG2CiWSqMXnp3JMbiaVJ6mvoY8= 197 | github.com/pingcap/log v1.1.0 h1:ELiPxACz7vdo1qAvvaWJg1NrYFoY6gqAh/+Uo6aXdD8= 198 | github.com/pingcap/log v1.1.0/go.mod h1:DWQW5jICDR7UJh4HtxXSM20Churx4CQL0fwL/SoOSA4= 199 | github.com/pingcap/parser v0.0.0-20200623164729-3a18f1e5dceb h1:v9iX5qIr8nG3QxMtlcTT+1DI0YD4HqABy7tuohbp28E= 200 | github.com/pingcap/parser v0.0.0-20200623164729-3a18f1e5dceb/go.mod h1:vQdbJqobJAgFyiRNNtXahpMoGWwPEuWciVEK5A20NS0= 201 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 202 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 203 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 204 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 205 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 206 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 207 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 208 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 209 | github.com/remyoudompheng/bigfft v0.0.0-20190728182440-6a916e37a237/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= 210 | github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= 211 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 212 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 213 | github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= 214 | github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= 215 | github.com/rqlite/sql v0.0.0-20240312185922-ffac88a740bd h1:wW6BtayFoKaaDeIvXRE3SZVPOscSKlYD+X3bB749+zk= 216 | github.com/rqlite/sql v0.0.0-20240312185922-ffac88a740bd/go.mod h1:ib9zVtNgRKiGuoMyUqqL5aNpk+r+++YlyiVIkclVqPg= 217 | github.com/rqlite/sql v0.0.0-20241111133259-a4122fabb196 h1:SjRKMwKLTEE3STO6unJlz4VlMjMv5NZgIdI9HikBeAc= 218 | github.com/rqlite/sql v0.0.0-20241111133259-a4122fabb196/go.mod h1:ib9zVtNgRKiGuoMyUqqL5aNpk+r+++YlyiVIkclVqPg= 219 | github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= 220 | github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= 221 | github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw= 222 | github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= 223 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 224 | github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= 225 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 226 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 227 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= 228 | github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= 229 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= 230 | github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 231 | github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= 232 | github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= 233 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 234 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 235 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 236 | github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= 237 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 238 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 239 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 240 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 241 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 242 | github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= 243 | github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= 244 | github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= 245 | github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4= 246 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 247 | github.com/valyala/fasthttp v1.6.0/go.mod h1:FstJa9V+Pj9vQ7OJie2qMHdwemEDaDiSdBnvPM1Su9w= 248 | github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= 249 | github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio= 250 | github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= 251 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= 252 | github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= 253 | github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= 254 | github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= 255 | github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= 256 | github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= 257 | github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc= 258 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 259 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 260 | go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 261 | go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 262 | go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 263 | go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 264 | go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= 265 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 266 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 267 | go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= 268 | go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= 269 | go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= 270 | go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 271 | go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 272 | go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= 273 | go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 274 | go.uber.org/zap v1.12.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= 275 | go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= 276 | go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= 277 | go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= 278 | golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 279 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 280 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 281 | golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 282 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 283 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 284 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 285 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 286 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 287 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 288 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 289 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 290 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 291 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 292 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 293 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 294 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 295 | golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 296 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 297 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 298 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 299 | golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 300 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 301 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 302 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 303 | golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 304 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 305 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 306 | golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 307 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 308 | golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 309 | golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= 310 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 311 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 312 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 313 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 314 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 315 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 316 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 317 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 318 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 319 | golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= 320 | golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 321 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 322 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 323 | golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 324 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 325 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 326 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 327 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 328 | golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 329 | golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 330 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 331 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 332 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 333 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 334 | golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 335 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 336 | golang.org/x/sys v0.0.0-20201214210602-f9fddec55a1e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 337 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 338 | golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= 339 | golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 340 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 341 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 342 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 343 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 344 | golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 345 | golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= 346 | golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= 347 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 348 | golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 349 | golang.org/x/tools v0.0.0-20181221001348-537d06c36207/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 350 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 351 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 352 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 353 | golang.org/x/tools v0.0.0-20190327201419-c70d86f8b7cf/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 354 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 355 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 356 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 357 | golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 358 | golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 359 | golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 360 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 361 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 362 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 363 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 364 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 365 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 366 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 367 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 368 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 369 | google.golang.org/genproto v0.0.0-20180518175338-11a468237815/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 370 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 371 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 372 | google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 373 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 374 | google.golang.org/genproto v0.0.0-20200911024640-645f7a48b24f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 375 | google.golang.org/genproto v0.0.0-20240617180043-68d350f18fd4 h1:CUiCqkPw1nNrNQzCCG4WA65m0nAmQiwXHpub3dNyruU= 376 | google.golang.org/genproto v0.0.0-20240617180043-68d350f18fd4/go.mod h1:EvuUDCulqGgV80RvP1BHuom+smhX4qtlhnNatHuroGQ= 377 | google.golang.org/genproto/googleapis/api v0.0.0-20240617180043-68d350f18fd4 h1:MuYw1wJzT+ZkybKfaOXKp5hJiZDn2iHaXRw0mRYdHSc= 378 | google.golang.org/genproto/googleapis/api v0.0.0-20240617180043-68d350f18fd4/go.mod h1:px9SlOOZBg1wM1zdnr8jEL4CNGUBZ+ZKYtNPApNQc4c= 379 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240617180043-68d350f18fd4 h1:Di6ANFilr+S60a4S61ZM00vLdw0IrQOSMS2/6mrnOU0= 380 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240617180043-68d350f18fd4/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= 381 | google.golang.org/grpc v1.12.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= 382 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 383 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 384 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 385 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 386 | google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= 387 | google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= 388 | google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY= 389 | google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg= 390 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 391 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 392 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 393 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 394 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 395 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 396 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 397 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 398 | google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= 399 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 400 | google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= 401 | google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= 402 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 403 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 404 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 405 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 406 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 407 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 408 | gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= 409 | gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y= 410 | gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= 411 | gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= 412 | gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= 413 | gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= 414 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 415 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 416 | gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 417 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 418 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 419 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 420 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 421 | gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 422 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 423 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 424 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 425 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 426 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 427 | -------------------------------------------------------------------------------- /options.go: -------------------------------------------------------------------------------- 1 | package sqlize 2 | 3 | import ( 4 | sql_templates "github.com/sunary/sqlize/sql-templates" 5 | ) 6 | 7 | type sqlizeOptions struct { 8 | migrationFolder string 9 | migrationUpSuffix string 10 | migrationDownSuffix string 11 | migrationTable string 12 | 13 | sqlTag string 14 | dialect sql_templates.SqlDialect 15 | lowercase bool 16 | pluralTableName bool 17 | generateComment bool 18 | ignoreFieldOrder bool 19 | } 20 | 21 | type funcSqlizeOption struct { 22 | f func(*sqlizeOptions) 23 | } 24 | 25 | func (fmo *funcSqlizeOption) apply(do *sqlizeOptions) { 26 | fmo.f(do) 27 | } 28 | 29 | func newFuncSqlizeOption(f func(*sqlizeOptions)) *funcSqlizeOption { 30 | return &funcSqlizeOption{ 31 | f: f, 32 | } 33 | } 34 | 35 | // SqlizeOption ... 36 | type SqlizeOption interface { 37 | apply(*sqlizeOptions) 38 | } 39 | 40 | // WithMigrationFolder ... 41 | func WithMigrationFolder(path string) SqlizeOption { 42 | return newFuncSqlizeOption(func(o *sqlizeOptions) { 43 | o.migrationFolder = path 44 | }) 45 | } 46 | 47 | // WithMigrationSuffix ... 48 | func WithMigrationSuffix(upSuffix, downSuffix string) SqlizeOption { 49 | return newFuncSqlizeOption(func(o *sqlizeOptions) { 50 | o.migrationUpSuffix = upSuffix 51 | o.migrationDownSuffix = downSuffix 52 | }) 53 | } 54 | 55 | // WithMigrationTable default is 'schema_migration' 56 | func WithMigrationTable(table string) SqlizeOption { 57 | return newFuncSqlizeOption(func(o *sqlizeOptions) { 58 | o.migrationTable = table 59 | }) 60 | } 61 | 62 | // WithSqlTag default is `sql` 63 | func WithSqlTag(sqlTag string) SqlizeOption { 64 | return newFuncSqlizeOption(func(o *sqlizeOptions) { 65 | o.sqlTag = sqlTag 66 | }) 67 | } 68 | 69 | // WithMysql default 70 | func WithMysql() SqlizeOption { 71 | return newFuncSqlizeOption(func(o *sqlizeOptions) { 72 | o.dialect = sql_templates.MysqlDialect 73 | }) 74 | } 75 | 76 | // WithPostgresql ... 77 | func WithPostgresql() SqlizeOption { 78 | return newFuncSqlizeOption(func(o *sqlizeOptions) { 79 | o.dialect = sql_templates.PostgresDialect 80 | }) 81 | } 82 | 83 | // WithSqlserver ... 84 | func WithSqlserver() SqlizeOption { 85 | return newFuncSqlizeOption(func(o *sqlizeOptions) { 86 | o.dialect = sql_templates.SqlserverDialect 87 | }) 88 | } 89 | 90 | // WithSqlite ... 91 | func WithSqlite() SqlizeOption { 92 | return newFuncSqlizeOption(func(o *sqlizeOptions) { 93 | o.dialect = sql_templates.SqliteDialect 94 | }) 95 | } 96 | 97 | // WithSqlUppercase default 98 | func WithSqlUppercase() SqlizeOption { 99 | return newFuncSqlizeOption(func(o *sqlizeOptions) { 100 | o.lowercase = false 101 | }) 102 | } 103 | 104 | // WithSqlLowercase ... 105 | func WithSqlLowercase() SqlizeOption { 106 | return newFuncSqlizeOption(func(o *sqlizeOptions) { 107 | o.lowercase = true 108 | }) 109 | } 110 | 111 | // Table name plus s default 112 | func WithPluralTableName() SqlizeOption { 113 | return newFuncSqlizeOption(func(o *sqlizeOptions) { 114 | o.pluralTableName = true 115 | }) 116 | } 117 | 118 | // WithCommentGenerate default is off 119 | func WithCommentGenerate() SqlizeOption { 120 | return newFuncSqlizeOption(func(o *sqlizeOptions) { 121 | o.generateComment = true 122 | }) 123 | } 124 | 125 | // WithIgnoreFieldOrder ... 126 | func WithIgnoreFieldOrder() SqlizeOption { 127 | return newFuncSqlizeOption(func(o *sqlizeOptions) { 128 | o.ignoreFieldOrder = true 129 | }) 130 | } 131 | -------------------------------------------------------------------------------- /sql-builder/builder.go: -------------------------------------------------------------------------------- 1 | package sql_builder 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "reflect" 7 | "regexp" 8 | "strings" 9 | "time" 10 | 11 | sql_templates "github.com/sunary/sqlize/sql-templates" 12 | "github.com/sunary/sqlize/utils" 13 | ) 14 | 15 | // Tag prefixes 16 | const ( 17 | SqlTagDefault = "sql" 18 | prefixColumn = "column:" // set column name, eg: 'column:column_name' 19 | prefixEmbedded = "embedded_prefix:" // set embed prefix for flatten struct, eg: 'embedded_prefix:base_' 20 | prefixPreviousName = ",previous:" // mark previous name-field, eg: 'column:column_name,previous:old_name' 21 | prefixType = "type:" // set field type, eg: 'type:VARCHAR(64)' 22 | prefixDefault = "default:" // set default value, eg: 'default:0' 23 | prefixComment = "comment:" // comment field, eg: 'comment:sth you want to comment' 24 | ) 25 | 26 | // Special tags 27 | const ( 28 | tagIsSquash = "squash" 29 | tagIsEmbedded = "embedded" 30 | tagEnum = "enum" // type:ENUM('open','close') 31 | ) 32 | 33 | // Null and key constraints 34 | const ( 35 | tagIsNull = "null" 36 | tagIsNotNull = "not_null" 37 | tagIsAutoIncrement = "auto_increment" 38 | tagIsPrimaryKey = "primary_key" // this field is primary key, eg: 'primary_key' 39 | ) 40 | 41 | // Index related 42 | const ( 43 | tagIsIndex = "index" // indexing this field, eg: 'index' (=> idx_column_name) 44 | tagIsUniqueIndex = "unique" // unique indexing this field, eg: 'unique' (=> idx_column_name) 45 | prefixIndex = "index:" // indexing with name, eg: 'index:idx_name' 46 | prefixUniqueIndex = "unique:" // unique indexing with name, eg: 'unique:idx_name' 47 | prefixIndexColumns = "index_columns:" // indexing these fields, eg: 'index_columns:col1,col2' (=> idx_col1_col2) 48 | prefixIndexType = "index_type:" // indexing with type, eg: 'index_type:btree' (default) or 'index_type:hash' 49 | ) 50 | 51 | // Foreign key related 52 | const ( 53 | prefixForeignKey = "foreign_key:" // 'foreign_key:' 54 | prefixFkReferences = "references:" // 'references:' 55 | prefixFkConstraint = "constraint:" // 'constraint:' 56 | ) 57 | 58 | // Function names 59 | const ( 60 | funcTableName = "TableName" 61 | ) 62 | 63 | var ( 64 | reflectValueFields = map[string]struct{}{ 65 | "typ": {}, 66 | "ptr": {}, 67 | "flag": {}, 68 | } 69 | ignoredFieldComment = map[string]struct{}{ 70 | "id": {}, 71 | "created_at": {}, 72 | "updated_at": {}, 73 | "deleted_at": {}, 74 | } 75 | compileKeepEnumChar = regexp.MustCompile(`[^0-9A-Za-z,\\-_]+`) 76 | ) 77 | 78 | // SqlBuilder ... 79 | type SqlBuilder struct { 80 | sql *sql_templates.Sql 81 | sqlTag string 82 | dialect sql_templates.SqlDialect 83 | generateComment bool 84 | pluralTableName bool 85 | tables map[string]string 86 | } 87 | 88 | // NewSqlBuilder ... 89 | func NewSqlBuilder(opts ...SqlBuilderOption) *SqlBuilder { 90 | o := sqlBuilderOptions{ 91 | dialect: sql_templates.MysqlDialect, 92 | lowercase: false, 93 | pluralTableName: false, 94 | sqlTag: SqlTagDefault, 95 | } 96 | for i := range opts { 97 | opts[i].apply(&o) 98 | } 99 | 100 | return &SqlBuilder{ 101 | sql: sql_templates.NewSql(o.dialect, o.lowercase), 102 | dialect: o.dialect, 103 | sqlTag: o.sqlTag, 104 | pluralTableName: o.pluralTableName, 105 | generateComment: o.generateComment, 106 | tables: map[string]string{}, 107 | } 108 | } 109 | 110 | // MappingTables ... 111 | func (s *SqlBuilder) MappingTables(m map[string]string) { 112 | for k, v := range m { 113 | s.tables[k] = v 114 | } 115 | } 116 | 117 | // AddTable ... 118 | func (s SqlBuilder) AddTable(obj interface{}) string { 119 | _, tableName := s.GetTableName(obj) 120 | columns, columnsHistory, indexes := s.parseStruct(tableName, "", obj) 121 | 122 | sqlPrimaryKey := s.sql.PrimaryOption() 123 | for i := range columns { 124 | if strings.Index(columns[i], sqlPrimaryKey) > 0 { 125 | columns[0], columns[i] = columns[i], columns[0] 126 | break 127 | } 128 | } 129 | 130 | tableComment := "" 131 | comments := []string{} 132 | if s.generateComment { 133 | switch s.dialect { 134 | case sql_templates.PostgresDialect: 135 | comments = append(comments, fmt.Sprintf(s.sql.TableComment(), s.sql.EscapeSqlName(tableName), tableName)) 136 | 137 | default: 138 | tableComment = " " + fmt.Sprintf(s.sql.TableComment(), tableName) 139 | } 140 | } 141 | 142 | sqls := []string{fmt.Sprintf(s.sql.CreateTableStm(), 143 | s.sql.EscapeSqlName(tableName), 144 | strings.Join(columns, ",\n"), tableComment)} 145 | for _, h := range columnsHistory { 146 | sqls = append(sqls, 147 | fmt.Sprintf(s.sql.AlterTableRenameColumnStm(), 148 | s.sql.EscapeSqlName(tableName), 149 | s.sql.EscapeSqlName(h[0]), 150 | s.sql.EscapeSqlName(h[1]))) 151 | } 152 | 153 | sqls = append(sqls, append(comments, indexes...)...) 154 | 155 | return strings.Join(sqls, "\n") 156 | } 157 | 158 | type attrs struct { 159 | // Basic attributes 160 | Name string 161 | Prefix string 162 | Type string 163 | Value string 164 | Comment string 165 | 166 | // Key and constraint attributes 167 | IsPk bool 168 | IsUnique bool 169 | IsNull bool 170 | IsNotNull bool 171 | IsAutoIncr bool 172 | 173 | // Foreign key 174 | ForeignKey *fkAttrs 175 | 176 | // Index attributes 177 | Index string 178 | IndexType string 179 | IndexColumns string 180 | 181 | // Special attribute 182 | IsEmbedded bool 183 | } 184 | 185 | type fkAttrs struct { 186 | Name string 187 | Table string 188 | Column string 189 | RefTable string 190 | RefColumn string 191 | Constraint string 192 | } 193 | 194 | // parseStruct return columns, columnsHistory, indexes 195 | func (s SqlBuilder) parseStruct(tableName, prefix string, obj interface{}) ([]string, [][2]string, []string) { 196 | maxLen := 0 197 | 198 | rawCols := make([][]string, 0) 199 | 200 | embedColumns := make([]string, 0) 201 | embedColumnsHistory := make([][2]string, 0) 202 | embedIndexes := make([]string, 0) 203 | 204 | columns := make([]string, 0) 205 | columnsHistory := make([][2]string, 0) 206 | comments := make([]string, 0) 207 | indexes := make([]string, 0) 208 | 209 | var pkFields []string 210 | v := reflect.ValueOf(obj) 211 | t := reflect.TypeOf(obj) 212 | for j := 0; j < t.NumField(); j++ { 213 | field := t.Field(j) 214 | stag := field.Tag.Get(s.sqlTag) 215 | if stag == "-" { 216 | continue 217 | } 218 | 219 | at := attrs{ 220 | Name: prefix + utils.ToSnakeCase(field.Name), 221 | } 222 | 223 | xstag := strings.Split(stag, ";") 224 | for _, ot := range xstag { 225 | // normalize tag, convert `primaryKey` => `primary_key` 226 | normTag := utils.ToSnakeCase(ot) 227 | 228 | switch { 229 | case strings.HasPrefix(normTag, prefixColumn): 230 | columnNames := strings.Split(trimPrefix(ot, prefixColumn), prefixPreviousName) 231 | if len(columnNames) == 1 { 232 | at.Name = columnNames[0] 233 | } else { 234 | columnsHistory = append(columnsHistory, [2]string{columnNames[1], columnNames[0]}) 235 | at.Name = columnNames[1] 236 | } 237 | at.Name = prefix + at.Name 238 | 239 | case strings.HasPrefix(normTag, prefixEmbedded): 240 | at.Prefix = trimPrefix(ot, prefixEmbedded) 241 | at.IsEmbedded = true 242 | 243 | case strings.HasPrefix(normTag, prefixForeignKey): 244 | if at.ForeignKey == nil { 245 | at.ForeignKey = &fkAttrs{ 246 | Table: tableName, 247 | RefColumn: trimPrefix(ot, prefixForeignKey), 248 | } 249 | } 250 | 251 | objectNames := strings.Split(field.Type.String(), ".") 252 | obName := objectNames[len(objectNames)-1] 253 | if rt, ok := s.tables[obName]; ok { 254 | at.ForeignKey.RefTable = rt 255 | } else { 256 | at.ForeignKey.RefTable = utils.ToSnakeCase(obName) 257 | } 258 | 259 | at.ForeignKey.Name = fmt.Sprintf("fk_%s_%s", at.ForeignKey.RefTable, tableName) // default 260 | if at.ForeignKey.Column == "" { 261 | at.ForeignKey.Column = at.ForeignKey.RefColumn // default, override by tag `references` 262 | } 263 | 264 | case strings.HasPrefix(normTag, prefixFkReferences): 265 | if at.ForeignKey == nil { 266 | at.ForeignKey = &fkAttrs{ 267 | Table: tableName, 268 | } 269 | } 270 | 271 | at.ForeignKey.Column = trimPrefix(ot, prefixFkReferences) 272 | 273 | case strings.HasPrefix(normTag, prefixFkConstraint): 274 | if at.ForeignKey == nil { 275 | at.ForeignKey = &fkAttrs{ 276 | Table: tableName, 277 | } 278 | } 279 | 280 | at.ForeignKey.Constraint = trimPrefix(ot, prefixFkConstraint) 281 | 282 | case strings.HasPrefix(normTag, prefixType): 283 | at.Type = trimPrefix(ot, prefixType) 284 | if s.generateComment && at.Comment == "" && strings.HasPrefix(strings.ToLower(at.Type), tagEnum) { 285 | enumRaw := ot[len(prefixType)+len(tagEnum):] // this tag is safe, remove prefix: "type:ENUM" 286 | enumStr := compileKeepEnumChar.ReplaceAllString(enumRaw, "") 287 | at.Comment = createCommentFromEnum(strings.Split(enumStr, ",")) 288 | } 289 | 290 | case strings.HasPrefix(normTag, prefixDefault): 291 | at.Value = fmt.Sprintf(s.sql.DefaultOption(), trimPrefix(ot, (prefixDefault))) 292 | 293 | case strings.HasPrefix(normTag, prefixComment): 294 | at.Comment = trimPrefix(ot, prefixComment) 295 | 296 | case normTag == tagIsPrimaryKey: 297 | at.IsPk = true 298 | 299 | case normTag == tagIsIndex: 300 | at.Index = getWhenEmpty(at.Index, createIndexName("", nil, at.Name)) 301 | at.IndexColumns = s.sql.EscapeSqlName(at.Name) 302 | 303 | case normTag == tagIsUniqueIndex: 304 | at.IsUnique = true 305 | at.Index = getWhenEmpty(at.Index, createIndexName("", nil, at.Name)) 306 | if at.IndexColumns == "" { 307 | at.IndexColumns = s.sql.EscapeSqlName(at.Name) 308 | } 309 | 310 | case strings.HasPrefix(normTag, prefixIndex): 311 | idxFields := strings.Split(trimPrefix(ot, prefixIndex), ",") 312 | at.Index = createIndexName(prefix, idxFields, at.Name) 313 | 314 | if len(idxFields) > 1 { 315 | at.IndexColumns = strings.Join(s.sql.EscapeSqlNames(idxFields), ", ") 316 | } else { 317 | at.IndexColumns = s.sql.EscapeSqlName(at.Name) 318 | } 319 | 320 | case strings.HasPrefix(normTag, prefixUniqueIndex): 321 | at.IsUnique = true 322 | at.Index = createIndexName(prefix, []string{trimPrefix(ot, prefixUniqueIndex)}, at.Name) 323 | at.IndexColumns = s.sql.EscapeSqlName(at.Name) 324 | 325 | case strings.HasPrefix(normTag, prefixIndexColumns): 326 | idxFields := strings.Split(trimPrefix(ot, prefixIndexColumns), ",") 327 | if at.IsPk { 328 | pkFields = idxFields 329 | } 330 | 331 | at.Index = createIndexName(prefix, idxFields, at.Name) 332 | at.IndexColumns = strings.Join(s.sql.EscapeSqlNames(idxFields), ", ") 333 | 334 | case strings.HasPrefix(normTag, prefixIndexType): 335 | at.Index = getWhenEmpty(at.Index, createIndexName(prefix, nil, at.Name)) 336 | 337 | if len(at.IndexColumns) == 0 { 338 | at.IndexColumns = strings.Join(s.sql.EscapeSqlNames([]string{at.Name}), ", ") 339 | } 340 | at.IndexType = trimPrefix(ot, prefixIndexType) 341 | 342 | case normTag == tagIsNull: 343 | at.IsNotNull = true 344 | 345 | case normTag == tagIsNotNull: 346 | at.IsNotNull = true 347 | 348 | case normTag == tagIsAutoIncrement: 349 | at.IsAutoIncr = true 350 | 351 | case normTag == tagIsSquash, normTag == tagIsEmbedded: 352 | at.IsEmbedded = true 353 | } 354 | } 355 | 356 | if at.ForeignKey != nil { 357 | indexes = append(indexes, fmt.Sprintf(s.sql.CreateForeignKeyStm(), 358 | s.sql.EscapeSqlName(at.ForeignKey.Table), s.sql.EscapeSqlName(at.ForeignKey.Name), s.sql.EscapeSqlName(at.ForeignKey.Column), 359 | s.sql.EscapeSqlName(at.ForeignKey.RefTable), s.sql.EscapeSqlName(at.ForeignKey.RefColumn))) 360 | continue 361 | } 362 | 363 | if at.IsPk { 364 | // create primary key multiple field as constraint 365 | if len(pkFields) > 1 { 366 | primaryKey := strings.Join(s.sql.EscapeSqlNames(pkFields), ", ") 367 | indexes = append(indexes, fmt.Sprintf(s.sql.CreatePrimaryKeyStm(), tableName, primaryKey)) 368 | } 369 | } else if at.Index != "" { 370 | var strIndex string 371 | if at.IsUnique { 372 | strIndex = fmt.Sprintf(s.sql.CreateUniqueIndexStm(at.IndexType), 373 | s.sql.EscapeSqlName(at.Index), 374 | s.sql.EscapeSqlName(tableName), at.IndexColumns) 375 | } else { 376 | strIndex = fmt.Sprintf(s.sql.CreateIndexStm(at.IndexType), 377 | s.sql.EscapeSqlName(at.Index), 378 | s.sql.EscapeSqlName(tableName), at.IndexColumns) 379 | } 380 | 381 | indexes = append(indexes, strIndex) 382 | } 383 | 384 | if len(at.Name) > maxLen { 385 | maxLen = len(at.Name) 386 | } 387 | 388 | col := []string{at.Name} 389 | if at.Type != "" { 390 | col = append(col, at.Type) 391 | } else { 392 | if _, ok := reflectValueFields[t.Field(j).Name]; ok { 393 | continue 394 | } 395 | 396 | strType, isEmbedded := s.sqlType(v.Field(j).Interface(), "") 397 | if isEmbedded && (at.IsEmbedded || len(at.Prefix) > 0) { 398 | _columns, _columnsHistory, _indexes := s.parseStruct(tableName, prefix+at.Prefix, v.Field(j).Interface()) 399 | embedColumns = append(embedColumns, _columns...) 400 | embedColumnsHistory = append(embedColumnsHistory, _columnsHistory...) 401 | embedIndexes = append(embedIndexes, _indexes...) 402 | continue 403 | } else { 404 | if isEmbedded { // default type for struct is "TEXT" 405 | strType = s.sql.TextType() 406 | } 407 | col = append(col, strType) 408 | } 409 | } 410 | 411 | if at.IsNotNull { 412 | col = append(col, s.sql.NotNullValue()) 413 | } else if at.IsNull { 414 | col = append(col, s.sql.NullValue()) 415 | } 416 | 417 | if at.Value != "" { 418 | col = append(col, at.Value) 419 | } 420 | 421 | if at.IsAutoIncr { 422 | col = append(col, s.sql.AutoIncrementOption()) 423 | } 424 | 425 | // primary key attribute appended after field 426 | if at.IsPk && len(pkFields) <= 1 { 427 | col = append(col, s.sql.PrimaryOption()) 428 | } 429 | 430 | if s.generateComment && at.Comment == "" { 431 | at.Comment = createCommentFromFieldName(at.Name) 432 | } 433 | 434 | if at.Comment != "" { 435 | switch s.dialect { 436 | case sql_templates.PostgresDialect: 437 | comments = append(comments, 438 | fmt.Sprintf(s.sql.ColumnComment(), s.sql.EscapeSqlName(tableName), s.sql.EscapeSqlName(at.Name), at.Comment)) 439 | 440 | default: 441 | col = append(col, fmt.Sprintf(s.sql.ColumnComment(), at.Comment)) 442 | } 443 | } 444 | 445 | rawCols = append(rawCols, col) 446 | } 447 | 448 | for _, f := range rawCols { 449 | columns = append(columns, 450 | fmt.Sprintf(" %s%s%s", s.sql.EscapeSqlName(f[0]), strings.Repeat(" ", maxLen-len(f[0])+1), strings.Join(f[1:], " "))) 451 | } 452 | 453 | return append(columns, embedColumns...), 454 | append(columnsHistory, embedColumnsHistory...), 455 | append(comments, append(indexes, embedIndexes...)...) 456 | } 457 | 458 | func getWhenEmpty(s, s2 string) string { 459 | if s == "" { 460 | return s2 461 | } 462 | return s 463 | } 464 | 465 | // because tag is norm 466 | func trimPrefix(ot, prefix string) string { 467 | if strings.HasPrefix(ot, prefix) { 468 | return ot[len(prefix):] 469 | } 470 | 471 | // all prefix tag is end with `:` 472 | return ot[strings.Index(ot, ":")+1:] 473 | } 474 | 475 | // RemoveTable ... 476 | func (s SqlBuilder) RemoveTable(tb interface{}) string { 477 | _, table := s.GetTableName(tb) 478 | return fmt.Sprintf(s.sql.DropTableStm(), s.sql.EscapeSqlName(table)) 479 | } 480 | 481 | // createIndexName format idx_field_names 482 | func createIndexName(prefix string, indexColumns []string, column string) string { 483 | cols := make([]string, len(indexColumns)) 484 | for i := range indexColumns { 485 | cols[i] = prefix + indexColumns[i] 486 | } 487 | 488 | if len(indexColumns) == 1 && indexColumns[0] != column { 489 | return indexColumns[0] 490 | } 491 | 492 | if len(indexColumns) == 0 { 493 | indexColumns = []string{column} 494 | } 495 | 496 | return fmt.Sprintf("idx_%s", strings.Join(indexColumns, "_")) 497 | } 498 | 499 | // createCommentFromEnum by enum values or field name 500 | func createCommentFromEnum(enums []string) string { 501 | if len(enums) > 0 { 502 | return fmt.Sprintf("enum values: %s", strings.Join(enums, ", ")) 503 | } 504 | 505 | return "" 506 | } 507 | 508 | // createCommentFromFieldName by enum values or field name 509 | func createCommentFromFieldName(column string) string { 510 | if _, ok := ignoredFieldComment[column]; ok { 511 | return "" 512 | } 513 | return strings.Replace(column, "_", " ", -1) 514 | } 515 | 516 | // prefix return sqlType, isEmbedded 517 | func (s SqlBuilder) sqlType(v interface{}, suffix string) (string, bool) { 518 | if t, ok := s.sqlNullType(v, s.sql.NullValue()); ok { 519 | return t, false 520 | } 521 | 522 | switch reflect.ValueOf(v).Kind() { 523 | case reflect.Ptr: 524 | vv := reflect.Indirect(reflect.ValueOf(v)) 525 | if reflect.ValueOf(v).Pointer() == 0 || vv.IsZero() { 526 | return s.sql.PointerType(), false 527 | } 528 | 529 | return s.sqlType(vv.Interface(), s.sql.NullValue()) 530 | 531 | case reflect.Struct: 532 | if _, ok := v.(time.Time); ok { 533 | break 534 | } 535 | 536 | return "", true 537 | } 538 | 539 | return s.sqlPrimitiveType(v, suffix), false 540 | } 541 | 542 | // sqlNullType return sqlNullType, isPrimitiveType 543 | func (s SqlBuilder) sqlNullType(v interface{}, suffix string) (string, bool) { 544 | if suffix != "" { 545 | suffix = " " + suffix 546 | } 547 | 548 | switch v.(type) { 549 | case sql.NullBool: 550 | return s.sql.BooleanType() + suffix, true 551 | 552 | case sql.NullInt32: 553 | return s.sql.IntType() + suffix, true 554 | 555 | case sql.NullInt64: 556 | return s.sql.BigIntType() + suffix, true 557 | 558 | case sql.NullFloat64: 559 | return s.sql.DoubleType() + suffix, true 560 | 561 | case sql.NullString: 562 | return s.sql.TextType() + suffix, true 563 | 564 | case sql.NullTime: 565 | return s.sql.DatetimeType() + suffix, true 566 | 567 | default: 568 | return "", false 569 | } 570 | } 571 | 572 | // sqlPrimitiveType ... 573 | func (s SqlBuilder) sqlPrimitiveType(v interface{}, suffix string) string { 574 | if suffix != "" { 575 | suffix = " " + suffix 576 | } 577 | 578 | switch v.(type) { 579 | case bool: 580 | return s.sql.BooleanType() + suffix 581 | 582 | case int8, uint8: 583 | return s.sql.TinyIntType() + suffix 584 | 585 | case int16, uint16: 586 | return s.sql.SmallIntType() + suffix 587 | 588 | case int, int32, uint32: 589 | return s.sql.IntType() + suffix 590 | 591 | case int64, uint64: 592 | return s.sql.BigIntType() + suffix 593 | 594 | case float32: 595 | return s.sql.FloatType() + suffix 596 | 597 | case float64: 598 | return s.sql.DoubleType() + suffix 599 | 600 | case string: 601 | return s.sql.TextType() + suffix 602 | 603 | case time.Time: 604 | return s.sql.DatetimeType() + suffix 605 | 606 | default: 607 | return s.sql.UnspecificType() 608 | } 609 | } 610 | 611 | // GetTableName read from TableNameFn or parse table name from model as snake_case 612 | // return object name and table name 613 | func (s SqlBuilder) GetTableName(t interface{}) (string, string) { 614 | name := "" 615 | if t := reflect.TypeOf(t); t.Kind() == reflect.Ptr { 616 | name = t.Elem().Name() 617 | } else { 618 | name = t.Name() 619 | } 620 | 621 | st := reflect.TypeOf(t) 622 | if _, ok := st.MethodByName(funcTableName); ok { 623 | v := reflect.ValueOf(t).MethodByName(funcTableName).Call(nil) 624 | if len(v) > 0 { 625 | return name, v[0].String() 626 | } 627 | } 628 | 629 | if s.pluralTableName { 630 | name = name + "s" 631 | } 632 | return name, utils.ToSnakeCase(name) 633 | } 634 | -------------------------------------------------------------------------------- /sql-builder/options.go: -------------------------------------------------------------------------------- 1 | package sql_builder 2 | 3 | import ( 4 | sql_templates "github.com/sunary/sqlize/sql-templates" 5 | ) 6 | 7 | type sqlBuilderOptions struct { 8 | sqlTag string 9 | dialect sql_templates.SqlDialect 10 | lowercase bool 11 | generateComment bool 12 | pluralTableName bool 13 | } 14 | 15 | type funcSqlBuilderOption struct { 16 | f func(*sqlBuilderOptions) 17 | } 18 | 19 | func (fso *funcSqlBuilderOption) apply(do *sqlBuilderOptions) { 20 | fso.f(do) 21 | } 22 | 23 | func newFuncSqlBuilderOption(f func(*sqlBuilderOptions)) *funcSqlBuilderOption { 24 | return &funcSqlBuilderOption{ 25 | f: f, 26 | } 27 | } 28 | 29 | // SqlBuilderOption ... 30 | type SqlBuilderOption interface { 31 | apply(*sqlBuilderOptions) 32 | } 33 | 34 | // WithSqlTag default tag is `sql` 35 | func WithSqlTag(sqlTag string) SqlBuilderOption { 36 | return newFuncSqlBuilderOption(func(o *sqlBuilderOptions) { 37 | o.sqlTag = sqlTag 38 | }) 39 | } 40 | 41 | // WithMysql default 42 | func WithMysql() SqlBuilderOption { 43 | return newFuncSqlBuilderOption(func(o *sqlBuilderOptions) { 44 | o.dialect = sql_templates.MysqlDialect 45 | }) 46 | } 47 | 48 | // WithPostgresql ... 49 | func WithPostgresql() SqlBuilderOption { 50 | return newFuncSqlBuilderOption(func(o *sqlBuilderOptions) { 51 | o.dialect = sql_templates.PostgresDialect 52 | }) 53 | } 54 | 55 | // WithSqlserver ... 56 | func WithSqlserver() SqlBuilderOption { 57 | return newFuncSqlBuilderOption(func(o *sqlBuilderOptions) { 58 | o.dialect = sql_templates.SqlserverDialect 59 | }) 60 | } 61 | 62 | // WithSqlite ... 63 | func WithSqlite() SqlBuilderOption { 64 | return newFuncSqlBuilderOption(func(o *sqlBuilderOptions) { 65 | o.dialect = sql_templates.SqliteDialect 66 | }) 67 | } 68 | 69 | // WithDialect ... 70 | func WithDialect(dialect sql_templates.SqlDialect) SqlBuilderOption { 71 | return newFuncSqlBuilderOption(func(o *sqlBuilderOptions) { 72 | o.dialect = dialect 73 | }) 74 | } 75 | 76 | // WithSqlUppercase default 77 | func WithSqlUppercase() SqlBuilderOption { 78 | return newFuncSqlBuilderOption(func(o *sqlBuilderOptions) { 79 | o.lowercase = false 80 | }) 81 | } 82 | 83 | // WithSqlLowercase ... 84 | func WithSqlLowercase() SqlBuilderOption { 85 | return newFuncSqlBuilderOption(func(o *sqlBuilderOptions) { 86 | o.lowercase = true 87 | }) 88 | } 89 | 90 | // WithPluralTableName Table name in plural convention - ending with `s` 91 | func WithPluralTableName() SqlBuilderOption { 92 | return newFuncSqlBuilderOption(func(o *sqlBuilderOptions) { 93 | o.pluralTableName = true 94 | }) 95 | } 96 | 97 | // WithCommentGenerate default is off 98 | func WithCommentGenerate() SqlBuilderOption { 99 | return newFuncSqlBuilderOption(func(o *sqlBuilderOptions) { 100 | o.generateComment = true 101 | }) 102 | } 103 | -------------------------------------------------------------------------------- /sql-parser/mysql.go: -------------------------------------------------------------------------------- 1 | package sql_parser 2 | 3 | import ( 4 | "github.com/pingcap/parser" 5 | "github.com/pingcap/parser/ast" 6 | "github.com/pingcap/parser/model" 7 | "github.com/sunary/sqlize/element" 8 | ) 9 | 10 | // ParserMysql ... 11 | func (p *Parser) ParserMysql(sql string) error { 12 | ps := parser.New() 13 | stmtNodes, _, err := ps.Parse(sql, "", "") 14 | if err != nil { 15 | return err 16 | } 17 | 18 | for _, n := range stmtNodes { 19 | switch n.(type) { 20 | case ast.DDLNode: 21 | n.Accept(p) 22 | } 23 | } 24 | 25 | return nil 26 | } 27 | 28 | /* 29 | Walk with statements: 30 | 31 | DropTableStmt 32 | AlterTableStmt 33 | DropIndexStmt 34 | CreateTableStmt 35 | CreateIndexStmt 36 | */ 37 | func (p *Parser) Enter(in ast.Node) (ast.Node, bool) { 38 | // get Table name 39 | if tb, ok := in.(*ast.TableName); ok { 40 | p.Migration.Using(tb.Name.O) 41 | } 42 | 43 | // drop Table 44 | if tb, ok := in.(*ast.DropTableStmt); ok { 45 | for i := range tb.Tables { 46 | p.Migration.RemoveTable(tb.Tables[i].Name.O) 47 | } 48 | } 49 | 50 | // alter Table 51 | if alter, ok := in.(*ast.AlterTableStmt); ok { 52 | for i := range alter.Specs { 53 | switch alter.Specs[i].Tp { 54 | case ast.AlterTableAddColumns: 55 | if alter.Specs[i].Position != nil { 56 | p.Migration.SetColumnPosition("", alter.Specs[i].Position) 57 | } 58 | 59 | case ast.AlterTableAddConstraint: 60 | switch alter.Specs[i].Constraint.Tp { 61 | case ast.ConstraintPrimaryKey: 62 | cols := make([]string, len(alter.Specs[i].Constraint.Keys)) 63 | for j, key := range alter.Specs[i].Constraint.Keys { 64 | cols[j] = key.Column.Name.O 65 | } 66 | 67 | if alter.Specs[i].Constraint.Keys != nil { 68 | p.Migration.AddIndex(alter.Table.Text(), element.Index{ 69 | Node: element.Node{Name: "primary_key", Action: element.MigrateAddAction}, 70 | Typ: ast.IndexKeyTypeNone, 71 | CnsTyp: ast.ConstraintPrimaryKey, 72 | Columns: cols, 73 | }) 74 | } else { 75 | p.Migration.AddColumn(alter.Table.Text(), element.Column{ 76 | Node: element.Node{Name: cols[0], Action: element.MigrateAddAction}, 77 | CurrentAttr: element.SqlAttr{ 78 | MysqlType: nil, 79 | Options: []*ast.ColumnOption{ 80 | { 81 | Tp: ast.ColumnOptionPrimaryKey, 82 | }, 83 | }, 84 | }, 85 | }) 86 | } 87 | 88 | case ast.ConstraintForeignKey: 89 | cols := make([]string, len(alter.Specs[i].Constraint.Keys)) 90 | for j, key := range alter.Specs[i].Constraint.Keys { 91 | cols[j] = key.Column.Name.O 92 | } 93 | 94 | p.Migration.AddForeignKey(alter.Table.Text(), element.ForeignKey{ 95 | Node: element.Node{Name: alter.Specs[i].Constraint.Name, Action: element.MigrateAddAction}, 96 | Table: alter.Table.Text(), 97 | Column: cols[0], 98 | RefTable: alter.Specs[i].Constraint.Refer.Table.Name.String(), 99 | RefColumn: alter.Specs[i].Constraint.Refer.IndexPartSpecifications[0].Column.Name.String(), 100 | Constraint: "", 101 | }) 102 | } 103 | 104 | case ast.AlterTableDropColumn: 105 | p.Migration.RemoveColumn(alter.Table.Name.O, alter.Specs[i].OldColumnName.Name.O) 106 | 107 | case ast.AlterTableDropPrimaryKey: 108 | 109 | case ast.AlterTableDropIndex: 110 | 111 | case ast.AlterTableDropForeignKey: 112 | p.Migration.RemoveForeignKey(alter.Table.Name.O, alter.Specs[i].Constraint.Name) 113 | 114 | case ast.AlterTableModifyColumn: 115 | if len(alter.Specs[i].NewColumns) > 0 { 116 | for j := range alter.Specs[i].NewColumns { 117 | col := element.Column{ 118 | Node: element.Node{Name: alter.Specs[i].NewColumns[j].Name.Name.O, Action: element.MigrateModifyAction}, 119 | CurrentAttr: element.SqlAttr{ 120 | MysqlType: alter.Specs[i].NewColumns[j].Tp, 121 | Comment: alter.Specs[i].Comment, 122 | }, 123 | } 124 | p.Migration.AddColumn(alter.Table.Name.O, col) 125 | } 126 | } 127 | 128 | case ast.AlterTableRenameColumn: 129 | p.Migration.RenameColumn(alter.Table.Name.O, alter.Specs[i].OldColumnName.Name.O, alter.Specs[i].NewColumnName.Name.O) 130 | 131 | case ast.AlterTableRenameTable: 132 | p.Migration.RenameTable(alter.Table.Name.O, alter.Specs[i].NewTable.Name.O) 133 | 134 | case ast.AlterTableRenameIndex: 135 | p.Migration.RenameIndex(alter.Table.Name.O, alter.Specs[i].FromKey.O, alter.Specs[i].ToKey.O) 136 | } 137 | } 138 | } 139 | 140 | // drop Index 141 | if idx, ok := in.(*ast.DropIndexStmt); ok { 142 | p.Migration.RemoveIndex(idx.Table.Name.O, idx.IndexName) 143 | } 144 | 145 | // create Table 146 | if tab, ok := in.(*ast.CreateTableStmt); ok { 147 | tbName := tab.Table.Name.O 148 | tb := element.NewTableWithAction(tbName, element.MigrateAddAction) 149 | 150 | p.Migration.Using(tbName) 151 | for i := range tab.Constraints { 152 | cols := make([]string, len(tab.Constraints[i].Keys)) 153 | for j, key := range tab.Constraints[i].Keys { 154 | cols[j] = key.Column.Name.O 155 | } 156 | 157 | switch tab.Constraints[i].Tp { 158 | case ast.ConstraintPrimaryKey: 159 | if tab.Constraints[i].Keys != nil { 160 | tb.AddIndex(element.Index{ 161 | Node: element.Node{Name: "primary_key", Action: element.MigrateAddAction}, 162 | Typ: ast.IndexKeyTypeNone, 163 | CnsTyp: ast.ConstraintPrimaryKey, 164 | Columns: cols, 165 | }) 166 | } else { 167 | tb.AddColumn(element.Column{ 168 | Node: element.Node{Name: cols[0], Action: element.MigrateAddAction}, 169 | CurrentAttr: element.SqlAttr{ 170 | MysqlType: nil, 171 | Options: []*ast.ColumnOption{ 172 | { 173 | Tp: ast.ColumnOptionPrimaryKey, 174 | }, 175 | }, 176 | }, 177 | }) 178 | } 179 | 180 | case ast.ConstraintKey, ast.ConstraintIndex: 181 | indexType := model.IndexTypeBtree 182 | if tab.Constraints[i].Option != nil { 183 | indexType = tab.Constraints[i].Option.Tp 184 | } 185 | 186 | tb.AddIndex(element.Index{ 187 | Node: element.Node{ 188 | Name: tab.Constraints[i].Name, 189 | Action: element.MigrateAddAction, 190 | }, 191 | Typ: ast.IndexKeyTypeNone, 192 | IndexType: indexType, 193 | Columns: cols, 194 | }) 195 | 196 | case ast.ConstraintUniqKey, ast.ConstraintUniqIndex: 197 | indexType := model.IndexTypeBtree 198 | if tab.Constraints[i].Option != nil { 199 | indexType = tab.Constraints[i].Option.Tp 200 | } 201 | 202 | tb.AddIndex(element.Index{ 203 | Node: element.Node{ 204 | Name: tab.Constraints[i].Name, 205 | Action: element.MigrateAddAction, 206 | }, 207 | Typ: ast.IndexKeyTypeUnique, 208 | IndexType: indexType, 209 | Columns: cols, 210 | }) 211 | } 212 | } 213 | 214 | p.Migration.AddTable(*tb) 215 | } 216 | 217 | // define Column 218 | if def, ok := in.(*ast.ColumnDef); ok { 219 | comment := "" 220 | for i := range def.Options { 221 | if def.Options[i].Tp == ast.ColumnOptionComment { 222 | comment = def.Options[i].StrValue 223 | } 224 | } 225 | 226 | column := element.Column{ 227 | Node: element.Node{Name: def.Name.Name.O, Action: element.MigrateAddAction}, 228 | CurrentAttr: element.SqlAttr{ 229 | MysqlType: def.Tp, 230 | Options: def.Options, 231 | Comment: comment, 232 | }, 233 | } 234 | p.Migration.AddColumn("", column) 235 | } 236 | 237 | // create Index 238 | if idx, ok := in.(*ast.CreateIndexStmt); ok { 239 | cols := make([]string, len(idx.IndexPartSpecifications)) 240 | for i := range idx.IndexPartSpecifications { 241 | cols[i] = idx.IndexPartSpecifications[i].Column.Name.O 242 | } 243 | 244 | p.Migration.AddIndex(idx.Table.Name.O, element.Index{ 245 | Node: element.Node{ 246 | Name: idx.IndexName, 247 | Action: element.MigrateAddAction, 248 | }, 249 | Typ: idx.KeyType, 250 | Columns: cols, 251 | }) 252 | } 253 | 254 | return in, false 255 | } 256 | 257 | // Leave ... 258 | func (p *Parser) Leave(in ast.Node) (ast.Node, bool) { 259 | return in, true 260 | } 261 | -------------------------------------------------------------------------------- /sql-parser/parser.go: -------------------------------------------------------------------------------- 1 | package sql_parser 2 | 3 | import ( 4 | "github.com/sunary/sqlize/element" 5 | sql_templates "github.com/sunary/sqlize/sql-templates" 6 | ) 7 | 8 | // Parser ... 9 | type Parser struct { 10 | dialect sql_templates.SqlDialect 11 | Migration element.Migration 12 | ignoreOrder bool 13 | } 14 | 15 | // NewParser ... 16 | func NewParser(dialect sql_templates.SqlDialect, lowercase, ignoreOrder bool) *Parser { 17 | return &Parser{ 18 | dialect: dialect, 19 | Migration: element.NewMigration(dialect, lowercase, ignoreOrder), 20 | } 21 | } 22 | 23 | // Parser ... 24 | func (p *Parser) Parser(sql string) error { 25 | switch p.dialect { 26 | case sql_templates.PostgresDialect: 27 | return p.ParserPostgresql(sql) 28 | 29 | case sql_templates.SqliteDialect: 30 | return p.ParserSqlite(sql) 31 | 32 | default: 33 | // TODO: mysql parser is default for remaining dialects 34 | return p.ParserMysql(sql) 35 | } 36 | } 37 | 38 | // HashValue ... 39 | func (p *Parser) HashValue() int64 { 40 | return p.Migration.HashValue() 41 | } 42 | 43 | // Diff differ between 2 migrations 44 | func (p *Parser) Diff(old Parser) { 45 | p.Migration.Diff(old.Migration) 46 | } 47 | 48 | // MigrationUp migration up 49 | func (p Parser) MigrationUp() string { 50 | return p.Migration.MigrationUp() 51 | } 52 | 53 | // MigrationDown migration down 54 | func (p Parser) MigrationDown() string { 55 | return p.Migration.MigrationDown() 56 | } 57 | -------------------------------------------------------------------------------- /sql-parser/postgresql.go: -------------------------------------------------------------------------------- 1 | package sql_parser 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/auxten/postgresql-parser/pkg/sql/parser" 7 | "github.com/auxten/postgresql-parser/pkg/sql/sem/tree" 8 | "github.com/auxten/postgresql-parser/pkg/walk" 9 | "github.com/pingcap/parser/ast" 10 | "github.com/sunary/sqlize/element" 11 | ) 12 | 13 | // ParserPostgresql ... 14 | func (p *Parser) ParserPostgresql(sql string) error { 15 | w := &walk.AstWalker{ 16 | Fn: p.walker, 17 | } 18 | 19 | stmts, err := parser.Parse(sql) 20 | if err != nil { 21 | return err 22 | } 23 | 24 | _, err = w.Walk(stmts, nil) 25 | return err 26 | } 27 | 28 | /* 29 | Walk with statements: 30 | 31 | CreateTable 32 | ColumnTableDef 33 | CreateIndex 34 | DropIndex 35 | AlterTable 36 | RenameTable 37 | */ 38 | func (p *Parser) walker(ctx interface{}, node interface{}) (stop bool) { 39 | switch n := node.(type) { 40 | case *tree.CreateTable: 41 | tbName := n.Table.Table() 42 | 43 | tb := element.NewTableWithAction(tbName, element.MigrateAddAction) 44 | p.Migration.AddTable(*tb) 45 | p.Migration.Using(tbName) 46 | 47 | case *tree.ColumnTableDef: 48 | col, indexes := postgresColumn(n) 49 | p.Migration.AddColumn("", col) 50 | if len(indexes) > 0 { 51 | for i := range indexes { 52 | p.Migration.AddIndex("", indexes[i]) 53 | } 54 | } 55 | 56 | case *tree.CommentOnColumn: 57 | p.Migration.AddComment(n.TableName.String(), n.Column(), *n.Comment) 58 | 59 | case *tree.CreateIndex: 60 | p.Migration.AddIndex("", postgresIndex(n)) 61 | 62 | case *tree.DropIndex: 63 | for i := range n.IndexList { 64 | p.Migration.RemoveIndex("", n.IndexList[i].Index.String()) 65 | } 66 | 67 | case *tree.AlterTable: 68 | switch nc := n.Cmds[0].(type) { 69 | case *tree.AlterTableRenameTable: 70 | p.Migration.RenameTable(n.Table.String(), nc.NewName.TableName.String()) 71 | 72 | case *tree.AlterTableRenameColumn: 73 | p.Migration.RenameColumn(n.Table.String(), nc.Column.String(), nc.NewName.String()) 74 | 75 | case *tree.AlterTableRenameConstraint: 76 | p.Migration.RenameIndex(n.Table.String(), nc.Constraint.String(), nc.NewName.String()) 77 | 78 | case *tree.AlterTableAddColumn: 79 | col, indexes := postgresColumn(nc.ColumnDef) 80 | p.Migration.AddColumn(n.Table.String(), col) 81 | if len(indexes) > 0 { 82 | for i := range indexes { 83 | p.Migration.AddIndex(n.Table.String(), indexes[i]) 84 | } 85 | } 86 | 87 | case *tree.AlterTableDropColumn: 88 | p.Migration.RemoveColumn(n.Table.String(), nc.Column.String()) 89 | 90 | case *tree.AlterTableDropNotNull: 91 | p.Migration.RemoveColumn(n.Table.String(), nc.Column.String()) 92 | 93 | case *tree.AlterTableAlterColumnType: 94 | col := element.Column{ 95 | Node: element.Node{Name: nc.Column.String(), Action: element.MigrateModifyAction}, 96 | CurrentAttr: element.SqlAttr{ 97 | PgType: nc.ToType, 98 | }, 99 | } 100 | p.Migration.AddColumn(n.Table.String(), col) 101 | 102 | case *tree.AlterTableSetDefault: 103 | if nc.Default != nil { 104 | col := element.Column{ 105 | Node: element.Node{Name: nc.Column.String(), Action: element.MigrateModifyAction}, 106 | CurrentAttr: element.SqlAttr{ 107 | Options: []*ast.ColumnOption{{ 108 | Expr: nil, 109 | Tp: ast.ColumnOptionDefaultValue, 110 | StrValue: nc.Default.String(), 111 | }}, 112 | }, 113 | } 114 | p.Migration.AddColumn(n.Table.String(), col) 115 | } 116 | 117 | case *tree.AlterTableAddConstraint: 118 | switch nc2 := nc.ConstraintDef.(type) { 119 | case *tree.UniqueConstraintTableDef: 120 | p.Migration.AddIndex(n.Table.String(), postgresUnique(nc2)) 121 | 122 | case *tree.ForeignKeyConstraintTableDef: 123 | p.Migration.AddForeignKey(n.Table.String(), postgresForeignKey(nc2)) 124 | } 125 | 126 | case *tree.AlterTableDropConstraint: 127 | consName := nc.Constraint.String() 128 | if strings.HasPrefix(strings.ToLower(consName), "fk") { // detect ForeignKey Constraint 129 | p.Migration.RemoveForeignKey(n.Table.String(), consName) 130 | } else { 131 | p.Migration.RemoveIndex(n.Table.String(), consName) 132 | } 133 | } 134 | 135 | case *tree.RenameTable: 136 | p.Migration.RenameTable(n.Name.String(), n.NewName.String()) 137 | } 138 | 139 | return false 140 | } 141 | 142 | func postgresColumn(n *tree.ColumnTableDef) (element.Column, []element.Index) { 143 | opts := make([]*ast.ColumnOption, 0, 1) 144 | if n.DefaultExpr.Expr != nil { 145 | opts = append(opts, &ast.ColumnOption{ 146 | Tp: ast.ColumnOptionDefaultValue, 147 | StrValue: n.DefaultExpr.Expr.String(), 148 | }) 149 | } 150 | 151 | indexes := []element.Index{} 152 | switch { 153 | case n.PrimaryKey.IsPrimaryKey: 154 | indexes = append(indexes, element.Index{ 155 | Node: element.Node{Name: "primary_key", Action: element.MigrateAddAction}, 156 | Typ: ast.IndexKeyTypeNone, 157 | CnsTyp: ast.ConstraintPrimaryKey, 158 | Columns: []string{n.Name.String()}, 159 | }) 160 | case n.Unique: 161 | indexes = append(indexes, element.Index{ 162 | Node: element.Node{ 163 | Name: n.UniqueConstraintName.String(), 164 | Action: element.MigrateAddAction, 165 | }, 166 | Typ: ast.IndexKeyTypeUnique, 167 | Columns: []string{n.Name.String()}, 168 | }) 169 | case n.References.Table != nil: 170 | } 171 | 172 | return element.Column{ 173 | Node: element.Node{Name: n.Name.String(), Action: element.MigrateAddAction}, 174 | CurrentAttr: element.SqlAttr{ 175 | PgType: n.Type, 176 | Options: opts, 177 | }, 178 | }, indexes 179 | } 180 | 181 | func postgresUnique(n *tree.UniqueConstraintTableDef) element.Index { 182 | cols := make([]string, len(n.Columns)) 183 | for i := range n.Columns { 184 | cols[i] = n.Columns[i].Column.String() 185 | } 186 | 187 | if n.PrimaryKey { 188 | return element.Index{ 189 | Node: element.Node{Name: "primary_key", Action: element.MigrateAddAction}, 190 | Typ: ast.IndexKeyTypeNone, 191 | CnsTyp: ast.ConstraintPrimaryKey, 192 | Columns: cols, 193 | } 194 | } 195 | 196 | return element.Index{ 197 | Node: element.Node{ 198 | Name: n.Name.String(), 199 | Action: element.MigrateAddAction, 200 | }, 201 | Typ: ast.IndexKeyTypeUnique, 202 | Columns: cols, 203 | } 204 | } 205 | 206 | func postgresIndex(n *tree.CreateIndex) element.Index { 207 | cols := make([]string, len(n.Columns)) 208 | for i := range n.Columns { 209 | cols[i] = n.Columns[i].Column.String() 210 | } 211 | 212 | typ := ast.IndexKeyTypeNone 213 | if n.Unique { 214 | typ = ast.IndexKeyTypeUnique 215 | } 216 | 217 | return element.Index{ 218 | Node: element.Node{ 219 | Name: n.Name.String(), 220 | Action: element.MigrateAddAction, 221 | }, 222 | Typ: typ, 223 | Columns: cols, 224 | } 225 | } 226 | 227 | func postgresForeignKey(n *tree.ForeignKeyConstraintTableDef) element.ForeignKey { 228 | return element.ForeignKey{ 229 | Node: element.Node{Name: n.Name.String(), Action: element.MigrateAddAction}, 230 | Table: "", 231 | Column: n.FromCols[0].String(), 232 | RefTable: n.Table.Table(), 233 | RefColumn: n.ToCols[0].String(), 234 | Constraint: "", 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /sql-parser/sqlite.go: -------------------------------------------------------------------------------- 1 | package sql_parser 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/pingcap/parser/ast" 7 | sqlite "github.com/rqlite/sql" 8 | "github.com/sunary/sqlize/element" 9 | ) 10 | 11 | // ParserSqlite ... 12 | func (p *Parser) ParserSqlite(sql string) error { 13 | ps := sqlite.NewParser(strings.NewReader(sql)) 14 | 15 | node, err := ps.ParseStatement() 16 | if err != nil { 17 | return err 18 | } 19 | 20 | _, err = sqlite.Walk(p, node) 21 | return err 22 | } 23 | 24 | /* 25 | Walk with statements: 26 | 27 | CreateTableStatement 28 | CreateIndexStatement 29 | DropTableStatement 30 | DropIndexStatement 31 | AlterTableStatement 32 | 33 | sqlite does not support drop column 34 | */ 35 | func (p *Parser) Visit(node sqlite.Node) (w sqlite.Visitor, n sqlite.Node, err error) { 36 | switch n := node.(type) { 37 | case *sqlite.CreateTableStatement: 38 | tbName := n.Name.String() 39 | tb := element.NewTableWithAction(tbName, element.MigrateAddAction) 40 | p.Migration.AddTable(*tb) 41 | p.Migration.Using(tbName) 42 | 43 | // TODO: rqlite/sql doesn't support parse constraints 44 | for i := range n.Columns { 45 | col := element.Column{ 46 | Node: element.Node{ 47 | Name: n.Columns[i].Name.String(), 48 | Action: element.MigrateAddAction, 49 | }, 50 | CurrentAttr: element.SqlAttr{ 51 | LiteType: n.Columns[i].Type, 52 | Options: p.parseSqliteConstrains(tbName, n.Columns[i]), 53 | }, 54 | } 55 | 56 | p.Migration.AddColumn(tbName, col) 57 | } 58 | 59 | for _, cons := range n.Constraints { 60 | switch cons := cons.(type) { 61 | case *sqlite.UniqueConstraint: 62 | indexCol := make([]string, len(cons.Columns)) 63 | for i := range cons.Columns { 64 | indexCol[i] = cons.Columns[i].Collation.Name 65 | } 66 | 67 | p.Migration.AddIndex(tbName, element.Index{ 68 | Node: element.Node{ 69 | Name: cons.Name.Name, 70 | Action: element.MigrateAddAction, 71 | }, 72 | Columns: indexCol, 73 | Typ: ast.IndexKeyTypeUnique, 74 | }) 75 | 76 | case *sqlite.ForeignKeyConstraint: 77 | // p.Migration.AddForeignKey(tbName, element.ForeignKey{}) 78 | } 79 | } 80 | 81 | case *sqlite.CreateIndexStatement: 82 | tbName := n.Table.String() 83 | 84 | indexCol := make([]string, len(n.Columns)) 85 | for i := range n.Columns { 86 | indexCol[i] = n.Columns[i].String() 87 | } 88 | 89 | typ := ast.IndexKeyTypeNone 90 | if n.Unique.IsValid() { 91 | typ = ast.IndexKeyTypeUnique 92 | } 93 | 94 | p.Migration.AddIndex(tbName, element.Index{ 95 | Node: element.Node{ 96 | Name: n.Name.Name, 97 | Action: element.MigrateAddAction, 98 | }, 99 | Columns: indexCol, 100 | Typ: typ, 101 | }) 102 | 103 | case *sqlite.DropTableStatement: 104 | tbName := n.Table.String() 105 | p.Migration.RemoveTable(tbName) 106 | 107 | case *sqlite.DropIndexStatement: 108 | 109 | case *sqlite.AlterTableStatement: 110 | tbName := n.Table.String() 111 | switch { 112 | case n.Rename.IsValid(): 113 | p.Migration.RenameTable(tbName, n.NewName.Name) 114 | 115 | case n.RenameColumn.IsValid(): 116 | p.Migration.RenameColumn(tbName, n.ColumnName.String(), n.NewColumnName.String()) 117 | 118 | case n.AddColumn.IsValid(): 119 | p.Migration.AddColumn(tbName, element.Column{ 120 | Node: element.Node{ 121 | Name: n.ColumnDef.Name.String(), 122 | Action: element.MigrateAddAction, 123 | }, 124 | CurrentAttr: element.SqlAttr{ 125 | LiteType: n.ColumnDef.Type, 126 | Options: p.parseSqliteConstrains(tbName, n.ColumnDef), 127 | }, 128 | }) 129 | } 130 | } 131 | 132 | return p, nil, nil 133 | } 134 | 135 | func (p *Parser) parseSqliteConstrains(tbName string, columnDefinition *sqlite.ColumnDefinition) []*ast.ColumnOption { 136 | // https://www.sqlite.org/syntax/column-constraint.html 137 | // Also, Sqlite does not support dropping constraints, so we safely can add them here 138 | conss := columnDefinition.Constraints 139 | opts := []*ast.ColumnOption{} 140 | for _, cons := range conss { 141 | switch cons := cons.(type) { 142 | case *sqlite.PrimaryKeyConstraint: 143 | opts = append(opts, &ast.ColumnOption{Tp: ast.ColumnOptionPrimaryKey}) 144 | if cons.Autoincrement.IsValid() { 145 | opts = append(opts, &ast.ColumnOption{Tp: ast.ColumnOptionAutoIncrement}) 146 | } 147 | 148 | case *sqlite.NotNullConstraint: 149 | opts = append(opts, &ast.ColumnOption{Tp: ast.ColumnOptionNotNull}) 150 | 151 | case *sqlite.UniqueConstraint: 152 | opts = append(opts, &ast.ColumnOption{Tp: ast.ColumnOptionUniqKey}) 153 | 154 | case *sqlite.CheckConstraint: 155 | opts = append(opts, &ast.ColumnOption{ 156 | Tp: ast.ColumnOptionCheck, 157 | StrValue: cons.Expr.String(), 158 | }) 159 | 160 | case *sqlite.DefaultConstraint: 161 | opts = append(opts, &ast.ColumnOption{ 162 | Tp: ast.ColumnOptionDefaultValue, 163 | StrValue: cons.Expr.String(), 164 | }) 165 | } 166 | } 167 | 168 | return opts 169 | } 170 | 171 | func (p Parser) VisitEnd(node sqlite.Node) (sqlite.Node, error) { 172 | return nil, nil 173 | } 174 | -------------------------------------------------------------------------------- /sql-templates/ddl.go: -------------------------------------------------------------------------------- 1 | package sql_templates 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | // Sql ... 8 | type Sql struct { 9 | dialect SqlDialect 10 | lowercase bool 11 | } 12 | 13 | // NewSql ... 14 | func NewSql(dialect SqlDialect, lowercase bool) *Sql { 15 | return &Sql{ 16 | dialect: dialect, 17 | lowercase: lowercase, 18 | } 19 | } 20 | 21 | func (s Sql) apply(t string) string { 22 | if s.lowercase { 23 | return strings.ToLower(t) 24 | } 25 | 26 | return t 27 | } 28 | 29 | // GetDialect ... 30 | func (s Sql) GetDialect() SqlDialect { 31 | return s.dialect 32 | } 33 | 34 | // IsMysql ... 35 | func (s Sql) IsMysql() bool { 36 | return s.dialect == MysqlDialect 37 | } 38 | 39 | // IsPostgres ... 40 | func (s Sql) IsPostgres() bool { 41 | return s.dialect == PostgresDialect 42 | } 43 | 44 | // IsSqlserver ... 45 | func (s Sql) IsSqlserver() bool { 46 | return s.dialect == SqlserverDialect 47 | } 48 | 49 | // IsSqlite ... 50 | func (s Sql) IsSqlite() bool { 51 | return s.dialect == SqliteDialect 52 | } 53 | 54 | // IsLowercase ... 55 | func (s Sql) IsLowercase() bool { 56 | return s.lowercase 57 | } 58 | 59 | // CreateTableStm ... 60 | func (s Sql) CreateTableStm() string { 61 | return s.apply("CREATE TABLE %s (\n%s\n)%s;") 62 | } 63 | 64 | // DropTableStm ... 65 | func (s Sql) DropTableStm() string { 66 | return s.apply("DROP TABLE IF EXISTS %s;") 67 | } 68 | 69 | // RenameTableStm ... 70 | func (s Sql) RenameTableStm() string { 71 | return s.apply("ALTER TABLE %s RENAME TO %s;") 72 | } 73 | 74 | // AlterTableAddColumnFirstStm ... 75 | func (s Sql) AlterTableAddColumnFirstStm() string { 76 | return s.apply("ALTER TABLE %s ADD COLUMN %s FIRST;") 77 | } 78 | 79 | // AlterTableAddColumnAfterStm ... 80 | func (s Sql) AlterTableAddColumnAfterStm() string { 81 | return s.apply("ALTER TABLE %s ADD COLUMN %s AFTER %s;") 82 | } 83 | 84 | // AlterTableDropColumnStm ... 85 | func (s Sql) AlterTableDropColumnStm() string { 86 | if s.IsSqlite() { 87 | return "" 88 | } 89 | 90 | return s.apply("ALTER TABLE %s DROP COLUMN %s;") 91 | } 92 | 93 | // AlterTableModifyColumnStm ... 94 | func (s Sql) AlterTableModifyColumnStm() string { 95 | return s.apply("ALTER TABLE %s MODIFY COLUMN %s;") 96 | } 97 | 98 | // AlterTableRenameColumnStm ... 99 | func (s Sql) AlterTableRenameColumnStm() string { 100 | return s.apply("ALTER TABLE %s RENAME COLUMN %s TO %s;") 101 | } 102 | 103 | // CreatePrimaryKeyStm ... 104 | func (s Sql) CreatePrimaryKeyStm() string { 105 | return s.apply("ALTER TABLE %s ADD PRIMARY KEY(%s);") 106 | } 107 | 108 | // CreateForeignKeyStm ... 109 | func (s Sql) CreateForeignKeyStm() string { 110 | return s.apply("ALTER TABLE %s ADD CONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s(%s);") 111 | } 112 | 113 | // CreateIndexStm ... 114 | func (s Sql) CreateIndexStm(indexType string) string { 115 | if indexType != "" { 116 | return s.apply("CREATE INDEX %s ON %s(%s) USING " + strings.ToUpper(indexType) + ";") 117 | } 118 | 119 | return s.apply("CREATE INDEX %s ON %s(%s);") 120 | } 121 | 122 | // CreateUniqueIndexStm ... 123 | func (s Sql) CreateUniqueIndexStm(indexType string) string { 124 | if indexType != "" { 125 | return s.apply("CREATE UNIQUE INDEX %s ON %s(%s) USING " + strings.ToUpper(indexType) + ";") 126 | } 127 | 128 | return s.apply("CREATE UNIQUE INDEX %s ON %s(%s);") 129 | } 130 | 131 | // DropPrimaryKeyStm ... 132 | func (s Sql) DropPrimaryKeyStm() string { 133 | return s.apply("ALTER TABLE %s DROP PRIMARY KEY;") 134 | } 135 | 136 | // DropForeignKeyStm ... 137 | func (s Sql) DropForeignKeyStm() string { 138 | return s.apply("ALTER TABLE %s DROP CONSTRAINT %s;") 139 | } 140 | 141 | // DropIndexStm ... 142 | func (s Sql) DropIndexStm() string { 143 | if s.IsSqlite() { 144 | return s.apply("DROP INDEX %s;") 145 | } 146 | 147 | return s.apply("DROP INDEX %s ON %s;") 148 | } 149 | 150 | // AlterTableRenameIndexStm ... 151 | func (s Sql) AlterTableRenameIndexStm() string { 152 | return s.apply("ALTER TABLE %s RENAME INDEX %s TO %s;") 153 | } 154 | 155 | // CreateTableMigration ... 156 | func (s Sql) CreateTableMigration() string { 157 | switch s.dialect { 158 | case PostgresDialect: 159 | return s.apply(`CREATE TABLE IF NOT EXISTS %s ( 160 | version BIGINT PRIMARY KEY, 161 | dirty BOOLEAN 162 | );`) 163 | 164 | default: 165 | // TODO: mysql template is default for other dialects 166 | return s.apply(`CREATE TABLE IF NOT EXISTS %s ( 167 | version bigint(20) PRIMARY KEY, 168 | dirty BOOLEAN 169 | );`) 170 | } 171 | } 172 | 173 | // DropTableMigration ... 174 | func (s Sql) DropTableMigration() string { 175 | return s.apply("DROP TABLE IF EXISTS %s;") 176 | } 177 | 178 | // InsertMigrationVersion ... 179 | func (s Sql) InsertMigrationVersion() string { 180 | return s.apply("DELETE FROM %s LIMIT 1;\nINSERT INTO %s (version, dirty) VALUES (%d, %t);") 181 | } 182 | 183 | // RollbackMigrationVersion ... 184 | func (s Sql) RollbackMigrationVersion() string { 185 | return s.apply("DELETE FROM %s LIMIT 1;") 186 | } 187 | -------------------------------------------------------------------------------- /sql-templates/option.go: -------------------------------------------------------------------------------- 1 | package sql_templates 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | type SqlDialect string 9 | 10 | const ( 11 | MysqlDialect = "mysql" 12 | PostgresDialect = "postgres" 13 | SqlserverDialect = "sqlserver" 14 | SqliteDialect = "sqlite3" 15 | ) 16 | 17 | // PrimaryOption ... 18 | func (s Sql) PrimaryOption() string { 19 | return s.apply("PRIMARY KEY") 20 | } 21 | 22 | // AutoIncrementOption ... 23 | func (s Sql) AutoIncrementOption() string { 24 | switch s.dialect { 25 | case PostgresDialect: 26 | // for postgres, you should set type is `serial` so dont need auto_increment 27 | // empty this avoid duplicate 28 | return "" 29 | 30 | default: 31 | // TODO: mysql template is default for other dialects 32 | return s.apply("AUTO_INCREMENT") 33 | } 34 | } 35 | 36 | // DefaultOption ... 37 | func (s Sql) DefaultOption() string { 38 | return s.apply("DEFAULT %s") 39 | } 40 | 41 | // NotNullValue ... 42 | func (s Sql) NotNullValue() string { 43 | return s.apply("NOT NULL") 44 | } 45 | 46 | // NullValue ... 47 | func (s Sql) NullValue() string { 48 | return s.apply("NULL") 49 | } 50 | 51 | // ColumnComment ... 52 | func (s Sql) ColumnComment() string { 53 | switch s.dialect { 54 | case PostgresDialect: 55 | return s.apply("COMMENT ON COLUMN %s.%s IS '%s';") 56 | 57 | default: 58 | return s.apply("COMMENT '%s'") 59 | } 60 | } 61 | 62 | // TableComment ... 63 | func (s Sql) TableComment() string { 64 | switch s.dialect { 65 | case PostgresDialect: 66 | return s.apply("COMMENT ON TABLE %s IS '%s';") 67 | 68 | default: 69 | return s.apply("COMMENT '%s'") 70 | } 71 | } 72 | 73 | // EscapeSqlName ... 74 | func (s Sql) EscapeSqlName(name string) string { 75 | if name == "" { 76 | return name 77 | } 78 | 79 | escapeChar := "`" 80 | switch s.dialect { 81 | case PostgresDialect, SqliteDialect: 82 | escapeChar = "\"" 83 | } 84 | 85 | return fmt.Sprintf("%s%s%s", escapeChar, strings.Trim(name, escapeChar), escapeChar) 86 | } 87 | 88 | // EscapeSqlNames ... 89 | func (s Sql) EscapeSqlNames(names []string) []string { 90 | ns := make([]string, len(names)) 91 | for i := range names { 92 | ns[i] = s.EscapeSqlName(names[i]) 93 | } 94 | 95 | return ns 96 | } 97 | -------------------------------------------------------------------------------- /sql-templates/type.go: -------------------------------------------------------------------------------- 1 | package sql_templates 2 | 3 | // BooleanType ... 4 | func (s Sql) BooleanType() string { 5 | if s.IsSqlite() { 6 | return s.apply("INTEGER") 7 | } 8 | 9 | return s.apply("BOOLEAN") 10 | } 11 | 12 | // TinyIntType ... 13 | func (s Sql) TinyIntType() string { 14 | return s.apply("TINYINT") 15 | } 16 | 17 | // SmallIntType ... 18 | func (s Sql) SmallIntType() string { 19 | return s.apply("SMALLINT") 20 | } 21 | 22 | // IntType ... 23 | func (s Sql) IntType() string { 24 | if s.IsSqlite() { 25 | return s.apply("INTEGER") 26 | } 27 | 28 | return s.apply("INT") 29 | } 30 | 31 | // BigIntType ... 32 | func (s Sql) BigIntType() string { 33 | if s.IsSqlite() { 34 | return s.apply("INTEGER") 35 | } 36 | 37 | return s.apply("BIGINT") 38 | } 39 | 40 | // FloatType ... 41 | func (s Sql) FloatType() string { 42 | if s.IsSqlite() { 43 | return s.apply("REAL") 44 | } 45 | 46 | return s.apply("FLOAT") 47 | } 48 | 49 | // DoubleType ... 50 | func (s Sql) DoubleType() string { 51 | if s.IsSqlite() { 52 | return s.apply("REAL") 53 | } 54 | 55 | return s.apply("DOUBLE") 56 | } 57 | 58 | // TextType ... 59 | func (s Sql) TextType() string { 60 | switch s.dialect { 61 | case SqliteDialect: 62 | return "TEXT" 63 | 64 | default: 65 | return s.apply("TEXT") 66 | } 67 | } 68 | 69 | // DatetimeType ... 70 | func (s Sql) DatetimeType() string { 71 | switch s.dialect { 72 | case PostgresDialect: 73 | return s.apply("TIMESTAMP") 74 | 75 | case SqliteDialect: 76 | return "TEXT" // TEXT as ISO8601 strings ("YYYY-MM-DD HH:MM:SS.SSS") 77 | 78 | default: 79 | return s.apply("DATETIME") 80 | } 81 | } 82 | 83 | // PointerType ... 84 | func (s Sql) PointerType() string { 85 | return s.apply("POINTER") 86 | } 87 | 88 | // UnspecificType ... 89 | func (s Sql) UnspecificType() string { 90 | return s.apply("UNSPECIFIED") 91 | } 92 | -------------------------------------------------------------------------------- /sqlize.go: -------------------------------------------------------------------------------- 1 | package sqlize 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | 9 | _ "github.com/pingcap/parser/test_driver" // driver parser 10 | "github.com/sunary/sqlize/element" 11 | "github.com/sunary/sqlize/export/avro" 12 | "github.com/sunary/sqlize/export/mermaidjs" 13 | sql_builder "github.com/sunary/sqlize/sql-builder" 14 | sql_parser "github.com/sunary/sqlize/sql-parser" 15 | sql_templates "github.com/sunary/sqlize/sql-templates" 16 | "github.com/sunary/sqlize/utils" 17 | ) 18 | 19 | const ( 20 | genDescription = "/* generate by sqlize */\n\n" 21 | emptyMigration = "/* empty */" 22 | ) 23 | 24 | // Sqlize ... 25 | type Sqlize struct { 26 | migrationFolder string 27 | migrationUpSuffix string 28 | migrationDownSuffix string 29 | migrationTable string 30 | dialect sql_templates.SqlDialect 31 | lowercase bool 32 | pluralTableName bool 33 | sqlBuilder *sql_builder.SqlBuilder 34 | parser *sql_parser.Parser 35 | } 36 | 37 | // NewSqlize ... 38 | func NewSqlize(opts ...SqlizeOption) *Sqlize { 39 | o := sqlizeOptions{ 40 | migrationFolder: utils.DefaultMigrationFolder, 41 | migrationUpSuffix: utils.DefaultMigrationUpSuffix, 42 | migrationDownSuffix: utils.DefaultMigrationDownSuffix, 43 | migrationTable: utils.DefaultMigrationTable, 44 | 45 | dialect: sql_templates.MysqlDialect, 46 | lowercase: false, 47 | sqlTag: sql_builder.SqlTagDefault, 48 | pluralTableName: false, 49 | generateComment: false, 50 | ignoreFieldOrder: false, 51 | } 52 | for i := range opts { 53 | opts[i].apply(&o) 54 | } 55 | 56 | opt := []sql_builder.SqlBuilderOption{sql_builder.WithSqlTag(o.sqlTag), sql_builder.WithDialect(o.dialect)} 57 | 58 | if o.lowercase { 59 | opt = append(opt, sql_builder.WithSqlLowercase()) 60 | } 61 | 62 | if o.generateComment { 63 | opt = append(opt, sql_builder.WithCommentGenerate()) 64 | } 65 | 66 | if o.pluralTableName { 67 | opt = append(opt, sql_builder.WithPluralTableName()) 68 | } 69 | 70 | sb := sql_builder.NewSqlBuilder(opt...) 71 | 72 | return &Sqlize{ 73 | migrationFolder: o.migrationFolder, 74 | migrationUpSuffix: o.migrationUpSuffix, 75 | migrationDownSuffix: o.migrationDownSuffix, 76 | migrationTable: o.migrationTable, 77 | dialect: o.dialect, 78 | lowercase: o.lowercase, 79 | pluralTableName: o.pluralTableName, 80 | sqlBuilder: sb, 81 | parser: sql_parser.NewParser(o.dialect, o.lowercase, o.ignoreFieldOrder), 82 | } 83 | } 84 | 85 | // FromObjects load from objects 86 | func (s *Sqlize) FromObjects(objs ...interface{}) error { 87 | m := map[string]string{} 88 | for i := range objs { 89 | ob, tb := s.sqlBuilder.GetTableName(objs[i]) 90 | m[ob] = tb 91 | } 92 | 93 | s.sqlBuilder.MappingTables(m) 94 | 95 | for i := range objs { 96 | if err := s.FromString(s.sqlBuilder.AddTable(objs[i])); err != nil { 97 | return err 98 | } 99 | } 100 | 101 | return nil 102 | } 103 | 104 | // FromString load migration from sql 105 | func (s *Sqlize) FromString(sql string) error { 106 | return s.parser.Parser(sql) 107 | } 108 | 109 | // FromMigrationFolder load migration from folder `migrations` 110 | func (s *Sqlize) FromMigrationFolder() error { 111 | sqls, err := utils.ReadPath(s.migrationFolder, s.migrationUpSuffix) 112 | if err != nil { 113 | return err 114 | } 115 | 116 | for _, sql := range sqls { 117 | if err := s.FromString(sql); err != nil { 118 | return err 119 | } 120 | } 121 | 122 | return nil 123 | } 124 | 125 | // HashValue ... 126 | func (s Sqlize) HashValue() int64 { 127 | return s.parser.HashValue() 128 | } 129 | 130 | // Diff differ between 2 migrations 131 | func (s Sqlize) Diff(old Sqlize) { 132 | if s.dialect != old.dialect { 133 | panic("unable to differentiate between two distinct dialects.") 134 | } 135 | 136 | s.parser.Diff(*old.parser) 137 | } 138 | 139 | // StringUp migration up 140 | func (s Sqlize) StringUp() string { 141 | return s.parser.MigrationUp() 142 | } 143 | 144 | // StringUpWithVersion migration up with version 145 | func (s Sqlize) StringUpWithVersion(ver int64, dirty bool) string { 146 | return s.StringUp() + "\n" + s.migrationUpVersion(ver, dirty) 147 | } 148 | 149 | // StringDown migration down 150 | func (s Sqlize) StringDown() string { 151 | return s.parser.MigrationDown() 152 | } 153 | 154 | // StringDownWithVersion migration down with version 155 | func (s Sqlize) StringDownWithVersion(ver int64) string { 156 | return s.StringDown() + "\n" + s.migrationDownVersion(ver) 157 | } 158 | 159 | func (s Sqlize) writeFiles(name, migUp, migDown string) error { 160 | if migUp == "" && migDown == "" { 161 | return nil 162 | } 163 | 164 | if migUp == "" { 165 | migUp = emptyMigration 166 | } 167 | 168 | if migDown == "" { 169 | migDown = emptyMigration 170 | } 171 | 172 | fileName := utils.MigrationFileName(name) 173 | 174 | filePath := filepath.Join(s.migrationFolder, fileName+s.migrationUpSuffix) 175 | err := os.WriteFile(filePath, []byte(genDescription+migUp), 0644) 176 | if err != nil { 177 | return err 178 | } 179 | 180 | if s.migrationDownSuffix != "" && s.migrationDownSuffix != s.migrationUpSuffix { 181 | filePath := filepath.Join(s.migrationFolder, fileName+s.migrationDownSuffix) 182 | err := os.WriteFile(filePath, []byte(genDescription+migDown), 0644) 183 | if err != nil { 184 | return err 185 | } 186 | } 187 | 188 | return nil 189 | } 190 | 191 | // WriteFiles create migration files 192 | func (s Sqlize) WriteFiles(name string) error { 193 | return s.writeFiles(name, 194 | s.StringUp(), 195 | s.StringDown()) 196 | } 197 | 198 | // WriteFilesVersion create migration version only 199 | func (s Sqlize) WriteFilesVersion(name string, ver int64, dirty bool) error { 200 | return s.writeFiles(name, 201 | s.migrationUpVersion(ver, dirty), 202 | s.migrationDownVersion(ver)) 203 | } 204 | 205 | // WriteFilesWithVersion create migration files with version 206 | func (s Sqlize) WriteFilesWithVersion(name string, ver int64, dirty bool) error { 207 | return s.writeFiles(name, 208 | s.StringUp()+"\n\n"+s.migrationUpVersion(ver, dirty), 209 | s.StringDown()+"\n\n"+s.migrationDownVersion(ver)) 210 | } 211 | 212 | func (s Sqlize) migrationUpVersion(ver int64, dirty bool) string { 213 | tmp := sql_templates.NewSql(s.dialect, s.lowercase) 214 | if ver == 0 { 215 | return fmt.Sprintf(tmp.CreateTableMigration(), s.migrationTable) 216 | } 217 | 218 | return fmt.Sprintf(tmp.InsertMigrationVersion(), s.migrationTable, s.migrationTable, ver, dirty) 219 | } 220 | 221 | func (s Sqlize) migrationDownVersion(ver int64) string { 222 | tmp := sql_templates.NewSql(s.dialect, s.lowercase) 223 | if ver == 0 { 224 | return fmt.Sprintf(tmp.DropTableMigration(), s.migrationTable) 225 | } 226 | 227 | return fmt.Sprintf(tmp.RollbackMigrationVersion(), s.migrationTable) 228 | } 229 | 230 | func (s Sqlize) selectTable(needTables ...string) []element.Table { 231 | tables := make([]element.Table, 0, len(needTables)) 232 | 233 | for i := range s.parser.Migration.Tables { 234 | if len(needTables) == 0 || utils.ContainStr(needTables, s.parser.Migration.Tables[i].Name) { 235 | tables = append(tables, s.parser.Migration.Tables[i]) 236 | } 237 | } 238 | 239 | return tables 240 | } 241 | 242 | // MermaidJsErd export MermaidJs ERD 243 | func (s Sqlize) MermaidJsErd(needTables ...string) string { 244 | mm := mermaidjs.NewMermaidJs(s.selectTable(needTables...)) 245 | return mm.String() 246 | } 247 | 248 | // MermaidJsLive export MermaidJs Live 249 | func (s Sqlize) MermaidJsLive(needTables ...string) string { 250 | mm := mermaidjs.NewMermaidJs(s.selectTable(needTables...)) 251 | return mm.Live() 252 | } 253 | 254 | // ArvoSchema export arvo schema, support mysql only 255 | func (s Sqlize) ArvoSchema(needTables ...string) []string { 256 | if s.dialect != sql_templates.MysqlDialect { 257 | return nil 258 | } 259 | 260 | tables := s.selectTable(needTables...) 261 | schemas := make([]string, 0, len(tables)) 262 | for i := range tables { 263 | record := avro.NewArvoSchema(tables[i]) 264 | jsonData, _ := json.Marshal(record) 265 | schemas = append(schemas, string(jsonData)) 266 | } 267 | 268 | return schemas 269 | } 270 | -------------------------------------------------------------------------------- /sqlize_test.go: -------------------------------------------------------------------------------- 1 | package sqlize 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "reflect" 7 | "regexp" 8 | "strings" 9 | "testing" 10 | "time" 11 | 12 | "github.com/sunary/sqlize/utils" 13 | ) 14 | 15 | type Base struct { 16 | CreatedAt time.Time 17 | UpdatedAt time.Time 18 | } 19 | 20 | type person struct { 21 | ID int32 `sql:"primary_key;auto_increment"` 22 | Name string `sql:"type:VARCHAR(64);unique;index:name,age"` 23 | Alias string `sql:"-"` 24 | Age int 25 | IsFemale bool 26 | CreatedAt time.Time `sql:"default:CURRENT_TIMESTAMP"` 27 | } 28 | 29 | type anotherPerson struct { 30 | ID int32 `sql:"primary_key;auto_increment"` 31 | Name string `sql:"type:VARCHAR(64);index:name,age;unique"` 32 | Alias string `sql:"-"` 33 | Age int 34 | IsFemale bool 35 | CreatedAt time.Time `sql:"default:CURRENT_TIMESTAMP"` 36 | } 37 | 38 | func (anotherPerson) TableName() string { 39 | return "another_person" 40 | } 41 | 42 | type hotel struct { 43 | B1 Base `sql:"embedded"` 44 | B2 Base `sql:"embedded_prefix:base_"` 45 | ID int32 `sql:"primary_key"` 46 | Name string 47 | GrandOpening *time.Time 48 | } 49 | 50 | type city struct { 51 | ID int32 `sql:"primary_key;auto_increment"` 52 | Name string `sql:"column:name"` 53 | Region string `sql:"type:ENUM('northern','southern');default:'northern'"` 54 | } 55 | 56 | type movie struct { 57 | ID int32 `sql:"primary_key;auto_increment"` 58 | Title string `sql:"type:varchar(255)"` 59 | Director string `sql:"column:director;type:varchar(255)"` 60 | YearReleased string `sql:"column:year_released,previous:released_at"` 61 | } 62 | 63 | type order struct { 64 | B1 Base `sql:"embedded"` 65 | ClientID string `sql:"type:varchar(255);primary_key;index_columns:client_id,country"` 66 | Country string `sql:"type:varchar(255)"` 67 | Email string `sql:"type:varchar(255);unique"` 68 | User *user `sql:"foreign_key:email;references:email"` 69 | } 70 | 71 | func (order) TableName() string { 72 | return "orders" 73 | } 74 | 75 | type order_sqlite struct { 76 | B1 Base `sql:"embedded"` 77 | ClientID string `sql:"type:TEXT;primary_key;index_columns:client_id,country"` 78 | Country string `sql:"type:TEXT"` 79 | Email string `sql:"type:TEXT;unique"` 80 | } 81 | 82 | func (order_sqlite) TableName() string { 83 | return "orders_sqlite" 84 | } 85 | 86 | type user struct { 87 | Email string 88 | } 89 | 90 | var ( 91 | space = regexp.MustCompile(`\s+`) 92 | 93 | createAnotherPersonStm = `CREATE TABLE another_person ( 94 | id int(11) AUTO_INCREMENT PRIMARY KEY, 95 | name varchar(64), 96 | age int(11), 97 | is_female tinyint(1), 98 | created_at datetime DEFAULT CURRENT_TIMESTAMP() 99 | );` 100 | alterAnotherPersonUpStm = ` 101 | CREATE UNIQUE INDEX idx_name_age ON another_person(name, age);` 102 | alterAnotherPersonDownStm = ` 103 | DROP INDEX idx_name_age ON another_person;` 104 | 105 | createPersonStm = `CREATE TABLE person ( 106 | id int(11) AUTO_INCREMENT PRIMARY KEY, 107 | name varchar(64), 108 | age int(11), 109 | is_female tinyint(1), 110 | created_at datetime DEFAULT CURRENT_TIMESTAMP() 111 | );` 112 | alterPersonUpStm = ` 113 | CREATE UNIQUE INDEX idx_name_age ON person(name, age);` 114 | alterPersonDownStm = ` 115 | DROP INDEX idx_name_age ON person;` 116 | 117 | createHotelStm = ` 118 | CREATE TABLE hotel ( 119 | id int(11) PRIMARY KEY, 120 | name text, 121 | star tinyint(4), 122 | grand_opening datetime NULL, 123 | created_at datetime, 124 | updated_at datetime, 125 | base_created_at datetime, 126 | base_updated_at datetime 127 | );` 128 | alterHotelUpStm = ` 129 | ALTER TABLE hotel DROP COLUMN star;` 130 | alterHotelDownStm = ` 131 | ALTER TABLE hotel ADD COLUMN star tinyint(4) AFTER name;` 132 | 133 | createCityStm = ` 134 | CREATE TABLE city ( 135 | code varchar(3), 136 | id int(11) AUTO_INCREMENT PRIMARY KEY, 137 | region enum('northern','southern') DEFAULT 'northern' 138 | );` 139 | alterCityUpStm = ` 140 | ALTER TABLE city DROP COLUMN code; 141 | ALTER TABLE city ADD COLUMN name text AFTER id;` 142 | alterCityDownStm = ` 143 | ALTER TABLE city ADD COLUMN code varchar(3) FIRST; 144 | ALTER TABLE city DROP COLUMN name;` 145 | 146 | createMovieStm = ` 147 | CREATE TABLE movie ( 148 | id int(11) AUTO_INCREMENT PRIMARY KEY, 149 | title varchar(255), 150 | director varchar(255) 151 | );` 152 | alterMovieUpStm = ` 153 | ALTER TABLE movie RENAME COLUMN released_at TO year_released;` 154 | alterMovieDownStm = ` 155 | ALTER TABLE movie RENAME COLUMN year_released TO released_at;` 156 | 157 | expectCreateAnotherPersonUp = ` 158 | CREATE TABLE another_person ( 159 | id int(11) AUTO_INCREMENT PRIMARY KEY, 160 | name varchar(64), 161 | age int(11), 162 | is_female tinyint(1), 163 | created_at datetime DEFAULT CURRENT_TIMESTAMP() 164 | ); 165 | CREATE UNIQUE INDEX idx_name_age ON another_person(name, age);` 166 | expectCreateAnotherPersonDown = ` 167 | DROP TABLE IF EXISTS another_person;` 168 | 169 | expectCreatePersonUp = ` 170 | CREATE TABLE person ( 171 | id int(11) AUTO_INCREMENT PRIMARY KEY, 172 | name varchar(64), 173 | age int(11), 174 | is_female tinyint(1), 175 | created_at datetime DEFAULT CURRENT_TIMESTAMP() 176 | ); 177 | CREATE UNIQUE INDEX idx_name_age ON person(name, age);` 178 | expectCreatePersonDown = ` 179 | DROP TABLE IF EXISTS person;` 180 | 181 | expectCreateHotelUp = ` 182 | CREATE TABLE hotel ( 183 | id int(11) PRIMARY KEY, 184 | name text, 185 | grand_opening datetime NULL, 186 | created_at datetime, 187 | updated_at datetime, 188 | base_created_at datetime, 189 | base_updated_at datetime 190 | );` 191 | expectCreateHotelDown = ` 192 | DROP TABLE IF EXISTS hotel;` 193 | 194 | expectCreateCityUp = ` 195 | CREATE TABLE city ( 196 | id int(11) AUTO_INCREMENT PRIMARY KEY, 197 | name text, 198 | region enum('northern','southern') DEFAULT 'northern' 199 | );` 200 | expectCreateCityHasCommentUp = ` 201 | CREATE TABLE city ( 202 | id int(11) AUTO_INCREMENT PRIMARY KEY, 203 | name text COMMENT 'name', 204 | region enum('northern','southern') DEFAULT 'northern' COMMENT 'enum values: northern, southern' 205 | );` 206 | expectCreateCityDown = ` 207 | DROP TABLE IF EXISTS city;` 208 | expectCreateOrderUp = ` 209 | CREATE TABLE orders ( 210 | client_id varchar(255) COMMENT 'client id', 211 | country varchar(255) COMMENT 'country', 212 | email varchar(255) COMMENT 'email', 213 | created_at datetime, 214 | updated_at datetime 215 | ); 216 | ALTER TABLE orders ADD PRIMARY KEY(client_id, country); 217 | CREATE UNIQUE INDEX idx_email ON orders(email); 218 | ALTER TABLE orders ADD CONSTRAINT fk_user_orders FOREIGN KEY (email) REFERENCES user(email);` 219 | expectCreateOrderDown = ` 220 | DROP TABLE IF EXISTS orders;` 221 | expectCreateOrderPostgresUp = ` 222 | CREATE TABLE orders ( 223 | client_id VARCHAR(255), 224 | country VARCHAR(255), 225 | email VARCHAR(255), 226 | created_at TIMESTAMP, 227 | updated_at TIMESTAMP 228 | ); 229 | COMMENT ON COLUMN orders.client_id IS 'client id'; 230 | COMMENT ON COLUMN orders.country IS 'country'; 231 | COMMENT ON COLUMN orders.email IS 'email'; 232 | ALTER TABLE orders ADD PRIMARY KEY(client_id, country); 233 | CREATE UNIQUE INDEX idx_email ON orders(email); 234 | ALTER TABLE orders ADD CONSTRAINT fk_user_orders FOREIGN KEY (email) REFERENCES "user"(email);` 235 | expectCreateOrderPostgresDown = ` 236 | DROP TABLE IF EXISTS orders;` 237 | expectCreateOrderSqliteUp = ` 238 | CREATE TABLE orders_sqlite ( 239 | client_id TEXT, 240 | country TEXT, 241 | email TEXT, 242 | created_at TEXT, 243 | updated_at TEXT 244 | );` 245 | expectCreateOrderSqliteDown = ` 246 | DROP TABLE IF EXISTS orders_sqlite;` 247 | 248 | expectPersonMermaidJsErd = `erDiagram 249 | PERSON { 250 | int(11) id PK 251 | varchar(64) name 252 | int(11) age 253 | tinyint(1) is_female 254 | datetime created_at 255 | }` 256 | expectHotelMermaidJsErd = `erDiagram 257 | HOTEL { 258 | int(11) id PK 259 | text name 260 | datetime grand_opening 261 | datetime created_at 262 | datetime updated_at 263 | datetime base_created_at 264 | datetime base_updated_at 265 | }` 266 | expectPersonCityMermaidJsErd = `erDiagram 267 | PERSON { 268 | int(11) id PK 269 | varchar(64) name 270 | int(11) age 271 | tinyint(1) is_female 272 | datetime created_at 273 | } 274 | CITY { 275 | int(11) id PK 276 | text name 277 | enum region 278 | }` 279 | expectOrderUserMermaidJsErd = `erDiagram 280 | ORDERS { 281 | varchar(255) client_id 282 | varchar(255) country 283 | varchar(255) email FK 284 | datetime created_at 285 | datetime updated_at 286 | } 287 | USER { 288 | text email 289 | } 290 | ORDERS }o--|| USER: email` 291 | 292 | expectPersonMermaidJsLive = `https://mermaid.ink/img/ZXJEaWFncmFtCiBQRVJTT04gewogIGludCgxMSkgaWQgUEsgCiAgdmFyY2hhcig2NCkgbmFtZSAgCiAgaW50KDExKSBhZ2UgIAogIHRpbnlpbnQoMSkgaXNfZmVtYWxlICAKICBkYXRldGltZSBjcmVhdGVkX2F0ICAKIH0K` 293 | expectHotelMermaidJsLive = `https://mermaid.ink/img/ZXJEaWFncmFtCiBIT1RFTCB7CiAgaW50KDExKSBpZCBQSyAKICB0ZXh0IG5hbWUgIAogIGRhdGV0aW1lIGdyYW5kX29wZW5pbmcgIAogIGRhdGV0aW1lIGNyZWF0ZWRfYXQgIAogIGRhdGV0aW1lIHVwZGF0ZWRfYXQgIAogIGRhdGV0aW1lIGJhc2VfY3JlYXRlZF9hdCAgCiAgZGF0ZXRpbWUgYmFzZV91cGRhdGVkX2F0ICAKIH0K` 294 | expectPersonCityMermaidJsLive = `https://mermaid.ink/img/ZXJEaWFncmFtCiBQRVJTT04gewogIGludCgxMSkgaWQgUEsgCiAgdmFyY2hhcig2NCkgbmFtZSAgCiAgaW50KDExKSBhZ2UgIAogIHRpbnlpbnQoMSkgaXNfZmVtYWxlICAKICBkYXRldGltZSBjcmVhdGVkX2F0ICAKIH0KIENJVFkgewogIGludCgxMSkgaWQgUEsgCiAgdGV4dCBuYW1lICAKICBlbnVtIHJlZ2lvbiAgCiB9Cg==` 295 | expectOrderUserMermaidJsLive = `https://mermaid.ink/img/ZXJEaWFncmFtCiBPUkRFUlMgewogIHZhcmNoYXIoMjU1KSBjbGllbnRfaWQgIAogIHZhcmNoYXIoMjU1KSBjb3VudHJ5ICAKICB2YXJjaGFyKDI1NSkgZW1haWwgRksgCiAgZGF0ZXRpbWUgY3JlYXRlZF9hdCAgCiAgZGF0ZXRpbWUgdXBkYXRlZF9hdCAgCiB9CiBVU0VSIHsKICB0ZXh0IGVtYWlsICAKIH0KIE9SREVSUyB9by0tfHwgVVNFUjogZW1haWw=` 296 | 297 | expectPersonArvo = ` 298 | {"type":"record","name":"person","namespace":"person","fields":[{"name":"before","type":["null",{"type":"record","name":"Value","namespace":"","fields":[{"name":"id","type":"int"},{"name":"name","type":"string"},{"name":"age","type":"int"},{"name":"is_female","type":"bool"},{"name":"created_at","type":["null",{"connect.default":"1970-01-01T00:00:00Z","connect.name":"io.debezium.time.ZonedTimestamp","connect.version":1,"type":"string"}]}],"connect.name":""}]},{"name":"after","type":["null","Value"]},{"name":"op","type":"string"},{"name":"ts_ms","type":["null","long"]},{"name":"transaction","type":["null",{"type":"record","name":"ConnectDefault","namespace":"io.confluent.connect.avro","fields":[{"name":"id","type":"string"},{"name":"total_order","type":"long"},{"name":"data_collection_order","type":"long"}],"connect.name":""}]}],"connect.name":"person"}` 299 | expectHotelArvo = ` 300 | {"type":"record","name":"hotel","namespace":"hotel","fields":[{"name":"before","type":["null",{"type":"record","name":"Value","namespace":"","fields":[{"name":"id","type":"int"},{"name":"name","type":"string"},{"name":"grand_opening","type":{"connect.default":"1970-01-01T00:00:00Z","connect.name":"io.debezium.time.ZonedTimestamp","connect.version":1,"type":"string"}},{"name":"created_at","type":{"connect.default":"1970-01-01T00:00:00Z","connect.name":"io.debezium.time.ZonedTimestamp","connect.version":1,"type":"string"}},{"name":"updated_at","type":{"connect.default":"1970-01-01T00:00:00Z","connect.name":"io.debezium.time.ZonedTimestamp","connect.version":1,"type":"string"}},{"name":"base_created_at","type":{"connect.default":"1970-01-01T00:00:00Z","connect.name":"io.debezium.time.ZonedTimestamp","connect.version":1,"type":"string"}},{"name":"base_updated_at","type":{"connect.default":"1970-01-01T00:00:00Z","connect.name":"io.debezium.time.ZonedTimestamp","connect.version":1,"type":"string"}}],"connect.name":""}]},{"name":"after","type":["null","Value"]},{"name":"op","type":"string"},{"name":"ts_ms","type":["null","long"]},{"name":"transaction","type":["null",{"type":"record","name":"ConnectDefault","namespace":"io.confluent.connect.avro","fields":[{"name":"id","type":"string"},{"name":"total_order","type":"long"},{"name":"data_collection_order","type":"long"}],"connect.name":""}]}],"connect.name":"hotel"}` 301 | expectCityArvo = ` 302 | {"type":"record","name":"city","namespace":"city","fields":[{"name":"before","type":["null",{"type":"record","name":"Value","namespace":"","fields":[{"name":"id","type":"int"},{"name":"name","type":"string"},{"name":"region","type":["null",{"connect.default":"init","connect.name":"io.debezium.data.Enum","connect.parameters":{"allowed":"northern,southern"},"connect.version":1,"type":"string"}]}],"connect.name":""}]},{"name":"after","type":["null","Value"]},{"name":"op","type":"string"},{"name":"ts_ms","type":["null","long"]},{"name":"transaction","type":["null",{"type":"record","name":"ConnectDefault","namespace":"io.confluent.connect.avro","fields":[{"name":"id","type":"string"},{"name":"total_order","type":"long"},{"name":"data_collection_order","type":"long"}],"connect.name":""}]}],"connect.name":"city"}` 303 | 304 | expectCreateMigrationTableUp = `CREATE TABLE IF NOT EXISTS schema_migrations ( 305 | version bigint(20) PRIMARY KEY, 306 | dirty BOOLEAN 307 | );` 308 | expectCreateMigrationTableDown = ` 309 | DROP TABLE IF EXISTS schema_migrations;` 310 | expectMigrationVersion1Up = ` 311 | DELETE FROM schema_migrations LIMIT 1; 312 | INSERT INTO schema_migrations (version, dirty) VALUES (1, false);` 313 | expectMigrationVersion1Down = ` 314 | DELETE FROM schema_migrations LIMIT 1;` 315 | ) 316 | 317 | func TestSqlize_FromObjects(t *testing.T) { 318 | now := time.Now() 319 | 320 | type args struct { 321 | objs []interface{} 322 | migrationFolder string 323 | } 324 | 325 | fromObjectMysqlTestcase := []struct { 326 | name string 327 | generateComment bool 328 | pluralTableName bool 329 | args args 330 | wantMigrationUp string 331 | wantMigrationDown string 332 | wantErr bool 333 | }{ 334 | { 335 | name: "from anotherPerson object", 336 | pluralTableName: true, 337 | args: args{ 338 | []interface{}{anotherPerson{}}, 339 | "", 340 | }, 341 | wantMigrationUp: expectCreateAnotherPersonUp, 342 | wantMigrationDown: expectCreateAnotherPersonDown, 343 | wantErr: false, 344 | }, 345 | { 346 | name: "from person object", 347 | args: args{ 348 | []interface{}{person{}}, 349 | "", 350 | }, 351 | wantMigrationUp: expectCreatePersonUp, 352 | wantMigrationDown: expectCreatePersonDown, 353 | wantErr: false, 354 | }, 355 | { 356 | name: "from hotel object", 357 | args: args{ 358 | []interface{}{hotel{GrandOpening: &now}}, 359 | "", 360 | }, 361 | wantMigrationUp: expectCreateHotelUp, 362 | wantMigrationDown: expectCreateHotelDown, 363 | wantErr: false, 364 | }, 365 | { 366 | name: "from city object", 367 | generateComment: true, 368 | args: args{ 369 | []interface{}{city{}}, 370 | "/", 371 | }, 372 | wantMigrationUp: expectCreateCityHasCommentUp, 373 | wantMigrationDown: expectCreateCityDown, 374 | wantErr: false, 375 | }, 376 | { 377 | name: "from order object", 378 | generateComment: true, 379 | args: args{ 380 | []interface{}{order{}}, 381 | "/", 382 | }, 383 | wantMigrationUp: expectCreateOrderUp, 384 | wantMigrationDown: expectCreateOrderDown, 385 | wantErr: false, 386 | }, 387 | { 388 | name: "from all object", 389 | args: args{ 390 | []interface{}{person{}, hotel{GrandOpening: &now}, city{}}, 391 | "/", 392 | }, 393 | wantMigrationUp: joinSql(expectCreatePersonUp, expectCreateHotelUp, expectCreateCityUp), 394 | wantMigrationDown: joinSql(expectCreatePersonDown, expectCreateHotelDown, expectCreateCityDown), 395 | wantErr: false, 396 | }, 397 | } 398 | 399 | for i, tt := range fromObjectMysqlTestcase { 400 | t.Run(tt.name, func(t *testing.T) { 401 | opts := []SqlizeOption{ 402 | WithMigrationSuffix(".up.test", ".down.test"), WithMigrationFolder(tt.args.migrationFolder), 403 | } 404 | if tt.generateComment { 405 | opts = append(opts, WithCommentGenerate()) 406 | } 407 | if tt.pluralTableName { 408 | opts = append(opts, WithPluralTableName()) 409 | } 410 | 411 | if i%3 == 1 { 412 | opts = append(opts, WithSqlserver()) //fallback mysql 413 | } 414 | 415 | s := NewSqlize(opts...) 416 | if tt.args.migrationFolder == "" { 417 | if err := s.FromMigrationFolder(); err == nil { 418 | t.Errorf("FromMigrationFolder() mysql error = %v,\n wantErr = %v", err, utils.PathDoesNotExistErr) 419 | } 420 | } else if tt.args.migrationFolder == "/" { 421 | if err := s.FromMigrationFolder(); err != nil { 422 | t.Errorf("FromMigrationFolder() mysql error = %v,\n wantErr = %v", err, nil) 423 | } 424 | } 425 | 426 | if err := s.FromObjects(tt.args.objs...); (err != nil) != tt.wantErr { 427 | t.Errorf("FromObjects() mysql error = %v,\n wantErr = %v", err, tt.wantErr) 428 | } 429 | 430 | if strUp := s.StringUp(); normSql(strUp) != normSql(tt.wantMigrationUp) { 431 | t.Errorf("StringUp() mysql got = \n%s,\nexpected = \n%s", strUp, tt.wantMigrationUp) 432 | } 433 | 434 | if strDown := s.StringDown(); normSql(strDown) != normSql(tt.wantMigrationDown) { 435 | t.Errorf("StringDown() mysql got = \n%s,\nexpected = \n%s", strDown, tt.wantMigrationDown) 436 | } 437 | 438 | if tt.args.migrationFolder == "" { 439 | if err := s.WriteFiles(tt.name); err != nil { 440 | t.Errorf("WriteFiles() mysql error = \n%v,\nwantErr = \n%v", err, nil) 441 | } 442 | } else if tt.args.migrationFolder == "/" { 443 | if err := s.WriteFiles(tt.name); err == nil { 444 | t.Errorf("WriteFiles() mysql error = \n%v,\nwantErr = \n%v", err, errors.New("read-only file system")) 445 | } 446 | } 447 | }) 448 | } 449 | 450 | fromObjectPostgresTestcase := []struct { 451 | name string 452 | generateComment bool 453 | pluralTableName bool 454 | args args 455 | wantMigrationUp string 456 | wantMigrationDown string 457 | wantErr bool 458 | }{ 459 | { 460 | name: "from order object", 461 | generateComment: true, 462 | args: args{ 463 | []interface{}{order{}}, 464 | "/", 465 | }, 466 | wantMigrationUp: expectCreateOrderPostgresUp, 467 | wantMigrationDown: expectCreateOrderPostgresDown, 468 | wantErr: false, 469 | }, 470 | } 471 | for _, tt := range fromObjectPostgresTestcase { 472 | t.Run(tt.name, func(t *testing.T) { 473 | s := NewSqlize(WithPostgresql(), WithCommentGenerate()) 474 | if err := s.FromObjects(tt.args.objs...); (err != nil) != tt.wantErr { 475 | t.Errorf("FromObjects() postgres error = %v,\n wantErr = %v", err, tt.wantErr) 476 | } 477 | 478 | if strUp := s.StringUp(); normSql(strUp) != normSql(tt.wantMigrationUp) { 479 | t.Errorf("StringUp() postgres got = \n%s,\nexpected = \n%s", strUp, tt.wantMigrationUp) 480 | } 481 | 482 | if strDown := s.StringDown(); normSql(strDown) != normSql(tt.wantMigrationDown) { 483 | t.Errorf("StringDown() postgres got = \n%s,\nexpected = \n%s", strDown, tt.wantMigrationDown) 484 | } 485 | }) 486 | } 487 | 488 | fromObjectSqliteTestcase := []struct { 489 | name string 490 | generateComment bool 491 | pluralTableName bool 492 | args args 493 | wantMigrationUp string 494 | wantMigrationDown string 495 | wantErr bool 496 | }{ 497 | { 498 | name: "from order sqlite object", 499 | generateComment: true, 500 | args: args{ 501 | []interface{}{order_sqlite{}}, 502 | "/", 503 | }, 504 | wantMigrationUp: expectCreateOrderSqliteUp, 505 | wantMigrationDown: expectCreateOrderSqliteDown, 506 | wantErr: false, 507 | }, 508 | } 509 | for _, tt := range fromObjectSqliteTestcase { 510 | t.Run(tt.name, func(t *testing.T) { 511 | s := NewSqlize(WithSqlite()) 512 | if err := s.FromObjects(tt.args.objs...); (err != nil) != tt.wantErr { 513 | t.Errorf("FromObjects() sqlite error = %v,\n wantErr = %v", err, tt.wantErr) 514 | } 515 | 516 | if strUp := s.StringUp(); normSql(strUp) != normSql(tt.wantMigrationUp) { 517 | t.Errorf("StringUp() sqlite got = \n%s,\nexpected = \n%s", strUp, tt.wantMigrationUp) 518 | } 519 | 520 | if strDown := s.StringDown(); normSql(strDown) != normSql(tt.wantMigrationDown) { 521 | t.Errorf("StringDown() sqlite got = \n%s,\nexpected = \n%s", strDown, tt.wantMigrationDown) 522 | } 523 | }) 524 | } 525 | } 526 | 527 | func TestSqlize_FromString(t *testing.T) { 528 | type args struct { 529 | sql string 530 | } 531 | 532 | fromStringMysqlTestcases := []struct { 533 | name string 534 | args args 535 | wantMigrationUp string 536 | wantMigrationDown string 537 | wantErr bool 538 | }{ 539 | { 540 | name: "from person sql", 541 | args: args{ 542 | joinSql(createPersonStm, alterPersonUpStm), 543 | }, 544 | wantMigrationUp: expectCreatePersonUp, 545 | wantMigrationDown: expectCreatePersonDown, 546 | wantErr: false, 547 | }, 548 | { 549 | name: "from hotel sql", 550 | args: args{ 551 | joinSql(createHotelStm, alterHotelUpStm), 552 | }, 553 | wantMigrationUp: expectCreateHotelUp, 554 | wantMigrationDown: expectCreateHotelDown, 555 | wantErr: false, 556 | }, 557 | { 558 | name: "from city sql", 559 | args: args{ 560 | joinSql(createCityStm, alterCityUpStm), 561 | }, 562 | wantMigrationUp: expectCreateCityUp, 563 | wantMigrationDown: expectCreateCityDown, 564 | wantErr: false, 565 | }, 566 | } 567 | 568 | for _, tt := range fromStringMysqlTestcases { 569 | t.Run(tt.name, func(t *testing.T) { 570 | s := NewSqlize(WithMysql(), WithSqlUppercase()) 571 | if err := s.FromString(tt.args.sql); (err != nil) != tt.wantErr { 572 | t.Errorf("FromString() mysql error = %v,\n wantErr = %v", err, tt.wantErr) 573 | } 574 | 575 | if strUp := s.StringUp(); normSql(strUp) != normSql(tt.wantMigrationUp) { 576 | t.Errorf("StringUp() mysql got = \n%s,\nexpected = \n%s", strUp, tt.wantMigrationUp) 577 | } 578 | 579 | if strDown := s.StringDown(); normSql(strDown) != normSql(tt.wantMigrationDown) { 580 | t.Errorf("StringDown() mysql got = \n%s,\nexpected = \n%s", strDown, tt.wantMigrationDown) 581 | } 582 | }) 583 | } 584 | 585 | fromStringPostgresTestcases := []struct { 586 | name string 587 | args args 588 | wantMigrationUp string 589 | wantMigrationDown string 590 | wantErr bool 591 | }{} 592 | for _, tt := range fromStringPostgresTestcases { 593 | t.Run(tt.name, func(t *testing.T) { 594 | s := NewSqlize(WithPostgresql()) 595 | if err := s.FromString(tt.args.sql); (err != nil) != tt.wantErr { 596 | t.Errorf("FromString() postgres error = %v,\n wantErr = %v", err, tt.wantErr) 597 | } 598 | 599 | if strUp := s.StringUp(); normSql(strUp) != normSql(tt.wantMigrationUp) { 600 | t.Errorf("StringUp() postgres got = \n%s,\nexpected = \n%s", strUp, tt.wantMigrationUp) 601 | } 602 | 603 | if strDown := s.StringDown(); normSql(strDown) != normSql(tt.wantMigrationDown) { 604 | t.Errorf("StringDown() postgres got = \n%s,\nexpected = \n%s", strDown, tt.wantMigrationDown) 605 | } 606 | }) 607 | } 608 | } 609 | 610 | func TestSqlize_Diff(t *testing.T) { 611 | now := time.Now() 612 | 613 | type args struct { 614 | newObj interface{} 615 | oldSql string 616 | } 617 | 618 | diffMysqlTestcases := []struct { 619 | name string 620 | args args 621 | wantMigrationUp string 622 | wantMigrationDown string 623 | }{ 624 | { 625 | name: "diff person sql", 626 | args: args{ 627 | person{}, 628 | createPersonStm, 629 | }, 630 | wantMigrationUp: alterPersonUpStm, 631 | wantMigrationDown: alterPersonDownStm, 632 | }, 633 | { 634 | name: "diff hotel sql", 635 | args: args{ 636 | hotel{GrandOpening: &now}, 637 | createHotelStm, 638 | }, 639 | wantMigrationUp: alterHotelUpStm, 640 | wantMigrationDown: alterHotelDownStm, 641 | }, 642 | { 643 | name: "diff city sql", 644 | args: args{ 645 | city{}, 646 | createCityStm, 647 | }, 648 | wantMigrationUp: alterCityUpStm, 649 | wantMigrationDown: alterCityDownStm, 650 | }, 651 | { 652 | name: "diff movie sql", 653 | args: args{ 654 | movie{}, 655 | createMovieStm, 656 | }, 657 | wantMigrationUp: alterMovieUpStm, 658 | wantMigrationDown: alterMovieDownStm, 659 | }, 660 | } 661 | 662 | for _, tt := range diffMysqlTestcases { 663 | t.Run(tt.name, func(t *testing.T) { 664 | s := NewSqlize(WithSqlTag("sql"), WithSqlLowercase()) 665 | _ = s.FromObjects(tt.args.newObj) 666 | 667 | o := NewSqlize() 668 | _ = o.FromString(tt.args.oldSql) 669 | 670 | s.Diff(*o) 671 | if strUp := s.StringUp(); normSql(strUp) != normSql(tt.wantMigrationUp) { 672 | t.Errorf("StringUp() mysql got = \n%s,\nexpected = \n%s", strUp, tt.wantMigrationUp) 673 | } 674 | 675 | if strDown := s.StringDown(); normSql(strDown) != normSql(tt.wantMigrationDown) { 676 | t.Errorf("StringDown() mysql got = \n%s,\nexpected = \n%s", strDown, tt.wantMigrationDown) 677 | } 678 | }) 679 | } 680 | 681 | diffPostgresTestcases := []struct { 682 | name string 683 | args args 684 | wantMigrationUp string 685 | wantMigrationDown string 686 | }{} 687 | for _, tt := range diffPostgresTestcases { 688 | t.Run(tt.name, func(t *testing.T) { 689 | s := NewSqlize(WithPostgresql()) 690 | _ = s.FromObjects(tt.args.newObj) 691 | 692 | o := NewSqlize(WithPostgresql()) 693 | _ = o.FromString(tt.args.oldSql) 694 | 695 | s.Diff(*o) 696 | if strUp := s.StringUp(); normSql(strUp) != normSql(tt.wantMigrationUp) { 697 | t.Errorf("StringUp() postgres got = \n%s,\nexpected = \n%s", strUp, tt.wantMigrationUp) 698 | } 699 | 700 | if strDown := s.StringDown(); normSql(strDown) != normSql(tt.wantMigrationDown) { 701 | t.Errorf("StringDown() postgres got = \n%s,\nexpected = \n%s", strDown, tt.wantMigrationDown) 702 | } 703 | }) 704 | } 705 | } 706 | 707 | func TestSqlize_MigrationVersion(t *testing.T) { 708 | now := time.Now() 709 | 710 | type args struct { 711 | models []interface{} 712 | version int64 713 | isDirty bool 714 | } 715 | 716 | migrationVersionMysqlTestcases := []struct { 717 | name string 718 | args args 719 | wantMigrationUp string 720 | wantMigrationDown string 721 | }{ 722 | { 723 | name: "person migration version", 724 | args: args{ 725 | []interface{}{person{}}, 726 | 0, 727 | false, 728 | }, 729 | wantMigrationUp: expectCreatePersonUp + "\n" + expectCreateMigrationTableUp, 730 | wantMigrationDown: expectCreatePersonDown + "\n" + expectCreateMigrationTableDown, 731 | }, 732 | { 733 | name: "hotel migration version", 734 | args: args{ 735 | []interface{}{hotel{GrandOpening: &now}}, 736 | 1, 737 | false, 738 | }, 739 | wantMigrationUp: expectCreateHotelUp + "\n" + expectMigrationVersion1Up, 740 | wantMigrationDown: expectCreateHotelDown + "\n" + expectMigrationVersion1Down, 741 | }, 742 | { 743 | name: "city migration version", 744 | args: args{ 745 | []interface{}{city{}}, 746 | 1, 747 | false, 748 | }, 749 | wantMigrationUp: expectCreateCityUp + "\n" + expectMigrationVersion1Up, 750 | wantMigrationDown: expectCreateCityDown + "\n" + expectMigrationVersion1Down, 751 | }, 752 | } 753 | for _, tt := range migrationVersionMysqlTestcases { 754 | t.Run(tt.name, func(t *testing.T) { 755 | opts := []SqlizeOption{ 756 | WithMigrationSuffix(".up.test", ".down.test"), 757 | WithMigrationFolder(""), 758 | WithMigrationTable(utils.DefaultMigrationTable), 759 | } 760 | s := NewSqlize(opts...) 761 | 762 | s.FromObjects(tt.args.models...) 763 | if got := s.StringUpWithVersion(tt.args.version, tt.args.isDirty); normSql(got) != normSql(tt.wantMigrationUp) { 764 | t.Errorf("StringUpWithVersion() mysql got = \n%s,\nexpected = \n%s", got, tt.wantMigrationUp) 765 | } 766 | 767 | if got := s.StringDownWithVersion(tt.args.version); normSql(got) != normSql(tt.wantMigrationDown) { 768 | t.Errorf("StringDownWithVersion() mysql got = \n%s,\nexpected = \n%s", got, tt.wantMigrationDown) 769 | } 770 | 771 | if err := s.WriteFilesWithVersion(tt.name, tt.args.version, tt.args.isDirty); err != nil { 772 | t.Errorf("WriteFilesWithVersion() mysql error = \n%v,\nwantErr = \n%v", err, nil) 773 | } 774 | 775 | if err := s.WriteFilesVersion(tt.name, tt.args.version, tt.args.isDirty); err != nil { 776 | t.Errorf("WriteFilesVersion() mysql error = \n%v,\nwantErr = \n%v", err, nil) 777 | } 778 | }) 779 | } 780 | 781 | migrationVersionPostgresTestcases := []struct { 782 | name string 783 | args args 784 | wantMigrationUp string 785 | wantMigrationDown string 786 | }{} 787 | for _, tt := range migrationVersionPostgresTestcases { 788 | t.Run(tt.name, func(t *testing.T) { 789 | opts := []SqlizeOption{ 790 | WithMigrationSuffix(".up.test", ".down.test"), 791 | WithMigrationFolder(""), 792 | WithMigrationTable(utils.DefaultMigrationTable), 793 | WithPostgresql(), 794 | WithIgnoreFieldOrder(), 795 | } 796 | s := NewSqlize(opts...) 797 | 798 | s.FromObjects(tt.args.models...) 799 | if got := s.StringUpWithVersion(tt.args.version, tt.args.isDirty); normSql(got) != normSql(tt.wantMigrationUp) { 800 | t.Errorf("StringUpWithVersion() postgres got = \n%s,\nexpected = \n%s", got, tt.wantMigrationUp) 801 | } 802 | 803 | if got := s.StringDownWithVersion(tt.args.version); normSql(got) != normSql(tt.wantMigrationDown) { 804 | t.Errorf("StringDownWithVersion() postgres got = \n%s,\nexpected = \n%s", got, tt.wantMigrationDown) 805 | } 806 | 807 | if err := s.WriteFilesWithVersion(tt.name, tt.args.version, tt.args.isDirty); err != nil { 808 | t.Errorf("WriteFilesWithVersion() postgres error = \n%v,\nwantErr = \n%v", err, nil) 809 | } 810 | 811 | if err := s.WriteFilesVersion(tt.name, tt.args.version, tt.args.isDirty); err != nil { 812 | t.Errorf("WriteFilesVersion() postgres error = \n%v,\nwantErr = \n%v", err, nil) 813 | } 814 | }) 815 | } 816 | 817 | } 818 | 819 | func TestSqlize_HashValue(t *testing.T) { 820 | now := time.Now() 821 | 822 | type args struct { 823 | models []interface{} 824 | } 825 | 826 | hashValueMysqlTestcases := []struct { 827 | name string 828 | args args 829 | want int64 830 | }{ 831 | { 832 | name: "person hash value", 833 | args: args{ 834 | []interface{}{person{}}, 835 | }, 836 | want: -5168892191412708041, 837 | }, 838 | { 839 | name: "hotel hash value", 840 | args: args{ 841 | []interface{}{hotel{GrandOpening: &now}}, 842 | }, 843 | want: -3590096811374758567, 844 | }, 845 | { 846 | name: "city hash value", 847 | args: args{ 848 | []interface{}{city{}}, 849 | }, 850 | want: -2026584327433441245, 851 | }, { 852 | name: "movie hash value", 853 | args: args{ 854 | []interface{}{movie{}}, 855 | }, 856 | want: -5515853333036032887, 857 | }, 858 | } 859 | for _, tt := range hashValueMysqlTestcases { 860 | t.Run(tt.name, func(t *testing.T) { 861 | opts := []SqlizeOption{} 862 | s := NewSqlize(opts...) 863 | 864 | s.FromObjects(tt.args.models...) 865 | if got := s.HashValue(); got != tt.want { 866 | t.Errorf("HashValue() mysql got = \n%d,\nexpected = \n%d", got, tt.want) 867 | } 868 | }) 869 | } 870 | 871 | hashValuePostgresTestcases := []struct { 872 | name string 873 | args args 874 | want int64 875 | }{} 876 | for _, tt := range hashValuePostgresTestcases { 877 | t.Run(tt.name, func(t *testing.T) { 878 | opts := []SqlizeOption{WithPostgresql()} 879 | s := NewSqlize(opts...) 880 | 881 | s.FromObjects(tt.args.models...) 882 | if got := s.HashValue(); got != tt.want { 883 | t.Errorf("HashValue() postgres got = \n%d,\nexpected = \n%d", got, tt.want) 884 | } 885 | }) 886 | } 887 | } 888 | 889 | func TestSqlize_Mermaidjs(t *testing.T) { 890 | now := time.Now() 891 | 892 | type args struct { 893 | models []interface{} 894 | needTables []string 895 | } 896 | 897 | MermaidJsTestcases := []struct { 898 | name string 899 | args args 900 | wantErd string 901 | wantLive string 902 | }{ 903 | { 904 | name: "person mermaidjs", 905 | args: args{ 906 | []interface{}{person{}}, 907 | []string{"person"}, 908 | }, 909 | wantErd: expectPersonMermaidJsErd, 910 | wantLive: expectPersonMermaidJsLive, 911 | }, 912 | { 913 | name: "hotel mermaidjs", 914 | args: args{ 915 | []interface{}{hotel{GrandOpening: &now}}, 916 | []string{"hotel"}, 917 | }, 918 | wantErd: expectHotelMermaidJsErd, 919 | wantLive: expectHotelMermaidJsLive, 920 | }, 921 | { 922 | name: "person city mermaidjs", 923 | args: args{ 924 | []interface{}{person{}, city{}}, 925 | []string{"person", "city"}, 926 | }, 927 | wantErd: expectPersonCityMermaidJsErd, 928 | wantLive: expectPersonCityMermaidJsLive, 929 | }, 930 | { 931 | name: "order user mermaidjs", 932 | args: args{ 933 | []interface{}{order{}, user{}}, 934 | []string{"orders", "user"}, 935 | }, 936 | wantErd: expectOrderUserMermaidJsErd, 937 | wantLive: expectOrderUserMermaidJsLive, 938 | }, 939 | } 940 | for _, tt := range MermaidJsTestcases { 941 | t.Run(tt.name, func(t *testing.T) { 942 | opts := []SqlizeOption{} 943 | s := NewSqlize(opts...) 944 | 945 | s.FromObjects(tt.args.models...) 946 | if got := s.MermaidJsErd(tt.args.needTables...); normStr(got) != normStr(tt.wantErd) { 947 | t.Errorf("MermaidJsErd() got = \n%v,\nexpected = \n%v", got, tt.wantErd) 948 | } 949 | 950 | if got := s.MermaidJsLive(tt.args.needTables...); got != tt.wantLive { 951 | t.Errorf("MermaidJsLive() got = \n%v,\nexpected = \n%v", got, tt.wantLive) 952 | } 953 | }) 954 | } 955 | } 956 | 957 | func TestSqlize_ArvoSchema(t *testing.T) { 958 | now := time.Now() 959 | 960 | type args struct { 961 | models []interface{} 962 | needTables []string 963 | } 964 | 965 | arvoSchemaMysqlTestcases := []struct { 966 | name string 967 | args args 968 | want []string 969 | }{ 970 | { 971 | name: "person arvo", 972 | args: args{ 973 | []interface{}{person{}}, 974 | []string{"person"}, 975 | }, 976 | want: []string{expectPersonArvo}, 977 | }, 978 | { 979 | name: "hotel arvo", 980 | args: args{ 981 | []interface{}{hotel{GrandOpening: &now}}, 982 | []string{"hotel"}, 983 | }, 984 | want: []string{expectHotelArvo}, 985 | }, 986 | { 987 | name: "city arvo", 988 | args: args{ 989 | []interface{}{city{}}, 990 | []string{"city"}, 991 | }, 992 | want: []string{expectCityArvo}, 993 | }, 994 | } 995 | for _, tt := range arvoSchemaMysqlTestcases { 996 | t.Run(tt.name, func(t *testing.T) { 997 | opts := []SqlizeOption{} 998 | s := NewSqlize(opts...) 999 | 1000 | s.FromObjects(tt.args.models...) 1001 | if got := s.ArvoSchema(tt.args.needTables...); (got != nil || tt.want != nil) && !areEqualJSON(got[0], tt.want[0]) { 1002 | t.Errorf("ArvoSchema() mysql got = \n%v,\nexpected = \n%v", got, tt.want) 1003 | } 1004 | }) 1005 | } 1006 | } 1007 | 1008 | func normSql(s string) string { 1009 | s = strings.Replace(s, "`", "", -1) // mysql escape keywords 1010 | s = strings.Replace(s, "\"", "", -1) // postgres escape keywords 1011 | return strings.TrimSpace(space.ReplaceAllString(s, " ")) 1012 | } 1013 | 1014 | func normStr(s string) string { 1015 | return strings.TrimSpace(space.ReplaceAllString(s, " ")) 1016 | } 1017 | 1018 | func joinSql(s ...string) string { 1019 | return strings.Join(s, "\n") 1020 | } 1021 | 1022 | func areEqualJSON(s1, s2 string) bool { 1023 | var o1 interface{} 1024 | var o2 interface{} 1025 | 1026 | var err error 1027 | err = json.Unmarshal([]byte(s1), &o1) 1028 | if err != nil { 1029 | return false 1030 | } 1031 | err = json.Unmarshal([]byte(s2), &o2) 1032 | if err != nil { 1033 | return false 1034 | } 1035 | 1036 | return reflect.DeepEqual(o1, o2) 1037 | } 1038 | -------------------------------------------------------------------------------- /test/README.md: -------------------------------------------------------------------------------- 1 | # SQLize tests 2 | 3 | This folder aims to align with the common guidelines of the [golang project layout](https://github.com/golang-standards/project-layout/tree/master/test). 4 | 5 | ## How to run? 6 | ```bash 7 | go test ./test/... 8 | ``` 9 | -------------------------------------------------------------------------------- /test/sqlite/common.go: -------------------------------------------------------------------------------- 1 | package sqlite 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | "testing" 7 | ) 8 | 9 | const ( 10 | schemaWithOneTable = "./testdata/schema_one_table.sql" 11 | schemaWithTwoTables = "./testdata/schema_two_tables.sql" 12 | ) 13 | 14 | func assertContains(t *testing.T, str, substr, message string) { 15 | if !strings.Contains(str, substr) { 16 | t.Errorf("%s: expected to find '%s' in:\n%s", message, substr, str) 17 | } 18 | } 19 | 20 | func readFile(t *testing.T, path string) string { 21 | t.Helper() 22 | data, err := os.ReadFile(path) 23 | if err != nil { 24 | t.Fatalf("Failed to read file %s: %v", path, err) 25 | } 26 | return string(data) 27 | } 28 | -------------------------------------------------------------------------------- /test/sqlite/migration_test.go: -------------------------------------------------------------------------------- 1 | package sqlite 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/sunary/sqlize" 8 | ) 9 | 10 | // TestMigrationGeneratorSingleTable tests that Sqlize can generate a migration script for the simplest schema. 11 | func TestMigrationGeneratorSingleTable(t *testing.T) { 12 | s := sqlize.NewSqlize( 13 | sqlize.WithSqlite(), 14 | ) 15 | 16 | schemaSql := readFile(t, schemaWithOneTable) 17 | if err := s.FromString(schemaSql); err != nil { 18 | t.Fatalf("failed to parse schema: %v", err) 19 | } 20 | 21 | upSQL := s.StringUp() 22 | downSQL := s.StringDown() 23 | 24 | // Validate generated migration scripts 25 | assertContains(t, upSQL, "CREATE TABLE", "Up migration should create the table") 26 | assertContains(t, upSQL, "AUTOINCREMENT", "Up migration should include AUTOINCREMENT") 27 | assertContains(t, upSQL, "CHECK (\"age\" >= 18)", "Up migration should include CHECK constraint") 28 | assertContains(t, upSQL, "UNIQUE", "Up migration should include UNIQUE values") 29 | assertContains(t, upSQL, "DEFAULT", "Up migration should include DEFAULT values") 30 | 31 | assertContains(t, downSQL, "DROP TABLE", "Down migration should drop the table") 32 | 33 | upWithVersionSQL := s.StringUpWithVersion(0, false) 34 | downWithVersionSQL := s.StringDownWithVersion(0) 35 | 36 | // Validate versioned migration scripts 37 | assertContains(t, upWithVersionSQL, "CREATE TABLE IF NOT EXISTS schema_migrations", "Initial migration should create the migrations table") 38 | assertContains(t, downWithVersionSQL, "DROP TABLE IF EXISTS schema_migrations", "Down migration from before initial should drop the migrations table") 39 | 40 | version := s.HashValue() 41 | upWithVersionNumberSQL := s.StringUpWithVersion(version, false) 42 | 43 | // Validate versioned migration scripts 44 | expectedVersionInsert := fmt.Sprintf("INSERT INTO schema_migrations (version, dirty) VALUES (%d, false);", version) 45 | 46 | assertContains(t, upWithVersionNumberSQL, expectedVersionInsert, "Versioned up migration should include version comment") 47 | } 48 | -------------------------------------------------------------------------------- /test/sqlite/parser_test.go: -------------------------------------------------------------------------------- 1 | package sqlite 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/sunary/sqlize" 7 | ) 8 | 9 | // TestParserSingleTable tests that Sqlize can parse a sqlite schema with one table. 10 | func TestParserSingleTable(t *testing.T) { 11 | sqlizeCurrent := sqlize.NewSqlize( 12 | sqlize.WithSqlite(), 13 | ) 14 | 15 | schemaSql := readFile(t, schemaWithOneTable) 16 | if err := sqlizeCurrent.FromString(schemaSql); err != nil { 17 | t.Fatalf("failed to parse schema: %v", err) 18 | } 19 | } 20 | 21 | // TestParserMultipleTables tests that Sqlize can parse a sqlite schema with foreign keys. 22 | func TestParserMultipleTables(t *testing.T) { 23 | sqlizeCurrent := sqlize.NewSqlize( 24 | sqlize.WithSqlite(), 25 | ) 26 | 27 | schemaSql := readFile(t, schemaWithTwoTables) 28 | if err := sqlizeCurrent.FromString(schemaSql); err != nil { 29 | t.Fatalf("failed to parse schema: %v", err) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /test/sqlite/testdata/schema_one_table.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS table_with_all_types ( 2 | id INTEGER PRIMARY KEY AUTOINCREMENT, 3 | 4 | name TEXT NOT NULL, 5 | 6 | unique_number INTEGER UNIQUE, 7 | number_with_default INTEGER DEFAULT 123, 8 | 9 | price REAL, 10 | price_with_default REAL DEFAULT 0.0, 11 | 12 | is_active BOOLEAN DEFAULT TRUE, 13 | 14 | binary_data BLOB, 15 | 16 | created_at DATETIME DEFAULT CURRENT_TIMESTAMP, 17 | 18 | age INTEGER CHECK (age >= 18), 19 | 20 | description TEXT DEFAULT "Some description", 21 | 22 | empty_text TEXT DEFAULT "", 23 | single_char_text TEXT DEFAULT "x", 24 | single_quote_escape TEXT DEFAULT "It\'s a test", 25 | backslash_escape TEXT DEFAULT "C:\\Program Files", 26 | newline_escape TEXT DEFAULT "Line1\nLine2", 27 | tab_escape TEXT DEFAULT "Column1\tColumn2", 28 | unicode_escape TEXT DEFAULT "Unicode: \u263A" 29 | ); 30 | 31 | CREATE INDEX IF NOT EXISTS idx_name ON table_with_all_types (name); 32 | 33 | CREATE INDEX IF NOT EXISTS idx_unique_number_is_active ON table_with_all_types (unique_number, is_active); 34 | -------------------------------------------------------------------------------- /test/sqlite/testdata/schema_two_tables.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS table_a ( 2 | id INTEGER PRIMARY KEY AUTOINCREMENT, 3 | name TEXT NOT NULL 4 | ); 5 | 6 | CREATE TABLE IF NOT EXISTS table_b ( 7 | id INTEGER PRIMARY KEY AUTOINCREMENT, 8 | unique_number INTEGER UNIQUE, 9 | a_id INTEGER, 10 | FOREIGN KEY (a_id) REFERENCES table_a(id) 11 | ); 12 | -------------------------------------------------------------------------------- /utils/file.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "strings" 7 | 8 | "github.com/pkg/errors" 9 | ) 10 | 11 | var ( 12 | // PathDoesNotExistErr ... 13 | PathDoesNotExistErr = errors.Errorf("path does not exist") 14 | ) 15 | 16 | // ReadPath ... 17 | func ReadPath(path string, suffix string) ([]string, error) { 18 | files, err := glob(path, suffix) 19 | if err != nil { 20 | return nil, err 21 | } 22 | 23 | contents := make([]string, len(files)) 24 | for i := range files { 25 | content, err := os.ReadFile(files[i]) 26 | if err != nil { 27 | return nil, err 28 | } 29 | 30 | contents[i] = string(content) 31 | } 32 | return contents, nil 33 | } 34 | 35 | // Return a list of SQL files in the listed paths. Only includes files ending 36 | // in .sql. Omits hidden files, directories, and migrations. 37 | func glob(path string, suffix string) ([]string, error) { 38 | f, err := os.Stat(path) 39 | if err != nil { 40 | return nil, PathDoesNotExistErr 41 | } 42 | 43 | files := []string{} 44 | // listing 45 | if f.IsDir() { 46 | listing, err := os.ReadDir(path) 47 | if err != nil { 48 | return nil, err 49 | } 50 | 51 | for _, f := range listing { 52 | files = append(files, filepath.Join(path, f.Name())) 53 | } 54 | } else { 55 | files = append(files, path) 56 | } 57 | 58 | // validate 59 | var sqlFiles []string 60 | for _, file := range files { 61 | if !strings.HasSuffix(file, suffix) { 62 | continue 63 | } 64 | if strings.HasPrefix(filepath.Base(file), ".") { 65 | continue 66 | } 67 | 68 | sqlFiles = append(sqlFiles, file) 69 | } 70 | 71 | return sqlFiles, nil 72 | } 73 | -------------------------------------------------------------------------------- /utils/slc.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | // SlideStrEqual ... 4 | func SlideStrEqual(a, b []string) bool { 5 | if len(a) != len(b) { 6 | return false 7 | } 8 | 9 | for i := range a { 10 | if a[i] != b[i] { 11 | return false 12 | } 13 | } 14 | 15 | return true 16 | } 17 | 18 | // ContainStr ... 19 | func ContainStr(ss []string, s string) bool { 20 | for i := range ss { 21 | if s == ss[i] { 22 | return true 23 | } 24 | } 25 | return false 26 | } 27 | -------------------------------------------------------------------------------- /utils/str.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "strings" 7 | "time" 8 | ) 9 | 10 | const ( 11 | defaultMigrationTimeFormat = "20060102150405" 12 | // DefaultMigrationFolder ... 13 | DefaultMigrationFolder = "migrations/" 14 | // DefaultMigrationUpSuffix ... 15 | DefaultMigrationUpSuffix = ".up.sql" 16 | // DefaultMigrationDownSuffix ... 17 | DefaultMigrationDownSuffix = ".down.sql" 18 | // DefaultMigrationTable ... 19 | DefaultMigrationTable = "schema_migrations" 20 | ) 21 | 22 | // MigrationFileName ... 23 | func MigrationFileName(name string) string { 24 | re, _ := regexp.Compile(`[^\w\d\s-_]`) 25 | name = strings.ToLower(re.ReplaceAllString(name, "")) 26 | name = strings.Replace(name, " ", " ", -1) 27 | name = strings.Replace(name, " ", "_", -1) 28 | name = strings.Replace(name, "-", "_", -1) 29 | return fmt.Sprintf("%s_%s", time.Now().Format(defaultMigrationTimeFormat), name) 30 | } 31 | 32 | // ToSnakeCase ... 33 | func ToSnakeCase(input string) string { 34 | var sb strings.Builder 35 | var upperCount int 36 | for i, c := range input { 37 | switch { 38 | case isUppercase(c): 39 | if i > 0 && (upperCount == 0 || nextIsLower(input, i)) { 40 | sb.WriteByte('_') 41 | } 42 | sb.WriteByte(byte(c - 'A' + 'a')) 43 | upperCount++ 44 | 45 | case isLowercase(c): 46 | sb.WriteByte(byte(c)) 47 | upperCount = 0 48 | 49 | case isDigit(c): 50 | sb.WriteByte(byte(c)) 51 | 52 | default: 53 | sb.WriteByte(byte(c)) 54 | } 55 | } 56 | 57 | return sb.String() 58 | } 59 | 60 | // nextIsLower The next character is lower case, but not the last 's'. 61 | // nextIsLower("HTMLFile", 1) expected: "html_file" 62 | // => true 63 | // nextIsLower("URLs", -1) expected: "urls" 64 | // => false 65 | func nextIsLower(input string, i int) bool { 66 | i++ 67 | if i >= len(input) { 68 | return false 69 | } 70 | 71 | c := input[i] 72 | if c == 's' && i == len(input)-1 { 73 | return false 74 | } 75 | 76 | return isLowercase(rune(c)) 77 | } 78 | 79 | func isDigit(r rune) bool { 80 | return r >= '0' && r <= '9' 81 | } 82 | 83 | func isLowercase(r rune) bool { 84 | return r >= 'a' && r <= 'z' 85 | } 86 | 87 | func isUppercase(r rune) bool { 88 | return r >= 'A' && r <= 'Z' 89 | } 90 | --------------------------------------------------------------------------------