diff --git a/sql/mysql/driver.go b/sql/mysql/driver.go index e9de037d3ca..4e7edb6dacf 100644 --- a/sql/mysql/driver.go +++ b/sql/mysql/driver.go @@ -464,6 +464,7 @@ const ( currentTS = "current_timestamp" defaultGen = "default_generated" autoIncrement = "auto_increment" + invisible = "INVISIBLE" virtual = "VIRTUAL" stored = "STORED" diff --git a/sql/mysql/inspect.go b/sql/mysql/inspect.go index 88129eee981..6cdfe551d5f 100644 --- a/sql/mysql/inspect.go +++ b/sql/mysql/inspect.go @@ -294,6 +294,8 @@ func (i *inspect) addColumn(s *schema.Schema, rows *sql.Rows) error { if err != nil { return err } + // The column has an expression default value, + // and it is handled in Driver.addColumn. if attr.autoinc { a := &AutoIncrement{} if !sqlx.Has(t.Attrs, a) { @@ -307,6 +309,9 @@ func (i *inspect) addColumn(s *schema.Schema, rows *sql.Rows) error { if attr.onUpdate != "" { c.Attrs = append(c.Attrs, &OnUpdate{A: attr.onUpdate}) } + if attr.invisible { + c.Attrs = append(c.Attrs, &Invisible{}) + } if x := expr.String; x != "" { if !i.Maria() { x = unescape(x) @@ -519,33 +524,56 @@ type extraAttr struct { onUpdate string generatedType string defaultGenerated bool + invisible bool } var ( - reGenerateType = regexp.MustCompile(`(?i)^(stored|persistent|virtual) generated$`) - reTimeOnUpdate = regexp.MustCompile(`(?i)^(?:default_generated )?on update (current_timestamp(?:\(\d?\))?)$`) + reTimeOnUpdate = regexp.MustCompile(`(?i)^(?:default_generated )?on update (current_timestamp(?:\(\d?\))?)$`) + reGenerateType = regexp.MustCompile(`(?i)^(virtual|stored|persistent)( generated)?|generated always as \((.*)\) (stored|virtual|persistent)$`) + reAutoIncrement = regexp.MustCompile(`(?i)\bauto_increment\b`) + reDefaultGen = regexp.MustCompile(`(?i)\bDEFAULT_GENERATED\b`) + reInvisible = regexp.MustCompile(`(?i)\bINVISIBLE\b`) ) // 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: + + if extra == "" || strings.ToLower(extra) == "null" { + return attr, nil + } + + if reDefaultGen.MatchString(extra) { attr.defaultGenerated = true - // The column has an expression default value, - // and it is handled in Driver.addColumn. - case el == autoIncrement: + } + + if reAutoIncrement.MatchString(extra) { attr.autoinc = true - case reTimeOnUpdate.MatchString(extra): - attr.onUpdate = reTimeOnUpdate.FindStringSubmatch(extra)[1] - case reGenerateType.MatchString(extra): - attr.generatedType = reGenerateType.FindStringSubmatch(extra)[1] - default: - return nil, fmt.Errorf("unknown extra column attribute %q", extra) - } - return attr, nil + } + + if reInvisible.MatchString(extra) { + attr.invisible = true + } + + if match := reTimeOnUpdate.FindStringSubmatch(extra); match != nil { + attr.onUpdate = match[1] + } + + if match := reGenerateType.FindStringSubmatch(extra); match != nil { + if match[1] != "" { + attr.generatedType = strings.ToUpper(match[1]) + } else if match[4] != "" { + attr.generatedType = strings.ToUpper(match[4]) + } + + } + + if attr.defaultGenerated || attr.autoinc || attr.invisible || attr.onUpdate != "" || attr.generatedType != "" { + return attr, nil + } + + return nil, fmt.Errorf("unknown extra column attribute %q", extra) } // showCreate sets and fixes schema elements that require information from @@ -837,6 +865,10 @@ type ( V int64 } + Invisible struct { + schema.Attr + } + // CreateOptions attribute for describing extra options used with CREATE TABLE. CreateOptions struct { schema.Attr diff --git a/sql/mysql/inspect_test.go b/sql/mysql/inspect_test.go index 4a435c9234e..9bd0292239b 100644 --- a/sql/mysql/inspect_test.go +++ b/sql/mysql/inspect_test.go @@ -506,14 +506,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 | 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`)" + ` | -| users | c4 | varchar(20) | | NO | | NULL | STORED GENERATED | NULL | NULL | concat(_latin1\'\\\'\',_latin1\'"\') | -+------------+-------------+-------------+----------------+-------------+------------+----------------+-------------------+--------------------+----------------+--------------------------------------+ ++------------+-------------+-------------+----------------+-------------+------------+--------------------------------------------+-----------------------------+--------------------+----------------+--------------------------------------+ +| 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`)" + ` | +| users | c4 | varchar(20) | | NO | | NULL | STORED GENERATED | NULL | NULL | concat(_latin1\'\\\'\',_latin1\'"\') | +| users | c5 | varchar(50) | | NO | | NULL | STORED GENERATED INVISIBLE | NULL | NULL |` + "`char_length(`email`)`" + ` | +| users | c6 | varchar(20) | | NO | | concat(_latin1\'Hello \',` + "`name`" + `) | DEFAULT_GENERATED INVISIBLE | NULL | NULL | NULL | ++------------+-------------+-------------+----------------+-------------+------------+--------------------------------------------+-----------------------------+--------------------+----------------+--------------------------------------+ `)) m.noIndexes() m.noFKs() @@ -526,6 +528,11 @@ func TestDriver_InspectTable(t *testing.T) { {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"}}}, {Name: "c4", Type: &schema.ColumnType{Raw: "varchar(20)", Type: &schema.StringType{T: "varchar", Size: 20}}, Attrs: []schema.Attr{&schema.GeneratedExpr{Expr: "concat(_latin1'\\'',_latin1'\"')", Type: "STORED"}}}, + {Name: "c5", Type: &schema.ColumnType{Raw: "varchar(50)", Type: &schema.StringType{T: "varchar", Size: 50}}, Attrs: []schema.Attr{ + &Invisible{}, + &schema.GeneratedExpr{Expr: "`char_length(`email`)`", Type: "STORED"}, + }}, + {Name: "c6", Type: &schema.ColumnType{Raw: "varchar(20)", Type: &schema.StringType{T: "varchar", Size: 20}}, Attrs: []schema.Attr{&Invisible{}}, Default: &schema.RawExpr{X: "(concat(_latin1'Hello ',`name`))"}}, }, t.Columns) }, }, diff --git a/sql/mysql/migrate.go b/sql/mysql/migrate.go index 96da373483d..c8b61a19318 100644 --- a/sql/mysql/migrate.go +++ b/sql/mysql/migrate.go @@ -600,6 +600,9 @@ func (s *state) column(b *sqlx.Builder, t *schema.Table, c *schema.Column) error if a.V > 0 && !sqlx.Has(t.Attrs, &AutoIncrement{}) { t.Attrs = append(t.Attrs, a) } + case *Invisible: + b.P("INVISIBLE") + default: s.attr(b, a) } diff --git a/sql/mysql/sqlspec.go b/sql/mysql/sqlspec.go index 4520fde8f6e..4f18a86c789 100644 --- a/sql/mysql/sqlspec.go +++ b/sql/mysql/sqlspec.go @@ -262,6 +262,15 @@ func convertColumn(spec *sqlspec.Column, _ *schema.Table) (*schema.Column, error c.AddAttrs(&AutoIncrement{}) } } + if attr, ok := spec.Attr("invisible"); ok { + b, err := attr.Bool() + if err != nil { + return nil, err + } + if b { + c.AddAttrs(&Invisible{}) + } + } if err := specutil.ConvertGenExpr(spec.Remain(), c, storedOrVirtual); err != nil { return nil, err } @@ -384,6 +393,9 @@ func columnSpec(c *schema.Column, t *schema.Table) (*sqlspec.Column, error) { if sqlx.Has(c.Attrs, &AutoIncrement{}) { spec.Extra.Attrs = append(spec.Extra.Attrs, schemahcl.BoolAttr("auto_increment", true)) } + if sqlx.Has(c.Attrs, &Invisible{}) { + spec.Extra.Attrs = append(spec.Extra.Attrs, schemahcl.BoolAttr("invisible", true)) + } if x := (schema.GeneratedExpr{}); sqlx.Has(c.Attrs, &x) { spec.Extra.Children = append(spec.Extra.Children, specutil.FromGenExpr(x, storedOrVirtual)) } diff --git a/sql/mysql/sqlspec_test.go b/sql/mysql/sqlspec_test.go index e778085e8c0..a8f902eb69f 100644 --- a/sql/mysql/sqlspec_test.go +++ b/sql/mysql/sqlspec_test.go @@ -30,16 +30,25 @@ func TestSQLSpec(t *testing.T) { column "price1" { null = false type = int + + } column "price2" { null = false type = int auto_increment = true + } + column "price_deprecated" { + null = false + type = int + invisible = true + } column "account_name" { null = false type = varchar(32) default = "unknown" + } column "account_type" { null = false @@ -151,6 +160,15 @@ schema "schema" { }, Attrs: []schema.Attr{&AutoIncrement{}}, }, + { + Name: "price_deprecated", + Type: &schema.ColumnType{ + Type: &schema.IntegerType{ + T: TypeInt, + }, + }, + Attrs: []schema.Attr{&Invisible{}}, + }, { Name: "account_name", Type: &schema.ColumnType{ @@ -269,7 +287,7 @@ schema "schema" { { Symbol: "accounts", Table: exp.Tables[0], - Columns: []*schema.Column{exp.Tables[0].Columns[4]}, + Columns: []*schema.Column{exp.Tables[0].Columns[5]}, RefTable: exp.Tables[1], RefColumns: []*schema.Column{exp.Tables[1].Columns[0]}, OnDelete: schema.SetNull, @@ -522,6 +540,41 @@ schema "test" { ` require.EqualValues(t, expected, string(buf)) } +func TestMarshalSpec_Invisible(t *testing.T) { + s := &schema.Schema{ + Name: "test", + Tables: []*schema.Table{ + { + Name: "users", + Columns: []*schema.Column{ + { + Name: "id", + Type: &schema.ColumnType{Type: &schema.IntegerType{T: "bigint"}}, + Attrs: []schema.Attr{ + &Invisible{}, + }, + }, + }, + }, + }, + } + s.Tables[0].Schema = s + buf, err := MarshalSpec(s, hclState) + require.NoError(t, err) + const expected = `table "users" { + schema = schema.test + column "id" { + null = false + type = bigint + invisible = true + } +} +schema "test" { +} +` + require.EqualValues(t, expected, string(buf)) +} + func TestMarshalSpec_Check(t *testing.T) { s := schema.New("test").