Repository: cloudspannerecosystem/harbourbridge
Pull request #122 (2021-04-06 01:35:58)

[refactor] Move spanner ddl functionality to spanner/ddl package.
- Resolves an existing TODO as well.
- Moves the test cases to spanner/ddl package
- Simplifies composite literal definition in some of the source files that were touched in this PR. For more context, please see https://golang.org/cmd/gofmt/#hdr-The_simplify_command.


Patch:
diff --git a/cmd/cmd.go b/cmd/cmd.go
new file mode 100644
index 0000000..e3e61df
--- /dev/null
+++ b/cmd/cmd.go
@@ -0,0 +1,91 @@
+// Copyright 2020 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package cmd implements command line utility for HarbourBridge.
+package cmd
+
+import (
+	"fmt"
+	"time"
+
+	"github.com/cloudspannerecosystem/harbourbridge/conversion"
+	"github.com/cloudspannerecosystem/harbourbridge/internal"
+)
+
+var (
+	badDataFile = "dropped.txt"
+	reportFile  = "report.txt"
+	schemaFile  = "schema.txt"
+	sessionFile = "session.json"
+)
+
+// CommandLine provides the core processing for HarbourBridge when run as a command-line tool.
+// It performs the following steps:
+// 1. Run schema conversion (if dataOnly is set to false)
+// 2. Create database (if schemaOnly is set to false)
+// 3. Run data conversion (if schemaOnly is set to false)
+// 4. Generate report
+func CommandLine(driver, projectID, instanceID, dbName string, dataOnly, schemaOnly bool, schemaSampleSize int64, sessionJSON string, ioHelper *conversion.IOStreams, outputFilePrefix string, now time.Time) error {
+	var conv *internal.Conv
+	var err error
+	if !dataOnly {
+		conv, err = conversion.SchemaConv(driver, ioHelper, schemaSampleSize)
+		if err != nil {
+			return err
+		}
+		if ioHelper.SeekableIn != nil {
+			defer ioHelper.In.Close()
+		}
+
+		conversion.WriteSchemaFile(conv, now, outputFilePrefix+schemaFile, ioHelper.Out)
+		conversion.WriteSessionFile(conv, outputFilePrefix+sessionFile, ioHelper.Out)
+		if schemaOnly {
+			conversion.Report(driver, nil, ioHelper.BytesRead, "", conv, outputFilePrefix+reportFile, ioHelper.Out)
+			return nil
+		}
+	} else {
+		conv = internal.MakeConv()
+		err = conversion.ReadSessionFile(conv, sessionJSON)
+		if err != nil {
+			return err
+		}
+	}
+
+	db, err := conversion.CreateDatabase(projectID, instanceID, dbName, conv, ioHelper.Out)
+	if err != nil {
+		fmt.Printf("\nCan't create database: %v\n", err)
+		return fmt.Errorf("can't create database")
+	}
+
+	client, err := conversion.GetClient(db)
+	if err != nil {
+		fmt.Printf("\nCan't create client for db %s: %v\n", db, err)
+		return fmt.Errorf("can't create Spanner client")
+	}
+
+	bw, err := conversion.DataConv(driver, ioHelper, client, conv, dataOnly)
+	if err != nil {
+		fmt.Printf("\nCan't finish data conversion for db %s: %v\n", db, err)
+		return fmt.Errorf("can't finish data conversion")
+	}
+
+	if err = conversion.UpdateDDLForeignKeys(projectID, instanceID, dbName, conv, ioHelper.Out); err != nil {
+		fmt.Printf("\nCan't perform update operation on db %s with foreign keys: %v\n", db, err)
+		return fmt.Errorf("can't perform update schema with foreign keys")
+	}
+	banner := conversion.GetBanner(now, db)
+	conversion.Report(driver, bw.DroppedRowsByTable(), ioHelper.BytesRead, banner, conv, outputFilePrefix+reportFile, ioHelper.Out)
+	conversion.WriteBadData(bw, conv, banner, outputFilePrefix+badDataFile, ioHelper.Out)
+	return nil
+}
diff --git a/conversion/conversion.go b/conversion/conversion.go
index c6ffe81..4bc62f5 100644
--- a/conversion/conversion.go
+++ b/conversion/conversion.go
@@ -87,16 +87,6 @@ func SchemaConv(driver string, ioHelper *IOStreams, schemaSampleSize int64) (*in
 	}
 }
 
