refactor: distinguish between Unique and UniqueIndex (#156)

* refactor: distinguish between Unique and UniqueIndex

* take care of unique created by old version

* update gorm to master latest (for https://github.com/go-gorm/gorm/pull/6386)

* merge
This commit is contained in:
black-06
2024-02-06 11:39:22 +08:00
committed by GitHub
parent e64f7a5816
commit 87158cf45d
5 changed files with 126 additions and 80 deletions

View File

@@ -13,6 +13,7 @@ import (
var ( var (
sqliteSeparator = "`|\"|'|\t" sqliteSeparator = "`|\"|'|\t"
uniqueRegexp = regexp.MustCompile(fmt.Sprintf(`^CONSTRAINT [%v]?[\w-]+[%v]? UNIQUE (.*)$`, sqliteSeparator, sqliteSeparator))
indexRegexp = regexp.MustCompile(fmt.Sprintf(`(?is)CREATE(?: UNIQUE)? INDEX [%v]?[\w\d-]+[%v]?(?s:.*?)ON (.*)$`, sqliteSeparator, sqliteSeparator)) indexRegexp = regexp.MustCompile(fmt.Sprintf(`(?is)CREATE(?: UNIQUE)? INDEX [%v]?[\w\d-]+[%v]?(?s:.*?)ON (.*)$`, sqliteSeparator, sqliteSeparator))
tableRegexp = regexp.MustCompile(fmt.Sprintf(`(?is)(CREATE TABLE [%v]?[\w\d-]+[%v]?)(?:\s*\((.*)\))?`, sqliteSeparator, sqliteSeparator)) tableRegexp = regexp.MustCompile(fmt.Sprintf(`(?is)(CREATE TABLE [%v]?[\w\d-]+[%v]?)(?:\s*\((.*)\))?`, sqliteSeparator, sqliteSeparator))
separatorRegexp = regexp.MustCompile(fmt.Sprintf("[%v]", sqliteSeparator)) separatorRegexp = regexp.MustCompile(fmt.Sprintf("[%v]", sqliteSeparator))
@@ -103,11 +104,24 @@ func parseDDL(strs ...string) (*ddl, error) {
for _, f := range result.fields { for _, f := range result.fields {
fUpper := strings.ToUpper(f) fUpper := strings.ToUpper(f)
if strings.HasPrefix(fUpper, "CHECK") || if strings.HasPrefix(fUpper, "CHECK") {
strings.HasPrefix(fUpper, "CONSTRAINT") { continue
}
if strings.HasPrefix(fUpper, "CONSTRAINT") {
matches := uniqueRegexp.FindStringSubmatch(f)
if len(matches) > 0 {
if columns := getAllColumns(matches[1]); len(columns) == 1 {
for idx, column := range result.columns {
if column.NameValue.String == columns[0] {
column.UniqueValue = sql.NullBool{Bool: true, Valid: true}
result.columns[idx] = column
break
}
}
}
}
continue continue
} }
if strings.HasPrefix(fUpper, "PRIMARY KEY") { if strings.HasPrefix(fUpper, "PRIMARY KEY") {
for _, name := range getAllColumns(f) { for _, name := range getAllColumns(f) {
for idx, column := range result.columns { for idx, column := range result.columns {
@@ -159,14 +173,7 @@ func parseDDL(strs ...string) (*ddl, error) {
} }
} }
} else if matches := indexRegexp.FindStringSubmatch(str); len(matches) > 0 { } else if matches := indexRegexp.FindStringSubmatch(str); len(matches) > 0 {
for _, column := range getAllColumns(matches[1]) { // don't report Unique by UniqueIndex
for idx, c := range result.columns {
if c.NameValue.String == column {
c.UniqueValue = sql.NullBool{Bool: strings.ToUpper(strings.Fields(str)[1]) == "UNIQUE", Valid: true}
result.columns[idx] = c
}
}
}
} else { } else {
return nil, errors.New("invalid DDL") return nil, errors.New("invalid DDL")
} }
@@ -268,20 +275,6 @@ func (d *ddl) getColumns() []string {
return res return res
} }
func (d *ddl) alterColumn(name, sql string) bool {
reg := regexp.MustCompile("^(`|'|\"| )" + regexp.QuoteMeta(name) + "(`|'|\"| ) .*?$")
for i := 0; i < len(d.fields); i++ {
if reg.MatchString(d.fields[i]) {
d.fields[i] = sql
return false
}
}
d.fields = append(d.fields, sql)
return true
}
func (d *ddl) removeColumn(name string) bool { func (d *ddl) removeColumn(name string) bool {
reg := regexp.MustCompile("^(`|'|\"| )" + regexp.QuoteMeta(name) + "(`|'|\"| ) .*?$") reg := regexp.MustCompile("^(`|'|\"| )" + regexp.QuoteMeta(name) + "(`|'|\"| ) .*?$")

View File

@@ -16,11 +16,12 @@ func TestParseDDL(t *testing.T) {
columns []migrator.ColumnType columns []migrator.ColumnType
}{ }{
{"with_fk", []string{ {"with_fk", []string{
"CREATE TABLE `notes` (`id` integer NOT NULL,`text` varchar(500) DEFAULT \"hello\",`age` integer DEFAULT 18,`user_id` integer,PRIMARY KEY (`id`),CONSTRAINT `fk_users_notes` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`))", "CREATE TABLE `notes` (" +
"`id` integer NOT NULL,`text` varchar(500) DEFAULT \"hello\",`age` integer DEFAULT 18,`user_id` integer,PRIMARY KEY (`id`),CONSTRAINT `fk_users_notes` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`))",
"CREATE UNIQUE INDEX `idx_profiles_refer` ON `profiles`(`text`)", "CREATE UNIQUE INDEX `idx_profiles_refer` ON `profiles`(`text`)",
}, 6, []migrator.ColumnType{ }, 6, []migrator.ColumnType{
{NameValue: sql.NullString{String: "id", Valid: true}, DataTypeValue: sql.NullString{String: "integer", Valid: true}, ColumnTypeValue: sql.NullString{String: "integer", Valid: true}, PrimaryKeyValue: sql.NullBool{Bool: true, Valid: true}, NullableValue: sql.NullBool{Valid: true}, UniqueValue: sql.NullBool{Valid: true}, DefaultValueValue: sql.NullString{Valid: false}}, {NameValue: sql.NullString{String: "id", Valid: true}, DataTypeValue: sql.NullString{String: "integer", Valid: true}, ColumnTypeValue: sql.NullString{String: "integer", Valid: true}, PrimaryKeyValue: sql.NullBool{Bool: true, Valid: true}, NullableValue: sql.NullBool{Valid: true}, UniqueValue: sql.NullBool{Valid: true}, DefaultValueValue: sql.NullString{Valid: false}},
{NameValue: sql.NullString{String: "text", Valid: true}, DataTypeValue: sql.NullString{String: "varchar", Valid: true}, LengthValue: sql.NullInt64{Int64: 500, Valid: true}, ColumnTypeValue: sql.NullString{String: "varchar(500)", Valid: true}, DefaultValueValue: sql.NullString{String: "hello", Valid: true}, NullableValue: sql.NullBool{Bool: true, Valid: true}, UniqueValue: sql.NullBool{Bool: true, Valid: true}, PrimaryKeyValue: sql.NullBool{Valid: true}}, {NameValue: sql.NullString{String: "text", Valid: true}, DataTypeValue: sql.NullString{String: "varchar", Valid: true}, LengthValue: sql.NullInt64{Int64: 500, Valid: true}, ColumnTypeValue: sql.NullString{String: "varchar(500)", Valid: true}, DefaultValueValue: sql.NullString{String: "hello", Valid: true}, NullableValue: sql.NullBool{Bool: true, Valid: true}, UniqueValue: sql.NullBool{Bool: false, Valid: true}, PrimaryKeyValue: sql.NullBool{Valid: true}},
{NameValue: sql.NullString{String: "age", Valid: true}, DataTypeValue: sql.NullString{String: "integer", Valid: true}, ColumnTypeValue: sql.NullString{String: "integer", Valid: true}, DefaultValueValue: sql.NullString{String: "18", Valid: true}, NullableValue: sql.NullBool{Bool: true, Valid: true}, UniqueValue: sql.NullBool{Valid: true}, PrimaryKeyValue: sql.NullBool{Valid: true}}, {NameValue: sql.NullString{String: "age", Valid: true}, DataTypeValue: sql.NullString{String: "integer", Valid: true}, ColumnTypeValue: sql.NullString{String: "integer", Valid: true}, DefaultValueValue: sql.NullString{String: "18", Valid: true}, NullableValue: sql.NullBool{Bool: true, Valid: true}, UniqueValue: sql.NullBool{Valid: true}, PrimaryKeyValue: sql.NullBool{Valid: true}},
{NameValue: sql.NullString{String: "user_id", Valid: true}, DataTypeValue: sql.NullString{String: "integer", Valid: true}, ColumnTypeValue: sql.NullString{String: "integer", Valid: true}, DefaultValueValue: sql.NullString{Valid: false}, NullableValue: sql.NullBool{Bool: true, Valid: true}, UniqueValue: sql.NullBool{Valid: true}, PrimaryKeyValue: sql.NullBool{Valid: true}}, {NameValue: sql.NullString{String: "user_id", Valid: true}, DataTypeValue: sql.NullString{String: "integer", Valid: true}, ColumnTypeValue: sql.NullString{String: "integer", Valid: true}, DefaultValueValue: sql.NullString{Valid: false}, NullableValue: sql.NullBool{Bool: true, Valid: true}, UniqueValue: sql.NullBool{Valid: true}, PrimaryKeyValue: sql.NullBool{Valid: true}},
}, },
@@ -56,28 +57,54 @@ func TestParseDDL(t *testing.T) {
ColumnTypeValue: sql.NullString{String: "int", Valid: true}, ColumnTypeValue: sql.NullString{String: "int", Valid: true},
NullableValue: sql.NullBool{Bool: false, Valid: true}, NullableValue: sql.NullBool{Bool: false, Valid: true},
DefaultValueValue: sql.NullString{Valid: false}, DefaultValueValue: sql.NullString{Valid: false},
UniqueValue: sql.NullBool{Bool: true, Valid: true}, UniqueValue: sql.NullBool{Bool: false, Valid: true},
PrimaryKeyValue: sql.NullBool{Valid: true}, PrimaryKeyValue: sql.NullBool{Valid: true},
}, },
}, },
}, }, {
{
"unique index", "unique index",
[]string{ []string{
"CREATE TABLE `test-b` (`field` integer NOT NULL)", "CREATE TABLE `test-b` (`field` integer NOT NULL)",
"CREATE UNIQUE INDEX `idx_uq` ON `test-b`(`field`) WHERE field = 0", "CREATE UNIQUE INDEX `idx_uq` ON `test-b`(`field`) WHERE field = 0",
}, },
1, 1,
[]migrator.ColumnType{ []migrator.ColumnType{{
{
NameValue: sql.NullString{String: "field", Valid: true}, NameValue: sql.NullString{String: "field", Valid: true},
DataTypeValue: sql.NullString{String: "integer", Valid: true}, DataTypeValue: sql.NullString{String: "integer", Valid: true},
ColumnTypeValue: sql.NullString{String: "integer", Valid: true}, ColumnTypeValue: sql.NullString{String: "integer", Valid: true},
PrimaryKeyValue: sql.NullBool{Bool: false, Valid: true}, PrimaryKeyValue: sql.NullBool{Bool: false, Valid: true},
UniqueValue: sql.NullBool{Bool: true, Valid: true}, UniqueValue: sql.NullBool{Bool: false, Valid: true},
NullableValue: sql.NullBool{Bool: false, Valid: true}, NullableValue: sql.NullBool{Bool: false, Valid: true},
}},
}, {
"normal index",
[]string{
"CREATE TABLE `test-c` (`field` integer NOT NULL)",
"CREATE INDEX `idx_uq` ON `test-c`(`field`)",
}, },
1,
[]migrator.ColumnType{{
NameValue: sql.NullString{String: "field", Valid: true},
DataTypeValue: sql.NullString{String: "integer", Valid: true},
ColumnTypeValue: sql.NullString{String: "integer", Valid: true},
PrimaryKeyValue: sql.NullBool{Bool: false, Valid: true},
UniqueValue: sql.NullBool{Bool: false, Valid: true},
NullableValue: sql.NullBool{Bool: false, Valid: true},
}},
}, {
"unique constraint",
[]string{
"CREATE TABLE `unique_struct` (`name` text,CONSTRAINT `uni_unique_struct_name` UNIQUE (`name`))",
}, },
2,
[]migrator.ColumnType{{
NameValue: sql.NullString{String: "name", Valid: true},
DataTypeValue: sql.NullString{String: "text", Valid: true},
ColumnTypeValue: sql.NullString{String: "text", Valid: true},
PrimaryKeyValue: sql.NullBool{Bool: false, Valid: true},
UniqueValue: sql.NullBool{Bool: true, Valid: true},
NullableValue: sql.NullBool{Bool: true, Valid: true},
}},
}, },
{ {
"non-unique index", "non-unique index",

2
go.mod
View File

@@ -4,7 +4,7 @@ go 1.20
require ( require (
github.com/mattn/go-sqlite3 v1.14.17 github.com/mattn/go-sqlite3 v1.14.17
gorm.io/gorm v1.25.2-0.20230530020048-26663ab9bf55 gorm.io/gorm v1.25.7-0.20240204074919-46816ad31dde
) )
require ( require (

2
go.sum
View File

@@ -8,3 +8,5 @@ gorm.io/gorm v1.25.0 h1:+KtYtb2roDz14EQe4bla8CbQlmb9dN3VejSai3lprfU=
gorm.io/gorm v1.25.0/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= gorm.io/gorm v1.25.0/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
gorm.io/gorm v1.25.2-0.20230530020048-26663ab9bf55 h1:sC1Xj4TYrLqg1n3AN10w871An7wJM0gzgcm8jkIkECQ= gorm.io/gorm v1.25.2-0.20230530020048-26663ab9bf55 h1:sC1Xj4TYrLqg1n3AN10w871An7wJM0gzgcm8jkIkECQ=
gorm.io/gorm v1.25.2-0.20230530020048-26663ab9bf55/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= gorm.io/gorm v1.25.2-0.20230530020048-26663ab9bf55/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
gorm.io/gorm v1.25.7-0.20240204074919-46816ad31dde h1:9DShaph9qhkIYw7QF91I/ynrr4cOO2PZra2PFD7Mfeg=
gorm.io/gorm v1.25.7-0.20240204074919-46816ad31dde/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=

View File

@@ -79,14 +79,28 @@ func (m Migrator) AlterColumn(value interface{}, name string) error {
return m.RunWithoutForeignKey(func() error { return m.RunWithoutForeignKey(func() error {
return m.recreateTable(value, nil, func(ddl *ddl, stmt *gorm.Statement) (*ddl, []interface{}, error) { return m.recreateTable(value, nil, func(ddl *ddl, stmt *gorm.Statement) (*ddl, []interface{}, error) {
if field := stmt.Schema.LookUpField(name); field != nil { if field := stmt.Schema.LookUpField(name); field != nil {
if ddl.alterColumn(field.DBName, fmt.Sprintf("`%s` ?", field.DBName)) { var sqlArgs []interface{}
return nil, nil, fmt.Errorf("field `%s` not found in origin ddl, ddl= '%s'", name, ddl.compile()) for i, f := range ddl.fields {
if matches := columnRegexp.FindStringSubmatch(f); len(matches) > 1 && matches[1] == field.DBName {
ddl.fields[i] = fmt.Sprintf("`%v` ?", field.DBName)
sqlArgs = []interface{}{m.FullDataTypeOf(field)}
// table created by old version might look like `CREATE TABLE ? (? varchar(10) UNIQUE)`.
// FullDataTypeOf doesn't contain UNIQUE, so we need to add unique constraint.
if strings.Contains(strings.ToUpper(matches[3]), " UNIQUE") {
uniName := m.DB.NamingStrategy.UniqueName(stmt.Table, field.DBName)
uni, _ := m.GuessConstraintInterfaceAndTable(stmt, uniName)
if uni != nil {
uniSQL, uniArgs := uni.Build()
ddl.addConstraint(uniName, uniSQL)
sqlArgs = append(sqlArgs, uniArgs...)
} }
return ddl, []interface{}{m.FullDataTypeOf(field)}, nil
} }
break
return nil, nil, fmt.Errorf("failed to alter field with name `%s`", name) }
}
return ddl, sqlArgs, nil
}
return nil, nil, fmt.Errorf("failed to alter field with name %v", name)
}) })
}) })
} }
@@ -153,7 +167,7 @@ func (m Migrator) DropColumn(value interface{}, name string) error {
func (m Migrator) CreateConstraint(value interface{}, name string) error { func (m Migrator) CreateConstraint(value interface{}, name string) error {
return m.RunWithValue(value, func(stmt *gorm.Statement) error { return m.RunWithValue(value, func(stmt *gorm.Statement) error {
constraint, chk, table := m.GuessConstraintAndTable(stmt, name) constraint, table := m.GuessConstraintInterfaceAndTable(stmt, name)
return m.recreateTable(value, &table, return m.recreateTable(value, &table,
func(ddl *ddl, stmt *gorm.Statement) (*ddl, []interface{}, error) { func(ddl *ddl, stmt *gorm.Statement) (*ddl, []interface{}, error) {
@@ -164,12 +178,8 @@ func (m Migrator) CreateConstraint(value interface{}, name string) error {
) )
if constraint != nil { if constraint != nil {
constraintName = constraint.Name constraintName = constraint.GetName()
constraintSql, constraintValues = buildConstraint(constraint) constraintSql, constraintValues = constraint.Build()
} else if chk != nil {
constraintName = chk.Name
constraintSql = "CONSTRAINT ? CHECK (?)"
constraintValues = []interface{}{clause.Column{Name: chk.Name}, clause.Expr{SQL: chk.Constraint}}
} else { } else {
return nil, nil, nil return nil, nil, nil
} }
@@ -182,11 +192,9 @@ func (m Migrator) CreateConstraint(value interface{}, name string) error {
func (m Migrator) DropConstraint(value interface{}, name string) error { func (m Migrator) DropConstraint(value interface{}, name string) error {
return m.RunWithValue(value, func(stmt *gorm.Statement) error { return m.RunWithValue(value, func(stmt *gorm.Statement) error {
constraint, chk, table := m.GuessConstraintAndTable(stmt, name) constraint, table := m.GuessConstraintInterfaceAndTable(stmt, name)
if constraint != nil { if constraint != nil {
name = constraint.Name name = constraint.GetName()
} else if chk != nil {
name = chk.Name
} }
return m.recreateTable(value, &table, return m.recreateTable(value, &table,
@@ -200,11 +208,9 @@ func (m Migrator) DropConstraint(value interface{}, name string) error {
func (m Migrator) HasConstraint(value interface{}, name string) bool { func (m Migrator) HasConstraint(value interface{}, name string) bool {
var count int64 var count int64
m.RunWithValue(value, func(stmt *gorm.Statement) error { m.RunWithValue(value, func(stmt *gorm.Statement) error {
constraint, chk, table := m.GuessConstraintAndTable(stmt, name) constraint, table := m.GuessConstraintInterfaceAndTable(stmt, name)
if constraint != nil { if constraint != nil {
name = constraint.Name name = constraint.GetName()
} else if chk != nil {
name = chk.Name
} }
m.DB.Raw( m.DB.Raw(
@@ -317,26 +323,44 @@ func (m Migrator) DropIndex(value interface{}, name string) error {
}) })
} }
func buildConstraint(constraint *schema.Constraint) (sql string, results []interface{}) { type Index struct {
sql = "CONSTRAINT ? FOREIGN KEY ? REFERENCES ??" Seq int
if constraint.OnDelete != "" { Name string
sql += " ON DELETE " + constraint.OnDelete Unique bool
Origin string
Partial bool
} }
if constraint.OnUpdate != "" { // GetIndexes return Indexes []gorm.Index and execErr error,
sql += " ON UPDATE " + constraint.OnUpdate // See the [doc]
//
// [doc]: https://www.sqlite.org/pragma.html#pragma_index_list
func (m Migrator) GetIndexes(value interface{}) ([]gorm.Index, error) {
indexes := make([]gorm.Index, 0)
err := m.RunWithValue(value, func(stmt *gorm.Statement) error {
rst := make([]*Index, 0)
if err := m.DB.Debug().Raw(fmt.Sprintf("PRAGMA index_list(%q)", stmt.Table)).Scan(&rst).Error; err != nil {
return err
} }
for _, index := range rst {
var foreignKeys, references []interface{} if index.Origin == "u" { // skip the index was created by a UNIQUE constraint
for _, field := range constraint.ForeignKeys { continue
foreignKeys = append(foreignKeys, clause.Column{Name: field.DBName})
} }
var columns []string
for _, field := range constraint.References { if err := m.DB.Raw(fmt.Sprintf("SELECT name from PRAGMA_index_info(%q)", index.Name)).Scan(&columns).Error; err != nil { // alias `PRAGMA index_info(?)`
references = append(references, clause.Column{Name: field.DBName}) return err
} }
results = append(results, clause.Table{Name: constraint.Name}, foreignKeys, clause.Table{Name: constraint.ReferenceSchema.Table}, references) indexes = append(indexes, &migrator.Index{
return TableName: stmt.Table,
NameValue: index.Name,
ColumnList: columns,
PrimaryKeyValue: sql.NullBool{Bool: index.Origin == "pk", Valid: true}, // The exceptions are INTEGER PRIMARY KEY
UniqueValue: sql.NullBool{Bool: index.Unique, Valid: true},
})
}
return nil
})
return indexes, err
} }
func (m Migrator) getRawDDL(table string) (string, error) { func (m Migrator) getRawDDL(table string) (string, error) {