Skip to content

Commit

Permalink
Export indexes and foreign keys
Browse files Browse the repository at this point in the history
  • Loading branch information
Sergey Podgornyy committed Aug 7, 2020
1 parent a389afa commit d5ba72c
Show file tree
Hide file tree
Showing 10 changed files with 158 additions and 117 deletions.
45 changes: 45 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down Expand Up @@ -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 |
+----+-------------------------------------+-------+----------------------------+
```
Expand Down
36 changes: 21 additions & 15 deletions foreign.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import (
"strings"
)

type foreigns []foreign
type foreigns []Foreign

func (f foreigns) render() string {
values := []string{}
Expand All @@ -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
Expand Down
24 changes: 14 additions & 10 deletions foreign_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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"))
}
30 changes: 18 additions & 12 deletions key.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package migrator

import "strings"

type keys []key
type keys []Key

func (k keys) render() string {
values := []string{}
Expand All @@ -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"
}
28 changes: 19 additions & 9 deletions key_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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"))
})
}
24 changes: 12 additions & 12 deletions schema_command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand All @@ -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}
Expand Down Expand Up @@ -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",
Expand Down
Loading

0 comments on commit d5ba72c

Please sign in to comment.