Repository: cloudspannerecosystem/harbourbridge
Pull request #62 (2020-06-11 20:15:19)

Added support for Mysqldump
Added `mysql` package to support mysqldump. [pingcap parser](https://github.com/pingcap/parser) is used to parse mysqldump file 

hengfengli: @Dharmil-searce Thanks for fixing my comments. 

@nevinheintze Please take a further review. 

Patch:
diff --git a/go.mod b/go.mod
index 66015bf..70d02e5 100644
--- a/go.mod
+++ b/go.mod
@@ -6,12 +6,20 @@ require (
 	cloud.google.com/go v0.55.0
 	cloud.google.com/go/spanner v1.4.0
 	github.com/DATA-DOG/go-sqlmock v1.4.1
+	github.com/davecgh/go-spew v1.1.1
+	github.com/go-sql-driver/mysql v1.5.0
 	github.com/goware/modvendor v0.0.0-20190516042800-ce72b408a8fe // indirect
+	github.com/juju/errors v0.0.0-20200330140219-3fe23663418f
 	github.com/lfittl/pg_query_go v1.0.0
 	github.com/lib/pq v1.3.0
 	github.com/mattn/go-zglob v0.0.1 // indirect
+	//github.com/pingcap/parser v3.0.12+incompatible
+	github.com/pingcap/parser v0.0.0-20200422082501-7329d80eaf2c
+	github.com/pingcap/tidb v1.1.0-beta.0.20200423105559-af376db3dc46
+	github.com/pingcap/tipb v0.0.0-20200417094153-7316d94df1ee // indirect
+	github.com/sirupsen/logrus v1.5.0 // indirect
 	github.com/stretchr/testify v1.4.0
-	golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550
+	golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413
 	google.golang.org/api v0.20.0
 	google.golang.org/genproto v0.0.0-20200318110522-7735f76e9fa5
 	google.golang.org/grpc v1.28.0
diff --git a/go.sum b/go.sum
index 8cfcffc..a5e0f59 100644
--- a/go.sum
+++ b/go.sum
@@ -34,26 +34,141 @@ cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiy
 cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
 cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
 dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
+github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
 github.com/DATA-DOG/go-sqlmock v1.4.1 h1:ThlnYciV1iM/V0OSF/dtkqWb6xo5qITT1TJBG1MRDJM=
 github.com/DATA-DOG/go-sqlmock v1.4.1/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
+github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
+github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
+github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
+github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
+github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
+github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
+github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
+github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
+github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
+github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
+github.com/antihax/optional v0.0.0-20180407024304-ca021399b1a6/go.mod h1:V8iCPQYkqmusNa815XgQio277wI47sdRh1dUOLdyC6Q=
+github.com/appleboy/gin-jwt/v2 v2.6.3/go.mod h1:MfPYA4ogzvOcVkRwAxT7quHOtQmVKDpTwxyUrC2DNw0=
+github.com/appleboy/gofight/v2 v2.1.2/go.mod h1:frW+U1QZEdDgixycTj4CygQ48yLTUhplt43+Wczp3rw=
+github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
+github.com/beorn7/perks v1.0.0 h1:HWo1m869IqiPhD389kmkxeTalrjNbbJTC8LXupb+sl0=
+github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
+github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
+github.com/blacktear23/go-proxyprotocol v0.0.0-20180807104634-af7a81e8dd0d h1:rQlvB2AYWme2bIB18r/SipGiMEVJYE9U0z+MGoU/LtQ=
+github.com/blacktear23/go-proxyprotocol v0.0.0-20180807104634-af7a81e8dd0d/go.mod h1:VKt7CNAQxpFpSDz3sXyj9hY/GbVsQCr0sB3w59nE7lU=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
+github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
+github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
 github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
+github.com/chzyer/readline v0.0.0-20171208011716-f6d7a1f6fbf3/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
 github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
 github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
 github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
 github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
+github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa h1:OaNxuTZr7kxeODyLWsRMC+OD03aFUH+mW6r2d+MWa5Y=
+github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8=
+github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd h1:qMd81Ts1T2OTKmB4acZcyKaMtRnY5Y44NuXGX2GFJ1w=
+github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI=
+github.com/coreos/go-semver v0.2.0 h1:3Jm3tLmsgAYcjC+4Up7hJrFBPr+n7rAqYeSw/SZazuY=
+github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
+github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
+github.com/coreos/go-systemd v0.0.0-20181031085051-9002847aa142 h1:3jFq2xL4ZajGK4aZY8jz+DAF0FHjI51BXjjSwCzS1Dk=
+github.com/coreos/go-systemd v0.0.0-20181031085051-9002847aa142/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
+github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
+github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f h1:lBNOc5arjvs8E5mO2tbpBpLoyyu8B6e44T7hJy6potg=
+github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
+github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
+github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
+github.com/cznic/golex v0.0.0-20181122101858-9c343928389c/go.mod h1:+bmmJDNmKlhWNG+gwWCkaBoTy39Fs+bzRxVBzoTQbIc=
+github.com/cznic/mathutil v0.0.0-20181122101859-297441e03548 h1:iwZdTE0PVqJCos1vaoKsclOGD3ADKpshg3SRtYBbwso=
+github.com/cznic/mathutil v0.0.0-20181122101859-297441e03548/go.mod h1:e6NPNENfs9mPDVNRekM7lKScauxd5kXTr1Mfyig6TDM=
+github.com/cznic/parser v0.0.0-20160622100904-31edd927e5b1/go.mod h1:2B43mz36vGZNZEwkWi8ayRSSUXLfjL8OkbzwW4NcPMM=
+github.com/cznic/sortutil v0.0.0-20181122101858-f5f958428db8 h1:LpMLYGyy67BoAFGda1NeOBQwqlv7nUXpm+rIVHGxZZ4=
+github.com/cznic/sortutil v0.0.0-20181122101858-f5f958428db8/go.mod h1:q2w6Bg5jeox1B+QkJ6Wp/+Vn0G/bo3f1uY7Fn3vivIQ=
+github.com/cznic/strutil v0.0.0-20171016134553-529a34b1c186/go.mod h1:AHHPPPXTw0h6pVabbcbyGRK1DckRn7r/STdZEeIDzZc=
+github.com/cznic/y v0.0.0-20170802143616-045f81c6662a/go.mod h1:1rk5VM7oSnA4vjp+hrLQ3HWHa+Y4yPCa3/CsJrcNnvs=
+github.com/danjacques/gofslock v0.0.0-20191023191349-0a45f885bc37/go.mod h1:DC3JtzuG7kxMvJ6dZmf2ymjNyoXwgtklr7FN+Um2B0U=
 github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
+github.com/dgraph-io/ristretto v0.0.1 h1:cJwdnj42uV8Jg4+KLrYovLiCgIfz9wtWm6E6KA+1tLs=
+github.com/dgraph-io/ristretto v0.0.1/go.mod h1:T40EBc7CJke8TkpiYfGGKAeFjSaxuFXhuXRyumBd6RE=
+github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
+github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
+github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA=
+github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
+github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=
+github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
+github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
+github.com/dustin/go-humanize v0.0.0-20180421182945-02af3965c54e/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
+github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
+github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
+github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM=
+github.com/elazarl/go-bindata-assetfs v1.0.0/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4=
 github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
 github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
 github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
 github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
+github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
+github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
+github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
+github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94=
+github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
+github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
+github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
+github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32/go.mod h1:GIjDIg/heH5DOkXY3YJ/wNhfHsQHoXGjl8G8amsYQ1I=
+github.com/gin-contrib/gzip v0.0.1/go.mod h1:fGBJBCdt6qCZuCAOwWuFhBB4OOq9EFqlo5dEaFhhu5w=
+github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
+github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
+github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
+github.com/gin-gonic/gin v1.3.0/go.mod h1:7cKuhb5qV2ggCFctp2fJQ+ErvciLZrIeoOSOm6mUr7Y=
+github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM=
+github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do=
+github.com/go-bindata/go-bindata/v3 v3.1.3/go.mod h1:1/zrpXsLD8YDIbhZRqXzm1Ghc7NhEvIN9+Z6R5/xH4I=
+github.com/go-chi/chi v4.0.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
 github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
+github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
+github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
+github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas=
+github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM=
+github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M=
+github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg=
+github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
+github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I=
+github.com/go-openapi/jsonreference v0.19.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I=
+github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc=
+github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8=
+github.com/go-openapi/spec v0.19.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI=
+github.com/go-openapi/spec v0.19.4/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo=
+github.com/go-openapi/spec v0.19.7/go.mod h1:Hm2Jr4jv8G1ciIAo+frC/Ft+rR2kQDh8JHKHb3gWUSk=
+github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg=
+github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
+github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
+github.com/go-openapi/swag v0.19.8/go.mod h1:ao+8BpOPyKdpQz3AOJfbeEVpLmWAvlT1IfTe5McPyhY=
+github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM=
+github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
+github.com/go-playground/overalls v0.0.0-20180201144345-22ec1a223b7c/go.mod h1:UqxAgEOt89sCiXlrc/ycnx00LVvUO/eS8tMUkWX4R7w=
+github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY=
+github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
+github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
+github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
+github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
+github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
+github.com/gogo/protobuf v0.0.0-20180717141946-636bf0302bc9/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
+github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
+github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
+github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls=
+github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
+github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20181024230925-c65c006176ff/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY=
@@ -64,6 +179,7 @@ github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFU
 github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
 github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
 github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
+github.com/golang/protobuf v0.0.0-20180814211427-aa810b61a9c7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
@@ -72,7 +188,10 @@ github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaW
 github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
 github.com/golang/protobuf v1.3.5 h1:F768QJ1E9tib+q5Sc8MkdJi1RxLTbRcTf8LJV56aRls=
 github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
+github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db h1:woRePGFeVFfLKN/pOkfl+p/TAqKOfFu+7KPlMVpok/w=
+github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo=
 github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
 github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
 github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
@@ -80,57 +199,313 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
 github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
 github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
 github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
 github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
 github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
+github.com/google/pprof v0.0.0-20190930153522-6ce02741cba3/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
 github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
 github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3 h1:SRgJV+IoxM5MKyFdlSUeNy6/ycRUF2yBAKdAQswoHUk=
 github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
 github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
+github.com/google/shlex v0.0.0-20181106134648-c34317bd91bf/go.mod h1:RpwtwJQFrIEPstU94h88MWPXP2ektJZ8cZ0YntAmXiE=
+github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
+github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
 github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM=
 github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
+github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw=
+github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
+github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
+github.com/gorilla/websocket v1.2.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
+github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
+github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
 github.com/goware/modvendor v0.0.0-20190516042800-ce72b408a8fe h1:UH0Uvgblv5tsV8QWER09+ZtxIs90pjelhtvWndSJvRM=
 github.com/goware/modvendor v0.0.0-20190516042800-ce72b408a8fe/go.mod h1:iTv+KoD55Rr5NW/P2WlDEJQ2Wa7+LRblD6/u/xJl/i0=
+github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4 h1:z53tR0945TRRQO/fLEVPI6SMv7ZflF0TEaTAoU7tOzg=
+github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
+github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho=
+github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
+github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
+github.com/grpc-ecosystem/grpc-gateway v1.12.1/go.mod h1:8XEsbTttt/W+VvjtQhLACqCisSPWTxCZ7sBRjU6iH9c=
+github.com/grpc-ecosystem/grpc-gateway v1.14.3 h1:OCJlWkOUoTnl0neNGlf4fUm3TmbEtguw7vR+nGtnDjY=
+github.com/grpc-ecosystem/grpc-gateway v1.14.3/go.mod h1:6CwZWGDSPRJidgKAtJVvND6soZe6fT7iteq8wDPdhb0=
+github.com/gtank/cryptopasta v0.0.0-20170601214702-1f550f6f2f69/go.mod h1:YLEMZOtU+AZ7dhN9T/IpGhXVGly2bvkJQ+zxj3WeVQo=
 github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
 github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU=
 github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
+github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
 github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
+github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
+github.com/jinzhu/gorm v1.9.12/go.mod h1:vhTjlKSJUTWNtcbQtrMBFCxy7eXTzeCAzfL5fBZT/Qs=
+github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
+github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
+github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
+github.com/jonboulle/clockwork v0.1.0 h1:VKV+ZcuP6l3yW9doeqz6ziZGgcynBVQO+obU0+0hcPo=
+github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
+github.com/joomcode/errorx v1.0.1/go.mod h1:kgco15ekB6cs+4Xjzo7SPeXzx38PbJzBwbnu9qfVNHQ=
+github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
+github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
+github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
+github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
 github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
 github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
+github.com/juju/errors v0.0.0-20200330140219-3fe23663418f h1:MCOvExGLpaSIzLYB4iQXEHP4jYVU6vmzLNQPdMVrxnM=
+github.com/juju/errors v0.0.0-20200330140219-3fe23663418f/go.mod h1:W54LbzXuIE0boCoNJfwqpmkKJ1O4TCTZMetAt6jGk7Q=
+github.com/juju/ratelimit v1.0.1 h1:+7AIFJVQ0EQgq/K9+0Krm7m530Du7tIz0METWzN0RgY=
+github.com/juju/ratelimit v1.0.1/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk=
+github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
+github.com/kevinburke/go-bindata v3.18.0+incompatible/go.mod h1:/pEEZ72flUW2p0yi30bslSp9YqD9pysLxunQDdb2CPM=
+github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
+github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/klauspost/cpuid v0.0.0-20170728055534-ae7887de9fa5 h1:2U0HzY8BJ8hVwDKIzp7y4voR9CX/nvcfymLmg2UiOio=
+github.com/klauspost/cpuid v0.0.0-20170728055534-ae7887de9fa5/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
+github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
 github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
 github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA=
 github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw=
+github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
 github.com/lfittl/pg_query_go v1.0.0 h1:rcHZK5DBEUoxtO6dACP+UVCHKtA1ZsELBW0rSjOXMAE=
 github.com/lfittl/pg_query_go v1.0.0/go.mod h1:jcikG62RKf+NIWmbLzjjk73m4x6um2pKf3h+TJyINms=
+github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
 github.com/lib/pq v1.3.0 h1:/qkRGz8zljWiDcFvgpwUpwIAPu3r07TDvs3Rws+o/pU=
 github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
+github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
+github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
+github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
+github.com/mailru/easyjson v0.7.1/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs=
+github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
+github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
+github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
+github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
+github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
+github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
+github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
+github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
+github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
+github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=
+github.com/mattn/go-sqlite3 v2.0.1+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
 github.com/mattn/go-zglob v0.0.0-20180803001819-2ea3427bfa53 h1:tGfIHhDghvEnneeRhODvGYOt305TPwingKt6p90F4MU=
 github.com/mattn/go-zglob v0.0.0-20180803001819-2ea3427bfa53/go.mod h1:9fxibJccNxU2cnpIKLRRFA7zX7qhkJIQWBb449FYHOo=
 github.com/mattn/go-zglob v0.0.1 h1:xsEx/XUoVlI6yXjqBK062zYhRTZltCNmYPx6v+8DNaY=
 github.com/mattn/go-zglob v0.0.1/go.mod h1:9fxibJccNxU2cnpIKLRRFA7zX7qhkJIQWBb449FYHOo=
+github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
+github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
+github.com/mgechev/dots v0.0.0-20190921121421-c36f7dcfbb81/go.mod h1:KQ7+USdGKfpPjXk4Ga+5XxQM4Lm4e3gAogrreFAYpOg=
+github.com/mgechev/revive v1.0.2/go.mod h1:rb0dQy1LVAxW9SWy5R3LPUjevzUbUS316U5MFySA2lo=
+github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
+github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/montanaflynn/stats v0.0.0-20151014174947-eeaced052adb/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
+github.com/montanaflynn/stats v0.0.0-20180911141734-db72e6cae808 h1:pmpDGKLw4n82EtrNiLqB+xSz/JQwFOaZuMALYUHwX5s=
+github.com/montanaflynn/stats v0.0.0-20180911141734-db72e6cae808/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
+github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
+github.com/ngaut/pools v0.0.0-20180318154953-b7bc8c42aac7 h1:7KAv7KMGTTqSmYZtNdcNTgsos+vFzULLwyElndwn+5c=
+github.com/ngaut/pools v0.0.0-20180318154953-b7bc8c42aac7/go.mod h1:iWMfgwqYW+e8n5lC/jjNEhwcjbRDpl5NT7n2h+4UNcI=
+github.com/ngaut/sync2 v0.0.0-20141008032647-7a24ed77b2ef h1:K0Fn+DoFqNqktdZtdV3bPQ/0cuYh2H4rkg0tytX/07k=
+github.com/ngaut/sync2 v0.0.0-20141008032647-7a24ed77b2ef/go.mod h1:7WjlapSfwQyo6LNmIvEWzsW1hbBQfpUO4JWnuQRmva8=
+github.com/nicksnyder/go-i18n v1.10.0/go.mod h1:HrK7VCrbOvQoUAQ7Vpy7i87N7JZZZ7R2xBGjv0j365Q=
+github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
+github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA=
+github.com/onsi/ginkgo v1.6.0 h1:Ix8l273rp3QzYgXSR+c8d1fTG7UPgYkOSELPhiY/YGw=
+github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/gomega v1.4.2 h1:3mYCb7aPxS/RU7TI1y4rkEn1oKmPRjNJLNEXgw7MH2I=
+github.com/onsi/gomega v1.4.2/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
+github.com/opentracing/basictracer-go v1.0.0 h1:YyUAhaEfjoWXclZVJ9sGoNct7j4TVk7lZWlQw5UXuoo=
+github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74=
+github.com/opentracing/opentracing-go v1.0.2 h1:3jA2P6O1F9UOrWVpwrIo17pu01KWvNWg4X946/Y5Zwg=
+github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
+github.com/pelletier/go-toml v1.3.0/go.mod h1:PN7xzY2wHTK0K9p34ErDQMlFxa51Fk0OUruD3k1mMwo=
+github.com/phf/go-queue v0.0.0-20170504031614-9abe38d0371d h1:U+PMnTlV2tu7RuMK5etusZG3Cf+rpow5hqQByeCzJ2g=
+github.com/phf/go-queue v0.0.0-20170504031614-9abe38d0371d/go.mod h1:lXfE4PvvTW5xOjO6Mba8zDPyw8M93B6AQ7frTGnMlA8=
+github.com/pingcap-incubator/tidb-dashboard v0.0.0-20200407064406-b2b8ad403d01/go.mod h1:77fCh8d3oKzC5ceOJWeZXAS/mLzVgdZ7rKniwmOyFuo=
+github.com/pingcap/check v0.0.0-20190102082844-67f458068fc8/go.mod h1:B1+S9LNcuMyLH/4HMTViQOJevkGiik3wW2AN9zb2fNQ=
+github.com/pingcap/check v0.0.0-20191107115940-caf2b9e6ccf4/go.mod h1:PYMCGwN0JHjoqGr3HrZoD+b8Tgx8bKnArhSq8YVzUMc=
+github.com/pingcap/check v0.0.0-20191216031241-8a5a85928f12/go.mod h1:PYMCGwN0JHjoqGr3HrZoD+b8Tgx8bKnArhSq8YVzUMc=
+github.com/pingcap/check v0.0.0-20200212061837-5e12011dc712 h1:R8gStypOBmpnHEx1qi//SaqxJVI4inOqljg/Aj5/390=
+github.com/pingcap/check v0.0.0-20200212061837-5e12011dc712/go.mod h1:PYMCGwN0JHjoqGr3HrZoD+b8Tgx8bKnArhSq8YVzUMc=
+github.com/pingcap/errcode v0.0.0-20180921232412-a1a7271709d9 h1:KH4f4Si9XK6/IW50HtoaiLIFHGkapOM6w83za47UYik=
+github.com/pingcap/errcode v0.0.0-20180921232412-a1a7271709d9/go.mod h1:4b2X8xSqxIroj/IZ9MX/VGZhAwc11wB9wRIzHvz6SeM=
+github.com/pingcap/errors v0.11.0/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
+github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
+github.com/pingcap/errors v0.11.5-0.20190809092503-95897b64e011 h1:58naV4XMEqm0hl9LcYo6cZoGBGiLtefMQMF/vo3XLgQ=
+github.com/pingcap/errors v0.11.5-0.20190809092503-95897b64e011/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
+github.com/pingcap/failpoint v0.0.0-20191029060244-12f4ac2fd11d/go.mod h1:DNS3Qg7bEDhU6EXNHF+XSv/PGznQaMJ5FWvctpm6pQI=
+github.com/pingcap/failpoint v0.0.0-20200210140405-f8f9fb234798 h1:6DMbRqPI1qzQ8N1xc3+nKY8IxSACd9VqQKkRVvbyoIg=
+github.com/pingcap/failpoint v0.0.0-20200210140405-f8f9fb234798/go.mod h1:DNS3Qg7bEDhU6EXNHF+XSv/PGznQaMJ5FWvctpm6pQI=
+github.com/pingcap/fn v0.0.0-20191016082858-07623b84a47d h1:rCmRK0lCRrHMUbS99BKFYhK9YxJDNw0xB033cQbYo0s=
+github.com/pingcap/fn v0.0.0-20191016082858-07623b84a47d/go.mod h1:fMRU1BA1y+r89AxUoaAar4JjrhUkVDt0o0Np6V8XbDQ=
+github.com/pingcap/goleveldb v0.0.0-20191226122134-f82aafb29989 h1:surzm05a8C9dN8dIUmo4Be2+pMRb6f55i+UIYrluu2E=
+github.com/pingcap/goleveldb v0.0.0-20191226122134-f82aafb29989/go.mod h1:O17XtbryoCJhkKGbT62+L2OlrniwqiGLSqrmdHCMzZw=
+github.com/pingcap/kvproto v0.0.0-20191211054548-3c6b38ea5107/go.mod h1:WWLmULLO7l8IOcQG+t+ItJ3fEcrL5FxF0Wu+HrMy26w=
+github.com/pingcap/kvproto v0.0.0-20200214064158-62d31900d88e/go.mod h1:IOdRDPLyda8GX2hE/jO7gqaCV/PNFh8BZQCQZXfIOqI=
+github.com/pingcap/kvproto v0.0.0-20200417092353-efbe03bcffbd/go.mod h1:IOdRDPLyda8GX2hE/jO7gqaCV/PNFh8BZQCQZXfIOqI=
+github.com/pingcap/kvproto v0.0.0-20200420075417-e0c6e8842f22 h1:D5EBGKd6o4A0PV0sUaUduPSCShiNi0OwFJmf+xRzpuI=
+github.com/pingcap/kvproto v0.0.0-20200420075417-e0c6e8842f22/go.mod h1:IOdRDPLyda8GX2hE/jO7gqaCV/PNFh8BZQCQZXfIOqI=
+github.com/pingcap/log v0.0.0-20191012051959-b742a5d432e9/go.mod h1:4rbK1p9ILyIfb6hU7OG2CiWSqMXnp3JMbiaVJ6mvoY8=
+github.com/pingcap/log v0.0.0-20200117041106-d28c14d3b1cd h1:CV3VsP3Z02MVtdpTMfEgRJ4T9NGgGTxdHpJerent7rM=
+github.com/pingcap/log v0.0.0-20200117041106-d28c14d3b1cd/go.mod h1:4rbK1p9ILyIfb6hU7OG2CiWSqMXnp3JMbiaVJ6mvoY8=
+github.com/pingcap/parser v0.0.0-20200422082501-7329d80eaf2c h1:eXC+xkHerLvR6+mceugr4e8ALAQHj25S5slt8A2f6Ho=
+github.com/pingcap/parser v0.0.0-20200422082501-7329d80eaf2c/go.mod h1:9v0Edh8IbgjGYW2ArJr19E+bvL8zKahsFp+ixWeId+4=
+github.com/pingcap/pd/v4 v4.0.0-rc.1.0.20200422143320-428acd53eba2 h1:JTzYYukREvxVSKW/ncrzNjFitd8snoQ/Xz32pw8i+s8=
+github.com/pingcap/pd/v4 v4.0.0-rc.1.0.20200422143320-428acd53eba2/go.mod h1:s+utZtXDznOiL24VK0qGmtoHjjXNsscJx3m1n8cC56s=
+github.com/pingcap/sysutil v0.0.0-20200206130906-2bfa6dc40bcd/go.mod h1:EB/852NMQ+aRKioCpToQ94Wl7fktV+FNnxf3CX/TTXI=
+github.com/pingcap/sysutil v0.0.0-20200408114249-ed3bd6f7fdb1 h1:PI8YpTl45F8ilNkrPtT4IdbcZB1SCEa+gK/U5GJYl3E=
+github.com/pingcap/sysutil v0.0.0-20200408114249-ed3bd6f7fdb1/go.mod h1:EB/852NMQ+aRKioCpToQ94Wl7fktV+FNnxf3CX/TTXI=
+github.com/pingcap/tidb v1.1.0-beta.0.20200423105559-af376db3dc46 h1:NvFwbQpRZl7SErObrMOY8KIMbdSVdr9J6KrnHIm2ZGA=
+github.com/pingcap/tidb v1.1.0-beta.0.20200423105559-af376db3dc46/go.mod h1:KJXj2xHYfl1x4zcusC2JEANzVci+ietFOMh/CAmrYdw=
+github.com/pingcap/tidb-tools v4.0.0-beta.1.0.20200306084441-875bd09aa3d5+incompatible h1:84F7MFMfdAYObrznvRslmVu43aoihrlL+7mMyMlOi0o=
+github.com/pingcap/tidb-tools v4.0.0-beta.1.0.20200306084441-875bd09aa3d5+incompatible/go.mod h1:XGdcy9+yqlDSEMTpOXnwf3hiTeqrV6MN/u1se9N8yIM=
+github.com/pingcap/tipb v0.0.0-20190428032612-535e1abaa330/go.mod h1:RtkHW8WbcNxj8lsbzjaILci01CtYnYbIkQhjyZWrWVI=
+github.com/pingcap/tipb v0.0.0-20200417094153-7316d94df1ee h1:XJQ6/LGzOSc/jo33AD8t7jtc4GohxcyODsYnb+kZXJM=
+github.com/pingcap/tipb v0.0.0-20200417094153-7316d94df1ee/go.mod h1:RtkHW8WbcNxj8lsbzjaILci01CtYnYbIkQhjyZWrWVI=
+github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
+github.com/prometheus/client_golang v1.0.0 h1:vrDKnkGzuGvhNAL56c7DBz29ZL+KxnoR0x7enabFceM=
+github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
+github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
+github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 h1:gQz4mCbXsO+nc9n1hCxHcGA3Zx3Eo+UHZoInFGUIXNM=
 github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/common v0.4.1 h1:K0MGApIoQvMw27RTdJkPbr3JZ7DNbtxQNyi5STVM6Kw=
+github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
+github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
+github.com/prometheus/procfs v0.0.2 h1:6LJUbpNm42llc4HRCuvApCSWB/WfhuNo9K98Q9sNGfs=
+github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
+github.com/remyoudompheng/bigfft v0.0.0-20190728182440-6a916e37a237 h1:HQagqIiBmr8YXawX/le3+O26N+vPPC1PtjaF3mwnook=
+github.com/remyoudompheng/bigfft v0.0.0-20190728182440-6a916e37a237/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
+github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
+github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
+github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
+github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
+github.com/sergi/go-diff v1.0.1-0.20180205163309-da645544ed44/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
+github.com/shirou/gopsutil v2.19.10+incompatible h1:lA4Pi29JEVIQIgATSeftHSY0rMGI9CLrl2ZvDLiahto=
+github.com/shirou/gopsutil v2.19.10+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
+github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4/go.mod h1:qsXQc7+bwAM3Q1u/4XEfrquwF8Lw7D7y5cD8CuHnfIc=
+github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
+github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
+github.com/shurcooL/vfsgen v0.0.0-20181020040650-a97a25d856ca/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw=
+github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
+github.com/sirupsen/logrus v1.5.0 h1:1N5EYkVAPEywqZRJd7cwnRtCb6xJx7NH3T3WUTF980Q=
+github.com/sirupsen/logrus v1.5.0/go.mod h1:+F7Ogzej0PZc/94MaYx/nvG9jOFMD2osvC3s+Squfpo=
+github.com/soheilhy/cmux v0.1.4 h1:0HKaf1o97UwFjHH9o5XsHUOF+tqmdA7KEzXLpiyaw0E=
+github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
+github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72 h1:qLC7fQah7D6K1B0ujays3HV9gkFtllcxhzImRR7ArPQ=
+github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
+github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
+github.com/spf13/pflag v1.0.1 h1:aCvUg6QPl3ibpQUxyLkrEkCHtPqYJL4x9AuhqVqFis4=
+github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/swaggo/files v0.0.0-20190704085106-630677cd5c14/go.mod h1:gxQT6pBGRuIGunNf/+tSOB5OHvguWi8Tbt82WOkf35E=
+github.com/swaggo/gin-swagger v1.2.0/go.mod h1:qlH2+W7zXGZkczuL+r2nEBR2JTT+/lX05Nn6vPhc7OI=
+github.com/swaggo/http-swagger v0.0.0-20200103000832-0e9263c4b516/go.mod h1:O1lAbCgAAX/KZ80LM/OXwtWFI/5TvZlwxSg8Cq08PV0=
+github.com/swaggo/http-swagger v0.0.0-20200308142732-58ac5e232fba/go.mod h1:O1lAbCgAAX/KZ80LM/OXwtWFI/5TvZlwxSg8Cq08PV0=
+github.com/swaggo/swag v1.5.1/go.mod h1:1Bl9F/ZBpVWh22nY0zmYyASPO1lI/zIwRDrpZU+tv8Y=
+github.com/swaggo/swag v1.6.3/go.mod h1:wcc83tB4Mb2aNiL/HP4MFeQdpHUrca+Rp/DRNgWAUio=
+github.com/swaggo/swag v1.6.5/go.mod h1:Y7ZLSS0d0DdxhWGVhQdu+Bu1QhaF5k0RD7FKdiAykeY=
+github.com/swaggo/swag v1.6.6-0.20200323071853-8e21f4cefeea/go.mod h1:xDhTyuFIujYiN3DKWC/H/83xcfHp+UE/IzWWampG7Zc=
+github.com/syndtr/goleveldb v0.0.0-20180815032940-ae2bd5eed72d h1:4J9HCZVpvDmj2tiKGSTUnb3Ok/9CEQb9oqu9LHKQQpc=
+github.com/syndtr/goleveldb v0.0.0-20180815032940-ae2bd5eed72d/go.mod h1:Z4AUp2Km+PwemOoO/VB5AOx9XSsIItzFjoJlOSiYmn0=
+github.com/tiancaiamao/appdash v0.0.0-20181126055449-889f96f722a2 h1:mbAskLJ0oJfDRtkanvQPiooDH8HvJ2FBh+iKT/OmiQQ=
+github.com/tiancaiamao/appdash v0.0.0-20181126055449-889f96f722a2/go.mod h1:2PfKggNGDuadAa0LElHrByyrz4JPZ9fFx6Gs7nx7ZZU=
+github.com/tidwall/gjson v1.3.5/go.mod h1:P256ACg0Mn+j1RXIDXoss50DeIABTYK1PULOJHhxOls=
+github.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E=
+github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
+github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
+github.com/tmc/grpc-websocket-proxy v0.0.0-20171017195756-830351dc03c6 h1:lYIiVDtZnyTWlNwiAxLj0bbpTcx1BWCFhXjfsvmPdNc=
+github.com/tmc/grpc-websocket-proxy v0.0.0-20171017195756-830351dc03c6/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
+github.com/uber-go/atomic v1.3.2 h1:Azu9lPBWRNKzYXSIwRfgRuDuS0YKsK4NFhiQv98gkxo=
+github.com/uber-go/atomic v1.3.2/go.mod h1:/Ct5t2lcmbJ4OSe/waGBoaVvVqtO0bmtfVNex1PFV8g=
+github.com/uber/jaeger-client-go v2.15.0+incompatible h1:NP3qsSqNxh8VYr956ur1N/1C1PjvOJnJykCzcD5QHbk=
+github.com/uber/jaeger-client-go v2.15.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk=
+github.com/uber/jaeger-lib v1.5.0 h1:OHbgr8l656Ub3Fw5k9SWnBfIEwvoHQ+W2y+Aa9D1Uyo=
+github.com/uber/jaeger-lib v1.5.0/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U=
+github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
+github.com/ugorji/go v1.1.5-pre/go.mod h1:FwP/aQVg39TXzItUBMwnWp9T9gPQnXw4Poh4/oBQZ/0=
+github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
+github.com/ugorji/go/codec v0.0.0-20181022190402-e5e69e061d4f/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
+github.com/ugorji/go/codec v1.1.5-pre/go.mod h1:tULtS6Gy1AE1yCENaw4Vb//HLH5njI2tfCQDUqRd8fI=
+github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
+github.com/unrolled/render v0.0.0-20171102162132-65450fb6b2d3/go.mod h1:tu82oB5W2ykJRVioYsB+IQKcft7ryBr7w12qMBUPyXg=
+github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
+github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
+github.com/urfave/cli/v2 v2.1.1/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ=
+github.com/urfave/negroni v0.3.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4=
+github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5Qo6v2eYzo7kUS51QINcR5jNpbZS8=
+github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
+github.com/yookoala/realpath v1.0.0/go.mod h1:gJJMA9wuX7AcqLy1+ffPatSCySA1FQ2S8Ya9AIoYBpE=
 github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+go.etcd.io/bbolt v1.3.3 h1:MUGmc65QhB3pIlaQ5bB4LwqSj6GIonVJXpZiaKNyaKk=
+go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
+go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg=
+go.etcd.io/etcd v0.5.0-alpha.5.0.20191023171146-3cf2f69b5738 h1:lWF4f9Nypl1ZqSb4gLeh/DGvBYVaUYHuiB93teOmwgc=
+go.etcd.io/etcd v0.5.0-alpha.5.0.20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg=
 go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
 go.opencensus.io v0.22.0 h1:C9hSCOW830chIVkdja34wa6Ky+IzWllkUinR+BtRZd4=
 go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
 go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
 go.opencensus.io v0.22.3 h1:8sGtKOrtQqkN1bp2AtX+misvLIlOmsEsNd+9NIcPEm8=
 go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
+go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
+go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
+go.uber.org/atomic v1.6.0 h1:Ezj3JGmsOnG1MoRWQkPBsKLe9DwWD9QeXzTRzzldNVk=
+go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
+go.uber.org/automaxprocs v1.2.0/go.mod h1:YfO3fm683kQpzETxlTGZhGIVmXAhaw3gxeBADbpZtnU=
+go.uber.org/dig v1.8.0/go.mod h1:X34SnWGr8Fyla9zQNO2GSO2D+TIuqB14OS8JhYocIyw=
+go.uber.org/fx v1.10.0/go.mod h1:vLRicqpG/qQEzno4SYU86iCwfT95EZza+Eba0ItuxqY=
+go.uber.org/goleak v0.10.0 h1:G3eWbSNIskeRqtsN/1uI5B+eP73y3JUuBsv9AZjehb4=
+go.uber.org/goleak v0.10.0/go.mod h1:VCZuO8V8mFPlL0F5J5GK1rtHV3DrFcQ1R8ryq7FK0aI=
+go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
+go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
+go.uber.org/multierr v1.4.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
+go.uber.org/multierr v1.5.0 h1:KCa4XfM8CWFCpxXRGok+Q0SS/0XBhMDbHHGABQLvD2A=
+go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
+go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
+go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
+go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
+go.uber.org/zap v1.12.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
+go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
+go.uber.org/zap v1.14.1 h1:nYDKopTbvAPq/NrUVZwT15y2lpROBiLLyoRTbXOYWOo=
+go.uber.org/zap v1.14.1/go.mod h1:Mb2vm2krFEG5DV0W9qcHBYFtp/Wku1cvYaqPsS/WYfc=
+golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413 h1:ULYEB3JvPRE/IfO+9uO7vKV/xzVTO7XPAwm8xbf4w2g=
+golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -165,6 +540,10 @@ golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzB
 golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
@@ -172,9 +551,14 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
 golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
+golang.org/x/net v0.0.0-20190611141213-3f473d35a33a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI=
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20191002035440-2ec189313ef0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -182,6 +566,8 @@ golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLL
 golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200301022130-244492dfa37a h1:GuSPYbZzB5/dcLNCwLQLsg3obCJtX9IJhpXkvY7kzk0=
 golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e h1:3G+cUijn7XD+S4eJFddp53Pv7+slrESplyjG25HgL+k=
+golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0=
@@ -195,18 +581,31 @@ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJ
 golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a h1:WXEvlFVvvGxCJLG6REjsT03iWnKLEWinaScsxF2Vm2o=
 golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181228144115-9a3f9b0469bb/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190610200419-93c9922d18ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0 h1:HyfiK1WMnHj5FXFXatD+Qs1A/xC2Run6RzeW1SyHxpc=
 golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -217,15 +616,21 @@ golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200317113312-5766fd39f98d h1:62ap6LNOjDU6uGmKXHJbSfciMoV+FeI1sRXx/pLDL44=
 golang.org/x/sys v0.0.0-20200317113312-5766fd39f98d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884=
+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs=
 golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
 golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
@@ -234,7 +639,10 @@ golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3
 golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
 golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
 golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190606050223-4d9ae51c2468/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
 golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190611222205-d73e1c7e250b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
 golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
 golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
 golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
@@ -242,7 +650,12 @@ golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtn
 golang.org/x/tools v0.0.0-20190917162342-3b4f30a44f3b h1:5PDpbTpVmeVPIQOoxshLbs4ATaIDQrZN5z3nTUtm2+8=
 golang.org/x/tools v0.0.0-20190917162342-3b4f30a44f3b/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191030062658-86caa796c7ab/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191107010934-f79515f33823/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191114200427-caa0b0f7d508/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
@@ -258,11 +671,15 @@ golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapK
 golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
 golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
 golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200225230052-807dcd883420/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
 golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
 golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
 golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
 golang.org/x/tools v0.0.0-20200317043434-63da46f3035e/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
 golang.org/x/tools v0.0.0-20200318054722-11a475a590ac/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
+golang.org/x/tools v0.0.0-20200325010219-a49f79bcc224/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
+golang.org/x/tools v0.0.0-20200325203130-f53864d0dba1 h1:odiryKYJy7CjdrZxhrcE1Z8L9+kGyGZOnfpuauvdCeU=
+golang.org/x/tools v0.0.0-20200325203130-f53864d0dba1/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
@@ -286,6 +703,7 @@ google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7
 google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
 google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
 google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/genproto v0.0.0-20181004005441-af9cb2a35e7f/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
 google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
 google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
 google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
@@ -294,6 +712,7 @@ google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98
 google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
 google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51 h1:Ex1mq5jaJof+kRnYi3SlYJ8KKa9Ao3NHyIT5XJ1gF6U=
 google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
+google.golang.org/genproto v0.0.0-20190927181202-20e1ac93f88c/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
 google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
 google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
 google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
@@ -309,28 +728,62 @@ google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfG
 google.golang.org/genproto v0.0.0-20200317114155-1f3552e48f24/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
 google.golang.org/genproto v0.0.0-20200318110522-7735f76e9fa5 h1:Bs8aCQBqwnuSvG/tB3ip/W8JLeuQt1+1ppSHYi4n9RM=
 google.golang.org/genproto v0.0.0-20200318110522-7735f76e9fa5/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/grpc v0.0.0-20180607172857-7a6a684ca69e/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
 google.golang.org/grpc v1.21.1 h1:j6XxA85m/6txkUCHvzlV5f+HBNl/1r5cZ2A/3IEFOO8=
 google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
 google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
+google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
+google.golang.org/grpc v1.24.0/go.mod h1:XDChyiUovWa60DnaeDeZmSW86xtLtjtZbwvSiRnRtcA=
 google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
 google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
 google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
 google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
 google.golang.org/grpc v1.28.0 h1:bO/TA4OxCOummhSf10siHuG7vJOiwh7SpRpFZDkOgl4=
 google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
+gopkg.in/alecthomas/gometalinter.v2 v2.0.12/go.mod h1:NDRytsqEZyolNuAgTzJkZMkSQM7FIKyzVzGhjB/qfYo=
+gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
+gopkg.in/alecthomas/kingpin.v3-unstable v3.0.0-20180810215634-df19058c872c/go.mod h1:3HH7i1SgMqlzxCcBmUHW657sD4Kvv9sC3HpL3YukzwA=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
 gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
+gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
+gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
+gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
+gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y=
+gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ=
+gopkg.in/go-playground/validator.v9 v9.31.0/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ=
+gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
+gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
+gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
+gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
+gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
+gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
 honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
+k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I=
 rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
 rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
 rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
+sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs=
+sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
+sourcegraph.com/sourcegraph/appdash v0.0.0-20180531100431-4c381bd170b4 h1:VO9oZbbkvTwqLimlQt15QNdOOBArT2dw/bvzsMZBiqQ=
+sourcegraph.com/sourcegraph/appdash v0.0.0-20180531100431-4c381bd170b4/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU=
+sourcegraph.com/sourcegraph/appdash-data v0.0.0-20151005221446-73f23eafcf67 h1:e1sMhtVq9AfcEy8AXNb8eSg6gbzfdpYhoNqnPJa+GzI=
+sourcegraph.com/sourcegraph/appdash-data v0.0.0-20151005221446-73f23eafcf67/go.mod h1:L5q+DGLGOQFpo1snNEkLOJT2d1YTW66rWNzatr3He1k=
diff --git a/internal/convert.go b/internal/convert.go
index ef1e627..4dca660 100644
--- a/internal/convert.go
+++ b/internal/convert.go
@@ -54,6 +54,7 @@ type SyntheticPKey struct {
 	Sequence int64
 }
 
+// SchemaIssue specifies a schema conversion issue.
 type SchemaIssue int
 
 // Defines all of the schema issues we track. Includes issues
@@ -67,9 +68,13 @@ const (
 	NoGoodType
 	Numeric
 	NumericThatFits
+	Decimal
+	DecimalThatFits
 	Serial
 	Timestamp
+	Datetime
 	Widened
+	Time
 )
 
 // NameAndCols contains the name of a table and its columns.
@@ -141,7 +146,7 @@ func (conv *Conv) SetDataSink(ds func(table string, cols []string, values []inte
 }
 
 // Note on modes.
-// We process the pg_dump output twice. In the first pass (schema mode) we
+// We process the dump output twice. In the first pass (schema mode) we
 // build the schema, and the second pass (data mode) we write data to
 // Spanner.
 
@@ -233,7 +238,7 @@ func (conv *Conv) Unexpecteds() int64 {
 	return int64(len(conv.Stats.Unexpected))
 }
 
-// CollectBadRows updates the list of bad rows, while respecting
+// CollectBadRow updates the list of bad rows, while respecting
 // the byte limit for bad rows.
 func (conv *Conv) CollectBadRow(srcTable string, srcCols, vals []string) {
 	r := &row{table: srcTable, cols: srcCols, vals: vals}
@@ -296,7 +301,7 @@ func (conv *Conv) buildPrimaryKey(spTable string) string {
 	}
 }
 
-// unexpected records stats about corner-cases and conditions
+// 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 dump data twice.
@@ -346,6 +351,7 @@ func (conv *Conv) getStatementStat(s string) *statementStat {
 	return conv.Stats.Statement[s]
 }
 
+// SkipStatement increments the skip statement stats for 'stmtType'.
 func (conv *Conv) SkipStatement(stmtType string) {
 	if conv.SchemaMode() { // Record statement stats on first pass only.
 		VerbosePrintf("Skipping statement: %s\n", stmtType)
@@ -353,6 +359,7 @@ func (conv *Conv) SkipStatement(stmtType string) {
 	}
 }
 
+// ErrorInStatement increments the error statement stats for 'stmtType'.
 func (conv *Conv) ErrorInStatement(stmtType string) {
 	if conv.SchemaMode() { // Record statement stats on first pass only.
 		VerbosePrintf("Error processing statement: %s\n", stmtType)
@@ -360,22 +367,26 @@ func (conv *Conv) ErrorInStatement(stmtType string) {
 	}
 }
 
+// SchemaStatement increments the schema statement stats for 'stmtType'.
 func (conv *Conv) SchemaStatement(stmtType string) {
 	if conv.SchemaMode() { // Record statement stats on first pass only.
 		conv.getStatementStat(stmtType).schema++
 	}
 }
 
+// DataStatement increments the data statement stats for 'stmtType'.
 func (conv *Conv) DataStatement(stmtType string) {
 	if conv.SchemaMode() { // Record statement stats on first pass only.
 		conv.getStatementStat(stmtType).data++
 	}
 }
 
+// SchemaMode returns true if conv is configured to schemaOnly.
 func (conv *Conv) SchemaMode() bool {
 	return conv.mode == schemaOnly
 }
 
+// DataMode returns true if conv is configured to dataOnly.
 func (conv *Conv) DataMode() bool {
 	return conv.mode == dataOnly
 }
diff --git a/internal/report.go b/internal/report.go
index 7f1a0a7..f9afe10 100644
--- a/internal/report.go
+++ b/internal/report.go
@@ -26,7 +26,7 @@ import (
 
 // GenerateReport analyzes schema and data conversion stats and writes a
 // detailed report to w and returns a brief summary (as a string).
-func GenerateReport(fromPgDump bool, conv *Conv, w *bufio.Writer, badWrites map[string]int64) string {
+func GenerateReport(driverName string, conv *Conv, w *bufio.Writer, badWrites map[string]int64) string {
 	reports := analyzeTables(conv, badWrites)
 	summary := generateSummary(conv, reports, badWrites)
 	writeHeading(w, "Summary of Conversion")
@@ -40,8 +40,12 @@ func GenerateReport(fromPgDump bool, conv *Conv, w *bufio.Writer, badWrites map[
 		w.WriteString("\n\n")
 	}
 	statementsMsg := ""
-	if fromPgDump {
-		statementsMsg = "stats on the pg_dump statements processed, followed by "
+	var isDump bool
+	if strings.Contains(driverName, "dump") {
+		isDump = true
+	}
+	if isDump {
+		statementsMsg = "stats on the " + driverName + " statements processed, followed by "
 	}
 	justifyLines(w, "The remainder of this report provides "+statementsMsg+
 		"a table-by-table listing of schema and data conversion details. "+
@@ -49,8 +53,8 @@ func GenerateReport(fromPgDump bool, conv *Conv, w *bufio.Writer, badWrites map[
 		"and explanations of the terms and notes used in this "+
 		"report, see HarbourBridge's README.", 80, 0)
 	w.WriteString("\n\n")
-	if fromPgDump {
-		writeStmtStats(conv, w)
+	if isDump {
+		writeStmtStats(driverName, conv, w)
 	}
 	for _, t := range reports {
 		h := fmt.Sprintf("Table %s", t.srcTable)
@@ -58,7 +62,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)
@@ -68,7 +72,7 @@ func GenerateReport(fromPgDump bool, conv *Conv, w *bufio.Writer, badWrites map[
 			w.WriteString("\n")
 		}
 	}
-	writeUnexpectedConditions(conv, w)
+	writeUnexpectedConditions(driverName, conv, w)
 	return summary
 }
 
@@ -79,7 +83,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
 }
 
@@ -117,7 +121,7 @@ func buildTableReport(conv *Conv, srcTable string, badWrites map[string]int64) t
 	tr.cols = cols
 	tr.warnings = warnings
 	if pk, ok := conv.SyntheticPKeys[spTable]; ok {
-		tr.SyntheticPKey = pk.Col
+		tr.syntheticPKey = pk.Col
 		tr.body = buildTableReportBody(conv, srcTable, issues, spSchema, srcSchema, &pk.Col)
 	} else {
 		tr.body = buildTableReportBody(conv, srcTable, issues, spSchema, srcSchema, nil)
@@ -187,6 +191,8 @@ func buildTableReportBody(conv *Conv, srcTable string, issues map[string][]Schem
 				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 Datetime:
+					l = append(l, fmt.Sprintf("Some columns have source DB type 'datetime' which is mapped to Spanner type timestamp e.g. column '%s'. %s", srcCol, issueDB[i].brief))
 				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:
@@ -242,8 +248,12 @@ var issueDB = map[SchemaIssue]struct {
 	NoGoodType:            {brief: "No appropriate Spanner type", severity: warning},
 	Numeric:               {brief: "Spanner does not support numeric. This type mapping could lose precision and is not recommended for production use", severity: warning},
 	NumericThatFits:       {brief: "Spanner does not support numeric, but this type mapping preserves the numeric's specified precision", severity: note},
+	Decimal:               {brief: "Spanner does not support decimal. This type mapping could lose precision and is not recommended for production use", severity: warning},
+	DecimalThatFits:       {brief: "Spanner does not support decimal, but this type mapping preserves the decimal's specified precision", severity: note},
 	Serial:                {brief: "Spanner does not support autoincrementing types", severity: warning},
 	Timestamp:             {brief: "Spanner timestamp is closer to PostgreSQL timestamptz", severity: note, batch: true},
+	Datetime:              {brief: "Spanner timestamp is closer to MySQL timestamp", severity: note, batch: true},
+	Time:                  {brief: "Spanner does not support time/year types", severity: note, batch: true},
 	Widened:               {brief: "Some columns will consume more storage in Spanner", severity: note, batch: true},
 }
 
@@ -358,14 +368,14 @@ 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
 		}
 	}
 	// Don't use tableReport for rows/badRows stats because tableReport
 	// provides per-table stats for each table in the schema i.e. it omits
 	// rows for tables not in the schema. To handle this corner-case, use
-	// the source of truth for row stats: conv.stats.
+	// the source of truth for row stats: conv.Stats.
 	rows := conv.Rows()
 	badRows := conv.BadRows() // Bad rows encountered during data conversion.
 	// Add in bad rows while writing to Spanner.
@@ -396,7 +406,7 @@ func ignoredStatements(conv *Conv) (l []string) {
 	return l
 }
 
-func writeStmtStats(conv *Conv, w *bufio.Writer) {
+func writeStmtStats(driverName string, conv *Conv, w *bufio.Writer) {
 	type stat struct {
 		statement string
 		count     int64
@@ -410,7 +420,7 @@ func writeStmtStats(conv *Conv, w *bufio.Writer) {
 		return l[i].statement < l[j].statement
 	})
 	writeHeading(w, "Statements Processed")
-	w.WriteString("Analysis of statements in pg_dump output, broken down by statement type.\n")
+	w.WriteString("Analysis of statements in " + driverName + " output, broken down by statement type.\n")
 	w.WriteString("  schema: statements successfully processed for Spanner schema information.\n")
 	w.WriteString("    data: statements successfully processed for data.\n")
 	w.WriteString("    skip: statements not relevant for Spanner schema or data.\n")
@@ -422,15 +432,21 @@ func writeStmtStats(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)
 	}
-	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")
+	if driverName == "pgdump" {
+		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")
+	} else if driverName == "mysqldump" {
+		w.WriteString("See https://github.com/pingcap/parser for definitions of statement types\n")
+		w.WriteString("(pingcap/parser is the library we use for parsing mysqldump output).\n")
+		w.WriteString("\n")
+	}
 }
 
-func writeUnexpectedConditions(conv *Conv, w *bufio.Writer) {
+func writeUnexpectedConditions(driverName string, 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)
+			fmt.Fprintf(w, "Note: there were %d %s reparse events while looking for statement boundaries.\n\n", conv.Stats.Reparsed, driverName)
 		}
 	}
 	writeHeading(w, "Unexpected Conditions")
@@ -439,12 +455,26 @@ func writeUnexpectedConditions(conv *Conv, w *bufio.Writer) {
 		reparseInfo()
 		return
 	}
-	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")
-	w.WriteString("pg_dump 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")
+	switch driverName {
+	case "mysqldump":
+		w.WriteString("For debugging only. This section provides details of unexpected conditions\n")
+		w.WriteString("encountered as we processed the mysqldump data. In particular, the AST node\n")
+		w.WriteString("representation used by the pingcap/parser library used for parsing\n")
+		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":
+		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")
+		w.WriteString("pg_dump 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")
+	default:
+		w.WriteString("For debugging only. This section provides details of unexpected conditions\n")
+		w.WriteString("encountered as we processed the " + driverName + " data. The list details\n")
+		w.WriteString("all unexpected conditions\n")
+	}
 	w.WriteString("  --------------------------------------\n")
 	fmt.Fprintf(w, "  %6s  %s\n", "count", "condition")
 	w.WriteString("  --------------------------------------\n")
diff --git a/main.go b/main.go
index 9d41af4..eca404a 100644
--- a/main.go
+++ b/main.go
@@ -13,7 +13,7 @@
 // limitations under the License.
 
 // Package main implements HarbourBridge, a stand-alone tool for Cloud Spanner
-// evaluation, using data from an existing PostgreSQL database. See README.md
+// evaluation, using data from an existing PostgreSQL/MySQL database. See README.md
 // for details.
 package main
 
@@ -37,6 +37,7 @@ import (
 	sp "cloud.google.com/go/spanner"
 	database "cloud.google.com/go/spanner/admin/database/apiv1"
 	instance "cloud.google.com/go/spanner/admin/instance/apiv1"
+	_ "github.com/go-sql-driver/mysql"
 	_ "github.com/lib/pq"
 	"golang.org/x/crypto/ssh/terminal"
 	"google.golang.org/api/iterator"
@@ -46,6 +47,7 @@ import (
 	"google.golang.org/grpc"
 
 	"github.com/cloudspannerecosystem/harbourbridge/internal"
+	"github.com/cloudspannerecosystem/harbourbridge/mysql"
 	"github.com/cloudspannerecosystem/harbourbridge/postgres"
 	"github.com/cloudspannerecosystem/harbourbridge/spanner"
 	"github.com/cloudspannerecosystem/harbourbridge/spanner/ddl"
@@ -56,6 +58,10 @@ const (
 	PGDUMP string = "pgdump"
 	// POSTGRES is the driver name for PostgreSQL.
 	POSTGRES string = "postgres"
+	// MYSQLDUMP is the driver name for mysqldump.
+	MYSQLDUMP string = "mysqldump"
+	// MYSQL is the driver name for MySQL.
+	MYSQL string = "mysql"
 )
 
 var (
@@ -67,14 +73,13 @@ var (
 	filePrefix       = ""
 	driverName       = ""
 	verbose          bool
-	fromPgDump       bool
 )
 
 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", "", "driver name: experimental flag for accessing source DB via database/sql driver (only accepted value is \"postgres\")")
+	flag.StringVar(&driverName, "driver", "", "driver name: experimental flag for accessing source DB via database/sql driver (accepted values are \"postgres\" and \"mysql\")")
 	flag.BoolVar(&verbose, "v", false, "verbose: print additional output")
 }
 
@@ -100,6 +105,7 @@ func main() {
 	defer close(lf)
 
 	ioHelper := &ioStreams{in: os.Stdin, out: os.Stdout}
+
 	project, err := getProject()
 	if err != nil {
 		fmt.Printf("\nCan't get project: %v\n", err)
@@ -116,12 +122,12 @@ func main() {
 		}
 	}
 	fmt.Printf("Using Spanner instance: %s\n", instance)
-	printPermissionsWarning(ioHelper.out)
+	printPermissionsWarning(driverName, ioHelper.out)
 
 	now := time.Now()
 	dbName := dbNameOverride
 	if dbName == "" {
-		dbName, err = getDatabaseName(now)
+		dbName, err = getDatabaseName(driverName, now)
 		if err != nil {
 			fmt.Printf("\nCan't get database name: %v\n", err)
 			panic(fmt.Errorf("can't get database name"))
@@ -133,15 +139,11 @@ func main() {
 		filePrefix = dbName + "."
 	}
 
-	// If driverName specified, access source DB via database/sql
-	// driver. Otherwise read pgdump data from stdin.
-	if driverName == "" {
-		driverName = PGDUMP
-	}
 	err = toSpanner(driverName, project, instance, dbName, ioHelper, filePrefix, now)
 	if err != nil {
 		panic(err)
 	}
+
 }
 
 // toSpanner is the main entrance of the entire conversion, which runs the
@@ -156,11 +158,13 @@ func toSpanner(driver, projectID, instanceID, dbName string, ioHelper *ioStreams
 		return err
 	}
 	// close the seekable file
+
 	if ioHelper.seekableIn != nil {
 		defer ioHelper.in.Close()
 	}
 
 	writeSchemaFile(conv, now, outputFilePrefix+schemaFile, ioHelper.out)
+
 	db, err := createDatabase(projectID, instanceID, dbName, conv, ioHelper.out)
 	if err != nil {
 		fmt.Printf("\nCan't create database: %v\n", err)
@@ -179,17 +183,17 @@ func toSpanner(driver, projectID, instanceID, dbName string, ioHelper *ioStreams
 		return fmt.Errorf("can't finish data conversion")
 	}
 	banner := getBanner(now, db)
-	report(bw.DroppedRowsByTable(), 0, banner, conv, outputFilePrefix+reportFile, ioHelper.out)
+	report(driver, bw.DroppedRowsByTable(), ioHelper.bytesRead, banner, conv, outputFilePrefix+reportFile, ioHelper.out)
 	writeBadData(bw, conv, banner, outputFilePrefix+badDataFile, ioHelper.out)
 	return nil
 }
 
 func schemaConv(driver string, ioHelper *ioStreams) (*internal.Conv, error) {
 	switch driver {
-	case POSTGRES:
+	case POSTGRES, MYSQL:
 		return schemaFromSQL(driver)
-	case PGDUMP:
-		return schemaFromPgDump(ioHelper)
+	case PGDUMP, MYSQLDUMP:
+		return schemaFromDump(driver, ioHelper)
 	default:
 		return nil, fmt.Errorf("schema conversion for driver %s not supported", driver)
 	}
@@ -203,10 +207,10 @@ func dataConv(driver string, ioHelper *ioStreams, client *sp.Client, conv *inter
 		Verbose:    internal.Verbose(),
 	}
 	switch driver {
-	case POSTGRES:
-		return dataFromSQL(config, client, conv, driver)
-	case PGDUMP:
-		return dataFromPgDump(config, ioHelper, client, conv)
+	case POSTGRES, MYSQL:
+		return dataFromSQL(driver, config, client, conv)
+	case PGDUMP, MYSQLDUMP:
+		return dataFromDump(driver, config, ioHelper, client, conv)
 	default:
 		return nil, fmt.Errorf("data conversion for driver %s not supported", driver)
 	}
@@ -216,6 +220,8 @@ func driverConfig(driver string) (string, error) {
 	switch driver {
 	case POSTGRES:
 		return pgDriverConfig()
+	case MYSQL:
+		return mysqlDriverConfig()
 	default:
 		return "", fmt.Errorf("Driver %s not supported", driver)
 	}
@@ -230,10 +236,29 @@ func pgDriverConfig() (string, error) {
 		fmt.Printf("Please specify host, port, user and database using PGHOST, PGPORT, PGUSER and PGDATABASE environment variables\n")
 		return "", fmt.Errorf("Could not connect to source database")
 	}
-	password := getPassword()
+	password := os.Getenv("PGPASSWORD")
+	if password == "" {
+		password = getPassword()
+	}
 	return fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable", server, port, user, password, dbname), nil
 }
 
+func mysqlDriverConfig() (string, error) {
+	server := os.Getenv("MYSQLHOST")
+	port := os.Getenv("MYSQLPORT")
+	user := os.Getenv("MYSQLUSER")
+	dbname := os.Getenv("MYSQLDATABASE")
+	if server == "" || port == "" || user == "" || dbname == "" {
+		fmt.Printf("Please specify host, port, user and database using MYSQLHOST, MYSQLPORT, MYSQLUSER and MYSQLDATABASE environment variables\n")
+		return "", fmt.Errorf("Could not connect to source database")
+	}
+	password := os.Getenv("MYSQL_PWD")
+	if password == "" {
+		password = getPassword()
+	}
+	return fmt.Sprintf("%s:%s@tcp(%s:%s)/%s", user, password, server, port, dbname), nil
+}
+
 func schemaFromSQL(driver string) (*internal.Conv, error) {
 	driverConfig, err := driverConfig(driver)
 	if err != nil {
@@ -251,7 +276,7 @@ func schemaFromSQL(driver string) (*internal.Conv, error) {
 	return conv, nil
 }
 
-func dataFromSQL(config spanner.BatchWriterConfig, client *sp.Client, conv *internal.Conv, driver string) (*spanner.BatchWriter, error) {
+func dataFromSQL(driver string, config spanner.BatchWriterConfig, client *sp.Client, conv *internal.Conv) (*spanner.BatchWriter, error) {
 	// TODO: Refactor to avoid redundant calls to driverConfig and
 	// Open in schemaFromSQL and dataFromSQL. Also refactor to
 	// share code with dataFromPgDump. Use single transaction for
@@ -285,7 +310,7 @@ func dataFromSQL(config spanner.BatchWriterConfig, client *sp.Client, conv *inte
 		func(table string, cols []string, vals []interface{}) {
 			writer.AddRow(table, cols, vals)
 		})
-	postgres.ProcessSqlData(conv, sourceDB)
+	postgres.ProcessSQLData(conv, sourceDB)
 	writer.Flush()
 	return writer, nil
 }
@@ -295,10 +320,10 @@ type ioStreams struct {
 	bytesRead           int64
 }
 
-func schemaFromPgDump(ioHelper *ioStreams) (*internal.Conv, error) {
+func schemaFromDump(driver string, ioHelper *ioStreams) (*internal.Conv, error) {
 	f, n, err := getSeekable(ioHelper.in)
 	if err != nil {
-		printSeekError(err, ioHelper.out)
+		printSeekError(driver, err, ioHelper.out)
 		return nil, fmt.Errorf("can't get seekable input file")
 	}
 	ioHelper.seekableIn = f
@@ -306,9 +331,9 @@ func schemaFromPgDump(ioHelper *ioStreams) (*internal.Conv, error) {
 	conv := internal.MakeConv()
 	p := internal.NewProgress(n, "Generating schema", internal.Verbose())
 	r := internal.NewReader(bufio.NewReader(f), p)
-	conv.SetSchemaMode() // Build schema and ignore data in pg_dump.
+	conv.SetSchemaMode() // Build schema and ignore data in dump.
 	conv.SetDataSink(nil)
-	err = postgres.ProcessPgDump(conv, r)
+	err = ProcessDump(driver, conv, r)
 	if err != nil {
 		fmt.Fprintf(ioHelper.out, "Failed to parse the data file: %v", err)
 		return nil, fmt.Errorf("failed to parse the data file")
@@ -317,7 +342,7 @@ func schemaFromPgDump(ioHelper *ioStreams) (*internal.Conv, error) {
 	return conv, nil
 }
 
-func dataFromPgDump(config spanner.BatchWriterConfig, ioHelper *ioStreams, client *sp.Client, conv *internal.Conv) (*spanner.BatchWriter, error) {
+func dataFromDump(driver string, config spanner.BatchWriterConfig, ioHelper *ioStreams, client *sp.Client, conv *internal.Conv) (*spanner.BatchWriter, error) {
 	_, err := ioHelper.seekableIn.Seek(0, 0)
 	if err != nil {
 		fmt.Printf("\nCan't seek to start of file (preparation for second pass): %v\n", err)
@@ -338,19 +363,19 @@ func dataFromPgDump(config spanner.BatchWriterConfig, ioHelper *ioStreams, clien
 		return nil
 	}
 	writer := spanner.NewBatchWriter(config)
-	conv.SetDataMode() // Process data in pg_dump; schema is unchanged.
+	conv.SetDataMode() // Process data in dump; schema is unchanged.
 	conv.SetDataSink(
 		func(table string, cols []string, vals []interface{}) {
 			writer.AddRow(table, cols, vals)
 		})
-	postgres.ProcessPgDump(conv, r)
+	ProcessDump(driver, conv, r)
 	writer.Flush()
 	p.Done()
 
 	return writer, nil
 }
 
-func report(badWrites map[string]int64, bytesRead int64, banner string, conv *internal.Conv, reportFileName string, out *os.File) {
+func report(driver string, badWrites map[string]int64, bytesRead int64, banner string, conv *internal.Conv, reportFileName string, out *os.File) {
 	f, err := os.Create(reportFileName)
 	if err != nil {
 		fmt.Fprintf(out, "Can't write out report file %s: %v\n", reportFileName, err)
@@ -361,14 +386,19 @@ func report(badWrites map[string]int64, bytesRead int64, banner string, conv *in
 	}
 	w := bufio.NewWriter(f)
 	w.WriteString(banner)
-	summary := internal.GenerateReport(fromPgDump, conv, w, badWrites)
+
+	summary := internal.GenerateReport(driver, conv, w, badWrites)
 	w.Flush()
-	if fromPgDump {
-		fmt.Fprintf(out, "Processed %d bytes of pg_dump data (%d statements, %d rows of data, %d errors, %d unexpected conditions).\n",
-			bytesRead, conv.Statements(), conv.Rows(), conv.StatementErrors(), conv.Unexpecteds())
+	var isDump bool
+	if strings.Contains(driver, "dump") {
+		isDump = true
+	}
+	if isDump {
+		fmt.Fprintf(out, "Processed %d bytes of %s data (%d statements, %d rows of data, %d errors, %d unexpected conditions).\n",
+			bytesRead, driver, conv.Statements(), conv.Rows(), conv.StatementErrors(), conv.Unexpecteds())
 	} else {
 		fmt.Fprintf(out, "Processed source database via %s driver (%d rows of data, %d unexpected conditions).\n",
-			driverName, conv.Rows(), conv.Unexpecteds())
+			driver, conv.Rows(), conv.Unexpecteds())
 	}
 	// We've already written summary to f (as part of GenerateReport).
 	// In the case where f is stdout, don't write a duplicate copy.
@@ -388,7 +418,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 output is large (tens of GBs) and os.TempDir points to a directory
+	// pg_dump/mysql_dump 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.
@@ -480,7 +510,7 @@ func getInstance(project string, out *os.File) (string, error) {
 		return "", fmt.Errorf("no Spanner instances for %s", project)
 	}
 	// Note: we could ask for user input to select/confirm which Spanner
-	// instance to use, but that interacts poorly with piping pg_dump data
+	// instance to use, but that interacts poorly with piping pg_dump/mysqldump data
 	// to the tool via stdin.
 	if len(l) == 1 {
 		fmt.Fprintf(out, "Using only available Spanner instance: %s\n", l[0])
@@ -593,15 +623,11 @@ func writeBadData(bw *spanner.BatchWriter, conv *internal.Conv, banner, name str
 	fmt.Fprintf(out, "See file '%s' for details of bad rows\n", name)
 }
 
-func getDatabaseName(now time.Time) (string, error) {
-	return generateName(fmt.Sprintf("pg_dump_%s", now.Format("2006-01-02")))
+func getDatabaseName(driver string, now time.Time) (string, error) {
+	return generateName(fmt.Sprintf("%s_%s", driver, now.Format("2006-01-02")))
 }
 
 func getPassword() string {
-	password := os.Getenv("PGPASSWORD")
-	if password != "" {
-		return password
-	}
 	fmt.Print("Enter Password: ")
 	bytePassword, err := terminal.ReadPassword(int(syscall.Stdin))
 	if err != nil {
@@ -636,24 +662,24 @@ instance for project %s.
 	return err
 }
 
-func printPermissionsWarning(out *os.File) {
+func printPermissionsWarning(driver string, out *os.File) {
 	fmt.Fprintf(out,
 		`
 WARNING: Please check that permissions for this Spanner instance are
 appropriate. Spanner manages access control at the database level, and the
 database created by HarbourBridge will inherit default permissions from this
 instance. All data written to Spanner will be visible to anyone who can
-access the created database. Note that PostgreSQL table-level and row-level
+access the created database. Note that `+driver+` table-level and row-level
 ACLs are dropped during conversion since they are not supported by Spanner.
 
 `)
 }
 
-func printSeekError(err error, out *os.File) {
+func printSeekError(driver string, err error, out *os.File) {
 	fmt.Fprintf(out, "\nCan't get seekable input file: %v\n", err)
 	fmt.Fprintf(out, "Likely cause: not enough space in %s.\n", os.TempDir())
-	fmt.Fprintf(out, "Try writing pg_dump output to a file first i.e.\n")
-	fmt.Fprintf(out, "  pg_dump > tmpfile\n")
+	fmt.Fprintf(out, "Try writing "+driver+" output to a file first i.e.\n")
+	fmt.Fprintf(out, " "+driver+" > tmpfile\n")
 	fmt.Fprintf(out, "  harbourbridge < tmpfile\n")
 }
 
@@ -727,3 +753,15 @@ func sum(m map[string]int64) int64 {
 func getBanner(now time.Time, db string) string {
 	return fmt.Sprintf("Generated at %s for db %s\n\n", now.Format("2006-01-02 15:04:05"), db)
 }
+
+// ProcessDump invokes process dump function from a sql package based on driver selected.
+func ProcessDump(driver string, conv *internal.Conv, r *internal.Reader) error {
+	switch driver {
+	case MYSQLDUMP:
+		return mysql.ProcessMySQLDump(conv, r)
+	case PGDUMP:
+		return postgres.ProcessPgDump(conv, r)
+	default:
+		return fmt.Errorf("schema conversion for driver %s not supported", driver)
+	}
+}
diff --git a/mysql/data.go b/mysql/data.go
new file mode 100644
index 0000000..670eabb
--- /dev/null
+++ b/mysql/data.go
@@ -0,0 +1,249 @@
+// Copyright 2020 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package mysql
+
+import (
+	"fmt"
+	"math/bits"
+	"reflect"
+	"strconv"
+	"strings"
+	"time"
+
+	"cloud.google.com/go/civil"
+	"cloud.google.com/go/spanner"
+
+	"github.com/cloudspannerecosystem/harbourbridge/internal"
+	"github.com/cloudspannerecosystem/harbourbridge/schema"
+	"github.com/cloudspannerecosystem/harbourbridge/spanner/ddl"
+)
+
+// TODO: mysql/data.go and postgres/data.go share significant chunks of common code.
+// The key differences are in convArray and convTimestamp. Consider refactoring using
+// a go interface that provides the basic conv functions for each type.
+
+// ProcessDataRow converts a row of data and writes it out to Spanner.
+// srcTable and srcCols are the source table and columns respectively,
+// and vals contains string data to be converted to appropriate types
+// to send to Spanner. ProcessDataRow is only called in DataMode.
+func ProcessDataRow(conv *internal.Conv, srcTable string, srcCols []string, srcSchema schema.Table, spTable string, spCols []string, spSchema ddl.CreateTable, vals []string) {
+	spTable, cvtCols, cvtVals, err := ConvertData(conv, srcTable, srcCols, srcSchema, spTable, spCols, spSchema, vals)
+	if err != nil {
+		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, cvtCols, cvtVals)
+	}
+}
+
+// ConvertData maps the source DB data in vals into Spanner data,
+// based on the Spanner and source DB schemas. Note that since entries
+// in vals may be empty, we also return the list of columns (empty
+// cols are dropped).
+func ConvertData(conv *internal.Conv, srcTable string, srcCols []string, srcSchema schema.Table, spTable string, spCols []string, spSchema ddl.CreateTable, vals []string) (string, []string, []interface{}, error) {
+	var c []string
+	var v []interface{}
+	if len(spCols) != len(srcCols) || len(spCols) != len(vals) {
+		return "", []string{}, []interface{}{}, fmt.Errorf("ConvertData: spCols, srcCols and vals don't all have the same lengths: len(spCols)=%d, len(srcCols)=%d, len(vals)=%d", len(spCols), len(srcCols), len(vals))
+	}
+	for i, spCol := range spCols {
+		srcCol := srcCols[i]
+		// Skip columns with 'NULL' values. When processing data rows from mysqldump, these values
+		// are represented as nil (by pingcap/tidb/types/parser_driver's ValueExpr), which is
+		// converted to the string '<nil>'. When processing data rows obtained from the MySQL driver,
+		// 'NULL' values are represented as "NULL" (because we retrieve the values as strings).
+		if vals[i] == "<nil>" || vals[i] == "NULL" {
+			continue
+		}
+		spColDef, ok1 := spSchema.ColDefs[spCol]
+		srcColDef, ok2 := srcSchema.ColDefs[srcCol]
+		if !ok1 || !ok2 {
+			return "", []string{}, []interface{}{}, fmt.Errorf("can't find Spanner and source-db schema for col %s", spCol)
+		}
+		var x interface{}
+		var err error
+		if spColDef.IsArray {
+			x, err = convArray(spColDef.T, srcColDef.Type.Name, vals[i])
+		} else {
+			x, err = convScalar(spColDef.T, srcColDef.Type.Name, conv.TimezoneOffset, vals[i])
+		}
+		if err != nil {
+			return "", []string{}, []interface{}{}, err
+		}
+		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
+	}
+	return spTable, c, v, nil
+}
+
+// convScalar converts a source database string value to an
+// appropriate Spanner value. It is the caller's responsibility to
+// detect and handle NULL values: convScalar will return error if a
+// NULL value is passed.
+func convScalar(spannerType ddl.ScalarType, srcTypeName string, TimezoneOffset string, val string) (interface{}, error) {
+	// Whitespace within the val string is considered part of the data value.
+	// Note that many of the underlying conversions functions we use (like
+	// strconv.ParseFloat and strconv.ParseInt) return "invalid syntax"
+	// errors if whitespace were to appear at the start or end of a string.
+	// We do not expect mysqldump to generate such output.
+	switch spannerType.(type) {
+	case ddl.Bool:
+		return convBool(val)
+	case ddl.Bytes:
+		return convBytes(val)
+	case ddl.Date:
+		return convDate(val)
+	case ddl.Float64:
+		return convFloat64(val)
+	case ddl.Int64:
+		return convInt64(val)
+	case ddl.String:
+		return val, nil
+	case ddl.Timestamp:
+		return convTimestamp(srcTypeName, TimezoneOffset, val)
+	default:
+		return val, fmt.Errorf("data conversion not implemented for type %v", reflect.TypeOf(spannerType))
+	}
+}
+
+func convBool(val string) (bool, error) {
+	b, err := strconv.ParseBool(val)
+	if err != nil {
+		return b, fmt.Errorf("can't convert to bool: %w", err)
+	}
+	return b, err
+}
+
+func convBytes(val string) ([]byte, error) {
+	// convert a string to a byte slice.
+	b := []byte(val)
+	return b, nil
+}
+
+func convDate(val string) (civil.Date, error) {
+	d, err := civil.ParseDate(val)
+	if err != nil {
+		return d, fmt.Errorf("can't convert to date: %w", err)
+	}
+	return d, err
+}
+
+func convFloat64(val string) (float64, error) {
+	f, err := strconv.ParseFloat(val, 64)
+	if err != nil {
+		return f, fmt.Errorf("can't convert to float64: %w", err)
+	}
+	return f, err
+}
+
+func convInt64(val string) (int64, error) {
+	i, err := strconv.ParseInt(val, 10, 64)
+	if err != nil {
+		return i, fmt.Errorf("can't convert to int64: %w", err)
+	}
+	return i, err
+}
+
+// convTimestamp maps a source DB timestamp into a go Time Spanner timestamp
+// It handles both datetime and timestamp conversions.
+func convTimestamp(srcTypeName string, TimezoneOffset string, val string) (t time.Time, err error) {
+	// mysqldump outputs timestamps as ISO 8601, except
+	// it uses space instead of T.
+	if srcTypeName == "timestamp" {
+		// We consider timezone for timestamp datatype.
+		// If timezone is not specified in mysqldump, we consider UTC time.
+		if TimezoneOffset == "" {
+			TimezoneOffset = "+00:00"
+		}
+		// convert timestamp from format "2006-01-02 15:04:05" to
+		// "2006-01-02T15:04:05+00:00".
+		timeNew := strings.Split(val, " ")
+		timeJoined := strings.Join(timeNew, "T")
+		timeJoined = timeJoined + TimezoneOffset
+		t, err = time.Parse(time.RFC3339, timeJoined)
+	} else {
+		// datetime: data should just consist of date and time.
+		// timestamp conversion should ignore timezone. We mimic this using Parse
+		// i.e. treat it as UTC, so it will be stored 'as-is' in Spanner.
+		t, err = time.Parse("2006-01-02 15:04:05", val)
+	}
+	if err != nil {
+		return t, fmt.Errorf("can't convert to timestamp (mysql type: %s)", srcTypeName)
+	}
+	return t, err
+}
+
+// convArray converts a source database string value (representing an
+// array) to an appropriate Spanner array value. It is the caller's
+// responsibility to detect and handle the case where the entire array
+// is NULL. However, convArray does handle the case where individual
+// array elements are NULL. In other words, convArray handles "{1,
+// NULL, 2}", but it does not handle "NULL" (it returns error).
+// NOTE : convArray would only be called when MySQL 'SET' datatype is encountered.
+func convArray(spannerType ddl.ScalarType, srcTypeName string, v string) (interface{}, error) {
+	v = strings.TrimSpace(v)
+	// Handle empty array. Note that we use an empty NullString array
+	// for all Spanner array types since this will be converted to the
+	// appropriate type by the Spanner client.
+	if v == "" {
+		return []spanner.NullString{}, nil
+	}
+
+	a := strings.Split(v, ",")
+
+	// The Spanner client for go does not accept []interface{} for arrays.
+	// Instead it only accepts slices of a specific type eg: []string
+	// Hence we have to do the following case analysis.
+	// NOTE: MySQL only supports SET of string which will be translated
+	// to spanner array<string>.
+	switch spannerType.(type) {
+	case ddl.String:
+		var r []spanner.NullString
+		for _, s := range a {
+			if s == "NULL" {
+				r = append(r, spanner.NullString{Valid: false})
+				continue
+			}
+			s, err := processQuote(s)
+			if err != nil {
+				return []spanner.NullString{}, err
+			}
+			r = append(r, spanner.NullString{StringVal: s, Valid: true})
+		}
+		return r, nil
+	}
+	return []interface{}{}, fmt.Errorf("array type conversion not implemented for type %v", reflect.TypeOf(spannerType))
+}
+
+// processQuote returns the unquoted version of s.
+// Note: The element values of a MySQL array ('SET' datatype) may have double
+// quotes around them. The array output routine will put double
+// quotes around element values if they are empty strings, contain
+// curly braces, delimiter characters, double quotes, backslashes, or
+// white space, or match the word NULL. Double quotes and backslashes
+// embedded in element values will be backslash-escaped.
+func processQuote(s string) (string, error) {
+	if len(s) >= 2 && s[0] == '"' && s[len(s)-1] == '"' {
+		return strconv.Unquote(s)
+	}
+	return s, nil
+}
diff --git a/mysql/mysqldump.go b/mysql/mysqldump.go
new file mode 100644
index 0000000..22d6c7d
--- /dev/null
+++ b/mysql/mysqldump.go
@@ -0,0 +1,583 @@
+// Copyright 2020 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package mysql
+
+import (
+	"fmt"
+	"reflect"
+	"regexp"
+	"strconv"
+	"strings"
+
+	"github.com/cloudspannerecosystem/harbourbridge/internal"
+	"github.com/cloudspannerecosystem/harbourbridge/schema"
+	"github.com/pingcap/parser"
+	"github.com/pingcap/parser/ast"
+	driver "github.com/pingcap/tidb/types/parser_driver"
+)
+
+// TODO: consider refactoring mysqldump.go and pgdump.go to extract
+// common code-paths/logic into a shared set of utils.
+
+var valuesRegexp = regexp.MustCompile("\\((.*?)\\)")
+var insertRegexp = regexp.MustCompile("INSERT\\sINTO\\s(.*?)\\sVALUES\\s")
+
+// MysqlSpatialDataTypes is an array of all MySQL spatial data types.
+var MysqlSpatialDataTypes = []string{"geometrycollection", "multipoint", "multilinestring", "multipolygon", "point", "linestring", "polygon", "geometry"}
+
+// ProcessMySQLDump reads mysqldump data from r and does schema or data conversion,
+// depending on whether conv is configured for schema mode or data mode.
+// In schema mode, ProcessMySQLDump incrementally builds a schema (updating conv).
+// In data mode, ProcessMySQLDump uses this schema to convert MySQL data
+// and writes it to Spanner, using the data sink specified in conv.
+func ProcessMySQLDump(conv *internal.Conv, r *internal.Reader) error {
+	for {
+		startLine := r.LineNumber
+		startOffset := r.Offset
+		b, stmts, err := readAndParseChunk(conv, r)
+		if err != nil {
+			return err
+		}
+		for _, stmt := range stmts {
+			isInsert := processStatement(conv, stmt)
+			internal.VerbosePrintf("Parsed SQL command at line=%d/fpos=%d: %d stmts (%d lines, %d bytes) Insert Statement=%v\n", startLine, startOffset, 1, r.LineNumber-startLine, len(b), isInsert)
+		}
+		if r.EOF {
+			break
+		}
+	}
+	if conv.SchemaMode() {
+		schemaToDDL(conv)
+		conv.AddPrimaryKeys()
+	}
+	return nil
+}
+
+// readAndParseChunk parses a chunk of mysqldump data, returning the bytes read,
+// the parsed AST (nil if nothing read), error and whether we've hit end-of-file.
+// In effect, we proceed through the file, statement by statement. Many
+// statements (e.g. DDL statements) are small, but insert statements can
+// be large. Fortunately mysqldump limits the size of insert statements
+// (default is 24MB, but configurable via --max-allowed-packet), and so
+// the chunks of file we read/parse are manageable, even for mysqldump
+// files containing tens or hundreds of GB of data.
+func readAndParseChunk(conv *internal.Conv, r *internal.Reader) ([]byte, []ast.StmtNode, error) {
+	var l [][]byte
+	for {
+		b := r.ReadLine()
+		l = append(l, b)
+		// If we see a semicolon or eof, we're likely to have a command, so try to parse it.
+		// Note: we could just parse every iteration, but that would mean more attempts at parsing.
+		if strings.Contains(string(b), ";") || r.EOF {
+			n := 0
+			for i := range l {
+				n += len(l[i])
+			}
+			s := make([]byte, n)
+			n = 0
+			for i := range l {
+				n += copy(s[n:], l[i])
+			}
+			chunk := string(s)
+			tree, _, err := parser.New().Parse(chunk, "", "")
+			if err == nil {
+				return s, tree, nil
+			}
+			newTree, ok := handleParseError(conv, chunk, err, l)
+			if ok {
+				return s, newTree, nil
+			}
+			// Likely causes of failing to parse:
+			// a) complex statements with embedded semicolons e.g. 'CREATE FUNCTION'
+			// 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++
+		}
+		if r.EOF {
+			return nil, nil, fmt.Errorf("Error parsing last %d line(s) of input", len(l))
+		}
+	}
+}
+
+// processStatement extracts schema information from MySQL
+// statements, updating Conv with new schema information, and returning
+// true if INSERT statement is encountered.
+func processStatement(conv *internal.Conv, stmt ast.StmtNode) bool {
+	switch s := stmt.(type) {
+	case *ast.CreateTableStmt:
+		if conv.SchemaMode() {
+			processCreateTable(conv, s)
+		}
+	case *ast.AlterTableStmt:
+		if conv.SchemaMode() {
+			processAlterTable(conv, s)
+		}
+	case *ast.SetStmt:
+		if conv.SchemaMode() {
+			processSetStmt(conv, s)
+		}
+	case *ast.InsertStmt:
+		processInsertStmt(conv, s)
+		return true
+	default:
+		conv.SkipStatement(reflect.TypeOf(stmt).String())
+	}
+	return false
+}
+
+func processSetStmt(conv *internal.Conv, stmt *ast.SetStmt) {
+	if stmt.Variables != nil && len(stmt.Variables) > 0 {
+		for _, variable := range stmt.Variables {
+			if variable.Name == "TIME_ZONE" {
+				value := variable.Value
+				switch val := value.(type) {
+				case *driver.ValueExpr:
+					if val.GetValue() == nil {
+						logStmtError(conv, stmt, fmt.Errorf("found nil value in set 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)))
+					return
+				}
+			}
+		}
+	}
+}
+
+func processCreateTable(conv *internal.Conv, stmt *ast.CreateTableStmt) {
+	if stmt.Table == nil {
+		logStmtError(conv, stmt, fmt.Errorf("table is nil"))
+		return
+	}
+	tableName, err := getTableName(stmt.Table)
+	if err != nil {
+		logStmtError(conv, stmt, fmt.Errorf("can't get table name: %w", err))
+		return
+	}
+	var colNames []string
+	colDef := make(map[string]schema.Column)
+	var keys []schema.Key
+	for _, element := range stmt.Cols {
+		colname, col, isPk, err := processColumn(conv, tableName, element)
+		if err != nil {
+			logStmtError(conv, stmt, err)
+			return
+		}
+		colNames = append(colNames, colname)
+		colDef[colname] = col
+		if isPk {
+			keys = append(keys, schema.Key{Column: colname})
+		}
+	}
+	conv.SchemaStatement(reflect.TypeOf(stmt).String())
+	conv.SrcSchema[tableName] = schema.Table{
+		Name:        tableName,
+		ColNames:    colNames,
+		ColDefs:     colDef,
+		PrimaryKeys: keys}
+	for _, constraint := range stmt.Constraints {
+		processConstraint(conv, tableName, constraint, "CREATE TABLE")
+	}
+}
+
+func processConstraint(conv *internal.Conv, table string, constraint *ast.Constraint, stmtType string) {
+	st := conv.SrcSchema[table]
+	switch ct := constraint.Tp; ct {
+	case ast.ConstraintPrimaryKey:
+		checkEmpty(conv, st.PrimaryKeys, stmtType) // Drop any previous primary keys.
+		st.PrimaryKeys = toSchemaKeys(constraint.Keys)
+		// In Spanner, primary key columns are usually annotated with NOT NULL,
+		// but this can be omitted to allow NULL values in key columns.
+		// In MySQL, the primary key constraint is a combination of
+		// NOT NULL and UNIQUE i.e. primary keys must be NOT NULL and UNIQUE.
+		// We preserve MySQL semantics and enforce NOT NULL and UNIQUE.
+		updateCols(conv, ast.ConstraintPrimaryKey, constraint.Keys, st.ColDefs, table)
+	default:
+		updateCols(conv, ct, constraint.Keys, st.ColDefs, table)
+	}
+	conv.SrcSchema[table] = st
+}
+
+// toSchemaKeys converts a string list of MySQL primary keys to
+// schema primary keys.
+func toSchemaKeys(columns []*ast.IndexPartSpecification) (keys []schema.Key) {
+	for _, colname := range columns {
+		// MySQL primary keys have no notation of ascending/descending.
+		// We map them all into ascending primary keys.
+		keys = append(keys, schema.Key{Column: colname.Column.Name.String()})
+	}
+	return keys
+}
+
+func updateCols(conv *internal.Conv, ct ast.ConstraintType, colNames []*ast.IndexPartSpecification, colDef map[string]schema.Column, tableName string) {
+	for _, column := range colNames {
+		colName := column.Column.OrigColName()
+		cd := colDef[colName]
+		switch ct {
+		case ast.ConstraintForeignKey:
+			cd.Ignored.ForeignKey = true
+		case ast.ConstraintUniq:
+			cd.Unique = true
+		case ast.ConstraintCheck:
+			cd.Ignored.Check = true
+		case ast.ConstraintPrimaryKey:
+			cd.NotNull = true
+			cd.Unique = true
+		default:
+			conv.Unexpected(fmt.Sprintf("Found constraint type of const value `%v` in table : %s , column : %s. Mapping of ConstraintType can be found here: 'https://godoc.org/github.com/pingcap/parser/ast#ConstraintType'", ct, tableName, colName))
+		}
+		colDef[colName] = cd
+	}
+}
+
+func processAlterTable(conv *internal.Conv, stmt *ast.AlterTableStmt) {
+	if stmt.Table == nil {
+		logStmtError(conv, stmt, fmt.Errorf("table is nil"))
+		return
+	}
+	tableName, err := getTableName(stmt.Table)
+	if err != nil {
+		logStmtError(conv, stmt, fmt.Errorf("can't get table name: %w", err))
+		return
+	}
+	if _, ok := conv.SrcSchema[tableName]; ok {
+		for _, item := range stmt.Specs {
+			switch alterType := item.Tp; alterType {
+			case ast.AlterTableAddConstraint:
+				processConstraint(conv, tableName, item.Constraint, "ALTER TABLE")
+				conv.SchemaStatement(reflect.TypeOf(stmt).String())
+			case ast.AlterTableModifyColumn:
+				colname, col, isPk, err := processColumn(conv, tableName, item.NewColumns[0])
+				if err != nil {
+					logStmtError(conv, stmt, err)
+					return
+				}
+				conv.SrcSchema[tableName].ColDefs[colname] = col
+				if isPk {
+					ctable := conv.SrcSchema[tableName]
+					checkEmpty(conv, ctable.PrimaryKeys, "ALTER TABLE")
+					ctable.PrimaryKeys = []schema.Key{{Column: colname}}
+					conv.SrcSchema[tableName] = ctable
+				}
+				conv.SchemaStatement(reflect.TypeOf(stmt).String())
+			default:
+				conv.SkipStatement(reflect.TypeOf(stmt).String())
+			}
+		}
+
+	} else {
+		conv.SkipStatement(reflect.TypeOf(stmt).String())
+	}
+}
+
+// getTableName extracts the table name from *ast.TableName table, and returns
+// the raw extracted name (the MySQL table name).
+// *ast.TableName is used to represent table names. It consists of two components:
+//  Schema: schemas in MySQL db often unspecified;
+//  Name: name of the table
+// We build a table name from these components as follows:
+// a) nil components are dropped.
+// b) if more than one component is specified, they are joined using "."
+//    (Note that Spanner doesn't allow "." in table names, so this
+//    will eventually get re-mapped when we construct the Spanner table name).
+// c) return error if Table is nil or "".
+func getTableName(table *ast.TableName) (string, error) {
+	var l []string
+
+	if table.Schema.String() != "" {
+		l = append(l, table.Schema.String())
+	}
+	if table.Name.String() == "" {
+		return "", fmt.Errorf("tablename is empty: can't build table name")
+	}
+	l = append(l, table.Name.String())
+	return strings.Join(l, "."), nil
+}
+
+func processColumn(conv *internal.Conv, tableName string, col *ast.ColumnDef) (string, schema.Column, bool, error) {
+	if col.Name == nil {
+		return "", schema.Column{}, false, fmt.Errorf("column name is nil")
+	}
+	name := col.Name.OrigColName()
+	if col.Tp == nil {
+		return "", schema.Column{}, false, fmt.Errorf("can't get column type for %s: %w", name, fmt.Errorf("found nil *ast.ColumnDef.Tp"))
+	}
+	tid, mods := getTypeModsAndID(conv, col.Tp.String())
+	ty := schema.Type{
+		Name:        tid,
+		Mods:        mods,
+		ArrayBounds: getArrayBounds(col.Tp.String(), col.Tp.Elems)}
+	column := schema.Column{Name: name, Type: ty}
+	return name, column, updateColsByOption(conv, tableName, col, &column), nil
+}
+
+// updateColsByOption is specifially for ColDef constraints.
+// ColumnOption type is used for parsing column constraint info from MySQL.
+func updateColsByOption(conv *internal.Conv, tableName string, col *ast.ColumnDef, column *schema.Column) bool {
+	isPk := false
+	for _, elem := range col.Options {
+		switch op := elem.Tp; op {
+		case ast.ColumnOptionPrimaryKey:
+			column.NotNull = true
+			column.Unique = true
+			// If primary key is defined in a column then `isPk` will be true
+			// and this column will be added in colDef as primary keys.
+			isPk = true
+		case ast.ColumnOptionNotNull:
+			column.NotNull = true
+		case ast.ColumnOptionDefaultValue:
+			column.Ignored.Default = true
+		case ast.ColumnOptionUniqKey:
+			column.Unique = true
+		case ast.ColumnOptionCheck:
+			column.Ignored.Check = true
+		case ast.ColumnOptionReference:
+			column.Ignored.ForeignKey = true
+		default:
+			conv.Unexpected(fmt.Sprintf("Not supported type option of const value `%v` found in table : %s , column : %s. Mapping of columnOptionType can be found here: 'https://godoc.org/github.com/pingcap/parser/ast#ColumnOptionType'", op, tableName, column.Name))
+		}
+	}
+	return isPk
+}
+
+// getTypeModsAndID returns ID and mods of column datatype.
+func getTypeModsAndID(conv *internal.Conv, columnType string) (string, []int64) {
+	// There are no methods in pincap parser to retirieve ID and mods.
+	// We will process columnType eg:'varchar(40)' and split ID from the string.
+	// We retrieve mods using regex expression and convert it to INT64.
+	id := columnType
+	var mods []int64
+	if strings.Contains(columnType, "(") {
+		id = strings.Split(columnType, "(")[0]
+		// For 'set' and 'enum' datatypes, values provided are not maxLength.
+		if id == "set" || id == "enum" {
+			return id, nil
+		}
+		values := valuesRegexp.FindString(columnType)
+		strMods := strings.Split(values[1:len(values)-1], ",")
+		for _, i := range strMods {
+			j, err := strconv.ParseInt(i, 10, 64)
+			if err != nil {
+				conv.Unexpected(fmt.Sprintf("Unable to get modifiers for `%s` datatype.", id))
+				return id, nil
+			}
+			mods = append(mods, j)
+		}
+	}
+	// 'BINARY' keyword suffix will be added to all blob datatypes by parser.
+	// Eg: mediumblob BINARY. It needs to be trimmed to retrieve ID.
+	if strings.Contains(id, " ") {
+		id = strings.TrimSuffix(columnType, " BINARY")
+	}
+	return id, mods
+}
+
+// handleParseError handles error while parsing mysqldump
+// statements and attempts at creating parsable chunk.
+// Error can be due to insert statement or unsupported Spatial
+// datatypes in create statement.
+func handleParseError(conv *internal.Conv, chunk string, err error, l [][]byte) ([]ast.StmtNode, bool) {
+	// Check if error is due to Insert statement.
+	insertStmtPrefix := insertRegexp.FindString(chunk)
+	if insertStmtPrefix != "" {
+		// Sending chunk as list of values and insertStmtPrefix separately
+		// to avoid column names being treated as values by valuesRegexp.
+		// Eg : INSERT INTO mytable (a, b c) VALUES (1, 2, 3),(4, 5, 6);
+		// insertStmtPrefix = INSERT INTO mytable (a, b c) VALUES
+		// valuesChunk = (1, 2, 3),(4, 5, 6);
+		valuesChunk := insertRegexp.Split(chunk, 2)[1] // stripping off insertStmtPrefix
+		return handleInsertStatement(conv, valuesChunk, insertStmtPrefix)
+	}
+
+	// Handle error if it is due to spatial datatype as it is not supported by Pingcap parser.
+	for _, spatial := range MysqlSpatialDataTypes {
+		if strings.Contains(strings.ToLower(err.Error()), spatial) {
+			if conv.SchemaMode() {
+				conv.Unexpected(fmt.Sprintf("Unsupported datatype '%s' encountered while parsing following statement at line number %d : \n%s", spatial, len(l), chunk))
+				internal.VerbosePrintf("Converting datatype '%s' to 'Text' and retrying to parse the statement\n", spatial)
+			}
+			return handleSpatialDatatype(conv, strings.ToLower(chunk), l)
+		}
+	}
+
+	return nil, false
+}
+
+// handleInsertStatement handles error in parsing the insert statement.
+// Likely causes of failing to parse Insert statement:
+// 	a) Due to some invalid value.
+// 	b) chunk size is more than what pingcap parser could handle (more than 40MB in size).
+// We deal with this cases by extracting all rows and creating
+// extended insert statements. Then we parse one Insert statement
+// at a time, ensuring no size issue and skipping only invalid entries.
+func handleInsertStatement(conv *internal.Conv, chunk, insertStmtPrefix string) ([]ast.StmtNode, bool) {
+	var stmts []ast.StmtNode
+	values := valuesRegexp.FindAllString(chunk, -1)
+
+	if len(values) == 0 {
+		return nil, false
+	}
+	for _, value := range values {
+		chunk = insertStmtPrefix + value + ";"
+		newTree, _, err := parser.New().Parse(chunk, "", "")
+		if err != nil {
+			if conv.SchemaMode() {
+				conv.Unexpected(fmt.Sprintf("Either unsupported value is encountered or syntax is incorrect for following statement : \n%s", chunk))
+			}
+			conv.SkipStatement("*ast.InsertStmt")
+			continue
+		}
+		stmts = append(stmts, newTree[0])
+	}
+	return stmts, true
+}
+
+// handleSpatialDatatype handles error in parsing spatial datatype.
+// We try to replace spatial datatype with 'text' and parse chunk again.
+func handleSpatialDatatype(conv *internal.Conv, chunk string, l [][]byte) ([]ast.StmtNode, bool) {
+	if !conv.SchemaMode() {
+		return nil, true
+	}
+	for _, spatial := range MysqlSpatialDataTypes {
+		chunk = strings.ReplaceAll(chunk, " "+spatial, " text")
+	}
+	newTree, _, err := parser.New().Parse(chunk, "", "")
+	if err != nil {
+		return nil, false
+	}
+	return newTree, true
+}
+
+// getArrayBounds calculate array bound for only set data type
+// and we do not expect multidimensional array.
+func getArrayBounds(ft string, elem []string) []int64 {
+	if strings.HasPrefix(ft, "set") {
+		return []int64{int64(len(elem))}
+	}
+	return nil
+}
+
+func processInsertStmt(conv *internal.Conv, stmt *ast.InsertStmt) {
+	if stmt.Table == nil {
+		logStmtError(conv, stmt, fmt.Errorf("source table is nil"))
+		return
+	}
+	srcTable, err := getTableNameInsert(stmt.Table)
+	if err != nil {
+		logStmtError(conv, stmt, fmt.Errorf("can't get source table name: %w", err))
+		return
+	}
+	if conv.SchemaMode() {
+		conv.Stats.Rows[srcTable] += int64(len(stmt.Lists))
+		return
+	}
+	spTable, err1 := internal.GetSpannerTable(conv, srcTable)
+	if err1 != nil {
+		logStmtError(conv, stmt, fmt.Errorf("can't get spanner table name for source table '%s' : err=%w", srcTable, err1))
+		return
+	}
+	spSchema, ok1 := conv.SpSchema[spTable]
+	srcSchema, ok2 := conv.SrcSchema[srcTable]
+	if !ok1 || !ok2 {
+		conv.Unexpected(fmt.Sprintf("Can't get schemas for table %s", srcTable))
+		conv.Stats.BadRows[srcTable] += conv.Stats.Rows[srcTable]
+		return
+	}
+	srcCols, err2 := getCols(stmt)
+	if err2 != nil {
+		// In MySQL, column names might not be specified in insert statement so instead of
+		// throwing error we will try to retrieve columns from source schema.
+		srcCols = conv.SrcSchema[srcTable].ColNames
+		if len(srcCols) == 0 {
+			conv.Unexpected(fmt.Sprintf("Can't get columns for table %s", srcTable))
+			conv.Stats.BadRows[srcTable] += conv.Stats.Rows[srcTable]
+			return
+		}
+	}
+	spCols, err3 := internal.GetSpannerCols(conv, srcTable, srcCols)
+	if err3 != nil {
+		conv.Unexpected(fmt.Sprintf("Can't get spanner columns for table %s: err=%s", srcTable, err3))
+		conv.Stats.BadRows[srcTable] += conv.Stats.Rows[srcTable]
+		return
+	}
+	var values []string
+	if stmt.Lists == nil {
+		logStmtError(conv, stmt, fmt.Errorf("Can't get column values"))
+		return
+	}
+	for _, row := range stmt.Lists {
+		values, err = getVals(row)
+		ProcessDataRow(conv, srcTable, srcCols, srcSchema, spTable, spCols, spSchema, values)
+	}
+}
+
+func getCols(stmt *ast.InsertStmt) ([]string, error) {
+	if stmt.Columns == nil {
+		return nil, fmt.Errorf("No columns found in insert statement ")
+	}
+	var colnames []string
+	for _, column := range stmt.Columns {
+		colnames = append(colnames, column.OrigColName())
+	}
+	return colnames, nil
+}
+
+func getVals(row []ast.ExprNode) ([]string, error) {
+	if len(row) == 0 {
+		return nil, fmt.Errorf("Found row with zero length")
+	}
+	var values []string
+	for _, item := range row {
+		switch valueNode := item.(type) {
+		case *driver.ValueExpr:
+			values = append(values, fmt.Sprintf("%v", valueNode.GetValue()))
+		default:
+			return nil, fmt.Errorf("unexpected value node %T", valueNode)
+		}
+	}
+	return values, nil
+}
+
+func getTableNameInsert(stmt *ast.TableRefsClause) (string, error) {
+	if stmt.TableRefs == nil {
+		return "", fmt.Errorf("can't build table name as tablerefs is empty")
+	}
+	if stmt.TableRefs.Left == nil {
+		return "", fmt.Errorf("can't build table name as Tablerefs.Left is empty")
+	}
+	if table, ok := stmt.TableRefs.Left.(*ast.TableSource); ok {
+		if tablenode, ok := table.Source.(*ast.TableName); ok {
+			return getTableName(tablenode)
+		}
+		return "", fmt.Errorf("Can't build table name as table source is of different type")
+	}
+	return "", fmt.Errorf("stmt.TableRefs.Left is different type, can't build table name")
+}
+
+func logStmtError(conv *internal.Conv, stmt ast.StmtNode, err error) {
+	conv.Unexpected(fmt.Sprintf("Processing %v statement: %s", reflect.TypeOf(stmt), err))
+	conv.ErrorInStatement(reflect.TypeOf(stmt).String())
+}
+
+// checkEmpty verifies that pkeys is empty and generates a warning if it isn't.
+// MySQL explicitly forbids multiple primary keys.
+func checkEmpty(conv *internal.Conv, pkeys []schema.Key, stmtType string) {
+	if len(pkeys) != 0 {
+		conv.Unexpected(fmt.Sprintf("Multiple primary keys found. `%s` statement is overwriting primary key", stmtType))
+	}
+}
diff --git a/mysql/toddl.go b/mysql/toddl.go
new file mode 100644
index 0000000..7424846
--- /dev/null
+++ b/mysql/toddl.go
@@ -0,0 +1,190 @@
+// Copyright 2020 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package mysql
+
+import (
+	"fmt"
+	"strconv"
+	"unicode"
+
+	"github.com/cloudspannerecosystem/harbourbridge/internal"
+	"github.com/cloudspannerecosystem/harbourbridge/schema"
+	"github.com/cloudspannerecosystem/harbourbridge/spanner/ddl"
+)
+
+// TODO: refactor this file to avoid the duplication with postgres/toddl.go.
+// The core difference between the two files is toSpannerType, which maps
+// type ids (which differ between MySQL and PostgreSQL) to Spanner types.
+
+// 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.
+func schemaToDDL(conv *internal.Conv) error {
+	for _, srcTable := range conv.SrcSchema {
+		spTableName, err := internal.GetSpannerTable(conv, srcTable.Name)
+		if err != nil {
+			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][]internal.SchemaIssue)
+		// Iterate over columns using ColNames order.
+		for _, srcColName := range srcTable.ColNames {
+			srcCol := srcTable.ColDefs[srcColName]
+			colName, err := internal.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))
+				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, internal.MultiDimensionalArray)
+			}
+			// TODO(hengfeng): add issues for all elements of srcCol.Ignored.
+			if srcCol.Ignored.ForeignKey {
+				issues = append(issues, internal.ForeignKey)
+			}
+			if srcCol.Ignored.Default {
+				issues = append(issues, internal.DefaultValue)
+			}
+			if len(issues) > 0 {
+				conv.Issues[srcTable.Name][srcCol.Name] = issues
+			}
+			spColDef[colName] = ddl.ColumnDef{
+				Name:    colName,
+				T:       ty,
+				IsArray: len(srcCol.Type.ArrayBounds) == 1,
+				NotNull: srcCol.NotNull,
+				Comment: "From: " + quoteIfNeeded(srcCol.Name) + " " + srcCol.Type.Print(),
+			}
+		}
+		comment := "Spanner schema for source table " + quoteIfNeeded(srcTable.Name)
+		conv.SpSchema[spTableName] = ddl.CreateTable{
+			Name:     spTableName,
+			ColNames: spColNames,
+			ColDefs:  spColDef,
+			Pks:      cvtPrimaryKeys(conv, srcTable.Name, srcTable.PrimaryKeys),
+			Comment:  comment}
+	}
+	return nil
+}
+
+// toSpannerType maps a scalar source schema type (defined by id and
+// 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 *internal.Conv, id string, mods []int64) (ddl.ScalarType, []internal.SchemaIssue) {
+	maxExpectedMods := func(n int) {
+		if len(mods) > n {
+			conv.Unexpected(fmt.Sprintf("Found %d mods while processing type id=%s", len(mods), id))
+		}
+	}
+	switch id {
+	case "bool", "boolean":
+		maxExpectedMods(0)
+		return ddl.Bool{}, nil
+	case "tinyint":
+		maxExpectedMods(1)
+		// tinyint(1) is a bool in MySQL
+		if len(mods) > 0 && mods[0] == 1 {
+			return ddl.Bool{}, nil
+		}
+		return ddl.Int64{}, []internal.SchemaIssue{internal.Widened}
+	case "double":
+		maxExpectedMods(2)
+		return ddl.Float64{}, nil
+	case "float":
+		maxExpectedMods(2)
+		return ddl.Float64{}, []internal.SchemaIssue{internal.Widened}
+	case "numeric", "decimal": // Map all numeric and decimal 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{}, []internal.SchemaIssue{internal.DecimalThatFits}
+		}
+		return ddl.Float64{}, []internal.SchemaIssue{internal.Decimal}
+	case "bigint":
+		maxExpectedMods(1)
+		return ddl.Int64{}, nil
+	case "smallint", "mediumint", "integer", "int":
+		maxExpectedMods(1)
+		return ddl.Int64{}, []internal.SchemaIssue{internal.Widened}
+	case "bit":
+		maxExpectedMods(1)
+		return ddl.Bytes{Len: ddl.MaxLength{}}, nil
+	case "varchar", "char":
+		maxExpectedMods(1)
+		if len(mods) > 0 {
+			return ddl.String{Len: ddl.Int64Length{Value: mods[0]}}, nil
+		}
+		return ddl.String{Len: ddl.MaxLength{}}, nil
+	case "text", "tinytext", "mediumtext", "longtext":
+		maxExpectedMods(0)
+		return ddl.String{Len: ddl.MaxLength{}}, nil
+	case "set", "enum":
+		maxExpectedMods(0)
+		return ddl.String{Len: ddl.MaxLength{}}, nil
+	case "json":
+		maxExpectedMods(0)
+		return ddl.String{Len: ddl.MaxLength{}}, nil
+	case "binary", "varbinary":
+		maxExpectedMods(1)
+		return ddl.Bytes{Len: ddl.MaxLength{}}, nil
+	case "tinyblob", "mediumblob", "blob", "longblob":
+		maxExpectedMods(0)
+		return ddl.Bytes{Len: ddl.MaxLength{}}, nil
+	case "date":
+		maxExpectedMods(1)
+		return ddl.Date{}, nil
+	case "datetime":
+		maxExpectedMods(1)
+		return ddl.Timestamp{}, []internal.SchemaIssue{internal.Datetime}
+	case "timestamp":
+		maxExpectedMods(1)
+		return ddl.Timestamp{}, nil
+	case "time", "year":
+		maxExpectedMods(1)
+		return ddl.String{Len: ddl.MaxLength{}}, []internal.SchemaIssue{internal.Time}
+	}
+	return ddl.String{Len: ddl.MaxLength{}}, []internal.SchemaIssue{internal.NoGoodType}
+}
+
+func quoteIfNeeded(s string) string {
+	for _, r := range s {
+		if unicode.IsLetter(r) || unicode.IsDigit(r) || unicode.IsPunct(r) {
+			continue
+		}
+		return strconv.Quote(s)
+	}
+	return s
+}
+
+func cvtPrimaryKeys(conv *internal.Conv, srcTable string, srcKeys []schema.Key) []ddl.IndexKey {
+	var spKeys []ddl.IndexKey
+	for _, k := range srcKeys {
+		spCol, err := internal.GetSpannerCol(conv, srcTable, k.Column, true)
+		if err != nil {
+			conv.Unexpected(fmt.Sprintf("Can't map key for table %s", srcTable))
+			continue
+		}
+		spKeys = append(spKeys, ddl.IndexKey{Col: spCol, Desc: k.Desc})
+	}
+	return spKeys
+}
diff --git a/postgres/infoschema.go b/postgres/infoschema.go
index 3dee9eb..d30b60d 100644
--- a/postgres/infoschema.go
+++ b/postgres/infoschema.go
@@ -23,7 +23,7 @@ import (
 	"time"
 
 	"cloud.google.com/go/civil"
-	_ "github.com/lib/pq"
+	_ "github.com/lib/pq" // we will use database/sql package instead of using this package directly
 
 	"github.com/cloudspannerecosystem/harbourbridge/internal"
 	"github.com/cloudspannerecosystem/harbourbridge/schema"
@@ -54,7 +54,7 @@ func ProcessInfoSchema(conv *internal.Conv, db *sql.DB) error {
 	return nil
 }
 
-// ProcessSqlData performs data conversion for source database
+// ProcessSQLData performs data conversion for source database
 // 'db'. For each table, we extract data using a "SELECT *" query,
 // convert the data to Spanner data (based on the source and Spanner
 // schemas), and write it to Spanner.  If we can't get/process data
@@ -73,7 +73,7 @@ func ProcessInfoSchema(conv *internal.Conv, db *sql.DB) error {
 // We choose to do all type conversions explicitly ourselves so that
 // we can generate more targeted error messages: hence we pass
 // *interface{} parameters to row.Scan.
-func ProcessSqlData(conv *internal.Conv, db *sql.DB) {
+func ProcessSQLData(conv *internal.Conv, db *sql.DB) {
 	// TODO: refactor to use the set of tables computed by
 	// ProcessInfoSchema instead of computing them again.
 	tables, err := getTables(db)
@@ -113,7 +113,7 @@ func ProcessSqlData(conv *internal.Conv, db *sql.DB) {
 				conv.StatsAddBadRow(srcTable, conv.DataMode())
 				continue
 			}
-			cvtCols, cvtVals, err := ConvertSqlRow(conv, srcTable, srcCols, srcSchema, spTable, spCols, spSchema, v)
+			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())
@@ -125,13 +125,13 @@ func ProcessSqlData(conv *internal.Conv, db *sql.DB) {
 	}
 }
 
-// ConvertSqlRow performs data conversion for a single row of data
-// returned from a 'SELECT *' query. ConvertSqlRow assumes that
+// ConvertSQLRow performs data conversion for a single row of data
+// returned from a 'SELECT *' query. ConvertSQLRow assumes that
 // srcCols, spCols and srcVals all have the same length. Note that
-// ConvertSqlRow returns cols as well as converted values. This is
+// ConvertSQLRow returns cols as well as converted values. This is
 // because cols can change when we add a column (synthetic primary
 // key) or because we drop columns (handling of NULL values).
-func ConvertSqlRow(conv *internal.Conv, srcTable string, srcCols []string, srcSchema schema.Table, spTable string, spCols []string, spSchema ddl.CreateTable, srcVals []interface{}) ([]string, []interface{}, error) {
+func ConvertSQLRow(conv *internal.Conv, srcTable string, srcCols []string, srcSchema schema.Table, spTable string, spCols []string, spSchema ddl.CreateTable, srcVals []interface{}) ([]string, []interface{}, error) {
 	var vs []interface{}
 	var cs []string
 	for i := range srcCols {
@@ -146,9 +146,9 @@ func ConvertSqlRow(conv *internal.Conv, srcTable string, srcCols []string, srcSc
 		var spVal interface{}
 		var err error
 		if spCd.IsArray {
-			spVal, err = cvtSqlArray(conv, srcCd, spCd, srcVals[i])
+			spVal, err = cvtSQLArray(conv, srcCd, spCd, srcVals[i])
 		} else {
-			spVal, err = cvtSqlScalar(conv, srcCd, spCd, srcVals[i])
+			spVal, err = cvtSQLScalar(conv, srcCd, spCd, srcVals[i])
 		}
 		if err != nil { // Skip entire row if we hit error.
 			return nil, nil, fmt.Errorf("can't convert sql data for column %s of table %s: %w", srcCols[i], srcTable, err)
@@ -212,7 +212,7 @@ func getTables(db *sql.DB) ([]schemaAndName, error) {
 	q := "SELECT table_schema, table_name FROM information_schema.tables where table_type = 'BASE TABLE'"
 	rows, err := db.Query(q)
 	if err != nil {
-		return nil, fmt.Errorf("couldn't get tables: %w\n", err)
+		return nil, fmt.Errorf("couldn't get tables: %w", err)
 	}
 	defer rows.Close()
 	var tableSchema, tableName string
@@ -229,12 +229,12 @@ func getTables(db *sql.DB) ([]schemaAndName, error) {
 func processTable(conv *internal.Conv, db *sql.DB, table schemaAndName) error {
 	cols, err := getColumns(table, db)
 	if err != nil {
-		return fmt.Errorf("couldn't get schema for table %s.%s: %s\n", table.schema, table.name, err)
+		return fmt.Errorf("couldn't get schema for table %s.%s: %s", table.schema, table.name, err)
 	}
 	defer cols.Close()
 	primaryKeys, constraints, err := getConstraints(conv, db, table)
 	if err != nil {
-		return fmt.Errorf("couldn't get constraints for table %s.%s: %s\n", table.schema, table.name, err)
+		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)
@@ -365,7 +365,7 @@ func toNotNull(conv *internal.Conv, isNullable string) bool {
 	return false
 }
 
-func cvtSqlArray(conv *internal.Conv, srcCd schema.Column, spCd ddl.ColumnDef, val interface{}) (interface{}, error) {
+func cvtSQLArray(conv *internal.Conv, srcCd schema.Column, spCd ddl.ColumnDef, val interface{}) (interface{}, error) {
 	a, ok := val.([]byte)
 	if !ok {
 		return nil, fmt.Errorf("can't convert array values to []byte")
@@ -373,7 +373,7 @@ func cvtSqlArray(conv *internal.Conv, srcCd schema.Column, spCd ddl.ColumnDef, v
 	return convArray(spCd.T, srcCd.Type.Name, conv.Location, string(a))
 }
 
-// cvtSqlScalar converts a values returned from a SQL query to a
+// cvtSQLScalar converts a values returned from a SQL query to a
 // Spanner value.  In principle, we could just hand the values we get
 // from the driver over to Spanner and have the Spanner client handle
 // conversions and errors. However we handle the conversions
@@ -387,7 +387,7 @@ func cvtSqlArray(conv *internal.Conv, srcCd schema.Column, spCd ddl.ColumnDef, v
 //    float64
 //    string
 //    time.Time
-func cvtSqlScalar(conv *internal.Conv, srcCd schema.Column, spCd ddl.ColumnDef, val interface{}) (interface{}, error) {
+func cvtSQLScalar(conv *internal.Conv, srcCd schema.Column, spCd ddl.ColumnDef, val interface{}) (interface{}, error) {
 	switch spCd.T.(type) {
 	case ddl.Bool:
 		switch v := val.(type) {
diff --git a/postgres/pgdump.go b/postgres/pgdump.go
index 623070c..c212fda 100644
--- a/postgres/pgdump.go
+++ b/postgres/pgdump.go
@@ -501,12 +501,12 @@ func analyzeColDefConstraints(conv *internal.Conv, n nodes.Node, table string, l
 
 // updateSchema updates the schema for table based on the given constraints.
 // 's' is the statement type being processed, and is used for debug messages.
-func updateSchema(conv *internal.Conv, table string, cs []constraint, s string) {
+func updateSchema(conv *internal.Conv, table string, cs []constraint, stmtType string) {
 	for _, c := range cs {
 		switch c.ct {
 		case nodes.CONSTR_PRIMARY:
 			ct := conv.SrcSchema[table]
-			checkEmpty(conv, ct.PrimaryKeys, s)
+			checkEmpty(conv, ct.PrimaryKeys, stmtType)
 			ct.PrimaryKeys = toSchemaKeys(conv, table, c.cols) // Drop any previous primary keys.
 			// In Spanner, primary key columns are usually annotated with NOT NULL,
 			// but this can be omitted to allow NULL values in key columns.
@@ -609,14 +609,14 @@ func getString(node nodes.Node) (string, error) {
 
 // checkEmpty verifies that pkeys is empty and generates a warning if it isn't.
 // PostgreSQL explicitly forbids multiple primary keys.
-func checkEmpty(conv *internal.Conv, pkeys []schema.Key, s string) {
+func checkEmpty(conv *internal.Conv, pkeys []schema.Key, stmtType 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", stmtType))
 	}
 }
 
+// PrNodeType strips off "pg_query." prefix from nodes.Nodes type.
 func PrNodeType(n nodes.Node) string {
-	// Strip off "pg_query." prefix from nodes.Nodes type.
 	return strings.TrimPrefix(reflect.TypeOf(n).Name(), "pg_query.")
 }
 
diff --git a/schema/schema.go b/schema/schema.go
index b6dd7cc..0997813 100644
--- a/schema/schema.go
+++ b/schema/schema.go
@@ -80,6 +80,7 @@ type Ignored struct {
 	ForeignKey bool
 }
 
+// Print converts ty to a string suitable for printing.
 func (ty Type) Print() string {
 	s := ty.Name
 	if len(ty.Mods) > 0 {
diff --git a/spanner/batchwriter.go b/spanner/batchwriter.go
index 1080bf5..b27030b 100644
--- a/spanner/batchwriter.go
+++ b/spanner/batchwriter.go
@@ -191,7 +191,7 @@ func (bw *BatchWriter) getBadRowsForTest() []*row {
 // getBatch returns a slice of data from the front of bw.rows.  The slice
 // returned is the largest one not exceeding countThreshold and byteThreshold.
 func (bw *BatchWriter) getBatch() (rows []*row, count int64, bytes int64) {
-	for i, _ := range bw.rows {
+	for i := range bw.rows {
 		c := count + int64(len(bw.rows[i].cols))
 		b := bytes + byteSize(bw.rows[i])
 		// If next row puts us over the thresholds, then stop. But make sure


Test patch:
diff --git a/integration_test.go b/integration_test.go
index 235d146..3c5a71f 100644
--- a/integration_test.go
+++ b/integration_test.go
@@ -122,9 +122,8 @@ func TestIntegration_SimpleUse(t *testing.T) {
 	defer os.RemoveAll(tmpdir)
 
 	now := time.Now()
-	dbName, _ := getDatabaseName(now)
+	dbName, _ := getDatabaseName("pgdump", now)
 	dbPath := fmt.Sprintf("projects/%s/instances/%s/databases/%s", projectID, instanceID, dbName)
-
 	dataFilepath := "test_data/pg_dump.test.out"
 	filePrefix = filepath.Join(tmpdir, dbName+".")
 	f, err := os.Open(dataFilepath)
@@ -148,7 +147,7 @@ func TestIntegration_Command(t *testing.T) {
 	defer os.RemoveAll(tmpdir)
 
 	now := time.Now()
-	dbName, _ := getDatabaseName(now)
+	dbName, _ := getDatabaseName("pgdump", now)
 	dbPath := fmt.Sprintf("projects/%s/instances/%s/databases/%s", projectID, instanceID, dbName)
 
 	dataFilepath := "test_data/pg_dump.test.out"
diff --git a/mysql/data_test.go b/mysql/data_test.go
new file mode 100644
index 0000000..dd95dd5
--- /dev/null
+++ b/mysql/data_test.go
@@ -0,0 +1,343 @@
+// Copyright 2020 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package mysql
+
+import (
+	"fmt"
+	"math/bits"
+	"testing"
+	"time"
+
+	"cloud.google.com/go/civil"
+	"cloud.google.com/go/spanner"
+	"github.com/stretchr/testify/assert"
+
+	"github.com/cloudspannerecosystem/harbourbridge/internal"
+	"github.com/cloudspannerecosystem/harbourbridge/schema"
+	"github.com/cloudspannerecosystem/harbourbridge/spanner/ddl"
+)
+
+type spannerData struct {
+	table string
+	cols  []string
+	vals  []interface{}
+}
+
+// Basic smoke test of ProcessDataRow. The core part of this code path
+// (ConvertData) is tested in TestConvertData.
+func TestProcessDataRow(t *testing.T) {
+	tableName := "testtable"
+	cols := []string{"a", "b", "c"}
+	conv := buildConv(
+		ddl.CreateTable{
+			Name:     tableName,
+			ColNames: cols,
+			ColDefs: map[string]ddl.ColumnDef{
+				"a": ddl.ColumnDef{Name: "a", T: ddl.Float64{}},
+				"b": ddl.ColumnDef{Name: "b", T: ddl.Int64{}},
+				"c": ddl.ColumnDef{Name: "c", T: ddl.String{Len: ddl.MaxLength{}}},
+			}},
+		schema.Table{
+			Name:     tableName,
+			ColNames: cols,
+			ColDefs: map[string]schema.Column{
+				"a": schema.Column{Name: "a", Type: schema.Type{Name: "float"}},
+				"b": schema.Column{Name: "b", Type: schema.Type{Name: "int"}},
+				"c": schema.Column{Name: "c", Type: schema.Type{Name: "text"}},
+			}})
+	conv.SetDataMode()
+	var rows []spannerData
+	conv.SetDataSink(func(table string, cols []string, vals []interface{}) {
+		rows = append(rows, spannerData{table: table, cols: cols, vals: vals})
+	})
+	ProcessDataRow(conv, tableName, cols, conv.SrcSchema[tableName], tableName, cols, conv.SpSchema[tableName], []string{"4.2", "6", "prisoner zero"})
+	assert.Equal(t, []spannerData{spannerData{table: tableName, cols: cols, vals: []interface{}{float64(4.2), int64(6), "prisoner zero"}}}, rows)
+}
+
+func TestConvertData(t *testing.T) {
+	singleColTests := []struct {
+		name    string
+		ty      ddl.ScalarType
+		isArray bool
+		srcTy   string      // Source DB type (Used by e.g. timestamp conversions).
+		in      string      // Input value for conversion.
+		e       interface{} // Expected result.
+	}{
+		{"bool", ddl.Bool{}, false, "", "1", true},
+		{"bytes", ddl.Bytes{Len: ddl.MaxLength{}}, false, "", string([]byte{137, 80}), []byte{0x89, 0x50}}, // need some other approach to testblob type
+		{"date", ddl.Date{}, false, "", "2019-10-29", getDate("2019-10-29")},
+		{"float64", ddl.Float64{}, false, "", "42.6", float64(42.6)},
+		{"int64", ddl.Int64{}, false, "", "42", int64(42)},
+		{"string", ddl.String{Len: ddl.MaxLength{}}, false, "", "eh", "eh"},
+		{"datetime", ddl.Timestamp{}, false, "datetime", "2019-10-29 05:30:00", getTimeWithoutTimezone(t, "2019-10-29 05:30:00")},
+		{"timestamp", ddl.Timestamp{}, false, "timestamp", "2019-10-29 05:30:00", getTime(t, "2019-10-29T05:30:00+05:30")},
+		{"string array(set)", ddl.String{Len: ddl.MaxLength{}}, true, "", "1,Travel,3,Dance", []spanner.NullString{
+			spanner.NullString{StringVal: "1", Valid: true},
+			spanner.NullString{StringVal: "Travel", Valid: true},
+			spanner.NullString{StringVal: "3", Valid: true},
+			spanner.NullString{StringVal: "Dance", Valid: true}}},
+	}
+	tableName := "testtable"
+	for _, tc := range singleColTests {
+		col := "a"
+		conv := buildConv(
+			ddl.CreateTable{
+				Name:     tableName,
+				ColNames: []string{col},
+				ColDefs:  map[string]ddl.ColumnDef{col: ddl.ColumnDef{Name: col, T: tc.ty, IsArray: tc.isArray, NotNull: false}},
+				Pks:      []ddl.IndexKey{}},
+			schema.Table{Name: tableName, ColNames: []string{col}, ColDefs: map[string]schema.Column{col: schema.Column{Type: schema.Type{Name: tc.srcTy}}}})
+		conv.TimezoneOffset = "+05:30"
+		t.Run(tc.in, func(t *testing.T) {
+			at, ac, av, err := ConvertData(conv, tableName, []string{col}, conv.SrcSchema[tableName], tableName, []string{col}, conv.SpSchema[tableName], []string{tc.in})
+			checkResults(t, at, ac, av, err, tableName, []string{col}, []interface{}{tc.e}, tc.name)
+		})
+	}
+}
+
+func TestConvertTimestampData(t *testing.T) {
+	timestampTests := []struct {
+		name  string
+		srcTy string
+		in    string
+		e     interface{}
+	}{
+		{"timestampt", "timestamp", "2019-10-29 05:30:00", getTime(t, "2019-10-29T05:30:00+10:00")},
+		{"datetime", "datetime", "2019-10-29 05:30:00", getTimeWithoutTimezone(t, "2019-10-29 05:30:00")},
+	}
+	tableName := "testtable"
+	for _, tc := range timestampTests {
+		col := "a"
+		conv := buildConv(
+			ddl.CreateTable{
+				Name:     tableName,
+				ColNames: []string{col},
+				ColDefs:  map[string]ddl.ColumnDef{col: ddl.ColumnDef{Name: col, T: ddl.Timestamp{}}}},
+			schema.Table{
+				Name:     tableName,
+				ColNames: []string{col},
+				ColDefs:  map[string]schema.Column{col: schema.Column{Type: schema.Type{Name: tc.srcTy}}}})
+		conv.TimezoneOffset = "+10:00" // Set offset so test is robust i.e. doesn't depent on local timezone.
+		t.Run(tc.in, func(t *testing.T) {
+			atable, ac, av, err := ConvertData(conv, tableName, []string{col}, conv.SrcSchema[tableName], tableName, []string{col}, conv.SpSchema[tableName], []string{tc.in})
+			assert.Nil(t, err, tc.name)
+			assert.Equal(t, atable, tableName, tc.name+": table mismatch")
+			assert.Equal(t, []string{col}, ac, tc.name+": column mismatch")
+			assert.Equal(t, 1, len(av))
+			at, ok1 := av[0].(time.Time)
+			et, ok2 := tc.e.(time.Time)
+			assert.True(t, ok1 && ok2, tc.name+": cast to Time failed")
+			assert.True(t, at.Equal(et), tc.name+": value mismatch")
+		})
+	}
+}
+
+func TestConvertMultiColData(t *testing.T) {
+	multiColTests := []struct {
+		name  string
+		cols  []string      // Input columns.
+		vals  []string      // Input values.
+		ecols []string      // Expected columns.
+		evals []interface{} // Expected values.
+	}{
+		{
+			name:  "Cols in order",
+			cols:  []string{"a", "b", "c"},
+			vals:  []string{"6", "6.6", "1"},
+			ecols: []string{"a", "b", "c"},
+			evals: []interface{}{int64(6), float64(6.6), true},
+		},
+		{
+			name:  "Cols out of order",
+			cols:  []string{"b", "c", "a"},
+			vals:  []string{"6.6", "1", "6"},
+			ecols: []string{"b", "c", "a"},
+			evals: []interface{}{float64(6.6), true, int64(6)},
+		},
+		{
+			name:  "Null column",
+			cols:  []string{"a", "b", "c"},
+			vals:  []string{"6", "<nil>", "1"},
+			ecols: []string{"a", "c"},
+			evals: []interface{}{int64(6), true},
+		},
+		{
+			name:  "Missing columns",
+			cols:  []string{"a"},
+			vals:  []string{"6"},
+			ecols: []string{"a"},
+			evals: []interface{}{int64(6)},
+		},
+	}
+	tableName := "testtable"
+	spTable := ddl.CreateTable{
+		Name:     tableName,
+		ColNames: []string{"a", "b", "c"},
+		ColDefs: map[string]ddl.ColumnDef{
+			"a": ddl.ColumnDef{Name: "a", T: ddl.Int64{}},
+			"b": ddl.ColumnDef{Name: "b", T: ddl.Float64{}},
+			"c": ddl.ColumnDef{Name: "c", T: ddl.Bool{}},
+		}}
+	srcTable := schema.Table{
+		Name:     tableName,
+		ColNames: []string{"a", "b", "c"},
+		ColDefs: map[string]schema.Column{
+			"a": schema.Column{Type: schema.Type{Name: "int"}},
+			"b": schema.Column{Type: schema.Type{Name: "float"}},
+			"c": schema.Column{Type: schema.Type{Name: "bool"}},
+		}}
+	for _, tc := range multiColTests {
+		t.Run(tc.name, func(t *testing.T) {
+			conv := buildConv(spTable, srcTable)
+			atable, acols, avals, err := ConvertData(conv, srcTable.Name, tc.cols, conv.SrcSchema[tableName], spTable.Name, tc.cols, conv.SpSchema[tableName], tc.vals)
+			checkResults(t, atable, acols, avals, err, tableName, tc.ecols, tc.evals, tc.name)
+		})
+	}
+}
+
+func TestConvertError(t *testing.T) {
+	errorTests := []struct {
+		name string
+		cols []string // Input columns.
+		vals []string // Input values.
+	}{
+		{
+			name: "Error in int64",
+			cols: []string{"a", "b", "c"},
+			vals: []string{" 6", "6.6", "true"},
+		},
+		{
+			name: "Error in float64",
+			cols: []string{"a", "b", "c"},
+			vals: []string{"6", "6.6e", "true"},
+		},
+		{
+			name: "Error in bool",
+			cols: []string{"a", "b", "c"},
+			vals: []string{"6", "6.6", "truee"},
+		},
+	}
+	tableName := "testtable"
+	spTable := ddl.CreateTable{
+		Name:     tableName,
+		ColNames: []string{"a", "b", "c"},
+		ColDefs: map[string]ddl.ColumnDef{
+			"a": ddl.ColumnDef{Name: "a", T: ddl.Int64{}},
+			"b": ddl.ColumnDef{Name: "b", T: ddl.Float64{}},
+			"c": ddl.ColumnDef{Name: "c", T: ddl.Bool{}},
+		}}
+	srcTable := schema.Table{
+		Name:     tableName,
+		ColNames: []string{"a", "b", "c"},
+		ColDefs: map[string]schema.Column{
+			"a": schema.Column{Type: schema.Type{Name: "int"}},
+			"b": schema.Column{Type: schema.Type{Name: "float"}},
+			"c": schema.Column{Type: schema.Type{Name: "bool"}},
+		}}
+	for _, tc := range errorTests {
+		t.Run(tc.name, func(t *testing.T) {
+			conv := buildConv(spTable, srcTable)
+			_, _, _, err := ConvertData(conv, srcTable.Name, tc.cols, conv.SrcSchema[tableName], spTable.Name, tc.cols, conv.SpSchema[tableName], tc.vals)
+			assert.NotNil(t, err, tc.name)
+		})
+	}
+}
+
+func TestConvertsyntheticPKey(t *testing.T) {
+	syntheticPKeyTests := []struct {
+		name  string
+		cols  []string      // Input columns.
+		vals  []string      // Input values.
+		ecols []string      // Expected columns.
+		evals []interface{} // Expected values.
+	}{
+		{
+			name:  "Sequence 0",
+			cols:  []string{"a", "b", "c"},
+			vals:  []string{"6", "6.6", "true"},
+			ecols: []string{"a", "b", "c", "synth_id"},
+			evals: []interface{}{int64(6), float64(6.6), true, int64(0)},
+		},
+		{
+			name:  "Sequence 1",
+			cols:  []string{"a"},
+			vals:  []string{"7"},
+			ecols: []string{"a", "synth_id"},
+			evals: []interface{}{int64(7), int64(bits.Reverse64(1))},
+		},
+	}
+	tableName := "testtable"
+	spTable := ddl.CreateTable{
+		Name:     tableName,
+		ColNames: []string{"a", "b", "c"},
+		ColDefs: map[string]ddl.ColumnDef{
+			"a": ddl.ColumnDef{Name: "a", T: ddl.Int64{}},
+			"b": ddl.ColumnDef{Name: "b", T: ddl.Float64{}},
+			"c": ddl.ColumnDef{Name: "c", T: ddl.Bool{}},
+		}}
+	srcTable := schema.Table{
+		Name:     tableName,
+		ColNames: []string{"a", "b", "c"},
+		ColDefs: map[string]schema.Column{
+			"a": schema.Column{Type: schema.Type{Name: "int"}},
+			"b": schema.Column{Type: schema.Type{Name: "float"}},
+			"c": schema.Column{Type: schema.Type{Name: "bool"}},
+		}}
+	conv := buildConv(spTable, srcTable)
+	conv.SyntheticPKeys[spTable.Name] = internal.SyntheticPKey{Col: "synth_id", Sequence: 0}
+	for _, tc := range syntheticPKeyTests {
+		t.Run(tc.name, func(t *testing.T) {
+			atable, acols, avals, err := ConvertData(conv, srcTable.Name, tc.cols, conv.SrcSchema[tableName], spTable.Name, tc.cols, conv.SpSchema[tableName], tc.vals)
+			checkResults(t, atable, acols, avals, err, tableName, tc.ecols, tc.evals, tc.name)
+		})
+	}
+}
+
+func buildConv(spTable ddl.CreateTable, srcTable schema.Table) *internal.Conv {
+	conv := internal.MakeConv()
+	conv.SpSchema[spTable.Name] = spTable
+	conv.SrcSchema[srcTable.Name] = srcTable
+	conv.ToSource[spTable.Name] = internal.NameAndCols{Name: srcTable.Name, Cols: make(map[string]string)}
+	conv.ToSpanner[srcTable.Name] = internal.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]
+	}
+	return conv
+}
+
+func checkResults(t *testing.T, atable string, acols []string, avals []interface{}, err error, etable string, ecols []string, evals []interface{}, name string) {
+	assert.Nil(t, err, name)
+	assert.Equal(t, atable, etable, name+": table mismatch")
+	assert.Equal(t, ecols, acols, name+": column mismatch")
+	assert.Equal(t, evals, avals, name+": value mismatch")
+}
+
+func getTime(t *testing.T, s string) time.Time {
+	x, err := time.Parse(time.RFC3339, s)
+	assert.Nil(t, err, fmt.Sprintf("getTime can't parse %s:", s))
+	return x
+}
+
+func getTimeWithoutTimezone(t *testing.T, s string) time.Time {
+	x, err := time.Parse("2006-01-02 15:04:05", s)
+	assert.Nil(t, err, fmt.Sprintf("getTime can't parse %s:", s))
+	return x
+}
+
+func getDate(s string) civil.Date {
+	d, _ := civil.ParseDate(s)
+	return d
+}
diff --git a/mysql/mysqldump_test.go b/mysql/mysqldump_test.go
new file mode 100644
index 0000000..10de571
--- /dev/null
+++ b/mysql/mysqldump_test.go
@@ -0,0 +1,577 @@
+// Copyright 2020 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package mysql
+
+import (
+	"bufio"
+	"fmt"
+	"math/bits"
+	"strings"
+	"testing"
+	"time"
+
+	"cloud.google.com/go/spanner"
+	"github.com/cloudspannerecosystem/harbourbridge/internal"
+	"github.com/cloudspannerecosystem/harbourbridge/spanner/ddl"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestProcessMySQLDump_Scalar(t *testing.T) {
+	// First, test scalar types.
+	scalarTests := []struct {
+		ty       string
+		expected ddl.ScalarType
+	}{
+		{"bigint", ddl.Int64{}},
+		{"bool", ddl.Bool{}},
+		{"boolean", ddl.Bool{}},
+		{"tinyint(1)", ddl.Bool{}},
+		{"tinyint(4)", ddl.Int64{}},
+		{"blob", ddl.Bytes{Len: ddl.MaxLength{}}},
+		{"char(42)", ddl.String{Len: ddl.Int64Length{Value: 42}}},
+		{"date", ddl.Date{}},
+		{"decimal(4,10)", ddl.Float64{}},
+		{"double(4,10)", ddl.Float64{}},
+		{"float(4,10)", ddl.Float64{}},
+		{"integer", ddl.Int64{}},
+		{"mediumint", ddl.Int64{}},
+		{"int", ddl.Int64{}},
+		{"bit", ddl.Bytes{Len: ddl.MaxLength{}}},
+		{"smallint", ddl.Int64{}},
+		{"text", ddl.String{Len: ddl.MaxLength{}}},
+		{"tinytext", ddl.String{Len: ddl.MaxLength{}}},
+		{"mediumtext", ddl.String{Len: ddl.MaxLength{}}},
+		{"longtext", ddl.String{Len: ddl.MaxLength{}}},
+		{"enum('a','b')", ddl.String{Len: ddl.MaxLength{}}},
+		{"timestamp", ddl.Timestamp{}},
+		{"datetime", ddl.Timestamp{}},
+		{"varchar(42)", ddl.String{Len: ddl.Int64Length{Value: 42}}},
+	}
+	for _, tc := range scalarTests {
+		t.Run(tc.ty, func(t *testing.T) {
+			conv, _ := runProcessMySQLDump(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)
+		})
+	}
+}
+
+func TestProcessMySQLDump_SingleCol(t *testing.T) {
+	// Test array types and not null.
+	singleColTests := []struct {
+		ty       string
+		expected ddl.ColumnDef
+	}{
+		{"set('a','b','c')", ddl.ColumnDef{Name: "a", T: ddl.String{Len: ddl.MaxLength{}}, IsArray: true}},
+		{"text NOT NULL", ddl.ColumnDef{Name: "a", T: ddl.String{Len: ddl.MaxLength{}}, NotNull: true}},
+	}
+	for _, tc := range singleColTests {
+		t.Run(tc.ty, func(t *testing.T) {
+			conv, _ := runProcessMySQLDump(fmt.Sprintf("CREATE TABLE t (a %s);", tc.ty))
+			noIssues(conv, t, "Not null: "+tc.ty)
+			cd := conv.SpSchema["t"].ColDefs["a"]
+			cd.Comment = ""
+			assert.Equal(t, tc.expected, cd, "Not null: "+tc.ty)
+		})
+	}
+}
+
+func TestProcessMySQLDump_MultiCol(t *testing.T) {
+	// Next test more general cases: multi-column schemas and data conversion.
+	multiColTests := []struct {
+		name           string
+		input          string
+		expectedSchema map[string]ddl.CreateTable
+		expectedData   []spannerData
+		expectIssues   bool // True if we expect to encounter issues during conversion.
+	}{
+		{
+			name: "Shopping cart",
+			input: "CREATE TABLE cart (productid text, userid text, quantity bigint);\n" +
+				"ALTER TABLE cart ADD CONSTRAINT cart_pkey PRIMARY KEY (productid, userid);\n",
+			expectedSchema: map[string]ddl.CreateTable{
+				"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},
+						"userid":    ddl.ColumnDef{Name: "userid", T: ddl.String{Len: ddl.MaxLength{}}, NotNull: true},
+						"quantity":  ddl.ColumnDef{Name: "quantity", T: ddl.Int64{}},
+					},
+					Pks: []ddl.IndexKey{ddl.IndexKey{Col: "productid"}, ddl.IndexKey{Col: "userid"}}}},
+		},
+		{
+			name:  "Shopping cart with no primary key",
+			input: "CREATE TABLE cart (productid text, userid text NOT NULL, quantity bigint);\n",
+			expectedSchema: map[string]ddl.CreateTable{
+				"cart": ddl.CreateTable{
+					Name:     "cart",
+					ColNames: []string{"productid", "userid", "quantity", "synth_id"},
+					ColDefs: map[string]ddl.ColumnDef{
+						"productid": ddl.ColumnDef{Name: "productid", T: ddl.String{Len: ddl.MaxLength{}}},
+						"userid":    ddl.ColumnDef{Name: "userid", T: ddl.String{Len: ddl.MaxLength{}}, NotNull: true},
+						"quantity":  ddl.ColumnDef{Name: "quantity", T: ddl.Int64{}},
+						"synth_id":  ddl.ColumnDef{Name: "synth_id", T: ddl.Int64{}},
+					},
+					Pks: []ddl.IndexKey{ddl.IndexKey{Col: "synth_id"}}}},
+		},
+		{
+			name:  "Create table with single primary key",
+			input: "CREATE TABLE test (a text PRIMARY KEY, b text);\n",
+			expectedSchema: map[string]ddl.CreateTable{
+				"test": ddl.CreateTable{
+					Name:     "test",
+					ColNames: []string{"a", "b"},
+					ColDefs: map[string]ddl.ColumnDef{
+						"a": ddl.ColumnDef{Name: "a", T: ddl.String{Len: ddl.MaxLength{}}, NotNull: true},
+						"b": ddl.ColumnDef{Name: "b", T: ddl.String{Len: ddl.MaxLength{}}},
+					},
+					Pks: []ddl.IndexKey{ddl.IndexKey{Col: "a"}}}},
+		},
+		{
+			name:  "Create table with multiple primary keys",
+			input: "CREATE TABLE test (a text, b text, n bigint, PRIMARY KEY (a, b) );\n",
+			expectedSchema: map[string]ddl.CreateTable{
+				"test": ddl.CreateTable{
+					Name:     "test",
+					ColNames: []string{"a", "b", "n"},
+					ColDefs: map[string]ddl.ColumnDef{
+						"a": ddl.ColumnDef{Name: "a", T: ddl.String{Len: ddl.MaxLength{}}, NotNull: true},
+						"b": ddl.ColumnDef{Name: "b", T: ddl.String{Len: ddl.MaxLength{}}, NotNull: true},
+						"n": ddl.ColumnDef{Name: "n", T: ddl.Int64{}},
+					},
+					Pks: []ddl.IndexKey{ddl.IndexKey{Col: "a"}, ddl.IndexKey{Col: "b"}}}},
+		},
+		{
+			name:  "Create table with mysql schema",
+			input: "CREATE TABLE myschema.test (a text PRIMARY KEY, b text);\n",
+			expectedSchema: map[string]ddl.CreateTable{
+				"myschema_test": ddl.CreateTable{
+					Name:     "myschema_test",
+					ColNames: []string{"a", "b"},
+					ColDefs: map[string]ddl.ColumnDef{
+						"a": ddl.ColumnDef{Name: "a", T: ddl.String{Len: ddl.MaxLength{}}, NotNull: true},
+						"b": ddl.ColumnDef{Name: "b", T: ddl.String{Len: ddl.MaxLength{}}},
+					},
+					Pks: []ddl.IndexKey{ddl.IndexKey{Col: "a"}}}},
+		},
+		{
+			name: "ALTER TABLE SET NOT NULL",
+			input: "CREATE TABLE test (a text PRIMARY KEY, b text);\n" +
+				"ALTER TABLE test MODIFY b text NOT NULL;\n",
+			expectedSchema: map[string]ddl.CreateTable{
+				"test": ddl.CreateTable{
+					Name:     "test",
+					ColNames: []string{"a", "b"},
+					ColDefs: map[string]ddl.ColumnDef{
+						"a": ddl.ColumnDef{Name: "a", T: ddl.String{Len: ddl.MaxLength{}}, NotNull: true},
+						"b": ddl.ColumnDef{Name: "b", T: ddl.String{Len: ddl.MaxLength{}}, NotNull: true},
+					},
+					Pks: []ddl.IndexKey{ddl.IndexKey{Col: "a"}}}},
+		},
+		{
+			name: "Multiple statements on one line",
+			input: "CREATE TABLE t1 (a text, b text); CREATE TABLE t2 (c text);\n" +
+				"ALTER TABLE t1 ADD CONSTRAINT t1_pkey PRIMARY KEY (a);\n" +
+				"ALTER TABLE t2 ADD CONSTRAINT t2_pkey PRIMARY KEY (c);",
+			expectedSchema: map[string]ddl.CreateTable{
+				"t1": ddl.CreateTable{
+					Name:     "t1",
+					ColNames: []string{"a", "b"},
+					ColDefs: map[string]ddl.ColumnDef{
+						"a": ddl.ColumnDef{Name: "a", T: ddl.String{Len: ddl.MaxLength{}}, NotNull: true},
+						"b": ddl.ColumnDef{Name: "b", T: ddl.String{Len: ddl.MaxLength{}}}},
+					Pks: []ddl.IndexKey{ddl.IndexKey{Col: "a"}}},
+				"t2": ddl.CreateTable{
+					Name:     "t2",
+					ColNames: []string{"c"},
+					ColDefs: map[string]ddl.ColumnDef{
+						"c": ddl.ColumnDef{Name: "c", T: ddl.String{Len: ddl.MaxLength{}}, NotNull: true}},
+					Pks: []ddl.IndexKey{ddl.IndexKey{Col: "c"}}}},
+		},
+		{
+			name: "INSERT statement",
+			input: "CREATE TABLE test (a text, b text, n bigint);\n" +
+				"ALTER TABLE test ADD CONSTRAINT test_pkey PRIMARY KEY (a, b);\n" +
+				"INSERT INTO test (a, b, n) VALUES ('a1','b1',42),\n" +
+				"('a22','b99', 6);",
+			expectedData: []spannerData{
+				spannerData{table: "test", cols: []string{"a", "b", "n"}, vals: []interface{}{"a1", "b1", int64(42)}},
+				spannerData{table: "test", cols: []string{"a", "b", "n"}, vals: []interface{}{"a22", "b99", int64(6)}}},
+		},
+		{
+			name: "INSERT INTO with renamed table/cols",
+			input: "CREATE TABLE _test (_a text, b text, n bigint);\n" +
+				"ALTER TABLE _test ADD CONSTRAINT test_pkey PRIMARY KEY (_a, b);\n" +
+				"INSERT INTO _test (_a, b, n) VALUES ('a1','b1',42),\n" +
+				"('a22','b99', 6);",
+			expectedData: []spannerData{
+				spannerData{table: "Atest", cols: []string{"Aa", "b", "n"}, vals: []interface{}{"a1", "b1", int64(42)}},
+				spannerData{table: "Atest", cols: []string{"Aa", "b", "n"}, vals: []interface{}{"a22", "b99", int64(6)}}},
+		},
+		{
+			name: "INSERT INTO with CRLF",
+			input: "CREATE TABLE test (a text, b text, n bigint);\r\n" +
+				"INSERT INTO test (a, b, n) VALUES \r\n ('a1','b1',42),\r\n" +
+				"('a22','b99', 6);\r\n" +
+				"ALTER TABLE test ADD CONSTRAINT test_pkey PRIMARY KEY (a, b);\r\n",
+			expectedData: []spannerData{
+				spannerData{table: "test", cols: []string{"a", "b", "n"}, vals: []interface{}{"a1", "b1", int64(42)}},
+				spannerData{table: "test", cols: []string{"a", "b", "n"}, vals: []interface{}{"a22", "b99", int64(6)}}},
+		},
+		{
+			name: "INSERT INTO with spaces",
+			input: "CREATE TABLE test (a text NOT NULL, b text NOT NULL, n bigint);\n" +
+				"ALTER TABLE test ADD CONSTRAINT test_pkey PRIMARY KEY (a, b);\n" +
+				"INSERT INTO test (a, b, n) VALUES ('a1 ',' b1',42),\n" +
+				"('a22','b 99 ', 6);",
+			expectedData: []spannerData{
+				spannerData{table: "test", cols: []string{"a", "b", "n"}, vals: []interface{}{"a1 ", " b1", int64(42)}},
+				spannerData{table: "test", cols: []string{"a", "b", "n"}, vals: []interface{}{"a22", "b 99 ", int64(6)}}},
+		},
+		{
+			name: "INSERT INTO with no primary key",
+			input: "CREATE TABLE test (a text, b text, n bigint);\n" +
+				"INSERT INTO test (a, b, n) VALUES\n" +
+				"('a1','b1',42),\n" +
+				"('a22','b99', 6),\n" +
+				"('a33','b',9),\n" +
+				"('a3','b',7);",
+			expectedData: []spannerData{
+				spannerData{table: "test", cols: []string{"a", "b", "n", "synth_id"}, vals: []interface{}{"a1", "b1", int64(42), bitReverse(0)}},
+				spannerData{table: "test", cols: []string{"a", "b", "n", "synth_id"}, vals: []interface{}{"a22", "b99", int64(6), bitReverse(1)}},
+				spannerData{table: "test", cols: []string{"a", "b", "n", "synth_id"}, vals: []interface{}{"a33", "b", int64(9), bitReverse(2)}},
+				spannerData{table: "test", cols: []string{"a", "b", "n", "synth_id"}, vals: []interface{}{"a3", "b", int64(7), bitReverse(3)}}},
+		},
+		{
+			name: "INSERT INTO with empty cols",
+			input: "CREATE TABLE test (a text, b text, n bigint);\n" +
+				"INSERT INTO test (a, b, n) VALUES\n" +
+				"(NULL,'b1',42),\n" +
+				"('a22',NULL,6),\n" +
+				"('a33','b',NULL),\n" +
+				"('a3','b',7);\n",
+			expectedData: []spannerData{
+				spannerData{table: "test", cols: []string{"b", "n", "synth_id"}, vals: []interface{}{"b1", int64(42), bitReverse(0)}},
+				spannerData{table: "test", cols: []string{"a", "n", "synth_id"}, vals: []interface{}{"a22", int64(6), bitReverse(1)}},
+				spannerData{table: "test", cols: []string{"a", "b", "synth_id"}, vals: []interface{}{"a33", "b", bitReverse(2)}},
+				spannerData{table: "test", cols: []string{"a", "b", "n", "synth_id"}, vals: []interface{}{"a3", "b", int64(7), bitReverse(3)}}},
+		},
+		{
+			name: "INSERT",
+			input: "CREATE TABLE test (a text NOT NULL, b text NOT NULL, n bigint);" +
+				"ALTER TABLE test ADD CONSTRAINT test_pkey PRIMARY KEY (a, b);\n" +
+				"INSERT INTO test (a, b, n) VALUES ('a42', 'b6', 2);",
+			expectedData: []spannerData{
+				spannerData{table: "test", cols: []string{"a", "b", "n"}, vals: []interface{}{"a42", "b6", int64(2)}}},
+		},
+		{
+			name: "INSERT with no primary key",
+			input: "CREATE TABLE test (a text NOT NULL, b text NOT NULL, n bigint);\n" +
+				"INSERT INTO test (a, b, n) VALUES ('a42', 'b6', 2);",
+			expectedData: []spannerData{
+				spannerData{table: "test", cols: []string{"a", "b", "n", "synth_id"}, vals: []interface{}{"a42", "b6", int64(2), bitReverse(0)}}},
+		},
+		{
+			name: "INSERT with spaces",
+			input: "CREATE TABLE test (a text NOT NULL, b text NOT NULL, n bigint);\n" +
+				"ALTER TABLE test ADD CONSTRAINT test_pkey PRIMARY KEY (a, b);\n" +
+				"INSERT INTO test (a, b, n) VALUES (' a42 ', '\nb6\n', 2);\n",
+			expectedData: []spannerData{
+				spannerData{table: "test", cols: []string{"a", "b", "n"}, vals: []interface{}{" a42 ", "\nb6\n", int64(2)}}},
+		},
+		{
+			name: "Statements with embedded semicolons",
+			input: "CREATE TABLE test (a text NOT NULL, b text NOT NULL, n bigint);\n" +
+				"ALTER TABLE test ADD CONSTRAINT test_pkey PRIMARY KEY (a, b);\n" +
+				`INSERT INTO test (a, b, n) VALUES ('a;\n2', 'b;6\n', 2);` + "\n",
+			expectedData: []spannerData{
+				spannerData{table: "test", cols: []string{"a", "b", "n"}, vals: []interface{}{"a;\n2", "b;6\n", int64(2)}}},
+		},
+		{
+			name: "Tables and columns with illegal characters",
+			input: "CREATE TABLE `te.s t` (`a?^` text PRIMARY KEY, `b.b` text, `n*n` bigint);\n" +
+				"INSERT INTO `te.s t` (`a?^`, `b.b`, `n*n`) VALUES ('a', 'b', 2);",
+			expectedData: []spannerData{
+				spannerData{table: "te_s_t", cols: []string{"a__", "b_b", "n_n"}, vals: []interface{}{"a", "b", int64(2)}}},
+			expectedSchema: map[string]ddl.CreateTable{
+				"te_s_t": ddl.CreateTable{
+					Name:     "te_s_t",
+					ColNames: []string{"a__", "b_b", "n_n"},
+					ColDefs: map[string]ddl.ColumnDef{
+						"a__": ddl.ColumnDef{Name: "a__", T: ddl.String{Len: ddl.MaxLength{}}, NotNull: true},
+						"b_b": ddl.ColumnDef{Name: "b_b", T: ddl.String{Len: ddl.MaxLength{}}},
+						"n_n": ddl.ColumnDef{Name: "n_n", T: ddl.Int64{}},
+					},
+					Pks: []ddl.IndexKey{ddl.IndexKey{Col: "a__"}}}},
+		},
+		{
+			name: "Tables and columns with illegal characters: CREATE-ALTER",
+			input: "CREATE TABLE `te.s t` (`a?^` text, `b.b` text, `n*n` bigint);\n" +
+				"ALTER TABLE `te.s t` ADD CONSTRAINT test_pkey PRIMARY KEY (`a?^`);\n" +
+				"INSERT INTO `te.s t` (`a?^`, `b.b`, `n*n`) VALUES ('a', 'b', 2);",
+			expectedData: []spannerData{
+				spannerData{table: "te_s_t", cols: []string{"a__", "b_b", "n_n"}, vals: []interface{}{"a", "b", int64(2)}}},
+			expectedSchema: map[string]ddl.CreateTable{
+				"te_s_t": ddl.CreateTable{
+					Name:     "te_s_t",
+					ColNames: []string{"a__", "b_b", "n_n"},
+					ColDefs: map[string]ddl.ColumnDef{
+						"a__": ddl.ColumnDef{Name: "a__", T: ddl.String{Len: ddl.MaxLength{}}, NotNull: true},
+						"b_b": ddl.ColumnDef{Name: "b_b", T: ddl.String{Len: ddl.MaxLength{}}},
+						"n_n": ddl.ColumnDef{Name: "n_n", T: ddl.Int64{}},
+					},
+					Pks: []ddl.IndexKey{ddl.IndexKey{Col: "a__"}}}},
+		},
+		// The "Data conversion: ..." cases check data conversion for each type.
+		{
+			name: "Data conversion: bool, bigint, char, blob",
+			input: `
+	CREATE TABLE test (id integer PRIMARY KEY, a bool, b bigint, c char(1),d blob);
+	INSERT INTO test (id, a, b, c, d) VALUES (1, 1, 42, 'x',_binary '` + string([]byte{137, 80}) + `');`,
+			expectedData: []spannerData{
+				spannerData{table: "test", cols: []string{"id", "a", "b", "c", "d"}, vals: []interface{}{int64(1), true, int64(42), "x", []byte{0x89, 0x50}}}},
+		},
+		{
+			name: "Data conversion: date, float, decimal, mediumint",
+			input: `
+	CREATE TABLE test (id integer PRIMARY KEY, a date, b float, c decimal(3,5));
+	INSERT INTO test (id, a, b, c) VALUES (1,'2019-10-29',4.444,5.44444);
+	`,
+			expectedData: []spannerData{
+				spannerData{table: "test", cols: []string{"id", "a", "b", "c"}, vals: []interface{}{int64(1), getDate("2019-10-29"), float64(4.444), float64(5.44444)}}},
+		},
+		{
+			name: "Data conversion: smallint, mediumint, bigint, double",
+			input: `
+	CREATE TABLE test (id integer PRIMARY KEY, a smallint, b mediumint, c bigint, d double);
+	INSERT INTO test (id, a, b, c, d) VALUES (1, 88, 44, 22, 444.9876);
+	`,
+			expectedData: []spannerData{
+				spannerData{table: "test", cols: []string{"id", "a", "b", "c", "d"}, vals: []interface{}{int64(1), int64(88), int64(44), int64(22), float64(444.9876)}}},
+		},
+		// test with different timezone
+		{
+			name: "Data conversion:  text, timestamp, datetime, varchar",
+			input: `
+	SET TIME_ZONE='+02:30';
+	CREATE TABLE test (id integer PRIMARY KEY, a text, b timestamp, c datetime, d varchar(15));
+	INSERT INTO test (id, a, b, c, d) VALUES (1, 'my text', '2019-10-29 05:30:00', '2019-10-29 05:30:00', 'my varchar');
+	`,
+			expectedData: []spannerData{
+				spannerData{table: "test", cols: []string{"id", "a", "b", "c", "d"}, vals: []interface{}{int64(1), "my text", getTime(t, "2019-10-29T05:30:00+02:30"), getTimeWithoutTimezone(t, "2019-10-29 05:30:00"), "my varchar"}}},
+		},
+	}
+	for _, tc := range multiColTests {
+		t.Run(tc.name, func(t *testing.T) {
+			conv, rows := runProcessMySQLDump(tc.input)
+			if !tc.expectIssues {
+				noIssues(conv, t, tc.name)
+			}
+			if tc.expectedSchema != nil {
+				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")
+			}
+		})
+	}
+}
+
+func TestProcessMySQLDump_TimeZone(t *testing.T) {
+	// Test set timezone statement.
+	conv, _ := runProcessMySQLDump("SET TIME_ZONE='+02:30';")
+	assert.Equal(t, conv.TimezoneOffset, "+02:30", "Set timezone")
+}
+
+func TestProcessMySQLDump_DataError(t *testing.T) {
+	// Finally test data conversion errors.
+	dataErrorTests := []struct {
+		name         string
+		input        string
+		expectedData []spannerData
+	}{
+		{
+			// Test bad data for each scalar type (except text, which accepts all values) and an array type.
+			name: "Data conversion errors",
+			input: "CREATE TABLE test (a int, b float, c bool, d date, e blob, f set('42','6'));\n" +
+				`INSERT INTO test (a, b, c, d, e, f) VALUES (7,42.1,1,'2019-10-29',_binary '` + string([]byte{137, 80}) + `','42,6');` + // Baseline (good)
+				"INSERT INTO test (a, b, c, d, e, f) VALUES (7,NULL,NULL,NULL,NULL,NULL);\n" + // Good
+				"INSERT INTO test (a, b, c, d, e, f) VALUES (7.1,NULL,NULL,NULL,NULL,NULL);\n" + // Error
+				"INSERT INTO test (a, b, c, d, e, f) VALUES (NULL,42.1,NULL,NULL,NULL,NULL);\n" + // Good
+				"INSERT INTO test (a, b, c, d, e, f) VALUES (NULL,'42-1',NULL,NULL,NULL,NULL);\n" + // Error
+				"INSERT INTO test (a, b, c, d, e, f) VALUES (NULL,NULL,true,NULL,NULL,NULL);\n" + // Good
+				"INSERT INTO test (a, b, c, d, e, f) VALUES (NULL,NULL,'truefalse',NULL,NULL,NULL);\n" + // Error
+				"INSERT INTO test (a, b, c, d, e, f) VALUES (NULL,NULL,NULL,'2019-10-29',NULL,NULL);\n" + // Good
+				"INSERT INTO test (a, b, c, d, e, f) VALUES (NULL,NULL,NULL,'2019-10-42',NULL,NULL);\n" + // Error
+				`INSERT INTO test (a, b, c, d, e, f) VALUES (NULL,NULL,NULL,NULL,_binary '` + string([]byte{137, 80}) + `',NULL);` + // Good
+				"INSERT INTO test (a, b, c, d, e, f) VALUES (NULL,NULL,NULL,NULL,NULL,'42,6');\n" + // Good
+				"INSERT INTO test (a, b, c, d, e, f) VALUES (NULL,NULL,NULL,NULL,NULL,42,6);\n", // Error
+			expectedData: []spannerData{
+				spannerData{
+					table: "test", cols: []string{"a", "b", "c", "d", "e", "f", "synth_id"},
+					vals: []interface{}{int64(7), float64(42.1), true,
+						getDate("2019-10-29"), []byte{0x89, 0x50},
+						[]spanner.NullString{{StringVal: "42", Valid: true}, {StringVal: "6", Valid: true}},
+						bitReverse(0)}},
+				spannerData{table: "test", cols: []string{"a", "synth_id"}, vals: []interface{}{int64(7), bitReverse(1)}},
+				spannerData{table: "test", cols: []string{"b", "synth_id"}, vals: []interface{}{float64(42.1), bitReverse(2)}},
+				spannerData{table: "test", cols: []string{"c", "synth_id"}, vals: []interface{}{true, bitReverse(3)}},
+				spannerData{table: "test", cols: []string{"d", "synth_id"}, vals: []interface{}{getDate("2019-10-29"), bitReverse(4)}},
+				spannerData{table: "test", cols: []string{"e", "synth_id"}, vals: []interface{}{[]byte{0x89, 0x50}, bitReverse(5)}},
+				spannerData{table: "test", cols: []string{"f", "synth_id"},
+					vals: []interface{}{[]spanner.NullString{{StringVal: "42", Valid: true}, {StringVal: "6", Valid: true}}, bitReverse(6)}},
+			},
+		},
+	}
+	for _, tc := range dataErrorTests {
+		conv, rows := runProcessMySQLDump(tc.input)
+		assert.Equal(t, tc.expectedData, rows, tc.name+": Data rows did not match")
+		assert.Equal(t, conv.BadRows(), int64(5), tc.name+": Error count did not match")
+	}
+}
+
+// The following test Conv API calls based on data generated by ProcessMySQLDump.
+func TestProcessMySQLDump_GetDDL(t *testing.T) {
+	conv, _ := runProcessMySQLDump("CREATE TABLE cart (productid text, userid text, quantity bigint);\n" +
+		"ALTER TABLE cart ADD CONSTRAINT cart_pkey PRIMARY KEY (productid, userid);")
+	expected := "CREATE TABLE cart (\n" +
+		"productid STRING(MAX) NOT NULL,\n" +
+		"userid STRING(MAX) NOT NULL,\n" +
+		"quantity INT64\n" +
+		") PRIMARY KEY (productid, userid)"
+	c := ddl.Config{}
+	// normalizeSpace isn't perfect, but it handles most of the
+	// usual discretionary space issues.
+	assert.Equal(t, normalizeSpace(expected), normalizeSpace(strings.Join(conv.GetDDL(c), " ")))
+}
+
+func TestProcessMySQLDump_Rows(t *testing.T) {
+	conv, _ := runProcessMySQLDump("CREATE TABLE cart (a text, n bigint);\n" +
+		"INSERT INTO cart (a, n) VALUES ('a42', 2);")
+	assert.Equal(t, int64(1), conv.Rows())
+}
+
+func TestProcessMySQLDump_BadRows(t *testing.T) {
+	conv, _ := runProcessMySQLDump("CREATE TABLE cart (a text, n bigint);\n" +
+		"INSERT INTO cart (a, n) VALUES ('a42', 'not_a_number');")
+	assert.Equal(t, int64(1), conv.BadRows())
+}
+
+func TestProcessMySQLDump_GetBadRows(t *testing.T) {
+	conv, _ := runProcessMySQLDump("CREATE TABLE cart (a text, n bigint);\n" +
+		"INSERT INTO cart (a, n) VALUES ('a42', 'not_a_number');")
+	assert.Equal(t, 1, len(conv.SampleBadRows(100)))
+}
+
+func TestProcessMySQLDump_AddPrimaryKeys(t *testing.T) {
+	cases := []struct {
+		name           string
+		input          string
+		expectedSchema map[string]ddl.CreateTable
+	}{
+		{
+			name:  "Shopping cart",
+			input: "CREATE TABLE cart (productid text, userid text, quantity bigint);",
+			expectedSchema: map[string]ddl.CreateTable{
+				"cart": ddl.CreateTable{
+					Name:     "cart",
+					ColNames: []string{"productid", "userid", "quantity", "synth_id"},
+					ColDefs: map[string]ddl.ColumnDef{
+						"productid": ddl.ColumnDef{Name: "productid", T: ddl.String{Len: ddl.MaxLength{}}},
+						"userid":    ddl.ColumnDef{Name: "userid", T: ddl.String{Len: ddl.MaxLength{}}},
+						"quantity":  ddl.ColumnDef{Name: "quantity", T: ddl.Int64{}},
+						"synth_id":  ddl.ColumnDef{Name: "synth_id", T: ddl.Int64{}},
+					},
+					Pks: []ddl.IndexKey{ddl.IndexKey{Col: "synth_id"}}}},
+		},
+		{
+			name:  "synth_id clash",
+			input: "CREATE TABLE test (synth_id text, synth_id0 text, synth_id1 bigint);",
+			expectedSchema: map[string]ddl.CreateTable{
+				"test": ddl.CreateTable{
+					Name:     "test",
+					ColNames: []string{"synth_id", "synth_id0", "synth_id1", "synth_id2"},
+					ColDefs: map[string]ddl.ColumnDef{
+						"synth_id":  ddl.ColumnDef{Name: "synth_id", T: ddl.String{Len: ddl.MaxLength{}}},
+						"synth_id0": ddl.ColumnDef{Name: "synth_id0", T: ddl.String{Len: ddl.MaxLength{}}},
+						"synth_id1": ddl.ColumnDef{Name: "synth_id1", T: ddl.Int64{}},
+						"synth_id2": ddl.ColumnDef{Name: "synth_id2", T: ddl.Int64{}},
+					},
+					Pks: []ddl.IndexKey{ddl.IndexKey{Col: "synth_id2"}}}},
+		},
+	}
+	for _, tc := range cases {
+		t.Run(tc.name, func(t *testing.T) {
+			conv, _ := runProcessMySQLDump(tc.input)
+			conv.AddPrimaryKeys()
+			if tc.expectedSchema != nil {
+				assert.Equal(t, tc.expectedSchema, stripSchemaComments(conv.SpSchema), tc.name+": Schema did not match")
+			}
+		})
+	}
+}
+
+func runProcessMySQLDump(s string) (*internal.Conv, []spannerData) {
+	conv := internal.MakeConv()
+	conv.SetLocation(time.UTC)
+	conv.SetSchemaMode()
+	ProcessMySQLDump(conv, internal.NewReader(bufio.NewReader(strings.NewReader(s)), nil))
+	conv.SetDataMode()
+	var rows []spannerData
+	conv.SetDataSink(func(table string, cols []string, vals []interface{}) {
+		rows = append(rows, spannerData{table: table, cols: cols, vals: vals})
+	})
+	ProcessMySQLDump(conv, internal.NewReader(bufio.NewReader(strings.NewReader(s)), nil))
+	return conv, rows
+}
+
+// noIssues verifies that conversion was issue-free by checking that conv
+// contains no unexpected conditions, statement errors, etc. Note that
+// many tests are issue-free, but several explicitly test handling of
+// various issues (so don't call nonIssue for them!).
+func noIssues(conv *internal.Conv, t *testing.T, name string) {
+	assert.Zero(t, conv.Unexpecteds(), 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))
+}
+
+// stripSchemaComments returns a schema with all comments removed.
+// We mostly ignore schema comments in testing since schema comments
+// are often changed and are not a core part of conversion functionality.
+func stripSchemaComments(spSchema map[string]ddl.CreateTable) map[string]ddl.CreateTable {
+	for t, ct := range spSchema {
+		for c, cd := range ct.ColDefs {
+			cd.Comment = ""
+			ct.ColDefs[c] = cd
+		}
+		ct.Comment = ""
+		spSchema[t] = ct
+	}
+	return spSchema
+}
+
+func normalizeSpace(s string) string {
+	return strings.Join(strings.Fields(s), " ")
+}
+
+func bitReverse(i int64) int64 {
+	return int64(bits.Reverse64(uint64(i)))
+}
diff --git a/mysql/report_test.go b/mysql/report_test.go
new file mode 100644
index 0000000..3fe2979
--- /dev/null
+++ b/mysql/report_test.go
@@ -0,0 +1,167 @@
+// Copyright 2020 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package mysql
+
+import (
+	"bufio"
+	"bytes"
+	"fmt"
+	"strings"
+	"testing"
+
+	"github.com/cloudspannerecosystem/harbourbridge/internal"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestReport(t *testing.T) {
+	s := `
+   CREATE TABLE bad_schema (
+      a float,
+      b integer NOT NULL);
+  CREATE TABLE default_value (
+      a text,
+      b bigint DEFAULT 42,
+      PRIMARY KEY (a)
+      );
+  CREATE TABLE excellent_schema (
+      a text,
+      b bigint,
+      PRIMARY KEY (a)
+      );
+  CREATE TABLE foreign_key (
+      a text,
+      b bigint,
+      PRIMARY KEY (a),
+      FOREIGN KEY (a) REFERENCES excellent_schema(a));
+  CREATE TABLE no_pk (
+      a bigint,
+      b integer NOT NULL,
+      c text);`
+	conv := internal.MakeConv()
+	conv.SetSchemaMode()
+	ProcessMySQLDump(conv, internal.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}
+	badWrites := map[string]int64{"bad_schema": 50, "no_pk": 0}
+	conv.Stats.Unexpected["Testing unexpected messages"] = 5
+	buf := new(bytes.Buffer)
+	w := bufio.NewWriter(buf)
+	internal.GenerateReport("mysqldump", conv, w, badWrites)
+	w.Flush()
+	// Print copy of report to stdout (shows up when running go test -v).
+	fmt.Print(buf.String())
+	// Do a dumb comparison with a static 'expected' string.
+	// If 'expected' is painful to maintain, delete it and just
+	// use this test as a smoke test and a way to see report output
+	// for some canned/sample data.
+	expected :=
+		`----------------------------
+Summary of Conversion
+----------------------------
+Schema conversion: GOOD (most columns mapped cleanly, but some missing primary keys).
+Data conversion: POOR (66% of 6000 rows written to Spanner).
+
+The remainder of this report provides stats on the mysqldump 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.
+
+----------------------------
+Statements Processed
+----------------------------
+Analysis of statements in mysqldump 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.
+   error: statements that could not be processed.
+  --------------------------------------
+  schema   data   skip  error  statement
+  --------------------------------------
+       5      0      0      0  *ast.CreateTableStmt
+See https://github.com/pingcap/parser for definitions of statement types
+(pingcap/parser is the library we use for parsing mysqldump output).
+
+----------------------------
+Table bad_schema
+----------------------------
+Schema conversion: GOOD (all columns mapped cleanly, but missing primary key).
+Data conversion: OK (94% of 1000 rows written to Spanner).
+
+Warning
+1) Column 'synth_id' was added because this table didn't have a primary key.
+   Spanner requires a primary key for every table.
+
+Note
+1) Some columns will consume more storage in Spanner e.g. for column 'a', source
+   DB type float is mapped to Spanner type float64.
+
+----------------------------
+Table default_value
+----------------------------
+Schema conversion: POOR (many columns did not map cleanly).
+Data conversion: NONE (no data rows found).
+
+Warning
+1) Some columns have default values which Spanner does not support e.g. column
+   'b'.
+
+----------------------------
+Table excellent_schema
+----------------------------
+Schema conversion: EXCELLENT (all columns mapped cleanly).
+Data conversion: NONE (no data rows found).
+
+----------------------------
+Table foreign_key
+----------------------------
+Schema conversion: POOR (many columns did not map cleanly).
+Data conversion: NONE (no data rows found).
+
+Warning
+1) Column 'a' uses foreign keys which Spanner does not support.
+
+----------------------------
+Table no_pk
+----------------------------
+Schema conversion: GOOD (all columns mapped cleanly, but missing primary key).
+Data conversion: POOR (60% of 5000 rows written to Spanner).
+
+Warning
+1) Column 'synth_id' was added because this table didn't have a primary key.
+   Spanner requires a primary key for every table.
+
+Note
+1) Some columns will consume more storage in Spanner e.g. for column 'b', source
+   DB type int(11) is mapped to Spanner type int64.
+
+----------------------------
+Unexpected Conditions
+----------------------------
+For debugging only. This section provides details of unexpected conditions
+encountered as we processed the mysqldump data. In particular, the AST node
+representation used by the pingcap/parser library used for parsing
+mysqldump output is highly permissive: almost any construct can appear at
+any node in the AST tree. The list details all unexpected nodes and
+conditions.
+  --------------------------------------
+   count  condition
+  --------------------------------------
+       5  Testing unexpected messages
+
+`
+	assert.Equal(t, expected, buf.String())
+}
diff --git a/mysql/toddl_test.go b/mysql/toddl_test.go
new file mode 100644
index 0000000..4ec7320
--- /dev/null
+++ b/mysql/toddl_test.go
@@ -0,0 +1,75 @@
+// Copyright 2020 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package mysql
+
+import (
+	"testing"
+
+	"github.com/cloudspannerecosystem/harbourbridge/internal"
+	"github.com/cloudspannerecosystem/harbourbridge/schema"
+	"github.com/cloudspannerecosystem/harbourbridge/spanner/ddl"
+	"github.com/stretchr/testify/assert"
+)
+
+// This is just a very basic smoke-test for toSpannerType.
+func TestToSpannerType(t *testing.T) {
+	conv := internal.MakeConv()
+	conv.SetSchemaMode()
+	name := "test"
+	srcSchema := schema.Table{
+		Name:     name,
+		ColNames: []string{"a", "b", "c", "d", "e", "f"},
+		ColDefs: map[string]schema.Column{
+			"a": schema.Column{Name: "a", Type: schema.Type{Name: "int"}},
+			"b": schema.Column{Name: "b", Type: schema.Type{Name: "float"}},
+			"c": schema.Column{Name: "c", Type: schema.Type{Name: "tinyint", Mods: []int64{1}}},
+			"d": schema.Column{Name: "d", Type: schema.Type{Name: "varchar", Mods: []int64{6}}},
+			"e": schema.Column{Name: "e", Type: schema.Type{Name: "double"}},
+			"f": schema.Column{Name: "f", Type: schema.Type{Name: "timestamp"}},
+		},
+		PrimaryKeys: []schema.Key{schema.Key{Column: "a"}}}
+	conv.SrcSchema[name] = srcSchema
+	assert.Nil(t, schemaToDDL(conv))
+	actual := conv.SpSchema[name]
+	dropComments(&actual) // Don't test comment.
+	expected := ddl.CreateTable{
+		Name:     name,
+		ColNames: []string{"a", "b", "c", "d", "e", "f"},
+		ColDefs: map[string]ddl.ColumnDef{
+			"a": ddl.ColumnDef{Name: "a", T: ddl.Int64{}},
+			"b": ddl.ColumnDef{Name: "b", T: ddl.Float64{}},
+			"c": ddl.ColumnDef{Name: "c", T: ddl.Bool{}},
+			"d": ddl.ColumnDef{Name: "d", T: ddl.String{Len: ddl.Int64Length{Value: 6}}},
+			"e": ddl.ColumnDef{Name: "e", T: ddl.Float64{}},
+			"f": ddl.ColumnDef{Name: "f", T: ddl.Timestamp{}},
+		},
+		Pks: []ddl.IndexKey{ddl.IndexKey{Col: "a"}},
+	}
+	assert.Equal(t, expected, actual)
+	expectedIssues := map[string][]internal.SchemaIssue{
+		"a": []internal.SchemaIssue{internal.Widened},
+		"b": []internal.SchemaIssue{internal.Widened},
+	}
+	assert.Equal(t, expectedIssues, conv.Issues[name])
+}
+
+func dropComments(t *ddl.CreateTable) {
+	t.Comment = ""
+	for _, c := range t.ColNames {
+		cd := t.ColDefs[c]
+		cd.Comment = ""
+		t.ColDefs[c] = cd
+	}
+}
diff --git a/postgres/infoschema_test.go b/postgres/infoschema_test.go
index 0d11e29..a4468e2 100644
--- a/postgres/infoschema_test.go
+++ b/postgres/infoschema_test.go
@@ -193,7 +193,7 @@ 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)
 
 	assert.Equal(t,
 		[]spannerData{
@@ -270,7 +270,7 @@ func TestConvertSqlRow_SingleCol(t *testing.T) {
 			Name:     tableName,
 			ColNames: []string{col},
 			ColDefs:  map[string]ddl.ColumnDef{col: ddl.ColumnDef{Name: col, T: tc.spType, IsArray: tc.isArray}}}
-		ac, av, err := ConvertSqlRow(conv, tableName, cols, srcSchema, tableName, cols, spSchema, []interface{}{tc.in})
+		ac, av, err := ConvertSQLRow(conv, tableName, cols, srcSchema, tableName, cols, spSchema, []interface{}{tc.in})
 		assert.Equal(t, cols, ac)
 		assert.Equal(t, []interface{}{tc.e}, av)
 		assert.Nil(t, err)
@@ -329,7 +329,7 @@ func TestConvertSqlRow_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)
 	assert.Equal(t, []spannerData{
 		{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)}}},
diff --git a/postgres/report_test.go b/postgres/report_test.go
index 0b93739..c331c6b 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(true, conv, w, badWrites)
+	internal.GenerateReport("pgdump", 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 pg_dump statements processed,
+The remainder of this report provides stats on the pgdump 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 pg_dump output, broken down by statement type.
+Analysis of statements in pgdump 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.
diff --git a/spanner/batchwriter_test.go b/spanner/batchwriter_test.go
index 1c4f15e..282de76 100644
--- a/spanner/batchwriter_test.go
+++ b/spanner/batchwriter_test.go
@@ -223,7 +223,7 @@ func toMutations(r []*row) []*sp.Mutation {
 // partitionRows splits data into goodRows and badRows based on badRowIndex,
 // which specifies the indices of bad rows.
 func partitionRows(badRowIndex map[int]bool, data []*row) (goodRows []*row, badRows []*row) {
-	for i, _ := range data {
+	for i := range data {
 		if badRowIndex[i] {
 			badRows = append(badRows, data[i])
 		} else {