Repository: ariga/atlas
Pull request #482 (2022-01-24 20:31:51)

sql/mysql: add support for bool/boolean types
See first 2 rows in https://dev.mysql.com/doc/refman/8.0/en/other-vendor-data-types.html.
The rest are not common.


Patch:
diff --git a/internal/integration/testdata/mysql/add-foreign-key.txt b/internal/integration/testdata/mysql/foreign-key-add.txt
similarity index 100%
rename from internal/integration/testdata/mysql/add-foreign-key.txt
rename to internal/integration/testdata/mysql/foreign-key-add.txt
diff --git a/internal/integration/testdata/mysql/modify-foreign-key-action.txt b/internal/integration/testdata/mysql/foreign-key-modify-action.txt
similarity index 100%
rename from internal/integration/testdata/mysql/modify-foreign-key-action.txt
rename to internal/integration/testdata/mysql/foreign-key-modify-action.txt
diff --git a/internal/integration/testdata/mysql/add-drop-index.txt b/internal/integration/testdata/mysql/index-add-drop.txt
similarity index 100%
rename from internal/integration/testdata/mysql/add-drop-index.txt
rename to internal/integration/testdata/mysql/index-add-drop.txt
diff --git a/sql/mysql/convert.go b/sql/mysql/convert.go
index 78cf5486..f59d26a7 100644
--- a/sql/mysql/convert.go
+++ b/sql/mysql/convert.go
@@ -106,6 +106,12 @@ func ParseType(raw string) (schema.Type, error) {
 		return &BitType{
 			T: t,
 		}, nil
+	// bool and booleans are synonyms for
+	// tinyint with display-width set to 1.
+	case TypeBool, TypeBoolean:
+		return &schema.BoolType{
+			T: TypeBool,
+		}, nil
 	case TypeTinyInt, TypeSmallInt, TypeMediumInt, TypeInt, TypeBigInt:
 		if size == 1 {
 			return &schema.BoolType{
diff --git a/sql/mysql/driver.go b/sql/mysql/driver.go
index 9627f426..ce7ed624 100644
--- a/sql/mysql/driver.go
+++ b/sql/mysql/driver.go
@@ -134,7 +134,11 @@ func unescape(s string) string {
 // https://github.com/mysql/mysql-server/blob/8.0/sql/dd/types/column.h
 // https://github.com/mysql/mysql-server/blob/8.0/sql/sql_show.cc
 // https://github.com/mysql/mysql-server/blob/8.0/sql/gis/geometries.cc
+// https://dev.mysql.com/doc/refman/8.0/en/other-vendor-data-types.html
 const (
+	TypeBool    = "bool"
+	TypeBoolean = "boolean"
+
 	TypeBit       = "bit"       // MYSQL_TYPE_BIT
 	TypeInt       = "int"       // MYSQL_TYPE_LONG
 	TypeTinyInt   = "tinyint"   // MYSQL_TYPE_TINY
diff --git a/sql/mysql/sqlspec.go b/sql/mysql/sqlspec.go
index 08fd66c0..9b55be71 100644
--- a/sql/mysql/sqlspec.go
+++ b/sql/mysql/sqlspec.go
@@ -261,6 +261,8 @@ var TypeRegistry = specutil.NewRegistry(
 			},
 			RType: reflect.TypeOf(SetType{}),
 		},
+		specutil.TypeSpec(TypeBool),
+		specutil.TypeSpec(TypeBoolean),
 		specutil.TypeSpec(TypeBit, specutil.SizeTypeAttr(false)),
 		specutil.TypeSpec(TypeInt, unsignedTypeAttr(), specutil.SizeTypeAttr(false)),
 		specutil.TypeSpec(TypeTinyInt, unsignedTypeAttr(), specutil.SizeTypeAttr(false)),
diff --git a/sql/sqlite/inspect.go b/sql/sqlite/inspect.go
index 80dc9c4d..9207e032 100644
--- a/sql/sqlite/inspect.go
+++ b/sql/sqlite/inspect.go
@@ -189,7 +189,7 @@ func (i *inspect) indexes(ctx context.Context, t *schema.Table) error {
 		return fmt.Errorf("sqlite: scan %q indexes: %w", t.Name, err)
 	}
 	for _, idx := range t.Indexes {
-		if err := i.indexColumns(ctx, t, idx); err != nil {
+		if err := i.indexInfo(ctx, t, idx); err != nil {
 			return err
 		}
 	}
@@ -233,33 +233,36 @@ func (i *inspect) addIndexes(t *schema.Table, rows *sql.Rows) error {
 	return nil
 }
 
-func (i *inspect) indexColumns(ctx context.Context, t *schema.Table, idx *schema.Index) error {
+func (i *inspect) indexInfo(ctx context.Context, t *schema.Table, idx *schema.Index) error {
 	rows, err := i.QueryContext(ctx, fmt.Sprintf(indexColumnsQuery, idx.Name))
 	if err != nil {
 		return fmt.Errorf("sqlite: querying %q indexes: %w", t.Name, err)
 	}
 	defer rows.Close()
 	for rows.Next() {
-		var name sql.NullString
-		if err := rows.Scan(&name); err != nil {
+		var (
+			desc sql.NullBool
+			name sql.NullString
+		)
+		if err := rows.Scan(&name, &desc); err != nil {
 			return fmt.Errorf("sqlite: scanning index names: %w", err)
 		}
+		part := &schema.IndexPart{SeqNo: len(idx.Parts) + 1}
 		switch c, ok := t.Column(name.String); {
 		case ok:
-			idx.Parts = append(idx.Parts, &schema.IndexPart{
-				SeqNo: len(idx.Parts) + 1,
-				C:     c,
-			})
+			part.C = c
 		// NULL name indicates that the index-part is an expression and we
 		// should extract it from the `CREATE INDEX` statement (not supported atm).
 		case !sqlx.ValidString(name):
-			idx.Parts = append(idx.Parts, &schema.IndexPart{
-				SeqNo: len(idx.Parts) + 1,
-				X:     &schema.RawExpr{X: "<unsupported>"},
-			})
+			part.X = &schema.RawExpr{X: "<unsupported>"}
 		default:
 			return fmt.Errorf("sqlite: column %q was not found for index %q", name.String, idx.Name)
 		}
+		if desc.Bool {
+			// The default sort order is "ASC".
+			part.Attrs = append(part.Attrs, &IndexOrder{Desc: desc.Bool})
+		}
+		idx.Parts = append(idx.Parts, part)
 	}
 	return nil
 }
@@ -441,6 +444,13 @@ type (
 		P string
 	}
 
+	// IndexOrder describes the sort order of an index.
+	// See: https://www.sqlite.org/lang_createindex.html#descending_indexes
+	IndexOrder struct {
+		schema.Attr
+		Desc bool
+	}
+
 	// IndexOrigin describes how the index was created.
 	// See: https://www.sqlite.org/pragma.html#pragma_index_list
 	IndexOrigin struct {
@@ -663,7 +673,7 @@ const (
 	// Query to list table indexes.
 	indexesQuery = "SELECT `il`.`name`, `il`.`unique`, `il`.`origin`, `il`.`partial`, `m`.`sql` FROM pragma_index_list('%s') AS il JOIN sqlite_master AS m ON il.name = m.name"
 	// Query to list index columns.
-	indexColumnsQuery = "SELECT name FROM pragma_index_info('%s') ORDER BY seqno"
+	indexColumnsQuery = "SELECT name, desc FROM pragma_index_xinfo('%s') WHERE key = 1 ORDER BY seqno"
 	// Query to list table foreign-keys.
 	fksQuery = "SELECT `id`, `from`, `to`, `table`, `on_update`, `on_delete` FROM pragma_foreign_key_list('%s') ORDER BY id, seq"
 )


Test patch:
diff --git a/internal/integration/integration_test.go b/internal/integration/integration_test.go
index 9cc86d30..bd870160 100644
--- a/internal/integration/integration_test.go
+++ b/internal/integration/integration_test.go
@@ -4,25 +4,18 @@ import (
 	"bytes"
 	"context"
 	"database/sql"
-	"fmt"
 	"io"
 	"io/ioutil"
 	"os"
 	"os/exec"
-	"path/filepath"
-	"regexp"
-	"strings"
 	"testing"
 	"time"
 
 	"ariga.io/atlas/schema/schemaspec"
-	"ariga.io/atlas/sql/mysql"
 	"ariga.io/atlas/sql/schema"
 
 	entsql "entgo.io/ent/dialect/sql"
 	"entgo.io/ent/entc/integration/ent"
-	"github.com/pkg/diff"
-	"github.com/rogpeppe/go-internal/testscript"
 	"github.com/stretchr/testify/require"
 )
 
@@ -322,118 +315,3 @@ func sanity(c *ent.Client) {
 		SetInfo(c.GroupInfo.Create().SetDesc("desc").SaveX(ctx)).
 		SaveX(ctx)
 }
-
-func TestMySQLScript(t *testing.T) {
-	myRun(t, func(t *myTest) {
-		var (
-			attrs            = t.defaultAttrs()
-			charset, collate = attrs[0].(*schema.Charset).V, attrs[1].(*schema.Collation).V
-		)
-		testscript.Run(t.T, testscript.Params{
-			Dir: "testdata/mysql",
-			Setup: func(env *testscript.Env) error {
-				ctx := context.Background()
-				conn, err := t.db.Conn(ctx)
-				if err != nil {
-					return err
-				}
-				name := strings.ReplaceAll(filepath.Base(env.WorkDir), "-", "_")
-				env.Setenv("db", name)
-				if _, err := conn.ExecContext(ctx, fmt.Sprintf("CREATE DATABASE IF NOT EXISTS `%s`", name)); err != nil {
-					return err
-				}
-				if _, err := conn.ExecContext(ctx, fmt.Sprintf("USE `%s`", name)); err != nil {
-					return err
-				}
-				env.Defer(func() {
-					if _, err := conn.ExecContext(ctx, fmt.Sprintf("DROP DATABASE IF EXISTS `%s`", name)); err != nil {
-						t.Fatal(err)
-					}
-					if _, err := conn.ExecContext(ctx, "USE test"); err != nil {
-						t.Fatal(err)
-					}
-					if err := conn.Close(); err != nil {
-						t.Fatal(err)
-					}
-				})
-				return nil
-			},
-			Cmds: map[string]func(ts *testscript.TestScript, neg bool, args []string){
-				"apply": func(ts *testscript.TestScript, neg bool, args []string) {
-					var (
-						desired schema.Schema
-						f       = ts.ReadFile(args[0])
-						r       = strings.NewReplacer("$charset", charset, "$collate", collate, "$db", ts.Getenv("db"))
-					)
-					ts.Check(mysql.UnmarshalHCL([]byte(r.Replace(f)), &desired))
-					current, err := t.drv.InspectSchema(context.Background(), desired.Name, nil)
-					ts.Check(err)
-					changes, err := t.drv.SchemaDiff(current, &desired)
-					ts.Check(err)
-					switch err := t.drv.ApplyChanges(context.Background(), changes); {
-					case err != nil && !neg:
-						ts.Fatalf("apply changes: %v", err)
-					case err == nil && neg:
-						ts.Fatalf("unexpected apply success")
-					// If we expect to fail, and there's a specific error to compare.
-					case err != nil && len(args) == 2:
-						re, rerr := regexp.Compile(`(?m)` + args[1])
-						ts.Check(rerr)
-						if !re.MatchString(err.Error()) {
-							t.Fatalf("mismatched errors: %v != %s", err, args[1])
-						}
-					// Apply passed. Make sure there is no drift.
-					case !neg:
-						current, err := t.drv.InspectSchema(context.Background(), desired.Name, nil)
-						ts.Check(err)
-						changes, err := t.drv.SchemaDiff(current, &desired)
-						ts.Check(err)
-						if len(changes) > 0 {
-							ts.Fatalf("unexpected schema drift: %v", changes)
-						}
-					}
-				},
-				"exist": func(ts *testscript.TestScript, neg bool, args []string) {
-					for _, name := range args {
-						var b bool
-						if err := t.db.QueryRow("SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?", ts.Getenv("db"), name).Scan(&b); err != nil {
-							ts.Fatalf("failed query table existence %q: %v", name, err)
-						}
-						if !b != neg {
-							ts.Fatalf("table %q existence failed", name)
-						}
-					}
-				},
-				"cmpshow": func(ts *testscript.TestScript, neg bool, args []string) {
-					if len(args) < 2 {
-						ts.Fatalf("invalid number of args to 'cmpshow': %d", len(args))
-					}
-					var (
-						fname = args[len(args)-1]
-						stmts = make([]string, 0, len(args)-1)
-					)
-					for _, name := range args[:len(args)-1] {
-						var create string
-						if err := t.db.QueryRow(fmt.Sprintf("SHOW CREATE TABLE `%s`.`%s`", ts.Getenv("db"), name)).Scan(&name, &create); err != nil {
-							ts.Fatalf("show table %q: %v", name, err)
-						}
-						// Trim the "table_options" if it was not requested explicitly.
-						stmts = append(stmts, create[:strings.LastIndexByte(create, ')')+1])
-					}
-
-					// Check if there is a file prefixed by database version (1.sql and <version>/1.sql).
-					if _, err := os.Stat(ts.MkAbs(filepath.Join(t.version, fname))); err == nil {
-						fname = filepath.Join(t.version, fname)
-					}
-					t1, t2 := strings.Join(stmts, "\n"), ts.ReadFile(fname)
-					if strings.TrimSpace(t1) == strings.TrimSpace(t2) {
-						return
-					}
-					var sb strings.Builder
-					ts.Check(diff.Text("show", fname, t1, t2, &sb))
-					ts.Fatalf(sb.String())
-				},
-			},
-		})
-	})
-}
diff --git a/internal/integration/mysql_test.go b/internal/integration/mysql_test.go
index c65c9117..da2254c5 100644
--- a/internal/integration/mysql_test.go
+++ b/internal/integration/mysql_test.go
@@ -8,14 +8,20 @@ import (
 	"context"
 	"database/sql"
 	"fmt"
+	"os"
+	"path/filepath"
+	"regexp"
 	"strings"
 	"sync"
 	"testing"
 
 	"ariga.io/atlas/sql/mysql"
 	"ariga.io/atlas/sql/schema"
+
 	"entgo.io/ent/dialect"
 	_ "github.com/go-sql-driver/mysql"
+	"github.com/pkg/diff"
+	"github.com/rogpeppe/go-internal/testscript"
 	"github.com/stretchr/testify/require"
 )
 
@@ -1209,3 +1215,137 @@ func rmCreateStmt(t *schema.Table) {
 		}
 	}
 }
+
+func TestMySQL_Script(t *testing.T) {
+	myRun(t, func(t *myTest) {
+		var (
+			attrs            = t.defaultAttrs()
+			charset, collate = attrs[0].(*schema.Charset).V, attrs[1].(*schema.Collation).V
+		)
+		testscript.Run(t.T, testscript.Params{
+			Dir: "testdata/mysql",
+			Setup: func(env *testscript.Env) error {
+				ctx := context.Background()
+				conn, err := t.db.Conn(ctx)
+				if err != nil {
+					return err
+				}
+				name := strings.ReplaceAll(filepath.Base(env.WorkDir), "-", "_")
+				env.Setenv("db", name)
+				env.Setenv("charset", charset)
+				env.Setenv("collate", collate)
+				if _, err := conn.ExecContext(ctx, fmt.Sprintf("CREATE DATABASE IF NOT EXISTS `%s`", name)); err != nil {
+					return err
+				}
+				if _, err := conn.ExecContext(ctx, fmt.Sprintf("USE `%s`", name)); err != nil {
+					return err
+				}
+				env.Defer(func() {
+					if _, err := conn.ExecContext(ctx, fmt.Sprintf("DROP DATABASE IF EXISTS `%s`", name)); err != nil {
+						t.Fatal(err)
+					}
+					if _, err := conn.ExecContext(ctx, "USE test"); err != nil {
+						t.Fatal(err)
+					}
+					if err := conn.Close(); err != nil {
+						t.Fatal(err)
+					}
+				})
+				return nil
+			},
+			Cmds: map[string]func(ts *testscript.TestScript, neg bool, args []string){
+				"apply":   t.cmdApply,
+				"exist":   t.cmdExist,
+				"synced":  t.cmdSynced,
+				"cmpshow": t.cmdCmpShow,
+			},
+		})
+	})
+}
+
+func (t *myTest) cmdCmpShow(ts *testscript.TestScript, neg bool, args []string) {
+	if len(args) < 2 {
+		ts.Fatalf("invalid number of args to 'cmpshow': %d", len(args))
+	}
+	var (
+		fname = args[len(args)-1]
+		stmts = make([]string, 0, len(args)-1)
+	)
+	for _, name := range args[:len(args)-1] {
+		var create string
+		if err := t.db.QueryRow(fmt.Sprintf("SHOW CREATE TABLE `%s`.`%s`", ts.Getenv("db"), name)).Scan(&name, &create); err != nil {
+			ts.Fatalf("show table %q: %v", name, err)
+		}
+		// Trim the "table_options" if it was not requested explicitly.
+		stmts = append(stmts, create[:strings.LastIndexByte(create, ')')+1])
+	}
+
+	// Check if there is a file prefixed by database version (1.sql and <version>/1.sql).
+	if _, err := os.Stat(ts.MkAbs(filepath.Join(t.version, fname))); err == nil {
+		fname = filepath.Join(t.version, fname)
+	}
+	t1, t2 := strings.Join(stmts, "\n"), ts.ReadFile(fname)
+	if strings.TrimSpace(t1) == strings.TrimSpace(t2) {
+		return
+	}
+	var sb strings.Builder
+	ts.Check(diff.Text("show", fname, t1, t2, &sb))
+	ts.Fatalf(sb.String())
+}
+
+func (t *myTest) cmdExist(ts *testscript.TestScript, neg bool, args []string) {
+	for _, name := range args {
+		var b bool
+		if err := t.db.QueryRow("SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?", ts.Getenv("db"), name).Scan(&b); err != nil {
+			ts.Fatalf("failed query table existence %q: %v", name, err)
+		}
+		if !b != neg {
+			ts.Fatalf("table %q existence failed", name)
+		}
+	}
+}
+
+func (t *myTest) cmdSynced(ts *testscript.TestScript, neg bool, args []string) {
+	switch changes := t.hclDiff(ts, args[0]); {
+	case len(changes) > 0 && !neg:
+		ts.Fatalf("expect no schema changes, but got: %d", len(changes))
+	case len(changes) == 0 && neg:
+		ts.Fatalf("expect schema changes, but there are none")
+	}
+}
+
+func (t *myTest) cmdApply(ts *testscript.TestScript, neg bool, args []string) {
+	changes := t.hclDiff(ts, args[0])
+	switch err := t.drv.ApplyChanges(context.Background(), changes); {
+	case err != nil && !neg:
+		ts.Fatalf("apply changes: %v", err)
+	case err == nil && neg:
+		ts.Fatalf("unexpected apply success")
+	// If we expect to fail, and there's a specific error to compare.
+	case err != nil && len(args) == 2:
+		re, rerr := regexp.Compile(`(?m)` + args[1])
+		ts.Check(rerr)
+		if !re.MatchString(err.Error()) {
+			t.Fatalf("mismatched errors: %v != %s", err, args[1])
+		}
+	// Apply passed. Make sure there is no drift.
+	case !neg:
+		if changes := t.hclDiff(ts, args[0]); len(changes) > 0 {
+			ts.Fatalf("unexpected schema changes: %d", len(changes))
+		}
+	}
+}
+
+func (t *myTest) hclDiff(ts *testscript.TestScript, name string) []schema.Change {
+	var (
+		desired schema.Schema
+		f       = ts.ReadFile(name)
+		r       = strings.NewReplacer("$charset", ts.Getenv("charset"), "$collate", ts.Getenv("collate"), "$db", ts.Getenv("db"))
+	)
+	ts.Check(mysql.UnmarshalHCL([]byte(r.Replace(f)), &desired))
+	current, err := t.drv.InspectSchema(context.Background(), desired.Name, nil)
+	ts.Check(err)
+	changes, err := t.drv.SchemaDiff(current, &desired)
+	ts.Check(err)
+	return changes
+}
diff --git a/internal/integration/testdata/mysql/add-drop-column.txt b/internal/integration/testdata/mysql/column-add-drop.txt
similarity index 100%
rename from internal/integration/testdata/mysql/add-drop-column.txt
rename to internal/integration/testdata/mysql/column-add-drop.txt
diff --git a/internal/integration/testdata/mysql/column-bool.txt b/internal/integration/testdata/mysql/column-bool.txt
new file mode 100644
index 00000000..fbb9e6d5
--- /dev/null
+++ b/internal/integration/testdata/mysql/column-bool.txt
@@ -0,0 +1,90 @@
+# Each test runs on a clean database.
+
+apply 1.hcl
+cmpshow users 1.sql
+
+# "bool", "boolean" and "tinyint(1)" are equal.
+synced 2.hcl
+
+# Changing "tinyint(1)" to "tinyint" should cause a schema change.
+apply 3.hcl
+cmpshow users 3.sql
+synced 3.hcl
+
+-- 1.hcl --
+schema "$db" {
+    charset = "$charset"
+    collation = "$collate"
+}
+
+table "users" {
+    schema = schema.$db
+    column "a" {
+        type = bool
+    }
+    column "b" {
+        type = boolean
+    }
+    column "c" {
+        type = tinyint(1)
+    }
+}
+
+-- 1.sql --
+CREATE TABLE `users` (
+  `a` tinyint(1) NOT NULL,
+  `b` tinyint(1) NOT NULL,
+  `c` tinyint(1) NOT NULL
+)
+
+-- 2.hcl --
+schema "$db" {
+    charset = "$charset"
+    collation = "$collate"
+}
+
+table "users" {
+    schema = schema.$db
+    column "a" {
+        type = boolean
+    }
+    column "b" {
+        type = tinyint(1)
+    }
+    column "c" {
+        type = bool
+    }
+}
+
+-- 3.hcl --
+schema "$db" {
+    charset = "$charset"
+    collation = "$collate"
+}
+
+table "users" {
+    schema = schema.$db
+    column "a" {
+        type = boolean
+    }
+    column "b" {
+        type = tinyint
+    }
+    column "c" {
+        type = bool
+    }
+}
+
+-- 3.sql --
+CREATE TABLE `users` (
+  `a` tinyint(1) NOT NULL,
+  `b` tinyint(4) NOT NULL,
+  `c` tinyint(1) NOT NULL
+)
+
+-- 8/3.sql --
+CREATE TABLE `users` (
+  `a` tinyint(1) NOT NULL,
+  `b` tinyint NOT NULL,
+  `c` tinyint(1) NOT NULL
+)
diff --git a/sql/mysql/sqlspec_test.go b/sql/mysql/sqlspec_test.go
index b5e16f4c..45c39566 100644
--- a/sql/mysql/sqlspec_test.go
+++ b/sql/mysql/sqlspec_test.go
@@ -772,6 +772,14 @@ func TestTypes(t *testing.T) {
 			typeExpr: "geometrycollection",
 			expected: &schema.SpatialType{T: TypeGeometryCollection},
 		},
+		{
+			typeExpr: "bool",
+			expected: &schema.BoolType{T: TypeBool},
+		},
+		{
+			typeExpr: "boolean",
+			expected: &schema.BoolType{T: TypeBool},
+		},
 	}
 	for _, tt := range tests {
 		t.Run(tt.typeExpr, func(t *testing.T) {
diff --git a/sql/sqlite/inspect_test.go b/sql/sqlite/inspect_test.go
index fadd14e4..a516cf73 100644
--- a/sql/sqlite/inspect_test.go
+++ b/sql/sqlite/inspect_test.go
@@ -105,17 +105,17 @@ func TestDriver_InspectTable(t *testing.T) {
 `))
 				m.ExpectQuery(sqltest.Escape(fmt.Sprintf(indexColumnsQuery, "c1u"))).
 					WillReturnRows(sqltest.Rows(`
- name 
-------
- c1   
- c2
+ name  |   desc |
+-------+--------+
+ c1   |  1      |
+ c2   |  0      |
 `))
 				m.ExpectQuery(sqltest.Escape(fmt.Sprintf(indexColumnsQuery, "c1_c2"))).
 					WillReturnRows(sqltest.Rows(`
- name 
-------
- c1   
- nil
+ name  |   desc |     
+-------+--------+     
+ c1    |  0     |     
+ nil   |  0     |     
 `))
 				m.noFKs("users")
 			},
@@ -131,7 +131,7 @@ func TestDriver_InspectTable(t *testing.T) {
 						Unique: true,
 						Table:  t,
 						Parts: []*schema.IndexPart{
-							{SeqNo: 1, C: columns[0]},
+							{SeqNo: 1, C: columns[0], Attrs: []schema.Attr{&IndexOrder{Desc: true}}},
 							{SeqNo: 2, C: columns[1]},
 						},
 						Attrs: []schema.Attr{