Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions ddlmod.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import (
var (
sqliteSeparator = "`|\"|'"
sqliteColumnQuote = "`"
uniqueRegexp = regexp.MustCompile(fmt.Sprintf(`^(?:CONSTRAINT [%v]?[\w-]+[%v]? )?UNIQUE (.*)$`, sqliteSeparator, sqliteSeparator))
uniqueRegexp = regexp.MustCompile(fmt.Sprintf(`^(?i)(?:CONSTRAINT [%v]?[\w-]+[%v]? )?UNIQUE\s*(\(.*)$`, 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))
checkRegexp = regexp.MustCompile(`^(?i)CHECK[\s]*\(`)
Expand Down Expand Up @@ -217,7 +217,9 @@ func (d *ddl) renameTable(dst, src string) error {
}

func compileConstraintRegexp(name string) *regexp.Regexp {
return regexp.MustCompile("^(?i:CONSTRAINT)\\s+[\"`]?" + regexp.QuoteMeta(name) + "[\"`\\s]")
// the name may be quoted with backquotes, double quotes, single quotes
// or brackets, or not at all
return regexp.MustCompile("^(?i:CONSTRAINT)\\s+[\"'`\\[]?" + regexp.QuoteMeta(name) + "[\"'`\\]\\s]")
}

func (d *ddl) addConstraint(name string, sql string) {
Expand Down Expand Up @@ -282,7 +284,7 @@ func (d *ddl) getColumns() []string {
}

func (d *ddl) removeColumn(name string) bool {
reg := regexp.MustCompile("^(`|'|\"| )" + regexp.QuoteMeta(name) + "(`|'|\"| ) .*?$")
reg := regexp.MustCompile("^[`'\" ]?" + regexp.QuoteMeta(name) + "[`'\" ]")

for i := 0; i < len(d.fields); i++ {
if reg.MatchString(d.fields[i]) {
Expand Down
40 changes: 40 additions & 0 deletions ddlmod_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -475,3 +475,43 @@ func TestGetColumns(t *testing.T) {
})
}
}

func TestParseDDL_LowercaseUnique(t *testing.T) {
d, err := parseDDL("CREATE TABLE `t` (`a` text, unique(`a`))")
if err != nil {
t.Fatal(err)
}
found := false
for _, c := range d.columns {
if c.NameValue.String == "a" {
found = true
if uniq, _ := c.Unique(); !uniq {
t.Error("lowercase table-level unique(...) not recognized")
}
}
}
if !found {
t.Fatal("column a was not parsed at all")
}
}

func TestConstraintNameQuoting(t *testing.T) {
forms := map[string]string{
"backquotes": "CREATE TABLE `t` (`a` integer, CONSTRAINT `chk_a` CHECK (`a` > 0))",
"double quotes": `CREATE TABLE "t" ("a" integer, CONSTRAINT "chk_a" CHECK ("a" > 0))`,
"brackets": "CREATE TABLE t (a integer, CONSTRAINT [chk_a] CHECK (a > 0))",
"unquoted": "CREATE TABLE t (a integer, CONSTRAINT chk_a CHECK (a > 0))",
}
for form, ddlSQL := range forms {
d, err := parseDDL(ddlSQL)
if err != nil {
t.Fatalf("%s: %v", form, err)
}
if !d.hasConstraint("chk_a") {
t.Errorf("%s: constraint chk_a not matched", form)
}
if d.hasConstraint("chk") {
t.Errorf("%s: prefix chk must not match", form)
}
}
}
36 changes: 24 additions & 12 deletions migrator.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package sqlite

import (
"database/sql"
"errors"
"fmt"
"strings"

Expand Down Expand Up @@ -52,7 +53,9 @@ func (m Migrator) DropTable(values ...interface{}) error {
}

func (m Migrator) GetTables() (tableList []string, err error) {
return tableList, m.DB.Raw("SELECT name FROM sqlite_master where type=?", "table").Scan(&tableList).Error
return tableList, m.DB.Raw(
"SELECT name FROM sqlite_master WHERE type = ? AND name NOT LIKE ?", "table", "sqlite_%",
).Scan(&tableList).Error
}

func (m Migrator) HasColumn(value interface{}, name string) bool {
Expand Down Expand Up @@ -161,7 +164,9 @@ func (m Migrator) DropColumn(value interface{}, name string) error {
name = field.DBName
}

ddl.removeColumn(name)
if !ddl.removeColumn(name) {
return nil, nil, fmt.Errorf("failed to drop column %v: not found in the DDL of table %v", name, stmt.Table)
}
return ddl, nil, nil
})
})
Expand Down Expand Up @@ -208,22 +213,26 @@ func (m Migrator) DropConstraint(value interface{}, name string) error {
}

func (m Migrator) HasConstraint(value interface{}, name string) bool {
var count int64
var has bool
m.RunWithValue(value, func(stmt *gorm.Statement) error {
constraint, table := m.GuessConstraintInterfaceAndTable(stmt, name)
if constraint != nil {
name = constraint.GetName()
}

m.DB.Raw(
"SELECT count(*) FROM sqlite_master WHERE type = ? AND tbl_name = ? AND (sql LIKE ? OR sql LIKE ? OR sql LIKE ? OR sql LIKE ? OR sql LIKE ?)",
"table", table, `%CONSTRAINT "`+name+`" %`, `%CONSTRAINT `+name+` %`, "%CONSTRAINT `"+name+"`%", "%CONSTRAINT ["+name+"]%", "%CONSTRAINT \t"+name+"\t%",
).Row().Scan(&count)

rawDDL, err := m.getRawDDL(table)
if err != nil {
return err
}
parsed, err := parseDDL(rawDDL)
if err != nil {
return err
}
has = parsed.hasConstraint(name)
Comment thread
h2zi marked this conversation as resolved.
return nil
})

return count > 0
return has
}

func (m Migrator) CurrentDatabase() (name string) {
Expand Down Expand Up @@ -367,10 +376,13 @@ func (m Migrator) GetIndexes(value interface{}) ([]gorm.Index, error) {

func (m Migrator) getRawDDL(table string) (string, error) {
var createSQL string
m.DB.Raw("SELECT sql FROM sqlite_master WHERE type = ? AND tbl_name = ? AND name = ?", "table", table, table).Row().Scan(&createSQL)
err := m.DB.Raw("SELECT sql FROM sqlite_master WHERE type = ? AND tbl_name = ? AND name = ?", "table", table, table).Row().Scan(&createSQL)
if err != nil && !errors.Is(err, sql.ErrNoRows) {
return "", err
}

if m.DB.Error != nil {
return "", m.DB.Error
if createSQL == "" {
return "", fmt.Errorf("failed to get DDL of table %q: table not found", table)
}
return createSQL, nil
}
Expand Down
98 changes: 98 additions & 0 deletions migrator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,3 +178,101 @@ func openRecreateTestDB(t *testing.T, name string) *gorm.DB {
})
return db
}

// GetTables must not report SQLite internal tables such as sqlite_sequence,
// which appears as soon as any table uses AUTOINCREMENT.
func TestGetTablesExcludesInternal(t *testing.T) {
db, err := gorm.Open(Open("file:internal_tables?mode=memory&cache=shared"), &gorm.Config{})
if err != nil {
t.Fatalf("gorm.Open: %v", err)
}
t.Cleanup(func() {
if sqlDB, err := db.DB(); err == nil {
_ = sqlDB.Close()
}
})

if err := db.Exec("CREATE TABLE `seq_table` (`id` integer PRIMARY KEY AUTOINCREMENT)").Error; err != nil {
t.Fatal(err)
}
if err := db.Exec("INSERT INTO `seq_table` DEFAULT VALUES").Error; err != nil {
t.Fatal(err)
}

tables, err := db.Migrator().GetTables()
if err != nil {
t.Fatal(err)
}
for _, tb := range tables {
if strings.HasPrefix(tb, "sqlite_") {
t.Errorf("GetTables returned internal table %q", tb)
}
}
if len(tables) != 1 || tables[0] != "seq_table" {
t.Errorf("GetTables = %v, want [seq_table]", tables)
}
}

type checkedModel struct {
ID int
Age int `gorm:"check:age_positive,age > 0"`
}

func (checkedModel) TableName() string { return "checked_models" }

// HasConstraint must match the exact constraint name instead of a LIKE
// substring, and DropColumn must fail for a column that is not in the DDL
// instead of silently rebuilding an identical table.
func TestConstraintAndColumnMatching(t *testing.T) {
db, err := gorm.Open(Open("file:constraint_matching?mode=memory&cache=shared"), &gorm.Config{})
if err != nil {
t.Fatalf("gorm.Open: %v", err)
}
t.Cleanup(func() {
if sqlDB, err := db.DB(); err == nil {
_ = sqlDB.Close()
}
})

if err := db.AutoMigrate(&checkedModel{}); err != nil {
t.Fatal(err)
}
if !db.Migrator().HasConstraint(&checkedModel{}, "age_positive") {
t.Error("HasConstraint(age_positive) = false, want true")
}
if db.Migrator().HasConstraint(&checkedModel{}, "age_pos") {
t.Error("HasConstraint(age_pos) matched by prefix, want false")
}

// unquoted DDL: the column must still be dropped
if err := db.Exec("CREATE TABLE plain_cols (id integer, name text)").Error; err != nil {
t.Fatal(err)
}
if err := db.Migrator().DropColumn(&plainColModel{}, "name"); err != nil {
t.Fatalf("DropColumn on unquoted DDL: %v", err)
}
if db.Migrator().HasColumn(&plainColModel{}, "name") {
t.Error("column still present after DropColumn on unquoted DDL")
}
// a missing column must be an error, not a silent no-op
if err := db.Migrator().DropColumn(&plainColModel{}, "missing"); err == nil {
t.Error("DropColumn(missing) = nil, want error")
}
// a missing table must be reported clearly
if err := db.Migrator().DropColumn(&noSuchTableModel{}, "col"); err == nil || !strings.Contains(err.Error(), "table not found") {
t.Errorf("DropColumn on a missing table = %v, want a table-not-found error", err)
}
}

type plainColModel struct {
ID int
Name string
}

func (plainColModel) TableName() string { return "plain_cols" }

type noSuchTableModel struct {
ID int
}

func (noSuchTableModel) TableName() string { return "no_such_table" }
Loading