-// Check if schema contains interleaved tables.
-func checkInterleaved(conv *internal.Conv) bool {
-	for _, table := range conv.SpSchema {
-		if table.Parent != "" {
-			return true
-		}
-	}
-	return false
-}
-
 func DataConv(driver string, ioHelper *IOStreams, client *sp.Client, conv *internal.Conv, dataOnly bool) (*spanner.BatchWriter, error) {
 	config := spanner.BatchWriterConfig{
 		BytesLimit: 100 * 1000 * 1000,
@@ -108,7 +98,7 @@ func DataConv(driver string, ioHelper *IOStreams, client *sp.Client, conv *inter
 	case POSTGRES, MYSQL:
 		return dataFromSQL(driver, config, client, conv)
 	case PGDUMP, MYSQLDUMP:
-		if checkInterleaved(conv) {
+		if conv.SpSchema.CheckInterleaved() {
 			return nil, fmt.Errorf("HarbourBridge does not currently support data conversion from dump files\nif the schema contains interleaved tables. Suggest using direct access to source database\ni.e. using drivers postgres and mysql.")
 		}
 		return dataFromDump(driver, config, ioHelper, client, conv, dataOnly)
@@ -421,7 +411,7 @@ func CreateDatabase(project, instance, dbName string, conv *internal.Conv, out *
 	// The schema we send to Spanner excludes comments (since Cloud
 	// Spanner DDL doesn't accept them), and protects table and col names
 	// using backticks (to avoid any issues with Spanner reserved words).
-	schema := conv.GetDDL(ddl.Config{Comments: false, ProtectIds: true, Tables: true, ForeignKeys: false})
+	schema := conv.SpSchema.GetDDL(ddl.Config{Comments: false, ProtectIds: true, Tables: true, ForeignKeys: false})
 	op, err := adminClient.CreateDatabase(ctx, &adminpb.CreateDatabaseRequest{
 		Parent:          fmt.Sprintf("projects/%s/instances/%s", project, instance),
 		CreateStatement: "CREATE DATABASE `" + dbName + "`",
@@ -449,7 +439,7 @@ func UpdateDDLForeignKeys(project, instance, dbName string, conv *internal.Conv,
 	// The schema we send to Spanner excludes comments (since Cloud
 	// Spanner DDL doesn't accept them), and protects table and col names
 	// using backticks (to avoid any issues with Spanner reserved words).
-	fkStmts := conv.GetDDL(ddl.Config{Comments: false, ProtectIds: true, Tables: false, ForeignKeys: true})
+	fkStmts := conv.SpSchema.GetDDL(ddl.Config{Comments: false, ProtectIds: true, Tables: false, ForeignKeys: true})
 	if len(fkStmts) == 0 {
 		return nil
 	}
@@ -560,7 +550,7 @@ func WriteSchemaFile(conv *internal.Conv, now time.Time, name string, out *os.Fi
 	// legal Cloud Spanner DDL (Cloud Spanner doesn't currently support comments).
 	// Change 'Comments' to false and 'ProtectIds' to true to write out a
 	// schema file that is legal Cloud Spanner DDL.
-	ddl := conv.GetDDL(ddl.Config{Comments: true, ProtectIds: false, Tables: true, ForeignKeys: true})
+	ddl := conv.SpSchema.GetDDL(ddl.Config{Comments: true, ProtectIds: false, Tables: true, ForeignKeys: true})
 	if len(ddl) == 0 {
 		ddl = []string{"\n-- Schema is empty -- no tables found\n"}
 	}
@@ -828,7 +818,7 @@ func SetRowStats(driver string, conv *internal.Conv, db *sql.DB) error {
 	case POSTGRES:
 		postgres.SetRowStats(conv, db)
 	default:
-		return fmt.Errorf("Could get rows stats for '%s' driver", driver)
+		return fmt.Errorf("Could not set rows stats for '%s' driver", driver)
 	}
 	return nil
 }
diff --git a/go.mod b/go.mod
index 35ee6eb..f0c28f8 100644
--- a/go.mod
+++ b/go.mod
@@ -3,7 +3,7 @@ module github.com/cloudspannerecosystem/harbourbridge
 go 1.13
 
 require (
-	cloud.google.com/go v0.65.0
+	cloud.google.com/go v0.78.0
 	cloud.google.com/go/spanner v1.10.0
 	github.com/DATA-DOG/go-sqlmock v1.4.1
 	github.com/aws/aws-sdk-go v1.34.5
@@ -14,7 +14,7 @@ require (
 	github.com/goware/modvendor v0.0.0-20190516042800-ce72b408a8fe // indirect
 	github.com/juju/errors v0.0.0-20200330140219-3fe23663418f
 	github.com/lfittl/pg_query_go v1.0.0
-	github.com/lib/pq v1.3.0
+	github.com/lib/pq v1.9.0
 	github.com/mattn/go-zglob v0.0.1 // indirect
 	//github.com/pingcap/parser v3.0.12+incompatible
 	github.com/pingcap/parser v0.0.0-20200422082501-7329d80eaf2c
@@ -23,8 +23,7 @@ require (
 	github.com/sirupsen/logrus v1.5.0 // indirect
 	github.com/stretchr/testify v1.5.1
 	golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
-	golang.org/x/sys v0.0.0-20200916084744-dbad9cb7cb7a // indirect
-	google.golang.org/api v0.31.0
-	google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d
-	google.golang.org/grpc v1.31.1
+	google.golang.org/api v0.40.0
+	google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c
+	google.golang.org/grpc v1.35.0
 )
diff --git a/go.sum b/go.sum
index 78348df..c9054f5 100644
--- a/go.sum
+++ b/go.sum
@@ -1,3 +1,5 @@
+bazil.org/fuse v0.0.0-20180421153158-65cc252bf669 h1:FNCRpXiquG1aoyqcIWVFmpTSKVcx2bQD38uZZeGtdlw=
+bazil.org/fuse v0.0.0-20180421153158-65cc252bf669/go.mod h1:Xbm+BRKSBEpa4q4hTSxohYNQpsxXPbPry4JJWOB3LB8=
 cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
 cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
 cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
@@ -21,6 +23,10 @@ cloud.google.com/go v0.60.0/go.mod h1:yw2G51M9IfRboUH61Us8GqCeF1PzPblB823Mn2q2eA
 cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
 cloud.google.com/go v0.65.0 h1:Dg9iHVQfrhq82rUNu9ZxUDrJLaxFUe/HlCVaLyRruq8=
 cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
+cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
+cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
+cloud.google.com/go v0.78.0 h1:oKpsiyKMfVpwR3zSAkQixGzlVE5ovitBuO0qSmCf0bI=
+cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg=
 cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
 cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
 cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
@@ -84,6 +90,8 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5P
 github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
 github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
 github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
+github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
+github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
 github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa h1:OaNxuTZr7kxeODyLWsRMC+OD03aFUH+mW6r2d+MWa5Y=
 github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8=
 github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd h1:qMd81Ts1T2OTKmB4acZcyKaMtRnY5Y44NuXGX2GFJ1w=
@@ -112,6 +120,7 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
+github.com/denisenkom/go-mssqldb v0.9.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
 github.com/dgraph-io/ristretto v0.0.1 h1:cJwdnj42uV8Jg4+KLrYovLiCgIfz9wtWm6E6KA+1tLs=
 github.com/dgraph-io/ristretto v0.0.1/go.mod h1:T40EBc7CJke8TkpiYfGGKAeFjSaxuFXhuXRyumBd6RE=
 github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
@@ -129,6 +138,8 @@ github.com/elazarl/go-bindata-assetfs v1.0.0/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo
 github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
 github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
 github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
+github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
+github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
 github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
 github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
 github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
@@ -216,6 +227,8 @@ github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvq
 github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
 github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=
 github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM=
+github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
 github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db h1:woRePGFeVFfLKN/pOkfl+p/TAqKOfFu+7KPlMVpok/w=
 github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
@@ -233,9 +246,12 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
 github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM=
 github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M=
+github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
 github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
 github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
+github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
 github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
 github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
 github.com/google/pprof v0.0.0-20190930153522-6ce02741cba3/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
@@ -246,11 +262,15 @@ github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hf
 github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
 github.com/google/pprof v0.0.0-20200507031123-427632fa3b1c/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
 github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
 github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
 github.com/google/shlex v0.0.0-20181106134648-c34317bd91bf/go.mod h1:RpwtwJQFrIEPstU94h88MWPXP2ektJZ8cZ0YntAmXiE=
 github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
 github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
 github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM=
 github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
@@ -279,6 +299,7 @@ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ
 github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
 github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
 github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
+github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
 github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
 github.com/jinzhu/gorm v1.9.12/go.mod h1:vhTjlKSJUTWNtcbQtrMBFCxy7eXTzeCAzfL5fBZT/Qs=
 github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
@@ -324,6 +345,8 @@ github.com/lfittl/pg_query_go v1.0.0/go.mod h1:jcikG62RKf+NIWmbLzjjk73m4x6um2pKf
 github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
 github.com/lib/pq v1.3.0 h1:/qkRGz8zljWiDcFvgpwUpwIAPu3r07TDvs3Rws+o/pU=
 github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
+github.com/lib/pq v1.9.0 h1:L8nSXQQzAYByakOFMTwpjRoHsMJklur4Gi59b6VivR8=
+github.com/lib/pq v1.9.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
 github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
 github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
 github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
@@ -524,6 +547,8 @@ go.opencensus.io v0.22.3 h1:8sGtKOrtQqkN1bp2AtX+misvLIlOmsEsNd+9NIcPEm8=
 go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
 go.opencensus.io v0.22.4 h1:LYy1Hy3MJdrCdMwwzxA/dRok4ejH+RwNGbuoD9fCjto=
 go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
+go.opencensus.io v0.22.5 h1:dntmOdLpSpHlVqbW5Eay97DelsZHe+55D+xC6i0dDS0=
+go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
 go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
 go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
 go.uber.org/atomic v1.6.0 h1:Ezj3JGmsOnG1MoRWQkPBsKLe9DwWD9QeXzTRzzldNVk=
@@ -584,6 +609,7 @@ golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRu
 golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
 golang.org/x/lint v0.0.0-20200302205851-738671d3881b h1:Wh+f8QHJXR411sJR8/vRBTZ7YapZaRvUcLFFJhusH0k=
 golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
+golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
 golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
 golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
 golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
@@ -593,6 +619,8 @@ golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzB
 golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=
 golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -633,6 +661,13 @@ golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81R
 golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
 golang.org/x/net v0.0.0-20200822124328-c89045814202 h1:VvcQYSHwXgi7W+TpUR6A9g6Up98WAHf3f/ulnJ62IyA=
 golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20210119194325-5f4716e94777 h1:003p0dJM77cxMSyCPFphvZf/Y5/NXf5fzg6ufd1/Oew=
+golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0=
@@ -640,6 +675,13 @@ golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4Iltr
 golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw=
 golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99 h1:5vD4XjIc0X5+kHZjx4UecYdjA6mJo+XXNoaW0EjU5Os=
+golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93 h1:alLDrZkL34Y2bnGHfvC1CYBRBXCXgx8AC2vY4MRtYX4=
+golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -649,6 +691,8 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ
 golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a h1:WXEvlFVvvGxCJLG6REjsT03iWnKLEWinaScsxF2Vm2o=
 golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -693,8 +737,19 @@ golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200828194041-157a740278f4 h1:kCCpuwSAoYJPkNc6x0xT9yTtV4oKtARo4RGBQWOfg9E=
 golang.org/x/sys v0.0.0-20200828194041-157a740278f4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200916084744-dbad9cb7cb7a h1:chkwkn8HYWVtTE5DCQNKYlkyptadXYY0+PuyaVdyMo4=
 golang.org/x/sys v0.0.0-20200916084744-dbad9cb7cb7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43 h1:SgQ6LNaYJU0JIuEHv9+s6EbhSCwYeAf5Yvj6lpYlqAE=
+golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210304195927-444254391f19 h1:B4VmpLO5AaG7XCNX3FF2Zj6LSat1kaKifJN9yL09sCg=
+golang.org/x/sys v0.0.0-20210304195927-444254391f19/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -702,6 +757,9 @@ golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ=
+golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -773,6 +831,12 @@ golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc
 golang.org/x/tools v0.0.0-20200828161849-5deb26317202/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
 golang.org/x/tools v0.0.0-20200903185744-af4cc2cd812e h1:RvNtqusJ+6DJ07/by/M84a6/Dd17XU6n8QvhvknjJno=
 golang.org/x/tools v0.0.0-20200903185744-af4cc2cd812e/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
+golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
+golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
@@ -800,6 +864,10 @@ google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSr
 google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
 google.golang.org/api v0.31.0 h1:1w5Sz/puhxFo9lTtip2n47k7toB/U2nCqOKNHd3Yrbo=
 google.golang.org/api v0.31.0/go.mod h1:CL+9IBCa2WWU6gRuBWaKqGWLFFwbEUXkfeMkHLQWYWo=
+google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
+google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
+google.golang.org/api v0.40.0 h1:uWrpz12dpVPn7cojP82mk02XDgTJLDPc2KbVTxrWb4A=
+google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
 google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
 google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
 google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
@@ -807,6 +875,8 @@ google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww
 google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
 google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc=
 google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
+google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
 google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
 google.golang.org/genproto v0.0.0-20181004005441-af9cb2a35e7f/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
 google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
@@ -847,6 +917,12 @@ google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6D
 google.golang.org/genproto v0.0.0-20200831141814-d751682dd103/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
 google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d h1:92D1fum1bJLKSdr11OJ+54YeCMCGYIygTA7R/YZxH5M=
 google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c h1:7A9LQhrZmuCPI79/sYSbscFqBp4XFYf6oaIQuV1xji4=
+google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
 google.golang.org/grpc v0.0.0-20180607172857-7a6a684ca69e/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
@@ -867,6 +943,10 @@ google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM
 google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
 google.golang.org/grpc v1.31.1 h1:SfXqXS5hkufcdZ/mHtYCh53P2b+92WQq/DZcKLgsFRs=
 google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
+google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
+google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
+google.golang.org/grpc v1.35.0 h1:TwIQcH3es+MojMVojxxfQ3l3OF2KzlRxML2xZq0kRo8=
+google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
 google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
 google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
 google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
diff --git a/internal/convert.go b/internal/convert.go
index 56af3f6..cae88aa 100644
--- a/internal/convert.go
+++ b/internal/convert.go
@@ -16,7 +16,6 @@ package internal
 
 import (
 	"fmt"
-	"sort"
 	"time"
 
 	"github.com/cloudspannerecosystem/harbourbridge/schema"
@@ -26,7 +25,7 @@ import (
 // Conv contains all schema and data conversion state.
 type Conv struct {
 	mode           mode                                // Schema mode or data mode.
-	SpSchema       map[string]ddl.CreateTable          // Maps Spanner table name to Spanner schema.
+	SpSchema       ddl.Schema                          // Maps Spanner table name to Spanner schema.
 	SyntheticPKeys map[string]SyntheticPKey            // Maps Spanner table name to synthetic primary key (if needed).
 	SrcSchema      map[string]schema.Table             // Maps source-DB table name to schema information.
 	Issues         map[string]map[string][]SchemaIssue // Maps source-DB table/col to list of schema conversion issues.
@@ -122,7 +121,7 @@ type statementStat struct {
 // MakeConv returns a default-configured Conv.
 func MakeConv() *Conv {
 	return &Conv{
-		SpSchema:       make(map[string]ddl.CreateTable),
+		SpSchema:       ddl.NewSchema(),
 		SyntheticPKeys: make(map[string]SyntheticPKey),
 		SrcSchema:      make(map[string]schema.Table),
 		Issues:         make(map[string]map[string][]SchemaIssue),
@@ -168,61 +167,6 @@ func (conv *Conv) SetDataMode() {
 	conv.mode = dataOnly
 }
 
-// GetDDL Schema returns the Spanner schema that has been constructed so far.
-// We return DDL in alphabetical order with one exception: interleaved tables are
-// potentially out of order since they must appear after the definition of their
-// parent table.
-// TODO: Move GetDDL function to ddl/ast.go.
-func (conv *Conv) GetDDL(c ddl.Config) []string {
-	var tables []string
-	for t := range conv.SpSchema {
-		tables = append(tables, t)
-	}
-	sort.Strings(tables)
-	var ddl []string
-	if c.Tables {
-		tableQueue := tables
-		printed := make(map[string]bool)
-		for len(tableQueue) > 0 {
-			t := tableQueue[0]
-			tableQueue = tableQueue[1:]
-			_, found := printed[conv.SpSchema[t].Parent]
-			// Print table t if either:
-			// a) t is not interleaved in another table, or
-			// b) t is interleaved in another table and that table has already been printed.
-			if conv.SpSchema[t].Parent == "" || found {
-				ddl = append(ddl, conv.SpSchema[t].PrintCreateTable(c))
-				for _, index := range conv.SpSchema[t].Indexes {
-					ddl = append(ddl, index.PrintCreateIndex(c))
-				}
-				printed[t] = true
-			} else {
-				// We can't print table t now because its parent hasn't been printed.
-				// Add it at end of tables and we'll try again later.
-				// We might need multiple iterations to print chains of interleaved tables,
-				// but we will always make progress because interleaved tables can't
-				// have cycles. In principle this could be O(n^2), but in practice chains
-				// of interleaved tables are small.
-				tableQueue = append(tableQueue, t)
-			}
-		}
-	}
-	// Append foreign key constraints to DDL.
-	// We always use alter table statements for foreign key constraints.
-	// The alternative of putting foreign key constraints in-line as part of create
-	// table statements is tricky because of table order (need to define tables
-	// before they are referenced by foreign key constraints) and the possibility
-	// of circular foreign keys definitions. We opt for simplicity.
-	if c.ForeignKeys {
-		for _, t := range tables {
-			for _, fk := range conv.SpSchema[t].Fks {
-				ddl = append(ddl, fk.PrintForeignKeyAlterTable(c, t))
-			}
-		}
-	}
-	return ddl
-}
-
 // WriteRow calls dataSink and updates row stats.
 func (conv *Conv) WriteRow(srcTable, spTable string, spCols []string, spVals []interface{}) {
 	if conv.dataSink == nil {
@@ -312,7 +256,7 @@ func (conv *Conv) AddPrimaryKeys() {
 			k := conv.buildPrimaryKey(t)
 			ct.ColNames = append(ct.ColNames, k)
 			ct.ColDefs[k] = ddl.ColumnDef{Name: k, T: ddl.Type{Name: ddl.Int64}}
-			ct.Pks = []ddl.IndexKey{ddl.IndexKey{Col: k}}
+			ct.Pks = []ddl.IndexKey{{Col: k}}
 			conv.SpSchema[t] = ct
 			conv.SyntheticPKeys[t] = SyntheticPKey{k, 0}
 		}
diff --git a/main.go b/main.go
index e587051..75d119f 100644
--- a/main.go
+++ b/main.go
@@ -26,16 +26,13 @@ import (
 	_ "github.com/go-sql-driver/mysql"
 	_ "github.com/lib/pq"
 
+	"github.com/cloudspannerecosystem/harbourbridge/cmd"
 	"github.com/cloudspannerecosystem/harbourbridge/conversion"
 	"github.com/cloudspannerecosystem/harbourbridge/internal"
 	"github.com/cloudspannerecosystem/harbourbridge/web"
 )
 
 var (
-	badDataFile      = "dropped.txt"
-	schemaFile       = "schema.txt"
-	sessionFile      = "session.json"
-	reportFile       = "report.txt"
 	dbNameOverride   string
 	instanceOverride string
 	filePrefix       = ""
@@ -74,10 +71,13 @@ Sample usage:
 func main() {
 	flag.Usage = usage
 	flag.Parse()
+
+	// Note: the web interface does not use any commandline flags.
 	if webapi {
 		web.WebApp()
 		return
 	}
+
 	internal.VerboseInit(verbose)
 	lf, err := conversion.SetupLogFile()
 	if err != nil {
@@ -86,8 +86,16 @@ func main() {
 	}
 	defer conversion.Close(lf)
 
+	if schemaOnly && dataOnly {
+		panic(fmt.Errorf("can't use both schema-only and data-only modes at once"))
+	}
+	if dataOnly && sessionJSON == "" {
+		panic(fmt.Errorf("when using data-only mode, the session must specify the session file to use"))
+	}
+
 	ioHelper := &conversion.IOStreams{In: os.Stdin, Out: os.Stdout}
 	fmt.Println("Using driver (source DB):", driverName)
+
 	var project, instance string
 	if !schemaOnly {
 		project, err = conversion.GetProject()
@@ -124,77 +132,10 @@ func main() {
 		filePrefix = dbName + "."
 	}
 
-	if schemaOnly && dataOnly {
-		panic(fmt.Errorf("can't use both schema-only and data-only modes at once"))
-	}
-
-	if dataOnly && sessionJSON == "" {
-		panic(fmt.Errorf("when using data-only mode, the session must specify the session file to use"))
-	}
-
-	err = commandLine(driverName, project, instance, dbName, ioHelper, filePrefix, now)
+	// TODO (agasheesh@): Collect all the config state in a single struct and pass the same to CommandLine instead of
+	// passing multiple parameters. Config state would be populated by parsing the flags and environment variables.
+	err = cmd.CommandLine(driverName, project, instance, dbName, dataOnly, schemaOnly, schemaSampleSize, sessionJSON, ioHelper, filePrefix, now)
 	if err != nil {
 		panic(err)
 	}
-
-}
-
-// commandLine provides the core processing for HarbourBridge
-// when run as a command-line tool. It performs the following steps:
-// 1. Run schema conversion
-// 2. Create database
-// 3. Run data conversion
-// 4. Generate report
-func commandLine(driver, projectID, instanceID, dbName string, ioHelper *conversion.IOStreams, outputFilePrefix string, now time.Time) error {
-	var conv *internal.Conv
-	var err error
-	if !dataOnly {
-		conv, err = conversion.SchemaConv(driver, ioHelper, schemaSampleSize)
-		if err != nil {
-			return err
-		}
-		if ioHelper.SeekableIn != nil {
-			defer ioHelper.In.Close()
-		}
-
-		conversion.WriteSchemaFile(conv, now, outputFilePrefix+schemaFile, ioHelper.Out)
-		conversion.WriteSessionFile(conv, outputFilePrefix+sessionFile, ioHelper.Out)
-		if schemaOnly {
-			conversion.Report(driver, nil, ioHelper.BytesRead, "", conv, outputFilePrefix+reportFile, ioHelper.Out)
-			return nil
-		}
-	} else {
-		conv = internal.MakeConv()
-		err = conversion.ReadSessionFile(conv, sessionJSON)
-		if err != nil {
-			return err
-		}
-	}
-
-	db, err := conversion.CreateDatabase(projectID, instanceID, dbName, conv, ioHelper.Out)
-	if err != nil {
-		fmt.Printf("\nCan't create database: %v\n", err)
-		return fmt.Errorf("can't create database")
-	}
-
-	client, err := conversion.GetClient(db)
-	if err != nil {
-		fmt.Printf("\nCan't create client for db %s: %v\n", db, err)
-		return fmt.Errorf("can't create Spanner client")
-	}
-
-	bw, err := conversion.DataConv(driver, ioHelper, client, conv, dataOnly)
-	if err != nil {
-		fmt.Printf("\nCan't finish data conversion for db %s: %v\n", db, err)
-		return fmt.Errorf("can't finish data conversion")
-	}
-
-	if err = conversion.UpdateDDLForeignKeys(projectID, instanceID, dbName, conv, ioHelper.Out); err != nil {
-		fmt.Printf("\nCan't perform update operation on db %s with foreign keys: %v\n", db, err)
-		return fmt.Errorf("can't perform update schema with foreign keys")
-	}
-	banner := conversion.GetBanner(now, db)
-	conversion.Report(driver, bw.DroppedRowsByTable(), ioHelper.BytesRead, banner, conv, outputFilePrefix+reportFile, ioHelper.Out)
-	conversion.WriteBadData(bw, conv, banner, outputFilePrefix+badDataFile, ioHelper.Out)
-	return nil
 }
diff --git a/spanner/ddl/ast.go b/spanner/ddl/ast.go
index 1576832..a6896d5 100644
--- a/spanner/ddl/ast.go
+++ b/spanner/ddl/ast.go
@@ -23,6 +23,7 @@ package ddl
 import (
 	"fmt"
 	"math"
+	"sort"
 	"strconv"
 	"strings"
 )
@@ -249,6 +250,79 @@ func (k Foreignkey) PrintForeignKeyAlterTable(c Config, tableName string) string
 	return fmt.Sprintf("ALTER TABLE %s ADD %sFOREIGN KEY (%s) REFERENCES %s (%s)", c.quote(tableName), s, strings.Join(cols, ", "), c.quote(k.ReferTable), strings.Join(referCols, ", "))
 }
 
+type Schema map[string]CreateTable
+
+func NewSchema() Schema {
+	return make(map[string]CreateTable)
+}
+
+// GetDDL returns the string representation of Spanner schema represented by Schema struct.
+// Tables are printed in alphabetical order with one exception: interleaved
+// tables are potentially out of order since they must appear after the
+// definition of their parent table.
+func (s Schema) GetDDL(c Config) []string {
+	var ddl []string
+
+	var tableNames []string
+	for t := range s {
+		tableNames = append(tableNames, t)
+	}
+	sort.Strings(tableNames)
+
+	if c.Tables {
+		tableQueue := tableNames
+		printed := make(map[string]bool)
+		for len(tableQueue) > 0 {
+			tableName := tableQueue[0]
+			table := s[tableName]
+			tableQueue = tableQueue[1:]
+
+			// Print table t if either:
+			// a) t is not interleaved in another table, or
+			// b) t is interleaved in another table and that table has already been printed.
+			if table.Parent == "" || printed[table.Parent] {
+				ddl = append(ddl, table.PrintCreateTable(c))
+				for _, index := range table.Indexes {
+					ddl = append(ddl, index.PrintCreateIndex(c))
+				}
+				printed[tableName] = true
+			} else {
+				// We can't print table t now because its parent hasn't been printed.
+				// Add it at end of tables and we'll try again later.
+				// We might need multiple iterations to print chains of interleaved tables,
+				// but we will always make progress because interleaved tables can't
+				// have cycles. In principle this could be O(n^2), but in practice chains
+				// of interleaved tables are small.
+				tableQueue = append(tableQueue, tableName)
+			}
+		}
+	}
+	// Append foreign key constraints to DDL.
+	// We always use alter table statements for foreign key constraints.
+	// The alternative of putting foreign key constraints in-line as part of create
+	// table statements is tricky because of table order (need to define tables
+	// before they are referenced by foreign key constraints) and the possibility
+	// of circular foreign keys definitions. We opt for simplicity.
+	if c.ForeignKeys {
+		for _, t := range tableNames {
+			for _, fk := range s[t].Fks {
+				ddl = append(ddl, fk.PrintForeignKeyAlterTable(c, t))
+			}
+		}
+	}
+	return ddl
+}
+
+// CheckInterleaved checks if schema contains interleaved tables.
+func (s Schema) CheckInterleaved() bool {
+	for _, table := range s {
+		if table.Parent != "" {
+			return true
+		}
+	}
+	return false
+}
+
 func maxStringLength(s []string) int {
 	n := 0
 	for _, x := range s {


Test patch:
diff --git a/.github/workflows/integration-tests-against-emulator.yaml b/.github/workflows/integration-tests-against-emulator.yaml
index 1218786..60e63df 100644
--- a/.github/workflows/integration-tests-against-emulator.yaml
+++ b/.github/workflows/integration-tests-against-emulator.yaml
@@ -7,14 +7,22 @@ name: integration-tests-against-emulator
 jobs:
   test:
     runs-on: ubuntu-latest
-    # set PostgreSQL related environment variables
     env:
+      # set PostgreSQL related environment variables
       PGHOST: localhost
       PGPORT: 5432
       PGUSER: postgres
       PGDATABASE: postgres
       PGPASSWORD: postgres
 
+      # set MySQL related environment variables
+      DB_HOST: localhost
+      MYSQLHOST: localhost
+      MYSQLPORT: 3306
+      MYSQLUSER: root
+      MYSQLDATABASE: test
+      MYSQLPWD: root
+
     services:
       emulator:
         image: gcr.io/cloud-spanner-emulator/emulator:1.2.0
@@ -31,6 +39,15 @@ jobs:
         - 5432:5432
         # needed because the postgres container does not provide a healthcheck
         options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
+      mariadb:
+        image: mariadb:10.3
+        env:
+          MYSQL_ROOT_PASSWORD: root
+          MYSQL_DATABASE: test
+        ports:
+        - 3306:3306
+        # needed because the mysql container does not provide a healthcheck
+        options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
 
     steps:
       - uses: actions/checkout@v2
@@ -45,6 +62,14 @@ jobs:
       - run: psql --version
       - run: psql -f test_data/pg_dump.test.out
 
+      # init a MySQL database from the test_data
+      - name: Install MySQL client required for loading .sql files
+        run: |
+          sudo apt-get update
+          sudo apt-get install -y mysql-client
+      - run: mysql --version
+      - run: mysql -v -P 3306 --protocol=tcp -u root -proot test < test_data/mysqldump.test.out
+
       # create a spanner instance
       - uses: GoogleCloudPlatform/github-actions/setup-gcloud@master
         with:
diff --git a/internal/convert_test.go b/internal/convert_test.go
index 16c8e05..989102d 100644
--- a/internal/convert_test.go
+++ b/internal/convert_test.go
@@ -42,73 +42,6 @@ func TestSetDataMode(t *testing.T) {
 	assert.True(t, conv.DataMode())
 }
 
-func TestGetDDL(t *testing.T) {
-	conv := MakeConv()
-	conv.SpSchema["table1"] = ddl.CreateTable{
-		Name:     "table1",
-		ColNames: []string{"a", "b"},
-		ColDefs: map[string]ddl.ColumnDef{
-			"a": ddl.ColumnDef{Name: "a", T: ddl.Type{Name: ddl.Int64}},
-			"b": ddl.ColumnDef{Name: "b", T: ddl.Type{Name: ddl.Int64}},
-		},
-		Pks:     []ddl.IndexKey{ddl.IndexKey{Col: "a"}},
-		Fks:     []ddl.Foreignkey{ddl.Foreignkey{Name: "fk1", Columns: []string{"b"}, ReferTable: "ref_table1", ReferColumns: []string{"ref_b"}}},
-		Indexes: []ddl.CreateIndex{ddl.CreateIndex{Name: "index1", Table: "table1", Unique: false, Keys: []ddl.IndexKey{ddl.IndexKey{Col: "b", Desc: false}}}},
-	}
-	conv.SpSchema["table2"] = ddl.CreateTable{
-		Name:     "table2",
-		ColNames: []string{"a", "b", "c"},
-		ColDefs: map[string]ddl.ColumnDef{
-			"a": ddl.ColumnDef{Name: "a", T: ddl.Type{Name: ddl.Int64}},
-			"b": ddl.ColumnDef{Name: "b", T: ddl.Type{Name: ddl.Int64}},
-			"c": ddl.ColumnDef{Name: "c", T: ddl.Type{Name: ddl.Int64}},
-		},
-		Pks:     []ddl.IndexKey{ddl.IndexKey{Col: "a"}},
-		Fks:     []ddl.Foreignkey{ddl.Foreignkey{Name: "fk2", Columns: []string{"b", "c"}, ReferTable: "ref_table2", ReferColumns: []string{"ref_b", "ref_c"}}},
-		Indexes: []ddl.CreateIndex{ddl.CreateIndex{Name: "index2", Table: "table2", Unique: true, Keys: []ddl.IndexKey{ddl.IndexKey{Col: "b", Desc: true}, ddl.IndexKey{Col: "c", Desc: false}}}},
-	}
-	conv.SpSchema["table3"] = ddl.CreateTable{
-		Name:     "table3",
-		ColNames: []string{"a", "b", "c"},
-		ColDefs: map[string]ddl.ColumnDef{
-			"a": ddl.ColumnDef{Name: "a", T: ddl.Type{Name: ddl.Int64}},
-			"b": ddl.ColumnDef{Name: "b", T: ddl.Type{Name: ddl.Int64}},
-			"c": ddl.ColumnDef{Name: "c", T: ddl.Type{Name: ddl.Int64}},
-		},
-		Pks:    []ddl.IndexKey{ddl.IndexKey{Col: "a"}, ddl.IndexKey{Col: "b"}},
-		Fks:    []ddl.Foreignkey{ddl.Foreignkey{Name: "fk3", Columns: []string{"c"}, ReferTable: "ref_table3", ReferColumns: []string{"ref_c"}}},
-		Parent: "table1",
-	}
-	tables := conv.GetDDL(ddl.Config{Tables: true, ForeignKeys: false})
-	e := []string{
-		"CREATE TABLE table1 (\n    a INT64,\n    b INT64 \n) PRIMARY KEY (a)",
-		"CREATE INDEX index1 ON table1 (b)",
-		"CREATE TABLE table2 (\n    a INT64,\n    b INT64,\n    c INT64 \n) PRIMARY KEY (a)",
-		"CREATE UNIQUE INDEX index2 ON table2 (b DESC, c)",
-		"CREATE TABLE table3 (\n    a INT64,\n    b INT64,\n    c INT64 \n) PRIMARY KEY (a, b),\nINTERLEAVE IN PARENT table1",
-	}
-	assert.ElementsMatch(t, e, tables)
-	fks := conv.GetDDL(ddl.Config{Tables: false, ForeignKeys: true})
-	e2 := []string{
-		"ALTER TABLE table1 ADD CONSTRAINT fk1 FOREIGN KEY (b) REFERENCES ref_table1 (ref_b)",
-		"ALTER TABLE table2 ADD CONSTRAINT fk2 FOREIGN KEY (b, c) REFERENCES ref_table2 (ref_b, ref_c)",
-		"ALTER TABLE table3 ADD CONSTRAINT fk3 FOREIGN KEY (c) REFERENCES ref_table3 (ref_c)",
-	}
-	assert.ElementsMatch(t, e2, fks)
-	tablesAndFks := conv.GetDDL(ddl.Config{Tables: true, ForeignKeys: true})
-	e3 := []string{
-		"CREATE TABLE table1 (\n    a INT64,\n    b INT64 \n) PRIMARY KEY (a)",
-		"CREATE INDEX index1 ON table1 (b)",
-		"CREATE TABLE table2 (\n    a INT64,\n    b INT64,\n    c INT64 \n) PRIMARY KEY (a)",
-		"CREATE UNIQUE INDEX index2 ON table2 (b DESC, c)",
-		"CREATE TABLE table3 (\n    a INT64,\n    b INT64,\n    c INT64 \n) PRIMARY KEY (a, b),\nINTERLEAVE IN PARENT table1",
-		"ALTER TABLE table1 ADD CONSTRAINT fk1 FOREIGN KEY (b) REFERENCES ref_table1 (ref_b)",
-		"ALTER TABLE table2 ADD CONSTRAINT fk2 FOREIGN KEY (b, c) REFERENCES ref_table2 (ref_b, ref_c)",
-		"ALTER TABLE table3 ADD CONSTRAINT fk3 FOREIGN KEY (c) REFERENCES ref_table3 (ref_c)",
-	}
-	assert.ElementsMatch(t, e3, tablesAndFks)
-}
-
 func TestRows(t *testing.T) {
 	conv := MakeConv()
 	conv.Stats.Rows["table1"] = 42
@@ -158,8 +91,8 @@ func TestAddPrimaryKeys(t *testing.T) {
 		Name:     "table",
 		ColNames: []string{"a", "b"},
 		ColDefs: map[string]ddl.ColumnDef{
-			"a": ddl.ColumnDef{Name: "a", T: ddl.Type{Name: ddl.Int64}},
-			"b": ddl.ColumnDef{Name: "b", T: ddl.Type{Name: ddl.Float64}},
+			"a": {Name: "a", T: ddl.Type{Name: ddl.Int64}},
+			"b": {Name: "b", T: ddl.Type{Name: ddl.Float64}},
 		},
 		Pks: []ddl.IndexKey{}}
 	conv.AddPrimaryKeys()
@@ -167,11 +100,11 @@ func TestAddPrimaryKeys(t *testing.T) {
 		Name:     "table",
 		ColNames: []string{"a", "b", "synth_id"},
 		ColDefs: map[string]ddl.ColumnDef{
-			"a":        ddl.ColumnDef{Name: "a", T: ddl.Type{Name: ddl.Int64}},
-			"b":        ddl.ColumnDef{Name: "b", T: ddl.Type{Name: ddl.Float64}},
-			"synth_id": ddl.ColumnDef{Name: "synth_id", T: ddl.Type{Name: ddl.Int64}},
+			"a":        {Name: "a", T: ddl.Type{Name: ddl.Int64}},
+			"b":        {Name: "b", T: ddl.Type{Name: ddl.Float64}},
+			"synth_id": {Name: "synth_id", T: ddl.Type{Name: ddl.Int64}},
 		},
-		Pks: []ddl.IndexKey{ddl.IndexKey{Col: "synth_id"}}}
+		Pks: []ddl.IndexKey{{Col: "synth_id"}}}
 	assert.Equal(t, e, conv.SpSchema["table"])
 	assert.Equal(t, SyntheticPKey{Col: "synth_id", Sequence: 0}, conv.SyntheticPKeys["table"])
 }
diff --git a/internal/mapping_test.go b/internal/mapping_test.go
index 95a2184..b242673 100644
--- a/internal/mapping_test.go
+++ b/internal/mapping_test.go
@@ -139,50 +139,50 @@ func TestGetSpannerId(t *testing.T) {
 
 func TestResolveRefs(t *testing.T) {
 	basicTests := []struct {
-		name             string                     // Name of test.
-		spSchema         map[string]ddl.CreateTable // Spanner schema.
-		expectedSpSchema map[string]ddl.CreateTable // Expected Spanner schema.
-		unexpecteds      int64                      // Expected unexpected conditions
+		name             string     // Name of test.
+		spSchema         ddl.Schema // Spanner schema.
+		expectedSpSchema ddl.Schema // Expected Spanner schema.
+		unexpecteds      int64      // Expected unexpected conditions
 	}{
 		{
 			name: "Table name case mismatch",
 			spSchema: map[string]ddl.CreateTable{
-				"a": ddl.CreateTable{
+				"a": {
 					Name:     "a",
 					ColNames: []string{"acol1", "acol2"},
 					ColDefs: map[string]ddl.ColumnDef{
-						"acol1": ddl.ColumnDef{Name: "acol1", T: ddl.Type{Name: ddl.Int64}},
-						"acol2": ddl.ColumnDef{Name: "acol2", T: ddl.Type{Name: ddl.Int64}},
+						"acol1": {Name: "acol1", T: ddl.Type{Name: ddl.Int64}},
+						"acol2": {Name: "acol2", T: ddl.Type{Name: ddl.Int64}},
 					},
-					Fks: []ddl.Foreignkey{ddl.Foreignkey{Name: "fk_test", Columns: []string{"acol1"}, ReferTable: "bB", ReferColumns: []string{"bcol1"}}},
+					Fks: []ddl.Foreignkey{{Name: "fk_test", Columns: []string{"acol1"}, ReferTable: "bB", ReferColumns: []string{"bcol1"}}},
 				},
-				"bb": ddl.CreateTable{
+				"bb": {
 					Name:     "bb",
 					ColNames: []string{"bcol1", "bcol2", "bcol3"},
 					ColDefs: map[string]ddl.ColumnDef{
-						"bcol1": ddl.ColumnDef{Name: "bcol1", T: ddl.Type{Name: ddl.Int64}},
-						"bcol2": ddl.ColumnDef{Name: "bcol2", T: ddl.Type{Name: ddl.Int64}},
-						"bcol3": ddl.ColumnDef{Name: "bcol3", T: ddl.Type{Name: ddl.Int64}},
+						"bcol1": {Name: "bcol1", T: ddl.Type{Name: ddl.Int64}},
+						"bcol2": {Name: "bcol2", T: ddl.Type{Name: ddl.Int64}},
+						"bcol3": {Name: "bcol3", T: ddl.Type{Name: ddl.Int64}},
 					},
 				},
 			},
 			expectedSpSchema: map[string]ddl.CreateTable{
-				"a": ddl.CreateTable{
+				"a": {
 					Name:     "a",
 					ColNames: []string{"acol1", "acol2"},
 					ColDefs: map[string]ddl.ColumnDef{
-						"acol1": ddl.ColumnDef{Name: "acol1", T: ddl.Type{Name: ddl.Int64}},
-						"acol2": ddl.ColumnDef{Name: "acol2", T: ddl.Type{Name: ddl.Int64}},
+						"acol1": {Name: "acol1", T: ddl.Type{Name: ddl.Int64}},
+						"acol2": {Name: "acol2", T: ddl.Type{Name: ddl.Int64}},
 					},
-					Fks: []ddl.Foreignkey{ddl.Foreignkey{Name: "fk_test", Columns: []string{"acol1"}, ReferTable: "bb", ReferColumns: []string{"bcol1"}}},
+					Fks: []ddl.Foreignkey{{Name: "fk_test", Columns: []string{"acol1"}, ReferTable: "bb", ReferColumns: []string{"bcol1"}}},
 				},
-				"bb": ddl.CreateTable{
+				"bb": {
 					Name:     "bb",
 					ColNames: []string{"bcol1", "bcol2", "bcol3"},
 					ColDefs: map[string]ddl.ColumnDef{
-						"bcol1": ddl.ColumnDef{Name: "bcol1", T: ddl.Type{Name: ddl.Int64}},
-						"bcol2": ddl.ColumnDef{Name: "bcol2", T: ddl.Type{Name: ddl.Int64}},
-						"bcol3": ddl.ColumnDef{Name: "bcol3", T: ddl.Type{Name: ddl.Int64}},
+						"bcol1": {Name: "bcol1", T: ddl.Type{Name: ddl.Int64}},
+						"bcol2": {Name: "bcol2", T: ddl.Type{Name: ddl.Int64}},
+						"bcol3": {Name: "bcol3", T: ddl.Type{Name: ddl.Int64}},
 					},
 				},
 			},
@@ -191,44 +191,44 @@ func TestResolveRefs(t *testing.T) {
 		{
 			name: "Column name case mismatch",
 			spSchema: map[string]ddl.CreateTable{
-				"bb": ddl.CreateTable{
+				"bb": {
 					Name:     "bb",
 					ColNames: []string{"bcol1", "bcol2", "bcol3"},
 					ColDefs: map[string]ddl.ColumnDef{
-						"bcol1": ddl.ColumnDef{Name: "bcol1", T: ddl.Type{Name: ddl.Int64}},
-						"bcol2": ddl.ColumnDef{Name: "bcol2", T: ddl.Type{Name: ddl.Int64}},
-						"bcol3": ddl.ColumnDef{Name: "bcol3", T: ddl.Type{Name: ddl.Int64}},
+						"bcol1": {Name: "bcol1", T: ddl.Type{Name: ddl.Int64}},
+						"bcol2": {Name: "bcol2", T: ddl.Type{Name: ddl.Int64}},
+						"bcol3": {Name: "bcol3", T: ddl.Type{Name: ddl.Int64}},
 					},
-					Fks: []ddl.Foreignkey{ddl.Foreignkey{Name: "fk_test2", Columns: []string{"bcol2", "bcol3"}, ReferTable: "cc", ReferColumns: []string{"cCol1", "ccol2"}}},
+					Fks: []ddl.Foreignkey{{Name: "fk_test2", Columns: []string{"bcol2", "bcol3"}, ReferTable: "cc", ReferColumns: []string{"cCol1", "ccol2"}}},
 				},
-				"cc": ddl.CreateTable{
+				"cc": {
 					Name:     "cc",
 					ColNames: []string{"ccol1", "ccol2", "ccol3"},
 					ColDefs: map[string]ddl.ColumnDef{
-						"ccol1": ddl.ColumnDef{Name: "ccol1", T: ddl.Type{Name: ddl.Int64}},
-						"ccol2": ddl.ColumnDef{Name: "ccol2", T: ddl.Type{Name: ddl.Int64}},
-						"ccol3": ddl.ColumnDef{Name: "ccol3", T: ddl.Type{Name: ddl.Int64}},
+						"ccol1": {Name: "ccol1", T: ddl.Type{Name: ddl.Int64}},
+						"ccol2": {Name: "ccol2", T: ddl.Type{Name: ddl.Int64}},
+						"ccol3": {Name: "ccol3", T: ddl.Type{Name: ddl.Int64}},
 					},
 				},
 			},
 			expectedSpSchema: map[string]ddl.CreateTable{
-				"bb": ddl.CreateTable{
+				"bb": {
 					Name:     "bb",
 					ColNames: []string{"bcol1", "bcol2", "bcol3"},
 					ColDefs: map[string]ddl.ColumnDef{
-						"bcol1": ddl.ColumnDef{Name: "bcol1", T: ddl.Type{Name: ddl.Int64}},
-						"bcol2": ddl.ColumnDef{Name: "bcol2", T: ddl.Type{Name: ddl.Int64}},
-						"bcol3": ddl.ColumnDef{Name: "bcol3", T: ddl.Type{Name: ddl.Int64}},
+						"bcol1": {Name: "bcol1", T: ddl.Type{Name: ddl.Int64}},
+						"bcol2": {Name: "bcol2", T: ddl.Type{Name: ddl.Int64}},
+						"bcol3": {Name: "bcol3", T: ddl.Type{Name: ddl.Int64}},
 					},
-					Fks: []ddl.Foreignkey{ddl.Foreignkey{Name: "fk_test2", Columns: []string{"bcol2", "bcol3"}, ReferTable: "cc", ReferColumns: []string{"ccol1", "ccol2"}}},
+					Fks: []ddl.Foreignkey{{Name: "fk_test2", Columns: []string{"bcol2", "bcol3"}, ReferTable: "cc", ReferColumns: []string{"ccol1", "ccol2"}}},
 				},
-				"cc": ddl.CreateTable{
+				"cc": {
 					Name:     "cc",
 					ColNames: []string{"ccol1", "ccol2", "ccol3"},
 					ColDefs: map[string]ddl.ColumnDef{
-						"ccol1": ddl.ColumnDef{Name: "ccol1", T: ddl.Type{Name: ddl.Int64}},
-						"ccol2": ddl.ColumnDef{Name: "ccol2", T: ddl.Type{Name: ddl.Int64}},
-						"ccol3": ddl.ColumnDef{Name: "ccol3", T: ddl.Type{Name: ddl.Int64}},
+						"ccol1": {Name: "ccol1", T: ddl.Type{Name: ddl.Int64}},
+						"ccol2": {Name: "ccol2", T: ddl.Type{Name: ddl.Int64}},
+						"ccol3": {Name: "ccol3", T: ddl.Type{Name: ddl.Int64}},
 					},
 				},
 			},
@@ -237,43 +237,43 @@ func TestResolveRefs(t *testing.T) {
 		{
 			name: "Column name not found after lower case check",
 			spSchema: map[string]ddl.CreateTable{
-				"cc": ddl.CreateTable{
+				"cc": {
 					Name:     "cc",
 					ColNames: []string{"ccol1", "ccol2", "ccol3"},
 					ColDefs: map[string]ddl.ColumnDef{
-						"ccol1": ddl.ColumnDef{Name: "ccol1", T: ddl.Type{Name: ddl.Int64}},
-						"ccol2": ddl.ColumnDef{Name: "ccol2", T: ddl.Type{Name: ddl.Int64}},
-						"ccol3": ddl.ColumnDef{Name: "ccol3", T: ddl.Type{Name: ddl.Int64}},
+						"ccol1": {Name: "ccol1", T: ddl.Type{Name: ddl.Int64}},
+						"ccol2": {Name: "ccol2", T: ddl.Type{Name: ddl.Int64}},
+						"ccol3": {Name: "ccol3", T: ddl.Type{Name: ddl.Int64}},
 					},
-					Fks: []ddl.Foreignkey{ddl.Foreignkey{Name: "fk_test3", Columns: []string{"ccol2", "ccol3"}, ReferTable: "dd", ReferColumns: []string{"dcol1", "dcol2"}}},
+					Fks: []ddl.Foreignkey{{Name: "fk_test3", Columns: []string{"ccol2", "ccol3"}, ReferTable: "dd", ReferColumns: []string{"dcol1", "dcol2"}}},
 				},
-				"dd": ddl.CreateTable{
+				"dd": {
 					Name:     "dd",
 					ColNames: []string{"dcol1", "ddcol2", "dcol3"},
 					ColDefs: map[string]ddl.ColumnDef{
-						"dcol1":  ddl.ColumnDef{Name: "dcol1", T: ddl.Type{Name: ddl.Int64}},
-						"ddcol2": ddl.ColumnDef{Name: "ddcol2", T: ddl.Type{Name: ddl.Int64}},
-						"dcol3":  ddl.ColumnDef{Name: "dcol3", T: ddl.Type{Name: ddl.Int64}},
+						"dcol1":  {Name: "dcol1", T: ddl.Type{Name: ddl.Int64}},
+						"ddcol2": {Name: "ddcol2", T: ddl.Type{Name: ddl.Int64}},
+						"dcol3":  {Name: "dcol3", T: ddl.Type{Name: ddl.Int64}},
 					},
 				},
 			},
 			expectedSpSchema: map[string]ddl.CreateTable{
-				"cc": ddl.CreateTable{
+				"cc": {
 					Name:     "cc",
 					ColNames: []string{"ccol1", "ccol2", "ccol3"},
 					ColDefs: map[string]ddl.ColumnDef{
-						"ccol1": ddl.ColumnDef{Name: "ccol1", T: ddl.Type{Name: ddl.Int64}},
-						"ccol2": ddl.ColumnDef{Name: "ccol2", T: ddl.Type{Name: ddl.Int64}},
-						"ccol3": ddl.ColumnDef{Name: "ccol3", T: ddl.Type{Name: ddl.Int64}},
+						"ccol1": {Name: "ccol1", T: ddl.Type{Name: ddl.Int64}},
+						"ccol2": {Name: "ccol2", T: ddl.Type{Name: ddl.Int64}},
+						"ccol3": {Name: "ccol3", T: ddl.Type{Name: ddl.Int64}},
 					},
 				},
-				"dd": ddl.CreateTable{
+				"dd": {
 					Name:     "dd",
 					ColNames: []string{"dcol1", "ddcol2", "dcol3"},
 					ColDefs: map[string]ddl.ColumnDef{
-						"dcol1":  ddl.ColumnDef{Name: "dcol1", T: ddl.Type{Name: ddl.Int64}},
-						"ddcol2": ddl.ColumnDef{Name: "ddcol2", T: ddl.Type{Name: ddl.Int64}},
-						"dcol3":  ddl.ColumnDef{Name: "dcol3", T: ddl.Type{Name: ddl.Int64}},
+						"dcol1":  {Name: "dcol1", T: ddl.Type{Name: ddl.Int64}},
+						"ddcol2": {Name: "ddcol2", T: ddl.Type{Name: ddl.Int64}},
+						"dcol3":  {Name: "dcol3", T: ddl.Type{Name: ddl.Int64}},
 					},
 				},
 			},
@@ -282,25 +282,25 @@ func TestResolveRefs(t *testing.T) {
 		{
 			name: "Table name not found after lower case check",
 			spSchema: map[string]ddl.CreateTable{
-				"dd": ddl.CreateTable{
+				"dd": {
 					Name:     "dd",
 					ColNames: []string{"dcol1", "ddcol2", "dcol3"},
 					ColDefs: map[string]ddl.ColumnDef{
-						"dcol1":  ddl.ColumnDef{Name: "dcol1", T: ddl.Type{Name: ddl.Int64}},
-						"ddcol2": ddl.ColumnDef{Name: "ddcol2", T: ddl.Type{Name: ddl.Int64}},
-						"dcol3":  ddl.ColumnDef{Name: "dcol3", T: ddl.Type{Name: ddl.Int64}},
+						"dcol1":  {Name: "dcol1", T: ddl.Type{Name: ddl.Int64}},
+						"ddcol2": {Name: "ddcol2", T: ddl.Type{Name: ddl.Int64}},
+						"dcol3":  {Name: "dcol3", T: ddl.Type{Name: ddl.Int64}},
 					},
-					Fks: []ddl.Foreignkey{ddl.Foreignkey{Name: "fk_test4", Columns: []string{"dcol3"}, ReferTable: "ee", ReferColumns: []string{"ecol1"}}},
+					Fks: []ddl.Foreignkey{{Name: "fk_test4", Columns: []string{"dcol3"}, ReferTable: "ee", ReferColumns: []string{"ecol1"}}},
 				},
 			},
 			expectedSpSchema: map[string]ddl.CreateTable{
-				"dd": ddl.CreateTable{
+				"dd": {
 					Name:     "dd",
 					ColNames: []string{"dcol1", "ddcol2", "dcol3"},
 					ColDefs: map[string]ddl.ColumnDef{
-						"dcol1":  ddl.ColumnDef{Name: "dcol1", T: ddl.Type{Name: ddl.Int64}},
-						"ddcol2": ddl.ColumnDef{Name: "ddcol2", T: ddl.Type{Name: ddl.Int64}},
-						"dcol3":  ddl.ColumnDef{Name: "dcol3", T: ddl.Type{Name: ddl.Int64}},
+						"dcol1":  {Name: "dcol1", T: ddl.Type{Name: ddl.Int64}},
+						"ddcol2": {Name: "ddcol2", T: ddl.Type{Name: ddl.Int64}},
+						"dcol3":  {Name: "dcol3", T: ddl.Type{Name: ddl.Int64}},
 					},
 				},
 			},
diff --git a/mysql/mysqldump_test.go b/mysql/mysqldump_test.go
index 3549621..1215b89 100644
--- a/mysql/mysqldump_test.go
+++ b/mysql/mysqldump_test.go
@@ -831,7 +831,7 @@ func TestProcessMySQLDump_GetDDL(t *testing.T) {
 	c := ddl.Config{Tables: true}
 	// normalizeSpace isn't perfect, but it handles most of the
 	// usual discretionary space issues.
-	assert.Equal(t, normalizeSpace(expected), normalizeSpace(strings.Join(conv.GetDDL(c), " ")))
+	assert.Equal(t, normalizeSpace(expected), normalizeSpace(strings.Join(conv.SpSchema.GetDDL(c), " ")))
 }
 
 func TestProcessMySQLDump_Rows(t *testing.T) {
diff --git a/postgres/pgdump_test.go b/postgres/pgdump_test.go
index 4d34ecf..0425760 100644
--- a/postgres/pgdump_test.go
+++ b/postgres/pgdump_test.go
@@ -707,7 +707,7 @@ func TestProcessPgDump_GetDDL(t *testing.T) {
 	// normalizeSpace isn't perfect, but it handles most of the
 	// usual discretionary space issues.
 	c := ddl.Config{Tables: true}
-	assert.Equal(t, normalizeSpace(expected), normalizeSpace(strings.Join(conv.GetDDL(c), " ")))
+	assert.Equal(t, normalizeSpace(expected), normalizeSpace(strings.Join(conv.SpSchema.GetDDL(c), " ")))
 }
 
 func TestProcessPgDump_Rows(t *testing.T) {
diff --git a/spanner/ddl/ast_test.go b/spanner/ddl/ast_test.go
index f43fda7..d56de6c 100644
--- a/spanner/ddl/ast_test.go
+++ b/spanner/ddl/ast_test.go
@@ -83,7 +83,7 @@ func TestPrintCreateTable(t *testing.T) {
 		"mytable",
 		[]string{"col1", "col2", "col3"},
 		cds,
-		[]IndexKey{IndexKey{Col: "col1", Desc: true}},
+		[]IndexKey{{Col: "col1", Desc: true}},
 		nil,
 		nil,
 		"",
@@ -93,7 +93,7 @@ func TestPrintCreateTable(t *testing.T) {
 		"mytable",
 		[]string{"col1", "col2", "col3"},
 		cds,
-		[]IndexKey{IndexKey{Col: "col1", Desc: true}},
+		[]IndexKey{{Col: "col1", Desc: true}},
 		nil,
 		nil,
 		"parent",
@@ -116,17 +116,17 @@ func TestPrintCreateTable(t *testing.T) {
 
 func TestPrintCreateIndex(t *testing.T) {
 	ci := []CreateIndex{
-		CreateIndex{
+		{
 			"myindex",
 			"mytable",
 			/*Unique =*/ false,
-			[]IndexKey{IndexKey{Col: "col1", Desc: true}, IndexKey{Col: "col2"}},
+			[]IndexKey{{Col: "col1", Desc: true}, {Col: "col2"}},
 		},
-		CreateIndex{
+		{
 			"myindex2",
 			"mytable",
 			/*Unique =*/ true,
-			[]IndexKey{IndexKey{Col: "col1", Desc: true}, IndexKey{Col: "col2"}},
+			[]IndexKey{{Col: "col1", Desc: true}, {Col: "col2"}},
 		}}
 	tests := []struct {
 		name       string
@@ -145,13 +145,13 @@ func TestPrintCreateIndex(t *testing.T) {
 
 func TestPrintForeignKey(t *testing.T) {
 	fk := []Foreignkey{
-		Foreignkey{
+		{
 			"fk_test",
 			[]string{"c1", "c2"},
 			"ref_table",
 			[]string{"ref_c1", "ref_c2"},
 		},
-		Foreignkey{
+		{
 			"",
 			[]string{"c1"},
 			"ref_table",
@@ -174,13 +174,13 @@ func TestPrintForeignKey(t *testing.T) {
 }
 func TestPrintForeignKeyAlterTable(t *testing.T) {
 	fk := []Foreignkey{
-		Foreignkey{
+		{
 			"fk_test",
 			[]string{"c1", "c2"},
 			"ref_table",
 			[]string{"ref_c1", "ref_c2"},
 		},
-		Foreignkey{
+		{
 			"",
 			[]string{"c1"},
 			"ref_table",
@@ -203,6 +203,75 @@ func TestPrintForeignKeyAlterTable(t *testing.T) {
 	}
 }
 
+func TestGetDDL(t *testing.T) {
+	s := NewSchema()
+	s["table1"] = CreateTable{
+		Name:     "table1",
+		ColNames: []string{"a", "b"},
+		ColDefs: map[string]ColumnDef{
+			"a": {Name: "a", T: Type{Name: Int64}},
+			"b": {Name: "b", T: Type{Name: Int64}},
+		},
+		Pks:     []IndexKey{{Col: "a"}},
+		Fks:     []Foreignkey{{Name: "fk1", Columns: []string{"b"}, ReferTable: "ref_table1", ReferColumns: []string{"ref_b"}}},
+		Indexes: []CreateIndex{{Name: "index1", Table: "table1", Unique: false, Keys: []IndexKey{{Col: "b", Desc: false}}}},
+	}
+	s["table2"] = CreateTable{
+		Name:     "table2",
+		ColNames: []string{"a", "b", "c"},
+		ColDefs: map[string]ColumnDef{
+			"a": {Name: "a", T: Type{Name: Int64}},
+			"b": {Name: "b", T: Type{Name: Int64}},
+			"c": {Name: "c", T: Type{Name: Int64}},
+		},
+		Pks:     []IndexKey{{Col: "a"}},
+		Fks:     []Foreignkey{{Name: "fk2", Columns: []string{"b", "c"}, ReferTable: "ref_table2", ReferColumns: []string{"ref_b", "ref_c"}}},
+		Indexes: []CreateIndex{{Name: "index2", Table: "table2", Unique: true, Keys: []IndexKey{{Col: "b", Desc: true}, {Col: "c", Desc: false}}}},
+	}
+	s["table3"] = CreateTable{
+		Name:     "table3",
+		ColNames: []string{"a", "b", "c"},
+		ColDefs: map[string]ColumnDef{
+			"a": ColumnDef{Name: "a", T: Type{Name: Int64}},
+			"b": ColumnDef{Name: "b", T: Type{Name: Int64}},
+			"c": ColumnDef{Name: "c", T: Type{Name: Int64}},
+		},
+		Pks:    []IndexKey{IndexKey{Col: "a"}, IndexKey{Col: "b"}},
+		Fks:    []Foreignkey{Foreignkey{Name: "fk3", Columns: []string{"c"}, ReferTable: "ref_table3", ReferColumns: []string{"ref_c"}}},
+		Parent: "table1",
+	}
+	tablesOnly := s.GetDDL(Config{Tables: true, ForeignKeys: false})
+	e := []string{
+		"CREATE TABLE table1 (\n    a INT64,\n    b INT64 \n) PRIMARY KEY (a)",
+		"CREATE INDEX index1 ON table1 (b)",
+		"CREATE TABLE table2 (\n    a INT64,\n    b INT64,\n    c INT64 \n) PRIMARY KEY (a)",
+		"CREATE UNIQUE INDEX index2 ON table2 (b DESC, c)",
+		"CREATE TABLE table3 (\n    a INT64,\n    b INT64,\n    c INT64 \n) PRIMARY KEY (a, b),\nINTERLEAVE IN PARENT table1",
+	}
+	assert.ElementsMatch(t, e, tablesOnly)
+
+	fksOnly := s.GetDDL(Config{Tables: false, ForeignKeys: true})
+	e2 := []string{
+		"ALTER TABLE table1 ADD CONSTRAINT fk1 FOREIGN KEY (b) REFERENCES ref_table1 (ref_b)",
+		"ALTER TABLE table2 ADD CONSTRAINT fk2 FOREIGN KEY (b, c) REFERENCES ref_table2 (ref_b, ref_c)",
+		"ALTER TABLE table3 ADD CONSTRAINT fk3 FOREIGN KEY (c) REFERENCES ref_table3 (ref_c)",
+	}
+	assert.ElementsMatch(t, e2, fksOnly)
+
+	tablesAndFks := s.GetDDL(Config{Tables: true, ForeignKeys: true})
+	e3 := []string{
+		"CREATE TABLE table1 (\n    a INT64,\n    b INT64 \n) PRIMARY KEY (a)",
+		"CREATE INDEX index1 ON table1 (b)",
+		"CREATE TABLE table2 (\n    a INT64,\n    b INT64,\n    c INT64 \n) PRIMARY KEY (a)",
+		"CREATE UNIQUE INDEX index2 ON table2 (b DESC, c)",
+		"CREATE TABLE table3 (\n    a INT64,\n    b INT64,\n    c INT64 \n) PRIMARY KEY (a, b),\nINTERLEAVE IN PARENT table1",
+		"ALTER TABLE table1 ADD CONSTRAINT fk1 FOREIGN KEY (b) REFERENCES ref_table1 (ref_b)",
+		"ALTER TABLE table2 ADD CONSTRAINT fk2 FOREIGN KEY (b, c) REFERENCES ref_table2 (ref_b, ref_c)",
+		"ALTER TABLE table3 ADD CONSTRAINT fk3 FOREIGN KEY (c) REFERENCES ref_table3 (ref_c)",
+	}
+	assert.ElementsMatch(t, e3, tablesAndFks)
+}
+
 func normalizeSpace(s string) string {
 	// Insert whitespace around parenthesis and commas.
 	s = strings.ReplaceAll(s, ")", " ) ")
diff --git a/test_data/mysqldump.test.out b/test_data/mysqldump.test.out
new file mode 100644
index 0000000..dccebe1
--- /dev/null
+++ b/test_data/mysqldump.test.out
@@ -0,0 +1,79 @@
+-- MySQL dump 10.17  Distrib 10.3.23-MariaDB, for debian-linux-gnu (x86_64)
+--
+-- Host: 127.0.0.1    Database: cart
+-- ------------------------------------------------------
+-- Server version	5.7.25-google-log
+
+/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
+/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
+/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
+/*!40101 SET NAMES utf8mb4 */;
+/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
+/*!40103 SET TIME_ZONE='+00:00' */;
+/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
+/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
+/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
+/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
+
+--
+-- Table structure for table `cart`
+--
+
+DROP TABLE IF EXISTS `cart`;
+/*!40101 SET @saved_cs_client     = @@character_set_client */;
+/*!40101 SET character_set_client = utf8 */;
+CREATE TABLE `cart` (
+  `user_id` varchar(20) NOT NULL,
+  `product_id` varchar(20) NOT NULL,
+  `quantity` bigint(20) DEFAULT NULL,
+  `last_modified` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+  PRIMARY KEY (`user_id`,`product_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+/*!40101 SET character_set_client = @saved_cs_client */;
+
+--
+-- Dumping data for table `cart`
+--
+
+LOCK TABLES `cart` WRITE;
+/*!40000 ALTER TABLE `cart` DISABLE KEYS */;
+INSERT INTO `cart` VALUES ('901e-a6cfc2b502dc','abc-123',1,'2020-07-20 05:10:26'),('901e-a6cfc2b502dc','axd-673',2,'2020-07-20 05:10:43'),('a86b-82493320a775','zxi-631',5,'2020-07-20 05:10:46');
+/*!40000 ALTER TABLE `cart` ENABLE KEYS */;
+UNLOCK TABLES;
+
+--
+-- Table structure for table `products`
+--
+
+DROP TABLE IF EXISTS `products`;
+/*!40101 SET @saved_cs_client     = @@character_set_client */;
+/*!40101 SET character_set_client = utf8 */;
+CREATE TABLE `products` (
+  `product_id` varchar(20) NOT NULL,
+  `description` varchar(1000) DEFAULT NULL,
+  `price` decimal(10,2) DEFAULT NULL,
+  `date_added` date DEFAULT NULL,
+  PRIMARY KEY (`product_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+/*!40101 SET character_set_client = @saved_cs_client */;
+
+--
+-- Dumping data for table `products`
+--
+
+LOCK TABLES `products` WRITE;
+/*!40000 ALTER TABLE `products` DISABLE KEYS */;
+INSERT INTO `products` VALUES ('abc-123','Blue suede shoes',141.99,'2020-06-06'),('axd-673','Antique typewriter',99.99,'2020-06-07'),('zxi-631','Glass vase',55.50,'2020-06-10');
+/*!40000 ALTER TABLE `products` ENABLE KEYS */;
+UNLOCK TABLES;
+/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
+
+/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
+/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
+/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
+/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
+/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
+/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
+/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
+
+-- Dump completed on 2020-08-10 17:40:01
diff --git a/testing/mysql/integration_test.go b/testing/mysql/integration_test.go
new file mode 100644
index 0000000..4606e7a
--- /dev/null
+++ b/testing/mysql/integration_test.go
@@ -0,0 +1,190 @@
+// Copyright 2020 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package mysql_test
+
+import (
+	"context"
+	"flag"
+	"fmt"
+	"io/ioutil"
+	"log"
+	"os"
+	"path/filepath"
+	"testing"
+	"time"
+
+	"github.com/cloudspannerecosystem/harbourbridge/cmd"
+	"github.com/cloudspannerecosystem/harbourbridge/conversion"
+
+	"cloud.google.com/go/spanner"
+	database "cloud.google.com/go/spanner/admin/database/apiv1"
+	"google.golang.org/api/iterator"
+
+	databasepb "google.golang.org/genproto/googleapis/spanner/admin/database/v1"
+)
+
+var (
+	projectID  string
+	instanceID string
+
+	databaseAdmin *database.DatabaseAdminClient
+)
+
+func TestMain(m *testing.M) {
+	cleanup := initIntegrationTests()
+	res := m.Run()
+	cleanup()
+	os.Exit(res)
+}
+
+func initIntegrationTests() (cleanup func()) {
+	projectID = os.Getenv("HARBOURBRIDGE_TESTS_GCLOUD_PROJECT_ID")
+	instanceID = os.Getenv("HARBOURBRIDGE_TESTS_GCLOUD_INSTANCE_ID")
+
+	ctx := context.Background()
+	flag.Parse() // Needed for testing.Short().
+	noop := func() {}
+
+	if testing.Short() {
+		log.Println("Integration tests skipped in -short mode.")
+		return noop
+	}
+
+	if projectID == "" {
+		log.Println("Integration tests skipped: HARBOURBRIDGE_TESTS_GCLOUD_PROJECT_ID is missing")
+		return noop
+	}
+
+	if instanceID == "" {
+		log.Println("Integration tests skipped: HARBOURBRIDGE_TESTS_GCLOUD_INSTANCE_ID is missing")
+		return noop
+	}
+
+	var err error
+	databaseAdmin, err = database.NewDatabaseAdminClient(ctx)
+	if err != nil {
+		log.Fatalf("cannot create databaseAdmin client: %v", err)
+	}
+
+	return func() {
+		databaseAdmin.Close()
+	}
+}
+
+func dropDatabase(t *testing.T, dbPath string) {
+	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
+	defer cancel()
+	// Drop the testing database.
+	if err := databaseAdmin.DropDatabase(ctx, &databasepb.DropDatabaseRequest{Database: dbPath}); err != nil {
+		t.Fatalf("failed to drop testing database %v: %v", dbPath, err)
+	}
+}
+
+func prepareIntegrationTest(t *testing.T) string {
+	if databaseAdmin == nil {
+		t.Skip("Integration tests skipped")
+	}
+	tmpdir, err := ioutil.TempDir(".", "int-test-")
+	if err != nil {
+		log.Fatal(err)
+	}
+	return tmpdir
+}
+
+func TestIntegration_MYSQLDUMP_SimpleUse(t *testing.T) {
+	t.Parallel()
+
+	tmpdir := prepareIntegrationTest(t)
+	defer os.RemoveAll(tmpdir)
+
+	now := time.Now()
+	dbName, _ := conversion.GetDatabaseName(conversion.MYSQLDUMP, now)
+	dbPath := fmt.Sprintf("projects/%s/instances/%s/databases/%s", projectID, instanceID, dbName)
+	dataFilepath := "../../test_data/mysqldump.test.out"
+	filePrefix := filepath.Join(tmpdir, dbName+".")
+	f, err := os.Open(dataFilepath)
+	if err != nil {
+		t.Fatalf("failed to open the test data file: %v", err)
+	}
+	err = cmd.CommandLine(conversion.MYSQLDUMP, projectID, instanceID, dbName, false, false, 0, "", &conversion.IOStreams{In: f, Out: os.Stdout}, filePrefix, now)
+	if err != nil {
+		t.Fatal(err)
+	}
+	// Drop the database later.
+	defer dropDatabase(t, dbPath)
+
+	checkResults(t, dbPath)
+}
+
+func TestIntegration_MYSQL_SimpleUse(t *testing.T) {
+	onlyRunForEmulatorTest(t)
+	t.Parallel()
+
+	tmpdir := prepareIntegrationTest(t)
+	defer os.RemoveAll(tmpdir)
+
+	now := time.Now()
+	dbName, _ := conversion.GetDatabaseName(conversion.MYSQL, now)
+	dbPath := fmt.Sprintf("projects/%s/instances/%s/databases/%s", projectID, instanceID, dbName)
+	filePrefix := filepath.Join(tmpdir, dbName+".")
+
+	err := cmd.CommandLine(conversion.MYSQL, projectID, instanceID, dbName, false, false, 0, "", &conversion.IOStreams{Out: os.Stdout}, filePrefix, now)
+	if err != nil {
+		t.Fatal(err)
+	}
+	// Drop the database later.
+	defer dropDatabase(t, dbPath)
+
+	checkResults(t, dbPath)
+}
+
+func checkResults(t *testing.T, dbPath string) {
+	// Make a query to check results.
+	ctx := context.Background()
+	client, err := spanner.NewClient(ctx, dbPath)
+	if err != nil {
+		log.Fatal(err)
+	}
+	defer client.Close()
+
+	checkBigInt(ctx, t, client)
+}
+
+func checkBigInt(ctx context.Context, t *testing.T, client *spanner.Client) {
+	var quantity int64
+	iter := client.Single().Read(ctx, "cart", spanner.Key{"901e-a6cfc2b502dc", "abc-123"}, []string{"quantity"})
+	defer iter.Stop()
+	for {
+		row, err := iter.Next()
+		if err == iterator.Done {
+			break
+		}
+		if err != nil {
+			t.Fatal(err)
+		}
+		if err := row.Columns(&quantity); err != nil {
+			t.Fatal(err)
+		}
+	}
+	if got, want := quantity, int64(1); got != want {
+		t.Fatalf("quantities are not correct: got %v, want %v", got, want)
+	}
+}
+
+func onlyRunForEmulatorTest(t *testing.T) {
+	if os.Getenv("SPANNER_EMULATOR_HOST") == "" {
+		t.Skip("Skipping tests only running against the emulator.")
+	}
+}
diff --git a/integration_test.go b/testing/postgres/integration_test.go
similarity index 91%
rename from integration_test.go
rename to testing/postgres/integration_test.go
index f4bb89a..536704f 100644
--- a/integration_test.go
+++ b/testing/postgres/integration_test.go
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package main
+package postgres_test
 
 import (
 	"bytes"
@@ -31,7 +31,7 @@ import (
 
 	"cloud.google.com/go/spanner"
 	database "cloud.google.com/go/spanner/admin/database/apiv1"
-	instance "cloud.google.com/go/spanner/admin/instance/apiv1"
+	"github.com/cloudspannerecosystem/harbourbridge/cmd"
 	"github.com/cloudspannerecosystem/harbourbridge/conversion"
 	"google.golang.org/api/iterator"
 	databasepb "google.golang.org/genproto/googleapis/spanner/admin/database/v1"
@@ -41,7 +41,6 @@ var (
 	projectID  string
 	instanceID string
 
-	instanceAdmin *instance.InstanceAdminClient
 	databaseAdmin *database.DatabaseAdminClient
 )
 
@@ -95,17 +94,6 @@ func dropDatabase(t *testing.T, dbPath string) {
 	}
 }
 
-func cleanupFiles(t *testing.T, files []string) {
-	for _, file := range files {
-		if _, err := os.Stat(file); err == nil {
-			err = os.Remove(file)
-			if err != nil {
-				t.Errorf("failed to delete file: %v", file)
-			}
-		}
-	}
-}
-
 func prepareIntegrationTest(t *testing.T) string {
 	if databaseAdmin == nil {
 		t.Skip("Integration tests skipped")
@@ -126,13 +114,13 @@ func TestIntegration_PGDUMP_SimpleUse(t *testing.T) {
 	now := time.Now()
 	dbName, _ := conversion.GetDatabaseName(conversion.PGDUMP, now)
 	dbPath := fmt.Sprintf("projects/%s/instances/%s/databases/%s", projectID, instanceID, dbName)
-	dataFilepath := "test_data/pg_dump.test.out"
-	filePrefix = filepath.Join(tmpdir, dbName+".")
+	dataFilepath := "../../test_data/pg_dump.test.out"
+	filePrefix := filepath.Join(tmpdir, dbName+".")
 	f, err := os.Open(dataFilepath)
 	if err != nil {
 		t.Fatalf("failed to open the test data file: %v", err)
 	}
-	err = commandLine(conversion.PGDUMP, projectID, instanceID, dbName, &conversion.IOStreams{In: f, Out: os.Stdout}, filePrefix, now)
+	err = cmd.CommandLine(conversion.PGDUMP, projectID, instanceID, dbName, false, false, 0, "", &conversion.IOStreams{In: f, Out: os.Stdout}, filePrefix, now)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -152,8 +140,8 @@ func TestIntegration_PGDUMP_Command(t *testing.T) {
 	dbName, _ := conversion.GetDatabaseName(conversion.PGDUMP, now)
 	dbPath := fmt.Sprintf("projects/%s/instances/%s/databases/%s", projectID, instanceID, dbName)
 
-	dataFilepath := "test_data/pg_dump.test.out"
-	filePrefix = filepath.Join(tmpdir, dbName+".")
+	dataFilepath := "../../test_data/pg_dump.test.out"
+	filePrefix := filepath.Join(tmpdir, dbName+".")
 	// Be aware that when testing with the command, the time `now` might be
 	// different between file prefixes and the contents in the files. This
 	// is because file prefixes use `now` from here (the test function) and
@@ -187,9 +175,9 @@ func TestIntegration_POSTGRES_SimpleUse(t *testing.T) {
 	now := time.Now()
 	dbName, _ := conversion.GetDatabaseName(conversion.POSTGRES, now)
 	dbPath := fmt.Sprintf("projects/%s/instances/%s/databases/%s", projectID, instanceID, dbName)
-	filePrefix = filepath.Join(tmpdir, dbName+".")
+	filePrefix := filepath.Join(tmpdir, dbName+".")
 
-	err := commandLine(conversion.POSTGRES, projectID, instanceID, dbName, &conversion.IOStreams{Out: os.Stdout}, filePrefix, now)
+	err := cmd.CommandLine(conversion.POSTGRES, projectID, instanceID, dbName, false, false, 0, "", &conversion.IOStreams{Out: os.Stdout}, filePrefix, now)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -209,7 +197,7 @@ func TestIntegration_POSTGRES_Command(t *testing.T) {
 	now := time.Now()
 	dbName, _ := conversion.GetDatabaseName(conversion.POSTGRES, now)
 	dbPath := fmt.Sprintf("projects/%s/instances/%s/databases/%s", projectID, instanceID, dbName)
-	filePrefix = filepath.Join(tmpdir, dbName+".")
+	filePrefix := filepath.Join(tmpdir, dbName+".")
 
 	cmd := exec.Command("bash", "-c", fmt.Sprintf("go run github.com/cloudspannerecosystem/harbourbridge -instance %s -dbname %s -prefix %s -driver %s", instanceID, dbName, filePrefix, conversion.POSTGRES))
 	var out, stderr bytes.Buffer