diff --git a/README.md b/README.md index c5e851c..7b71030 100644 --- a/README.md +++ b/README.md @@ -84,6 +84,50 @@ var migrations = []migrator.Migration{ return s }, }, + { + Name: "19700101_0003_rename_foreign_key", + Up: func() migrator.Schema { + var s migrator.Schema + + keyName := migrator.BuildForeignNameOnTable("comments", "post_id") + newKeyName := migrator.BuildForeignNameOnTable("comments", "article_id") + + s.AlterTable("comments", migrator.TableCommands{ + migrator.DropForeignCommand(keyName), + migrator.DropIndexCommand(keyName), + migrator.RenameColumnCommand{"post_id", "article_id"}, + migrator.AddIndexCommand{newKeyName, []string{"article_id"}}, + migrator.AddForeignCommand{migrator.Foreign{ + Key: newKeyName, + Column: "article_id", + Reference: "id", + On: "posts", + }}, + }) + + return s + }, + Down: func() migrator.Schema { + var s migrator.Schema + + keyName := migrator.BuildForeignNameOnTable("comments", "article_id") + newKeyName := migrator.BuildForeignNameOnTable("comments", "post_id") + + s.AlterTable("comments", migrator.TableCommands{ + migrator.DropForeignCommand(keyName), + migrator.DropIndexCommand(keyName), + migrator.RenameColumnCommand{"article_id", "post_id"}, + migrator.AddIndexCommand{newKeyName, []string{"post_id"}}, + migrator.AddForeignCommand{migrator.Foreign{ + Key: newKeyName, + Column: "post_id", + Reference: "id", + On: "posts", + }}, + }) + + return s + }, } m := migrator.Migrator{Pool: migrations} @@ -113,6 +157,7 @@ After the first migration run, `migrations` table will be created: +----+-------------------------------------+-------+----------------------------+ | 1 | 19700101_0001_create_posts_table | 1 | 2020-06-27 00:00:00.000000 | | 2 | 19700101_0002_create_comments_table | 1 | 2020-06-27 00:00:00.000000 | +| 3 | 19700101_0003_rename_foreign_key | 1 | 2020-06-27 00:00:00.000000 | +----+-------------------------------------+-------+----------------------------+ ``` diff --git a/foreign.go b/foreign.go index 768e474..f94816b 100644 --- a/foreign.go +++ b/foreign.go @@ -5,7 +5,7 @@ import ( "strings" ) -type foreigns []foreign +type foreigns []Foreign func (f foreigns) render() string { values := []string{} @@ -17,31 +17,37 @@ func (f foreigns) render() string { return strings.Join(values, ", ") } -type foreign struct { - key string - column string - reference string // reference field - on string // reference table - onUpdate string - onDelete string +// Foreign represents an instance to handle foreign key interactions +type Foreign struct { + Key string + Column string + Reference string // reference field + On string // reference table + OnUpdate string + OnDelete string } -func (f foreign) render() string { - if f.key == "" || f.column == "" || f.on == "" || f.reference == "" { +func (f Foreign) render() string { + if f.Key == "" || f.Column == "" || f.On == "" || f.Reference == "" { return "" } - sql := fmt.Sprintf("CONSTRAINT `%s` FOREIGN KEY (`%s`) REFERENCES `%s` (`%s`)", f.key, f.column, f.on, f.reference) - if referenceOptions.has(strings.ToUpper(f.onDelete)) { - sql += " ON DELETE " + strings.ToUpper(f.onDelete) + sql := fmt.Sprintf("CONSTRAINT `%s` FOREIGN KEY (`%s`) REFERENCES `%s` (`%s`)", f.Key, f.Column, f.On, f.Reference) + if referenceOptions.has(strings.ToUpper(f.OnDelete)) { + sql += " ON DELETE " + strings.ToUpper(f.OnDelete) } - if referenceOptions.has(strings.ToUpper(f.onUpdate)) { - sql += " ON UPDATE " + strings.ToUpper(f.onUpdate) + if referenceOptions.has(strings.ToUpper(f.OnUpdate)) { + sql += " ON UPDATE " + strings.ToUpper(f.OnUpdate) } return sql } +// BuildForeignNameOnTable builds a name for the foreign key on the table +func BuildForeignNameOnTable(table string, column string) string { + return table + "_" + column + "_foreign" +} + var referenceOptions = list{"SET NULL", "CASCADE", "RESTRICT", "NO ACTION", "SET DEFAULT"} type list []string diff --git a/foreign_test.go b/foreign_test.go index 09836ed..676c61d 100644 --- a/foreign_test.go +++ b/foreign_test.go @@ -8,21 +8,21 @@ import ( func TestForeigns(t *testing.T) { t.Run("it returns empty on empty keys", func(t *testing.T) { - f := foreigns{foreign{}} + f := foreigns{Foreign{}} assert.Equal(t, "", f.render()) }) t.Run("it renders row from one foreign", func(t *testing.T) { - f := foreigns{foreign{key: "idx_foreign", column: "test_id", reference: "id", on: "tests"}} + f := foreigns{Foreign{Key: "idx_foreign", Column: "test_id", Reference: "id", On: "tests"}} assert.Equal(t, "CONSTRAINT `idx_foreign` FOREIGN KEY (`test_id`) REFERENCES `tests` (`id`)", f.render()) }) t.Run("it renders row from multiple foreigns", func(t *testing.T) { f := foreigns{ - foreign{key: "idx_foreign", column: "test_id", reference: "id", on: "tests"}, - foreign{key: "foreign_idx", column: "random_id", reference: "id", on: "randoms"}, + Foreign{Key: "idx_foreign", Column: "test_id", Reference: "id", On: "tests"}, + Foreign{Key: "foreign_idx", Column: "random_id", Reference: "id", On: "randoms"}, } assert.Equal( @@ -35,38 +35,42 @@ func TestForeigns(t *testing.T) { func TestForeign(t *testing.T) { t.Run("it builds base constraint", func(t *testing.T) { - f := foreign{key: "foreign_idx", column: "test_id", reference: "id", on: "tests"} + f := Foreign{Key: "foreign_idx", Column: "test_id", Reference: "id", On: "tests"} assert.Equal(t, "CONSTRAINT `foreign_idx` FOREIGN KEY (`test_id`) REFERENCES `tests` (`id`)", f.render()) }) t.Run("it builds contraint with on_update", func(t *testing.T) { - f := foreign{key: "foreign_idx", column: "test_id", reference: "id", on: "tests", onUpdate: "no action"} + f := Foreign{Key: "foreign_idx", Column: "test_id", Reference: "id", On: "tests", OnUpdate: "no action"} assert.Equal(t, "CONSTRAINT `foreign_idx` FOREIGN KEY (`test_id`) REFERENCES `tests` (`id`) ON UPDATE NO ACTION", f.render()) }) t.Run("it builds contraint without invalid on_update", func(t *testing.T) { - f := foreign{key: "foreign_idx", column: "test_id", reference: "id", on: "tests", onUpdate: "null"} + f := Foreign{Key: "foreign_idx", Column: "test_id", Reference: "id", On: "tests", OnUpdate: "null"} assert.Equal(t, "CONSTRAINT `foreign_idx` FOREIGN KEY (`test_id`) REFERENCES `tests` (`id`)", f.render()) }) t.Run("it builds contraint with on_update", func(t *testing.T) { - f := foreign{key: "foreign_idx", column: "test_id", reference: "id", on: "tests", onDelete: "set default"} + f := Foreign{Key: "foreign_idx", Column: "test_id", Reference: "id", On: "tests", OnDelete: "set default"} assert.Equal(t, "CONSTRAINT `foreign_idx` FOREIGN KEY (`test_id`) REFERENCES `tests` (`id`) ON DELETE SET DEFAULT", f.render()) }) t.Run("it builds contraint without invalid on_update", func(t *testing.T) { - f := foreign{key: "foreign_idx", column: "test_id", reference: "id", on: "tests", onDelete: "default"} + f := Foreign{Key: "foreign_idx", Column: "test_id", Reference: "id", On: "tests", OnDelete: "default"} assert.Equal(t, "CONSTRAINT `foreign_idx` FOREIGN KEY (`test_id`) REFERENCES `tests` (`id`)", f.render()) }) t.Run("it builds full contraint", func(t *testing.T) { - f := foreign{key: "foreign_idx", column: "test_id", reference: "id", on: "tests", onUpdate: "cascade", onDelete: "restrict"} + f := Foreign{Key: "foreign_idx", Column: "test_id", Reference: "id", On: "tests", OnUpdate: "cascade", OnDelete: "restrict"} assert.Equal(t, "CONSTRAINT `foreign_idx` FOREIGN KEY (`test_id`) REFERENCES `tests` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE", f.render()) }) } + +func TestBuildForeignIndexNameOnTable(t *testing.T) { + assert.Equal(t, "table_test_foreign", BuildForeignNameOnTable("table", "test")) +} diff --git a/key.go b/key.go index 3d65dcb..0de1839 100644 --- a/key.go +++ b/key.go @@ -2,7 +2,7 @@ package migrator import "strings" -type keys []key +type keys []Key func (k keys) render() string { values := []string{} @@ -17,31 +17,37 @@ func (k keys) render() string { return strings.Join(values, ", ") } -type key struct { - name string - typ string // primary, unique - columns []string +// Key represents an instance to handle key (index) interactions +type Key struct { + Name string + Type string // primary, unique + Columns []string } var keyTypes = list{"PRIMARY", "UNIQUE"} -func (k key) render() string { - if len(k.columns) == 0 { +func (k Key) render() string { + if len(k.Columns) == 0 { return "" } sql := "" - if keyTypes.has(strings.ToUpper(k.typ)) { - sql += strings.ToUpper(k.typ) + " " + if keyTypes.has(strings.ToUpper(k.Type)) { + sql += strings.ToUpper(k.Type) + " " } sql += "KEY" - if k.name != "" { - sql += " `" + k.name + "`" + if k.Name != "" { + sql += " `" + k.Name + "`" } - sql += " (`" + strings.Join(k.columns, "`, `") + "`)" + sql += " (`" + strings.Join(k.Columns, "`, `") + "`)" return sql } + +// BuildUniqueKeyNameOnTable builds a name for the foreign key on the table +func BuildUniqueKeyNameOnTable(table string, columns ...string) string { + return table + "_" + strings.Join(columns, "_") + "_unique" +} diff --git a/key_test.go b/key_test.go index 1cdc950..5b26cf4 100644 --- a/key_test.go +++ b/key_test.go @@ -8,21 +8,21 @@ import ( func TestKeys(t *testing.T) { t.Run("it returns empty on empty keys", func(t *testing.T) { - k := keys{key{}} + k := keys{Key{}} assert.Equal(t, "", k.render()) }) t.Run("it renders row from one key", func(t *testing.T) { - k := keys{key{columns: []string{"test_id"}}} + k := keys{Key{Columns: []string{"test_id"}}} assert.Equal(t, "KEY (`test_id`)", k.render()) }) t.Run("it renders row from multiple keys", func(t *testing.T) { k := keys{ - key{columns: []string{"test_id"}}, - key{columns: []string{"random_id"}}, + Key{Columns: []string{"test_id"}}, + Key{Columns: []string{"random_id"}}, } assert.Equal( @@ -35,32 +35,42 @@ func TestKeys(t *testing.T) { func TestKey(t *testing.T) { t.Run("it returns empty on empty keys", func(t *testing.T) { - k := key{} + k := Key{} assert.Equal(t, "", k.render()) }) t.Run("it skips type if it is not in valid list", func(t *testing.T) { - k := key{typ: "random", columns: []string{"test_id"}} + k := Key{Type: "random", Columns: []string{"test_id"}} assert.Equal(t, "KEY (`test_id`)", k.render()) }) t.Run("it renders with type", func(t *testing.T) { - k := key{typ: "primary", columns: []string{"test_id"}} + k := Key{Type: "primary", Columns: []string{"test_id"}} assert.Equal(t, "PRIMARY KEY (`test_id`)", k.render()) }) t.Run("it renders with multiple columns", func(t *testing.T) { - k := key{typ: "unique", columns: []string{"test_id", "random_id"}} + k := Key{Type: "unique", Columns: []string{"test_id", "random_id"}} assert.Equal(t, "UNIQUE KEY (`test_id`, `random_id`)", k.render()) }) t.Run("it renders with name", func(t *testing.T) { - k := key{name: "random_idx", columns: []string{"test_id"}} + k := Key{Name: "random_idx", Columns: []string{"test_id"}} assert.Equal(t, "KEY `random_idx` (`test_id`)", k.render()) }) } + +func TestBuildUniqueIndexName(t *testing.T) { + t.Run("It builds name from one column", func(t *testing.T) { + assert.Equal(t, "table_test_unique", BuildUniqueKeyNameOnTable("table", "test")) + }) + + t.Run("it builds name from multiple columns", func(t *testing.T) { + assert.Equal(t, "table_test_again_unique", BuildUniqueKeyNameOnTable("table", "test", "again")) + }) +} diff --git a/schema_command_test.go b/schema_command_test.go index 1e3b063..b023a1e 100644 --- a/schema_command_test.go +++ b/schema_command_test.go @@ -52,9 +52,9 @@ func TestCreateTableCommand(t *testing.T) { t.Run("it renders indexes", func(t *testing.T) { tb := Table{ Name: "test", - indexes: []key{ - {name: "idx_rand", columns: []string{"id"}}, - {columns: []string{"id", "name"}}, + indexes: []Key{ + {Name: "idx_rand", Columns: []string{"id"}}, + {Columns: []string{"id", "name"}}, }, } c := createTableCommand{tb} @@ -74,9 +74,9 @@ func TestCreateTableCommand(t *testing.T) { t.Run("it renders foreigns", func(t *testing.T) { tb := Table{ Name: "test", - foreigns: []foreign{ - {key: "idx_foreign", column: "test_id", reference: "id", on: "tests"}, - {key: "foreign_idx", column: "random_id", reference: "id", on: "randoms"}, + foreigns: []Foreign{ + {Key: "idx_foreign", Column: "test_id", Reference: "id", On: "tests"}, + {Key: "foreign_idx", Column: "random_id", Reference: "id", On: "randoms"}, }, } c := createTableCommand{tb} @@ -145,13 +145,13 @@ func TestCreateTableCommand(t *testing.T) { {"test", testColumnType("random thing")}, {"random", testColumnType("another thing")}, }, - indexes: []key{ - {name: "idx_rand", columns: []string{"id"}}, - {columns: []string{"id", "name"}}, + indexes: []Key{ + {Name: "idx_rand", Columns: []string{"id"}}, + {Columns: []string{"id", "name"}}, }, - foreigns: []foreign{ - {key: "idx_foreign", column: "test_id", reference: "id", on: "tests"}, - {key: "foreign_idx", column: "random_id", reference: "id", on: "randoms"}, + foreigns: []Foreign{ + {Key: "idx_foreign", Column: "test_id", Reference: "id", On: "tests"}, + {Key: "foreign_idx", Column: "random_id", Reference: "id", On: "randoms"}, }, Engine: "MyISAM", Charset: "rand", diff --git a/table.go b/table.go index 409a955..f643ea3 100644 --- a/table.go +++ b/table.go @@ -1,7 +1,5 @@ package migrator -import "strings" - // Table is an entity to create a table. // // - Name table name @@ -177,9 +175,9 @@ func (t *Table) Primary(columns ...string) { return } - t.indexes = append(t.indexes, key{ - typ: "primary", - columns: columns, + t.indexes = append(t.indexes, Key{ + Type: "primary", + Columns: columns, }) } @@ -189,10 +187,10 @@ func (t *Table) Unique(columns ...string) { return } - t.indexes = append(t.indexes, key{ - name: t.buildUniqueKeyName(columns...), - typ: "unique", - columns: columns, + t.indexes = append(t.indexes, Key{ + Name: BuildUniqueKeyNameOnTable(t.Name, columns...), + Type: "unique", + Columns: columns, }) } @@ -202,30 +200,22 @@ func (t *Table) Index(name string, columns ...string) { return } - t.indexes = append(t.indexes, key{name: name, columns: columns}) + t.indexes = append(t.indexes, Key{Name: name, Columns: columns}) } // Foreign adds foreign key constraints func (t *Table) Foreign(column string, reference string, on string, onUpdate string, onDelete string) { - name := t.buildForeignKeyName(column) - t.indexes = append(t.indexes, key{ - name: name, - columns: []string{column}, + name := BuildForeignNameOnTable(t.Name, column) + t.indexes = append(t.indexes, Key{ + Name: name, + Columns: []string{column}, }) - t.foreigns = append(t.foreigns, foreign{ - key: name, - column: column, - reference: reference, - on: on, - onUpdate: onUpdate, - onDelete: onDelete, + t.foreigns = append(t.foreigns, Foreign{ + Key: name, + Column: column, + Reference: reference, + On: on, + OnUpdate: onUpdate, + OnDelete: onDelete, }) } - -func (t *Table) buildUniqueKeyName(columns ...string) string { - return t.Name + "_" + strings.Join(columns, "_") + "_unique" -} - -func (t *Table) buildForeignKeyName(column string) string { - return t.Name + "_" + column + "_foreign" -} diff --git a/table_command.go b/table_command.go index 82758e9..2ccd5dc 100644 --- a/table_command.go +++ b/table_command.go @@ -148,7 +148,7 @@ func (c DropIndexCommand) toSQL() string { // AddForeignCommand adds the foreign key constraint to the table. type AddForeignCommand struct { - Foreign foreign + Foreign Foreign } func (c AddForeignCommand) toSQL() string { diff --git a/table_command_test.go b/table_command_test.go index ae63677..78a0b55 100644 --- a/table_command_test.go +++ b/table_command_test.go @@ -169,7 +169,7 @@ func TestAddForeignCommand(t *testing.T) { }) t.Run("it builds a proper row", func(t *testing.T) { - c := AddForeignCommand{foreign{key: "idx_foreign", column: "test_id", reference: "id", on: "tests"}} + c := AddForeignCommand{Foreign{Key: "idx_foreign", Column: "test_id", Reference: "id", On: "tests"}} assert.Equal(t, "ADD CONSTRAINT `idx_foreign` FOREIGN KEY (`test_id`) REFERENCES `tests` (`id`)", c.toSQL()) }) } diff --git a/table_test.go b/table_test.go index 08056da..08262a0 100644 --- a/table_test.go +++ b/table_test.go @@ -33,7 +33,7 @@ func TestIDColumn(t *testing.T) { assert.Equal("id", table.columns[0].field) assert.Equal(Integer{Prefix: "big", Unsigned: true, Autoincrement: true}, table.columns[0].definition) assert.Len(table.indexes, 1) - assert.Equal(key{typ: "primary", columns: []string{"id"}}, table.indexes[0]) + assert.Equal(Key{Type: "primary", Columns: []string{"id"}}, table.indexes[0]) } func TestUniqueIDColumn(t *testing.T) { @@ -49,7 +49,7 @@ func TestUniqueIDColumn(t *testing.T) { assert.Equal("id", table.columns[0].field) assert.Equal(String{Default: "(UUID())", Fixed: true, Precision: 36}, table.columns[0].definition) assert.Len(table.indexes, 1) - assert.Equal(key{typ: "primary", columns: []string{"id"}}, table.indexes[0]) + assert.Equal(Key{Type: "primary", Columns: []string{"id"}}, table.indexes[0]) } func TestBinaryID(t *testing.T) { @@ -65,7 +65,7 @@ func TestBinaryID(t *testing.T) { assert.Equal("id", table.columns[0].field) assert.Equal(Binary{Default: "(UUID_TO_BIN(UUID()))", Fixed: true, Precision: 16}, table.columns[0].definition) assert.Len(table.indexes, 1) - assert.Equal(key{typ: "primary", columns: []string{"id"}}, table.indexes[0]) + assert.Equal(Key{Type: "primary", Columns: []string{"id"}}, table.indexes[0]) } func TestBooleanColumn(t *testing.T) { @@ -351,7 +351,7 @@ func TestTablePrimaryIndex(t *testing.T) { table.Primary("id", "name") assert.Len(table.indexes, 1) - assert.Equal(key{typ: "primary", columns: []string{"id", "name"}}, table.indexes[0]) + assert.Equal(Key{Type: "primary", Columns: []string{"id", "name"}}, table.indexes[0]) }) } @@ -376,7 +376,7 @@ func TestTableUniqueIndex(t *testing.T) { table.Unique("id", "name") assert.Len(table.indexes, 1) - assert.Equal(key{name: "table_id_name_unique", typ: "unique", columns: []string{"id", "name"}}, table.indexes[0]) + assert.Equal(Key{Name: "table_id_name_unique", Type: "unique", Columns: []string{"id", "name"}}, table.indexes[0]) }) } @@ -401,7 +401,7 @@ func TestTableIndex(t *testing.T) { table.Index("test_idx", "id", "name") assert.Len(table.indexes, 1) - assert.Equal(key{name: "test_idx", columns: []string{"id", "name"}}, table.indexes[0]) + assert.Equal(Key{Name: "test_idx", Columns: []string{"id", "name"}}, table.indexes[0]) }) } @@ -415,30 +415,10 @@ func TestTableForeignIndex(t *testing.T) { table.Foreign("test_id", "id", "tests", "set null", "cascade") assert.Len(table.indexes, 1) - assert.Equal(key{name: "table_test_id_foreign", columns: []string{"test_id"}}, table.indexes[0]) + assert.Equal(Key{Name: "table_test_id_foreign", Columns: []string{"test_id"}}, table.indexes[0]) assert.Len(table.foreigns, 1) assert.Equal( - foreign{key: "table_test_id_foreign", column: "test_id", reference: "id", on: "tests", onUpdate: "set null", onDelete: "cascade"}, + Foreign{Key: "table_test_id_foreign", Column: "test_id", Reference: "id", On: "tests", OnUpdate: "set null", OnDelete: "cascade"}, table.foreigns[0], ) } - -func TestBuildUniqueIndexName(t *testing.T) { - t.Run("It builds name from one column", func(t *testing.T) { - table := Table{Name: "table"} - - assert.Equal(t, "table_test_unique", table.buildUniqueKeyName("test")) - }) - - t.Run("it builds name from multiple columns", func(t *testing.T) { - table := Table{Name: "table"} - - assert.Equal(t, "table_test_again_unique", table.buildUniqueKeyName("test", "again")) - }) -} - -func TestBuildForeignIndexName(t *testing.T) { - table := Table{Name: "table"} - - assert.Equal(t, "table_test_foreign", table.buildForeignKeyName("test")) -}