Repository: cloudspannerecosystem/harbourbridge
Pull request #60 (2020-05-30 07:21:10)

Postgres - common changes
Refactored convert.go to make functions public which would be useful to make a separate postgres package

googlebot: 
Thanks for your pull request. It looks like this may be your first contribution to a Google open source project (if not, look below for help). Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

:memo: **Please visit <https://cla.developers.google.com/> to sign.**

Once you've signed (or fixed any issues), please reply here with `@googlebot I signed it!` and we'll verify it.

----

#### What to do if you already signed the CLA

##### Individual signers

*   It's possible we don't have your GitHub username or you're using a different email address on your commit. Check [your existing CLA data](https://cla.developers.google.com/clas) and verify that your [email is set on your git commits](https://help.github.com/articles/setting-your-email-in-git/).

##### Corporate signers

*   Your company has a Point of Contact who decides which employees are authorized to participate. Ask your POC to be added to the group of authorized contributors. If you don't know who your Point of Contact is, direct the Google project maintainer to [go/cla#troubleshoot](http://go/cla#troubleshoot) ([Public version](https://opensource.google/docs/cla/#troubleshoot)).
*   The email used to register you as an authorized contributor must be the email used for the Git commit. Check [your existing CLA data](https://cla.developers.google.com/clas) and verify that your [email is set on your git commits](https://help.github.com/articles/setting-your-email-in-git/).
*   The email used to register you as an authorized contributor must also be [attached to your GitHub account](https://github.com/settings/emails).
		

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

<!-- need_sender_cla -->
Dharmil-searce: @googlebot I signed it!
googlebot: CLAs look good, thanks!

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

<!-- ok -->
hengfengli: @Dharmil-searce Is this the first step in our planning doc? It seems that it includes the first two steps in this PR. Or maybe I am wrong. 
Dharmil-searce: @hengfengli Yes, it includes 2 steps from the doc. 1st commit changes are useful for moving Postgres in different package. Only 1st commit standalone is not useful and justified. That is the reason I have added 2 steps in this PR for 'postgres package'. If you suggest then we can also do different PR for each commit.
hengfengli: I would prefer to split them into separate PRs so that each PR can become smaller and better for code reviews. Thanks for doing that. 
Dharmil-searce: @hengfengli Please check. I have removed 2nd commit and I will make separate PR for postgres package.
hengfengli: @Dharmil-searce Thanks for resolving the comments. It can be merged once @nevinheintze approves it. 
Dharmil-searce: @nevinheintze Sure, we can discuss on our weekly sync up call. Handling of timezone is different in mysql and postgres but sure I will check on how we can have only one timezone spec in convert.go.
@hengfengli I have renamed renaming Offset to TimezoneOffset.
PR can me merged now

Patch:
diff --git a/internal/convert.go b/internal/convert.go
index 687d611..ef1e627 100644
--- a/internal/convert.go
+++ b/internal/convert.go
@@ -16,13 +16,9 @@ package internal
 
 import (
 	"fmt"
-	"reflect"
 	"sort"
-	"strings"
 	"time"
 
-	nodes "github.com/lfittl/pg_query_go/nodes"
-
 	"github.com/cloudspannerecosystem/harbourbridge/schema"
 	"github.com/cloudspannerecosystem/harbourbridge/spanner/ddl"
 )
@@ -30,16 +26,17 @@ import (
 // Conv contains all schema and data conversion state.
 type Conv struct {
 	mode           mode                                // Schema mode or data mode.
-	spSchema       map[string]ddl.CreateTable          // Maps Spanner table name to Spanner schema.
-	syntheticPKeys map[string]syntheticPKey            // Maps Spanner table name to synthetic primary key (if needed).
-	srcSchema      map[string]schema.Table             // Maps source-DB table name to schema information.
-	issues         map[string]map[string][]schemaIssue // Maps source-DB table/col to list of schema conversion issues.
-	toSpanner      map[string]nameAndCols              // Maps from source-DB table name to Spanner name and column mapping.
-	toSource       map[string]nameAndCols              // Maps from Spanner table name to source-DB table name and column mapping.
+	SpSchema       map[string]ddl.CreateTable          // Maps Spanner table name to Spanner schema.
+	SyntheticPKeys map[string]SyntheticPKey            // Maps Spanner table name to synthetic primary key (if needed).
+	SrcSchema      map[string]schema.Table             // Maps source-DB table name to schema information.
+	Issues         map[string]map[string][]SchemaIssue // Maps source-DB table/col to list of schema conversion issues.
+	ToSpanner      map[string]NameAndCols              // Maps from source-DB table name to Spanner name and column mapping.
+	ToSource       map[string]NameAndCols              // Maps from Spanner table name to source-DB table name and column mapping.
 	dataSink       func(table string, cols []string, values []interface{})
-	location       *time.Location // Timezone (for timestamp conversion).
+	Location       *time.Location // Timezone (for timestamp conversion).
 	sampleBadRows  rowSamples     // Rows that generated errors during conversion.
-	stats          stats
+	Stats          stats
+	TimezoneOffset string // Timezone offset for timestamp conversion.
 }
 
 type mode int
@@ -49,37 +46,37 @@ const (
 	dataOnly
 )
 
-// syntheticPKey specifies a synthetic primary key and current sequence
+// SyntheticPKey specifies a synthetic primary key and current sequence
 // count for a table, if needed. We use a synthetic primary key when
 // the source DB table has no primary key.
-type syntheticPKey struct {
-	col      string
-	sequence int64
+type SyntheticPKey struct {
+	Col      string
+	Sequence int64
 }
 
-type schemaIssue int
+type SchemaIssue int
 
 // Defines all of the schema issues we track. Includes issues
 // with type mappings, as well as features (such as source
 // DB constraints) that aren't supported in Spanner.
 const (
-	defaultValue schemaIssue = iota
-	foreignKey
-	missingPrimaryKey
-	multiDimensionalArray
-	noGoodType
-	numeric
-	numericThatFits
-	serial
-	timestamp
-	widened
+	DefaultValue SchemaIssue = iota
+	ForeignKey
+	MissingPrimaryKey
+	MultiDimensionalArray
+	NoGoodType
+	Numeric
+	NumericThatFits
+	Serial
+	Timestamp
+	Widened
 )
 
-// nameAndCols contains the name of a table and its columns.
+// NameAndCols contains the name of a table and its columns.
 // Used to map between source DB and Spanner table and column names.
-type nameAndCols struct {
-	name string
-	cols map[string]string
+type NameAndCols struct {
+	Name string
+	Cols map[string]string
 }
 
 type rowSamples struct {
@@ -101,39 +98,40 @@ type row struct {
 // c) successfully converted, but an error occurs when writing the row to Spanner.
 // d) unsuccessfully converted (we won't try to write such rows to Spanner).
 type stats struct {
-	rows       map[string]int64          // Count of rows encountered during processing (a + b + c + d), broken down by source table.
-	goodRows   map[string]int64          // Count of rows successfully converted (b + c), broken down by source table.
-	badRows    map[string]int64          // Count of rows where conversion failed (d), broken down by source table.
-	statement  map[string]*statementStat // Count of processed statements, broken down by statement type.
-	unexpected map[string]int64          // Count of unexpected conditions, broken down by condition description.
-	reparsed   int64                     // Count of times we re-parse pg_dump data looking for end-of-statement.
+	Rows       map[string]int64          // Count of rows encountered during processing (a + b + c + d), broken down by source table.
+	GoodRows   map[string]int64          // Count of rows successfully converted (b + c), broken down by source table.
+	BadRows    map[string]int64          // Count of rows where conversion failed (d), broken down by source table.
+	Statement  map[string]*statementStat // Count of processed statements, broken down by statement type.
+	Unexpected map[string]int64          // Count of unexpected conditions, broken down by condition description.
+	Reparsed   int64                     // Count of times we re-parse dump data looking for end-of-statement.
 }
 
 type statementStat struct {
 	schema int64
 	data   int64
 	skip   int64
-	error  int64
+	Error  int64
 }
 
 // MakeConv returns a default-configured Conv.
 func MakeConv() *Conv {
 	return &Conv{
-		spSchema:       make(map[string]ddl.CreateTable),
-		syntheticPKeys: make(map[string]syntheticPKey),
-		srcSchema:      make(map[string]schema.Table),
-		issues:         make(map[string]map[string][]schemaIssue),
-		toSpanner:      make(map[string]nameAndCols),
-		toSource:       make(map[string]nameAndCols),
-		location:       time.Local, // By default, use go's local time, which uses $TZ (when set).
+		SpSchema:       make(map[string]ddl.CreateTable),
+		SyntheticPKeys: make(map[string]SyntheticPKey),
+		SrcSchema:      make(map[string]schema.Table),
+		Issues:         make(map[string]map[string][]SchemaIssue),
+		ToSpanner:      make(map[string]NameAndCols),
+		ToSource:       make(map[string]NameAndCols),
+		Location:       time.Local, // By default, use go's local time, which uses $TZ (when set).
 		sampleBadRows:  rowSamples{bytesLimit: 10 * 1000 * 1000},
-		stats: stats{
-			rows:       make(map[string]int64),
-			goodRows:   make(map[string]int64),
-			badRows:    make(map[string]int64),
-			statement:  make(map[string]*statementStat),
-			unexpected: make(map[string]int64),
+		Stats: stats{
+			Rows:       make(map[string]int64),
+			GoodRows:   make(map[string]int64),
+			BadRows:    make(map[string]int64),
+			Statement:  make(map[string]*statementStat),
+			Unexpected: make(map[string]int64),
 		},
+		TimezoneOffset: "+00:00", // By default, use +00:00 offset which is equal to UTC timezone
 	}
 }
 
@@ -168,13 +166,13 @@ func (conv *Conv) SetDataMode() {
 // Return DDL in alphabetical table order.
 func (conv *Conv) GetDDL(c ddl.Config) []string {
 	var tables []string
-	for t := range conv.spSchema {
+	for t := range conv.SpSchema {
 		tables = append(tables, t)
 	}
 	sort.Strings(tables)
 	var ddl []string
 	for _, t := range tables {
-		ddl = append(ddl, conv.spSchema[t].PrintCreateTable(c))
+		ddl = append(ddl, conv.SpSchema[t].PrintCreateTable(c))
 	}
 	return ddl
 }
@@ -184,18 +182,18 @@ func (conv *Conv) WriteRow(srcTable, spTable string, spCols []string, spVals []i
 	if conv.dataSink == nil {
 		msg := "Internal error: ProcessDataRow called but dataSink not configured"
 		VerbosePrintf("%s\n", msg)
-		conv.unexpected(msg)
-		conv.statsAddBadRow(srcTable, conv.dataMode())
+		conv.Unexpected(msg)
+		conv.StatsAddBadRow(srcTable, conv.DataMode())
 	} else {
 		conv.dataSink(spTable, spCols, spVals)
-		conv.statsAddGoodRow(srcTable, conv.dataMode())
+		conv.statsAddGoodRow(srcTable, conv.DataMode())
 	}
 }
 
 // Rows returns the total count of data rows processed.
 func (conv *Conv) Rows() int64 {
 	n := int64(0)
-	for _, c := range conv.stats.rows {
+	for _, c := range conv.Stats.Rows {
 		n += c
 	}
 	return n
@@ -205,7 +203,7 @@ func (conv *Conv) Rows() int64 {
 // data conversion.
 func (conv *Conv) BadRows() int64 {
 	n := int64(0)
-	for _, c := range conv.stats.badRows {
+	for _, c := range conv.Stats.BadRows {
 		n += c
 	}
 	return n
@@ -214,8 +212,8 @@ func (conv *Conv) BadRows() int64 {
 // Statements returns the total number of statements processed.
 func (conv *Conv) Statements() int64 {
 	n := int64(0)
-	for _, x := range conv.stats.statement {
-		n += x.schema + x.data + x.skip + x.error
+	for _, x := range conv.Stats.Statement {
+		n += x.schema + x.data + x.skip + x.Error
 	}
 	return n
 }
@@ -223,8 +221,8 @@ func (conv *Conv) Statements() int64 {
 // StatementErrors returns the number of statement errors encountered.
 func (conv *Conv) StatementErrors() int64 {
 	n := int64(0)
-	for _, x := range conv.stats.statement {
-		n += x.error
+	for _, x := range conv.Stats.Statement {
+		n += x.Error
 	}
 	return n
 }
@@ -232,7 +230,7 @@ func (conv *Conv) StatementErrors() int64 {
 // Unexpecteds returns the total number of distinct unexpected conditions
 // encountered during processing.
 func (conv *Conv) Unexpecteds() int64 {
-	return int64(len(conv.stats.unexpected))
+	return int64(len(conv.Stats.Unexpected))
 }
 
 // CollectBadRows updates the list of bad rows, while respecting
@@ -263,34 +261,34 @@ func (conv *Conv) SampleBadRows(n int) []string {
 // AddPrimaryKeys analyzes all tables in conv.schema and adds synthetic primary
 // keys for any tables that don't have primary key.
 func (conv *Conv) AddPrimaryKeys() {
-	for t, ct := range conv.spSchema {
+	for t, ct := range conv.SpSchema {
 		if len(ct.Pks) == 0 {
 			k := conv.buildPrimaryKey(t)
 			ct.ColNames = append(ct.ColNames, k)
 			ct.ColDefs[k] = ddl.ColumnDef{Name: k, T: ddl.Int64{}}
 			ct.Pks = []ddl.IndexKey{ddl.IndexKey{Col: k}}
-			conv.spSchema[t] = ct
-			conv.syntheticPKeys[t] = syntheticPKey{k, 0}
+			conv.SpSchema[t] = ct
+			conv.SyntheticPKeys[t] = SyntheticPKey{k, 0}
 		}
 	}
 }
 
 // SetLocation configures the timezone for data conversion.
 func (conv *Conv) SetLocation(loc *time.Location) {
-	conv.location = loc
+	conv.Location = loc
 }
 
 func (conv *Conv) buildPrimaryKey(spTable string) string {
 	base := "synth_id"
-	if _, ok := conv.toSource[spTable]; !ok {
-		conv.unexpected(fmt.Sprintf("toSource lookup fails for table %s: ", spTable))
+	if _, ok := conv.ToSource[spTable]; !ok {
+		conv.Unexpected(fmt.Sprintf("ToSource lookup fails for table %s: ", spTable))
 		return base
 	}
 	count := 0
 	key := base
 	for {
 		// Check key isn't already a column in the table.
-		if _, ok := conv.toSource[spTable].cols[key]; !ok {
+		if _, ok := conv.ToSource[spTable].Cols[key]; !ok {
 			return key
 		}
 		key = fmt.Sprintf("%s%d", base, count)
@@ -301,17 +299,17 @@ func (conv *Conv) buildPrimaryKey(spTable string) string {
 // unexpected records stats about corner-cases and conditions
 // that were not expected. Note that the counts maybe not
 // be completely reliable due to potential double-counting
-// because we process pg_dump data twice.
-func (conv *Conv) unexpected(u string) {
+// because we process dump data twice.
+func (conv *Conv) Unexpected(u string) {
 	VerbosePrintf("Unexpected condition: %s\n", u)
 	// Limit size of unexpected map. If over limit, then only
 	// update existing entries.
-	if _, ok := conv.stats.unexpected[u]; ok || len(conv.stats.unexpected) < 1000 {
-		conv.stats.unexpected[u]++
+	if _, ok := conv.Stats.Unexpected[u]; ok || len(conv.Stats.Unexpected) < 1000 {
+		conv.Stats.Unexpected[u]++
 	}
 }
 
-// statsAddRow increments the count of rows for 'srcTable' if b is
+// StatsAddRow increments the count of rows for 'srcTable' if b is
 // true.  The boolean arg 'b' is used to avoid double counting of
 // stats. Specifically, some code paths that report row stats run in
 // both schema-mode and data-mode e.g. statement.go.  To avoid double
@@ -319,92 +317,76 @@ func (conv *Conv) unexpected(u string) {
 // place where row stats are collected. When specifying this mode take
 // care to ensure that the code actually runs in the mode you specify,
 // otherwise stats will be dropped.
-func (conv *Conv) statsAddRow(srcTable string, b bool) {
+func (conv *Conv) StatsAddRow(srcTable string, b bool) {
 	if b {
-		conv.stats.rows[srcTable]++
+		conv.Stats.Rows[srcTable]++
 	}
 }
 
-func (conv *Conv) statsAddRows(srcTable string, count int64) {
-	conv.stats.rows[srcTable] += count
-}
-
 // statsAddGoodRow increments the good-row stats for 'srcTable' if b
-// is true.  See statsAddRow comments for context.
+// is true.  See StatsAddRow comments for context.
 func (conv *Conv) statsAddGoodRow(srcTable string, b bool) {
 	if b {
-		conv.stats.goodRows[srcTable]++
+		conv.Stats.GoodRows[srcTable]++
 	}
 }
 
-func (conv *Conv) statsAddGoodRows(srcTable string, count int64) {
-	conv.stats.goodRows[srcTable] += count
-}
-
-// statsAddBadRow increments the bad-row stats for 'srcTable' if b is
-// true.  See statsAddRow comments for context.
-func (conv *Conv) statsAddBadRow(srcTable string, b bool) {
+// StatsAddBadRow increments the bad-row stats for 'srcTable' if b is
+// true.  See StatsAddRow comments for context.
+func (conv *Conv) StatsAddBadRow(srcTable string, b bool) {
 	if b {
-		conv.stats.badRows[srcTable]++
+		conv.Stats.BadRows[srcTable]++
 	}
 }
 
-func (conv *Conv) statsAddBadRows(srcTable string, count int64) {
-	conv.stats.badRows[srcTable] += count
-}
-
-func prNodeType(n nodes.Node) string {
-	// Strip off "pg_query." prefix from nodes.Nodes type.
-	return strings.TrimPrefix(reflect.TypeOf(n).Name(), "pg_query.")
-}
-
-func prNodes(l []nodes.Node) string {
-	var s []string
-	for _, n := range l {
-		s = append(s, prNodeType(n))
-	}
-	return strings.Join(s, ".")
-}
-
 func (conv *Conv) getStatementStat(s string) *statementStat {
-	if conv.stats.statement[s] == nil {
-		conv.stats.statement[s] = &statementStat{}
+	if conv.Stats.Statement[s] == nil {
+		conv.Stats.Statement[s] = &statementStat{}
 	}
-	return conv.stats.statement[s]
+	return conv.Stats.Statement[s]
 }
 
-func (conv *Conv) skipStatement(l []nodes.Node) {
-	if conv.schemaMode() { // Record statement stats on first pass only.
-		s := prNodes(l)
-		VerbosePrintf("Skipping statement: %s\n", s)
-		conv.getStatementStat(s).skip++
+func (conv *Conv) SkipStatement(stmtType string) {
+	if conv.SchemaMode() { // Record statement stats on first pass only.
+		VerbosePrintf("Skipping statement: %s\n", stmtType)
+		conv.getStatementStat(stmtType).skip++
 	}
 }
 
-func (conv *Conv) errorInStatement(l []nodes.Node) {
-	if conv.schemaMode() { // Record statement stats on first pass only.
-		s := prNodes(l)
-		VerbosePrintf("Error processing statement: %s\n", s)
-		conv.getStatementStat(s).error++
+func (conv *Conv) ErrorInStatement(stmtType string) {
+	if conv.SchemaMode() { // Record statement stats on first pass only.
+		VerbosePrintf("Error processing statement: %s\n", stmtType)
+		conv.getStatementStat(stmtType).Error++
 	}
 }
 
-func (conv *Conv) schemaStatement(l []nodes.Node) {
-	if conv.schemaMode() { // Record statement stats on first pass only.
-		conv.getStatementStat(prNodes(l)).schema++
+func (conv *Conv) SchemaStatement(stmtType string) {
+	if conv.SchemaMode() { // Record statement stats on first pass only.
+		conv.getStatementStat(stmtType).schema++
 	}
 }
 
-func (conv *Conv) dataStatement(l []nodes.Node) {
-	if conv.schemaMode() { // Record statement stats on first pass only.
-		conv.getStatementStat(prNodes(l)).data++
+func (conv *Conv) DataStatement(stmtType string) {
+	if conv.SchemaMode() { // Record statement stats on first pass only.
+		conv.getStatementStat(stmtType).data++
 	}
 }
 
-func (conv *Conv) schemaMode() bool {
+func (conv *Conv) SchemaMode() bool {
 	return conv.mode == schemaOnly
 }
 
-func (conv *Conv) dataMode() bool {
+func (conv *Conv) DataMode() bool {
 	return conv.mode == dataOnly
 }
+
+func byteSize(r *row) int64 {
+	n := int64(len(r.table))
+	for _, c := range r.cols {
+		n += int64(len(c))
+	}
+	for _, v := range r.vals {
+		n += int64(len(v))
+	}
+	return n
+}
diff --git a/internal/data.go b/internal/data.go
index d2ce0f0..758a199 100644
--- a/internal/data.go
+++ b/internal/data.go
@@ -36,8 +36,8 @@ import (
 func ProcessDataRow(conv *Conv, srcTable string, srcCols, vals []string) {
 	spTable, spCols, spVals, err := ConvertData(conv, srcTable, srcCols, vals)
 	if err != nil {
-		conv.unexpected(fmt.Sprintf("Error while converting data: %s\n", err))
-		conv.statsAddBadRow(srcTable, conv.dataMode())
+		conv.Unexpected(fmt.Sprintf("Error while converting data: %s\n", err))
+		conv.StatsAddBadRow(srcTable, conv.DataMode())
 		conv.CollectBadRow(srcTable, srcCols, vals)
 	} else {
 		conv.WriteRow(srcTable, spTable, spCols, spVals)
@@ -62,8 +62,8 @@ func ConvertData(conv *Conv, srcTable string, srcCols []string, vals []string) (
 	if err != nil {
 		return "", []string{}, []interface{}{}, fmt.Errorf("can't map source columns %v", srcCols)
 	}
-	spSchema, ok1 := conv.spSchema[spTable]
-	srcSchema, ok2 := conv.srcSchema[srcTable]
+	spSchema, ok1 := conv.SpSchema[spTable]
+	srcSchema, ok2 := conv.SrcSchema[srcTable]
 	if !ok1 || !ok2 {
 		return "", []string{}, []interface{}{}, fmt.Errorf("can't find table %s in schema", spTable)
 	}
@@ -85,9 +85,9 @@ func ConvertData(conv *Conv, srcTable string, srcCols []string, vals []string) (
 		var x interface{}
 		var err error
 		if spColDef.IsArray {
-			x, err = convArray(spColDef.T, srcColDef.Type.Name, conv.location, vals[i])
+			x, err = convArray(spColDef.T, srcColDef.Type.Name, conv.Location, vals[i])
 		} else {
-			x, err = convScalar(spColDef.T, srcColDef.Type.Name, conv.location, vals[i])
+			x, err = convScalar(spColDef.T, srcColDef.Type.Name, conv.Location, vals[i])
 		}
 		if err != nil {
 			return "", []string{}, []interface{}{}, err
@@ -95,11 +95,11 @@ func ConvertData(conv *Conv, srcTable string, srcCols []string, vals []string) (
 		v = append(v, x)
 		c = append(c, spCol)
 	}
-	if aux, ok := conv.syntheticPKeys[spTable]; ok {
-		c = append(c, aux.col)
-		v = append(v, int64(bits.Reverse64(uint64(aux.sequence))))
-		aux.sequence++
-		conv.syntheticPKeys[spTable] = aux
+	if aux, ok := conv.SyntheticPKeys[spTable]; ok {
+		c = append(c, aux.Col)
+		v = append(v, int64(bits.Reverse64(uint64(aux.Sequence))))
+		aux.Sequence++
+		conv.SyntheticPKeys[spTable] = aux
 	}
 	return spTable, c, v, nil
 }
@@ -380,14 +380,3 @@ func processQuote(s string) (string, error) {
 	}
 	return s, nil
 }
-
-func byteSize(r *row) int64 {
-	n := int64(len(r.table))
-	for _, c := range r.cols {
-		n += int64(len(c))
-	}
-	for _, v := range r.vals {
-		n += int64(len(v))
-	}
-	return n
-}
diff --git a/internal/infoschema.go b/internal/infoschema.go
index 36558bb..09b8249 100644
--- a/internal/infoschema.go
+++ b/internal/infoschema.go
@@ -77,7 +77,7 @@ func ProcessSqlData(conv *Conv, db *sql.DB) {
 	// ProcessInfoSchema instead of computing them again.
 	tables, err := getTables(db)
 	if err != nil {
-		conv.unexpected(fmt.Sprintf("Couldn't get list of table: %s", err))
+		conv.Unexpected(fmt.Sprintf("Couldn't get list of table: %s", err))
 		return
 	}
 	for _, t := range tables {
@@ -87,7 +87,7 @@ func ProcessSqlData(conv *Conv, db *sql.DB) {
 		q := fmt.Sprintf(`SELECT * FROM "%s"."%s";`, t.schema, t.name)
 		rows, err := db.Query(q)
 		if err != nil {
-			conv.unexpected(fmt.Sprintf("Couldn't get data for table: %s", err))
+			conv.Unexpected(fmt.Sprintf("Couldn't get data for table: %s", err))
 			continue
 		}
 		defer rows.Close()
@@ -95,11 +95,11 @@ func ProcessSqlData(conv *Conv, db *sql.DB) {
 		srcCols, err1 := rows.Columns()
 		spTable, err2 := GetSpannerTable(conv, srcTable)
 		spCols, err3 := GetSpannerCols(conv, srcTable, srcCols)
-		spSchema, ok1 := conv.spSchema[spTable]
-		srcSchema, ok2 := conv.srcSchema[srcTable]
+		spSchema, ok1 := conv.SpSchema[spTable]
+		srcSchema, ok2 := conv.SrcSchema[srcTable]
 		if err1 != nil || err2 != nil || err3 != nil || !ok1 || !ok2 {
-			conv.statsAddBadRows(srcTable, conv.stats.rows[srcTable])
-			conv.unexpected(fmt.Sprintf("Can't get cols and schemas for table %s: err1=%s, err2=%s, err3=%s, ok1=%t, ok2=%t",
+			conv.Stats.BadRows[srcTable] += conv.Stats.Rows[srcTable]
+			conv.Unexpected(fmt.Sprintf("Can't get cols and schemas for table %s: err1=%s, err2=%s, err3=%s, ok1=%t, ok2=%t",
 				srcTable, err1, err2, err3, ok1, ok2))
 			continue
 		}
@@ -107,15 +107,15 @@ func ProcessSqlData(conv *Conv, db *sql.DB) {
 		for rows.Next() {
 			err := rows.Scan(iv...)
 			if err != nil {
-				conv.unexpected(fmt.Sprintf("Couldn't process sql data row: %s", err))
+				conv.Unexpected(fmt.Sprintf("Couldn't process sql data row: %s", err))
 				// Scan failed, so we don't have any data to add to bad rows.
-				conv.statsAddBadRow(srcTable, conv.dataMode())
+				conv.StatsAddBadRow(srcTable, conv.DataMode())
 				continue
 			}
 			cvtCols, cvtVals, err := ConvertSqlRow(conv, srcTable, srcCols, srcSchema, spTable, spCols, spSchema, v)
 			if err != nil {
-				conv.unexpected(fmt.Sprintf("Couldn't process sql data row: %s", err))
-				conv.statsAddBadRow(srcTable, conv.dataMode())
+				conv.Unexpected(fmt.Sprintf("Couldn't process sql data row: %s", err))
+				conv.StatsAddBadRow(srcTable, conv.DataMode())
 				conv.CollectBadRow(srcTable, srcCols, valsToStrings(v))
 				continue
 			}
@@ -155,11 +155,11 @@ func ConvertSqlRow(conv *Conv, srcTable string, srcCols []string, srcSchema sche
 		vs = append(vs, spVal)
 		cs = append(cs, srcCols[i])
 	}
-	if aux, ok := conv.syntheticPKeys[spTable]; ok {
-		cs = append(cs, aux.col)
-		vs = append(vs, int64(bits.Reverse64(uint64(aux.sequence))))
-		aux.sequence++
-		conv.syntheticPKeys[spTable] = aux
+	if aux, ok := conv.SyntheticPKeys[spTable]; ok {
+		cs = append(cs, aux.Col)
+		vs = append(vs, int64(bits.Reverse64(uint64(aux.Sequence))))
+		aux.Sequence++
+		conv.SyntheticPKeys[spTable] = aux
 	}
 	return cs, vs, nil
 }
@@ -170,7 +170,7 @@ func SetRowStats(conv *Conv, db *sql.DB) {
 	// ProcessInfoSchema instead of computing them again.
 	tables, err := getTables(db)
 	if err != nil {
-		conv.unexpected(fmt.Sprintf("Couldn't get list of table: %s", err))
+		conv.Unexpected(fmt.Sprintf("Couldn't get list of table: %s", err))
 		return
 	}
 	for _, t := range tables {
@@ -181,7 +181,7 @@ func SetRowStats(conv *Conv, db *sql.DB) {
 		tableName := buildTableName(t.schema, t.name)
 		rows, err := db.Query(q)
 		if err != nil {
-			conv.unexpected(fmt.Sprintf("Couldn't get number of rows for table %s", tableName))
+			conv.Unexpected(fmt.Sprintf("Couldn't get number of rows for table %s", tableName))
 			continue
 		}
 		defer rows.Close()
@@ -192,7 +192,7 @@ func SetRowStats(conv *Conv, db *sql.DB) {
 				fmt.Printf("Can't get row count: %s\n", err)
 				continue
 			}
-			conv.statsAddRows(tableName, count)
+			conv.Stats.Rows[tableName] += count
 		}
 	}
 }
@@ -241,7 +241,7 @@ func processTable(conv *Conv, db *sql.DB, table schemaAndName) error {
 	for _, k := range primaryKeys {
 		schemaPKeys = append(schemaPKeys, schema.Key{Column: k})
 	}
-	conv.srcSchema[name] = schema.Table{
+	conv.SrcSchema[name] = schema.Table{
 		Name:        name,
 		ColNames:    colNames,
 		ColDefs:     colDefs,
@@ -360,7 +360,7 @@ func toNotNull(conv *Conv, isNullable string) bool {
 	case "NO":
 		return true
 	}
-	conv.unexpected(fmt.Sprintf("isNullable column has unknown value: %s", isNullable))
+	conv.Unexpected(fmt.Sprintf("isNullable column has unknown value: %s", isNullable))
 	return false
 }
 
@@ -369,7 +369,7 @@ func cvtSqlArray(conv *Conv, srcCd schema.Column, spCd ddl.ColumnDef, val interf
 	if !ok {
 		return nil, fmt.Errorf("can't convert array values to []byte")
 	}
-	return convArray(spCd.T, srcCd.Type.Name, conv.location, string(a))
+	return convArray(spCd.T, srcCd.Type.Name, conv.Location, string(a))
 }
 
 // cvtSqlScalar converts a values returned from a SQL query to a
@@ -453,7 +453,7 @@ func cvtSqlScalar(conv *Conv, srcCd schema.Column, spCd ddl.ColumnDef, val inter
 	case ddl.Timestamp:
 		switch v := val.(type) {
 		case string:
-			return convTimestamp(srcCd.Type.Name, conv.location, v)
+			return convTimestamp(srcCd.Type.Name, conv.Location, v)
 		case time.Time:
 			return v, nil
 		}
diff --git a/internal/pgdump.go b/internal/pgdump.go
index d926953..21951c4 100644
--- a/internal/pgdump.go
+++ b/internal/pgdump.go
@@ -68,7 +68,7 @@ func ProcessPgDump(conv *Conv, r *Reader) error {
 			break
 		}
 	}
-	if conv.schemaMode() {
+	if conv.SchemaMode() {
 		schemaToDDL(conv)
 		conv.AddPrimaryKeys()
 	}
@@ -104,7 +104,7 @@ func readAndParseChunk(conv *Conv, r *Reader) ([]byte, []nodes.Node, error) {
 			// b) a semicolon embedded in a multi-line comment, or
 			// c) a semicolon embedded a string constant or column/table name.
 			// We deal with this case by reading another line and trying again.
-			conv.stats.reparsed++
+			conv.Stats.Reparsed++
 		}
 		if r.EOF {
 			return nil, nil, fmt.Errorf("Error parsing last %d line(s) of input", len(l))
@@ -121,15 +121,15 @@ func processCopyBlock(conv *Conv, srcTable string, srcCols []string, r *Reader)
 			return
 		}
 		if r.EOF {
-			conv.unexpected("Reached eof while parsing copy-block")
+			conv.Unexpected("Reached eof while parsing copy-block")
 			return
 		}
-		conv.statsAddRow(srcTable, conv.schemaMode())
+		conv.StatsAddRow(srcTable, conv.SchemaMode())
 		// We have to read the copy-block data so that we can process the remaining
 		// pg_dump content. However, if we don't want the data, stop here.
 		// In particular, avoid the strings.Split and ProcessDataRow calls below, which
 		// will be expensive for huge datasets.
-		if !conv.dataMode() {
+		if !conv.DataMode() {
 			continue
 		}
 		// Pgdump escapes backslash in copy-block statements. For example:
@@ -159,27 +159,27 @@ func processStatements(conv *Conv, statements []nodes.Node) *copyOrInsert {
 		}
 		switch n := node.(type) {
 		case nodes.AlterTableStmt:
-			if conv.schemaMode() {
+			if conv.SchemaMode() {
 				processAlterTableStmt(conv, n)
 			}
 		case nodes.CopyStmt:
 			if i != len(statements)-1 {
-				conv.unexpected("CopyFrom is not the last statement in batch: ignoring following statements")
-				conv.errorInStatement([]nodes.Node{node})
+				conv.Unexpected("CopyFrom is not the last statement in batch: ignoring following statements")
+				conv.ErrorInStatement(prNodes([]nodes.Node{node}))
 			}
 			return processCopyStmt(conv, n)
 		case nodes.CreateStmt:
-			if conv.schemaMode() {
+			if conv.SchemaMode() {
 				processCreateStmt(conv, n)
 			}
 		case nodes.InsertStmt:
 			return processInsertStmt(conv, n)
 		case nodes.VariableSetStmt:
-			if conv.schemaMode() {
+			if conv.SchemaMode() {
 				processVariableSetStmt(conv, n)
 			}
 		default:
-			conv.skipStatement([]nodes.Node{node})
+			conv.SkipStatement(prNodes([]nodes.Node{node}))
 		}
 	}
 	return nil
@@ -195,7 +195,7 @@ func processAlterTableStmt(conv *Conv, n nodes.AlterTableStmt) {
 		logStmtError(conv, n, fmt.Errorf("can't get table name: %w", err))
 		return
 	}
-	if _, ok := conv.srcSchema[table]; ok {
+	if _, ok := conv.SrcSchema[table]; ok {
 		for _, i := range n.Cmds.Items {
 			switch a := i.(type) {
 			case nodes.AlterTableCmd:
@@ -203,20 +203,20 @@ func processAlterTableStmt(conv *Conv, n nodes.AlterTableStmt) {
 				case a.Subtype == nodes.AT_SetNotNull && a.Name != nil:
 					c := constraint{ct: nodes.CONSTR_NOTNULL, cols: []string{*a.Name}}
 					updateSchema(conv, table, []constraint{c}, "ALTER TABLE")
-					conv.schemaStatement([]nodes.Node{n, a})
+					conv.SchemaStatement(prNodes([]nodes.Node{n, a}))
 				case a.Subtype == nodes.AT_AddConstraint && a.Def != nil:
 					switch d := a.Def.(type) {
 					case nodes.Constraint:
 						updateSchema(conv, table, extractConstraints(conv, n, table, []nodes.Node{d}), "ALTER TABLE")
-						conv.schemaStatement([]nodes.Node{n, a, d})
+						conv.SchemaStatement(prNodes([]nodes.Node{n, a, d}))
 					default:
-						conv.skipStatement([]nodes.Node{n, a, d})
+						conv.SkipStatement(prNodes([]nodes.Node{n, a, d}))
 					}
 				default:
-					conv.skipStatement([]nodes.Node{n, a})
+					conv.SkipStatement(prNodes([]nodes.Node{n, a}))
 				}
 			default:
-				conv.skipStatement([]nodes.Node{n, a})
+				conv.SkipStatement(prNodes([]nodes.Node{n, a}))
 			}
 		}
 	} else {
@@ -225,7 +225,7 @@ func processAlterTableStmt(conv *Conv, n nodes.AlterTableStmt) {
 		// track tables created by "CREATE TABLE", this lookup can fail.
 		// For debugging purposes we log the lookup failure if we're
 		// in verbose mode, but otherwise  we just skip these statements.
-		conv.skipStatement([]nodes.Node{n})
+		conv.SkipStatement(prNodes([]nodes.Node{n}))
 		VerbosePrintf("Processing %v statement: table %s not found", reflect.TypeOf(n), table)
 	}
 }
@@ -260,11 +260,11 @@ func processCreateStmt(conv *Conv, n nodes.CreateStmt) {
 			// collecting constraints.
 			constraints = extractConstraints(conv, n, table, []nodes.Node{i})
 		default:
-			conv.unexpected(fmt.Sprintf("Found %s node while processing CreateStmt TableElts", prNodeType(i)))
+			conv.Unexpected(fmt.Sprintf("Found %s node while processing CreateStmt TableElts", PrNodeType(i)))
 		}
 	}
-	conv.schemaStatement([]nodes.Node{n})
-	conv.srcSchema[table] = schema.Table{
+	conv.SchemaStatement(prNodes([]nodes.Node{n}))
+	conv.SrcSchema[table] = schema.Table{
 		Name:     table,
 		ColNames: colNames,
 		ColDefs:  colDef}
@@ -299,23 +299,23 @@ func processInsertStmt(conv *Conv, n nodes.InsertStmt) *copyOrInsert {
 		logStmtError(conv, n, fmt.Errorf("can't get table name: %w", err))
 		return nil
 	}
-	conv.statsAddRow(table, conv.schemaMode())
+	conv.StatsAddRow(table, conv.SchemaMode())
 	colNames, err := getCols(conv, table, n.Cols.Items)
 	if err != nil {
 		logStmtError(conv, n, fmt.Errorf("can't get col name: %w", err))
-		conv.statsAddBadRow(table, conv.schemaMode())
+		conv.StatsAddBadRow(table, conv.SchemaMode())
 		return nil
 	}
 	var values []string
 	switch sel := n.SelectStmt.(type) {
 	case nodes.SelectStmt:
 		values = getVals(conv, sel.ValuesLists, n)
-		conv.dataStatement([]nodes.Node{n})
-		if conv.dataMode() {
+		conv.DataStatement(prNodes([]nodes.Node{n}))
+		if conv.DataMode() {
 			return &copyOrInsert{stmt: insert, table: table, cols: colNames, vals: values}
 		}
 	default:
-		conv.unexpected(fmt.Sprintf("Found %s node while processing InsertStmt SelectStmt", prNodeType(sel)))
+		conv.Unexpected(fmt.Sprintf("Found %s node while processing InsertStmt SelectStmt", PrNodeType(sel)))
 	}
 	return nil
 }
@@ -330,7 +330,7 @@ func processCopyStmt(conv *Conv, n nodes.CopyStmt) *copyOrInsert {
 	if n.Relation != nil {
 		table, err = getTableName(conv, *n.Relation)
 		if err != nil {
-			conv.unexpected(fmt.Sprintf("Processing %v statement: %s", reflect.TypeOf(n), err))
+			conv.Unexpected(fmt.Sprintf("Processing %v statement: %s", reflect.TypeOf(n), err))
 		}
 	} else {
 		logStmtError(conv, n, fmt.Errorf("relation is nil"))
@@ -339,12 +339,12 @@ func processCopyStmt(conv *Conv, n nodes.CopyStmt) *copyOrInsert {
 	for _, a := range n.Attlist.Items {
 		s, err := getString(a)
 		if err != nil {
-			conv.unexpected(fmt.Sprintf("Processing %v statement Attlist: %s", reflect.TypeOf(n), err))
+			conv.Unexpected(fmt.Sprintf("Processing %v statement Attlist: %s", reflect.TypeOf(n), err))
 			s = "BOGUS_COPY_FROM_COLUMN"
 		}
 		cols = append(cols, s)
 	}
-	conv.dataStatement([]nodes.Node{n})
+	conv.DataStatement(prNodes([]nodes.Node{n}))
 	return &copyOrInsert{stmt: copyFrom, table: table, cols: cols}
 }
 
@@ -380,10 +380,10 @@ func getTypeMods(conv *Conv, t nodes.List) (l []int64) {
 			case nodes.Integer:
 				l = append(l, t2.Ival)
 			default:
-				conv.unexpected(fmt.Sprintf("Found %s node while processing Typmods", prNodeType(t1.Val)))
+				conv.Unexpected(fmt.Sprintf("Found %s node while processing Typmods", PrNodeType(t1.Val)))
 			}
 		default:
-			conv.unexpected(fmt.Sprintf("Found %s node while processing Typmods", prNodeType(x)))
+			conv.Unexpected(fmt.Sprintf("Found %s node while processing Typmods", PrNodeType(x)))
 		}
 	}
 	return l
@@ -396,7 +396,7 @@ func getArrayBounds(conv *Conv, t nodes.List) (l []int64) {
 			// 'Ival' provides the array bound (-1 for an array where bound is not specified).
 			l = append(l, t.Ival)
 		default:
-			conv.unexpected(fmt.Sprintf("Found %s node while processing ArrayBounds", prNodeType(x)))
+			conv.Unexpected(fmt.Sprintf("Found %s node while processing ArrayBounds", PrNodeType(x)))
 		}
 	}
 	return l
@@ -470,13 +470,13 @@ func extractConstraints(conv *Conv, n nodes.Node, table string, l []nodes.Node)
 					cols = append(cols, k)
 				}
 				if err != nil {
-					conv.unexpected(fmt.Sprintf("Processing %v statement: error processing constraints: %s", reflect.TypeOf(n), err.Error()))
-					conv.errorInStatement([]nodes.Node{n, d})
+					conv.Unexpected(fmt.Sprintf("Processing %v statement: error processing constraints: %s", reflect.TypeOf(n), err.Error()))
+					conv.ErrorInStatement(prNodes([]nodes.Node{n, d}))
 				}
 			}
 			cs = append(cs, constraint{ct: d.Contype, cols: cols})
 		default:
-			conv.unexpected(fmt.Sprintf("Processing %v statement: found %s node while processing constraints\n", reflect.TypeOf(n), reflect.TypeOf(d)))
+			conv.Unexpected(fmt.Sprintf("Processing %v statement: found %s node while processing constraints\n", reflect.TypeOf(n), reflect.TypeOf(d)))
 		}
 	}
 	return cs
@@ -490,7 +490,7 @@ func analyzeColDefConstraints(conv *Conv, n nodes.Node, table string, l []nodes.
 	// to {pgCol}.
 	for _, c := range extractConstraints(conv, n, table, l) {
 		if len(c.cols) != 0 {
-			conv.unexpected("ColumnDef constraint has keys")
+			conv.Unexpected("ColumnDef constraint has keys")
 		}
 		c.cols = []string{pgCol}
 		cs = append(cs, c)
@@ -504,7 +504,7 @@ func updateSchema(conv *Conv, table string, cs []constraint, s string) {
 	for _, c := range cs {
 		switch c.ct {
 		case nodes.CONSTR_PRIMARY:
-			ct := conv.srcSchema[table]
+			ct := conv.SrcSchema[table]
 			checkEmpty(conv, ct.PrimaryKeys, s)
 			ct.PrimaryKeys = toSchemaKeys(conv, table, c.cols) // Drop any previous primary keys.
 			// In Spanner, primary key columns are usually annotated with NOT NULL,
@@ -513,11 +513,11 @@ func updateSchema(conv *Conv, table string, cs []constraint, s string) {
 			// NOT NULL and UNIQUE i.e. primary keys must be NOT NULL.
 			// We preserve PostgreSQL semantics and enforce NOT NULL.
 			updateCols(nodes.CONSTR_NOTNULL, c.cols, ct.ColDefs)
-			conv.srcSchema[table] = ct
+			conv.SrcSchema[table] = ct
 		default:
-			ct := conv.srcSchema[table]
+			ct := conv.SrcSchema[table]
 			updateCols(c.ct, c.cols, ct.ColDefs)
-			conv.srcSchema[table] = ct
+			conv.SrcSchema[table] = ct
 		}
 	}
 }
@@ -582,10 +582,10 @@ func getVals(conv *Conv, l [][]nodes.Node, n nodes.InsertStmt) (values []string)
 					// here to avoid the int64 -> string -> int64 conversions.
 					values = append(values, strconv.FormatInt(st.Ival, 10))
 				default:
-					conv.unexpected(fmt.Sprintf("Processing %v statement: found %s node for A_Const Val", reflect.TypeOf(n), reflect.TypeOf(c.Val)))
+					conv.Unexpected(fmt.Sprintf("Processing %v statement: found %s node for A_Const Val", reflect.TypeOf(n), reflect.TypeOf(c.Val)))
 				}
 			default:
-				conv.unexpected(fmt.Sprintf("Processing %v statement: found %s node in ValuesList", reflect.TypeOf(n), reflect.TypeOf(v)))
+				conv.Unexpected(fmt.Sprintf("Processing %v statement: found %s node in ValuesList", reflect.TypeOf(n), reflect.TypeOf(v)))
 			}
 		}
 	}
@@ -593,8 +593,8 @@ func getVals(conv *Conv, l [][]nodes.Node, n nodes.InsertStmt) (values []string)
 }
 
 func logStmtError(conv *Conv, n nodes.Node, err error) {
-	conv.unexpected(fmt.Sprintf("Processing %v statement: %s", reflect.TypeOf(n), err))
-	conv.errorInStatement([]nodes.Node{n})
+	conv.Unexpected(fmt.Sprintf("Processing %v statement: %s", reflect.TypeOf(n), err))
+	conv.ErrorInStatement(prNodes([]nodes.Node{n}))
 }
 
 func getString(node nodes.Node) (string, error) {
@@ -610,6 +610,19 @@ func getString(node nodes.Node) (string, error) {
 // PostgreSQL explicitly forbids multiple primary keys.
 func checkEmpty(conv *Conv, pkeys []schema.Key, s string) {
 	if len(pkeys) != 0 {
-		conv.unexpected(fmt.Sprintf("%s statement is adding a second primary key", s))
+		conv.Unexpected(fmt.Sprintf("%s statement is adding a second primary key", s))
 	}
 }
+
+func PrNodeType(n nodes.Node) string {
+	// Strip off "pg_query." prefix from nodes.Nodes type.
+	return strings.TrimPrefix(reflect.TypeOf(n).Name(), "pg_query.")
+}
+
+func prNodes(l []nodes.Node) string {
+	var s []string
+	for _, n := range l {
+		s = append(s, PrNodeType(n))
+	}
+	return strings.Join(s, ".")
+}
diff --git a/internal/report.go b/internal/report.go
index 3a44ea5..77a6105 100644
--- a/internal/report.go
+++ b/internal/report.go
@@ -58,7 +58,7 @@ func GenerateReport(fromPgDump bool, conv *Conv, w *bufio.Writer, badWrites map[
 			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))
+		w.WriteString(rateConversion(t.rows, t.badRows, t.cols, t.warnings, t.SyntheticPKey != "", false))
 		w.WriteString("\n")
 		for _, x := range t.body {
 			fmt.Fprintf(w, "%s\n", x.heading)
@@ -79,7 +79,7 @@ type tableReport struct {
 	badRows       int64
 	cols          int64
 	warnings      int64
-	syntheticPKey string // Empty string means no synthetic primary key was needed.
+	SyntheticPKey string // Empty string means no synthetic primary key was needed.
 	body          []tableReportBody
 }
 
@@ -92,7 +92,7 @@ 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
-	for t := range conv.srcSchema {
+	for t := range conv.SrcSchema {
 		tables = append(tables, t)
 	}
 	sort.Strings(tables)
@@ -104,21 +104,21 @@ func analyzeTables(conv *Conv, badWrites map[string]int64) (r []tableReport) {
 
 func buildTableReport(conv *Conv, srcTable string, badWrites map[string]int64) tableReport {
 	spTable, err := GetSpannerTable(conv, srcTable)
-	srcSchema, ok1 := conv.srcSchema[srcTable]
-	spSchema, ok2 := conv.spSchema[spTable]
+	srcSchema, ok1 := conv.SrcSchema[srcTable]
+	spSchema, ok2 := conv.SpSchema[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)
+		conv.Unexpected("report: " + m)
 		tr.body = []tableReportBody{tableReportBody{heading: "Internal error: " + m}}
 		return tr
 	}
 	issues, cols, warnings := analyzeCols(conv, srcTable, spTable)
 	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)
+	if pk, ok := conv.SyntheticPKeys[spTable]; ok {
+		tr.SyntheticPKey = pk.Col
+		tr.body = buildTableReportBody(conv, srcTable, issues, spSchema, srcSchema, &pk.Col)
 	} else {
 		tr.body = buildTableReportBody(conv, srcTable, issues, spSchema, srcSchema, nil)
 	}
@@ -126,7 +126,7 @@ func buildTableReport(conv *Conv, srcTable string, badWrites map[string]int64) t
 	return tr
 }
 
-func buildTableReportBody(conv *Conv, srcTable string, issues map[string][]schemaIssue, spSchema ddl.CreateTable, srcSchema schema.Table, syntheticPK *string) []tableReportBody {
+func buildTableReportBody(conv *Conv, srcTable string, issues map[string][]SchemaIssue, spSchema ddl.CreateTable, srcSchema schema.Table, syntheticPK *string) []tableReportBody {
 	var body []tableReportBody
 	for _, p := range []struct {
 		heading  string
@@ -150,7 +150,7 @@ func buildTableReportBody(conv *Conv, srcTable string, issues map[string][]schem
 				l = append(l, fmt.Sprintf("Column '%s' was added because this table didn't have a primary key. Spanner requires a primary key for every table", *syntheticPK))
 			}
 		}
-		issueBatcher := make(map[schemaIssue]bool)
+		issueBatcher := make(map[SchemaIssue]bool)
 		for _, srcCol := range cols {
 			for _, i := range issues[srcCol] {
 				if issueDB[i].severity != p.severity {
@@ -166,7 +166,7 @@ func buildTableReportBody(conv *Conv, srcTable string, issues map[string][]schem
 				}
 				spCol, err := GetSpannerCol(conv, srcTable, srcCol, true)
 				if err != nil {
-					conv.unexpected(err.Error())
+					conv.Unexpected(err.Error())
 				}
 				srcType := printSourceType(srcSchema.ColDefs[srcCol].Type)
 				spType := spSchema.ColDefs[spCol].PrintColumnDefType()
@@ -180,14 +180,14 @@ func buildTableReportBody(conv *Conv, srcTable string, issues map[string][]schem
 				// on case of srcType.
 				spType = strings.ToLower(spType)
 				switch i {
-				case defaultValue:
+				case DefaultValue:
 					l = append(l, fmt.Sprintf("%s e.g. column '%s'", issueDB[i].brief, srcCol))
-				case foreignKey:
+				case ForeignKey:
 					l = append(l, fmt.Sprintf("Column '%s' uses foreign keys which Spanner does not support", srcCol))
-				case timestamp:
+				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))
-				case widened:
+				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))
 				default:
 					l = append(l, fmt.Sprintf("Column '%s': type %s is mapped to %s. %s", srcCol, srcType, spType, issueDB[i].brief))
@@ -207,9 +207,9 @@ func buildTableReportBody(conv *Conv, srcTable string, issues map[string][]schem
 }
 
 func fillRowStats(conv *Conv, srcTable string, badWrites map[string]int64, tr *tableReport) {
-	rows := conv.stats.rows[srcTable]
-	goodConvRows := conv.stats.goodRows[srcTable]
-	badConvRows := conv.stats.badRows[srcTable]
+	rows := conv.Stats.Rows[srcTable]
+	goodConvRows := conv.Stats.GoodRows[srcTable]
+	badConvRows := conv.Stats.BadRows[srcTable]
 	badRowWrites := badWrites[srcTable]
 	// Note on rows:
 	// rows: all rows we encountered during processing.
@@ -217,7 +217,7 @@ func fillRowStats(conv *Conv, srcTable string, badWrites map[string]int64, tr *t
 	// badConvRows: rows we failed to convert.
 	// badRowWrites: rows we converted, but could not write to Spanner.
 	if rows != goodConvRows+badConvRows || badRowWrites > goodConvRows {
-		conv.unexpected(fmt.Sprintf("Inconsistent row counts for table %s: %d %d %d %d\n", srcTable, rows, goodConvRows, badConvRows, badRowWrites))
+		conv.Unexpected(fmt.Sprintf("Inconsistent row counts for table %s: %d %d %d %d\n", srcTable, rows, goodConvRows, badConvRows, badRowWrites))
 	}
 	tr.rows = rows
 	tr.badRows = badConvRows + badRowWrites
@@ -231,20 +231,20 @@ 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 {
+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},
-	serial:                {brief: "Spanner does not support autoincrementing types", severity: warning},
-	timestamp:             {brief: "Spanner timestamp is closer to PostgreSQL timestamptz", 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},
+	Serial:                {brief: "Spanner does not support autoincrementing types", severity: warning},
+	Timestamp:             {brief: "Spanner timestamp is closer to PostgreSQL timestamptz", severity: note, batch: true},
+	Widened:               {brief: "Some columns will consume more storage in Spanner", severity: note, batch: true},
 }
 
 type severity int
@@ -255,17 +255,17 @@ const (
 )
 
 // analyzeCols returns information about the quality of schema mappings
-// for table 'srcTable'. It assumes 'srcTable' is in the conv.srcSchema map.
-func analyzeCols(conv *Conv, srcTable, spTable string) (map[string][]schemaIssue, int64, int64) {
-	srcSchema := conv.srcSchema[srcTable]
-	m := make(map[string][]schemaIssue)
+// for table 'srcTable'. It assumes 'srcTable' is in the conv.SrcSchema map.
+func analyzeCols(conv *Conv, srcTable, spTable string) (map[string][]SchemaIssue, int64, int64) {
+	srcSchema := conv.SrcSchema[srcTable]
+	m := make(map[string][]SchemaIssue)
 	warnings := int64(0)
-	warningBatcher := make(map[schemaIssue]bool)
+	warningBatcher := make(map[SchemaIssue]bool)
 	// Note on how we count warnings when there are multiple warnings
 	// per column and/or multiple warnings per table.
 	// non-batched warnings: count at most one warning per column.
 	// batched warnings: count at most one warning per table.
-	for c, l := range conv.issues[srcTable] {
+	for c, l := range conv.Issues[srcTable] {
 		colWarning := false
 		m[c] = l
 		for _, i := range l {
@@ -358,7 +358,7 @@ func generateSummary(conv *Conv, r []tableReport, badWrites map[string]int64) st
 		}
 		cols += t.cols * weight
 		warnings += t.warnings * weight
-		if t.syntheticPKey != "" {
+		if t.SyntheticPKey != "" {
 			missingPKey = true
 		}
 	}
@@ -376,7 +376,7 @@ func generateSummary(conv *Conv, r []tableReport, badWrites map[string]int64) st
 }
 
 func ignoredStatements(conv *Conv) (l []string) {
-	for s := range conv.stats.statement {
+	for s := range conv.Stats.Statement {
 		switch s {
 		case "CreateFunctionStmt":
 			l = append(l, "functions")
@@ -402,8 +402,8 @@ func writeStmtStats(conv *Conv, w *bufio.Writer) {
 		count     int64
 	}
 	var l []stat
-	for s, x := range conv.stats.statement {
-		l = append(l, stat{s, x.schema + x.data + x.skip + x.error})
+	for s, x := range conv.Stats.Statement {
+		l = append(l, stat{s, x.schema + x.data + x.skip + x.Error})
 	}
 	// Sort by alphabetical order of statements.
 	sort.Slice(l, func(i, j int) bool {
@@ -419,8 +419,8 @@ func writeStmtStats(conv *Conv, w *bufio.Writer) {
 	fmt.Fprintf(w, "  %6s %6s %6s %6s  %s\n", "schema", "data", "skip", "error", "statement")
 	w.WriteString("  --------------------------------------\n")
 	for _, x := range l {
-		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)
+		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)
 	}
 	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")
@@ -429,12 +429,12 @@ func writeStmtStats(conv *Conv, w *bufio.Writer) {
 
 func writeUnexpectedConditions(conv *Conv, w *bufio.Writer) {
 	reparseInfo := func() {
-		if conv.stats.reparsed > 0 {
-			fmt.Fprintf(w, "Note: there were %d pg_dump reparse events while looking for statement boundaries.\n\n", conv.stats.reparsed)
+		if conv.Stats.Reparsed > 0 {
+			fmt.Fprintf(w, "Note: there were %d pg_dump reparse events while looking for statement boundaries.\n\n", conv.Stats.Reparsed)
 		}
 	}
 	writeHeading(w, "Unexpected Conditions")
-	if len(conv.stats.unexpected) == 0 {
+	if len(conv.Stats.Unexpected) == 0 {
 		w.WriteString("There were no unexpected conditions encountered during processing.\n\n")
 		reparseInfo()
 		return
@@ -448,7 +448,7 @@ func writeUnexpectedConditions(conv *Conv, w *bufio.Writer) {
 	w.WriteString("  --------------------------------------\n")
 	fmt.Fprintf(w, "  %6s  %s\n", "count", "condition")
 	w.WriteString("  --------------------------------------\n")
-	for s, n := range conv.stats.unexpected {
+	for s, n := range conv.Stats.Unexpected {
 		fmt.Fprintf(w, "  %6d  %s\n", n, s)
 	}
 	w.WriteString("\n")
diff --git a/internal/tablemap.go b/internal/tablemap.go
index 16feb86..aab41f8 100644
--- a/internal/tablemap.go
+++ b/internal/tablemap.go
@@ -31,19 +31,19 @@ func GetSpannerTable(conv *Conv, srcTable string) (string, error) {
 	if srcTable == "" {
 		return "", fmt.Errorf("bad parameter: table string is empty")
 	}
-	if sp, found := conv.toSpanner[srcTable]; found {
-		return sp.name, nil
+	if sp, found := conv.ToSpanner[srcTable]; found {
+		return sp.Name, nil
 	}
 	spTable, _ := FixName(srcTable)
-	if _, found := conv.toSource[spTable]; found {
+	if _, found := conv.ToSource[spTable]; found {
 		// s has been used before i.e. FixName caused a collision.
 		// Add unique postfix: use number of tables so far.
 		// However, there is a chance this has already been used,
 		// so need to iterate.
-		id := len(conv.toSpanner)
+		id := len(conv.ToSpanner)
 		for {
 			t := spTable + "_" + strconv.Itoa(id)
-			if _, found := conv.toSource[t]; !found {
+			if _, found := conv.ToSource[t]; !found {
 				spTable = t
 				break
 			}
@@ -53,8 +53,8 @@ func GetSpannerTable(conv *Conv, srcTable string) (string, error) {
 	if spTable != srcTable {
 		VerbosePrintf("Mapping source DB table %s to Spanner table %s\n", srcTable, spTable)
 	}
-	conv.toSpanner[srcTable] = nameAndCols{name: spTable, cols: make(map[string]string)}
-	conv.toSource[spTable] = nameAndCols{name: srcTable, cols: make(map[string]string)}
+	conv.ToSpanner[srcTable] = NameAndCols{Name: spTable, Cols: make(map[string]string)}
+	conv.ToSource[spTable] = NameAndCols{Name: srcTable, Cols: make(map[string]string)}
 	return spTable, nil
 }
 
@@ -74,32 +74,32 @@ func GetSpannerCol(conv *Conv, srcTable, srcCol string, mustExist bool) (string,
 	if srcCol == "" {
 		return "", fmt.Errorf("bad parameter: col string is empty")
 	}
-	sp, found := conv.toSpanner[srcTable]
+	sp, found := conv.ToSpanner[srcTable]
 	if !found {
 		return "", fmt.Errorf("unknown table %s", srcTable)
 	}
 	// Sanity check: do reverse mapping and check consistency.
 	// Consider dropping this check.
-	src, found := conv.toSource[sp.name]
-	if !found || src.name != srcTable {
-		return "", fmt.Errorf("internal error: table mapping inconsistency for table %s (%s)", srcTable, src.name)
+	src, found := conv.ToSource[sp.Name]
+	if !found || src.Name != srcTable {
+		return "", fmt.Errorf("internal error: table mapping inconsistency for table %s (%s)", srcTable, src.Name)
 	}
-	if spCol, found := sp.cols[srcCol]; found {
+	if spCol, found := sp.Cols[srcCol]; found {
 		return spCol, nil
 	}
 	if mustExist {
 		return "", fmt.Errorf("table %s does not have a column %s", srcTable, srcCol)
 	}
 	spCol, _ := FixName(srcCol)
-	if _, found := conv.toSource[sp.name].cols[spCol]; found {
+	if _, found := conv.ToSource[sp.Name].Cols[spCol]; found {
 		// spCol has been used before i.e. FixName caused a collision.
 		// Add unique postfix: use number of cols in this table so far.
 		// However, there is a chance this has already been used,
 		// so need to iterate.
-		id := len(sp.cols)
+		id := len(sp.Cols)
 		for {
 			c := spCol + "_" + strconv.Itoa(id)
-			if _, found := conv.toSource[sp.name].cols[c]; !found {
+			if _, found := conv.ToSource[sp.Name].Cols[c]; !found {
 				spCol = c
 				break
 			}
@@ -109,8 +109,8 @@ func GetSpannerCol(conv *Conv, srcTable, srcCol string, mustExist bool) (string,
 	if spCol != srcCol {
 		VerbosePrintf("Mapping source DB col %s (table %s) to Spanner col %s\n", srcCol, srcTable, spCol)
 	}
-	conv.toSpanner[srcTable].cols[srcCol] = spCol
-	conv.toSource[sp.name].cols[spCol] = srcCol
+	conv.ToSpanner[srcTable].Cols[srcCol] = spCol
+	conv.ToSource[sp.Name].Cols[spCol] = srcCol
 	return spCol, nil
 }
 
diff --git a/internal/toddl.go b/internal/toddl.go
index 47a7cd7..420f7db 100644
--- a/internal/toddl.go
+++ b/internal/toddl.go
@@ -25,41 +25,41 @@ import (
 )
 
 // schemaToDDL performs schema conversion from the source DB schema to
-// Spanner. It uses the source schema in conv.srcSchema, and writes
-// the Spanner schema to conv.spSchema.
+// Spanner. It uses the source schema in conv.SrcSchema, and writes
+// the Spanner schema to conv.SpSchema.
 func schemaToDDL(conv *Conv) error {
-	for _, srcTable := range conv.srcSchema {
+	for _, srcTable := range conv.SrcSchema {
 		spTableName, err := GetSpannerTable(conv, srcTable.Name)
 		if err != nil {
-			conv.unexpected(fmt.Sprintf("Couldn't map source table %s to Spanner: %s", srcTable.Name, err))
+			conv.Unexpected(fmt.Sprintf("Couldn't map source table %s to Spanner: %s", srcTable.Name, err))
 			continue
 		}
 		var spColNames []string
 		spColDef := make(map[string]ddl.ColumnDef)
-		conv.issues[srcTable.Name] = make(map[string][]schemaIssue)
+		conv.Issues[srcTable.Name] = make(map[string][]SchemaIssue)
 		// Iterate over columns using ColNames order.
 		for _, srcColName := range srcTable.ColNames {
 			srcCol := srcTable.ColDefs[srcColName]
 			colName, err := GetSpannerCol(conv, srcTable.Name, srcCol.Name, false)
 			if err != nil {
-				conv.unexpected(fmt.Sprintf("Couldn't map source column %s of table %s to Spanner: %s", srcTable.Name, srcCol.Name, err))
+				conv.Unexpected(fmt.Sprintf("Couldn't map source column %s of table %s to Spanner: %s", srcTable.Name, srcCol.Name, err))
 				continue
 			}
 			spColNames = append(spColNames, colName)
 			ty, issues := toSpannerType(conv, srcCol.Type.Name, srcCol.Type.Mods)
 			if len(srcCol.Type.ArrayBounds) > 1 {
 				ty = ddl.String{Len: ddl.MaxLength{}}
-				issues = append(issues, multiDimensionalArray)
+				issues = append(issues, MultiDimensionalArray)
 			}
 			// TODO: add issues for all elements of srcCol.Ignored.
 			if srcCol.Ignored.ForeignKey {
-				issues = append(issues, foreignKey)
+				issues = append(issues, ForeignKey)
 			}
 			if srcCol.Ignored.Default {
-				issues = append(issues, defaultValue)
+				issues = append(issues, DefaultValue)
 			}
 			if len(issues) > 0 {
-				conv.issues[srcTable.Name][srcCol.Name] = issues
+				conv.Issues[srcTable.Name][srcCol.Name] = issues
 			}
 			spColDef[colName] = ddl.ColumnDef{
 				Name:    colName,
@@ -70,7 +70,7 @@ func schemaToDDL(conv *Conv) error {
 			}
 		}
 		comment := "Spanner schema for source table " + quoteIfNeeded(srcTable.Name)
-		conv.spSchema[spTableName] = ddl.CreateTable{
+		conv.SpSchema[spTableName] = ddl.CreateTable{
 			Name:     spTableName,
 			ColNames: spColNames,
 			ColDefs:  spColDef,
@@ -84,10 +84,10 @@ func schemaToDDL(conv *Conv) error {
 // mods) into a Spanner type. This is the core source-to-Spanner type
 // mapping.  toSpannerType returns the Spanner type and a list of type
 // conversion issues encountered.
-func toSpannerType(conv *Conv, id string, mods []int64) (ddl.ScalarType, []schemaIssue) {
+func toSpannerType(conv *Conv, id string, mods []int64) (ddl.ScalarType, []SchemaIssue) {
 	maxExpectedMods := func(n int) {
 		if len(mods) > n {
-			conv.unexpected(fmt.Sprintf("Found %d mods while processing type id=%s", len(mods), id))
+			conv.Unexpected(fmt.Sprintf("Found %d mods while processing type id=%s", len(mods), id))
 		}
 	}
 	switch id {
@@ -96,7 +96,7 @@ func toSpannerType(conv *Conv, id string, mods []int64) (ddl.ScalarType, []schem
 		return ddl.Bool{}, nil
 	case "bigserial":
 		maxExpectedMods(0)
-		return ddl.Int64{}, []schemaIssue{serial}
+		return ddl.Int64{}, []SchemaIssue{Serial}
 	case "bpchar", "character": // Note: Postgres internal name for char is bpchar (aka blank padded char).
 		maxExpectedMods(1)
 		if len(mods) > 0 {
@@ -115,28 +115,28 @@ func toSpannerType(conv *Conv, id string, mods []int64) (ddl.ScalarType, []schem
 		return ddl.Float64{}, nil
 	case "float4", "real":
 		maxExpectedMods(0)
-		return ddl.Float64{}, []schemaIssue{widened}
+		return ddl.Float64{}, []SchemaIssue{Widened}
 	case "int8", "bigint":
 		maxExpectedMods(0)
 		return ddl.Int64{}, nil
 	case "int4", "integer":
 		maxExpectedMods(0)
-		return ddl.Int64{}, []schemaIssue{widened}
+		return ddl.Int64{}, []SchemaIssue{Widened}
 	case "int2", "smallint":
 		maxExpectedMods(0)
-		return ddl.Int64{}, []schemaIssue{widened}
+		return ddl.Int64{}, []SchemaIssue{Widened}
 	case "numeric": // Map all numeric types to float64.
 		maxExpectedMods(2)
 		if len(mods) > 0 && mods[0] <= 15 {
 			// float64 can represent this numeric type faithfully.
 			// Note: int64 has 53 bits for mantissa, which is ~15.96
 			// decimal digits.
-			return ddl.Float64{}, []schemaIssue{numericThatFits}
+			return ddl.Float64{}, []SchemaIssue{NumericThatFits}
 		}
-		return ddl.Float64{}, []schemaIssue{numeric}
+		return ddl.Float64{}, []SchemaIssue{Numeric}
 	case "serial":
 		maxExpectedMods(0)
-		return ddl.Int64{}, []schemaIssue{serial}
+		return ddl.Int64{}, []SchemaIssue{Serial}
 	case "text":
 		maxExpectedMods(0)
 		return ddl.String{Len: ddl.MaxLength{}}, nil
@@ -146,7 +146,7 @@ func toSpannerType(conv *Conv, id string, mods []int64) (ddl.ScalarType, []schem
 	case "timestamp", "timestamp without time zone":
 		maxExpectedMods(1)
 		// Map timestamp without timezone to Spanner timestamp.
-		return ddl.Timestamp{}, []schemaIssue{timestamp}
+		return ddl.Timestamp{}, []SchemaIssue{Timestamp}
 	case "varchar", "character varying":
 		maxExpectedMods(1)
 		if len(mods) > 0 {
@@ -154,7 +154,7 @@ func toSpannerType(conv *Conv, id string, mods []int64) (ddl.ScalarType, []schem
 		}
 		return ddl.String{Len: ddl.MaxLength{}}, nil
 	}
-	return ddl.String{Len: ddl.MaxLength{}}, []schemaIssue{noGoodType}
+	return ddl.String{Len: ddl.MaxLength{}}, []SchemaIssue{NoGoodType}
 }
 
 func printSourceType(ty schema.Type) string {
@@ -195,7 +195,7 @@ func cvtPrimaryKeys(conv *Conv, srcTable string, srcKeys []schema.Key) []ddl.Ind
 	for _, k := range srcKeys {
 		spCol, err := GetSpannerCol(conv, srcTable, k.Column, true)
 		if err != nil {
-			conv.unexpected(fmt.Sprintf("Can't map key for table %s", srcTable))
+			conv.Unexpected(fmt.Sprintf("Can't map key for table %s", srcTable))
 			continue
 		}
 		spKeys = append(spKeys, ddl.IndexKey{Col: spCol, Desc: k.Desc})


Test patch:
diff --git a/internal/convert_test.go b/internal/convert_test.go
index ca507d2..d811277 100644
--- a/internal/convert_test.go
+++ b/internal/convert_test.go
@@ -32,20 +32,20 @@ import (
 func TestSetSchemaMode(t *testing.T) {
 	conv := MakeConv()
 	conv.SetSchemaMode()
-	assert.True(t, conv.schemaMode())
-	assert.False(t, conv.dataMode())
+	assert.True(t, conv.SchemaMode())
+	assert.False(t, conv.DataMode())
 }
 
 func TestSetDataMode(t *testing.T) {
 	conv := MakeConv()
 	conv.SetDataMode()
-	assert.False(t, conv.schemaMode())
-	assert.True(t, conv.dataMode())
+	assert.False(t, conv.SchemaMode())
+	assert.True(t, conv.DataMode())
 }
 
 func TestGetDDL(t *testing.T) {
 	conv := MakeConv()
-	conv.spSchema["table1"] = ddl.CreateTable{
+	conv.SpSchema["table1"] = ddl.CreateTable{
 		Name:     "table1",
 		ColNames: []string{"a", "b"},
 		ColDefs: map[string]ddl.ColumnDef{
@@ -53,7 +53,7 @@ func TestGetDDL(t *testing.T) {
 			"b": ddl.ColumnDef{Name: "b", T: ddl.Float64{}},
 		},
 		Pks: []ddl.IndexKey{ddl.IndexKey{Col: "a"}}}
-	conv.spSchema["table2"] = ddl.CreateTable{
+	conv.SpSchema["table2"] = ddl.CreateTable{
 		Name:     "table2",
 		ColNames: []string{"a"},
 		ColDefs: map[string]ddl.ColumnDef{
@@ -76,36 +76,36 @@ func TestGetDDL(t *testing.T) {
 
 func TestRows(t *testing.T) {
 	conv := MakeConv()
-	conv.stats.rows["table1"] = 42
-	conv.stats.rows["table2"] = 6
+	conv.Stats.Rows["table1"] = 42
+	conv.Stats.Rows["table2"] = 6
 	assert.Equal(t, int64(48), conv.Rows())
 }
 
 func TestBadRows(t *testing.T) {
 	conv := MakeConv()
-	conv.stats.badRows["table1"] = 6
-	conv.stats.badRows["table2"] = 2
+	conv.Stats.BadRows["table1"] = 6
+	conv.Stats.BadRows["table2"] = 2
 	assert.Equal(t, int64(8), conv.BadRows())
 }
 
 func TestStatements(t *testing.T) {
 	conv := MakeConv()
-	conv.errorInStatement(parse(t, "CREATE TABLE cart (pid text NOT NULL);"))
-	conv.schemaStatement(parse(t, "CREATE TABLE cart (pid text NOT NULL);"))
-	conv.dataStatement(parse(t, "INSERT INTO cart (pid) VALUES ('p42');"))
-	conv.skipStatement(parse(t, "GRANT ALL ON SCHEMA public TO PUBLIC;"))
+	conv.ErrorInStatement(prNodes(parse(t, "CREATE TABLE cart (pid text NOT NULL);")))
+	conv.SchemaStatement(prNodes(parse(t, "CREATE TABLE cart (pid text NOT NULL);")))
+	conv.DataStatement(prNodes(parse(t, "INSERT INTO cart (pid) VALUES ('p42');")))
+	conv.SkipStatement(prNodes(parse(t, "GRANT ALL ON SCHEMA public TO PUBLIC;")))
 	assert.Equal(t, int64(4), conv.Statements())
 }
 
 func TestStatementErrors(t *testing.T) {
 	conv := MakeConv()
-	conv.errorInStatement(parse(t, "CREATE TABLE cart (pid text NOT NULL);"))
+	conv.ErrorInStatement(prNodes(parse(t, "CREATE TABLE cart (pid text NOT NULL);")))
 	assert.Equal(t, int64(1), conv.StatementErrors())
 }
 
 func TestUnexpecteds(t *testing.T) {
 	conv := MakeConv()
-	conv.unexpected("expected-the-unexpected")
+	conv.Unexpected("expected-the-unexpected")
 	assert.Equal(t, int64(1), conv.Unexpecteds())
 }
 
@@ -119,7 +119,7 @@ func TestGetBadRows(t *testing.T) {
 
 func TestAddPrimaryKeys(t *testing.T) {
 	conv := MakeConv()
-	conv.spSchema["table"] = ddl.CreateTable{
+	conv.SpSchema["table"] = ddl.CreateTable{
 		Name:     "table",
 		ColNames: []string{"a", "b"},
 		ColDefs: map[string]ddl.ColumnDef{
@@ -137,8 +137,8 @@ func TestAddPrimaryKeys(t *testing.T) {
 			"synth_id": ddl.ColumnDef{Name: "synth_id", T: ddl.Int64{}},
 		},
 		Pks: []ddl.IndexKey{ddl.IndexKey{Col: "synth_id"}}}
-	assert.Equal(t, e, conv.spSchema["table"])
-	assert.Equal(t, syntheticPKey{col: "synth_id", sequence: 0}, conv.syntheticPKeys["table"])
+	assert.Equal(t, e, conv.SpSchema["table"])
+	assert.Equal(t, SyntheticPKey{Col: "synth_id", Sequence: 0}, conv.SyntheticPKeys["table"])
 }
 
 func parse(t *testing.T, s string) []nodes.Node {
diff --git a/internal/data_test.go b/internal/data_test.go
index 27e582d..a487b97 100644
--- a/internal/data_test.go
+++ b/internal/data_test.go
@@ -270,7 +270,7 @@ func TestConvertData(t *testing.T) {
 		},
 	}
 	conv := buildConv(spTable, srcTable)
-	conv.syntheticPKeys[spTable.Name] = syntheticPKey{col: "synth_id", sequence: 0}
+	conv.SyntheticPKeys[spTable.Name] = SyntheticPKey{Col: "synth_id", Sequence: 0}
 	for _, tc := range syntheticPKeyTests {
 		atable, acols, avals, err := ConvertData(conv, spTable.Name, tc.cols, tc.vals)
 		checkResults(t, atable, acols, avals, err, tableName, tc.ecols, tc.evals, tc.name)
@@ -279,13 +279,13 @@ func TestConvertData(t *testing.T) {
 
 func buildConv(spTable ddl.CreateTable, srcTable schema.Table) *Conv {
 	conv := MakeConv()
-	conv.spSchema[spTable.Name] = spTable
-	conv.srcSchema[srcTable.Name] = srcTable
-	conv.toSource[spTable.Name] = nameAndCols{name: srcTable.Name, cols: make(map[string]string)}
-	conv.toSpanner[srcTable.Name] = nameAndCols{name: spTable.Name, cols: make(map[string]string)}
+	conv.SpSchema[spTable.Name] = spTable
+	conv.SrcSchema[srcTable.Name] = srcTable
+	conv.ToSource[spTable.Name] = NameAndCols{Name: srcTable.Name, Cols: make(map[string]string)}
+	conv.ToSpanner[srcTable.Name] = NameAndCols{Name: spTable.Name, Cols: make(map[string]string)}
 	for i := range spTable.ColNames {
-		conv.toSource[spTable.Name].cols[spTable.ColNames[i]] = srcTable.ColNames[i]
-		conv.toSpanner[srcTable.Name].cols[srcTable.ColNames[i]] = spTable.ColNames[i]
+		conv.ToSource[spTable.Name].Cols[spTable.ColNames[i]] = srcTable.ColNames[i]
+		conv.ToSpanner[srcTable.Name].Cols[srcTable.ColNames[i]] = spTable.ColNames[i]
 	}
 	return conv
 }
diff --git a/internal/infoschema_test.go b/internal/infoschema_test.go
index 168823b..48589b8 100644
--- a/internal/infoschema_test.go
+++ b/internal/infoschema_test.go
@@ -133,19 +133,19 @@ func TestProcessInfoSchema(t *testing.T) {
 			},
 			Pks: []ddl.IndexKey{ddl.IndexKey{Col: "id"}}},
 	}
-	assert.Equal(t, expectedSchema, stripSchemaComments(conv.spSchema))
-	assert.Equal(t, len(conv.issues["cart"]), 0)
-	expectedIssues := map[string][]schemaIssue{
-		"aint": []schemaIssue{widened},
-		"bs":   []schemaIssue{defaultValue},
-		"f4":   []schemaIssue{widened},
-		"i4":   []schemaIssue{widened},
-		"i2":   []schemaIssue{widened},
-		"num":  []schemaIssue{numeric},
-		"s":    []schemaIssue{widened, defaultValue},
-		"ts":   []schemaIssue{timestamp},
+	assert.Equal(t, expectedSchema, stripSchemaComments(conv.SpSchema))
+	assert.Equal(t, len(conv.Issues["cart"]), 0)
+	expectedIssues := map[string][]SchemaIssue{
+		"aint": []SchemaIssue{Widened},
+		"bs":   []SchemaIssue{DefaultValue},
+		"f4":   []SchemaIssue{Widened},
+		"i4":   []SchemaIssue{Widened},
+		"i2":   []SchemaIssue{Widened},
+		"num":  []SchemaIssue{Numeric},
+		"s":    []SchemaIssue{Widened, DefaultValue},
+		"ts":   []SchemaIssue{Timestamp},
 	}
-	assert.Equal(t, expectedIssues, conv.issues["test"])
+	assert.Equal(t, expectedIssues, conv.Issues["test"])
 	assert.Equal(t, int64(0), conv.Unexpecteds())
 }
 
@@ -355,8 +355,8 @@ func TestSetRowStats(t *testing.T) {
 	conv := MakeConv()
 	conv.SetDataMode()
 	SetRowStats(conv, db)
-	assert.Equal(t, int64(5), conv.stats.rows["test1"])
-	assert.Equal(t, int64(142), conv.stats.rows["test2"])
+	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/internal/pgdump_test.go b/internal/pgdump_test.go
index 1c954ae..04219db 100644
--- a/internal/pgdump_test.go
+++ b/internal/pgdump_test.go
@@ -70,7 +70,7 @@ func TestProcessPgDump(t *testing.T) {
 	for _, tc := range scalarTests {
 		conv, _ := runProcessPgDump(fmt.Sprintf("CREATE TABLE t (a %s);", tc.ty))
 		noIssues(conv, t, "Scalar type: "+tc.ty)
-		assert.Equal(t, conv.spSchema["t"].ColDefs["a"].T, tc.expected, "Scalar type: "+tc.ty)
+		assert.Equal(t, conv.SpSchema["t"].ColDefs["a"].T, tc.expected, "Scalar type: "+tc.ty)
 	}
 	// Next test array types and not null.
 	singleColTests := []struct {
@@ -87,7 +87,7 @@ func TestProcessPgDump(t *testing.T) {
 	for _, tc := range singleColTests {
 		conv, _ := runProcessPgDump(fmt.Sprintf("CREATE TABLE t (a %s);", tc.ty))
 		noIssues(conv, t, "Not null: "+tc.ty)
-		cd := conv.spSchema["t"].ColDefs["a"]
+		cd := conv.SpSchema["t"].ColDefs["a"]
 		cd.Comment = ""
 		assert.Equal(t, tc.expected, cd, "Not null: "+tc.ty)
 	}
@@ -400,7 +400,7 @@ COPY test (id, a, b, c, d, e) FROM stdin;
 			noIssues(conv, t, tc.name)
 		}
 		if tc.expectedSchema != nil {
-			assert.Equal(t, tc.expectedSchema, stripSchemaComments(conv.spSchema), tc.name+": Schema did not match")
+			assert.Equal(t, tc.expectedSchema, stripSchemaComments(conv.SpSchema), tc.name+": Schema did not match")
 		}
 		if tc.expectedData != nil {
 			assert.Equal(t, tc.expectedData, rows, tc.name+": Data rows did not match")
@@ -410,7 +410,7 @@ COPY test (id, a, b, c, d, e) FROM stdin;
 	{ // Test set timezone statement.
 		conv, _ := runProcessPgDump("set timezone='US/Eastern';")
 		loc, _ := time.LoadLocation("US/Eastern")
-		assert.Equal(t, conv.location, loc, "Set timezone")
+		assert.Equal(t, conv.Location, loc, "Set timezone")
 	}
 
 	// Finally test data conversion errors.
@@ -540,7 +540,7 @@ func TestProcessPgDump_AddPrimaryKeys(t *testing.T) {
 		conv, _ := runProcessPgDump(tc.input)
 		conv.AddPrimaryKeys()
 		if tc.expectedSchema != nil {
-			assert.Equal(t, tc.expectedSchema, stripSchemaComments(conv.spSchema), tc.name+": Schema did not match")
+			assert.Equal(t, tc.expectedSchema, stripSchemaComments(conv.SpSchema), tc.name+": Schema did not match")
 		}
 	}
 }
@@ -579,14 +579,14 @@ func runProcessPgDump(s string) (*Conv, []spannerData) {
 // many tests are issue-free, but several explicitly test handling of
 // various issues (so don't call nonIssue for them!).
 func noIssues(conv *Conv, t *testing.T, name string) {
-	assert.Zero(t, len(conv.stats.unexpected), fmt.Sprintf("'%s' generated unexpected conditions: %v", name, conv.stats.unexpected))
-	for s, stat := range conv.stats.statement {
-		assert.Zero(t, stat.error, fmt.Sprintf("'%s' generated %d errors for %s statements", name, stat.error, s))
-		if stat.error > 0 {
+	assert.Zero(t, len(conv.Stats.Unexpected), fmt.Sprintf("'%s' generated unexpected conditions: %v", name, conv.Stats.Unexpected))
+	for s, stat := range conv.Stats.Statement {
+		assert.Zero(t, stat.Error, fmt.Sprintf("'%s' generated %d errors for %s statements", name, stat.Error, s))
+		if stat.Error > 0 {
 		}
 	}
-	assert.Zero(t, len(conv.stats.badRows), fmt.Sprintf("'%s' generated bad rows: %v", name, conv.stats.badRows))
-	assert.Zero(t, conv.stats.reparsed, fmt.Sprintf("'%s' generated %d reparse events", name, conv.stats.reparsed))
+	assert.Zero(t, len(conv.Stats.BadRows), fmt.Sprintf("'%s' generated bad rows: %v", name, conv.Stats.BadRows))
+	assert.Zero(t, conv.Stats.Reparsed, fmt.Sprintf("'%s' generated %d reparse events", name, conv.Stats.Reparsed))
 }
 
 // stripSchemaComments returns a schema with all comments removed.
diff --git a/internal/report_test.go b/internal/report_test.go
index e88d074..2ded744 100644
--- a/internal/report_test.go
+++ b/internal/report_test.go
@@ -47,11 +47,11 @@ func TestReport(t *testing.T) {
 	conv := MakeConv()
 	conv.SetSchemaMode()
 	ProcessPgDump(conv, NewReader(bufio.NewReader(strings.NewReader(s)), nil))
-	conv.stats.rows = map[string]int64{"bad_schema": 1000, "no_pk": 5000}
-	conv.stats.goodRows = map[string]int64{"bad_schema": 990, "no_pk": 3000}
-	conv.stats.badRows = map[string]int64{"bad_schema": 10, "no_pk": 2000}
+	conv.Stats.Rows = map[string]int64{"bad_schema": 1000, "no_pk": 5000}
+	conv.Stats.GoodRows = map[string]int64{"bad_schema": 990, "no_pk": 3000}
+	conv.Stats.BadRows = map[string]int64{"bad_schema": 10, "no_pk": 2000}
 	badWrites := map[string]int64{"bad_schema": 50, "no_pk": 0}
-	conv.stats.unexpected["Testing unexpected messages"] = 5
+	conv.Stats.Unexpected["Testing unexpected messages"] = 5
 	buf := new(bytes.Buffer)
 	w := bufio.NewWriter(buf)
 	GenerateReport(true, conv, w, badWrites)
diff --git a/internal/toddl_test.go b/internal/toddl_test.go
index 4ff18ee..5e133a8 100644
--- a/internal/toddl_test.go
+++ b/internal/toddl_test.go
@@ -41,9 +41,9 @@ func TestToSpannerType(t *testing.T) {
 			"f": schema.Column{Name: "f", Type: schema.Type{Name: "timestamptz"}},
 		},
 		PrimaryKeys: []schema.Key{schema.Key{Column: "a"}}}
-	conv.srcSchema[name] = srcSchema
+	conv.SrcSchema[name] = srcSchema
 	assert.Nil(t, schemaToDDL(conv))
-	actual := conv.spSchema[name]
+	actual := conv.SpSchema[name]
 	dropComments(&actual) // Don't test comment.
 	expected := ddl.CreateTable{
 		Name:     name,
@@ -59,11 +59,11 @@ func TestToSpannerType(t *testing.T) {
 		Pks: []ddl.IndexKey{ddl.IndexKey{Col: "a"}},
 	}
 	assert.Equal(t, expected, actual)
-	expectedIssues := map[string][]schemaIssue{
-		"b": []schemaIssue{widened},
-		"e": []schemaIssue{numeric},
+	expectedIssues := map[string][]SchemaIssue{
+		"b": []SchemaIssue{Widened},
+		"e": []SchemaIssue{Numeric},
 	}
-	assert.Equal(t, expectedIssues, conv.issues[name])
+	assert.Equal(t, expectedIssues, conv.Issues[name])
 }
 
 func dropComments(t *ddl.CreateTable) {