mirror of
https://github.com/ent/ent.git
synced 2026-05-22 09:31:45 +03:00
ent/sql/migrate: support indexes
Reviewed By: alexsn Differential Revision: D16711184 fbshipit-source-id: 632b02c5c77c6289b242263647d45d9f28752e3f
This commit is contained in:
committed by
Facebook Github Bot
parent
933fe91741
commit
329b5ddf77
@@ -168,7 +168,10 @@ func (c *ColumnBuilder) Attr(a string) *ColumnBuilder {
|
||||
|
||||
// Query returns query representation of a Column.
|
||||
func (c *ColumnBuilder) Query() (string, []interface{}) {
|
||||
c.b.Append(c.name).Pad().WriteString(c.typ)
|
||||
c.b.Append(c.name)
|
||||
if c.typ != "" {
|
||||
c.b.Pad().WriteString(c.typ)
|
||||
}
|
||||
if c.attr != "" {
|
||||
c.b.Pad().WriteString(c.attr)
|
||||
}
|
||||
@@ -335,6 +338,12 @@ func (t *TableAlter) ModifyColumn(c *ColumnBuilder) *TableAlter {
|
||||
return t
|
||||
}
|
||||
|
||||
// DropColumn appends the `DROP COLUMN` clause to the given `ALTER TABLE` statement.
|
||||
func (t *TableAlter) DropColumn(c *ColumnBuilder) *TableAlter {
|
||||
t.Queriers = append(t.Queriers, &Wrapper{"DROP COLUMN %s", c})
|
||||
return t
|
||||
}
|
||||
|
||||
// AddForeignKey adds a foreign key constraint to the `ALTER TABLE` statement.
|
||||
func (t *TableAlter) AddForeignKey(fk *ForeignKeyBuilder) *TableAlter {
|
||||
t.Queriers = append(t.Queriers, &Wrapper{"ADD CONSTRAINT %s", fk})
|
||||
@@ -456,6 +465,111 @@ func (r *ReferenceBuilder) Query() (string, []interface{}) {
|
||||
return r.b.String(), r.b.args
|
||||
}
|
||||
|
||||
// IndexBuilder is a builder for `CREATE INDEX` statement.
|
||||
type IndexBuilder struct {
|
||||
b Builder
|
||||
name string
|
||||
unique bool
|
||||
table string
|
||||
columns []string
|
||||
}
|
||||
|
||||
// CreateIndex creates a builder for the `CREATE INDEX` statement.
|
||||
//
|
||||
// CreateIndex("index_name").
|
||||
// Unique().
|
||||
// Table("users").
|
||||
// Column("name")
|
||||
//
|
||||
// Or:
|
||||
//
|
||||
// CreateIndex("index_name").
|
||||
// Unique().
|
||||
// Table("users").
|
||||
// Columns("name", "age")
|
||||
//
|
||||
func CreateIndex(name string) *IndexBuilder {
|
||||
return &IndexBuilder{name: name}
|
||||
}
|
||||
|
||||
// Unique sets the index to be a unique index.
|
||||
func (i *IndexBuilder) Unique() *IndexBuilder {
|
||||
i.unique = true
|
||||
return i
|
||||
}
|
||||
|
||||
// Table defines the table for the index.
|
||||
func (i *IndexBuilder) Table(table string) *IndexBuilder {
|
||||
i.table = table
|
||||
return i
|
||||
}
|
||||
|
||||
// Column appends a column to the column list for the index.
|
||||
func (i *IndexBuilder) Column(column string) *IndexBuilder {
|
||||
i.columns = append(i.columns, column)
|
||||
return i
|
||||
}
|
||||
|
||||
// Columns appends the given columns to the column list for the index.
|
||||
func (i *IndexBuilder) Columns(columns ...string) *IndexBuilder {
|
||||
i.columns = append(i.columns, columns...)
|
||||
return i
|
||||
}
|
||||
|
||||
// Query returns query representation of a reference clause.
|
||||
func (i *IndexBuilder) Query() (string, []interface{}) {
|
||||
i.b.WriteString("CREATE ")
|
||||
if i.unique {
|
||||
i.b.WriteString("UNIQUE ")
|
||||
}
|
||||
i.b.WriteString("INDEX ")
|
||||
i.b.Append(i.name)
|
||||
i.b.WriteString(" ON ")
|
||||
i.b.Append(i.table).Nested(func(b *Builder) {
|
||||
b.AppendComma(i.columns...)
|
||||
})
|
||||
return i.b.String(), nil
|
||||
}
|
||||
|
||||
// DropIndexBuilder is a builder for `DROP INDEX` statement.
|
||||
type DropIndexBuilder struct {
|
||||
b Builder
|
||||
name string
|
||||
table string
|
||||
}
|
||||
|
||||
// DropIndex creates a builder for the `DROP INDEX` statement.
|
||||
//
|
||||
// MySQL:
|
||||
//
|
||||
// DropIndex("index_name").
|
||||
// Table("users").
|
||||
//
|
||||
// SQLite/PostgreSQL:
|
||||
//
|
||||
// DropIndex("index_name")
|
||||
//
|
||||
func DropIndex(name string) *DropIndexBuilder {
|
||||
return &DropIndexBuilder{name: name}
|
||||
}
|
||||
|
||||
// Table defines the table for the index.
|
||||
func (d *DropIndexBuilder) Table(table string) *DropIndexBuilder {
|
||||
d.table = table
|
||||
return d
|
||||
}
|
||||
|
||||
// Query returns query representation of a reference clause.
|
||||
func (d *DropIndexBuilder) Query() (string, []interface{}) {
|
||||
d.b.WriteString("DROP INDEX ")
|
||||
d.b.Append(d.name)
|
||||
if d.table != "" {
|
||||
d.b.WriteString(" ON ")
|
||||
d.b.Append(d.table)
|
||||
}
|
||||
return d.b.String(), nil
|
||||
}
|
||||
|
||||
// InsertBuilder is a builder for `INSERT INTO` statement.
|
||||
type InsertBuilder struct {
|
||||
b Builder
|
||||
|
||||
@@ -106,6 +106,12 @@ func TestBuilder(t *testing.T) {
|
||||
ModifyColumn(Column("age").Type("int")),
|
||||
wantQuery: "ALTER TABLE `users` MODIFY COLUMN `age` int",
|
||||
},
|
||||
{
|
||||
input: AlterTable("users").
|
||||
ModifyColumn(Column("age").Type("int")).
|
||||
DropColumn(Column("name")),
|
||||
wantQuery: "ALTER TABLE `users` MODIFY COLUMN `age` int, DROP COLUMN `name`",
|
||||
},
|
||||
{
|
||||
input: Insert("users").Columns("age").Values(1),
|
||||
wantQuery: "INSERT INTO `users` (`age`) VALUES (?)",
|
||||
@@ -424,6 +430,22 @@ func TestBuilder(t *testing.T) {
|
||||
wantQuery: "WITH groups AS (SELECT * FROM `groups` WHERE `name` = ?) SELECT `age` FROM `groups`",
|
||||
wantArgs: []interface{}{"bar"},
|
||||
},
|
||||
{
|
||||
input: CreateIndex("name_index").Table("users").Column("name"),
|
||||
wantQuery: "CREATE INDEX `name_index` ON `users`(`name`)",
|
||||
},
|
||||
{
|
||||
input: CreateIndex("unique_name").Unique().Table("users").Columns("first", "last"),
|
||||
wantQuery: "CREATE UNIQUE INDEX `unique_name` ON `users`(`first`, `last`)",
|
||||
},
|
||||
{
|
||||
input: DropIndex("name_index"),
|
||||
wantQuery: "DROP INDEX `name_index`",
|
||||
},
|
||||
{
|
||||
input: DropIndex("name_index").Table("users"),
|
||||
wantQuery: "DROP INDEX `name_index` ON `users`",
|
||||
},
|
||||
}
|
||||
for i, tt := range tests {
|
||||
t.Run(strconv.Itoa(i), func(t *testing.T) {
|
||||
|
||||
@@ -24,16 +24,35 @@ const (
|
||||
type MigrateOption func(m *Migrate)
|
||||
|
||||
// WithGlobalUniqueID sets the universal ids options to the migration.
|
||||
// Defaults to false.
|
||||
func WithGlobalUniqueID(b bool) MigrateOption {
|
||||
return func(o *Migrate) {
|
||||
o.universalID = b
|
||||
return func(m *Migrate) {
|
||||
m.universalID = b
|
||||
}
|
||||
}
|
||||
|
||||
// WithDropColumn sets the columns dropping option to the migration.
|
||||
// Defaults to false.
|
||||
func WithDropColumn(b bool) MigrateOption {
|
||||
return func(m *Migrate) {
|
||||
m.dropColumn = b
|
||||
}
|
||||
}
|
||||
|
||||
// WithDropIndex sets the indexes dropping option to the migration.
|
||||
// Defaults to false.
|
||||
func WithDropIndex(b bool) MigrateOption {
|
||||
return func(m *Migrate) {
|
||||
m.dropIndex = b
|
||||
}
|
||||
}
|
||||
|
||||
// Migrate runs the migrations logic for the SQL dialects.
|
||||
type Migrate struct {
|
||||
sqlDialect
|
||||
universalID bool // global unique id flag.
|
||||
universalID bool // global unique ids.
|
||||
dropColumn bool // drop deleted columns.
|
||||
dropIndex bool // drop deleted indexes.
|
||||
typeRanges []string // types order by their range.
|
||||
}
|
||||
|
||||
@@ -96,21 +115,8 @@ func (m *Migrate) create(ctx context.Context, tx dialect.Tx, tables ...*Table) e
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(change.add) != 0 || len(change.modify) != 0 {
|
||||
b := sql.AlterTable(curr.Name)
|
||||
for _, c := range change.add {
|
||||
b.AddColumn(m.cBuilder(c))
|
||||
}
|
||||
for _, c := range change.modify {
|
||||
b.ModifyColumn(m.cBuilder(c))
|
||||
}
|
||||
query, args := b.Query()
|
||||
if err := tx.Exec(ctx, query, args, new(sql.Result)); err != nil {
|
||||
return fmt.Errorf("alter table %q: %v", t.Name, err)
|
||||
}
|
||||
}
|
||||
if len(change.indexes) > 0 {
|
||||
panic("missing implementation")
|
||||
if err := m.apply(ctx, tx, t.Name, change); err != nil {
|
||||
return err
|
||||
}
|
||||
default: // !exist
|
||||
query, args := m.tBuilder(t).Query()
|
||||
@@ -158,11 +164,60 @@ func (m *Migrate) create(ctx context.Context, tx dialect.Tx, tables ...*Table) e
|
||||
return nil
|
||||
}
|
||||
|
||||
// apply applies changes on the given table.
|
||||
func (m *Migrate) apply(ctx context.Context, tx dialect.Tx, table string, change *changes) error {
|
||||
// constraints should be dropped before dropping columns, because if a column
|
||||
// is a part of multi-column constraints (like, unique index), ALTER TABLE
|
||||
// might fail if the intermediate state violates the constraints.
|
||||
if m.dropIndex {
|
||||
for _, idx := range change.index.drop {
|
||||
query, args := idx.DropBuilder(table).Query()
|
||||
if err := tx.Exec(ctx, query, args, new(sql.Result)); err != nil {
|
||||
return fmt.Errorf("drop index %q: %v", table, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
b := sql.AlterTable(table)
|
||||
for _, c := range change.column.add {
|
||||
b.AddColumn(m.cBuilder(c))
|
||||
}
|
||||
for _, c := range change.column.modify {
|
||||
b.ModifyColumn(m.cBuilder(c))
|
||||
}
|
||||
if m.dropColumn {
|
||||
for _, c := range change.column.drop {
|
||||
b.DropColumn(sql.Column(c.Name))
|
||||
}
|
||||
}
|
||||
// if there's actual action to execute on ALTER TABLE.
|
||||
if len(b.Queriers) != 0 {
|
||||
query, args := b.Query()
|
||||
if err := tx.Exec(ctx, query, args, new(sql.Result)); err != nil {
|
||||
return fmt.Errorf("alter table %q: %v", table, err)
|
||||
}
|
||||
}
|
||||
for _, idx := range change.index.add {
|
||||
query, args := idx.Builder(table).Query()
|
||||
if err := tx.Exec(ctx, query, args, new(sql.Result)); err != nil {
|
||||
return fmt.Errorf("create index %q: %v", table, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// changes to apply on existing table.
|
||||
type changes struct {
|
||||
add []*Column
|
||||
modify []*Column
|
||||
indexes []*Index
|
||||
// column changes.
|
||||
column struct {
|
||||
add []*Column
|
||||
drop []*Column
|
||||
modify []*Column
|
||||
}
|
||||
// index changes.
|
||||
index struct {
|
||||
add []*Index
|
||||
drop []*Index
|
||||
}
|
||||
}
|
||||
|
||||
// changeSet returns a changes object to be applied on existing table.
|
||||
@@ -180,29 +235,65 @@ func (m *Migrate) changeSet(curr, new *Table) (*changes, error) {
|
||||
return nil, fmt.Errorf("cannot change primary key for table: %q", curr.Name)
|
||||
}
|
||||
}
|
||||
// columns.
|
||||
// add or modify columns.
|
||||
for _, c1 := range new.Columns {
|
||||
switch c2, ok := curr.column(c1.Name); {
|
||||
case !ok:
|
||||
change.add = append(change.add, c1)
|
||||
case c1.Unique != c2.Unique:
|
||||
return nil, fmt.Errorf("changing column cardinality for %q is invalid", c1.Name)
|
||||
change.column.add = append(change.column.add, c1)
|
||||
// modify a non-unique column to unique.
|
||||
case c1.Unique && !c2.Unique:
|
||||
change.index.add = append(change.index.add, &Index{
|
||||
Name: c1.Name,
|
||||
Unique: true,
|
||||
Columns: []*Column{c1},
|
||||
columns: []string{c1.Name},
|
||||
})
|
||||
// modify a unique column to non-unique.
|
||||
case !c1.Unique && c2.Unique:
|
||||
idx, ok := curr.index(c2.Name)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("missing index to drop for column %q", c2.Name)
|
||||
}
|
||||
change.index.drop = append(change.index.drop, idx)
|
||||
// extending column types.
|
||||
case m.cType(c1) != m.cType(c2):
|
||||
if !c2.ConvertibleTo(c1) {
|
||||
return nil, fmt.Errorf("changing column type for %q is invalid (%s != %s)", c1.Name, m.cType(c1), m.cType(c2))
|
||||
}
|
||||
fallthrough
|
||||
// modify character encoding.
|
||||
case c1.Charset != "" && c1.Charset != c2.Charset || c1.Collation != "" && c1.Charset != c2.Collation:
|
||||
change.modify = append(change.modify, c1)
|
||||
change.column.modify = append(change.column.modify, c1)
|
||||
}
|
||||
}
|
||||
// indexes.
|
||||
|
||||
// drop columns.
|
||||
for _, c1 := range curr.Columns {
|
||||
// if a column was dropped, multi-columns indexes that are associated with this column will
|
||||
// no longer behave the same. Therefore, these indexes should be dropped too. There's no need
|
||||
// to do it explicitly (here), because entc will remove them from the schema specification,
|
||||
// and they will be dropped in the block below.
|
||||
if _, ok := new.column(c1.Name); !ok {
|
||||
change.column.drop = append(change.column.drop, c1)
|
||||
}
|
||||
}
|
||||
|
||||
// add or modify indexes.
|
||||
for _, idx1 := range new.Indexes {
|
||||
switch idx2, ok := curr.index(idx1.Name); {
|
||||
case !ok:
|
||||
change.indexes = append(change.indexes, idx1)
|
||||
change.index.add = append(change.index.add, idx1)
|
||||
// changing index cardinality require drop and create.
|
||||
case idx1.Unique != idx2.Unique:
|
||||
return nil, fmt.Errorf("changing index %q uniqness is invalid", idx1.Name)
|
||||
change.index.drop = append(change.index.drop, idx2)
|
||||
change.index.add = append(change.index.add, idx1)
|
||||
}
|
||||
}
|
||||
|
||||
// drop indexes.
|
||||
for _, idx1 := range curr.Indexes {
|
||||
if _, ok := new.index(idx1.Name); ok {
|
||||
change.index.drop = append(change.index.drop, idx1)
|
||||
}
|
||||
}
|
||||
return change, nil
|
||||
@@ -300,7 +391,7 @@ type sqlDialect interface {
|
||||
tableExist(context.Context, dialect.Tx, string) (bool, error)
|
||||
fkExist(context.Context, dialect.Tx, string) (bool, error)
|
||||
setRange(context.Context, dialect.Tx, string, int) error
|
||||
// table and column builder per dialect.
|
||||
// table, column and index builder per dialect.
|
||||
cType(*Column) string
|
||||
tBuilder(*Table) *sql.TableBuilder
|
||||
cBuilder(*Column) *sql.ColumnBuilder
|
||||
|
||||
@@ -53,6 +53,7 @@ func (d *MySQL) table(ctx context.Context, tx dialect.Tx, name string) (*Table,
|
||||
if err := tx.Query(ctx, query, args, rows); err != nil {
|
||||
return nil, fmt.Errorf("mysql: reading table description %v", err)
|
||||
}
|
||||
// call `Close` in cases of failures (`Close` is idempotent).
|
||||
defer rows.Close()
|
||||
t := &Table{Name: name}
|
||||
for rows.Next() {
|
||||
@@ -65,9 +66,34 @@ func (d *MySQL) table(ctx context.Context, tx dialect.Tx, name string) (*Table,
|
||||
}
|
||||
t.Columns = append(t.Columns, c)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, fmt.Errorf("mysql: closing rows %v", err)
|
||||
}
|
||||
indexes, err := d.indexes(ctx, tx, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
t.Indexes = indexes
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// table loads the table indexes from the database.
|
||||
func (d *MySQL) indexes(ctx context.Context, tx dialect.Tx, name string) ([]*Index, error) {
|
||||
rows := &sql.Rows{}
|
||||
query, args := sql.Select("index_name", "column_name", "non_unique", "seq_in_index").
|
||||
From(sql.Table("INFORMATION_SCHEMA.STATISTICS").Unquote()).
|
||||
Where(sql.EQ("TABLE_SCHEMA", sql.Raw("(SELECT DATABASE())")).And().EQ("TABLE_NAME", name)).Query()
|
||||
if err := tx.Query(ctx, query, args, rows); err != nil {
|
||||
return nil, fmt.Errorf("mysql: reading index description %v", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
var idx Indexes
|
||||
if err := idx.ScanMySQL(rows); err != nil {
|
||||
return nil, fmt.Errorf("mysql: %v", err)
|
||||
}
|
||||
return idx, nil
|
||||
}
|
||||
|
||||
func (d *MySQL) setRange(ctx context.Context, tx dialect.Tx, name string, value int) error {
|
||||
return tx.Exec(ctx, fmt.Sprintf("ALTER TABLE `%s` AUTO_INCREMENT = %d", name, value), []interface{}{}, new(sql.Result))
|
||||
}
|
||||
|
||||
@@ -177,6 +177,10 @@ func TestMySQL_Create(t *testing.T) {
|
||||
WillReturnRows(sqlmock.NewRows([]string{"column_name", "column_type", "is_nullable", "column_key", "column_default", "extra", "character_set_name", "collation_name"}).
|
||||
AddRow("id", "bigint(20)", "NO", "PRI", "NULL", "auto_increment", "", "").
|
||||
AddRow("name", "varchar(255)", "NO", "YES", "NULL", "", "", ""))
|
||||
mock.ExpectQuery(escape("SELECT `index_name`, `column_name`, `non_unique`, `seq_in_index` FROM INFORMATION_SCHEMA.STATISTICS WHERE `TABLE_SCHEMA` = (SELECT DATABASE()) AND `TABLE_NAME` = ?")).
|
||||
WithArgs("users").
|
||||
WillReturnRows(sqlmock.NewRows([]string{"index_name", "column_name", "non_unique", "seq_in_index"}).
|
||||
AddRow("PRIMARY", "id", "0", "1"))
|
||||
mock.ExpectExec(escape("ALTER TABLE `users` ADD COLUMN `age` bigint")).
|
||||
WillReturnResult(sqlmock.NewResult(0, 1))
|
||||
mock.ExpectCommit()
|
||||
@@ -210,11 +214,123 @@ func TestMySQL_Create(t *testing.T) {
|
||||
AddRow("id", "bigint(20)", "NO", "PRI", "NULL", "auto_increment", "", "").
|
||||
AddRow("name", "varchar(255)", "NO", "YES", "NULL", "", "", "").
|
||||
AddRow("age", "bigint(20)", "NO", "NO", "NULL", "", "", ""))
|
||||
mock.ExpectQuery(escape("SELECT `index_name`, `column_name`, `non_unique`, `seq_in_index` FROM INFORMATION_SCHEMA.STATISTICS WHERE `TABLE_SCHEMA` = (SELECT DATABASE()) AND `TABLE_NAME` = ?")).
|
||||
WithArgs("users").
|
||||
WillReturnRows(sqlmock.NewRows([]string{"index_name", "column_name", "non_unique", "seq_in_index"}).
|
||||
AddRow("PRIMARY", "id", "0", "1"))
|
||||
mock.ExpectExec(escape("ALTER TABLE `users` MODIFY COLUMN `name` varchar(255) CHARSET utf8 NULL")).
|
||||
WillReturnResult(sqlmock.NewResult(0, 1))
|
||||
mock.ExpectCommit()
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "apply uniqueness on column",
|
||||
tables: []*Table{
|
||||
{
|
||||
Name: "users",
|
||||
Columns: []*Column{
|
||||
{Name: "id", Type: field.TypeInt, Increment: true},
|
||||
{Name: "age", Type: field.TypeInt, Unique: true},
|
||||
},
|
||||
PrimaryKey: []*Column{
|
||||
{Name: "id", Type: field.TypeInt, Increment: true},
|
||||
},
|
||||
},
|
||||
},
|
||||
before: func(mock sqlmock.Sqlmock) {
|
||||
mock.ExpectBegin()
|
||||
mock.ExpectQuery(escape("SHOW VARIABLES LIKE 'version'")).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"Variable_name", "Value"}).AddRow("version", "5.7.23"))
|
||||
mock.ExpectQuery(escape("SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE `TABLE_SCHEMA` = (SELECT DATABASE()) AND `TABLE_NAME` = ?")).
|
||||
WithArgs("users").
|
||||
WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(1))
|
||||
mock.ExpectQuery(escape("SELECT `column_name`, `column_type`, `is_nullable`, `column_key`, `column_default`, `extra`, `character_set_name`, `collation_name` FROM INFORMATION_SCHEMA.COLUMNS WHERE `TABLE_SCHEMA` = (SELECT DATABASE()) AND `TABLE_NAME` = ?")).
|
||||
WithArgs("users").
|
||||
WillReturnRows(sqlmock.NewRows([]string{"column_name", "column_type", "is_nullable", "column_key", "column_default", "extra", "character_set_name", "collation_name"}).
|
||||
AddRow("id", "bigint(20)", "NO", "PRI", "NULL", "auto_increment", "", "").
|
||||
AddRow("age", "bigint(20)", "NO", "", "NULL", "", "", ""))
|
||||
mock.ExpectQuery(escape("SELECT `index_name`, `column_name`, `non_unique`, `seq_in_index` FROM INFORMATION_SCHEMA.STATISTICS WHERE `TABLE_SCHEMA` = (SELECT DATABASE()) AND `TABLE_NAME` = ?")).
|
||||
WithArgs("users").
|
||||
WillReturnRows(sqlmock.NewRows([]string{"index_name", "column_name", "non_unique", "seq_in_index"}).
|
||||
AddRow("PRIMARY", "id", "0", "1"))
|
||||
// create the unique index.
|
||||
mock.ExpectExec(escape("CREATE UNIQUE INDEX `age` ON `users`(`age`)")).
|
||||
WillReturnResult(sqlmock.NewResult(0, 1))
|
||||
mock.ExpectCommit()
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "remove uniqueness from column without option",
|
||||
tables: []*Table{
|
||||
{
|
||||
Name: "users",
|
||||
Columns: []*Column{
|
||||
{Name: "id", Type: field.TypeInt, Increment: true},
|
||||
{Name: "age", Type: field.TypeInt},
|
||||
},
|
||||
PrimaryKey: []*Column{
|
||||
{Name: "id", Type: field.TypeInt, Increment: true},
|
||||
},
|
||||
},
|
||||
},
|
||||
before: func(mock sqlmock.Sqlmock) {
|
||||
mock.ExpectBegin()
|
||||
mock.ExpectQuery(escape("SHOW VARIABLES LIKE 'version'")).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"Variable_name", "Value"}).AddRow("version", "5.7.23"))
|
||||
mock.ExpectQuery(escape("SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE `TABLE_SCHEMA` = (SELECT DATABASE()) AND `TABLE_NAME` = ?")).
|
||||
WithArgs("users").
|
||||
WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(1))
|
||||
mock.ExpectQuery(escape("SELECT `column_name`, `column_type`, `is_nullable`, `column_key`, `column_default`, `extra`, `character_set_name`, `collation_name` FROM INFORMATION_SCHEMA.COLUMNS WHERE `TABLE_SCHEMA` = (SELECT DATABASE()) AND `TABLE_NAME` = ?")).
|
||||
WithArgs("users").
|
||||
WillReturnRows(sqlmock.NewRows([]string{"column_name", "column_type", "is_nullable", "column_key", "column_default", "extra", "character_set_name", "collation_name"}).
|
||||
AddRow("id", "bigint(20)", "NO", "PRI", "NULL", "auto_increment", "", "").
|
||||
AddRow("age", "bigint(20)", "NO", "UNI", "NULL", "", "", ""))
|
||||
mock.ExpectQuery(escape("SELECT `index_name`, `column_name`, `non_unique`, `seq_in_index` FROM INFORMATION_SCHEMA.STATISTICS WHERE `TABLE_SCHEMA` = (SELECT DATABASE()) AND `TABLE_NAME` = ?")).
|
||||
WithArgs("users").
|
||||
WillReturnRows(sqlmock.NewRows([]string{"index_name", "column_name", "non_unique", "seq_in_index"}).
|
||||
AddRow("PRIMARY", "id", "0", "1").
|
||||
AddRow("age", "age", "0", "1"))
|
||||
mock.ExpectCommit()
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "remove uniqueness from column without option",
|
||||
tables: []*Table{
|
||||
{
|
||||
Name: "users",
|
||||
Columns: []*Column{
|
||||
{Name: "id", Type: field.TypeInt, Increment: true},
|
||||
{Name: "age", Type: field.TypeInt},
|
||||
},
|
||||
PrimaryKey: []*Column{
|
||||
{Name: "id", Type: field.TypeInt, Increment: true},
|
||||
},
|
||||
},
|
||||
},
|
||||
options: []MigrateOption{WithDropIndex(true)},
|
||||
before: func(mock sqlmock.Sqlmock) {
|
||||
mock.ExpectBegin()
|
||||
mock.ExpectQuery(escape("SHOW VARIABLES LIKE 'version'")).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"Variable_name", "Value"}).AddRow("version", "5.7.23"))
|
||||
mock.ExpectQuery(escape("SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE `TABLE_SCHEMA` = (SELECT DATABASE()) AND `TABLE_NAME` = ?")).
|
||||
WithArgs("users").
|
||||
WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(1))
|
||||
mock.ExpectQuery(escape("SELECT `column_name`, `column_type`, `is_nullable`, `column_key`, `column_default`, `extra`, `character_set_name`, `collation_name` FROM INFORMATION_SCHEMA.COLUMNS WHERE `TABLE_SCHEMA` = (SELECT DATABASE()) AND `TABLE_NAME` = ?")).
|
||||
WithArgs("users").
|
||||
WillReturnRows(sqlmock.NewRows([]string{"column_name", "column_type", "is_nullable", "column_key", "column_default", "extra", "character_set_name", "collation_name"}).
|
||||
AddRow("id", "bigint(20)", "NO", "PRI", "NULL", "auto_increment", "", "").
|
||||
AddRow("age", "bigint(20)", "NO", "UNI", "NULL", "", "", ""))
|
||||
mock.ExpectQuery(escape("SELECT `index_name`, `column_name`, `non_unique`, `seq_in_index` FROM INFORMATION_SCHEMA.STATISTICS WHERE `TABLE_SCHEMA` = (SELECT DATABASE()) AND `TABLE_NAME` = ?")).
|
||||
WithArgs("users").
|
||||
WillReturnRows(sqlmock.NewRows([]string{"index_name", "column_name", "non_unique", "seq_in_index"}).
|
||||
AddRow("PRIMARY", "id", "0", "1").
|
||||
AddRow("age", "age", "0", "1"))
|
||||
// drop the unique index.
|
||||
mock.ExpectExec(escape("DROP INDEX `age` ON `users`")).
|
||||
WillReturnResult(sqlmock.NewResult(0, 1))
|
||||
mock.ExpectCommit()
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "add edge to table",
|
||||
tables: func() []*Table {
|
||||
@@ -253,6 +369,10 @@ func TestMySQL_Create(t *testing.T) {
|
||||
WillReturnRows(sqlmock.NewRows([]string{"column_name", "column_type", "is_nullable", "column_key", "column_default", "extra", "character_set_name", "collation_name"}).
|
||||
AddRow("id", "bigint(20)", "NO", "PRI", "NULL", "auto_increment", "", "").
|
||||
AddRow("name", "varchar(255)", "NO", "YES", "NULL", "", "", ""))
|
||||
mock.ExpectQuery(escape("SELECT `index_name`, `column_name`, `non_unique`, `seq_in_index` FROM INFORMATION_SCHEMA.STATISTICS WHERE `TABLE_SCHEMA` = (SELECT DATABASE()) AND `TABLE_NAME` = ?")).
|
||||
WithArgs("users").
|
||||
WillReturnRows(sqlmock.NewRows([]string{"index_name", "column_name", "non_unique", "seq_in_index"}).
|
||||
AddRow("PRIMARY", "id", "0", "1"))
|
||||
mock.ExpectExec(escape("ALTER TABLE `users` ADD COLUMN `spouse_id` bigint")).
|
||||
WillReturnResult(sqlmock.NewResult(0, 1))
|
||||
mock.ExpectQuery(escape("SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE `TABLE_SCHEMA` = (SELECT DATABASE()) AND `CONSTRAINT_TYPE` = ? AND `CONSTRAINT_NAME` = ?")).
|
||||
@@ -329,6 +449,10 @@ func TestMySQL_Create(t *testing.T) {
|
||||
WithArgs("users").
|
||||
WillReturnRows(sqlmock.NewRows([]string{"column_name", "column_type", "is_nullable", "column_key", "column_default", "extra", "character_set_name", "collation_name"}).
|
||||
AddRow("id", "bigint(20)", "NO", "PRI", "NULL", "auto_increment", "", ""))
|
||||
mock.ExpectQuery(escape("SELECT `index_name`, `column_name`, `non_unique`, `seq_in_index` FROM INFORMATION_SCHEMA.STATISTICS WHERE `TABLE_SCHEMA` = (SELECT DATABASE()) AND `TABLE_NAME` = ?")).
|
||||
WithArgs("users").
|
||||
WillReturnRows(sqlmock.NewRows([]string{"index_name", "column_name", "non_unique", "seq_in_index"}).
|
||||
AddRow("PRIMARY", "id", "0", "1"))
|
||||
mock.ExpectQuery(escape("SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE `TABLE_SCHEMA` = (SELECT DATABASE()) AND `TABLE_NAME` = ?")).
|
||||
WithArgs("groups").
|
||||
WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(0))
|
||||
|
||||
@@ -409,11 +409,60 @@ func (r ReferenceOption) ConstName() string {
|
||||
|
||||
// Index definition for table index.
|
||||
type Index struct {
|
||||
Name string
|
||||
Unique bool
|
||||
Columns []*Column
|
||||
Name string // index name.
|
||||
Unique bool // uniqueness.
|
||||
Columns []*Column // actual table columns.
|
||||
columns []string // columns loaded from query scan.
|
||||
}
|
||||
|
||||
// Primary indicates if this index is a primary key.
|
||||
// Used by the migration tool when parsing the `DESCRIBE TABLE` output Go objects.
|
||||
func (i *Index) Primary() bool { return i.Name == "PRIMARY" }
|
||||
|
||||
// Builder returns the query builder for index creation. The DSL is identical in all dialects.
|
||||
func (i *Index) Builder(table string) *sql.IndexBuilder {
|
||||
idx := sql.CreateIndex(i.Name).Table(table)
|
||||
if i.Unique {
|
||||
idx.Unique()
|
||||
}
|
||||
for _, c := range i.Columns {
|
||||
idx.Column(c.Name)
|
||||
}
|
||||
return idx
|
||||
}
|
||||
|
||||
// DropBuilder returns the query builder for the drop index.
|
||||
func (i *Index) DropBuilder(table string) *sql.DropIndexBuilder {
|
||||
idx := sql.DropIndex(i.Name).Table(table)
|
||||
return idx
|
||||
}
|
||||
|
||||
// Indexes used for scanning all sql.Rows into a list of indexes, because
|
||||
// multiple sql rows can represent the same index (multi-columns indexes).
|
||||
type Indexes []*Index
|
||||
|
||||
// ScanMySQL scans sql.Rows into an Indexes list. The query for returning the rows,
|
||||
// should return the following 4 columns: INDEX_NAME, COLUMN_NAME, NON_UNIQUE, SEQ_IN_INDEX.
|
||||
// SEQ_IN_INDEX specifies the position of the column in the index columns.
|
||||
func (i *Indexes) ScanMySQL(rows *sql.Rows) error {
|
||||
names := make(map[string]*Index)
|
||||
for rows.Next() {
|
||||
var (
|
||||
name string
|
||||
column string
|
||||
nonuniq bool
|
||||
seqindex int
|
||||
)
|
||||
if err := rows.Scan(&name, &column, &nonuniq, &seqindex); err != nil {
|
||||
return fmt.Errorf("scanning index description: %v", err)
|
||||
}
|
||||
idx, ok := names[name]
|
||||
if !ok {
|
||||
idx = &Index{Name: name, Unique: !nonuniq}
|
||||
*i = append(*i, idx)
|
||||
names[name] = idx
|
||||
}
|
||||
idx.columns = append(idx.columns, column)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -57,9 +57,9 @@ func (d *SQLite) setRange(ctx context.Context, tx dialect.Tx, name string, value
|
||||
return tx.Exec(ctx, query, args, new(sql.Result))
|
||||
}
|
||||
|
||||
func (d *SQLite) cType(c *Column) string { return c.SQLiteType() }
|
||||
func (d *SQLite) tBuilder(t *Table) *sql.TableBuilder { return t.SQLite() }
|
||||
func (d *SQLite) cBuilder(c *Column) *sql.ColumnBuilder { return c.SQLite() }
|
||||
func (*SQLite) cType(c *Column) string { return c.SQLiteType() }
|
||||
func (*SQLite) tBuilder(t *Table) *sql.TableBuilder { return t.SQLite() }
|
||||
func (*SQLite) cBuilder(c *Column) *sql.ColumnBuilder { return c.SQLite() }
|
||||
|
||||
// fkExist returns always tru to disable foreign-keys creation after the table was created.
|
||||
func (d *SQLite) fkExist(context.Context, dialect.Tx, string) (bool, error) { return true, nil }
|
||||
|
||||
Reference in New Issue
Block a user