diff --git a/README.md b/README.md index 1b14759bda..8506fbaa5f 100644 --- a/README.md +++ b/README.md @@ -307,6 +307,8 @@ Each package document has the following keys: - Directory of SQL queries or path to single SQL file - `schema`: - Directory of SQL migrations or path to single SQL file +- `engine`: + - Either `postgresql` or `mysql`. Defaults to `postgresql`. MySQL support is experimental ### Type Overrides @@ -411,13 +413,17 @@ Each commit is deployed to the [`devel` channel on Equinox](https://dl.equinox.i - [Linux](https://bin.equinox.io/c/gvM95th6ps1/sqlc-devel-linux-amd64.tgz) - [macOS](https://bin.equinox.io/c/gvM95th6ps1/sqlc-devel-darwin-amd64.zip) -## Other Database Engines +## Other Databases and Languages -sqlc currently only supports PostgreSQL. If you'd like to support another database, we'd welcome a contribution. +sqlc currently only supports PostgreSQL / Go. MySQL support has been merged, +but it's marked as experimental. SQLite and TypeScript support are planned. -## Other Language Backends +| Language | PostgreSQL | MySQL | SQLite | +| ------------ |:----------------:|:----------------:|:----------------:| +| Go |:white_check_mark:|:warning: |:timer_clock: | +| TypeScript |:timer_clock: |:timer_clock: |:timer_clock: | -sqlc currently only generates Go code, but if you'd like to build another language backend, we'd welcome a contribution. +If you'd like to add another database or language, we'd welcome a contribution. ## Acknowledgements diff --git a/go.mod b/go.mod index 1f46d34a1d..0ab1d9fcd6 100644 --- a/go.mod +++ b/go.mod @@ -8,4 +8,10 @@ require ( github.com/jinzhu/inflection v1.0.0 github.com/lfittl/pg_query_go v1.0.0 github.com/spf13/cobra v0.0.5 + github.com/spf13/pflag v1.0.5 // indirect + golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 // indirect + golang.org/x/sys v0.0.0-20191220220014-0732a990476f // indirect + google.golang.org/genproto v0.0.0-20191223191004-3caeed10a8bf // indirect + google.golang.org/grpc v1.26.0 // indirect + vitess.io/vitess v0.0.0-20191113025808-0629f0da20ab ) diff --git a/go.sum b/go.sum index 30821273b3..ea10b1ce2d 100644 --- a/go.sum +++ b/go.sum @@ -1,29 +1,176 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +github.com/Bowery/prompt v0.0.0-20190419144237-972d0ceb96f5/go.mod h1:4/6eNcqZ09BZ9wLK3tZOjBA1nDj+B0728nlX5YRlSmQ= 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/DataDog/datadog-go v2.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= +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/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-metrics v0.0.0-20190430140413-ec5e00d3c878/go.mod h1:3AMJUQhVx52RsWOnlkpikZr01T/yAVN2gn0861vByNg= +github.com/aws/aws-sdk-go v0.0.0-20180223184012-ebef4262e06a/go.mod h1:ZRmQr0FajVIyZ4ZzBYKG5P3ZqPz9IHG41ZoMu1ADI3k= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= +github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cockroachdb/cmux v0.0.0-20170110192607-30d10be49292/go.mod h1:qRiX68mZX1lGBkTWyp3CLcenw9I94W2dLeRvMzcn9N4= +github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= +github.com/coreos/etcd v0.0.0-20170626015032-703663d1f6ed/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= +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/dchest/safefile v0.0.0-20151022103144-855e8d98f185/go.mod h1:cFRxtTwTOJkz2x3rQUNCYKWC93yP1VKjR8NUhqFxZNU= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/evanphx/json-patch v4.5.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/ghodss/yaml v0.0.0-20161207003320-04f313413ffd/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-ini/ini v1.12.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +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/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +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= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/snappy v0.0.0-20170215233205-553a64147049/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/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= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +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/shlex v0.0.0-20181106134648-c34317bd91bf/go.mod h1:RpwtwJQFrIEPstU94h88MWPXP2ektJZ8cZ0YntAmXiE= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/websocket v0.0.0-20160912153041-2d1e4548da23/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/grpc-ecosystem/go-grpc-middleware v1.1.0/go.mod h1:f5nM7jw/oeRSadq3xCzHAvxcr8HZnzsqU6ILg/0NiiE= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v0.0.0-20161128002007-199c40a060d1/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= +github.com/hashicorp/consul v1.4.0/go.mod h1:mFrjN1mfidgJfYP1xrJCF+AfRhr6Eaqhb2+sfyn/OOI= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-msgpack v0.5.5/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= +github.com/hashicorp/go-rootcerts v0.0.0-20160503143440-6bb64b370b90/go.mod h1:o4zcYY1e0GEZI6eSEr+43QDYmuGglw1qSO6qdHUHCgg= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.3/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/memberlist v0.1.4/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= +github.com/hashicorp/serf v0.0.0-20161207011743-d3a67ab21bc8/go.mod h1:h/Ru6tmZazX7WO/GDmwdpS975F019L4t5ng5IgwbNrE= +github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +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/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v0.0.0-20180801095237-b50017755d44/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +github.com/klauspost/crc32 v1.2.0/go.mod h1:+ZoRqAPRLkC4NPOvfYeR5KNOrY6TD+/sAC3HXPZgDYg= +github.com/klauspost/pgzip v1.2.0/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/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/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/krishicks/yaml-patch v0.0.10/go.mod h1:Sm5TchwZS6sm7RJoyg87tzxm2ZcKzdRE4Q7TjNhPrME= 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/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mattn/go-runewidth v0.0.1/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/minio/minio-go v0.0.0-20190131015406-c8a261de75c1/go.mod h1:vuvdOZLJuf5HmJAJrKV64MmozrSsk+or0PB5dzdfspg= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +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/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/olekukonko/tablewriter v0.0.0-20160115111002-cca8bbc07984/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= +github.com/opentracing-contrib/go-grpc v0.0.0-20180928155321-4b5a12d3ff02/go.mod h1:JNdpVEzCpXBgIiv4ds+TzhN1hrtxq6ClLrTlT9OQRSc= +github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pborman/uuid v0.0.0-20160824210600-b984ec7fa9ff/go.mod h1:VyrYX9gd7irzKovcSS6BIIEwPRkP2Wm2m9ufcdFSJ34= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +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/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 v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= +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/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= +github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= +github.com/prometheus/procfs v0.0.5/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/satori/go.uuid v0.0.0-20160713180306-0aa62d5ddceb/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s= @@ -31,12 +178,154 @@ github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tL github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +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/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/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/tchap/go-patricia v0.0.0-20160729071656-dd168db6051b/go.mod h1:bmLyhP68RS6kStMGxByiQ23RP/odRBOTVjwp2cDyi6I= +github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= +github.com/uber-go/atomic v1.4.0/go.mod h1:/Ct5t2lcmbJ4OSe/waGBoaVvVqtO0bmtfVNex1PFV8g= +github.com/uber/jaeger-client-go v2.16.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= +github.com/uber/jaeger-lib v2.0.0+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= +github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/youtube/vitess v2.1.1+incompatible h1:SE+P7DNX/jw5RHFs5CHRhZQjq402EJFCD33JhzQMdDw= +github.com/youtube/vitess v2.1.1+incompatible/go.mod h1:hpMim5/30F1r+0P8GGtB29d0gWHr0IZ5unS+CG0zMx8= +github.com/z-division/go-zookeeper v0.0.0-20190128072838-6d7457066b9b/go.mod h1:JNALoWa+nCXR8SmgLluHcBNVJgyejzpKPZk9pX2yXXE= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190128193316-c7b33c32a30b/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-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190829043050-9756ffdc2472/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +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-20181023162649-9b4f9f5ad519/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-20181201002055-351d144fa1fc/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-20190125091013-d26f9f9a57f3/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= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +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-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190926025831-c00fd9afed17/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 h1:efeOvDhwQ29Dj3SdAV/MJf8oukgn+8D8WgaCaRMchF8= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +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/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +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/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-20181026203630-95b1ffbd15a5/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-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190124100055-b90733256f2e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/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-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190926180325-855e68c8590b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191220220014-0732a990476f h1:72l8qCJ1nGxMGH26QVBVIxKd/D34cfGt0OvrPtpemyY= +golang.org/x/sys v0.0.0-20191220220014-0732a990476f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= 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-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/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-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= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +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-20190606124116-d0a3d012864b/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-20190830154057-c17b040389b9/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/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= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55 h1:gSJIx1SDwno+2ElGhA4+qG2zF97qiUzTM+rQ0klBOcE= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190926190326-7ee9db18f195/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191223191004-3caeed10a8bf h1:1x8rC5/IgdLMPbPTvlQTN28+rcy8XL9Q19UWUMDyqYs= +google.golang.org/genproto v0.0.0-20191223191004-3caeed10a8bf/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +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/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.24.0/go.mod h1:XDChyiUovWa60DnaeDeZmSW86xtLtjtZbwvSiRnRtcA= +google.golang.org/grpc v1.26.0 h1:2dTRdpdFEEhJYQD8EMLB61nnrzSCTbG38PhqdhvOltg= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/asn1-ber.v1 v1.0.0-20150924051756-4e86f4367175/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/ini.v1 v1.41.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ldap.v2 v2.5.0/go.mod h1:oI0cpe/D7HRtBQl8aTg+ZmzFUAvu4lsv3eLXMLGFxWk= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/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= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +vitess.io/vitess v0.0.0-20191113025808-0629f0da20ab h1:Lu6PzNvcd0AVzWPohZE1bCqxvICJGwhpxyiunaes3p4= +vitess.io/vitess v0.0.0-20191113025808-0629f0da20ab/go.mod h1:tLyyCa/hBqubuTQeB34djK7LTtM52DCUcfWYAYf1oZI= +vitess.io/vitess v2.1.1+incompatible h1:nuuGHiWYWpudD3gOCLeGzol2EJ25e/u5Wer2wV1O130= +vitess.io/vitess v2.1.1+incompatible/go.mod h1:h4qvkyNYTOC0xI+vcidSWoka0gQAZc9ZPHbkHo48gP0= diff --git a/internal/cmd/cmd.go b/internal/cmd/cmd.go index 3c7ffcece3..361c1484e3 100644 --- a/internal/cmd/cmd.go +++ b/internal/cmd/cmd.go @@ -11,6 +11,7 @@ import ( "path/filepath" "github.com/kyleconroy/sqlc/internal/dinosql" + "github.com/kyleconroy/sqlc/internal/mysql" "github.com/davecgh/go-spew/spew" pg "github.com/lfittl/pg_query_go" @@ -126,48 +127,57 @@ var genCmd = &cobra.Command{ output := map[string]string{} - for i, pkg := range settings.Packages { + for _, pkg := range settings.Packages { name := pkg.Name - if pkg.Path == "" { - fmt.Fprintf(os.Stderr, "package[%d]: path must be set\n", i) - errored = true - continue - } + var result dinosql.Generateable - if name == "" { - name = filepath.Base(pkg.Path) - } + switch pkg.Engine { - c, err := dinosql.ParseCatalog(pkg.Schema) - if err != nil { - fmt.Fprintf(os.Stderr, "# package %s\n", name) - if parserErr, ok := err.(*dinosql.ParserErr); ok { - for _, fileErr := range parserErr.Errs { - fmt.Fprintf(os.Stderr, "%s:%d:%d: %s\n", fileErr.Filename, fileErr.Line, fileErr.Column, fileErr.Err) + case dinosql.EngineMySQL: + // Experimental MySQL support + q, err := mysql.GeneratePkg(name, pkg.Schema, pkg.Queries, settings) + if err != nil { + fmt.Fprintf(os.Stderr, "# package %s\n", name) + fmt.Fprintf(os.Stderr, "error parsing file: %s\n", err) + errored = true + continue + } + result = q + + case dinosql.EnginePostgreSQL: + c, err := dinosql.ParseCatalog(pkg.Schema) + if err != nil { + fmt.Fprintf(os.Stderr, "# package %s\n", name) + if parserErr, ok := err.(*dinosql.ParserErr); ok { + for _, fileErr := range parserErr.Errs { + fmt.Fprintf(os.Stderr, "%s:%d:%d: %s\n", fileErr.Filename, fileErr.Line, fileErr.Column, fileErr.Err) + } + } else { + fmt.Fprintf(os.Stderr, "error parsing schema: %s\n", err) } - } else { - fmt.Fprintf(os.Stderr, "error parsing schema: %s\n", err) + errored = true + continue } - errored = true - continue - } - q, err := dinosql.ParseQueries(c, settings, pkg) - if err != nil { - fmt.Fprintf(os.Stderr, "# package %s\n", name) - if parserErr, ok := err.(*dinosql.ParserErr); ok { - for _, fileErr := range parserErr.Errs { - fmt.Fprintf(os.Stderr, "%s:%d:%d: %s\n", fileErr.Filename, fileErr.Line, fileErr.Column, fileErr.Err) + q, err := dinosql.ParseQueries(c, pkg) + if err != nil { + fmt.Fprintf(os.Stderr, "# package %s\n", name) + if parserErr, ok := err.(*dinosql.ParserErr); ok { + for _, fileErr := range parserErr.Errs { + fmt.Fprintf(os.Stderr, "%s:%d:%d: %s\n", fileErr.Filename, fileErr.Line, fileErr.Column, fileErr.Err) + } + } else { + fmt.Fprintf(os.Stderr, "error parsing queries: %s\n", err) } - } else { - fmt.Fprintf(os.Stderr, "error parsing queries: %s\n", err) + errored = true + continue } - errored = true - continue + result = q + } - files, err := dinosql.Generate(q, settings, pkg) + files, err := dinosql.Generate(result, settings) if err != nil { fmt.Fprintf(os.Stderr, "# package %s\n", name) fmt.Fprintf(os.Stderr, "error generating code: %s\n", err) @@ -199,13 +209,13 @@ var checkCmd = &cobra.Command{ Use: "compile", Short: "Statically check SQL for syntax and type errors", RunE: func(cmd *cobra.Command, args []string) error { - blob, err := ioutil.ReadFile("sqlc.json") + file, err := os.Open("sqlc.json") if err != nil { return err } - var settings dinosql.GenerateSettings - if err := json.Unmarshal(blob, &settings); err != nil { + settings, err := dinosql.ParseConfig(file) + if err != nil { return err } @@ -214,7 +224,7 @@ var checkCmd = &cobra.Command{ if err != nil { return err } - if _, err := dinosql.ParseQueries(c, settings, pkg); err != nil { + if _, err := dinosql.ParseQueries(c, pkg); err != nil { return err } } diff --git a/internal/dinosql/checks_test.go b/internal/dinosql/checks_test.go index 783bfa7b07..96108c15cd 100644 --- a/internal/dinosql/checks_test.go +++ b/internal/dinosql/checks_test.go @@ -12,7 +12,6 @@ import ( func TestFuncs(t *testing.T) { _, err := ParseQueries( pg.NewCatalog(), - GenerateSettings{}, PackageSettings{ Queries: filepath.Join("testdata", "funcs"), }, diff --git a/internal/dinosql/config.go b/internal/dinosql/config.go index 473d776d53..999b7ab8f9 100644 --- a/internal/dinosql/config.go +++ b/internal/dinosql/config.go @@ -5,20 +5,45 @@ import ( "errors" "fmt" "io" + "path/filepath" "strings" "github.com/kyleconroy/sqlc/internal/pg" ) +const errMessageNoVersion = `The configuration file must have a version number. +Set the version to 1 at the top of sqlc.json: + +{ + "version": "1" + ... +} +` + +const errMessageUnknownVersion = `The configuration file has an invalid version number. +The only supported version is "1". +` + +const errMessageNoPackages = `No packages are configured` + type GenerateSettings struct { - Version string `json:"version"` - Packages []PackageSettings `json:"packages"` - Overrides []Override `json:"overrides,omitempty"` - Rename map[string]string `json:"rename,omitempty"` + Version string `json:"version"` + Packages []PackageSettings `json:"packages"` + Overrides []Override `json:"overrides,omitempty"` + Rename map[string]string `json:"rename,omitempty"` + PackageMap map[string]PackageSettings } +type Engine string + +const ( + EngineMySQL Engine = "mysql" + EnginePostgreSQL Engine = "postgresql" +) + type PackageSettings struct { Name string `json:"name"` + Engine Engine `json:"engine,omitempty"` Path string `json:"path"` Schema string `json:"schema"` Queries string `json:"queries"` @@ -96,6 +121,8 @@ func (o *Override) Parse() error { var ErrMissingVersion = errors.New("no version number") var ErrUnknownVersion = errors.New("invalid version number") var ErrNoPackages = errors.New("no packages") +var ErrNoPackageName = errors.New("missing package name") +var ErrNoPackagePath = errors.New("missing package path") func ParseConfig(rd io.Reader) (GenerateSettings, error) { dec := json.NewDecoder(rd) @@ -119,11 +146,36 @@ func ParseConfig(rd io.Reader) (GenerateSettings, error) { } } for j := range config.Packages { + if config.Packages[j].Path == "" { + return config, ErrNoPackagePath + } for i := range config.Packages[j].Overrides { if err := config.Packages[j].Overrides[i].Parse(); err != nil { return config, err } } + if config.Packages[j].Name == "" { + config.Packages[j].Name = filepath.Base(config.Packages[j].Path) + } + if config.Packages[j].Engine == "" { + config.Packages[j].Engine = EnginePostgreSQL + } } - return config, nil + err := config.PopulatePkgMap() + + return config, err +} + +func (s *GenerateSettings) PopulatePkgMap() error { + packageMap := make(map[string]PackageSettings) + + for _, c := range s.Packages { + if c.Name == "" { + return ErrNoPackageName + } + packageMap[c.Name] = c + } + s.PackageMap = packageMap + + return nil } diff --git a/internal/dinosql/gen.go b/internal/dinosql/gen.go index bf95c9185d..71f2f40b1e 100644 --- a/internal/dinosql/gen.go +++ b/internal/dinosql/gen.go @@ -52,18 +52,39 @@ func (gf GoField) Tag() string { } type GoStruct struct { - Table *core.FQN + Table Comparable Name string Fields []GoField Comment string } -// TODO: Terrible name type GoQueryValue struct { Emit bool Name string Struct *GoStruct - typ string + Typ string +} + +// TODO: consider making this deep equality from stdlib? +type Comparable interface { + EqualTo(b interface{}) bool +} + +type FQNAlias core.FQN + +// Check whether tables are equal +func (a *FQNAlias) EqualTo(other interface{}) bool { + b, ok := other.(*core.FQN) + if !ok { + panic("Unknown ") + } + if a == nil && b == nil { + return true + } + if a == nil || b == nil { + return false + } + return a.Catalog == b.Catalog && a.Schema == b.Schema && a.Rel == b.Rel } func (v GoQueryValue) EmitStruct() bool { @@ -75,7 +96,7 @@ func (v GoQueryValue) IsStruct() bool { } func (v GoQueryValue) isEmpty() bool { - return v.typ == "" && v.Name == "" && v.Struct == nil + return v.Typ == "" && v.Name == "" && v.Struct == nil } func (v GoQueryValue) Pair() string { @@ -86,8 +107,8 @@ func (v GoQueryValue) Pair() string { } func (v GoQueryValue) Type() string { - if v.typ != "" { - return v.typ + if v.Typ != "" { + return v.Typ } if v.Struct != nil { return v.Struct.Name @@ -101,7 +122,7 @@ func (v GoQueryValue) Params() string { } var out []string if v.Struct == nil { - if strings.HasPrefix(v.typ, "[]") && v.typ != "[]byte" { + if strings.HasPrefix(v.Typ, "[]") && v.Typ != "[]byte" { out = append(out, "pq.Array("+v.Name+")") } else { out = append(out, v.Name) @@ -125,7 +146,7 @@ func (v GoQueryValue) Params() string { func (v GoQueryValue) Scan() string { var out []string if v.Struct == nil { - if strings.HasPrefix(v.typ, "[]") && v.typ != "[]byte" { + if strings.HasPrefix(v.Typ, "[]") && v.Typ != "[]byte" { out = append(out, "pq.Array(&"+v.Name+")") } else { out = append(out, "&"+v.Name) @@ -159,8 +180,15 @@ type GoQuery struct { Arg GoQueryValue } -func (r Result) UsesType(typ string) bool { - for _, strct := range r.Structs() { +type Generateable interface { + Structs(settings GenerateSettings) []GoStruct + PkgName() string + GoQueries(settings GenerateSettings) []GoQuery + Enums(settings GenerateSettings) []GoEnum +} + +func UsesType(r Generateable, typ string, settings GenerateSettings) bool { + for _, strct := range r.Structs(settings) { for _, f := range strct.Fields { fType := strings.TrimPrefix(f.Type, "[]") if strings.HasPrefix(fType, typ) { @@ -171,8 +199,8 @@ func (r Result) UsesType(typ string) bool { return false } -func (r Result) UsesArrays() bool { - for _, strct := range r.Structs() { +func UsesArrays(r Generateable, settings GenerateSettings) bool { + for _, strct := range r.Structs(settings) { for _, f := range strct.Fields { if strings.HasPrefix(f.Type, "[]") { return true @@ -182,58 +210,58 @@ func (r Result) UsesArrays() bool { return false } -func (r Result) Imports(settings PackageSettings) func(string) [][]string { +func Imports(r Generateable, settings GenerateSettings) func(string) [][]string { return func(filename string) [][]string { if filename == "db.go" { imps := []string{"context", "database/sql"} - if settings.EmitPreparedQueries { + if settings.PackageMap[r.PkgName()].EmitPreparedQueries { imps = append(imps, "fmt") } return [][]string{imps} } if filename == "models.go" { - return r.ModelImports() + return ModelImports(r, settings) } - return r.QueryImports(filename) + return QueryImports(r, settings, filename) } } -func (r Result) ModelImports() [][]string { +func ModelImports(r Generateable, settings GenerateSettings) [][]string { std := make(map[string]struct{}) - if r.UsesType("sql.Null") { + if UsesType(r, "sql.Null", settings) { std["database/sql"] = struct{}{} } - if r.UsesType("json.RawMessage") { + if UsesType(r, "json.RawMessage", settings) { std["encoding/json"] = struct{}{} } - if r.UsesType("time.Time") { + if UsesType(r, "time.Time", settings) { std["time"] = struct{}{} } - if r.UsesType("net.IP") { + if UsesType(r, "net.IP", settings) { std["net"] = struct{}{} } // Custom imports pkg := make(map[string]struct{}) overrideTypes := map[string]string{} - for _, o := range append(r.Settings.Overrides, r.packageSettings.Overrides...) { + for _, o := range append(settings.Overrides, settings.PackageMap[r.PkgName()].Overrides...) { overrideTypes[o.goTypeName] = o.goPackage } _, overrideNullTime := overrideTypes["pq.NullTime"] - if r.UsesType("pq.NullTime") && !overrideNullTime { + if UsesType(r, "pq.NullTime", settings) && !overrideNullTime { pkg["github.com/lib/pq"] = struct{}{} } _, overrideUUID := overrideTypes["uuid.UUID"] - if r.UsesType("uuid.UUID") && !overrideUUID { + if UsesType(r, "uuid.UUID", settings) && !overrideUUID { pkg["github.com/google/uuid"] = struct{}{} } for goType, importPath := range overrideTypes { - if _, ok := std[importPath]; !ok && r.UsesType(goType) { + if _, ok := std[importPath]; !ok && UsesType(r, goType, settings) { pkg[importPath] = struct{}{} } } @@ -253,7 +281,7 @@ func (r Result) ModelImports() [][]string { return [][]string{stds, pkgs} } -func (r Result) QueryImports(filename string) [][]string { +func QueryImports(r Generateable, settings GenerateSettings, filename string) [][]string { // for _, strct := range r.Structs() { // for _, f := range strct.Fields { // if strings.HasPrefix(f.Type, "[]") { @@ -262,7 +290,7 @@ func (r Result) QueryImports(filename string) [][]string { // } // } var gq []GoQuery - for _, query := range r.GoQueries() { + for _, query := range r.GoQueries(settings) { if query.SourceName == filename { gq = append(gq, query) } @@ -350,7 +378,7 @@ func (r Result) QueryImports(filename string) [][]string { pkg := make(map[string]struct{}) overrideTypes := map[string]string{} - for _, o := range append(r.Settings.Overrides, r.packageSettings.Overrides...) { + for _, o := range append(settings.Overrides, settings.PackageMap[r.PkgName()].Overrides...) { overrideTypes[o.goTypeName] = o.goPackage } @@ -400,7 +428,7 @@ func enumValueName(value string) string { return name } -func (r Result) Enums() []GoEnum { +func (r Result) Enums(settings GenerateSettings) []GoEnum { var enums []GoEnum for name, schema := range r.Catalog.Schemas { if name == "pg_catalog" { @@ -414,7 +442,7 @@ func (r Result) Enums() []GoEnum { enumName = name + "_" + enum.Name } e := GoEnum{ - Name: r.structName(enumName), + Name: StructName(enumName, settings), Comment: enum.Comment, } for _, v := range enum.Vals { @@ -433,8 +461,8 @@ func (r Result) Enums() []GoEnum { return enums } -func (r Result) structName(name string) string { - if rename := r.Settings.Rename[name]; rename != "" { +func StructName(name string, settings GenerateSettings) string { + if rename := settings.Rename[name]; rename != "" { return rename } out := "" @@ -448,7 +476,7 @@ func (r Result) structName(name string) string { return out } -func (r Result) Structs() []GoStruct { +func (r Result) Structs(settings GenerateSettings) []GoStruct { var structs []GoStruct for name, schema := range r.Catalog.Schemas { if name == "pg_catalog" { @@ -462,14 +490,14 @@ func (r Result) Structs() []GoStruct { tableName = name + "_" + table.Name } s := GoStruct{ - Table: &core.FQN{Schema: name, Rel: table.Name}, - Name: inflection.Singular(r.structName(tableName)), + Table: &FQNAlias{Schema: name, Rel: table.Name}, + Name: inflection.Singular(StructName(tableName, settings)), Comment: table.Comment, } for _, column := range table.Columns { s.Fields = append(s.Fields, GoField{ - Name: r.structName(column.Name), - Type: r.goType(column), + Name: StructName(column.Name, settings), + Type: r.goType(column, settings), Tags: map[string]string{"json:": column.Name}, Comment: column.Comment, }) @@ -483,26 +511,26 @@ func (r Result) Structs() []GoStruct { return structs } -func (r Result) goType(col core.Column) string { +func (r Result) goType(col core.Column, settings GenerateSettings) string { // package overrides have a higher precedence - for _, oride := range append(r.Settings.Overrides, r.packageSettings.Overrides...) { + for _, oride := range append(settings.Overrides, settings.PackageMap[r.PkgName()].Overrides...) { if oride.Column != "" && oride.columnName == col.Name && oride.table == col.Table { return oride.goTypeName } } - typ := r.goInnerType(col) + typ := r.goInnerType(col, settings) if col.IsArray { return "[]" + typ } return typ } -func (r Result) goInnerType(col core.Column) string { +func (r Result) goInnerType(col core.Column, settings GenerateSettings) string { columnType := col.DataType notNull := col.NotNull || col.IsArray // package overrides have a higher precedence - for _, oride := range append(r.Settings.Overrides, r.packageSettings.Overrides...) { + for _, oride := range append(settings.Overrides, settings.PackageMap[r.PkgName()].Overrides...) { if oride.PostgresType != "" && oride.PostgresType == columnType && oride.Null != notNull { return oride.goTypeName } @@ -619,14 +647,14 @@ func (r Result) goInnerType(col core.Column) string { for _, enum := range schema.Enums { if columnType == enum.Name { if name == "public" { - return r.structName(enum.Name) + return StructName(enum.Name, settings) } - return r.structName(name + "_" + enum.Name) + return StructName(name+"_"+enum.Name, settings) } } } - log.Printf("unknown Postgres type: %s\n", columnType) + log.Printf("unknown PostgreSQL type: %s\n", columnType) return "interface{}" } } @@ -638,21 +666,21 @@ func (r Result) goInnerType(col core.Column) string { // JSON tags: count, count_2, count_2 // // This is unlikely to happen, so don't fix it yet -func (r Result) columnsToStruct(name string, columns []core.Column) *GoStruct { +func (r Result) columnsToStruct(name string, columns []core.Column, settings GenerateSettings) *GoStruct { gs := GoStruct{ Name: name, } seen := map[string]int{} for i, c := range columns { tagName := c.Name - fieldName := r.structName(columnName(c, i)) + fieldName := StructName(columnName(c, i), settings) if v := seen[c.Name]; v > 0 { tagName = fmt.Sprintf("%s_%d", tagName, v+1) fieldName = fmt.Sprintf("%s_%d", fieldName, v+1) } gs.Fields = append(gs.Fields, GoField{ Name: fieldName, - Type: r.goType(c), + Type: r.goType(c, settings), Tags: map[string]string{"json:": tagName}, }) seen[c.Name]++ @@ -698,8 +726,8 @@ func compareFQN(a *core.FQN, b *core.FQN) bool { return a.Catalog == b.Catalog && a.Schema == b.Schema && a.Rel == b.Rel } -func (r Result) GoQueries() []GoQuery { - structs := r.Structs() +func (r Result) GoQueries(settings GenerateSettings) []GoQuery { + structs := r.Structs(settings) qs := make([]GoQuery, 0, len(r.Queries)) for _, query := range r.Queries { @@ -712,8 +740,8 @@ func (r Result) GoQueries() []GoQuery { gq := GoQuery{ Cmd: query.Cmd, - ConstantName: lowerTitle(query.Name), - FieldName: lowerTitle(query.Name) + "Stmt", + ConstantName: LowerTitle(query.Name), + FieldName: LowerTitle(query.Name) + "Stmt", MethodName: query.Name, SourceName: query.Filename, SQL: query.SQL, @@ -724,7 +752,7 @@ func (r Result) GoQueries() []GoQuery { p := query.Params[0] gq.Arg = GoQueryValue{ Name: paramName(p), - typ: r.goType(p.Column), + Typ: r.goType(p.Column, settings), } } else if len(query.Params) > 1 { var cols []core.Column @@ -734,7 +762,7 @@ func (r Result) GoQueries() []GoQuery { gq.Arg = GoQueryValue{ Emit: true, Name: "arg", - Struct: r.columnsToStruct(gq.MethodName+"Params", cols), + Struct: r.columnsToStruct(gq.MethodName+"Params", cols, settings), } } @@ -742,7 +770,7 @@ func (r Result) GoQueries() []GoQuery { c := query.Columns[0] gq.Ret = GoQueryValue{ Name: columnName(c, 0), - typ: r.goType(c), + Typ: r.goType(c, settings), } } else if len(query.Columns) > 1 { var gs *GoStruct @@ -755,10 +783,12 @@ func (r Result) GoQueries() []GoQuery { same := true for i, f := range s.Fields { c := query.Columns[i] - sameName := f.Name == r.structName(columnName(c, i)) - sameType := f.Type == r.goType(c) - sameFQN := compareFQN(s.Table, &c.Table) - if !sameName || !sameType || !sameFQN { + sameName := f.Name == StructName(columnName(c, i), settings) + sameType := f.Type == r.goType(c, settings) + + // TODO: clean this up!! + sameTable := s.Table.EqualTo(&c.Table) + if !sameName || !sameType || !sameTable { same = false } } @@ -769,7 +799,7 @@ func (r Result) GoQueries() []GoQuery { } if gs == nil { - gs = r.columnsToStruct(gq.MethodName+"Row", query.Columns) + gs = r.columnsToStruct(gq.MethodName+"Row", query.Columns, settings) emit = true } gq.Ret = GoQueryValue{ @@ -1056,22 +1086,22 @@ type tmplCtx struct { EmitPreparedQueries bool } -func lowerTitle(s string) string { +func LowerTitle(s string) string { a := []rune(s) a[0] = unicode.ToLower(a[0]) return string(a) } -func Generate(r *Result, global GenerateSettings, settings PackageSettings) (map[string]string, error) { - r.packageSettings = settings +func Generate(r Generateable, settings GenerateSettings) (map[string]string, error) { funcMap := template.FuncMap{ - "lowerTitle": lowerTitle, - "imports": r.Imports(settings), + "lowerTitle": LowerTitle, + "imports": Imports(r, settings), } - pkg := settings.Name - if pkg == "" { - pkg = filepath.Base(settings.Path) + pkgName := r.PkgName() + pkgConfig := settings.PackageMap[pkgName] + if pkgName == "" { + pkgName = filepath.Base(pkgConfig.Path) } dbFile := template.Must(template.New("table").Funcs(funcMap).Parse(dbTmpl)) @@ -1079,14 +1109,14 @@ func Generate(r *Result, global GenerateSettings, settings PackageSettings) (map sqlFile := template.Must(template.New("table").Funcs(funcMap).Parse(sqlTmpl)) tctx := tmplCtx{ - Settings: global, - EmitPreparedQueries: settings.EmitPreparedQueries, - EmitJSONTags: settings.EmitJSONTags, + Settings: settings, + EmitPreparedQueries: pkgConfig.EmitPreparedQueries, + EmitJSONTags: pkgConfig.EmitJSONTags, Q: "`", - Package: pkg, - GoQueries: r.GoQueries(), - Enums: r.Enums(), - Structs: r.Structs(), + Package: pkgName, + GoQueries: r.GoQueries(settings), + Enums: r.Enums(settings), + Structs: r.Structs(settings), } output := map[string]string{} @@ -1120,7 +1150,7 @@ func Generate(r *Result, global GenerateSettings, settings PackageSettings) (map } files := map[string]struct{}{} - for _, gq := range r.GoQueries() { + for _, gq := range r.GoQueries(settings) { files[gq.SourceName] = struct{}{} } diff --git a/internal/dinosql/gen_test.go b/internal/dinosql/gen_test.go index b63dec1611..312f782151 100644 --- a/internal/dinosql/gen_test.go +++ b/internal/dinosql/gen_test.go @@ -1,6 +1,7 @@ package dinosql import ( + "path/filepath" "testing" "github.com/google/go-cmp/cmp" @@ -52,8 +53,6 @@ func TestColumnsToStruct(t *testing.T) { cols[i].Table = pg.FQN{Schema: "public", Rel: "foo"} } - r := Result{} - // set up column-based override test o := Override{ GoType: "example.com/pkg.CustomType", @@ -68,11 +67,16 @@ func TestColumnsToStruct(t *testing.T) { } oa.Parse() - r.packageSettings = PackageSettings{ + pkgName := "test_override" + + r := Result{ + packageName: pkgName, + } + mockSettings.PackageMap[pkgName] = PackageSettings{ Overrides: []Override{o, oa}, } - actual := r.columnsToStruct("Foo", cols) + actual := r.columnsToStruct("Foo", cols, mockSettings) expected := &GoStruct{ Name: "Foo", Fields: []GoField{ @@ -90,8 +94,33 @@ func TestColumnsToStruct(t *testing.T) { } } +var mockSettings GenerateSettings + +func init() { + mockSettings = GenerateSettings{ + Version: "1", + Packages: []PackageSettings{ + PackageSettings{ + Name: "db", + }, + PackageSettings{ + Name: "prepared", + Queries: filepath.Join("testdata", "ondeck", "query"), + EmitPreparedQueries: true, + }, + PackageSettings{ + Name: "ondeck", + Queries: filepath.Join("testdata", "ondeck", "query"), + EmitJSONTags: true, + }, + }, + Overrides: []Override{}, + } + mockSettings.PopulatePkgMap() +} + func TestInnerType(t *testing.T) { - r := Result{} + r := Result{packageName: "db"} types := map[string]string{ // Numeric Types // https://www.postgresql.org/docs/current/datatype-numeric.html @@ -118,15 +147,15 @@ func TestInnerType(t *testing.T) { goType := v t.Run(k+"-"+v, func(t *testing.T) { col := pg.Column{DataType: dbType, NotNull: true} - if goType != r.goType(col) { - t.Errorf("expected Go type for %s to be %s, not %s", dbType, goType, r.goType(col)) + if goType != r.goType(col, mockSettings) { + t.Errorf("expected Go type for %s to be %s, not %s", dbType, goType, r.goType(col, mockSettings)) } }) } } func TestNullInnerType(t *testing.T) { - r := Result{} + r := Result{packageName: "db"} types := map[string]string{ // Numeric Types // https://www.postgresql.org/docs/current/datatype-numeric.html @@ -153,8 +182,8 @@ func TestNullInnerType(t *testing.T) { goType := v t.Run(k+"-"+v, func(t *testing.T) { col := pg.Column{DataType: dbType, NotNull: false} - if goType != r.goType(col) { - t.Errorf("expected Go type for %s to be %s, not %s", dbType, goType, r.goType(col)) + if goType != r.goType(col, mockSettings) { + t.Errorf("expected Go type for %s to be %s, not %s", dbType, goType, r.goType(col, mockSettings)) } }) } diff --git a/internal/dinosql/parser.go b/internal/dinosql/parser.go index 52a8b28d2a..03c29d6546 100644 --- a/internal/dinosql/parser.go +++ b/internal/dinosql/parser.go @@ -58,27 +58,26 @@ func (e *ParserErr) Error() string { return fmt.Sprintf("multiple errors: %d errors", len(e.Errs)) } -func ParseCatalog(schema string) (core.Catalog, error) { - f, err := os.Stat(schema) +func ReadSQLFiles(path string) ([]string, error) { + f, err := os.Stat(path) if err != nil { - return core.Catalog{}, fmt.Errorf("path %s does not exist", schema) + return nil, fmt.Errorf("path %s does not exist", path) } var files []string if f.IsDir() { - listing, err := ioutil.ReadDir(schema) + listing, err := ioutil.ReadDir(path) if err != nil { - return core.Catalog{}, err + return nil, err } for _, f := range listing { - files = append(files, filepath.Join(schema, f.Name())) + files = append(files, filepath.Join(path, f.Name())) } } else { - files = append(files, schema) + files = append(files, path) } - merr := NewParserErr() - c := core.NewCatalog() + var sql []string for _, filename := range files { if !strings.HasSuffix(filename, ".sql") { continue @@ -86,6 +85,20 @@ func ParseCatalog(schema string) (core.Catalog, error) { if strings.HasPrefix(filepath.Base(filename), ".") { continue } + sql = append(sql, filename) + } + return sql, nil +} + +func ParseCatalog(schema string) (core.Catalog, error) { + files, err := ReadSQLFiles(schema) + if err != nil { + return core.Catalog{}, err + } + + merr := NewParserErr() + c := core.NewCatalog() + for _, filename := range files { blob, err := ioutil.ReadFile(filename) if err != nil { merr.Add(filename, "", 0, err) @@ -171,17 +184,19 @@ type Query struct { } type Result struct { - Settings GenerateSettings - Queries []*Query - Catalog core.Catalog - - // XXX: this is hack so that all of the functions used during Generate can access - // package settings during that process without threading them through every function - // call. we should probably have another type just for generation instead of reusing Result - packageSettings PackageSettings + Queries []*Query + Catalog core.Catalog + packageName string } -func ParseQueries(c core.Catalog, settings GenerateSettings, pkg PackageSettings) (*Result, error) { +func (r Result) PkgName() string { + if r.packageName == "" { + panic("Package name is empty") + } + return r.packageName +} + +func ParseQueries(c core.Catalog, pkg PackageSettings) (*Result, error) { f, err := os.Stat(pkg.Queries) if err != nil { return nil, fmt.Errorf("path %s does not exist", pkg.Queries) @@ -249,7 +264,11 @@ func ParseQueries(c core.Catalog, settings GenerateSettings, pkg PackageSettings if len(q) == 0 { return nil, fmt.Errorf("path %s contains no queries", pkg.Queries) } - return &Result{Catalog: c, Queries: q, Settings: settings}, nil + return &Result{ + Catalog: c, + Queries: q, + packageName: pkg.Name, + }, nil } func location(node nodes.Node) int { diff --git a/internal/dinosql/parser_test.go b/internal/dinosql/parser_test.go index 398a18c1f6..757165cf52 100644 --- a/internal/dinosql/parser_test.go +++ b/internal/dinosql/parser_test.go @@ -164,19 +164,12 @@ func TestParseSchema(t *testing.T) { t.Fatal(err) } - q, err := ParseQueries(c, GenerateSettings{}, PackageSettings{ - Queries: filepath.Join("testdata", "ondeck", "query"), - EmitJSONTags: true, - }) - if err != nil { - t.Fatal(err) - } - t.Run("default", func(t *testing.T) { - output, err := Generate(q, GenerateSettings{}, PackageSettings{ - Name: "ondeck", - EmitJSONTags: true, - }) + q, err := ParseQueries(c, mockSettings.PackageMap["ondeck"]) + if err != nil { + t.Fatal(err) + } + output, err := Generate(q, mockSettings) if err != nil { t.Fatal(err) } @@ -185,10 +178,11 @@ func TestParseSchema(t *testing.T) { }) t.Run("prepared", func(t *testing.T) { - output, err := Generate(q, GenerateSettings{}, PackageSettings{ - Name: "prepared", - EmitPreparedQueries: true, - }) + q, err := ParseQueries(c, mockSettings.PackageMap["prepared"]) + if err != nil { + t.Fatal(err) + } + output, err := Generate(q, mockSettings) if err != nil { t.Fatal(err) } diff --git a/internal/mysql/README.md b/internal/mysql/README.md new file mode 100644 index 0000000000..f831ec269b --- /dev/null +++ b/internal/mysql/README.md @@ -0,0 +1,13 @@ +# MySQL Support + +## Example Usage + +``` +$ cd ./example +$ sqlc unstable__mysql generate +``` + +## Missing Features + +- support for the `queries` field being specified as a directory of files +- missing many MySQL types and function returns types diff --git a/internal/mysql/example/.gitignore b/internal/mysql/example/.gitignore new file mode 100644 index 0000000000..472fecd969 --- /dev/null +++ b/internal/mysql/example/.gitignore @@ -0,0 +1 @@ +*.go \ No newline at end of file diff --git a/internal/mysql/example/queries.sql b/internal/mysql/example/queries.sql new file mode 100644 index 0000000000..ff927fba39 --- /dev/null +++ b/internal/mysql/example/queries.sql @@ -0,0 +1,35 @@ +CREATE TABLE teachers ( + id int NOT NULL, + first_name varchar(255), + last_name varchar(255), + school_id int NOT NULL, + class_id int NOT NULL, + school_lat FLOAT, + school_lng FLOAT, + department ENUM("English", "Math"), + PRIMARY KEY (id) +); + +CREATE TABLE students ( + id int NOT NULL, + class_id int NOT NULL, + first_name varchar(255), + last_name varchar(255), + PRIMARY KEY (id) +) + +/* name: GetTeachersByID :one */ +SELECT * FROM teachers WHERE id = ? + +/* name: GetSomeTeachers :one */ +SELECT school_id, id FROM teachers WHERE school_lng > ? AND school_lat < ?; + +/* name: TeachersByID :one */ +SELECT id, school_lat FROM teachers WHERE id = ? LIMIT 10 + +/* name: GetStudentsTeacher :one */ +SELECT students.first_name, students.last_name, teachers.first_name teacherFirstName, +teachers.id teacher_id + FROM students + Left JOIN teachers on teachers.class_id = students.class_id + WHERE students.id = :studentID diff --git a/internal/mysql/example/sqlc.json b/internal/mysql/example/sqlc.json new file mode 100644 index 0000000000..223bd64de1 --- /dev/null +++ b/internal/mysql/example/sqlc.json @@ -0,0 +1,14 @@ +{ + "version": "1", + "packages": [ + { + "name": "teachersDB", + "database": "mysql", + "emit_json_tags": true, + "emit_prepared_queries": false, + "path": "./", + "queries": "./queries.sql", + "schema": "./queries.sql" + } + ] +} diff --git a/internal/mysql/functions.go b/internal/mysql/functions.go new file mode 100644 index 0000000000..01d6a53aaf --- /dev/null +++ b/internal/mysql/functions.go @@ -0,0 +1,17 @@ +package mysql + +import ( + "fmt" +) + +// converts MySQL function name to MySQL return type +func functionReturnType(f string) string { + switch f { + case "avg", "count", "instr", "sum", "min", "max", "length", "char_length": + return "int" + case "concat", "left", "replace", "substring", "trim", "find_in_set", "format", "group_concat": + return "varchar" + default: + panic(fmt.Sprintf("unknown mysql function type [%v]", f)) + } +} diff --git a/internal/mysql/gen.go b/internal/mysql/gen.go new file mode 100644 index 0000000000..9d6d24e7d9 --- /dev/null +++ b/internal/mysql/gen.go @@ -0,0 +1,271 @@ +package mysql + +import ( + "fmt" + "log" + "sort" + "strings" + + "github.com/jinzhu/inflection" + "github.com/kyleconroy/sqlc/internal/dinosql" + "vitess.io/vitess/go/vt/sqlparser" +) + +// Result holds the mysql validated queries schema +type Result struct { + Queries []*Query + Schema *Schema + packageName string +} + +// PkgName exposes the result set's associated go package identifier as specified in the sqlc.json config. +func (r *Result) PkgName() string { + return r.packageName +} + +// Enums generates parser-agnostic GoEnum types +func (r *Result) Enums(settings dinosql.GenerateSettings) []dinosql.GoEnum { + var enums []dinosql.GoEnum + for _, table := range r.Schema.tables { + for _, col := range table { + if col.Type.Type == "enum" { + constants := []dinosql.GoConstant{} + enumName := enumNameFromColDef(col, settings) + for _, c := range col.Type.EnumValues { + stripped := stripInnerQuotes(c) + constants = append(constants, dinosql.GoConstant{ + // TODO: maybe add the struct name call to capitalize the name here + Name: stripped, + Value: stripped, + Type: enumName, + }) + } + + goEnum := dinosql.GoEnum{ + Name: enumName, + Comment: "", + Constants: constants, + } + enums = append(enums, goEnum) + } + } + } + return enums +} + +func stripInnerQuotes(identifier string) string { + return strings.Replace(identifier, "'", "", 2) +} + +func enumNameFromColDef(col *sqlparser.ColumnDefinition, settings dinosql.GenerateSettings) string { + return fmt.Sprintf("%sType", + dinosql.StructName(col.Name.String(), settings)) +} + +// Structs marshels each query into a go struct for generation +func (r *Result) Structs(settings dinosql.GenerateSettings) []dinosql.GoStruct { + var structs []dinosql.GoStruct + for tableName, cols := range r.Schema.tables { + s := dinosql.GoStruct{ + Name: inflection.Singular(dinosql.StructName(tableName, settings)), + } + + for _, col := range cols { + s.Fields = append(s.Fields, dinosql.GoField{ + Name: dinosql.StructName(col.Name.String(), settings), + Type: goTypeCol(col, settings), + Tags: map[string]string{"json:": col.Name.String()}, + Comment: "", + }) + } + structs = append(structs, s) + } + + return structs +} + +// GoQueries generates parser-agnostic query information for code generation +func (r *Result) GoQueries(settings dinosql.GenerateSettings) []dinosql.GoQuery { + structs := r.Structs(settings) + + qs := make([]dinosql.GoQuery, 0, len(r.Queries)) + for ix, query := range r.Queries { + if query == nil { + panic(fmt.Sprintf("query is nil on index: %v, len: %v", ix, len(r.Queries))) + } + if query.Name == "" { + continue + } + if query.Cmd == "" { + continue + } + + gq := dinosql.GoQuery{ + Cmd: query.Cmd, + ConstantName: dinosql.LowerTitle(query.Name), + FieldName: dinosql.LowerTitle(query.Name) + "Stmt", + MethodName: query.Name, + SourceName: query.Filename, + SQL: query.SQL, + // Comments: query.Comments, + } + + if len(query.Params) == 1 { + p := query.Params[0] + gq.Arg = dinosql.GoQueryValue{ + Name: p.Name, + Typ: p.Typ, + } + } else if len(query.Params) > 1 { + + structInfo := make([]structParams, len(query.Params)) + for i := range query.Params { + structInfo[i] = structParams{ + originalName: query.Params[i].Name, + goType: query.Params[i].Typ, + } + } + + gq.Arg = dinosql.GoQueryValue{ + Emit: true, + Name: "arg", + Struct: r.columnsToStruct(gq.MethodName+"Params", structInfo, settings), + } + } + + if len(query.Columns) == 1 { + c := query.Columns[0] + gq.Ret = dinosql.GoQueryValue{ + Name: columnName(c, 0), + Typ: goTypeCol(c, settings), + } + } else if len(query.Columns) > 1 { + var gs *dinosql.GoStruct + var emit bool + + for _, s := range structs { + if len(s.Fields) != len(query.Columns) { + continue + } + same := true + for i, f := range s.Fields { + c := query.Columns[i] + sameName := f.Name == dinosql.StructName(columnName(c, i), settings) + sameType := f.Type == goTypeCol(c, settings) + // TODO: consider making this deep equality from stdlib? + // sameFQN := s.Table.EqualTo(&c.Table) + if !sameName || !sameType || true { // !sameFQN + same = false + } + } + if same { + gs = &s + break + } + } + + if gs == nil { + structInfo := make([]structParams, len(query.Columns)) + for i := range query.Columns { + structInfo[i] = structParams{ + originalName: query.Columns[i].Name.String(), + goType: goTypeCol(query.Columns[i], settings), + } + } + gs = r.columnsToStruct(gq.MethodName+"Row", structInfo, settings) + emit = true + } + gq.Ret = dinosql.GoQueryValue{ + Emit: emit, + Name: "i", + Struct: gs, + } + } + + qs = append(qs, gq) + } + sort.Slice(qs, func(i, j int) bool { return qs[i].MethodName < qs[j].MethodName }) + return qs +} + +type structParams struct { + originalName string + goType string +} + +func (r *Result) columnsToStruct(name string, items []structParams, settings dinosql.GenerateSettings) *dinosql.GoStruct { + gs := dinosql.GoStruct{ + Name: name, + } + seen := map[string]int{} + for _, item := range items { + name := item.originalName + typ := item.goType + tagName := name + fieldName := dinosql.StructName(name, settings) + if v := seen[name]; v > 0 { + tagName = fmt.Sprintf("%s_%d", tagName, v+1) + fieldName = fmt.Sprintf("%s_%d", fieldName, v+1) + } + gs.Fields = append(gs.Fields, dinosql.GoField{ + Name: fieldName, + Type: typ, + Tags: map[string]string{"json:": tagName}, + }) + seen[name]++ + } + return &gs +} + +func goTypeCol(col *sqlparser.ColumnDefinition, settings dinosql.GenerateSettings) string { + switch t := col.Type.Type; { + case "varchar" == t, "text" == t: + if col.Type.NotNull { + return "string" + } + return "sql.NullString" + case "int" == t, "integer" == t: + if col.Type.NotNull { + return "int" + } + return "sql.NullInt64" + case "float" == t, strings.HasPrefix(strings.ToLower(t), "decimal"): + if col.Type.NotNull { + return "float64" + } + return "sql.NullFloat64" + case "enum" == t: + return enumNameFromColDef(col, settings) + case "date" == t, "timestamp" == t, "datetime" == t: + if col.Type.NotNull { + return "time.Time" + } + return "sql.NullTime" + case "boolean" == t: + return "bool" + default: + log.Printf("unknown MySQL type: %s\n", t) + return "interface{}" + } +} + +func columnName(c *sqlparser.ColumnDefinition, pos int) string { + if !c.Name.IsEmpty() { + return c.Name.String() + } + return fmt.Sprintf("column_%d", pos+1) +} + +func argName(name string) string { + out := "" + for i, p := range strings.Split(name, "_") { + if i == 0 { + out += strings.ToLower(p) + } else if p == "id" { + out += "ID" + } else { + out += strings.Title(p) + } + } + return out +} diff --git a/internal/mysql/param.go b/internal/mysql/param.go new file mode 100644 index 0000000000..021f074c48 --- /dev/null +++ b/internal/mysql/param.go @@ -0,0 +1,145 @@ +package mysql + +import ( + "fmt" + "regexp" + "strings" + + "github.com/kyleconroy/sqlc/internal/dinosql" + "vitess.io/vitess/go/vt/sqlparser" +) + +// Param describes a runtime query parameter with its +// associated type. Example: "SELECT name FROM users id = ?" +type Param struct { + OriginalName string + Name string + Typ string +} + +func paramsInLimitExpr(limit *sqlparser.Limit, s *Schema, tableAliasMap FromTables, settings dinosql.GenerateSettings) ([]*Param, error) { + params := []*Param{} + if limit == nil { + return params, nil + } + + parseLimitSubExp := func(node sqlparser.Expr) { + switch v := node.(type) { + case *sqlparser.SQLVal: + if v.Type == sqlparser.ValArg { + params = append(params, &Param{ + OriginalName: string(v.Val), + Name: "limit", + Typ: "uint32", + }) + } + } + } + + parseLimitSubExp(limit.Offset) + parseLimitSubExp(limit.Rowcount) + + return params, nil +} + +func paramsInWhereExpr(e sqlparser.SQLNode, s *Schema, tableAliasMap FromTables, defaultTable string, settings dinosql.GenerateSettings) ([]*Param, error) { + params := []*Param{} + switch v := e.(type) { + case *sqlparser.Where: + if v == nil { + return params, nil + } + return paramsInWhereExpr(v.Expr, s, tableAliasMap, defaultTable, settings) + case *sqlparser.ComparisonExpr: + p, found, err := paramInComparison(v, s, tableAliasMap, defaultTable, settings) + if err != nil { + return nil, err + } + if found { + params = append(params, p) + } + case *sqlparser.AndExpr: + left, err := paramsInWhereExpr(v.Left, s, tableAliasMap, defaultTable, settings) + if err != nil { + return nil, err + } + params = append(params, left...) + right, err := paramsInWhereExpr(v.Right, s, tableAliasMap, defaultTable, settings) + if err != nil { + return nil, err + } + params = append(params, right...) + case *sqlparser.OrExpr: + left, err := paramsInWhereExpr(v.Left, s, tableAliasMap, defaultTable, settings) + if err != nil { + return nil, err + } + params = append(params, left...) + right, err := paramsInWhereExpr(v.Right, s, tableAliasMap, defaultTable, settings) + if err != nil { + return nil, err + } + params = append(params, right...) + case *sqlparser.IsExpr: + // TODO: see if there is a use case for params in IS expressions + return []*Param{}, nil + default: + panic(fmt.Sprintf("Failed to handle %T in where", v)) + } + + return params, nil +} + +func paramInComparison(cond *sqlparser.ComparisonExpr, s *Schema, tableAliasMap FromTables, defaultTable string, settings dinosql.GenerateSettings) (*Param, bool, error) { + p := &Param{} + var colIdent sqlparser.ColIdent + walker := func(node sqlparser.SQLNode) (bool, error) { + switch v := node.(type) { + case *sqlparser.ColName: + colDfn, err := s.getColType(v, tableAliasMap, defaultTable) + if err != nil { + return false, err + } + p.Typ = goTypeCol(colDfn, settings) + colIdent = colDfn.Name + + case *sqlparser.SQLVal: + if v.Type == sqlparser.ValArg { + p.OriginalName = string(v.Val) + } + } + return true, nil + } + err := sqlparser.Walk(walker, cond) + if err != nil { + return nil, false, err + } + if p.OriginalName != "" && p.Typ != "" { + p.Name = paramName(colIdent, p.OriginalName) + return p, true, nil + } + return nil, false, nil +} + +func paramName(col sqlparser.ColIdent, originalName string) string { + str := col.String() + if !strings.HasPrefix(originalName, ":v") { + return originalName[1:] + } + if str != "" { + return str + } + num := originalName[2] + return fmt.Sprintf("param%v", num) +} + +func replaceParamStrs(query string, params []*Param) (string, error) { + for _, p := range params { + re, err := regexp.Compile(fmt.Sprintf("(%v)", p.OriginalName)) + if err != nil { + return "", err + } + query = re.ReplaceAllString(query, "?") + } + return query, nil +} diff --git a/internal/mysql/param_test.go b/internal/mysql/param_test.go new file mode 100644 index 0000000000..40c7268c18 --- /dev/null +++ b/internal/mysql/param_test.go @@ -0,0 +1,165 @@ +package mysql + +import ( + "reflect" + "testing" + + "github.com/davecgh/go-spew/spew" + "vitess.io/vitess/go/vt/sqlparser" +) + +func TestSelectParamSearcher(t *testing.T) { + type testCase struct { + input string + output []*Param + } + + tests := []testCase{ + testCase{ + input: "SELECT first_name, id, last_name FROM users WHERE id < ?", + output: []*Param{&Param{ + OriginalName: ":v1", + Name: "id", + Typ: "int", + }, + }, + }, + testCase{ + input: `SELECT + users.id, + users.first_name, + orders.price + FROM + orders + LEFT JOIN users ON orders.user_id = users.id + WHERE orders.price > :minPrice`, + output: []*Param{ + &Param{ + OriginalName: ":minPrice", + Name: "minPrice", + Typ: "float64", + }, + }, + }, + testCase{ + input: "SELECT first_name, id, last_name FROM users WHERE id = :targetID", + output: []*Param{&Param{ + OriginalName: ":targetID", + Name: "targetID", + Typ: "int", + }, + }, + }, + testCase{ + input: "SELECT first_name, last_name FROM users WHERE age < :maxAge AND last_name = :inFamily", + output: []*Param{ + &Param{ + OriginalName: ":maxAge", + Name: "maxAge", + Typ: "int", + }, + &Param{ + OriginalName: ":inFamily", + Name: "inFamily", + Typ: "sql.NullString", + }, + }, + }, + testCase{ + input: "SELECT first_name, last_name FROM users LIMIT ?", + output: []*Param{ + &Param{ + OriginalName: ":v1", + Name: "limit", + Typ: "uint32", + }, + }, + }, + } + for _, tCase := range tests { + tree, err := sqlparser.Parse(tCase.input) + if err != nil { + t.Errorf("Failed to parse input query") + } + selectStm, ok := tree.(*sqlparser.Select) + + tableAliasMap, err := parseFrom(selectStm.From, false) + if err != nil { + t.Errorf("Failed to parse table name alias's: %v", err) + } + + limitParams, err := paramsInLimitExpr(selectStm.Limit, mockSchema, tableAliasMap, mockSettings) + if err != nil { + t.Errorf("Failed to parse limit expression params: %v", err) + } + whereParams, err := paramsInWhereExpr(selectStm.Where, mockSchema, tableAliasMap, "users", mockSettings) + if err != nil { + t.Errorf("Failed to parse where expression params: %v", err) + } + + params := append(limitParams, whereParams...) + if !ok { + t.Errorf("Test case is not SELECT statement as expected") + } + + if !reflect.DeepEqual(params, tCase.output) { + t.Errorf("Param searcher returned unexpected result\nResult: %v\nExpected: %v", + spew.Sdump(params), spew.Sdump(tCase.output)) + } + } +} + +func TestInsertParamSearcher(t *testing.T) { + type testCase struct { + input string + output []*Param + expectedNames []string + } + + tests := []testCase{ + testCase{ + input: "INSERT INTO users (first_name, last_name) VALUES (?, ?)", + output: []*Param{ + &Param{ + OriginalName: ":v1", + Name: "first_name", + Typ: "string", + }, + &Param{ + OriginalName: ":v2", + Name: "last_name", + Typ: "sql.NullString", + }, + }, + expectedNames: []string{"first_name", "last_name"}, + }, + } + for _, tCase := range tests { + tree, err := sqlparser.Parse(tCase.input) + if err != nil { + t.Errorf("Failed to parse input query") + } + insertStm, ok := tree.(*sqlparser.Insert) + if !ok { + t.Errorf("Test case is not SELECT statement as expected") + } + result, err := parseInsert(insertStm, tCase.input, mockSchema, mockSettings) + if err != nil { + t.Errorf("Failed to parse insert statement.") + } + + if !reflect.DeepEqual(result.Params, tCase.output) { + t.Errorf("Param searcher returned unexpected result\nResult: %v\nExpected: %v\nQuery: %s", + spew.Sdump(result.Params), spew.Sdump(tCase.output), tCase.input) + } + if len(result.Params) != len(tCase.expectedNames) { + t.Errorf("Insufficient test cases. Mismatch in length of expected param names and parsed params") + } + for ix, p := range result.Params { + if p.Name != tCase.expectedNames[ix] { + t.Errorf("Derived param does not match expected output.\nResult: %v\nExpected: %v", + p.Name, tCase.expectedNames[ix]) + } + } + } +} diff --git a/internal/mysql/parse.go b/internal/mysql/parse.go new file mode 100644 index 0000000000..0a33ed6915 --- /dev/null +++ b/internal/mysql/parse.go @@ -0,0 +1,469 @@ +package mysql + +import ( + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + "strings" + + "github.com/davecgh/go-spew/spew" + "github.com/kyleconroy/sqlc/internal/dinosql" + "vitess.io/vitess/go/vt/sqlparser" +) + +// Query holds the data for walking and validating mysql querys +type Query struct { + SQL string // the string representation of the parsed query + Columns []*sqlparser.ColumnDefinition // definitions for all columns returned by this query + Params []*Param // "?" params in the query string + Name string // the Go function name + Cmd string // TODO: Pick a better name. One of: one, many, exec, execrows + DefaultTableName string // for columns that are not qualified + SchemaLookup *Schema // for validation and conversion to Go types + + Filename string +} + +func parsePath(sqlPath string, inPkg string, s *Schema, settings dinosql.GenerateSettings) (*Result, error) { + files, err := dinosql.ReadSQLFiles(sqlPath) + if err != nil { + return nil, err + } + + parsedQueries := []*Query{} + for _, filename := range files { + file, err := os.Open(filename) + if err != nil { + return nil, fmt.Errorf("Failed to open file [%v]: %v", filename, err) + } + contents, err := ioutil.ReadAll(file) + if err != nil { + return nil, fmt.Errorf("Failed to read contents of file [%v]: %v", filename, err) + } + queries, err := parseContents(filename, string(contents), s, settings) + if err != nil { + return nil, err + } + parsedQueries = append(parsedQueries, queries...) + } + + return &Result{ + Queries: parsedQueries, + Schema: s, + packageName: inPkg, + }, nil +} + +func parseContents(filename, contents string, s *Schema, settings dinosql.GenerateSettings) ([]*Query, error) { + t := sqlparser.NewStringTokenizer(contents) + var queries []*Query + var start int + for { + q, err := sqlparser.ParseNextStrictDDL(t) + if err == io.EOF { + break + } else if err != nil { + return nil, err + } + query := contents[start : t.Position-1] + result, err := parseQueryString(q, query, s, settings) + if err != nil { + return nil, fmt.Errorf("Failed to parse query in filepath [%v]: %v", filename, err) + } + start = t.Position + if result == nil { + continue + } + result.Filename = filepath.Base(filename) + queries = append(queries, result) + } + return queries, nil +} + +func parseQueryString(tree sqlparser.Statement, query string, s *Schema, settings dinosql.GenerateSettings) (*Query, error) { + var parsedQuery *Query + switch tree := tree.(type) { + case *sqlparser.Select: + selectQuery, err := parseSelect(tree, query, s, settings) + if err != nil { + return nil, fmt.Errorf("Failed to parse SELECT query: %v", err) + } + parsedQuery = selectQuery + case *sqlparser.Insert: + insert, err := parseInsert(tree, query, s, settings) + if err != nil { + return nil, fmt.Errorf("Failed to parse INSERT query: %v", err) + } + parsedQuery = insert + case *sqlparser.Update: + update, err := parseUpdate(tree, query, s, settings) + if err != nil { + return nil, fmt.Errorf("Failed to parse UPDATE query: %v", err) + } + parsedQuery = update + case *sqlparser.Delete: + delete, err := parseDelete(tree, query, s, settings) + delete.SchemaLookup = nil + if err != nil { + return nil, fmt.Errorf("Failed to parse DELETE query: %v", err) + } + parsedQuery = delete + case *sqlparser.DDL: + s.Add(tree) + return nil, nil + default: + // panic("Unsupported SQL statement type") + return nil, nil + } + paramsReplacedQuery, err := replaceParamStrs(sqlparser.String(tree), parsedQuery.Params) + if err != nil { + return nil, fmt.Errorf("Failed to replace param variables in query string: %v", err) + } + parsedQuery.SQL = paramsReplacedQuery + return parsedQuery, nil +} + +func (q *Query) parseNameAndCmd() error { + if q == nil { + return fmt.Errorf("Cannot parse name and cmd from null query") + } + _, comments := sqlparser.SplitMarginComments(q.SQL) + err := q.parseLeadingComment(comments.Leading) + if err != nil { + return fmt.Errorf("Failed to parse leading comment %v", err) + } + return nil +} + +func parseSelect(tree *sqlparser.Select, query string, s *Schema, settings dinosql.GenerateSettings) (*Query, error) { + tableAliasMap, err := parseFrom(tree.From, false) + if err != nil { + return nil, fmt.Errorf("Failed to parse table name alias's: %v", err) + } + defaultTableName := getDefaultTable(tableAliasMap) + + // handle * expressions first by expanding all columns of the default table + _, ok := tree.SelectExprs[0].(*sqlparser.StarExpr) + if ok { + colNames := []sqlparser.SelectExpr{} + colDfns := s.tables[defaultTableName] + for _, col := range colDfns { + colNames = append(colNames, &sqlparser.AliasedExpr{ + Expr: &sqlparser.ColName{ + Name: col.Name, + }}, + ) + } + tree.SelectExprs = colNames + } + + parsedQuery := Query{ + SQL: query, + DefaultTableName: defaultTableName, + SchemaLookup: s, + } + cols, err := parseSelectAliasExpr(tree.SelectExprs, s, tableAliasMap, defaultTableName) + if err != nil { + return nil, err + } + parsedQuery.Columns = cols + + whereParams, err := paramsInWhereExpr(tree.Where, s, tableAliasMap, defaultTableName, settings) + if err != nil { + return nil, err + } + + limitParams, err := paramsInLimitExpr(tree.Limit, s, tableAliasMap, settings) + if err != nil { + return nil, err + } + parsedQuery.Params = append(whereParams, limitParams...) + + err = parsedQuery.parseNameAndCmd() + if err != nil { + return nil, err + } + + return &parsedQuery, nil +} + +// FromTable describes a table reference in the "FROM" clause of a query. +type FromTable struct { + TrueName string // the true table name as described in the schema + IsLeftJoined bool // which could result in null columns +} + +// FromTables describes a map between table alias expressions and the +// proper table name +type FromTables map[string]FromTable + +func parseFrom(from sqlparser.TableExprs, isLeftJoined bool) (FromTables, error) { + tables := make(map[string]FromTable) + for _, expr := range from { + switch v := expr.(type) { + case *sqlparser.AliasedTableExpr: + name, ok := v.Expr.(sqlparser.TableName) + if !ok { + return nil, fmt.Errorf("Failed to parse AliasedTableExpr name: %v", spew.Sdump(v)) + } + t := FromTable{ + TrueName: name.Name.String(), + IsLeftJoined: isLeftJoined, + } + if v.As.String() != "" { + tables[v.As.String()] = t + } else { + tables[name.Name.String()] = t + } + case *sqlparser.JoinTableExpr: + isLeftJoin := v.Join == "left join" + left, err := parseFrom([]sqlparser.TableExpr{v.LeftExpr}, false) + if err != nil { + return nil, err + } + right, err := parseFrom([]sqlparser.TableExpr{v.RightExpr}, isLeftJoin) + if err != nil { + return nil, err + } + // merge the left and right maps + for k, v := range left { + right[k] = v + } + return right, nil + default: + return nil, fmt.Errorf("Failed to parse table expr: %v", spew.Sdump(v)) + } + } + return tables, nil +} + +func getDefaultTable(tableAliasMap FromTables) string { + if len(tableAliasMap) != 1 { + return "" + } + for _, val := range tableAliasMap { + return val.TrueName + } + panic("Should never be reached.") +} + +func parseUpdate(node *sqlparser.Update, query string, s *Schema, settings dinosql.GenerateSettings) (*Query, error) { + tableAliasMap, err := parseFrom(node.TableExprs, false) + if err != nil { + return nil, fmt.Errorf("Failed to parse table name alias's: %v", err) + } + defaultTable := getDefaultTable(tableAliasMap) + if err != nil { + return nil, err + } + + params := []*Param{} + for _, updateExpr := range node.Exprs { + col := updateExpr.Name + newValue, isParam := updateExpr.Expr.(*sqlparser.SQLVal) + if !isParam { + continue + } + colDfn, err := s.getColType(col, tableAliasMap, defaultTable) + if err != nil { + return nil, fmt.Errorf("Failed to determine type of a parameter's column: %v", err) + } + originalParamName := string(newValue.Val) + param := Param{ + OriginalName: originalParamName, + Name: paramName(colDfn.Name, originalParamName), + Typ: goTypeCol(colDfn, settings), + } + params = append(params, ¶m) + } + + whereParams, err := paramsInWhereExpr(node.Where.Expr, s, tableAliasMap, defaultTable, settings) + if err != nil { + return nil, fmt.Errorf("Failed to parse params from WHERE expression: %v", err) + } + + parsedQuery := Query{ + SQL: query, + Columns: nil, + Params: append(params, whereParams...), + DefaultTableName: defaultTable, + SchemaLookup: s, + } + parsedQuery.parseNameAndCmd() + + return &parsedQuery, nil +} + +func parseInsert(node *sqlparser.Insert, query string, s *Schema, settings dinosql.GenerateSettings) (*Query, error) { + cols := node.Columns + tableName := node.Table.Name.String() + rows, ok := node.Rows.(sqlparser.Values) + if !ok { + return nil, fmt.Errorf("Unknown insert row type of %T", node.Rows) + } + + params := []*Param{} + + for _, row := range rows { + for colIx, item := range row { + switch v := item.(type) { + case *sqlparser.SQLVal: + if v.Type == sqlparser.ValArg { + colName := cols[colIx].String() + colDfn, err := s.schemaLookup(tableName, colName) + varName := string(v.Val) + p := &Param{OriginalName: varName} + if err == nil { + p.Name = paramName(colDfn.Name, varName) + p.Typ = goTypeCol(colDfn, settings) + } else { + p.Name = "Unknown" + p.Typ = "interface{}" + } + params = append(params, p) + } + default: + panic("Error occurred in parsing INSERT statement") + } + } + } + parsedQuery := &Query{ + SQL: query, + Params: params, + Columns: nil, + DefaultTableName: tableName, + SchemaLookup: s, + } + parsedQuery.parseNameAndCmd() + return parsedQuery, nil +} + +func parseDelete(node *sqlparser.Delete, query string, s *Schema, settings dinosql.GenerateSettings) (*Query, error) { + tableAliasMap, err := parseFrom(node.TableExprs, false) + if err != nil { + return nil, fmt.Errorf("Failed to parse table name alias's: %v", err) + } + defaultTableName := getDefaultTable(tableAliasMap) + if err != nil { + return nil, err + } + + whereParams, err := paramsInWhereExpr(node.Where, s, tableAliasMap, defaultTableName, settings) + if err != nil { + return nil, err + } + + limitParams, err := paramsInLimitExpr(node.Limit, s, tableAliasMap, settings) + if err != nil { + return nil, err + } + parsedQuery := &Query{ + SQL: query, + Params: append(whereParams, limitParams...), + Columns: nil, + DefaultTableName: defaultTableName, + SchemaLookup: s, + } + err = parsedQuery.parseNameAndCmd() + if err != nil { + return nil, err + } + + return parsedQuery, nil +} + +func (q *Query) parseLeadingComment(comment string) error { + for _, line := range strings.Split(comment, "\n") { + if !strings.HasPrefix(line, "/* name:") { + continue + } + part := strings.Split(strings.TrimSpace(line), " ") + if len(part) == 3 { + return fmt.Errorf("missing query type [':one', ':many', ':exec', ':execrows']: %s", line) + } + if len(part) != 5 { + return fmt.Errorf("invalid query comment: %s", line) + } + queryName := part[2] + queryType := strings.TrimSpace(part[3]) + switch queryType { + case ":one", ":many", ":exec", ":execrows": + default: + return fmt.Errorf("invalid query type: %s", queryType) + } + // if err := validateQueryName(queryName); err != nil { + // return err + // } + q.Name = queryName + q.Cmd = queryType + } + return nil +} + +func parseSelectAliasExpr(exprs sqlparser.SelectExprs, s *Schema, tableAliasMap FromTables, defaultTable string) ([]*sqlparser.ColumnDefinition, error) { + colDfns := []*sqlparser.ColumnDefinition{} + for _, col := range exprs { + switch expr := col.(type) { + case *sqlparser.AliasedExpr: + hasAlias := !expr.As.IsEmpty() + + switch v := expr.Expr.(type) { + case *sqlparser.ColName: + res, err := s.getColType(v, tableAliasMap, defaultTable) + if err != nil { + panic(fmt.Sprintf("Column not found in schema: %v", err)) + } + if hasAlias { + res.Name = expr.As // applys the alias + } + colDfns = append(colDfns, res) + case *sqlparser.GroupConcatExpr: + colDfns = append(colDfns, &sqlparser.ColumnDefinition{ + Name: sqlparser.NewColIdent(expr.As.String()), + Type: sqlparser.ColumnType{ + Type: "varchar", + NotNull: true, + }, + }) + case *sqlparser.FuncExpr: + funcName := v.Name.Lowered() + funcType := functionReturnType(funcName) + + var returnVal sqlparser.ColIdent + if hasAlias { + returnVal = expr.As + } else { + returnVal = sqlparser.NewColIdent(funcName) + } + + colDfn := &sqlparser.ColumnDefinition{ + Name: returnVal, + Type: sqlparser.ColumnType{ + Type: funcType, + NotNull: true, + }, + } + colDfns = append(colDfns, colDfn) + } + default: + return nil, fmt.Errorf("Failed to handle select expr of type : %T", expr) + } + } + return colDfns, nil +} + +// GeneratePkg is the main entry to mysql generator package +func GeneratePkg(pkgName, schemaPath, querysPath string, settings dinosql.GenerateSettings) (*Result, error) { + s := NewSchema() + _, err := parsePath(schemaPath, pkgName, s, settings) + if err != nil { + return nil, fmt.Errorf("schema failure: %w", err) + } + result, err := parsePath(querysPath, pkgName, s, settings) + if err != nil { + return nil, fmt.Errorf("query failure: %w", err) + } + return result, nil +} diff --git a/internal/mysql/parse_test.go b/internal/mysql/parse_test.go new file mode 100644 index 0000000000..0a1b39ca7c --- /dev/null +++ b/internal/mysql/parse_test.go @@ -0,0 +1,428 @@ +package mysql + +import ( + "encoding/json" + "io/ioutil" + "reflect" + "testing" + + "github.com/davecgh/go-spew/spew" + "github.com/google/go-cmp/cmp" + "github.com/kyleconroy/sqlc/internal/dinosql" + "vitess.io/vitess/go/vt/sqlparser" +) + +func init() { + initMockSchema() +} + +const filename = "test_data/queries.sql" +const configPath = "test_data/sqlc.json" + +var mockSettings = dinosql.GenerateSettings{ + Version: "1", + Packages: []dinosql.PackageSettings{ + dinosql.PackageSettings{ + Name: "db", + }, + }, + Overrides: []dinosql.Override{}, +} + +func TestParseConfig(t *testing.T) { + blob, err := ioutil.ReadFile(configPath) + if err != nil { + t.Fatal(err) + } + + var settings dinosql.GenerateSettings + if err := json.Unmarshal(blob, &settings); err != nil { + t.Fatal(err) + } +} + +func TestGeneratePkg(t *testing.T) { + _, err := GeneratePkg(mockSettings.Packages[0].Name, filename, filename, mockSettings) + if err != nil { + t.Fatal(err) + } +} + +func keep(interface{}) {} + +var mockSchema *Schema + +func initMockSchema() { + var schemaMap = make(map[string][]*sqlparser.ColumnDefinition) + mockSchema = &Schema{ + tables: schemaMap, + } + schemaMap["users"] = []*sqlparser.ColumnDefinition{ + &sqlparser.ColumnDefinition{ + Name: sqlparser.NewColIdent("first_name"), + Type: sqlparser.ColumnType{ + Type: "varchar", + NotNull: true, + }, + }, + &sqlparser.ColumnDefinition{ + Name: sqlparser.NewColIdent("last_name"), + Type: sqlparser.ColumnType{ + Type: "varchar", + NotNull: false, + }, + }, + &sqlparser.ColumnDefinition{ + Name: sqlparser.NewColIdent("id"), + Type: sqlparser.ColumnType{ + Type: "int", + NotNull: true, + Autoincrement: true, + }, + }, + &sqlparser.ColumnDefinition{ + Name: sqlparser.NewColIdent("age"), + Type: sqlparser.ColumnType{ + Type: "int", + NotNull: true, + }, + }, + } + schemaMap["orders"] = []*sqlparser.ColumnDefinition{ + &sqlparser.ColumnDefinition{ + Name: sqlparser.NewColIdent("id"), + Type: sqlparser.ColumnType{ + Type: "int", + NotNull: true, + Autoincrement: true, + }, + }, + &sqlparser.ColumnDefinition{ + Name: sqlparser.NewColIdent("price"), + Type: sqlparser.ColumnType{ + Type: "DECIMAL(13, 4)", + NotNull: true, + Autoincrement: true, + }, + }, + &sqlparser.ColumnDefinition{ + Name: sqlparser.NewColIdent("user_id"), + Type: sqlparser.ColumnType{ + Type: "int", + NotNull: true, + }, + }, + } +} + +func filterCols(allCols []*sqlparser.ColumnDefinition, tableNames map[string]struct{}) []*sqlparser.ColumnDefinition { + filteredCols := []*sqlparser.ColumnDefinition{} + for _, col := range allCols { + if _, ok := tableNames[col.Name.String()]; ok { + filteredCols = append(filteredCols, col) + } + } + return filteredCols +} + +func TestParseSelect(t *testing.T) { + type expected struct { + query string + schema *Schema + } + type testCase struct { + name string + input expected + output *Query + } + tests := []testCase{ + testCase{ + name: "get_count", + input: expected{ + query: `/* name: GetCount :one */ + SELECT id my_id, COUNT(id) id_count FROM users WHERE id > 4`, + schema: mockSchema, + }, + output: &Query{ + SQL: "select id as my_id, COUNT(id) as id_count from users where id > 4", + Columns: []*sqlparser.ColumnDefinition{ + &sqlparser.ColumnDefinition{ + Name: sqlparser.NewColIdent("my_id"), + Type: sqlparser.ColumnType{ + Type: "int", + NotNull: true, + Autoincrement: true, + }, + }, + &sqlparser.ColumnDefinition{ + Name: sqlparser.NewColIdent("id_count"), + Type: sqlparser.ColumnType{ + Type: "int", + NotNull: true, + }, + }, + }, + Params: []*Param{}, + Name: "GetCount", + Cmd: ":one", + DefaultTableName: "users", + SchemaLookup: mockSchema, + }, + }, + testCase{ + name: "get_name_by_id", + input: expected{ + query: `/* name: GetNameByID :one */ + SELECT first_name, last_name FROM users WHERE id = ?`, + schema: mockSchema, + }, + output: &Query{ + SQL: `select first_name, last_name from users where id = ?`, + Columns: filterCols(mockSchema.tables["users"], map[string]struct{}{"first_name": struct{}{}, "last_name": struct{}{}}), + Params: []*Param{ + &Param{ + OriginalName: ":v1", + Name: "id", + Typ: "int", + }}, + Name: "GetNameByID", + Cmd: ":one", + DefaultTableName: "users", + SchemaLookup: mockSchema, + }, + }, + testCase{ + name: "get_all", + input: expected{ + query: `/* name: GetAll :many */ + SELECT * FROM users;`, + schema: mockSchema, + }, + output: &Query{ + SQL: "select first_name, last_name, id, age from users", + Columns: mockSchema.tables["users"], + Params: []*Param{}, + Name: "GetAll", + Cmd: ":many", + DefaultTableName: "users", + SchemaLookup: mockSchema, + }, + }, + testCase{ + name: "get_all_users_orders", + input: expected{ + query: `/* name: GetAllUsersOrders :many */ + SELECT u.id user_id, u.first_name, o.price, o.id order_id + FROM orders o LEFT JOIN users u ON u.id = o.user_id`, + schema: mockSchema, + }, + output: &Query{ + SQL: "select u.id as user_id, u.first_name, o.price, o.id as order_id from orders as o left join users as u on u.id = o.user_id", + Columns: []*sqlparser.ColumnDefinition{ + &sqlparser.ColumnDefinition{ + Name: sqlparser.NewColIdent("user_id"), + Type: sqlparser.ColumnType{ + Type: "int", + Autoincrement: true, + NotNull: false, // beause of the left join + }, + }, + &sqlparser.ColumnDefinition{ + Name: sqlparser.NewColIdent("first_name"), + Type: sqlparser.ColumnType{ + Type: "varchar", + NotNull: false, // because of left join + }, + }, + &sqlparser.ColumnDefinition{ + Name: sqlparser.NewColIdent("price"), + Type: sqlparser.ColumnType{ + Type: "DECIMAL(13, 4)", + Autoincrement: true, + NotNull: true, + }, + }, + &sqlparser.ColumnDefinition{ + Name: sqlparser.NewColIdent("order_id"), + Type: sqlparser.ColumnType{ + Type: "int", + Autoincrement: true, + NotNull: true, + }, + }, + }, + Params: []*Param{}, + Name: "GetAllUsersOrders", + Cmd: ":many", + DefaultTableName: "", // TODO: verify that this is desired behaviour + SchemaLookup: mockSchema, + }, + }, + } + + for _, tt := range tests { + testCase := tt + t.Run(tt.name, func(t *testing.T) { + qs, err := parseContents("example.sql", testCase.input.query, testCase.input.schema, mockSettings) + if err != nil { + t.Fatalf("Parsing failed with query: [%v]\n", err) + } + if len(qs) != 1 { + t.Fatalf("Expected one query, not %d", len(qs)) + } + q := qs[0] + q.SchemaLookup = nil + q.Filename = "" + testCase.output.SchemaLookup = nil + if diff := cmp.Diff(testCase.output, q); diff != "" { + t.Errorf("parsed query differs: \n%s", diff) + } + }) + } +} + +func TestParseLeadingComment(t *testing.T) { + type expected struct { + name string + cmd string + } + type testCase struct { + input string + output expected + } + + tests := []testCase{ + testCase{ + input: "/* name: GetPeopleByID :many */", + output: expected{name: "GetPeopleByID", cmd: ":many"}, + }, + } + + for _, tCase := range tests { + qu := &Query{} + err := qu.parseLeadingComment(tCase.input) + if err != nil { + t.Errorf("Failed to parse leading comment %v", err) + } + if qu.Name != tCase.output.name || qu.Cmd != tCase.output.cmd { + t.Errorf("Leading comment parser returned unexpcted result: %v\n:\n Expected: [%v]\nRecieved:[%v]\n", + err, spew.Sdump(tCase.output), spew.Sdump(qu)) + } + + } +} + +func TestSchemaLookup(t *testing.T) { + firstNameColDfn, err := mockSchema.schemaLookup("users", "first_name") + if err != nil { + t.Errorf("Failed to get column schema from mock schema: %v", err) + } + + expected := filterCols(mockSchema.tables["users"], map[string]struct{}{"first_name": struct{}{}}) + if !reflect.DeepEqual(firstNameColDfn, expected[0]) { + t.Errorf("Table schema lookup returned unexpected result") + } +} + +func TestParseInsertUpdate(t *testing.T) { + type expected struct { + query string + schema *Schema + } + type testCase struct { + name string + input expected + output *Query + } + query1 := `/* name: InsertNewUser :exec */ +INSERT INTO users (first_name, last_name) VALUES (?, ?)` + query2 := `/* name: UpdateUserAt :exec */ +UPDATE users SET first_name = ?, last_name = ? WHERE id > ? AND first_name = ? LIMIT 3` + + tests := []testCase{ + testCase{ + name: "insert_users", + input: expected{ + query: query1, + schema: mockSchema, + }, + output: &Query{ + SQL: "insert into users(first_name, last_name) values (?, ?)", + Columns: nil, + Params: []*Param{ + &Param{ + OriginalName: ":v1", + Name: "first_name", + Typ: "string", + }, + &Param{ + OriginalName: ":v2", + Name: "last_name", + Typ: "sql.NullString", + }, + }, + Name: "InsertNewUser", + Cmd: ":exec", + DefaultTableName: "users", + SchemaLookup: mockSchema, + }, + }, + testCase{ + name: "update_users", + input: expected{ + query: query2, + schema: mockSchema, + }, + output: &Query{ + SQL: "update users set first_name = ?, last_name = ? where id > ? and first_name = ? limit 3", + Columns: nil, + Params: []*Param{ + &Param{ + OriginalName: ":v1", + Name: "first_name", + Typ: "string", + }, + &Param{ + OriginalName: ":v2", + Name: "last_name", + Typ: "sql.NullString", + }, + &Param{ + OriginalName: ":v3", + Name: "id", + Typ: "int", + }, + &Param{ + OriginalName: ":v4", + Name: "first_name", + Typ: "string", + }, + }, + Name: "UpdateUserAt", + Cmd: ":exec", + DefaultTableName: "users", + SchemaLookup: mockSchema, + }, + }, + } + + for _, tt := range tests { + testCase := tt + t.Run(tt.name, func(t *testing.T) { + qs, err := parseContents("example.sql", testCase.input.query, testCase.input.schema, mockSettings) + if err != nil { + t.Fatalf("Parsing failed with query: [%v]\n", err) + } + if len(qs) != 1 { + t.Fatalf("Expected one query, not %d", len(qs)) + } + q := qs[0] + testCase.output.SchemaLookup = nil + q.SchemaLookup = nil + q.Filename = "" + if diff := cmp.Diff(testCase.output, q); diff != "" { + t.Errorf("parsed query differs: \n%s", diff) + } + }) + } +} diff --git a/internal/mysql/schema.go b/internal/mysql/schema.go new file mode 100644 index 0000000000..f0f2cf4850 --- /dev/null +++ b/internal/mysql/schema.go @@ -0,0 +1,76 @@ +package mysql + +import ( + "fmt" + + "vitess.io/vitess/go/vt/sqlparser" +) + +// NewSchema gives a newly instantiated MySQL schema map +func NewSchema() *Schema { + return &Schema{ + tables: make(map[string]([]*sqlparser.ColumnDefinition)), + } +} + +// Schema proves that information for mapping columns in queries to their respective table definitions +// and validating that they are correct so as to map to the correct Go type +type Schema struct { + tables map[string]([]*sqlparser.ColumnDefinition) +} + +// returns a deep copy of the column definition for using as a query return type or param type +func (s *Schema) getColType(col *sqlparser.ColName, tableAliasMap FromTables, defaultTableName string) (*sqlparser.ColumnDefinition, error) { + if !col.Qualifier.IsEmpty() { + realTable, ok := tableAliasMap[col.Qualifier.Name.String()] + if !ok { + return nil, fmt.Errorf("Column qualifier [%v] not found in table alias map", col.Qualifier.Name.String()) + } + colDfn, err := s.schemaLookup(realTable.TrueName, col.Name.String()) + if err != nil { + return nil, err + } + colDfnCopy := *colDfn + if realTable.IsLeftJoined { + colDfnCopy.Type.NotNull = false + } + return &colDfnCopy, nil + } + if defaultTableName == "" { + return nil, fmt.Errorf("Column reference [%v] is ambiguous -- Add a qualifier", col.Name.String()) + } + colDfn, err := s.schemaLookup(defaultTableName, col.Name.String()) + if err != nil { + return nil, err + } + return &sqlparser.ColumnDefinition{ + Name: colDfn.Name, Type: colDfn.Type, + }, nil +} + +// Add add a MySQL table definition to the schema map +func (s *Schema) Add(ddl *sqlparser.DDL) { + switch ddl.Action { + case "create": + name := ddl.Table.Name.String() + if ddl.TableSpec == nil { + panic(fmt.Sprintf("Failed to parse table [%v] schema.", name)) + } + s.tables[name] = ddl.TableSpec.Columns + } +} + +func (s *Schema) schemaLookup(table string, col string) (*sqlparser.ColumnDefinition, error) { + cols, ok := s.tables[table] + if !ok { + return nil, fmt.Errorf("Table [%v] not found in Schema", table) + } + + for _, colDef := range cols { + if colDef.Name.EqualString(col) { + return colDef, nil + } + } + + return nil, fmt.Errorf("Column [%v] not found in table [%v]", col, table) +} diff --git a/internal/mysql/test_data/queries.sql b/internal/mysql/test_data/queries.sql new file mode 100644 index 0000000000..9342dec835 --- /dev/null +++ b/internal/mysql/test_data/queries.sql @@ -0,0 +1,17 @@ +CREATE TABLE teachers ( + id int NOT NULL, + school_id VARCHAR(255) NOT NULL, + school_lat FLOAT, + school_lng FLOAT, + department ENUM("English", "Math"), + PRIMARY KEY (ID) +); + +/* name: GetTeachersByID :one */ +SELECT * FROM teachers WHERE id = ?; + +-- name: GetSomeTeachers :one +SELECT school_id, id FROM teachers WHERE school_lng > ? AND school_lat < ?; + +-- name: TeachersByID :one +SELECT id, school_lat FROM teachers WHERE id = ? LIMIT 10; diff --git a/internal/mysql/test_data/sqlc.json b/internal/mysql/test_data/sqlc.json new file mode 100644 index 0000000000..b09d8467af --- /dev/null +++ b/internal/mysql/test_data/sqlc.json @@ -0,0 +1,13 @@ +{ + "version": "1", + "packages": [ + { + "name": "teachersDB", + "emit_json_tags": true, + "emit_prepared_queries": false, + "path": "./", + "queries": "./queries.sql", + "schema": "./queries.sql" + } + ] +}