Repository: cloudspannerecosystem/harbourbridge
Pull request #93 (2020-12-12 06:03:10)

feature: web-api
> This PR is for the new feature: Web APIs, changes included in this PR:
> *`web` - added a new package which includes all APIs and tests related to it.
> *`conversion` - added a new package to duplicate some functions from `main.go` which needs to be used in web apis. they are duplicated so that main flow doesn't get affected.  
> * `main.go` - added a new command line flag `-w` to start the web server. command to start the server : `go run main.go --web`
> * `report.go` - We need to make few of the functions and struct variables public as they need to be accessed from web apis.

> This PR is completed except README
> 
> 1. MySQL
>    
>      * [x]  Handle mysql direct connection and mysqldump.
>      * [x]  Tests for mysql APIs

> 2. PostgreSQL
>        
>      * [x]  Handle postgres direct connection and pg_dump.
>      * [x]  Tests for postgres APIs.
> 3. Interleave tabels
>
>       * [x]  Support for interleave tables 
>       * [x]  Tests for interleave tables 
> * [x]  Update README



agasheesh: One meta level comment I have here is that it's really much easier to review a  pull request when divided into smaller chunks and get those reviewed and submit quickly. You can then rebase branches corresponding to those pull requests when updating for comments.

One possible split would be:

1. PR for adding support for interleave in backend code.
2. PR for defining routes.go
3. PR for pure refactoring work
4. PR for web API implementation.

You may be able to think of a better split. Also think about what packages should be defined and send out pull requests for each package separately with corresponding unit tests.

