Repository: ariga/atlas
Pull request #1893 (2023-07-24 19:26:51)

sql/mysql: inspect fulltext index parsers


Patch:
diff --git a/sql/mysql/diff.go b/sql/mysql/diff.go
index 3368e5ca..c1f88666 100644
--- a/sql/mysql/diff.go
+++ b/sql/mysql/diff.go
@@ -176,7 +176,14 @@ func (d *diff) IsGeneratedIndexName(_ *schema.Table, idx *schema.Index) bool {
 
 // IndexAttrChanged reports if the index attributes were changed.
 func (*diff) IndexAttrChanged(from, to []schema.Attr) bool {
-	return indexType(from).T != indexType(to).T
+	if indexType(from).T != indexType(to).T {
+		return true
+	}
+	var (
+		fromP, toP     IndexParser
+		fromHas, toHas = sqlx.Has(from, &fromP), sqlx.Has(to, &toP)
+	)
+	return fromHas != toHas || (fromHas && fromP.P != toP.P)
 }
 
 // IndexPartAttrChanged reports if the index-part attributes (collation or prefix) were changed.
diff --git a/sql/mysql/driver.go b/sql/mysql/driver.go
index b96970eb..2abc838b 100644
--- a/sql/mysql/driver.go
+++ b/sql/mysql/driver.go
@@ -385,6 +385,9 @@ const (
 	IndexTypeFullText = "FULLTEXT"
 	IndexTypeSpatial  = "SPATIAL"
 
+	IndexParserNGram = "ngram"
+	IndexParserMeCab = "mecab"
+
 	EngineInnoDB = "InnoDB"
 	EngineMyISAM = "MyISAM"
 	EngineMemory = "Memory"
diff --git a/sql/mysql/inspect.go b/sql/mysql/inspect.go
index b6cc9e92..e09d8595 100644
--- a/sql/mysql/inspect.go
+++ b/sql/mysql/inspect.go
@@ -357,20 +357,16 @@ func (i *inspect) addIndexes(s *schema.Schema, rows *sql.Rows) error {
 		}
 		idx, ok := t.Index(name)
 		if !ok {
-			idx = &schema.Index{
-				Name:   name,
-				Unique: !nonuniq.Bool,
-				Table:  t,
-				Attrs: []schema.Attr{
-					&IndexType{T: indexType},
-				},
+			idx = schema.NewIndex(name).
+				SetUnique(!nonuniq.Bool).
+				AddAttrs(&IndexType{T: indexType})
+			if indexType == IndexTypeFullText {
+				putShow(t).addFullText(idx)
 			}
 			if sqlx.ValidString(comment) {
-				idx.Attrs = append(t.Attrs, &schema.Comment{
-					Text: comment.String,
-				})
+				idx.SetComment(comment.String)
 			}
-			t.Indexes = append(t.Indexes, idx)
+			t.AddIndexes(idx)
 		}
 		// Rows are ordered by SEQ_IN_INDEX that specifies the
 		// position of the column in the index definition.
@@ -532,10 +528,12 @@ func (i *inspect) showCreate(ctx context.Context, s *schema.Schema) error {
 		if !ok {
 			continue
 		}
-		if err := i.createStmt(ctx, t); err != nil {
+		c, err := i.createStmt(ctx, t)
+		if err != nil {
 			return err
 		}
-		if err := i.setAutoInc(st, t); err != nil {
+		st.setIndexParser(c)
+		if err := st.setAutoInc(t, c); err != nil {
 			return err
 		}
 	}
@@ -544,44 +542,19 @@ func (i *inspect) showCreate(ctx context.Context, s *schema.Schema) error {
 
 var reAutoinc = regexp.MustCompile(`(?i)\s*AUTO_INCREMENT\s*=\s*(\d+)\s*`)
 
-// setAutoInc extracts the updated AUTO_INCREMENT from CREATE TABLE.
-func (i *inspect) setAutoInc(s *showTable, t *schema.Table) error {
-	if s.auto == nil {
-		return nil
-	}
-	var c CreateStmt
-	if !sqlx.Has(t.Attrs, &c) {
-		return fmt.Errorf("missing CREATE TABLE statement in attributes for %q", t.Name)
-	}
-	if sqlx.Has(t.Attrs, &AutoIncrement{}) {
-		return fmt.Errorf("unexpected AUTO_INCREMENT attributes for table: %q", t.Name)
-	}
-	matches := reAutoinc.FindStringSubmatch(c.S)
-	if len(matches) != 2 {
-		return nil
-	}
-	v, err := strconv.ParseInt(matches[1], 10, 64)
-	if err != nil {
-		return err
-	}
-	s.auto.V = v
-	t.Attrs = append(t.Attrs, s.auto)
-	return nil
-}
-
 // createStmt loads the CREATE TABLE statement for the table.
-func (i *inspect) createStmt(ctx context.Context, t *schema.Table) error {
+func (i *inspect) createStmt(ctx context.Context, t *schema.Table) (*CreateStmt, error) {
 	c := &CreateStmt{}
 	b := &sqlx.Builder{QuoteOpening: '`', QuoteClosing: '`'}
 	rows, err := i.QueryContext(ctx, b.P("SHOW CREATE TABLE").Table(t).String())
 	if err != nil {
-		return fmt.Errorf("query CREATE TABLE %q: %w", t.Name, err)
+		return nil, fmt.Errorf("query CREATE TABLE %q: %w", t.Name, err)
 	}
 	if err := sqlx.ScanOne(rows, &sql.NullString{}, &c.S); err != nil {
-		return fmt.Errorf("scan CREATE TABLE %q: %w", t.Name, err)
+		return nil, fmt.Errorf("scan CREATE TABLE %q: %w", t.Name, err)
 	}
 	t.Attrs = append(t.Attrs, c)
-	return nil
+	return c, nil
 }
 
 var reCurrTimestamp = regexp.MustCompile(`(?i)^current_timestamp(?:\(\d?\))?$`)
@@ -870,6 +843,13 @@ type (
 		T string // BTREE, HASH, FULLTEXT, SPATIAL, RTREE
 	}
 
+	// IndexParser defines the parser plugin used
+	// by a FULLTEXT index.
+	IndexParser struct {
+		schema.Attr
+		P string // Name of the parser plugin. e.g., ngram or mecab.
+	}
+
 	// BitType represents the type bit.
 	BitType struct {
 		schema.Type
@@ -888,11 +868,72 @@ type (
 	// required and for what.
 	showTable struct {
 		schema.Attr
-		// AUTO_INCREMENT value to due missing value in information_schema.
+		// AUTO_INCREMENT value due to missing value in information_schema.
 		auto *AutoIncrement
+		// FULLTEXT indexes that might have custom parser.
+		idxs []*schema.Index
 	}
 )
 
+// addIndex adds an index to the list of indexes
+// that needs further processing.
+func (s *showTable) addFullText(idx *schema.Index) {
+	s.idxs = append(s.idxs, idx)
+}
+
+// setAutoInc extracts the updated AUTO_INCREMENT from CREATE TABLE.
+func (s *showTable) setAutoInc(t *schema.Table, c *CreateStmt) error {
+	if s.auto == nil {
+		return nil
+	}
+	if sqlx.Has(t.Attrs, &AutoIncrement{}) {
+		return fmt.Errorf("unexpected AUTO_INCREMENT attributes for table: %q", t.Name)
+	}
+	matches := reAutoinc.FindStringSubmatch(c.S)
+	if len(matches) != 2 {
+		return nil
+	}
+	v, err := strconv.ParseInt(matches[1], 10, 64)
+	if err != nil {
+		return err
+	}
+	s.auto.V = v
+	t.Attrs = append(t.Attrs, s.auto)
+	return nil
+}
+
+// reIndexParser matches the parser name from the index definition.
+var reIndexParser = regexp.MustCompile("/\\*!50100 WITH PARSER `([^`]+)` \\*/")
+
+// setIndexParser updates the FULLTEXT parser from CREATE TABLE statement.
+func (s *showTable) setIndexParser(c *CreateStmt) {
+	b := (&sqlx.Builder{QuoteOpening: '`', QuoteClosing: '`'}).P("FULLTEXT KEY")
+	for _, idx := range s.idxs {
+		bi := b.Clone().Ident(idx.Name).Wrap(func(b *sqlx.Builder) {
+			b.MapComma(idx.Parts, func(i int, b *sqlx.Builder) {
+				// We expect column names only, as functional
+				// fulltext indexes are not supported by MySQL.
+				if idx.Parts[i].C != nil {
+					b.Ident(idx.Parts[i].C.Name)
+				}
+			})
+		})
+		i := strings.Index(c.S, bi.String())
+		if i == -1 || i+bi.Len() >= len(c.S) {
+			continue
+		}
+		i += bi.Len()
+		j := strings.Index(c.S[i:], "\n")
+		if j == -1 {
+			continue
+		}
+		// The rest of the line holds index, algorithm and lock options.
+		if matches := reIndexParser.FindStringSubmatch(c.S[i : i+j]); len(matches) == 2 {
+			idx.AddAttrs(&IndexParser{P: matches[1]})
+		}
+	}
+}
+
 func putShow(t *schema.Table) *showTable {
 	for i := range t.Attrs {
 		if s, ok := t.Attrs[i].(*showTable); ok {
diff --git a/sql/mysql/migrate.go b/sql/mysql/migrate.go
index 9a7fb46d..f18e4907 100644
--- a/sql/mysql/migrate.go
+++ b/sql/mysql/migrate.go
@@ -587,7 +587,12 @@ func index(b *sqlx.Builder, idx *schema.Index) {
 	switch t := indexType(idx.Attrs); {
 	case idx.Unique:
 		b.P("UNIQUE")
-	case t.T == IndexTypeFullText || t.T == IndexTypeSpatial:
+	case t.T == IndexTypeFullText:
+		if p := (&IndexParser{}); sqlx.Has(idx.Attrs, p) {
+			defer func() { b.P("WITH PARSER").Ident(p.P) }()
+		}
+		b.P(t.T)
+	case t.T == IndexTypeSpatial:
 		b.P(t.T)
 	}
 	b.P("INDEX").Ident(idx.Name)
diff --git a/sql/mysql/sqlspec.go b/sql/mysql/sqlspec.go
index 7ac13390..f8dc3082 100644
--- a/sql/mysql/sqlspec.go
+++ b/sql/mysql/sqlspec.go
@@ -87,6 +87,7 @@ var (
 		schemahcl.WithTypes("view.column.type", TypeRegistry.Specs()),
 		schemahcl.WithScopedEnums("table.engine", EngineInnoDB, EngineMyISAM, EngineMemory, EngineCSV, EngineNDB),
 		schemahcl.WithScopedEnums("table.index.type", IndexTypeBTree, IndexTypeHash, IndexTypeFullText, IndexTypeSpatial),
+		schemahcl.WithScopedEnums("table.index.parser", IndexParserNGram, IndexParserMeCab),
 		schemahcl.WithScopedEnums("table.primary_key.type", IndexTypeBTree, IndexTypeHash, IndexTypeFullText, IndexTypeSpatial),
 		schemahcl.WithScopedEnums("table.column.as.type", stored, persistent, virtual),
 		schemahcl.WithScopedEnums("table.foreign_key.on_update", specutil.ReferenceVars...),
@@ -154,6 +155,9 @@ func convertPK(spec *sqlspec.PrimaryKey, parent *schema.Table) (*schema.Index, e
 	if err := convertIndexType(spec, idx); err != nil {
 		return nil, err
 	}
+	if err := convertIndexParser(spec, idx); err != nil {
+		return nil, err
+	}
 	return idx, nil
 }
 
@@ -166,6 +170,9 @@ func convertIndex(spec *sqlspec.Index, parent *schema.Table) (*schema.Index, err
 	if err := convertIndexType(spec, idx); err != nil {
 		return nil, err
 	}
+	if err := convertIndexParser(spec, idx); err != nil {
+		return nil, err
+	}
 	return idx, nil
 }
 
@@ -180,6 +187,17 @@ func convertIndexType(spec specutil.Attrer, idx *schema.Index) error {
 	return nil
 }
 
+func convertIndexParser(spec specutil.Attrer, idx *schema.Index) error {
+	if attr, ok := spec.Attr("parser"); ok {
+		p, err := attr.String()
+		if err != nil {
+			return err
+		}
+		idx.AddAttrs(&IndexParser{P: p})
+	}
+	return nil
+}
+
 func convertPart(spec *sqlspec.IndexPart, part *schema.IndexPart) error {
 	if attr, ok := spec.Attr("prefix"); ok {
 		if part.X != nil {
@@ -328,6 +346,17 @@ func indexTypeSpec(idx *schema.Index, attrs []*schemahcl.Attr) []*schemahcl.Attr
 	if i := (IndexType{}); sqlx.Has(idx.Attrs, &i) && i.T != IndexTypeBTree {
 		attrs = append(attrs, specutil.VarAttr("type", strings.ToUpper(i.T)))
 	}
+	// Print fulltext index parser. Use the pre-defined parser variables if known.
+	if p := (IndexParser{}); sqlx.Has(idx.Attrs, &p) && p.P != "" {
+		attr := schemahcl.StringAttr("parser", p.P)
+		for _, p1 := range []string{IndexParserNGram, IndexParserMeCab} {
+			if strings.EqualFold(p.P, p1) {
+				attr = specutil.VarAttr("parser", p1)
+				break
+			}
+		}
+		attrs = append(attrs, attr)
+	}
 	return attrs
 }
 
diff --git a/sql/mysql/tidb.go b/sql/mysql/tidb.go
index 6665d46e..da019a06 100644
--- a/sql/mysql/tidb.go
+++ b/sql/mysql/tidb.go
@@ -143,7 +143,7 @@ func (i *tinspect) patchSchema(ctx context.Context, s *schema.Schema) (*schema.S
 	for _, t := range s.Tables {
 		var createStmt CreateStmt
 		if ok := sqlx.Has(t.Attrs, &createStmt); !ok {
-			if err := i.createStmt(ctx, t); err != nil {
+			if _, err := i.createStmt(ctx, t); err != nil {
 				return nil, err
 			}
 		}


Test patch:
diff --git a/internal/integration/testdata/mysql/index-type.txt b/internal/integration/testdata/mysql/index-type.txt
index c18e6bf0..6998edaa 100644
--- a/internal/integration/testdata/mysql/index-type.txt
+++ b/internal/integration/testdata/mysql/index-type.txt
@@ -1,3 +1,5 @@
+only mysql8 mysql57
+
 apply 1.hcl
 cmpshow users 1.sql
 
@@ -18,11 +20,17 @@ table "users" {
     type = FULLTEXT
     columns = [column.text]
   }
+  index "ngram_text" {
+    type = FULLTEXT
+    parser = ngram
+    columns = [column.text]
+  }
 }
 
 -- 1.sql --
 CREATE TABLE `users` (
   `text` text NOT NULL,
+  FULLTEXT KEY `ngram_text` (`text`) /*!50100 WITH PARSER `ngram` */ ,
   FULLTEXT KEY `users_text` (`text`)
 )
 
diff --git a/sql/mysql/diff_test.go b/sql/mysql/diff_test.go
index 9e586bdf..ca5be3a0 100644
--- a/sql/mysql/diff_test.go
+++ b/sql/mysql/diff_test.go
@@ -441,6 +441,7 @@ func TestDiff_TableDiff(t *testing.T) {
 						{Name: "c1", Type: &schema.ColumnType{Raw: "json", Type: &schema.JSONType{T: "json"}}},
 						{Name: "c2", Type: &schema.ColumnType{Raw: "tinyint", Type: &schema.IntegerType{T: "tinyint"}}},
 						{Name: "c3", Type: &schema.ColumnType{Raw: "int", Type: &schema.IntegerType{T: "int"}}},
+						{Name: "c4", Type: &schema.ColumnType{Raw: "text", Type: &schema.StringType{T: "text"}}},
 					},
 				}
 				to = &schema.Table{
@@ -452,6 +453,7 @@ func TestDiff_TableDiff(t *testing.T) {
 						{Name: "c1", Type: &schema.ColumnType{Raw: "json", Type: &schema.JSONType{T: "json"}}},
 						{Name: "c2", Type: &schema.ColumnType{Raw: "tinyint", Type: &schema.IntegerType{T: "tinyint"}}},
 						{Name: "c3", Type: &schema.ColumnType{Raw: "int", Type: &schema.IntegerType{T: "int"}}},
+						{Name: "c4", Type: &schema.ColumnType{Raw: "text", Type: &schema.StringType{T: "text"}}},
 					},
 				}
 			)
@@ -460,12 +462,14 @@ func TestDiff_TableDiff(t *testing.T) {
 				{Name: "c2_unique", Unique: true, Table: from, Parts: []*schema.IndexPart{{SeqNo: 1, C: from.Columns[1]}}},
 				{Name: "c1_prefix", Table: from, Parts: []*schema.IndexPart{{SeqNo: 1, C: from.Columns[1], Attrs: []schema.Attr{&SubPart{Len: 50}}}}},
 				{Name: "c1_desc", Table: from, Parts: []*schema.IndexPart{{SeqNo: 1, C: from.Columns[1]}}},
+				{Name: "parser", Table: from, Parts: []*schema.IndexPart{{SeqNo: 1, C: from.Columns[3]}}, Attrs: []schema.Attr{&IndexType{T: IndexTypeFullText}, &IndexParser{P: "ngram"}}},
 			}
 			to.Indexes = []*schema.Index{
 				{Name: "c1_index", Table: from, Parts: []*schema.IndexPart{{SeqNo: 1, C: from.Columns[0]}}},
 				{Name: "c3_unique", Unique: true, Table: from, Parts: []*schema.IndexPart{{SeqNo: 1, C: to.Columns[1]}}},
 				{Name: "c1_prefix", Table: from, Parts: []*schema.IndexPart{{SeqNo: 1, C: from.Columns[0], Attrs: []schema.Attr{&SubPart{Len: 100}}}}},
 				{Name: "c1_desc", Table: from, Parts: []*schema.IndexPart{{SeqNo: 1, C: from.Columns[1], Desc: true}}},
+				{Name: "parser", Table: from, Parts: []*schema.IndexPart{{SeqNo: 1, C: from.Columns[3]}}, Attrs: []schema.Attr{&IndexType{T: IndexTypeFullText}}},
 			}
 			return testcase{
 				name: "indexes",
@@ -476,6 +480,7 @@ func TestDiff_TableDiff(t *testing.T) {
 					&schema.DropIndex{I: from.Indexes[1]},
 					&schema.ModifyIndex{From: from.Indexes[2], To: to.Indexes[2], Change: schema.ChangeParts},
 					&schema.ModifyIndex{From: from.Indexes[3], To: to.Indexes[3], Change: schema.ChangeParts},
+					&schema.ModifyIndex{From: from.Indexes[4], To: to.Indexes[4], Change: schema.ChangeAttr},
 					&schema.AddIndex{I: to.Indexes[1]},
 				},
 			}
diff --git a/sql/mysql/migrate_test.go b/sql/mysql/migrate_test.go
index acd86341..922f2f5d 100644
--- a/sql/mysql/migrate_test.go
+++ b/sql/mysql/migrate_test.go
@@ -258,25 +258,20 @@ func TestPlanChanges(t *testing.T) {
 		{
 			changes: []schema.Change{
 				func() schema.Change {
-					users := &schema.Table{
-						Name: "users",
-						Columns: []*schema.Column{
-							{Name: "id", Type: &schema.ColumnType{Type: &schema.IntegerType{T: "bigint"}}},
-							{
-								Name: "name",
-								Type: &schema.ColumnType{Type: &schema.StringType{T: "varchar(255)"}},
-								Indexes: []*schema.Index{
-									schema.NewIndex("name_index").
-										AddParts(schema.NewColumnPart(schema.NewColumn("name"))),
-								},
-							}},
-					}
+					users := schema.NewTable("users").
+						AddColumns(
+							schema.NewIntColumn("id", "bigint"),
+							schema.NewStringColumn("name", "varchar(255)"),
+						).
+						AddIndexes(
+							schema.NewIndex("name_index").
+								AddParts(schema.NewColumnPart(schema.NewColumn("name"))),
+						)
 					return &schema.ModifyTable{
 						T: users,
 						Changes: []schema.Change{
 							&schema.DropIndex{
-								I: schema.NewIndex("name_index").
-									AddParts(schema.NewColumnPart(schema.NewColumn("name"))),
+								I: users.Indexes[0],
 							},
 						},
 					}
@@ -524,12 +519,22 @@ func TestPlanChanges(t *testing.T) {
 						},
 					}
 					t.PrimaryKey = &schema.Index{Parts: []*schema.IndexPart{{C: t.Columns[0]}}}
+					t.AddIndexes(
+						schema.NewIndex("text").
+							AddParts(&schema.IndexPart{C: t.Columns[1]}).
+							AddAttrs(&IndexType{T: "FULLTEXT"}, &schema.Comment{Text: "text index"}, &IndexParser{P: "ngram"}),
+					)
 					return &schema.AddTable{T: t}
 				}(),
 			},
 			wantPlan: &migrate.Plan{
 				Reversible: true,
-				Changes:    []*migrate.Change{{Cmd: "CREATE TABLE `posts` (`id` bigint NOT NULL AUTO_INCREMENT, `text` text NULL, `uuid` char(36) CHARSET utf8mb4 NULL COLLATE utf8mb4_bin, PRIMARY KEY (`id`))", Reverse: "DROP TABLE `posts`"}},
+				Changes: []*migrate.Change{
+					{
+						Cmd:     "CREATE TABLE `posts` (`id` bigint NOT NULL AUTO_INCREMENT, `text` text NULL, `uuid` char(36) CHARSET utf8mb4 NULL COLLATE utf8mb4_bin, PRIMARY KEY (`id`), FULLTEXT INDEX `text` (`text`) COMMENT \"text index\" WITH PARSER `ngram`)",
+						Reverse: "DROP TABLE `posts`",
+					},
+				},
 			},
 		},
 		{
diff --git a/sql/mysql/sqlspec_test.go b/sql/mysql/sqlspec_test.go
index 7ae3011c..ae5f3ecd 100644
--- a/sql/mysql/sqlspec_test.go
+++ b/sql/mysql/sqlspec_test.go
@@ -747,6 +747,100 @@ table "users" {
 	require.EqualValues(t, exp, &s)
 }
 
+func TestMarshalSpec_IndexParser(t *testing.T) {
+	s := schema.New("test").
+		AddTables(
+			schema.NewTable("users").
+				AddColumns(
+					schema.NewStringColumn("id", "text"),
+				).
+				AddIndexes(
+					schema.NewIndex("idx1").
+						AddColumns(
+							schema.NewStringColumn("id", "text"),
+						).
+						AddAttrs(&IndexType{T: IndexTypeFullText}, &IndexParser{P: IndexParserNGram}),
+					schema.NewIndex("idx2").
+						AddColumns(
+							schema.NewStringColumn("id", "text"),
+						).
+						AddAttrs(&IndexType{T: IndexTypeFullText}, &IndexParser{P: "custom"}),
+				),
+		)
+	s.Tables[0].SetPrimaryKey(
+		schema.NewPrimaryKey(s.Tables[0].Columns...).
+			AddAttrs(&IndexType{T: IndexTypeHash}),
+	)
+	buf, err := MarshalHCL(s)
+	require.NoError(t, err)
+	require.Equal(t, `table "users" {
+  schema = schema.test
+  column "id" {
+    null = false
+    type = text
+  }
+  primary_key {
+    columns = [column.id]
+    type    = HASH
+  }
+  index "idx1" {
+    columns = [column.id]
+    type    = FULLTEXT
+    parser  = ngram
+  }
+  index "idx2" {
+    columns = [column.id]
+    type    = FULLTEXT
+    parser  = "custom"
+  }
+}
+schema "test" {
+}
+`, string(buf))
+}
+
+func TestUnmarshalSpec_IndexParser(t *testing.T) {
+	var s schema.Schema
+	err := EvalHCLBytes([]byte(`table "users" {
+  schema = schema.test
+  column "id" {
+    null = false
+    type = text
+  }
+  index "idx1" {
+    columns = [column.id]
+    type    = FULLTEXT
+    parser  = ngram
+  }
+  index "idx2" {
+    columns = [column.id]
+    type    = FULLTEXT
+    parser  = "custom"
+  }
+}
+schema "test" {
+}`), &s, nil)
+	require.NoError(t, err)
+	c := schema.NewStringColumn("id", "text")
+	exp := schema.New("test").
+		AddTables(
+			schema.NewTable("users").
+				AddColumns(c).
+				AddIndexes(
+					schema.NewIndex("idx1").
+						AddColumns(c).
+						AddAttrs(&IndexType{T: IndexTypeFullText}, &IndexParser{P: IndexParserNGram}),
+					schema.NewIndex("idx2").
+						AddColumns(c).
+						AddAttrs(&IndexType{T: IndexTypeFullText}, &IndexParser{P: "custom"}),
+
+				),
+		)
+	c.Indexes = nil
+	schema.NewRealm(exp)
+	require.EqualValues(t, exp, &s)
+}
+
 func TestMarshalSpec_PrimaryKeyType(t *testing.T) {
 	s := schema.New("test").
 		AddTables(