Repository: ariga/atlas
Pull request #865 (2022-06-14 20:29:57)

sql/mysql: support column-level charset and collate diffing


Patch:
diff --git a/sql/internal/sqlx/diff.go b/sql/internal/sqlx/diff.go
index 8dfa6951..aaacc808 100644
--- a/sql/internal/sqlx/diff.go
+++ b/sql/internal/sqlx/diff.go
@@ -36,7 +36,7 @@ type (
 		TableAttrDiff(from, to *schema.Table) ([]schema.Change, error)
 
 		// ColumnChange returns the schema changes (if any) for migrating one column to the other.
-		ColumnChange(from, to *schema.Column) (schema.ChangeKind, error)
+		ColumnChange(fromT *schema.Table, from, to *schema.Column) (schema.ChangeKind, error)
 
 		// IndexAttrChanged reports if the index attributes were changed.
 		// For example, an index type or predicate (for partial indexes).
@@ -64,7 +64,7 @@ type (
 	// If the DiffDriver implements the Normalizer interface, TableDiff normalizes its table
 	// inputs before starting the diff process.
 	Normalizer interface {
-		Normalize(from, to *schema.Table)
+		Normalize(from, to *schema.Table) error
 	}
 )
 
@@ -148,7 +148,9 @@ func (d *Diff) TableDiff(from, to *schema.Table) ([]schema.Change, error) {
 	}
 	// Normalizing tables before starting the diff process.
 	if n, ok := d.DiffDriver.(Normalizer); ok {
-		n.Normalize(from, to)
+		if err := n.Normalize(from, to); err != nil {
+			return nil, err
+		}
 	}
 	var changes []schema.Change
 	if from.Name != to.Name {
@@ -173,7 +175,7 @@ func (d *Diff) TableDiff(from, to *schema.Table) ([]schema.Change, error) {
 			changes = append(changes, &schema.DropColumn{C: c1})
 			continue
 		}
-		change, err := d.ColumnChange(c1, c2)
+		change, err := d.ColumnChange(from, c1, c2)
 		if err != nil {
 			return nil, err
 		}
diff --git a/sql/mysql/diff.go b/sql/mysql/diff.go
index c503bb13..54b3e48f 100644
--- a/sql/mysql/diff.go
+++ b/sql/mysql/diff.go
@@ -10,13 +10,23 @@ import (
 	"reflect"
 	"strconv"
 	"strings"
+	"sync"
 
 	"ariga.io/atlas/sql/internal/sqlx"
 	"ariga.io/atlas/sql/schema"
 )
 
 // A diff provides a MySQL implementation for sqlx.DiffDriver.
-type diff struct{ conn }
+type diff struct {
+	conn
+	// charset to collation mapping.
+	// See, internal directory.
+	ch2co, co2ch struct {
+		sync.Once
+		v   map[string]string
+		err error
+	}
+}
 
 // SchemaAttrDiff returns a changeset for migrating schema attributes from one state to the other.
 func (d *diff) SchemaAttrDiff(from, to *schema.Schema) []schema.Change {
@@ -79,7 +89,7 @@ func (d *diff) TableAttrDiff(from, to *schema.Table) ([]schema.Change, error) {
 }
 
 // ColumnChange returns the schema changes (if any) for migrating one column to the other.
-func (d *diff) ColumnChange(from, to *schema.Column) (schema.ChangeKind, error) {
+func (d *diff) ColumnChange(fromT *schema.Table, from, to *schema.Column) (schema.ChangeKind, error) {
 	change := sqlx.CommentChange(from.Attrs, to.Attrs)
 	if from.Type.Null != to.Type.Null {
 		change |= schema.ChangeNull
@@ -103,6 +113,18 @@ func (d *diff) ColumnChange(from, to *schema.Column) (schema.ChangeKind, error)
 	if changed {
 		change |= schema.ChangeGenerated
 	}
+	if changed, err = d.columnCharsetChanged(fromT, from, to); err != nil {
+		return schema.NoChange, err
+	}
+	if changed {
+		change |= schema.ChangeCharset
+	}
+	if changed, err = d.columnCollateChanged(fromT, from, to); err != nil {
+		return schema.NoChange, err
+	}
+	if changed {
+		change |= schema.ChangeCollate
+	}
 	return change, nil
 }
 
@@ -161,7 +183,7 @@ func (*diff) ReferenceChanged(from, to schema.ReferenceOption) bool {
 }
 
 // Normalize implements the sqlx.Normalizer interface.
-func (*diff) Normalize(from, to *schema.Table) {
+func (d *diff) Normalize(from, to *schema.Table) error {
 	indexes := make([]*schema.Index, 0, len(from.Indexes))
 	for _, idx := range from.Indexes {
 		// MySQL requires that foreign key columns be indexed; Therefore, if the child
@@ -175,6 +197,13 @@ func (*diff) Normalize(from, to *schema.Table) {
 		}
 	}
 	from.Indexes = indexes
+
+	// Avoid proposing changes to the table COLLATE or CHARSET
+	// in case only one of these properties is defined.
+	if err := d.defaultCollate(&to.Attrs); err != nil {
+		return err
+	}
+	return d.defaultCharset(&to.Attrs)
 }
 
 // collationChange returns the schema change for migrating the collation if
@@ -235,6 +264,46 @@ func (*diff) charsetChange(from, top, to []schema.Attr) schema.Change {
 	return noChange
 }
 
+// columnCharsetChange indicates if there is a change to the column charset.
+func (d *diff) columnCharsetChanged(fromT *schema.Table, from, to *schema.Column) (bool, error) {
+	if err := d.defaultCharset(&to.Attrs); err != nil {
+		return false, err
+	}
+	var (
+		fromC, topC, toC       schema.Charset
+		fromHas, topHas, toHas = sqlx.Has(from.Attrs, &fromC), sqlx.Has(fromT.Attrs, &topC), sqlx.Has(to.Attrs, &toC)
+	)
+	// Column was updated with custom CHARSET that was dropped.
+	// Hence, we should revert to the one defined on the table.
+	return fromHas && !toHas && topHas && fromC.V != topC.V ||
+		// Custom CHARSET was added to the column. Hence,
+		// Does not match the one defined in the table.
+		!fromHas && toHas && topHas && toC.V != topC.V ||
+		// CHARSET was explicitly changed.
+		fromHas && toHas && fromC.V != toC.V, nil
+
+}
+
+// columnCollateChanged indicates if there is a change to the column charset.
+func (d *diff) columnCollateChanged(fromT *schema.Table, from, to *schema.Column) (bool, error) {
+	if err := d.defaultCollate(&to.Attrs); err != nil {
+		return false, err
+	}
+	var (
+		fromC, topC, toC       schema.Collation
+		fromHas, topHas, toHas = sqlx.Has(from.Attrs, &fromC), sqlx.Has(fromT.Attrs, &topC), sqlx.Has(to.Attrs, &toC)
+	)
+	// Column was updated with custom COLLATE that was dropped.
+	// Hence, we should revert to the one defined on the table.
+	return fromHas && !toHas && topHas && fromC.V != topC.V ||
+		// Custom COLLATE was added to the column. Hence,
+		// Does not match the one defined in the table.
+		!fromHas && toHas && topHas && toC.V != topC.V ||
+		// COLLATE was explicitly changed.
+		fromHas && toHas && fromC.V != toC.V, nil
+
+}
+
 // autoIncChange returns the schema change for changing the AUTO_INCREMENT
 // attribute in case it is not the default.
 func (*diff) autoIncChange(from, to []schema.Attr) schema.Change {
@@ -487,3 +556,45 @@ search:
 	}
 	return false
 }
+
+// defaultCollate appends the default COLLATE to the attributes in case a
+// custom character-set was defined for the element and the COLLATE was not.
+func (d *diff) defaultCollate(attrs *[]schema.Attr) error {
+	var charset schema.Charset
+	if !sqlx.Has(*attrs, &charset) || sqlx.Has(*attrs, &schema.Collation{}) {
+		return nil
+	}
+	d.ch2co.Do(func() {
+		d.ch2co.v, d.ch2co.err = d.charsetToCollate()
+	})
+	if d.ch2co.err != nil {
+		return d.ch2co.err
+	}
+	v, ok := d.ch2co.v[charset.V]
+	if !ok {
+		return fmt.Errorf("mysql: unknown character set: %q", charset.V)
+	}
+	schema.ReplaceOrAppend(attrs, &schema.Collation{V: v})
+	return nil
+}
+
+// defaultCharset appends the default CHARSET to the attributes in case a
+// custom collation was defined for the element and the CHARSET was not.
+func (d *diff) defaultCharset(attrs *[]schema.Attr) error {
+	var collate schema.Collation
+	if !sqlx.Has(*attrs, &collate) || sqlx.Has(*attrs, &schema.Charset{}) {
+		return nil
+	}
+	d.co2ch.Do(func() {
+		d.co2ch.v, d.co2ch.err = d.collateToCharset()
+	})
+	if d.co2ch.err != nil {
+		return d.co2ch.err
+	}
+	v, ok := d.co2ch.v[collate.V]
+	if !ok {
+		return fmt.Errorf("mysql: unknown collation: %q", collate.V)
+	}
+	schema.ReplaceOrAppend(attrs, &schema.Charset{V: v})
+	return nil
+}
diff --git a/sql/mysql/driver.go b/sql/mysql/driver.go
index cda7ce14..b2298aa2 100644
--- a/sql/mysql/driver.go
+++ b/sql/mysql/driver.go
@@ -6,6 +6,8 @@ package mysql
 
 import (
 	"context"
+	"embed"
+	"encoding/json"
 	"fmt"
 	"net/url"
 	"strings"
@@ -61,14 +63,14 @@ func Open(db schema.ExecQuerier) (migrate.Driver, error) {
 	if c.tidb() {
 		return &Driver{
 			conn:        c,
-			Differ:      &sqlx.Diff{DiffDriver: &tdiff{diff{c}}},
+			Differ:      &sqlx.Diff{DiffDriver: &tdiff{diff{conn: c}}},
 			Inspector:   &tinspect{inspect{c}},
 			PlanApplier: &tplanApply{planApply{c}},
 		}, nil
 	}
 	return &Driver{
 		conn:        c,
-		Differ:      &sqlx.Diff{DiffDriver: &diff{c}},
+		Differ:      &sqlx.Diff{DiffDriver: &diff{conn: c}},
 		Inspector:   &inspect{c},
 		PlanApplier: &planApply{c},
 	}, nil
@@ -133,89 +135,107 @@ func acquire(ctx context.Context, conn schema.ExecQuerier, name string, timeout
 
 // supportsCheck reports if the connected database supports
 // the CHECK clause, and return the querying for getting them.
-func (d *conn) supportsCheck() (string, bool) {
+func (c *conn) supportsCheck() (string, bool) {
 	v, q := "8.0.16", myChecksQuery
-	if d.mariadb() {
+	if c.mariadb() {
 		v, q = "10.2.1", marChecksQuery
 	}
-	return q, d.gteV(v)
+	return q, c.gteV(v)
 }
 
 // supportsIndexExpr reports if the connected database supports
 // index expressions (functional key part).
-func (d *conn) supportsIndexExpr() bool {
-	return !d.mariadb() && d.gteV("8.0.13")
+func (c *conn) supportsIndexExpr() bool {
+	return !c.mariadb() && c.gteV("8.0.13")
 }
 
 // supportsDisplayWidth reports if the connected database supports
 // getting the display width information from the information schema.
-func (d *conn) supportsDisplayWidth() bool {
+func (c *conn) supportsDisplayWidth() bool {
 	// MySQL v8.0.19 dropped the display width
 	// information from the information schema
-	return d.mariadb() || d.ltV("8.0.19")
+	return c.mariadb() || c.ltV("8.0.19")
 }
 
 // supportsExprDefault reports if the connected database supports
 // expressions in the DEFAULT clause on column definition.
-func (d *conn) supportsExprDefault() bool {
+func (c *conn) supportsExprDefault() bool {
 	v := "8.0.13"
-	if d.mariadb() {
+	if c.mariadb() {
 		v = "10.2.1"
 	}
-	return d.gteV(v)
+	return c.gteV(v)
 }
 
 // supportsEnforceCheck reports if the connected database supports
 // the ENFORCED option in CHECK constraint syntax.
-func (d *conn) supportsEnforceCheck() bool {
-	return !d.mariadb() && d.gteV("8.0.16")
+func (c *conn) supportsEnforceCheck() bool {
+	return !c.mariadb() && c.gteV("8.0.16")
 }
 
 // supportsGeneratedColumns reports if the connected database
 // supports the generated columns in information schema.
-func (d *conn) supportsGeneratedColumns() bool {
+func (c *conn) supportsGeneratedColumns() bool {
 	v := "5.7"
-	if d.mariadb() {
+	if c.mariadb() {
 		v = "10.2"
 	}
-	return d.gteV(v)
+	return c.gteV(v)
 }
 
 // supportsRenameColumn reports if the connected database
 // supports the "RENAME COLUMN" clause.
-func (d *conn) supportsRenameColumn() bool {
+func (c *conn) supportsRenameColumn() bool {
 	v := "8"
-	if d.mariadb() {
+	if c.mariadb() {
 		v = "10.5.2"
 	}
-	return d.gteV(v)
+	return c.gteV(v)
+}
+
+// charsetToCollate returns the mapping from charset to its default collation.
+func (c *conn) charsetToCollate() (map[string]string, error) {
+	name := "internal/charset2collate"
+	if c.mariadb() {
+		name += ".maria"
+	}
+	return decode(name)
+}
+
+// collateToCharset returns the mapping from a collation to its charset.
+func (c *conn) collateToCharset() (map[string]string, error) {
+	name := "internal/collate2charset"
+	if c.mariadb() {
+		name += ".maria"
+	}
+	return decode(name)
 }
 
 // mariadb reports if the Driver is connected to a MariaDB database.
-func (d *conn) mariadb() bool {
-	return strings.Index(d.version, "MariaDB") > 0
+func (c *conn) mariadb() bool {
+	return strings.Index(c.version, "MariaDB") > 0
 }
 
 // tidb reports if the Driver is connected to a TiDB database.
-func (d *conn) tidb() bool {
-	return strings.Index(d.version, "TiDB") > 0
+func (c *conn) tidb() bool {
+	return strings.Index(c.version, "TiDB") > 0
 }
 
 // compareV returns an integer comparing two versions according to
 // semantic version precedence.
-func (d *conn) compareV(w string) int {
-	v := d.version
-	if d.mariadb() {
+func (c *conn) compareV(w string) int {
+	v := c.version
+	if c.mariadb() {
 		v = v[:strings.Index(v, "MariaDB")-1]
 	}
 	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 }
+func (c *conn) gteV(w string) bool { return c.compareV(w) >= 0 }
 
 // ltV reports if the connection version is < w.
-func (d *conn) ltV(w string) bool { return d.compareV(w) == -1 }
+func (c *conn) ltV(w string) bool { return c.compareV(w) == -1 }
 
 // unescape strings with backslashes returned
 // for SQL expressions from information schema.
@@ -276,6 +296,21 @@ func dsn(u *url.URL) string {
 	return b.String()
 }
 
+//go:embed internal/*
+var encoding embed.FS
+
+func decode(name string) (map[string]string, error) {
+	f, err := encoding.Open(name)
+	if err != nil {
+		return nil, err
+	}
+	var m map[string]string
+	if err := json.NewDecoder(f).Decode(&m); err != nil {
+		return nil, fmt.Errorf("decode %q", name)
+	}
+	return m, nil
+}
+
 // MySQL standard column types as defined in its codebase. Name and order
 // is organized differently than MySQL.
 //
diff --git a/sql/mysql/internal/.README.md b/sql/mysql/internal/.README.md
new file mode 100644
index 00000000..cd483b32
--- /dev/null
+++ b/sql/mysql/internal/.README.md
@@ -0,0 +1,14 @@
+## Charset and Collation of MySQL and MariaDB latest versions
+
+`collate2charset` and `collate2charset.maria` hold a mapping from the collation to their charset.
+
+```sql
+select json_objectagg(collation_name, character_set_name) from information_schema.collations\G;
+```
+
+`charset2collate` and `charset2collate.maria` hold a mapping from the charset to its default collation extracted
+by the following query:
+
+```sql
+select json_objectagg(character_set_name, default_collate_name) from information_schema.character_sets\G;
+```
\ No newline at end of file
diff --git a/sql/mysql/internal/charset2collate b/sql/mysql/internal/charset2collate
new file mode 100644
index 00000000..ee694a76
--- /dev/null
+++ b/sql/mysql/internal/charset2collate
@@ -0,0 +1,1 @@
+{"gbk": "gbk_chinese_ci", "hp8": "hp8_english_ci", "big5": "big5_chinese_ci", "dec8": "dec8_swedish_ci", "sjis": "sjis_japanese_ci", "swe7": "swe7_swedish_ci", "ucs2": "ucs2_general_ci", "ujis": "ujis_japanese_ci", "utf8": "utf8_general_ci", "ascii": "ascii_general_ci", "cp850": "cp850_general_ci", "cp852": "cp852_general_ci", "cp866": "cp866_general_ci", "cp932": "cp932_japanese_ci", "euckr": "euckr_korean_ci", "greek": "greek_general_ci", "koi8r": "koi8r_general_ci", "koi8u": "koi8u_general_ci", "macce": "macce_general_ci", "utf16": "utf16_general_ci", "utf32": "utf32_general_ci", "binary": "binary", "cp1250": "cp1250_general_ci", "cp1251": "cp1251_general_ci", "cp1256": "cp1256_general_ci", "cp1257": "cp1257_general_ci", "gb2312": "gb2312_chinese_ci", "hebrew": "hebrew_general_ci", "latin1": "latin1_swedish_ci", "latin2": "latin2_general_ci", "latin5": "latin5_turkish_ci", "latin7": "latin7_general_ci", "tis620": "tis620_thai_ci", "eucjpms": "eucjpms_japanese_ci", "gb18030": "gb18030_chinese_ci", "geostd8": "geostd8_general_ci", "keybcs2": "keybcs2_general_ci", "utf16le": "utf16le_general_ci", "utf8mb4": "utf8mb4_0900_ai_ci", "armscii8": "armscii8_general_ci", "macroman": "macroman_general_ci"}
\ No newline at end of file
diff --git a/sql/mysql/internal/charset2collate.maria b/sql/mysql/internal/charset2collate.maria
new file mode 100644
index 00000000..a78a7b48
--- /dev/null
+++ b/sql/mysql/internal/charset2collate.maria
@@ -0,0 +1,1 @@
+{"big5":"big5_chinese_ci", "dec8":"dec8_swedish_ci", "cp850":"cp850_general_ci", "hp8":"hp8_english_ci", "koi8r":"koi8r_general_ci", "latin1":"latin1_swedish_ci", "latin2":"latin2_general_ci", "swe7":"swe7_swedish_ci", "ascii":"ascii_general_ci", "ujis":"ujis_japanese_ci", "sjis":"sjis_japanese_ci", "hebrew":"hebrew_general_ci", "tis620":"tis620_thai_ci", "euckr":"euckr_korean_ci", "koi8u":"koi8u_general_ci", "gb2312":"gb2312_chinese_ci", "greek":"greek_general_ci", "cp1250":"cp1250_general_ci", "gbk":"gbk_chinese_ci", "latin5":"latin5_turkish_ci", "armscii8":"armscii8_general_ci", "utf8mb3":"utf8mb3_general_ci", "ucs2":"ucs2_general_ci", "cp866":"cp866_general_ci", "keybcs2":"keybcs2_general_ci", "macce":"macce_general_ci", "macroman":"macroman_general_ci", "cp852":"cp852_general_ci", "latin7":"latin7_general_ci", "utf8mb4":"utf8mb4_general_ci", "cp1251":"cp1251_general_ci", "utf16":"utf16_general_ci", "utf16le":"utf16le_general_ci", "cp1256":"cp1256_general_ci", "cp1257":"cp1257_general_ci", "utf32":"utf32_general_ci", "binary":"binary", "geostd8":"geostd8_general_ci", "cp932":"cp932_japanese_ci", "eucjpms":"eucjpms_japanese_ci"}
\ No newline at end of file
diff --git a/sql/mysql/internal/collate2charset b/sql/mysql/internal/collate2charset
new file mode 100644
index 00000000..92698cc5
--- /dev/null
+++ b/sql/mysql/internal/collate2charset
@@ -0,0 +1,1 @@
+{"binary": "binary", "gbk_bin": "gbk", "hp8_bin": "hp8", "big5_bin": "big5", "dec8_bin": "dec8", "sjis_bin": "sjis", "swe7_bin": "swe7", "ucs2_bin": "ucs2", "ujis_bin": "ujis", "utf8_bin": "utf8", "ascii_bin": "ascii", "cp850_bin": "cp850", "cp852_bin": "cp852", "cp866_bin": "cp866", "cp932_bin": "cp932", "euckr_bin": "euckr", "greek_bin": "greek", "koi8r_bin": "koi8r", "koi8u_bin": "koi8u", "macce_bin": "macce", "utf16_bin": "utf16", "utf32_bin": "utf32", "cp1250_bin": "cp1250", "cp1251_bin": "cp1251", "cp1256_bin": "cp1256", "cp1257_bin": "cp1257", "gb2312_bin": "gb2312", "hebrew_bin": "hebrew", "latin1_bin": "latin1", "latin2_bin": "latin2", "latin5_bin": "latin5", "latin7_bin": "latin7", "tis620_bin": "tis620", "eucjpms_bin": "eucjpms", "gb18030_bin": "gb18030", "geostd8_bin": "geostd8", "keybcs2_bin": "keybcs2", "utf16le_bin": "utf16le", "utf8mb4_bin": "utf8mb4", "armscii8_bin": "armscii8", "macroman_bin": "macroman", "ucs2_czech_ci": "ucs2", "ucs2_roman_ci": "ucs2", "utf8_czech_ci": "utf8", "utf8_roman_ci": "utf8", "gbk_chinese_ci": "gbk", "hp8_english_ci": "hp8", "tis620_thai_ci": "tis620", "ucs2_danish_ci": "ucs2", "ucs2_polish_ci": "ucs2", "ucs2_slovak_ci": "ucs2", "utf16_czech_ci": "utf16", "utf16_roman_ci": "utf16", "utf32_czech_ci": "utf32", "utf32_roman_ci": "utf32", "utf8_danish_ci": "utf8", "utf8_polish_ci": "utf8", "utf8_slovak_ci": "utf8", "big5_chinese_ci": "big5", "cp1250_czech_cs": "cp1250", "dec8_swedish_ci": "dec8", "euckr_korean_ci": "euckr", "latin2_czech_cs": "latin2", "swe7_swedish_ci": "swe7", "ucs2_general_ci": "ucs2", "ucs2_german2_ci": "ucs2", "ucs2_latvian_ci": "ucs2", "ucs2_persian_ci": "ucs2", "ucs2_sinhala_ci": "ucs2", "ucs2_spanish_ci": "ucs2", "ucs2_swedish_ci": "ucs2", "ucs2_turkish_ci": "ucs2", "ucs2_unicode_ci": "ucs2", "utf16_danish_ci": "utf16", "utf16_polish_ci": "utf16", "utf16_slovak_ci": "utf16", "utf32_danish_ci": "utf32", "utf32_polish_ci": "utf32", "utf32_slovak_ci": "utf32", "utf8_general_ci": "utf8", "utf8_german2_ci": "utf8", "utf8_latvian_ci": "utf8", "utf8_persian_ci": "utf8", "utf8_sinhala_ci": "utf8", "utf8_spanish_ci": "utf8", "utf8_swedish_ci": "utf8", "utf8_tolower_ci": "utf8", "utf8_turkish_ci": "utf8", "utf8_unicode_ci": "utf8", "ascii_general_ci": "ascii", "cp1250_polish_ci": "cp1250", "cp850_general_ci": "cp850", "cp852_general_ci": "cp852", "cp866_general_ci": "cp866", "greek_general_ci": "greek", "koi8r_general_ci": "koi8r", "koi8u_general_ci": "koi8u", "latin1_danish_ci": "latin1", "macce_general_ci": "macce", "sjis_japanese_ci": "sjis", "ucs2_croatian_ci": "ucs2", "ucs2_estonian_ci": "ucs2", "ucs2_romanian_ci": "ucs2", "ucs2_spanish2_ci": "ucs2", "ujis_japanese_ci": "ujis", "utf16_general_ci": "utf16", "utf16_german2_ci": "utf16", "utf16_latvian_ci": "utf16", "utf16_persian_ci": "utf16", "utf16_sinhala_ci": "utf16", "utf16_spanish_ci": "utf16", "utf16_swedish_ci": "utf16", "utf16_turkish_ci": "utf16", "utf16_unicode_ci": "utf16", "utf32_general_ci": "utf32", "utf32_german2_ci": "utf32", "utf32_latvian_ci": "utf32", "utf32_persian_ci": "utf32", "utf32_sinhala_ci": "utf32", "utf32_spanish_ci": "utf32", "utf32_swedish_ci": "utf32", "utf32_turkish_ci": "utf32", "utf32_unicode_ci": "utf32", "utf8_croatian_ci": "utf8", "utf8_estonian_ci": "utf8", "utf8_romanian_ci": "utf8", "utf8_spanish2_ci": "utf8", "utf8mb4_0900_bin": "utf8mb4", "utf8mb4_czech_ci": "utf8mb4", "utf8mb4_roman_ci": "utf8mb4", "cp1250_general_ci": "cp1250", "cp1251_general_ci": "cp1251", "cp1251_general_cs": "cp1251", "cp1256_general_ci": "cp1256", "cp1257_general_ci": "cp1257", "cp932_japanese_ci": "cp932", "gb2312_chinese_ci": "gb2312", "hebrew_general_ci": "hebrew", "latin1_general_ci": "latin1", "latin1_general_cs": "latin1", "latin1_german1_ci": "latin1", "latin1_german2_ci": "latin1", "latin1_spanish_ci": "latin1", "latin1_swedish_ci": "latin1", "latin2_general_ci": "latin2", "latin5_turkish_ci": "latin5", "latin7_general_ci": "latin7", "latin7_general_cs": "latin7", "ucs2_esperanto_ci": "ucs2", "ucs2_hungarian_ci": "ucs2", "ucs2_icelandic_ci": "ucs2", "ucs2_slovenian_ci": "ucs2", "utf16_croatian_ci": "utf16", "utf16_estonian_ci": "utf16", "utf16_romanian_ci": "utf16", "utf16_spanish2_ci": "utf16", "utf32_croatian_ci": "utf32", "utf32_estonian_ci": "utf32", "utf32_romanian_ci": "utf32", "utf32_spanish2_ci": "utf32", "utf8_esperanto_ci": "utf8", "utf8_hungarian_ci": "utf8", "utf8_icelandic_ci": "utf8", "utf8_slovenian_ci": "utf8", "utf8mb4_danish_ci": "utf8mb4", "utf8mb4_polish_ci": "utf8mb4", "utf8mb4_slovak_ci": "utf8mb4", "cp1250_croatian_ci": "cp1250", "gb18030_chinese_ci": "gb18030", "geostd8_general_ci": "geostd8", "keybcs2_general_ci": "keybcs2", "latin2_croatian_ci": "latin2", "latin7_estonian_cs": "latin7", "ucs2_lithuanian_ci": "ucs2", "ucs2_vietnamese_ci": "ucs2", "utf16_esperanto_ci": "utf16", "utf16_hungarian_ci": "utf16", "utf16_icelandic_ci": "utf16", "utf16_slovenian_ci": "utf16", "utf16le_general_ci": "utf16le", "utf32_esperanto_ci": "utf32", "utf32_hungarian_ci": "utf32", "utf32_icelandic_ci": "utf32", "utf32_slovenian_ci": "utf32", "utf8_lithuanian_ci": "utf8", "utf8_vietnamese_ci": "utf8", "utf8mb4_0900_ai_ci": "utf8mb4", "utf8mb4_0900_as_ci": "utf8mb4", "utf8mb4_0900_as_cs": "utf8mb4", "utf8mb4_general_ci": "utf8mb4", "utf8mb4_german2_ci": "utf8mb4", "utf8mb4_latvian_ci": "utf8mb4", "utf8mb4_persian_ci": "utf8mb4", "utf8mb4_sinhala_ci": "utf8mb4", "utf8mb4_spanish_ci": "utf8mb4", "utf8mb4_swedish_ci": "utf8mb4", "utf8mb4_turkish_ci": "utf8mb4", "utf8mb4_unicode_ci": "utf8mb4", "armscii8_general_ci": "armscii8", "cp1251_bulgarian_ci": "cp1251", "cp1251_ukrainian_ci": "cp1251", "eucjpms_japanese_ci": "eucjpms", "latin2_hungarian_ci": "latin2", "macroman_general_ci": "macroman", "ucs2_unicode_520_ci": "ucs2", "utf16_lithuanian_ci": "utf16", "utf16_vietnamese_ci": "utf16", "utf32_lithuanian_ci": "utf32", "utf32_vietnamese_ci": "utf32", "utf8_unicode_520_ci": "utf8", "utf8mb4_croatian_ci": "utf8mb4", "utf8mb4_estonian_ci": "utf8mb4", "utf8mb4_romanian_ci": "utf8mb4", "utf8mb4_spanish2_ci": "utf8mb4", "cp1257_lithuanian_ci": "cp1257", "utf16_unicode_520_ci": "utf16", "utf32_unicode_520_ci": "utf32", "utf8mb4_esperanto_ci": "utf8mb4", "utf8mb4_hungarian_ci": "utf8mb4", "utf8mb4_icelandic_ci": "utf8mb4", "utf8mb4_slovenian_ci": "utf8mb4", "utf8mb4_cs_0900_ai_ci": "utf8mb4", "utf8mb4_cs_0900_as_cs": "utf8mb4", "utf8mb4_da_0900_ai_ci": "utf8mb4", "utf8mb4_da_0900_as_cs": "utf8mb4", "utf8mb4_eo_0900_ai_ci": "utf8mb4", "utf8mb4_eo_0900_as_cs": "utf8mb4", "utf8mb4_es_0900_ai_ci": "utf8mb4", "utf8mb4_es_0900_as_cs": "utf8mb4", "utf8mb4_et_0900_ai_ci": "utf8mb4", "utf8mb4_et_0900_as_cs": "utf8mb4", "utf8mb4_hr_0900_ai_ci": "utf8mb4", "utf8mb4_hr_0900_as_cs": "utf8mb4", "utf8mb4_hu_0900_ai_ci": "utf8mb4", "utf8mb4_hu_0900_as_cs": "utf8mb4", "utf8mb4_is_0900_ai_ci": "utf8mb4", "utf8mb4_is_0900_as_cs": "utf8mb4", "utf8mb4_ja_0900_as_cs": "utf8mb4", "utf8mb4_la_0900_ai_ci": "utf8mb4", "utf8mb4_la_0900_as_cs": "utf8mb4", "utf8mb4_lithuanian_ci": "utf8mb4", "utf8mb4_lt_0900_ai_ci": "utf8mb4", "utf8mb4_lt_0900_as_cs": "utf8mb4", "utf8mb4_lv_0900_ai_ci": "utf8mb4", "utf8mb4_lv_0900_as_cs": "utf8mb4", "utf8mb4_pl_0900_ai_ci": "utf8mb4", "utf8mb4_pl_0900_as_cs": "utf8mb4", "utf8mb4_ro_0900_ai_ci": "utf8mb4", "utf8mb4_ro_0900_as_cs": "utf8mb4", "utf8mb4_ru_0900_ai_ci": "utf8mb4", "utf8mb4_ru_0900_as_cs": "utf8mb4", "utf8mb4_sk_0900_ai_ci": "utf8mb4", "utf8mb4_sk_0900_as_cs": "utf8mb4", "utf8mb4_sl_0900_ai_ci": "utf8mb4", "utf8mb4_sl_0900_as_cs": "utf8mb4", "utf8mb4_sv_0900_ai_ci": "utf8mb4", "utf8mb4_sv_0900_as_cs": "utf8mb4", "utf8mb4_tr_0900_ai_ci": "utf8mb4", "utf8mb4_tr_0900_as_cs": "utf8mb4", "utf8mb4_vi_0900_ai_ci": "utf8mb4", "utf8mb4_vi_0900_as_cs": "utf8mb4", "utf8mb4_vietnamese_ci": "utf8mb4", "utf8mb4_zh_0900_as_cs": "utf8mb4", "gb18030_unicode_520_ci": "gb18030", "utf8mb4_unicode_520_ci": "utf8mb4", "ucs2_general_mysql500_ci": "ucs2", "utf8_general_mysql500_ci": "utf8", "utf8mb4_de_pb_0900_ai_ci": "utf8mb4", "utf8mb4_de_pb_0900_as_cs": "utf8mb4", "utf8mb4_ja_0900_as_cs_ks": "utf8mb4", "utf8mb4_es_trad_0900_ai_ci": "utf8mb4", "utf8mb4_es_trad_0900_as_cs": "utf8mb4"}
\ No newline at end of file
diff --git a/sql/mysql/internal/collate2charset.maria b/sql/mysql/internal/collate2charset.maria
new file mode 100644
index 00000000..100fe455
--- /dev/null
+++ b/sql/mysql/internal/collate2charset.maria
@@ -0,0 +1,1 @@
+{"big5_chinese_ci":"big5", "big5_bin":"big5", "big5_chinese_nopad_ci":"big5", "big5_nopad_bin":"big5", "dec8_swedish_ci":"dec8", "dec8_bin":"dec8", "dec8_swedish_nopad_ci":"dec8", "dec8_nopad_bin":"dec8", "cp850_general_ci":"cp850", "cp850_bin":"cp850", "cp850_general_nopad_ci":"cp850", "cp850_nopad_bin":"cp850", "hp8_english_ci":"hp8", "hp8_bin":"hp8", "hp8_english_nopad_ci":"hp8", "hp8_nopad_bin":"hp8", "koi8r_general_ci":"koi8r", "koi8r_bin":"koi8r", "koi8r_general_nopad_ci":"koi8r", "koi8r_nopad_bin":"koi8r", "latin1_german1_ci":"latin1", "latin1_swedish_ci":"latin1", "latin1_danish_ci":"latin1", "latin1_german2_ci":"latin1", "latin1_bin":"latin1", "latin1_general_ci":"latin1", "latin1_general_cs":"latin1", "latin1_spanish_ci":"latin1", "latin1_swedish_nopad_ci":"latin1", "latin1_nopad_bin":"latin1", "latin2_czech_cs":"latin2", "latin2_general_ci":"latin2", "latin2_hungarian_ci":"latin2", "latin2_croatian_ci":"latin2", "latin2_bin":"latin2", "latin2_general_nopad_ci":"latin2", "latin2_nopad_bin":"latin2", "swe7_swedish_ci":"swe7", "swe7_bin":"swe7", "swe7_swedish_nopad_ci":"swe7", "swe7_nopad_bin":"swe7", "ascii_general_ci":"ascii", "ascii_bin":"ascii", "ascii_general_nopad_ci":"ascii", "ascii_nopad_bin":"ascii", "ujis_japanese_ci":"ujis", "ujis_bin":"ujis", "ujis_japanese_nopad_ci":"ujis", "ujis_nopad_bin":"ujis", "sjis_japanese_ci":"sjis", "sjis_bin":"sjis", "sjis_japanese_nopad_ci":"sjis", "sjis_nopad_bin":"sjis", "hebrew_general_ci":"hebrew", "hebrew_bin":"hebrew", "hebrew_general_nopad_ci":"hebrew", "hebrew_nopad_bin":"hebrew", "tis620_thai_ci":"tis620", "tis620_bin":"tis620", "tis620_thai_nopad_ci":"tis620", "tis620_nopad_bin":"tis620", "euckr_korean_ci":"euckr", "euckr_bin":"euckr", "euckr_korean_nopad_ci":"euckr", "euckr_nopad_bin":"euckr", "koi8u_general_ci":"koi8u", "koi8u_bin":"koi8u", "koi8u_general_nopad_ci":"koi8u", "koi8u_nopad_bin":"koi8u", "gb2312_chinese_ci":"gb2312", "gb2312_bin":"gb2312", "gb2312_chinese_nopad_ci":"gb2312", "gb2312_nopad_bin":"gb2312", "greek_general_ci":"greek", "greek_bin":"greek", "greek_general_nopad_ci":"greek", "greek_nopad_bin":"greek", "cp1250_general_ci":"cp1250", "cp1250_czech_cs":"cp1250", "cp1250_croatian_ci":"cp1250", "cp1250_bin":"cp1250", "cp1250_polish_ci":"cp1250", "cp1250_general_nopad_ci":"cp1250", "cp1250_nopad_bin":"cp1250", "gbk_chinese_ci":"gbk", "gbk_bin":"gbk", "gbk_chinese_nopad_ci":"gbk", "gbk_nopad_bin":"gbk", "latin5_turkish_ci":"latin5", "latin5_bin":"latin5", "latin5_turkish_nopad_ci":"latin5", "latin5_nopad_bin":"latin5", "armscii8_general_ci":"armscii8", "armscii8_bin":"armscii8", "armscii8_general_nopad_ci":"armscii8", "armscii8_nopad_bin":"armscii8", "utf8mb3_general_ci":"utf8mb3", "utf8mb3_bin":"utf8mb3", "utf8mb3_unicode_ci":"utf8mb3", "utf8mb3_icelandic_ci":"utf8mb3", "utf8mb3_latvian_ci":"utf8mb3", "utf8mb3_romanian_ci":"utf8mb3", "utf8mb3_slovenian_ci":"utf8mb3", "utf8mb3_polish_ci":"utf8mb3", "utf8mb3_estonian_ci":"utf8mb3", "utf8mb3_spanish_ci":"utf8mb3", "utf8mb3_swedish_ci":"utf8mb3", "utf8mb3_turkish_ci":"utf8mb3", "utf8mb3_czech_ci":"utf8mb3", "utf8mb3_danish_ci":"utf8mb3", "utf8mb3_lithuanian_ci":"utf8mb3", "utf8mb3_slovak_ci":"utf8mb3", "utf8mb3_spanish2_ci":"utf8mb3", "utf8mb3_roman_ci":"utf8mb3", "utf8mb3_persian_ci":"utf8mb3", "utf8mb3_esperanto_ci":"utf8mb3", "utf8mb3_hungarian_ci":"utf8mb3", "utf8mb3_sinhala_ci":"utf8mb3", "utf8mb3_german2_ci":"utf8mb3", "utf8mb3_croatian_mysql561_ci":"utf8mb3", "utf8mb3_unicode_520_ci":"utf8mb3", "utf8mb3_vietnamese_ci":"utf8mb3", "utf8mb3_general_mysql500_ci":"utf8mb3", "utf8mb3_croatian_ci":"utf8mb3", "utf8mb3_myanmar_ci":"utf8mb3", "utf8mb3_thai_520_w2":"utf8mb3", "utf8mb3_general_nopad_ci":"utf8mb3", "utf8mb3_nopad_bin":"utf8mb3", "utf8mb3_unicode_nopad_ci":"utf8mb3", "utf8mb3_unicode_520_nopad_ci":"utf8mb3", "ucs2_general_ci":"ucs2", "ucs2_bin":"ucs2", "ucs2_unicode_ci":"ucs2", "ucs2_icelandic_ci":"ucs2", "ucs2_latvian_ci":"ucs2", "ucs2_romanian_ci":"ucs2", "ucs2_slovenian_ci":"ucs2", "ucs2_polish_ci":"ucs2", "ucs2_estonian_ci":"ucs2", "ucs2_spanish_ci":"ucs2", "ucs2_swedish_ci":"ucs2", "ucs2_turkish_ci":"ucs2", "ucs2_czech_ci":"ucs2", "ucs2_danish_ci":"ucs2", "ucs2_lithuanian_ci":"ucs2", "ucs2_slovak_ci":"ucs2", "ucs2_spanish2_ci":"ucs2", "ucs2_roman_ci":"ucs2", "ucs2_persian_ci":"ucs2", "ucs2_esperanto_ci":"ucs2", "ucs2_hungarian_ci":"ucs2", "ucs2_sinhala_ci":"ucs2", "ucs2_german2_ci":"ucs2", "ucs2_croatian_mysql561_ci":"ucs2", "ucs2_unicode_520_ci":"ucs2", "ucs2_vietnamese_ci":"ucs2", "ucs2_general_mysql500_ci":"ucs2", "ucs2_croatian_ci":"ucs2", "ucs2_myanmar_ci":"ucs2", "ucs2_thai_520_w2":"ucs2", "ucs2_general_nopad_ci":"ucs2", "ucs2_nopad_bin":"ucs2", "ucs2_unicode_nopad_ci":"ucs2", "ucs2_unicode_520_nopad_ci":"ucs2", "cp866_general_ci":"cp866", "cp866_bin":"cp866", "cp866_general_nopad_ci":"cp866", "cp866_nopad_bin":"cp866", "keybcs2_general_ci":"keybcs2", "keybcs2_bin":"keybcs2", "keybcs2_general_nopad_ci":"keybcs2", "keybcs2_nopad_bin":"keybcs2", "macce_general_ci":"macce", "macce_bin":"macce", "macce_general_nopad_ci":"macce", "macce_nopad_bin":"macce", "macroman_general_ci":"macroman", "macroman_bin":"macroman", "macroman_general_nopad_ci":"macroman", "macroman_nopad_bin":"macroman", "cp852_general_ci":"cp852", "cp852_bin":"cp852", "cp852_general_nopad_ci":"cp852", "cp852_nopad_bin":"cp852", "latin7_estonian_cs":"latin7", "latin7_general_ci":"latin7", "latin7_general_cs":"latin7", "latin7_bin":"latin7", "latin7_general_nopad_ci":"latin7", "latin7_nopad_bin":"latin7", "utf8mb4_general_ci":"utf8mb4", "utf8mb4_bin":"utf8mb4", "utf8mb4_unicode_ci":"utf8mb4", "utf8mb4_icelandic_ci":"utf8mb4", "utf8mb4_latvian_ci":"utf8mb4", "utf8mb4_romanian_ci":"utf8mb4", "utf8mb4_slovenian_ci":"utf8mb4", "utf8mb4_polish_ci":"utf8mb4", "utf8mb4_estonian_ci":"utf8mb4", "utf8mb4_spanish_ci":"utf8mb4", "utf8mb4_swedish_ci":"utf8mb4", "utf8mb4_turkish_ci":"utf8mb4", "utf8mb4_czech_ci":"utf8mb4", "utf8mb4_danish_ci":"utf8mb4", "utf8mb4_lithuanian_ci":"utf8mb4", "utf8mb4_slovak_ci":"utf8mb4", "utf8mb4_spanish2_ci":"utf8mb4", "utf8mb4_roman_ci":"utf8mb4", "utf8mb4_persian_ci":"utf8mb4", "utf8mb4_esperanto_ci":"utf8mb4", "utf8mb4_hungarian_ci":"utf8mb4", "utf8mb4_sinhala_ci":"utf8mb4", "utf8mb4_german2_ci":"utf8mb4", "utf8mb4_croatian_mysql561_ci":"utf8mb4", "utf8mb4_unicode_520_ci":"utf8mb4", "utf8mb4_vietnamese_ci":"utf8mb4", "utf8mb4_croatian_ci":"utf8mb4", "utf8mb4_myanmar_ci":"utf8mb4", "utf8mb4_thai_520_w2":"utf8mb4", "utf8mb4_general_nopad_ci":"utf8mb4", "utf8mb4_nopad_bin":"utf8mb4", "utf8mb4_unicode_nopad_ci":"utf8mb4", "utf8mb4_unicode_520_nopad_ci":"utf8mb4", "cp1251_bulgarian_ci":"cp1251", "cp1251_ukrainian_ci":"cp1251", "cp1251_bin":"cp1251", "cp1251_general_ci":"cp1251", "cp1251_general_cs":"cp1251", "cp1251_nopad_bin":"cp1251", "cp1251_general_nopad_ci":"cp1251", "utf16_general_ci":"utf16", "utf16_bin":"utf16", "utf16_unicode_ci":"utf16", "utf16_icelandic_ci":"utf16", "utf16_latvian_ci":"utf16", "utf16_romanian_ci":"utf16", "utf16_slovenian_ci":"utf16", "utf16_polish_ci":"utf16", "utf16_estonian_ci":"utf16", "utf16_spanish_ci":"utf16", "utf16_swedish_ci":"utf16", "utf16_turkish_ci":"utf16", "utf16_czech_ci":"utf16", "utf16_danish_ci":"utf16", "utf16_lithuanian_ci":"utf16", "utf16_slovak_ci":"utf16", "utf16_spanish2_ci":"utf16", "utf16_roman_ci":"utf16", "utf16_persian_ci":"utf16", "utf16_esperanto_ci":"utf16", "utf16_hungarian_ci":"utf16", "utf16_sinhala_ci":"utf16", "utf16_german2_ci":"utf16", "utf16_croatian_mysql561_ci":"utf16", "utf16_unicode_520_ci":"utf16", "utf16_vietnamese_ci":"utf16", "utf16_croatian_ci":"utf16", "utf16_myanmar_ci":"utf16", "utf16_thai_520_w2":"utf16", "utf16_general_nopad_ci":"utf16", "utf16_nopad_bin":"utf16", "utf16_unicode_nopad_ci":"utf16", "utf16_unicode_520_nopad_ci":"utf16", "utf16le_general_ci":"utf16le", "utf16le_bin":"utf16le", "utf16le_general_nopad_ci":"utf16le", "utf16le_nopad_bin":"utf16le", "cp1256_general_ci":"cp1256", "cp1256_bin":"cp1256", "cp1256_general_nopad_ci":"cp1256", "cp1256_nopad_bin":"cp1256", "cp1257_lithuanian_ci":"cp1257", "cp1257_bin":"cp1257", "cp1257_general_ci":"cp1257", "cp1257_nopad_bin":"cp1257", "cp1257_general_nopad_ci":"cp1257", "utf32_general_ci":"utf32", "utf32_bin":"utf32", "utf32_unicode_ci":"utf32", "utf32_icelandic_ci":"utf32", "utf32_latvian_ci":"utf32", "utf32_romanian_ci":"utf32", "utf32_slovenian_ci":"utf32", "utf32_polish_ci":"utf32", "utf32_estonian_ci":"utf32", "utf32_spanish_ci":"utf32", "utf32_swedish_ci":"utf32", "utf32_turkish_ci":"utf32", "utf32_czech_ci":"utf32", "utf32_danish_ci":"utf32", "utf32_lithuanian_ci":"utf32", "utf32_slovak_ci":"utf32", "utf32_spanish2_ci":"utf32", "utf32_roman_ci":"utf32", "utf32_persian_ci":"utf32", "utf32_esperanto_ci":"utf32", "utf32_hungarian_ci":"utf32", "utf32_sinhala_ci":"utf32", "utf32_german2_ci":"utf32", "utf32_croatian_mysql561_ci":"utf32", "utf32_unicode_520_ci":"utf32", "utf32_vietnamese_ci":"utf32", "utf32_croatian_ci":"utf32", "utf32_myanmar_ci":"utf32", "utf32_thai_520_w2":"utf32", "utf32_general_nopad_ci":"utf32", "utf32_nopad_bin":"utf32", "utf32_unicode_nopad_ci":"utf32", "utf32_unicode_520_nopad_ci":"utf32", "binary":"binary", "geostd8_general_ci":"geostd8", "geostd8_bin":"geostd8", "geostd8_general_nopad_ci":"geostd8", "geostd8_nopad_bin":"geostd8", "cp932_japanese_ci":"cp932", "cp932_bin":"cp932", "cp932_japanese_nopad_ci":"cp932", "cp932_nopad_bin":"cp932", "eucjpms_japanese_ci":"eucjpms", "eucjpms_bin":"eucjpms", "eucjpms_japanese_nopad_ci":"eucjpms", "eucjpms_nopad_bin":"eucjpms"}
\ No newline at end of file
diff --git a/sql/postgres/crdb.go b/sql/postgres/crdb.go
index 80bb4d25..cde4ee97 100644
--- a/sql/postgres/crdb.go
+++ b/sql/postgres/crdb.go
@@ -75,13 +75,15 @@ func (i *crdbInspect) InspectRealm(ctx context.Context, opts *schema.InspectReal
 }
 
 // Normalize implements the sqlx.Normalizer.
-func (cd *crdbDiff) Normalize(from, to *schema.Table) {
+func (cd *crdbDiff) Normalize(from, to *schema.Table) error {
 	cd.normalize(from)
 	cd.normalize(to)
+	return nil
 }
 
-func (cd *crdbDiff) ColumnChange(from, to *schema.Column) (schema.ChangeKind, error) {
-	// All serial types in crdb become bigints under the hood, see: https://www.cockroachlabs.com/docs/stable/serial.html#generated-values-for-mode-sql_sequence-and-sql_sequence_cached.
+func (cd *crdbDiff) ColumnChange(fromT *schema.Table, from, to *schema.Column) (schema.ChangeKind, error) {
+	// All serial types in crdb become bigints under the hood,
+	// See: https://www.cockroachlabs.com/docs/stable/serial.html#generated-values-for-mode-sql_sequence-and-sql_sequence_cached.
 	if s, ok := to.Type.Type.(*SerialType); ok {
 		to.Type.Type = &schema.IntegerType{
 			T: TypeBigInt,
@@ -90,7 +92,7 @@ func (cd *crdbDiff) ColumnChange(from, to *schema.Column) (schema.ChangeKind, er
 			to.Type.Type = s
 		}()
 	}
-	return cd.diff.ColumnChange(from, to)
+	return cd.diff.ColumnChange(fromT, from, to)
 }
 
 func (cd *crdbDiff) normalize(table *schema.Table) {
diff --git a/sql/postgres/diff.go b/sql/postgres/diff.go
index d61c0a59..f4307735 100644
--- a/sql/postgres/diff.go
+++ b/sql/postgres/diff.go
@@ -41,7 +41,7 @@ func (d *diff) TableAttrDiff(from, to *schema.Table) ([]schema.Change, error) {
 }
 
 // ColumnChange returns the schema changes (if any) for migrating one column to the other.
-func (d *diff) ColumnChange(from, to *schema.Column) (schema.ChangeKind, error) {
+func (d *diff) ColumnChange(_ *schema.Table, from, to *schema.Column) (schema.ChangeKind, error) {
 	change := sqlx.CommentChange(from.Attrs, to.Attrs)
 	if from.Type.Null != to.Type.Null {
 		change |= schema.ChangeNull
@@ -262,9 +262,10 @@ func (d *diff) typeChanged(from, to *schema.Column) (bool, error) {
 }
 
 // Normalize implements the sqlx.Normalizer interface.
-func (d *diff) Normalize(from, to *schema.Table) {
+func (d *diff) Normalize(from, to *schema.Table) error {
 	d.normalize(from)
 	d.normalize(to)
+	return nil
 }
 
 func (d *diff) normalize(table *schema.Table) {
diff --git a/sql/schema/migrate.go b/sql/schema/migrate.go
index b2dccc85..5067ae14 100644
--- a/sql/schema/migrate.go
+++ b/sql/schema/migrate.go
@@ -199,8 +199,8 @@ const (
 	ChangeAttr ChangeKind = 1 << (iota - 1)
 	// ChangeCharset describes character-set change.
 	ChangeCharset
-	// ChangeCollation describes collation/encoding change.
-	ChangeCollation
+	// ChangeCollate describes collation/encoding change.
+	ChangeCollate
 	// ChangeComment describes comment chang (of any element).
 	ChangeComment
 
diff --git a/sql/sqlcheck/sqlcheck.go b/sql/sqlcheck/sqlcheck.go
index 0d46da91..9fe50a54 100644
--- a/sql/sqlcheck/sqlcheck.go
+++ b/sql/sqlcheck/sqlcheck.go
@@ -88,9 +88,6 @@ func (f ReportWriterFunc) WriteReport(r Report) {
 	f(r)
 }
 
-// NopReportWriter is a Reporter that does nothing.
-var NopReportWriter ReportWriter = ReportWriterFunc(func(Report) {})
-
 // Destructive checks destructive changes.
 var Destructive = &driverAware{
 	run: func(ctx context.Context, diags []Diagnostic, p *Pass) error {
diff --git a/sql/sqlite/diff.go b/sql/sqlite/diff.go
index bfbaf7c9..cda60702 100644
--- a/sql/sqlite/diff.go
+++ b/sql/sqlite/diff.go
@@ -40,7 +40,7 @@ func (d *diff) TableAttrDiff(from, to *schema.Table) ([]schema.Change, error) {
 }
 
 // ColumnChange returns the schema changes (if any) for migrating one column to the other.
-func (d *diff) ColumnChange(from, to *schema.Column) (schema.ChangeKind, error) {
+func (d *diff) ColumnChange(_ *schema.Table, from, to *schema.Column) (schema.ChangeKind, error) {
 	change := sqlx.CommentChange(from.Attrs, to.Attrs)
 	if from.Type.Null != to.Type.Null {
 		change |= schema.ChangeNull
@@ -131,7 +131,7 @@ func (*diff) ReferenceChanged(from, to schema.ReferenceOption) bool {
 }
 
 // Normalize implements the sqlx.Normalizer interface.
-func (d *diff) Normalize(from, to *schema.Table) {
+func (d *diff) Normalize(from, to *schema.Table) error {
 	used := make([]bool, len(to.ForeignKeys))
 	// In SQLite, there is no easy way to get the foreign-key constraint
 	// name, except for parsing the CREATE statement). Therefore, we check
@@ -147,6 +147,7 @@ func (d *diff) Normalize(from, to *schema.Table) {
 			}
 		}
 	}
+	return nil
 }
 
 func sameFK(fk1, fk2 *schema.ForeignKey) bool {


Test patch:
diff --git a/internal/integration/testdata/mysql/column-charset.txt b/internal/integration/testdata/mysql/column-charset.txt
index 0e012a16..ad89c960 100644
--- a/internal/integration/testdata/mysql/column-charset.txt
+++ b/internal/integration/testdata/mysql/column-charset.txt
@@ -1,6 +1,22 @@
 apply 1.hcl
 cmpshow users 1.sql
 
+# Dropping the default COLLATE from the HCL does not have any effect.
+apply 2.hcl
+cmpshow users 1.sql
+
+# Changing the default COLLATE to hebrew_bin.
+apply 3.hcl
+cmpshow users 3.sql
+
+# Dropping custom COLLATE reverts to the default.
+apply 4.hcl
+cmpshow users 1.sql
+
+# Dropping CHARSET and COLLATE.
+apply 5.hcl
+cmpshow users 5.sql
+
 -- 1.hcl --
 schema "$db" {
     charset = "$charset"
@@ -27,3 +43,77 @@ CREATE TABLE `users` (
 CREATE TABLE `users` (
   `name` varchar(255) CHARACTER SET hebrew COLLATE hebrew_general_ci NOT NULL
 )
+
+-- 2.hcl --
+schema "$db" {
+    charset = "$charset"
+    collate = "$collate"
+}
+
+table "users" {
+    schema = schema.$db
+    column "name" {
+        type = varchar(255)
+        charset = "hebrew"
+    }
+    charset = "$charset"
+    collate = "$collate"
+}
+
+-- 3.hcl --
+schema "$db" {
+    charset = "$charset"
+    collate = "$collate"
+}
+
+table "users" {
+    schema = schema.$db
+    column "name" {
+        type = varchar(255)
+        charset = "hebrew"
+        collate = "hebrew_bin"
+    }
+    charset = "$charset"
+    collate = "$collate"
+}
+
+-- 3.sql --
+CREATE TABLE `users` (
+  `name` varchar(255) CHARACTER SET hebrew COLLATE hebrew_bin NOT NULL
+)
+
+-- 4.hcl --
+schema "$db" {
+    charset = "$charset"
+    collate = "$collate"
+}
+
+table "users" {
+    schema = schema.$db
+    column "name" {
+        type = varchar(255)
+        charset = "hebrew"
+    }
+    charset = "$charset"
+    collate = "$collate"
+}
+
+-- 5.hcl --
+schema "$db" {
+    charset = "$charset"
+    collate = "$collate"
+}
+
+table "users" {
+    schema = schema.$db
+    column "name" {
+        type = varchar(255)
+    }
+    charset = "$charset"
+    collate = "$collate"
+}
+
+-- 5.sql --
+CREATE TABLE `users` (
+  `name` varchar(255) NOT NULL
+)
\ No newline at end of file
diff --git a/internal/integration/testdata/sqlite/cli-atlasci-sanity.txt b/internal/integration/testdata/sqlite/cli-atlasci-sanity.txt
index c4e537a2..9b2bd625 100644
--- a/internal/integration/testdata/sqlite/cli-atlasci-sanity.txt
+++ b/internal/integration/testdata/sqlite/cli-atlasci-sanity.txt
@@ -6,11 +6,15 @@ cmp got2.txt expected2.txt
 
 -- migrations/1.sql --
 CREATE TABLE users (id int);
+
 CREATE TABLE pets (id int);
+
 -- migrations/2.sql --
 DROP TABLE users;
+
 -- migrations/3.sql --
 DROP TABLE pets;
+
 -- expected1.txt --
 Destructive changes detected in file 3.sql:
 
diff --git a/sql/mysql/diff_test.go b/sql/mysql/diff_test.go
index bba06bfb..c279a37a 100644
--- a/sql/mysql/diff_test.go
+++ b/sql/mysql/diff_test.go
@@ -62,11 +62,11 @@ func TestDiff_TableDiff(t *testing.T) {
 		},
 		{
 			name: "add collation",
-			from: &schema.Table{Name: "users", Schema: &schema.Schema{Name: "public"}},
-			to:   &schema.Table{Name: "users", Attrs: []schema.Attr{&schema.Collation{V: "latin1"}}},
+			from: &schema.Table{Name: "users", Schema: &schema.Schema{Name: "public"}, Attrs: []schema.Attr{&schema.Charset{V: "latin1"}}},
+			to:   &schema.Table{Name: "users", Attrs: []schema.Attr{&schema.Collation{V: "latin1_bin"}}},
 			wantChanges: []schema.Change{
 				&schema.AddAttr{
-					A: &schema.Collation{V: "latin1"},
+					A: &schema.Collation{V: "latin1_bin"},
 				},
 			},
 		},
@@ -83,22 +83,12 @@ func TestDiff_TableDiff(t *testing.T) {
 		},
 		{
 			name: "modify collation",
-			from: &schema.Table{Name: "users", Schema: &schema.Schema{Name: "public"}, Attrs: []schema.Attr{&schema.Collation{V: "utf8"}}},
-			to:   &schema.Table{Name: "users", Attrs: []schema.Attr{&schema.Collation{V: "latin1"}}},
+			from: &schema.Table{Name: "users", Schema: &schema.Schema{Name: "public"}, Attrs: []schema.Attr{&schema.Charset{V: "latin1"}, &schema.Collation{V: "latin1_swedish_ci"}}},
+			to:   &schema.Table{Name: "users", Attrs: []schema.Attr{&schema.Collation{V: "latin1_bin"}}},
 			wantChanges: []schema.Change{
 				&schema.ModifyAttr{
-					From: &schema.Collation{V: "utf8"},
-					To:   &schema.Collation{V: "latin1"},
-				},
-			},
-		},
-		{
-			name: "add charset",
-			from: &schema.Table{Name: "users", Schema: &schema.Schema{Name: "public"}},
-			to:   &schema.Table{Name: "users", Attrs: []schema.Attr{&schema.Charset{V: "hebrew"}}},
-			wantChanges: []schema.Change{
-				&schema.AddAttr{
-					A: &schema.Charset{V: "hebrew"},
+					From: &schema.Collation{V: "latin1_swedish_ci"},
+					To:   &schema.Collation{V: "latin1_bin"},
 				},
 			},
 		},
@@ -115,12 +105,16 @@ func TestDiff_TableDiff(t *testing.T) {
 		},
 		{
 			name: "modify charset",
-			from: &schema.Table{Name: "users", Schema: &schema.Schema{Name: "public"}, Attrs: []schema.Attr{&schema.Charset{V: "hebrew"}}},
-			to:   &schema.Table{Name: "users", Attrs: []schema.Attr{&schema.Charset{V: "binary"}}},
+			from: &schema.Table{Name: "users", Schema: &schema.Schema{Name: "public"}, Attrs: []schema.Attr{&schema.Charset{V: "utf8"}, &schema.Collation{V: "utf8_general_ci"}}},
+			to:   &schema.Table{Name: "users", Attrs: []schema.Attr{&schema.Charset{V: "utf8mb4"}}},
 			wantChanges: []schema.Change{
 				&schema.ModifyAttr{
-					From: &schema.Charset{V: "hebrew"},
-					To:   &schema.Charset{V: "binary"},
+					From: &schema.Charset{V: "utf8"},
+					To:   &schema.Charset{V: "utf8mb4"},
+				},
+				&schema.ModifyAttr{
+					From: &schema.Collation{V: "utf8_general_ci"},
+					To:   &schema.Collation{V: "utf8mb4_0900_ai_ci"},
 				},
 			},
 		},
@@ -229,6 +223,110 @@ func TestDiff_TableDiff(t *testing.T) {
 				},
 			}
 		}(),
+		// Custom CHARSET was dropped.
+		func() testcase {
+			var (
+				from = schema.NewTable("t1").
+					SetSchema(schema.New("public")).
+					SetCharset("utf8").
+					SetCollation("utf8_general_ci").
+					AddColumns(schema.NewStringColumn("c1", "text").SetCharset("latin1"))
+				to = schema.NewTable("t1").
+					SetSchema(schema.New("public")).
+					SetCharset("utf8").
+					AddColumns(schema.NewStringColumn("c1", "text"))
+			)
+			return testcase{
+				name: "columns",
+				from: from,
+				to:   to,
+				wantChanges: []schema.Change{
+					&schema.ModifyColumn{
+						From:   from.Columns[0],
+						To:     to.Columns[0],
+						Change: schema.ChangeCharset,
+					},
+				},
+			}
+		}(),
+		// Custom CHARSET was added.
+		func() testcase {
+			var (
+				from = schema.NewTable("t1").
+					SetSchema(schema.New("public")).
+					SetCharset("utf8").
+					SetCollation("utf8_general_ci").
+					AddColumns(schema.NewStringColumn("c1", "text"))
+				to = schema.NewTable("t1").
+					SetSchema(schema.New("public")).
+					SetCharset("utf8").
+					AddColumns(schema.NewStringColumn("c1", "text").SetCharset("latin1"))
+			)
+			return testcase{
+				name: "columns",
+				from: from,
+				to:   to,
+				wantChanges: []schema.Change{
+					&schema.ModifyColumn{
+						From:   from.Columns[0],
+						To:     to.Columns[0],
+						Change: schema.ChangeCharset | schema.ChangeCollate,
+					},
+				},
+			}
+		}(),
+		// Custom CHARSET was changed.
+		func() testcase {
+			var (
+				from = schema.NewTable("t1").
+					SetSchema(schema.New("public")).
+					SetCharset("utf8").
+					SetCollation("utf8_general_ci").
+					AddColumns(schema.NewStringColumn("c1", "text").SetCharset("hebrew"))
+				to = schema.NewTable("t1").
+					SetSchema(schema.New("public")).
+					SetCharset("utf8").
+					SetCollation("utf8_general_ci").
+					AddColumns(schema.NewStringColumn("c1", "text").SetCharset("latin1"))
+			)
+			return testcase{
+				name: "columns",
+				from: from,
+				to:   to,
+				wantChanges: []schema.Change{
+					&schema.ModifyColumn{
+						From:   from.Columns[0],
+						To:     to.Columns[0],
+						Change: schema.ChangeCharset | schema.ChangeCollate,
+					},
+				},
+			}
+		}(),
+		// Nop CHARSET change.
+		func() testcase {
+			var (
+				from = schema.NewTable("t1").
+					SetSchema(schema.New("public")).
+					SetCharset("utf8").
+					SetCollation("utf8_general_ci").
+					AddColumns(
+						schema.NewStringColumn("c1", "text").SetCharset("utf8"),
+						schema.NewStringColumn("c2", "text"),
+					)
+				to = schema.NewTable("t1").
+					SetSchema(schema.New("public")).
+					SetCollation("utf8_general_ci").
+					AddColumns(
+						schema.NewStringColumn("c1", "text"),
+						schema.NewStringColumn("c2", "text").SetCharset("utf8"),
+					)
+			)
+			return testcase{
+				name: "columns",
+				from: from,
+				to:   to,
+			}
+		}(),
 		func() testcase {
 			var (
 				s    = schema.New("public")