Repository: ariga/atlas
Pull request #699 (2022-04-12 17:22:18)

sql/mysql: support inspecting and applying generated columns


Patch:
diff --git a/sql/mysql/diff.go b/sql/mysql/diff.go
index 85f405ec..3af96f72 100644
--- a/sql/mysql/diff.go
+++ b/sql/mysql/diff.go
@@ -163,7 +163,7 @@ func (*diff) Normalize(from, to *schema.Table) {
 		// table is defined on non-indexed columns, an index is automatically created
 		// to satisfy the constraint.
 		// Therefore, if no such key was defined on the desired state, the diff will
-		// recommend to drop it on migration. Therefore, we fix it by dropping it from
+		// recommend dropping it on migration. Therefore, we fix it by dropping it from
 		// the current state manually.
 		if _, ok := to.Index(idx.Name); ok || !keySupportsFK(from, idx) {
 			indexes = append(indexes, idx)
@@ -183,7 +183,7 @@ func (*diff) collationChange(from, top, to []schema.Attr) schema.Change {
 			A: &toC,
 		}
 	case !toHas:
-		// There is no way to DROP a COLLATE that was configured on the table
+		// There is no way to DROP a COLLATE that was configured on the table,
 		// and it is not the default. Therefore, we use ModifyAttr and give it
 		// the inherited (and default) collation from schema or server.
 		if topHas && fromC.V != topC.V {
@@ -202,7 +202,7 @@ func (*diff) collationChange(from, top, to []schema.Attr) schema.Change {
 }
 
 // charsetChange returns the schema change for migrating the collation if
-// it was changed and its not the default attribute inherited from its parent.
+// it was changed, and it is not the default attribute inherited from its parent.
 func (*diff) charsetChange(from, top, to []schema.Attr) schema.Change {
 	var fromC, topC, toC schema.Charset
 	switch fromHas, topHas, toHas := sqlx.Has(from, &fromC), sqlx.Has(top, &topC), sqlx.Has(to, &toC); {
@@ -212,7 +212,7 @@ func (*diff) charsetChange(from, top, to []schema.Attr) schema.Change {
 			A: &toC,
 		}
 	case !toHas:
-		// There is no way to DROP a CHARSET that was configured on the table
+		// There is no way to DROP a CHARSET that was configured on the table,
 		// and it is not the default. Therefore, we use ModifyAttr and give it
 		// the inherited (and default) collation from schema or server.
 		if topHas && fromC.V != topC.V {
@@ -237,7 +237,7 @@ func (*diff) autoIncChange(from, to []schema.Attr) schema.Change {
 	switch fromHas, toHas := sqlx.Has(from, &fromA), sqlx.Has(to, &toA); {
 	// Ignore if the AUTO_INCREMENT attribute was dropped from the desired schema.
 	case fromHas && !toHas:
-	// The AUTO_INCREMENT exists in the desired schema, and may not exists in the inspected one.
+	// The AUTO_INCREMENT exists in the desired schema, and may not exist in the inspected one.
 	// This can happen because older versions of MySQL (< 8.0) stored the AUTO_INCREMENT counter
 	// in main memory (not persistent), and the value is reset on process restart for empty tables.
 	case toA.V > 1 && toA.V > fromA.V:
diff --git a/sql/mysql/driver.go b/sql/mysql/driver.go
index 22a42630..437b60cf 100644
--- a/sql/mysql/driver.go
+++ b/sql/mysql/driver.go
@@ -160,6 +160,16 @@ func (d *conn) supportsEnforceCheck() bool {
 	return !d.mariadb() && d.gteV("8.0.16")
 }
 
+// supportsGeneratedColumns reports if the connected database
+// supports the generated columns in information schema.
+func (d *conn) supportsGeneratedColumns() bool {
+	v := "5.7"
+	if d.mariadb() {
+		v = "10.2"
+	}
+	return d.gteV(v)
+}
+
 // mariadb reports if the Driver is connected to a MariaDB database.
 func (d *conn) mariadb() bool {
 	return strings.Index(d.version, "MariaDB") > 0
@@ -271,4 +281,8 @@ const (
 	currentTS     = "current_timestamp"
 	defaultGen    = "default_generated"
 	autoIncrement = "auto_increment"
+
+	genVirtual    = "virtual"
+	genStored     = "stored"
+	genPersistent = "persistent"
 )
diff --git a/sql/mysql/inspect.go b/sql/mysql/inspect.go
index fd687913..328d9fc8 100644
--- a/sql/mysql/inspect.go
+++ b/sql/mysql/inspect.go
@@ -201,7 +201,11 @@ func (i *inspect) tables(ctx context.Context, realm *schema.Realm, opts *schema.
 
 // columns queries and appends the columns of the given table.
 func (i *inspect) columns(ctx context.Context, s *schema.Schema) error {
-	rows, err := i.querySchema(ctx, columnsQuery, s)
+	query := columnsQuery
+	if i.supportsGeneratedColumns() {
+		query = columnsExprQuery
+	}
+	rows, err := i.querySchema(ctx, query, s)
 	if err != nil {
 		return fmt.Errorf("mysql: query schema %q columns: %w", s.Name, err)
 	}
@@ -216,8 +220,8 @@ func (i *inspect) columns(ctx context.Context, s *schema.Schema) error {
 
 // addColumn scans the current row and adds a new column from it to the table.
 func (i *inspect) addColumn(s *schema.Schema, rows *sql.Rows) error {
-	var table, name, typ, comment, nullable, key, defaults, extra, charset, collation sql.NullString
-	if err := rows.Scan(&table, &name, &typ, &comment, &nullable, &key, &defaults, &extra, &charset, &collation); err != nil {
+	var table, name, typ, comment, nullable, key, defaults, extra, charset, collation, expr sql.NullString
+	if err := rows.Scan(&table, &name, &typ, &comment, &nullable, &key, &defaults, &extra, &charset, &collation, &expr); err != nil {
 		return err
 	}
 	t, ok := s.Table(table.String)
@@ -236,32 +240,43 @@ func (i *inspect) addColumn(s *schema.Schema, rows *sql.Rows) error {
 		return err
 	}
 	c.Type.Type = ct
-	if err := i.extraAttr(t, c, extra.String); err != nil {
+	attr, err := parseExtra(extra.String)
+	if err != nil {
 		return err
 	}
+	if attr.autoinc {
+		a := &AutoIncrement{}
+		if !sqlx.Has(t.Attrs, a) {
+			// A table can have only one AUTO_INCREMENT column. If it was returned as NULL
+			// from INFORMATION_SCHEMA, it is due to information_schema_stats_expiry, and
+			// we need to extract it from the 'CREATE TABLE' command.
+			putShow(t).auto = a
+		}
+		c.Attrs = append(c.Attrs, a)
+	}
+	if attr.onUpdate != "" {
+		c.Attrs = append(c.Attrs, &OnUpdate{A: attr.onUpdate})
+	}
+	if expr.String != "" {
+		c.SetGeneratedExpr(&schema.GeneratedExpr{Expr: expr.String, Type: attr.generatedType})
+	}
 	if defaults.Valid {
 		if i.mariadb() {
 			c.Default = i.marDefaultExpr(c, defaults.String)
 		} else {
-			c.Default = i.myDefaultExpr(c, defaults.String, extra.String)
+			c.Default = i.myDefaultExpr(c, defaults.String, attr)
 		}
 	}
 	if sqlx.ValidString(comment) {
-		c.Attrs = append(c.Attrs, &schema.Comment{
-			Text: comment.String,
-		})
+		c.SetComment(comment.String)
 	}
 	if sqlx.ValidString(charset) {
-		c.Attrs = append(c.Attrs, &schema.Charset{
-			V: charset.String,
-		})
+		c.SetCharset(charset.String)
 	}
 	if sqlx.ValidString(collation) {
-		c.Attrs = append(c.Attrs, &schema.Collation{
-			V: collation.String,
-		})
+		c.SetCollation(collation.String)
 	}
-	t.Columns = append(t.Columns, c)
+	t.AddColumns(c)
 	// From MySQL doc: A UNIQUE index may be displayed as "PRI" if it is NOT NULL
 	// and there is no PRIMARY KEY in the table. We detect this in `addIndexes`.
 	if key.String == "PRI" {
@@ -427,32 +442,39 @@ func (i *inspect) checks(ctx context.Context, s *schema.Schema) error {
 	return rows.Err()
 }
 
-var reTimeOnUpdate = regexp.MustCompile(`(?i)^(?:default_generated )?on update (current_timestamp(?:\(\d?\))?)$`)
+// extraAttr is a parsed version of the information_schema EXTRA column.
+type extraAttr struct {
+	autoinc          bool
+	onUpdate         string
+	generatedType    string
+	defaultGenerated bool
+}
+
+var (
+	reGenerateType = regexp.MustCompile(`(?i)^(stored|persistent|virtual) generated$`)
+	reTimeOnUpdate = regexp.MustCompile(`(?i)^(?:default_generated )?on update (current_timestamp(?:\(\d?\))?)$`)
+)
 
-// extraAttr parses the EXTRA column from the INFORMATION_SCHEMA.COLUMNS table
-// and appends its parsed representation to the column.
-func (i *inspect) extraAttr(t *schema.Table, c *schema.Column, extra string) error {
-	el := strings.ToLower(extra)
-	switch {
-	case el == "", el == "null": // ignore.
+// parseExtra returns a parsed version of the EXTRA column
+// from the INFORMATION_SCHEMA.COLUMNS table.
+func parseExtra(extra string) (*extraAttr, error) {
+	attr := &extraAttr{}
+	switch el := strings.ToLower(extra); {
+	case el == "", el == "null":
 	case el == defaultGen:
-		// The column has an expression default value
+		attr.defaultGenerated = true
+		// The column has an expression default value,
 		// and it is handled in Driver.addColumn.
 	case el == autoIncrement:
-		a := &AutoIncrement{}
-		if !sqlx.Has(t.Attrs, a) {
-			// A table can have only one AUTO_INCREMENT column. If it was returned as NULL
-			// from INFORMATION_SCHEMA, it is due to information_schema_stats_expiry and we
-			// need to extract it from the 'CREATE TABLE' command.
-			putShow(t).auto = a
-		}
-		c.Attrs = append(c.Attrs, a)
+		attr.autoinc = true
 	case reTimeOnUpdate.MatchString(extra):
-		c.Attrs = append(c.Attrs, &OnUpdate{A: reTimeOnUpdate.FindStringSubmatch(extra)[1]})
+		attr.onUpdate = reTimeOnUpdate.FindStringSubmatch(extra)[1]
+	case reGenerateType.MatchString(extra):
+		attr.generatedType = reGenerateType.FindStringSubmatch(extra)[1]
 	default:
-		return fmt.Errorf("unknown attribute %q", extra)
+		return nil, fmt.Errorf("unknown extra column attribute %q", extra)
 	}
-	return nil
+	return attr, nil
 }
 
 // showCreate sets and fixes schema elements that require information from
@@ -517,9 +539,9 @@ func (i *inspect) createStmt(ctx context.Context, t *schema.Table) error {
 var reCurrTimestamp = regexp.MustCompile(`(?i)^current_timestamp(?:\(\d?\))?$`)
 
 // 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 {
+func (i *inspect) myDefaultExpr(c *schema.Column, x string, attr *extraAttr) schema.Expr {
 	// In MySQL, the DEFAULT_GENERATED indicates the column has an expression default value.
-	if i.supportsExprDefault() && strings.Contains(strings.ToLower(extra), defaultGen) {
+	if i.supportsExprDefault() && attr.defaultGenerated {
 		return &schema.RawExpr{X: x}
 	}
 	switch c.Type.Type.(type) {
@@ -628,7 +650,8 @@ const (
 	schemasQueryArgs = "SELECT `SCHEMA_NAME`, `DEFAULT_CHARACTER_SET_NAME`, `DEFAULT_COLLATION_NAME` from `INFORMATION_SCHEMA`.`SCHEMATA` WHERE `SCHEMA_NAME` %s ORDER BY `SCHEMA_NAME`"
 
 	// Query to list table columns.
-	columnsQuery = "SELECT `TABLE_NAME`, `COLUMN_NAME`, `COLUMN_TYPE`, `COLUMN_COMMENT`, `IS_NULLABLE`, `COLUMN_KEY`, `COLUMN_DEFAULT`, `EXTRA`, `CHARACTER_SET_NAME`, `COLLATION_NAME` FROM `INFORMATION_SCHEMA`.`COLUMNS` WHERE `TABLE_SCHEMA` = ? AND `TABLE_NAME` IN (%s) ORDER BY `ORDINAL_POSITION`"
+	columnsQuery     = "SELECT `TABLE_NAME`, `COLUMN_NAME`, `COLUMN_TYPE`, `COLUMN_COMMENT`, `IS_NULLABLE`, `COLUMN_KEY`, `COLUMN_DEFAULT`, `EXTRA`, `CHARACTER_SET_NAME`, `COLLATION_NAME`, NULL AS `GENERATION_EXPRESSION` FROM `INFORMATION_SCHEMA`.`COLUMNS` WHERE `TABLE_SCHEMA` = ? AND `TABLE_NAME` IN (%s) ORDER BY `ORDINAL_POSITION`"
+	columnsExprQuery = "SELECT `TABLE_NAME`, `COLUMN_NAME`, `COLUMN_TYPE`, `COLUMN_COMMENT`, `IS_NULLABLE`, `COLUMN_KEY`, `COLUMN_DEFAULT`, `EXTRA`, `CHARACTER_SET_NAME`, `COLLATION_NAME`, `GENERATION_EXPRESSION` FROM `INFORMATION_SCHEMA`.`COLUMNS` WHERE `TABLE_SCHEMA` = ? AND `TABLE_NAME` IN (%s) ORDER BY `ORDINAL_POSITION`"
 
 	// Query to list table indexes.
 	indexesQuery     = "SELECT `TABLE_NAME`, `INDEX_NAME`, `COLUMN_NAME`, `NON_UNIQUE`, `SEQ_IN_INDEX`, `INDEX_TYPE`, UPPER(`COLLATION`) = 'D' AS `DESC`, `INDEX_COMMENT`, `SUB_PART`, NULL AS `EXPRESSION` FROM `INFORMATION_SCHEMA`.`STATISTICS` WHERE `TABLE_SCHEMA` = ? AND `TABLE_NAME` IN (%s) ORDER BY `index_name`, `seq_in_index`"
diff --git a/sql/mysql/migrate.go b/sql/mysql/migrate.go
index 85d71fca..5a4cd479 100644
--- a/sql/mysql/migrate.go
+++ b/sql/mysql/migrate.go
@@ -439,6 +439,9 @@ func (s *state) column(b *sqlx.Builder, t *schema.Table, c *schema.Column) error
 		return fmt.Errorf("format type for column %q: %w", c.Name, err)
 	}
 	b.Ident(c.Name).P(typ)
+	if x := (schema.GeneratedExpr{}); sqlx.Has(c.Attrs, &x) {
+		b.P("AS", sqlx.MayWrap(x.Expr), x.Type)
+	}
 	if !c.Type.Null {
 		b.P("NOT")
 	}
diff --git a/sql/schema/dsl.go b/sql/schema/dsl.go
index 1a9b6fff..2c9e2fa5 100644
--- a/sql/schema/dsl.go
+++ b/sql/schema/dsl.go
@@ -510,6 +510,12 @@ func (c *Column) SetComment(v string) *Column {
 	return c
 }
 
+// SetGeneratedExpr sets or appends the GeneratedExpr attribute.
+func (c *Column) SetGeneratedExpr(x *GeneratedExpr) *Column {
+	replaceOrAppend(&c.Attrs, x)
+	return c
+}
+
 // AddAttrs adds additional attributes to the column.
 func (c *Column) AddAttrs(attrs ...Attr) *Column {
 	c.Attrs = append(c.Attrs, attrs...)
diff --git a/sql/schema/schema.go b/sql/schema/schema.go
index 536c58e6..287e5089 100644
--- a/sql/schema/schema.go
+++ b/sql/schema/schema.go
@@ -298,6 +298,13 @@ type (
 		Expr  string // Actual CHECK.
 		Attrs []Attr // Additional attributes (e.g. ENFORCED).
 	}
+
+	// GeneratedExpr describes the expression used for generating
+	// the value of a generated/virtual column.
+	GeneratedExpr struct {
+		Expr string
+		Type string // Optional type. e.g. STORED or VIRTUAL.
+	}
 )
 
 // expressions.
@@ -318,7 +325,8 @@ func (*DecimalType) typ()     {}
 func (*UnsupportedType) typ() {}
 
 // attributes.
-func (*Check) attr()     {}
-func (*Comment) attr()   {}
-func (*Charset) attr()   {}
-func (*Collation) attr() {}
+func (*Check) attr()         {}
+func (*Comment) attr()       {}
+func (*Charset) attr()       {}
+func (*Collation) attr()     {}
+func (*GeneratedExpr) attr() {}


Test patch:
diff --git a/sql/mysql/inspect_test.go b/sql/mysql/inspect_test.go
index f048f7a3..838bfee8 100644
--- a/sql/mysql/inspect_test.go
+++ b/sql/mysql/inspect_test.go
@@ -21,7 +21,7 @@ import (
 var (
 	queryFKs         = sqltest.Escape(fmt.Sprintf(fksQuery, "?"))
 	queryTable       = sqltest.Escape(fmt.Sprintf(tablesQuery, "?"))
-	queryColumns     = sqltest.Escape(fmt.Sprintf(columnsQuery, "?"))
+	queryColumns     = sqltest.Escape(fmt.Sprintf(columnsExprQuery, "?"))
 	queryIndexes     = sqltest.Escape(fmt.Sprintf(indexesQuery, "?"))
 	queryIndexesExpr = sqltest.Escape(fmt.Sprintf(indexesExprQuery, "?"))
 	queryMyChecks    = sqltest.Escape(fmt.Sprintf(myChecksQuery, "?"))
@@ -50,11 +50,11 @@ func TestDriver_InspectTable(t *testing.T) {
 				m.ExpectQuery(queryColumns).
 					WithArgs("public", "users").
 					WillReturnRows(sqltest.Rows(`
-+--------------------+--------------------+----------------------+----------------------+-------------+------------+----------------+----------------+--------------------+----------------+
-| table_name         | column_name        | column_type          | column_comment       | is_nullable | column_key | column_default | extra          | character_set_name | collation_name |
-+--------------------+--------------------+----------------------+----------------------+-------------+------------+----------------+----------------+--------------------+----------------+
-| users              | id                 | bigint(20)           |                      | NO          | PRI        | NULL           | auto_increment | NULL               | NULL           |
-+--------------------+--------------------+----------------------+----------------------+-------------+------------+----------------+----------------+--------------------+----------------+
++--------------------+--------------------+----------------------+----------------------+-------------+------------+----------------+----------------+--------------------+----------------+---------------------------+
+| table_name         | column_name        | column_type          | column_comment       | is_nullable | column_key | column_default | extra          | character_set_name | collation_name | generation_expression     |
++--------------------+--------------------+----------------------+----------------------+-------------+------------+----------------+----------------+--------------------+----------------+---------------------------+
+| users              | id                 | bigint(20)           |                      | NO          | PRI        | NULL           | auto_increment | NULL               | NULL           | NULL                      |
++--------------------+--------------------+----------------------+----------------------+-------------+------------+----------------+----------------+--------------------+----------------+---------------------------+
 `))
 				m.ExpectQuery(queryIndexesExpr).
 					WithArgs("public", "users").
@@ -108,24 +108,24 @@ func TestDriver_InspectTable(t *testing.T) {
 				m.ExpectQuery(queryColumns).
 					WithArgs("public", "users").
 					WillReturnRows(sqltest.Rows(`
-+------------+--------------------+------------------------------+----------------------+-------------+------------+----------------+----------------+--------------------+----------------+
-| table_name | column_name        | column_type                  | column_comment       | is_nullable | column_key | column_default | extra          | character_set_name | collation_name |
-+----------- +--------------------+------------------------------+----------------------+-------------+------------+----------------+----------------+--------------------+----------------+
-| users      | id                 | bigint(20)                   |                      | NO          | PRI        | NULL           |                | NULL               | NULL           |
-| users      | v57_tiny           | tinyint(1)                   |                      | NO          |            | NULL           |                | NULL               | NULL           |
-| users      | v57_tiny_unsigned  | tinyint(4) unsigned          |                      | NO          |            | NULL           |                | NULL               | NULL           |
-| users      | v57_small          | smallint(6)                  |                      | NO          |            | NULL           |                | NULL               | NULL           |
-| users      | v57_small_unsigned | smallint(6) unsigned         |                      | NO          |            | NULL           |                | NULL               | NULL           |
-| users      | v57_int            | bigint(11)                   |                      | NO          |            | NULL           |                | NULL               | NULL           |
-| users      | v57_int_unsigned   | bigint(11) unsigned          |                      | NO          |            | NULL           |                | NULL               | NULL           |
-| users      | v8_tiny            | tinyint                      |                      | NO          |            | NULL           |                | NULL               | NULL           |
-| users      | v8_tiny_unsigned   | tinyint unsigned             |                      | NO          |            | NULL           |                | NULL               | NULL           |
-| users      | v8_small           | smallint                     |                      | NO          |            | NULL           |                | NULL               | NULL           |
-| users      | v8_small_unsigned  | smallint unsigned            |                      | NO          |            | NULL           |                | NULL               | NULL           |
-| users      | v8_big             | bigint                       |                      | NO          |            | NULL           |                | NULL               | NULL           |
-| users      | v8_big_unsigned    | bigint unsigned              | comment              | NO          |            | NULL           |                | NULL               | NULL           |
-| users      | v8_big_zerofill    | bigint(20) unsigned zerofill | comment              | NO          |            | NULL           |                | NULL               | NULL           |
-+------------+--------------------+------------------------------+----------------------+-------------+------------+----------------+----------------+--------------------+----------------+
++------------+--------------------+------------------------------+----------------------+-------------+------------+----------------+----------------+--------------------+----------------+---------------------------+
+| table_name | column_name        | column_type                  | column_comment       | is_nullable | column_key | column_default | extra          | character_set_name | collation_name | generation_expression     |
++----------- +--------------------+------------------------------+----------------------+-------------+------------+----------------+----------------+--------------------+----------------+---------------------------+
+| users      | id                 | bigint(20)                   |                      | NO          | PRI        | NULL           |                | NULL               | NULL           | NULL                      |
+| users      | v57_tiny           | tinyint(1)                   |                      | NO          |            | NULL           |                | NULL               | NULL           | NULL                      |
+| users      | v57_tiny_unsigned  | tinyint(4) unsigned          |                      | NO          |            | NULL           |                | NULL               | NULL           | NULL                      |
+| users      | v57_small          | smallint(6)                  |                      | NO          |            | NULL           |                | NULL               | NULL           | NULL                      |
+| users      | v57_small_unsigned | smallint(6) unsigned         |                      | NO          |            | NULL           |                | NULL               | NULL           | NULL                      |
+| users      | v57_int            | bigint(11)                   |                      | NO          |            | NULL           |                | NULL               | NULL           | NULL                      |
+| users      | v57_int_unsigned   | bigint(11) unsigned          |                      | NO          |            | NULL           |                | NULL               | NULL           | NULL                      |
+| users      | v8_tiny            | tinyint                      |                      | NO          |            | NULL           |                | NULL               | NULL           | NULL                      |
+| users      | v8_tiny_unsigned   | tinyint unsigned             |                      | NO          |            | NULL           |                | NULL               | NULL           | NULL                      |
+| users      | v8_small           | smallint                     |                      | NO          |            | NULL           |                | NULL               | NULL           | NULL                      |
+| users      | v8_small_unsigned  | smallint unsigned            |                      | NO          |            | NULL           |                | NULL               | NULL           | NULL                      |
+| users      | v8_big             | bigint                       |                      | NO          |            | NULL           |                | NULL               | NULL           | NULL                      |
+| users      | v8_big_unsigned    | bigint unsigned              | comment              | NO          |            | NULL           |                | NULL               | NULL           | NULL                      |
+| users      | v8_big_zerofill    | bigint(20) unsigned zerofill | comment              | NO          |            | NULL           |                | NULL               | NULL           | NULL                      |
++------------+--------------------+------------------------------+----------------------+-------------+------------+----------------+----------------+--------------------+----------------+---------------------------+
 `))
 				m.noIndexes()
 				m.noFKs()
@@ -159,14 +159,14 @@ func TestDriver_InspectTable(t *testing.T) {
 				m.ExpectQuery(queryColumns).
 					WithArgs("public", "users").
 					WillReturnRows(sqltest.Rows(`
-+------------+----------------+------------------------------+----------------------+-------------+------------+----------------+----------------+--------------------+----------------+
-| table_name |  column_name   | column_type                  | column_comment       | is_nullable | column_key | column_default | extra          | character_set_name | collation_name |
-+------------+----------------+------------------------------+----------------------+-------------+------------+----------------+----------------+--------------------+----------------+
-| users      |  id            | bigint(20)                   |                      | NO          | PRI        | NULL           |                | NULL               | NULL           |
-| users      |  tiny_int      | tinyint(1)                   |                      | NO          |            | NULL           |                | NULL               | NULL           |
-| users      |  longtext      | longtext                     |                      | NO          |            | NULL           |                | NULL               | NULL           |
-| users      |  jsonc         | longtext                     |                      | NO          |            | NULL           |                | NULL               | NULL           |
-+------------+----------------+------------------------------+----------------------+-------------+------------+----------------+----------------+--------------------+----------------+
++------------+----------------+------------------------------+----------------------+-------------+------------+----------------+----------------+--------------------+----------------+---------------------------+
+| table_name |  column_name   | column_type                  | column_comment       | is_nullable | column_key | column_default | extra          | character_set_name | collation_name | generation_expression     |
++------------+----------------+------------------------------+----------------------+-------------+------------+----------------+----------------+--------------------+----------------+---------------------------+
+| users      |  id            | bigint(20)                   |                      | NO          | PRI        | NULL           |                | NULL               | NULL           | NULL                      |
+| users      |  tiny_int      | tinyint(1)                   |                      | NO          |            | NULL           |                | NULL               | NULL           | NULL                      |
+| users      |  longtext      | longtext                     |                      | NO          |            | NULL           |                | NULL               | NULL           | NULL                      |
+| users      |  jsonc         | longtext                     |                      | NO          |            | NULL           |                | NULL               | NULL           | NULL                      |
++------------+----------------+------------------------------+----------------------+-------------+------------+----------------+----------------+--------------------+----------------+---------------------------+
 `))
 				m.ExpectQuery(queryIndexes).
 					WillReturnRows(sqlmock.NewRows([]string{"table_name", "index_name", "column_name", "non_unique", "key_part", "expression"}))
@@ -204,14 +204,14 @@ func TestDriver_InspectTable(t *testing.T) {
 				m.ExpectQuery(queryColumns).
 					WithArgs("public", "users").
 					WillReturnRows(sqltest.Rows(`
-+------------+--------------+------------------------+----------------+-------------+------------+----------------+-------+--------------------+----------------+
-| table_name |  column_name |      column_type       | column_comment | is_nullable | column_key | column_default | extra | character_set_name | collation_name |
-+------------+--------------+------------------------+----------------+-------------+------------+----------------+-------+--------------------+----------------+
-| users      |  d1          | decimal(10,2)          |                | NO          |            | 10.20          |       | NULL               | NULL           |
-| users      |  d2          | decimal(10,0)          |                | NO          |            | 10             |       | NULL               | NULL           |
-| users      |  d3          | decimal(10,2) unsigned |                | NO          |            | 10.20          |       | NULL               | NULL           |
-| users      |  d4          | decimal(10,0) unsigned |                | NO          |            | 10             |       | NULL               | NULL           |
-+------------+-------------+-------------------------+----------------+-------------+------------+----------------+-------+--------------------+----------------+
++------------+--------------+------------------------+----------------+-------------+------------+----------------+-------+--------------------+----------------+---------------------------+
+| table_name |  column_name |      column_type       | column_comment | is_nullable | column_key | column_default | extra | character_set_name | collation_name | generation_expression     |
++------------+--------------+------------------------+----------------+-------------+------------+----------------+-------+--------------------+----------------+---------------------------+
+| users      |  d1          | decimal(10,2)          |                | NO          |            | 10.20          |       | NULL               | NULL           | NULL                      |
+| users      |  d2          | decimal(10,0)          |                | NO          |            | 10             |       | NULL               | NULL           | NULL                      |
+| users      |  d3          | decimal(10,2) unsigned |                | NO          |            | 10.20          |       | NULL               | NULL           | NULL                      |
+| users      |  d4          | decimal(10,0) unsigned |                | NO          |            | 10             |       | NULL               | NULL           | NULL                      |
++------------+-------------+-------------------------+----------------+-------------+------------+----------------+-------+--------------------+----------------+---------------------------+
 `))
 				m.noIndexes()
 				m.noFKs()
@@ -234,15 +234,15 @@ func TestDriver_InspectTable(t *testing.T) {
 				m.ExpectQuery(queryColumns).
 					WithArgs("public", "users").
 					WillReturnRows(sqltest.Rows(`
-+------------+-------------------+--------------------+----------------+-------------+------------+----------------+-------+--------------------+----------------+
-| table_name |    column_name    | column_type        | column_comment | is_nullable | column_key | column_default | extra | character_set_name | collation_name |
-+------------+-------------------+--------------------+----------------+-------------+------------+----------------+-------+--------------------+----------------+
-| users      |  float            | float              |                | NO          |            |                |       | NULL               | NULL           |
-| users      |  double           | double             |                | NO          |            |                |       | NULL               | NULL           |
-| users      |  float_unsigned   | float unsigned     |                | NO          |            |                |       | NULL               | NULL           |
-| users      |  double_unsigned  | double unsigned    |                | NO          |            |                |       | NULL               | NULL           |
-| users      |  float_unsigned_p | float(10) unsigned |                | NO          |            |                |       | NULL               | NULL           |
-+------------+-------------------+--------------------+----------------+-------------+------------+----------------+-------+--------------------+----------------+
++------------+-------------------+--------------------+----------------+-------------+------------+----------------+-------+--------------------+----------------+---------------------------+ 
+| table_name |    column_name    | column_type        | column_comment | is_nullable | column_key | column_default | extra | character_set_name | collation_name | generation_expression     | 
++------------+-------------------+--------------------+----------------+-------------+------------+----------------+-------+--------------------+----------------+---------------------------+ 
+| users      |  float            | float              |                | NO          |            |                |       | NULL               | NULL           | NULL                      | 
+| users      |  double           | double             |                | NO          |            |                |       | NULL               | NULL           | NULL                      | 
+| users      |  float_unsigned   | float unsigned     |                | NO          |            |                |       | NULL               | NULL           | NULL                      | 
+| users      |  double_unsigned  | double unsigned    |                | NO          |            |                |       | NULL               | NULL           | NULL                      | 
+| users      |  float_unsigned_p | float(10) unsigned |                | NO          |            |                |       | NULL               | NULL           | NULL                      | 
++------------+-------------------+--------------------+----------------+-------------+------------+----------------+-------+--------------------+----------------+---------------------------+
 `))
 				m.noIndexes()
 				m.noFKs()
@@ -266,16 +266,16 @@ func TestDriver_InspectTable(t *testing.T) {
 				m.ExpectQuery(queryColumns).
 					WithArgs("public", "users").
 					WillReturnRows(sqltest.Rows(`
-+------------+--------------+---------------+----------------+-------------+------------+----------------+-------+--------------------+----------------+
-| table_name |  column_name | column_type   | column_comment | is_nullable | column_key | column_default | extra | character_set_name | collation_name |
-+------------+--------------+---------------+----------------+-------------+------------+----------------+-------+--------------------+----------------+
-| users      |  c1          | binary(20)    |                | NO          |            | NULL           |       | NULL               | NULL           |
-| users      |  c2          | varbinary(30) |                | NO          |            | NULL           |       | NULL               | NULL           |
-| users      |  c3          | tinyblob      |                | NO          |            | NULL           |       | NULL               | NULL           |
-| users      |  c4          | mediumblob    |                | NO          |            | NULL           |       | NULL               | NULL           |
-| users      |  c5          | blob          |                | NO          |            | NULL           |       | NULL               | NULL           |
-| users      |  c6          | longblob      |                | NO          |            | NULL           |       | NULL               | NULL           |
-+------------+--------------+---------------+----------------+-------------+------------+----------------+-------+--------------------+----------------+
++------------+--------------+---------------+----------------+-------------+------------+----------------+-------+--------------------+----------------+---------------------------+
+| table_name |  column_name | column_type   | column_comment | is_nullable | column_key | column_default | extra | character_set_name | collation_name | generation_expression     |
++------------+--------------+---------------+----------------+-------------+------------+----------------+-------+--------------------+----------------+---------------------------+
+| users      |  c1          | binary(20)    |                | NO          |            | NULL           |       | NULL               | NULL           | NULL                      |
+| users      |  c2          | varbinary(30) |                | NO          |            | NULL           |       | NULL               | NULL           | NULL                      |
+| users      |  c3          | tinyblob      |                | NO          |            | NULL           |       | NULL               | NULL           | NULL                      |
+| users      |  c4          | mediumblob    |                | NO          |            | NULL           |       | NULL               | NULL           | NULL                      |
+| users      |  c5          | blob          |                | NO          |            | NULL           |       | NULL               | NULL           | NULL                      |
+| users      |  c6          | longblob      |                | NO          |            | NULL           |       | NULL               | NULL           | NULL                      |
++------------+--------------+---------------+----------------+-------------+------------+----------------+-------+--------------------+----------------+---------------------------+
 `))
 				m.noIndexes()
 				m.noFKs()
@@ -300,16 +300,16 @@ func TestDriver_InspectTable(t *testing.T) {
 				m.ExpectQuery(queryColumns).
 					WithArgs("public", "users").
 					WillReturnRows(sqltest.Rows(`
-+------------+-------------+---------------+----------------+-------------+------------+----------------+-------+--------------------+----------------+
-| table_name | column_name | column_type   | column_comment | is_nullable | column_key | column_default | extra | character_set_name | collation_name |
-+------------+-------------+---------------+----------------+-------------+------------+----------------+-------+--------------------+----------------+
-| users      | c1          | char(20)      |                | NO          |            | char           |       | NULL               | NULL           |
-| users      | c2          | varchar(30)   |                | NO          |            | NULL           |       | NULL               | NULL           |
-| users      | c3          | tinytext      |                | NO          |            | NULL           |       | NULL               | NULL           |
-| users      | c4          | mediumtext    |                | NO          |            | NULL           |       | NULL               | NULL           |
-| users      | c5          | text          |                | NO          |            | NULL           |       | NULL               | NULL           |
-| users      | c6          | longtext      |                | NO          |            | NULL           |       | NULL               | NULL           |
-+------------+-------------+---------------+----------------+-------------+------------+----------------+-------+--------------------+----------------+
++------------+-------------+---------------+----------------+-------------+------------+----------------+-------+--------------------+----------------+---------------------------+
+| table_name | column_name | column_type   | column_comment | is_nullable | column_key | column_default | extra | character_set_name | collation_name | generation_expression     |
++------------+-------------+---------------+----------------+-------------+------------+----------------+-------+--------------------+----------------+---------------------------+
+| users      | c1          | char(20)      |                | NO          |            | char           |       | NULL               | NULL           | NULL                      |
+| users      | c2          | varchar(30)   |                | NO          |            | NULL           |       | NULL               | NULL           | NULL                      |
+| users      | c3          | tinytext      |                | NO          |            | NULL           |       | NULL               | NULL           | NULL                      |
+| users      | c4          | mediumtext    |                | NO          |            | NULL           |       | NULL               | NULL           | NULL                      |
+| users      | c5          | text          |                | NO          |            | NULL           |       | NULL               | NULL           | NULL                      |
+| users      | c6          | longtext      |                | NO          |            | NULL           |       | NULL               | NULL           | NULL                      |
++------------+-------------+---------------+----------------+-------------+------------+----------------+-------+--------------------+----------------+---------------------------+
 `))
 				m.noIndexes()
 				m.noFKs()
@@ -334,12 +334,12 @@ func TestDriver_InspectTable(t *testing.T) {
 				m.ExpectQuery(queryColumns).
 					WithArgs("public", "users").
 					WillReturnRows(sqltest.Rows(`
-+------------+-------------+---------------+----------------+-------------+------------+----------------+-------+--------------------+-------------------+
-| table_name | column_name | column_type   | column_comment | is_nullable | column_key | column_default | extra | character_set_name | collation_name    |
-+------------+-------------+---------------+----------------+-------------+------------+----------------+-------+--------------------+-------------------+
-| users      | c1          | enum('a','b') |                | NO          |            | NULL           |       | latin1             | latin1_swedish_ci |
-| users      | c2          | enum('c','d') |                | NO          |            | d              |       | latin1             | latin1_swedish_ci |
-+------------+-------------+---------------+----------------+-------------+------------+----------------+-------+--------------------+-------------------+
++------------+-------------+---------------+----------------+-------------+------------+----------------+-------+--------------------+-------------------+---------------------------+
+| table_name | column_name | column_type   | column_comment | is_nullable | column_key | column_default | extra | character_set_name | collation_name    | generation_expression     |
++------------+-------------+---------------+----------------+-------------+------------+----------------+-------+--------------------+-------------------+---------------------------+
+| users      | c1          | enum('a','b') |                | NO          |            | NULL           |       | latin1             | latin1_swedish_ci | NULL                      |
+| users      | c2          | enum('c','d') |                | NO          |            | d              |       | latin1             | latin1_swedish_ci | NULL                      |
++------------+-------------+---------------+----------------+-------------+------------+----------------+-------+--------------------+-------------------+---------------------------+
 `))
 				m.noIndexes()
 				m.noFKs()
@@ -360,17 +360,17 @@ func TestDriver_InspectTable(t *testing.T) {
 				m.ExpectQuery(queryColumns).
 					WithArgs("public", "users").
 					WillReturnRows(sqltest.Rows(`
-+------------+-------------+--------------+-------------------+-------------+------------+----------------------+--------------------------------+--------------------+----------------+
-| table_name | column_name | column_type  | column_comment    | is_nullable | column_key | column_default       | extra                          | character_set_name | collation_name |
-+------------+-------------+--------------+-------------------+-------------+------------+----------------------+--------------------------------+--------------------+----------------+
-| users      | c1          | date         |                   | NO          |            | NULL                 |                                | NULL               | NULL           |
-| users      | c2          | datetime     |                   | NO          |            | NULL                 |                                | NULL               | NULL           |
-| users      | c3          | time         |                   | NO          |            | NULL                 |                                | NULL               | NULL           |
-| users      | c4          | timestamp    |                   | NO          |            | CURRENT_TIMESTAMP    | on update CURRENT_TIMESTAMP    | NULL               | NULL           |
-| users      | c5          | year(4)      |                   | NO          |            | NULL                 |                                | NULL               | NULL           |
-| users      | c6          | year         |                   | NO          |            | NULL                 |                                | NULL               | NULL           |
-| users      | c7          | timestamp(6) |                   | NO          |            | CURRENT_TIMESTAMP(6) | on update CURRENT_TIMESTAMP(6) | NULL               | NULL           |
-+------------+--------------+-------------------+-------------+------------+----------------------+--------------------------------+--------------------+----------------+
++------------+-------------+--------------+-------------------+-------------+------------+----------------------+--------------------------------+--------------------+----------------+---------------------------+
+| table_name | column_name | column_type  | column_comment    | is_nullable | column_key | column_default       | extra                          | character_set_name | collation_name | generation_expression     |
++------------+-------------+--------------+-------------------+-------------+------------+----------------------+--------------------------------+--------------------+----------------+---------------------------+
+| users      | c1          | date         |                   | NO          |            | NULL                 |                                | NULL               | NULL           | NULL                      |
+| users      | c2          | datetime     |                   | NO          |            | NULL                 |                                | NULL               | NULL           | NULL                      |
+| users      | c3          | time         |                   | NO          |            | NULL                 |                                | NULL               | NULL           | NULL                      |
+| users      | c4          | timestamp    |                   | NO          |            | CURRENT_TIMESTAMP    | on update CURRENT_TIMESTAMP    | NULL               | NULL           | NULL                      |
+| users      | c5          | year(4)      |                   | NO          |            | NULL                 |                                | NULL               | NULL           | NULL                      |
+| users      | c6          | year         |                   | NO          |            | NULL                 |                                | NULL               | NULL           | NULL                      |
+| users      | c7          | timestamp(6) |                   | NO          |            | CURRENT_TIMESTAMP(6) | on update CURRENT_TIMESTAMP(6) | NULL               | NULL           | NULL                      |
++------------+--------------+-------------------+-------------+------------+----------------------+--------------------------------+--------------------+-------------+----------------+---------------------------+
 `))
 				m.noIndexes()
 				m.noFKs()
@@ -397,11 +397,11 @@ func TestDriver_InspectTable(t *testing.T) {
 				m.ExpectQuery(queryColumns).
 					WithArgs("public", "users").
 					WillReturnRows(sqltest.Rows(`
-+------------+------------+-------------+----------------+-------------+------------+----------------+-------+--------------------+----------------+
-| TABLE_NAME |COLUMN_NAME | COLUMN_TYPE | COLUMN_COMMENT | IS_NULLABLE | COLUMN_KEY | COLUMN_DEFAULT | EXTRA | CHARACTER_SET_NAME | COLLATION_NAME |
-+------------+------------+-------------+----------------+-------------+------------+----------------+-------+--------------------+----------------+
-| users      |c1          | json        |                | NO          |            | NULL           |       | NULL               | NULL           |
-+------------+------------+-------------+----------------+-------------+------------+----------------+-------+--------------------+----------------+
++------------+------------+-------------+----------------+-------------+------------+----------------+-------+--------------------+----------------+---------------------------+
+| TABLE_NAME |COLUMN_NAME | COLUMN_TYPE | COLUMN_COMMENT | IS_NULLABLE | COLUMN_KEY | COLUMN_DEFAULT | EXTRA | CHARACTER_SET_NAME | COLLATION_NAME | GENERATION_EXPRESSION     |
++------------+------------+-------------+----------------+-------------+------------+----------------+-------+--------------------+----------------+---------------------------+
+| users      |c1          | json        |                | NO          |            | NULL           |       | NULL               | NULL           | NULL                      |
++------------+------------+-------------+----------------+-------------+------------+----------------+-------+--------------------+----------------+---------------------------+
 `))
 				m.noIndexes()
 				m.noFKs()
@@ -421,19 +421,19 @@ func TestDriver_InspectTable(t *testing.T) {
 				m.ExpectQuery(queryColumns).
 					WithArgs("public", "users").
 					WillReturnRows(sqltest.Rows(`
-+------------+-------------+--------------------+----------------+-------------+------------+----------------+-------+--------------------+----------------+
-| table_name | column_name | column_type        | column_comment | is_nullable | column_key | column_default | extra | character_set_name | collation_name |
-+------------+-------------+--------------------+----------------+-------------+------------+----------------+-------+--------------------+----------------+
-| users      | c1          | point              |                | NO          |            | NULL           |       | NULL               | NULL           |
-| users      | c2          | multipoint         |                | NO          |            | NULL           |       | NULL               | NULL           |
-| users      | c3          | linestring         |                | NO          |            | NULL           |       | NULL               | NULL           |
-| users      | c4          | multilinestring    |                | NO          |            | NULL           |       | NULL               | NULL           |
-| users      | c5          | polygon            |                | NO          |            | NULL           |       | NULL               | NULL           |
-| users      | c6          | multipolygon       |                | NO          |            | NULL           |       | NULL               | NULL           |
-| users      | c7          | geometry           |                | NO          |            | NULL           |       | NULL               | NULL           |
-| users      | c8          | geometrycollection |                | NO          |            | NULL           |       | NULL               | NULL           |
-| users      | c9          | geomcollection     |                | NO          |            | NULL           |       | NULL               | NULL           |
-+------------+-------------+--------------------+----------------+-------------+------------+----------------+-------+--------------------+----------------+
++------------+-------------+--------------------+----------------+-------------+------------+----------------+-------+--------------------+----------------+---------------------------+ 
+| table_name | column_name | column_type        | column_comment | is_nullable | column_key | column_default | extra | character_set_name | collation_name | GENERATION_EXPRESSION     | 
++------------+-------------+--------------------+----------------+-------------+------------+----------------+-------+--------------------+----------------+---------------------------+ 
+| users      | c1          | point              |                | NO          |            | NULL           |       | NULL               | NULL           | NULL                      | 
+| users      | c2          | multipoint         |                | NO          |            | NULL           |       | NULL               | NULL           | NULL                      | 
+| users      | c3          | linestring         |                | NO          |            | NULL           |       | NULL               | NULL           | NULL                      | 
+| users      | c4          | multilinestring    |                | NO          |            | NULL           |       | NULL               | NULL           | NULL                      | 
+| users      | c5          | polygon            |                | NO          |            | NULL           |       | NULL               | NULL           | NULL                      |  
+| users      | c6          | multipolygon       |                | NO          |            | NULL           |       | NULL               | NULL           | NULL                      |  
+| users      | c7          | geometry           |                | NO          |            | NULL           |       | NULL               | NULL           | NULL                      |  
+| users      | c8          | geometrycollection |                | NO          |            | NULL           |       | NULL               | NULL           | NULL                      |  
+| users      | c9          | geomcollection     |                | NO          |            | NULL           |       | NULL               | NULL           | NULL                      |  
++------------+-------------+--------------------+----------------+-------------+------------+----------------+-------+--------------------+----------------+---------------------------+
 `))
 				m.noIndexes()
 				m.noFKs()
@@ -454,6 +454,34 @@ func TestDriver_InspectTable(t *testing.T) {
 				}, t.Columns)
 			},
 		},
+		{
+			name: "generated columns",
+			before: func(m mock) {
+				m.tableExists("public", "users", true)
+				m.ExpectQuery(queryColumns).
+					WithArgs("public", "users").
+					WillReturnRows(sqltest.Rows(`
++------------+-------------+-------------+----------------+-------------+------------+----------------+-------------------+--------------------+----------------+-----------------------+
+| TABLE_NAME | COLUMN_NAME | COLUMN_TYPE | COLUMN_COMMENT | IS_NULLABLE | COLUMN_KEY | COLUMN_DEFAULT | EXTRA             | CHARACTER_SET_NAME | COLLATION_NAME | GENERATION_EXPRESSION |
++------------+-------------+-------------+----------------+-------------+------------+----------------+-------------------+--------------------+----------------+-----------------------+
+| users      | c1          | int         |                | NO          |            | NULL           |                   | NULL               | NULL           |                       |
+| users      | c2          | int         |                | NO          |            | NULL           | VIRTUAL GENERATED | NULL               | NULL           | ` + "(`c1` * `c1`)" + `   |
+| users      | c3          | int         |                | NO          |            | NULL           | STORED GENERATED  | NULL               | NULL           | ` + "(`c1` + `c2`)" + `   |
++------------+-------------+-------------+----------------+-------------+------------+----------------+-------------------+--------------------+----------------+-----------------------+
+`))
+				m.noIndexes()
+				m.noFKs()
+			},
+			expect: func(require *require.Assertions, t *schema.Table, err error) {
+				require.NoError(err)
+				require.Equal("users", t.Name)
+				require.EqualValues([]*schema.Column{
+					{Name: "c1", Type: &schema.ColumnType{Raw: "int", Type: &schema.IntegerType{T: "int"}}},
+					{Name: "c2", Type: &schema.ColumnType{Raw: "int", Type: &schema.IntegerType{T: "int"}}, Attrs: []schema.Attr{&schema.GeneratedExpr{Expr: "(`c1` * `c1`)", Type: "VIRTUAL"}}},
+					{Name: "c3", Type: &schema.ColumnType{Raw: "int", Type: &schema.IntegerType{T: "int"}}, Attrs: []schema.Attr{&schema.GeneratedExpr{Expr: "(`c1` + `c2`)", Type: "STORED"}}},
+				}, t.Columns)
+			},
+		},
 		{
 			name: "indexes",
 			before: func(m mock) {
@@ -461,14 +489,14 @@ func TestDriver_InspectTable(t *testing.T) {
 				m.ExpectQuery(queryColumns).
 					WithArgs("public", "users").
 					WillReturnRows(sqltest.Rows(`
-+------------+-------------+--------------+----------------+-------------+------------+----------------+----------------+--------------------+--------------------+
-| TABLE_NAME | COLUMN_NAME | COLUMN_TYPE  | COLUMN_COMMENT | IS_NULLABLE | COLUMN_KEY | COLUMN_DEFAULT | EXTRA          | CHARACTER_SET_NAME | COLLATION_NAME     |
-+------------+-------------+--------------+----------------+-------------+------------+----------------+----------------+--------------------+--------------------+
-| users      | id          | int          |                | NO          | PRI        | NULL           |                | NULL               | NULL               |
-| users      | nickname    | varchar(255) |                | NO          | UNI        | NULL           |                | utf8mb4            | utf8mb4_0900_ai_ci |
-| users      | oid         | int          |                | NO          | MUL        | NULL           |                | NULL               | NULL               |
-| users      | uid         | int          |                | NO          | MUL        | NULL           |                | NULL               | NULL               |
-+------------+-------------+--------------+----------------+-------------+------------+----------------+----------------+--------------------+--------------------+
++------------+-------------+--------------+----------------+-------------+------------+----------------+----------------+--------------------+--------------------+---------------------------+
+| TABLE_NAME | COLUMN_NAME | COLUMN_TYPE  | COLUMN_COMMENT | IS_NULLABLE | COLUMN_KEY | COLUMN_DEFAULT | EXTRA          | CHARACTER_SET_NAME | COLLATION_NAME     | GENERATION_EXPRESSION     |
++------------+-------------+--------------+----------------+-------------+------------+----------------+----------------+--------------------+--------------------+---------------------------+
+| users      | id          | int          |                | NO          | PRI        | NULL           |                | NULL               | NULL               | NULL                      |
+| users      | nickname    | varchar(255) |                | NO          | UNI        | NULL           |                | utf8mb4            | utf8mb4_0900_ai_ci | NULL                      |
+| users      | oid         | int          |                | NO          | MUL        | NULL           |                | NULL               | NULL               | NULL                      |
+| users      | uid         | int          |                | NO          | MUL        | NULL           |                | NULL               | NULL               | NULL                      |
++------------+-------------+--------------+----------------+-------------+------------+----------------+----------------+--------------------+--------------------+---------------------------+
 `))
 				m.ExpectQuery(queryIndexesExpr).
 					WithArgs("public", "users").
@@ -491,6 +519,7 @@ func TestDriver_InspectTable(t *testing.T) {
 +-------+---------------------------------------------------------------------------------------------------------------------------------------------+
 | Table | Create Table                                                                                                                                |
 +-------+---------------------------------------------------------------------------------------------------------------------------------------------+
++-------+---------------------------------------------------------------------------------------------------------------------------------------------+
 | users | CREATE TABLE users (id bigint NOT NULL AUTO_INCREMENT) ENGINE=InnoDB AUTO_INCREMENT=55834574848 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin |
 +-------+---------------------------------------------------------------------------------------------------------------------------------------------+
 `))
@@ -539,13 +568,13 @@ func TestDriver_InspectTable(t *testing.T) {
 				m.ExpectQuery(queryColumns).
 					WithArgs("public", "users").
 					WillReturnRows(sqltest.Rows(`
-+------------+-------------+--------------+----------------+-------------+------------+----------------+----------------+--------------------+--------------------+
-| TABLE_NAME | COLUMN_NAME | COLUMN_TYPE  | COLUMN_COMMENT | IS_NULLABLE | COLUMN_KEY | COLUMN_DEFAULT | EXTRA          | CHARACTER_SET_NAME | COLLATION_NAME     |
-+------------+-------------+--------------+----------------+-------------+------------+----------------+----------------+--------------------+--------------------+
-| users      | id          | int          |                | NO          | PRI        | NULL           |                | NULL               | NULL               |
-| users      | oid         | int          |                | NO          | MUL        | NULL           |                | NULL               | NULL               |
-| users      | uid         | int          |                | NO          | MUL        | NULL           |                | NULL               | NULL               |
-+------------+-------------+--------------+----------------+-------------+------------+----------------+----------------+--------------------+--------------------+
++------------+-------------+--------------+----------------+-------------+------------+----------------+----------------+--------------------+--------------------+---------------------------+ 
+| TABLE_NAME | COLUMN_NAME | COLUMN_TYPE  | COLUMN_COMMENT | IS_NULLABLE | COLUMN_KEY | COLUMN_DEFAULT | EXTRA          | CHARACTER_SET_NAME | COLLATION_NAME     | GENERATION_EXPRESSION     | 
++------------+-------------+--------------+----------------+-------------+------------+----------------+----------------+--------------------+--------------------+---------------------------+ 
+| users      | id          | int          |                | NO          | PRI        | NULL           |                | NULL               | NULL               | NULL                      | 
+| users      | oid         | int          |                | NO          | MUL        | NULL           |                | NULL               | NULL               | NULL                      | 
+| users      | uid         | int          |                | NO          | MUL        | NULL           |                | NULL               | NULL               | NULL                      | 
++------------+-------------+--------------+----------------+-------------+------------+----------------+----------------+--------------------+--------------------+---------------------------+ 
 `))
 				m.noIndexes()
 				m.ExpectQuery(queryFKs).
@@ -588,12 +617,12 @@ func TestDriver_InspectTable(t *testing.T) {
 				m.ExpectQuery(queryColumns).
 					WithArgs("public", "users").
 					WillReturnRows(sqltest.Rows(`
-+-------------+-------------+--------------+----------------+-------------+------------+----------------+----------------+--------------------+--------------------+
-| TABLE_NAME  | COLUMN_NAME | COLUMN_TYPE  | COLUMN_COMMENT | IS_NULLABLE | COLUMN_KEY | COLUMN_DEFAULT | EXTRA          | CHARACTER_SET_NAME | COLLATION_NAME     |
-+-------------+-------------+--------------+----------------+-------------+------------+----------------+----------------+--------------------+--------------------+
-| users       | id          | int          |                | NO          | PRI        | NULL           |                | NULL               | NULL               |
-| users       | c1          | int          |                | NO          | MUL        | NULL           |                | NULL               | NULL               |
-+-------------+-------------+--------------+----------------+-------------+------------+----------------+----------------+--------------------+--------------------+
++-------------+-------------+--------------+----------------+-------------+------------+----------------+----------------+--------------------+--------------------+---------------------------+ 
+| TABLE_NAME  | COLUMN_NAME | COLUMN_TYPE  | COLUMN_COMMENT | IS_NULLABLE | COLUMN_KEY | COLUMN_DEFAULT | EXTRA          | CHARACTER_SET_NAME | COLLATION_NAME     | GENERATION_EXPRESSION     | 
++-------------+-------------+--------------+----------------+-------------+------------+----------------+----------------+--------------------+--------------------+---------------------------+ 
+| users       | id          | int          |                | NO          | PRI        | NULL           |                | NULL               | NULL               | NULL                      | 
+| users       | c1          | int          |                | NO          | MUL        | NULL           |                | NULL               | NULL               | NULL                      | 
++-------------+-------------+--------------+----------------+-------------+------------+----------------+----------------+--------------------+--------------------+---------------------------+ 
 `))
 				m.noIndexes()
 				m.noFKs()
@@ -731,17 +760,17 @@ func TestDriver_InspectSchema(t *testing.T) {
 +-------------+----------------------------+------------------------+
 `))
 				m.tables("public", "users", "pets")
-				m.ExpectQuery(sqltest.Escape(fmt.Sprintf(columnsQuery, "?, ?"))).
+				m.ExpectQuery(sqltest.Escape(fmt.Sprintf(columnsExprQuery, "?, ?"))).
 					WithArgs("public", "users", "pets").
 					WillReturnRows(sqltest.Rows(`
-+-------------+-------------+--------------+----------------+-------------+------------+----------------+----------------+--------------------+--------------------+
-| TABLE_NAME  | COLUMN_NAME | COLUMN_TYPE  | COLUMN_COMMENT | IS_NULLABLE | COLUMN_KEY | COLUMN_DEFAULT | EXTRA          | CHARACTER_SET_NAME | COLLATION_NAME     |
-+-------------+-------------+--------------+----------------+-------------+------------+----------------+----------------+--------------------+--------------------+
-| users       | id          | int          |                | NO          | PRI        | NULL           |                | NULL               | NULL               |
-| users       | spouse_id   | int          |                | YES         | NULL       | NULL           |                | NULL               | NULL               |
-| pets        | id          | int          |                | NO          | PRI        | NULL           |                | NULL               | NULL               |  
-| pets        | owner_id    | int          |                | YES         | NULL       | NULL           |                | NULL               | NULL               |  
-+-------------+-------------+--------------+----------------+-------------+------------+----------------+----------------+--------------------+--------------------+
++-------------+-------------+--------------+----------------+-------------+------------+----------------+----------------+--------------------+--------------------+---------------------------+
+| TABLE_NAME  | COLUMN_NAME | COLUMN_TYPE  | COLUMN_COMMENT | IS_NULLABLE | COLUMN_KEY | COLUMN_DEFAULT | EXTRA          | CHARACTER_SET_NAME | COLLATION_NAME     | GENERATION_EXPRESSION     |
++-------------+-------------+--------------+----------------+-------------+------------+----------------+----------------+--------------------+--------------------+---------------------------+
+| users       | id          | int          |                | NO          | PRI        | NULL           |                | NULL               | NULL               | NULL                      |
+| users       | spouse_id   | int          |                | YES         | NULL       | NULL           |                | NULL               | NULL               | NULL                      |
+| pets        | id          | int          |                | NO          | PRI        | NULL           |                | NULL               | NULL               | NULL                      |
+| pets        | owner_id    | int          |                | YES         | NULL       | NULL           |                | NULL               | NULL               | NULL                      |
++-------------+-------------+--------------+----------------+-------------+------------+----------------+----------------+--------------------+--------------------+---------------------------+
 				`))
 				m.ExpectQuery(sqltest.Escape(fmt.Sprintf(indexesExprQuery, "?, ?"))).
 					WillReturnRows(sqlmock.NewRows([]string{"table_name", "index_name", "column_name", "non_unique", "key_part", "expression"}))
diff --git a/sql/mysql/migrate_test.go b/sql/mysql/migrate_test.go
index 75b844db..aed9b689 100644
--- a/sql/mysql/migrate_test.go
+++ b/sql/mysql/migrate_test.go
@@ -488,6 +488,34 @@ func TestPlanChanges(t *testing.T) {
 				},
 			},
 		},
+		{
+			input: []schema.Change{
+				func() schema.Change {
+					users := schema.NewTable("users").
+						AddColumns(schema.NewIntColumn("c1", "int"))
+					return &schema.ModifyTable{
+						T: users,
+						Changes: []schema.Change{
+							&schema.AddColumn{
+								C: schema.NewIntColumn("c2", "int").SetGeneratedExpr(&schema.GeneratedExpr{Expr: "c1*2"}),
+							},
+							&schema.AddColumn{
+								C: schema.NewIntColumn("c3", "int").SetGeneratedExpr(&schema.GeneratedExpr{Expr: "c1*c2", Type: "STORED"}),
+							},
+						},
+					}
+				}(),
+			},
+			wantPlan: &migrate.Plan{
+				Reversible: true,
+				Changes: []*migrate.Change{
+					{
+						Cmd:     "ALTER TABLE `users` ADD COLUMN `c2` int AS (c1*2) NOT NULL, ADD COLUMN `c3` int AS (c1*c2) STORED NOT NULL",
+						Reverse: "ALTER TABLE `users` DROP COLUMN `c2`, DROP COLUMN `c3`",
+					},
+				},
+			},
+		},
 		{
 			input: []schema.Change{
 				func() schema.Change {
diff --git a/sql/schema/dsl_test.go b/sql/schema/dsl_test.go
index b721028b..0989dff1 100644
--- a/sql/schema/dsl_test.go
+++ b/sql/schema/dsl_test.go
@@ -170,6 +170,14 @@ func TestSchema_SetComment(t *testing.T) {
 	require.Equal(t, &schema.Comment{Text: "2"}, s.Attrs[0])
 }
 
+func TestSchema_SetGeneratedExpr(t *testing.T) {
+	c := schema.NewIntColumn("c", "int")
+	require.Empty(t, c.Attrs)
+	x := &schema.GeneratedExpr{Expr: "d*2", Type: "VIRTUAL"}
+	c.SetGeneratedExpr(x)
+	require.Equal(t, []schema.Attr{x}, c.Attrs)
+}
+
 func TestCheck(t *testing.T) {
 	enforced := &struct{ schema.Attr }{}
 	tbl := schema.NewTable("table").