Skip to content

Commit a871805

Browse files
committed
Merge upstream
2 parents 084ab1e + 7544227 commit a871805

File tree

6 files changed

+140
-78
lines changed

6 files changed

+140
-78
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ jobs:
1212
test:
1313
strategy:
1414
matrix:
15-
go: ['1.20','1.19','1.18']
15+
go: ['1.21', '1.20', '1.19']
1616
platform: [ubuntu-latest, macos-latest, windows-latest]
1717
runs-on: ${{ matrix.platform }}
1818

ddlmod.go

Lines changed: 18 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313

1414
var (
1515
sqliteSeparator = "`|\"|'|\t"
16+
uniqueRegexp = regexp.MustCompile(fmt.Sprintf(`^CONSTRAINT [%v]?[\w-]+[%v]? UNIQUE (.*)$`, sqliteSeparator, sqliteSeparator))
1617
indexRegexp = regexp.MustCompile(fmt.Sprintf(`(?is)CREATE(?: UNIQUE)? INDEX [%v]?[\w\d-]+[%v]?(?s:.*?)ON (.*)$`, sqliteSeparator, sqliteSeparator))
1718
tableRegexp = regexp.MustCompile(fmt.Sprintf(`(?is)(CREATE TABLE [%v]?[\w\d-]+[%v]?)(?:\s*\((.*)\))?`, sqliteSeparator, sqliteSeparator))
1819
separatorRegexp = regexp.MustCompile(fmt.Sprintf("[%v]", sqliteSeparator))
@@ -103,11 +104,24 @@ func parseDDL(strs ...string) (*ddl, error) {
103104

104105
for _, f := range result.fields {
105106
fUpper := strings.ToUpper(f)
106-
if strings.HasPrefix(fUpper, "CHECK") ||
107-
strings.HasPrefix(fUpper, "CONSTRAINT") {
107+
if strings.HasPrefix(fUpper, "CHECK") {
108+
continue
109+
}
110+
if strings.HasPrefix(fUpper, "CONSTRAINT") {
111+
matches := uniqueRegexp.FindStringSubmatch(f)
112+
if len(matches) > 0 {
113+
if columns := getAllColumns(matches[1]); len(columns) == 1 {
114+
for idx, column := range result.columns {
115+
if column.NameValue.String == columns[0] {
116+
column.UniqueValue = sql.NullBool{Bool: true, Valid: true}
117+
result.columns[idx] = column
118+
break
119+
}
120+
}
121+
}
122+
}
108123
continue
109124
}
110-
111125
if strings.HasPrefix(fUpper, "PRIMARY KEY") {
112126
for _, name := range getAllColumns(f) {
113127
for idx, column := range result.columns {
@@ -159,14 +173,7 @@ func parseDDL(strs ...string) (*ddl, error) {
159173
}
160174
}
161175
} else if matches := indexRegexp.FindStringSubmatch(str); len(matches) > 0 {
162-
for _, column := range getAllColumns(matches[1]) {
163-
for idx, c := range result.columns {
164-
if c.NameValue.String == column {
165-
c.UniqueValue = sql.NullBool{Bool: strings.ToUpper(strings.Fields(str)[1]) == "UNIQUE", Valid: true}
166-
result.columns[idx] = c
167-
}
168-
}
169-
}
176+
// don't report Unique by UniqueIndex
170177
} else {
171178
return nil, errors.New("invalid DDL")
172179
}
@@ -268,20 +275,6 @@ func (d *ddl) getColumns() []string {
268275
return res
269276
}
270277

271-
func (d *ddl) alterColumn(name, sql string) bool {
272-
reg := regexp.MustCompile("^(`|'|\"| )" + regexp.QuoteMeta(name) + "(`|'|\"| ) .*?$")
273-
274-
for i := 0; i < len(d.fields); i++ {
275-
if reg.MatchString(d.fields[i]) {
276-
d.fields[i] = sql
277-
return false
278-
}
279-
}
280-
281-
d.fields = append(d.fields, sql)
282-
return true
283-
}
284-
285278
func (d *ddl) removeColumn(name string) bool {
286279
reg := regexp.MustCompile("^(`|'|\"| )" + regexp.QuoteMeta(name) + "(`|'|\"| ) .*?$")
287280

ddlmod_test.go

Lines changed: 54 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,12 @@ func TestParseDDL(t *testing.T) {
1616
columns []migrator.ColumnType
1717
}{
1818
{"with_fk", []string{
19-
"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`))",
19+
"CREATE TABLE `notes` (" +
20+
"`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`))",
2021
"CREATE UNIQUE INDEX `idx_profiles_refer` ON `profiles`(`text`)",
2122
}, 6, []migrator.ColumnType{
2223
{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}},
23-
{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}},
24+
{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}},
2425
{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}},
2526
{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}},
2627
},
@@ -56,34 +57,78 @@ func TestParseDDL(t *testing.T) {
5657
ColumnTypeValue: sql.NullString{String: "int", Valid: true},
5758
NullableValue: sql.NullBool{Bool: false, Valid: true},
5859
DefaultValueValue: sql.NullString{Valid: false},
59-
UniqueValue: sql.NullBool{Bool: true, Valid: true},
60+
UniqueValue: sql.NullBool{Bool: false, Valid: true},
6061
PrimaryKeyValue: sql.NullBool{Valid: true},
6162
},
6263
},
63-
},
64-
{
64+
}, {
6565
"unique index",
6666
[]string{
6767
"CREATE TABLE `test-b` (`field` integer NOT NULL)",
6868
"CREATE UNIQUE INDEX `idx_uq` ON `test-b`(`field`) WHERE field = 0",
6969
},
7070
1,
71+
[]migrator.ColumnType{{
72+
NameValue: sql.NullString{String: "field", Valid: true},
73+
DataTypeValue: sql.NullString{String: "integer", Valid: true},
74+
ColumnTypeValue: sql.NullString{String: "integer", Valid: true},
75+
PrimaryKeyValue: sql.NullBool{Bool: false, Valid: true},
76+
UniqueValue: sql.NullBool{Bool: false, Valid: true},
77+
NullableValue: sql.NullBool{Bool: false, Valid: true},
78+
}},
79+
}, {
80+
"normal index",
81+
[]string{
82+
"CREATE TABLE `test-c` (`field` integer NOT NULL)",
83+
"CREATE INDEX `idx_uq` ON `test-c`(`field`)",
84+
},
85+
1,
86+
[]migrator.ColumnType{{
87+
NameValue: sql.NullString{String: "field", Valid: true},
88+
DataTypeValue: sql.NullString{String: "integer", Valid: true},
89+
ColumnTypeValue: sql.NullString{String: "integer", Valid: true},
90+
PrimaryKeyValue: sql.NullBool{Bool: false, Valid: true},
91+
UniqueValue: sql.NullBool{Bool: false, Valid: true},
92+
NullableValue: sql.NullBool{Bool: false, Valid: true},
93+
}},
94+
}, {
95+
"unique constraint",
96+
[]string{
97+
"CREATE TABLE `unique_struct` (`name` text,CONSTRAINT `uni_unique_struct_name` UNIQUE (`name`))",
98+
},
99+
2,
100+
[]migrator.ColumnType{{
101+
NameValue: sql.NullString{String: "name", Valid: true},
102+
DataTypeValue: sql.NullString{String: "text", Valid: true},
103+
ColumnTypeValue: sql.NullString{String: "text", Valid: true},
104+
PrimaryKeyValue: sql.NullBool{Bool: false, Valid: true},
105+
UniqueValue: sql.NullBool{Bool: true, Valid: true},
106+
NullableValue: sql.NullBool{Bool: true, Valid: true},
107+
}},
108+
},
109+
{
110+
"non-unique index",
111+
[]string{
112+
"CREATE TABLE `test-c` (`field` integer NOT NULL)",
113+
"CREATE INDEX `idx_uq` ON `test-b`(`field`) WHERE field = 0",
114+
},
115+
1,
71116
[]migrator.ColumnType{
72117
{
73118
NameValue: sql.NullString{String: "field", Valid: true},
74119
DataTypeValue: sql.NullString{String: "integer", Valid: true},
75120
ColumnTypeValue: sql.NullString{String: "integer", Valid: true},
76121
PrimaryKeyValue: sql.NullBool{Bool: false, Valid: true},
77-
UniqueValue: sql.NullBool{Bool: true, Valid: true},
122+
UniqueValue: sql.NullBool{Bool: false, Valid: true},
78123
NullableValue: sql.NullBool{Bool: false, Valid: true},
79124
},
80125
},
81126
},
82127
{
83-
"non-unique index",
128+
"index with \n from .schema sqlite",
84129
[]string{
85-
"CREATE TABLE `test-c` (`field` integer NOT NULL)",
86-
"CREATE INDEX `idx_uq` ON `test-b`(`field`) WHERE field = 0",
130+
"CREATE TABLE `test-d` (`field` integer NOT NULL)",
131+
"CREATE INDEX `idx_uq`\n ON `test-b`(`field`) WHERE field = 0",
87132
},
88133
1,
89134
[]migrator.ColumnType{

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ go 1.18
44

55
require (
66
github.com/glebarez/go-sqlite v1.21.2
7-
gorm.io/gorm v1.25.5
7+
gorm.io/gorm v1.25.7-0.20240204074919-46816ad31dde
88
modernc.org/sqlite v1.23.1
99
)
1010

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qq
1717
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
1818
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
1919
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
20-
gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls=
21-
gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
20+
gorm.io/gorm v1.25.7-0.20240204074919-46816ad31dde h1:9DShaph9qhkIYw7QF91I/ynrr4cOO2PZra2PFD7Mfeg=
21+
gorm.io/gorm v1.25.7-0.20240204074919-46816ad31dde/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
2222
modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE=
2323
modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY=
2424
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=

migrator.go

Lines changed: 64 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -79,14 +79,28 @@ func (m Migrator) AlterColumn(value interface{}, name string) error {
7979
return m.RunWithoutForeignKey(func() error {
8080
return m.recreateTable(value, nil, func(ddl *ddl, stmt *gorm.Statement) (*ddl, []interface{}, error) {
8181
if field := stmt.Schema.LookUpField(name); field != nil {
82-
if ddl.alterColumn(field.DBName, fmt.Sprintf("`%s` ?", field.DBName)) {
83-
return nil, nil, fmt.Errorf("field `%s` not found in origin ddl, ddl= '%s'", name, ddl.compile())
82+
var sqlArgs []interface{}
83+
for i, f := range ddl.fields {
84+
if matches := columnRegexp.FindStringSubmatch(f); len(matches) > 1 && matches[1] == field.DBName {
85+
ddl.fields[i] = fmt.Sprintf("`%v` ?", field.DBName)
86+
sqlArgs = []interface{}{m.FullDataTypeOf(field)}
87+
// table created by old version might look like `CREATE TABLE ? (? varchar(10) UNIQUE)`.
88+
// FullDataTypeOf doesn't contain UNIQUE, so we need to add unique constraint.
89+
if strings.Contains(strings.ToUpper(matches[3]), " UNIQUE") {
90+
uniName := m.DB.NamingStrategy.UniqueName(stmt.Table, field.DBName)
91+
uni, _ := m.GuessConstraintInterfaceAndTable(stmt, uniName)
92+
if uni != nil {
93+
uniSQL, uniArgs := uni.Build()
94+
ddl.addConstraint(uniName, uniSQL)
95+
sqlArgs = append(sqlArgs, uniArgs...)
96+
}
97+
}
98+
break
99+
}
84100
}
85-
86-
return ddl, []interface{}{m.FullDataTypeOf(field)}, nil
101+
return ddl, sqlArgs, nil
87102
}
88-
89-
return nil, nil, fmt.Errorf("failed to alter field with name `%s`", name)
103+
return nil, nil, fmt.Errorf("failed to alter field with name %v", name)
90104
})
91105
})
92106
}
@@ -153,7 +167,7 @@ func (m Migrator) DropColumn(value interface{}, name string) error {
153167

154168
func (m Migrator) CreateConstraint(value interface{}, name string) error {
155169
return m.RunWithValue(value, func(stmt *gorm.Statement) error {
156-
constraint, chk, table := m.GuessConstraintAndTable(stmt, name)
170+
constraint, table := m.GuessConstraintInterfaceAndTable(stmt, name)
157171

158172
return m.recreateTable(value, &table,
159173
func(ddl *ddl, stmt *gorm.Statement) (*ddl, []interface{}, error) {
@@ -164,12 +178,8 @@ func (m Migrator) CreateConstraint(value interface{}, name string) error {
164178
)
165179

166180
if constraint != nil {
167-
constraintName = constraint.Name
168-
constraintSql, constraintValues = buildConstraint(constraint)
169-
} else if chk != nil {
170-
constraintName = chk.Name
171-
constraintSql = "CONSTRAINT ? CHECK (?)"
172-
constraintValues = []interface{}{clause.Column{Name: chk.Name}, clause.Expr{SQL: chk.Constraint}}
181+
constraintName = constraint.GetName()
182+
constraintSql, constraintValues = constraint.Build()
173183
} else {
174184
return nil, nil, nil
175185
}
@@ -182,11 +192,9 @@ func (m Migrator) CreateConstraint(value interface{}, name string) error {
182192

183193
func (m Migrator) DropConstraint(value interface{}, name string) error {
184194
return m.RunWithValue(value, func(stmt *gorm.Statement) error {
185-
constraint, chk, table := m.GuessConstraintAndTable(stmt, name)
195+
constraint, table := m.GuessConstraintInterfaceAndTable(stmt, name)
186196
if constraint != nil {
187-
name = constraint.Name
188-
} else if chk != nil {
189-
name = chk.Name
197+
name = constraint.GetName()
190198
}
191199

192200
return m.recreateTable(value, &table,
@@ -200,11 +208,9 @@ func (m Migrator) DropConstraint(value interface{}, name string) error {
200208
func (m Migrator) HasConstraint(value interface{}, name string) bool {
201209
var count int64
202210
m.RunWithValue(value, func(stmt *gorm.Statement) error {
203-
constraint, chk, table := m.GuessConstraintAndTable(stmt, name)
211+
constraint, table := m.GuessConstraintInterfaceAndTable(stmt, name)
204212
if constraint != nil {
205-
name = constraint.Name
206-
} else if chk != nil {
207-
name = chk.Name
213+
name = constraint.GetName()
208214
}
209215

210216
m.DB.Raw(
@@ -317,26 +323,44 @@ func (m Migrator) DropIndex(value interface{}, name string) error {
317323
})
318324
}
319325

320-
func buildConstraint(constraint *schema.Constraint) (sql string, results []interface{}) {
321-
sql = "CONSTRAINT ? FOREIGN KEY ? REFERENCES ??"
322-
if constraint.OnDelete != "" {
323-
sql += " ON DELETE " + constraint.OnDelete
324-
}
325-
326-
if constraint.OnUpdate != "" {
327-
sql += " ON UPDATE " + constraint.OnUpdate
328-
}
329-
330-
var foreignKeys, references []interface{}
331-
for _, field := range constraint.ForeignKeys {
332-
foreignKeys = append(foreignKeys, clause.Column{Name: field.DBName})
333-
}
326+
type Index struct {
327+
Seq int
328+
Name string
329+
Unique bool
330+
Origin string
331+
Partial bool
332+
}
334333

335-
for _, field := range constraint.References {
336-
references = append(references, clause.Column{Name: field.DBName})
337-
}
338-
results = append(results, clause.Table{Name: constraint.Name}, foreignKeys, clause.Table{Name: constraint.ReferenceSchema.Table}, references)
339-
return
334+
// GetIndexes return Indexes []gorm.Index and execErr error,
335+
// See the [doc]
336+
//
337+
// [doc]: https://www.sqlite.org/pragma.html#pragma_index_list
338+
func (m Migrator) GetIndexes(value interface{}) ([]gorm.Index, error) {
339+
indexes := make([]gorm.Index, 0)
340+
err := m.RunWithValue(value, func(stmt *gorm.Statement) error {
341+
rst := make([]*Index, 0)
342+
if err := m.DB.Debug().Raw("SELECT * FROM PRAGMA_index_list(?)", stmt.Table).Scan(&rst).Error; err != nil { // alias `PRAGMA index_list(?)`
343+
return err
344+
}
345+
for _, index := range rst {
346+
if index.Origin == "u" { // skip the index was created by a UNIQUE constraint
347+
continue
348+
}
349+
var columns []string
350+
if err := m.DB.Raw("SELECT name FROM PRAGMA_index_info(?)", index.Name).Scan(&columns).Error; err != nil { // alias `PRAGMA index_info(?)`
351+
return err
352+
}
353+
indexes = append(indexes, &migrator.Index{
354+
TableName: stmt.Table,
355+
NameValue: index.Name,
356+
ColumnList: columns,
357+
PrimaryKeyValue: sql.NullBool{Bool: index.Origin == "pk", Valid: true}, // The exceptions are INTEGER PRIMARY KEY
358+
UniqueValue: sql.NullBool{Bool: index.Unique, Valid: true},
359+
})
360+
}
361+
return nil
362+
})
363+
return indexes, err
340364
}
341365

342366
func (m Migrator) getRawDDL(table string) (string, error) {

0 commit comments

Comments
 (0)