Repository: ariga/atlas
Pull request #897 (2022-06-29 15:40:37)

doc/md: documenting sqlcheck analyzers
Currently not connecting it to the navigation in the website.


Patch:
diff --git a/README.md b/README.md
index 08da9770..c39cb4dc 100644
--- a/README.md
+++ b/README.md
@@ -37,7 +37,7 @@ atlas schema inspect -u "mysql://root:pass@localhost:3306/example" > atlas.hcl
 ```shell
 atlas schema apply -u "mysql://root:pass@localhost:3306/example" -f atlas.hcl
 ```
-- **Declarative Migrations vs. Versioned Migrations**: Atlas offers two workflows. Declarative migrations allow the user to provide a desired state and Atlas gets the schema there instantly (simply using inspect and apply commands). Alternatively, declarative migrations are explicitly defined and assigned a version. Atlas can then bring a schema to the desired version by following the migrations between the current version and the specified one.
+- **Declarative Migrations vs. Versioned Migrations**: Atlas offers two workflows. Declarative migrations allow the user to provide a desired state and Atlas gets the schema there instantly (simply using inspect and apply commands). Alternatively, versioned migrations are explicitly defined and assigned a version. Atlas can then bring a schema to the desired version by following the migrations between the current version and the specified one.
 
 ### About the Project
 Read more about the motivation of the project [here](https://atlasgo.io/blog/2021/11/25/meet-atlas).
diff --git a/cmd/atlascmd/migrate/migrate.go b/cmd/atlascmd/migrate/migrate.go
index a0e408f9..28f58cb9 100644
--- a/cmd/atlascmd/migrate/migrate.go
+++ b/cmd/atlascmd/migrate/migrate.go
@@ -66,11 +66,11 @@ func (r *EntRevisions) Init(ctx context.Context) error {
 		return r.ec.Schema.Create(ctx, entschema.WithAtlas(true))
 	}
 	// Driver does support changing schemas. Make sure the schema does exist before proceeding.
-	_, err2 := r.ac.InspectSchema(ctx, r.schema, &schema.InspectOptions{Mode: schema.InspectSchemas})
-	if err2 != nil && !schema.IsNotExistError(err2) {
-		return err2
+	_, err = r.ac.InspectSchema(ctx, r.schema, &schema.InspectOptions{Mode: schema.InspectSchemas})
+	if err != nil && !schema.IsNotExistError(err) {
+		return err
 	}
-	if schema.IsNotExistError(err2) {
+	if schema.IsNotExistError(err) {
 		if err := r.ac.ApplyChanges(ctx, []schema.Change{
 			&schema.AddSchema{S: &schema.Schema{Name: r.schema}},
 		}); err != nil {
diff --git a/doc/md/lint/analyzers.md b/doc/md/lint/analyzers.md
new file mode 100644
index 00000000..ca04a309
--- /dev/null
+++ b/doc/md/lint/analyzers.md
@@ -0,0 +1,65 @@
+---
+title: Migration Analyzers
+slug: /lint/analyzers
+---
+The database is often the most critical component in software architectures. Being a stateful component, it cannot be
+easily rebuilt, scaled-out or fixed by a restart. Outages that involve damage to data or simply unavailability of the database
+are notoriously hard to manage and recover from, often taking long hours of careful work by a team's most senior
+engineers.
+
+As most outages happen directly as a result of a change to a system, Atlas provides users with means to verify the
+safety of planned changes before they happen. The [`sqlcheck`](https://pkg.go.dev/ariga.io/atlas@master/sql/sqlcheck)
+package provides interfaces for analyzing the contents of SQL files to generate insights on the safety of many kinds of
+changes to database schemas. With this package developers may define an `Analyzer` that can be used to diagnose the impact
+of SQL statements on the target database.
+
+Using these interfaces, Atlas provides different `Analyzer` implementations that are useful for determining the
+safety of migration scripts.
+
+## Analyzers
+
+Below are the `Analyzer` implementations currently supported by Atlas. 
+
+### Destructive Changes 
+
+Destructive changes are changes to a database schema that result in loss of data. For instance,
+consider a statement such as:
+```sql
+ALTER TABLE `users` DROP COLUMN `email_address`;
+```
+This statement is considered destructive because whatever data is stored in the `email_address` column
+will be deleted from disk, with no way to recover it. There are definitely situations where this type
+of change is desired, but they are relatively rare. Using the `destructive` ([GoDoc](https://pkg.go.dev/ariga.io/atlas@master/sql/sqlcheck/destructive))
+Analyzer, teams can detect this type of change and design workflows that prevent it from happening accidentally. 
+
+### Data-dependant Changes 
+
+Data-dependant changes are changes to a database schema that _may_ succeed or fail, depending on the
+data that is stored in the database. For instance, consider a statement such as:
+
+```sql
+ALTER TABLE `example`.`orders` ADD UNIQUE INDEX `idx_name` (`name`);
+```
+This statement is considered data-dependant because if the `orders` table 
+contains duplicate values on the name column we will not be able to add a uniqueness
+constraint. Consider we added two records with the name `atlas` to the table:
+```
+mysql> create table orders ( name varchar(100) );
+Query OK, 0 rows affected (0.11 sec)
+
+mysql> insert into orders (name) values ("atlas");
+Query OK, 1 row affected (0.06 sec)
+
+mysql> insert into orders (name) values ("atlas");
+Query OK, 1 row affected (0.01 sec)
+```
+Attempting to add a uniqueness constraint on the `name` column, will fail:
+```
+mysql> ALTER TABLE `example`.`orders` ADD UNIQUE INDEX `idx_name` (`name`);
+ERROR 1062 (23000): Duplicate entry 'atlas' for key 'orders.idx_name'
+```
+This type of change is tricky because a developer trying to simulate it locally
+might succeed in performing it only to be surprised that their migration script
+fails in production, breaking a deployment sequence or causing other unexpected
+behavior. Using the `datadepend` ([GoDoc](https://pkg.go.dev/ariga.io/atlas@master/sql/sqlcheck/datadepend)) 
+Analyzer, teams can detect this risk early and account for it in pre-deployment checks to a database. 
\ No newline at end of file
diff --git a/sql/migrate/migrate.go b/sql/migrate/migrate.go
index b586d226..5f37db42 100644
--- a/sql/migrate/migrate.go
+++ b/sql/migrate/migrate.go
@@ -183,7 +183,7 @@ type (
 		WriteRevision(context.Context, *Revision) error
 	}
 
-	// Planner can plan the steps to take to migrate from one state to another. It uses the enclosed FS to write
+	// Planner can plan the steps to take to migrate from one state to another. It uses the enclosed Dir to write
 	// those changes to versioned migration files.
 	Planner struct {
 		drv Driver    // driver to use
@@ -265,11 +265,18 @@ func DisableChecksum() PlannerOption {
 // Plan calculates the migration Plan required for moving the current state (from) state to
 // the next state (to). A StateReader can be a directory, static schema elements or a Driver connection.
 func (p *Planner) Plan(ctx context.Context, name string, to StateReader) (*Plan, error) {
-	ex, err := NewExecutor(p.drv, p.dir, NopRevisionReadWriter{})
-	if err != nil {
-		return nil, err
+	var from StateReader
+	if sr, ok := p.dir.(StateReader); ok {
+		from = sr
+	}
+	if from == nil {
+		ex, err := NewExecutor(p.drv, p.dir, NopRevisionReadWriter{})
+		if err != nil {
+			return nil, err
+		}
+		from = ex
 	}
-	current, err := ex.ReadState(ctx)
+	current, err := from.ReadState(ctx)
 	if err != nil {
 		return nil, err
 	}
diff --git a/sql/mysql/driver.go b/sql/mysql/driver.go
index b2298aa2..5b3bee49 100644
--- a/sql/mysql/driver.go
+++ b/sql/mysql/driver.go
@@ -211,6 +211,14 @@ func (c *conn) collateToCharset() (map[string]string, error) {
 	return decode(name)
 }
 
+// supportsIndexComment reports if the connected database
+// supports comments on indexes.
+func (c *conn) supportsIndexComment() bool {
+	// According to Oracle release notes, comments on
+	// indexes were added in version 5.5.3.
+	return c.mariadb() || c.gteV("5.5.3")
+}
+
 // mariadb reports if the Driver is connected to a MariaDB database.
 func (c *conn) mariadb() bool {
 	return strings.Index(c.version, "MariaDB") > 0
diff --git a/sql/mysql/inspect.go b/sql/mysql/inspect.go
index bb9b9b05..f9213403 100644
--- a/sql/mysql/inspect.go
+++ b/sql/mysql/inspect.go
@@ -293,10 +293,7 @@ func (i *inspect) addColumn(s *schema.Schema, rows *sql.Rows) error {
 
 // indexes queries and appends the indexes of the given table.
 func (i *inspect) indexes(ctx context.Context, s *schema.Schema) error {
-	query := indexesQuery
-	if i.supportsIndexExpr() {
-		query = indexesExprQuery
-	}
+	query := i.indexQuery()
 	rows, err := i.querySchema(ctx, query, s)
 	if err != nil {
 		return fmt.Errorf("mysql: query schema %q indexes: %w", s.Name, err)
@@ -442,6 +439,18 @@ func (i *inspect) checks(ctx context.Context, s *schema.Schema) error {
 	return rows.Err()
 }
 
+// indexQuery returns the query to retrieve the indexes of the given table.
+func (i *inspect) indexQuery() string {
+	query := indexesNoCommentQuery
+	if i.supportsIndexComment() {
+		query = indexesQuery
+	}
+	if i.supportsIndexExpr() {
+		query = indexesExprQuery
+	}
+	return query
+}
+
 // extraAttr is a parsed version of the information_schema EXTRA column.
 type extraAttr struct {
 	autoinc          bool
@@ -658,8 +667,9 @@ const (
 	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`"
-	indexesExprQuery = "SELECT `TABLE_NAME`, `INDEX_NAME`, `COLUMN_NAME`, `NON_UNIQUE`, `SEQ_IN_INDEX`, `INDEX_TYPE`, UPPER(`COLLATION`) = 'D' AS `DESC`, `INDEX_COMMENT`, `SUB_PART`, `EXPRESSION` FROM `INFORMATION_SCHEMA`.`STATISTICS` WHERE `TABLE_SCHEMA` = ? AND `TABLE_NAME` IN (%s) ORDER BY `index_name`, `seq_in_index`"
+	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`"
+	indexesExprQuery      = "SELECT `TABLE_NAME`, `INDEX_NAME`, `COLUMN_NAME`, `NON_UNIQUE`, `SEQ_IN_INDEX`, `INDEX_TYPE`, UPPER(`COLLATION`) = 'D' AS `DESC`, `INDEX_COMMENT`, `SUB_PART`, `EXPRESSION` FROM `INFORMATION_SCHEMA`.`STATISTICS` WHERE `TABLE_SCHEMA` = ? AND `TABLE_NAME` IN (%s) ORDER BY `index_name`, `seq_in_index`"
+	indexesNoCommentQuery = "SELECT `TABLE_NAME`, `INDEX_NAME`, `COLUMN_NAME`, `NON_UNIQUE`, `SEQ_IN_INDEX`, `INDEX_TYPE`, UPPER(`COLLATION`) = 'D' AS `DESC`, NULL AS `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`"
 
 	tablesQuery = `
 SELECT
diff --git a/sql/sqlcheck/sqlcheck.go b/sql/sqlcheck/sqlcheck.go
index b73aba96..0c219f96 100644
--- a/sql/sqlcheck/sqlcheck.go
+++ b/sql/sqlcheck/sqlcheck.go
@@ -2,6 +2,12 @@
 // This source code is licensed under the Apache 2.0 license found
 // in the LICENSE file in the root directory of this source tree.
 
+// Package sqlcheck provides interfaces for analyzing the contents of SQL files
+// to generate insights on the safety of many kinds of changes to database
+// schemas. With this package developers may define an Analyzer that can be used
+// to diagnose the impact of SQL statements on the target database. For instance,
+// The `destructive` package exposes an Analyzer that detects destructive changes
+// to the database schema, such as the dropping of tables or columns.
 package sqlcheck
 
 import (
diff --git a/sql/sqlite/driver.go b/sql/sqlite/driver.go
index 2e2412e2..202b59a0 100644
--- a/sql/sqlite/driver.go
+++ b/sql/sqlite/driver.go
@@ -95,7 +95,7 @@ func (d *Driver) IsClean(ctx context.Context) (bool, error) {
 	return r == nil || len(r.Schemas) == 1 && r.Schemas[0].Name == mainFile && len(r.Schemas[0].Tables) == 0, nil
 }
 
-// Clean implements the inlined migrate.Clean interface to override the "emptying" behavior.
+// Clean implements the inlined Clean interface to override the "emptying" behavior.
 func (d *Driver) Clean(ctx context.Context) error {
 	for _, stmt := range []string{
 		"PRAGMA writable_schema = 1;",


Test patch:
diff --git a/sql/mysql/inspect_test.go b/sql/mysql/inspect_test.go
index 838bfee8..15d119d3 100644
--- a/sql/mysql/inspect_test.go
+++ b/sql/mysql/inspect_test.go
@@ -19,13 +19,15 @@ import (
 
 // Single table queries used by the different tests.
 var (
-	queryFKs         = sqltest.Escape(fmt.Sprintf(fksQuery, "?"))
-	queryTable       = sqltest.Escape(fmt.Sprintf(tablesQuery, "?"))
-	queryColumns     = sqltest.Escape(fmt.Sprintf(columnsExprQuery, "?"))
-	queryIndexes     = sqltest.Escape(fmt.Sprintf(indexesQuery, "?"))
-	queryIndexesExpr = sqltest.Escape(fmt.Sprintf(indexesExprQuery, "?"))
-	queryMyChecks    = sqltest.Escape(fmt.Sprintf(myChecksQuery, "?"))
-	queryMarChecks   = sqltest.Escape(fmt.Sprintf(marChecksQuery, "?"))
+	queryFKs              = sqltest.Escape(fmt.Sprintf(fksQuery, "?"))
+	queryTable            = sqltest.Escape(fmt.Sprintf(tablesQuery, "?"))
+	queryColumns          = sqltest.Escape(fmt.Sprintf(columnsExprQuery, "?"))
+	queryColumnsNoExpr    = sqltest.Escape(fmt.Sprintf(columnsQuery, "?"))
+	queryIndexes          = sqltest.Escape(fmt.Sprintf(indexesQuery, "?"))
+	queryIndexesNoComment = sqltest.Escape(fmt.Sprintf(indexesNoCommentQuery, "?"))
+	queryIndexesExpr      = sqltest.Escape(fmt.Sprintf(indexesExprQuery, "?"))
+	queryMyChecks         = sqltest.Escape(fmt.Sprintf(myChecksQuery, "?"))
+	queryMarChecks        = sqltest.Escape(fmt.Sprintf(marChecksQuery, "?"))
 )
 
 func TestDriver_InspectTable(t *testing.T) {
@@ -234,14 +236,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 | 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                      | 
++------------+-------------------+--------------------+----------------+-------------+------------+----------------+-------+--------------------+----------------+---------------------------+
+| 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()
@@ -421,18 +423,18 @@ 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          | 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                      |  
++------------+-------------+--------------------+----------------+-------------+------------+----------------+-------+--------------------+----------------+---------------------------+
+| 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()
@@ -561,6 +563,36 @@ func TestDriver_InspectTable(t *testing.T) {
 				require.EqualValues(indexes, t.Indexes)
 			},
 		},
+		{
+			name:    "indexes/not_support_comment",
+			version: "5.1.60",
+			before: func(m mock) {
+				m.tableExists("public", "users", true)
+				m.ExpectQuery(queryColumnsNoExpr).
+					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      | id          | int          |                | NO          | PRI        | NULL           |                | NULL               | NULL               | NULL                      |
++------------+-------------+--------------+----------------+-------------+------------+----------------+----------------+--------------------+--------------------+---------------------------+
+`))
+				m.ExpectQuery(queryIndexesNoComment).
+					WithArgs("public", "users").
+					WillReturnRows(sqltest.Rows(`
++--------------+--------------+-------------+------------+--------------+--------------+---------+--------------+------------+------------------+
+| TABLE_NAME   | INDEX_NAME   | COLUMN_NAME | NON_UNIQUE | SEQ_IN_INDEX | INDEX_TYPE   | DESC    | COMMENT      | SUB_PART   | EXPRESSION       |
++--------------+--------------+-------------+------------+--------------+--------------+---------+--------------+------------+------------------+
+| users        | PRIMARY      | id          |          0 |            1 | BTREE        | 0       | NULL         |       NULL |      NULL        |
++--------------+--------------+-------------+------------+--------------+--------------+---------+--------------+------------+------------------+
+`))
+				m.noFKs()
+			},
+			expect: func(require *require.Assertions, t *schema.Table, err error) {
+				// nothing to expect, ExpectQuery is enough for this test
+				require.NoError(err)
+			},
+		},
 		{
 			name: "fks",
 			before: func(m mock) {
@@ -568,13 +600,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     | 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                      | 
-+------------+-------------+--------------+----------------+-------------+------------+----------------+----------------+--------------------+--------------------+---------------------------+ 
++------------+-------------+--------------+----------------+-------------+------------+----------------+----------------+--------------------+--------------------+---------------------------+
+| 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).
@@ -617,12 +649,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     | GENERATION_EXPRESSION     | 
-+-------------+-------------+--------------+----------------+-------------+------------+----------------+----------------+--------------------+--------------------+---------------------------+ 
-| users       | id          | int          |                | NO          | PRI        | NULL           |                | NULL               | NULL               | NULL                      | 
-| users       | c1          | int          |                | NO          | MUL        | 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       | c1          | int          |                | NO          | MUL        | NULL           |                | NULL               | NULL               | NULL                      |
++-------------+-------------+--------------+----------------+-------------+------------+----------------+----------------+--------------------+--------------------+---------------------------+
 `))
 				m.noIndexes()
 				m.noFKs()