ent/sql/migrate: support indexes

Reviewed By: alexsn

Differential Revision: D16711184

fbshipit-source-id: 632b02c5c77c6289b242263647d45d9f28752e3f
This commit is contained in:
Ariel Mashraki
2019-08-11 05:43:27 -07:00
committed by Facebook Github Bot
parent 933fe91741
commit 329b5ddf77
13 changed files with 502 additions and 43 deletions

View File

@@ -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

View File

@@ -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) {

View File

@@ -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

View File

@@ -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))
}

View File

@@ -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))

View File

@@ -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
}

View File

@@ -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 }