Repository: cloudspannerecosystem/harbourbridge
Pull request #72 (2020-07-08 06:42:58)

Fix : pgdump driver inconsistency
There is inconsistency in postgres dump command ('pg_dump') and driver name ('pgdump'). 
For consistency, updated `pgdump` --> `pg_dump`. 


Patch:
diff --git a/README.md b/README.md
index afc9139..fce57bd 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@
 
 HarbourBridge is a stand-alone open source tool for Cloud Spanner evaluation,
 using data from an existing PostgreSQL or MySQL database. The tool ingests schema
-and data from either a pgdump/mysqldump file or directly from the source database,
+and data from either a pg_dump/mysqldump file or directly from the source database,
 automatically builds a Spanner schema, and creates a new Spanner database populated
 with data from the source database.
 
@@ -29,10 +29,10 @@ intended for production database migration.
 To use the tool on a PostgreSQL database called mydb, run
 
 ```sh
-# By default, the driver is "pgdump".
+# By default, the driver is "pg_dump".
 pg_dump mydb | harbourbridge
 # Or,
-pg_dump mydb | harbourbridge -driver=pgdump
+pg_dump mydb | harbourbridge -driver=pg_dump
 ```
 
 To use the tool on a MySQL database called mydb, run
@@ -124,7 +124,7 @@ The tool should now be installed as `$GOPATH/bin/harbourbridge`
 To use the tool on a PostgreSQL database called mydb, run
 
 ```sh
-pg_dump mydb | $GOPATH/bin/harbourbridge -driver=pgdump
+pg_dump mydb | $GOPATH/bin/harbourbridge -driver=pg_dump
 ```
 
 To use the tool on a MySQL database called mydb, run
@@ -242,7 +242,7 @@ created in this instance. If not specified, the tool automatically determines an
 appropriate instance using gcloud.
 
 `-driver` Specifies the driver to use for schema and data conversion. Supported drivers
-are _'postgres'_, _'pgdump'_, _'mysql'_ and _'mysqldump'_. By default, the driver is _'pgdump'_.
+are _'postgres'_, _'pg_dump'_, _'mysql'_ and _'mysqldump'_. By default, the driver is _'pg_dump'_.
 
 `-prefix` Specifies a file prefix for the report, schema, and bad-data files
 written by the tool. If no file prefix is specified, the name of the Spanner
@@ -283,9 +283,9 @@ HarbourBridge.
 First, check that the driver is correctly configured to connect to your
 database. Driver configuration varies depending on the driver used.
 
-#### 1.1 pgdump
+#### 1.1 pg_dump
 
