diff --git a/ddlmod.go b/ddlmod.go index d00f3ef..54d6c78 100644 --- a/ddlmod.go +++ b/ddlmod.go @@ -156,7 +156,9 @@ func parseDDL(strs ...string) (*ddl, error) { } if defaultMatches := defaultValueRegexp.FindStringSubmatch(matches[3]); len(defaultMatches) > 1 { if strings.ToLower(defaultMatches[1]) != "null" { - columnType.DefaultValueValue = sql.NullString{String: strings.Trim(defaultMatches[1], `"`), Valid: true} + // single quotes are standard SQL string literals, + // double quotes come from tables created by older versions + columnType.DefaultValueValue = sql.NullString{String: trimQuote(defaultMatches[1]), Valid: true} } } @@ -293,3 +295,14 @@ func (d *ddl) removeColumn(name string) bool { return false } + +// trimQuote removes one pair of matching outer quotes from a default value +// literal, so inner quotes of values like '"x"' survive. +func trimQuote(s string) string { + if len(s) >= 2 { + if (s[0] == '\'' && s[len(s)-1] == '\'') || (s[0] == '"' && s[len(s)-1] == '"') { + return s[1 : len(s)-1] + } + } + return s +} diff --git a/sqlite.go b/sqlite.go index b3c8e31..1589954 100644 --- a/sqlite.go +++ b/sqlite.go @@ -203,7 +203,8 @@ func (dialector Dialector) QuoteTo(writer clause.Writer, str string) { } func (dialector Dialector) Explain(sql string, vars ...interface{}) string { - return logger.ExplainSQL(sql, nil, `"`, vars...) + // single quotes: double quotes are identifier quoting in SQLite + return logger.ExplainSQL(sql, nil, `'`, vars...) } func (dialector Dialector) DataTypeOf(field *schema.Field) string { diff --git a/sqlite_test.go b/sqlite_test.go index d33ea02..a694d30 100644 --- a/sqlite_test.go +++ b/sqlite_test.go @@ -3,6 +3,7 @@ package sqlite import ( "database/sql" "fmt" + "strings" "testing" "github.com/mattn/go-sqlite3" @@ -130,3 +131,80 @@ func TestDialector(t *testing.T) { }) } } + +func TestExplainQuotesStrings(t *testing.T) { + out := Dialector{}.Explain("SELECT * FROM t WHERE name = ?", "hello") + if !strings.Contains(out, "'hello'") { + t.Errorf("Explain must quote string literals with single quotes, got: %s", out) + } +} + +type explainDefaultModel struct { + ID int + Code string `gorm:"default:hello"` +} + +func (explainDefaultModel) TableName() string { return "explain_defaults" } + +// GORM embeds string default values into the DDL via Dialector.Explain; +// parseDDL must strip the single quotes (and double quotes from tables +// created by older versions) so migrations stay idempotent. +func TestDefaultValueRoundTrip(t *testing.T) { + db, err := gorm.Open(Open("file:explain_defaults?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(&explainDefaultModel{}); err != nil { + t.Fatal(err) + } + cols, err := db.Migrator().ColumnTypes(&explainDefaultModel{}) + if err != nil { + t.Fatal(err) + } + for _, c := range cols { + if c.Name() == "code" { + if dv, ok := c.DefaultValue(); !ok || dv != "hello" { + t.Errorf("DefaultValue = (%q,%v), want (hello,true)", dv, ok) + } + } + } + // second AutoMigrate must not rebuild the table + var before string + if err := db.Raw("SELECT sql FROM sqlite_master WHERE type='table' AND name='explain_defaults'").Scan(&before).Error; err != nil { + t.Fatal(err) + } + if err := db.AutoMigrate(&explainDefaultModel{}); err != nil { + t.Fatal(err) + } + var after string + if err := db.Raw("SELECT sql FROM sqlite_master WHERE type='table' AND name='explain_defaults'").Scan(&after).Error; err != nil { + t.Fatal(err) + } + if before != after { + t.Errorf("DDL changed after second AutoMigrate:\n before: %s\n after: %s", before, after) + } + + // double quotes from tables created by older driver versions still parse + d, err := parseDDL("CREATE TABLE `legacy` (`code` text DEFAULT \"hi\")") + if err != nil { + t.Fatal(err) + } + if dv, ok := d.columns[0].DefaultValue(); !ok || dv != "hi" { + t.Errorf("legacy DefaultValue = (%q,%v), want (hi,true)", dv, ok) + } + + // only one outer quote pair is stripped; inner quotes survive + d, err = parseDDL("CREATE TABLE `q` (`code` text DEFAULT '\"x\"')") + if err != nil { + t.Fatal(err) + } + if dv, ok := d.columns[0].DefaultValue(); !ok || dv != `"x"` { + t.Errorf(`quoted DefaultValue = (%q,%v), want ("x",true)`, dv, ok) + } +}