├── .gitignore ├── LICENSE ├── README.md ├── config └── config.go ├── go.mod ├── go.sum ├── handler └── handler.go ├── main.go └── render ├── async.go ├── console.go ├── csv.go ├── format.go ├── format_test.go ├── html.go ├── resultset.go └── types.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | *.html 8 | *.csv 9 | 10 | # Test binary, built with `go test -c` 11 | *.test 12 | 13 | # Output of the go coverage tool, specifically when used with LiteIDE 14 | *.out 15 | 16 | # Dependency directories (remove the comment below to include it) 17 | # vendor/ 18 | .idea 19 | 20 | ticmp -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TiCmp 2 | 3 | Compare query results between MySQL and TiDB server. 4 | 5 | ## Usage 6 | 7 | ``` 8 | ➜ ticmp git:(master) ✗ ./ticmp -h 9 | Usage: 10 | ticmp [flags] 11 | 12 | Flags: 13 | -P, --port int Listen port of TiCmp shadow server (default 5001) 14 | --user string TiCmp shadow server user name (default "root") 15 | --pass string TiCmp shadow server password 16 | --html string Output compare to specified html file 17 | --csv string Output compare to specified csv file 18 | --mysql.host string MySQL server host name (default "127.0.0.1") 19 | --mysql.port int MySQL server port (default 3306) 20 | --mysql.user string MySQL server user name (default "root") 21 | --mysql.pass string MySQL server password 22 | --mysql.name string MySQL server database name 23 | --mysql.options string MySQL server connection options (default "charset=utf8mb4") 24 | --tidb.host string TiDB server host name (default "127.0.0.1") 25 | --tidb.port int TiDB server port (default 4000) 26 | --tidb.user string TiDB server user name (default "root") 27 | --tidb.pass string TiDB server password 28 | --tidb.name string TiDB server database name 29 | --tidb.options string TiDB server connection options (default "charset=utf8mb4") 30 | -h, --help help for ticmp 31 | ``` 32 | 33 | ### 34 | 35 | 1. Run ticmp and connect to local MySQL/TiDB server 36 | 37 | ```shell 38 | # Login local mysql server with user name: lonng 39 | ./ticmp --port 6000 --mysql.user lonng 40 | ``` 41 | 42 | 2. Connect to ticmp and treat it as a normal MySQL server 43 | 44 | ```shell 45 | # Login into TiCmp server 46 | mysql -h 127.0.0.1 -P 6000 -uroot 47 | 48 | # Query 49 | mysql> select uuid(); 50 | +--------------------------------------+ 51 | | uuid() | 52 | +--------------------------------------+ 53 | | bbfb289e-e125-11ec-b832-c8f6766ec590 | 54 | +--------------------------------------+ 55 | 1 row in set (0.01 sec) 56 | ``` 57 | 58 | 3. Check your TiCmp output and it should be like following content with diff highlight 59 | 60 | ``` 61 | QUERY > select uuid() 62 | TiDB > 63 | +--------------------------------------+ 64 | | uuid() | 65 | +--------------------------------------+ 66 | | bbfb6642-e125-11ec-846a-acde48001122 | 67 | +--------------------------------------+ 68 | MySQL > 69 | +--------------------------------------+ 70 | | uuid() | 71 | +--------------------------------------+ 72 | | bbfb289e-e125-11ec-b832-c8f6766ec590 | 73 | +--------------------------------------+ 74 | ``` 75 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import "strconv" 4 | 5 | // DBConfig configures a database connection. 6 | type DBConfig struct { 7 | Host string 8 | Port int 9 | User string 10 | Pass string 11 | Name string 12 | Options string 13 | } 14 | 15 | // Config is the configuration for the server. 16 | type Config struct { 17 | User string 18 | Pass string 19 | HTMLPath string 20 | CSVPath string 21 | Port int 22 | MySQL DBConfig 23 | TiDB DBConfig 24 | } 25 | 26 | // DSN returns the data source name for the given database. 27 | func (c *DBConfig) DSN() string { 28 | return c.User + ":" + c.Pass + "@tcp(" + c.Host + ":" + strconv.Itoa(c.Port) + ")/" + c.Name + "?" + c.Options 29 | } 30 | 31 | // Address returns the address for the given database. 32 | func (c *DBConfig) Address() string { 33 | return c.Host + ":" + strconv.Itoa(c.Port) 34 | } 35 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/lonng/ticmp 2 | 3 | go 1.18 4 | 5 | require github.com/go-mysql-org/go-mysql v1.5.0 6 | 7 | require ( 8 | github.com/fatih/color v1.13.0 9 | github.com/hashicorp/go-multierror v1.1.1 10 | github.com/sergi/go-diff v1.2.0 11 | github.com/siddontang/go v0.0.0-20180604090527-bdc77568d726 12 | github.com/spf13/cobra v1.4.0 13 | github.com/stretchr/testify v1.7.1 14 | ) 15 | 16 | require ( 17 | github.com/davecgh/go-spew v1.1.1 // indirect 18 | github.com/google/uuid v1.3.0 // indirect 19 | github.com/hashicorp/errwrap v1.0.0 // indirect 20 | github.com/inconshreveable/mousetrap v1.0.0 // indirect 21 | github.com/mattn/go-colorable v0.1.9 // indirect 22 | github.com/mattn/go-isatty v0.0.14 // indirect 23 | github.com/pingcap/errors v0.11.5-0.20201126102027-b0a155152ca3 // indirect 24 | github.com/pmezard/go-difflib v1.0.0 // indirect 25 | github.com/siddontang/go-log v0.0.0-20180807004314-8d05993dda07 // indirect 26 | github.com/spf13/pflag v1.0.5 // indirect 27 | go.uber.org/atomic v1.7.0 // indirect 28 | golang.org/x/sys v0.1.0 // indirect 29 | gopkg.in/yaml.v3 v3.0.0 // indirect 30 | ) 31 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 2 | github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 3 | github.com/cznic/golex v0.0.0-20181122101858-9c343928389c/go.mod h1:+bmmJDNmKlhWNG+gwWCkaBoTy39Fs+bzRxVBzoTQbIc= 4 | github.com/cznic/mathutil v0.0.0-20181122101859-297441e03548/go.mod h1:e6NPNENfs9mPDVNRekM7lKScauxd5kXTr1Mfyig6TDM= 5 | github.com/cznic/parser v0.0.0-20160622100904-31edd927e5b1/go.mod h1:2B43mz36vGZNZEwkWi8ayRSSUXLfjL8OkbzwW4NcPMM= 6 | github.com/cznic/sortutil v0.0.0-20181122101858-f5f958428db8/go.mod h1:q2w6Bg5jeox1B+QkJ6Wp/+Vn0G/bo3f1uY7Fn3vivIQ= 7 | github.com/cznic/strutil v0.0.0-20171016134553-529a34b1c186/go.mod h1:AHHPPPXTw0h6pVabbcbyGRK1DckRn7r/STdZEeIDzZc= 8 | github.com/cznic/y v0.0.0-20170802143616-045f81c6662a/go.mod h1:1rk5VM7oSnA4vjp+hrLQ3HWHa+Y4yPCa3/CsJrcNnvs= 9 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 11 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 12 | github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= 13 | github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= 14 | github.com/go-mysql-org/go-mysql v1.5.0 h1:Hyj3DH3AkWswW/MmWLsvNpJr1v6y8Dp90kW+1nzE+Vc= 15 | github.com/go-mysql-org/go-mysql v1.5.0/go.mod h1:GX0clmylJLdZEYAojPCDTCvwZxbTBrke93dV55715u0= 16 | github.com/go-sql-driver/mysql v1.3.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= 17 | github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= 18 | github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= 19 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 20 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 21 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 22 | github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= 23 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 24 | github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= 25 | github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= 26 | github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= 27 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 28 | github.com/jmoiron/sqlx v1.3.3 h1:j82X0bf7oQ27XeqxicSZsTU5suPwKElg3oyxNn43iTk= 29 | github.com/jmoiron/sqlx v1.3.3/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ= 30 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 31 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 32 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 33 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 34 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 35 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 36 | github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 37 | github.com/mattn/go-colorable v0.1.9 h1:sqDoxXbdeALODt0DAeJCVp38ps9ZogZEAXjus69YV3U= 38 | github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 39 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 40 | github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= 41 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 42 | github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= 43 | github.com/pingcap/check v0.0.0-20190102082844-67f458068fc8 h1:USx2/E1bX46VG32FIw034Au6seQ2fY9NEILmNh/UlQg= 44 | github.com/pingcap/check v0.0.0-20190102082844-67f458068fc8/go.mod h1:B1+S9LNcuMyLH/4HMTViQOJevkGiik3wW2AN9zb2fNQ= 45 | github.com/pingcap/errors v0.11.0/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= 46 | github.com/pingcap/errors v0.11.5-0.20201029093017-5a7df2af2ac7/go.mod h1:G7x87le1poQzLB/TqvTJI2ILrSgobnq4Ut7luOwvfvI= 47 | github.com/pingcap/errors v0.11.5-0.20201126102027-b0a155152ca3 h1:LllgC9eGfqzkfubMgjKIDyZYaa609nNWAyNZtpy2B3M= 48 | github.com/pingcap/errors v0.11.5-0.20201126102027-b0a155152ca3/go.mod h1:G7x87le1poQzLB/TqvTJI2ILrSgobnq4Ut7luOwvfvI= 49 | github.com/pingcap/log v0.0.0-20200511115504-543df19646ad/go.mod h1:4rbK1p9ILyIfb6hU7OG2CiWSqMXnp3JMbiaVJ6mvoY8= 50 | github.com/pingcap/log v0.0.0-20210317133921-96f4fcab92a4/go.mod h1:4rbK1p9ILyIfb6hU7OG2CiWSqMXnp3JMbiaVJ6mvoY8= 51 | github.com/pingcap/parser v0.0.0-20210415081931-48e7f467fd74/go.mod h1:xZC8I7bug4GJ5KtHhgAikjTfU4kBv1Sbo3Pf1MZ6lVw= 52 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 53 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 54 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 55 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 56 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 57 | github.com/remyoudompheng/bigfft v0.0.0-20190728182440-6a916e37a237/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= 58 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 59 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 60 | github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= 61 | github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= 62 | github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= 63 | github.com/siddontang/go v0.0.0-20180604090527-bdc77568d726 h1:xT+JlYxNGqyT+XcU8iUrN18JYed2TvG9yN5ULG2jATM= 64 | github.com/siddontang/go v0.0.0-20180604090527-bdc77568d726/go.mod h1:3yhqj7WBBfRhbBlzyOC3gUxftwsU0u8gqevxwIHQpMw= 65 | github.com/siddontang/go-log v0.0.0-20180807004314-8d05993dda07 h1:oI+RNwuC9jF2g2lP0u0cVEEZrc/AYBCuFdvwrLWM/6Q= 66 | github.com/siddontang/go-log v0.0.0-20180807004314-8d05993dda07/go.mod h1:yFdBgwXP24JziuRl2NMUahT7nGLNOKi1SIiFxMttVD4= 67 | github.com/spf13/cobra v1.4.0 h1:y+wJpx64xcgO1V+RcnwW0LEHxTKRi2ZDPSBjWnrg88Q= 68 | github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g= 69 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 70 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 71 | github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= 72 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 73 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 74 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 75 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 76 | github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= 77 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 78 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 79 | go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 80 | go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 81 | go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= 82 | go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 83 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 84 | go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= 85 | go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= 86 | go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= 87 | go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 88 | go.uber.org/zap v1.15.0/go.mod h1:Mb2vm2krFEG5DV0W9qcHBYFtp/Wku1cvYaqPsS/WYfc= 89 | go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ= 90 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 91 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 92 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 93 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 94 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 95 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 96 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 97 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 98 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 99 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 100 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 101 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 102 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 103 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 104 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 105 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 106 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 107 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 108 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 109 | golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= 110 | golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 111 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 112 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 113 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 114 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 115 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 116 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 117 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 118 | golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 119 | golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 120 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 121 | golang.org/x/tools v0.0.0-20201125231158-b5590deeca9b/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 122 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 123 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 124 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 125 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 126 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 127 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 128 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 129 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 130 | gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= 131 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 132 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 133 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 134 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 135 | gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA= 136 | gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 137 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 138 | -------------------------------------------------------------------------------- /handler/handler.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/go-mysql-org/go-mysql/client" 8 | _ "github.com/go-mysql-org/go-mysql/driver" 9 | "github.com/go-mysql-org/go-mysql/mysql" 10 | "github.com/go-mysql-org/go-mysql/server" 11 | "github.com/hashicorp/go-multierror" 12 | "github.com/lonng/ticmp/config" 13 | "github.com/lonng/ticmp/render" 14 | ) 15 | 16 | type ShadowHandler struct { 17 | server.EmptyHandler 18 | 19 | cfg *config.Config 20 | mysqlConn *client.Conn 21 | tidbConn *client.Conn 22 | connIdent string 23 | render render.Render 24 | } 25 | 26 | func NewShadowHandler(config *config.Config, connIdent string, render render.Render) *ShadowHandler { 27 | return &ShadowHandler{ 28 | cfg: config, 29 | connIdent: connIdent, 30 | render: render, 31 | } 32 | } 33 | 34 | func (h *ShadowHandler) Initialize() error { 35 | mycfg := h.cfg.MySQL 36 | mycon, err := client.Connect(fmt.Sprintf("%s:%d", mycfg.Host, mycfg.Port), mycfg.User, mycfg.Pass, mycfg.Name) 37 | if err != nil { 38 | return err 39 | } 40 | if err := mycon.Ping(); err != nil { 41 | return err 42 | } 43 | h.mysqlConn = mycon 44 | 45 | ticfg := h.cfg.TiDB 46 | ticon, err := client.Connect(fmt.Sprintf("%s:%d", ticfg.Host, ticfg.Port), ticfg.User, ticfg.Pass, ticfg.Name) 47 | if err != nil { 48 | return err 49 | } 50 | if err := ticon.Ping(); err != nil { 51 | return err 52 | } 53 | h.tidbConn = ticon 54 | 55 | return nil 56 | } 57 | 58 | func (h *ShadowHandler) Finalize() error { 59 | var result error 60 | if h.mysqlConn != nil { 61 | err := h.mysqlConn.Close() 62 | if err != nil { 63 | result = multierror.Append(result, err) 64 | } 65 | } 66 | if h.tidbConn != nil { 67 | err := h.tidbConn.Close() 68 | if err != nil { 69 | result = multierror.Append(result, err) 70 | } 71 | } 72 | return result 73 | } 74 | 75 | func (h *ShadowHandler) UseDB(dbName string) error { 76 | var result error 77 | if err := h.mysqlConn.UseDB(dbName); err != nil { 78 | result = multierror.Append(result, err) 79 | } 80 | if err := h.tidbConn.UseDB(dbName); err != nil { 81 | result = multierror.Append(result, err) 82 | } 83 | return result 84 | } 85 | 86 | // HandleQuery overwrites the original HandleQuery. 87 | func (h *ShadowHandler) HandleQuery(query string) (*mysql.Result, error) { 88 | start := time.Now() 89 | myResult, err1 := h.mysqlConn.Execute(query) 90 | myTime := time.Now().Sub(start) 91 | 92 | start = time.Now() 93 | tiResult, err2 := h.tidbConn.Execute(query) 94 | tiTime := time.Now().Sub(start) 95 | 96 | h.render.Push(&render.Frame{ 97 | Ident: h.connIdent, 98 | Query: query, 99 | TiDB: render.QueryResult{ 100 | Result: tiResult, 101 | Error: err2, 102 | Duration: tiTime, 103 | }, 104 | MySQL: render.QueryResult{ 105 | Result: myResult, 106 | Error: err1, 107 | Duration: myTime, 108 | }, 109 | }) 110 | 111 | return myResult, err1 112 | } 113 | 114 | func (h *ShadowHandler) HandleFieldList(table string, fieldWildcard string) ([]*mysql.Field, error) { 115 | myFields, err1 := h.mysqlConn.FieldList(table, fieldWildcard) 116 | 117 | // TODO(lonng): implement diff result for field list. 118 | _, _ = h.tidbConn.FieldList(table, fieldWildcard) 119 | 120 | return myFields, err1 121 | } 122 | 123 | func (h *ShadowHandler) HandleStmtPrepare(query string) (int, int, interface{}, error) { 124 | mystmt, err := h.mysqlConn.Prepare(query) 125 | 126 | // TODO(lonng): implement diff result for preparing. 127 | _, _ = h.tidbConn.Prepare(query) 128 | 129 | return mystmt.ParamNum(), mystmt.ColumnNum(), nil, err 130 | } 131 | 132 | func (h *ShadowHandler) HandleStmtExecute(context interface{}, query string, args []interface{}) (*mysql.Result, error) { 133 | start := time.Now() 134 | myResult, err1 := h.mysqlConn.Execute(query, args...) 135 | myTime := time.Now().Sub(start) 136 | 137 | start = time.Now() 138 | tiResult, err2 := h.tidbConn.Execute(query, args...) 139 | tiTime := time.Now().Sub(start) 140 | 141 | h.render.Push(&render.Frame{ 142 | Ident: h.connIdent, 143 | Query: query, 144 | Args: args, 145 | TiDB: render.QueryResult{ 146 | Result: tiResult, 147 | Error: err2, 148 | Duration: tiTime, 149 | }, 150 | MySQL: render.QueryResult{ 151 | Result: myResult, 152 | Error: err1, 153 | Duration: myTime, 154 | }, 155 | }) 156 | 157 | return myResult, err1 158 | } 159 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "os" 7 | 8 | "github.com/go-mysql-org/go-mysql/server" 9 | "github.com/lonng/ticmp/config" 10 | "github.com/lonng/ticmp/handler" 11 | "github.com/lonng/ticmp/render" 12 | "github.com/spf13/cobra" 13 | ) 14 | 15 | func onConnect(c net.Conn, cfg *config.Config, rndr render.Render) error { 16 | connIdent := c.RemoteAddr().String() 17 | h := handler.NewShadowHandler(cfg, connIdent, rndr) 18 | 19 | err := h.Initialize() 20 | if err != nil { 21 | return err 22 | } 23 | 24 | defer func() { 25 | if err := h.Finalize(); err != nil { 26 | fmt.Println("Finalize shadow handler failed", err) 27 | } 28 | }() 29 | 30 | // Create a connection with user root and password. 31 | // You can use your own handler to handle command here. 32 | conn, err := server.NewConn(c, cfg.User, cfg.Pass, h) 33 | if err != nil { 34 | return err 35 | } 36 | 37 | // as long as the client keeps sending commands, keep handling them 38 | for { 39 | if err := conn.HandleCommand(); err != nil { 40 | if conn.Conn == nil { 41 | return nil 42 | } 43 | fmt.Printf("handle command error: %v\n", err) 44 | return err 45 | } 46 | } 47 | } 48 | 49 | func main() { 50 | cfg := &config.Config{} 51 | cmd := &cobra.Command{ 52 | Use: "ticmp", 53 | SilenceUsage: true, 54 | RunE: func(cmd *cobra.Command, args []string) error { 55 | var rndr render.Render 56 | if cfg.HTMLPath != "" { 57 | hr := &render.HTMLRender{} 58 | if err := hr.Open(cfg.HTMLPath); err != nil { 59 | return err 60 | } 61 | rndr = hr 62 | defer hr.Close() 63 | } else if cfg.CSVPath != "" { 64 | hr := &render.CSVRender{} 65 | if err := hr.Open(cfg.CSVPath); err != nil { 66 | return err 67 | } 68 | rndr = hr 69 | defer hr.Close() 70 | } else { 71 | rndr = render.ConsoleRender{} 72 | } 73 | 74 | // Wrap with asynchronous render 75 | rd := render.NewAsyncRender(rndr) 76 | go rd.Start() 77 | 78 | l, err := net.Listen("tcp4", fmt.Sprintf(":%d", cfg.Port)) 79 | if err != nil { 80 | return err 81 | } 82 | 83 | fmt.Printf("Serve successfully (mysql -h 127.0.0.1 -P %d -u%s -p)\n", cfg.Port, cfg.User) 84 | 85 | // Accept a new connection once 86 | for { 87 | c, err := l.Accept() 88 | if err != nil { 89 | return err 90 | } 91 | 92 | go onConnect(c, cfg, rd) 93 | } 94 | }, 95 | } 96 | 97 | flags := cmd.Flags() 98 | flags.SortFlags = false 99 | 100 | // Shadow server configurations 101 | flags.IntVarP(&cfg.Port, "port", "P", 5001, "Listen port of TiCmp shadow server") 102 | flags.StringVar(&cfg.User, "user", "root", "TiCmp shadow server user name") 103 | flags.StringVar(&cfg.Pass, "pass", "", "TiCmp shadow server password") 104 | flags.StringVar(&cfg.HTMLPath, "html", "", "Output compare to specified html file") 105 | flags.StringVar(&cfg.CSVPath, "csv", "", "Output compare to specified csv file") 106 | 107 | // MySQL server configurations 108 | flags.StringVar(&cfg.MySQL.Host, "mysql.host", "127.0.0.1", "MySQL server host name") 109 | flags.IntVar(&cfg.MySQL.Port, "mysql.port", 3306, "MySQL server port") 110 | flags.StringVar(&cfg.MySQL.User, "mysql.user", "root", "MySQL server user name") 111 | flags.StringVar(&cfg.MySQL.Pass, "mysql.pass", "", "MySQL server password") 112 | flags.StringVar(&cfg.MySQL.Name, "mysql.name", "", "MySQL server database name") 113 | flags.StringVar(&cfg.MySQL.Options, "mysql.options", "charset=utf8mb4", "MySQL server connection options") 114 | 115 | // TiDB server configurations 116 | flags.StringVar(&cfg.TiDB.Host, "tidb.host", "127.0.0.1", "TiDB server host name") 117 | flags.IntVar(&cfg.TiDB.Port, "tidb.port", 4000, "TiDB server port") 118 | flags.StringVar(&cfg.TiDB.User, "tidb.user", "root", "TiDB server user name") 119 | flags.StringVar(&cfg.TiDB.Pass, "tidb.pass", "", "TiDB server password") 120 | flags.StringVar(&cfg.TiDB.Name, "tidb.name", "", "TiDB server database name") 121 | flags.StringVar(&cfg.TiDB.Options, "tidb.options", "charset=utf8mb4", "TiDB server connection options") 122 | 123 | if err := cmd.Execute(); err != nil { 124 | fmt.Println("Execute command failed", err) 125 | os.Exit(2) 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /render/async.go: -------------------------------------------------------------------------------- 1 | package render 2 | 3 | type AsyncRender struct { 4 | frames chan *Frame 5 | render Render 6 | } 7 | 8 | func NewAsyncRender(render Render) *AsyncRender { 9 | return &AsyncRender{ 10 | frames: make(chan *Frame, 1<<8), 11 | render: render, 12 | } 13 | } 14 | 15 | func (a *AsyncRender) Start() { 16 | for frame := range a.frames { 17 | a.render.Push(frame) 18 | } 19 | } 20 | 21 | func (a *AsyncRender) Push(frame *Frame) { 22 | a.frames <- frame 23 | } 24 | -------------------------------------------------------------------------------- /render/console.go: -------------------------------------------------------------------------------- 1 | package render 2 | 3 | import ( 4 | "bytes" 5 | sqldriver "database/sql/driver" 6 | "fmt" 7 | "io" 8 | "reflect" 9 | "strings" 10 | 11 | "github.com/fatih/color" 12 | "github.com/go-mysql-org/go-mysql/mysql" 13 | "github.com/sergi/go-diff/diffmatchpatch" 14 | ) 15 | 16 | type ConsoleRender struct{} 17 | 18 | func (c ConsoleRender) Push(frame *Frame) { 19 | c1, c2 := c.diffResult(frame.MySQL.Error, frame.TiDB.Error, frame.MySQL.Result, frame.TiDB.Result) 20 | var argStr string 21 | if len(frame.Args) > 0 { 22 | argStr = strings.Join(FormatArgs(frame.Args), ", ") 23 | argStr = fmt.Sprintf("(%s)", argStr) 24 | } 25 | if c1 == c2 { 26 | color.Green("%s [MySQL %s, TiDB %s] ==> %s (%s)", frame.Ident, frame.MySQL.Duration, frame.TiDB.Duration, frame.Query, argStr) 27 | } else { 28 | color.Red("%s [MySQL %s, TiDB %s] ==> %s (%s)", frame.Ident, frame.MySQL.Duration, frame.TiDB.Duration, frame.Query, argStr) 29 | fmt.Printf("%s MySQL >\n%s\n", frame.Ident, c1) 30 | fmt.Printf("%s TiDB >\n%s\n", frame.Ident, c2) 31 | } 32 | } 33 | 34 | type colorFunc func(a ...interface{}) string 35 | 36 | func genDiffResult(mysqlContent string, tidbContent string, 37 | diffDelete colorFunc, diffInsert colorFunc) (string, string) { 38 | patch := diffmatchpatch.New() 39 | diff := patch.DiffMain(mysqlContent, tidbContent, false) 40 | var newMySQLContent, newTiDBContent bytes.Buffer 41 | for _, d := range diff { 42 | switch d.Type { 43 | case diffmatchpatch.DiffEqual: 44 | newMySQLContent.WriteString(d.Text) 45 | newTiDBContent.WriteString(d.Text) 46 | case diffmatchpatch.DiffDelete: 47 | newMySQLContent.WriteString(diffDelete(d.Text)) 48 | case diffmatchpatch.DiffInsert: 49 | newTiDBContent.WriteString(diffInsert(d.Text)) 50 | } 51 | } 52 | mysqlContent = newMySQLContent.String() 53 | tidbContent = newTiDBContent.String() 54 | return mysqlContent, tidbContent 55 | } 56 | 57 | func formatError(err error) string { 58 | if err == nil { 59 | return "" 60 | } 61 | return fmt.Sprintf("%s", err.Error()) 62 | } 63 | 64 | func (c ConsoleRender) diffResult(myErr error, tiErr error, myResult, tiResult *mysql.Result) (mysqlContent, tidbContent string) { 65 | if myErr != tiErr { 66 | mysqlContent = formatError(myErr) 67 | tidbContent = formatError(tiErr) 68 | return 69 | } 70 | 71 | if reflect.DeepEqual(myResult.Resultset, tiResult.Resultset) { 72 | return "", "" 73 | } 74 | 75 | mysqlResult, _ := newRows(myResult.Resultset) 76 | tidbResult, _ := newRows(tiResult.Resultset) 77 | defer mysqlResult.Close() 78 | defer tidbResult.Close() 79 | 80 | mysqlContent, tidbContent = prettyText(mysqlResult), prettyText(tidbResult) 81 | yellow := color.New(color.FgYellow).SprintFunc() 82 | red := color.New(color.FgRed).SprintFunc() 83 | 84 | mysqlContent, tidbContent = genDiffResult(mysqlContent, tidbContent, red, yellow) 85 | 86 | return 87 | } 88 | 89 | func prettyText(r *rows) string { 90 | cols := r.columns 91 | var allRows [][]string 92 | for { 93 | dest := make([]sqldriver.Value, len(cols)) 94 | if err := r.Next(dest); err == io.EOF { 95 | break 96 | } 97 | var row []string 98 | for _, c := range dest { 99 | if c == nil { 100 | row = append(row, "NULL") 101 | } else { 102 | // Ref: https://github.com/go-mysql-org/go-mysql/blob/33ea963610607f7b5505fd39d0955b78039ef783/mysql/field.go#L199 103 | // Only four types need to be asserted. 104 | switch x := c.(type) { 105 | case uint64, int64: 106 | row = append(row, fmt.Sprintf("%d", x)) 107 | case float64: 108 | row = append(row, fmt.Sprintf("%f", x)) 109 | case string: 110 | row = append(row, x) 111 | default: 112 | row = append(row, fmt.Sprintf("%s", c)) 113 | } 114 | } 115 | } 116 | allRows = append(allRows, row) 117 | } 118 | 119 | // Calculate the max column length 120 | var colLength []int 121 | for _, c := range cols { 122 | colLength = append(colLength, len(c)) 123 | } 124 | for _, row := range allRows { 125 | for n, col := range row { 126 | if l := len(col); colLength[n] < l { 127 | colLength[n] = l 128 | } 129 | } 130 | } 131 | // The total length 132 | var total = len(cols) - 1 133 | for index := range colLength { 134 | colLength[index] += 2 // Value will wrap with space 135 | total += colLength[index] 136 | } 137 | 138 | var lines []string 139 | var push = func(line string) { 140 | lines = append(lines, line) 141 | } 142 | 143 | // Write table header 144 | var header string 145 | for index, col := range cols { 146 | length := colLength[index] 147 | padding := length - 1 - len(col) 148 | if index == 0 { 149 | header += "|" 150 | } 151 | header += " " + col + strings.Repeat(" ", padding) + "|" 152 | } 153 | splitLine := "+" + strings.Repeat("-", total) + "+" 154 | push(splitLine) 155 | push(strings.ToLower(header)) 156 | push(splitLine) 157 | 158 | // Write rows data 159 | for _, row := range allRows { 160 | var line string 161 | for index, col := range row { 162 | length := colLength[index] 163 | padding := length - 1 - len(col) 164 | if index == 0 { 165 | line += "|" 166 | } 167 | line += " " + string(col) + strings.Repeat(" ", padding) + "|" 168 | } 169 | push(line) 170 | } 171 | push(splitLine) 172 | return strings.Join(lines, "\n") 173 | } 174 | -------------------------------------------------------------------------------- /render/csv.go: -------------------------------------------------------------------------------- 1 | package render 2 | 3 | import ( 4 | "encoding/csv" 5 | "fmt" 6 | "io" 7 | "os" 8 | "reflect" 9 | "strings" 10 | "time" 11 | 12 | "github.com/go-mysql-org/go-mysql/mysql" 13 | ) 14 | 15 | type CSVRender struct { 16 | csvWriter *csv.Writer 17 | w io.WriteCloser 18 | } 19 | 20 | func (c *CSVRender) Open(file string) error { 21 | f, err := os.OpenFile(file, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, os.ModePerm) 22 | if err != nil { 23 | return err 24 | } 25 | c.w = f 26 | c.csvWriter = csv.NewWriter(f) 27 | 28 | c.csvWriter.Write([]string{"IsDiff", "Query", "Args", "MySQL Time(ms)", "TiDB Time(ms)", "Is TiDB Slow", "MySQL", "TiDB", "Ident"}) 29 | c.csvWriter.Flush() 30 | return nil 31 | } 32 | 33 | func (r *CSVRender) Close() error { 34 | r.csvWriter.Flush() 35 | return r.w.Close() 36 | } 37 | 38 | func (c *CSVRender) Push(frame *Frame) { 39 | c1, c2 := c.diffResult(frame.MySQL.Error, frame.TiDB.Error, frame.MySQL.Result, frame.TiDB.Result) 40 | 41 | var argStr string 42 | if len(frame.Args) > 0 { 43 | argStr = strings.Join(FormatArgs(frame.Args), ", ") 44 | } 45 | 46 | records := make([]string, 9) 47 | if c1 == c2 { 48 | records[0] = "NO" 49 | } else { 50 | records[0] = "YES" 51 | records[6] = c1 52 | records[7] = c2 53 | } 54 | records[1] = frame.Query 55 | records[2] = argStr 56 | records[3] = fmt.Sprintf("%f", float64(frame.MySQL.Duration)/float64(time.Millisecond)) 57 | records[4] = fmt.Sprintf("%v", float64(frame.TiDB.Duration)/float64(time.Millisecond)) 58 | records[5] = fmt.Sprintf("%v", frame.TiDB.Duration > frame.MySQL.Duration) 59 | 60 | records[8] = frame.Ident 61 | 62 | c.csvWriter.Write(records) 63 | c.csvWriter.Flush() 64 | 65 | } 66 | 67 | func (c *CSVRender) diffResult(myErr error, tiErr error, myResult, tiResult *mysql.Result) (mysqlContent, tidbContent string) { 68 | if myErr != tiErr { 69 | mysqlContent = formatError(myErr) 70 | tidbContent = formatError(tiErr) 71 | return 72 | } 73 | 74 | if reflect.DeepEqual(myResult.Resultset, tiResult.Resultset) { 75 | return "", "" 76 | } 77 | 78 | mysqlResult, _ := newRows(myResult.Resultset) 79 | tidbResult, _ := newRows(tiResult.Resultset) 80 | defer mysqlResult.Close() 81 | defer tidbResult.Close() 82 | 83 | mysqlContent, tidbContent = prettyText(mysqlResult), prettyText(tidbResult) 84 | noColor := func(a ...interface{}) string { 85 | return a[0].(string) 86 | } 87 | 88 | mysqlContent, tidbContent = genDiffResult(mysqlContent, tidbContent, noColor, noColor) 89 | 90 | return 91 | } 92 | -------------------------------------------------------------------------------- /render/format.go: -------------------------------------------------------------------------------- 1 | package render 2 | 3 | import "fmt" 4 | 5 | // FormatArgs formats arguments into string slice. 6 | // Ref: https://github.com/go-mysql-org/go-mysql/blob/33ea963610607f7b5505fd39d0955b78039ef783/server/stmt.go#L186 7 | func FormatArgs(args []interface{}) []string { 8 | var results []string 9 | for _, arg := range args { 10 | if arg == nil { 11 | results = append(results, "NULL") 12 | continue 13 | } 14 | switch x := arg.(type) { 15 | case int8, uint8, int16, uint16, int32, uint32, int64, uint64: 16 | results = append(results, fmt.Sprintf("%d", x)) 17 | case float32, float64: 18 | results = append(results, fmt.Sprintf("%f", x)) 19 | case string: 20 | results = append(results, fmt.Sprintf("%q", x)) 21 | case []byte: 22 | results = append(results, fmt.Sprintf("%q", string(x))) 23 | default: 24 | results = append(results, fmt.Sprintf("%v", x)) 25 | } 26 | } 27 | return results 28 | } 29 | -------------------------------------------------------------------------------- /render/format_test.go: -------------------------------------------------------------------------------- 1 | package render 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestFormatArgs(t *testing.T) { 10 | args := []interface{}{ 11 | int8(1), 12 | uint8(2), 13 | int16(3), 14 | uint16(4), 15 | int32(5), 16 | uint32(6), 17 | int64(7), 18 | uint64(8), 19 | "hello string", 20 | []byte("hello bytes"), 21 | } 22 | 23 | results := FormatArgs(args) 24 | expected := []string{ 25 | "1", 26 | "2", 27 | "3", 28 | "4", 29 | "5", 30 | "6", 31 | "7", 32 | "8", 33 | `"hello string"`, 34 | `"hello bytes"`, 35 | } 36 | 37 | assert.Equal(t, expected, results) 38 | } 39 | -------------------------------------------------------------------------------- /render/html.go: -------------------------------------------------------------------------------- 1 | package render 2 | 3 | import ( 4 | sqldriver "database/sql/driver" 5 | "fmt" 6 | "io" 7 | "os" 8 | "reflect" 9 | "strings" 10 | 11 | "github.com/go-mysql-org/go-mysql/mysql" 12 | ) 13 | 14 | type HTMLRender struct { 15 | writer io.WriteCloser 16 | } 17 | 18 | func (c *HTMLRender) Open(file string) error { 19 | f, err := os.OpenFile(file, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, os.ModePerm) 20 | if err != nil { 21 | return err 22 | } 23 | c.writer = f 24 | 25 | c.output(` 26 | 45 | `) 46 | 47 | return nil 48 | } 49 | 50 | func (c *HTMLRender) Close() error { 51 | if c.writer != nil { 52 | return c.writer.Close() 53 | } 54 | return nil 55 | } 56 | 57 | func (c *HTMLRender) output(format string, args ...interface{}) { 58 | _, err := fmt.Fprintf(c.writer, format, args...) 59 | if err != nil { 60 | fmt.Println("Write HTML file failed", err) 61 | } 62 | } 63 | 64 | func (c *HTMLRender) Push(frame *Frame) { 65 | c1, c2 := c.diffResult(frame.MySQL.Error, frame.TiDB.Error, frame.MySQL.Result, frame.TiDB.Result) 66 | var argStr string 67 | if len(frame.Args) > 0 { 68 | argStr = strings.Join(FormatArgs(frame.Args), ", ") 69 | argStr = fmt.Sprintf(" (%s)", argStr) 70 | } 71 | if c1 == c2 { 72 | c.output("