I understand that this may be difficult to do now for this current PR but please consider doing so if you think it would make it easy for you to address comments.
google-cla[bot]: We found a Contributor License Agreement for you (the sender of this pull request), but were unable to find agreements for all the commit author(s) or Co-authors.  If you authored these, maybe you used a different email address in the git commits than was used to sign the CLA ([login here](https://cla.developers.google.com/) to double check)?  If these were authored by someone else, then they will need to sign a CLA as well, and confirm that they're okay with these being contributed to Google.
In order to pass this check, please resolve this problem and then comment `@googlebot I fixed it.`. If the bot doesn't comment, it means it doesn't think anything has changed.

ℹ️ **Googlers: [Go here](https://goto.google.com/prinfo/https%3A%2F%2Fgithub.com%2Fcloudspannerecosystem%2Fharbourbridge%2Fpull%2F93) for more info**.

<!-- need_author_cla -->
google-cla[bot]: We found a Contributor License Agreement for you (the sender of this pull request), but were unable to find agreements for all the commit author(s) or Co-authors.  If you authored these, maybe you used a different email address in the git commits than was used to sign the CLA ([login here](https://cla.developers.google.com/) to double check)?  If these were authored by someone else, then they will need to sign a CLA as well, and confirm that they're okay with these being contributed to Google.
In order to pass this check, please resolve this problem and then comment `@googlebot I fixed it.`. If the bot doesn't comment, it means it doesn't think anything has changed.

ℹ️ **Googlers: [Go here](https://goto.google.com/prinfo/https%3A%2F%2Fgithub.com%2Fcloudspannerecosystem%2Fharbourbridge%2Fpull%2F93) for more info**.

<!-- need_author_cla -->
google-cla[bot]: We found a Contributor License Agreement for you (the sender of this pull request), but were unable to find agreements for all the commit author(s) or Co-authors.  If you authored these, maybe you used a different email address in the git commits than was used to sign the CLA ([login here](https://cla.developers.google.com/) to double check)?  If these were authored by someone else, then they will need to sign a CLA as well, and confirm that they're okay with these being contributed to Google.
In order to pass this check, please resolve this problem and then comment `@googlebot I fixed it.`. If the bot doesn't comment, it means it doesn't think anything has changed.

ℹ️ **Googlers: [Go here](https://goto.google.com/prinfo/https%3A%2F%2Fgithub.com%2Fcloudspannerecosystem%2Fharbourbridge%2Fpull%2F93) for more info**.

<!-- need_author_cla -->
sivamforeverd: @googlebot I fixed it.
google-cla[bot]: All (the pull request submitter and all commit authors) CLAs are signed, **but** one or more commits were authored or co-authored by someone other than the pull request submitter.

We need to confirm that all authors are ok with their commits being contributed to this project.  Please have them confirm that by leaving a comment that contains only `@googlebot I consent.` in this pull request.

*Note to project maintainer:* There may be cases where the author cannot leave a comment, or the comment is not properly detected as consent.  In those cases, you can manually confirm consent of the commit author(s), and set the `cla` label to `yes` (if enabled on your project).

ℹ️ **Googlers: [Go here](https://goto.google.com/prinfo/https%3A%2F%2Fgithub.com%2Fcloudspannerecosystem%2Fharbourbridge%2Fpull%2F93) for more info**.

<!-- need_author_consent -->
sivamforeverd: @googlebot I consent.
nevinheintze: Thanks for all of the changes. I think this is ready to merge. I see merge is blocked on an old review. Asheesh -- can you pls take a quick look and resolve that review? Thanks!
agasheesh: We found some issues with the integraton_test.go not passing. It's run as part of the presubmits. Thus, please hold on to submitting the PR for a little while we uncover and resolve the issue.
nevinheintze: Let's go ahead and submit this -- we've tracked down the issue and it has nothing to do with this PR. Sorry for the false alarm!




Patch:
diff --git a/conversion/conversion.go b/conversion/conversion.go
new file mode 100644
index 0000000..c6ffe81
--- /dev/null
+++ b/conversion/conversion.go
@@ -0,0 +1,847 @@
+// 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 conversion handles initial setup for the command line tool
+// and web APIs.
+
+// TODO:(searce) Organize code in go style format to make this file more readable.
+// 			public constants first
+// 			key public type definitions next (although often it makes sense to put them next to public functions that use them)
+// 			then public functions (and relevant type definitions)
+// 			and helper functions and other non-public definitions last (generally in order of importance)
+
+package conversion
+
+import (
+	"bufio"
+	"context"
+	"crypto/rand"
+	"database/sql"
+	"encoding/json"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"log"
+	"os"
+	"os/exec"
+	"strings"
+	"sync/atomic"
+	"syscall"
+	"time"
+
+	sp "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/aws/aws-sdk-go/aws/session"
+	dydb "github.com/aws/aws-sdk-go/service/dynamodb"
+	_ "github.com/go-sql-driver/mysql"
+	_ "github.com/lib/pq"
+	"golang.org/x/crypto/ssh/terminal"
+	"google.golang.org/api/iterator"
+	adminpb "google.golang.org/genproto/googleapis/spanner/admin/database/v1"
+	instancepb "google.golang.org/genproto/googleapis/spanner/admin/instance/v1"
+
+	"github.com/cloudspannerecosystem/harbourbridge/dynamodb"
+	"github.com/cloudspannerecosystem/harbourbridge/internal"
+	"github.com/cloudspannerecosystem/harbourbridge/mysql"
+	"github.com/cloudspannerecosystem/harbourbridge/postgres"
+	"github.com/cloudspannerecosystem/harbourbridge/spanner"
+	"github.com/cloudspannerecosystem/harbourbridge/spanner/ddl"
+)
+
+const (
+	// PGDUMP is the driver name for pg_dump.
+	PGDUMP string = "pg_dump"
+	// POSTGRES is the driver name for PostgreSQL.
+	POSTGRES string = "postgres"
+	// MYSQLDUMP is the driver name for mysqldump.
+	MYSQLDUMP string = "mysqldump"
+	// MYSQL is the driver name for MySQL.
+	MYSQL string = "mysql"
+	// DYNAMODB is the driver name for AWS DynamoDB.
+	// This is an experimental driver; implementation in progress.
+	DYNAMODB string = "dynamodb"
+)
+
+func SchemaConv(driver string, ioHelper *IOStreams, schemaSampleSize int64) (*internal.Conv, error) {
+	switch driver {
+	case POSTGRES, MYSQL:
+		return schemaFromSQL(driver)
+	case PGDUMP, MYSQLDUMP:
+		return schemaFromDump(driver, ioHelper)
+	case DYNAMODB:
+		return schemaFromDynamoDB(schemaSampleSize)
+	default:
+		return nil, fmt.Errorf("schema conversion for driver %s not supported", driver)
+	}
+}
+
+// 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,
+		WriteLimit: 40,
+		RetryLimit: 1000,
+		Verbose:    internal.Verbose(),
+	}
+	switch driver {
+	case POSTGRES, MYSQL:
+		return dataFromSQL(driver, config, client, conv)
+	case PGDUMP, MYSQLDUMP:
+		if checkInterleaved(conv) {
+			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)
+	case DYNAMODB:
+		return dataFromDynamoDB(config, client, conv)
+	default:
+		return nil, fmt.Errorf("data conversion for driver %s not supported", driver)
+	}
+}
+
+func driverConfig(driver string) (string, error) {
+	switch driver {
+	case POSTGRES:
+		return pgDriverConfig()
+	case MYSQL:
+		return mysqlDriverConfig()
+	default:
+		return "", fmt.Errorf("Driver %s not supported", driver)
+	}
+}
+
+func pgDriverConfig() (string, error) {
+	server := os.Getenv("PGHOST")
+	port := os.Getenv("PGPORT")
+	user := os.Getenv("PGUSER")
+	dbname := os.Getenv("PGDATABASE")
+	if server == "" || port == "" || user == "" || dbname == "" {
+		fmt.Printf("Please specify host, port, user and database using PGHOST, PGPORT, PGUSER and PGDATABASE environment variables\n")
+		return "", fmt.Errorf("Could not connect to source database")
+	}
+	password := os.Getenv("PGPASSWORD")
+	if password == "" {
+		password = getPassword()
+	}
+	return fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable", server, port, user, password, dbname), nil
+}
+
+func mysqlDriverConfig() (string, error) {
+	server := os.Getenv("MYSQLHOST")
+	port := os.Getenv("MYSQLPORT")
+	user := os.Getenv("MYSQLUSER")
+	dbname := os.Getenv("MYSQLDATABASE")
+	if server == "" || port == "" || user == "" || dbname == "" {
+		fmt.Printf("Please specify host, port, user and database using MYSQLHOST, MYSQLPORT, MYSQLUSER and MYSQLDATABASE environment variables\n")
+		return "", fmt.Errorf("Could not connect to source database")
+	}
+	password := os.Getenv("MYSQLPWD")
+	if password == "" {
+		password = getPassword()
+	}
+	return fmt.Sprintf("%s:%s@tcp(%s:%s)/%s", user, password, server, port, dbname), nil
+}
+
+func schemaFromSQL(driver string) (*internal.Conv, error) {
+	driverConfig, err := driverConfig(driver)
+	if err != nil {
+		return nil, err
+	}
+	sourceDB, err := sql.Open(driver, driverConfig)
+	if err != nil {
+		return nil, err
+	}
+	conv := internal.MakeConv()
+	err = ProcessInfoSchema(driver, conv, sourceDB)
+	if err != nil {
+		return nil, err
+	}
+	return conv, nil
+}
+
+func dataFromSQL(driver string, config spanner.BatchWriterConfig, client *sp.Client, conv *internal.Conv) (*spanner.BatchWriter, error) {
+	// TODO: Refactor to avoid redundant calls to driverConfig and
+	// Open in schemaFromSQL and dataFromSQL. Also refactor to
+	// share code with dataFromPgDump. Use single transaction for
+	// reading schema and data from source db to get consistent
+	// dump.
+	driverConfig, err := driverConfig(driver)
+	if err != nil {
+		return nil, err
+	}
+	sourceDB, err := sql.Open(driver, driverConfig)
+	if err != nil {
+		return nil, err
+	}
+	err = SetRowStats(driver, conv, sourceDB)
+	if err != nil {
+		return nil, err
+	}
+	totalRows := conv.Rows()
+	p := internal.NewProgress(totalRows, "Writing data to Spanner", internal.Verbose())
+	rows := int64(0)
+	config.Write = func(m []*sp.Mutation) error {
+		_, err := client.Apply(context.Background(), m)
+		if err != nil {
+			return err
+		}
+		atomic.AddInt64(&rows, int64(len(m)))
+		p.MaybeReport(atomic.LoadInt64(&rows))
+		return nil
+	}
+	writer := spanner.NewBatchWriter(config)
+	conv.SetDataMode()
+	conv.SetDataSink(
+		func(table string, cols []string, vals []interface{}) {
+			writer.AddRow(table, cols, vals)
+		})
+	err = ProcessSQLData(driver, conv, sourceDB)
+	if err != nil {
+		return nil, err
+	}
+	writer.Flush()
+	return writer, nil
+}
+
+func schemaFromDynamoDB(sampleSize int64) (*internal.Conv, error) {
+	conv := internal.MakeConv()
+	mySession := session.Must(session.NewSession())
+	client := dydb.New(mySession)
+	err := dynamodb.ProcessSchema(conv, client, []string{}, sampleSize)
+	if err != nil {
+		return nil, err
+	}
+	return conv, nil
+}
+
+func dataFromDynamoDB(config spanner.BatchWriterConfig, client *sp.Client, conv *internal.Conv) (*spanner.BatchWriter, error) {
+	mySession := session.Must(session.NewSession())
+	dyclient := dydb.New(mySession)
+
+	dynamodb.SetRowStats(conv, dyclient)
+	totalRows := conv.Rows()
+	p := internal.NewProgress(totalRows, "Writing data to Spanner", internal.Verbose())
+
+	rows := int64(0)
+	config.Write = func(m []*sp.Mutation) error {
+		_, err := client.Apply(context.Background(), m)
+		if err != nil {
+			return err
+		}
+		atomic.AddInt64(&rows, int64(len(m)))
+		p.MaybeReport(atomic.LoadInt64(&rows))
+		return nil
+	}
+	writer := spanner.NewBatchWriter(config)
+	conv.SetDataMode()
+	conv.SetDataSink(
+		func(table string, cols []string, vals []interface{}) {
+			writer.AddRow(table, cols, vals)
+		})
+
+	err := dynamodb.ProcessData(conv, dyclient)
+	if err != nil {
+		return nil, err
+	}
+	writer.Flush()
+	return writer, nil
+}
+
+type IOStreams struct {
+	In, SeekableIn, Out *os.File
+	BytesRead           int64
+}
+
+func schemaFromDump(driver string, ioHelper *IOStreams) (*internal.Conv, error) {
+	f, n, err := getSeekable(ioHelper.In)
+	if err != nil {
+		printSeekError(driver, err, ioHelper.Out)
+		return nil, fmt.Errorf("can't get seekable input file")
+	}
+	ioHelper.SeekableIn = f
+	ioHelper.BytesRead = n
+	conv := internal.MakeConv()
+	p := internal.NewProgress(n, "Generating schema", internal.Verbose())
+	r := internal.NewReader(bufio.NewReader(f), p)
+	conv.SetSchemaMode() // Build schema and ignore data in dump.
+	conv.SetDataSink(nil)
+	err = ProcessDump(driver, conv, r)
+	if err != nil {
+		fmt.Fprintf(ioHelper.Out, "Failed to parse the data file: %v", err)
+		return nil, fmt.Errorf("failed to parse the data file")
+	}
+	p.Done()
+	return conv, nil
+}
+
+func dataFromDump(driver string, config spanner.BatchWriterConfig, ioHelper *IOStreams, client *sp.Client, conv *internal.Conv, dataOnly bool) (*spanner.BatchWriter, error) {
+	// TODO: refactor of the way we handle getSeekable
+	// to avoid the code duplication here
+	if !dataOnly {
+		_, err := ioHelper.SeekableIn.Seek(0, 0)
+		if err != nil {
+			fmt.Printf("\nCan't seek to start of file (preparation for second pass): %v\n", err)
+			return nil, fmt.Errorf("can't seek to start of file")
+		}
+	} else {
+		// Note: input file is kept seekable to plan for future
+		// changes in showing progress for data migration.
+		f, n, err := getSeekable(ioHelper.In)
+		if err != nil {
+			printSeekError(driver, err, ioHelper.Out)
+			return nil, fmt.Errorf("can't get seekable input file")
+		}
+		ioHelper.SeekableIn = f
+		ioHelper.BytesRead = n
+	}
+	totalRows := conv.Rows()
+
+	p := internal.NewProgress(totalRows, "Writing data to Spanner", internal.Verbose())
+	r := internal.NewReader(bufio.NewReader(ioHelper.SeekableIn), nil)
+	rows := int64(0)
+	config.Write = func(m []*sp.Mutation) error {
+		_, err := client.Apply(context.Background(), m)
+		if err != nil {
+			return err
+		}
+		atomic.AddInt64(&rows, int64(len(m)))
+		p.MaybeReport(atomic.LoadInt64(&rows))
+		return nil
+	}
+	writer := spanner.NewBatchWriter(config)
+	conv.SetDataMode() // Process data in dump; schema is unchanged.
+	conv.SetDataSink(
+		func(table string, cols []string, vals []interface{}) {
+			writer.AddRow(table, cols, vals)
+		})
+	ProcessDump(driver, conv, r)
+	writer.Flush()
+	p.Done()
+
+	return writer, nil
+}
+
+// Report generates a report of schema and data conversion.
+func Report(driver string, badWrites map[string]int64, BytesRead int64, banner string, conv *internal.Conv, reportFileName string, out *os.File) {
+	f, err := os.Create(reportFileName)
+	if err != nil {
+		fmt.Fprintf(out, "Can't write out report file %s: %v\n", reportFileName, err)
+		fmt.Fprintf(out, "Writing report to stdout\n")
+		f = out
+	} else {
+		defer f.Close()
+	}
+	w := bufio.NewWriter(f)
+	w.WriteString(banner)
+
+	summary := internal.GenerateReport(driver, conv, w, badWrites, true, true)
+	w.Flush()
+	var isDump bool
+	if strings.Contains(driver, "dump") {
+		isDump = true
+	}
+	if isDump {
+		fmt.Fprintf(out, "Processed %d bytes of %s data (%d statements, %d rows of data, %d errors, %d unexpected conditions).\n",
+			BytesRead, driver, conv.Statements(), conv.Rows(), conv.StatementErrors(), conv.Unexpecteds())
+	} else {
+		fmt.Fprintf(out, "Processed source database via %s driver (%d rows of data, %d unexpected conditions).\n",
+			driver, conv.Rows(), conv.Unexpecteds())
+	}
+	// We've already written summary to f (as part of GenerateReport).
+	// In the case where f is stdout, don't write a duplicate copy.
+	if f != out {
+		fmt.Fprint(out, summary)
+		fmt.Fprintf(out, "See file '%s' for details of the schema and data conversions.\n", reportFileName)
+	}
+}
+
+// getSeekable returns a seekable file (with same content as f) and the size of the content (in bytes).
+func getSeekable(f *os.File) (*os.File, int64, error) {
+	_, err := f.Seek(0, 0)
+	if err == nil { // Stdin is seekable, let's just use that. This happens when you run 'cmd < file'.
+		n, err := getSize(f)
+		return f, n, err
+	}
+	internal.VerbosePrintln("Creating a tmp file with a copy of stdin because stdin is not seekable.")
+
+	// Create file in os.TempDir. Its not clear this is a good idea e.g. if the
+	// pg_dump/mysqldump output is large (tens of GBs) and os.TempDir points to a directory
+	// (such as /tmp) that's configured with a small amount of disk space.
+	// To workaround such limits on Unix, set $TMPDIR to a directory with lots
+	// of disk space.
+	fcopy, err := ioutil.TempFile("", "harbourbridge.data")
+	if err != nil {
+		return nil, 0, err
+	}
+	syscall.Unlink(fcopy.Name()) // File will be deleted when this process exits.
+	_, err = io.Copy(fcopy, f)
+	if err != nil {
+		return nil, 0, fmt.Errorf("can't write stdin to tmp file: %w", err)
+	}
+	_, err = fcopy.Seek(0, 0)
+	if err != nil {
+		return nil, 0, fmt.Errorf("can't reset file offset: %w", err)
+	}
+	n, err := getSize(fcopy)
+	return fcopy, n, nil
+}
+
+// CreateDatabase returns a newly create Spanner DB.
+// It automatically determines an appropriate project, selects a
+// Spanner instance to use, generates a new Spanner DB name,
+// and call into the Spanner admin interface to create the new DB.
+func CreateDatabase(project, instance, dbName string, conv *internal.Conv, out *os.File) (string, error) {
+	fmt.Fprintf(out, "Creating new database %s in instance %s with default permissions ... ", dbName, instance)
+	ctx := context.Background()
+	adminClient, err := database.NewDatabaseAdminClient(ctx)
+	if err != nil {
+		return "", fmt.Errorf("can't create admin client: %w", analyzeError(err, project, instance))
+	}
+	defer adminClient.Close()
+	// 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})
+	op, err := adminClient.CreateDatabase(ctx, &adminpb.CreateDatabaseRequest{
+		Parent:          fmt.Sprintf("projects/%s/instances/%s", project, instance),
+		CreateStatement: "CREATE DATABASE `" + dbName + "`",
+		ExtraStatements: schema,
+	})
+	if err != nil {
+		return "", fmt.Errorf("can't build CreateDatabaseRequest: %w", analyzeError(err, project, instance))
+	}
+	if _, err := op.Wait(ctx); err != nil {
+		return "", fmt.Errorf("createDatabase call failed: %w", analyzeError(err, project, instance))
+	}
+	fmt.Fprintf(out, "done.\n")
+	return fmt.Sprintf("projects/%s/instances/%s/databases/%s", project, instance, dbName), nil
+}
+
+// UpdateDDLForeignKeys updates the Spanner database with foreign key
+// constraints using ALTER TABLE statements.
+func UpdateDDLForeignKeys(project, instance, dbName string, conv *internal.Conv, out *os.File) error {
+	ctx := context.Background()
+	adminClient, err := database.NewDatabaseAdminClient(ctx)
+	if err != nil {
+		return fmt.Errorf("can't create admin client: %w\n", analyzeError(err, project, instance))
+	}
+	defer adminClient.Close()
+	// 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})
+	if len(fkStmts) == 0 {
+		return nil
+	}
+	msg := fmt.Sprintf("Updating schema of database %s in instance %s with foreign key constraints ...", dbName, instance)
+	p := internal.NewProgress(int64(len(fkStmts)), msg, internal.Verbose())
+	for i, fkStmt := range fkStmts {
+		// TODO: Improve performance of the foreign key constraint updates.
+		// For example, issue all of the update ops first before waiting for them to complete
+		// so that that can execute in parallel. We could also print out ids of the
+		// long-running operations.
+		op, err := adminClient.UpdateDatabaseDdl(ctx, &adminpb.UpdateDatabaseDdlRequest{
+			Database:   fmt.Sprintf("projects/%s/instances/%s/databases/%s", project, instance, dbName),
+			Statements: []string{fkStmt},
+		})
+		if err != nil {
+			fmt.Printf("Can't add foreign key with statement %s: %s\n", fkStmt, err)
+			conv.Unexpected(fmt.Sprintf("Can't add foreign key with statement %s: %s", fkStmt, err))
+			continue
+		}
+		if err := op.Wait(ctx); err != nil {
+			fmt.Printf("Can't add foreign key with statement %s: %s\n", fkStmt, err)
+			conv.Unexpected(fmt.Sprintf("Can't add foreign key with statement %s: %s", fkStmt, err))
+		}
+		internal.VerbosePrintln("Updated schema with statement: " + fkStmt)
+		p.MaybeReport(int64(i + 1))
+	}
+	p.Done()
+	return nil
+}
+
+// GetProject returns the cloud project we should use for accessing Spanner.
+// Use environment variable GCLOUD_PROJECT if it is set.
+// Otherwise, use the default project returned from gcloud.
+func GetProject() (string, error) {
+	project := os.Getenv("GCLOUD_PROJECT")
+	if project != "" {
+		return project, nil
+	}
+	cmd := exec.Command("gcloud", "config", "list", "--format", "value(core.project)")
+	out, err := cmd.CombinedOutput()
+	if err != nil {
+		return "", fmt.Errorf("call to gcloud to get project failed: %w", err)
+	}
+	project = strings.TrimSpace(string(out))
+	return project, nil
+}
+
+// GetInstance returns the Spanner instance we should use for creating DBs.
+// If the user specified instance (via flag 'instance') then use that.
+// Otherwise try to deduce the instance using gcloud.
+func GetInstance(project string, out *os.File) (string, error) {
+	l, err := getInstances(project)
+	if err != nil {
+		return "", err
+	}
+	if len(l) == 0 {
+		fmt.Fprintf(out, "Could not find any Spanner instances for project %s\n", project)
+		return "", fmt.Errorf("no Spanner instances for %s", project)
+	}
+	// Note: we could ask for user input to select/confirm which Spanner
+	// instance to use, but that interacts poorly with piping pg_dump/mysqldump data
+	// to the tool via stdin.
+	if len(l) == 1 {
+		fmt.Fprintf(out, "Using only available Spanner instance: %s\n", l[0])
+		return l[0], nil
+	}
+	fmt.Fprintf(out, "Available Spanner instances:\n")
+	for i, x := range l {
+		fmt.Fprintf(out, " %d) %s\n", i+1, x)
+	}
+	fmt.Fprintf(out, "Please pick one of the available instances and set the flag '--instance'\n\n")
+	return "", fmt.Errorf("auto-selection of instance failed: project %s has more than one Spanner instance. "+
+		"Please use the flag '--instance' to select an instance", project)
+}
+
+func getInstances(project string) ([]string, error) {
+	ctx := context.Background()
+	instanceClient, err := instance.NewInstanceAdminClient(ctx)
+	if err != nil {
+		return nil, analyzeError(err, project, "")
+	}
+	it := instanceClient.ListInstances(ctx, &instancepb.ListInstancesRequest{Parent: fmt.Sprintf("projects/%s", project)})
+	var l []string
+	for {
+		resp, err := it.Next()
+		if err == iterator.Done {
+			break
+		}
+		if err != nil {
+			return nil, analyzeError(err, project, "")
+		}
+		l = append(l, strings.TrimPrefix(resp.Name, fmt.Sprintf("projects/%s/instances/", project)))
+	}
+	return l, nil
+}
+
+// WriteSchemaFile writes DDL statements in a file. It includes CREATE TABLE
+// statements and ALTER TABLE statements to add foreign keys.
+func WriteSchemaFile(conv *internal.Conv, now time.Time, name string, out *os.File) {
+	f, err := os.Create(name)
+	if err != nil {
+		fmt.Fprintf(out, "Can't create schema file %s: %v\n", name, err)
+		return
+	}
+	// The schema file we write out includes comments, includes foreign keys
+	// and doesn't add backticks around table and column names. This file is
+	// intended for explanatory and documentation purposes, and is not strictly
+	// 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})
+	if len(ddl) == 0 {
+		ddl = []string{"\n-- Schema is empty -- no tables found\n"}
+	}
+	l := []string{
+		fmt.Sprintf("-- Schema generated %s\n", now.Format("2006-01-02 15:04:05")),
+		strings.Join(ddl, ";\n\n"),
+		"\n",
+	}
+	if _, err := f.WriteString(strings.Join(l, "")); err != nil {
+		fmt.Fprintf(out, "Can't write out schema file: %v\n", err)
+		return
+	}
+	fmt.Fprintf(out, "Wrote schema to file '%s'.\n", name)
+}
+
+// WriteSessionFile writes conv struct to a file in JSON format.
+func WriteSessionFile(conv *internal.Conv, name string, out *os.File) {
+	f, err := os.Create(name)
+	if err != nil {
+		fmt.Fprintf(out, "Can't create session file %s: %v\n", name, err)
+		return
+	}
+	// Session file will basically contain 'conv' struct in JSON format.
+	// It contains all the information for schema and data conversion state.
+	convJSON, err := json.MarshalIndent(conv, "", " ")
+	if err != nil {
+		fmt.Fprintf(out, "Can't encode session state to JSON: %v\n", err)
+		return
+	}
+	if _, err := f.Write(convJSON); err != nil {
+		fmt.Fprintf(out, "Can't write out session file: %v\n", err)
+		return
+	}
+	fmt.Fprintf(out, "Wrote session to file '%s'.\n", name)
+}
+
+// ReadSessionFile reads a session JSON file and
+// unmarshal it's content into *internal.Conv.
+func ReadSessionFile(conv *internal.Conv, sessionJSON string) error {
+	s, err := ioutil.ReadFile(sessionJSON)
+	if err != nil {
+		return err
+	}
+	err = json.Unmarshal(s, &conv)
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+// WriteBadData prints summary stats about bad rows and writes detailed info
+// to file 'name'.
+func WriteBadData(bw *spanner.BatchWriter, conv *internal.Conv, banner, name string, out *os.File) {
+	badConversions := conv.BadRows()
+	badWrites := sum(bw.DroppedRowsByTable())
+	if badConversions == 0 && badWrites == 0 {
+		os.Remove(name) // Cleanup bad-data file from previous run.
+		return
+	}
+	f, err := os.Create(name)
+	if err != nil {
+		fmt.Fprintf(out, "Can't write out bad data file: %v\n", err)
+		return
+	}
+	f.WriteString(banner)
+	maxRows := 100
+	if badConversions > 0 {
+		l := conv.SampleBadRows(maxRows)
+		if int64(len(l)) < badConversions {
+			f.WriteString("A sample of rows that generated conversion errors:\n")
+		} else {
+			f.WriteString("Rows that generated conversion errors:\n")
+		}
+		for _, r := range l {
+			_, err := f.WriteString("  " + r + "\n")
+			if err != nil {
+				fmt.Fprintf(out, "Can't write out bad data file: %v\n", err)
+				return
+			}
+		}
+	}
+	if badWrites > 0 {
+		l := bw.SampleBadRows(maxRows)
+		if int64(len(l)) < badWrites {
+			f.WriteString("A sample of rows that successfully converted but couldn't be written to Spanner:\n")
+		} else {
+			f.WriteString("Rows that successfully converted but couldn't be written to Spanner:\n")
+		}
+		for _, r := range l {
+			_, err := f.WriteString("  " + r + "\n")
+			if err != nil {
+				fmt.Fprintf(out, "Can't write out bad data file: %v\n", err)
+				return
+			}
+		}
+	}
+	fmt.Fprintf(out, "See file '%s' for details of bad rows\n", name)
+}
+
+// GetDatabaseName generates database name with driver_date prefix.
+func GetDatabaseName(driver string, now time.Time) (string, error) {
+	return generateName(fmt.Sprintf("%s_%s", driver, now.Format("2006-01-02")))
+}
+
+func getPassword() string {
+	fmt.Print("Enter Password: ")
+	bytePassword, err := terminal.ReadPassword(int(syscall.Stdin))
+	if err != nil {
+		fmt.Println("\nCoudln't read password")
+		return ""
+	}
+	fmt.Printf("\n")
+	return strings.TrimSpace(string(bytePassword))
+}
+
+// analyzeError inspects an error returned from Cloud Spanner and adds information
+// about potential root causes e.g. authentication issues.
+func analyzeError(err error, project, instance string) error {
+	e := strings.ToLower(err.Error())
+	if containsAny(e, []string{"unauthenticated", "cannot fetch token", "default credentials"}) {
+		return fmt.Errorf("%w.\n"+`
+Possible cause: credentials are mis-configured. Do you need to run
+
+  gcloud auth application-default login
+
+or configure environment variable GOOGLE_APPLICATION_CREDENTIALS.
+See https://cloud.google.com/docs/authentication/getting-started.
+`, err)
+	}
+	if containsAny(e, []string{"instance not found"}) && instance != "" {
+		return fmt.Errorf("%w.\n"+`
+Possible cause: Spanner instance specified via instance option does not exist.
+Please check that '%s' is correct and that it is a valid Spanner
+instance for project %s.
+`, err, instance, project)
+	}
+	return err
+}
+
+// PrintPermissionsWarning prints permission warning.
+func PrintPermissionsWarning(driver string, out *os.File) {
+	fmt.Fprintf(out,
+		`
+WARNING: Please check that permissions for this Spanner instance are
+appropriate. Spanner manages access control at the database level, and the
+database created by HarbourBridge will inherit default permissions from this
+instance. All data written to Spanner will be visible to anyone who can
+access the created database. Note that `+driver+` table-level and row-level
+ACLs are dropped during conversion since they are not supported by Spanner.
+
+`)
+}
+
+func printSeekError(driver string, err error, out *os.File) {
+	fmt.Fprintf(out, "\nCan't get seekable input file: %v\n", err)
+	fmt.Fprintf(out, "Likely cause: not enough space in %s.\n", os.TempDir())
+	fmt.Fprintf(out, "Try writing "+driver+" output to a file first i.e.\n")
+	fmt.Fprintf(out, " "+driver+" > tmpfile\n")
+	fmt.Fprintf(out, "  harbourbridge < tmpfile\n")
+}
+
+func containsAny(s string, l []string) bool {
+	for _, a := range l {
+		if strings.Contains(s, a) {
+			return true
+		}
+	}
+	return false
+}
+
+func generateName(prefix string) (string, error) {
+	b := make([]byte, 4)
+	_, err := rand.Read(b)
+	if err != nil {
+		return "", fmt.Errorf("error generating name: %w", err)
+
+	}
+	return fmt.Sprintf("%s_%x-%x", prefix, b[0:2], b[2:4]), nil
+}
+
+// GetClient returns new spanner client.
+func GetClient(db string) (*sp.Client, error) {
+	ctx := context.Background()
+	return sp.NewClient(ctx, db)
+}
+
+func getSize(f *os.File) (int64, error) {
+	info, err := f.Stat()
+	if err != nil {
+		return 0, fmt.Errorf("can't stat file: %w", err)
+	}
+	return info.Size(), nil
+}
+
+// SetupLogFile configures the file used for logs.
+// By default we just drop logs on the floor. To enable them (e.g. to debug
+// Cloud Spanner client library issues), set logfile to a non-empty filename.
+// Note: this tool itself doesn't generate logs, but some of the libraries it
+// uses do. If we don't set the log file, we see a number of unhelpful and
+// unactionable logs spamming stdout, which is annoying and confusing.
+func SetupLogFile() (*os.File, error) {
+	// To enable debug logs, set logfile to a non-empty filename.
+	logfile := ""
+	if logfile == "" {
+		log.SetOutput(ioutil.Discard)
+		return nil, nil
+	}
+	f, err := os.Create(logfile)
+	if err != nil {
+		return nil, err
+	}
+	log.SetOutput(f)
+	return f, nil
+}
+
+// Close closes file.
+func Close(f *os.File) {
+	if f != nil {
+		f.Close()
+	}
+}
+
+func sum(m map[string]int64) int64 {
+	n := int64(0)
+	for _, c := range m {
+		n += c
+	}
+	return n
+}
+
+// GetBanner prints banner message after command line process is finished.
+func GetBanner(now time.Time, db string) string {
+	return fmt.Sprintf("Generated at %s for db %s\n\n", now.Format("2006-01-02 15:04:05"), db)
+}
+
+// ProcessDump invokes process dump function from a sql package based on driver selected.
+func ProcessDump(driver string, conv *internal.Conv, r *internal.Reader) error {
+	switch driver {
+	case MYSQLDUMP:
+		return mysql.ProcessMySQLDump(conv, r)
+	case PGDUMP:
+		return postgres.ProcessPgDump(conv, r)
+	default:
+		return fmt.Errorf("process dump for driver %s not supported", driver)
+	}
+}
+
+// ProcessInfoSchema invokes process infoschema function from a sql package based on driver selected.
+func ProcessInfoSchema(driver string, conv *internal.Conv, db *sql.DB) error {
+	switch driver {
+	case MYSQL:
+		return mysql.ProcessInfoSchema(conv, db, os.Getenv("MYSQLDATABASE"))
+	case POSTGRES:
+		return postgres.ProcessInfoSchema(conv, db)
+	default:
+		return fmt.Errorf("schema conversion for driver %s not supported", driver)
+	}
+}
+
+// SetRowStats invokes SetRowStats function from a sql package based on driver selected.
+func SetRowStats(driver string, conv *internal.Conv, db *sql.DB) error {
+	switch driver {
+	case MYSQL:
+		mysql.SetRowStats(conv, db, os.Getenv("MYSQLDATABASE"))
+	case POSTGRES:
+		postgres.SetRowStats(conv, db)
+	default:
+		return fmt.Errorf("Could get rows stats for '%s' driver", driver)
+	}
+	return nil
+}
+
+// ProcessSQLData invokes ProcessSQLData function from a sql package based on driver selected.
+func ProcessSQLData(driver string, conv *internal.Conv, db *sql.DB) error {
+	switch driver {
+	case MYSQL:
+		mysql.ProcessSQLData(conv, db, os.Getenv("MYSQLDATABASE"))
+	case POSTGRES:
+		postgres.ProcessSQLData(conv, db)
+	default:
+		return fmt.Errorf("Data conversion for driver %s is not supported", driver)
+	}
+	return nil
+}
diff --git a/internal/convert.go b/internal/convert.go
index 1e4e876..56af3f6 100644
--- a/internal/convert.go
+++ b/internal/convert.go
@@ -169,7 +169,9 @@ func (conv *Conv) SetDataMode() {
 }
 
 // GetDDL Schema returns the Spanner schema that has been constructed so far.
-// Return DDL in alphabetical table order.
+// 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
@@ -179,10 +181,29 @@ func (conv *Conv) GetDDL(c ddl.Config) []string {
 	sort.Strings(tables)
 	var ddl []string
 	if c.Tables {
-		for _, t := range tables {
-			ddl = append(ddl, conv.SpSchema[t].PrintCreateTable(c))
-			for _, index := range conv.SpSchema[t].Indexes {
-				ddl = append(ddl, index.PrintCreateIndex(c))
+		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)
 			}
 		}
 	}
diff --git a/internal/report.go b/internal/report.go
index accc56a..af7dc07 100644
--- a/internal/report.go
+++ b/internal/report.go
@@ -26,12 +26,12 @@ import (
 
 // GenerateReport analyzes schema and data conversion stats and writes a
 // detailed report to w and returns a brief summary (as a string).
-func GenerateReport(driverName string, conv *Conv, w *bufio.Writer, badWrites map[string]int64) string {
-	reports := analyzeTables(conv, badWrites)
-	summary := generateSummary(conv, reports, badWrites)
+func GenerateReport(driverName string, conv *Conv, w *bufio.Writer, badWrites map[string]int64, printTableReports bool, printUnexpecteds bool) string {
+	reports := AnalyzeTables(conv, badWrites)
+	summary := GenerateSummary(conv, reports, badWrites)
 	writeHeading(w, "Summary of Conversion")
 	w.WriteString(summary)
-	ignored := ignoredStatements(conv)
+	ignored := IgnoredStatements(conv)
 	w.WriteString("\n")
 	if len(ignored) > 0 {
 		justifyLines(w, fmt.Sprintf("Note that the following source DB statements "+
@@ -56,43 +56,47 @@ func GenerateReport(driverName string, conv *Conv, w *bufio.Writer, badWrites ma
 	if isDump {
 		writeStmtStats(driverName, conv, w)
 	}
-	for _, t := range reports {
-		h := fmt.Sprintf("Table %s", t.srcTable)
-		if t.srcTable != t.spTable {
-			h = h + fmt.Sprintf(" (mapped to Spanner table %s)", t.spTable)
-		}
-		writeHeading(w, h)
-		w.WriteString(rateConversion(t.rows, t.badRows, t.cols, t.warnings, t.syntheticPKey != "", false, conv.SchemaMode()))
-		w.WriteString("\n")
-		for _, x := range t.body {
-			fmt.Fprintf(w, "%s\n", x.heading)
-			for i, l := range x.lines {
-				justifyLines(w, fmt.Sprintf("%d) %s.\n", i+1, l), 80, 3)
+	if printTableReports {
+		for _, t := range reports {
+			h := fmt.Sprintf("Table %s", t.SrcTable)
+			if t.SrcTable != t.SpTable {
+				h = h + fmt.Sprintf(" (mapped to Spanner table %s)", t.SpTable)
 			}
+			writeHeading(w, h)
+			w.WriteString(rateConversion(t.rows, t.badRows, t.Cols, t.Warnings, t.SyntheticPKey != "", false, conv.SchemaMode()))
 			w.WriteString("\n")
+			for _, x := range t.Body {
+				fmt.Fprintf(w, "%s\n", x.Heading)
+				for i, l := range x.Lines {
+					justifyLines(w, fmt.Sprintf("%d) %s.\n", i+1, l), 80, 3)
+				}
+				w.WriteString("\n")
+			}
 		}
 	}
-	writeUnexpectedConditions(driverName, conv, w)
+	if printUnexpecteds {
+		writeUnexpectedConditions(driverName, conv, w)
+	}
 	return summary
 }
 
 type tableReport struct {
-	srcTable      string
-	spTable       string
+	SrcTable      string
+	SpTable       string
 	rows          int64
 	badRows       int64
-	cols          int64
-	warnings      int64
-	syntheticPKey string // Empty string means no synthetic primary key was needed.
-	body          []tableReportBody
+	Cols          int64
+	Warnings      int64
+	SyntheticPKey string // Empty string means no synthetic primary key was needed.
+	Body          []tableReportBody
 }
 
 type tableReportBody struct {
-	heading string
-	lines   []string
+	Heading string
+	Lines   []string
 }
 
-func analyzeTables(conv *Conv, badWrites map[string]int64) (r []tableReport) {
+func AnalyzeTables(conv *Conv, badWrites map[string]int64) (r []tableReport) {
 	// Process tables in alphabetical order. This ensures that tables
 	// appear in alphabetical order in report.txt.
 	var tables []string
@@ -110,21 +114,21 @@ func buildTableReport(conv *Conv, srcTable string, badWrites map[string]int64) t
 	spTable, err := GetSpannerTable(conv, srcTable)
 	srcSchema, ok1 := conv.SrcSchema[srcTable]
 	spSchema, ok2 := conv.SpSchema[spTable]
-	tr := tableReport{srcTable: srcTable, spTable: spTable}
+	tr := tableReport{SrcTable: srcTable, SpTable: spTable}
 	if err != nil || !ok1 || !ok2 {
 		m := "bad source-DB-to-Spanner table mapping or Spanner schema"
 		conv.Unexpected("report: " + m)
-		tr.body = []tableReportBody{tableReportBody{heading: "Internal error: " + m}}
+		tr.Body = []tableReportBody{tableReportBody{Heading: "Internal error: " + m}}
 		return tr
 	}
 	issues, cols, warnings := analyzeCols(conv, srcTable, spTable)
-	tr.cols = cols
-	tr.warnings = warnings
+	tr.Cols = cols
+	tr.Warnings = warnings
 	if pk, ok := conv.SyntheticPKeys[spTable]; ok {
-		tr.syntheticPKey = pk.Col
-		tr.body = buildTableReportBody(conv, srcTable, issues, spSchema, srcSchema, &pk.Col)
+		tr.SyntheticPKey = pk.Col
+		tr.Body = buildTableReportBody(conv, srcTable, issues, spSchema, srcSchema, &pk.Col)
 	} else {
-		tr.body = buildTableReportBody(conv, srcTable, issues, spSchema, srcSchema, nil)
+		tr.Body = buildTableReportBody(conv, srcTable, issues, spSchema, srcSchema, nil)
 	}
 	if !conv.SchemaMode() {
 		fillRowStats(conv, srcTable, badWrites, &tr)
@@ -159,10 +163,10 @@ func buildTableReportBody(conv *Conv, srcTable string, issues map[string][]Schem
 		issueBatcher := make(map[SchemaIssue]bool)
 		for _, srcCol := range cols {
 			for _, i := range issues[srcCol] {
-				if issueDB[i].severity != p.severity {
+				if IssueDB[i].severity != p.severity {
 					continue
 				}
-				if issueDB[i].batch {
+				if IssueDB[i].batch {
 					if issueBatcher[i] {
 						// Have already reported a previous instance of this
 						// (batched) issue, so skip this one.
@@ -187,20 +191,20 @@ func buildTableReportBody(conv *Conv, srcTable string, issues map[string][]Schem
 				spType = strings.ToLower(spType)
 				switch i {
 				case DefaultValue:
-					l = append(l, fmt.Sprintf("%s e.g. column '%s'", issueDB[i].brief, srcCol))
+					l = append(l, fmt.Sprintf("%s e.g. column '%s'", IssueDB[i].Brief, srcCol))
 				case ForeignKey:
 					l = append(l, fmt.Sprintf("Column '%s' uses foreign keys which HarbourBridge does not support yet", srcCol))
 				case AutoIncrement:
-					l = append(l, fmt.Sprintf("Column '%s' is an autoincrement column. %s", srcCol, issueDB[i].brief))
+					l = append(l, fmt.Sprintf("Column '%s' is an autoincrement column. %s", srcCol, IssueDB[i].Brief))
 				case Timestamp:
 					// Avoid the confusing "timestamp is mapped to timestamp" message.
-					l = append(l, fmt.Sprintf("Some columns have source DB type 'timestamp without timezone' which is mapped to Spanner type timestamp e.g. column '%s'. %s", srcCol, issueDB[i].brief))
+					l = append(l, fmt.Sprintf("Some columns have source DB type 'timestamp without timezone' which is mapped to Spanner type timestamp e.g. column '%s'. %s", srcCol, IssueDB[i].Brief))
 				case Datetime:
-					l = append(l, fmt.Sprintf("Some columns have source DB type 'datetime' which is mapped to Spanner type timestamp e.g. column '%s'. %s", srcCol, issueDB[i].brief))
+					l = append(l, fmt.Sprintf("Some columns have source DB type 'datetime' which is mapped to Spanner type timestamp e.g. column '%s'. %s", srcCol, IssueDB[i].Brief))
 				case Widened:
-					l = append(l, fmt.Sprintf("%s e.g. for column '%s', source DB type %s is mapped to Spanner type %s", issueDB[i].brief, srcCol, srcType, spType))
+					l = append(l, fmt.Sprintf("%s e.g. for column '%s', source DB type %s is mapped to Spanner type %s", IssueDB[i].Brief, srcCol, srcType, spType))
 				default:
-					l = append(l, fmt.Sprintf("Column '%s': type %s is mapped to %s. %s", srcCol, srcType, spType, issueDB[i].brief))
+					l = append(l, fmt.Sprintf("Column '%s': type %s is mapped to %s. %s", srcCol, srcType, spType, IssueDB[i].Brief))
 				}
 			}
 		}
@@ -211,7 +215,7 @@ func buildTableReportBody(conv *Conv, srcTable string, issues map[string][]Schem
 		if len(l) > 1 {
 			heading = heading + "s"
 		}
-		body = append(body, tableReportBody{heading: heading, lines: l})
+		body = append(body, tableReportBody{Heading: heading, Lines: l})
 	}
 	return body
 }
@@ -241,25 +245,25 @@ func fillRowStats(conv *Conv, srcTable string, badWrites map[string]int64, tr *t
 // for assessing warnings, and we give only the first instance in the report.
 // TODO: add links in these descriptions to further documentation
 // e.g. for timestamp description.
-var issueDB = map[SchemaIssue]struct {
-	brief    string // Short description of issue.
+var IssueDB = map[SchemaIssue]struct {
+	Brief    string // Short description of issue.
 	severity severity
 	batch    bool // Whether multiple instances of this issue are combined.
 }{
-	DefaultValue:          {brief: "Some columns have default values which Spanner does not support", severity: warning, batch: true},
-	ForeignKey:            {brief: "Spanner does not support foreign keys", severity: warning},
-	MultiDimensionalArray: {brief: "Spanner doesn't support multi-dimensional arrays", severity: warning},
-	NoGoodType:            {brief: "No appropriate Spanner type", severity: warning},
-	Numeric:               {brief: "Spanner does not support numeric. This type mapping could lose precision and is not recommended for production use", severity: warning},
-	NumericThatFits:       {brief: "Spanner does not support numeric, but this type mapping preserves the numeric's specified precision", severity: note},
-	Decimal:               {brief: "Spanner does not support decimal. This type mapping could lose precision and is not recommended for production use", severity: warning},
-	DecimalThatFits:       {brief: "Spanner does not support decimal, but this type mapping preserves the decimal's specified precision", severity: note},
-	Serial:                {brief: "Spanner does not support autoincrementing types", severity: warning},
-	AutoIncrement:         {brief: "Spanner does not support auto_increment attribute", severity: warning},
-	Timestamp:             {brief: "Spanner timestamp is closer to PostgreSQL timestamptz", severity: note, batch: true},
-	Datetime:              {brief: "Spanner timestamp is closer to MySQL timestamp", severity: note, batch: true},
-	Time:                  {brief: "Spanner does not support time/year types", severity: note, batch: true},
-	Widened:               {brief: "Some columns will consume more storage in Spanner", severity: note, batch: true},
+	DefaultValue:          {Brief: "Some columns have default values which Spanner does not support", severity: warning, batch: true},
+	ForeignKey:            {Brief: "Spanner does not support foreign keys", severity: warning},
+	MultiDimensionalArray: {Brief: "Spanner doesn't support multi-dimensional arrays", severity: warning},
+	NoGoodType:            {Brief: "No appropriate Spanner type", severity: warning},
+	Numeric:               {Brief: "Spanner does not support numeric. This type mapping could lose precision and is not recommended for production use", severity: warning},
+	NumericThatFits:       {Brief: "Spanner does not support numeric, but this type mapping preserves the numeric's specified precision", severity: note},
+	Decimal:               {Brief: "Spanner does not support decimal. This type mapping could lose precision and is not recommended for production use", severity: warning},
+	DecimalThatFits:       {Brief: "Spanner does not support decimal, but this type mapping preserves the decimal's specified precision", severity: note},
+	Serial:                {Brief: "Spanner does not support autoincrementing types", severity: warning},
+	AutoIncrement:         {Brief: "Spanner does not support auto_increment attribute", severity: warning},
+	Timestamp:             {Brief: "Spanner timestamp is closer to PostgreSQL timestamptz", severity: note, batch: true},
+	Datetime:              {Brief: "Spanner timestamp is closer to MySQL timestamp", severity: note, batch: true},
+	Time:                  {Brief: "Spanner does not support time/year types", severity: note, batch: true},
+	Widened:               {Brief: "Some columns will consume more storage in Spanner", severity: note, batch: true},
 }
 
 type severity int
@@ -285,9 +289,9 @@ func analyzeCols(conv *Conv, srcTable, spTable string) (map[string][]SchemaIssue
 		m[c] = l
 		for _, i := range l {
 			switch {
-			case issueDB[i].severity == warning && issueDB[i].batch:
+			case IssueDB[i].severity == warning && IssueDB[i].batch:
 				warningBatcher[i] = true
-			case issueDB[i].severity == warning && !issueDB[i].batch:
+			case IssueDB[i].severity == warning && !IssueDB[i].batch:
 				colWarning = true
 			}
 		}
@@ -365,7 +369,7 @@ func rateConversion(rows, badRows, cols, warnings int64, missingPKey, summary bo
 	return rate
 }
 
-func generateSummary(conv *Conv, r []tableReport, badWrites map[string]int64) string {
+func GenerateSummary(conv *Conv, r []tableReport, badWrites map[string]int64) string {
 	cols := int64(0)
 	warnings := int64(0)
 	missingPKey := false
@@ -374,9 +378,9 @@ func generateSummary(conv *Conv, r []tableReport, badWrites map[string]int64) st
 		if weight == 0 { // Tables without data count as if they had one row.
 			weight = 1
 		}
-		cols += t.cols * weight
-		warnings += t.warnings * weight
-		if t.syntheticPKey != "" {
+		cols += t.Cols * weight
+		warnings += t.Warnings * weight
+		if t.SyntheticPKey != "" {
 			missingPKey = true
 		}
 	}
@@ -393,7 +397,7 @@ func generateSummary(conv *Conv, r []tableReport, badWrites map[string]int64) st
 	return rateConversion(rows, badRows, cols, warnings, missingPKey, true, conv.SchemaMode())
 }
 
-func ignoredStatements(conv *Conv) (l []string) {
+func IgnoredStatements(conv *Conv) (l []string) {
 	for s := range conv.Stats.Statement {
 		switch s {
 		case "CreateFunctionStmt":
diff --git a/main.go b/main.go
index 1ca91e1..e587051 100644
--- a/main.go
+++ b/main.go
@@ -18,55 +18,17 @@
 package main
 
 import (
-	"bufio"
-	"context"
-	"crypto/rand"
-	"database/sql"
-	"encoding/json"
 	"flag"
 	"fmt"
-	"io"
-	"io/ioutil"
-	"log"
 	"os"
-	"os/exec"
-	"strings"
-	"sync/atomic"
-	"syscall"
 	"time"
 
-	sp "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/aws/aws-sdk-go/aws/session"
-	dydb "github.com/aws/aws-sdk-go/service/dynamodb"
 	_ "github.com/go-sql-driver/mysql"
 	_ "github.com/lib/pq"
-	"golang.org/x/crypto/ssh/terminal"
-	"google.golang.org/api/iterator"
-	adminpb "google.golang.org/genproto/googleapis/spanner/admin/database/v1"
-	instancepb "google.golang.org/genproto/googleapis/spanner/admin/instance/v1"
 
-	"github.com/cloudspannerecosystem/harbourbridge/dynamodb"
+	"github.com/cloudspannerecosystem/harbourbridge/conversion"
 	"github.com/cloudspannerecosystem/harbourbridge/internal"
-	"github.com/cloudspannerecosystem/harbourbridge/mysql"
-	"github.com/cloudspannerecosystem/harbourbridge/postgres"
-	"github.com/cloudspannerecosystem/harbourbridge/spanner"
-	"github.com/cloudspannerecosystem/harbourbridge/spanner/ddl"
-)
-
-const (
-	// PGDUMP is the driver name for pg_dump.
-	PGDUMP string = "pg_dump"
-	// POSTGRES is the driver name for PostgreSQL.
-	POSTGRES string = "postgres"
-	// MYSQLDUMP is the driver name for mysqldump.
-	MYSQLDUMP string = "mysqldump"
-	// MYSQL is the driver name for MySQL.
-	MYSQL string = "mysql"
-	// DYNAMODB is the driver name for AWS DynamoDB.
-	// This is an experimental driver; implementation in progress.
-	DYNAMODB string = "dynamodb"
+	"github.com/cloudspannerecosystem/harbourbridge/web"
 )
 
 var (
@@ -77,12 +39,13 @@ var (
 	dbNameOverride   string
 	instanceOverride string
 	filePrefix       = ""
-	driverName       = PGDUMP
+	driverName       = conversion.PGDUMP
 	schemaSampleSize = int64(0)
 	verbose          bool
 	schemaOnly       bool
 	dataOnly         bool
 	sessionJSON      string
+	webapi           bool
 )
 
 func init() {
@@ -95,6 +58,7 @@ func init() {
 	flag.BoolVar(&schemaOnly, "schema-only", false, "schema-only: in this mode we do schema conversion, but skip data conversion")
 	flag.BoolVar(&dataOnly, "data-only", false, "data-only: in this mode we skip schema conversion and just do data conversion (use the session flag to specify the session file for schema and data mapping)")
 	flag.StringVar(&sessionJSON, "session", "", "session: specifies the file we restore session state from (used in schema-only to provide schema and data mapping)")
+	flag.BoolVar(&webapi, "web", false, "web: run the web interface (experimental)")
 }
 
 func usage() {
@@ -110,19 +74,23 @@ Sample usage:
 func main() {
 	flag.Usage = usage
 	flag.Parse()
+	if webapi {
+		web.WebApp()
+		return
+	}
 	internal.VerboseInit(verbose)
-	lf, err := setupLogFile()
+	lf, err := conversion.SetupLogFile()
 	if err != nil {
 		fmt.Printf("\nCan't set up log file: %v\n", err)
 		panic(fmt.Errorf("can't set up log file"))
 	}
-	defer close(lf)
+	defer conversion.Close(lf)
 
-	ioHelper := &ioStreams{in: os.Stdin, out: os.Stdout}
+	ioHelper := &conversion.IOStreams{In: os.Stdin, Out: os.Stdout}
 	fmt.Println("Using driver (source DB):", driverName)
 	var project, instance string
 	if !schemaOnly {
-		project, err = getProject()
+		project, err = conversion.GetProject()
 		if err != nil {
 			fmt.Printf("\nCan't get project: %v\n", err)
 			panic(fmt.Errorf("can't get project"))
@@ -131,20 +99,20 @@ func main() {
 
 		instance = instanceOverride
 		if instance == "" {
-			instance, err = getInstance(project, ioHelper.out)
+			instance, err = conversion.GetInstance(project, ioHelper.Out)
 			if err != nil {
 				fmt.Printf("\nCan't get instance: %v\n", err)
 				panic(fmt.Errorf("can't get instance"))
 			}
 		}
 		fmt.Println("Using Cloud Spanner instance:", instance)
-		printPermissionsWarning(driverName, ioHelper.out)
+		conversion.PrintPermissionsWarning(driverName, ioHelper.Out)
 	}
 
 	now := time.Now()
 	dbName := dbNameOverride
 	if dbName == "" {
-		dbName, err = getDatabaseName(driverName, now)
+		dbName, err = conversion.GetDatabaseName(driverName, now)
 		if err != nil {
 			fmt.Printf("\nCan't get database name: %v\n", err)
 			panic(fmt.Errorf("can't get database name"))
@@ -164,820 +132,69 @@ func main() {
 		panic(fmt.Errorf("when using data-only mode, the session must specify the session file to use"))
 	}
 
-	err = toSpanner(driverName, project, instance, dbName, ioHelper, filePrefix, now)
+	err = commandLine(driverName, project, instance, dbName, ioHelper, filePrefix, now)
 	if err != nil {
 		panic(err)
 	}
 
 }
 
-// toSpanner is the main entrance of the entire conversion, which runs the
-// following steps:
-//   1. Run schema conversion
-//   2. Create database
-//   3. Run data conversion
-//   4. Generate report
-func toSpanner(driver, projectID, instanceID, dbName string, ioHelper *ioStreams, outputFilePrefix string, now time.Time) error {
+// 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 = schemaConv(driver, ioHelper)
+		conv, err = conversion.SchemaConv(driver, ioHelper, schemaSampleSize)
 		if err != nil {
 			return err
 		}
-		if ioHelper.seekableIn != nil {
-			defer ioHelper.in.Close()
+		if ioHelper.SeekableIn != nil {
+			defer ioHelper.In.Close()
 		}
 
-		writeSchemaFile(conv, now, outputFilePrefix+schemaFile, ioHelper.out)
-		writeSessionFile(conv, now, outputFilePrefix+sessionFile, ioHelper.out)
+		conversion.WriteSchemaFile(conv, now, outputFilePrefix+schemaFile, ioHelper.Out)
+		conversion.WriteSessionFile(conv, outputFilePrefix+sessionFile, ioHelper.Out)
 		if schemaOnly {
-			report(driver, nil, ioHelper.bytesRead, "", conv, outputFilePrefix+reportFile, ioHelper.out)
+			conversion.Report(driver, nil, ioHelper.BytesRead, "", conv, outputFilePrefix+reportFile, ioHelper.Out)
 			return nil
 		}
 	} else {
 		conv = internal.MakeConv()
-		err = readSessionFile(conv)
+		err = conversion.ReadSessionFile(conv, sessionJSON)
 		if err != nil {
 			return err
 		}
 	}
 
-	db, err := createDatabase(projectID, instanceID, dbName, conv, ioHelper.out)
+	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 := getClient(db)
+	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 := dataConv(driver, ioHelper, client, conv)
+	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 = updateDDLForeignKeys(projectID, instanceID, dbName, conv, ioHelper.out); err != nil {
+	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 := getBanner(now, db)
-	report(driver, bw.DroppedRowsByTable(), ioHelper.bytesRead, banner, conv, outputFilePrefix+reportFile, ioHelper.out)
-	writeBadData(bw, conv, banner, outputFilePrefix+badDataFile, ioHelper.out)
-	return nil
-}
-
-func schemaConv(driver string, ioHelper *ioStreams) (*internal.Conv, error) {
-	switch driver {
-	case POSTGRES, MYSQL:
-		return schemaFromSQL(driver)
-	case PGDUMP, MYSQLDUMP:
-		return schemaFromDump(driver, ioHelper)
-	case DYNAMODB:
-		return schemaFromDynamoDB(schemaSampleSize)
-	default:
-		return nil, fmt.Errorf("schema conversion for driver %s not supported", driver)
-	}
-}
-
-func dataConv(driver string, ioHelper *ioStreams, client *sp.Client, conv *internal.Conv) (*spanner.BatchWriter, error) {
-	config := spanner.BatchWriterConfig{
-		BytesLimit: 100 * 1000 * 1000,
-		WriteLimit: 40,
-		RetryLimit: 1000,
-		Verbose:    internal.Verbose(),
-	}
-	switch driver {
-	case POSTGRES, MYSQL:
-		return dataFromSQL(driver, config, client, conv)
-	case PGDUMP, MYSQLDUMP:
-		return dataFromDump(driver, config, ioHelper, client, conv)
-	case DYNAMODB:
-		return dataFromDynamoDB(config, client, conv)
-	default:
-		return nil, fmt.Errorf("data conversion for driver %s not supported", driver)
-	}
-}
-
-func driverConfig(driver string) (string, error) {
-	switch driver {
-	case POSTGRES:
-		return pgDriverConfig()
-	case MYSQL:
-		return mysqlDriverConfig()
-	default:
-		return "", fmt.Errorf("Driver %s not supported", driver)
-	}
-}
-
-func pgDriverConfig() (string, error) {
-	server := os.Getenv("PGHOST")
-	port := os.Getenv("PGPORT")
-	user := os.Getenv("PGUSER")
-	dbname := os.Getenv("PGDATABASE")
-	if server == "" || port == "" || user == "" || dbname == "" {
-		fmt.Printf("Please specify host, port, user and database using PGHOST, PGPORT, PGUSER and PGDATABASE environment variables\n")
-		return "", fmt.Errorf("Could not connect to source database")
-	}
-	password := os.Getenv("PGPASSWORD")
-	if password == "" {
-		password = getPassword()
-	}
-	return fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable", server, port, user, password, dbname), nil
-}
-
-func mysqlDriverConfig() (string, error) {
-	server := os.Getenv("MYSQLHOST")
-	port := os.Getenv("MYSQLPORT")
-	user := os.Getenv("MYSQLUSER")
-	dbname := os.Getenv("MYSQLDATABASE")
-	if server == "" || port == "" || user == "" || dbname == "" {
-		fmt.Printf("Please specify host, port, user and database using MYSQLHOST, MYSQLPORT, MYSQLUSER and MYSQLDATABASE environment variables\n")
-		return "", fmt.Errorf("Could not connect to source database")
-	}
-	password := os.Getenv("MYSQLPWD")
-	if password == "" {
-		password = getPassword()
-	}
-	return fmt.Sprintf("%s:%s@tcp(%s:%s)/%s", user, password, server, port, dbname), nil
-}
-
-func schemaFromSQL(driver string) (*internal.Conv, error) {
-	driverConfig, err := driverConfig(driver)
-	if err != nil {
-		return nil, err
-	}
-	sourceDB, err := sql.Open(driver, driverConfig)
-	if err != nil {
-		return nil, err
-	}
-	conv := internal.MakeConv()
-	err = ProcessInfoSchema(driver, conv, sourceDB)
-	if err != nil {
-		return nil, err
-	}
-	return conv, nil
-}
-
-func dataFromSQL(driver string, config spanner.BatchWriterConfig, client *sp.Client, conv *internal.Conv) (*spanner.BatchWriter, error) {
-	// TODO: Refactor to avoid redundant calls to driverConfig and
-	// Open in schemaFromSQL and dataFromSQL. Also refactor to
-	// share code with dataFromPgDump. Use single transaction for
-	// reading schema and data from source db to get consistent
-	// dump.
-	driverConfig, err := driverConfig(driver)
-	if err != nil {
-		return nil, err
-	}
-	sourceDB, err := sql.Open(driver, driverConfig)
-	if err != nil {
-		return nil, err
-	}
-	err = SetRowStats(driver, conv, sourceDB)
-	if err != nil {
-		return nil, err
-	}
-	totalRows := conv.Rows()
-	p := internal.NewProgress(totalRows, "Writing data to Spanner", internal.Verbose())
-	rows := int64(0)
-	config.Write = func(m []*sp.Mutation) error {
-		_, err := client.Apply(context.Background(), m)
-		if err != nil {
-			return err
-		}
-		atomic.AddInt64(&rows, int64(len(m)))
-		p.MaybeReport(atomic.LoadInt64(&rows))
-		return nil
-	}
-	writer := spanner.NewBatchWriter(config)
-	conv.SetDataMode()
-	conv.SetDataSink(
-		func(table string, cols []string, vals []interface{}) {
-			writer.AddRow(table, cols, vals)
-		})
-	err = ProcessSQLData(driver, conv, sourceDB)
-	if err != nil {
-		return nil, err
-	}
-	writer.Flush()
-	return writer, nil
-}
-
-func schemaFromDynamoDB(sampleSize int64) (*internal.Conv, error) {
-	conv := internal.MakeConv()
-	mySession := session.Must(session.NewSession())
-	client := dydb.New(mySession)
-	err := dynamodb.ProcessSchema(conv, client, []string{}, sampleSize)
-	if err != nil {
-		return nil, err
-	}
-	return conv, nil
-}
-
-func dataFromDynamoDB(config spanner.BatchWriterConfig, client *sp.Client, conv *internal.Conv) (*spanner.BatchWriter, error) {
-	mySession := session.Must(session.NewSession())
-	dyclient := dydb.New(mySession)
-
-	dynamodb.SetRowStats(conv, dyclient)
-	totalRows := conv.Rows()
-	p := internal.NewProgress(totalRows, "Writing data to Spanner", internal.Verbose())
-
-	rows := int64(0)
-	config.Write = func(m []*sp.Mutation) error {
-		_, err := client.Apply(context.Background(), m)
-		if err != nil {
-			return err
-		}
-		atomic.AddInt64(&rows, int64(len(m)))
-		p.MaybeReport(atomic.LoadInt64(&rows))
-		return nil
-	}
-	writer := spanner.NewBatchWriter(config)
-	conv.SetDataMode()
-	conv.SetDataSink(
-		func(table string, cols []string, vals []interface{}) {
-			writer.AddRow(table, cols, vals)
-		})
-
-	err := dynamodb.ProcessData(conv, dyclient)
-	if err != nil {
-		return nil, err
-	}
-	writer.Flush()
-	return writer, nil
-}
-
-type ioStreams struct {
-	in, seekableIn, out *os.File
-	bytesRead           int64
-}
-
-func schemaFromDump(driver string, ioHelper *ioStreams) (*internal.Conv, error) {
-	f, n, err := getSeekable(ioHelper.in)
-	if err != nil {
-		printSeekError(driver, err, ioHelper.out)
-		return nil, fmt.Errorf("can't get seekable input file")
-	}
-	ioHelper.seekableIn = f
-	ioHelper.bytesRead = n
-	conv := internal.MakeConv()
-	p := internal.NewProgress(n, "Generating schema", internal.Verbose())
-	r := internal.NewReader(bufio.NewReader(f), p)
-	conv.SetSchemaMode() // Build schema and ignore data in dump.
-	conv.SetDataSink(nil)
-	err = ProcessDump(driver, conv, r)
-	if err != nil {
-		fmt.Fprintf(ioHelper.out, "Failed to parse the data file: %v", err)
-		return nil, fmt.Errorf("failed to parse the data file")
-	}
-	p.Done()
-	return conv, nil
-}
-
-func dataFromDump(driver string, config spanner.BatchWriterConfig, ioHelper *ioStreams, client *sp.Client, conv *internal.Conv) (*spanner.BatchWriter, error) {
-	// TODO: refactor of the way we handle getSeekable
-	// to avoid the code duplication here
-	if !dataOnly {
-		_, err := ioHelper.seekableIn.Seek(0, 0)
-		if err != nil {
-			fmt.Printf("\nCan't seek to start of file (preparation for second pass): %v\n", err)
-			return nil, fmt.Errorf("can't seek to start of file")
-		}
-	} else {
-		// Note: input file is kept seekable to plan for future
-		// changes in showing progress for data migration.
-		f, n, err := getSeekable(ioHelper.in)
-		if err != nil {
-			printSeekError(driver, err, ioHelper.out)
-			return nil, fmt.Errorf("can't get seekable input file")
-		}
-		ioHelper.seekableIn = f
-		ioHelper.bytesRead = n
-	}
-	totalRows := conv.Rows()
-
-	p := internal.NewProgress(totalRows, "Writing data to Spanner", internal.Verbose())
-	r := internal.NewReader(bufio.NewReader(ioHelper.seekableIn), nil)
-	rows := int64(0)
-	config.Write = func(m []*sp.Mutation) error {
-		_, err := client.Apply(context.Background(), m)
-		if err != nil {
-			return err
-		}
-		atomic.AddInt64(&rows, int64(len(m)))
-		p.MaybeReport(atomic.LoadInt64(&rows))
-		return nil
-	}
-	writer := spanner.NewBatchWriter(config)
-	conv.SetDataMode() // Process data in dump; schema is unchanged.
-	conv.SetDataSink(
-		func(table string, cols []string, vals []interface{}) {
-			writer.AddRow(table, cols, vals)
-		})
-	ProcessDump(driver, conv, r)
-	writer.Flush()
-	p.Done()
-
-	return writer, nil
-}
-
-func report(driver string, badWrites map[string]int64, bytesRead int64, banner string, conv *internal.Conv, reportFileName string, out *os.File) {
-	f, err := os.Create(reportFileName)
-	if err != nil {
-		fmt.Fprintf(out, "Can't write out report file %s: %v\n", reportFileName, err)
-		fmt.Fprintf(out, "Writing report to stdout\n")
-		f = out
-	} else {
-		defer f.Close()
-	}
-	w := bufio.NewWriter(f)
-	w.WriteString(banner)
-
-	summary := internal.GenerateReport(driver, conv, w, badWrites)
-	w.Flush()
-	var isDump bool
-	if strings.Contains(driver, "dump") {
-		isDump = true
-	}
-	if isDump {
-		fmt.Fprintf(out, "Processed %d bytes of %s data (%d statements, %d rows of data, %d errors, %d unexpected conditions).\n",
-			bytesRead, driver, conv.Statements(), conv.Rows(), conv.StatementErrors(), conv.Unexpecteds())
-	} else {
-		fmt.Fprintf(out, "Processed source database via %s driver (%d rows of data, %d unexpected conditions).\n",
-			driver, conv.Rows(), conv.Unexpecteds())
-	}
-	// We've already written summary to f (as part of GenerateReport).
-	// In the case where f is stdout, don't write a duplicate copy.
-	if f != out {
-		fmt.Fprint(out, summary)
-		fmt.Fprintf(out, "See file '%s' for details of the schema and data conversions.\n", reportFileName)
-	}
-}
-
-// getSeekable returns a seekable file (with same content as f) and the size of the content (in bytes).
-func getSeekable(f *os.File) (*os.File, int64, error) {
-	_, err := f.Seek(0, 0)
-	if err == nil { // Stdin is seekable, let's just use that. This happens when you run 'cmd < file'.
-		n, err := getSize(f)
-		return f, n, err
-	}
-	internal.VerbosePrintln("Creating a tmp file with a copy of stdin because stdin is not seekable.")
-
-	// Create file in os.TempDir. Its not clear this is a good idea e.g. if the
-	// pg_dump/mysqldump output is large (tens of GBs) and os.TempDir points to a directory
-	// (such as /tmp) that's configured with a small amount of disk space.
-	// To workaround such limits on Unix, set $TMPDIR to a directory with lots
-	// of disk space.
-	fcopy, err := ioutil.TempFile("", "harbourbridge.data")
-	if err != nil {
-		return nil, 0, err
-	}
-	syscall.Unlink(fcopy.Name()) // File will be deleted when this process exits.
-	_, err = io.Copy(fcopy, f)
-	if err != nil {
-		return nil, 0, fmt.Errorf("can't write stdin to tmp file: %w", err)
-	}
-	_, err = fcopy.Seek(0, 0)
-	if err != nil {
-		return nil, 0, fmt.Errorf("can't reset file offset: %w", err)
-	}
-	n, err := getSize(fcopy)
-	return fcopy, n, nil
-}
-
-// createDatabase returns a newly create Spanner DB.
-// It automatically determines an appropriate project, selects a
-// Spanner instance to use, generates a new Spanner DB name,
-// and call into the Spanner admin interface to create the new DB.
-func createDatabase(project, instance, dbName string, conv *internal.Conv, out *os.File) (string, error) {
-	fmt.Fprintf(out, "Creating new database %s in instance %s with default permissions ... ", dbName, instance)
-	ctx := context.Background()
-	adminClient, err := database.NewDatabaseAdminClient(ctx)
-	if err != nil {
-		return "", fmt.Errorf("can't create admin client: %w", analyzeError(err, project, instance))
-	}
-	defer adminClient.Close()
-	// 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).
-	// The database is created without foreign key constraints. We will add them later once
-	// data has been written to the database to avoid foreign key data dependencies during
-	// data writes.
-	schema := conv.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 + "`",
-		ExtraStatements: schema,
-	})
-	if err != nil {
-		return "", fmt.Errorf("can't build CreateDatabaseRequest: %w", analyzeError(err, project, instance))
-	}
-	if _, err := op.Wait(ctx); err != nil {
-		return "", fmt.Errorf("createDatabase call failed: %w", analyzeError(err, project, instance))
-	}
-	fmt.Fprintf(out, "done.\n")
-	return fmt.Sprintf("projects/%s/instances/%s/databases/%s", project, instance, dbName), nil
-}
-
-func updateDDLForeignKeys(project, instance, dbName string, conv *internal.Conv, out *os.File) error {
-	ctx := context.Background()
-	adminClient, err := database.NewDatabaseAdminClient(ctx)
-	if err != nil {
-		return fmt.Errorf("can't create admin client: %w\n", analyzeError(err, project, instance))
-	}
-	defer adminClient.Close()
-	// 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})
-	if len(fkStmts) == 0 {
-		return nil
-	}
-	msg := fmt.Sprintf("Updating schema of database %s in instance %s with foreign key constraints ...", dbName, instance)
-	p := internal.NewProgress(int64(len(fkStmts)), msg, internal.Verbose())
-	for i, fkStmt := range fkStmts {
-		// TODO: Improve performance of the foreign key constraint updates.
-		// For example, issue all of the update ops first before waiting for them to complete
-		// so that that can execute in parallel. We could also print out ids of the
-		// long-running operations.
-		op, err := adminClient.UpdateDatabaseDdl(ctx, &adminpb.UpdateDatabaseDdlRequest{
-			Database:   fmt.Sprintf("projects/%s/instances/%s/databases/%s", project, instance, dbName),
-			Statements: []string{fkStmt},
-		})
-		if err != nil {
-			fmt.Printf("Can't add foreign key with statement %s: %s\n", fkStmt, err)
-			conv.Unexpected(fmt.Sprintf("Can't add foreign key with statement %s: %s", fkStmt, err))
-			continue
-		}
-		if err := op.Wait(ctx); err != nil {
-			fmt.Printf("Can't add foreign key with statement %s: %s\n", fkStmt, err)
-			conv.Unexpected(fmt.Sprintf("Can't add foreign key with statement %s: %s", fkStmt, err))
-		}
-		internal.VerbosePrintln("Updated schema with statement: " + fkStmt)
-		p.MaybeReport(int64(i + 1))
-	}
-	p.Done()
-	return nil
-}
-
-// getProject returns the cloud project we should use for accessing Spanner.
-// Use environment variable GCLOUD_PROJECT if it is set.
-// Otherwise, use the default project returned from gcloud.
-func getProject() (string, error) {
-	project := os.Getenv("GCLOUD_PROJECT")
-	if project != "" {
-		return project, nil
-	}
-	cmd := exec.Command("gcloud", "config", "list", "--format", "value(core.project)")
-	out, err := cmd.CombinedOutput()
-	if err != nil {
-		return "", fmt.Errorf("call to gcloud to get project failed: %w", err)
-	}
-	project = strings.TrimSpace(string(out))
-	return project, nil
-}
-
-// getInstance returns the Spanner instance we should use for creating DBs.
-// If the user specified instance (via flag 'instance') then use that.
-// Otherwise try to deduce the instance using gcloud.
-func getInstance(project string, out *os.File) (string, error) {
-	l, err := getInstances(project)
-	if err != nil {
-		return "", err
-	}
-	if len(l) == 0 {
-		fmt.Fprintf(out, "Could not find any Spanner instances for project %s\n", project)
-		return "", fmt.Errorf("no Spanner instances for %s", project)
-	}
-	// Note: we could ask for user input to select/confirm which Spanner
-	// instance to use, but that interacts poorly with piping pg_dump/mysqldump data
-	// to the tool via stdin.
-	if len(l) == 1 {
-		fmt.Fprintf(out, "Using only available Spanner instance: %s\n", l[0])
-		return l[0], nil
-	}
-	fmt.Fprintf(out, "Available Spanner instances:\n")
-	for i, x := range l {
-		fmt.Fprintf(out, " %d) %s\n", i+1, x)
-	}
-	fmt.Fprintf(out, "Please pick one of the available instances and set the flag '--instance'\n\n")
-	return "", fmt.Errorf("auto-selection of instance failed: project %s has more than one Spanner instance. "+
-		"Please use the flag '--instance' to select an instance", project)
-}
-
-func getInstances(project string) ([]string, error) {
-	ctx := context.Background()
-	instanceClient, err := instance.NewInstanceAdminClient(ctx)
-	if err != nil {
-		return nil, analyzeError(err, project, "")
-	}
-	it := instanceClient.ListInstances(ctx, &instancepb.ListInstancesRequest{Parent: fmt.Sprintf("projects/%s", project)})
-	var l []string
-	for {
-		resp, err := it.Next()
-		if err == iterator.Done {
-			break
-		}
-		if err != nil {
-			return nil, analyzeError(err, project, "")
-		}
-		l = append(l, strings.TrimPrefix(resp.Name, fmt.Sprintf("projects/%s/instances/", project)))
-	}
-	return l, nil
-}
-
-func writeSchemaFile(conv *internal.Conv, now time.Time, name string, out *os.File) {
-	f, err := os.Create(name)
-	if err != nil {
-		fmt.Fprintf(out, "Can't create schema file %s: %v\n", name, err)
-		return
-	}
-	// The schema file we write out includes comments, includes foreign keys
-	// and doesn't add backticks around table and column names. This file is
-	// intended for explanatory and documentation purposes, and is not strictly
-	// 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})
-	if len(ddl) == 0 {
-		ddl = []string{"\n-- Schema is empty -- no tables found\n"}
-	}
-	l := []string{
-		fmt.Sprintf("-- Schema generated %s\n", now.Format("2006-01-02 15:04:05")),
-		strings.Join(ddl, ";\n\n"),
-		"\n",
-	}
-	if _, err := f.WriteString(strings.Join(l, "")); err != nil {
-		fmt.Fprintf(out, "Can't write out schema file: %v\n", err)
-		return
-	}
-	fmt.Fprintf(out, "Wrote schema to file '%s'.\n", name)
-}
-
-func writeSessionFile(conv *internal.Conv, now time.Time, name string, out *os.File) {
-	f, err := os.Create(name)
-	if err != nil {
-		fmt.Fprintf(out, "Can't create session file %s: %v\n", name, err)
-		return
-	}
-	// Session file will basically contain 'conv' struct in JSON format.
-	// It contains all the information for schema and data conversion state.
-	convJSON, err := json.MarshalIndent(conv, "", " ")
-	if err != nil {
-		fmt.Fprintf(out, "Can't encode session state to JSON: %v\n", err)
-		return
-	}
-	if _, err := f.Write(convJSON); err != nil {
-		fmt.Fprintf(out, "Can't write out session file: %v\n", err)
-		return
-	}
-	fmt.Fprintf(out, "Wrote session to file '%s'.\n", name)
-}
-
-// readSessionFile reads a session JSON file and
-// unmarshal it's content into *internal.Conv.
-func readSessionFile(conv *internal.Conv) error {
-	s, err := ioutil.ReadFile(sessionJSON)
-	if err != nil {
-		return err
-	}
-	err = json.Unmarshal(s, &conv)
-	if err != nil {
-		return err
-	}
-	return nil
-}
-
-// writeBadData prints summary stats about bad rows and writes detailed info
-// to file 'name'.
-func writeBadData(bw *spanner.BatchWriter, conv *internal.Conv, banner, name string, out *os.File) {
-	badConversions := conv.BadRows()
-	badWrites := sum(bw.DroppedRowsByTable())
-	if badConversions == 0 && badWrites == 0 {
-		os.Remove(name) // Cleanup bad-data file from previous run.
-		return
-	}
-	f, err := os.Create(name)
-	if err != nil {
-		fmt.Fprintf(out, "Can't write out bad data file: %v\n", err)
-		return
-	}
-	f.WriteString(banner)
-	maxRows := 100
-	if badConversions > 0 {
-		l := conv.SampleBadRows(maxRows)
-		if int64(len(l)) < badConversions {
-			f.WriteString("A sample of rows that generated conversion errors:\n")
-		} else {
-			f.WriteString("Rows that generated conversion errors:\n")
-		}
-		for _, r := range l {
-			_, err := f.WriteString("  " + r + "\n")
-			if err != nil {
-				fmt.Fprintf(out, "Can't write out bad data file: %v\n", err)
-				return
-			}
-		}
-	}
-	if badWrites > 0 {
-		l := bw.SampleBadRows(maxRows)
-		if int64(len(l)) < badWrites {
-			f.WriteString("A sample of rows that successfully converted but couldn't be written to Spanner:\n")
-		} else {
-			f.WriteString("Rows that successfully converted but couldn't be written to Spanner:\n")
-		}
-		for _, r := range l {
-			_, err := f.WriteString("  " + r + "\n")
-			if err != nil {
-				fmt.Fprintf(out, "Can't write out bad data file: %v\n", err)
-				return
-			}
-		}
-	}
-	fmt.Fprintf(out, "See file '%s' for details of bad rows\n", name)
-}
-
-func getDatabaseName(driver string, now time.Time) (string, error) {
-	return generateName(fmt.Sprintf("%s_%s", driver, now.Format("2006-01-02")))
-}
-
-func getPassword() string {
-	fmt.Print("Enter Password: ")
-	bytePassword, err := terminal.ReadPassword(int(syscall.Stdin))
-	if err != nil {
-		fmt.Println("\nCoudln't read password")
-		return ""
-	}
-	fmt.Printf("\n")
-	return strings.TrimSpace(string(bytePassword))
-}
-
-// analyzeError inspects an error returned from Cloud Spanner and adds information
-// about potential root causes e.g. authentication issues.
-func analyzeError(err error, project, instance string) error {
-	e := strings.ToLower(err.Error())
-	if containsAny(e, []string{"unauthenticated", "cannot fetch token", "default credentials"}) {
-		return fmt.Errorf("%w.\n"+`
-Possible cause: credentials are mis-configured. Do you need to run
-
-  gcloud auth application-default login
-
-or configure environment variable GOOGLE_APPLICATION_CREDENTIALS.
-See https://cloud.google.com/docs/authentication/getting-started.
-`, err)
-	}
-	if containsAny(e, []string{"instance not found"}) && instance != "" {
-		return fmt.Errorf("%w.\n"+`
-Possible cause: Spanner instance specified via instance option does not exist.
-Please check that '%s' is correct and that it is a valid Spanner
-instance for project %s.
-`, err, instance, project)
-	}
-	return err
-}
-
-func printPermissionsWarning(driver string, out *os.File) {
-	fmt.Fprintf(out,
-		`
-WARNING: Please check that permissions for this Spanner instance are
-appropriate. Spanner manages access control at the database level, and the
-database created by HarbourBridge will inherit default permissions from this
-instance. All data written to Spanner will be visible to anyone who can
-access the created database. Note that `+driver+` table-level and row-level
-ACLs are dropped during conversion since they are not supported by Spanner.
-
-`)
-}
-
-func printSeekError(driver string, err error, out *os.File) {
-	fmt.Fprintf(out, "\nCan't get seekable input file: %v\n", err)
-	fmt.Fprintf(out, "Likely cause: not enough space in %s.\n", os.TempDir())
-	fmt.Fprintf(out, "Try writing "+driver+" output to a file first i.e.\n")
-	fmt.Fprintf(out, " "+driver+" > tmpfile\n")
-	fmt.Fprintf(out, "  harbourbridge < tmpfile\n")
-}
-
-func containsAny(s string, l []string) bool {
-	for _, a := range l {
-		if strings.Contains(s, a) {
-			return true
-		}
-	}
-	return false
-}
-
-func generateName(prefix string) (string, error) {
-	b := make([]byte, 4)
-	_, err := rand.Read(b)
-	if err != nil {
-		return "", fmt.Errorf("error generating name: %w", err)
-
-	}
-	return fmt.Sprintf("%s_%x-%x", prefix, b[0:2], b[2:4]), nil
-}
-
-func getClient(db string) (*sp.Client, error) {
-	ctx := context.Background()
-	return sp.NewClient(ctx, db)
-}
-
-func getSize(f *os.File) (int64, error) {
-	info, err := f.Stat()
-	if err != nil {
-		return 0, fmt.Errorf("can't stat file: %w", err)
-	}
-	return info.Size(), nil
-}
-
-// setupLogfile configures the file used for logs.
-// By default we just drop logs on the floor. To enable them (e.g. to debug
-// Cloud Spanner client library issues), set logfile to a non-empty filename.
-// Note: this tool itself doesn't generate logs, but some of the libraries it
-// uses do. If we don't set the log file, we see a number of unhelpful and
-// unactionable logs spamming stdout, which is annoying and confusing.
-func setupLogFile() (*os.File, error) {
-	// To enable debug logs, set logfile to a non-empty filename.
-	logfile := ""
-	if logfile == "" {
-		log.SetOutput(ioutil.Discard)
-		return nil, nil
-	}
-	f, err := os.Create(logfile)
-	if err != nil {
-		return nil, err
-	}
-	log.SetOutput(f)
-	return f, nil
-}
-
-func close(f *os.File) {
-	if f != nil {
-		f.Close()
-	}
-}
-
-func sum(m map[string]int64) int64 {
-	n := int64(0)
-	for _, c := range m {
-		n += c
-	}
-	return n
-}
-
-func getBanner(now time.Time, db string) string {
-	return fmt.Sprintf("Generated at %s for db %s\n\n", now.Format("2006-01-02 15:04:05"), db)
-}
-
-// ProcessDump invokes process dump function from a sql package based on driver selected.
-func ProcessDump(driver string, conv *internal.Conv, r *internal.Reader) error {
-	switch driver {
-	case MYSQLDUMP:
-		return mysql.ProcessMySQLDump(conv, r)
-	case PGDUMP:
-		return postgres.ProcessPgDump(conv, r)
-	default:
-		return fmt.Errorf("process dump for driver %s not supported", driver)
-	}
-}
-
-// ProcessInfoSchema invokes process infoschema function from a sql package based on driver selected.
-func ProcessInfoSchema(driver string, conv *internal.Conv, db *sql.DB) error {
-	switch driver {
-	case MYSQL:
-		return mysql.ProcessInfoSchema(conv, db, os.Getenv("MYSQLDATABASE"))
-	case POSTGRES:
-		return postgres.ProcessInfoSchema(conv, db)
-	default:
-		return fmt.Errorf("schema conversion for driver %s not supported", driver)
-	}
-}
-
-// SetRowStats invokes SetRowStats function from a sql package based on driver selected.
-func SetRowStats(driver string, conv *internal.Conv, db *sql.DB) error {
-	switch driver {
-	case MYSQL:
-		mysql.SetRowStats(conv, db, os.Getenv("MYSQLDATABASE"))
-	case POSTGRES:
-		postgres.SetRowStats(conv, db)
-	default:
-		return fmt.Errorf("Could get rows stats for '%s' driver", driver)
-	}
-	return nil
-}
-
-// ProcessSQLData invokes ProcessSQLData function from a sql package based on driver selected.
-func ProcessSQLData(driver string, conv *internal.Conv, db *sql.DB) error {
-	switch driver {
-	case MYSQL:
-		mysql.ProcessSQLData(conv, db, os.Getenv("MYSQLDATABASE"))
-	case POSTGRES:
-		postgres.ProcessSQLData(conv, db)
-	default:
-		return fmt.Errorf("Data conversion for driver %s is not supported", driver)
-	}
+	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 d849ee8..1576832 100644
--- a/spanner/ddl/ast.go
+++ b/spanner/ddl/ast.go
@@ -169,6 +169,7 @@ type CreateTable struct {
 	Pks      []IndexKey
 	Fks      []Foreignkey
 	Indexes  []CreateIndex
+	Parent   string //if not empty, this table will be interleaved
 	Comment  string
 }
 
@@ -203,7 +204,11 @@ func (ct CreateTable) PrintCreateTable(config Config) string {
 	if config.Comments && len(ct.Comment) > 0 {
 		tableComment = "--\n-- " + ct.Comment + "\n--\n"
 	}
-	return fmt.Sprintf("%sCREATE TABLE %s (%s\n) PRIMARY KEY (%s)", tableComment, config.quote(ct.Name), cols, strings.Join(keys, ", "))
+	var interleave string
+	if ct.Parent != "" {
+		interleave = ",\nINTERLEAVE IN PARENT " + config.quote(ct.Parent)
+	}
+	return fmt.Sprintf("%sCREATE TABLE %s (%s\n) PRIMARY KEY (%s)%s", tableComment, config.quote(ct.Name), cols, strings.Join(keys, ", "), interleave)
 }
 
 // CreateIndex encodes the following DDL definition:
diff --git a/web/README.md b/web/README.md
new file mode 100644
index 0000000..2dd5538
--- /dev/null
+++ b/web/README.md
@@ -0,0 +1,499 @@
+# HarbourBridge: Web APIs (Experimental)
+
+HarbourBridge is a stand-alone open source tool for Cloud Spanner evaluation
+and migration. This README provides details of the tool's Web APIs capabilities,
+Web APIs allows user to visualize the conversion using UI and interact with the
+tool to customize some of the conversion. For general HarbourBridge information
+see this [README](https:githubcomcloudspannerecosystemharbourbridge#harbourbridge-turnkey-spanner-evaluation). Note that this feature is experimental, and should not be used for
+production database.
+
+### Starting web server for HarbourBridge
+
+The following example assume `harbourbridge` has been added to your PATH
+environment variable.
+
+HarbourBridge's Web API feature can be used with all the driver modes available,
+using mysql or postgres dump or direct connection.
+
+To start HarbourBridge web server, run:
+
+```sh
+harbourbridge --web
+```
+
+The tool will be available on port 8080
+
+You can go to `<base-url>/` to visit the UI of the tool.
+
+    eg: localhost:8080/
+
+For more details on how to use the UI, you can visit: `<base-url>/userManual.html`.
+
+    eg: localhost:8080/userManual.html
+
+<ins>**Note:**</ins>
+
+The `pg_dump` and `mysqldump` drivers cannot be used for data migration if the
+Spanner schema has interleaved tables. Note that the `postgres` or `mysql` drivers
+do not have this restriction -- consider using these as an alternative.
+
+## APIs
+
+These are the REST APIs and their details:
+
+### Connect
+
+`/connect` is a POST API used to configure direct connection to a database.
+The request body contains connection details and the driver name.
+
+#### Method
+
+`POST`
+
+#### Request body
+
+Connection information along with driver name.
+
+Example
+
+```json
+{
+  "Driver": "postgres",
+  "Host": "localhost",
+  "Port": "5432",
+  "Database": "dbname",
+  "User": "user",
+  "Password": "password"
+}
+```
+
+#### Response body
+
+No response body is returned.
+
+### Convert
+
+(1) `/convert/infoschema` is a GET API followed by `/connect` API to convert using
+infoschema mode. It returns the schema conversion state in json format.
+
+#### Method
+
+`GET`
+
+#### Request body
+
+No request body is needed.
+
+#### Response body
+
+Conv struct in JSON format.
+
+(2) `/convert/dump` is a POST API used to perform schema conversion on a dump file.
+The request body contains the path to the dump file.
+
+#### Method
+
+`POST`
+
+#### Request body
+
+Provide driver and path to dump file.
+
+Example
+
+```json
+{
+  "Driver": "postgres",
+  "Path": "/path/to/dumpFile"
+}
+```
+
+#### Response body
+
+Conv struct in JSON format.
+
+### DDL
+
+`/ddl` is a GET API which must be used after using conversion APIs (i.e, `/connect`
+or `/convert`). This API returns the DDL statements of the converted schema.
+
+#### Method
+
+`GET`
+
+#### Request body
+
+No request body is needed.
+
+#### Response body
+
+Table wise DDL statements.
+
+Example
+
+```json
+{
+  "Albums": "--\n-- Spanner schema for source table Albums\n--\nCREATE TABLE
+Albums (\n    SingerId INT64 NOT NULL, -- From: SingerId smallint(6)\n
+AlbumId INT64 NOT NULL, -- From: AlbumId smallint(6)\n    AlbumTitle STRING(50)
+-- From: AlbumTitle varchar(50)\n) PRIMARY KEY (SingerId, AlbumId)"
+}
+```
+
+### Session
+
+(1) `/session` is a GET API which returns the schema conversion state in json format.
+It also create a file with suffix `.session.json` in the `frontend/` folder.
+
+#### Method
+
+`GET`
+
+#### Request body
+
+No request body is needed.
+
+#### Response body
+
+Session file info with driver name.
+
+Example
+
+```json
+{
+  "Driver": "postgres",
+  "FilePath": "/path/to/sessionFile",
+  "FileName": "sessionFile.session.json",
+  "CreatedAt": "Thu, 04 Feb 2021 18:10:32 IST"
+}
+```
+
+(2) `/session/resume` is a POST API which can be used to resume a previous session.
+The request body contains the path to the previous session. Note that sessions are
+created by the `/session` API.
+
+#### Method
+
+`POST`
+
+#### Request body
+
+Provide driver and session file path.
+
+Example
+
+```json
+{
+  "driver": "postgres",
+  "path": "/path/to/sessionFile",
+  "fileName": "sessionFile.session.json"
+}
+```
+
+#### Response body
+
+No response body is returned.
+
+### Summary
+
+`/summary` is a GET API which returns a table-by-table report of the conversion.
+
+#### Method
+
+`GET`
+
+#### Request body
+
+No request body is needed.
+
+#### Response body
+
+Table wise summary of conversion.
+
+Example
+
+```json
+{
+  "Albums": "Note\n1) Some columns will consume more storage in Spanner
+e.g. for column 'AlbumId', source DB type smallint(6) is mapped to Spanner
+type int64.\n\n"
+}
+```
+
+### Overview
+
+`/overview` is a GET API which returns the overview of the conversion.
+
+#### Method
+
+`GET`
+
+#### Request body
+
+No request body is needed.
+
+#### Response body
+
+Overall summary of conversion in string format.
+
+### Conversion
+
+`/conversion` is a GET API which returns table wise rate of conversion which is
+encoded in color values.
+
+#### Method
+
+`GET`
+
+#### Request body
+
+No request body is needed.
+
+#### Response body
+
+Table wise rate of conversion encoded in color values.
+
+Example
+
+```json
+{
+  "Albums": "GREEN",
+  "Singers": "BLUE",
+  "Userinfo": "ORANGE"
+}
+```
+
+### Typemap
+
+(1) `/typemap` is a GET API which returns a map that, for each source type,
+provides the potential Spanner types that can be used for that source type.
+This map can be used for customization of type mapping in the UI.
+
+#### Method
+
+`GET`
+
+#### Request body
+
+No request body is needed.
+
+#### Response body
+
+Map from each source type to the list of possible Spanner types and issues.
+
+Example
+
+```json
+{
+  "smallint": [
+    {
+      "T": "INT64",
+      "Brief": "Some columns will consume more storage in Spanner"
+    },
+    {
+      "T": "STRING",
+      "Brief": "Some columns will consume more storage in Spanner"
+    }
+  ],
+  "varchar": [
+    {
+      "T": "BYTES",
+      "Brief": ""
+    },
+    {
+      "T": "STRING",
+      "Brief": ""
+    }
+  ]
+}
+```
+
+(2) `/typemap/global` is a POST API to customize schema conversion on global level.
+The request body contains a map from source type to Spanner type in json format.
+
+#### Method
+
+`POST`
+
+#### Request body
+
+Map from source type to Spanner type.
+
+Example
+
+```json
+{
+  "smallint": "STRING",
+  "varchar": "BYTES"
+}
+```
+
+#### Response body
+
+Updated Conv struct in JSON format.
+
+(3) `/typemap/table?table=<table_name>` is a POST API which performs following
+operations on a single table.
+
+- Remove column
+- Rename column
+- Remove or Add primary key
+- Update type of column
+- Remove or Add NOT NULL constraint
+
+#### Method
+
+`POST`
+
+#### Request body
+
+Column wise actions to be performed with following possible values for each field.
+
+- Removed : true/false
+- Rename : New name or empty string
+- PK : "" | "ADDED" | "REMOVED"
+- NotNull : "" | "ADDED" | "REMOVED"
+- ToType : New Spanner type or empty string
+
+Example
+
+```json
+{
+  "UpdateCols": {
+    "AlbumTitle": {
+      "Removed": false,
+      "Rename": "AlbumName",
+      "PK": "",
+      "NotNull": "ADDED",
+      "ToType": "BYTES"
+    }
+  }
+}
+```
+
+#### Response body
+
+Updated Conv struct in JSON format.
+
+### Report file
+
+`/report` is a GET API which generates report file and returns file path.
+
+#### Method
+
+`GET`
+
+#### Request body
+
+No request body is needed.
+
+#### Response body
+
+Path to report file.
+
+Example
+
+```
+/path/to/reportfile.report.txt
+```
+
+### Schema file
+
+`/schema` is a GET API which generates schema file and returns file path.
+
+#### Method
+
+`GET`
+
+#### Request body
+
+No request body is needed.
+
+#### Response body
+
+Path to schema file.
+
+Example
+
+```
+/path/to/schemafile.schema.txt
+```
+
+### Interleave tables
+
+`/setparent?table=<table_name>` is a GET API which checks whether it is
+possible to convert a table into a Spanner interleaved table. If this conversion is possible,
+then the schema is changed and the parent table name is returned.
+If the conversion is not possible, a failure message is returned.
+
+#### Method
+
+`GET`
+
+#### Request body
+
+No request body is needed.
+
+#### Response body
+
+If it is possible to make given table interleaved, it will return with table
+name of parent.
+
+Example
+
+```json
+{
+  "Possible": true,
+  "Parent": "Singers",
+  "Comment": ""
+}
+```
+
+If it is not possible to interleave given table, it will return with the reason
+of why it failed.
+
+Example
+
+```json
+{
+  "Possible": false,
+  "Parent": "",
+  "Comment": "No valid prefix"
+}
+```
+
+### Drop foreign key
+
+`/drop/fk?table=<table_name>&pos=<position>` is a GET API which takes table name
+and array position of foreign key in query params. It drops foreign key at given
+position for given table name.
+
+#### Method
+
+`GET`
+
+#### Request body
+
+No request body is needed.
+
+#### Response body
+
+Updated Conv struct in JSON format.
+
+### Drop secondary index
+
+`/drop/secondaryindex?table=<table_name>&pos=<position>` is a GET API which takes
+table name and array position of secondary index in query params. It drops secondary
+index at given position for given table name.
+
+#### Method
+
+`GET`
+
+#### Request body
+
+No request body is needed.
+
+#### Response body
+
+Updated Conv struct in JSON format.
diff --git a/web/mysql_typemap.go b/web/mysql_typemap.go
new file mode 100644
index 0000000..d6e0466
--- /dev/null
+++ b/web/mysql_typemap.go
@@ -0,0 +1,173 @@
+// 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 web
+
+import (
+	"github.com/cloudspannerecosystem/harbourbridge/internal"
+	"github.com/cloudspannerecosystem/harbourbridge/spanner/ddl"
+)
+
+// toSpannerTypeMySQL defines the mapping of source types into Spanner
+// types. Each source type has a default Spanner type, as well as other potential
+// Spanner types it could map to. When calling toSpannerTypeMySQL, you specify
+// the source type name (along with any modifiers), and optionally you specify
+// a target Spanner type name (empty string if you don't have one). If the target
+// Spanner type name is specified and is a potential mapping for this source type,
+// then it will be used to build the returned ddl.Type. If not, the default
+// Spanner type for this source type will be used.
+// Note that toSpannerTypeMySQL is extensively tested via tests in web_test.go.
+//
+// TODO: Move the type remapping function to toddl.go (once we've merged
+// dynamodb/toddl.go, mysql/toddl.go and postgres/toddl.go).
+// Consider some refactoring to reduce code duplication (although note
+// that this type remapping has to preserve all previous changes done via the UI!)
+func toSpannerTypeMySQL(srcType string, spType string, mods []int64) (ddl.Type, []internal.SchemaIssue) {
+	switch srcType {
+	case "bool", "boolean":
+		switch spType {
+		case ddl.String:
+			return ddl.Type{Name: ddl.String, Len: ddl.MaxLength}, []internal.SchemaIssue{internal.Widened}
+		case ddl.Int64:
+			return ddl.Type{Name: ddl.Int64}, []internal.SchemaIssue{internal.Widened}
+		default:
+			return ddl.Type{Name: ddl.Bool}, nil
+		}
+	case "tinyint":
+		switch spType {
+		case ddl.String:
+			return ddl.Type{Name: ddl.String, Len: ddl.MaxLength}, []internal.SchemaIssue{internal.Widened}
+		case ddl.Int64:
+			return ddl.Type{Name: ddl.Int64}, []internal.SchemaIssue{internal.Widened}
+		default:
+			// tinyint(1) is a bool in MySQL
+			if len(mods) > 0 && mods[0] == 1 {
+				return ddl.Type{Name: ddl.Bool}, nil
+			}
+			return ddl.Type{Name: ddl.Int64}, []internal.SchemaIssue{internal.Widened}
+		}
+	case "double":
+		switch spType {
+		case ddl.String:
+			return ddl.Type{Name: ddl.String, Len: ddl.MaxLength}, []internal.SchemaIssue{internal.Widened}
+		default:
+			return ddl.Type{Name: ddl.Float64}, nil
+		}
+	case "float":
+		switch spType {
+		case ddl.String:
+			return ddl.Type{Name: ddl.String, Len: ddl.MaxLength}, []internal.SchemaIssue{internal.Widened}
+		default:
+			return ddl.Type{Name: ddl.Float64}, []internal.SchemaIssue{internal.Widened}
+		}
+	case "numeric", "decimal":
+		switch spType {
+		case ddl.String:
+			return ddl.Type{Name: ddl.String, Len: ddl.MaxLength}, []internal.SchemaIssue{internal.Widened}
+		default:
+			// TODO: check mod[0] and mod[1] and generate a warning
+			// if this numeric won't fit in Spanner's NUMERIC.
+			return ddl.Type{Name: ddl.Numeric}, nil
+		}
+	case "bigint":
+		switch spType {
+		case ddl.String:
+			return ddl.Type{Name: ddl.String, Len: ddl.MaxLength}, []internal.SchemaIssue{internal.Widened}
+		default:
+			return ddl.Type{Name: ddl.Int64}, nil
+		}
+	case "smallint", "mediumint", "integer", "int":
+		switch spType {
+		case ddl.String:
+			return ddl.Type{Name: ddl.String, Len: ddl.MaxLength}, []internal.SchemaIssue{internal.Widened}
+		default:
+			return ddl.Type{Name: ddl.Int64}, []internal.SchemaIssue{internal.Widened}
+		}
+	case "bit":
+		switch spType {
+		case ddl.String:
+			return ddl.Type{Name: ddl.String, Len: ddl.MaxLength}, nil
+		default:
+			return ddl.Type{Name: ddl.Bytes, Len: ddl.MaxLength}, nil
+		}
+	case "varchar", "char":
+		switch spType {
+		case ddl.Bytes:
+			if len(mods) > 0 {
+				return ddl.Type{Name: ddl.Bytes, Len: mods[0]}, nil
+			}
+			return ddl.Type{Name: ddl.Bytes, Len: ddl.MaxLength}, nil
+		default:
+			if len(mods) > 0 {
+				return ddl.Type{Name: ddl.String, Len: mods[0]}, nil
+			}
+			return ddl.Type{Name: ddl.String, Len: ddl.MaxLength}, nil
+		}
+	case "text", "tinytext", "mediumtext", "longtext":
+		switch spType {
+		case ddl.Bytes:
+			return ddl.Type{Name: ddl.Bytes, Len: ddl.MaxLength}, nil
+		default:
+			return ddl.Type{Name: ddl.String, Len: ddl.MaxLength}, nil
+		}
+	case "set", "enum":
+		return ddl.Type{Name: ddl.String, Len: ddl.MaxLength}, nil
+	case "json":
+		switch spType {
+		case ddl.Bytes:
+			return ddl.Type{Name: ddl.Bytes, Len: ddl.MaxLength}, nil
+		default:
+			return ddl.Type{Name: ddl.String, Len: ddl.MaxLength}, nil
+		}
+	case "binary", "varbinary":
+		switch spType {
+		case ddl.String:
+			return ddl.Type{Name: ddl.String, Len: ddl.MaxLength}, nil
+		default:
+			return ddl.Type{Name: ddl.Bytes, Len: ddl.MaxLength}, nil
+		}
+	case "tinyblob", "mediumblob", "blob", "longblob":
+		switch spType {
+		case ddl.String:
+			return ddl.Type{Name: ddl.String, Len: ddl.MaxLength}, nil
+		default:
+			return ddl.Type{Name: ddl.Bytes, Len: ddl.MaxLength}, nil
+		}
+	case "date":
+		switch spType {
+		case ddl.String:
+			return ddl.Type{Name: ddl.String, Len: ddl.MaxLength}, []internal.SchemaIssue{internal.Widened}
+		default:
+			return ddl.Type{Name: ddl.Date}, nil
+		}
+	case "datetime":
+		switch spType {
+		case ddl.String:
+			return ddl.Type{Name: ddl.String, Len: ddl.MaxLength}, []internal.SchemaIssue{internal.Widened}
+		default:
+			return ddl.Type{Name: ddl.Timestamp}, []internal.SchemaIssue{internal.Datetime}
+		}
+	case "timestamp":
+		switch spType {
+		case ddl.String:
+			return ddl.Type{Name: ddl.String, Len: ddl.MaxLength}, []internal.SchemaIssue{internal.Widened}
+		default:
+			return ddl.Type{Name: ddl.Timestamp}, nil
+		}
+	case "time", "year":
+		return ddl.Type{Name: ddl.String, Len: ddl.MaxLength}, []internal.SchemaIssue{internal.Time}
+
+	}
+	return ddl.Type{Name: ddl.String, Len: ddl.MaxLength}, []internal.SchemaIssue{internal.NoGoodType}
+}
diff --git a/web/postgres_typemap.go b/web/postgres_typemap.go
new file mode 100644
index 0000000..c0a2193
--- /dev/null
+++ b/web/postgres_typemap.go
@@ -0,0 +1,170 @@
+// 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 web
+
+import (
+	"github.com/cloudspannerecosystem/harbourbridge/internal"
+	"github.com/cloudspannerecosystem/harbourbridge/spanner/ddl"
+)
+
+// toSpannerTypePostgres defines the mapping of source types into Spanner
+// types. Each source type has a default Spanner type, as well as other potential
+// Spanner types it could map to. When calling toSpannerTypePostgres, you specify
+// the source type name (along with any modifiers), and optionally you specify
+// a target Spanner type name (empty string if you don't have one). If the target
+// Spanner type name is specified and is a potential mapping for this source type,
+// then it will be used to build the returned ddl.Type. If not, the default
+// Spanner type for this source type will be used.
+// Note that toSpannerTypePostgres is extensively tested via tests in web_test.go.
+//
+// TODO: Move the type remapping function to toddl.go (once we've merged
+// dynamodb/toddl.go, mysql/toddl.go and postgres/toddl.go).
+// Consider some refactoring to reduce code duplication (although note
+// that this type remapping has to preserve all previous changes done via the UI!)
+func toSpannerTypePostgres(srcType string, spType string, mods []int64) (ddl.Type, []internal.SchemaIssue) {
+	switch srcType {
+	case "bool", "boolean":
+		switch spType {
+		case ddl.String:
+			return ddl.Type{Name: ddl.String, Len: ddl.MaxLength}, []internal.SchemaIssue{internal.Widened}
+		case ddl.Int64:
+			return ddl.Type{Name: ddl.Int64}, []internal.SchemaIssue{internal.Widened}
+		default:
+			return ddl.Type{Name: ddl.Bool}, nil
+		}
+	case "bigserial":
+		switch spType {
+		case ddl.String:
+			return ddl.Type{Name: ddl.String, Len: ddl.MaxLength}, []internal.SchemaIssue{internal.Widened, internal.Serial}
+		default:
+			return ddl.Type{Name: ddl.Int64}, []internal.SchemaIssue{internal.Serial}
+		}
+	case "bpchar", "character": // Note: Postgres internal name for char is bpchar (aka blank padded char).
+		switch spType {
+		case ddl.Bytes:
+			if len(mods) > 0 {
+				return ddl.Type{Name: ddl.Bytes, Len: mods[0]}, nil
+			}
+			return ddl.Type{Name: ddl.Bytes, Len: 1}, nil
+		default:
+			if len(mods) > 0 {
+				return ddl.Type{Name: ddl.String, Len: mods[0]}, nil
+			}
+			// Note: bpchar without length specifier is equivalent to bpchar(1)
+			return ddl.Type{Name: ddl.String, Len: 1}, nil
+		}
+	case "bytea":
+		switch spType {
+		case ddl.String:
+			return ddl.Type{Name: ddl.String, Len: ddl.MaxLength}, nil
+		default:
+			return ddl.Type{Name: ddl.Bytes, Len: ddl.MaxLength}, nil
+		}
+	case "date":
+		switch spType {
+		case ddl.String:
+			return ddl.Type{Name: ddl.String, Len: ddl.MaxLength}, []internal.SchemaIssue{internal.Widened}
+		default:
+			return ddl.Type{Name: ddl.Date}, nil
+		}
+	case "float8", "double precision":
+		switch spType {
+		case ddl.String:
+			return ddl.Type{Name: ddl.String, Len: ddl.MaxLength}, []internal.SchemaIssue{internal.Widened}
+		default:
+			return ddl.Type{Name: ddl.Float64}, nil
+		}
+	case "float4", "real":
+		switch spType {
+		case ddl.String:
+			return ddl.Type{Name: ddl.String, Len: ddl.MaxLength}, []internal.SchemaIssue{internal.Widened}
+		default:
+			return ddl.Type{Name: ddl.Float64}, []internal.SchemaIssue{internal.Widened}
+		}
+	case "int8", "bigint":
+		switch spType {
+		case ddl.String:
+			return ddl.Type{Name: ddl.String, Len: ddl.MaxLength}, []internal.SchemaIssue{internal.Widened}
+		default:
+			return ddl.Type{Name: ddl.Int64}, nil
+		}
+	case "int4", "integer":
+		switch spType {
+		case ddl.String:
+			return ddl.Type{Name: ddl.String, Len: ddl.MaxLength}, []internal.SchemaIssue{internal.Widened}
+		default:
+			return ddl.Type{Name: ddl.Int64}, []internal.SchemaIssue{internal.Widened}
+		}
+	case "int2", "smallint":
+		switch spType {
+		case ddl.String:
+			return ddl.Type{Name: ddl.String, Len: ddl.MaxLength}, []internal.SchemaIssue{internal.Widened}
+		default:
+			return ddl.Type{Name: ddl.Int64}, []internal.SchemaIssue{internal.Widened}
+		}
+	case "numeric":
+		switch spType {
+		case ddl.String:
+			return ddl.Type{Name: ddl.String, Len: ddl.MaxLength}, []internal.SchemaIssue{internal.Widened}
+		default:
+			// TODO: check mod[0] and mod[1] and generate a warning
+			// if this numeric won't fit in Spanner's NUMERIC.
+			return ddl.Type{Name: ddl.Numeric}, nil
+		}
+	case "serial":
+		switch spType {
+		case ddl.String:
+			return ddl.Type{Name: ddl.String, Len: ddl.MaxLength}, []internal.SchemaIssue{internal.Widened, internal.Serial}
+		default:
+			return ddl.Type{Name: ddl.Int64}, []internal.SchemaIssue{internal.Serial}
+		}
+	case "text":
+		switch spType {
+		case ddl.Bytes:
+			return ddl.Type{Name: ddl.Bytes, Len: ddl.MaxLength}, nil
+		default:
+			return ddl.Type{Name: ddl.String, Len: ddl.MaxLength}, nil
+		}
+	case "timestamptz", "timestamp with time zone":
+		switch spType {
+		case ddl.String:
+			return ddl.Type{Name: ddl.String, Len: ddl.MaxLength}, []internal.SchemaIssue{internal.Widened}
+		default:
+			return ddl.Type{Name: ddl.Timestamp}, nil
+		}
+	case "timestamp", "timestamp without time zone":
+		// Map timestamp without timezone to Spanner timestamp.
+		switch spType {
+		case ddl.String:
+			return ddl.Type{Name: ddl.String, Len: ddl.MaxLength}, []internal.SchemaIssue{internal.Widened}
+		default:
+			return ddl.Type{Name: ddl.Timestamp}, []internal.SchemaIssue{internal.Timestamp}
+		}
+	case "varchar", "character varying":
+		switch spType {
+		case ddl.Bytes:
+			if len(mods) > 0 {
+				return ddl.Type{Name: ddl.Bytes, Len: mods[0]}, nil
+			}
+			return ddl.Type{Name: ddl.Bytes, Len: ddl.MaxLength}, nil
+		default:
+			if len(mods) > 0 {
+				return ddl.Type{Name: ddl.String, Len: mods[0]}, nil
+			}
+			return ddl.Type{Name: ddl.String, Len: ddl.MaxLength}, nil
+		}
+	}
+	return ddl.Type{Name: ddl.String, Len: ddl.MaxLength}, []internal.SchemaIssue{internal.NoGoodType}
+}
diff --git a/web/routes.go b/web/routes.go
new file mode 100644
index 0000000..399ffcd
--- /dev/null
+++ b/web/routes.go
@@ -0,0 +1,51 @@
+// 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 web
+
+import (
+	"net/http"
+
+	"github.com/gorilla/mux"
+)
+
+func getRoutes() *mux.Router {
+	router := mux.NewRouter().StrictSlash(true)
+	staticFileDirectory := http.Dir("./frontend/")
+	router.HandleFunc("/connect", databaseConnection).Methods("POST")
+	router.HandleFunc("/convert/infoschema", convertSchemaSQL).Methods("GET")
+	router.HandleFunc("/convert/dump", convertSchemaDump).Methods("POST")
+	router.HandleFunc("/ddl", getDDL).Methods("GET")
+	router.HandleFunc("/session", createSession).Methods("GET")
+	router.HandleFunc("/session/resume", resumeSession).Methods("POST")
+	router.HandleFunc("/summary", getSummary).Methods("GET")
+	router.HandleFunc("/overview", getOverview).Methods("GET")
+	router.HandleFunc("/conversion", getConversionRate).Methods("GET")
+	router.HandleFunc("/typemap", getTypeMap).Methods("GET")
+	router.HandleFunc("/report", getReportFile).Methods("GET")
+	router.HandleFunc("/schema", getSchemaFile).Methods("GET")
+	router.HandleFunc("/typemap/global", setTypeMapGlobal).Methods("POST")
+	router.HandleFunc("/typemap/table", updateTableSchema).Methods("POST")
+	router.HandleFunc("/setparent", setParentTable).Methods("GET")
+
+	//TODO:(searce) take constraint names themselves which are guaranteed to be unique for Spanner.
+	router.HandleFunc("/drop/fk", dropForeignKey).Methods("GET")
+
+	//TODO:(searce) take constraint names themselves which are guaranteed to be unique for Spanner.
+	router.HandleFunc("/drop/secondaryindex", dropSecondaryIndex).Methods("GET")
+
+	router.HandleFunc("/rename/fk", renameForeignKey).Methods("GET")
+	router.PathPrefix("/").Handler(http.FileServer(staticFileDirectory))
+	return router
+}
diff --git a/web/session.go b/web/session.go
new file mode 100644
index 0000000..9958c84
--- /dev/null
+++ b/web/session.go
@@ -0,0 +1,83 @@
+// 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 web
+
+import (
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"os"
+	"time"
+
+	"github.com/cloudspannerecosystem/harbourbridge/conversion"
+	"github.com/cloudspannerecosystem/harbourbridge/internal"
+)
+
+// session contains the metadata for a session file.
+// A session file is a snapshot of an ongoing HarbourBridge conversion session,
+// and consists of an internal.Conv struct in JSON format.
+type session struct {
+	Driver    string `json:"driver"`
+	FilePath  string `json:"filePath"`
+	DBName    string `json:"dbName"`
+	CreatedAt string `json:"createdAt"`
+}
+
+func createSession(w http.ResponseWriter, r *http.Request) {
+	now := time.Now()
+	dbName := sessionState.dbName
+	var err error
+	if dbName == "" {
+		dbName, err = conversion.GetDatabaseName(sessionState.driver, now)
+		if err != nil {
+			http.Error(w, fmt.Sprintf("Can not create database name : %v", err), http.StatusInternalServerError)
+		}
+	}
+	sessionFile := ".session.json"
+	filePrefix := "frontend/"
+	out := os.Stdout
+	filePath := filePrefix + dbName + sessionFile
+	conversion.WriteSessionFile(sessionState.conv, filePath, out)
+	session := session{Driver: sessionState.driver, FilePath: filePath, DBName: dbName, CreatedAt: now.Format(time.RFC1123)}
+	sessionState.sessionFile = filePath
+	w.WriteHeader(http.StatusOK)
+	json.NewEncoder(w).Encode(session)
+}
+
+func resumeSession(w http.ResponseWriter, r *http.Request) {
+	reqBody, err := ioutil.ReadAll(r.Body)
+	if err != nil {
+		http.Error(w, fmt.Sprintf("Body Read Error : %v", err), http.StatusInternalServerError)
+		return
+	}
+	var s session
+	err = json.Unmarshal(reqBody, &s)
+	if err != nil {
+		http.Error(w, fmt.Sprintf("Request Body parse error : %v", err), http.StatusBadRequest)
+		return
+	}
+	sessionState.conv = internal.MakeConv()
+	err = conversion.ReadSessionFile(sessionState.conv, s.FilePath)
+	if err != nil {
+		http.Error(w, fmt.Sprintf("failed to open the session file: %v", err), http.StatusNotFound)
+		return
+	}
+	sessionState.driver = s.Driver
+	sessionState.dbName = s.DBName
+	sessionState.sessionFile = s.FilePath
+	w.WriteHeader(http.StatusOK)
+	json.NewEncoder(w).Encode(sessionState.conv)
+}
diff --git a/web/web.go b/web/web.go
new file mode 100644
index 0000000..d97f6f4
--- /dev/null
+++ b/web/web.go
@@ -0,0 +1,995 @@
+// 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 web defines web APIs to be used with harbourbridge frontend.
+// Apart from schema conversion, this package involves API to update
+// converted schema.
+package web
+
+import (
+	"bufio"
+	"bytes"
+	"database/sql"
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"log"
+	"net/http"
+	"os"
+	"path/filepath"
+	"reflect"
+	"sort"
+	"strconv"
+	"strings"
+	"time"
+
+	"github.com/cloudspannerecosystem/harbourbridge/conversion"
+	"github.com/cloudspannerecosystem/harbourbridge/internal"
+	"github.com/cloudspannerecosystem/harbourbridge/mysql"
+	"github.com/cloudspannerecosystem/harbourbridge/postgres"
+	"github.com/cloudspannerecosystem/harbourbridge/spanner/ddl"
+	_ "github.com/go-sql-driver/mysql"
+	"github.com/gorilla/handlers"
+	_ "github.com/lib/pq"
+)
+
+// TODO:(searce):
+// 1) Test cases for APIs
+// 2) API for saving/updating table-level changes.
+// 3) API for showing logs
+// 4) Split all routing to an route.go file
+// 5) API for downloading the schema file, ddl file and summary report file.
+// 6) Update schema conv after setting global datatypes and return conv. (setTypeMap)
+// 7) Add rateConversion() in schema conversion, ddl and report APIs.
+// 8) Add an overview in summary report API
+var mysqlTypeMap = make(map[string][]typeIssue)
+var postgresTypeMap = make(map[string][]typeIssue)
+
+// TODO:(searce) organize this file according to go style guidelines: generally
+// have public constants and public type definitions first, then public
+// functions, and finally helper functions (usually in order of importance).
+
+// driverConfig contains the parameters needed to make a direct database connection. It is
+// used to communicate via HTTP with the frontend.
+type driverConfig struct {
+	Driver   string `json:"Driver"`
+	Host     string `json:"Host"`
+	Port     string `json:"Port"`
+	Database string `json:"Database"`
+	User     string `json:"User"`
+	Password string `json:"Password"`
+}
+
+// databaseConnection creates connection with database when using
+// with postgres and mysql driver.
+func databaseConnection(w http.ResponseWriter, r *http.Request) {
+	reqBody, err := ioutil.ReadAll(r.Body)
+	if err != nil {
+		http.Error(w, fmt.Sprintf("Body Read Error : %v", err), http.StatusInternalServerError)
+		return
+	}
+	var config driverConfig
+	err = json.Unmarshal(reqBody, &config)
+	if err != nil {
+		http.Error(w, fmt.Sprintf("Request Body parse error : %v", err), http.StatusBadRequest)
+		return
+	}
+	var dataSourceName string
+	switch config.Driver {
+	case "postgres":
+		dataSourceName = fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable", config.Host, config.Port, config.User, config.Password, config.Database)
+	case "mysql":
+		dataSourceName = fmt.Sprintf("%s:%s@tcp(%s:%s)/%s", config.User, config.Password, config.Host, config.Port, config.Database)
+	default:
+		http.Error(w, fmt.Sprintf("Driver : '%s' is not supported", config.Driver), http.StatusBadRequest)
+		return
+	}
+	sourceDB, err := sql.Open(config.Driver, dataSourceName)
+	if err != nil {
+		http.Error(w, fmt.Sprintf("SQL connection error : %v", err), http.StatusInternalServerError)
+		return
+	}
+	// Open doesn't open a connection. Validate database connection.
+	err = sourceDB.Ping()
+	if err != nil {
+		http.Error(w, fmt.Sprintf("Connection Error: %v. Check Configuration again.", err), http.StatusInternalServerError)
+		return
+	}
+	sessionState.sourceDB = sourceDB
+	sessionState.dbName = config.Database
+	sessionState.driver = config.Driver
+	sessionState.sessionFile = ""
+	w.WriteHeader(http.StatusOK)
+}
+
+// convertSchemaSQL converts source database to Spanner when using
+// with postgres and mysql driver.
+func convertSchemaSQL(w http.ResponseWriter, r *http.Request) {
+	if sessionState.sourceDB == nil || sessionState.dbName == "" || sessionState.driver == "" {
+		http.Error(w, fmt.Sprintf("Database is not configured or Database connection is lost. Please set configuration and connect to database."), http.StatusNotFound)
+		return
+	}
+	conv := internal.MakeConv()
+	var err error
+	switch sessionState.driver {
+	case "mysql":
+		err = mysql.ProcessInfoSchema(conv, sessionState.sourceDB, sessionState.dbName)
+	case "postgres":
+		err = postgres.ProcessInfoSchema(conv, sessionState.sourceDB)
+	default:
+		http.Error(w, fmt.Sprintf("Driver : '%s' is not supported", sessionState.driver), http.StatusBadRequest)
+		return
+	}
+	if err != nil {
+		http.Error(w, fmt.Sprintf("Schema Conversion Error : %v", err), http.StatusNotFound)
+		return
+	}
+	sessionState.conv = conv
+	w.WriteHeader(http.StatusOK)
+	json.NewEncoder(w).Encode(conv)
+}
+
+// dumpConfig contains the parameters needed to run the tool using dump approach. It is
+// used to communicate via HTTP with the frontend.
+type dumpConfig struct {
+	Driver   string `json:"Driver"`
+	FilePath string `json:"Path"`
+}
+
+// convertSchemaDump converts schema from dump file to Spanner schema for
+// mysqldump and pg_dump driver.
+func convertSchemaDump(w http.ResponseWriter, r *http.Request) {
+	reqBody, err := ioutil.ReadAll(r.Body)
+	if err != nil {
+		http.Error(w, fmt.Sprintf("Body Read Error : %v", err), http.StatusInternalServerError)
+		return
+	}
+	var dc dumpConfig
+	err = json.Unmarshal(reqBody, &dc)
+	if err != nil {
+		http.Error(w, fmt.Sprintf("Request Body parse error : %v", err), http.StatusBadRequest)
+		return
+	}
+	f, err := os.Open(dc.FilePath)
+	if err != nil {
+		http.Error(w, fmt.Sprintf("failed to open dump file %v : %v", dc.FilePath, err), http.StatusNotFound)
+		return
+	}
+	conv, err := conversion.SchemaConv(dc.Driver, &conversion.IOStreams{In: f, Out: os.Stdout}, 0)
+	if err != nil {
+		http.Error(w, fmt.Sprintf("Schema Conversion Error : %v", err), http.StatusNotFound)
+		return
+	}
+	sessionState.conv = conv
+	sessionState.driver = dc.Driver
+	sessionState.dbName = ""
+	sessionState.sessionFile = ""
+	sessionState.sourceDB = nil
+	w.WriteHeader(http.StatusOK)
+	json.NewEncoder(w).Encode(conv)
+}
+
+// getDDL returns the Spanner DDL for each table in alphabetical order.
+// Unlike internal/convert.go's GetDDL, it does not print tables in a way that
+// respects the parent/child ordering of interleaved tables, also foreign keys
+// and secondary indexes are skipped. This means that getDDL cannot be used to
+// build DDL to send to Spanner.
+func getDDL(w http.ResponseWriter, r *http.Request) {
+	c := ddl.Config{Comments: true, ProtectIds: false}
+	var tables []string
+	for t := range sessionState.conv.SpSchema {
+		tables = append(tables, t)
+	}
+	sort.Strings(tables)
+	ddl := make(map[string]string)
+	for _, t := range tables {
+		ddl[t] = sessionState.conv.SpSchema[t].PrintCreateTable(c)
+	}
+	w.WriteHeader(http.StatusOK)
+	json.NewEncoder(w).Encode(ddl)
+}
+
+// getSummary returns table wise summary of conversion.
+func getSummary(w http.ResponseWriter, r *http.Request) {
+	reports := internal.AnalyzeTables(sessionState.conv, nil)
+	summary := make(map[string]string)
+	for _, t := range reports {
+		var body strings.Builder
+		for _, x := range t.Body {
+			body.WriteString(x.Heading + "\n")
+			for i, l := range x.Lines {
+				body.WriteString(fmt.Sprintf("%d) %s.\n\n", i+1, l))
+			}
+		}
+		summary[t.SrcTable] = body.String()
+	}
+	w.WriteHeader(http.StatusOK)
+	json.NewEncoder(w).Encode(summary)
+}
+
+// getOverview returns the overview of conversion.
+func getOverview(w http.ResponseWriter, r *http.Request) {
+	var buf bytes.Buffer
+	bufWriter := bufio.NewWriter(&buf)
+	internal.GenerateReport(sessionState.driver, sessionState.conv, bufWriter, nil, false, false)
+	bufWriter.Flush()
+	overview := buf.String()
+	w.WriteHeader(http.StatusOK)
+	json.NewEncoder(w).Encode(overview)
+}
+
+// getTypeMap returns the source to Spanner typemap only for the
+// source types used in current conversion.
+func getTypeMap(w http.ResponseWriter, r *http.Request) {
+	if sessionState.conv == nil || sessionState.driver == "" {
+		http.Error(w, fmt.Sprintf("Schema is not converted or Driver is not configured properly. Please retry converting the database to Spanner."), http.StatusNotFound)
+		return
+	}
+	var typeMap map[string][]typeIssue
+	switch sessionState.driver {
+	case "mysql", "mysqldump":
+		typeMap = mysqlTypeMap
+	case "postgres", "pg_dump":
+		typeMap = postgresTypeMap
+	default:
+		http.Error(w, fmt.Sprintf("Driver : '%s' is not supported", sessionState.driver), http.StatusBadRequest)
+		return
+	}
+	// Filter typeMap so it contains just the types SrcSchema uses.
+	filteredTypeMap := make(map[string][]typeIssue)
+	for _, srcTable := range sessionState.conv.SrcSchema {
+		for _, colDef := range srcTable.ColDefs {
+			if _, ok := filteredTypeMap[colDef.Type.Name]; ok {
+				continue
+			}
+			filteredTypeMap[colDef.Type.Name] = typeMap[colDef.Type.Name]
+		}
+	}
+	w.WriteHeader(http.StatusOK)
+	json.NewEncoder(w).Encode(filteredTypeMap)
+}
+
+// setTypeMapGlobal allows to change Spanner type globally.
+// It takes a map from source type to Spanner type and updates
+// the Spanner schema accordingly.
+func setTypeMapGlobal(w http.ResponseWriter, r *http.Request) {
+	reqBody, err := ioutil.ReadAll(r.Body)
+	if err != nil {
+		http.Error(w, fmt.Sprintf("Body Read Error : %v", err), http.StatusInternalServerError)
+		return
+	}
+	var typeMap map[string]string
+	err = json.Unmarshal(reqBody, &typeMap)
+	if err != nil {
+		http.Error(w, fmt.Sprintf("Request Body parse error : %v", err), http.StatusBadRequest)
+		return
+	}
+	// Redo source-to-Spanner typeMap using t (the mapping specified in the http request).
+	// We drive this process by iterating over the Spanner schema because we want to preserve all
+	// other customizations that have been performed via the UI (dropping columns, renaming columns
+	// etc). In particular, note that we can't just blindly redo schema conversion (using an appropriate
+	// version of 'toDDL' with the new typeMap).
+	for t, spSchema := range sessionState.conv.SpSchema {
+		for col, _ := range spSchema.ColDefs {
+			srcTable := sessionState.conv.ToSource[t].Name
+			srcCol := sessionState.conv.ToSource[t].Cols[col]
+			srcColDef := sessionState.conv.SrcSchema[srcTable].ColDefs[srcCol]
+			// If the srcCol's type is in the map, then recalculate the Spanner type
+			// for this column using the map. Otherwise, leave the ColDef for this
+			// column as is. Note that per-column type overrides could be lost in
+			// this process -- the mapping in typeMap always takes precendence.
+			if _, found := typeMap[srcColDef.Type.Name]; found {
+				updateType(typeMap[srcColDef.Type.Name], t, col, srcTable, w)
+			}
+		}
+	}
+	updateSessionFile()
+	w.WriteHeader(http.StatusOK)
+	json.NewEncoder(w).Encode(sessionState.conv)
+}
+
+// Actions to be performed on a column.
+// (1) Removed: true/false
+// (2) Rename: New name or empty string
+// (3) PK: "ADDED", "REMOVED" or ""
+// (4) NotNull: "ADDED", "REMOVED" or ""
+// (5) ToType: New type or empty string
+type updateCol struct {
+	Removed bool   `json:"Removed"`
+	Rename  string `json:"Rename"`
+	PK      string `json:"PK"`
+	NotNull string `json:"NotNull"`
+	ToType  string `json:"ToType"`
+}
+
+type updateTable struct {
+	UpdateCols map[string]updateCol `json:"UpdateCols"`
+}
+
+// updateTableSchema updates the Spanner schema.
+// Following actions can be performed on a specified table:
+// (1) Remove column
+// (2) Rename column
+// (3) Add or Remove Primary Key
+// (4) Add or Remove NotNull constraint
+// (5) Update Spanner type
+func updateTableSchema(w http.ResponseWriter, r *http.Request) {
+	reqBody, err := ioutil.ReadAll(r.Body)
+	if err != nil {
+		http.Error(w, fmt.Sprintf("Body Read Error : %v", err), http.StatusInternalServerError)
+		return
+	}
+	var t updateTable
+	table := r.FormValue("table")
+	err = json.Unmarshal(reqBody, &t)
+	if err != nil {
+		http.Error(w, fmt.Sprintf("Request Body parse error : %v", err), http.StatusBadRequest)
+		return
+	}
+	srcTableName := sessionState.conv.ToSource[table].Name
+	for colName, v := range t.UpdateCols {
+		if v.Removed {
+			err, status := canRemoveColumn(colName, table)
+			if err != nil {
+				err = rollback(err)
+				http.Error(w, fmt.Sprintf("%v", err), status)
+				return
+			}
+			removeColumn(table, colName, srcTableName)
+			continue
+		}
+		if v.Rename != "" && v.Rename != colName {
+			err, status := canRenameOrChangeType(colName, table)
+			if err != nil {
+				err = rollback(err)
+				http.Error(w, fmt.Sprintf("%v", err), status)
+				return
+			}
+			renameColumn(v.Rename, table, colName, srcTableName)
+			colName = v.Rename
+		}
+		if v.PK != "" {
+			http.Error(w, "HarbourBridge currently doesn't support editing primary keys", http.StatusNotImplemented)
+			return
+		}
+
+		if v.ToType != "" {
+			typeChange, err := isTypeChanged(v.ToType, table, colName, srcTableName)
+			if err != nil {
+				http.Error(w, err.Error(), http.StatusBadRequest)
+				return
+			}
+
+			if typeChange {
+				err, status := canRenameOrChangeType(colName, table)
+				if err != nil {
+					err = rollback(err)
+					http.Error(w, fmt.Sprintf("%v", err), status)
+					return
+				}
+				updateType(v.ToType, table, colName, srcTableName, w)
+			}
+		}
+		if v.NotNull != "" {
+			updateNotNull(v.NotNull, table, colName)
+		}
+	}
+	updateSessionFile()
+	w.WriteHeader(http.StatusOK)
+	json.NewEncoder(w).Encode(sessionState.conv)
+}
+
+// getConversionRate returns table wise color coded conversion rate.
+func getConversionRate(w http.ResponseWriter, r *http.Request) {
+	reports := internal.AnalyzeTables(sessionState.conv, nil)
+	rate := make(map[string]string)
+	for _, t := range reports {
+		rate[t.SpTable] = rateSchema(t.Cols, t.Warnings, t.SyntheticPKey != "")
+	}
+	w.WriteHeader(http.StatusOK)
+	json.NewEncoder(w).Encode(rate)
+}
+
+// getSchemaFile generates schema file and returns file path.
+func getSchemaFile(w http.ResponseWriter, r *http.Request) {
+	ioHelper := &conversion.IOStreams{In: os.Stdin, Out: os.Stdout}
+	var err error
+	now := time.Now()
+	filePrefix, err := getFilePrefix(now)
+	if err != nil {
+		http.Error(w, fmt.Sprintf("Can not get file prefix : %v", err), http.StatusInternalServerError)
+	}
+	schemaFileName := "frontend/" + filePrefix + "schema.txt"
+	conversion.WriteSchemaFile(sessionState.conv, now, schemaFileName, ioHelper.Out)
+	schemaAbsPath, err := filepath.Abs(schemaFileName)
+	if err != nil {
+		http.Error(w, fmt.Sprintf("Can not create absolute path : %v", err), http.StatusInternalServerError)
+	}
+	w.WriteHeader(http.StatusOK)
+	w.Write([]byte(schemaAbsPath))
+}
+
+// getReportFile generates report file and returns file path.
+func getReportFile(w http.ResponseWriter, r *http.Request) {
+	ioHelper := &conversion.IOStreams{In: os.Stdin, Out: os.Stdout}
+	var err error
+	now := time.Now()
+	filePrefix, err := getFilePrefix(now)
+	if err != nil {
+		http.Error(w, fmt.Sprintf("Can not get file prefix : %v", err), http.StatusInternalServerError)
+	}
+	reportFileName := "frontend/" + filePrefix + "report.txt"
+	conversion.Report(sessionState.driver, nil, ioHelper.BytesRead, "", sessionState.conv, reportFileName, ioHelper.Out)
+	reportAbsPath, err := filepath.Abs(reportFileName)
+	if err != nil {
+		http.Error(w, fmt.Sprintf("Can not create absolute path : %v", err), http.StatusInternalServerError)
+	}
+	w.WriteHeader(http.StatusOK)
+	w.Write([]byte(reportAbsPath))
+}
+
+type TableInterleaveStatus struct {
+	Possible bool
+	Parent   string
+	Comment  string
+}
+
+// setParentTable checks whether specified table can be
+// interleaved, if yes then it sets the Parent table for the specified
+// table and returns Parent table name, otherwise returns the issue.
+func setParentTable(w http.ResponseWriter, r *http.Request) {
+	table := r.FormValue("table")
+	update := r.FormValue("update") == "true"
+	if sessionState.conv == nil || sessionState.driver == "" {
+		http.Error(w, fmt.Sprintf("Schema is not converted or Driver is not configured properly. Please retry converting the database to Spanner."), http.StatusNotFound)
+		return
+	}
+	if table == "" {
+		http.Error(w, fmt.Sprintf("Table name is empty"), http.StatusBadRequest)
+	}
+	tableInterleaveIssues := parentTableHelper(table, update)
+	updateSessionFile()
+	w.WriteHeader(http.StatusOK)
+	json.NewEncoder(w).Encode(tableInterleaveIssues)
+}
+
+func parentTableHelper(table string, update bool) *TableInterleaveStatus {
+	tableInterleaveIssues := &TableInterleaveStatus{Possible: true}
+	if _, found := sessionState.conv.SyntheticPKeys[table]; found {
+		tableInterleaveIssues.Possible = false
+		tableInterleaveIssues.Comment = "Has synthetic pk"
+	}
+	if tableInterleaveIssues.Possible == true {
+		// Search this table's foreign keys for a suitable parent table.
+		// If there are several possible parent tables, we pick the first one.
+		// TODO: Allow users to pick which parent to use if more than one.
+		for i, fk := range sessionState.conv.SpSchema[table].Fks {
+			refTable := fk.ReferTable
+			if _, found := sessionState.conv.SyntheticPKeys[refTable]; found {
+				continue
+			}
+			ok := checkPrimaryKeyPrefix(table, refTable, fk, tableInterleaveIssues)
+			if ok {
+				tableInterleaveIssues.Parent = refTable
+				if update {
+					sp := sessionState.conv.SpSchema[table]
+					sp.Parent = refTable
+					sp.Fks = removeFk(sp.Fks, i)
+					sessionState.conv.SpSchema[table] = sp
+				}
+				break
+			}
+		}
+		if tableInterleaveIssues.Parent == "" {
+			tableInterleaveIssues.Possible = false
+			tableInterleaveIssues.Comment = "No valid prefix"
+		}
+	}
+	return tableInterleaveIssues
+}
+
+func dropForeignKey(w http.ResponseWriter, r *http.Request) {
+	table := r.FormValue("table")
+	pos := r.FormValue("pos")
+	if sessionState.conv == nil || sessionState.driver == "" {
+		http.Error(w, fmt.Sprintf("Schema is not converted or Driver is not configured properly. Please retry converting the database to Spanner."), http.StatusNotFound)
+		return
+	}
+	if table == "" || pos == "" {
+		http.Error(w, fmt.Sprintf("Table name or position is empty"), http.StatusBadRequest)
+	}
+	sp := sessionState.conv.SpSchema[table]
+	position, err := strconv.Atoi(pos)
+	if err != nil {
+		http.Error(w, fmt.Sprintf("Error converting position to integer"), http.StatusBadRequest)
+		return
+	}
+	if position < 0 || position >= len(sp.Fks) {
+		http.Error(w, fmt.Sprintf("No foreign key found at position %d", position), http.StatusBadRequest)
+		return
+	}
+	sp.Fks = removeFk(sp.Fks, position)
+	sessionState.conv.SpSchema[table] = sp
+	updateSessionFile()
+	w.WriteHeader(http.StatusOK)
+	json.NewEncoder(w).Encode(sessionState.conv)
+}
+
+func renameForeignKey(w http.ResponseWriter, r *http.Request) {
+	table := r.FormValue("table")
+	pos := r.FormValue("pos")
+	newName := r.FormValue("name")
+	if sessionState.conv == nil || sessionState.driver == "" {
+		http.Error(w, fmt.Sprintf("Schema is not converted or Driver is not configured properly. Please retry converting the database to Spanner."), http.StatusNotFound)
+		return
+	}
+	if table == "" || pos == "" || newName == "" {
+		http.Error(w, fmt.Sprintf("Table name or position or new name is empty"), http.StatusBadRequest)
+	}
+	sp := sessionState.conv.SpSchema[table]
+	position, err := strconv.Atoi(pos)
+	if err != nil {
+		http.Error(w, fmt.Sprintf("Error converting position to integer"), http.StatusBadRequest)
+		return
+	}
+	if position < 0 || position >= len(sp.Fks) {
+		http.Error(w, fmt.Sprintf("No foreign key found at position %d", position), http.StatusBadRequest)
+		return
+	}
+	if !isUniqueName(newName) {
+		http.Error(w, fmt.Sprintf("New name %s is used before in the schema", newName), http.StatusBadRequest)
+		return
+	}
+	sp.Fks[position].Name = newName
+	sessionState.conv.SpSchema[table] = sp
+	updateSessionFile()
+	w.WriteHeader(http.StatusOK)
+	json.NewEncoder(w).Encode(sessionState.conv)
+}
+
+func dropSecondaryIndex(w http.ResponseWriter, r *http.Request) {
+	table := r.FormValue("table")
+	pos := r.FormValue("pos")
+	if sessionState.conv == nil || sessionState.driver == "" {
+		http.Error(w, fmt.Sprintf("Schema is not converted or Driver is not configured properly. Please retry converting the database to Spanner."), http.StatusNotFound)
+		return
+	}
+	if table == "" || pos == "" {
+		http.Error(w, fmt.Sprintf("Table name or position is empty"), http.StatusBadRequest)
+	}
+	sp := sessionState.conv.SpSchema[table]
+	position, err := strconv.Atoi(pos)
+	if err != nil {
+		http.Error(w, fmt.Sprintf("Error converting position to integer"), http.StatusBadRequest)
+		return
+	}
+	if position < 0 || position >= len(sp.Indexes) {
+		http.Error(w, fmt.Sprintf("No secondary index found at position %d", position), http.StatusBadRequest)
+		return
+	}
+	sp.Indexes = removeSecondaryIndex(sp.Indexes, position)
+	sessionState.conv.SpSchema[table] = sp
+	updateSessionFile()
+	w.WriteHeader(http.StatusOK)
+	json.NewEncoder(w).Encode(sessionState.conv)
+}
+
+// updateSessionFile updates the content of session file with
+// latest sessionState.conv.
+func updateSessionFile() error {
+	filePath := sessionState.sessionFile
+	if filePath == "" {
+		return fmt.Errorf("Session file path is empty")
+	}
+	f, err := os.OpenFile(filePath, os.O_RDWR|os.O_CREATE, 0644)
+	if err != nil {
+		return err
+	}
+	convJSON, err := json.MarshalIndent(sessionState.conv, "", " ")
+	if err != nil {
+		return err
+	}
+	err = f.Truncate(0)
+	if err != nil {
+		return err
+	}
+	if _, err := f.Write(convJSON); err != nil {
+		return err
+	}
+	return nil
+}
+
+// rollback is used to get previous state of conversion in case
+// some unexpected error occurs during update operations.
+func rollback(err error) error {
+	if sessionState.sessionFile == "" {
+		return fmt.Errorf("encountered error %w. rollback failed because we don't have a session file", err)
+	}
+	sessionState.conv = internal.MakeConv()
+	err2 := conversion.ReadSessionFile(sessionState.conv, sessionState.sessionFile)
+	if err2 != nil {
+		return fmt.Errorf("encountered error %w. rollback failed: %v", err, err2)
+	}
+	return err
+}
+
+func isPartOfPK(col, table string) bool {
+	for _, pk := range sessionState.conv.SpSchema[table].Pks {
+		if pk.Col == col {
+			return true
+		}
+	}
+	return false
+}
+
+func isParent(table string) (bool, string) {
+	for _, spSchema := range sessionState.conv.SpSchema {
+		if spSchema.Parent == table {
+			return true, spSchema.Name
+		}
+	}
+	return false, ""
+}
+
+func isPartOfSecondaryIndex(col, table string) (bool, string) {
+	for _, index := range sessionState.conv.SpSchema[table].Indexes {
+		for _, key := range index.Keys {
+			if key.Col == col {
+				return true, index.Name
+			}
+		}
+	}
+	return false, ""
+}
+
+func isPartOfFK(col, table string) bool {
+	for _, fk := range sessionState.conv.SpSchema[table].Fks {
+		for _, column := range fk.Columns {
+			if column == col {
+				return true
+			}
+		}
+	}
+	return false
+}
+
+// TODO: create a map to store referenced column to get
+// this information in O(1).
+//TODO:(searce) can have foreign key constraints between columns of the same table, as well as between same column on a given table.
+func isReferencedByFK(col, table string) (bool, string) {
+	for _, spSchema := range sessionState.conv.SpSchema {
+		if table != spSchema.Name {
+			for _, fk := range spSchema.Fks {
+				if fk.ReferTable == table {
+					for _, column := range fk.ReferColumns {
+						if column == col {
+							return true, spSchema.Name
+						}
+					}
+				}
+			}
+		}
+	}
+	return false, ""
+}
+
+func canRemoveColumn(colName, table string) (error, int) {
+	if isPartOfPK := isPartOfPK(colName, table); isPartOfPK {
+		return fmt.Errorf("column is part of primary key"), http.StatusBadRequest
+	}
+	if isPartOfSecondaryIndex, _ := isPartOfSecondaryIndex(colName, table); isPartOfSecondaryIndex {
+		return fmt.Errorf("column is part of secondary index, remove secondary index before making the update"), http.StatusPreconditionFailed
+	}
+	isPartOfFK := isPartOfFK(colName, table)
+	isReferencedByFK, _ := isReferencedByFK(colName, table)
+	if isPartOfFK || isReferencedByFK {
+		return fmt.Errorf("column is part of foreign key relation, remove foreign key constraint before making the update"), http.StatusPreconditionFailed
+	}
+	return nil, http.StatusOK
+}
+
+func canRenameOrChangeType(colName, table string) (error, int) {
+	isPartOfPK := isPartOfPK(colName, table)
+	isParent, childSchema := isParent(table)
+	isChild := sessionState.conv.SpSchema[table].Parent != ""
+	if isPartOfPK && (isParent || isChild) {
+		return fmt.Errorf("Column : '%s' in table : '%s' is part of parent-child relation with schema : '%s'", colName, table, childSchema), http.StatusBadRequest
+	}
+	if isPartOfSecondaryIndex, indexName := isPartOfSecondaryIndex(colName, table); isPartOfSecondaryIndex {
+		return fmt.Errorf("Column : '%s' in table : '%s' is part of secondary index : '%s', remove secondary index before making the update",
+			colName, table, indexName), http.StatusPreconditionFailed
+	}
+	isPartOfFK := isPartOfFK(colName, table)
+	isReferencedByFK, relationTable := isReferencedByFK(colName, table)
+	if isPartOfFK || isReferencedByFK {
+		if isReferencedByFK {
+			return fmt.Errorf("Column : '%s' in table : '%s' is part of foreign key relation with table : '%s', remove foreign key constraint before making the update",
+				colName, table, relationTable), http.StatusPreconditionFailed
+		} else {
+			return fmt.Errorf("Column : '%s' in table : '%s' is part of foreign keys, remove foreign key constraint before making the update",
+				colName, table), http.StatusPreconditionFailed
+		}
+	}
+	return nil, http.StatusOK
+}
+
+func checkPrimaryKeyPrefix(table string, refTable string, fk ddl.Foreignkey, tableInterleaveIssues *TableInterleaveStatus) bool {
+	childPks := sessionState.conv.SpSchema[table].Pks
+	parentPks := sessionState.conv.SpSchema[refTable].Pks
+	if len(childPks) >= len(parentPks) {
+		for i, pk := range parentPks {
+			if i >= len(fk.ReferColumns) || pk.Col != fk.ReferColumns[i] || pk.Col != childPks[i].Col || fk.Columns[i] != fk.ReferColumns[i] {
+				return false
+			}
+		}
+	} else {
+		return false
+	}
+	return true
+}
+
+func isUniqueName(name string) bool {
+	for table, _ := range sessionState.conv.SpSchema {
+		if table == name {
+			return false
+		}
+	}
+	for _, spSchema := range sessionState.conv.SpSchema {
+		for _, fk := range spSchema.Fks {
+			if fk.Name == name {
+				return false
+			}
+		}
+		for _, index := range spSchema.Indexes {
+			if index.Name == name {
+				return false
+			}
+		}
+	}
+	return true
+}
+
+func remove(slice []string, s int) []string {
+	return append(slice[:s], slice[s+1:]...)
+}
+
+func removePk(slice []ddl.IndexKey, s int) []ddl.IndexKey {
+	return append(slice[:s], slice[s+1:]...)
+}
+
+func removeFk(slice []ddl.Foreignkey, s int) []ddl.Foreignkey {
+	return append(slice[:s], slice[s+1:]...)
+}
+
+func removeSecondaryIndex(slice []ddl.CreateIndex, s int) []ddl.CreateIndex {
+	return append(slice[:s], slice[s+1:]...)
+}
+
+func removeColumn(table string, colName string, srcTableName string) {
+	sp := sessionState.conv.SpSchema[table]
+	for i, col := range sp.ColNames {
+		if col == colName {
+			sp.ColNames = remove(sp.ColNames, i)
+			break
+		}
+	}
+	delete(sp.ColDefs, colName)
+	for i, pk := range sp.Pks {
+		if pk.Col == colName {
+			sp.Pks = removePk(sp.Pks, i)
+			break
+		}
+	}
+	srcColName := sessionState.conv.ToSource[table].Cols[colName]
+	delete(sessionState.conv.ToSource[table].Cols, colName)
+	delete(sessionState.conv.ToSpanner[srcTableName].Cols, srcColName)
+	delete(sessionState.conv.Issues[srcTableName], srcColName)
+	sessionState.conv.SpSchema[table] = sp
+}
+
+func renameColumn(newName, table, colName, srcTableName string) {
+	sp := sessionState.conv.SpSchema[table]
+	for i, col := range sp.ColNames {
+		if col == colName {
+			sp.ColNames[i] = newName
+			break
+		}
+	}
+	if _, found := sp.ColDefs[colName]; found {
+		sp.ColDefs[newName] = ddl.ColumnDef{
+			Name:    newName,
+			T:       sp.ColDefs[colName].T,
+			NotNull: sp.ColDefs[colName].NotNull,
+			Comment: sp.ColDefs[colName].Comment,
+		}
+		delete(sp.ColDefs, colName)
+	}
+	for i, pk := range sp.Pks {
+		if pk.Col == colName {
+			sp.Pks[i].Col = newName
+			break
+		}
+	}
+	srcColName := sessionState.conv.ToSource[table].Cols[colName]
+	sessionState.conv.ToSpanner[srcTableName].Cols[srcColName] = newName
+	sessionState.conv.ToSource[table].Cols[newName] = srcColName
+	delete(sessionState.conv.ToSource[table].Cols, colName)
+	sessionState.conv.SpSchema[table] = sp
+}
+
+func updateType(newType, table, colName, srcTableName string, w http.ResponseWriter) {
+	sp, ty, err := getType(newType, table, colName, srcTableName)
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusBadRequest)
+		return
+	}
+	colDef := sp.ColDefs[colName]
+	colDef.T = ty
+	sp.ColDefs[colName] = colDef
+}
+
+func isTypeChanged(newType, table, colName, srcTableName string) (bool, error) {
+	sp, ty, err := getType(newType, table, colName, srcTableName)
+	if err != nil {
+		return false, err
+	}
+	colDef := sp.ColDefs[colName]
+	return !reflect.DeepEqual(colDef.T, ty), nil
+}
+
+func getType(newType, table, colName string, srcTableName string) (ddl.CreateTable, ddl.Type, error) {
+	sp := sessionState.conv.SpSchema[table]
+	srcColName := sessionState.conv.ToSource[table].Cols[colName]
+	srcCol := sessionState.conv.SrcSchema[srcTableName].ColDefs[srcColName]
+	var ty ddl.Type
+	var issues []internal.SchemaIssue
+	switch sessionState.driver {
+	case "mysql", "mysqldump":
+		ty, issues = toSpannerTypeMySQL(srcCol.Type.Name, newType, srcCol.Type.Mods)
+	case "pg_dump", "postgres":
+		ty, issues = toSpannerTypePostgres(srcCol.Type.Name, newType, srcCol.Type.Mods)
+	default:
+		return sp, ty, fmt.Errorf("driver : '%s' is not supported", sessionState.driver)
+	}
+	if len(srcCol.Type.ArrayBounds) > 1 {
+		ty = ddl.Type{Name: ddl.String, Len: ddl.MaxLength}
+		issues = append(issues, internal.MultiDimensionalArray)
+	}
+	if srcCol.Ignored.Default {
+		issues = append(issues, internal.DefaultValue)
+	}
+	if srcCol.Ignored.AutoIncrement {
+		issues = append(issues, internal.AutoIncrement)
+	}
+	if sessionState.conv.Issues != nil && len(issues) > 0 {
+		sessionState.conv.Issues[srcTableName][srcCol.Name] = issues
+	}
+	ty.IsArray = len(srcCol.Type.ArrayBounds) == 1
+	return sp, ty, nil
+}
+
+func updateNotNull(notNullChange, table, colName string) {
+	sp := sessionState.conv.SpSchema[table]
+	switch notNullChange {
+	case "ADDED":
+		spColDef := sp.ColDefs[colName]
+		spColDef.NotNull = true
+		sp.ColDefs[colName] = spColDef
+	case "REMOVED":
+		spColDef := sp.ColDefs[colName]
+		spColDef.NotNull = false
+		sp.ColDefs[colName] = spColDef
+	}
+}
+
+func rateSchema(cols, warnings int64, missingPKey bool) string {
+	good := func(total, badCount int64) bool { return badCount < total/20 }
+	ok := func(total, badCount int64) bool { return badCount < total/3 }
+	switch {
+	case cols == 0:
+		return "GRAY"
+	case warnings == 0 && !missingPKey:
+		return "GREEN"
+	case warnings == 0 && missingPKey:
+		return "BLUE"
+	case good(cols, warnings) && !missingPKey:
+		return "BLUE"
+	case good(cols, warnings) && missingPKey:
+		return "BLUE"
+	case ok(cols, warnings) && !missingPKey:
+		return "YELLOW"
+	case ok(cols, warnings) && missingPKey:
+		return "YELLOW"
+	case !missingPKey:
+		return "ORANGE"
+	default:
+		return "ORANGE"
+	}
+}
+
+func getFilePrefix(now time.Time) (string, error) {
+	dbName := sessionState.dbName
+	var err error
+	if dbName == "" {
+		dbName, err = conversion.GetDatabaseName(sessionState.driver, now)
+		if err != nil {
+			return "", fmt.Errorf("Can not create database name : %v", err)
+		}
+	}
+	return dbName + ".", nil
+}
+
+type SessionState struct {
+	sourceDB    *sql.DB        // Connection to source database in case of direct connection
+	dbName      string         // Name of source database
+	driver      string         // Name of HarbourBridge driver in use
+	conv        *internal.Conv // Current conversion state
+	sessionFile string         // Path to session file
+}
+
+// sessionState maintains the current state of the session, and is used to
+// track state from one request to the next. Session state is global:
+// all requests see the same session state.
+var sessionState SessionState
+
+// Type and issue.
+type typeIssue struct {
+	T     string
+	Brief string
+}
+
+func addTypeToList(convertedType string, spType string, issues []internal.SchemaIssue, l []typeIssue) []typeIssue {
+	if convertedType == spType {
+		if len(issues) > 0 {
+			var briefs []string
+			for _, issue := range issues {
+				briefs = append(briefs, internal.IssueDB[issue].Brief)
+			}
+			l = append(l, typeIssue{T: spType, Brief: fmt.Sprintf(strings.Join(briefs, ", "))})
+		} else {
+			l = append(l, typeIssue{T: spType})
+		}
+	}
+	return l
+}
+func init() {
+	// Initialize mysqlTypeMap.
+	for _, srcType := range []string{"bool", "boolean", "varchar", "char", "text", "tinytext", "mediumtext", "longtext", "set", "enum", "json", "bit", "binary", "varbinary", "blob", "tinyblob", "mediumblob", "longblob", "tinyint", "smallint", "mediumint", "int", "integer", "bigint", "double", "float", "numeric", "decimal", "date", "datetime", "timestamp", "time", "year"} {
+		var l []typeIssue
+		for _, spType := range []string{ddl.Bool, ddl.Bytes, ddl.Date, ddl.Float64, ddl.Int64, ddl.String, ddl.Timestamp, ddl.Numeric} {
+			ty, issues := toSpannerTypeMySQL(srcType, spType, []int64{})
+			l = addTypeToList(ty.Name, spType, issues, l)
+		}
+		if srcType == "tinyint" {
+			l = append(l, typeIssue{T: ddl.Bool, Brief: "Only tinyint(1) can be converted to BOOL, for any other mods it will be converted to INT64"})
+		}
+		mysqlTypeMap[srcType] = l
+	}
+	// Initialize postgresTypeMap.
+	for _, srcType := range []string{"bool", "boolean", "bigserial", "bpchar", "character", "bytea", "date", "float8", "double precision", "float4", "real", "int8", "bigint", "int4", "integer", "int2", "smallint", "numeric", "serial", "text", "timestamptz", "timestamp with time zone", "timestamp", "timestamp without time zone", "varchar", "character varying"} {
+		var l []typeIssue
+		for _, spType := range []string{ddl.Bool, ddl.Bytes, ddl.Date, ddl.Float64, ddl.Int64, ddl.String, ddl.Timestamp, ddl.Numeric} {
+			ty, issues := toSpannerTypePostgres(srcType, spType, []int64{})
+			l = addTypeToList(ty.Name, spType, issues, l)
+		}
+		postgresTypeMap[srcType] = l
+	}
+}
+
+func WebApp() {
+	addr := ":8080"
+	router := getRoutes()
+	log.Printf("Starting server at port 8080\n")
+	log.Fatal(http.ListenAndServe(addr, handlers.CORS(handlers.AllowedHeaders([]string{"X-Requested-With", "Content-Type", "Authorization"}), handlers.AllowedMethods([]string{"GET", "POST", "PUT", "HEAD", "OPTIONS"}), handlers.AllowedOrigins([]string{"*"}))(router)))
+}


Test patch:
diff --git a/integration_test.go b/integration_test.go
index 3caecc5..f4bb89a 100644
--- a/integration_test.go
+++ b/integration_test.go
@@ -32,6 +32,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/conversion"
 	"google.golang.org/api/iterator"
 	databasepb "google.golang.org/genproto/googleapis/spanner/admin/database/v1"
 )
@@ -123,7 +124,7 @@ func TestIntegration_PGDUMP_SimpleUse(t *testing.T) {
 	defer os.RemoveAll(tmpdir)
 
 	now := time.Now()
-	dbName, _ := getDatabaseName(PGDUMP, 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+".")
@@ -131,7 +132,7 @@ func TestIntegration_PGDUMP_SimpleUse(t *testing.T) {
 	if err != nil {
 		t.Fatalf("failed to open the test data file: %v", err)
 	}
-	err = toSpanner(PGDUMP, projectID, instanceID, dbName, &ioStreams{in: f, out: os.Stdout}, filePrefix, now)
+	err = commandLine(conversion.PGDUMP, projectID, instanceID, dbName, &conversion.IOStreams{In: f, Out: os.Stdout}, filePrefix, now)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -148,7 +149,7 @@ func TestIntegration_PGDUMP_Command(t *testing.T) {
 	defer os.RemoveAll(tmpdir)
 
 	now := time.Now()
-	dbName, _ := getDatabaseName(PGDUMP, 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"
@@ -184,11 +185,11 @@ func TestIntegration_POSTGRES_SimpleUse(t *testing.T) {
 	defer os.RemoveAll(tmpdir)
 
 	now := time.Now()
-	dbName, _ := getDatabaseName(POSTGRES, now)
+	dbName, _ := conversion.GetDatabaseName(conversion.POSTGRES, now)
 	dbPath := fmt.Sprintf("projects/%s/instances/%s/databases/%s", projectID, instanceID, dbName)
 	filePrefix = filepath.Join(tmpdir, dbName+".")
 
-	err := toSpanner(POSTGRES, projectID, instanceID, dbName, &ioStreams{out: os.Stdout}, filePrefix, now)
+	err := commandLine(conversion.POSTGRES, projectID, instanceID, dbName, &conversion.IOStreams{Out: os.Stdout}, filePrefix, now)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -206,11 +207,11 @@ func TestIntegration_POSTGRES_Command(t *testing.T) {
 	defer os.RemoveAll(tmpdir)
 
 	now := time.Now()
-	dbName, _ := getDatabaseName(POSTGRES, now)
+	dbName, _ := conversion.GetDatabaseName(conversion.POSTGRES, now)
 	dbPath := fmt.Sprintf("projects/%s/instances/%s/databases/%s", projectID, instanceID, 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, POSTGRES))
+	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
 	cmd.Stdout = &out
 	cmd.Stderr = &stderr
diff --git a/internal/convert_test.go b/internal/convert_test.go
index e00a097..16c8e05 100644
--- a/internal/convert_test.go
+++ b/internal/convert_test.go
@@ -67,18 +67,32 @@ func TestGetDDL(t *testing.T) {
 		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})
@@ -87,8 +101,10 @@ func TestGetDDL(t *testing.T) {
 		"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)
 }
diff --git a/mysql/report_test.go b/mysql/report_test.go
index fca0b0f..b0e6425 100644
--- a/mysql/report_test.go
+++ b/mysql/report_test.go
@@ -60,7 +60,7 @@ func TestReport(t *testing.T) {
 	conv.Stats.Unexpected["Testing unexpected messages"] = 5
 	buf := new(bytes.Buffer)
 	w := bufio.NewWriter(buf)
-	internal.GenerateReport("mysqldump", conv, w, badWrites)
+	internal.GenerateReport("mysqldump", conv, w, badWrites, true, true)
 	w.Flush()
 	// Print copy of report to stdout (shows up when running go test -v).
 	fmt.Print(buf.String())
diff --git a/postgres/report_test.go b/postgres/report_test.go
index 8409986..d4ca9fc 100644
--- a/postgres/report_test.go
+++ b/postgres/report_test.go
@@ -56,7 +56,7 @@ func TestReport(t *testing.T) {
 	conv.Stats.Unexpected["Testing unexpected messages"] = 5
 	buf := new(bytes.Buffer)
 	w := bufio.NewWriter(buf)
-	internal.GenerateReport("pg_dump", conv, w, badWrites)
+	internal.GenerateReport("pg_dump", conv, w, badWrites, true, true)
 	w.Flush()
 	// Print copy of report to stdout (shows up when running go test -v).
 	fmt.Print(buf.String())
diff --git a/spanner/ddl/ast_test.go b/spanner/ddl/ast_test.go
index b5cd243..f43fda7 100644
--- a/spanner/ddl/ast_test.go
+++ b/spanner/ddl/ast_test.go
@@ -79,7 +79,7 @@ func TestPrintCreateTable(t *testing.T) {
 	cds["col1"] = ColumnDef{Name: "col1", T: Type{Name: Int64}, NotNull: true}
 	cds["col2"] = ColumnDef{Name: "col2", T: Type{Name: String, Len: MaxLength}, NotNull: false}
 	cds["col3"] = ColumnDef{Name: "col3", T: Type{Name: Bytes, Len: int64(42)}, NotNull: false}
-	ct := CreateTable{
+	t1 := CreateTable{
 		"mytable",
 		[]string{"col1", "col2", "col3"},
 		cds,
@@ -87,17 +87,30 @@ func TestPrintCreateTable(t *testing.T) {
 		nil,
 		nil,
 		"",
+		"",
+	}
+	t2 := CreateTable{
+		"mytable",
+		[]string{"col1", "col2", "col3"},
+		cds,
+		[]IndexKey{IndexKey{Col: "col1", Desc: true}},
+		nil,
+		nil,
+		"parent",
+		"",
 	}
 	tests := []struct {
 		name       string
 		protectIds bool
 		expected   string
+		ct         CreateTable
 	}{
-		{"no quote", false, "CREATE TABLE mytable (col1 INT64 NOT NULL, col2 STRING(MAX), col3 BYTES(42)) PRIMARY KEY (col1 DESC)"},
-		{"quote", true, "CREATE TABLE `mytable` (`col1` INT64 NOT NULL, `col2` STRING(MAX), `col3` BYTES(42)) PRIMARY KEY (`col1` DESC)"},
+		{"no quote", false, "CREATE TABLE mytable (col1 INT64 NOT NULL, col2 STRING(MAX), col3 BYTES(42)) PRIMARY KEY (col1 DESC)", t1},
+		{"quote", true, "CREATE TABLE `mytable` (`col1` INT64 NOT NULL, `col2` STRING(MAX), `col3` BYTES(42)) PRIMARY KEY (`col1` DESC)", t1},
+		{"interleaved", false, "CREATE TABLE mytable (col1 INT64 NOT NULL, col2 STRING(MAX), col3 BYTES(42)) PRIMARY KEY (col1 DESC),\nINTERLEAVE IN PARENT parent", t2},
 	}
 	for _, tc := range tests {
-		assert.Equal(t, normalizeSpace(tc.expected), normalizeSpace(ct.PrintCreateTable(Config{ProtectIds: tc.protectIds})))
+		assert.Equal(t, normalizeSpace(tc.expected), normalizeSpace(tc.ct.PrintCreateTable(Config{ProtectIds: tc.protectIds})))
 	}
 }
 
diff --git a/web/web_test.go b/web/web_test.go
new file mode 100644
index 0000000..0291ce8
--- /dev/null
+++ b/web/web_test.go
@@ -0,0 +1,1693 @@
+// 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 web
+
+import (
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"net/http/httptest"
+	"strings"
+	"testing"
+
+	"github.com/cloudspannerecosystem/harbourbridge/internal"
+	"github.com/cloudspannerecosystem/harbourbridge/schema"
+	"github.com/cloudspannerecosystem/harbourbridge/spanner/ddl"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestGetTypeMapNoDriver(t *testing.T) {
+	req, err := http.NewRequest("GET", "/typemap", nil)
+	if err != nil {
+		t.Fatal(err)
+	}
+	rr := httptest.NewRecorder()
+	handler := http.HandlerFunc(getTypeMap)
+	handler.ServeHTTP(rr, req)
+	if status := rr.Code; status != http.StatusNotFound {
+		t.Errorf("handler returned wrong status code: got %v want %v",
+			status, http.StatusNotFound)
+	}
+
+}
+
+func TestGetTypeMapPostgres(t *testing.T) {
+	sessionState.driver = "postgres"
+	sessionState.conv = internal.MakeConv()
+	buildConvPostgres(sessionState.conv)
+	req, err := http.NewRequest("GET", "/typemap", nil)
+	if err != nil {
+		t.Fatal(err)
+	}
+	rr := httptest.NewRecorder()
+	handler := http.HandlerFunc(getTypeMap)
+	handler.ServeHTTP(rr, req)
+	var typemap map[string][]typeIssue
+	json.Unmarshal(rr.Body.Bytes(), &typemap)
+	if status := rr.Code; status != http.StatusOK {
+		t.Errorf("handler returned wrong status code: got %v want %v",
+			status, http.StatusOK)
+	}
+	expectedTypemap := map[string][]typeIssue{
+		"bool": []typeIssue{
+			typeIssue{T: ddl.Bool},
+			typeIssue{T: ddl.Int64, Brief: internal.IssueDB[internal.Widened].Brief},
+			typeIssue{T: ddl.String, Brief: internal.IssueDB[internal.Widened].Brief}},
+		"bigserial": []typeIssue{
+			typeIssue{T: ddl.Int64, Brief: internal.IssueDB[internal.Serial].Brief},
+			typeIssue{T: ddl.String, Brief: internal.IssueDB[internal.Widened].Brief + ", " + internal.IssueDB[internal.Serial].Brief}},
+		"bpchar": []typeIssue{
+			typeIssue{T: ddl.Bytes},
+			typeIssue{T: ddl.String}},
+		"bytea": []typeIssue{
+			typeIssue{T: ddl.Bytes},
+			typeIssue{T: ddl.String}},
+		"date": []typeIssue{
+			typeIssue{T: ddl.Date},
+			typeIssue{T: ddl.String, Brief: internal.IssueDB[internal.Widened].Brief}},
+		"float8": []typeIssue{
+			typeIssue{T: ddl.Float64},
+			typeIssue{T: ddl.String, Brief: internal.IssueDB[internal.Widened].Brief}},
+		"float4": []typeIssue{
+			typeIssue{T: ddl.Float64, Brief: internal.IssueDB[internal.Widened].Brief},
+			typeIssue{T: ddl.String, Brief: internal.IssueDB[internal.Widened].Brief}},
+		"int8": []typeIssue{
+			typeIssue{T: ddl.Int64},
+			typeIssue{T: ddl.String, Brief: internal.IssueDB[internal.Widened].Brief}},
+		"int4": []typeIssue{
+			typeIssue{T: ddl.Int64, Brief: internal.IssueDB[internal.Widened].Brief},
+			typeIssue{T: ddl.String, Brief: internal.IssueDB[internal.Widened].Brief}},
+		"numeric": []typeIssue{
+			typeIssue{T: ddl.String, Brief: internal.IssueDB[internal.Widened].Brief},
+			typeIssue{T: ddl.Numeric}},
+		"serial": []typeIssue{
+			typeIssue{T: ddl.Int64, Brief: internal.IssueDB[internal.Serial].Brief},
+			typeIssue{T: ddl.String, Brief: internal.IssueDB[internal.Widened].Brief + ", " + internal.IssueDB[internal.Serial].Brief}},
+		"text": []typeIssue{
+			typeIssue{T: ddl.Bytes},
+			typeIssue{T: ddl.String}},
+		"timestamptz": []typeIssue{
+			typeIssue{T: ddl.String, Brief: internal.IssueDB[internal.Widened].Brief},
+			typeIssue{T: ddl.Timestamp}},
+		"timestamp": []typeIssue{
+			typeIssue{T: ddl.String, Brief: internal.IssueDB[internal.Widened].Brief},
+			typeIssue{T: ddl.Timestamp, Brief: internal.IssueDB[internal.Timestamp].Brief}},
+		"varchar": []typeIssue{
+			typeIssue{T: ddl.Bytes},
+			typeIssue{T: ddl.String}},
+	}
+	assert.Equal(t, expectedTypemap, typemap)
+
+}
+
+func TestUpdateTableSchema(t *testing.T) {
+	tc := []struct {
+		name         string
+		table        string
+		payload      string
+		statusCode   int64
+		conv         *internal.Conv
+		expectedConv *internal.Conv
+	}{
+		{
+			name:  "Test remove fail column part of PK",
+			table: "t1",
+			payload: `
+    {
+      "UpdateCols":{
+		"a": { "Removed": true }
+	}
+    }`,
+			statusCode: http.StatusBadRequest,
+			conv: &internal.Conv{
+				SpSchema: map[string]ddl.CreateTable{
+					"t1": ddl.CreateTable{
+						Name:     "t1",
+						ColNames: []string{"a", "b"},
+						ColDefs: map[string]ddl.ColumnDef{
+							"a": ddl.ColumnDef{Name: "a", T: ddl.Type{Name: ddl.String, Len: ddl.MaxLength}},
+							"b": ddl.ColumnDef{Name: "b", T: ddl.Type{Name: ddl.String, Len: ddl.MaxLength}},
+						},
+						Pks: []ddl.IndexKey{ddl.IndexKey{Col: "a"}},
+					}},
+				ToSource: map[string]internal.NameAndCols{
+					"t1": internal.NameAndCols{Name: "t1", Cols: map[string]string{"a": "a", "b": "b"}},
+				},
+			},
+		},
+		{
+			name:  "Test remove fail column part of secondary index",
+			table: "t1",
+			payload: `
+    {
+      "UpdateCols":{
+		"b": { "Removed": true }
+	}
+    }`,
+			statusCode: http.StatusPreconditionFailed,
+			conv: &internal.Conv{
+				SpSchema: map[string]ddl.CreateTable{
+					"t1": ddl.CreateTable{
+						Name:     "t1",
+						ColNames: []string{"a", "b"},
+						ColDefs: map[string]ddl.ColumnDef{
+							"a": ddl.ColumnDef{Name: "a", T: ddl.Type{Name: ddl.String, Len: ddl.MaxLength}},
+							"b": ddl.ColumnDef{Name: "b", T: ddl.Type{Name: ddl.String, Len: ddl.MaxLength}},
+						},
+						Pks:     []ddl.IndexKey{ddl.IndexKey{Col: "a"}},
+						Indexes: []ddl.CreateIndex{ddl.CreateIndex{Name: "idx", Table: "t1", Unique: false, Keys: []ddl.IndexKey{ddl.IndexKey{Col: "b", Desc: false}}}},
+					}},
+				ToSource: map[string]internal.NameAndCols{
+					"t1": internal.NameAndCols{Name: "t1", Cols: map[string]string{"a": "a", "b": "b"}},
+				},
+			},
+		},
+		{
+			name:  "Test remove fail column part of FK",
+			table: "t1",
+			payload: `
+    {
+      "UpdateCols":{
+		"b": { "Removed": true }
+	}
+    }`,
+			statusCode: http.StatusPreconditionFailed,
+			conv: &internal.Conv{
+				SpSchema: map[string]ddl.CreateTable{
+					"t1": ddl.CreateTable{
+						Name:     "t1",
+						ColNames: []string{"a", "b"},
+						ColDefs: map[string]ddl.ColumnDef{
+							"a": ddl.ColumnDef{Name: "a", T: ddl.Type{Name: ddl.String, Len: ddl.MaxLength}},
+							"b": ddl.ColumnDef{Name: "b", T: ddl.Type{Name: ddl.String, Len: ddl.MaxLength}},
+						},
+						Pks: []ddl.IndexKey{ddl.IndexKey{Col: "a"}},
+						Fks: []ddl.Foreignkey{ddl.Foreignkey{Name: "fk1", Columns: []string{"b"}, ReferTable: "t2", ReferColumns: []string{"b"}}},
+					}},
+				ToSource: map[string]internal.NameAndCols{
+					"t1": internal.NameAndCols{Name: "t1", Cols: map[string]string{"a": "a", "b": "b"}},
+				},
+			},
+		},
+		{
+			name:  "Test remove fail column referenced by FK",
+			table: "t1",
+			payload: `
+    {
+      "UpdateCols":{
+		"b": { "Removed": true }
+	}
+    }`,
+			statusCode: http.StatusPreconditionFailed,
+			conv: &internal.Conv{
+				SpSchema: map[string]ddl.CreateTable{
+					"t1": ddl.CreateTable{
+						Name:     "t1",
+						ColNames: []string{"a", "b"},
+						ColDefs: map[string]ddl.ColumnDef{
+							"a": ddl.ColumnDef{Name: "a", T: ddl.Type{Name: ddl.String, Len: ddl.MaxLength}},
+							"b": ddl.ColumnDef{Name: "b", T: ddl.Type{Name: ddl.String, Len: ddl.MaxLength}},
+						},
+						Pks: []ddl.IndexKey{ddl.IndexKey{Col: "a"}},
+					},
+					"t2": ddl.CreateTable{
+						Name:     "t2",
+						ColNames: []string{"aa", "bb"},
+						ColDefs: map[string]ddl.ColumnDef{
+							"aa": ddl.ColumnDef{Name: "aa", T: ddl.Type{Name: ddl.String, Len: ddl.MaxLength}},
+							"bb": ddl.ColumnDef{Name: "bb", T: ddl.Type{Name: ddl.String, Len: ddl.MaxLength}},
+						},
+						Pks: []ddl.IndexKey{ddl.IndexKey{Col: "aa"}},
+						Fks: []ddl.Foreignkey{ddl.Foreignkey{Name: "fk1", Columns: []string{"bb"}, ReferTable: "t1", ReferColumns: []string{"b"}}},
+					},
+				},
+				ToSource: map[string]internal.NameAndCols{
+					"t1": internal.NameAndCols{Name: "t1", Cols: map[string]string{"a": "a", "b": "b"}},
+				},
+			},
+		},
+		{
+			name:  "Test remove success",
+			table: "t1",
+			payload: `
+    {
+      "UpdateCols":{
+		"c": { "Removed": true }
+	}
+    }`,
+			statusCode: http.StatusOK,
+			conv: &internal.Conv{
+				SpSchema: map[string]ddl.CreateTable{
+					"t1": ddl.CreateTable{
+						Name:     "t1",
+						ColNames: []string{"a", "b", "c"},
+						ColDefs: map[string]ddl.ColumnDef{
+							"a": ddl.ColumnDef{Name: "a", T: ddl.Type{Name: ddl.String, Len: ddl.MaxLength}},
+							"b": ddl.ColumnDef{Name: "b", T: ddl.Type{Name: ddl.String, Len: ddl.MaxLength}},
+							"c": ddl.ColumnDef{Name: "c", T: ddl.Type{Name: ddl.Int64}},
+						},
+						Pks: []ddl.IndexKey{ddl.IndexKey{Col: "a"}},
+					}},
+				Issues: map[string]map[string][]internal.SchemaIssue{
+					"t1": map[string][]internal.SchemaIssue{
+						"c": []internal.SchemaIssue{internal.Widened},
+					},
+				},
+				ToSource: map[string]internal.NameAndCols{
+					"t1": internal.NameAndCols{Name: "t1", Cols: map[string]string{"a": "a", "b": "b", "c": "c"}},
+				},
+				ToSpanner: map[string]internal.NameAndCols{
+					"t1": internal.NameAndCols{Name: "t1", Cols: map[string]string{"a": "a", "b": "b", "c": "c"}},
+				},
+			},
+			expectedConv: &internal.Conv{
+				SpSchema: map[string]ddl.CreateTable{
+					"t1": ddl.CreateTable{
+						Name:     "t1",
+						ColNames: []string{"a", "b"},
+						ColDefs: map[string]ddl.ColumnDef{
+							"a": ddl.ColumnDef{Name: "a", T: ddl.Type{Name: ddl.String, Len: ddl.MaxLength}},
+							"b": ddl.ColumnDef{Name: "b", T: ddl.Type{Name: ddl.String, Len: ddl.MaxLength}},
+						},
+						Pks: []ddl.IndexKey{ddl.IndexKey{Col: "a"}},
+					}},
+				Issues: map[string]map[string][]internal.SchemaIssue{
+					"t1": map[string][]internal.SchemaIssue{},
+				},
+				ToSource: map[string]internal.NameAndCols{
+					"t1": internal.NameAndCols{Name: "t1", Cols: map[string]string{"a": "a", "b": "b"}},
+				},
+				ToSpanner: map[string]internal.NameAndCols{
+					"t1": internal.NameAndCols{Name: "t1", Cols: map[string]string{"a": "a", "b": "b"}},
+				},
+			},
+		},
+		{
+			name:  "Test rename fail column part of PK and child table",
+			table: "t1",
+			payload: `
+    {
+      "UpdateCols":{
+		"b": { "Rename": "bb" }
+	}
+    }`,
+			statusCode: http.StatusBadRequest,
+			conv: &internal.Conv{
+				SpSchema: map[string]ddl.CreateTable{
+					"t1": ddl.CreateTable{
+						Name:     "t1",
+						ColNames: []string{"a", "b", "c"},
+						ColDefs: map[string]ddl.ColumnDef{
+							"a": ddl.ColumnDef{Name: "a", T: ddl.Type{Name: ddl.String, Len: ddl.MaxLength}},
+							"b": ddl.ColumnDef{Name: "b", T: ddl.Type{Name: ddl.String, Len: ddl.MaxLength}},
+							"c": ddl.ColumnDef{Name: "c", T: ddl.Type{Name: ddl.String, Len: ddl.MaxLength}},
+						},
+						Pks:    []ddl.IndexKey{ddl.IndexKey{Col: "a"}, ddl.IndexKey{Col: "b"}},
+						Parent: "t2",
+					}},
+				ToSource: map[string]internal.NameAndCols{
+					"t1": internal.NameAndCols{Name: "t1", Cols: map[string]string{"a": "a", "b": "b", "c": "c"}},
+				},
+			},
+		},
+		{
+			name:  "Test rename fail column part of PK and parent table",
+			table: "t1",
+			payload: `
+		{
+		  "UpdateCols":{
+			"a": { "Rename": "aa" }
+		}
+		}`,
+			statusCode: http.StatusBadRequest,
+			conv: &internal.Conv{
+				SpSchema: map[string]ddl.CreateTable{
+					"t1": ddl.CreateTable{
+						Name:     "t1",
+						ColNames: []string{"a", "b"},
+						ColDefs: map[string]ddl.ColumnDef{
+							"a": ddl.ColumnDef{Name: "a", T: ddl.Type{Name: ddl.String, Len: ddl.MaxLength}},
+							"b": ddl.ColumnDef{Name: "b", T: ddl.Type{Name: ddl.String, Len: ddl.MaxLength}},
+						},
+						Pks: []ddl.IndexKey{ddl.IndexKey{Col: "a"}},
+					},
+					"t2": ddl.CreateTable{
+						Name:     "t2",
+						ColNames: []string{"a", "b", "c"},
+						ColDefs: map[string]ddl.ColumnDef{
+							"a": ddl.ColumnDef{Name: "a", T: ddl.Type{Name: ddl.String, Len: ddl.MaxLength}},
+							"b": ddl.ColumnDef{Name: "b", T: ddl.Type{Name: ddl.String, Len: ddl.MaxLength}},
+							"c": ddl.ColumnDef{Name: "c", T: ddl.Type{Name: ddl.String, Len: ddl.MaxLength}},
+						},
+						Pks:    []ddl.IndexKey{ddl.IndexKey{Col: "a"}, ddl.IndexKey{Col: "b"}},
+						Parent: "t1",
+					},
+				},
+				ToSource: map[string]internal.NameAndCols{
+					"t1": internal.NameAndCols{Name: "t1", Cols: map[string]string{"a": "a", "b": "b"}},
+					"t2": internal.NameAndCols{Name: "t2", Cols: map[string]string{"a": "a", "b": "b", "c": "c"}},
+				},
+			},
+		},
+		{
+			name:  "Test rename fail column part of secondary index",
+			table: "t1",
+			payload: `
+		{
+		  "UpdateCols":{
+			"b": { "Rename": "bb" }
+		}
+		}`,
+			statusCode: http.StatusPreconditionFailed,
+			conv: &internal.Conv{
+				SpSchema: map[string]ddl.CreateTable{
+					"t1": ddl.CreateTable{
+						Name:     "t1",
+						ColNames: []string{"a", "b"},
+						ColDefs: map[string]ddl.ColumnDef{
+							"a": ddl.ColumnDef{Name: "a", T: ddl.Type{Name: ddl.String, Len: ddl.MaxLength}},
+							"b": ddl.ColumnDef{Name: "b", T: ddl.Type{Name: ddl.String, Len: ddl.MaxLength}},
+						},
+						Pks:     []ddl.IndexKey{ddl.IndexKey{Col: "a"}},
+						Indexes: []ddl.CreateIndex{ddl.CreateIndex{Name: "idx", Table: "t1", Unique: false, Keys: []ddl.IndexKey{ddl.IndexKey{Col: "b", Desc: false}}}},
+					}},
+				ToSource: map[string]internal.NameAndCols{
+					"t1": internal.NameAndCols{Name: "t1", Cols: map[string]string{"a": "a", "b": "b"}},
+				},
+			},
+		},
+		{
+			name:  "Test rename fail column part of FK",
+			table: "t1",
+			payload: `
+		{
+		  "UpdateCols":{
+			"b": { "Rename": "bb" }
+		}
+		}`,
+			statusCode: http.StatusPreconditionFailed,
+			conv: &internal.Conv{
+				SpSchema: map[string]ddl.CreateTable{
+					"t1": ddl.CreateTable{
+						Name:     "t1",
+						ColNames: []string{"a", "b"},
+						ColDefs: map[string]ddl.ColumnDef{
+							"a": ddl.ColumnDef{Name: "a", T: ddl.Type{Name: ddl.String, Len: ddl.MaxLength}},
+							"b": ddl.ColumnDef{Name: "b", T: ddl.Type{Name: ddl.String, Len: ddl.MaxLength}},
+						},
+						Pks: []ddl.IndexKey{ddl.IndexKey{Col: "a"}},
+						Fks: []ddl.Foreignkey{ddl.Foreignkey{Name: "fk1", Columns: []string{"b"}, ReferTable: "t2", ReferColumns: []string{"b"}}},
+					}},
+				ToSource: map[string]internal.NameAndCols{
+					"t1": internal.NameAndCols{Name: "t1", Cols: map[string]string{"a": "a", "b": "b"}},
+				},
+			},
+		},
+		{
+			name:  "Test rename fail column referenced by FK",
+			table: "t1",
+			payload: `
+		{
+		  "UpdateCols":{
+			"b": { "Rename": "bb" }
+		}
+		}`,
+			statusCode: http.StatusPreconditionFailed,
+			conv: &internal.Conv{
+				SpSchema: map[string]ddl.CreateTable{
+					"t1": ddl.CreateTable{
+						Name:     "t1",
+						ColNames: []string{"a", "b"},
+						ColDefs: map[string]ddl.ColumnDef{
+							"a": ddl.ColumnDef{Name: "a", T: ddl.Type{Name: ddl.String, Len: ddl.MaxLength}},
+							"b": ddl.ColumnDef{Name: "b", T: ddl.Type{Name: ddl.String, Len: ddl.MaxLength}},
+						},
+						Pks: []ddl.IndexKey{ddl.IndexKey{Col: "a"}},
+					},
+					"t2": ddl.CreateTable{
+						Name:     "t2",
+						ColNames: []string{"aa", "bb"},
+						ColDefs: map[string]ddl.ColumnDef{
+							"aa": ddl.ColumnDef{Name: "aa", T: ddl.Type{Name: ddl.String, Len: ddl.MaxLength}},
+							"bb": ddl.ColumnDef{Name: "bb", T: ddl.Type{Name: ddl.String, Len: ddl.MaxLength}},
+						},
+						Pks: []ddl.IndexKey{ddl.IndexKey{Col: "aa"}},
+						Fks: []ddl.Foreignkey{ddl.Foreignkey{Name: "fk1", Columns: []string{"bb"}, ReferTable: "t1", ReferColumns: []string{"b"}}},
+					},
+				},
+				ToSource: map[string]internal.NameAndCols{
+					"t1": internal.NameAndCols{Name: "t1", Cols: map[string]string{"a": "a", "b": "b"}},
+				},
+			},
+		},
+		{
+			name:  "Test rename success",
+			table: "t1",
+			payload: `
+    {
+      "UpdateCols":{
+		"a": { "Rename": "aa" }
+	}
+    }`,
+			statusCode: http.StatusOK,
+			conv: &internal.Conv{
+				SpSchema: map[string]ddl.CreateTable{
+					"t1": ddl.CreateTable{
+						Name:     "t1",
+						ColNames: []string{"a", "b", "c"},
+						ColDefs: map[string]ddl.ColumnDef{
+							"a": ddl.ColumnDef{Name: "a", T: ddl.Type{Name: ddl.String, Len: ddl.MaxLength}},
+							"b": ddl.ColumnDef{Name: "b", T: ddl.Type{Name: ddl.String, Len: ddl.MaxLength}},
+							"c": ddl.ColumnDef{Name: "c", T: ddl.Type{Name: ddl.Int64}},
+						},
+						Pks: []ddl.IndexKey{ddl.IndexKey{Col: "a"}},
+					}},
+				ToSource: map[string]internal.NameAndCols{
+					"t1": internal.NameAndCols{Name: "t1", Cols: map[string]string{"a": "a", "b": "b", "c": "c"}},
+				},
+				ToSpanner: map[string]internal.NameAndCols{
+					"t1": internal.NameAndCols{Name: "t1", Cols: map[string]string{"a": "a", "b": "b", "c": "c"}},
+				},
+			},
+			expectedConv: &internal.Conv{
+				SpSchema: map[string]ddl.CreateTable{
+					"t1": ddl.CreateTable{
+						Name:     "t1",
+						ColNames: []string{"aa", "b", "c"},
+						ColDefs: map[string]ddl.ColumnDef{
+							"aa": ddl.ColumnDef{Name: "aa", T: ddl.Type{Name: ddl.String, Len: ddl.MaxLength}},
+							"b":  ddl.ColumnDef{Name: "b", T: ddl.Type{Name: ddl.String, Len: ddl.MaxLength}},
+							"c":  ddl.ColumnDef{Name: "c", T: ddl.Type{Name: ddl.Int64}},
+						},
+						Pks: []ddl.IndexKey{ddl.IndexKey{Col: "aa"}},
+					}},
+				ToSource: map[string]internal.NameAndCols{
+					"t1": internal.NameAndCols{Name: "t1", Cols: map[string]string{"aa": "a", "b": "b", "c": "c"}},
+				},
+				ToSpanner: map[string]internal.NameAndCols{
+					"t1": internal.NameAndCols{Name: "t1", Cols: map[string]string{"a": "aa", "b": "b", "c": "c"}},
+				},
+			},
+		},
+		{
+			name:  "Test change type success",
+			table: "t1",
+			payload: `
+    {
+      "UpdateCols":{
+		"a": { "ToType": "STRING" },
+		"b": { "ToType": "BYTES" }
+	}
+    }`,
+			statusCode: http.StatusOK,
+			conv: &internal.Conv{
+				SpSchema: map[string]ddl.CreateTable{
+					"t1": ddl.CreateTable{
+						Name:     "t1",
+						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.String, Len: 6}},
+						},
+						Pks: []ddl.IndexKey{ddl.IndexKey{Col: "a"}},
+					}},
+				SrcSchema: map[string]schema.Table{
+					"t1": schema.Table{
+						Name:     "t1",
+						ColNames: []string{"a", "b"},
+						ColDefs: map[string]schema.Column{
+							"a": schema.Column{Name: "a", Type: schema.Type{Name: "bigint", Mods: []int64{}}},
+							"b": schema.Column{Name: "b", Type: schema.Type{Name: "varchar", Mods: []int64{6}}},
+						},
+						PrimaryKeys: []schema.Key{schema.Key{Column: "a"}},
+					}},
+				ToSource: map[string]internal.NameAndCols{
+					"t1": internal.NameAndCols{Name: "t1", Cols: map[string]string{"a": "a", "b": "b"}},
+				},
+				ToSpanner: map[string]internal.NameAndCols{
+					"t1": internal.NameAndCols{Name: "t1", Cols: map[string]string{"a": "a", "b": "b"}},
+				},
+				Issues: map[string]map[string][]internal.SchemaIssue{
+					"t1": map[string][]internal.SchemaIssue{},
+				},
+			},
+			expectedConv: &internal.Conv{
+				SpSchema: map[string]ddl.CreateTable{
+					"t1": ddl.CreateTable{
+						Name:     "t1",
+						ColNames: []string{"a", "b"},
+						ColDefs: map[string]ddl.ColumnDef{
+							"a": ddl.ColumnDef{Name: "a", T: ddl.Type{Name: ddl.String, Len: ddl.MaxLength}},
+							"b": ddl.ColumnDef{Name: "b", T: ddl.Type{Name: ddl.Bytes, Len: 6}},
+						},
+						Pks: []ddl.IndexKey{ddl.IndexKey{Col: "a"}},
+					}},
+				SrcSchema: map[string]schema.Table{
+					"t1": schema.Table{
+						Name:     "t1",
+						ColNames: []string{"a", "b"},
+						ColDefs: map[string]schema.Column{
+							"a": schema.Column{Name: "a", Type: schema.Type{Name: "bigint", Mods: []int64{}}},
+							"b": schema.Column{Name: "b", Type: schema.Type{Name: "varchar", Mods: []int64{6}}},
+						},
+						PrimaryKeys: []schema.Key{schema.Key{Column: "a"}},
+					}},
+				ToSource: map[string]internal.NameAndCols{
+					"t1": internal.NameAndCols{Name: "t1", Cols: map[string]string{"a": "a", "b": "b"}},
+				},
+				ToSpanner: map[string]internal.NameAndCols{
+					"t1": internal.NameAndCols{Name: "t1", Cols: map[string]string{"a": "a", "b": "b"}},
+				},
+				Issues: map[string]map[string][]internal.SchemaIssue{
+					"t1": map[string][]internal.SchemaIssue{
+						"a": []internal.SchemaIssue{internal.Widened},
+					},
+				},
+			},
+		},
+		{
+			name:  "Test add or remove not null",
+			table: "t1",
+			payload: `
+    {
+      "UpdateCols":{
+		"b": { "NotNull": "ADDED" },
+		"c": { "NotNull": "REMOVED" }
+	}
+    }`,
+			statusCode: http.StatusOK,
+			conv: &internal.Conv{
+				SpSchema: map[string]ddl.CreateTable{
+					"t1": ddl.CreateTable{
+						Name:     "t1",
+						ColNames: []string{"a", "b", "c"},
+						ColDefs: map[string]ddl.ColumnDef{
+							"a": ddl.ColumnDef{Name: "a", T: ddl.Type{Name: ddl.String, Len: ddl.MaxLength}},
+							"b": ddl.ColumnDef{Name: "b", T: ddl.Type{Name: ddl.String, Len: ddl.MaxLength}},
+							"c": ddl.ColumnDef{Name: "c", T: ddl.Type{Name: ddl.String, Len: ddl.MaxLength}, NotNull: true},
+						},
+						Pks: []ddl.IndexKey{ddl.IndexKey{Col: "a"}},
+					}},
+				ToSource: map[string]internal.NameAndCols{
+					"t1": internal.NameAndCols{Name: "t1", Cols: map[string]string{"a": "a", "b": "b", "c": "c"}},
+				},
+			},
+			expectedConv: &internal.Conv{
+				SpSchema: map[string]ddl.CreateTable{
+					"t1": ddl.CreateTable{
+						Name:     "t1",
+						ColNames: []string{"a", "b", "c"},
+						ColDefs: map[string]ddl.ColumnDef{
+							"a": ddl.ColumnDef{Name: "a", T: ddl.Type{Name: ddl.String, Len: ddl.MaxLength}},
+							"b": ddl.ColumnDef{Name: "b", T: ddl.Type{Name: ddl.String, Len: ddl.MaxLength}, NotNull: true},
+							"c": ddl.ColumnDef{Name: "c", T: ddl.Type{Name: ddl.String, Len: ddl.MaxLength}},
+						},
+						Pks: []ddl.IndexKey{ddl.IndexKey{Col: "a"}},
+					}},
+				ToSource: map[string]internal.NameAndCols{
+					"t1": internal.NameAndCols{Name: "t1", Cols: map[string]string{"a": "a", "b": "b", "c": "c"}},
+				},
+			},
+		},
+	}
+	for _, tc := range tc {
+		sessionState.driver = "mysql"
+		sessionState.conv = tc.conv
+		payload := tc.payload
+		req, err := http.NewRequest("POST", "/typemap/table?table="+tc.table, strings.NewReader(payload))
+		if err != nil {
+			t.Fatal(err)
+		}
+		req.Header.Set("Content-Type", "application/json")
+		rr := httptest.NewRecorder()
+		handler := http.HandlerFunc(updateTableSchema)
+		handler.ServeHTTP(rr, req)
+		var res *internal.Conv
+		json.Unmarshal(rr.Body.Bytes(), &res)
+		if status := rr.Code; int64(status) != tc.statusCode {
+			t.Errorf("handler returned wrong status code: got %v want %v",
+				status, tc.statusCode)
+		}
+		if tc.statusCode == http.StatusOK {
+			assert.Equal(t, tc.expectedConv, res)
+		}
+	}
+}
+
+func TestSetTypeMapGlobalLevelPostgres(t *testing.T) {
+	tc := []struct {
+		name           string
+		payload        string
+		statusCode     int64
+		expectedSchema ddl.CreateTable
+		expectedIssues map[string][]internal.SchemaIssue
+	}{
+		{
+			name: "Test type change",
+			payload: `
+    {
+      	"bool":"STRING",
+		"int8":"STRING",
+		"float4":"STRING",
+		"varchar":"BYTES",
+		"numeric":"STRING",
+		"timestamptz":"STRING",
+		"bigserial":"STRING",
+		"bpchar":"BYTES",
+		"bytea":"STRING",
+		"date":"STRING",
+		"float8":"STRING",
+		"int4":"STRING",
+		"serial":"STRING",
+		"text":"BYTES",
+		"timestamp":"STRING"
+    }`,
+			statusCode: http.StatusOK,
+			expectedSchema: ddl.CreateTable{
+				Name:     "t1",
+				ColNames: []string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p"},
+				ColDefs: map[string]ddl.ColumnDef{
+					"a": ddl.ColumnDef{Name: "a", T: ddl.Type{Name: ddl.String, Len: ddl.MaxLength}},
+					"b": ddl.ColumnDef{Name: "b", T: ddl.Type{Name: ddl.String, Len: ddl.MaxLength}},
+					"c": ddl.ColumnDef{Name: "c", T: ddl.Type{Name: ddl.String, Len: ddl.MaxLength}},
+					"d": ddl.ColumnDef{Name: "d", T: ddl.Type{Name: ddl.Bytes, Len: 6}},
+					"e": ddl.ColumnDef{Name: "e", T: ddl.Type{Name: ddl.String, Len: ddl.MaxLength}},
+					"f": ddl.ColumnDef{Name: "f", T: ddl.Type{Name: ddl.String, Len: ddl.MaxLength}},
+					"g": ddl.ColumnDef{Name: "g", T: ddl.Type{Name: ddl.String, Len: ddl.MaxLength}},
+					"h": ddl.ColumnDef{Name: "h", T: ddl.Type{Name: ddl.Bytes, Len: int64(1)}},
+					"i": ddl.ColumnDef{Name: "i", T: ddl.Type{Name: ddl.String, Len: ddl.MaxLength}},
+					"j": ddl.ColumnDef{Name: "j", T: ddl.Type{Name: ddl.String, Len: ddl.MaxLength}},
+					"k": ddl.ColumnDef{Name: "k", T: ddl.Type{Name: ddl.String, Len: ddl.MaxLength}},
+					"l": ddl.ColumnDef{Name: "l", T: ddl.Type{Name: ddl.String, Len: ddl.MaxLength}},
+					"m": ddl.ColumnDef{Name: "m", T: ddl.Type{Name: ddl.String, Len: ddl.MaxLength}},
+					"n": ddl.ColumnDef{Name: "n", T: ddl.Type{Name: ddl.Bytes, Len: ddl.MaxLength}},
+					"o": ddl.ColumnDef{Name: "o", T: ddl.Type{Name: ddl.String, Len: ddl.MaxLength}},
+					"p": ddl.ColumnDef{Name: "p", T: ddl.Type{Name: ddl.String, Len: ddl.MaxLength}},
+				},
+				Pks: []ddl.IndexKey{ddl.IndexKey{Col: "a"}},
+			},
+			expectedIssues: map[string][]internal.SchemaIssue{
+				"a": []internal.SchemaIssue{internal.Widened},
+				"b": []internal.SchemaIssue{internal.Widened},
+				"c": []internal.SchemaIssue{internal.Widened},
+				"e": []internal.SchemaIssue{internal.Widened},
+				"f": []internal.SchemaIssue{internal.Widened},
+				"g": []internal.SchemaIssue{internal.Widened, internal.Serial},
+				"j": []internal.SchemaIssue{internal.Widened},
+				"k": []internal.SchemaIssue{internal.Widened},
+				"l": []internal.SchemaIssue{internal.Widened},
+				"m": []internal.SchemaIssue{internal.Widened, internal.Serial},
+				"o": []internal.SchemaIssue{internal.Widened},
+				"p": []internal.SchemaIssue{internal.Widened},
+			},
+		},
+		{
+			name: "Test type change 2",
+			payload: `
+    {
+      	"bool":"INT64",
+		"int8":"STRING",
+		"float4":"STRING"
+    }`,
+			statusCode: http.StatusOK,
+			expectedSchema: ddl.CreateTable{
+				Name:     "t1",
+				ColNames: []string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p"},
+				ColDefs: map[string]ddl.ColumnDef{
+					"a": ddl.ColumnDef{Name: "a", T: ddl.Type{Name: ddl.String, Len: ddl.MaxLength}},
+					"b": ddl.ColumnDef{Name: "b", T: ddl.Type{Name: ddl.String, Len: ddl.MaxLength}},
+					"c": ddl.ColumnDef{Name: "c", T: ddl.Type{Name: ddl.Int64}},
+					"d": ddl.ColumnDef{Name: "d", T: ddl.Type{Name: ddl.String, Len: int64(6)}},
+					"e": ddl.ColumnDef{Name: "e", T: ddl.Type{Name: ddl.Numeric}},
+					"f": ddl.ColumnDef{Name: "f", T: ddl.Type{Name: ddl.Timestamp}},
+					"g": ddl.ColumnDef{Name: "g", T: ddl.Type{Name: ddl.Int64}},
+					"h": ddl.ColumnDef{Name: "h", T: ddl.Type{Name: ddl.String, Len: int64(1)}},
+					"i": ddl.ColumnDef{Name: "i", T: ddl.Type{Name: ddl.Bytes, Len: ddl.MaxLength}},
+					"j": ddl.ColumnDef{Name: "j", T: ddl.Type{Name: ddl.Date}},
+					"k": ddl.ColumnDef{Name: "k", T: ddl.Type{Name: ddl.Float64}},
+					"l": ddl.ColumnDef{Name: "l", T: ddl.Type{Name: ddl.Int64}},
+					"m": ddl.ColumnDef{Name: "m", T: ddl.Type{Name: ddl.Int64}},
+					"n": ddl.ColumnDef{Name: "n", T: ddl.Type{Name: ddl.String, Len: ddl.MaxLength}},
+					"o": ddl.ColumnDef{Name: "o", T: ddl.Type{Name: ddl.Timestamp}},
+					"p": ddl.ColumnDef{Name: "p", T: ddl.Type{Name: ddl.Int64}},
+				},
+				Pks: []ddl.IndexKey{ddl.IndexKey{Col: "a"}},
+			},
+			expectedIssues: map[string][]internal.SchemaIssue{
+				"a": []internal.SchemaIssue{internal.Widened},
+				"b": []internal.SchemaIssue{internal.Widened},
+				"c": []internal.SchemaIssue{internal.Widened},
+				"g": []internal.SchemaIssue{internal.Serial},
+				"l": []internal.SchemaIssue{internal.Widened},
+				"m": []internal.SchemaIssue{internal.Serial},
+				"o": []internal.SchemaIssue{internal.Timestamp},
+				"p": []internal.SchemaIssue{internal.Widened},
+			},
+		},
+		{
+			name: "Test bad request",
+			payload: `
+    {
+      	"bool":"INT64",
+		"int8":"STRING",
+		"float4":"STRING",
+    }`,
+			statusCode: http.StatusBadRequest,
+		},
+	}
+	for _, tc := range tc {
+		sessionState.driver = "postgres"
+		sessionState.conv = internal.MakeConv()
+		buildConvPostgres(sessionState.conv)
+		payload := tc.payload
+		req, err := http.NewRequest("POST", "/typemap/global", strings.NewReader(payload))
+		if err != nil {
+			t.Fatal(err)
+		}
+		req.Header.Set("Content-Type", "application/json")
+		rr := httptest.NewRecorder()
+		handler := http.HandlerFunc(setTypeMapGlobal)
+		handler.ServeHTTP(rr, req)
+		var res *internal.Conv
+		json.Unmarshal(rr.Body.Bytes(), &res)
+		if status := rr.Code; int64(status) != tc.statusCode {
+			t.Errorf("handler returned wrong status code: got %v want %v",
+				status, tc.statusCode)
+		}
+
+		if tc.statusCode == http.StatusOK {
+			assert.Equal(t, tc.expectedSchema, res.SpSchema["t1"])
+			assert.Equal(t, tc.expectedIssues, res.Issues["t1"])
+		}
+	}
+
+}
+
+func TestGetConversionPostgres(t *testing.T) {
+	sessionState.driver = "postgres"
+	sessionState.conv = internal.MakeConv()
+	buildConvPostgres(sessionState.conv)
+	req, err := http.NewRequest("GET", "/conversion", nil)
+	if err != nil {
+		t.Fatal(err)
+	}
+	rr := httptest.NewRecorder()
+	handler := http.HandlerFunc(getConversionRate)
+	handler.ServeHTTP(rr, req)
+	var result map[string]string
+	json.Unmarshal(rr.Body.Bytes(), &result)
+	if status := rr.Code; status != http.StatusOK {
+		t.Errorf("handler returned wrong status code: got %v want %v",
+			status, http.StatusOK)
+	}
+	assert.Equal(t, 2, len(result))
+	assert.Contains(t, result, "t1")
+	assert.Contains(t, result, "t2")
+}
+
+func TestGetTypeMapMySQL(t *testing.T) {
+	sessionState.driver = "mysql"
+	sessionState.conv = internal.MakeConv()
+	buildConvMySQL(sessionState.conv)
+	req, err := http.NewRequest("GET", "/typemap", nil)
+	if err != nil {
+		t.Fatal(err)
+	}
+	rr := httptest.NewRecorder()
+	handler := http.HandlerFunc(getTypeMap)
+	handler.ServeHTTP(rr, req)
+	var typemap map[string][]typeIssue
+	json.Unmarshal(rr.Body.Bytes(), &typemap)
+	if status := rr.Code; status != http.StatusOK {
+		t.Errorf("handler returned wrong status code: got %v want %v",
+			status, http.StatusOK)
+	}
+	expectedTypemap := map[string][]typeIssue{
+		"bool": []typeIssue{
+			typeIssue{T: ddl.Bool},
+			typeIssue{T: ddl.Int64, Brief: internal.IssueDB[internal.Widened].Brief},
+			typeIssue{T: ddl.String, Brief: internal.IssueDB[internal.Widened].Brief}},
+		"varchar": []typeIssue{
+			typeIssue{T: ddl.Bytes},
+			typeIssue{T: ddl.String}},
+		"text": []typeIssue{
+			typeIssue{T: ddl.Bytes},
+			typeIssue{T: ddl.String}},
+		"enum": []typeIssue{
+			typeIssue{T: ddl.String}},
+		"json": []typeIssue{
+			typeIssue{T: ddl.Bytes},
+			typeIssue{T: ddl.String}},
+		"binary": []typeIssue{
+			typeIssue{T: ddl.Bytes},
+			typeIssue{T: ddl.String}},
+		"blob": []typeIssue{
+			typeIssue{T: ddl.Bytes},
+			typeIssue{T: ddl.String}},
+		"integer": []typeIssue{
+			typeIssue{T: ddl.Int64, Brief: internal.IssueDB[internal.Widened].Brief},
+			typeIssue{T: ddl.String, Brief: internal.IssueDB[internal.Widened].Brief}},
+		"smallint": []typeIssue{
+			typeIssue{T: ddl.Int64, Brief: internal.IssueDB[internal.Widened].Brief},
+			typeIssue{T: ddl.String, Brief: internal.IssueDB[internal.Widened].Brief}},
+		"double": []typeIssue{
+			typeIssue{T: ddl.Float64},
+			typeIssue{T: ddl.String, Brief: internal.IssueDB[internal.Widened].Brief}},
+		"float": []typeIssue{
+			typeIssue{T: ddl.Float64, Brief: internal.IssueDB[internal.Widened].Brief},
+			typeIssue{T: ddl.String, Brief: internal.IssueDB[internal.Widened].Brief}},
+		"numeric": []typeIssue{
+			typeIssue{T: ddl.String, Brief: internal.IssueDB[internal.Widened].Brief},
+			typeIssue{T: ddl.Numeric}},
+		"decimal": []typeIssue{
+			typeIssue{T: ddl.String, Brief: internal.IssueDB[internal.Widened].Brief},
+			typeIssue{T: ddl.Numeric}},
+		"date": []typeIssue{
+			typeIssue{T: ddl.Date},
+			typeIssue{T: ddl.String, Brief: internal.IssueDB[internal.Widened].Brief}},
+		"timestamp": []typeIssue{
+			typeIssue{T: ddl.String, Brief: internal.IssueDB[internal.Widened].Brief},
+			typeIssue{T: ddl.Timestamp}},
+		"time": []typeIssue{
+			typeIssue{T: ddl.String, Brief: internal.IssueDB[internal.Time].Brief}},
+	}
+	assert.Equal(t, expectedTypemap, typemap)
+
+}
+
+func TestSetTypeMapGlobalLevelMySQL(t *testing.T) {
+	tc := []struct {
+		name           string
+		payload        string
+		statusCode     int64
+		expectedSchema ddl.CreateTable
+		expectedIssues map[string][]internal.SchemaIssue
+	}{
+		{
+			name: "Test type change",
+			payload: `
+    {
+      	"bool":"STRING",
+		"smallint":"STRING",
+		"float":"STRING",
+		"varchar":"BYTES",
+		"numeric":"STRING",
+		"timestamp":"STRING",
+		"decimal":"STRING",
+		"json":"BYTES",
+		"binary":"STRING",
+		"blob":"STRING",
+		"double":"STRING",
+		"date":"STRING",
+		"time":"STRING",
+		"enum":"STRING",
+		"text":"BYTES"
+    }`,
+			statusCode: http.StatusOK,
+			expectedSchema: ddl.CreateTable{
+				Name:     "t1",
+				ColNames: []string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p"},
+				ColDefs: map[string]ddl.ColumnDef{
+					"a": ddl.ColumnDef{Name: "a", T: ddl.Type{Name: ddl.String, Len: ddl.MaxLength}},
+					"b": ddl.ColumnDef{Name: "b", T: ddl.Type{Name: ddl.Bytes, Len: ddl.MaxLength}},
+					"c": ddl.ColumnDef{Name: "c", T: ddl.Type{Name: ddl.String, Len: ddl.MaxLength}},
+					"d": ddl.ColumnDef{Name: "d", T: ddl.Type{Name: ddl.Bytes, Len: 6}},
+					"e": ddl.ColumnDef{Name: "e", T: ddl.Type{Name: ddl.String, Len: ddl.MaxLength}},
+					"f": ddl.ColumnDef{Name: "f", T: ddl.Type{Name: ddl.String, Len: ddl.MaxLength}},
+					"g": ddl.ColumnDef{Name: "g", T: ddl.Type{Name: ddl.Bytes, Len: ddl.MaxLength}},
+					"h": ddl.ColumnDef{Name: "h", T: ddl.Type{Name: ddl.String, Len: ddl.MaxLength}},
+					"i": ddl.ColumnDef{Name: "i", T: ddl.Type{Name: ddl.String, Len: ddl.MaxLength}},
+					"j": ddl.ColumnDef{Name: "j", T: ddl.Type{Name: ddl.String, Len: ddl.MaxLength}},
+					"k": ddl.ColumnDef{Name: "k", T: ddl.Type{Name: ddl.String, Len: ddl.MaxLength}},
+					"l": ddl.ColumnDef{Name: "l", T: ddl.Type{Name: ddl.String, Len: ddl.MaxLength}},
+					"m": ddl.ColumnDef{Name: "m", T: ddl.Type{Name: ddl.String, Len: ddl.MaxLength}},
+					"n": ddl.ColumnDef{Name: "n", T: ddl.Type{Name: ddl.String, Len: ddl.MaxLength}},
+					"o": ddl.ColumnDef{Name: "o", T: ddl.Type{Name: ddl.String, Len: ddl.MaxLength}},
+					"p": ddl.ColumnDef{Name: "p", T: ddl.Type{Name: ddl.String, Len: ddl.MaxLength}},
+				},
+				Pks: []ddl.IndexKey{ddl.IndexKey{Col: "a"}},
+			},
+			expectedIssues: map[string][]internal.SchemaIssue{
+				"a": []internal.SchemaIssue{internal.Widened},
+				"c": []internal.SchemaIssue{internal.Widened},
+				"e": []internal.SchemaIssue{internal.Widened},
+				"j": []internal.SchemaIssue{internal.Widened},
+				"k": []internal.SchemaIssue{internal.Widened},
+				"l": []internal.SchemaIssue{internal.Widened},
+				"m": []internal.SchemaIssue{internal.Widened},
+				"n": []internal.SchemaIssue{internal.Widened},
+				"o": []internal.SchemaIssue{internal.Widened},
+				"p": []internal.SchemaIssue{internal.Time},
+			},
+		},
+		{
+			name: "Test type change 2",
+			payload: `
+    {
+      	"bool":"INT64",
+		"varchar":"BYTES"
+    }`,
+			statusCode: http.StatusOK,
+			expectedSchema: ddl.CreateTable{
+				Name:     "t1",
+				ColNames: []string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p"},
+				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.String, Len: ddl.MaxLength}},
+					"c": ddl.ColumnDef{Name: "c", T: ddl.Type{Name: ddl.Int64}},
+					"d": ddl.ColumnDef{Name: "d", T: ddl.Type{Name: ddl.Bytes, Len: 6}},
+					"e": ddl.ColumnDef{Name: "e", T: ddl.Type{Name: ddl.Numeric}},
+					"f": ddl.ColumnDef{Name: "f", T: ddl.Type{Name: ddl.String, Len: ddl.MaxLength}},
+					"g": ddl.ColumnDef{Name: "g", T: ddl.Type{Name: ddl.String, Len: ddl.MaxLength}},
+					"h": ddl.ColumnDef{Name: "h", T: ddl.Type{Name: ddl.Bytes, Len: ddl.MaxLength}},
+					"i": ddl.ColumnDef{Name: "i", T: ddl.Type{Name: ddl.Bytes, Len: ddl.MaxLength}},
+					"j": ddl.ColumnDef{Name: "j", T: ddl.Type{Name: ddl.Int64}},
+					"k": ddl.ColumnDef{Name: "k", T: ddl.Type{Name: ddl.Float64}},
+					"l": ddl.ColumnDef{Name: "l", T: ddl.Type{Name: ddl.Float64}},
+					"m": ddl.ColumnDef{Name: "m", T: ddl.Type{Name: ddl.Numeric}},
+					"n": ddl.ColumnDef{Name: "n", T: ddl.Type{Name: ddl.Date}},
+					"o": ddl.ColumnDef{Name: "o", T: ddl.Type{Name: ddl.Timestamp}},
+					"p": ddl.ColumnDef{Name: "p", T: ddl.Type{Name: ddl.String, Len: ddl.MaxLength}},
+				},
+				Pks: []ddl.IndexKey{ddl.IndexKey{Col: "a"}},
+			},
+			expectedIssues: map[string][]internal.SchemaIssue{
+				"a": []internal.SchemaIssue{internal.Widened},
+				"c": []internal.SchemaIssue{internal.Widened},
+				"j": []internal.SchemaIssue{internal.Widened},
+				"l": []internal.SchemaIssue{internal.Widened},
+				"o": []internal.SchemaIssue{internal.Time},
+			},
+		},
+		{
+			name: "Test bad request",
+			payload: `
+    {
+      	"bool":"INT64",
+		"smallint":"STRING",
+    }`,
+			statusCode: http.StatusBadRequest,
+		},
+	}
+	for _, tc := range tc {
+		sessionState.driver = "mysql"
+		sessionState.conv = internal.MakeConv()
+		buildConvMySQL(sessionState.conv)
+		payload := tc.payload
+		req, err := http.NewRequest("POST", "/typemap/global", strings.NewReader(payload))
+		if err != nil {
+			t.Fatal(err)
+		}
+		req.Header.Set("Content-Type", "application/json")
+		rr := httptest.NewRecorder()
+		handler := http.HandlerFunc(setTypeMapGlobal)
+		handler.ServeHTTP(rr, req)
+		var res *internal.Conv
+		json.Unmarshal(rr.Body.Bytes(), &res)
+		if status := rr.Code; int64(status) != tc.statusCode {
+			t.Errorf("handler returned wrong status code: got %v want %v",
+				status, tc.statusCode)
+		}
+
+		if tc.statusCode == http.StatusOK {
+			assert.Equal(t, tc.expectedSchema, res.SpSchema["t1"])
+			assert.Equal(t, tc.expectedIssues, res.Issues["t1"])
+		}
+	}
+}
+
+func TestGetConversionMySQL(t *testing.T) {
+	sessionState.driver = "mysql"
+	sessionState.conv = internal.MakeConv()
+	buildConvMySQL(sessionState.conv)
+	req, err := http.NewRequest("GET", "/conversion", nil)
+	if err != nil {
+		t.Fatal(err)
+	}
+	rr := httptest.NewRecorder()
+	handler := http.HandlerFunc(getConversionRate)
+	handler.ServeHTTP(rr, req)
+	var result map[string]string
+	json.Unmarshal(rr.Body.Bytes(), &result)
+	if status := rr.Code; status != http.StatusOK {
+		t.Errorf("handler returned wrong status code: got %v want %v",
+			status, http.StatusOK)
+	}
+	assert.Equal(t, 2, len(result))
+	assert.Contains(t, result, "t1")
+	assert.Contains(t, result, "t2")
+}
+
+func TestSetParentTable(t *testing.T) {
+
+	tests := []struct {
+		name             string
+		ct               *internal.Conv
+		table            string
+		statusCode       int64
+		expectedResponse *TableInterleaveStatus
+		expectedFKs      []ddl.Foreignkey
+		parentTable      string
+	}{
+		{
+			name:       "no conv provided",
+			statusCode: http.StatusNotFound,
+		},
+		{
+			name:       "no table name provided",
+			statusCode: http.StatusBadRequest,
+			ct: &internal.Conv{
+				SpSchema: map[string]ddl.CreateTable{"t1": ddl.CreateTable{
+					Name:     "t1",
+					ColNames: []string{"a", "b", "c"},
+					ColDefs: map[string]ddl.ColumnDef{"a": ddl.ColumnDef{Name: "a", T: ddl.Type{Name: ddl.Int64}, NotNull: true},
+						"b": ddl.ColumnDef{Name: "b", T: ddl.Type{Name: ddl.Int64}, NotNull: true},
+						"c": ddl.ColumnDef{Name: "c", T: ddl.Type{Name: ddl.String, Len: ddl.MaxLength}, NotNull: true}},
+					Pks: []ddl.IndexKey{ddl.IndexKey{Col: "a", Desc: false}},
+					Fks: []ddl.Foreignkey{ddl.Foreignkey{Name: "fk1", Columns: []string{"a"}, ReferTable: "ref_t1", ReferColumns: []string{"ref_c1"}},
+						ddl.Foreignkey{Name: "fk2", Columns: []string{"c"}, ReferTable: "ref_t2", ReferColumns: []string{"ref_c2"}}},
+				}}},
+		},
+		{
+			name: "table with synthetic PK",
+			ct: &internal.Conv{
+				SpSchema: map[string]ddl.CreateTable{"t1": ddl.CreateTable{
+					Name:     "t1",
+					ColNames: []string{"a", "b", "c"},
+					ColDefs: map[string]ddl.ColumnDef{"a": ddl.ColumnDef{Name: "a", T: ddl.Type{Name: ddl.Int64}, NotNull: true},
+						"b":        ddl.ColumnDef{Name: "b", T: ddl.Type{Name: ddl.Int64}, NotNull: true},
+						"c":        ddl.ColumnDef{Name: "c", T: ddl.Type{Name: ddl.String, Len: ddl.MaxLength}, NotNull: true},
+						"synth_id": ddl.ColumnDef{Name: "synth_id", T: ddl.Type{Name: ddl.Int64}, NotNull: true},
+					},
+					Pks: []ddl.IndexKey{ddl.IndexKey{Col: "synth_id", Desc: false}},
+					Fks: []ddl.Foreignkey{ddl.Foreignkey{Name: "fk1", Columns: []string{"a"}, ReferTable: "ref_t1", ReferColumns: []string{"ref_c1"}},
+						ddl.Foreignkey{Name: "fk2", Columns: []string{"c"}, ReferTable: "ref_t2", ReferColumns: []string{"ref_c2"}}},
+				}},
+				SyntheticPKeys: map[string]internal.SyntheticPKey{"t1": internal.SyntheticPKey{Col: "synth_id"}},
+			},
+			table:            "t1",
+			statusCode:       http.StatusOK,
+			expectedResponse: &TableInterleaveStatus{Possible: false, Comment: "Has synthetic pk"},
+		},
+		{
+			name: "no valid prefix 1",
+			ct: &internal.Conv{
+				SpSchema: map[string]ddl.CreateTable{
+					"t1": ddl.CreateTable{
+						Name:     "t1",
+						ColNames: []string{"a", "b", "c"},
+						ColDefs: map[string]ddl.ColumnDef{"a": ddl.ColumnDef{Name: "a", T: ddl.Type{Name: ddl.Int64}, NotNull: true},
+							"b": ddl.ColumnDef{Name: "b", T: ddl.Type{Name: ddl.Int64}, NotNull: true},
+							"c": ddl.ColumnDef{Name: "c", T: ddl.Type{Name: ddl.String, Len: ddl.MaxLength}, NotNull: true},
+						},
+						Pks: []ddl.IndexKey{ddl.IndexKey{Col: "a", Desc: false}},
+						Fks: []ddl.Foreignkey{ddl.Foreignkey{Name: "fk1", Columns: []string{"a"}, ReferTable: "t2", ReferColumns: []string{"a"}}},
+					},
+					"t2": ddl.CreateTable{
+						Name:     "t2",
+						ColNames: []string{"a", "b", "c"},
+						ColDefs: map[string]ddl.ColumnDef{"a": ddl.ColumnDef{Name: "a", T: ddl.Type{Name: ddl.Int64}, NotNull: true},
+							"b":        ddl.ColumnDef{Name: "b", T: ddl.Type{Name: ddl.Int64}, NotNull: true},
+							"c":        ddl.ColumnDef{Name: "c", T: ddl.Type{Name: ddl.String, Len: ddl.MaxLength}, NotNull: true},
+							"synth_id": ddl.ColumnDef{Name: "synth_id", T: ddl.Type{Name: ddl.Int64}, NotNull: true},
+						},
+						Pks: []ddl.IndexKey{ddl.IndexKey{Col: "synth_id", Desc: false}},
+					},
+				},
+				SyntheticPKeys: map[string]internal.SyntheticPKey{"t2": internal.SyntheticPKey{Col: "synth_id"}},
+			},
+			table:            "t1",
+			statusCode:       http.StatusOK,
+			expectedResponse: &TableInterleaveStatus{Possible: false, Comment: "No valid prefix"},
+			expectedFKs:      []ddl.Foreignkey{ddl.Foreignkey{Name: "fk1", Columns: []string{"a"}, ReferTable: "t2", ReferColumns: []string{"a"}}},
+		},
+		{
+			name: "no valid prefix 2",
+			ct: &internal.Conv{
+				SpSchema: map[string]ddl.CreateTable{
+					"t1": ddl.CreateTable{
+						Name:     "t1",
+						ColNames: []string{"a", "b", "c"},
+						ColDefs: map[string]ddl.ColumnDef{"a": ddl.ColumnDef{Name: "a", T: ddl.Type{Name: ddl.Int64}, NotNull: true},
+							"b": ddl.ColumnDef{Name: "b", T: ddl.Type{Name: ddl.Int64}, NotNull: true},
+							"c": ddl.ColumnDef{Name: "c", T: ddl.Type{Name: ddl.String, Len: ddl.MaxLength}, NotNull: true},
+						},
+						Pks: []ddl.IndexKey{ddl.IndexKey{Col: "a", Desc: false}},
+						Fks: []ddl.Foreignkey{ddl.Foreignkey{Name: "fk1", Columns: []string{"a"}, ReferTable: "t2", ReferColumns: []string{"a"}}},
+					},
+					"t2": ddl.CreateTable{
+						Name:     "t2",
+						ColNames: []string{"a", "b", "c"},
+						ColDefs: map[string]ddl.ColumnDef{"a": ddl.ColumnDef{Name: "a", T: ddl.Type{Name: ddl.Int64}, NotNull: true},
+							"b": ddl.ColumnDef{Name: "b", T: ddl.Type{Name: ddl.Int64}, NotNull: true},
+							"c": ddl.ColumnDef{Name: "c", T: ddl.Type{Name: ddl.String, Len: ddl.MaxLength}, NotNull: true},
+						},
+						Pks: []ddl.IndexKey{ddl.IndexKey{Col: "a", Desc: false}, ddl.IndexKey{Col: "b", Desc: false}},
+					},
+				},
+			},
+			table:            "t1",
+			statusCode:       http.StatusOK,
+			expectedResponse: &TableInterleaveStatus{Possible: false, Comment: "No valid prefix"},
+			expectedFKs:      []ddl.Foreignkey{ddl.Foreignkey{Name: "fk1", Columns: []string{"a"}, ReferTable: "t2", ReferColumns: []string{"a"}}},
+		},
+		{
+			name: "no valid prefix 3",
+			ct: &internal.Conv{
+				SpSchema: map[string]ddl.CreateTable{
+					"t1": ddl.CreateTable{
+						Name:     "t1",
+						ColNames: []string{"a", "b", "c"},
+						ColDefs: map[string]ddl.ColumnDef{"a": ddl.ColumnDef{Name: "a", T: ddl.Type{Name: ddl.Int64}, NotNull: true},
+							"b": ddl.ColumnDef{Name: "b", T: ddl.Type{Name: ddl.Int64}, NotNull: true},
+							"c": ddl.ColumnDef{Name: "c", T: ddl.Type{Name: ddl.String, Len: ddl.MaxLength}, NotNull: true},
+						},
+						Pks: []ddl.IndexKey{ddl.IndexKey{Col: "a", Desc: false}, ddl.IndexKey{Col: "b", Desc: false}},
+						Fks: []ddl.Foreignkey{ddl.Foreignkey{Name: "fk1", Columns: []string{"c"}, ReferTable: "t2", ReferColumns: []string{"c"}}},
+					},
+					"t2": ddl.CreateTable{
+						Name:     "t2",
+						ColNames: []string{"a", "b", "c"},
+						ColDefs: map[string]ddl.ColumnDef{"a": ddl.ColumnDef{Name: "a", T: ddl.Type{Name: ddl.Int64}, NotNull: true},
+							"b": ddl.ColumnDef{Name: "b", T: ddl.Type{Name: ddl.Int64}, NotNull: true},
+							"c": ddl.ColumnDef{Name: "c", T: ddl.Type{Name: ddl.String, Len: ddl.MaxLength}, NotNull: true},
+						},
+						Pks: []ddl.IndexKey{ddl.IndexKey{Col: "a", Desc: false}},
+					},
+				},
+			},
+			table:            "t1",
+			statusCode:       http.StatusOK,
+			expectedResponse: &TableInterleaveStatus{Possible: false, Comment: "No valid prefix"},
+			expectedFKs:      []ddl.Foreignkey{ddl.Foreignkey{Name: "fk1", Columns: []string{"c"}, ReferTable: "t2", ReferColumns: []string{"c"}}},
+		},
+		{
+			name: "successful interleave",
+			ct: &internal.Conv{
+				SpSchema: map[string]ddl.CreateTable{
+					"t1": ddl.CreateTable{
+						Name:     "t1",
+						ColNames: []string{"a", "b", "c"},
+						ColDefs: map[string]ddl.ColumnDef{"a": ddl.ColumnDef{Name: "a", T: ddl.Type{Name: ddl.Int64}, NotNull: true},
+							"b": ddl.ColumnDef{Name: "b", T: ddl.Type{Name: ddl.Int64}, NotNull: true},
+							"c": ddl.ColumnDef{Name: "c", T: ddl.Type{Name: ddl.String, Len: ddl.MaxLength}, NotNull: true},
+						},
+						Pks: []ddl.IndexKey{ddl.IndexKey{Col: "a", Desc: false}, ddl.IndexKey{Col: "b", Desc: false}},
+						Fks: []ddl.Foreignkey{ddl.Foreignkey{Name: "fk1", Columns: []string{"a"}, ReferTable: "t2", ReferColumns: []string{"a"}}},
+					},
+					"t2": ddl.CreateTable{
+						Name:     "t2",
+						ColNames: []string{"a", "b", "c"},
+						ColDefs: map[string]ddl.ColumnDef{"a": ddl.ColumnDef{Name: "a", T: ddl.Type{Name: ddl.Int64}, NotNull: true},
+							"b": ddl.ColumnDef{Name: "b", T: ddl.Type{Name: ddl.Int64}, NotNull: true},
+							"c": ddl.ColumnDef{Name: "c", T: ddl.Type{Name: ddl.String, Len: ddl.MaxLength}, NotNull: true},
+						},
+						Pks: []ddl.IndexKey{ddl.IndexKey{Col: "a", Desc: false}},
+					},
+				},
+			},
+			table:            "t1",
+			statusCode:       http.StatusOK,
+			expectedResponse: &TableInterleaveStatus{Possible: true, Parent: "t2"},
+			expectedFKs:      []ddl.Foreignkey{},
+			parentTable:      "t2",
+		},
+		{
+			name: "successful interleave with same primary key",
+			ct: &internal.Conv{
+				SpSchema: map[string]ddl.CreateTable{
+					"t1": ddl.CreateTable{
+						Name:     "t1",
+						ColNames: []string{"a", "b", "c"},
+						ColDefs: map[string]ddl.ColumnDef{"a": ddl.ColumnDef{Name: "a", T: ddl.Type{Name: ddl.Int64}, NotNull: true},
+							"b": ddl.ColumnDef{Name: "b", T: ddl.Type{Name: ddl.Int64}, NotNull: true},
+							"c": ddl.ColumnDef{Name: "c", T: ddl.Type{Name: ddl.String, Len: ddl.MaxLength}, NotNull: true},
+						},
+						Pks: []ddl.IndexKey{ddl.IndexKey{Col: "a", Desc: false}, ddl.IndexKey{Col: "b", Desc: false}},
+						Fks: []ddl.Foreignkey{ddl.Foreignkey{Name: "fk1", Columns: []string{"a", "b"}, ReferTable: "t2", ReferColumns: []string{"a", "b"}}},
+					},
+					"t2": ddl.CreateTable{
+						Name:     "t2",
+						ColNames: []string{"a", "b", "c"},
+						ColDefs: map[string]ddl.ColumnDef{"a": ddl.ColumnDef{Name: "a", T: ddl.Type{Name: ddl.Int64}, NotNull: true},
+							"b": ddl.ColumnDef{Name: "b", T: ddl.Type{Name: ddl.Int64}, NotNull: true},
+							"c": ddl.ColumnDef{Name: "c", T: ddl.Type{Name: ddl.String, Len: ddl.MaxLength}, NotNull: true},
+						},
+						Pks: []ddl.IndexKey{ddl.IndexKey{Col: "a", Desc: false}, ddl.IndexKey{Col: "b", Desc: false}},
+					},
+				},
+			},
+			table:            "t1",
+			statusCode:       http.StatusOK,
+			expectedResponse: &TableInterleaveStatus{Possible: true, Parent: "t2"},
+			expectedFKs:      []ddl.Foreignkey{},
+			parentTable:      "t2",
+		},
+		{
+			name: "successful interleave with multiple fks refering multiple tables",
+			ct: &internal.Conv{
+				SpSchema: map[string]ddl.CreateTable{
+					"t1": ddl.CreateTable{
+						Name:     "t1",
+						ColNames: []string{"a", "b", "c"},
+						ColDefs: map[string]ddl.ColumnDef{"a": ddl.ColumnDef{Name: "a", T: ddl.Type{Name: ddl.Int64}, NotNull: true},
+							"b": ddl.ColumnDef{Name: "b", T: ddl.Type{Name: ddl.Int64}, NotNull: true},
+							"c": ddl.ColumnDef{Name: "c", T: ddl.Type{Name: ddl.String, Len: ddl.MaxLength}, NotNull: true},
+						},
+						Pks: []ddl.IndexKey{ddl.IndexKey{Col: "a", Desc: false}, ddl.IndexKey{Col: "b", Desc: false}},
+						Fks: []ddl.Foreignkey{
+							ddl.Foreignkey{Name: "fk1", Columns: []string{"c"}, ReferTable: "t3", ReferColumns: []string{"c"}},
+							ddl.Foreignkey{Name: "fk1", Columns: []string{"a", "b"}, ReferTable: "t2", ReferColumns: []string{"a", "b"}}},
+					},
+					"t2": ddl.CreateTable{
+						Name:     "t2",
+						ColNames: []string{"a", "b", "c"},
+						ColDefs: map[string]ddl.ColumnDef{"a": ddl.ColumnDef{Name: "a", T: ddl.Type{Name: ddl.Int64}, NotNull: true},
+							"b": ddl.ColumnDef{Name: "b", T: ddl.Type{Name: ddl.Int64}, NotNull: true},
+							"c": ddl.ColumnDef{Name: "c", T: ddl.Type{Name: ddl.String, Len: ddl.MaxLength}, NotNull: true},
+						},
+						Pks: []ddl.IndexKey{ddl.IndexKey{Col: "a", Desc: false}, ddl.IndexKey{Col: "b", Desc: false}},
+					},
+					"t3": ddl.CreateTable{
+						Name:     "t3",
+						ColNames: []string{"a", "b", "c"},
+						ColDefs: map[string]ddl.ColumnDef{"a": ddl.ColumnDef{Name: "a", T: ddl.Type{Name: ddl.Int64}, NotNull: true},
+							"b": ddl.ColumnDef{Name: "b", T: ddl.Type{Name: ddl.Int64}, NotNull: true},
+							"c": ddl.ColumnDef{Name: "c", T: ddl.Type{Name: ddl.String, Len: ddl.MaxLength}, NotNull: true},
+						},
+						Pks: []ddl.IndexKey{ddl.IndexKey{Col: "c", Desc: false}},
+					},
+				},
+			},
+			table:            "t1",
+			statusCode:       http.StatusOK,
+			expectedResponse: &TableInterleaveStatus{Possible: true, Parent: "t2"},
+			expectedFKs: []ddl.Foreignkey{
+				ddl.Foreignkey{Name: "fk1", Columns: []string{"c"}, ReferTable: "t3", ReferColumns: []string{"c"}}},
+			parentTable: "t2",
+		},
+	}
+	for _, tc := range tests {
+		sessionState.driver = "mysql"
+		sessionState.conv = tc.ct
+		req, err := http.NewRequest("GET", fmt.Sprintf("/setparent?table=%s&update=%v", tc.table, true), nil)
+		if err != nil {
+			t.Fatal(err)
+		}
+		req.Header.Set("Content-Type", "application/json")
+		rr := httptest.NewRecorder()
+		handler := http.HandlerFunc(setParentTable)
+		handler.ServeHTTP(rr, req)
+		var res *TableInterleaveStatus
+		json.Unmarshal(rr.Body.Bytes(), &res)
+		if status := rr.Code; int64(status) != tc.statusCode {
+			t.Errorf("handler returned wrong status code: got %v want %v",
+				status, tc.statusCode)
+		}
+		if tc.statusCode == http.StatusOK {
+			assert.Equal(t, tc.expectedResponse, res)
+		}
+		if tc.parentTable != "" {
+			assert.Equal(t, tc.parentTable, sessionState.conv.SpSchema[tc.table].Parent)
+			assert.Equal(t, tc.expectedFKs, sessionState.conv.SpSchema[tc.table].Fks)
+		}
+	}
+}
+
+func TestDropForeignKey(t *testing.T) {
+	tc := []struct {
+		name         string
+		table        string
+		position     string
+		statusCode   int64
+		conv         *internal.Conv
+		expectedConv *internal.Conv
+	}{
+		{
+			name:       "Test drop valid FK success",
+			table:      "t1",
+			position:   "1",
+			statusCode: http.StatusOK,
+			conv: &internal.Conv{
+				SpSchema: map[string]ddl.CreateTable{
+					"t1": ddl.CreateTable{
+						Fks: []ddl.Foreignkey{ddl.Foreignkey{Name: "fk1", Columns: []string{"b"}, ReferTable: "reft1", ReferColumns: []string{"ref_b"}},
+							ddl.Foreignkey{Name: "fk2", Columns: []string{"c", "d"}, ReferTable: "reft2", ReferColumns: []string{"ref_c", "ref_d"}}},
+					}},
+			},
+			expectedConv: &internal.Conv{
+				SpSchema: map[string]ddl.CreateTable{
+					"t1": ddl.CreateTable{
+						Fks: []ddl.Foreignkey{ddl.Foreignkey{Name: "fk1", Columns: []string{"b"}, ReferTable: "reft1", ReferColumns: []string{"ref_b"}}},
+					}},
+			},
+		},
+		{
+			name:       "Test drop FK invalid position",
+			table:      "t1",
+			position:   "1",
+			statusCode: http.StatusBadRequest,
+			conv: &internal.Conv{
+				SpSchema: map[string]ddl.CreateTable{
+					"t1": ddl.CreateTable{
+						Fks: []ddl.Foreignkey{ddl.Foreignkey{Name: "fk1", Columns: []string{"b"}, ReferTable: "reft1", ReferColumns: []string{"ref_b"}}},
+					}},
+			},
+		},
+		{
+			name:       "Test drop FK invalid position 2",
+			table:      "t1",
+			position:   "AB",
+			statusCode: http.StatusBadRequest,
+			conv: &internal.Conv{
+				SpSchema: map[string]ddl.CreateTable{
+					"t1": ddl.CreateTable{
+						Fks: []ddl.Foreignkey{ddl.Foreignkey{Name: "fk1", Columns: []string{"b"}, ReferTable: "reft1", ReferColumns: []string{"ref_b"}}},
+					}},
+			},
+		},
+	}
+	for _, tc := range tc {
+		sessionState.driver = "mysql"
+		sessionState.conv = tc.conv
+		req, err := http.NewRequest("GET", "/drop/fk?table="+tc.table+"&pos="+tc.position, nil)
+		if err != nil {
+			t.Fatal(err)
+		}
+		req.Header.Set("Content-Type", "application/json")
+		rr := httptest.NewRecorder()
+		handler := http.HandlerFunc(dropForeignKey)
+		handler.ServeHTTP(rr, req)
+		var res *internal.Conv
+		json.Unmarshal(rr.Body.Bytes(), &res)
+		if status := rr.Code; int64(status) != tc.statusCode {
+			t.Errorf("handler returned wrong status code: got %v want %v",
+				status, tc.statusCode)
+		}
+		if tc.statusCode == http.StatusOK {
+			assert.Equal(t, tc.expectedConv, res)
+		}
+	}
+}
+
+func TestDropSecondaryIndex(t *testing.T) {
+	tc := []struct {
+		name         string
+		table        string
+		position     string
+		statusCode   int64
+		conv         *internal.Conv
+		expectedConv *internal.Conv
+	}{
+		{
+			name:       "Test drop valid secondary index success",
+			table:      "t1",
+			position:   "1",
+			statusCode: http.StatusOK,
+			conv: &internal.Conv{
+				SpSchema: map[string]ddl.CreateTable{
+					"t1": ddl.CreateTable{
+						Indexes: []ddl.CreateIndex{ddl.CreateIndex{Name: "idx1", Table: "t1", Unique: false, Keys: []ddl.IndexKey{ddl.IndexKey{Col: "b", Desc: false}}},
+							ddl.CreateIndex{Name: "idx2", Table: "t1", Unique: false, Keys: []ddl.IndexKey{ddl.IndexKey{Col: "c", Desc: false}, ddl.IndexKey{Col: "d", Desc: false}}}},
+					}},
+			},
+			expectedConv: &internal.Conv{
+				SpSchema: map[string]ddl.CreateTable{
+					"t1": ddl.CreateTable{
+						Indexes: []ddl.CreateIndex{ddl.CreateIndex{Name: "idx1", Table: "t1", Unique: false, Keys: []ddl.IndexKey{ddl.IndexKey{Col: "b", Desc: false}}}},
+					}},
+			},
+		},
+		{
+			name:       "Test drop secondary index invalid position",
+			table:      "t1",
+			position:   "1",
+			statusCode: http.StatusBadRequest,
+			conv: &internal.Conv{
+				SpSchema: map[string]ddl.CreateTable{
+					"t1": ddl.CreateTable{
+						Indexes: []ddl.CreateIndex{ddl.CreateIndex{Name: "idx1", Table: "t1", Unique: false, Keys: []ddl.IndexKey{ddl.IndexKey{Col: "b", Desc: false}}}},
+					}},
+			},
+		},
+		{
+			name:       "Test drop secondary index invalid position 2",
+			table:      "t1",
+			position:   "AB",
+			statusCode: http.StatusBadRequest,
+			conv: &internal.Conv{
+				SpSchema: map[string]ddl.CreateTable{
+					"t1": ddl.CreateTable{
+						Indexes: []ddl.CreateIndex{ddl.CreateIndex{Name: "idx1", Table: "t1", Unique: false, Keys: []ddl.IndexKey{ddl.IndexKey{Col: "b", Desc: false}}}},
+					}},
+			},
+		},
+	}
+	for _, tc := range tc {
+		sessionState.driver = "mysql"
+		sessionState.conv = tc.conv
+		req, err := http.NewRequest("GET", "/drop/secondaryindex?table="+tc.table+"&pos="+tc.position, nil)
+		if err != nil {
+			t.Fatal(err)
+		}
+		req.Header.Set("Content-Type", "application/json")
+		rr := httptest.NewRecorder()
+		handler := http.HandlerFunc(dropSecondaryIndex)
+		handler.ServeHTTP(rr, req)
+		var res *internal.Conv
+		json.Unmarshal(rr.Body.Bytes(), &res)
+		if status := rr.Code; int64(status) != tc.statusCode {
+			t.Errorf("handler returned wrong status code: got %v want %v",
+				status, tc.statusCode)
+		}
+		if tc.statusCode == http.StatusOK {
+			assert.Equal(t, tc.expectedConv, res)
+		}
+	}
+}
+
+func buildConvMySQL(conv *internal.Conv) {
+	conv.SrcSchema = map[string]schema.Table{
+		"t1": schema.Table{
+			Name:     "t1",
+			ColNames: []string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p"},
+			ColDefs: map[string]schema.Column{
+				"a": schema.Column{Name: "a", Type: schema.Type{Name: "bool"}},
+				"b": schema.Column{Name: "b", Type: schema.Type{Name: "text"}},
+				"c": schema.Column{Name: "c", Type: schema.Type{Name: "bool"}},
+				"d": schema.Column{Name: "d", Type: schema.Type{Name: "varchar", Mods: []int64{6}}},
+				"e": schema.Column{Name: "e", Type: schema.Type{Name: "numeric"}},
+				"f": schema.Column{Name: "f", Type: schema.Type{Name: "enum"}},
+				"g": schema.Column{Name: "g", Type: schema.Type{Name: "json"}},
+				"h": schema.Column{Name: "h", Type: schema.Type{Name: "binary"}},
+				"i": schema.Column{Name: "i", Type: schema.Type{Name: "blob"}},
+				"j": schema.Column{Name: "j", Type: schema.Type{Name: "smallint"}},
+				"k": schema.Column{Name: "k", Type: schema.Type{Name: "double"}},
+				"l": schema.Column{Name: "l", Type: schema.Type{Name: "float"}},
+				"m": schema.Column{Name: "m", Type: schema.Type{Name: "decimal"}},
+				"n": schema.Column{Name: "n", Type: schema.Type{Name: "date"}},
+				"o": schema.Column{Name: "o", Type: schema.Type{Name: "timestamp"}},
+				"p": schema.Column{Name: "p", Type: schema.Type{Name: "time"}},
+			},
+			PrimaryKeys: []schema.Key{schema.Key{Column: "a"}}},
+		"t2": schema.Table{
+			Name:     "t2",
+			ColNames: []string{"a", "b", "c"},
+			ColDefs: map[string]schema.Column{
+				"a": schema.Column{Name: "a", Type: schema.Type{Name: "integer"}},
+				"b": schema.Column{Name: "b", Type: schema.Type{Name: "double"}},
+				"c": schema.Column{Name: "c", Type: schema.Type{Name: "bool"}},
+			}},
+	}
+	conv.SpSchema = map[string]ddl.CreateTable{
+		"t1": ddl.CreateTable{
+			Name:     "t1",
+			ColNames: []string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p"},
+			ColDefs: map[string]ddl.ColumnDef{
+				"a": ddl.ColumnDef{Name: "a", T: ddl.Type{Name: ddl.Bool}},
+				"b": ddl.ColumnDef{Name: "b", T: ddl.Type{Name: ddl.String, Len: ddl.MaxLength}},
+				"c": ddl.ColumnDef{Name: "c", T: ddl.Type{Name: ddl.Bool}},
+				"d": ddl.ColumnDef{Name: "d", T: ddl.Type{Name: ddl.String, Len: int64(6)}},
+				"e": ddl.ColumnDef{Name: "e", T: ddl.Type{Name: ddl.Numeric}},
+				"f": ddl.ColumnDef{Name: "f", T: ddl.Type{Name: ddl.String, Len: ddl.MaxLength}},
+				"g": ddl.ColumnDef{Name: "g", T: ddl.Type{Name: ddl.String, Len: ddl.MaxLength}},
+				"h": ddl.ColumnDef{Name: "h", T: ddl.Type{Name: ddl.Bytes, Len: ddl.MaxLength}},
+				"i": ddl.ColumnDef{Name: "i", T: ddl.Type{Name: ddl.Bytes, Len: ddl.MaxLength}},
+				"j": ddl.ColumnDef{Name: "j", T: ddl.Type{Name: ddl.Int64}},
+				"k": ddl.ColumnDef{Name: "k", T: ddl.Type{Name: ddl.Float64}},
+				"l": ddl.ColumnDef{Name: "l", T: ddl.Type{Name: ddl.Float64}},
+				"m": ddl.ColumnDef{Name: "m", T: ddl.Type{Name: ddl.Numeric}},
+				"n": ddl.ColumnDef{Name: "n", T: ddl.Type{Name: ddl.Date}},
+				"o": ddl.ColumnDef{Name: "o", T: ddl.Type{Name: ddl.Timestamp}},
+				"p": ddl.ColumnDef{Name: "p", T: ddl.Type{Name: ddl.String, Len: ddl.MaxLength}},
+			},
+			Pks: []ddl.IndexKey{ddl.IndexKey{Col: "a"}},
+		},
+		"t2": ddl.CreateTable{
+			Name:     "t2",
+			ColNames: []string{"a", "b", "c", "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}},
+				"c":        ddl.ColumnDef{Name: "c", T: ddl.Type{Name: ddl.Bool}},
+				"synth_id": ddl.ColumnDef{Name: "synth_id", T: ddl.Type{Name: ddl.Int64}},
+			},
+			Pks: []ddl.IndexKey{ddl.IndexKey{Col: "synth_id"}},
+		},
+	}
+	conv.ToSource = map[string]internal.NameAndCols{
+		"t1": internal.NameAndCols{
+			Name: "t1",
+			Cols: map[string]string{
+				"a": "a", "b": "b", "c": "c", "d": "d", "e": "e", "f": "f", "g": "g", "h": "h", "i": "i", "j": "j", "k": "k", "l": "l", "m": "m", "n": "n", "o": "o", "p": "p",
+			}},
+		"t2": internal.NameAndCols{
+			Name: "t2",
+			Cols: map[string]string{
+				"a": "a", "b": "b", "c": "c",
+			}},
+	}
+	conv.ToSpanner = map[string]internal.NameAndCols{
+		"t1": internal.NameAndCols{
+			Name: "t1",
+			Cols: map[string]string{
+				"a": "a", "b": "b", "c": "c", "d": "d", "e": "e", "f": "f", "g": "g", "h": "h", "i": "i", "j": "j", "k": "k", "l": "l", "m": "m", "n": "n", "o": "o", "p": "p",
+			}},
+		"t2": internal.NameAndCols{
+			Name: "t2",
+			Cols: map[string]string{
+				"a": "a", "b": "b", "c": "c",
+			}},
+	}
+	conv.Issues = map[string]map[string][]internal.SchemaIssue{
+		"t1": map[string][]internal.SchemaIssue{
+			"j": []internal.SchemaIssue{internal.Widened},
+			"l": []internal.SchemaIssue{internal.Widened},
+			"o": []internal.SchemaIssue{internal.Time},
+		},
+		"t2": map[string][]internal.SchemaIssue{
+			"a": []internal.SchemaIssue{internal.Widened},
+		},
+	}
+	conv.SyntheticPKeys["t2"] = internal.SyntheticPKey{"synth_id", 0}
+}
+
+func buildConvPostgres(conv *internal.Conv) {
+	conv.SrcSchema = map[string]schema.Table{
+		"t1": schema.Table{
+			Name:     "t1",
+			ColNames: []string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p"},
+			ColDefs: map[string]schema.Column{
+				"a": schema.Column{Name: "a", Type: schema.Type{Name: "int8"}},
+				"b": schema.Column{Name: "b", Type: schema.Type{Name: "float4"}},
+				"c": schema.Column{Name: "c", Type: schema.Type{Name: "bool"}},
+				"d": schema.Column{Name: "d", Type: schema.Type{Name: "varchar", Mods: []int64{6}}},
+				"e": schema.Column{Name: "e", Type: schema.Type{Name: "numeric"}},
+				"f": schema.Column{Name: "f", Type: schema.Type{Name: "timestamptz"}},
+				"g": schema.Column{Name: "g", Type: schema.Type{Name: "bigserial"}},
+				"h": schema.Column{Name: "h", Type: schema.Type{Name: "bpchar"}},
+				"i": schema.Column{Name: "i", Type: schema.Type{Name: "bytea"}},
+				"j": schema.Column{Name: "j", Type: schema.Type{Name: "date"}},
+				"k": schema.Column{Name: "k", Type: schema.Type{Name: "float8"}},
+				"l": schema.Column{Name: "l", Type: schema.Type{Name: "int4"}},
+				"m": schema.Column{Name: "m", Type: schema.Type{Name: "serial"}},
+				"n": schema.Column{Name: "n", Type: schema.Type{Name: "text"}},
+				"o": schema.Column{Name: "o", Type: schema.Type{Name: "timestamp"}},
+				"p": schema.Column{Name: "p", Type: schema.Type{Name: "bool"}},
+			},
+			PrimaryKeys: []schema.Key{schema.Key{Column: "a"}}},
+		"t2": schema.Table{
+			Name:     "t2",
+			ColNames: []string{"a", "b", "c"},
+			ColDefs: map[string]schema.Column{
+				"a": schema.Column{Name: "a", Type: schema.Type{Name: "int8"}},
+				"b": schema.Column{Name: "b", Type: schema.Type{Name: "float4"}},
+				"c": schema.Column{Name: "c", Type: schema.Type{Name: "bool"}},
+			}},
+	}
+	conv.SpSchema = map[string]ddl.CreateTable{
+		"t1": ddl.CreateTable{
+			Name:     "t1",
+			ColNames: []string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p"},
+			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}},
+				"c": ddl.ColumnDef{Name: "c", T: ddl.Type{Name: ddl.Bool}},
+				"d": ddl.ColumnDef{Name: "d", T: ddl.Type{Name: ddl.String, Len: int64(6)}},
+				"e": ddl.ColumnDef{Name: "e", T: ddl.Type{Name: ddl.Numeric}},
+				"f": ddl.ColumnDef{Name: "f", T: ddl.Type{Name: ddl.Timestamp}},
+				"g": ddl.ColumnDef{Name: "g", T: ddl.Type{Name: ddl.Int64}},
+				"h": ddl.ColumnDef{Name: "h", T: ddl.Type{Name: ddl.String, Len: int64(1)}},
+				"i": ddl.ColumnDef{Name: "i", T: ddl.Type{Name: ddl.Bytes, Len: ddl.MaxLength}},
+				"j": ddl.ColumnDef{Name: "j", T: ddl.Type{Name: ddl.Date}},
+				"k": ddl.ColumnDef{Name: "k", T: ddl.Type{Name: ddl.Float64}},
+				"l": ddl.ColumnDef{Name: "l", T: ddl.Type{Name: ddl.Int64}},
+				"m": ddl.ColumnDef{Name: "m", T: ddl.Type{Name: ddl.Int64}},
+				"n": ddl.ColumnDef{Name: "n", T: ddl.Type{Name: ddl.String, Len: ddl.MaxLength}},
+				"o": ddl.ColumnDef{Name: "o", T: ddl.Type{Name: ddl.Timestamp}},
+				"p": ddl.ColumnDef{Name: "p", T: ddl.Type{Name: ddl.Int64}},
+			},
+			Pks: []ddl.IndexKey{ddl.IndexKey{Col: "a"}},
+		},
+		"t2": ddl.CreateTable{
+			Name:     "t2",
+			ColNames: []string{"a", "b", "c", "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}},
+				"c":        ddl.ColumnDef{Name: "c", T: ddl.Type{Name: ddl.Bool}},
+				"synth_id": ddl.ColumnDef{Name: "synth_id", T: ddl.Type{Name: ddl.Int64}},
+			},
+			Pks: []ddl.IndexKey{ddl.IndexKey{Col: "synth_id"}},
+		},
+	}
+	conv.ToSource = map[string]internal.NameAndCols{
+		"t1": internal.NameAndCols{
+			Name: "t1",
+			Cols: map[string]string{
+				"a": "a", "b": "b", "c": "c", "d": "d", "e": "e", "f": "f", "g": "g", "h": "h", "i": "i", "j": "j", "k": "k", "l": "l", "m": "m", "n": "n", "o": "o", "p": "p",
+			}},
+		"t2": internal.NameAndCols{
+			Name: "t2",
+			Cols: map[string]string{
+				"a": "a", "b": "b", "c": "c",
+			}},
+	}
+	conv.ToSpanner = map[string]internal.NameAndCols{
+		"t1": internal.NameAndCols{
+			Name: "t1",
+			Cols: map[string]string{
+				"a": "a", "b": "b", "c": "c", "d": "d", "e": "e", "f": "f", "g": "g", "h": "h", "i": "i", "j": "j", "k": "k", "l": "l", "m": "m", "n": "n", "o": "o", "p": "p",
+			}},
+		"t2": internal.NameAndCols{
+			Name: "t2",
+			Cols: map[string]string{
+				"a": "a", "b": "b", "c": "c",
+			}},
+	}
+	conv.Issues = map[string]map[string][]internal.SchemaIssue{
+		"t1": map[string][]internal.SchemaIssue{
+			"b": []internal.SchemaIssue{internal.Widened},
+			"g": []internal.SchemaIssue{internal.Serial},
+			"l": []internal.SchemaIssue{internal.Widened},
+			"m": []internal.SchemaIssue{internal.Serial},
+			"o": []internal.SchemaIssue{internal.Timestamp},
+		},
+		"t2": map[string][]internal.SchemaIssue{
+			"b": []internal.SchemaIssue{internal.Widened},
+		},
+	}
+	conv.SyntheticPKeys["t2"] = internal.SyntheticPKey{"synth_id", 0}
+}