Repository: ariga/atlas
Pull request #340 (2021-12-23 13:43:28)

sql/mysql: support constant literals in default values


Patch:
diff --git a/cmd/action/version.go b/cmd/action/version.go
new file mode 100644
index 00000000..89dee2ea
--- /dev/null
+++ b/cmd/action/version.go
@@ -0,0 +1,33 @@
+package action
+
+import (
+	"fmt"
+
+	"github.com/spf13/cobra"
+	"golang.org/x/mod/semver"
+)
+
+// Version is the atlas CLI build version
+// Should be set by build script "-X 'ariga.io/atlas/cmd/action.version=${version}'"
+var version string
+
+var versionCmd = &cobra.Command{
+	Use:   "version",
+	Short: "Show atlas CLI version",
+	Run: func(cmd *cobra.Command, args []string) {
+		v, u := parse(version)
+		RootCmd.Println(fmt.Sprintf("atlas CLI version %s\n%s", v, u))
+	},
+}
+
+func init() {
+	RootCmd.AddCommand(versionCmd)
+}
+
+// parse returns a user facing version and release notes url
+func parse(version string) (string, string) {
+	if ok := semver.IsValid(version); !ok {
+		return "- development", "https://github.com/ariga/atlas/releases/tag/latest"
+	}
+	return version, fmt.Sprintf("https://github.com/ariga/atlas/releases/tag/%s", version)
+}
diff --git a/sql/mysql/diff.go b/sql/mysql/diff.go
index dbc69ad1..baaf024f 100644
--- a/sql/mysql/diff.go
+++ b/sql/mysql/diff.go
@@ -5,6 +5,7 @@
 package mysql
 
 import (
+	"encoding/hex"
 	"fmt"
 	"reflect"
 	"strconv"
@@ -274,31 +275,51 @@ func (d *diff) typeChanged(from, to *schema.Column) (bool, error) {
 // defaultChanged reports if the a default value of a column
 // type was changed.
 func (d *diff) defaultChanged(from, to *schema.Column) (bool, error) {
-	d1, ok1 := from.Default.(*schema.RawExpr)
-	d2, ok2 := to.Default.(*schema.RawExpr)
-	if ok1 != ok2 {
+	var d1, d2 string
+	switch d := from.Default.(type) {
+	case *schema.Literal:
+		d1 = d.V
+	case *schema.RawExpr:
+		d1 = d.X
+	}
+	switch d := to.Default.(type) {
+	case *schema.Literal:
+		d2 = d.V
+	case *schema.RawExpr:
+		d2 = d.X
+	}
+	if (d1 != "") != (d2 != "") {
 		return true, nil
 	}
-	if d1 == nil || d1.X == d2.X {
+	if d1 == d2 {
 		return false, nil
 	}
 	switch from.Type.Type.(type) {
+	case *schema.BinaryType:
+		a, err1 := binValue(d1)
+		b, err2 := binValue(d2)
+		if err1 != nil || err2 != nil {
+			return true, nil
+		}
+		return !equalsStringValues(a, b), nil
 	case *schema.BoolType:
-		a, err1 := boolValue(d1.X)
-		b, err2 := boolValue(d2.X)
+		a, err1 := boolValue(d1)
+		b, err2 := boolValue(d2)
 		if err1 == nil && err2 == nil {
 			return a != b, nil
 		}
 		return false, nil
 	case *schema.IntegerType:
-		return !d.equalsIntValues(d1.X, d2.X), nil
+		return !d.equalsIntValues(d1, d2), nil
+	case *schema.StringType:
+		return !equalsStringValues(d1, d2), nil
 	case *schema.TimeType:
-		x1 := strings.ToLower(strings.Trim(d1.X, "' ()"))
-		x2 := strings.ToLower(strings.Trim(d2.X, "' ()"))
+		x1 := strings.ToLower(strings.Trim(d1, "' ()"))
+		x2 := strings.ToLower(strings.Trim(d2, "' ()"))
 		return x1 != x2, nil
 	default:
-		x1 := strings.Trim(d1.X, "'")
-		x2 := strings.Trim(d2.X, "'")
+		x1 := strings.Trim(d1, "'")
+		x2 := strings.Trim(d2, "'")
 		return x1 != x2, nil
 	}
 }
@@ -332,7 +353,15 @@ func (d *diff) equalsIntValues(x1, x2 string) bool {
 	return d1 == d2
 }
 
-// boolValue returns the MySQL boolean value for the given string (if it is known).
+// equalsStringValues report if the 2 string default values are
+// equal after dropping their quotes.
+func equalsStringValues(x1, x2 string) bool {
+	a, err1 := unquote(x1)
+	b, err2 := unquote(x2)
+	return a == b && err1 == nil && err2 == nil
+}
+
+// boolValue returns the MySQL boolean value from the given string (if it is known).
 func boolValue(x string) (bool, error) {
 	switch x {
 	case "1", "'1'", "TRUE", "true":
@@ -344,6 +373,18 @@ func boolValue(x string) (bool, error) {
 	}
 }
 
+// binValue returns the MySQL binary value from the given string (if it is known).
+func binValue(x string) (string, error) {
+	if !isHex(x) {
+		return x, nil
+	}
+	d, err := hex.DecodeString(x[2:])
+	if err != nil {
+		return x, err
+	}
+	return string(d), nil
+}
+
 func checkByName(attr []schema.Attr, name string) (*Check, bool) {
 	for i := range attr {
 		if c, ok := attr[i].(*Check); ok && c.Name == name {
diff --git a/sql/mysql/driver.go b/sql/mysql/driver.go
index 024647bc..5c0f3409 100644
--- a/sql/mysql/driver.go
+++ b/sql/mysql/driver.go
@@ -56,13 +56,13 @@ func (d *conn) supportsCheck() (string, bool) {
 	if d.mariadb() {
 		v, q = "10.2.1", marChecksQuery
 	}
-	return q, d.compareV(v) != -1
+	return q, d.gteV(v)
 }
 
 // supportsIndexExpr reports if the connected database supports
 // index expressions (functional key part).
 func (d *conn) supportsIndexExpr() bool {
-	return !d.mariadb() && d.compareV("8.0.13") != -1
+	return !d.mariadb() && d.gteV("8.0.13")
 }
 
 // supportsDisplayWidth reports if the connected database supports
@@ -70,7 +70,17 @@ func (d *conn) supportsIndexExpr() bool {
 func (d *conn) supportsDisplayWidth() bool {
 	// MySQL v8.0.19 dropped the display width
 	// information from the information schema
-	return d.mariadb() || d.compareV("8.0.19") == -1
+	return d.mariadb() || d.ltV("8.0.19")
+}
+
+// supportsExprDefault reports if the connected database supports
+// expressions in the DEFAULT clause on column definition.
+func (d *conn) supportsExprDefault() bool {
+	v := "8.0.13"
+	if d.mariadb() {
+		v = "10.2.1"
+	}
+	return d.gteV(v)
 }
 
 // mariadb reports if the Driver is connected to a MariaDB database.
@@ -88,6 +98,12 @@ func (d *conn) compareV(w string) int {
 	return semver.Compare("v"+v, "v"+w)
 }
 
+// gteV reports if the connection version is >= w.
+func (d *conn) gteV(w string) bool { return d.compareV(w) >= 0 }
+
+// ltV reports if the connection version is < w.
+func (d *conn) ltV(w string) bool { return d.compareV(w) == -1 }
+
 // MySQL standard unescape field function from its codebase:
 // https://github.com/mysql/mysql-server/blob/8.0/sql/dd/impl/utils.cc
 func unescape(s string) string {
@@ -154,3 +170,10 @@ const (
 	tGeoCollection      = "geomcollection"     // Geometry_type::kGeometrycollection
 	tGeometryCollection = "geometrycollection" // Geometry_type::kGeometrycollection
 )
+
+// Additional common constants in MySQL.
+const (
+	currentTS     = "current_timestamp"
+	defaultGen    = "default_generated"
+	autoIncrement = "auto_increment"
+)
diff --git a/sql/mysql/inspect.go b/sql/mysql/inspect.go
index 092ce024..8d6bc09c 100644
--- a/sql/mysql/inspect.go
+++ b/sql/mysql/inspect.go
@@ -228,12 +228,11 @@ func (i *inspect) addColumn(t *schema.Table, rows *sql.Rows) error {
 		return err
 	}
 	if sqlx.ValidString(defaults) {
-		x := defaults.String
-		// From MariaDB 10.2.7, literals are quoted to distinguish them from expressions.
-		if i.mariadb() && strings.HasPrefix(x, "'") && strings.HasSuffix(x, "'") {
-			x = x[1 : len(x)-1]
+		if i.mariadb() {
+			c.Default = i.marDefaultExpr(c, defaults.String)
+		} else {
+			c.Default = i.myDefaultExpr(c, defaults.String, extra.String)
 		}
-		c.Default = &schema.RawExpr{X: x}
 	}
 	if sqlx.ValidString(comment) {
 		c.Attrs = append(c.Attrs, &schema.Comment{
@@ -568,10 +567,10 @@ func parseColumn(typ string) (parts []string, size int64, unsigned bool, err err
 func extraAttr(c *schema.Column, extra string) error {
 	switch extra := strings.ToLower(extra); extra {
 	case "", "null": // ignore.
-	case "default_generated":
+	case defaultGen:
 		// The column has an expression default value
-		// and it's handled in Driver.addColumn.
-	case "auto_increment":
+		// and it is handled in Driver.addColumn.
+	case autoIncrement:
 		c.Attrs = append(c.Attrs, &AutoIncrement{A: extra})
 	case "default_generated on update current_timestamp", "on update current_timestamp",
 		"on update current_timestamp()" /* MariaDB format. */ :
@@ -582,6 +581,72 @@ func extraAttr(c *schema.Column, extra string) error {
 	return nil
 }
 
+// myDefaultExpr returns the correct schema.Expr based on the column attributes for MySQL.
+func (i *inspect) myDefaultExpr(c *schema.Column, x, extra string) schema.Expr {
+	// In MySQL, the DEFAULT_GENERATED indicates the column has an expression default value.
+	if i.supportsExprDefault() && strings.Contains(strings.ToLower(extra), defaultGen) {
+		return &schema.RawExpr{X: x}
+	}
+
+	switch c.Type.Type.(type) {
+	case *schema.BinaryType:
+		// MySQL v8 uses Hexadecimal representation.
+		if isHex(x) {
+			return &schema.Literal{V: x}
+		}
+	case *BitType, *schema.BoolType, *schema.IntegerType, *schema.DecimalType, *schema.FloatType:
+		return &schema.Literal{V: x}
+	case *schema.TimeType:
+		// "current_timestamp" is exceptional in old versions
+		// of MySQL for timestamp and datetime data types.
+		if strings.ToLower(x) == currentTS {
+			return &schema.RawExpr{X: x}
+		}
+	}
+	return &schema.Literal{V: quote(x)}
+}
+
+// hasNumericDefault reports if the given type has a numeric default value.
+func hasNumericDefault(t schema.Type) bool {
+	switch t.(type) {
+	case *BitType, *schema.BoolType, *schema.IntegerType, *schema.DecimalType, *schema.FloatType:
+		return true
+	}
+	return false
+}
+
+func isHex(x string) bool { return len(x) > 2 && strings.ToLower(x[:2]) == "0x" }
+
+// marDefaultExpr returns the correct schema.Expr based on the column attributes for MariaDB.
+func (i *inspect) marDefaultExpr(c *schema.Column, x string) schema.Expr {
+	// From MariaDB 10.2.7, string-based literals are quoted to distinguish them from expressions.
+	if i.gteV("10.2.7") && strings.HasPrefix(x, "'") && strings.HasSuffix(x, "'") {
+		return &schema.Literal{V: x}
+	}
+	// In this case, we need to manually check if the expression is literal, or fallback to raw expression.
+	switch c.Type.Type.(type) {
+	case *BitType:
+		// Bit literal values. See https://mariadb.com/kb/en/binary-literals.
+		if strings.HasPrefix(x, "b'") && strings.HasSuffix(x, "'") {
+			return &schema.Literal{V: x}
+		}
+	case *schema.BoolType, *schema.IntegerType, *schema.DecimalType, *schema.FloatType:
+		if _, err := strconv.ParseFloat(x, 64); err == nil {
+			return &schema.Literal{V: x}
+		}
+	case *schema.TimeType:
+		// "current_timestamp" is exceptional in old versions
+		// of MySQL (i.e. MariaDB in this case).
+		if strings.ToLower(x) == currentTS {
+			return &schema.RawExpr{X: x}
+		}
+	}
+	if !i.supportsExprDefault() {
+		return &schema.Literal{V: quote(x)}
+	}
+	return &schema.RawExpr{X: x}
+}
+
 const (
 	// Query to list system variables.
 	variablesQuery = "SELECT @@version, @@collation_server, @@character_set_server"
diff --git a/sql/mysql/migrate.go b/sql/mysql/migrate.go
index efd11ab3..d95ed2ef 100644
--- a/sql/mysql/migrate.go
+++ b/sql/mysql/migrate.go
@@ -299,18 +299,10 @@ func (s *state) column(b *sqlx.Builder, t *schema.Table, c *schema.Column) {
 		b.P("NOT")
 	}
 	b.P("NULL")
-	if x, ok := c.Default.(*schema.RawExpr); ok {
-		v := x.X
-		// Ensure string/enum default values are quoted.
-		switch c.Type.Type.(type) {
-		case *schema.EnumType, *schema.StringType:
-			v = quote(v)
-		}
-		b.P("DEFAULT", v)
-	}
+	s.columnDefault(b, c)
 	// Add manually the JSON_VALID constraint for older
 	// versions < 10.4.3. See Driver.checks for full info.
-	if _, ok := c.Type.Type.(*schema.JSONType); ok && s.mariadb() && s.compareV("10.4.3") == -1 && !sqlx.Has(c.Attrs, &Check{}) {
+	if _, ok := c.Type.Type.(*schema.JSONType); ok && s.mariadb() && s.ltV("10.4.3") && !sqlx.Has(c.Attrs, &Check{}) {
 		b.P("CHECK").Wrap(func(b *sqlx.Builder) {
 			b.WriteString(fmt.Sprintf("json_valid(`%s`)", c.Name))
 		})
@@ -424,6 +416,30 @@ func (*state) attr(b *sqlx.Builder, attrs ...schema.Attr) {
 	}
 }
 
+// columnDefault writes the default value of column to the builder.
+func (s *state) columnDefault(b *sqlx.Builder, c *schema.Column) {
+	switch x := c.Default.(type) {
+	case *schema.Literal:
+		v := x.V
+		if !hasNumericDefault(c.Type.Type) && !isHex(v) {
+			v = quote(v)
+		}
+		b.P("DEFAULT", v)
+	case *schema.RawExpr:
+		v := x.X
+		// For backwards compatibility, quote raw expressions that are not wrapped
+		// with parens for non-numeric column types (i.e. literals).
+		switch t := c.Type.Type; {
+		case isHex(v), hasNumericDefault(t), strings.HasPrefix(v, "(") && strings.HasSuffix(v, ")"):
+		default:
+			if _, ok := t.(*schema.TimeType); !ok || !strings.HasPrefix(strings.ToLower(v), currentTS) {
+				v = quote(v)
+			}
+		}
+		b.P("DEFAULT", v)
+	}
+}
+
 // Build instantiates a new builder and writes the given phrase to it.
 func Build(phrase string) *sqlx.Builder {
 	b := &sqlx.Builder{QuoteChar: '`'}
@@ -464,3 +480,14 @@ func quote(s string) string {
 	}
 	return strconv.Quote(s)
 }
+
+func unquote(s string) (string, error) {
+	switch {
+	case strings.HasPrefix(s, "\"") && strings.HasSuffix(s, "\""):
+		return strconv.Unquote(s)
+	case strings.HasPrefix(s, "'") && strings.HasSuffix(s, "'"):
+		return strings.ReplaceAll(s[1:len(s)-1], "''", "'"), nil
+	default:
+		return s, nil
+	}
+}
diff --git a/sql/schema/schema.go b/sql/schema/schema.go
index cce1c5c5..09479a7a 100644
--- a/sql/schema/schema.go
+++ b/sql/schema/schema.go
@@ -248,12 +248,14 @@ type (
 		expr()
 	}
 
-	// Literal represents a basic literal expression like "1".
+	// Literal represents a basic literal expression like 1, or '1'.
+	// String literals are usually quoted with single or double quotes.
 	Literal struct {
 		V string
 	}
 
-	// RawExpr represents a raw expression like "uuid()".
+	// RawExpr represents a raw expression like "uuid()" or "current_timestamp()".
+	// Unlike literals, raw expression are usually inlined as is on migration.
 	RawExpr struct {
 		X string
 	}


Test patch:
diff --git a/internal/integration/integration_test.go b/internal/integration/integration_test.go
index 054f7045..c4f5cb81 100644
--- a/internal/integration/integration_test.go
+++ b/internal/integration/integration_test.go
@@ -13,7 +13,6 @@ import (
 
 	"ariga.io/atlas/schema/schemaspec"
 	"ariga.io/atlas/sql/schema"
-
 	entsql "entgo.io/ent/dialect/sql"
 	"entgo.io/ent/entc/integration/ent"
 	"github.com/stretchr/testify/require"
@@ -170,6 +169,42 @@ func testCLISchemaApply(t T, h string, dsn string) {
 	require.NotNil(t, u)
 }
 
+func TestCLI_Version(t *testing.T) {
+	// Required to have a clean "stderr" while running first time.
+	require.NoError(t, exec.Command("go", "run", "-mod=mod", "ariga.io/atlas/cmd/atlas").Run())
+	tests := []struct {
+		name     string
+		cmd      *exec.Cmd
+		expected string
+	}{
+		{
+			name: "dev mode",
+			cmd: exec.Command("go", "run", "ariga.io/atlas/cmd/atlas",
+				"version",
+			),
+			expected: "atlas CLI version - development\nhttps://github.com/ariga/atlas/releases/tag/latest\n",
+		},
+		{
+			name: "release",
+			cmd: exec.Command("go", "run",
+				"-ldflags",
+				"-X ariga.io/atlas/cmd/action.version=v1.2.3",
+				"ariga.io/atlas/cmd/atlas",
+				"version",
+			),
+			expected: "atlas CLI version v1.2.3\nhttps://github.com/ariga/atlas/releases/tag/v1.2.3\n",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			stdout := bytes.NewBuffer(nil)
+			tt.cmd.Stdout = stdout
+			require.NoError(t, tt.cmd.Run())
+			require.Equal(t, tt.expected, stdout.String())
+		})
+	}
+}
+
 func ensureNoChange(t T, tables ...*schema.Table) {
 	realm := t.loadRealm()
 	require.Equal(t, len(realm.Schemas[0].Tables), len(tables))
diff --git a/internal/integration/mysql_test.go b/internal/integration/mysql_test.go
index 3ba4d990..958b57b6 100644
--- a/internal/integration/mysql_test.go
+++ b/internal/integration/mysql_test.go
@@ -139,8 +139,8 @@ func TestMySQL_AddIndexedColumns(t *testing.T) {
 func TestMySQL_AddColumns(t *testing.T) {
 	myRun(t, func(t *myTest) {
 		usersT := t.users()
-		t.migrate(&schema.AddTable{T: usersT})
 		t.dropTables(usersT.Name)
+		t.migrate(&schema.AddTable{T: usersT})
 		usersT.Columns = append(
 			usersT.Columns,
 			&schema.Column{Name: "a", Type: &schema.ColumnType{Raw: "tinyblob", Type: &schema.BinaryType{T: "tinyblob"}}},
@@ -148,7 +148,7 @@ func TestMySQL_AddColumns(t *testing.T) {
 			&schema.Column{Name: "c", Type: &schema.ColumnType{Raw: "blob", Type: &schema.BinaryType{T: "blob"}}},
 			&schema.Column{Name: "d", Type: &schema.ColumnType{Raw: "longblob", Type: &schema.BinaryType{T: "longblob"}}},
 			&schema.Column{Name: "e", Type: &schema.ColumnType{Raw: "binary", Type: &schema.BinaryType{T: "binary"}}},
-			&schema.Column{Name: "f", Type: &schema.ColumnType{Raw: "varbinary(255)", Type: &schema.BinaryType{T: "varbinary(255)"}}},
+			&schema.Column{Name: "f", Type: &schema.ColumnType{Raw: "varbinary(255)", Type: &schema.BinaryType{T: "varbinary(255)"}}, Default: &schema.Literal{V: "foo"}},
 			&schema.Column{Name: "g", Type: &schema.ColumnType{Type: &schema.StringType{T: "varchar", Size: 255}}},
 			&schema.Column{Name: "h", Type: &schema.ColumnType{Raw: "varchar(255)", Type: &schema.StringType{T: "varchar(255)"}}},
 			&schema.Column{Name: "i", Type: &schema.ColumnType{Raw: "tinytext", Type: &schema.StringType{T: "tinytext"}}},
@@ -595,67 +595,67 @@ create table atlas_types_sanity
 					{
 						Name:    "tBit",
 						Type:    &schema.ColumnType{Type: &mysql.BitType{T: "bit"}, Raw: "bit(10)", Null: true},
-						Default: &schema.RawExpr{X: "b'100'"},
+						Default: &schema.Literal{V: "b'100'"},
 					},
 					{
 						Name: "tInt",
 						Type: &schema.ColumnType{Type: &schema.IntegerType{T: "int", Unsigned: false},
 							Raw: t.valueByVersion(map[string]string{"8": "int"}, "int(10)"), Null: false},
-						Default: &schema.RawExpr{X: "4"},
+						Default: &schema.Literal{V: "4"},
 					},
 					{
 						Name: "tTinyInt",
 						Type: &schema.ColumnType{Type: &schema.IntegerType{T: "tinyint", Unsigned: false},
 							Raw: t.valueByVersion(map[string]string{"8": "tinyint"}, "tinyint(10)"), Null: true},
-						Default: &schema.RawExpr{X: "8"},
+						Default: &schema.Literal{V: "8"},
 					},
 					{
 						Name: "tSmallInt",
 						Type: &schema.ColumnType{Type: &schema.IntegerType{T: "smallint", Unsigned: false},
 							Raw: t.valueByVersion(map[string]string{"8": "smallint"}, "smallint(10)"), Null: true},
-						Default: &schema.RawExpr{X: "2"},
+						Default: &schema.Literal{V: "2"},
 					},
 					{
 						Name: "tMediumInt",
 						Type: &schema.ColumnType{Type: &schema.IntegerType{T: "mediumint", Unsigned: false},
 							Raw: t.valueByVersion(map[string]string{"8": "mediumint"}, "mediumint(10)"), Null: true},
-						Default: &schema.RawExpr{X: "11"},
+						Default: &schema.Literal{V: "11"},
 					},
 					{
 						Name: "tBigInt",
 						Type: &schema.ColumnType{Type: &schema.IntegerType{T: "bigint", Unsigned: false},
 							Raw: t.valueByVersion(map[string]string{"8": "bigint"}, "bigint(10)"), Null: true},
-						Default: &schema.RawExpr{X: "4"},
+						Default: &schema.Literal{V: "4"},
 					},
 					{
 						Name: "tDecimal",
 						Type: &schema.ColumnType{Type: &schema.DecimalType{T: "decimal", Precision: 10},
 							Raw: "decimal(10,0)", Null: true},
-						Default: &schema.RawExpr{X: "4"},
+						Default: &schema.Literal{V: "4"},
 					},
 					{
 						Name: "tNumeric",
 						Type: &schema.ColumnType{Type: &schema.DecimalType{T: "decimal", Precision: 10},
 							Raw: "decimal(10,0)", Null: false},
-						Default: &schema.RawExpr{X: "4"},
+						Default: &schema.Literal{V: "4"},
 					},
 					{
 						Name: "tFloat",
 						Type: &schema.ColumnType{Type: &schema.FloatType{T: "float", Precision: 10},
 							Raw: "float(10,0)", Null: true},
-						Default: &schema.RawExpr{X: "4"},
+						Default: &schema.Literal{V: "4"},
 					},
 					{
 						Name: "tDouble",
 						Type: &schema.ColumnType{Type: &schema.FloatType{T: "double", Precision: 10},
 							Raw: "double(10,0)", Null: true},
-						Default: &schema.RawExpr{X: "4"},
+						Default: &schema.Literal{V: "4"},
 					},
 					{
 						Name: "tReal",
 						Type: &schema.ColumnType{Type: &schema.FloatType{T: "double", Precision: 10},
 							Raw: "double(10,0)", Null: true},
-						Default: &schema.RawExpr{X: "4"},
+						Default: &schema.Literal{V: "4"},
 					},
 					{
 						Name: "tTimestamp",
@@ -694,7 +694,7 @@ create table atlas_types_sanity
 						Name: "tVarchar",
 						Type: &schema.ColumnType{Type: &schema.StringType{T: "varchar", Size: 10},
 							Raw: "varchar(10)", Null: true},
-						Default: &schema.RawExpr{X: "Titan"},
+						Default: &schema.Literal{V: t.quoted("Titan")},
 						Attrs: []schema.Attr{
 							&schema.Charset{V: "latin1"},
 							&schema.Collation{V: "latin1_swedish_ci"},
@@ -704,7 +704,7 @@ create table atlas_types_sanity
 						Name: "tChar",
 						Type: &schema.ColumnType{Type: &schema.StringType{T: "char", Size: 25},
 							Raw: "char(25)", Null: false},
-						Default: &schema.RawExpr{X: "Olimpia"},
+						Default: &schema.Literal{V: t.quoted("Olimpia")},
 						Attrs: []schema.Attr{
 							&schema.Charset{V: "latin1"},
 							&schema.Collation{V: "latin1_swedish_ci"},
@@ -714,13 +714,13 @@ create table atlas_types_sanity
 						Name: "tVarBinary",
 						Type: &schema.ColumnType{Type: &schema.BinaryType{T: "varbinary", Size: 30},
 							Raw: "varbinary(30)", Null: true},
-						Default: &schema.RawExpr{X: t.valueByVersion(map[string]string{"8": "0x546974616E"}, "Titan")},
+						Default: &schema.Literal{V: t.valueByVersion(map[string]string{"8": "0x546974616E"}, t.quoted("Titan"))},
 					},
 					{
 						Name: "tBinary",
 						Type: &schema.ColumnType{Type: &schema.BinaryType{T: "binary", Size: 5},
 							Raw: "binary(5)", Null: true},
-						Default: &schema.RawExpr{X: t.valueByVersion(map[string]string{"8": "0x546974616E"}, "Titan")},
+						Default: &schema.Literal{V: t.valueByVersion(map[string]string{"8": "0x546974616E"}, t.quoted("Titan"))},
 					},
 					{
 						Name: "tBlob",
@@ -1024,6 +1024,14 @@ func (t *myTest) valueByVersion(values map[string]string, defaults string) strin
 	return defaults
 }
 
+func (t *myTest) quoted(s string) string {
+	c := "\""
+	if t.mariadb() {
+		c = "'"
+	}
+	return c + s + c
+}
+
 func (t *myTest) loadRealm() *schema.Realm {
 	r, err := t.drv.InspectRealm(context.Background(), &schema.InspectRealmOption{
 		Schemas: []string{"test"},
diff --git a/sql/mysql/inspect_test.go b/sql/mysql/inspect_test.go
index 0fad1979..e0389660 100644
--- a/sql/mysql/inspect_test.go
+++ b/sql/mysql/inspect_test.go
@@ -208,8 +208,8 @@ func TestDriver_InspectTable(t *testing.T) {
 				require.NoError(err)
 				require.Equal("users", t.Name)
 				require.EqualValues([]*schema.Column{
-					{Name: "d1", Type: &schema.ColumnType{Raw: "decimal(10,2)", Type: &schema.DecimalType{T: "decimal", Precision: 10, Scale: 2}}, Default: &schema.RawExpr{X: "10.20"}},
-					{Name: "d2", Type: &schema.ColumnType{Raw: "decimal(10,0)", Type: &schema.DecimalType{T: "decimal", Precision: 10, Scale: 0}}, Default: &schema.RawExpr{X: "10"}},
+					{Name: "d1", Type: &schema.ColumnType{Raw: "decimal(10,2)", Type: &schema.DecimalType{T: "decimal", Precision: 10, Scale: 2}}, Default: &schema.Literal{V: "10.20"}},
+					{Name: "d2", Type: &schema.ColumnType{Raw: "decimal(10,0)", Type: &schema.DecimalType{T: "decimal", Precision: 10, Scale: 0}}, Default: &schema.Literal{V: "10"}},
 				}, t.Columns)
 			},
 		},
@@ -301,7 +301,7 @@ func TestDriver_InspectTable(t *testing.T) {
 				require.NoError(err)
 				require.Equal("users", t.Name)
 				require.EqualValues([]*schema.Column{
-					{Name: "c1", Type: &schema.ColumnType{Raw: "char(20)", Type: &schema.StringType{T: "char", Size: 20}}, Default: &schema.RawExpr{X: "char"}},
+					{Name: "c1", Type: &schema.ColumnType{Raw: "char(20)", Type: &schema.StringType{T: "char", Size: 20}}, Default: &schema.Literal{V: `"char"`}},
 					{Name: "c2", Type: &schema.ColumnType{Raw: "varchar(30)", Type: &schema.StringType{T: "varchar", Size: 30}}},
 					{Name: "c3", Type: &schema.ColumnType{Raw: "tinytext", Type: &schema.StringType{T: "tinytext"}}},
 					{Name: "c4", Type: &schema.ColumnType{Raw: "mediumtext", Type: &schema.StringType{T: "mediumtext"}}},
@@ -333,7 +333,7 @@ func TestDriver_InspectTable(t *testing.T) {
 				require.Equal("users", t.Name)
 				require.EqualValues([]*schema.Column{
 					{Name: "c1", Type: &schema.ColumnType{Raw: "enum('a','b')", Type: &schema.EnumType{Values: []string{"a", "b"}}}, Attrs: []schema.Attr{&schema.Charset{V: "latin1"}, &schema.Collation{V: "latin1_swedish_ci"}}},
-					{Name: "c2", Type: &schema.ColumnType{Raw: "enum('c','d')", Type: &schema.EnumType{Values: []string{"c", "d"}}}, Default: &schema.RawExpr{X: "d"}, Attrs: []schema.Attr{&schema.Charset{V: "latin1"}, &schema.Collation{V: "latin1_swedish_ci"}}},
+					{Name: "c2", Type: &schema.ColumnType{Raw: "enum('c','d')", Type: &schema.EnumType{Values: []string{"c", "d"}}}, Default: &schema.Literal{V: `"d"`}, Attrs: []schema.Attr{&schema.Charset{V: "latin1"}, &schema.Collation{V: "latin1_swedish_ci"}}},
 				}, t.Columns)
 			},
 		},
diff --git a/sql/mysql/migrate_test.go b/sql/mysql/migrate_test.go
index e898f660..712fa5b6 100644
--- a/sql/mysql/migrate_test.go
+++ b/sql/mysql/migrate_test.go
@@ -16,7 +16,7 @@ import (
 	"github.com/stretchr/testify/require"
 )
 
-func TestMigrate_Exec(t *testing.T) {
+func TestMigrate_ApplyChanges(t *testing.T) {
 	migrate, mk, err := newMigrate("8.0.13")
 	require.NoError(t, err)
 	mk.ExpectExec(sqltest.Escape("CREATE DATABASE `test` CHARACTER SET latin")).
@@ -27,7 +27,7 @@ func TestMigrate_Exec(t *testing.T) {
 		WillReturnResult(sqlmock.NewResult(0, 0))
 	mk.ExpectExec(sqltest.Escape("DROP TABLE `public`.`pets` IF EXISTS")).
 		WillReturnResult(sqlmock.NewResult(0, 0))
-	mk.ExpectExec(sqltest.Escape("CREATE TABLE `pets` IF NOT EXISTS (`a` int NOT NULL, `b` bigint NOT NULL, `c` bigint NULL, PRIMARY KEY (`a`, `b`), UNIQUE INDEX `b_c_unique` (`b`, `c`) COMMENT \"comment\")")).
+	mk.ExpectExec(sqltest.Escape("CREATE TABLE `pets` IF NOT EXISTS (`a` int NOT NULL DEFAULT (int(rand())), `b` bigint NOT NULL DEFAULT 1, `c` bigint NULL, PRIMARY KEY (`a`, `b`), UNIQUE INDEX `b_c_unique` (`b`, `c`) COMMENT \"comment\")")).
 		WillReturnResult(sqlmock.NewResult(0, 0))
 	mk.ExpectExec(sqltest.Escape("ALTER TABLE `users` DROP INDEX `id_spouse_id`")).
 		WillReturnResult(sqlmock.NewResult(0, 0))
@@ -47,8 +47,8 @@ func TestMigrate_Exec(t *testing.T) {
 				t := &schema.Table{
 					Name: "pets",
 					Columns: []*schema.Column{
-						{Name: "a", Type: &schema.ColumnType{Raw: "int", Type: &schema.IntegerType{T: "int"}}},
-						{Name: "b", Type: &schema.ColumnType{Raw: "bigint", Type: &schema.IntegerType{T: "bigint"}}},
+						{Name: "a", Type: &schema.ColumnType{Raw: "int", Type: &schema.IntegerType{T: "int"}}, Default: &schema.RawExpr{X: "(int(rand()))"}},
+						{Name: "b", Type: &schema.ColumnType{Raw: "bigint", Type: &schema.IntegerType{T: "bigint"}}, Default: &schema.Literal{V: "1"}},
 						{Name: "c", Type: &schema.ColumnType{Raw: "bigint", Type: &schema.IntegerType{T: "bigint"}, Null: true}},
 					},
 				}