-If you are using pg_dump (-driver=pgdump), check that pg_dump is
+If you are using pg_dump (-driver=pg_dump), check that pg_dump is
 correctly configured to connect to your PostgreSQL
 database. Note that pg_dump uses the same options as psql to connect to your
 database. See the [psql](https://www.postgresql.org/docs/9.3/app-psql.html) and
diff --git a/internal/report.go b/internal/report.go
index f9afe10..b58046c 100644
--- a/internal/report.go
+++ b/internal/report.go
@@ -432,7 +432,7 @@ func writeStmtStats(driverName string, conv *Conv, w *bufio.Writer) {
 		s := conv.Stats.Statement[x.statement]
 		fmt.Fprintf(w, "  %6d %6d %6d %6d  %s\n", s.schema, s.data, s.skip, s.Error, x.statement)
 	}
-	if driverName == "pgdump" {
+	if driverName == "pg_dump" {
 		w.WriteString("See github.com/lfittl/pg_query_go/nodes for definitions of statement types\n")
 		w.WriteString("(lfittl/pg_query_go is the library we use for parsing pg_dump output).\n")
 		w.WriteString("\n")
@@ -463,7 +463,7 @@ func writeUnexpectedConditions(driverName string, conv *Conv, w *bufio.Writer) {
 		w.WriteString("mysqldump output is highly permissive: almost any construct can appear at\n")
 		w.WriteString("any node in the AST tree. The list details all unexpected nodes and\n")
 		w.WriteString("conditions.\n")
-	case "pgdump":
+	case "pg_dump":
 		w.WriteString("For debugging only. This section provides details of unexpected conditions\n")
 		w.WriteString("encountered as we processed the pg_dump data. In particular, the AST node\n")
 		w.WriteString("representation used by the lfittl/pg_query_go library used for parsing\n")
diff --git a/main.go b/main.go
index 4008aa4..af68daf 100644
--- a/main.go
+++ b/main.go
@@ -53,7 +53,7 @@ import (
 
 const (
 	// PGDUMP is the driver name for pg_dump.
-	PGDUMP string = "pgdump"
+	PGDUMP string = "pg_dump"
 	// POSTGRES is the driver name for PostgreSQL.
 	POSTGRES string = "postgres"
 	// MYSQLDUMP is the driver name for mysqldump.
@@ -77,7 +77,7 @@ func init() {
 	flag.StringVar(&dbNameOverride, "dbname", "", "dbname: name to use for Spanner DB")
 	flag.StringVar(&instanceOverride, "instance", "", "instance: Spanner instance to use")
 	flag.StringVar(&filePrefix, "prefix", "", "prefix: file prefix for generated files")
-	flag.StringVar(&driverName, "driver", "pgdump", "driver name: flag for accessing source DB or dump files (accepted values are \"pgdump\", \"postgres\", \"mysqldump\", and \"mysql\")")
+	flag.StringVar(&driverName, "driver", "pg_dump", "driver name: flag for accessing source DB or dump files (accepted values are \"pg_dump\", \"postgres\", \"mysqldump\", and \"mysql\")")
 	flag.BoolVar(&verbose, "v", false, "verbose: print additional output")
 }
 
@@ -421,7 +421,7 @@ func getSeekable(f *os.File) (*os.File, int64, error) {
 	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/mysql_dump output is large (tens of GBs) and os.TempDir points to a directory
+	// 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.
@@ -763,7 +763,7 @@ func ProcessDump(driver string, conv *internal.Conv, r *internal.Reader) error {
 func ProcessInfoSchema(driver string, conv *internal.Conv, db *sql.DB) error {
 	switch driver {
 	case MYSQL:
-		return mysql.ProcessInfoSchema(conv, db)
+		return mysql.ProcessInfoSchema(conv, db, os.Getenv("MYSQLDATABASE"))
 	case POSTGRES:
 		return postgres.ProcessInfoSchema(conv, db)
 	default:
@@ -775,7 +775,7 @@ func ProcessInfoSchema(driver string, conv *internal.Conv, db *sql.DB) error {
 func SetRowStats(driver string, conv *internal.Conv, db *sql.DB) error {
 	switch driver {
 	case MYSQL:
-		mysql.SetRowStats(conv, db)
+		mysql.SetRowStats(conv, db, os.Getenv("MYSQLDATABASE"))
 	case POSTGRES:
 		postgres.SetRowStats(conv, db)
 	default:
@@ -788,7 +788,7 @@ func SetRowStats(driver string, conv *internal.Conv, db *sql.DB) error {
 func ProcessSQLData(driver string, conv *internal.Conv, db *sql.DB) error {
 	switch driver {
 	case MYSQL:
-		mysql.ProcessSQLData(conv, db)
+		mysql.ProcessSQLData(conv, db, os.Getenv("MYSQLDATABASE"))
 	case POSTGRES:
 		postgres.ProcessSQLData(conv, db)
 	default:
diff --git a/mysql/infoschema.go b/mysql/infoschema.go
index 646b8ae..d6c19d9 100644
--- a/mysql/infoschema.go
+++ b/mysql/infoschema.go
@@ -28,8 +28,8 @@ import (
 // ProcessInfoSchema performs schema conversion for source database
 // 'db'. Information schema tables are a broadly supported ANSI standard,
 // and we use them to obtain source database's schema information.
-func ProcessInfoSchema(conv *internal.Conv, db *sql.DB) error {
-	tables, err := getTables(db)
+func ProcessInfoSchema(conv *internal.Conv, db *sql.DB, dbName string) error {
+	tables, err := getTables(db, dbName)
 	if err != nil {
 		return err
 	}
@@ -51,16 +51,16 @@ func ProcessInfoSchema(conv *internal.Conv, db *sql.DB) error {
 //
 // Using database/sql library we pass *sql.RawBytes to rows.scan.
 // RawBytes is a byte slice and values can be easily converted to string.
-func ProcessSQLData(conv *internal.Conv, db *sql.DB) {
+func ProcessSQLData(conv *internal.Conv, db *sql.DB, dbName string) {
 	// TODO: refactor to use the set of tables computed by
 	// ProcessInfoSchema instead of computing them again.
-	tables, err := getTables(db)
+	tables, err := getTables(db, dbName)
 	if err != nil {
 		conv.Unexpected(fmt.Sprintf("Couldn't get list of table: %s", err))
 		return
 	}
 	for _, t := range tables {
-		srcTable := buildTableName(t.schema, t.name)
+		srcTable := t.name
 		srcSchema, ok := conv.SrcSchema[srcTable]
 		if !ok {
 			conv.Stats.BadRows[srcTable] += conv.Stats.Rows[srcTable]
@@ -137,8 +137,8 @@ func buildColNameList(srcSchema schema.Table, srcColName []string) string {
 }
 
 // SetRowStats populates conv with the number of rows in each table.
-func SetRowStats(conv *internal.Conv, db *sql.DB) {
-	tables, err := getTables(db)
+func SetRowStats(conv *internal.Conv, db *sql.DB, dbName string) {
+	tables, err := getTables(db, dbName)
 	if err != nil {
 		conv.Unexpected(fmt.Sprintf("Couldn't get list of table: %s", err))
 		return
@@ -148,7 +148,7 @@ func SetRowStats(conv *internal.Conv, db *sql.DB) {
 		// Ideally we would pass schema/name as a query parameter,
 		// but MySQL doesn't support this. So we quote it instead.
 		q := fmt.Sprintf(`SELECT COUNT(*) FROM %s.%s;`, t.schema, t.name)
-		tableName := buildTableName(t.schema, t.name)
+		tableName := t.name
 		rows, err := db.Query(q)
 		if err != nil {
 			conv.Unexpected(fmt.Sprintf("Couldn't get number of rows for table %s", tableName))
@@ -172,25 +172,23 @@ type schemaAndName struct {
 	name   string
 }
 
-func getTables(db *sql.DB) ([]schemaAndName, error) {
-	ignored := make(map[string]bool)
-	// Ignore all system tables: we just want to convert user tables.
-	for _, s := range []string{"information_schema", "mysql", "performance_schema", "sys"} {
-		ignored[s] = true
-	}
-	q := "SELECT table_schema, table_name FROM information_schema.tables where table_type = 'BASE TABLE'"
-	rows, err := db.Query(q)
+// getTables return list of tables in the selected database.
+// Note that sql.DB already effectively has the dbName
+// embedded within it (dbName is part of the DSN passed to sql.Open),
+// but unfortunately there is no way to extract it from sql.DB.
+func getTables(db *sql.DB, dbName string) ([]schemaAndName, error) {
+	// In MySQL, schema is the same as database name.
+	q := "SELECT table_name FROM information_schema.tables where table_type = 'BASE TABLE' and table_schema=?"
+	rows, err := db.Query(q, dbName)
 	if err != nil {
 		return nil, fmt.Errorf("couldn't get tables: %w", err)
 	}
 	defer rows.Close()
-	var tableSchema, tableName string
+	var tableName string
 	var tables []schemaAndName
 	for rows.Next() {
-		rows.Scan(&tableSchema, &tableName)
-		if !ignored[tableSchema] {
-			tables = append(tables, schemaAndName{schema: tableSchema, name: tableName})
-		}
+		rows.Scan(&tableName)
+		tables = append(tables, schemaAndName{schema: dbName, name: tableName})
 	}
 	return tables, nil
 }
@@ -206,7 +204,7 @@ func processTable(conv *internal.Conv, db *sql.DB, table schemaAndName) error {
 		return fmt.Errorf("couldn't get constraints for table %s.%s: %s", table.schema, table.name, err)
 	}
 	colDefs, colNames := processColumns(conv, cols, constraints)
-	name := buildTableName(table.schema, table.name)
+	name := table.name
 	var schemaPKeys []schema.Key
 	for _, k := range primaryKeys {
 		schemaPKeys = append(schemaPKeys, schema.Key{Column: k})
@@ -357,7 +355,3 @@ func valsToStrings(vals []sql.RawBytes) []string {
 	}
 	return s
 }
-
-func buildTableName(schema, name string) string {
-	return fmt.Sprintf("%s.%s", schema, name)
-}
diff --git a/mysql/mysqldump.go b/mysql/mysqldump.go
index 10d289f..d9fd400 100644
--- a/mysql/mysqldump.go
+++ b/mysql/mysqldump.go
@@ -146,12 +146,16 @@ func processSetStmt(conv *internal.Conv, stmt *ast.SetStmt) {
 				switch val := value.(type) {
 				case *driver.ValueExpr:
 					if val.GetValue() == nil {
-						logStmtError(conv, stmt, fmt.Errorf("found nil value in set statement"))
+						logStmtError(conv, stmt, fmt.Errorf("found nil value in 'SET TIME_ZONE' statement"))
 						return
 					}
 					conv.TimezoneOffset = fmt.Sprintf("%v", val.GetValue())
 				default:
-					logStmtError(conv, stmt, fmt.Errorf("found %s type value in set statement", reflect.TypeOf(val)))
+					// mysqldump saves the value of TIME_ZONE (in OLD_TIME_ZONE) at
+					// the start of the dump, changes TIME_ZONE, dumps table schema
+					// and data, and then restores TIME_ZONE using OLD_TIME_ZONE at the
+					// end of the dump file. We track the setting of TIME_ZONE, but
+					// ignore the restore statements.
 					return
 				}
 			}
diff --git a/postgres/README.md b/postgres/README.md
index 909bac5..5bd4fe8 100644
--- a/postgres/README.md
+++ b/postgres/README.md
@@ -18,26 +18,26 @@ on a PostgreSQL database (via go's database/sql package).
 To use HarbourBridge on a PostgreSQL database called mydb using pg_dump output,run:
 
 ```sh
-pg_dump mydb | harbourbridge -driver=pgdump
+pg_dump mydb | harbourbridge -driver=pg_dump
 ```
 
 The tool can also be applied to an existing pg_dump file:
 
 ```sh
-harbourbridge -driver=pgdump < my_pg_dump_file
+harbourbridge -driver=pg_dump < my_pg_dump_file
 ```
 
 To specify a particular Spanner instance to use, run:
 
 ```sh
-pg_dump mydb | harbourbridge -driver=pgdump -instance my-spanner-instance
+pg_dump mydb | harbourbridge -driver=pg_dump -instance my-spanner-instance
 ```
 
 By default, HarbourBridge will generate a new Spanner database name to populate.
 You can override this and specify the database name to use by:
 
 ```sh
-pg_dump mydb | harbourbridge -driver=pgdump -dbname my-spanner-database-name
+pg_dump mydb | harbourbridge -driver=pg_dump -dbname my-spanner-database-name
 ```
 
 HarbourBridge generates a report file, a schema file, and a bad-data file (if
@@ -45,14 +45,14 @@ there are bad-data rows). You can control where these files are written by
 specifying a file prefix. For example,
 
 ```sh
-pg_dump mydb | harbourbridge -driver=pgdump -prefix mydb.
+pg_dump mydb | harbourbridge -driver=pg_dump -prefix mydb.
 ```
 
 will write files `mydb.report.txt`, `mydb.schema.txt`, and
 `mydb.dropped.txt`. The prefix can also be a directory. For example,
 
 ```sh
-pg_dump mydb | harbourbridge -driver=pgdump -prefix ~/spanner-eval-mydb/
+pg_dump mydb | harbourbridge -driver=pg_dump -prefix ~/spanner-eval-mydb/
 ```
 
 would write the files into the directory `~/spanner-eval-mydb/`. Note
diff --git a/postgres/pgdump.go b/postgres/pgdump.go
index c212fda..7e203a9 100644
--- a/postgres/pgdump.go
+++ b/postgres/pgdump.go
@@ -133,7 +133,7 @@ func processCopyBlock(conv *internal.Conv, srcTable string, srcCols []string, r
 		if !conv.DataMode() {
 			continue
 		}
-		// Pgdump escapes backslash in copy-block statements. For example:
+		// pg_dump escapes backslash in copy-block statements. For example:
 		// a) a\"b becomes a\\"b in COPY-BLOCK (but 'a\"b' in INSERT-INTO)
 		// b) {"a\"b"} becomes {"a\\"b"} in COPY-BLOCK (but '{"a\"b"}' in INSERT-INTO)
 		// Note: a'b and {a'b} are unchanged in COPY-BLOCK and INSERT-INTO.


Test patch:
diff --git a/mysql/infoschema_test.go b/mysql/infoschema_test.go
index 51f10cc..88fc654 100644
--- a/mysql/infoschema_test.go
+++ b/mysql/infoschema_test.go
@@ -36,11 +36,12 @@ type mockSpec struct {
 func TestProcessInfoSchemaMYSQL(t *testing.T) {
 	ms := []mockSpec{
 		{
-			query: "SELECT table_schema, table_name FROM information_schema.tables where table_type = 'BASE TABLE'",
-			cols:  []string{"table_schema", "table_name"},
+			query: "SELECT (.+) FROM information_schema.tables where table_type = 'BASE TABLE'  and (.+)",
+			args:  []driver.Value{"test"},
+			cols:  []string{"table_name"},
 			rows: [][]driver.Value{
-				{"test", "cart"},
-				{"test", "test"}},
+				{"cart"},
+				{"test"}},
 		}, {
 			query: "SELECT (.+) FROM information_schema.COLUMNS (.+)",
 			args:  []driver.Value{"test", "cart"},
@@ -90,11 +91,11 @@ func TestProcessInfoSchemaMYSQL(t *testing.T) {
 	}
 	db := mkMockDB(t, ms)
 	conv := internal.MakeConv()
-	err := ProcessInfoSchema(conv, db)
+	err := ProcessInfoSchema(conv, db, "test")
 	assert.Nil(t, err)
 	expectedSchema := map[string]ddl.CreateTable{
-		"test_cart": ddl.CreateTable{
-			Name:     "test_cart",
+		"cart": ddl.CreateTable{
+			Name:     "cart",
 			ColNames: []string{"productid", "userid", "quantity"},
 			ColDefs: map[string]ddl.ColumnDef{
 				"productid": ddl.ColumnDef{Name: "productid", T: ddl.String{Len: ddl.MaxLength{}}, NotNull: true},
@@ -102,8 +103,8 @@ func TestProcessInfoSchemaMYSQL(t *testing.T) {
 				"quantity":  ddl.ColumnDef{Name: "quantity", T: ddl.Int64{}},
 			},
 			Pks: []ddl.IndexKey{ddl.IndexKey{Col: "productid"}, ddl.IndexKey{Col: "userid"}}},
-		"test_test": ddl.CreateTable{
-			Name:     "test_test",
+		"test": ddl.CreateTable{
+			Name:     "test",
 			ColNames: []string{"id", "s", "txt", "b", "bs", "bl", "c", "c8", "d", "f8", "f4", "i8", "i4", "i2", "si", "ts", "tz", "vc", "vc6"},
 			ColDefs: map[string]ddl.ColumnDef{
 				"id":  ddl.ColumnDef{Name: "id", T: ddl.Int64{}, NotNull: true},
@@ -129,7 +130,7 @@ func TestProcessInfoSchemaMYSQL(t *testing.T) {
 			Pks: []ddl.IndexKey{ddl.IndexKey{Col: "id"}}},
 	}
 	assert.Equal(t, expectedSchema, stripSchemaComments(conv.SpSchema))
-	assert.Equal(t, len(conv.Issues["test.cart"]), 0)
+	assert.Equal(t, len(conv.Issues["cart"]), 0)
 	expectedIssues := map[string][]internal.SchemaIssue{
 		"bs": []internal.SchemaIssue{internal.DefaultValue},
 		"f4": []internal.SchemaIssue{internal.Widened},
@@ -138,16 +139,17 @@ func TestProcessInfoSchemaMYSQL(t *testing.T) {
 		"si": []internal.SchemaIssue{internal.Widened, internal.DefaultValue},
 		"ts": []internal.SchemaIssue{internal.Datetime},
 	}
-	assert.Equal(t, expectedIssues, conv.Issues["test.test"])
+	assert.Equal(t, expectedIssues, conv.Issues["test"])
 	assert.Equal(t, int64(0), conv.Unexpecteds())
 }
 
 func TestProcessSQLData(t *testing.T) {
 	ms := []mockSpec{
 		{
-			query: "SELECT table_schema, table_name FROM information_schema.tables where table_type = 'BASE TABLE'",
-			cols:  []string{"table_schema", "table_name"},
-			rows:  [][]driver.Value{{"test", "te st"}},
+			query: "SELECT table_name FROM information_schema.tables where table_type = 'BASE TABLE' and (.+)",
+			args:  []driver.Value{"test"},
+			cols:  []string{"table_name"},
+			rows:  [][]driver.Value{{"te st"}},
 		}, {
 			query: "SELECT (.+) FROM test.te st",
 			cols:  []string{"a a", " b", " c "},
@@ -160,7 +162,7 @@ func TestProcessSQLData(t *testing.T) {
 	db := mkMockDB(t, ms)
 	conv := buildConv(
 		ddl.CreateTable{
-			Name:     "test_te_st",
+			Name:     "te_st",
 			ColNames: []string{"a a", " b", " c "},
 			ColDefs: map[string]ddl.ColumnDef{
 				"a_a": ddl.ColumnDef{Name: "a_a", T: ddl.Float64{}},
@@ -168,7 +170,7 @@ func TestProcessSQLData(t *testing.T) {
 				"Ac_": ddl.ColumnDef{Name: "Ac_", T: ddl.String{Len: ddl.MaxLength{}}},
 			}},
 		schema.Table{
-			Name:     "test.te st",
+			Name:     "te st",
 			ColNames: []string{"a_a", "_b", "_c_"},
 			ColDefs: map[string]schema.Column{
 				"a a": schema.Column{Name: "a a", Type: schema.Type{Name: "float"}},
@@ -182,15 +184,15 @@ func TestProcessSQLData(t *testing.T) {
 		func(table string, cols []string, vals []interface{}) {
 			rows = append(rows, spannerData{table: table, cols: cols, vals: vals})
 		})
-	ProcessSQLData(conv, db)
+	ProcessSQLData(conv, db, "test")
 	assert.Equal(t,
 		[]spannerData{
-			spannerData{table: "test_te_st", cols: []string{"a_a", "Ab", "Ac_"}, vals: []interface{}{float64(42.3), int64(3), "cat"}},
-			spannerData{table: "test_te_st", cols: []string{"a_a", "Ab", "Ac_"}, vals: []interface{}{float64(6.6), int64(22), "dog"}},
+			spannerData{table: "te_st", cols: []string{"a_a", "Ab", "Ac_"}, vals: []interface{}{float64(42.3), int64(3), "cat"}},
+			spannerData{table: "te_st", cols: []string{"a_a", "Ab", "Ac_"}, vals: []interface{}{float64(6.6), int64(22), "dog"}},
 		},
 		rows)
 	assert.Equal(t, conv.BadRows(), int64(1))
-	assert.Equal(t, conv.SampleBadRows(10), []string{"table=test.te st cols=[a a  b  c ] data=[6.6 2006-01-02 dog]\n"})
+	assert.Equal(t, conv.SampleBadRows(10), []string{"table=te st cols=[a a  b  c ] data=[6.6 2006-01-02 dog]\n"})
 	assert.Equal(t, int64(1), conv.Unexpecteds()) // Bad row generates an entry in unexpected.
 }
 
@@ -202,9 +204,10 @@ func TestProcessSQLData_MultiCol(t *testing.T) {
 	// ProcessInfoSchema.
 	ms := []mockSpec{
 		{
-			query: "SELECT table_schema, table_name FROM information_schema.tables where table_type = 'BASE TABLE'",
-			cols:  []string{"table_schema", "table_name"},
-			rows:  [][]driver.Value{{"test", "test"}},
+			query: "SELECT table_name FROM information_schema.tables where table_type = 'BASE TABLE' and (.+)",
+			args:  []driver.Value{"test"},
+			cols:  []string{"table_name"},
+			rows:  [][]driver.Value{{"test"}},
 		}, {
 			query: "SELECT (.+) FROM information_schema.COLUMNS (.+)",
 			args:  []driver.Value{"test", "test"},
@@ -218,12 +221,17 @@ func TestProcessSQLData_MultiCol(t *testing.T) {
 			query: "SELECT (.+) FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS (.+)",
 			args:  []driver.Value{"test", "test"},
 			cols:  []string{"column_name", "constraint_type"},
-			rows:  [][]driver.Value{},
+			rows:  [][]driver.Value{}, // No primary key --> force generation of synthetic key.
 		},
+		// Note: go-sqlmock mocks specify an ordered sequence
+		// of queries and results.  This (repeated) entry is
+		// needed because ProcessSQLData (redundantly) gets
+		// the set of tables via a SQL query.
 		{
-			query: "SELECT table_schema, table_name FROM information_schema.tables where table_type = 'BASE TABLE'",
-			cols:  []string{"table_schema", "table_name"},
-			rows:  [][]driver.Value{{"test", "test"}},
+			query: "SELECT table_name FROM information_schema.tables where table_type = 'BASE TABLE' and (.+)",
+			args:  []driver.Value{"test"},
+			cols:  []string{"table_name"},
+			rows:  [][]driver.Value{{"test"}},
 		}, {
 			query: "SELECT (.+) FROM test.test",
 			cols:  []string{"a", "b", "c"},
@@ -234,11 +242,11 @@ func TestProcessSQLData_MultiCol(t *testing.T) {
 	}
 	db := mkMockDB(t, ms)
 	conv := internal.MakeConv()
-	err := ProcessInfoSchema(conv, db)
+	err := ProcessInfoSchema(conv, db, "test")
 	assert.Nil(t, err)
 	expectedSchema := map[string]ddl.CreateTable{
-		"test_test": ddl.CreateTable{
-			Name:     "test_test",
+		"test": ddl.CreateTable{
+			Name:     "test",
 			ColNames: []string{"a", "b", "c", "synth_id"},
 			ColDefs: map[string]ddl.ColumnDef{
 				"a":        ddl.ColumnDef{Name: "a", T: ddl.String{Len: ddl.MaxLength{}}, NotNull: true},
@@ -250,7 +258,7 @@ func TestProcessSQLData_MultiCol(t *testing.T) {
 	}
 	assert.Equal(t, expectedSchema, stripSchemaComments(conv.SpSchema))
 	expectedIssues := map[string][]internal.SchemaIssue{}
-	assert.Equal(t, expectedIssues, conv.Issues["test.test"])
+	assert.Equal(t, expectedIssues, conv.Issues["test"])
 	assert.Equal(t, int64(0), conv.Unexpecteds())
 	conv.SetDataMode()
 	var rows []spannerData
@@ -258,10 +266,10 @@ func TestProcessSQLData_MultiCol(t *testing.T) {
 		func(table string, cols []string, vals []interface{}) {
 			rows = append(rows, spannerData{table: table, cols: cols, vals: vals})
 		})
-	ProcessSQLData(conv, db)
+	ProcessSQLData(conv, db, "test")
 	assert.Equal(t, []spannerData{
-		{table: "test_test", cols: []string{"a", "b", "synth_id"}, vals: []interface{}{"cat", float64(42.3), int64(0)}},
-		{table: "test_test", cols: []string{"a", "c", "synth_id"}, vals: []interface{}{"dog", int64(22), int64(-9223372036854775808)}}},
+		{table: "test", cols: []string{"a", "b", "synth_id"}, vals: []interface{}{"cat", float64(42.3), int64(0)}},
+		{table: "test", cols: []string{"a", "c", "synth_id"}, vals: []interface{}{"dog", int64(22), int64(-9223372036854775808)}}},
 		rows)
 	assert.Equal(t, int64(0), conv.Unexpecteds())
 }
@@ -269,9 +277,10 @@ func TestProcessSQLData_MultiCol(t *testing.T) {
 func TestSetRowStats(t *testing.T) {
 	ms := []mockSpec{
 		{
-			query: "SELECT table_schema, table_name FROM information_schema.tables where table_type = 'BASE TABLE'",
-			cols:  []string{"table_schema", "table_name"},
-			rows:  [][]driver.Value{{"test", "test1"}, {"test", "test2"}},
+			query: "SELECT table_name FROM information_schema.tables where table_type = 'BASE TABLE' and (.+)",
+			args:  []driver.Value{"test"},
+			cols:  []string{"table_name"},
+			rows:  [][]driver.Value{{"test1"}, {"test2"}},
 		}, {
 			query: `SELECT COUNT[(][*][)] FROM test.test1`,
 			cols:  []string{"count"},
@@ -285,9 +294,9 @@ func TestSetRowStats(t *testing.T) {
 	db := mkMockDB(t, ms)
 	conv := internal.MakeConv()
 	conv.SetDataMode()
-	SetRowStats(conv, db)
-	assert.Equal(t, int64(5), conv.Stats.Rows["test.test1"])
-	assert.Equal(t, int64(142), conv.Stats.Rows["test.test2"])
+	SetRowStats(conv, db, "test")
+	assert.Equal(t, int64(5), conv.Stats.Rows["test1"])
+	assert.Equal(t, int64(142), conv.Stats.Rows["test2"])
 	assert.Equal(t, int64(0), conv.Unexpecteds())
 }
 
diff --git a/postgres/report_test.go b/postgres/report_test.go
index c331c6b..ae16fb6 100644
--- a/postgres/report_test.go
+++ b/postgres/report_test.go
@@ -55,7 +55,7 @@ func TestReport(t *testing.T) {
 	conv.Stats.Unexpected["Testing unexpected messages"] = 5
 	buf := new(bytes.Buffer)
 	w := bufio.NewWriter(buf)
-	internal.GenerateReport("pgdump", conv, w, badWrites)
+	internal.GenerateReport("pg_dump", conv, w, badWrites)
 	w.Flush()
 	// Print copy of report to stdout (shows up when running go test -v).
 	fmt.Print(buf.String())
@@ -70,7 +70,7 @@ Summary of Conversion
 Schema conversion: OK (some columns did not map cleanly + some missing primary keys).
 Data conversion: POOR (66% of 6000 rows written to Spanner).
 
-The remainder of this report provides stats on the pgdump statements processed,
+The remainder of this report provides stats on the pg_dump statements processed,
 followed by a table-by-table listing of schema and data conversion details. For
 background on the schema and data conversion process used, and explanations of
 the terms and notes used in this report, see HarbourBridge's README.
@@ -78,7 +78,7 @@ the terms and notes used in this report, see HarbourBridge's README.
 ----------------------------
 Statements Processed
 ----------------------------
-Analysis of statements in pgdump output, broken down by statement type.
+Analysis of statements in pg_dump output, broken down by statement type.
   schema: statements successfully processed for Spanner schema information.
     data: statements successfully processed for data.
     skip: statements not relevant for Spanner schema or data.