From 7b6462c2ca586816a85c5d0101a06385e99163ea Mon Sep 17 00:00:00 2001 From: Leon Zhang Date: Sat, 20 Oct 2018 00:00:28 +0800 Subject: [PATCH] first commit --- .editorconfig | 10 + .github/CONTRIBUTING.md | 3 + .github/ISSUE_TEMPLATE/bug_report.md | 23 + .github/ISSUE_TEMPLATE/feature_request.md | 15 + .github/ISSUE_TEMPLATE/question.md | 10 + .gitignore | 21 + .travis.yml | 14 + CHANGES.md | 69 + LICENSE | 202 + Makefile | 206 + NOTICE.txt | 8 + README.md | 37 + README_EN.md | 36 + VERSION | 1 + advisor/doc.go | 18 + advisor/explainer.go | 285 + advisor/explainer_test.go | 37 + advisor/heuristic.go | 3252 +++++++++ advisor/heuristic_test.go | 3015 ++++++++ advisor/index.go | 1113 +++ advisor/index_test.go | 464 ++ advisor/rules.go | 1372 ++++ advisor/rules_test.go | 54 + advisor/testdata/TestDigestExplainText.golden | 26 + advisor/testdata/TestIndexAdviseNoEnv.golden | 115 + .../testdata/TestListHeuristicRules.golden | 1134 +++ advisor/testdata/TestListTestSQLs.golden | 80 + .../TestMergeConflictHeuristicRules.golden | 109 + ast/doc.go | 18 + ast/meta.go | 754 ++ ast/meta_test.go | 324 + ast/node_array.go | 123 + ast/pretty.go | 347 + ast/pretty_test.go | 198 + ast/rewrite.go | 1728 +++++ ast/rewrite_test.go | 685 ++ ast/testdata/TestListRewriteRules.golden | 272 + ast/testdata/TestPretty.golden | 1470 ++++ ast/tidb.go | 60 + ast/token.go | 1009 +++ ast/token_test.go | 144 + common/cases.go | 197 + common/config.go | 822 +++ common/config_test.go | 73 + common/doc.go | 18 + common/example_test.go | 63 + common/logger.go | 124 + common/logger_test.go | 59 + common/markdown.go | 156 + common/markdown_test.go | 90 + common/meta.go | 495 ++ common/meta_test.go | 140 + common/testdata/TestListReportTypes.golden | 133 + common/testdata/TestMarkdown2Html.golden | 374 + common/testdata/TestMarkdown2Html.md | 391 + common/testdata/TestMarkdownHTMLHeader.golden | 0 common/testdata/TestParseDSN.golden | 15 + common/tricks.go | 109 + database/doc.go | 18 + database/explain.go | 1069 +++ database/explain_test.go | 2454 +++++++ database/mysql.go | 310 + database/mysql_test.go | 90 + database/profiling.go | 135 + database/profiling_test.go | 53 + database/sampling.go | 230 + database/sampling_test.go | 50 + database/show.go | 584 ++ database/show_test.go | 94 + database/testdata/TestExplain.golden | 159 + .../testdata/TestExplainInfoTranslator.golden | 0 database/testdata/TestFormatProfiling.golden | 0 .../testdata/TestMySQLExplainQueryCost.golden | 0 .../testdata/TestMySQLExplainWarnings.golden | 0 .../TestPrintMarkdownExplainTable.golden | 0 database/testdata/TestSource.sql | 2 + database/trace.go | 159 + database/trace_test.go | 58 + deps.sh | 12 + doc/FAQ.md | 71 + doc/FAQ_en.md | 74 + doc/cheatsheet.md | 174 + doc/cheatsheet_en.md | 143 + doc/comparison.md | 13 + doc/comparison_en.md | 13 + doc/config.md | 102 + doc/editor_plugin.md | 34 + doc/enviorment.md | 23 + doc/example/digest_pt.py | 94 + doc/example/main_test.md | 4438 +++++++++++ doc/example/main_test.sh | 17 + doc/example/metalinter.json | 23 + doc/example/metalinter.sh | 18 + doc/example/metalinter.txt | 0 doc/example/revive.toml | 51 + doc/example/sakila.sql.gz | Bin 0 -> 690810 bytes doc/example/slow.log.digest | 83 + doc/example/soar.vim | 37 + doc/explain.md | 42 + doc/heuristic.md | 1134 +++ doc/images/env.png | Bin 0 -> 60038 bytes doc/images/logo.ascii | 5 + doc/images/logo.png | Bin 0 -> 63912 bytes doc/images/qq.jpg | Bin 0 -> 116720 bytes doc/images/qq.png | Bin 0 -> 88017 bytes doc/images/structure.png | Bin 0 -> 35279 bytes doc/images/vim_plugin.png | Bin 0 -> 38799 bytes doc/indexing.md | 218 + doc/install.md | 52 + doc/install_en.md | 19 + doc/js/pretty.js | 1110 +++ doc/report_type.md | 133 + doc/rewrite.md | 272 + doc/roadmap.md | 9 + doc/structure.md | 51 + doc/thanks.md | 39 + doc/thanks_en.md | 39 + doc/themes/foghorn.css | 141 + doc/themes/ghostwriter.css | 413 ++ doc/themes/github-dark.css | 765 ++ doc/themes/github.css | 713 ++ doc/themes/godspeed.css | 626 ++ doc/themes/markdown-alt.css | 75 + doc/themes/markdown.css | 102 + doc/themes/markdown5.css | 139 + doc/themes/markdown6.css | 222 + doc/themes/markdown7.css | 295 + doc/themes/markdown8.css | 136 + doc/themes/markdown9.css | 138 + doc/themes/markedapp-byword.css | 314 + doc/themes/new-modern.css | 482 ++ doc/themes/radar.css | 355 + doc/themes/screen.css | 77 + doc/themes/solarized-dark.css | 294 + doc/themes/solarized-light.css | 294 + doc/themes/torpedo.css | 666 ++ doc/themes/vostok.css | 712 ++ env/doc.go | 18 + env/env.go | 485 ++ env/env_test.go | 182 + env/testdata/TestNewVirtualEnv.golden | 42 + etc/soar.blacklist | 9 + etc/soar.yaml | 20 + genver.sh | 46 + retool-install.sh | 28 + revive.toml | 51 + tools.json | 45 + vendor/github.com/astaxie/beego/LICENSE | 13 + .../github.com/astaxie/beego/logs/README.md | 63 + .../astaxie/beego/logs/accesslog.go | 86 + vendor/github.com/astaxie/beego/logs/color.go | 28 + .../astaxie/beego/logs/color_windows.go | 428 ++ vendor/github.com/astaxie/beego/logs/conn.go | 117 + .../github.com/astaxie/beego/logs/console.go | 101 + vendor/github.com/astaxie/beego/logs/file.go | 335 + .../github.com/astaxie/beego/logs/jianliao.go | 72 + vendor/github.com/astaxie/beego/logs/log.go | 646 ++ .../github.com/astaxie/beego/logs/logger.go | 208 + .../astaxie/beego/logs/multifile.go | 116 + vendor/github.com/astaxie/beego/logs/slack.go | 60 + vendor/github.com/astaxie/beego/logs/smtp.go | 149 + vendor/github.com/dchest/uniuri/README.md | 97 + vendor/github.com/dchest/uniuri/uniuri.go | 81 + .../gedex/inflector/CakePHP_LICENSE.txt | 28 + vendor/github.com/gedex/inflector/LICENSE.md | 29 + vendor/github.com/gedex/inflector/README.md | 25 + .../github.com/gedex/inflector/inflector.go | 355 + vendor/github.com/golang/glog/LICENSE | 191 + vendor/github.com/golang/glog/README | 44 + vendor/github.com/golang/glog/glog.go | 1180 +++ vendor/github.com/golang/glog/glog_file.go | 124 + vendor/github.com/golang/protobuf/LICENSE | 28 + .../github.com/golang/protobuf/proto/clone.go | 253 + .../golang/protobuf/proto/decode.go | 428 ++ .../golang/protobuf/proto/discard.go | 350 + .../golang/protobuf/proto/encode.go | 218 + .../github.com/golang/protobuf/proto/equal.go | 300 + .../golang/protobuf/proto/extensions.go | 543 ++ .../github.com/golang/protobuf/proto/lib.go | 921 +++ .../golang/protobuf/proto/message_set.go | 314 + .../golang/protobuf/proto/pointer_reflect.go | 357 + .../golang/protobuf/proto/pointer_unsafe.go | 308 + .../golang/protobuf/proto/properties.go | 544 ++ .../golang/protobuf/proto/table_marshal.go | 2685 +++++++ .../golang/protobuf/proto/table_merge.go | 654 ++ .../golang/protobuf/proto/table_unmarshal.go | 1981 +++++ .../github.com/golang/protobuf/proto/text.go | 843 +++ .../golang/protobuf/proto/text_parser.go | 880 +++ .../github.com/golang/protobuf/ptypes/any.go | 141 + .../golang/protobuf/ptypes/any/any.pb.go | 191 + .../golang/protobuf/ptypes/any/any.proto | 149 + .../github.com/golang/protobuf/ptypes/doc.go | 35 + .../golang/protobuf/ptypes/duration.go | 102 + .../protobuf/ptypes/duration/duration.pb.go | 159 + .../protobuf/ptypes/duration/duration.proto | 117 + .../golang/protobuf/ptypes/timestamp.go | 134 + .../protobuf/ptypes/timestamp/timestamp.pb.go | 175 + .../protobuf/ptypes/timestamp/timestamp.proto | 133 + vendor/github.com/kr/pretty/License | 21 + vendor/github.com/kr/pretty/Readme | 9 + vendor/github.com/kr/pretty/diff.go | 265 + vendor/github.com/kr/pretty/formatter.go | 328 + vendor/github.com/kr/pretty/go.mod | 3 + vendor/github.com/kr/pretty/pretty.go | 108 + vendor/github.com/kr/pretty/zero.go | 41 + vendor/github.com/kr/text/License | 19 + vendor/github.com/kr/text/Readme | 3 + vendor/github.com/kr/text/doc.go | 3 + vendor/github.com/kr/text/go.mod | 3 + vendor/github.com/kr/text/indent.go | 74 + vendor/github.com/kr/text/wrap.go | 86 + vendor/github.com/percona/go-mysql/LICENSE | 661 ++ .../percona/go-mysql/query/query.go | 804 ++ .../russross/blackfriday/LICENSE.txt | 29 + .../github.com/russross/blackfriday/README.md | 363 + .../github.com/russross/blackfriday/block.go | 1451 ++++ vendor/github.com/russross/blackfriday/doc.go | 32 + .../github.com/russross/blackfriday/html.go | 938 +++ .../github.com/russross/blackfriday/inline.go | 1154 +++ .../github.com/russross/blackfriday/latex.go | 334 + .../russross/blackfriday/markdown.go | 941 +++ .../russross/blackfriday/smartypants.go | 430 ++ vendor/github.com/tidwall/gjson/LICENSE | 20 + vendor/github.com/tidwall/gjson/README.md | 401 + vendor/github.com/tidwall/gjson/gjson.go | 2118 ++++++ vendor/github.com/tidwall/gjson/logo.png | Bin 0 -> 15936 bytes vendor/github.com/tidwall/match/LICENSE | 20 + vendor/github.com/tidwall/match/README.md | 32 + vendor/github.com/tidwall/match/match.go | 192 + vendor/github.com/ziutek/mymysql/LICENSE | 24 + vendor/github.com/ziutek/mymysql/README.md | 724 ++ vendor/github.com/ziutek/mymysql/all.bash | 4 + vendor/github.com/ziutek/mymysql/doc.go | 29 + .../github.com/ziutek/mymysql/mysql/errors.go | 535 ++ .../github.com/ziutek/mymysql/mysql/field.go | 15 + .../ziutek/mymysql/mysql/interface.go | 107 + vendor/github.com/ziutek/mymysql/mysql/row.go | 472 ++ .../github.com/ziutek/mymysql/mysql/status.go | 18 + .../github.com/ziutek/mymysql/mysql/types.go | 225 + .../github.com/ziutek/mymysql/mysql/utils.go | 302 + .../github.com/ziutek/mymysql/native/LICENSE | 24 + .../ziutek/mymysql/native/addons.go | 17 + .../ziutek/mymysql/native/binding.go | 150 + .../ziutek/mymysql/native/codecs.go | 458 ++ .../ziutek/mymysql/native/command.go | 149 + .../ziutek/mymysql/native/common.go | 25 + .../ziutek/mymysql/native/consts.go | 183 + .../github.com/ziutek/mymysql/native/init.go | 241 + .../github.com/ziutek/mymysql/native/mysql.go | 837 +++ .../ziutek/mymysql/native/packet.go | 250 + .../ziutek/mymysql/native/paramvalue.go | 177 + .../ziutek/mymysql/native/passwd.go | 137 + .../ziutek/mymysql/native/prepared.go | 163 + .../ziutek/mymysql/native/result.go | 445 ++ .../ziutek/mymysql/native/unsafe.go-disabled | 116 + vendor/golang.org/x/net/LICENSE | 27 + vendor/golang.org/x/net/PATENTS | 22 + vendor/golang.org/x/net/context/context.go | 56 + vendor/golang.org/x/net/context/go17.go | 72 + vendor/golang.org/x/net/context/go19.go | 20 + vendor/golang.org/x/net/context/pre_go17.go | 300 + vendor/golang.org/x/net/context/pre_go19.go | 109 + vendor/google.golang.org/genproto/LICENSE | 202 + .../googleapis/rpc/status/status.pb.go | 156 + vendor/google.golang.org/grpc/LICENSE | 202 + .../grpc/codes/code_string.go | 62 + vendor/google.golang.org/grpc/codes/codes.go | 184 + vendor/google.golang.org/grpc/status/go16.go | 42 + vendor/google.golang.org/grpc/status/go17.go | 44 + .../google.golang.org/grpc/status/status.go | 189 + vendor/gopkg.in/yaml.v2/LICENSE | 201 + vendor/gopkg.in/yaml.v2/LICENSE.libyaml | 31 + vendor/gopkg.in/yaml.v2/NOTICE | 13 + vendor/gopkg.in/yaml.v2/README.md | 133 + vendor/gopkg.in/yaml.v2/apic.go | 739 ++ vendor/gopkg.in/yaml.v2/decode.go | 775 ++ vendor/gopkg.in/yaml.v2/emitterc.go | 1685 +++++ vendor/gopkg.in/yaml.v2/encode.go | 362 + vendor/gopkg.in/yaml.v2/go.mod | 5 + vendor/gopkg.in/yaml.v2/parserc.go | 1095 +++ vendor/gopkg.in/yaml.v2/readerc.go | 412 ++ vendor/gopkg.in/yaml.v2/resolve.go | 258 + vendor/gopkg.in/yaml.v2/scannerc.go | 2696 +++++++ vendor/gopkg.in/yaml.v2/sorter.go | 113 + vendor/gopkg.in/yaml.v2/writerc.go | 26 + vendor/gopkg.in/yaml.v2/yaml.go | 466 ++ vendor/gopkg.in/yaml.v2/yamlh.go | 738 ++ vendor/gopkg.in/yaml.v2/yamlprivateh.go | 173 + vendor/vendor.json | 1084 +++ vendor/vitess.io/vitess/ADOPTERS.md | 14 + vendor/vitess.io/vitess/CODE_OF_CONDUCT.md | 3 + vendor/vitess.io/vitess/CONTRIBUTING.md | 38 + vendor/vitess.io/vitess/DCO | 37 + vendor/vitess.io/vitess/Dockerfile | 20 + vendor/vitess.io/vitess/GOVERNANCE.md | 115 + vendor/vitess.io/vitess/GUIDING_PRINCIPLES.md | 27 + vendor/vitess.io/vitess/LICENSE | 202 + vendor/vitess.io/vitess/Makefile | 251 + vendor/vitess.io/vitess/README.md | 45 + vendor/vitess.io/vitess/Vagrantfile | 56 + vendor/vitess.io/vitess/bootstrap.sh | 346 + vendor/vitess.io/vitess/dev.env | 131 + vendor/vitess.io/vitess/go/bytes2/buffer.go | 65 + vendor/vitess.io/vitess/go/hack/hack.go | 79 + .../vitess/go/sqltypes/arithmetic.go | 486 ++ .../vitess/go/sqltypes/bind_variables.go | 319 + .../vitess/go/sqltypes/event_token.go | 40 + .../vitess/go/sqltypes/plan_value.go | 259 + vendor/vitess.io/vitess/go/sqltypes/proto3.go | 235 + .../vitess/go/sqltypes/query_response.go | 42 + vendor/vitess.io/vitess/go/sqltypes/result.go | 271 + .../vitess.io/vitess/go/sqltypes/testing.go | 154 + vendor/vitess.io/vitess/go/sqltypes/type.go | 288 + vendor/vitess.io/vitess/go/sqltypes/value.go | 386 + vendor/vitess.io/vitess/go/vt/log/log.go | 54 + .../vitess/go/vt/proto/query/query.pb.go | 4323 +++++++++++ .../go/vt/proto/topodata/topodata.pb.go | 1236 ++++ .../vitess/go/vt/proto/vtgate/vtgate.pb.go | 3577 +++++++++ .../vitess/go/vt/proto/vtrpc/vtrpc.pb.go | 462 ++ .../vitess.io/vitess/go/vt/sqlparser/Makefile | 22 + .../vitess/go/vt/sqlparser/analyzer.go | 335 + .../vitess.io/vitess/go/vt/sqlparser/ast.go | 3655 ++++++++++ .../vitess/go/vt/sqlparser/comments.go | 315 + .../vitess/go/vt/sqlparser/encodable.go | 99 + .../go/vt/sqlparser/impossible_query.go | 39 + .../vitess/go/vt/sqlparser/normalizer.go | 236 + .../vitess/go/vt/sqlparser/parsed_query.go | 127 + .../vitess/go/vt/sqlparser/redact_query.go | 19 + .../vitess.io/vitess/go/vt/sqlparser/sql.go | 6481 +++++++++++++++++ vendor/vitess.io/vitess/go/vt/sqlparser/sql.y | 3330 +++++++++ .../vitess.io/vitess/go/vt/sqlparser/token.go | 956 +++ .../vitess/go/vt/sqlparser/tracked_buffer.go | 140 + .../vitess/go/vt/sqlparser/truncate_query.go | 52 + .../vitess/go/vt/vterrors/aggregate.go | 106 + vendor/vitess.io/vitess/go/vt/vterrors/doc.go | 44 + .../vitess.io/vitess/go/vt/vterrors/grpc.go | 143 + .../vitess.io/vitess/go/vt/vterrors/proto3.go | 52 + .../vitess/go/vt/vterrors/vterrors.go | 101 + vendor/vitess.io/vitess/test.go | 881 +++ 339 files changed, 121628 insertions(+) create mode 100644 .editorconfig create mode 100644 .github/CONTRIBUTING.md create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/ISSUE_TEMPLATE/question.md create mode 100644 .gitignore create mode 100644 .travis.yml create mode 100644 CHANGES.md create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 NOTICE.txt create mode 100644 README.md create mode 100644 README_EN.md create mode 100644 VERSION create mode 100644 advisor/doc.go create mode 100644 advisor/explainer.go create mode 100644 advisor/explainer_test.go create mode 100644 advisor/heuristic.go create mode 100644 advisor/heuristic_test.go create mode 100644 advisor/index.go create mode 100644 advisor/index_test.go create mode 100644 advisor/rules.go create mode 100644 advisor/rules_test.go create mode 100644 advisor/testdata/TestDigestExplainText.golden create mode 100644 advisor/testdata/TestIndexAdviseNoEnv.golden create mode 100644 advisor/testdata/TestListHeuristicRules.golden create mode 100644 advisor/testdata/TestListTestSQLs.golden create mode 100644 advisor/testdata/TestMergeConflictHeuristicRules.golden create mode 100644 ast/doc.go create mode 100644 ast/meta.go create mode 100644 ast/meta_test.go create mode 100644 ast/node_array.go create mode 100644 ast/pretty.go create mode 100644 ast/pretty_test.go create mode 100644 ast/rewrite.go create mode 100644 ast/rewrite_test.go create mode 100644 ast/testdata/TestListRewriteRules.golden create mode 100644 ast/testdata/TestPretty.golden create mode 100644 ast/tidb.go create mode 100644 ast/token.go create mode 100644 ast/token_test.go create mode 100644 common/cases.go create mode 100644 common/config.go create mode 100644 common/config_test.go create mode 100644 common/doc.go create mode 100644 common/example_test.go create mode 100644 common/logger.go create mode 100644 common/logger_test.go create mode 100644 common/markdown.go create mode 100644 common/markdown_test.go create mode 100644 common/meta.go create mode 100644 common/meta_test.go create mode 100644 common/testdata/TestListReportTypes.golden create mode 100644 common/testdata/TestMarkdown2Html.golden create mode 100644 common/testdata/TestMarkdown2Html.md create mode 100644 common/testdata/TestMarkdownHTMLHeader.golden create mode 100644 common/testdata/TestParseDSN.golden create mode 100644 common/tricks.go create mode 100644 database/doc.go create mode 100644 database/explain.go create mode 100644 database/explain_test.go create mode 100644 database/mysql.go create mode 100644 database/mysql_test.go create mode 100644 database/profiling.go create mode 100644 database/profiling_test.go create mode 100644 database/sampling.go create mode 100644 database/sampling_test.go create mode 100644 database/show.go create mode 100644 database/show_test.go create mode 100644 database/testdata/TestExplain.golden create mode 100644 database/testdata/TestExplainInfoTranslator.golden create mode 100644 database/testdata/TestFormatProfiling.golden create mode 100644 database/testdata/TestMySQLExplainQueryCost.golden create mode 100644 database/testdata/TestMySQLExplainWarnings.golden create mode 100644 database/testdata/TestPrintMarkdownExplainTable.golden create mode 100644 database/testdata/TestSource.sql create mode 100644 database/trace.go create mode 100644 database/trace_test.go create mode 100755 deps.sh create mode 100644 doc/FAQ.md create mode 100644 doc/FAQ_en.md create mode 100644 doc/cheatsheet.md create mode 100644 doc/cheatsheet_en.md create mode 100644 doc/comparison.md create mode 100644 doc/comparison_en.md create mode 100644 doc/config.md create mode 100644 doc/editor_plugin.md create mode 100644 doc/enviorment.md create mode 100755 doc/example/digest_pt.py create mode 100644 doc/example/main_test.md create mode 100755 doc/example/main_test.sh create mode 100644 doc/example/metalinter.json create mode 100755 doc/example/metalinter.sh create mode 100644 doc/example/metalinter.txt create mode 100644 doc/example/revive.toml create mode 100644 doc/example/sakila.sql.gz create mode 100644 doc/example/slow.log.digest create mode 100644 doc/example/soar.vim create mode 100644 doc/explain.md create mode 100644 doc/heuristic.md create mode 100644 doc/images/env.png create mode 100644 doc/images/logo.ascii create mode 100644 doc/images/logo.png create mode 100644 doc/images/qq.jpg create mode 100644 doc/images/qq.png create mode 100644 doc/images/structure.png create mode 100644 doc/images/vim_plugin.png create mode 100644 doc/indexing.md create mode 100644 doc/install.md create mode 100644 doc/install_en.md create mode 100644 doc/js/pretty.js create mode 100644 doc/report_type.md create mode 100644 doc/rewrite.md create mode 100644 doc/roadmap.md create mode 100644 doc/structure.md create mode 100644 doc/thanks.md create mode 100644 doc/thanks_en.md create mode 100644 doc/themes/foghorn.css create mode 100644 doc/themes/ghostwriter.css create mode 100644 doc/themes/github-dark.css create mode 100644 doc/themes/github.css create mode 100644 doc/themes/godspeed.css create mode 100644 doc/themes/markdown-alt.css create mode 100644 doc/themes/markdown.css create mode 100644 doc/themes/markdown5.css create mode 100644 doc/themes/markdown6.css create mode 100644 doc/themes/markdown7.css create mode 100644 doc/themes/markdown8.css create mode 100644 doc/themes/markdown9.css create mode 100644 doc/themes/markedapp-byword.css create mode 100644 doc/themes/new-modern.css create mode 100644 doc/themes/radar.css create mode 100644 doc/themes/screen.css create mode 100644 doc/themes/solarized-dark.css create mode 100644 doc/themes/solarized-light.css create mode 100644 doc/themes/torpedo.css create mode 100644 doc/themes/vostok.css create mode 100644 env/doc.go create mode 100644 env/env.go create mode 100644 env/env_test.go create mode 100644 env/testdata/TestNewVirtualEnv.golden create mode 100644 etc/soar.blacklist create mode 100644 etc/soar.yaml create mode 100755 genver.sh create mode 100755 retool-install.sh create mode 100644 revive.toml create mode 100644 tools.json create mode 100644 vendor/github.com/astaxie/beego/LICENSE create mode 100644 vendor/github.com/astaxie/beego/logs/README.md create mode 100644 vendor/github.com/astaxie/beego/logs/accesslog.go create mode 100644 vendor/github.com/astaxie/beego/logs/color.go create mode 100644 vendor/github.com/astaxie/beego/logs/color_windows.go create mode 100644 vendor/github.com/astaxie/beego/logs/conn.go create mode 100644 vendor/github.com/astaxie/beego/logs/console.go create mode 100644 vendor/github.com/astaxie/beego/logs/file.go create mode 100644 vendor/github.com/astaxie/beego/logs/jianliao.go create mode 100644 vendor/github.com/astaxie/beego/logs/log.go create mode 100644 vendor/github.com/astaxie/beego/logs/logger.go create mode 100644 vendor/github.com/astaxie/beego/logs/multifile.go create mode 100644 vendor/github.com/astaxie/beego/logs/slack.go create mode 100644 vendor/github.com/astaxie/beego/logs/smtp.go create mode 100644 vendor/github.com/dchest/uniuri/README.md create mode 100644 vendor/github.com/dchest/uniuri/uniuri.go create mode 100644 vendor/github.com/gedex/inflector/CakePHP_LICENSE.txt create mode 100644 vendor/github.com/gedex/inflector/LICENSE.md create mode 100644 vendor/github.com/gedex/inflector/README.md create mode 100644 vendor/github.com/gedex/inflector/inflector.go create mode 100644 vendor/github.com/golang/glog/LICENSE create mode 100644 vendor/github.com/golang/glog/README create mode 100644 vendor/github.com/golang/glog/glog.go create mode 100644 vendor/github.com/golang/glog/glog_file.go create mode 100644 vendor/github.com/golang/protobuf/LICENSE create mode 100644 vendor/github.com/golang/protobuf/proto/clone.go create mode 100644 vendor/github.com/golang/protobuf/proto/decode.go create mode 100644 vendor/github.com/golang/protobuf/proto/discard.go create mode 100644 vendor/github.com/golang/protobuf/proto/encode.go create mode 100644 vendor/github.com/golang/protobuf/proto/equal.go create mode 100644 vendor/github.com/golang/protobuf/proto/extensions.go create mode 100644 vendor/github.com/golang/protobuf/proto/lib.go create mode 100644 vendor/github.com/golang/protobuf/proto/message_set.go create mode 100644 vendor/github.com/golang/protobuf/proto/pointer_reflect.go create mode 100644 vendor/github.com/golang/protobuf/proto/pointer_unsafe.go create mode 100644 vendor/github.com/golang/protobuf/proto/properties.go create mode 100644 vendor/github.com/golang/protobuf/proto/table_marshal.go create mode 100644 vendor/github.com/golang/protobuf/proto/table_merge.go create mode 100644 vendor/github.com/golang/protobuf/proto/table_unmarshal.go create mode 100644 vendor/github.com/golang/protobuf/proto/text.go create mode 100644 vendor/github.com/golang/protobuf/proto/text_parser.go create mode 100644 vendor/github.com/golang/protobuf/ptypes/any.go create mode 100644 vendor/github.com/golang/protobuf/ptypes/any/any.pb.go create mode 100644 vendor/github.com/golang/protobuf/ptypes/any/any.proto create mode 100644 vendor/github.com/golang/protobuf/ptypes/doc.go create mode 100644 vendor/github.com/golang/protobuf/ptypes/duration.go create mode 100644 vendor/github.com/golang/protobuf/ptypes/duration/duration.pb.go create mode 100644 vendor/github.com/golang/protobuf/ptypes/duration/duration.proto create mode 100644 vendor/github.com/golang/protobuf/ptypes/timestamp.go create mode 100644 vendor/github.com/golang/protobuf/ptypes/timestamp/timestamp.pb.go create mode 100644 vendor/github.com/golang/protobuf/ptypes/timestamp/timestamp.proto create mode 100644 vendor/github.com/kr/pretty/License create mode 100644 vendor/github.com/kr/pretty/Readme create mode 100644 vendor/github.com/kr/pretty/diff.go create mode 100644 vendor/github.com/kr/pretty/formatter.go create mode 100644 vendor/github.com/kr/pretty/go.mod create mode 100644 vendor/github.com/kr/pretty/pretty.go create mode 100644 vendor/github.com/kr/pretty/zero.go create mode 100644 vendor/github.com/kr/text/License create mode 100644 vendor/github.com/kr/text/Readme create mode 100644 vendor/github.com/kr/text/doc.go create mode 100644 vendor/github.com/kr/text/go.mod create mode 100644 vendor/github.com/kr/text/indent.go create mode 100644 vendor/github.com/kr/text/wrap.go create mode 100644 vendor/github.com/percona/go-mysql/LICENSE create mode 100644 vendor/github.com/percona/go-mysql/query/query.go create mode 100644 vendor/github.com/russross/blackfriday/LICENSE.txt create mode 100644 vendor/github.com/russross/blackfriday/README.md create mode 100644 vendor/github.com/russross/blackfriday/block.go create mode 100644 vendor/github.com/russross/blackfriday/doc.go create mode 100644 vendor/github.com/russross/blackfriday/html.go create mode 100644 vendor/github.com/russross/blackfriday/inline.go create mode 100644 vendor/github.com/russross/blackfriday/latex.go create mode 100644 vendor/github.com/russross/blackfriday/markdown.go create mode 100644 vendor/github.com/russross/blackfriday/smartypants.go create mode 100644 vendor/github.com/tidwall/gjson/LICENSE create mode 100644 vendor/github.com/tidwall/gjson/README.md create mode 100644 vendor/github.com/tidwall/gjson/gjson.go create mode 100644 vendor/github.com/tidwall/gjson/logo.png create mode 100644 vendor/github.com/tidwall/match/LICENSE create mode 100644 vendor/github.com/tidwall/match/README.md create mode 100644 vendor/github.com/tidwall/match/match.go create mode 100644 vendor/github.com/ziutek/mymysql/LICENSE create mode 100644 vendor/github.com/ziutek/mymysql/README.md create mode 100755 vendor/github.com/ziutek/mymysql/all.bash create mode 100644 vendor/github.com/ziutek/mymysql/doc.go create mode 100644 vendor/github.com/ziutek/mymysql/mysql/errors.go create mode 100644 vendor/github.com/ziutek/mymysql/mysql/field.go create mode 100644 vendor/github.com/ziutek/mymysql/mysql/interface.go create mode 100644 vendor/github.com/ziutek/mymysql/mysql/row.go create mode 100644 vendor/github.com/ziutek/mymysql/mysql/status.go create mode 100644 vendor/github.com/ziutek/mymysql/mysql/types.go create mode 100644 vendor/github.com/ziutek/mymysql/mysql/utils.go create mode 100644 vendor/github.com/ziutek/mymysql/native/LICENSE create mode 100644 vendor/github.com/ziutek/mymysql/native/addons.go create mode 100644 vendor/github.com/ziutek/mymysql/native/binding.go create mode 100644 vendor/github.com/ziutek/mymysql/native/codecs.go create mode 100644 vendor/github.com/ziutek/mymysql/native/command.go create mode 100644 vendor/github.com/ziutek/mymysql/native/common.go create mode 100644 vendor/github.com/ziutek/mymysql/native/consts.go create mode 100644 vendor/github.com/ziutek/mymysql/native/init.go create mode 100644 vendor/github.com/ziutek/mymysql/native/mysql.go create mode 100644 vendor/github.com/ziutek/mymysql/native/packet.go create mode 100644 vendor/github.com/ziutek/mymysql/native/paramvalue.go create mode 100644 vendor/github.com/ziutek/mymysql/native/passwd.go create mode 100644 vendor/github.com/ziutek/mymysql/native/prepared.go create mode 100644 vendor/github.com/ziutek/mymysql/native/result.go create mode 100644 vendor/github.com/ziutek/mymysql/native/unsafe.go-disabled create mode 100644 vendor/golang.org/x/net/LICENSE create mode 100644 vendor/golang.org/x/net/PATENTS create mode 100644 vendor/golang.org/x/net/context/context.go create mode 100644 vendor/golang.org/x/net/context/go17.go create mode 100644 vendor/golang.org/x/net/context/go19.go create mode 100644 vendor/golang.org/x/net/context/pre_go17.go create mode 100644 vendor/golang.org/x/net/context/pre_go19.go create mode 100644 vendor/google.golang.org/genproto/LICENSE create mode 100644 vendor/google.golang.org/genproto/googleapis/rpc/status/status.pb.go create mode 100644 vendor/google.golang.org/grpc/LICENSE create mode 100644 vendor/google.golang.org/grpc/codes/code_string.go create mode 100644 vendor/google.golang.org/grpc/codes/codes.go create mode 100644 vendor/google.golang.org/grpc/status/go16.go create mode 100644 vendor/google.golang.org/grpc/status/go17.go create mode 100644 vendor/google.golang.org/grpc/status/status.go create mode 100644 vendor/gopkg.in/yaml.v2/LICENSE create mode 100644 vendor/gopkg.in/yaml.v2/LICENSE.libyaml create mode 100644 vendor/gopkg.in/yaml.v2/NOTICE create mode 100644 vendor/gopkg.in/yaml.v2/README.md create mode 100644 vendor/gopkg.in/yaml.v2/apic.go create mode 100644 vendor/gopkg.in/yaml.v2/decode.go create mode 100644 vendor/gopkg.in/yaml.v2/emitterc.go create mode 100644 vendor/gopkg.in/yaml.v2/encode.go create mode 100644 vendor/gopkg.in/yaml.v2/go.mod create mode 100644 vendor/gopkg.in/yaml.v2/parserc.go create mode 100644 vendor/gopkg.in/yaml.v2/readerc.go create mode 100644 vendor/gopkg.in/yaml.v2/resolve.go create mode 100644 vendor/gopkg.in/yaml.v2/scannerc.go create mode 100644 vendor/gopkg.in/yaml.v2/sorter.go create mode 100644 vendor/gopkg.in/yaml.v2/writerc.go create mode 100644 vendor/gopkg.in/yaml.v2/yaml.go create mode 100644 vendor/gopkg.in/yaml.v2/yamlh.go create mode 100644 vendor/gopkg.in/yaml.v2/yamlprivateh.go create mode 100644 vendor/vendor.json create mode 100644 vendor/vitess.io/vitess/ADOPTERS.md create mode 100644 vendor/vitess.io/vitess/CODE_OF_CONDUCT.md create mode 100644 vendor/vitess.io/vitess/CONTRIBUTING.md create mode 100644 vendor/vitess.io/vitess/DCO create mode 100644 vendor/vitess.io/vitess/Dockerfile create mode 100644 vendor/vitess.io/vitess/GOVERNANCE.md create mode 100644 vendor/vitess.io/vitess/GUIDING_PRINCIPLES.md create mode 100644 vendor/vitess.io/vitess/LICENSE create mode 100644 vendor/vitess.io/vitess/Makefile create mode 100644 vendor/vitess.io/vitess/README.md create mode 100644 vendor/vitess.io/vitess/Vagrantfile create mode 100755 vendor/vitess.io/vitess/bootstrap.sh create mode 100644 vendor/vitess.io/vitess/dev.env create mode 100644 vendor/vitess.io/vitess/go/bytes2/buffer.go create mode 100644 vendor/vitess.io/vitess/go/hack/hack.go create mode 100644 vendor/vitess.io/vitess/go/sqltypes/arithmetic.go create mode 100644 vendor/vitess.io/vitess/go/sqltypes/bind_variables.go create mode 100644 vendor/vitess.io/vitess/go/sqltypes/event_token.go create mode 100644 vendor/vitess.io/vitess/go/sqltypes/plan_value.go create mode 100644 vendor/vitess.io/vitess/go/sqltypes/proto3.go create mode 100644 vendor/vitess.io/vitess/go/sqltypes/query_response.go create mode 100644 vendor/vitess.io/vitess/go/sqltypes/result.go create mode 100644 vendor/vitess.io/vitess/go/sqltypes/testing.go create mode 100644 vendor/vitess.io/vitess/go/sqltypes/type.go create mode 100644 vendor/vitess.io/vitess/go/sqltypes/value.go create mode 100644 vendor/vitess.io/vitess/go/vt/log/log.go create mode 100644 vendor/vitess.io/vitess/go/vt/proto/query/query.pb.go create mode 100644 vendor/vitess.io/vitess/go/vt/proto/topodata/topodata.pb.go create mode 100644 vendor/vitess.io/vitess/go/vt/proto/vtgate/vtgate.pb.go create mode 100644 vendor/vitess.io/vitess/go/vt/proto/vtrpc/vtrpc.pb.go create mode 100644 vendor/vitess.io/vitess/go/vt/sqlparser/Makefile create mode 100644 vendor/vitess.io/vitess/go/vt/sqlparser/analyzer.go create mode 100644 vendor/vitess.io/vitess/go/vt/sqlparser/ast.go create mode 100644 vendor/vitess.io/vitess/go/vt/sqlparser/comments.go create mode 100644 vendor/vitess.io/vitess/go/vt/sqlparser/encodable.go create mode 100644 vendor/vitess.io/vitess/go/vt/sqlparser/impossible_query.go create mode 100644 vendor/vitess.io/vitess/go/vt/sqlparser/normalizer.go create mode 100644 vendor/vitess.io/vitess/go/vt/sqlparser/parsed_query.go create mode 100644 vendor/vitess.io/vitess/go/vt/sqlparser/redact_query.go create mode 100644 vendor/vitess.io/vitess/go/vt/sqlparser/sql.go create mode 100644 vendor/vitess.io/vitess/go/vt/sqlparser/sql.y create mode 100644 vendor/vitess.io/vitess/go/vt/sqlparser/token.go create mode 100644 vendor/vitess.io/vitess/go/vt/sqlparser/tracked_buffer.go create mode 100644 vendor/vitess.io/vitess/go/vt/sqlparser/truncate_query.go create mode 100644 vendor/vitess.io/vitess/go/vt/vterrors/aggregate.go create mode 100644 vendor/vitess.io/vitess/go/vt/vterrors/doc.go create mode 100644 vendor/vitess.io/vitess/go/vt/vterrors/grpc.go create mode 100644 vendor/vitess.io/vitess/go/vt/vterrors/proto3.go create mode 100644 vendor/vitess.io/vitess/go/vt/vterrors/vterrors.go create mode 100755 vendor/vitess.io/vitess/test.go diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..43c6a002 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,10 @@ +[*] +end_of_line = lf +insert_final_newline = true +charset = utf-8 + +# tab_size = 4 spaces +[*.go] +indent_style = tab +indent_size = 4 +trim_trailing_whitespace = true diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 00000000..0346f4eb --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,3 @@ +Ask questions at [Gitter](https://gitter.im/xiaomi-dba/soar). + +[Open an issue](https://github.com/xiaomi/soar/issues/new) to discuss your plans before doing any work on SOAR. diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..6f9b70a5 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,23 @@ +--- +name: Bug Report +about: You're experiencing an issue with SOAR that is different than the documented behavior. + +--- + +Please answer these questions before submitting your issue. Thanks! + +1. What did you do? +If possible, provide a recipe for reproducing the error. + + +2. What did you expect to see? + + + +3. What did you see instead? + + + +4. What version of are you using (`soar -version`)? + + diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..fa3f698d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,15 @@ +--- +name: Feature Request +about: If you have something you think SOAR could improve or add support for. + +--- + +Please search the existing issues for relevant feature requests, add upvotes to pre-existing requests. + +#### Feature Description + +A written overview of the feature. + +#### Use Case(s) + +Any relevant use-cases that you see. diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md new file mode 100644 index 00000000..8212e47f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/question.md @@ -0,0 +1,10 @@ +--- +name: Question +about: If you have a question, please check out our other community resources instead of opening an issue. + +--- + +Issues on GitHub are intended to be related to bugs or feature requests, so we recommend using our other community resources instead of asking here. + +- [SOAR Doc](http://github.com/XiaoMi/soar/blob/master/README.md) +- Any other questions can be asked in the community [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/xiaomi-dba/soar) diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..8a726920 --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +soar +soar.darwin-386 +soar.darwin-amd64 +soar.linux-386 +soar.linux-amd64 +soar.windows-386 +soar.windows-amd64 +common/version.go +doc/blueprint/ +*.iml +*.swp +*.log +coverage.* +y.output + +.DS_Store +.vscode/ +.idea +_tools/ + +TestMarkdown2Html.html diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..d03d6d57 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,14 @@ +language: go +sudo: false +go: + - 1.10 + +before_install: + - go get -u gopkg.in/alecthomas/gometalinter.v1 + - gometalinter.v1 --install + +script: + - gometalinter.v1 --config doc/example/metalinter.json ./... + +after_success: + - bash <(curl -s https://codecov.io/bash) diff --git a/CHANGES.md b/CHANGES.md new file mode 100644 index 00000000..a3cd8d16 --- /dev/null +++ b/CHANGES.md @@ -0,0 +1,69 @@ +# 更新日志 + +## 2018-10 +- 2018-10-20 开源先锋日(OSCAR)对外正式开源发布代码 + +## 2018-09 +- 修复多个启发式建议不准确BUG,优化部分建议文案使得建议更清晰 +- 基于TiDB Parser完善多个DDL类型语句的建议 +- 新增lint report-type类型,支持Vim Plugin优化建议输出 +- 更新整理项目文档,开源准备 +- 2018-09-21 Gdevops SOAR首次对外进行技术分享宣传 + +## 2018-08 +- 利用docker临时容器进行daily测试 +- 添加main_test全功能回归测试 +- 修复在测试中发现的问题 +- mymysql合并MySQL8.0相关PR,修改vendor依赖 +- 改善HeuristicRule中的文案 +- 持续集成Vitess Parser的改进 +- NewQuery4Audit结构体中引入TiDB Parser +- 通过TiAST完成大量与 DDL 相关的TODO +- 修改heuristic rules检查的返回值,提升拓展性 +- 建议中引入Position,用于表示建议产生于SQL的位置 +- 新增多个HeuristicRule +- Makefile中添加依赖检查,优化Makefile中逻辑,添加新功能 +- 优化gometalinter性能,引入新的代码质量检测工具,提升代码质量 +- 引入 retool 用于管理依赖的工具 +- 优化 doc 文档 + +## 2018-07 +- 补充文档,添加项目LOGO +- 改善代码质量提升测试覆盖度 +- mymysql升级,支持MySQL 8.0 +- 提供remove-comment小工具 +- 提供索引重复检查小工具 +- HeuristicRule新增RuleSpaceAfterDot +- 支持字符集和Collation不相同时的隐式数据类型转换的检查 + +## 2018-06 +- 支持更多的SQL Rewrite规则 +- 添加SQL执行超时限制 +- 索引优化建议支持对约束的检查 +- 修复数据采样中null值处理不正确的问题 +- Explain支持last_query_cost + +## 2018-05 +- 添加数据采样功能 +- 添加语句执行安全检查 +- 支持DDL语法检查 +- 支持DDL在测试环境的执行 +- 支持隐式数据类型转换检查 +- 支持索引去重 +- 索引优化建议支持前缀索引 +- 支持SQL Pretty输出 + +## 2018-04 +- 支持语法检查 +- 支持测试环境 +- 支持MySQL原数据的获取 +- 支持基于数据库环境信息给予索引优化建议 +- 支持不依赖数据库原信息的简单索引优化建议 +- 添加日志模块 +- 引入配置文件 + +## 2018-03 +- 基本架构设计 +- 添加大量底层函数用于处理AST +- 添加Insert、Delete、Update转写成Select的基本函数 +- 支持MySQL Explain信息输出 diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..d6456956 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..b4c41545 --- /dev/null +++ b/Makefile @@ -0,0 +1,206 @@ +# This how we want to name the binary output +# +# use checkmake linter https://github.com/mrtazz/checkmake +# $ checkmake Makefile +# +BINARY=soar +PATH := ${GOPATH}/bin:$(PATH) + +# These are the values we want to pass for VERSION and BUILD +BUILD_TIME=`date +%Y%m%d%H%M` +COMMIT_VERSION=`git rev-parse HEAD` +GO_VERSION_MIN=1.10 + +# Add mysql version for testing `MYSQL_VERSION=5.7 make docker` +# use mysql:latest as default +MYSQL_VERSION := $(or ${MYSQL_VERSION}, ${MYSQL_VERSION}, latest) + +.PHONY: all +all: | fmt build + +# Dependency check +.PHONY: deps +deps: + @echo "\033[92mDependency check\033[0m" + @bash ./deps.sh + # The retool tools.json is setup from retool-install.sh + retool sync + retool do gometalinter.v2 intall + +# Code format +.PHONY: fmt +fmt: + @echo "\033[92mRun gofmt on all source files ...\033[0m" + @echo "gofmt -l -s -w ..." + @ret=0 && for d in $$(go list -f '{{.Dir}}' ./... | grep -v /vendor/); do \ + gofmt -l -s -w $$d/*.go || ret=$$? ; \ + done ; exit $$ret + +# Run golang test cases +.PHONY: test +test: + @echo "\033[92mRun all test cases ...\033[0m" + go test ./... + @echo "test Success!" + +# Code Coverage +# colorful coverage numerical >=90% GREEN, <80% RED, Other YELLOW +.PHONY: cover +cover: test + @echo "\033[92mRun test cover check ...\033[0m" + go test -coverpkg=./... -coverprofile=coverage.data ./... | column -t + go tool cover -html=coverage.data -o coverage.html + go tool cover -func=coverage.data -o coverage.txt + @tail -n 1 coverage.txt | awk '{sub(/%/, "", $$NF); \ + if($$NF < 80) \ + {print "\033[91m"$$0"%\033[0m"} \ + else if ($$NF >= 90) \ + {print "\033[92m"$$0"%\033[0m"} \ + else \ + {print "\033[93m"$$0"%\033[0m"}}' + +# Builds the project +build: fmt tidb-parser + @echo "\033[92mBuilding ...\033[0m" + @bash ./genver.sh $(GO_VERSION_MIN) + @ret=0 && for d in $$(go list -f '{{if (eq .Name "main")}}{{.ImportPath}}{{end}}' ./...); do \ + go build $$d || ret=$$? ; \ + done ; exit $$ret + @echo "build Success!" + +.PHONY: fast +fast: + @echo "\033[92mBuilding ...\033[0m" + @bash ./genver.sh $(GO_VERSION_MIN) + @ret=0 && for d in $$(go list -f '{{if (eq .Name "main")}}{{.ImportPath}}{{end}}' ./...); do \ + go build $$d || ret=$$? ; \ + done ; exit $$ret + @echo "build Success!" + +# Installs our project: copies binaries +install: build + @echo "\033[92mInstall ...\033[0m" + go install ./... + @echo "install Success!" + +# Generate doc use -list* command +.PHONY: doc +doc: fast + @echo "\033[92mAuto generate doc ...\033[0m" + ./soar -list-heuristic-rules > doc/heuristic.md + ./soar -list-rewrite-rules > doc/rewrite.md + ./soar -list-report-types > doc/report_type.md + +# Add or change a heuristic rule +.PHONY: heuristic +heuristic: doc docker + @echo "\033[92mUpdate Heuristic rule golden files ...\033[0m" + go test github.com/XiaoMi/soar/advisor -v -update -run TestListHeuristicRules + go test github.com/XiaoMi/soar/advisor -v -update -run TestMergeConflictHeuristicRules + docker stop soar-mysql 2>/dev/null || true + +# Update vitess vendor +.PHONY: vitess +vitess: + @echo "\033[92mUpdate vitess deps ...\033[0m" + govendor fetch -v vitess.io/vitess/... + +# Update tidb vendor +.PHONY: tidb +tidb: + @echo "\033[92mUpdate tidb deps ...\033[0m" + @echo -n "Current TiDB commit hash: " + @(cd ${GOPATH}/src/github.com/pingcap/tidb/ 2>/dev/null && git checkout master && git rev-parse HEAD) || echo "(init)" + go get -v -u github.com/pingcap/tidb/store/tikv + @echo -n "TiDB update to: " + @cd ${GOPATH}/src/github.com/pingcap/tidb/ && git rev-parse HEAD + +# Update all vendor +.PHONY: vendor +vendor: vitess tidb + +# make tidb parser +.PHONY: tidb-parser +tidb-parser: tidb + @echo "\033[92mimporting tidb sql parser ...\033[0m" + @cd ${GOPATH}/src/github.com/pingcap/tidb && git checkout -b soar ec9672cea6612481b1da845dbab620b7a5581ca4 && make parser + +# gometalinter +# 如果有不想改的lint问题可以使用metalinter.sh加黑名单 +#@bash doc/example/metalinter.sh +.PHONY: lint +lint: build + @echo "\033[92mRun linter check ...\033[0m" + CGO_ENABLED=0 retool do gometalinter.v2 -j 1 --config doc/example/metalinter.json ./... + retool do revive -formatter friendly --exclude vendor/... -config doc/example/revive.toml ./... + retool do golangci-lint --tests=false run + @echo "gometalinter check your code is pretty good" + +.PHONY: release +release: deps build + @echo "\033[92mCross platform building for release ...\033[0m" + @for GOOS in darwin linux windows; do \ + for GOARCH in 386 amd64; do \ + for d in $$(go list -f '{{if (eq .Name "main")}}{{.ImportPath}}{{end}}' ./...); do \ + b=$$(basename $${d}) ; \ + echo "Building $${b}.$${GOOS}-$${GOARCH} ..."; \ + GOOS=$${GOOS} GOARCH=$${GOARCH} go build -ldflags="-s -w" -v -o $${b}.$${GOOS}-$${GOARCH} $$d 2>/dev/null ; \ + done ; \ + done ;\ + done + +.PHONY: docker +docker: + @echo "\033[92mBuild mysql test enviorment\033[0m" + @docker stop soar-mysql 2>/dev/null || true + @echo "docker run --name soar-mysql mysql:$(MYSQL_VERSION)" + @docker run --name soar-mysql --rm -d \ + -e MYSQL_ROOT_PASSWORD=1tIsB1g3rt \ + -e MYSQL_DATABASE=sakila \ + -p 3306:3306 \ + -v `pwd`/doc/example/sakila.sql.gz:/docker-entrypoint-initdb.d/sakila.sql.gz \ + mysql:$(MYSQL_VERSION) + + @echo -n "waiting for sakila database initializing " + @while ! mysql -h 127.0.0.1 -u root sakila -p1tIsB1g3rt -NBe "do 1;" 2>/dev/null; do \ + printf '.' ; \ + sleep 1 ; \ + done ; \ + echo '.' + @echo "mysql test enviorment is ready!" + +.PHONY: connect +connect: + mysql -h 127.0.0.1 -u root -p1tIsB1g3rt + +.PHONY: main_test +main_test: install + @echo "\033[92mrunning main_test\033[0m" + @echo "soar -list-test-sqls | soar" + @./doc/example/main_test.sh + @echo "main_test Success!" + +.PHONY: daily +daily: | deps fmt vendor tidb-parser docker cover doc lint release install main_test clean logo + @echo "\033[92mdaily build finished\033[0m" + +.PHONY: logo +logo: + @echo "\033[93m" + @cat doc/images/logo.ascii + @echo "\033[m" + +# Cleans our projects: deletes binaries +.PHONY: clean +clean: + @echo "\033[92mCleanup ...\033[0m" + go clean + @for GOOS in darwin linux windows; do \ + for GOARCH in 386 amd64; do \ + rm -f ${BINARY}.$${GOOS}-$${GOARCH} ;\ + done ;\ + done + rm -f ${BINARY} coverage.* + find . -name "*.log" -delete + git clean -fi + docker stop soar-mysql 2>/dev/null || true diff --git a/NOTICE.txt b/NOTICE.txt new file mode 100644 index 00000000..84fbfb3a --- /dev/null +++ b/NOTICE.txt @@ -0,0 +1,8 @@ + +Copyright 2018 Xiaomi, Inc. All Rights Reserved. +This product includes software developed by Xiaomi, Inc. +(http://www.mi.com/). +This product is licensed to you under the Apache License, Version 2.0 +(the "License"). You may not use this product except in compliance with +the License. + diff --git a/README.md b/README.md new file mode 100644 index 00000000..d630b80b --- /dev/null +++ b/README.md @@ -0,0 +1,37 @@ +![SOAR](http://github.com/XiaoMi/soar/raw/master/doc/images/logo.png) + +[![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/xiaomi-dba/soar) [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](http://github.com/XiaoMi/soar/blob/master/LICENSE) + +[文档](http://github.com/XiaoMi/soar/tree/master/doc) | [FAQ](http://github.com/XiaoMi/soar/blob/master/doc/FAQ.md) | [变更记录](http://github.com/XiaoMi/soar/blob/master/CHANGES.md) | [路线图](http://github.com/XiaoMi/soar/blob/master/doc/roadmap.md) | [English](http://github.com/XiaoMi/soar/blob/master/README_EN.md) + +## SOAR + +SOAR(SQL Optimizer And Rewriter)是一个对SQL进行优化和改写的自动化工具。 由小米人工智能与云平台的数据库团队开发与维护。 + +## 功能特点 +* 跨平台支持(支持Linux, Mac环境,Windows环境理论上也支持,不过未全面测试) +* 支持基于启发式算法的语句优化 +* 支持复杂查询的多列索引优化(UPDATE, INSERT, DELETE, SELECT) +* 支持EXPLAIN信息丰富解读 +* 支持SQL指纹、压缩和美化 +* 支持同一张表多条ALTER请求合并 +* 支持自定义规则的SQL改写 + +## 快速入门 + +* [安装使用](http://github.com/XiaoMi/soar/blob/master/doc/install.md) +* [体系架构](http://github.com/XiaoMi/soar/blob/master/doc/structure.md) +* [配置文件](http://github.com/XiaoMi/soar/blob/master/doc/config.md) +* [常用命令](http://github.com/XiaoMi/soar/blob/master/doc/cheatsheet.md) +* [产品对比](http://github.com/XiaoMi/soar/blob/master/doc/comparison.md) +* [路线图](http://github.com/XiaoMi/soar/blob/master/doc/roadmap.md) + +## 交流与反馈 + +* 欢迎通过Github Issues提交问题报告与建议 +* QQ群: 779359816 +* [Gitter](https://gitter.im/xiaomi-dba/soar) + +## License + +[Apache License 2.0](http://github.com/XiaoMi/soar/blob/master/LICENSE). diff --git a/README_EN.md b/README_EN.md new file mode 100644 index 00000000..31c11daf --- /dev/null +++ b/README_EN.md @@ -0,0 +1,36 @@ +![SOAR](http://github.com/XiaoMi/soar/raw/master/doc/images/logo.png) + +[![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/xiaomi-dba/soar) [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](http://github.com/XiaoMi/soar/blob/master/LICENSE) + +[Docs](http://github.com/XiaoMi/soar/tree/master/doc) | [FAQ](http://github.com/XiaoMi/soar/blob/master/doc/FAQ_en.md) | [中文](http://github.com/XiaoMi/soar/blob/master/README.md) + +## SOAR + +SOAR (SQL Optimizer And Rewriter) is a tool, which can help SQL optimization and rewrite. It's developed and maintained by the DBA Team of Xiaomi AI&Cloud. + +## Features + +* Cross-platform support, such as Linux, Mac, and Windows +* Support Heuristic Rules Suggestion +* Support Complicate SQL Indexing Optimize +* Support EXPLAIN analyze for query plan +* Support SQL fingerprint, compress and built-in pretty print +* Support merge multi ALTER query into one SQL +* Support self-config rewrite rules from SQL Rewrite +* Suggestions were written in Chinese. But SOAR also gives many tools, which can be used without understanding Chinese. + +## QuickStart + +* [Install](http://github.com/XiaoMi/soar/blob/master/doc/install_en.md) +* [CheatSheet](http://github.com/XiaoMi/soar/blob/master/doc/cheatsheet_en.md) +* [Related works](http://github.com/XiaoMi/soar/blob/master/doc/comparison_en.md) + +## Communication + +* GitHub issues: bug reports, usage issues, feature requests +* [Gitter](https://gitter.im/xiaomi-dba/soar) +* IM QQ Group: 779359816 + +## License + +[Apache License 2.0](http://github.com/XiaoMi/soar/blob/master/LICENSE). diff --git a/VERSION b/VERSION new file mode 100644 index 00000000..a3df0a69 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +0.8.0 diff --git a/advisor/doc.go b/advisor/doc.go new file mode 100644 index 00000000..e435db2b --- /dev/null +++ b/advisor/doc.go @@ -0,0 +1,18 @@ +/* + * Copyright 2018 Xiaomi, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Package advisor contain heuristic rules, index rules and explain translator. +package advisor diff --git a/advisor/explainer.go b/advisor/explainer.go new file mode 100644 index 00000000..62c01b79 --- /dev/null +++ b/advisor/explainer.go @@ -0,0 +1,285 @@ +/* + * Copyright 2018 Xiaomi, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package advisor + +import ( + "fmt" + "strings" + + "github.com/XiaoMi/soar/common" + "github.com/XiaoMi/soar/database" +) + +var explainRuleID int + +// [EXP.XXX]Rule +var explainRules map[string]Rule + +// [table_name]"suggest text" +var tablesSuggests map[string][]string + +/* +var explainIgnoreTables = []string{ + "dual", + "", +} +*/ + +// explain建议的形式 +// Item: EXP.XXX +// Severity: L[0-8] +// Summary: full table scan, not use index, full index scan... +// Content: XX TABLE xxx + +// +func checkExplainSelectType(exp *database.ExplainInfo) { + // 判断是否跳过不检查 + if len(common.Config.ExplainWarnSelectType) == 1 { + if common.Config.ExplainWarnSelectType[0] == "" { + return + } + } else if len(common.Config.ExplainWarnSelectType) < 1 { + return + } + + if exp.ExplainFormat == database.JSONFormatExplain { + // TODO + // JSON形式遍历分析不方便,转成Row格式也没有SelectType暂不处理 + return + } + for _, v := range common.Config.ExplainWarnSelectType { + for _, row := range exp.ExplainRows { + if row.SelectType == v && v != "" { + tablesSuggests[row.TableName] = append(tablesSuggests[row.TableName], fmt.Sprintf("SelectType:%s", row.SelectType)) + } + } + } +} + +// 用户可以设置AccessType的建议级别,匹配到的查询会给出建议 +func checkExplainAccessType(exp *database.ExplainInfo) { + // 判断是否跳过不检查 + if len(common.Config.ExplainWarnAccessType) == 1 { + if common.Config.ExplainWarnAccessType[0] == "" { + return + } + } else if len(common.Config.ExplainWarnAccessType) < 1 { + return + } + + rows := exp.ExplainRows + if exp.ExplainFormat == database.JSONFormatExplain { + // JSON形式遍历分析不方便,转成Row格式统一处理 + rows = database.ConvertExplainJSON2Row(exp.ExplainJSON) + } + for _, v := range common.Config.ExplainWarnAccessType { + for _, row := range rows { + if row.AccessType == v && v != "" { + tablesSuggests[row.TableName] = append(tablesSuggests[row.TableName], fmt.Sprintf("Scalability:%s", row.Scalability)) + } + } + } +} + +// TODO: +/* +func checkExplainPossibleKeys(exp *database.ExplainInfo) { + // 判断是否跳过不检查 + if common.Config.ExplainMinPossibleKeys == 0 { + return + } + + rows := exp.ExplainRows + if exp.ExplainFormat == database.JSONFormatExplain { + // JSON形式遍历分析不方便,转成Row格式统一处理 + rows = database.ConvertExplainJSON2Row(exp.ExplainJSON) + } + for _, row := range rows { + if len(row.PossibleKeys) < common.Config.ExplainMinPossibleKeys { + tablesSuggests[row.TableName] = append(tablesSuggests[row.TableName], fmt.Sprintf("PossibleKeys:%d < %d", + len(row.PossibleKeys), common.Config.ExplainMinPossibleKeys)) + } + } +} +*/ + +// TODO: +/* +func checkExplainKeyLen(exp *database.ExplainInfo) { +} +*/ + +// TODO: +/* +func checkExplainKey(exp *database.ExplainInfo) { + // 小于最小使用试用key数量 + //return intval($explainResult) < intval($userCond); + //explain-min-keys int +} +*/ + +func checkExplainRef(exp *database.ExplainInfo) { + rows := exp.ExplainRows + if exp.ExplainFormat == database.JSONFormatExplain { + // JSON形式遍历分析不方便,转成Row格式统一处理 + rows = database.ConvertExplainJSON2Row(exp.ExplainJSON) + } + for i, row := range rows { + if strings.Join(row.Ref, "") == "NULL" || strings.Join(row.Ref, "") == "" { + if i == 0 && len(rows) > 1 { + continue + } + tablesSuggests[row.TableName] = append(tablesSuggests[row.TableName], fmt.Sprintf("Ref:null")) + } + } +} + +func checkExplainRows(exp *database.ExplainInfo) { + // 判断是否跳过不检查 + if common.Config.ExplainMaxRows <= 0 { + return + } + + rows := exp.ExplainRows + if exp.ExplainFormat == database.JSONFormatExplain { + // JSON形式遍历分析不方便,转成Row格式统一处理 + rows = database.ConvertExplainJSON2Row(exp.ExplainJSON) + } + + for _, row := range rows { + if row.Rows >= common.Config.ExplainMaxRows { + tablesSuggests[row.TableName] = append(tablesSuggests[row.TableName], fmt.Sprintf("Rows:%d", row.Rows)) + } + } +} + +// TODO: +/* +func checkExplainExtra(exp *database.ExplainInfo) { + // 包含用户配置的逗号分隔关键词之一则提醒 + // return self::contains($explainResult, $userCond); + // explain-warn-extra []string +} +*/ + +func checkExplainFiltered(exp *database.ExplainInfo) { + // 判断是否跳过不检查 + if common.Config.ExplainMaxFiltered <= 0.001 { + return + } + + rows := exp.ExplainRows + if exp.ExplainFormat == database.JSONFormatExplain { + // JSON形式遍历分析不方便,转成Row格式统一处理 + rows = database.ConvertExplainJSON2Row(exp.ExplainJSON) + } + for i, row := range rows { + if i == 0 && len(rows) > 1 { + continue + } + if row.Filtered >= common.Config.ExplainMaxFiltered { + tablesSuggests[row.TableName] = append(tablesSuggests[row.TableName], fmt.Sprintf("Filtered:%.2f%s", row.Filtered, "%")) + } + } +} + +// ExplainAdvisor 基于explain信息给出建议 +func ExplainAdvisor(exp *database.ExplainInfo) map[string]Rule { + common.Log.Debug("ExplainAdvisor SQL: %v", exp.SQL) + explainRuleID = 0 + explainRules = make(map[string]Rule) + tablesSuggests = make(map[string][]string) + + checkExplainSelectType(exp) + checkExplainAccessType(exp) + checkExplainFiltered(exp) + checkExplainRef(exp) + checkExplainRows(exp) + + // 打印explain table + content := database.PrintMarkdownExplainTable(exp) + + if common.Config.ShowWarnings { + content += "\n" + database.MySQLExplainWarnings(exp) + } + + // 对explain table中各项难于理解的值做解释 + cases := database.ExplainInfoTranslator(exp) + + // 添加last_query_cost + if common.Config.ShowLastQueryCost { + content += "\n" + database.MySQLExplainQueryCost(exp) + } + + if content != "" { + explainRules["EXP.000"] = Rule{ + Item: "EXP.000", + Severity: "L0", + Summary: "Explain信息", + Content: content, + Case: cases, + Func: (*Query4Audit).RuleOK, + } + } + /* + for t, s := range tablesSuggests { + // 检查explain对应的表是否需要跳过,如dual,空表等 + ig := false + for _, ti := range explainIgnoreTables { + if ti == t { + ig = true + } + } + if ig { + continue + } + ruleId := fmt.Sprintf("EXP.%03d", explainRuleId+1) + explainRuleId = explainRuleId + 1 + explainRules[ruleId] = Rule{ + Item: ruleId, + Severity: "L0", + Summary: fmt.Sprintf("表 `%s` 查询效率不高", t), + Content: fmt.Sprint("原因:", strings.Join(s, ",")), + Case: "", + Func: (*Query4Audit).RuleOK, + } + } + */ + return explainRules +} + +// DigestExplainText 分析用户输入的EXPLAIN信息 +func DigestExplainText(text string) { + // explain信息就不要显示完美了,美不美自己看吧。 + common.Config.IgnoreRules = append(common.Config.IgnoreRules, "OK") + + if !IsIgnoreRule("EXP.") { + explainInfo, err := database.ParseExplainText(text) + if err != nil { + common.Log.Error("main ParseExplainText Error: %v", err) + return + } + expSuggest := ExplainAdvisor(explainInfo) + _, output := FormatSuggest("", common.Config.ReportType, expSuggest) + if common.Config.ReportType == "html" { + fmt.Println(common.MarkdownHTMLHeader()) + fmt.Println(common.Markdown2HTML(output)) + } else { + fmt.Println(output) + } + } +} diff --git a/advisor/explainer_test.go b/advisor/explainer_test.go new file mode 100644 index 00000000..0fc8a86a --- /dev/null +++ b/advisor/explainer_test.go @@ -0,0 +1,37 @@ +/* + * Copyright 2018 Xiaomi, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package advisor + +import ( + "testing" + + "github.com/XiaoMi/soar/common" +) + +func TestDigestExplainText(t *testing.T) { + var text = `+----+-------------+---------+-------+---------------------------------------------------------+-------------------+---------+---------------------------+------+-------------+ +| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | ++----+-------------+---------+-------+---------------------------------------------------------+-------------------+---------+---------------------------+------+-------------+ +| 1 | SIMPLE | country | index | PRIMARY,country_id | country | 152 | NULL | 109 | Using index | +| 1 | SIMPLE | city | ref | idx_fk_country_id,idx_country_id_city,idx_all,idx_other | idx_fk_country_id | 2 | sakila.country.country_id | 2 | Using index | ++----+-------------+---------+-------+---------------------------------------------------------+-------------------+---------+---------------------------+------+-------------+` + common.Config.ReportType = "explain-digest" + err := common.GoldenDiff(func() { DigestExplainText(text) }, t.Name(), update) + if nil != err { + t.Fatal(err) + } +} diff --git a/advisor/heuristic.go b/advisor/heuristic.go new file mode 100644 index 00000000..b95cfe83 --- /dev/null +++ b/advisor/heuristic.go @@ -0,0 +1,3252 @@ +/* + * Copyright 2018 Xiaomi, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package advisor + +import ( + "bytes" + "fmt" + "regexp" + "strconv" + "strings" + + "github.com/XiaoMi/soar/ast" + "github.com/XiaoMi/soar/common" + "github.com/XiaoMi/soar/database" + + "github.com/gedex/inflector" + "github.com/percona/go-mysql/query" + tidb "github.com/pingcap/tidb/ast" + "github.com/pingcap/tidb/mysql" + "github.com/pingcap/tidb/types" + "vitess.io/vitess/go/vt/sqlparser" +) + +// RuleOK OK +func (q *Query4Audit) RuleOK() Rule { + return HeuristicRules["OK"] +} + +// RuleImplicitAlias ALI.001 +func (q *Query4Audit) RuleImplicitAlias() Rule { + var rule = q.RuleOK() + tkns := ast.Tokenizer(q.Query) + if tkns[0].Type != sqlparser.SELECT { + return rule + } + for i, tkn := range tkns { + if tkn.Type == sqlparser.ID && i+1 < len(tkns) && tkn.Type == tkns[i+1].Type { + rule = HeuristicRules["ALI.001"] + break + } + } + return rule +} + +// RuleStarAlias ALI.002 +func (q *Query4Audit) RuleStarAlias() Rule { + var rule = q.RuleOK() + re := regexp.MustCompile(`(?i)(\*\s+as\b)`) + if re.FindString(q.Query) != "" { + rule = HeuristicRules["ALI.002"] + } + return rule +} + +// RuleSameAlias ALI.003 +func (q *Query4Audit) RuleSameAlias() Rule { + var rule = q.RuleOK() + err := sqlparser.Walk(func(node sqlparser.SQLNode) (kontinue bool, err error) { + switch expr := node.(type) { + case *sqlparser.AliasedExpr: + switch n := expr.Expr.(type) { + case *sqlparser.ColName: + if n.Name.String() == expr.As.String() { + rule = HeuristicRules["ALI.003"] + return false, nil + } + } + case *sqlparser.AliasedTableExpr: + switch n := expr.Expr.(type) { + case sqlparser.TableName: + if n.Name.String() == expr.As.String() { + rule = HeuristicRules["ALI.003"] + return false, nil + } + } + } + return true, nil + }, q.Stmt) + common.LogIfError(err, "") + return rule +} + +// RulePrefixLike ARG.001 +func (q *Query4Audit) RulePrefixLike() Rule { + var rule = q.RuleOK() + err := sqlparser.Walk(func(node sqlparser.SQLNode) (kontinue bool, err error) { + switch expr := node.(type) { + case *sqlparser.ComparisonExpr: + if expr.Operator == "like" { + switch sqlval := expr.Right.(type) { + case *sqlparser.SQLVal: + // prefix like with '%', '_' + if sqlval.Type == 0 && (sqlval.Val[0] == 0x25 || sqlval.Val[0] == 0x5f) { + rule = HeuristicRules["ARG.001"] + return false, nil + } + } + } + } + return true, nil + }, q.Stmt) + common.LogIfError(err, "") + return rule +} + +// RuleEqualLike ARG.002 +func (q *Query4Audit) RuleEqualLike() Rule { + var rule = q.RuleOK() + err := sqlparser.Walk(func(node sqlparser.SQLNode) (kontinue bool, err error) { + switch expr := node.(type) { + case *sqlparser.ComparisonExpr: + if expr.Operator == "like" { + switch sqlval := expr.Right.(type) { + case *sqlparser.SQLVal: + // not start with '%', '_' && not end with '%', '_' + if sqlval.Type == 0 { + if sqlval.Val[0] != 0x25 && + sqlval.Val[0] != 0x5f && + sqlval.Val[len(sqlval.Val)-1] != 0x5f && + sqlval.Val[len(sqlval.Val)-1] != 0x25 { + rule = HeuristicRules["ARG.002"] + return false, nil + } + } else { + rule = HeuristicRules["ARG.002"] + return false, nil + } + } + } + } + return true, nil + }, q.Stmt) + common.LogIfError(err, "") + return rule +} + +// RuleImplicitConversion ARG.003 +// 隐式类型转换检查:该项检查一定是在开启测试环境或线上环境情境下下进行的 +func (idxAdv *IndexAdvisor) RuleImplicitConversion() Rule { + /* + * 两个参数至少有一个是 NULL 时,比较的结果也是 NULL,例外是使用 <=> 对两个 NULL 做比较时会返回 1,这两种情况都不需要做类型转换 + * 两个参数都是字符串,会按照字符串来比较,不做类型转换 + * 两个参数都是整数,按照整数来比较,不做类型转换 + * 十六进制的值和非数字做比较时,会被当做二进制串 + * 有一个参数是 TIMESTAMP 或 DATETIME,并且另外一个参数是常量,常量会被转换为 timestamp + * 有一个参数是 decimal 类型,如果另外一个参数是 decimal 或者整数,会将整数转换为 decimal 后进行比较,如果另外一个参数是浮点数,则会把 decimal 转换为浮点数进行比较 + * 所有其他情况下,两个参数都会被转换为浮点数再进行比较 + */ + rule := HeuristicRules["OK"] + // 未开启测试环境不进行检查 + if common.Config.TestDSN.Disable { + return rule + } + + var content string + conditions := ast.FindAllCondition(idxAdv.Ast) + for _, cond := range conditions { + var colList []*common.Column + var values []*sqlparser.SQLVal + + // condition 左右两侧有且只有如下几种可能: + // 1. 左列 & 右列 + // 2. 左列 & 右值(含函数) (或相反) + // 3. 左值(含函数) & 右值(含函数) (无需关注) + switch node := cond.(type) { + case *sqlparser.ComparisonExpr: + // 获取condition左侧的信息 + switch nLeft := node.Left.(type) { + case *sqlparser.SQLVal, *sqlparser.ValTuple: + err := sqlparser.Walk(func(node sqlparser.SQLNode) (kontinue bool, err error) { + switch val := node.(type) { + case *sqlparser.SQLVal: + values = append(values, val) + } + return true, nil + }, nLeft) + common.LogIfError(err, "") + + case *sqlparser.ColName: + left := &common.Column{Name: nLeft.Name.String()} + if !nLeft.Qualifier.Name.IsEmpty() { + left.Table = nLeft.Qualifier.Name.String() + } + colList = append(colList, left) + } + + // 获取condition右侧的信息 + switch nRight := node.Right.(type) { + case *sqlparser.SQLVal, *sqlparser.ValTuple: + err := sqlparser.Walk(func(node sqlparser.SQLNode) (kontinue bool, err error) { + switch val := node.(type) { + case *sqlparser.SQLVal: + values = append(values, val) + } + return true, nil + }, nRight) + common.LogIfError(err, "") + + case *sqlparser.ColName: + right := &common.Column{Name: nRight.Name.String()} + if !nRight.Qualifier.Name.IsEmpty() { + right.Table = nRight.Qualifier.Name.String() + } + colList = append(colList, right) + } + + if len(colList) == 0 { + continue + } + + // 补全列信息 + colList = CompleteColumnsInfo(idxAdv.Ast, colList, idxAdv.vEnv) + + // 列与列比较 + if len(colList) == 2 { + // 列信息补全后如果依然没有表信息,说明在该数据库中不存在该列 + // 如果列信息获取异常,可能会存在无法获取到数据类型的情况,对于这种情况将不会给予建议。 + needBreak := false + for _, col := range colList { + if col.Table == "" { + common.Log.Warning("Column %s not exists", col.Name) + needBreak = true + } + + if col.DataType == "" { + common.Log.Warning("Can't get column %s data type", col.Name) + needBreak = true + } + + } + + if needBreak { + break + } + + // 检查数据类型不一致导致的隐式数据转换 + type1 := common.GetDataTypeBase(colList[0].DataType) + type2 := common.GetDataTypeBase(colList[1].DataType) + common.Log.Debug("DataType: `%s`.`%s` (%s) VS `%s`.`%s` (%s)", + colList[0].Table, colList[0].Name, type1, + colList[1].Table, colList[1].Name, type2) + if strings.ToLower(type1) != strings.ToLower(type2) { + content += fmt.Sprintf("`%s`.`%s` (%s) VS `%s`.`%s` (%s) datatype not match", + colList[0].Table, colList[0].Name, type1, + colList[1].Table, colList[1].Name, type2) + continue + } + + // 检查字符集不一致导致的隐式数据转换 + common.Log.Debug("Charset: `%s`.`%s` (%s) VS `%s`.`%s` (%s)", + colList[0].Table, colList[0].Name, colList[0].Character, + colList[1].Table, colList[1].Name, colList[1].Character) + if colList[0].Character != colList[1].Character { + content += fmt.Sprintf("`%s`.`%s` (%s) VS `%s`.`%s` (%s) charset not match", + colList[0].Table, colList[0].Name, colList[0].Character, + colList[1].Table, colList[1].Name, colList[1].Character) + continue + } + + // 检查排序排序不一致导致的隐式数据转换 + common.Log.Debug("Collation: `%s`.`%s` (%s) VS `%s`.`%s` (%s)", + colList[0].Table, colList[0].Name, colList[0].Collation, + colList[1].Table, colList[1].Name, colList[1].Collation) + if colList[0].Collation != colList[1].Collation { + content += fmt.Sprintf("`%s`.`%s` (%s) VS `%s`.`%s` (%s) collation not match", + colList[0].Table, colList[0].Name, colList[0].Collation, + colList[1].Table, colList[1].Name, colList[1].Collation) + continue + } + } + + typMap := map[sqlparser.ValType][]string{ + // date, time, datetime, timestamp, year + sqlparser.StrVal: { + "char", "varchar", "tinytext", "text", "mediumtext", "longtext", + "date", "time", "datetime", "timestamp", "year", + }, + sqlparser.IntVal: { + "tinyint", "smallint", "mediumint", "int", "integer", "bigint", "timestamp", "year", + }, + sqlparser.FloatVal: { + "float", "double", "real", "decimal", + }, + } + + typNameMap := map[sqlparser.ValType]string{ + sqlparser.StrVal: "string", + sqlparser.IntVal: "int", + sqlparser.FloatVal: "float", + } + + // 列与值比较 + for _, val := range values { + if colList[0].DataType == "" { + common.Log.Debug("Can't get %s datatype", colList[0].Name) + break + } + + isCovered := true + if types, ok := typMap[val.Type]; ok { + for _, t := range types { + if strings.HasPrefix(colList[0].DataType, t) { + isCovered = false + } + } + } + + if isCovered { + if colList[0].Table == "" { + common.Log.Warning("Column %s not exists", colList[0].Name) + continue + } + + c := fmt.Sprintf("%s.%s definition is %s not %s", + colList[0].Table, colList[0].Name, colList[0].DataType, typNameMap[val.Type]) + + common.Log.Debug("Implicit data type conversion: %s", c) + content += c + } + } + + case *sqlparser.RangeCond: + // TODO + case *sqlparser.IsExpr: + // TODO + } + } + if content != "" { + rule = HeuristicRules["ARG.003"] + rule.Content = content + } + return rule +} + +// RuleNoWhere CLA.001 & CLA.014 & CLA.015 +func (q *Query4Audit) RuleNoWhere() Rule { + var rule = q.RuleOK() + err := sqlparser.Walk(func(node sqlparser.SQLNode) (kontinue bool, err error) { + switch n := node.(type) { + case *sqlparser.Select: + if n.Where == nil && sqlparser.String(n.From) != "dual" { + rule = HeuristicRules["CLA.001"] + return false, nil + } + case *sqlparser.Delete: + if n.Where == nil { + rule = HeuristicRules["CLA.014"] + return false, nil + } + case *sqlparser.Update: + if n.Where == nil { + rule = HeuristicRules["CLA.015"] + return false, nil + } + } + return true, nil + }, q.Stmt) + common.LogIfError(err, "") + return rule +} + +// RuleOrderByRand CLA.002 +func (q *Query4Audit) RuleOrderByRand() Rule { + var rule = q.RuleOK() + err := sqlparser.Walk(func(node sqlparser.SQLNode) (kontinue bool, err error) { + switch n := node.(type) { + case sqlparser.OrderBy: + for _, order := range n { + switch expr := order.Expr.(type) { + case *sqlparser.FuncExpr: + if expr.Name.String() == "rand" { + rule = HeuristicRules["CLA.002"] + return false, nil + } + } + } + } + return true, nil + }, q.Stmt) + common.LogIfError(err, "") + return rule +} + +// RuleOffsetLimit CLA.003 +func (q *Query4Audit) RuleOffsetLimit() Rule { + var rule = q.RuleOK() + err := sqlparser.Walk(func(node sqlparser.SQLNode) (kontinue bool, err error) { + switch n := node.(type) { + case *sqlparser.Limit: + if n != nil && n.Offset != nil { + switch v := n.Offset.(type) { + case *sqlparser.SQLVal: + offset, err := strconv.Atoi(string(v.Val)) + // 检查一下Offset阈值,太小了给这个建议也没什么用,阈值写死了没加配置 + if err == nil && offset > 1000 { + rule = HeuristicRules["CLA.003"] + return false, nil + } + } + } + } + return true, nil + }, q.Stmt) + common.LogIfError(err, "") + return rule +} + +// RuleGroupByConst CLA.004 +func (q *Query4Audit) RuleGroupByConst() Rule { + var rule = q.RuleOK() + err := sqlparser.Walk(func(node sqlparser.SQLNode) (kontinue bool, err error) { + switch n := node.(type) { + case sqlparser.GroupBy: + for _, group := range n { + switch group.(type) { + case *sqlparser.SQLVal: + rule = HeuristicRules["CLA.004"] + return false, nil + } + } + } + return true, nil + }, q.Stmt) + common.LogIfError(err, "") + return rule +} + +// RuleGroupByConst GRP.001 +func (idxAdv *IndexAdvisor) RuleGroupByConst() Rule { + rule := HeuristicRules["OK"] + + // 非GroupBy语句 + if len(idxAdv.groupBy) == 0 || len(idxAdv.whereEQ) == 0 { + return rule + } + + for _, groupByCols := range idxAdv.groupBy { + for _, whereEQCols := range idxAdv.whereEQ { + if (groupByCols.Name == whereEQCols.Name) && + (groupByCols.DB == whereEQCols.DB) && + (groupByCols.Table == whereEQCols.Table) { + rule = HeuristicRules["GRP.001"] + break + } + } + } + return rule +} + +// RuleOrderByConst CLA.005 +func (q *Query4Audit) RuleOrderByConst() Rule { + var rule = q.RuleOK() + err := sqlparser.Walk(func(node sqlparser.SQLNode) (kontinue bool, err error) { + switch n := node.(type) { + case sqlparser.OrderBy: + for _, order := range n { + switch order.Expr.(type) { + case *sqlparser.SQLVal: + rule = HeuristicRules["CLA.005"] + return false, nil + } + } + } + return true, nil + }, q.Stmt) + common.LogIfError(err, "") + return rule +} + +// RuleOrderByConst CLA.005 +// TODO: SELECT col FROM tbl WHERE col IN('NEWS') ORDER BY col; +func (idxAdv *IndexAdvisor) RuleOrderByConst() Rule { + rule := HeuristicRules["OK"] + + // 非GroupBy语句 + if len(idxAdv.orderBy) == 0 || len(idxAdv.whereEQ) == 0 { + return rule + } + + for _, groupbyCols := range idxAdv.orderBy { + for _, whereEQCols := range idxAdv.whereEQ { + if (groupbyCols.Name == whereEQCols.Name) && + (groupbyCols.DB == whereEQCols.DB) && + (groupbyCols.Table == whereEQCols.Table) { + rule = HeuristicRules["CLA.005"] + break + } + } + } + return rule +} + +// RuleDiffGroupByOrderBy CLA.006 +func (q *Query4Audit) RuleDiffGroupByOrderBy() Rule { + var rule = q.RuleOK() + var groupbyTbls []sqlparser.TableIdent + var orderbyTbls []sqlparser.TableIdent + err := sqlparser.Walk(func(node sqlparser.SQLNode) (kontinue bool, err error) { + switch n := node.(type) { + case sqlparser.GroupBy: + // 检查group by涉及到表的个数 + for _, group := range n { + switch g := group.(type) { + case *sqlparser.ColName: + tblExist := false + for _, t := range groupbyTbls { + if t.String() == g.Qualifier.Name.String() { + tblExist = true + } + } + if !tblExist { + groupbyTbls = append(groupbyTbls, g.Qualifier.Name) + if len(groupbyTbls) > 1 { + rule = HeuristicRules["CLA.006"] + + return false, nil + } + } + } + } + case sqlparser.OrderBy: + // 检查order by涉及到表的个数 + for _, order := range n { + switch o := order.Expr.(type) { + case *sqlparser.ColName: + tblExist := false + for _, t := range orderbyTbls { + if t.String() == o.Qualifier.Name.String() { + tblExist = true + } + } + if !tblExist { + orderbyTbls = append(orderbyTbls, o.Qualifier.Name) + if len(orderbyTbls) > 1 { + rule = HeuristicRules["CLA.006"] + + return false, nil + } + } + } + } + } + return true, nil + }, q.Stmt) + common.LogIfError(err, "") + + if rule.Item == "OK" { + // 检查group by, order by涉及到表的个数 + for _, g := range groupbyTbls { + tblExist := false + for _, o := range orderbyTbls { + if g.String() == o.String() { + tblExist = true + } + } + if !tblExist && len(orderbyTbls) > 0 { + rule = HeuristicRules["CLA.006"] + + return rule + } + } + } + + return rule +} + +// RuleMixOrderBy CLA.007 +func (q *Query4Audit) RuleMixOrderBy() Rule { + var rule = q.RuleOK() + var direction string + err := sqlparser.Walk(func(node sqlparser.SQLNode) (kontinue bool, err error) { + switch n := node.(type) { + case sqlparser.OrderBy: + for _, order := range n { + // 比较相邻两个order by列的方向 + if direction != "" && order.Direction != direction { + rule = HeuristicRules["CLA.007"] + + return false, nil + } + direction = order.Direction + } + } + return true, nil + }, q.Stmt) + common.LogIfError(err, "") + return rule +} + +// RuleExplicitOrderBy CLA.008 +func (q *Query4Audit) RuleExplicitOrderBy() Rule { + var rule = q.RuleOK() + err := sqlparser.Walk(func(node sqlparser.SQLNode) (kontinue bool, err error) { + switch n := node.(type) { + case *sqlparser.Select: + // 有group by,但没有order by + if n.GroupBy != nil && n.OrderBy == nil { + rule = HeuristicRules["CLA.008"] + + return false, nil + } + } + return true, nil + }, q.Stmt) + common.LogIfError(err, "") + return rule +} + +// RuleOrderByExpr CLA.009 +func (q *Query4Audit) RuleOrderByExpr() Rule { + var rule = q.RuleOK() + var orderByCols []string + var selectCols []string + funcExp := regexp.MustCompile(`[a-z0-9]\(`) + allowExp := regexp.MustCompile("[a-z0-9_,.` ()]") + err := sqlparser.Walk(func(node sqlparser.SQLNode) (kontinue bool, err error) { + switch n := node.(type) { + case sqlparser.OrderBy: + orderBy := sqlparser.String(n) + // 函数名方式,如:from_unixtime(col) + if funcExp.MatchString(orderBy) { + rule = HeuristicRules["CLA.009"] + + return false, nil + } + + // 运算符方式,如:colA - colB + trim := allowExp.ReplaceAllFunc([]byte(orderBy), func(s []byte) []byte { + return []byte("") + }) + if string(trim) != "" { + rule = HeuristicRules["CLA.009"] + + return false, nil + } + + for _, o := range strings.Split(strings.TrimPrefix(orderBy, " order by "), ",") { + orderByCols = append(orderByCols, strings.TrimSpace(strings.Split(o, " ")[0])) + } + case *sqlparser.Select: + for _, s := range n.SelectExprs { + selectCols = append(selectCols, sqlparser.String(s)) + } + } + return true, nil + }, q.Stmt) + common.LogIfError(err, "") + + // AS情况,如:SELECT colA-colB a FROM tbl ORDER BY a; + for _, o := range orderByCols { + if o == "" { + continue + } + for _, s := range selectCols { + if strings.HasSuffix(s, " as "+o) { + buf := strings.TrimSuffix(s, " as "+o) + // 运算符 + trim := allowExp.ReplaceAllFunc([]byte(buf), func(s []byte) []byte { + return []byte("") + }) + if string(trim) != "" { + rule = HeuristicRules["CLA.009"] + + } + // 函数 + if funcExp.MatchString(s) { + rule = HeuristicRules["CLA.009"] + + } + } + } + } + return rule +} + +// RuleGroupByExpr CLA.010 +func (q *Query4Audit) RuleGroupByExpr() Rule { + var rule = q.RuleOK() + var groupByCols []string + var selectCols []string + funcExp := regexp.MustCompile(`(?i)[a-z0-9]\(`) + allowExp := regexp.MustCompile("(?i)[a-z0-9_,.` ()]") + err := sqlparser.Walk(func(node sqlparser.SQLNode) (kontinue bool, err error) { + switch n := node.(type) { + case sqlparser.GroupBy: + groupBy := sqlparser.String(n) + // 函数名方式,如:from_unixtime(col) + if funcExp.MatchString(groupBy) { + rule = HeuristicRules["CLA.010"] + + return false, nil + } + + // 运算符方式,如:colA - colB + trim := allowExp.ReplaceAllFunc([]byte(groupBy), func(s []byte) []byte { + return []byte("") + }) + if string(trim) != "" { + rule = HeuristicRules["CLA.010"] + + return false, nil + } + + for _, o := range strings.Split(strings.TrimPrefix(groupBy, " group by "), ",") { + groupByCols = append(groupByCols, strings.TrimSpace(strings.Split(o, " ")[0])) + } + case *sqlparser.Select: + for _, s := range n.SelectExprs { + selectCols = append(selectCols, sqlparser.String(s)) + } + } + return true, nil + }, q.Stmt) + common.LogIfError(err, "") + + // AS情况,如:SELECT colA-colB a FROM tbl GROUP BY a; + for _, g := range groupByCols { + if g == "" { + continue + } + for _, s := range selectCols { + if strings.HasSuffix(s, " as "+g) { + buf := strings.TrimSuffix(s, " as "+g) + // 运算符 + trim := allowExp.ReplaceAllFunc([]byte(buf), func(s []byte) []byte { + return []byte("") + }) + if string(trim) != "" { + rule = HeuristicRules["CLA.010"] + + } + // 函数 + if funcExp.MatchString(s) { + rule = HeuristicRules["CLA.010"] + + } + } + } + } + return rule +} + +// RuleTblCommentCheck CLA.011 +func (q *Query4Audit) RuleTblCommentCheck() Rule { + var rule = q.RuleOK() + switch node := q.Stmt.(type) { + case *sqlparser.DDL: + if node.Action != "create" { + return rule + } + if node.TableSpec == nil { + return rule + } + if options := node.TableSpec.Options; options == "" { + rule = HeuristicRules["CLA.011"] + + } else { + reg := regexp.MustCompile("(?i)comment") + if !reg.MatchString(options) { + rule = HeuristicRules["CLA.011"] + } + } + } + return rule +} + +// RuleSelectStar COL.001 +func (q *Query4Audit) RuleSelectStar() Rule { + var rule = q.RuleOK() + // 先把count(*)替换为count(1) + re := regexp.MustCompile(`(?i)count\s*\(\s*\*\s*\)`) + sql := re.ReplaceAllString(q.Query, "count(1)") + stmt, err := sqlparser.Parse(sql) + if err != nil { + common.Log.Debug("RuleSelectStar sqlparser.Parse Error: %v", err) + return rule + } + err = sqlparser.Walk(func(node sqlparser.SQLNode) (kontinue bool, err error) { + switch node.(type) { + case *sqlparser.StarExpr: + rule = HeuristicRules["COL.001"] + return false, nil + } + return true, nil + }, stmt) + common.LogIfError(err, "") + return rule +} + +// RuleInsertColDef COL.002 +func (q *Query4Audit) RuleInsertColDef() Rule { + var rule = q.RuleOK() + switch node := q.Stmt.(type) { + case *sqlparser.Insert: + if node.Columns == nil { + rule = HeuristicRules["COL.002"] + return rule + } + } + return rule +} + +// RuleAddDefaultValue COL.004 +func (q *Query4Audit) RuleAddDefaultValue() Rule { + var rule = q.RuleOK() + for _, node := range q.TiStmt { + switch n := node.(type) { + case *tidb.CreateTableStmt: + for _, c := range n.Cols { + colDefault := false + for _, o := range c.Options { + // 忽略AutoIncrement类型的默认值检查 + if o.Tp == tidb.ColumnOptionDefaultValue || o.Tp == tidb.ColumnOptionAutoIncrement { + colDefault = true + } + } + if !colDefault { + rule = HeuristicRules["COL.004"] + break + } + } + case *tidb.AlterTableStmt: + for _, s := range n.Specs { + switch s.Tp { + case tidb.AlterTableAddColumns, tidb.AlterTableChangeColumn, tidb.AlterTableModifyColumn: + for _, c := range s.NewColumns { + colDefault := false + for _, o := range c.Options { + // 忽略AutoIncrement类型的默认值检查 + if o.Tp == tidb.ColumnOptionDefaultValue || o.Tp == tidb.ColumnOptionAutoIncrement { + colDefault = true + } + } + if !colDefault { + rule = HeuristicRules["COL.004"] + break + } + } + } + } + } + } + return rule +} + +// RuleColCommentCheck COL.005 +func (q *Query4Audit) RuleColCommentCheck() Rule { + var rule = q.RuleOK() + for _, node := range q.TiStmt { + switch n := node.(type) { + case *tidb.CreateTableStmt: + for _, c := range n.Cols { + colComment := false + for _, o := range c.Options { + if o.Tp == tidb.ColumnOptionComment { + colComment = true + } + } + if !colComment { + rule = HeuristicRules["COL.005"] + break + } + } + case *tidb.AlterTableStmt: + for _, s := range n.Specs { + switch s.Tp { + case tidb.AlterTableAddColumns, tidb.AlterTableChangeColumn, tidb.AlterTableModifyColumn: + for _, c := range s.NewColumns { + colComment := false + for _, o := range c.Options { + if o.Tp == tidb.ColumnOptionComment { + colComment = true + } + } + if !colComment { + rule = HeuristicRules["COL.005"] + break + } + } + } + } + } + } + return rule +} + +// RuleIPString LIT.001 +func (q *Query4Audit) RuleIPString() Rule { + var rule = q.RuleOK() + re := regexp.MustCompile(`['"]\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}`) + if re.FindString(q.Query) != "" { + rule = HeuristicRules["LIT.001"] + if position := re.FindIndex([]byte(q.Query)); len(position) > 0 { + rule.Position = position[0] + } + } + return rule +} + +// RuleDataNotQuote LIT.002 +func (q *Query4Audit) RuleDataNotQuote() Rule { + var rule = q.RuleOK() + // 2010-01-01 + re := regexp.MustCompile(`.\d{4}\s*-\s*\d{1,2}\s*-\s*\d{1,2}\b`) + sqls := re.FindAllString(q.Query, -1) + for _, sql := range sqls { + re = regexp.MustCompile(`^['"\w-].*`) + if re.FindString(sql) == "" { + rule = HeuristicRules["LIT.002"] + } + } + + // 10-01-01 + re = regexp.MustCompile(`.\d{2}\s*-\s*\d{1,2}\s*-\s*\d{1,2}\b`) + sqls = re.FindAllString(q.Query, -1) + for _, sql := range sqls { + re = regexp.MustCompile(`^['"\w-].*`) + if re.FindString(sql) == "" { + rule = HeuristicRules["LIT.002"] + } + } + + if position := re.FindIndex([]byte(q.Query)); len(position) > 0 { + rule.Position = position[0] + } + return rule +} + +// RuleSQLCalcFoundRows KWR.001 +func (q *Query4Audit) RuleSQLCalcFoundRows() Rule { + var rule = q.RuleOK() + tkns := ast.Tokenizer(q.Query) + for _, tkn := range tkns { + if tkn.Val == "sql_calc_found_rows" { + rule = HeuristicRules["KWR.001"] + break + } + } + return rule +} + +// RuleCommaAnsiJoin JOI.001 +func (q *Query4Audit) RuleCommaAnsiJoin() Rule { + var rule = q.RuleOK() + err := sqlparser.Walk(func(node sqlparser.SQLNode) (kontinue bool, err error) { + switch n := node.(type) { + case *sqlparser.Select: + ansiJoin := false + commaJoin := false + for _, f := range n.From { + switch f.(type) { + case *sqlparser.JoinTableExpr: + ansiJoin = true + case *sqlparser.AliasedTableExpr: + commaJoin = true + } + } + if ansiJoin && commaJoin { + rule = HeuristicRules["JOI.001"] + return false, nil + } + } + return true, nil + }, q.Stmt) + common.LogIfError(err, "") + return rule +} + +// RuleDupJoin JOI.002 +func (q *Query4Audit) RuleDupJoin() Rule { + var rule = q.RuleOK() + var tables []string + switch q.Stmt.(type) { + // TODO: 这里未检查UNION SELECT + case *sqlparser.Union: + return rule + default: + err := sqlparser.Walk(func(node sqlparser.SQLNode) (kontinue bool, err error) { + switch n := node.(type) { + case *sqlparser.AliasedTableExpr: + switch table := n.Expr.(type) { + case sqlparser.TableName: + for _, t := range tables { + if t == table.Name.String() { + rule = HeuristicRules["JOI.002"] + return false, nil + } + } + tables = append(tables, table.Name.String()) + } + } + return true, nil + }, q.Stmt) + common.LogIfError(err, "") + } + return rule +} + +// RuleImpossibleOuterJoin JOI.003 +// TODO: 未实现完 +func (idxAdv *IndexAdvisor) RuleImpossibleOuterJoin() Rule { + rule := HeuristicRules["OK"] + + var joinTables []string // JOIN相关表名 + var whereEQTables []string // WHERE等值判断条件表名 + var joinNotWhereTables []string // 是JOIN相关表,但未出现在WHERE等值判断条件中的表名 + + // 非JOIN语句 + if len(idxAdv.joinCond) == 0 || len(idxAdv.whereEQ) == 0 { + return rule + } + + for _, l1 := range idxAdv.joinCond { + for _, l2 := range l1 { + if l2.Table != "" && l2.Table != "dual" { + joinTables = append(joinTables, l2.Table) + } + } + } + + for _, w := range idxAdv.whereEQ { + whereEQTables = append(whereEQTables, w.Table) + } + + for _, j := range joinTables { + found := false + for _, w := range whereEQTables { + if j == w { + found = true + } + } + if !found { + joinNotWhereTables = append(joinNotWhereTables, j) + } + } + + // TODO: + fmt.Println(joinNotWhereTables) + /* + if len(joinNotWhereTables) == 0 { + rule = HeuristicRules["JOI.003"] + } + */ + rule = HeuristicRules["JOI.003"] + return rule +} + +// TODO: JOI.004 + +// RuleNoDeterministicGroupby RES.001 +func (q *Query4Audit) RuleNoDeterministicGroupby() Rule { + var rule = q.RuleOK() + var groupbyCols []*common.Column + var selectCols []*common.Column + err := sqlparser.Walk(func(node sqlparser.SQLNode) (kontinue bool, err error) { + switch n := node.(type) { + case *sqlparser.Select: + // 过滤select列 + selectCols = ast.FindColumn(n.SelectExprs) + // 过滤group by列 + groupbyCols = ast.FindColumn(n.GroupBy) + // `select *`, but not `select count(*)` + if strings.Contains(sqlparser.String(n), " * ") && len(groupbyCols) > 0 { + rule = HeuristicRules["RES.001"] + return false, nil + } + } + return true, nil + }, q.Stmt) + common.LogIfError(err, "") + + // TODO:暂时只检查了列名,未对库表名进行检查,也未处理AS + for _, s := range selectCols { + // 无group by退出 + if len(groupbyCols) == 0 { + break + } + found := false + for _, g := range groupbyCols { + if g.Name == s.Name { + found = true + } + } + if !found { + rule = HeuristicRules["RES.001"] + break + } + } + return rule +} + +// RuleNoDeterministicLimit RES.002 +func (q *Query4Audit) RuleNoDeterministicLimit() Rule { + var rule = q.RuleOK() + err := sqlparser.Walk(func(node sqlparser.SQLNode) (kontinue bool, err error) { + switch n := node.(type) { + case *sqlparser.Select: + if n.Limit != nil && n.OrderBy == nil { + rule = HeuristicRules["RES.002"] + return false, nil + } + } + return true, nil + }, q.Stmt) + common.LogIfError(err, "") + return rule +} + +// RuleUpdateDeleteWithLimit RES.003 +func (q *Query4Audit) RuleUpdateDeleteWithLimit() Rule { + var rule = q.RuleOK() + switch s := q.Stmt.(type) { + case *sqlparser.Update: + if s.Limit != nil { + rule = HeuristicRules["RES.003"] + } + } + return rule +} + +// RuleUpdateDeleteWithOrderby RES.004 +func (q *Query4Audit) RuleUpdateDeleteWithOrderby() Rule { + var rule = q.RuleOK() + switch s := q.Stmt.(type) { + case *sqlparser.Update: + if s.OrderBy != nil { + rule = HeuristicRules["RES.004"] + } + } + return rule +} + +// RuleUpdateSetAnd RES.005 +func (q *Query4Audit) RuleUpdateSetAnd() Rule { + var rule = q.RuleOK() + switch s := q.Stmt.(type) { + case *sqlparser.Update: + if strings.Contains(sqlparser.String(s.Exprs), " and ") { + rule = HeuristicRules["RES.005"] + } + } + return rule +} + +// RuleImpossibleWhere RES.006 +func (q *Query4Audit) RuleImpossibleWhere() Rule { + var rule = q.RuleOK() + // BETWEEN 10 AND 5 + err := sqlparser.Walk(func(node sqlparser.SQLNode) (kontinue bool, err error) { + switch n := node.(type) { + case *sqlparser.RangeCond: + if n.Operator == "between" { + from := 0 + to := 0 + switch s := n.From.(type) { + case *sqlparser.SQLVal: + from, _ = strconv.Atoi(string(s.Val)) + } + switch s := n.To.(type) { + case *sqlparser.SQLVal: + to, _ = strconv.Atoi(string(s.Val)) + } + if from > to { + rule = HeuristicRules["RES.006"] + return false, nil + } + } + case *sqlparser.ComparisonExpr: + factor := false + switch n.Operator { + case "!=", "<>": + case "=", "<=>": + factor = true + default: + return true, nil + } + + var left []byte + var right []byte + + // left + switch l := n.Left.(type) { + case *sqlparser.SQLVal: + left = l.Val + default: + return true, nil + } + + // right + switch r := n.Right.(type) { + case *sqlparser.SQLVal: + right = r.Val + default: + return true, nil + } + + // compare + if (!bytes.Equal(left, right) && factor) || (bytes.Equal(left, right) && !factor) { + rule = HeuristicRules["RES.006"] + } + return false, nil + } + + return true, nil + }, q.Stmt) + common.LogIfError(err, "") + return rule +} + +// RuleMeaninglessWhere RES.007 +func (q *Query4Audit) RuleMeaninglessWhere() Rule { + var rule = q.RuleOK() + // 1=1, 0=0 + err := sqlparser.Walk(func(node sqlparser.SQLNode) (kontinue bool, err error) { + switch n := node.(type) { + case *sqlparser.ComparisonExpr: + factor := false + switch n.Operator { + case "!=", "<>": + factor = true + case "=", "<=>": + default: + return true, nil + } + + var left []byte + var right []byte + + // left + switch l := n.Left.(type) { + case *sqlparser.SQLVal: + left = l.Val + default: + return true, nil + } + + // right + switch r := n.Right.(type) { + case *sqlparser.SQLVal: + right = r.Val + default: + return true, nil + } + + // compare + if (bytes.Equal(left, right) && !factor) || (!bytes.Equal(left, right) && factor) { + rule = HeuristicRules["RES.007"] + } + return false, nil + } + + return true, nil + }, q.Stmt) + common.LogIfError(err, "") + return rule +} + +// RuleLoadFile RES.008 +func (q *Query4Audit) RuleLoadFile() Rule { + var rule = q.RuleOK() + // 去除注释 + sql := string(database.RemoveSQLComments([]byte(q.Query))) + // 去除多余的空格和回车 + sql = strings.Join(strings.Fields(sql), " ") + tks := ast.Tokenize(sql) + for i, tk := range tks { + // 注意:每个关键字token的结尾是带空格的,这里偷懒没trimspace直接加空格比较 + // LOAD DATA... + if strings.ToLower(tk.Val) == "load " && i+1 < len(tks) && + strings.ToLower(tks[i+1].Val) == "data " { + rule = HeuristicRules["RES.008"] + break + } + + // SELECT ... INTO OUTFILE + if strings.ToLower(tk.Val) == "into " && i+1 < len(tks) && + (strings.ToLower(tks[i+1].Val) == "outfile " || strings.ToLower(tks[i+1].Val) == "dumpfile ") { + rule = HeuristicRules["RES.008"] + break + } + } + return rule +} + +// RuleStandardINEQ STA.001 +func (q *Query4Audit) RuleStandardINEQ() Rule { + var rule = q.RuleOK() + re := regexp.MustCompile(`(!=)`) + if re.FindString(q.Query) != "" { + rule = HeuristicRules["STA.001"] + if position := re.FindIndex([]byte(q.Query)); len(position) > 0 { + rule.Position = position[0] + } + } + return rule +} + +// RuleUseKeyWord KWR.002 +func (q *Query4Audit) RuleUseKeyWord() Rule { + var rule = q.RuleOK() + switch q.Stmt.(type) { + case *sqlparser.DDL: + if q.TiStmt == nil { + common.Log.Error("TiStmt is nil, SQL: %s", q.Query) + return rule + } + + for _, tiStmtNode := range q.TiStmt { + switch stmt := tiStmtNode.(type) { + case *tidb.AlterTableStmt: + // alter + for _, spec := range stmt.Specs { + for _, column := range spec.NewColumns { + if ast.IsMysqlKeyword(column.Name.String()) { + return HeuristicRules["KWR.002"] + } + } + } + + case *tidb.CreateTableStmt: + // create + if ast.IsMysqlKeyword(stmt.Table.Name.String()) { + return HeuristicRules["KWR.002"] + } + + for _, col := range stmt.Cols { + if ast.IsMysqlKeyword(col.Name.String()) { + return HeuristicRules["KWR.002"] + } + } + } + + } + } + + return rule +} + +// RulePluralWord KWR.003 +// Reference: https://en.wikipedia.org/wiki/English_plurals +func (q *Query4Audit) RulePluralWord() Rule { + var rule = q.RuleOK() + switch q.Stmt.(type) { + case *sqlparser.DDL: + if q.TiStmt == nil { + common.Log.Error("TiStmt is nil, SQL: %s", q.Query) + return rule + } + + for _, tiStmtNode := range q.TiStmt { + switch stmt := tiStmtNode.(type) { + case *tidb.AlterTableStmt: + // alter + for _, spec := range stmt.Specs { + for _, column := range spec.NewColumns { + if inflector.Singularize(column.Name.String()) != column.Name.String() { + return HeuristicRules["KWR.003"] + } + } + } + + case *tidb.CreateTableStmt: + // create + if inflector.Singularize(stmt.Table.Name.String()) != stmt.Table.Name.String() { + return HeuristicRules["KWR.003"] + } + + for _, col := range stmt.Cols { + if inflector.Singularize(col.Name.String()) != col.Name.String() { + return HeuristicRules["KWR.003"] + } + } + } + + } + + } + return rule +} + +// RuleInsertSelect LCK.001 +func (q *Query4Audit) RuleInsertSelect() Rule { + var rule = q.RuleOK() + switch n := q.Stmt.(type) { + case *sqlparser.Insert: + switch n.Rows.(type) { + case *sqlparser.Select: + rule = HeuristicRules["LCK.001"] + } + } + return rule +} + +// RuleInsertOnDup LCK.002 +func (q *Query4Audit) RuleInsertOnDup() Rule { + var rule = q.RuleOK() + switch n := q.Stmt.(type) { + case *sqlparser.Insert: + if n.OnDup != nil { + rule = HeuristicRules["LCK.002"] + return rule + } + } + return rule +} + +// RuleInSubquery SUB.001 +func (q *Query4Audit) RuleInSubquery() Rule { + var rule = q.RuleOK() + err := sqlparser.Walk(func(node sqlparser.SQLNode) (kontinue bool, err error) { + switch node.(type) { + case *sqlparser.Subquery: + rule = HeuristicRules["SUB.001"] + return false, nil + } + return true, nil + }, q.Stmt) + common.LogIfError(err, "") + return rule +} + +// RuleSubqueryDepth SUB.004 +func (q *Query4Audit) RuleSubqueryDepth() Rule { + var rule = q.RuleOK() + if depth := ast.GetSubqueryDepth(q.Stmt); depth > common.Config.MaxSubqueryDepth { + rule = HeuristicRules["SUB.004"] + } + return rule +} + +// RuleSubQueryLimit SUB.005 +// 只有IN的SUBQUERY限制了LIMIT,FROM子句中的SUBQUERY并未限制LIMIT +func (q *Query4Audit) RuleSubQueryLimit() Rule { + var rule = q.RuleOK() + err := sqlparser.Walk(func(node sqlparser.SQLNode) (kontinue bool, err error) { + switch n := node.(type) { + case *sqlparser.ComparisonExpr: + if n.Operator == "in" { + switch r := n.Right.(type) { + case *sqlparser.Subquery: + switch s := r.Select.(type) { + case *sqlparser.Select: + if s.Limit != nil { + rule = HeuristicRules["SUB.005"] + return false, nil + } + } + } + } + } + return true, nil + }, q.Stmt) + common.LogIfError(err, "") + return rule +} + +// RuleSubQueryFunctions SUB.006 +func (q *Query4Audit) RuleSubQueryFunctions() Rule { + var rule = q.RuleOK() + err := sqlparser.Walk(func(node sqlparser.SQLNode) (kontinue bool, err error) { + switch node.(type) { + case *sqlparser.Subquery: + err = sqlparser.Walk(func(node sqlparser.SQLNode) (kontinue bool, err error) { + switch node.(type) { + case *sqlparser.FuncExpr: + rule = HeuristicRules["SUB.006"] + return false, nil + } + return true, nil + }, node) + common.LogIfError(err, "") + } + + if rule.Item == "OK" { + return true, nil + } + return false, nil + }, q.Stmt) + common.LogIfError(err, "") + return rule +} + +// RuleMultiValueAttribute LIT.003 +func (q *Query4Audit) RuleMultiValueAttribute() Rule { + var rule = q.RuleOK() + re := regexp.MustCompile(`(?i)(id\s+varchar)|(id\s+text)|(id\s+regexp)`) + if re.FindString(q.Query) != "" { + rule = HeuristicRules["LIT.003"] + if position := re.FindIndex([]byte(q.Query)); len(position) > 0 { + rule.Position = position[0] + } + } + return rule +} + +// RuleAddDelimiter LIT.004 +func (q *Query4Audit) RuleAddDelimiter() Rule { + var rule = q.RuleOK() + re := regexp.MustCompile(`(?i)(^use\s+[0-9a-z_-]*)|(^show\s+databases)`) + if re.FindString(q.Query) != "" && !strings.HasSuffix(q.Query, common.Config.Delimiter) { + rule = HeuristicRules["LIT.004"] + if position := re.FindIndex([]byte(q.Query)); len(position) > 0 { + rule.Position = position[0] + } + } + return rule +} + +// RuleRecursiveDependency KEY.003 +func (q *Query4Audit) RuleRecursiveDependency() Rule { + var rule = q.RuleOK() + switch q.Stmt.(type) { + case *sqlparser.DDL: + for _, tiStmt := range q.TiStmt { + switch node := tiStmt.(type) { + case *tidb.CreateTableStmt: + // create statement + for _, ref := range node.Constraints { + if ref != nil && ref.Tp == tidb.ConstraintForeignKey { + rule = HeuristicRules["KEY.003"] + } + } + + case *tidb.AlterTableStmt: + // alter table statement + for _, spec := range node.Specs { + if spec.Constraint != nil && spec.Constraint.Tp == tidb.ConstraintForeignKey { + rule = HeuristicRules["KEY.003"] + } + } + } + } + } + + if rule.Item == "KEY.003" { + re := regexp.MustCompile(`(?i)(\s+references\s+)`) + if position := re.FindIndex([]byte(q.Query)); len(position) > 0 { + rule.Position = position[0] + } + } + + return rule +} + +// RuleImpreciseDataType COL.009 +func (q *Query4Audit) RuleImpreciseDataType() Rule { + var rule = q.RuleOK() + if q.TiStmt != nil { + for _, tiStmt := range q.TiStmt { + switch node := tiStmt.(type) { + case *tidb.CreateTableStmt: + // Create table statement + for _, col := range node.Cols { + switch col.Tp.Tp { + case mysql.TypeFloat, mysql.TypeDouble, mysql.TypeDecimal, mysql.TypeNewDecimal: + rule = HeuristicRules["COL.009"] + } + } + + case *tidb.AlterTableStmt: + // Alter table statement + for _, spec := range node.Specs { + switch spec.Tp { + case tidb.AlterTableAddColumns, tidb.AlterTableChangeColumn, tidb.AlterTableModifyColumn: + for _, col := range spec.NewColumns { + switch col.Tp.Tp { + case mysql.TypeFloat, mysql.TypeDouble, + mysql.TypeDecimal, mysql.TypeNewDecimal: + rule = HeuristicRules["COL.009"] + } + } + } + } + + case *tidb.InsertStmt: + // Insert statement + for _, values := range node.Lists { + for _, value := range values { + switch value.GetDatum().Kind() { + case types.KindFloat32, types.KindFloat64, types.KindMysqlDecimal: + rule = HeuristicRules["COL.009"] + } + } + } + + case *tidb.SelectStmt: + // Select statement + switch where := node.Where.(type) { + case *tidb.BinaryOperationExpr: + switch where.R.GetDatum().Kind() { + case types.KindFloat32, types.KindFloat64, types.KindMysqlDecimal: + rule = HeuristicRules["COL.009"] + } + } + } + } + } + + return rule +} + +// RuleValuesInDefinition COL.010 +func (q *Query4Audit) RuleValuesInDefinition() Rule { + var rule = q.RuleOK() + switch q.Stmt.(type) { + case *sqlparser.DDL: + for _, tiStmt := range q.TiStmt { + switch node := tiStmt.(type) { + case *tidb.CreateTableStmt: + for _, col := range node.Cols { + switch col.Tp.Tp { + case mysql.TypeSet, mysql.TypeEnum, mysql.TypeBit: + rule = HeuristicRules["COL.010"] + } + } + case *tidb.AlterTableStmt: + for _, spec := range node.Specs { + switch spec.Tp { + case tidb.AlterTableAddColumns, tidb.AlterTableChangeColumn, tidb.AlterTableModifyColumn: + for _, col := range spec.NewColumns { + switch col.Tp.Tp { + case mysql.TypeSet, mysql.TypeEnum, mysql.TypeBit: + rule = HeuristicRules["COL.010"] + } + } + } + } + } + } + } + return rule +} + +// RuleIndexAttributeOrder KEY.004 +func (q *Query4Audit) RuleIndexAttributeOrder() Rule { + var rule = q.RuleOK() + switch q.Stmt.(type) { + case *sqlparser.DDL: + for _, tiStmt := range q.TiStmt { + switch node := tiStmt.(type) { + case *tidb.CreateIndexStmt: + if len(node.IndexColNames) > 1 { + rule = HeuristicRules["KEY.004"] + break + } + case *tidb.CreateTableStmt: + for _, constraint := range node.Constraints { + // 当一条索引中包含多个列的时候给予建议 + if len(constraint.Keys) > 1 { + rule = HeuristicRules["KEY.004"] + break + } + } + case *tidb.AlterTableStmt: + for _, spec := range node.Specs { + if spec.Tp == tidb.AlterTableAddConstraint && len(spec.Constraint.Keys) > 1 { + rule = HeuristicRules["KEY.004"] + break + } + } + } + } + } + return rule +} + +// RuleNullUsage COL.011 +func (q *Query4Audit) RuleNullUsage() Rule { + var rule = q.RuleOK() + re := regexp.MustCompile(`(?i)(\s+null\s+)`) + if re.FindString(q.Query) != "" { + rule = HeuristicRules["COL.011"] + if position := re.FindIndex([]byte(q.Query)); len(position) > 0 { + rule.Position = position[0] + } + } + return rule +} + +// RuleStringConcatenation FUN.003 +func (q *Query4Audit) RuleStringConcatenation() Rule { + var rule = q.RuleOK() + re := regexp.MustCompile(`(?i)(\|\|)`) + if re.FindString(q.Query) != "" { + rule = HeuristicRules["FUN.003"] + if position := re.FindIndex([]byte(q.Query)); len(position) > 0 { + rule.Position = position[0] + } + } + return rule +} + +// RuleSysdate FUN.004 +func (q *Query4Audit) RuleSysdate() Rule { + var rule = q.RuleOK() + err := sqlparser.Walk(func(node sqlparser.SQLNode) (kontinue bool, err error) { + switch n := node.(type) { + case *sqlparser.FuncExpr: + if n.Name.String() == "sysdate" { + rule = HeuristicRules["FUN.004"] + return false, nil + } + } + return true, nil + }, q.Stmt) + common.LogIfError(err, "") + return rule +} + +// RuleCountConst FUN.005 +func (q *Query4Audit) RuleCountConst() Rule { + var rule = q.RuleOK() + fingerprint := query.Fingerprint(q.Query) + countReg := regexp.MustCompile(`(?i)count\(\s*[0-9a-z?]*\s*\)`) + if countReg.MatchString(fingerprint) { + rule = HeuristicRules["FUN.005"] + if position := countReg.FindIndex([]byte(q.Query)); len(position) > 0 { + rule.Position = position[0] + } + } + return rule +} + +// RuleSumNPE FUN.006 +func (q *Query4Audit) RuleSumNPE() Rule { + var rule = q.RuleOK() + fingerprint := query.Fingerprint(q.Query) + sumReg := regexp.MustCompile(`(?i)sum\(\s*[0-9a-z?]*\s*\)`) + isnullReg := regexp.MustCompile(`(?i)isnull\(sum\(\s*[0-9a-z?]*\s*\)\)`) + if sumReg.MatchString(fingerprint) && !isnullReg.MatchString(fingerprint) { + rule = HeuristicRules["FUN.006"] + if position := isnullReg.FindIndex([]byte(q.Query)); len(position) > 0 { + rule.Position = position[0] + } + } + return rule +} + +// RulePatternMatchingUsage ARG.007 +func (q *Query4Audit) RulePatternMatchingUsage() Rule { + var rule = q.RuleOK() + switch q.Stmt.(type) { + case *sqlparser.Select: + re := regexp.MustCompile(`(?i)(\bregexp\b)|(\bsimilar to\b)`) + if re.FindString(q.Query) != "" { + rule = HeuristicRules["ARG.007"] + } + } + return rule +} + +// RuleSpaghettiQueryAlert CLA.012 +func (q *Query4Audit) RuleSpaghettiQueryAlert() Rule { + var rule = q.RuleOK() + if len(query.Fingerprint(q.Query)) > common.Config.SpaghettiQueryLength { + rule = HeuristicRules["CLA.012"] + } + return rule +} + +// RuleReduceNumberOfJoin JOI.005 +func (q *Query4Audit) RuleReduceNumberOfJoin() Rule { + var rule = q.RuleOK() + var tables []string + switch q.Stmt.(type) { + // TODO: UNION有可能有多张表,这里未检查UNION SELECT + case *sqlparser.Union: + return rule + default: + err := sqlparser.Walk(func(node sqlparser.SQLNode) (kontinue bool, err error) { + switch n := node.(type) { + case *sqlparser.AliasedTableExpr: + switch table := n.Expr.(type) { + case sqlparser.TableName: + exist := false + for _, t := range tables { + if t == table.Name.String() { + exist = true + break + } + } + if !exist { + tables = append(tables, table.Name.String()) + } + } + } + return true, nil + }, q.Stmt) + common.LogIfError(err, "") + } + if len(tables) > common.Config.MaxJoinTableCount { + rule = HeuristicRules["JOI.005"] + } + return rule +} + +// RuleDistinctUsage DIS.001 +func (q *Query4Audit) RuleDistinctUsage() Rule { + // Distinct + var rule = q.RuleOK() + switch q.Stmt.(type) { + case *sqlparser.Select: + re := regexp.MustCompile(`(?i)(\bdistinct\b)`) + if len(re.FindAllString(q.Query, -1)) > common.Config.MaxDistinctCount { + rule = HeuristicRules["DIS.001"] + } + } + return rule +} + +// RuleCountDistinctMultiCol DIS.002 +func (q *Query4Audit) RuleCountDistinctMultiCol() Rule { + var rule = q.RuleOK() + err := sqlparser.Walk(func(node sqlparser.SQLNode) (kontinue bool, err error) { + switch n := node.(type) { + case *sqlparser.FuncExpr: + str := strings.ToLower(sqlparser.String(n)) + if strings.HasPrefix(str, "count") && strings.Contains(str, ",") { + rule = HeuristicRules["DIS.002"] + return false, nil + } + } + return true, nil + }, q.Stmt) + common.LogIfError(err, "") + return rule +} + +// RuleDistinctStar DIS.003 +func (q *Query4Audit) RuleDistinctStar() Rule { + var rule = q.RuleOK() + switch q.Stmt.(type) { + case *sqlparser.Select: + meta := ast.GetMeta(q.Stmt, nil) + for _, m := range meta { + if len(m.Table) == 1 { + // distinct tbl.* from tbl和 distinct * + re := regexp.MustCompile(`(?i)((\s+distinct\s*\*)|(\s+distinct\s+[0-9a-z_` + "`" + `]*\.\*))`) + if re.MatchString(q.Query) { + rule = HeuristicRules["DIS.003"] + } + } + break + } + } + return rule +} + +// RuleHavingClause CLA.013 +func (q *Query4Audit) RuleHavingClause() Rule { + var rule = q.RuleOK() + err := sqlparser.Walk(func(node sqlparser.SQLNode) (kontinue bool, err error) { + switch expr := node.(type) { + case *sqlparser.Select: + if expr.Having != nil { + rule = HeuristicRules["CLA.013"] + return false, nil + } + } + return true, nil + }, q.Stmt) + common.LogIfError(err, "") + return rule +} + +// RuleUpdatePrimaryKey CLA.016 +func (idxAdv *IndexAdvisor) RuleUpdatePrimaryKey() Rule { + rule := HeuristicRules["OK"] + switch node := idxAdv.Ast.(type) { + case *sqlparser.Update: + var setColumns []*common.Column + + err := sqlparser.Walk(func(node sqlparser.SQLNode) (kontinue bool, err error) { + switch node.(type) { + case *sqlparser.UpdateExpr: + // 获取set操作的全部column + setColumns = append(setColumns, ast.FindAllCols(node)...) + } + return true, nil + }, node) + common.LogIfError(err, "") + setColumns = idxAdv.calcCardinality(CompleteColumnsInfo(idxAdv.Ast, setColumns, idxAdv.vEnv)) + for _, col := range setColumns { + idxMeta := idxAdv.IndexMeta[idxAdv.vEnv.DBHash(col.DB)][col.Table] + if idxMeta == nil { + return rule + } + for _, idx := range idxMeta.IdxRows { + if idx.KeyName == "PRIMARY" { + if col.Name == idx.ColumnName { + rule = HeuristicRules["CLA.016"] + return rule + } + continue + } + } + } + } + + return rule +} + +// RuleForbiddenSyntax CLA.017 +func (q *Query4Audit) RuleForbiddenSyntax() Rule { + var rule = q.RuleOK() + + // 由于vitess对某些语法的支持不完善,使得如创建临时表等语句无法通过语法检查 + // 所以这里使用正则对触发器、临时表、存储过程等进行匹配 + // 但是目前支持的也不是非常全面,有待完善匹配规则 + // TODO TiDB 目前还不支持触发器、存储过程、自定义函数、外键 + + forbidden := []*regexp.Regexp{ + regexp.MustCompile(`(?i)CREATE\s+TRIGGER\s+`), + + regexp.MustCompile(`(?i)CREATE\s+TEMPORARY\s+TABLE\s+`), + + regexp.MustCompile(`(?i)CREATE\s+VIEW\s+`), + regexp.MustCompile(`(?i)REPLACE\s+VIEW\s+`), + + regexp.MustCompile(`(?i)CREATE\s+PROCEDURE\s+`), + regexp.MustCompile(`(?i)CREATE\s+FUNCTION\s+`), + } + + for _, reg := range forbidden { + if reg.MatchString(q.Query) { + rule = HeuristicRules["CLA.017"] + if position := reg.FindIndex([]byte(q.Query)); len(position) > 0 { + rule.Position = position[0] + } + break + } + } + return rule +} + +// RuleNestedSubQueries JOI.006 +func (q *Query4Audit) RuleNestedSubQueries() Rule { + var rule = q.RuleOK() + err := sqlparser.Walk(func(node sqlparser.SQLNode) (kontinue bool, err error) { + switch node.(type) { + case *sqlparser.Subquery: + rule = HeuristicRules["JOI.006"] + return false, nil + } + return true, nil + }, q.Stmt) + common.LogIfError(err, "") + return rule +} + +// RuleMultiDeleteUpdate JOI.007 +func (q *Query4Audit) RuleMultiDeleteUpdate() Rule { + var rule = q.RuleOK() + switch q.Stmt.(type) { + case *sqlparser.Delete, *sqlparser.Update: + err := sqlparser.Walk(func(node sqlparser.SQLNode) (kontinue bool, err error) { + switch node.(type) { + case *sqlparser.JoinTableExpr: + rule = HeuristicRules["JOI.007"] + return false, nil + } + return true, nil + }, q.Stmt) + common.LogIfError(err, "") + } + return rule +} + +// RuleMultiDBJoin JOI.008 +func (q *Query4Audit) RuleMultiDBJoin() Rule { + var rule = q.RuleOK() + meta := ast.GetMeta(q.Stmt, nil) + dbCount := 0 + for range meta { + dbCount++ + } + + if dbCount > 1 { + err := sqlparser.Walk(func(node sqlparser.SQLNode) (kontinue bool, err error) { + switch node.(type) { + case *sqlparser.JoinTableExpr: + rule = HeuristicRules["JOI.008"] + return false, nil + } + return true, nil + }, q.Stmt) + common.LogIfError(err, "") + } + return rule +} + +// RuleORUsage ARG.008 +func (q *Query4Audit) RuleORUsage() Rule { + var rule = q.RuleOK() + switch q.Stmt.(type) { + case *sqlparser.Select: + err := sqlparser.Walk(func(node sqlparser.SQLNode) (kontinue bool, err error) { + switch node.(type) { + case *sqlparser.OrExpr: + rule = HeuristicRules["ARG.008"] + return false, nil + } + return true, nil + }, q.Stmt) + common.LogIfError(err, "") + } + return rule +} + +// RuleSpaceWithQuote ARG.009 +func (q *Query4Audit) RuleSpaceWithQuote() Rule { + var rule = q.RuleOK() + for _, tk := range ast.Tokenize(q.Query) { + if tk.Type == ast.TokenTypeQuote { + // 序列化的Val是带引号,所以要取第2个最倒数第二个,这样也就不用担心len<2了。 + switch tk.Val[1] { + case ' ': + rule = HeuristicRules["ARG.009"] + } + switch tk.Val[len(tk.Val)-2] { + case ' ': + rule = HeuristicRules["ARG.009"] + } + } + } + return rule +} + +// RuleHint ARG.010 +func (q *Query4Audit) RuleHint() Rule { + var rule = q.RuleOK() + err := sqlparser.Walk(func(node sqlparser.SQLNode) (kontinue bool, err error) { + switch n := node.(type) { + case *sqlparser.IndexHints: + if n != nil { + rule = HeuristicRules["ARG.010"] + } + return false, nil + } + return true, nil + }, q.Stmt) + common.LogIfError(err, "") + return rule +} + +// RuleNot ARG.011 +func (q *Query4Audit) RuleNot() Rule { + var rule = q.RuleOK() + err := sqlparser.Walk(func(node sqlparser.SQLNode) (kontinue bool, err error) { + switch n := node.(type) { + case *sqlparser.ComparisonExpr: + if strings.HasPrefix(n.Operator, "not") { + rule = HeuristicRules["ARG.011"] + return false, nil + } + } + return true, nil + }, q.Stmt) + common.LogIfError(err, "") + return rule +} + +// RuleUNIONUsage SUB.002 +func (q *Query4Audit) RuleUNIONUsage() Rule { + var rule = q.RuleOK() + switch s := q.Stmt.(type) { + case *sqlparser.Union: + if s.Type == "union" { + rule = HeuristicRules["SUB.002"] + } + } + return rule +} + +// RuleDistinctJoinUsage SUB.003 +func (q *Query4Audit) RuleDistinctJoinUsage() Rule { + var rule = q.RuleOK() + switch expr := q.Stmt.(type) { + case *sqlparser.Select: + if expr.Distinct != "" { + if expr.From != nil { + if len(expr.From) > 1 { + rule = HeuristicRules["SUB.003"] + } + } + } + } + return rule +} + +// RuleReadablePasswords SEC.002 +func (q *Query4Audit) RuleReadablePasswords() Rule { + var rule = q.RuleOK() + switch q.Stmt.(type) { + case *sqlparser.DDL: + re := regexp.MustCompile(`(?i)(password)|(password)|(pwd)`) + for _, tiStmt := range q.TiStmt { + // create table stmt + switch node := tiStmt.(type) { + case *tidb.CreateTableStmt: + for _, col := range node.Cols { + switch col.Tp.Tp { + case mysql.TypeString, mysql.TypeVarchar, mysql.TypeVarString, + mysql.TypeBlob, mysql.TypeTinyBlob, mysql.TypeMediumBlob: + if re.FindString(q.Query) != "" { + return HeuristicRules["SEC.002"] + } + } + } + + case *tidb.AlterTableStmt: + // alter table stmt + for _, spec := range node.Specs { + switch spec.Tp { + case tidb.AlterTableModifyColumn, tidb.AlterTableChangeColumn, tidb.AlterTableAddColumns: + for _, col := range spec.NewColumns { + switch col.Tp.Tp { + case mysql.TypeString, mysql.TypeVarchar, mysql.TypeVarString, + mysql.TypeBlob, mysql.TypeTinyBlob, mysql.TypeMediumBlob: + if re.FindString(q.Query) != "" { + return HeuristicRules["SEC.002"] + } + } + } + } + } + } + } + } + return rule +} + +// RuleDataDrop SEC.003 +func (q *Query4Audit) RuleDataDrop() Rule { + var rule = q.RuleOK() + switch s := q.Stmt.(type) { + case *sqlparser.DBDDL: + if s.Action == "drop" { + rule = HeuristicRules["SEC.003"] + } + case *sqlparser.DDL: + if s.Action == "drop" || s.Action == "truncate" { + rule = HeuristicRules["SEC.003"] + } + case *sqlparser.Delete: + rule = HeuristicRules["SEC.003"] + } + return rule +} + +// RuleCompareWithFunction FUN.001 +func (q *Query4Audit) RuleCompareWithFunction() Rule { + var rule = q.RuleOK() + err := sqlparser.Walk(func(node sqlparser.SQLNode) (kontinue bool, err error) { + switch n := node.(type) { + case *sqlparser.ComparisonExpr: + if strings.HasSuffix(sqlparser.String(n.Left), ")") { + rule = HeuristicRules["FUN.001"] + return false, nil + } + } + return true, nil + }, q.Stmt) + common.LogIfError(err, "") + return rule +} + +// RuleCountStar FUN.002 +func (q *Query4Audit) RuleCountStar() Rule { + var rule = q.RuleOK() + switch n := q.Stmt.(type) { + case *sqlparser.Select: + // count(N), count(col), count(*) + re := regexp.MustCompile(`(?i)(count\(\s*[*0-9a-z_` + "`" + `]*\s*\))`) + if re.FindString(q.Query) != "" && n.Where != nil { + rule = HeuristicRules["FUN.002"] + } + } + return rule +} + +// RuleTruncateTable SEC.001 +func (q *Query4Audit) RuleTruncateTable() Rule { + var rule = q.RuleOK() + switch s := q.Stmt.(type) { + case *sqlparser.DDL: + if s.Action == "truncate" { + rule = HeuristicRules["SEC.001"] + } + } + return rule +} + +// RuleIn ARG.005 && ARG.004 +func (q *Query4Audit) RuleIn() Rule { + var rule = q.RuleOK() + err := sqlparser.Walk(func(node sqlparser.SQLNode) (kontinue bool, err error) { + switch n := node.(type) { + case *sqlparser.ComparisonExpr: + switch n.Operator { + case "in": + switch r := n.Right.(type) { + case sqlparser.ValTuple: + // IN (NULL) + for _, v := range r { + switch v.(type) { + case *sqlparser.NullVal: + rule = HeuristicRules["ARG.004"] + return false, nil + } + } + if len(r) > common.Config.MaxInCount { + rule = HeuristicRules["ARG.005"] + return false, nil + } + } + case "not in": + switch r := n.Right.(type) { + case sqlparser.ValTuple: + // NOT IN (NULL) + for _, v := range r { + switch v.(type) { + case *sqlparser.NullVal: + rule = HeuristicRules["ARG.004"] + return false, nil + } + } + } + } + } + return true, nil + }, q.Stmt) + common.LogIfError(err, "") + return rule +} + +// RuleIsNullIsNotNull ARG.006 +func (q *Query4Audit) RuleIsNullIsNotNull() Rule { + var rule = q.RuleOK() + switch q.Stmt.(type) { + case *sqlparser.Select: + re := regexp.MustCompile(`(?i)is\s*(not)?\s+null\b`) + if re.FindString(q.Query) != "" { + rule = HeuristicRules["ARG.006"] + } + } + return rule +} + +// RuleVarcharVSChar COL.008 +func (q *Query4Audit) RuleVarcharVSChar() Rule { + var rule = q.RuleOK() + switch q.Stmt.(type) { + case *sqlparser.DDL: + for _, tiStmt := range q.TiStmt { + switch node := tiStmt.(type) { + case *tidb.CreateTableStmt: + for _, col := range node.Cols { + switch col.Tp.Tp { + // 在 TiDB 的 AST 中,char 和 binary 的 type 都是 mysql.TypeString + // 只是 binary 数据类型的 character 和 collate 是 binary + case mysql.TypeString: + rule = HeuristicRules["COL.008"] + } + } + + case *tidb.AlterTableStmt: + for _, spec := range node.Specs { + switch spec.Tp { + case tidb.AlterTableAddColumns, tidb.AlterTableChangeColumn, tidb.AlterTableModifyColumn: + for _, col := range spec.NewColumns { + switch col.Tp.Tp { + case mysql.TypeString: + rule = HeuristicRules["COL.008"] + } + } + } + } + } + } + } + return rule +} + +// RuleCreateDualTable TBL.003 +func (q *Query4Audit) RuleCreateDualTable() Rule { + var rule = q.RuleOK() + switch s := q.Stmt.(type) { + case *sqlparser.DDL: + if s.NewName.Name.String() == "dual" { + rule = HeuristicRules["TBL.003"] + + } + } + return rule +} + +// RuleAlterCharset ALT.001 +func (q *Query4Audit) RuleAlterCharset() Rule { + var rule = q.RuleOK() + switch q.Stmt.(type) { + case *sqlparser.DDL: + for _, tiStmt := range q.TiStmt { + switch node := tiStmt.(type) { + case *tidb.AlterTableStmt: + for _, spec := range node.Specs { + switch spec.Tp { + case tidb.AlterTableOption: + for _, option := range spec.Options { + if option.Tp == tidb.TableOptionCharset || + option.Tp == tidb.TableOptionCollate { + rule = HeuristicRules["ALT.001"] + break + } + } + } + + if rule.Item == "ALT.001" { + break + } + } + } + } + } + return rule +} + +// RuleAlterDropColumn ALT.003 +func (q *Query4Audit) RuleAlterDropColumn() Rule { + var rule = q.RuleOK() + switch q.Stmt.(type) { + case *sqlparser.DDL: + for _, tiStmt := range q.TiStmt { + switch node := tiStmt.(type) { + case *tidb.AlterTableStmt: + for _, spec := range node.Specs { + switch spec.Tp { + case tidb.AlterTableDropColumn: + rule = HeuristicRules["ALT.003"] + } + } + } + } + + if rule.Item == "ALT.003" { + re := regexp.MustCompile(`(?i)(drop\s+column)`) + if position := re.FindIndex([]byte(q.Query)); len(position) > 0 { + rule.Position = position[0] + } + } + } + return rule +} + +// RuleAlterDropKey ALT.004 +func (q *Query4Audit) RuleAlterDropKey() Rule { + var rule = q.RuleOK() + switch q.Stmt.(type) { + case *sqlparser.DDL: + for _, tiStmt := range q.TiStmt { + switch node := tiStmt.(type) { + case *tidb.AlterTableStmt: + for _, spec := range node.Specs { + switch spec.Tp { + case tidb.AlterTableDropPrimaryKey, + tidb.AlterTableDropIndex, + tidb.AlterTableDropForeignKey: + rule = HeuristicRules["ALT.004"] + } + } + } + } + } + return rule +} + +// RuleCantBeNull COL.012 +func (q *Query4Audit) RuleCantBeNull() Rule { + var rule = q.RuleOK() + switch q.Stmt.(type) { + case *sqlparser.DDL: + for _, tiStmt := range q.TiStmt { + switch node := tiStmt.(type) { + case *tidb.CreateTableStmt: + for _, col := range node.Cols { + switch col.Tp.Tp { + case mysql.TypeBlob, mysql.TypeTinyBlob, mysql.TypeMediumBlob, mysql.TypeLongBlob: + if !mysql.HasNotNullFlag(col.Tp.Flag) { + rule = HeuristicRules["COL.012"] + } + } + } + + case *tidb.AlterTableStmt: + for _, spec := range node.Specs { + switch spec.Tp { + case tidb.AlterTableAddColumns, tidb.AlterTableModifyColumn, tidb.AlterTableChangeColumn: + for _, col := range spec.NewColumns { + switch col.Tp.Tp { + case mysql.TypeBlob, mysql.TypeTinyBlob, mysql.TypeMediumBlob, mysql.TypeLongBlob: + if !mysql.HasNotNullFlag(col.Tp.Flag) { + rule = HeuristicRules["COL.012"] + } + } + } + } + } + } + } + } + + return rule +} + +// RuleTooManyKeys KEY.005 +func (q *Query4Audit) RuleTooManyKeys() Rule { + var rule = q.RuleOK() + switch q.Stmt.(type) { + case *sqlparser.DDL: + for _, tiStmt := range q.TiStmt { + switch node := tiStmt.(type) { + case *tidb.CreateTableStmt: + if len(node.Constraints) > common.Config.MaxIdxCount { + rule = HeuristicRules["KEY.005"] + } + } + } + } + return rule +} + +// RuleTooManyKeyParts KEY.006 +func (q *Query4Audit) RuleTooManyKeyParts() Rule { + var rule = q.RuleOK() + switch q.Stmt.(type) { + case *sqlparser.DDL: + for _, tiStmt := range q.TiStmt { + switch node := tiStmt.(type) { + case *tidb.CreateTableStmt: + for _, constraint := range node.Constraints { + if len(constraint.Keys) > common.Config.MaxIdxColsCount { + return HeuristicRules["KEY.006"] + } + + if constraint.Refer != nil && len(constraint.Refer.IndexColNames) > common.Config.MaxIdxColsCount { + return HeuristicRules["KEY.006"] + } + } + + case *tidb.AlterTableStmt: + for _, spec := range node.Specs { + switch spec.Tp { + case tidb.AlterTableAddConstraint: + if spec.Constraint != nil { + if len(spec.Constraint.Keys) > common.Config.MaxIdxColsCount { + return HeuristicRules["KEY.006"] + } + + if spec.Constraint.Refer != nil { + if len(spec.Constraint.Refer.IndexColNames) > common.Config.MaxIdxColsCount { + return HeuristicRules["KEY.006"] + } + } + } + } + } + } + } + } + + return rule +} + +// RulePKNotInt KEY.007 && KEY.001 +func (q *Query4Audit) RulePKNotInt() Rule { + var rule = q.RuleOK() + var pk sqlparser.ColIdent + switch s := q.Stmt.(type) { + case *sqlparser.DDL: + if s.Action == "create" { + if s.TableSpec == nil { + return rule + } + for _, idx := range s.TableSpec.Indexes { + if idx.Info.Type == "primary key" { + if len(idx.Columns) == 1 { + pk = idx.Columns[0].Column + break + } + } + } + + // 未指定主键 + if pk.String() == "" { + rule = HeuristicRules["KEY.007"] + return rule + } + + // 主键非int, bigint类型 + for _, col := range s.TableSpec.Columns { + if pk.String() == col.Name.String() { + switch col.Type.Type { + case "int", "bigint", "integer": + if !col.Type.Unsigned { + rule = HeuristicRules["KEY.007"] + } + if !col.Type.Autoincrement { + rule = HeuristicRules["KEY.001"] + } + default: + rule = HeuristicRules["KEY.007"] + } + } + } + } + } + return rule +} + +// RuleOrderByMultiDirection KEY.008 +func (q *Query4Audit) RuleOrderByMultiDirection() Rule { + var rule = q.RuleOK() + err := sqlparser.Walk(func(node sqlparser.SQLNode) (kontinue bool, err error) { + switch n := node.(type) { + case sqlparser.OrderBy: + order := "" + for _, col := range strings.Split(sqlparser.String(n), ",") { + orders := strings.Split(col, " ") + if order != "" && order != orders[len(orders)-1] { + rule = HeuristicRules["KEY.008"] + return false, nil + } + order = orders[len(orders)-1] + } + } + return true, nil + }, q.Stmt) + common.LogIfError(err, "") + return rule +} + +// RuleUniqueKeyDup KEY.009 +// TODO: 目前只是给建议,期望能够实现自动检查 +func (q *Query4Audit) RuleUniqueKeyDup() Rule { + var rule = q.RuleOK() + switch q.Stmt.(type) { + case *sqlparser.DDL: + for _, tiStmt := range q.TiStmt { + switch node := tiStmt.(type) { + case *tidb.CreateIndexStmt: + // create index + if node.Unique { + re := regexp.MustCompile(`(?i)(create\s+(unique)\s)`) + rule = HeuristicRules["KEY.009"] + if position := re.FindIndex([]byte(q.Query)); len(position) > 0 { + rule.Position = position[0] + } + return rule + } + + case *tidb.AlterTableStmt: + // alter table add constraint + for _, spec := range node.Specs { + switch spec.Tp { + case tidb.AlterTableAddConstraint: + if spec.Constraint == nil { + continue + } + switch spec.Constraint.Tp { + case tidb.ConstraintPrimaryKey, tidb.ConstraintUniq, tidb.ConstraintUniqKey, tidb.ConstraintUniqIndex: + re := regexp.MustCompile(`(?i)(add\s+(unique)\s)`) + rule = HeuristicRules["KEY.009"] + if position := re.FindIndex([]byte(q.Query)); len(position) > 0 { + rule.Position = position[0] + } + return rule + } + } + } + } + } + } + return rule +} + +// RuleTimestampDefault COL.013 +func (q *Query4Audit) RuleTimestampDefault() Rule { + var rule = q.RuleOK() + switch q.Stmt.(type) { + case *sqlparser.DDL: + for _, tiStmt := range q.TiStmt { + switch node := tiStmt.(type) { + case *tidb.CreateTableStmt: + for _, col := range node.Cols { + if col.Tp.Tp == mysql.TypeTimestamp { + hasDefault := false + for _, option := range col.Options { + if option.Tp == tidb.ColumnOptionDefaultValue { + hasDefault = true + } + } + if !hasDefault { + rule = HeuristicRules["COL.013"] + break + } + } + } + case *tidb.AlterTableStmt: + for _, spec := range node.Specs { + switch spec.Tp { + case tidb.AlterTableAddColumns, + tidb.AlterTableModifyColumn, + tidb.AlterTableChangeColumn, + tidb.AlterTableAlterColumn: + for _, col := range spec.NewColumns { + if col.Tp.Tp == mysql.TypeTimestamp { + hasDefault := false + for _, option := range col.Options { + if option.Tp == tidb.ColumnOptionDefaultValue { + hasDefault = true + } + } + if !hasDefault { + rule = HeuristicRules["COL.013"] + break + } + } + } + } + } + } + } + } + return rule +} + +// RuleAutoIncrementInitNotZero TBL.004 +func (q *Query4Audit) RuleAutoIncrementInitNotZero() Rule { + var rule = q.RuleOK() + switch q.Stmt.(type) { + case *sqlparser.DDL: + for _, tiStmt := range q.TiStmt { + switch node := tiStmt.(type) { + case *tidb.CreateTableStmt: + for _, opt := range node.Options { + if opt.Tp == tidb.TableOptionAutoIncrement && opt.UintValue > 1 { + rule = HeuristicRules["TBL.004"] + } + } + + } + } + } + return rule +} + +// RuleColumnWithCharset COL.014 +func (q *Query4Audit) RuleColumnWithCharset() Rule { + var rule = q.RuleOK() + switch q.Stmt.(type) { + case *sqlparser.DDL: + for _, tiStmt := range q.TiStmt { + switch node := tiStmt.(type) { + case *tidb.CreateTableStmt: + for _, col := range node.Cols { + if col.Tp.Charset != "" || col.Tp.Collate != "" { + rule = HeuristicRules["COL.014"] + break + } + } + case *tidb.AlterTableStmt: + for _, spec := range node.Specs { + switch spec.Tp { + case tidb.AlterTableAlterColumn, tidb.AlterTableChangeColumn, + tidb.AlterTableModifyColumn, tidb.AlterTableAddColumns: + for _, col := range spec.NewColumns { + if col.Tp.Charset != "" || col.Tp.Collate != "" { + rule = HeuristicRules["COL.014"] + break + } + } + } + + } + } + } + } + return rule +} + +// RuleTableCharsetCheck TBL.005 +func (q *Query4Audit) RuleTableCharsetCheck() Rule { + var rule = q.RuleOK() + + switch q.Stmt.(type) { + case *sqlparser.DDL: + for _, tiStmt := range q.TiStmt { + switch node := tiStmt.(type) { + case *tidb.CreateTableStmt: + var allow bool + var hasCharset bool + for _, opt := range node.Options { + if opt.Tp == tidb.TableOptionCharset { + hasCharset = true + for _, ch := range common.Config.TableAllowCharsets { + if strings.TrimSpace(strings.ToLower(ch)) == strings.TrimSpace(strings.ToLower(opt.StrValue)) { + allow = true + break + } + } + } + } + + // 未指定字符集使用MySQL默认配置字符集,我们认为MySQL的配置是被优化过的。 + if hasCharset && !allow { + rule = HeuristicRules["TBL.005"] + break + } + + case *tidb.AlterTableStmt: + for _, spec := range node.Specs { + var allow bool + var hasCharset bool + switch spec.Tp { + case tidb.AlterTableOption: + for _, opt := range spec.Options { + if opt.Tp == tidb.TableOptionCharset { + hasCharset = true + for _, ch := range common.Config.TableAllowCharsets { + if strings.TrimSpace(strings.ToLower(ch)) == strings.TrimSpace(strings.ToLower(opt.StrValue)) { + allow = true + break + } + } + } + } + // 未指定字符集使用MySQL默认配置字符集,我们认为MySQL的配置是被优化过的。 + if hasCharset && !allow { + rule = HeuristicRules["TBL.005"] + break + } + } + } + } + } + } + return rule +} + +// RuleBlobDefaultValue COL.015 +func (q *Query4Audit) RuleBlobDefaultValue() Rule { + var rule = q.RuleOK() + switch q.Stmt.(type) { + case *sqlparser.DDL: + for _, tiStmt := range q.TiStmt { + switch node := tiStmt.(type) { + case *tidb.CreateTableStmt: + for _, col := range node.Cols { + switch col.Tp.Tp { + case mysql.TypeBlob, mysql.TypeMediumBlob, mysql.TypeTinyBlob, mysql.TypeLongBlob: + for _, opt := range col.Options { + if opt.Tp == tidb.ColumnOptionDefaultValue && opt.Expr.GetType().Tp != mysql.TypeNull { + rule = HeuristicRules["COL.015"] + break + } + } + } + } + case *tidb.AlterTableStmt: + for _, spec := range node.Specs { + switch spec.Tp { + case tidb.AlterTableModifyColumn, tidb.AlterTableAlterColumn, + tidb.AlterTableChangeColumn, tidb.AlterTableAddColumns: + for _, col := range spec.NewColumns { + switch col.Tp.Tp { + case mysql.TypeBlob, mysql.TypeMediumBlob, mysql.TypeTinyBlob, mysql.TypeLongBlob: + for _, opt := range col.Options { + if opt.Tp == tidb.ColumnOptionDefaultValue && opt.Expr.GetType().Tp != mysql.TypeNull { + rule = HeuristicRules["COL.015"] + break + } + } + } + } + } + } + } + } + } + return rule +} + +// RuleIntPrecision COL.016 +func (q *Query4Audit) RuleIntPrecision() Rule { + var rule = q.RuleOK() + switch q.Stmt.(type) { + case *sqlparser.DDL: + for _, tiStmt := range q.TiStmt { + switch node := tiStmt.(type) { + case *tidb.CreateTableStmt: + for _, col := range node.Cols { + switch col.Tp.Tp { + case mysql.TypeLong: + if (col.Tp.Flen < 10 || col.Tp.Flen > 11) && col.Tp.Flen > 0 { + // 有些语言ORM框架会生成int(11),有些语言的框架生成int(10) + rule = HeuristicRules["COL.016"] + break + } + case mysql.TypeLonglong: + if (col.Tp.Flen != 20) && col.Tp.Flen > 0 { + rule = HeuristicRules["COL.016"] + break + } + } + } + case *tidb.AlterTableStmt: + for _, spec := range node.Specs { + switch spec.Tp { + case tidb.AlterTableAddColumns, tidb.AlterTableChangeColumn, + tidb.AlterTableAlterColumn, tidb.AlterTableModifyColumn: + for _, col := range spec.NewColumns { + switch col.Tp.Tp { + case mysql.TypeLong: + if (col.Tp.Flen < 10 || col.Tp.Flen > 11) && col.Tp.Flen > 0 { + // 有些语言ORM框架会生成int(11),有些语言的框架生成int(10) + rule = HeuristicRules["COL.016"] + break + } + case mysql.TypeLonglong: + if col.Tp.Flen != 20 && col.Tp.Flen > 0 { + rule = HeuristicRules["COL.016"] + break + } + } + } + } + } + } + } + } + return rule +} + +// RuleVarcharLength COL.017 +func (q *Query4Audit) RuleVarcharLength() Rule { + var rule = q.RuleOK() + switch q.Stmt.(type) { + case *sqlparser.DDL: + for _, tiStmt := range q.TiStmt { + switch node := tiStmt.(type) { + case *tidb.CreateTableStmt: + for _, col := range node.Cols { + switch col.Tp.Tp { + case mysql.TypeVarchar, mysql.TypeVarString: + if col.Tp.Flen > common.Config.MaxVarcharLength { + rule = HeuristicRules["COL.017"] + break + } + } + } + case *tidb.AlterTableStmt: + for _, spec := range node.Specs { + switch spec.Tp { + case tidb.AlterTableAddColumns, tidb.AlterTableChangeColumn, + tidb.AlterTableAlterColumn, tidb.AlterTableModifyColumn: + for _, col := range spec.NewColumns { + switch col.Tp.Tp { + case mysql.TypeVarchar, mysql.TypeVarString: + if col.Tp.Flen > common.Config.MaxVarcharLength { + rule = HeuristicRules["COL.017"] + break + } + } + } + } + } + } + } + } + return rule +} + +// RuleNoOSCKey KEY.002 +func (q *Query4Audit) RuleNoOSCKey() Rule { + var rule = q.RuleOK() + switch s := q.Stmt.(type) { + case *sqlparser.DDL: + if s.Action == "create" { + pkReg := regexp.MustCompile(`(?i)(primary\s+key)`) + if !pkReg.MatchString(q.Query) { + ukReg := regexp.MustCompile(`(?i)(unique\s+((key)|(index)))`) + if !ukReg.MatchString(q.Query) { + rule = HeuristicRules["KEY.002"] + } + } + } + } + return rule +} + +// RuleTooManyFields COL.006 +func (q *Query4Audit) RuleTooManyFields() Rule { + var rule = q.RuleOK() + switch q.Stmt.(type) { + case *sqlparser.DDL: + for _, tiStmt := range q.TiStmt { + switch node := tiStmt.(type) { + case *tidb.CreateTableStmt: + if len(node.Cols) > common.Config.MaxColCount { + rule = HeuristicRules["COL.006"] + } + } + } + } + return rule +} + +// RuleAllowEngine TBL.002 +func (q *Query4Audit) RuleAllowEngine() Rule { + var rule = q.RuleOK() + var hasDefaultEngine bool + var allowedEngine bool + switch q.Stmt.(type) { + case *sqlparser.DDL: + for _, tiStmt := range q.TiStmt { + switch node := tiStmt.(type) { + case *tidb.CreateTableStmt: + for _, opt := range node.Options { + if opt.Tp == tidb.TableOptionEngine { + hasDefaultEngine = true + // 使用了非推荐的存储引擎 + for _, engine := range common.Config.TableAllowEngines { + if strings.EqualFold(opt.StrValue, engine) { + allowedEngine = true + } + } + // common.Config.TableAllowEngines 为空时不给予建议 + if !allowedEngine && len(common.Config.TableAllowEngines) > 0 { + rule = HeuristicRules["TBL.002"] + break + } + } + } + // 建表语句未指定表的存储引擎 + if !hasDefaultEngine { + rule = HeuristicRules["TBL.002"] + break + } + case *tidb.AlterTableStmt: + for _, spec := range node.Specs { + switch spec.Tp { + case tidb.AlterTableOption: + for _, opt := range spec.Options { + if opt.Tp == tidb.TableOptionEngine { + // 使用了非推荐的存储引擎 + for _, engine := range common.Config.TableAllowEngines { + if strings.EqualFold(opt.StrValue, engine) { + allowedEngine = true + } + } + // common.Config.TableAllowEngines 为空时不给予建议 + if !allowedEngine && len(common.Config.TableAllowEngines) > 0 { + rule = HeuristicRules["TBL.002"] + break + } + } + } + } + } + } + } + } + return rule +} + +// RulePartitionNotAllowed TBL.001 +func (q *Query4Audit) RulePartitionNotAllowed() Rule { + var rule = q.RuleOK() + switch q.Stmt.(type) { + case *sqlparser.DDL: + for _, tiStmt := range q.TiStmt { + switch node := tiStmt.(type) { + case *tidb.CreateTableStmt: + if node.Partition != nil { + rule = HeuristicRules["TBL.001"] + break + } + case *tidb.AlterTableStmt: + for _, spec := range node.Specs { + if len(spec.PartDefinitions) > 0 { + rule = HeuristicRules["TBL.001"] + break + } + } + } + } + } + return rule +} + +// RuleAutoIncUnsigned COL.003: +func (q *Query4Audit) RuleAutoIncUnsigned() Rule { + var rule = q.RuleOK() + switch q.Stmt.(type) { + case *sqlparser.DDL: + for _, tiStmt := range q.TiStmt { + switch node := tiStmt.(type) { + case *tidb.CreateTableStmt: + for _, col := range node.Cols { + for _, opt := range col.Options { + if opt.Tp == tidb.ColumnOptionAutoIncrement { + if !mysql.HasUnsignedFlag(col.Tp.Flag) { + rule = HeuristicRules["COL.003"] + break + } + } + + if rule.Item == "COL.003" { + break + } + } + } + case *tidb.AlterTableStmt: + for _, spec := range node.Specs { + switch spec.Tp { + case tidb.AlterTableChangeColumn, tidb.AlterTableAlterColumn, + tidb.AlterTableModifyColumn, tidb.AlterTableAddColumns: + for _, col := range spec.NewColumns { + for _, opt := range col.Options { + if opt.Tp == tidb.ColumnOptionAutoIncrement { + if !mysql.HasUnsignedFlag(col.Tp.Flag) { + rule = HeuristicRules["COL.003"] + break + } + } + + if rule.Item == "COL.003" { + break + } + } + } + } + } + } + } + } + return rule +} + +// RuleSpaceAfterDot STA.002 +func (q *Query4Audit) RuleSpaceAfterDot() Rule { + var rule = q.RuleOK() + tks := ast.Tokenize(q.Query) + for i, tk := range tks { + switch tk.Type { + + // SELECT * FROM db. tbl + // SELECT tbl. col FROM tbl + case ast.TokenTypeWord: + if len(tks) > i+1 && + tks[i+1].Type == ast.TokenTypeWhitespace && + strings.HasSuffix(tk.Val, ".") { + common.Log.Debug("RuleSpaceAfterDot: ", tk.Val, tks[i+1].Val) + rule = HeuristicRules["STA.002"] + return rule + } + default: + } + } + return rule +} + +// RuleIdxPrefix STA.003 +func (q *Query4Audit) RuleIdxPrefix() Rule { + var rule = q.RuleOK() + for _, node := range q.TiStmt { + switch n := node.(type) { + case *tidb.CreateTableStmt: + for _, c := range n.Constraints { + switch c.Tp { + case tidb.ConstraintIndex, tidb.ConstraintKey: + if !strings.HasPrefix(c.Name, common.Config.IdxPrefix) { + rule = HeuristicRules["STA.003"] + } + case tidb.ConstraintUniq, tidb.ConstraintUniqKey, tidb.ConstraintUniqIndex: + if !strings.HasPrefix(c.Name, common.Config.UkPrefix) { + rule = HeuristicRules["STA.003"] + } + } + } + case *tidb.AlterTableStmt: + for _, s := range n.Specs { + switch s.Tp { + case tidb.AlterTableAddConstraint: + switch s.Constraint.Tp { + case tidb.ConstraintIndex, tidb.ConstraintKey: + if !strings.HasPrefix(s.Constraint.Name, common.Config.IdxPrefix) { + rule = HeuristicRules["STA.003"] + } + case tidb.ConstraintUniq, tidb.ConstraintUniqKey, tidb.ConstraintUniqIndex: + if !strings.HasPrefix(s.Constraint.Name, common.Config.UkPrefix) { + rule = HeuristicRules["STA.003"] + } + } + } + } + } + } + return rule +} + +// RuleStandardName STA.004 +func (q *Query4Audit) RuleStandardName() Rule { + var rule = q.RuleOK() + allowReg := regexp.MustCompile(`(?i)[a-z0-9_` + "`" + `]`) + for _, tk := range ast.Tokenize(q.Query) { + if tk.Val == "``" { + rule = HeuristicRules["STA.004"] + } + + switch tk.Type { + // 反引号中可能有乱七八糟的东西 + case ast.TokenTypeBacktickQuote: + // 特殊字符,连续下划线 + if allowReg.ReplaceAllString(tk.Val, "") != "" || strings.Contains(tk.Val, "__") { + rule = HeuristicRules["STA.004"] + } + // 统一大小写 + if !(strings.ToLower(tk.Val) == tk.Val || strings.ToUpper(tk.Val) == tk.Val) { + rule = HeuristicRules["STA.004"] + } + case ast.TokenTypeWord: + // TOKEN_TYPE_WORD中处理连续下划线的情况,其他情况容易误伤 + if strings.Contains(tk.Val, "__") { + rule = HeuristicRules["STA.004"] + } + default: + } + } + return rule +} + +// MergeConflictHeuristicRules merge conflict rules +func MergeConflictHeuristicRules(rules map[string]Rule) map[string]Rule { + // KWR.001 VS ERR.000 + // select sql_calc_found_rows * from film + if _, ok := rules["KWR.001"]; ok { + delete(rules, "ERR.000") + } + + // SUB.001 VS OWN.004 VS JOI.006 + if _, ok := rules["SUB.001"]; ok { + delete(rules, "ARG.005") + delete(rules, "JOI.006") + } + + // SUB.004 VS SUB.001 + if _, ok := rules["SUB.004"]; ok { + delete(rules, "SUB.001") + } + + // KEY.007 VS KEY.002 + if _, ok := rules["KEY.007"]; ok { + delete(rules, "KEY.002") + } + + // JOI.002 VS JOI.006 + if _, ok := rules["JOI.002"]; ok { + delete(rules, "JOI.006") + } + + // JOI.008 VS JOI.007 + if _, ok := rules["JOI.008"]; ok { + delete(rules, "JOI.007") + } + return rules +} + +// RuleMySQLError ERR.XXX +func RuleMySQLError(item string, err error) Rule { + + type MySQLError struct { + ErrCode string + ErrString string + } + + // vitess 语法检查出错返回的是ERR.000 + switch item { + case "ERR.000": + return Rule{ + Item: item, + Summary: "MySQL执行出错 " + err.Error(), + Severity: "L8", + Content: err.Error(), + } + } + + // Received #1146 error from MySQL server: "table xxx doesn't exist" + errReg := regexp.MustCompile(`(?i)Received #([0-9]+) error from MySQL server: ['"](.*)['"]`) + errStr := err.Error() + msg := errReg.FindStringSubmatch(errStr) + var mysqlError MySQLError + + if len(msg) == 3 { + if msg[1] != "" && msg[2] != "" { + mysqlError = MySQLError{ + ErrCode: msg[1], + ErrString: msg[2], + } + } + } else { + var errcode string + if strings.HasPrefix(err.Error(), "syntax error at position") { + errcode = "1064" + } + mysqlError = MySQLError{ + ErrCode: errcode, + ErrString: err.Error(), + } + } + switch mysqlError.ErrCode { + // 1146 ER_NO_SUCH_TABLE + case "", "1146": + return Rule{ + Item: item, + Summary: "MySQL执行出错", + Severity: "L0", + Content: "", + } + default: + return Rule{ + Item: item, + Summary: "MySQL执行出错 " + mysqlError.ErrString, + Severity: "L8", + Content: mysqlError.ErrString, + } + } +} diff --git a/advisor/heuristic_test.go b/advisor/heuristic_test.go new file mode 100644 index 00000000..7366c452 --- /dev/null +++ b/advisor/heuristic_test.go @@ -0,0 +1,3015 @@ +/* + * Copyright 2018 Xiaomi, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package advisor + +import ( + "errors" + "sort" + "testing" + + "github.com/XiaoMi/soar/common" + + "github.com/kr/pretty" +) + +// ALI.001 +func TestRuleImplicitAlias(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := [][]string{ + { + "select col c from tbl where id < 1000", + "select col from tbl tb where id < 1000", + }, + { + "do 1", + }, + } + for _, sql := range sqls[0] { + q, _ := NewQuery4Audit(sql) + rule := q.RuleImplicitAlias() + if rule.Item != "ALI.001" { + t.Error("Rule not match:", rule.Item, "Expect : ALI.001") + } + } + for _, sql := range sqls[1] { + q, _ := NewQuery4Audit(sql) + rule := q.RuleImplicitAlias() + if rule.Item != "OK" { + t.Error("Rule not match:", rule.Item, "Expect : OK") + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// ALI.002 +func TestRuleStarAlias(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := []string{ + "select tbl.* as c1,c2,c3 from tbl where id < 1000", + } + for _, sql := range sqls { + q, _ := NewQuery4Audit(sql) + rule := q.RuleStarAlias() + if rule.Item != "ALI.002" { + t.Error("Rule not match:", rule.Item, "Expect : ALI.002") + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// ALI.003 +func TestRuleSameAlias(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := []string{ + "select col as col from tbl where id < 1000", + "select col from tbl as tbl where id < 1000", + } + for _, sql := range sqls { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleSameAlias() + if rule.Item != "ALI.003" { + t.Error("Rule not match:", rule.Item, "Expect : ALI.003") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// ARG.001 +func TestRulePrefixLike(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := []string{ + "select col from tbl where id like '%abc'", + "select col from tbl where id like '_abc'", + } + for _, sql := range sqls { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RulePrefixLike() + if rule.Item != "ARG.001" { + t.Error("Rule not match:", rule.Item, "Expect : ARG.001") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// ARG.002 +func TestRuleEqualLike(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := []string{ + "select col from tbl where id like 'abc'", + "select col from tbl where id like 1", + } + for _, sql := range sqls { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleEqualLike() + if rule.Item != "ARG.002" { + t.Error("Rule not match:", rule.Item, "Expect : ARG.002") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// CLA.001 +func TestRuleNoWhere(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := [][]string{ + {"select col from tbl", + "delete from tbl", + "update tbl set col=1", + "insert into city (country_id) select country_id from country", + }, + { + `select 1;`, + }, + } + for _, sql := range sqls[0] { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleNoWhere() + if rule.Item != "CLA.001" && rule.Item != "CLA.014" && rule.Item != "CLA.015" { + t.Error("Rule not match:", rule.Item, "Expect : CLA.001/CLA.014/CLA.015") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + for _, sql := range sqls[1] { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleNoWhere() + if rule.Item != "OK" { + t.Error("Rule not match:", rule.Item, "Expect : OK") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// CLA.002 +func TestRuleOrderByRand(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := []string{ + "select col from tbl where id = 1 order by rand()", + } + for _, sql := range sqls { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleOrderByRand() + if rule.Item != "CLA.002" { + t.Error("Rule not match:", rule.Item, "Expect : CLA.002") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// CLA.003 +func TestRuleOffsetLimit(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := []string{ + "select c1,c2 from tbl where name=xx order by number limit 1 offset 2000", + "select c1,c2 from tbl where name=xx order by number limit 2000,1", + } + for _, sql := range sqls { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleOffsetLimit() + if rule.Item != "CLA.003" { + t.Error("Rule not match:", rule.Item, "Expect : CLA.003") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// CLA.004 +func TestRuleGroupByConst(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := []string{ + "select col1,col2 from tbl where col1='abc' group by 1", + "select col1,col2 from tbl group by 1", + } + for _, sql := range sqls { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleGroupByConst() + if rule.Item != "CLA.004" { + t.Error("Rule not match:", rule.Item, "Expect : CLA.004") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// CLA.005 +func TestRuleOrderByConst(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := []string{ + // "select id from test where id=1 order by id", + "select id from test where id=1 order by 1", + } + for _, sql := range sqls { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleOrderByConst() + if rule.Item != "CLA.005" { + t.Error("Rule not match:", rule.Item, "Expect : CLA.005") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// CLA.006 +func TestRuleDiffGroupByOrderBy(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := []string{ + "select tb1.col, tb2.col from tb1, tb2 where id=1 group by tb1.col, tb2.col", + "select tb1.col, tb2.col from tb1, tb2 where id=1 order by tb1.col, tb2.col", + } + for _, sql := range sqls { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleDiffGroupByOrderBy() + if rule.Item != "CLA.006" { + t.Error("Rule not match:", rule.Item, "Expect : CLA.006") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// CLA.007 +func TestRuleMixOrderBy(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := []string{ + "select c1,c2,c3 from t1 where c1='foo' order by c2 desc, c3 asc", + } + for _, sql := range sqls { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleMixOrderBy() + if rule.Item != "CLA.007" { + t.Error("Rule not match:", rule.Item, "Expect : CLA.007") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// CLA.008 +func TestRuleExplicitOrderBy(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := []string{ + "select c1,c2,c3 from t1 where c1='foo' group by c2", + } + for _, sql := range sqls { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleExplicitOrderBy() + if rule.Item != "CLA.008" { + t.Error("Rule not match:", rule.Item, "Expect : CLA.008") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// CLA.009 +func TestRuleOrderByExpr(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := []string{ + "SELECT col FROM tbl order by cola - colb;", // order by 列运算 + "SELECT cola - colb col FROM tbl order by col;", // 别名为列运算 + "SELECT cola FROM tbl order by from_unixtime(col);", // order by 函数运算 + "SELECT from_unixtime(col) cola FROM tbl order by cola;", // 别名为函数运算 + + // 反面例子 + // `SELECT tbl.col FROM tbl ORDER BY col`, + // "SELECT sum(col) AS col FROM tbl ORDER BY dt", + // "SELECT tbl.col FROM tb, tbl WHERE tbl.tag_id = tb.id ORDER BY tbl.col", + // "SELECT col FROM tbl order by `timestamp`;", // 列名为关键字 + } + for _, sql := range sqls { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleOrderByExpr() + if rule.Item != "CLA.009" { + t.Error("Rule not match:", rule.Item, "Expect : CLA.009") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// CLA.010 +func TestRuleGroupByExpr(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := []string{ + "SELECT col FROM tbl GROUP by cola - colb;", + "SELECT cola - colb col FROM tbl GROUP by col;", + "SELECT cola FROM tbl GROUP by from_unixtime(col);", + "SELECT from_unixtime(col) cola FROM tbl GROUP by cola;", + + // 反面例子 + // `SELECT tbl.col FROM tbl GROUP BY col`, + // "SELECT dt, sum(col) AS col FROM tbl GROUP BY dt", + // "SELECT tbl.col FROM tb, tbl WHERE tbl.tag_id = tb.id GROUP BY tbl.col", + // "SELECT col FROM tbl GROUP by `timestamp`;", // 列名为关键字 + } + for _, sql := range sqls { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleGroupByExpr() + if rule.Item != "CLA.010" { + t.Error("Rule not match:", rule.Item, "Expect : CLA.010") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// CLA.011 +func TestRuleTblCommentCheck(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := []string{ + "CREATE TABLE `test1`( `ID` bigint(20) NOT NULL AUTO_INCREMENT," + + " `c1` varchar(128) DEFAULT NULL, `c2` varchar(300) DEFAULT NULL," + + " `c3` varchar(32) DEFAULT NULL, `c4` int(11) NOT NULL, `c5` double NOT NULL," + + " `c6` text NOT NULL, PRIMARY KEY (`ID`), KEY `idx_c3_c2_c4_c5_c6` " + + "(`c3`,`c2`(255),`c4`,`c5`,`c6`(255)), KEY `idx_c3_c2_c4` (`c3`,`c2`,`c4`)) " + + "ENGINE=InnoDB DEFAULT CHARSET=utf8", + } + for _, sql := range sqls { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleTblCommentCheck() + if rule.Item != "CLA.011" { + t.Error("Rule not match:", rule.Item, "Expect : CLA.011") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// COL.001 +func TestRuleSelectStar(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := []string{ + "select * from tbl where id=1", + "select col, * from tbl where id=1", + // 反面例子 + // "select count(*) from film where id=1", + // `select count(* ) from film where id=1`, + } + for _, sql := range sqls { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleSelectStar() + if rule.Item != "COL.001" { + t.Error("Rule not match:", rule.Item, "Expect : COL.001") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// COL.002 +func TestRuleInsertColDef(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := []string{ + "insert into tbl values(1,'name')", + "replace into tbl values(1,'name')", + } + for _, sql := range sqls { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleInsertColDef() + if rule.Item != "COL.002" { + t.Error("Rule not match:", rule.Item, "Expect : COL.002") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// COL.004 +func TestRuleAddDefaultValue(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := [][]string{ + { + "create table test(id int)", + `ALTER TABLE test change id id varchar(10);`, + `ALTER TABLE test modify id varchar(10);`, + }, + { + `ALTER TABLE test modify id varchar(10) DEFAULT '';`, + `ALTER TABLE test CHANGE id id varchar(10) DEFAULT '';`, + "create table test(id int not null default 0 comment '用户id')", + }, + } + for _, sql := range sqls[0] { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleAddDefaultValue() + if rule.Item != "COL.004" { + t.Error("Rule not match:", rule.Item, "Expect : COL.004") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + + for _, sql := range sqls[1] { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleAddDefaultValue() + if rule.Item != "OK" { + t.Error("Rule not match:", rule.Item, "Expect : OK") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// COL.005 +func TestRuleColCommentCheck(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := [][]string{ + { + "create table test(id int not null default 0)", + `alter table test add column a int`, + `ALTER TABLE t1 CHANGE b b INT NOT NULL;`, + }, + { + "create table test(id int not null default 0 comment '用户id')", + `alter table test add column a int comment 'test'`, + `ALTER TABLE t1 AUTO_INCREMENT = 13;`, + `ALTER TABLE t1 CHANGE b b INT NOT NULL COMMENT 'test';`, + }, + } + for _, sql := range sqls[0] { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleColCommentCheck() + if rule.Item != "COL.005" { + t.Error("Rule not match:", rule.Item, "Expect : COL.005") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + + for _, sql := range sqls[1] { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleColCommentCheck() + if rule.Item != "OK" { + t.Error("Rule not match:", rule.Item, "Expect : OK") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// LIT.001 +func TestRuleIPString(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := []string{ + "insert into tbl (IP,name) values('10.20.306.122','test')", + } + for _, sql := range sqls { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleIPString() + if rule.Item != "LIT.001" { + t.Error("Rule not match:", rule.Item, "Expect : LIT.001") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// LIT.002 +func TestRuleDataNotQuote(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := []string{ + "select col1,col2 from tbl where time < 2018-01-10", + "select col1,col2 from tbl where time < 18-01-10", + } + for _, sql := range sqls { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleDataNotQuote() + if rule.Item != "LIT.002" { + t.Error("Rule not match:", rule.Item, "Expect : LIT.002") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// KWR.001 +func TestRuleSQLCalcFoundRows(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := []string{ + "select SQL_CALC_FOUND_ROWS col from tbl where id>1000", + } + for _, sql := range sqls { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleSQLCalcFoundRows() + if rule.Item != "KWR.001" { + t.Error("Rule not match:", rule.Item, "Expect : KWR.001") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// JOI.001 +func TestRuleCommaAnsiJoin(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := []string{ + `select c1,c2,c3 from t1,t2 join t3 on t1.c1=t2.c1 and t1.c3=t3.c1 where id>1000;`, + } + for _, sql := range sqls { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleCommaAnsiJoin() + if rule.Item != "JOI.001" { + t.Error("Rule not match:", rule.Item, "Expect : JOI.001") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// JOI.002 +func TestRuleDupJoin(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := []string{ + `select tb1.col from (tb1, tb2) join tb2 on tb1.id=tb.id where tb1.id=1;`, + } + for _, sql := range sqls { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleDupJoin() + if rule.Item != "JOI.002" { + t.Error("Rule not match:", rule.Item, "Expect : JOI.002") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// RES.001 +func TestRuleNoDeterministicGroupby(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := [][]string{ + // 正面CASE + { + "select c1,c2,c3 from t1 where c2='foo' group by c2", + "select col, col2, sum(col1) from tb group by col", + "select col, col1 from tb group by col,sum(col1)", + "select * from tb group by col", + }, + + // 反面CASE + { + "select id from film", + "select col, sum(col1) from tb group by col", + "select * from file", + "SELECT COUNT(*) AS cnt, language_id FROM film GROUP BY language_id;", + "SELECT COUNT(*) AS cnt FROM film GROUP BY language_id;", + }, + } + for _, sql := range sqls[0] { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleNoDeterministicGroupby() + if rule.Item != "RES.001" { + t.Error("Rule not match:", rule.Item, "Expect : RES.001") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + + for _, sql := range sqls[1] { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleNoDeterministicGroupby() + if rule.Item != "OK" { + t.Error("Rule not match:", rule.Item, "Expect : OK") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// RES.002 +func TestRuleNoDeterministicLimit(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := []string{ + "select col1,col2 from tbl where name='zhangsan' limit 10", + } + for _, sql := range sqls { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleNoDeterministicLimit() + if rule.Item != "RES.002" { + t.Error("Rule not match:", rule.Item, "Expect : RES.002") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// RES.003 +func TestRuleUpdateDeleteWithLimit(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := [][]string{ + { + "UPDATE film SET length = 120 WHERE title = 'abc' LIMIT 1;", + }, + { + "UPDATE film SET length = 120 WHERE title = 'abc';", + }, + } + for _, sql := range sqls[0] { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleUpdateDeleteWithLimit() + if rule.Item != "RES.003" { + t.Error("Rule not match:", rule.Item, "Expect : RES.003") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + + for _, sql := range sqls[1] { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleUpdateDeleteWithLimit() + if rule.Item != "OK" { + t.Error("Rule not match:", rule.Item, "Expect : OK") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// RES.004 +func TestRuleUpdateDeleteWithOrderby(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := [][]string{ + { + "UPDATE film SET length = 120 WHERE title = 'abc' ORDER BY title;", + }, + { + "UPDATE film SET length = 120 WHERE title = 'abc';", + }, + } + for _, sql := range sqls[0] { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleUpdateDeleteWithOrderby() + if rule.Item != "RES.004" { + t.Error("Rule not match:", rule.Item, "Expect : RES.004") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + + for _, sql := range sqls[1] { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleUpdateDeleteWithOrderby() + if rule.Item != "OK" { + t.Error("Rule not match:", rule.Item, "Expect : OK") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// RES.005 +func TestRuleUpdateSetAnd(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := [][]string{ + { + "update tbl set col = 1 and cl = 2 where col=3;", + }, + { + "update tbl set col = 1 ,cl = 2 where col=3;", + }, + } + for _, sql := range sqls[0] { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleUpdateSetAnd() + if rule.Item != "RES.005" { + t.Error("Rule not match:", rule.Item, "Expect : RES.005") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + + for _, sql := range sqls[1] { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleUpdateSetAnd() + if rule.Item != "OK" { + t.Error("Rule not match:", rule.Item, "Expect : OK") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// RES.006 +func TestRuleImpossibleWhere(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := [][]string{ + { + "select * from tbl where 1 != 1;", + "select * from tbl where 'a' != 'a';", + "select * from tbl where col between 10 AND 5;", + }, + { + "select * from tbl where 1 = 1;", + "select * from tbl where 'a' != 1;", + }, + } + for _, sql := range sqls[0] { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleImpossibleWhere() + if rule.Item != "RES.006" { + t.Error("Rule not match:", rule.Item, "Expect : RES.006, SQL: ", sql) + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + + for _, sql := range sqls[1] { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleImpossibleWhere() + if rule.Item != "OK" { + t.Error("Rule not match:", rule.Item, "Expect : OK, SQL: ", sql) + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// RES.007 +func TestRuleMeaninglessWhere(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := [][]string{ + { + "select * from tbl where 1 = 1;", + "select * from tbl where 'a' = 'a';", + "select * from tbl where 'a' != 1;", + }, + { + "select * from tbl where 2 = 1;", + "select * from tbl where 'b' = 'a';", + }, + } + for _, sql := range sqls[0] { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleMeaninglessWhere() + if rule.Item != "RES.007" { + t.Error("Rule not match:", rule.Item, "Expect : RES.007, SQL: ", sql) + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + + for _, sql := range sqls[1] { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleMeaninglessWhere() + if rule.Item != "OK" { + t.Error("Rule not match:", rule.Item, "Expect : OK, SQL: ", sql) + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// RES.008 +func TestRuleLoadFile(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := [][]string{ + { + "LOAD DATA INFILE 'data.txt' INTO TABLE db2.my_table;", + "LOAD DATA INFILE 'data.txt' INTO TABLE db2.my_table;", + "LOAD /*COMMENT*/DATA INFILE 'data.txt' INTO TABLE db2.my_table;", + `SELECT a,b,a+b INTO OUTFILE '/tmp/result.txt' FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"' LINES TERMINATED BY '\n' FROM test_table;`, + }, + { + "SELECT id, data INTO @x, @y FROM test.t1 LIMIT 1;", + }, + } + for _, sql := range sqls[0] { + q := &Query4Audit{Query: sql} + rule := q.RuleLoadFile() + if rule.Item != "RES.008" { + t.Error("Rule not match:", rule.Item, "Expect : RES.008, SQL: ", sql) + } + } + + for _, sql := range sqls[1] { + q := &Query4Audit{Query: sql} + rule := q.RuleLoadFile() + if rule.Item != "OK" { + t.Error("Rule not match:", rule.Item, "Expect : OK, SQL: ", sql) + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// STA.001 +func TestRuleStandardINEQ(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := []string{ + "select col1,col2 from tbl where type!=0", + // "select col1,col2 from tbl where type<>0", + } + for _, sql := range sqls { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleStandardINEQ() + if rule.Item != "STA.001" { + t.Error("Rule not match:", rule.Item, "Expect : STA.001") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// KWR.002 +func TestRuleUseKeyWord(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := [][]string{ + { + "CREATE TABLE tbl (`select` int)", + "CREATE TABLE `select` (a int)", + "ALTER TABLE tbl ADD COLUMN `select` varchar(10)", + }, + { + "CREATE TABLE tbl (a int)", + "ALTER TABLE tbl ADD COLUMN col varchar(10)", + }, + } + for _, sql := range sqls[0] { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleUseKeyWord() + if rule.Item != "KWR.002" { + t.Error("Rule not match:", rule.Item, "Expect : KWR.002") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + for _, sql := range sqls[1] { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleUseKeyWord() + if rule.Item != "OK" { + t.Error("Rule not match:", rule.Item, "Expect : OK") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// KWR.003 +func TestRulePluralWord(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := [][]string{ + { + "CREATE TABLE tbl (`people` int)", + "CREATE TABLE people (a int)", + "ALTER TABLE tbl ADD COLUMN people varchar(10)", + }, + { + "CREATE TABLE tbl (`person` int)", + "ALTER TABLE tbl ADD COLUMN person varchar(10)", + }, + } + for _, sql := range sqls[0] { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RulePluralWord() + if rule.Item != "KWR.003" { + t.Error("Rule not match:", rule.Item, "Expect : KWR.003") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + for _, sql := range sqls[1] { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RulePluralWord() + if rule.Item != "OK" { + t.Error("Rule not match:", rule.Item, "Expect : OK") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// LCK.001 +func TestRuleInsertSelect(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := []string{ + `INSERT INTO tbl SELECT * FROM tbl2;`, + } + for _, sql := range sqls { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleInsertSelect() + if rule.Item != "LCK.001" { + t.Error("Rule not match:", rule.Item, "Expect : LCK.001") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// LCK.002 +func TestRuleInsertOnDup(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := []string{ + `INSERT INTO t1(a,b,c) VALUES (1,2,3) ON DUPLICATE KEY UPDATE c=c+1;`, + } + for _, sql := range sqls { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleInsertOnDup() + if rule.Item != "LCK.002" { + t.Error("Rule not match:", rule.Item, "Expect : LCK.002") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// SUB.001 +func TestRuleInSubquery(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := []string{ + "select col1,col2,col3 from table1 where col2 in(select col from table2)", + "SELECT col1,col2,col3 from table1 where col2 =(SELECT col2 FROM `table1` limit 1)", + } + for _, sql := range sqls { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleInSubquery() + if rule.Item != "SUB.001" { + t.Error("Rule not match:", rule.Item, "Expect : SUB.001") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// LIT.003 +func TestRuleMultiValueAttribute(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := []string{ + "select c1,c2,c3,c4 from tab1 where col_id REGEXP '[[:<:]]12[[:>:]]'", + } + for _, sql := range sqls { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleMultiValueAttribute() + if rule.Item != "LIT.003" { + t.Error("Rule not match:", rule.Item, "Expect : LIT.003") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// LIT.003 +func TestRuleAddDelimiter(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := [][]string{ + { + `use sakila + select * from film`, + `use sakila`, + `show databases`, + }, + { + `use sakila;`, + }, + } + for _, sql := range sqls[0] { + q, _ := NewQuery4Audit(sql) + + rule := q.RuleAddDelimiter() + if rule.Item != "LIT.004" { + t.Error("Rule not match:", rule.Item, "Expect : LIT.004") + } + } + for _, sql := range sqls[1] { + q, _ := NewQuery4Audit(sql) + + rule := q.RuleAddDelimiter() + if rule.Item != "OK" { + t.Error("Rule not match:", rule.Item, "Expect : OK") + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// KEY.003 +func TestRuleRecursiveDependency(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := [][]string{ + { + `CREATE TABLE tab2 ( + p_id BIGINT UNSIGNED NOT NULL, + a_id BIGINT UNSIGNED NOT NULL, + PRIMARY KEY (p_id, a_id), + FOREIGN KEY (p_id) REFERENCES tab1(p_id), + FOREIGN KEY (a_id) REFERENCES tab3(a_id) + );`, + `ALTER TABLE tbl2 add FOREIGN KEY (p_id) REFERENCES tab1(p_id);`, + }, + { + `ALTER TABLE tbl2 ADD KEY (p_id) p_id;`, + }, + } + for _, sql := range sqls[0] { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleRecursiveDependency() + if rule.Item != "KEY.003" { + t.Error("Rule not match:", rule.Item, "Expect : KEY.003") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + for _, sql := range sqls[1] { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleRecursiveDependency() + if rule.Item != "OK" { + t.Error("Rule not match:", rule.Item, "Expect : OK") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// COL.009 +func TestRuleImpreciseDataType(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := [][]string{ + { + `CREATE TABLE tab2 ( + p_id BIGINT UNSIGNED NOT NULL, + a_id BIGINT UNSIGNED NOT NULL, + hours float NOT null, + PRIMARY KEY (p_id, a_id) + );`, + `alter table tbl add column c float not null;`, + `insert into tb (col) values (0.00001);`, + `select * from tb where col = 0.00001;`, + }, + { + "REPLACE INTO `binks3` (`hostname`,`storagehost`, `filename`, `starttime`, `binlogstarttime`, `uploadname`, `binlogsize`, `filesize`, `md5`, `status`) VALUES (1, 1, 1, 1, 1, 1, ?, ?);", + }, + } + for _, sql := range sqls[0] { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleImpreciseDataType() + if rule.Item != "COL.009" { + t.Error("Rule not match:", rule.Item, "Expect : COL.009") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + + for _, sql := range sqls[1] { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleImpreciseDataType() + if rule.Item != "OK" { + t.Error("Rule not match:", rule.Item, "Expect : OK") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// COL.010 +func TestRuleValuesInDefinition(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := []string{ + `create table tab1(status ENUM('new', 'in progress', 'fixed'))`, + `alter table tab1 add column status ENUM('new', 'in progress', 'fixed')`, + } + + for _, sql := range sqls { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleValuesInDefinition() + if rule.Item != "COL.010" { + t.Error("Rule not match:", rule.Item, "Expect : COL.010") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// KEY.004 +func TestRuleIndexAttributeOrder(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := []string{ + `create index idx1 on tabl(last_name,first_name);`, + `alter table tabl add index idx1 (last_name,first_name);`, + `CREATE TABLE test (id int,blob_col BLOB, INDEX(blob_col(10),id));`, + } + for _, sql := range sqls { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleIndexAttributeOrder() + if rule.Item != "KEY.004" { + t.Error("Rule not match:", rule.Item, "Expect : KEY.004") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// COL.011 +func TestRuleNullUsage(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := []string{ + `select c1,c2,c3 from tabl where c4 is null or c4 <> 1;`, + } + for _, sql := range sqls { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleNullUsage() + if rule.Item != "COL.011" { + t.Error("Rule not match:", rule.Item, "Expect : COL.011") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// FUN.003 +func TestRuleStringConcatenation(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := []string{ + `select c1 || coalesce(' ' || c2 || ' ', ' ') || c3 as c from tabl;`, + } + for _, sql := range sqls { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleStringConcatenation() + if rule.Item != "FUN.003" { + t.Error("Rule not match:", rule.Item, "Expect : FUN.003") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// FUN.004 +func TestRuleSysdate(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := []string{ + `select sysdate();`, + } + for _, sql := range sqls { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleSysdate() + if rule.Item != "FUN.004" { + t.Error("Rule not match:", rule.Item, "Expect : FUN.004") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// FUN.005 +func TestRuleCountConst(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := [][]string{ + { + `select count(1) from tbl;`, + `select count(col) from tbl;`, + }, + { + `select count(*) from tbl`, + `select count(DISTINCT col) from tbl`, + }, + } + for _, sql := range sqls[0] { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleCountConst() + if rule.Item != "FUN.005" { + t.Error("Rule not match:", rule.Item, "Expect : FUN.005") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + + for _, sql := range sqls[1] { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleCountConst() + if rule.Item != "OK" { + t.Error("Rule not match:", rule.Item, "Expect : OK") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// FUN.006 +func TestRuleSumNPE(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := [][]string{ + { + `select sum(1) from tbl;`, + `select sum(col) from tbl;`, + }, + { + `SELECT IF(ISNULL(SUM(COL)), 0, SUM(COL)) FROM tbl`, + }, + } + for _, sql := range sqls[0] { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleSumNPE() + if rule.Item != "FUN.006" { + t.Error("Rule not match:", rule.Item, "Expect : FUN.006") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + + for _, sql := range sqls[1] { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleSumNPE() + if rule.Item != "OK" { + t.Error("Rule not match:", rule.Item, "Expect : OK") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// ARG.007 +func TestRulePatternMatchingUsage(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := []string{ + `select c1,c2,c3,c4 from tab1 where col_id REGEXP '[[:<:]]12[[:>:]]';`, + } + for _, sql := range sqls { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RulePatternMatchingUsage() + if rule.Item != "ARG.007" { + t.Error("Rule not match:", rule.Item, "Expect : ARG.007") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// CLA.012 +func TestRuleSpaghettiQueryAlert(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := []string{ + `select 1`, + } + for _, sql := range sqls { + q, err := NewQuery4Audit(sql) + if err == nil { + common.Config.SpaghettiQueryLength = 1 + rule := q.RuleSpaghettiQueryAlert() + if rule.Item != "CLA.012" { + t.Error("Rule not match:", rule.Item, "Expect : CLA.012") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// JOI.005 +func TestRuleReduceNumberOfJoin(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := []string{ + `select bp1.p_id, b1.d_d as l, b1.b_id from b1 join bp1 on (b1.b_id = bp1.b_id) left outer join (b1 as b2 join bp2 on (b2.b_id = bp2.b_id)) on (bp1.p_id = bp2.p_id ) join bp21 on (b1.b_id = bp1.b_id) join bp31 on (b1.b_id = bp1.b_id) join bp41 on (b1.b_id = bp1.b_id) where b2.b_id = 0; `, + } + for _, sql := range sqls { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleReduceNumberOfJoin() + if rule.Item != "JOI.005" { + t.Error("Rule not match:", rule.Item, "Expect : JOI.005") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// DIS.001 +func TestRuleDistinctUsage(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := []string{ + `SELECT DISTINCT c.c_id,count(DISTINCT c.c_name),count(DISTINCT c.c_e),count(DISTINCT c.c_n),count(DISTINCT c.c_me),c.c_d FROM (select distinct xing, name from B) as e WHERE e.country_id = c.country_id;`, + } + for _, sql := range sqls { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleDistinctUsage() + if rule.Item != "DIS.001" { + t.Error("Rule not match:", rule.Item, "Expect : DIS.001") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// DIS.002 +func TestRuleCountDistinctMultiCol(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := [][]string{ + { + "SELECT COUNT(DISTINCT col, col2) FROM tbl;", + }, + { + "SELECT COUNT(DISTINCT col) FROM tbl;", + `SELECT JSON_OBJECT( "key", p.id, "title", p.name, "manufacturer", p.manufacturer, "price", p.price, "specifications", JSON_OBJECTAGG(a.name, v.value)) as product FROM product as p JOIN value as v ON p.id = v.prod_id JOIN attribute as a ON a.id = v.attribute_id GROUP BY v.prod_id`, + }, + } + for _, sql := range sqls[0] { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleCountDistinctMultiCol() + if rule.Item != "DIS.002" { + t.Error("Rule not match:", rule.Item, "Expect : DIS.002") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + + for _, sql := range sqls[1] { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleCountDistinctMultiCol() + if rule.Item != "OK" { + t.Error("Rule not match:", rule.Item, "Expect : OK") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// DIS.003 +// RuleDistinctStar +func TestRuleDistinctStar(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := [][]string{ + { + "SELECT DISTINCT * FROM film;", + "SELECT DISTINCT film.* FROM film;", + }, + { + "SELECT DISTINCT col FROM film;", + "SELECT DISTINCT film.* FROM film, tbl;", + "SELECT DISTINCT * FROM film, tbl;", + }, + } + for _, sql := range sqls[0] { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleDistinctStar() + if rule.Item != "DIS.003" { + t.Error("Rule not match:", rule.Item, "Expect : DIS.003") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + + for _, sql := range sqls[1] { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleDistinctStar() + if rule.Item != "OK" { + t.Error("Rule not match:", rule.Item, "Expect : OK") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// CLA.013 +func TestRuleHavingClause(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := []string{ + `SELECT s.c_id,count(s.c_id) FROM s where c = test GROUP BY s.c_id HAVING s.c_id <> '1660' AND s.c_id <> '2' order by s.c_id;`, + } + for _, sql := range sqls { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleHavingClause() + if rule.Item != "CLA.013" { + t.Error("Rule not match:", rule.Item, "Expect : CLA.013") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// CLA.017 +func TestRuleForbiddenSyntax(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := []string{ + `create view v_today (today) AS SELECT CURRENT_DATE;`, + `CREATE VIEW v (mycol) AS SELECT 'abc';`, + `CREATE FUNCTION hello (s CHAR(20));`, + `CREATE PROCEDURE simpleproc (OUT param1 INT)`, + } + for _, sql := range sqls { + q, _ := NewQuery4Audit(sql) + rule := q.RuleForbiddenSyntax() + if rule.Item != "CLA.017" { + t.Error("Rule not match:", rule.Item, "Expect : CLA.017") + } + + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// JOI.006 +func TestRuleNestedSubQueries(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := []string{ + `SELECT s,p,d FROM tabl WHERE p.p_id = (SELECT s.p_id FROM tabl WHERE s.c_id = 100996 AND s.q = 1 );`, + } + for _, sql := range sqls { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleNestedSubQueries() + if rule.Item != "JOI.006" { + t.Error("Rule not match:", rule.Item, "Expect : JOI.006") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// JOI.007 +func TestRuleMultiDeleteUpdate(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := []string{ + `DELETE u FROM users u LEFT JOIN hobby tna ON u.id = tna.uid WHERE tna.hobby = 'piano'; `, + `UPDATE users u LEFT JOIN hobby h ON u.id = h.uid SET u.name = 'pianoboy' WHERE h.hobby = 'piano';`, + } + for _, sql := range sqls { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleMultiDeleteUpdate() + if rule.Item != "JOI.007" { + t.Error("Rule not match:", rule.Item, "Expect : JOI.007") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// JOI.008 +func TestRuleMultiDBJoin(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := []string{ + `SELECT s,p,d FROM db1.tb1 join db2.tb2 on db1.tb1.a = db2.tb2.a where db1.tb1.a > 10;`, + `SELECT s,p,d FROM db1.tb1 join tb2 on db1.tb1.a = tb2.a where db1.tb1.a > 10;`, + // `SELECT s,p,d FROM db1.tb1 join db1.tb2 on db1.tb1.a = db1.tb2.a where db1.tb1.a > 10;`, + } + for _, sql := range sqls { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleMultiDBJoin() + if rule.Item != "JOI.008" { + t.Error("Rule not match:", rule.Item, "Expect : JOI.008") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// ARG.008 +func TestRuleORUsage(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := []string{ + `SELECT c1,c2,c3 FROM tabl WHERE c1 = 14 OR c2 = 17;`, + } + for _, sql := range sqls { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleORUsage() + if rule.Item != "ARG.008" { + t.Error("Rule not match:", rule.Item, "Expect : ARG.008") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// ARG.009 +func TestRuleSpaceWithQuote(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := [][]string{ + { + `SELECT 'a ';`, + `SELECT ' a';`, + `SELECT "a ";`, + `SELECT " a";`, + }, + { + `select ''`, + `select 'a'`, + }, + } + for _, sql := range sqls[0] { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleSpaceWithQuote() + if rule.Item != "ARG.009" { + t.Error("Rule not match:", rule.Item, "Expect : ARG.009") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + for _, sql := range sqls[1] { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleSpaceWithQuote() + if rule.Item != "OK" { + t.Error("Rule not match:", rule.Item, "Expect : OK") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// ARG.010 +func TestRuleHint(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := [][]string{ + { + `SELECT * FROM t1 USE INDEX (i1) ORDER BY a;`, + `SELECT * FROM t1 IGNORE INDEX (i1) ORDER BY (i2);`, + // vitess syntax not support now + // `SELECT * FROM t1 USE INDEX (i1,i2) IGNORE INDEX (i2);`, + // `SELECT * FROM t1 USE INDEX (i1) IGNORE INDEX (i2) USE INDEX (i2);`, + }, + { + `select ''`, + `select 'a'`, + }, + } + for _, sql := range sqls[0] { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleHint() + if rule.Item != "ARG.010" { + t.Error("Rule not match:", rule.Item, "Expect : ARG.010") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + for _, sql := range sqls[1] { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleHint() + if rule.Item != "OK" { + t.Error("Rule not match:", rule.Item, "Expect : OK") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// ARG.011 +func TestNot(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := [][]string{ + { + `select id from t where num not in(1,2,3);`, + `select id from t where num not like "a%"`, + }, + { + `select id from t where num in(1,2,3);`, + `select id from t where num like "a%"`, + }, + } + for _, sql := range sqls[0] { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleNot() + if rule.Item != "ARG.011" { + t.Error("Rule not match:", rule.Item, "Expect : ARG.011") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + for _, sql := range sqls[1] { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleNot() + if rule.Item != "OK" { + t.Error("Rule not match:", rule.Item, "Expect : OK") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// SUB.002 +func TestRuleUNIONUsage(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := []string{ + `select teacher_id as id,people_name as name from t1,t2 where t1.teacher_id=t2.people_id union select student_id as id,people_name as name from t1,t2 where t1.student_id=t2.people_id;`, + } + for _, sql := range sqls { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleUNIONUsage() + if rule.Item != "SUB.002" { + t.Error("Rule not match:", rule.Item, "Expect : SUB.002") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// SUB.003 +func TestRuleDistinctJoinUsage(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := []string{ + `SELECT DISTINCT c.c_id, c.c_name FROM c,e WHERE e.c_id = c.c_id;`, + } + for _, sql := range sqls { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleDistinctJoinUsage() + if rule.Item != "SUB.003" { + t.Error("Rule not match:", rule.Item, "Expect : SUB.003") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// SUB.005 +func TestRuleSubQueryLimit(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := [][]string{ + { + `SELECT * FROM staff WHERE name IN (SELECT NAME FROM customer ORDER BY name LIMIT 1)`, + }, + { + `select * from (select id from tbl limit 3) as foo`, + `select * from tbl where id in (select t.id from (select * from tbl limit 3)as t)`, + }, + } + for _, sql := range sqls[0] { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleSubQueryLimit() + if rule.Item != "SUB.005" { + t.Error("Rule not match:", rule.Item, "Expect : SUB.005") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + for _, sql := range sqls[1] { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleSubQueryLimit() + if rule.Item != "OK" { + t.Error("Rule not match:", rule.Item, "Expect : OK") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// SUB.006 +func TestRuleSubQueryFunctions(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := [][]string{ + { + `SELECT * FROM staff WHERE name IN (SELECT max(NAME) FROM customer)`, + }, + { + `select * from (select id from tbl limit 3) as foo`, + `select * from tbl where id in (select t.id from (select * from tbl limit 3)as t)`, + }, + } + for _, sql := range sqls[0] { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleSubQueryFunctions() + if rule.Item != "SUB.006" { + t.Error("Rule not match:", rule.Item, "Expect : SUB.006") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + for _, sql := range sqls[1] { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleSubQueryFunctions() + if rule.Item != "OK" { + t.Error("Rule not match:", rule.Item, "Expect : OK") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// SEC.002 +func TestRuleReadablePasswords(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := []string{ + `create table test(id int,name varchar(20) not null,password varchar(200)not null);`, + `alter table test add column password varchar(200) not null;`, + } + for _, sql := range sqls { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleReadablePasswords() + if rule.Item != "SEC.002" { + t.Error("Rule not match:", rule.Item, "Expect : SEC.002") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// SEC.003 +func TestRuleDataDrop(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := []string{ + `delete from tb where a = b;`, + `truncate table tb;`, + `drop table tb;`, + `drop database db;`, + } + for _, sql := range sqls { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleDataDrop() + if rule.Item != "SEC.003" { + t.Error("Rule not match:", rule.Item, "Expect : SEC.003") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// FUN.001 +func TestCompareWithFunction(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := [][]string{ + {`select id from t where substring(name,1,3)='abc';`}, + // TODO: 右侧使用函数比较 + {`select id from t where 'abc'=substring(name,1,3);`}, + } + for _, sql := range sqls[0] { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleCompareWithFunction() + if rule.Item != "FUN.001" { + t.Error("Rule not match:", rule.Item, "Expect : FUN.001") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + for _, sql := range sqls[1] { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleCompareWithFunction() + if rule.Item != "OK" { + t.Error("Rule not match:", rule.Item, "Expect : OK") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// FUN.002 +func TestRuleCountStar(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := []string{ + `SELECT c3, COUNT(*) AS accounts FROM tab where c2 < 10000 GROUP BY c3 ORDER BY num;`, + } + for _, sql := range sqls { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleCountStar() + if rule.Item != "FUN.002" { + t.Error("Rule not match:", rule.Item, "Expect : FUN.002") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// SEC.001 +func TestRuleTruncateTable(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := []string{ + `TRUNCATE TABLE tbl_name;`, + } + for _, sql := range sqls { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleTruncateTable() + if rule.Item != "SEC.001" { + t.Error("Rule not match:", rule.Item, "Expect : SEC.001") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// ARG.005 +func TestRuleIn(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := []string{ + `select id from t where num in(1,2,3);`, + `SELECT * FROM tbl WHERE col IN (NULL)`, + `SELECT * FROM tbl WHERE col NOT IN (NULL)`, + } + common.Config.MaxInCount = 0 + for _, sql := range sqls { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleIn() + if rule.Item != "ARG.005" && rule.Item != "ARG.004" { + t.Error("Rule not match:", rule.Item, "Expect : ARG.005 OR ARG.004") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// ARG.006 +func TestRuleisNullIsNotNull(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := []string{ + `select id from t where num is null;`, + `select id from t where num is not null;`, + } + for _, sql := range sqls { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleIsNullIsNotNull() + if rule.Item != "ARG.006" { + t.Error("Rule not match:", rule.Item, "Expect : ARG.006") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// COL.008 +func TestRuleVarcharVSChar(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := []string{ + `create table t1(id int,name char(20),last_time date);`, + `create table t1(id int,name binary(20),last_time date);`, + `alter table t1 add column id int, add column name binary(20), add column last_time date;`, + } + for _, sql := range sqls { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleVarcharVSChar() + if rule.Item != "COL.008" { + t.Error("Rule not match:", rule.Item, "Expect : COL.008") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// TBL.003 +func TestRuleCreateDualTable(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := []string{ + `create table dual(id int, primary key (id));`, + } + for _, sql := range sqls { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleCreateDualTable() + if rule.Item != "TBL.003" { + t.Error("Rule not match:", rule.Item, "Expect : TBL.003") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// ALT.001 +func TestRuleAlterCharset(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := [][]string{ + { + `alter table tbl default character set 'utf8';`, + `alter table tbl default character set='utf8';`, + `ALTER TABLE t1 CHANGE a b BIGINT NOT NULL, default character set utf8`, + `ALTER TABLE t1 CHANGE a b BIGINT NOT NULL,default character set utf8`, + `ALTER TABLE tbl_name CHARACTER SET charset_name;`, + `ALTER TABLE t1 CHANGE a b BIGINT NOT NULL, character set utf8`, + `ALTER TABLE t1 CHANGE a b BIGINT NOT NULL,character set utf8`, + `alter table t1 convert to character set utf8 collate utf8_unicode_ci;`, + `alter table t1 default collate = utf8_unicode_ci;`, + }, + { + // 反面的例子 + `ALTER TABLE t MODIFY latin1_text_col TEXT CHARACTER SET utf8`, + `ALTER TABLE t1 CHANGE c1 c1 TEXT CHARACTER SET utf8;`, + }, + } + for _, sql := range sqls[0] { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleAlterCharset() + if rule.Item != "ALT.001" { + t.Error(sql, " Rule not match:", rule.Item, "Expect : ALT.001") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + for _, sql := range sqls[1] { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleAlterCharset() + if rule.Item != "OK" { + t.Error(sql, " Rule not match:", rule.Item, "Expect : OK") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// ALT.003 +func TestRuleAlterDropColumn(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := [][]string{ + { + `alter table film drop column title;`, + }, + { + // 反面的例子 + `ALTER TABLE t1 CHANGE c1 c1 TEXT CHARACTER SET utf8;`, + }, + } + for _, sql := range sqls[0] { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleAlterDropColumn() + if rule.Item != "ALT.003" { + t.Error(sql, " Rule not match:", rule.Item, "Expect : ALT.003") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + for _, sql := range sqls[1] { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleAlterDropColumn() + if rule.Item != "OK" { + t.Error(sql, " Rule not match:", rule.Item, "Expect : OK") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// ALT.004 +func TestRuleAlterDropKey(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := [][]string{ + { + `alter table film drop primary key`, + `alter table film drop foreign key fk_film_language`, + }, + { + // 反面的例子 + `ALTER TABLE t1 CHANGE c1 c1 TEXT CHARACTER SET utf8;`, + }, + } + for _, sql := range sqls[0] { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleAlterDropKey() + if rule.Item != "ALT.004" { + t.Error(sql, " Rule not match:", rule.Item, "Expect : ALT.004") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + for _, sql := range sqls[1] { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleAlterDropKey() + if rule.Item != "OK" { + t.Error(sql, " Rule not match:", rule.Item, "Expect : OK") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// COL.012 +func TestRuleCantBeNull(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := []string{ + "CREATE TABLE `sbtest` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `c` longblob, PRIMARY KEY (`id`));", + "alter TABLE `sbtest` add column `c` longblob;", + "alter TABLE `sbtest` add column `c` text;", + "alter TABLE `sbtest` add column `c` blob;", + } + for _, sql := range sqls { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleCantBeNull() + if rule.Item != "COL.012" { + t.Error("Rule not match:", rule.Item, "Expect : COL.012") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// KEY.006 +func TestRuleTooManyKeyParts(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := []string{ + "CREATE TABLE `sbtest` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `c` longblob NOT NULL DEFAULT '', PRIMARY KEY (`id`));", + "alter TABLE `sbtest` add index idx_idx (`id`);", + } + for _, sql := range sqls { + q, err := NewQuery4Audit(sql) + if err == nil { + common.Config.MaxIdxColsCount = 0 + rule := q.RuleTooManyKeyParts() + if rule.Item != "KEY.006" { + t.Error("Rule not match:", rule.Item, "Expect : KEY.006") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// KEY.005 +func TestRuleTooManyKeys(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := []string{ + "create table tbl ( a char(10), b int, primary key (`a`)) engine=InnoDB;", + "create table tbl ( a varchar(64) not null, b int, PRIMARY KEY (`a`), key `idx_a_b` (`a`,`b`)) engine=InnoDB", + } + for _, sql := range sqls { + q, err := NewQuery4Audit(sql) + if err == nil { + common.Config.MaxIdxCount = 0 + rule := q.RuleTooManyKeys() + if rule.Item != "KEY.005" { + t.Error("Rule not match:", rule.Item, "Expect : KEY.005") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// KEY.007 +func TestRulePKNotInt(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := [][]string{ + { + "create table tbl ( a char(10), b int, primary key (`a`)) engine=InnoDB;", + "create table tbl ( a int, b int, primary key (`a`)) engine=InnoDB;", + "create table tbl ( a bigint, b int, primary key (`a`)) engine=InnoDB;", + "create table tbl ( a int unsigned, b int, primary key (`a`)) engine=InnoDB;", + "create table tbl ( a bigint unsigned, b int, primary key (`a`)) engine=InnoDB;", + }, + { + "CREATE TABLE tbl (a int unsigned auto_increment, b int, primary key(`a`)) engine=InnoDB;", + }, + } + for _, sql := range sqls[0] { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RulePKNotInt() + if rule.Item != "KEY.007" && rule.Item != "KEY.001" { + t.Error("Rule not match:", rule.Item, "Expect : KEY.007 OR KEY.001") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + + for _, sql := range sqls[1] { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RulePKNotInt() + if rule.Item != "OK" { + t.Error("Rule not match:", rule.Item, "Expect : OK") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// KEY.008 +func TestRuleOrderByMultiDirection(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := [][]string{ + { + `SELECT col FROM tbl order by col desc, col2 asc`, + }, + { + `SELECT col FROM tbl order by col, col2`, + }, + } + for _, sql := range sqls[0] { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleOrderByMultiDirection() + if rule.Item != "KEY.008" { + t.Error("Rule not match:", rule.Item, "Expect : KEY.008") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + + for _, sql := range sqls[1] { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleOrderByMultiDirection() + if rule.Item != "OK" { + t.Error("Rule not match:", rule.Item, "Expect : OK") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// KEY.009 +func TestRuleUniqueKeyDup(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := [][]string{ + { + `ALTER TABLE customer ADD UNIQUE INDEX part_of_name (name(10));`, + `CREATE UNIQUE INDEX part_of_name ON customer (name(10));`, + }, + { + `ALTER TABLE tbl add INDEX idx_col (col);`, + `CREATE INDEX part_of_name ON customer (name(10));`, + }, + } + for _, sql := range sqls[0] { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleUniqueKeyDup() + if rule.Item != "KEY.009" { + t.Error("Rule not match:", rule.Item, "Expect : KEY.009") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + + for _, sql := range sqls[1] { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleUniqueKeyDup() + if rule.Item != "OK" { + t.Error("Rule not match:", rule.Item, "Expect : OK") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// COL.013 +func TestRuleTimestampDefault(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := [][]string{ + { + "CREATE TABLE tbl( `id` bigint not null, `create_time` timestamp) ENGINE=InnoDB DEFAULT CHARSET=utf8;", + "ALTER TABLE t1 MODIFY b timestamp NOT NULL;", + }, + { + "CREATE TABLE tbl (`id` bigint not null, `update_time` timestamp default current_timestamp)", + "ALTER TABLE t1 MODIFY b timestamp NOT NULL default current_timestamp;", + }, + } + for _, sql := range sqls[0] { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleTimestampDefault() + if rule.Item != "COL.013" { + t.Error("Rule not match:", rule.Item, "Expect : COL.013") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + + for _, sql := range sqls[1] { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleTimestampDefault() + if rule.Item != "OK" { + t.Error("Rule not match:", rule.Item, "Expect : OK") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// TBL.004 +func TestRuleAutoIncrementInitNotZero(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := [][]string{ + // 正面的例子 + { + "CREATE TABLE `sbtest` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `pad` char(60) NOT NULL DEFAULT '', PRIMARY KEY (`id`)) ENGINE=InnoDB AUTO_INCREMENT=13", + }, + // 反面的例子 + { + "CREATE TABLE `test1` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `pad` char(60) NOT NULL DEFAULT '', PRIMARY KEY (`id`))", + "CREATE TABLE `test1` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `pad` char(60) NOT NULL DEFAULT '', PRIMARY KEY (`id`)) auto_increment = 1", + "CREATE TABLE `test1` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `pad` char(60) NOT NULL DEFAULT '', PRIMARY KEY (`id`)) auto_increment = 1 DEFAULT CHARSET=latin1", + }, + } + for _, sql := range sqls[0] { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleAutoIncrementInitNotZero() + if rule.Item != "TBL.004" { + t.Error("Rule not match:", rule.Item, "Expect : TBL.004") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + for _, sql := range sqls[1] { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleAutoIncrementInitNotZero() + if rule.Item != "OK" { + t.Error("Rule not match:", rule.Item, "Expect : OK") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// COL.014 +func TestRuleColumnWithCharset(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := [][]string{ + // 正面的例子 + { + "CREATE TABLE `tb2` ( `id` int(11) DEFAULT NULL, `col` char(10) CHARACTER SET utf8 DEFAULT NULL)", + "alter table tb2 change col col char(10) CHARACTER SET utf8 DEFAULT NULL;", + }, + // 反面的例子 + { + "CREATE TABLE `sbtest` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `c` char(120) NOT NULL DEFAULT '', PRIMARY KEY (`id`))", + }, + } + for _, sql := range sqls[0] { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleColumnWithCharset() + if rule.Item != "COL.014" { + t.Error("Rule not match:", rule.Item, "Expect : COL.014") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + for _, sql := range sqls[1] { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleColumnWithCharset() + if rule.Item != "OK" { + t.Error("Rule not match:", rule.Item, "Expect : OK") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// TBL.005 +func TestRuleTableCharsetCheck(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := [][]string{ + { + "create table tbl (a int) DEFAULT CHARSET=latin1;", + "ALTER TABLE tbl CONVERT TO CHARACTER SET latin1;", + }, + { + "create table tlb (a int);", + "ALTER TABLE `tbl` add column a int, add column b int ;", + }, + } + for _, sql := range sqls[0] { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleTableCharsetCheck() + if rule.Item != "TBL.005" { + t.Error("Rule not match:", rule.Item, "Expect : TBL.005") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + for _, sql := range sqls[1] { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleTableCharsetCheck() + if rule.Item != "OK" { + t.Error("Rule not match:", rule.Item, "Expect : OK") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// COL.015 +func TestRuleBlobDefaultValue(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := [][]string{ + { + "CREATE TABLE `sbtest` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `c` blob NOT NULL DEFAULT '', PRIMARY KEY (`id`));", + "alter table `sbtest` add column `c` blob NOT NULL DEFAULT '';", + }, + { + "CREATE TABLE `sbtest` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `c` blob NOT NULL, PRIMARY KEY (`id`));", + "alter table `sbtest` add column `c` blob NOT NULL DEFAULT NULL;", + }, + } + + for _, sql := range sqls[0] { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleBlobDefaultValue() + if rule.Item != "COL.015" { + t.Error("Rule not match:", rule.Item, "Expect : COL.015") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + + for _, sql := range sqls[1] { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleBlobDefaultValue() + if rule.Item != "OK" { + t.Error("Rule not match:", rule.Item, "Expect : OK") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// COL.016 +func TestRuleIntPrecision(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := [][]string{ + { + "CREATE TABLE `sbtest` ( `id` int(1) );", + "CREATE TABLE `sbtest` ( `id` bigint(1) );", + "alter TABLE `sbtest` add column `id` bigint(1);", + "alter TABLE `sbtest` add column `id` int(1);", + }, + { + "CREATE TABLE `sbtest` ( `id` int(10));", + "CREATE TABLE `sbtest` ( `id` bigint(20));", + "alter TABLE `sbtest` add column `id` bigint(20);", + "alter TABLE `sbtest` add column `id` int(10);", + "CREATE TABLE `sbtest` ( `id` int);", + "alter TABLE `sbtest` add column `id` bigint;", + }, + } + + for _, sql := range sqls[0] { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleIntPrecision() + if rule.Item != "COL.016" { + t.Error("Rule not match:", rule.Item, "Expect : COL.016") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + + for _, sql := range sqls[1] { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleIntPrecision() + if rule.Item != "OK" { + t.Error("Rule not match:", rule.Item, "Expect : OK") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// COL.017 +func TestRuleVarcharLength(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := [][]string{ + { + "CREATE TABLE `sbtest` ( `id` varchar(4000) );", + "CREATE TABLE `sbtest` ( `id` varchar(3500) );", + "alter TABLE `sbtest` add column `id` varchar(3500);", + }, + { + "CREATE TABLE `sbtest` ( `id` varchar(1024));", + "CREATE TABLE `sbtest` ( `id` varchar(20));", + "alter TABLE `sbtest` add column `id` varchar(35);", + }, + } + + for _, sql := range sqls[0] { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleVarcharLength() + if rule.Item != "COL.017" { + t.Error("Rule not match:", rule.Item, "Expect : COL.017") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + + for _, sql := range sqls[1] { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleVarcharLength() + if rule.Item != "OK" { + t.Error("Rule not match:", rule.Item, "Expect : OK") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// KEY.002 +func TestRuleNoOSCKey(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := [][]string{ + // 正面的例子 + { + "CREATE TABLE tbl (a int, b int)", + }, + // 反面的例子 + { + "CREATE TABLE tbl (a int, primary key(`a`))", + "CREATE TABLE tbl (a int, unique key(`a`))", + }, + } + for _, sql := range sqls[0] { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleNoOSCKey() + if rule.Item != "KEY.002" { + t.Error("Rule not match:", rule.Item, "Expect : KEY.002") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + for _, sql := range sqls[1] { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleNoOSCKey() + if rule.Item != "OK" { + t.Error("Rule not match:", rule.Item, "Expect : OK") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// COL.006 +func TestRuleTooManyFields(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := []string{ + "create table tbl (a int);", + } + + common.Config.MaxColCount = 0 + for _, sql := range sqls { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleTooManyFields() + if rule.Item != "COL.006" { + t.Error("Rule not match:", rule.Item, "Expect : COL.006") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// TBL.002 +func TestRuleAllowEngine(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := [][]string{ + { + "CREATE TABLE tbl (a int) engine=myisam;", + "ALTER TABLE tbl engine=myisam;", + "CREATE TABLE tbl (a int);", + }, + { + "CREATE TABLE tbl (a int) engine = InnoDB;", + }, + } + for _, sql := range sqls[0] { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleAllowEngine() + if rule.Item != "TBL.002" { + t.Error("Rule not match:", rule.Item, "Expect : TBL.002") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + for _, sql := range sqls[1] { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleAllowEngine() + if rule.Item != "OK" { + t.Error("Rule not match:", rule.Item, "Expect : OK") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// TBL.001 +func TestRulePartitionNotAllowed(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := []string{ + `CREATE TABLE trb3 (id INT, name VARCHAR(50), purchased DATE) PARTITION BY RANGE( YEAR(purchased) ) + ( + PARTITION p0 VALUES LESS THAN (1990), + PARTITION p1 VALUES LESS THAN (1995), + PARTITION p2 VALUES LESS THAN (2000), + PARTITION p3 VALUES LESS THAN (2005) + );`, + `ALTER TABLE t1 ADD PARTITION (PARTITION p3 VALUES LESS THAN (2002));`, + } + for _, sql := range sqls { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RulePartitionNotAllowed() + if rule.Item != "TBL.001" { + t.Error("Rule not match:", rule.Item, "Expect : TBL.001") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// COL.003 +func TestRuleAutoIncUnsigned(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := []string{ + "CREATE TABLE `sbtest` ( `id` int(10) NOT NULL AUTO_INCREMENT, `c` longblob, PRIMARY KEY (`id`));", + "ALTER TABLE `tbl` ADD COLUMN `id` int(10) NOT NULL AUTO_INCREMENT;", + } + for _, sql := range sqls { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleAutoIncUnsigned() + if rule.Item != "COL.003" { + t.Error("Rule not match:", rule.Item, "Expect : COL.003") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// STA.003 +func TestRuleIdxPrefix(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := [][]string{ + { + "CREATE TABLE tbl (a int, unique key `xx_a` (`a`));", + "CREATE TABLE tbl (a int, key `xx_a` (`a`));", + `ALTER TABLE tbl ADD INDEX xx_a (a)`, + `ALTER TABLE tbl ADD UNIQUE INDEX xx_a (a)`, + }, + { + `ALTER TABLE tbl ADD INDEX idx_a (a)`, + `ALTER TABLE tbl ADD UNIQUE INDEX uk_a (a)`, + }, + } + for _, sql := range sqls[0] { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleIdxPrefix() + if rule.Item != "STA.003" { + t.Error("Rule not match:", rule.Item, "Expect : STA.003") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + for _, sql := range sqls[1] { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleIdxPrefix() + if rule.Item != "OK" { + t.Error("Rule not match:", rule.Item, "Expect : OK") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// STA.004 +func TestRuleStandardName(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := [][]string{ + { + "CREATE TABLE `tbl-name` (a int);", + "CREATE TABLE `tbl `(a int)", + "CREATE TABLE t__bl (a int);", + }, + { + "CREATE TABLE tbl (a int)", + "CREATE TABLE `tbl`(a int)", + "CREATE TABLE `tbl` (a int) ENGINE=InnoDB DEFAULT CHARSET=utf8", + }, + } + for _, sql := range sqls[0] { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleStandardName() + if rule.Item != "STA.004" { + t.Error("Rule not match:", rule.Item, "Expect : STA.004") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + + for _, sql := range sqls[1] { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleStandardName() + if rule.Item != "OK" { + t.Error("Rule not match:", rule.Item, "Expect : OK") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// STA.002 +func TestRuleSpaceAfterDot(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := [][]string{ + { + "SELECT * FROM sakila. film", + "SELECT film. length FROM film", + }, + { + "SELECT * FROM sakila.film", + "SELECT film.length FROM film", + "SELECT * FROM t1, t2 WHERE t1.title = t2.title", + }, + } + for _, sql := range sqls[0] { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleSpaceAfterDot() + if rule.Item != "STA.002" { + t.Error("Rule not match:", rule.Item, "Expect : STA.002") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + + for _, sql := range sqls[1] { + q, err := NewQuery4Audit(sql) + if err == nil { + rule := q.RuleSpaceAfterDot() + if rule.Item != "OK" { + t.Error("Rule not match:", rule.Item, "Expect : OK") + } + } else { + t.Error("sqlparser.Parse Error:", err) + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +func TestRuleMySQLError(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + err := errors.New(`Received #1146 error from MySQL server: "can't xxxx"`) + if RuleMySQLError("ERR.002", err).Content != "" { + t.Error("Want: '', Bug get: ", err) + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +func TestMergeConflictHeuristicRules(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + tmpRules := make(map[string]Rule) + for item, val := range HeuristicRules { + tmpRules[item] = val + } + err := common.GoldenDiff(func() { + suggest := MergeConflictHeuristicRules(tmpRules) + var sortedSuggest []string + for item := range suggest { + sortedSuggest = append(sortedSuggest, item) + } + sort.Strings(sortedSuggest) + for _, item := range sortedSuggest { + pretty.Println(suggest[item]) + } + }, t.Name(), update) + if err != nil { + t.Error(err) + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} diff --git a/advisor/index.go b/advisor/index.go new file mode 100644 index 00000000..046d8d1a --- /dev/null +++ b/advisor/index.go @@ -0,0 +1,1113 @@ +/* + * Copyright 2018 Xiaomi, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package advisor + +import ( + "fmt" + "strings" + + "github.com/XiaoMi/soar/ast" + "github.com/XiaoMi/soar/common" + "github.com/XiaoMi/soar/database" + "github.com/XiaoMi/soar/env" + + "github.com/dchest/uniuri" + "vitess.io/vitess/go/vt/sqlparser" +) + +// IndexAdvisor 索引建议需要使用到的所有信息 +type IndexAdvisor struct { + vEnv *env.VirtualEnv // 线下虚拟测试环境(测试环境) + rEnv database.Connector // 线上真实环境 + Ast sqlparser.Statement // Vitess Parser生成的抽象语法树 + where []*common.Column // 所有where条件中用到的列 + whereEQ []*common.Column // where条件中可以加索引的等值条件列 + whereINEQ []*common.Column // where条件中可以加索引的非等值条件列 + groupBy []*common.Column // group by可以加索引列 + orderBy []*common.Column // order by可以加索引列 + joinCond [][]*common.Column // 由于join condition跨层级间索引不可共用,需要多一个维度用来维护层级关系 + IndexMeta map[string]map[string]*database.TableIndexInfo +} + +// IndexInfo 创建一条索引需要的信息 +type IndexInfo struct { + Name string `json:"name"` // 索引名称 + Database string `json:"database"` // 数据库名 + Table string `json:"table"` // 表名 + DDL string `json:"ddl"` // ALTER, CREATE等类型的DDL语句 + ColumnDetails []*common.Column `json:"column_details"` // 列详情 +} + +// IndexAdvises IndexAdvises列表 +type IndexAdvises []IndexInfo + +// mergeAdvices 合并索引建议 +func mergeAdvices(dst []IndexInfo, src ...IndexInfo) IndexAdvises { + if len(src) == 0 { + return dst + } + + for _, newIdx := range src { + has := false + for _, idx := range dst { + if newIdx.DDL == idx.DDL { + common.Log.Debug("merge index %s and %s", idx.Name, newIdx.Name) + has = true + } + } + + if !has { + dst = append(dst, newIdx) + } + } + + return dst +} + +// NewAdvisor 构造一个 IndexAdvisor 的时候就会对其本身结构初始化 +// 获取 condition 中的等值条件、非等值条件,以及group by 、 order by信息 +func NewAdvisor(env *env.VirtualEnv, rEnv database.Connector, q Query4Audit) (*IndexAdvisor, error) { + common.Log.Debug("Enter: NewAdvisor(), Caller: %s", common.Caller()) + if common.Config.TestDSN.Disable { + return nil, fmt.Errorf("TestDSN is Disabled: %s", common.Config.TestDSN.Addr) + } + // DDL 检测 + switch stmt := q.Stmt.(type) { + case *sqlparser.DDL: + // 获取ast中用到的库表 + sqlMeta := ast.GetMeta(q.Stmt, nil) + for db := range sqlMeta { + dbRef := db + if db == "" { + dbRef = rEnv.Database + } + + // DDL在Env初始化的时候已经执行过了 + if _, ok := env.TableMap[dbRef]; !ok { + env.TableMap[dbRef] = make(map[string]string) + } + + for _, tb := range sqlMeta[db].Table { + env.TableMap[dbRef][tb.TableName] = tb.TableName + } + } + + return nil, nil + + case *sqlparser.DBDDL: + // 忽略建库语句 + return nil, nil + + case *sqlparser.Use: + // 如果是use,切基础环境 + env.Database = env.DBHash(stmt.DBName.String()) + return nil, nil + } + + return &IndexAdvisor{ + vEnv: env, + rEnv: rEnv, + Ast: q.Stmt, + + // 所有的FindXXXXCols尽最大可能先排除不需要加索引的列,但由于元数据在此阶段尚未补齐,给出的列有可能也无法添加索引 + // 后续需要通过CompleteColumnsInfo + calcCardinality补全后再进一步判断 + joinCond: ast.FindJoinCols(q.Stmt), + whereEQ: ast.FindWhereEQ(q.Stmt), + whereINEQ: ast.FindWhereINEQ(q.Stmt), + groupBy: ast.FindGroupByCols(q.Stmt), + orderBy: ast.FindOrderByCols(q.Stmt), + where: ast.FindAllCols(q.Stmt, "where"), + IndexMeta: make(map[string]map[string]*database.TableIndexInfo), + }, nil +} + +/* + +关于如何添加索引: +在《Relational Database Index Design and the Optimizers》一书中,作者提出著名的的三星索引理论(Three-Star Index) + +To Qualify for the First Star: +Pick the columns from all equal predicates (WHERE COL = . . .). +Make these the first columns of the index—in any order. For CURSOR41, the three-star index will begin with +columns LNAME, CITY or CITY, LNAME. In both cases the index slice that must be scanned will be as thin as possible. + +To Qualify for the Second Star: +Add the ORDER BY columns. Do not change the order of these columns, but ignore columns that were already +picked in step 1. For example, if CURSOR41 had redundant columns in the ORDER BY, say ORDER BY LNAME, +FNAME or ORDER BY FNAME, CITY, only FNAME would be added in this step. When FNAME is the third index column, +the result table will be in the right order without sorting. The first FETCH call will return the row with +the smallest FNAME value. + +To Qualify for the Third Star: +Add all the remaining columns from the SELECT statement. The order of the columns added in this step +has no impact on the performance of the SELECT, but the cost of updates should be reduced by placing volatile +columns at the end. Now the index contains all the columns required for an index-only access path. + +索引添加算法正是以这个理想化索策略添为基础,尽可能的给予"三星"索引建议。 + +但又如《High Performance MySQL》一书中所说,索引并不总是最好的工具。只有当索引帮助存储引擎快速查找到记录带来的好处大于其 +带来的额外工作时,索引才是有效的。 + +因此,在三星索引理论的基础上引入启发式索引算法,在第二颗星的实现上做了部分改进,对于非等值条件只会添加散粒度最高的一列到索引中, +并基于总体列的使用情况作出判断,按需对order by、group by添加索引,由此来想`增强索引建议的通用性。 + +*/ + +// IndexAdvise 索引优化建议算法入口主函数 +// TODO 索引顺序该如何确定 +func (idxAdv *IndexAdvisor) IndexAdvise() IndexAdvises { + // 支持不依赖DB的索引建议分析 + if common.Config.TestDSN.Disable { + // 未开启Env原数据依赖,信息不全的情况下可能会给予错误的索引建议,请人工进行核查。 + common.Log.Warn("TestDSN.Disable = true") + } + + // 检查否是否含有子查询 + subQueries := ast.FindSubquery(0, idxAdv.Ast) + var subQueryAdvises []IndexInfo + // 含有子查询对子查询进行单独评审,子查询评审建议报错忽略 + if len(subQueries) > 0 { + for _, subSQL := range subQueries { + stmt, err := sqlparser.Parse(subSQL) + if err != nil { + continue + } + q := Query4Audit{ + Query: subSQL, + Stmt: stmt, + } + subIdxAdv, _ := NewAdvisor(idxAdv.vEnv, idxAdv.rEnv, q) + subQueryAdvises = append(subQueryAdvises, subIdxAdv.IndexAdvise()...) + } + } + + // 变量初始化,用于存放索引信息,按照db.tb.[cols]组织 + indexList := make(map[string]map[string][]*common.Column) + + // 为用到的每一列填充库名,表名等信息 + var joinCond [][]*common.Column + for _, joinCols := range idxAdv.joinCond { + joinCond = append(joinCond, CompleteColumnsInfo(idxAdv.Ast, joinCols, idxAdv.vEnv)) + } + idxAdv.joinCond = joinCond + + idxAdv.where = CompleteColumnsInfo(idxAdv.Ast, idxAdv.where, idxAdv.vEnv) + idxAdv.whereEQ = CompleteColumnsInfo(idxAdv.Ast, idxAdv.whereEQ, idxAdv.vEnv) + idxAdv.whereINEQ = CompleteColumnsInfo(idxAdv.Ast, idxAdv.whereINEQ, idxAdv.vEnv) + idxAdv.groupBy = CompleteColumnsInfo(idxAdv.Ast, idxAdv.groupBy, idxAdv.vEnv) + idxAdv.orderBy = CompleteColumnsInfo(idxAdv.Ast, idxAdv.orderBy, idxAdv.vEnv) + + // 只要在开启使用env元数据的时候才会计算散粒度 + if !common.Config.TestDSN.Disable { + // 计算joinCond, whereEQ, whereINEQ用到的每一列的散粒度,并排序,方便后续添加复合索引 + // groupBy, orderBy列按书写顺序给索引建议,不需要按散粒度排序 + idxAdv.calcCardinality(idxAdv.whereEQ) + idxAdv.calcCardinality(idxAdv.whereINEQ) + idxAdv.calcCardinality(idxAdv.orderBy) + idxAdv.calcCardinality(idxAdv.groupBy) + + for i, joinCols := range idxAdv.joinCond { + idxAdv.calcCardinality(joinCols) + joinCols = common.ColumnSort(joinCols) + idxAdv.joinCond[i] = joinCols + } + + // 根据散粒度进行排序 + // 对所有列进行排序,按散粒度由大到小排序 + idxAdv.whereEQ = common.ColumnSort(idxAdv.whereEQ) + idxAdv.whereINEQ = common.ColumnSort(idxAdv.whereINEQ) + idxAdv.orderBy = common.ColumnSort(idxAdv.orderBy) + idxAdv.groupBy = common.ColumnSort(idxAdv.groupBy) + + } + + // 是否指定Where条件,打标签 + hasWhere := false + err := sqlparser.Walk(func(node sqlparser.SQLNode) (kontinue bool, err error) { + switch where := node.(type) { + case *sqlparser.Subquery: + return false, nil + case *sqlparser.Where: + if where != nil { + hasWhere = true + } + } + return true, nil + }, idxAdv.Ast) + common.LogIfError(err, "") + // 获取哪些列被忽略 + var ignore []*common.Column + usedCols := append(idxAdv.whereINEQ, idxAdv.whereEQ...) + + for _, whereCol := range idxAdv.where { + isUsed := false + for _, used := range usedCols { + if whereCol.Equal(used) { + isUsed = true + } + } + + if !isUsed { + common.Log.Debug("column %s in `%s`.`%s` will ignore when adding index", whereCol.DB, whereCol.Table, whereCol.Name) + ignore = append(ignore, whereCol) + } + + } + + // 索引优化算法入口,从这里开始放大招 + if hasWhere { + // 有Where条件的先分析 等值条件 + for _, index := range idxAdv.whereEQ { + // 对应列在前面已经按散粒度由大到小排序好了 + mergeIndex(indexList, index) + } + // 若存在非等值查询条件,可以给第一个非等值条件添加索引 + if len(idxAdv.whereINEQ) > 0 { + mergeIndex(indexList, idxAdv.whereINEQ[0]) + } + // 有WHERE条件,但WHERE条件未能给出索引建议就不能再加GROUP BY和ORDER BY建议了 + if len(ignore) == 0 { + // 没有非等值查询条件时可以再为GroupBy和OrderBy添加索引 + for _, index := range idxAdv.groupBy { + mergeIndex(indexList, index) + } + + // OrderBy + // 没有GroupBy时可以为OrderBy加索引 + if len(idxAdv.groupBy) == 0 { + for _, index := range idxAdv.orderBy { + mergeIndex(indexList, index) + } + } + } + } else { + // 未指定Where条件的,只需要GroupBy和OrderBy的索引建议 + for _, index := range idxAdv.groupBy { + mergeIndex(indexList, index) + } + + // OrderBy + // 没有GroupBy时可以为OrderBy加索引 + if len(idxAdv.groupBy) == 0 { + for _, index := range idxAdv.orderBy { + mergeIndex(indexList, index) + } + } + } + + // 开始整合索引信息,添加索引 + var indexes []IndexInfo + + // 为join添加索引 + // 获取 join condition 中需要加索引的表有哪些 + defaultDB := "" + if !common.Config.TestDSN.Disable { + defaultDB = idxAdv.vEnv.RealDB(idxAdv.vEnv.Database) + } + if !common.Config.OnlineDSN.Disable { + defaultDB = idxAdv.rEnv.Database + } + + // 根据join table的信息给予优化建议 + joinTableMeta := ast.FindJoinTable(idxAdv.Ast, nil).SetDefault(idxAdv.rEnv.Database).SetDefault(defaultDB) + indexes = mergeAdvices(indexes, idxAdv.buildJoinIndex(joinTableMeta)...) + + if common.Config.TestDSN.Disable || common.Config.OnlineDSN.Disable { + // 无env环境下只提供单列索引,无法确定table时不给予优化建议 + // 仅有table信息时给出的建议不包含DB信息 + indexes = mergeAdvices(indexes, idxAdv.buildIndexWithNoEnv(indexList)...) + } else { + // 给出尽可能详细的索引建议 + indexes = mergeAdvices(indexes, idxAdv.buildIndex(indexList)...) + } + + indexes = mergeAdvices(indexes, subQueryAdvises...) + + // 在开启env的情况下,检查数据库版本,字段类型,索引总长度 + indexes = idxAdv.idxColsTypeCheck(indexes) + + // 在开启env的情况下,会对索引进行检查,对全索引进行过滤 + // 在前几步都不会对idx生成DDL语句,DDL语句在这里生成 + return idxAdv.mergeIndexes(indexes) +} + +// idxColsTypeCheck 对超长的字段添加前缀索引,剔除无法添索引字段的列 +// TODO 暂不支持fulltext索引, +func (idxAdv *IndexAdvisor) idxColsTypeCheck(idxList []IndexInfo) []IndexInfo { + if common.Config.TestDSN.Disable { + return rmSelfDupIndex(idxList) + } + + var indexes []IndexInfo + + for _, idx := range idxList { + var newCols []*common.Column + var newColInfo []string + // 索引总长度 + idxBytesTotal := 0 + isOverFlow := false + for _, col := range idx.ColumnDetails { + // 获取字段bytes + bytes := col.GetDataBytes(common.Config.OnlineDSN.Version) + tmpCol := col.Name + overFlow := 0 + // 加上该列后是否索引长度过长 + if bytes < 0 { + // bytes < 0 说明字段的长度是无法计算的 + common.Log.Warning("%s.%s data type not support %s, can't add index", + col.Table, col.Name, col.DataType) + continue + } + + // idx bytes over flow + if total := idxBytesTotal + bytes; total > common.Config.MaxIdxBytes { + + common.Log.Debug("bytes: %d, idxBytesTotal: %d, total: %d, common.Config.MaxIdxBytes: %d", + bytes, idxBytesTotal, total, common.Config.MaxIdxBytes) + + overFlow = total - common.Config.MaxIdxBytes + isOverFlow = true + + } else { + idxBytesTotal = total + } + + // common.Config.MaxIdxColBytes 默认大小 767 + if bytes > common.Config.MaxIdxBytesPerColumn || isOverFlow { + // In 5.6, you may not include a column that equates to + // bigger than 767 bytes: VARCHAR(255) CHARACTER SET utf8 or VARCHAR(191) CHARACTER SET utf8mb4. + // In 5.7 you may not include a column that equates to + // bigger than 3072 bytes. + + // v : 在 col.Character 字符集下每个字符占用 v bytes + v, ok := common.CharSets[strings.ToLower(col.Character)] + if !ok { + // 找不到对应字符集,不添加索引 + // 如果出现不认识的字符集,认为每个字符占用4个字节 + common.Log.Warning("%s.%s(%s) charset not support yet %s, use default 4 bytes length", + col.Table, col.Name, col.DataType, col.Character) + v = 4 + } + + // 保留两个字节的安全余量 + length := (common.Config.MaxIdxBytesPerColumn - 2) / v + if isOverFlow { + // 在索引中添加该列会导致索引长度过长,建议根据需求转换为合理的前缀索引 + // _OPR_SPLIT_ 是自定的用于后续处理的特殊分隔符 + common.Log.Warning("adding index '%s(%s)' to table '%s' causes the index to be too long, overflow is %d", + col.Name, col.DataType, col.Table, overFlow) + tmpCol += fmt.Sprintf("_OPR_SPLIT_(N)") + } else { + // 索引没有过长,可以加一个最长的前缀索引 + common.Log.Warning("index column too large: %s.%s --> %s.%s(%d), data type: %s", + col.Table, col.Name, col.Table, tmpCol, length, col.DataType) + tmpCol += fmt.Sprintf("_OPR_SPLIT_(%d)", length) + } + + } + + newCols = append(newCols, col) + newColInfo = append(newColInfo, tmpCol) + } + + // 为新索引重建索引语句 + idxName := "idx_" + idxCols := "" + for i, newCol := range newColInfo { + // 对名称和可能存在的长度进行拼接 + // 用等号进行分割 + tmp := strings.Split(newCol, "_OPR_SPLIT_") + idxName += tmp[0] + if len(tmp) > 1 { + idxCols += tmp[0] + "`" + tmp[1] + } else { + idxCols += tmp[0] + "`" + } + + if i+1 < len(newColInfo) { + idxName += "_" + idxCols += ",`" + } + } + + // 索引名称最大长度64 + if len(idxName) > 64 { + common.Log.Warn("index '%s' name large than 64", idxName) + idxName = strings.TrimRight(idxName[:64], "_") + } + + // 新的alter语句 + newDDL := fmt.Sprintf("alter table `%s`.`%s` add index `%s` (`%s)", idxAdv.vEnv.RealDB(idx.Database), + idx.Table, idxName, idxCols) + + // 将筛选改造后的索引信息信息加入到新的索引列表中 + idx.ColumnDetails = newCols + idx.DDL = newDDL + indexes = append(indexes, idx) + } + + return indexes +} + +// mergeIndexes 与线上环境对比,将给出的索引建议进行去重 +func (idxAdv *IndexAdvisor) mergeIndexes(idxList []IndexInfo) []IndexInfo { + // TODO 暂不支持前缀索引去重 + if common.Config.TestDSN.Disable { + return rmSelfDupIndex(idxList) + } + + var indexes []IndexInfo + for _, idx := range idxList { + // 将DB替换成vEnv中的数据库名称 + dbInVEnv := idx.Database + if _, ok := idxAdv.vEnv.DBRef[idx.Database]; ok { + dbInVEnv = idxAdv.vEnv.DBRef[idx.Database] + } + + // 检测索引添加的表是否是视图 + if idxAdv.vEnv.IsView(idx.Table) { + common.Log.Info("%s.%s is a view. no need indexed", idx.Database, idx.Table) + continue + } + + // 检测是否存在重复索引 + indexMeta := idxAdv.IndexMeta[dbInVEnv][idx.Table] + isExisted := false + + // 检测无索引列的情况 + if len(idx.ColumnDetails) < 1 { + continue + } + + if existedIndexes := indexMeta.FindIndex(database.IndexColumnName, idx.ColumnDetails[0].Name); len(existedIndexes) > 0 { + for _, existedIdx := range existedIndexes { + // flag: 用于标记已存在的索引是否是约束条件 + isConstraint := false + + var cols []string + var colsDetail []*common.Column + + // 把已经存在的key摘出来遍历一遍对比是否是包含关系 + for _, col := range indexMeta.FindIndex(database.IndexKeyName, existedIdx.KeyName) { + cols = append(cols, col.ColumnName) + colsDetail = append(colsDetail, &common.Column{ + Name: col.ColumnName, + Table: idx.Table, + DB: idx.ColumnDetails[0].DB, + }) + } + + // 判断已存在的索引是否属于约束条件(唯一索引、主键) + // 这里可以忽略是否含有外键的情况,因为索引已经重复了,添加了新索引后原先重复的索引是可以删除的。 + if existedIdx.NonUnique == 0 { + common.Log.Debug("%s.%s表%s为约束条件", dbInVEnv, idx.Table, existedIdx.KeyName) + isConstraint = true + } + + // 如果已存在的索引与索引建议存在重叠,则说明无需添加新索引或可能需要给出删除索引的建议 + if common.IsColsPart(colsDetail, idx.ColumnDetails) { + idxName := existedIdx.KeyName + // 如果已经存在的索引包含需要添加的索引,则无需添加 + if len(colsDetail) >= len(idx.ColumnDetails) { + common.Log.Info(" `%s`.`%s` %s already had a index `%s`", + idx.Database, idx.Table, strings.Join(cols, ","), idxName) + isExisted = true + continue + } + + // 库、表、列名需要用反撇转义 + // TODO 关于外键索引去重的优雅解决方案 + if !isConstraint { + if common.Config.AllowDropIndex { + alterSQL := fmt.Sprintf("alter table `%s`.`%s` drop index `%s`", idx.Database, idx.Table, idxName) + indexes = append(indexes, IndexInfo{ + Name: idxName, + Database: idx.Database, + Table: idx.Table, + DDL: alterSQL, + ColumnDetails: colsDetail, + }) + } else { + common.Log.Warning("In table `%s`, the new index of column `%s` contains index `%s`,"+ + " maybe you could drop one of them.", existedIdx.Table, + strings.Join(cols, ","), idxName) + } + } + } + } + } + + if !isExisted { + // 检测索引名称是否重复? + if existedIndexes := indexMeta.FindIndex(database.IndexKeyName, idx.Name); len(existedIndexes) > 0 { + var newName string + if len(idx.Name) < 59 { + newName = idx.Name + "_" + uniuri.New()[:4] + } else { + newName = idx.Name[:59] + "_" + uniuri.New()[:4] + } + + common.Log.Warning("duplicate index name '%s', new name is '%s'", idx.Name, newName) + idx.DDL = strings.Replace(idx.DDL, idx.Name, newName, -1) + idx.Name = newName + } + + // 添加合并 + indexes = mergeAdvices(indexes, idx) + } + + } + + // 对索引进行去重 + return rmSelfDupIndex(indexes) +} + +// rmSelfDupIndex 去重传入的[]IndexInfo中重复的索引 +func rmSelfDupIndex(indexes []IndexInfo) []IndexInfo { + var resultIndex []IndexInfo + tmpIndexList := indexes + for _, a := range indexes { + tmp := a + for i, b := range tmpIndexList { + if common.IsColsPart(tmp.ColumnDetails, b.ColumnDetails) && tmp.Name != b.Name { + if len(b.ColumnDetails) > len(tmp.ColumnDetails) { + common.Log.Debug("remove duplicate index: %s", tmp.Name) + tmp = b + } + + if i < len(tmpIndexList) { + tmpIndexList = append(tmpIndexList[:i], tmpIndexList[i+1:]...) + } else { + tmpIndexList = tmpIndexList[:i] + } + + } + } + resultIndex = mergeAdvices(resultIndex, tmp) + } + + return resultIndex +} + +// buildJoinIndex 检查Join中使用的库表是否需要添加索引并给予索引建议 +func (idxAdv *IndexAdvisor) buildJoinIndex(meta common.Meta) []IndexInfo { + var indexes []IndexInfo + for _, IndexCols := range idxAdv.joinCond { + // 如果该列的库表为join condition中需要添加索引的库表 + indexColsList := make(map[string]map[string][]*common.Column) + for _, col := range IndexCols { + mergeIndex(indexColsList, col) + + } + + if common.Config.TestDSN.Disable || common.Config.OnlineDSN.Disable { + indexes = mergeAdvices(indexes, idxAdv.buildIndexWithNoEnv(indexColsList)...) + continue + } + + indexes = mergeAdvices(indexes, idxAdv.buildIndex(indexColsList)...) + } + return indexes +} + +// buildIndex 尽可能的将 map[string]map[string][]*common.Column 转换成 []IndexInfo +// 此处不判断索引是否重复 +func (idxAdv *IndexAdvisor) buildIndex(idxList map[string]map[string][]*common.Column) []IndexInfo { + var indexes []IndexInfo + for db, tbs := range idxList { + for tb, cols := range tbs { + + // 单个索引中含有的列收 config 中参数限制 + if len(cols) > common.Config.MaxIdxColsCount { + cols = cols[:common.Config.MaxIdxColsCount] + } + + var colNames []string + for _, col := range cols { + if col.DB == "" || col.Table == "" { + common.Log.Warn("can not get the meta info of column '%s'", col.Name) + continue + } + colNames = append(colNames, col.Name) + } + + if len(colNames) == 0 { + continue + } + + idxName := "idx_" + strings.Join(colNames, "_") + + // 索引名称最大长度64 + if len(idxName) > 64 { + common.Log.Warn("index '%s' name large than 64", idxName) + idxName = strings.TrimRight(idxName[:64], "_") + } + + alterSQL := fmt.Sprintf("alter table `%s`.`%s` add index `%s` (`%s`)", idxAdv.vEnv.RealDB(db), tb, + idxName, strings.Join(colNames, "`,`")) + + indexes = append(indexes, IndexInfo{ + Name: idxName, + Database: idxAdv.vEnv.RealDB(db), + Table: tb, + DDL: alterSQL, + ColumnDetails: cols, + }) + } + } + return indexes +} + +// buildIndexWithNoEnv 忽略原数据,给予最基础的索引 +func (idxAdv *IndexAdvisor) buildIndexWithNoEnv(indexList map[string]map[string][]*common.Column) []IndexInfo { + // 如果不获取数据库原信息,则不去判断索引是否重复,且只给单列加索引 + var indexes []IndexInfo + for _, tableIndex := range indexList { + for _, indexCols := range tableIndex { + for _, col := range indexCols { + if col.Table == "" { + common.Log.Warn("can not get the meta info of column '%s'", col.Name) + continue + } + idxName := "idx_" + col.Name + // 库、表、列名需要用反撇转义 + alterSQL := fmt.Sprintf("alter table `%s`.`%s` add index `%s` (`%s`)", idxAdv.vEnv.RealDB(col.DB), col.Table, idxName, col.Name) + if col.DB == "" { + alterSQL = fmt.Sprintf("alter table `%s` add index `%s` (`%s`)", col.Table, idxName, col.Name) + } + + indexes = append(indexes, IndexInfo{ + Name: idxName, + Database: idxAdv.vEnv.RealDB(col.DB), + Table: col.Table, + DDL: alterSQL, + ColumnDetails: []*common.Column{col}, + }) + } + + } + } + return indexes +} + +// mergeIndex 将索引用到的列去重后合并到一起 +func mergeIndex(idxList map[string]map[string][]*common.Column, column *common.Column) { + db := column.DB + tb := column.Table + if idxList[db] == nil { + idxList[db] = make(map[string][]*common.Column) + } + if idxList[db][tb] == nil { + idxList[db][tb] = make([]*common.Column, 0) + } + + // 去除重复列Append + exist := false + for _, cl := range idxList[db][tb] { + if cl.Name == column.Name { + exist = true + } + } + if !exist { + idxList[db][tb] = append(idxList[db][tb], column) + } +} + +// CompleteColumnsInfo 补全索引可能会用到列的所属库名、表名等信息 +func CompleteColumnsInfo(stmt sqlparser.Statement, cols []*common.Column, env *env.VirtualEnv) []*common.Column { + // 如果传过来的列是空的,没必要跑逻辑 + if len(cols) == 0 { + return cols + } + + // 从Ast中拿到DBStructure,包含所有表的相关信息 + dbs := ast.GetMeta(stmt, nil) + + // 此处生成的meta信息中不应该含有""db的信息,若DB为空则认为是已传入的db为默认db并进行信息补全 + // BUG Fix: + // 修补dbs中空DB的导致后续补全列信息时无法获取正确table名称的问题 + if _, ok := dbs[""]; ok { + dbs[env.Database] = dbs[""] + delete(dbs, "") + } + + tableCount := 0 + for db := range dbs { + for tb := range dbs[db].Table { + if tb != "" { + tableCount++ + } + } + } + + var noEnvTmp []*common.Column + for _, col := range cols { + for db := range dbs { + // 对每一列进行比对,将别名转换为正确的名称 + find := false + for _, tb := range dbs[db].Table { + for _, tbAlias := range tb.TableAliases { + if col.Table != "" && col.Table == tbAlias { + common.Log.Debug("column '%s' prefix change: %s --> %s", col.Name, col.Table, tb.TableName) + find = true + col.Table = tb.TableName + col.DB = db + break + } + } + if find { + break + } + + } + + // 如果不依赖env环境,利用ast中包含的信息推理列的库表信息 + if common.Config.TestDSN.Disable { + if tableCount == 1 { + for _, tb := range dbs[db].Table { + col.Table = tb.TableName + + // 因为tableMeta是按照库表组织的树状结构,db变量贯穿全局 + // 只有在最终赋值前才能根据逻辑变更补全 + if db == "" { + db = env.Database + } + col.DB = db + } + } + + // 如果SQL中含有的表大于一个,则使用的列中必须含有前缀,不然无法判断该列属于哪个表 + // 如果某一列未含有前缀信息,则认为两张表中都含有该列,需要由人去判断 + if tableCount > 1 { + if col.Table == "" { + for _, tb := range dbs[db].Table { + if tb.TableName == "" { + common.Log.Warn("can not get the meta info of column '%s'", col.Name) + } + + if db == "" { + db = env.RealDB(env.Database) + } + col.Table = tb.TableName + col.DB = db + + tmp := *col + tmp.Table = tb.TableName + tmp.DB = db + + noEnvTmp = append(noEnvTmp, &tmp) + } + } + + if col.DB == "" { + if db == "" { + db = env.Database + } + col.DB = db + } + } + + break + } + + // 将已经获取到正确表信息的列信息带入到env中,利用show columns where table 获取库表信息 + // 此出会传入之前从ast中,该 db 下获取的所有表来作为where限定条件, + // 防止与SQL无关的库表信息干扰准确性 + // 此处传入的是测试环境,DB是经过变换的,所以在寻找列名的时候需要将DB名称转换成测试环境中经过hash的DB名称 + // 不然会找不到col的信息 + realCols, err := env.FindColumn(col.Name, env.DBHash(db), dbs.Tables(db)...) + if err != nil { + common.Log.Warn("%v", err) + continue + } + + // 对比 column 信息中的表名与从 env 中获取的库表名的一致性 + for _, realCol := range realCols { + if col.Name == realCol.Name { + // 如果查询到了列名一致,但从ast中获取的列的前缀与env中的表信息不符 + // 1.存在一个同名列,但不同表,该情况下忽略 + // 2.存在一个未正确转换的别名(如表名为),该情况下修正,大概率是正确的 + if col.Table != "" && col.Table != realCol.Table { + has, _ := env.FindColumn(col.Name, env.DBHash(db), col.Table) + if len(has) > 0 { + realCol = has[0] + } + } + + col.DataType = realCol.DataType + col.Table = realCol.Table + col.DB = env.RealDB(realCol.DB) + col.Character = realCol.Character + col.Collation = realCol.Collation + + } + } + } + + } + + // 如果不依赖env环境,将可能存在的列也加入到索引预处理列表中 + if common.Config.TestDSN.Disable { + cols = append(cols, noEnvTmp...) + } + + return cols +} + +// calcCardinality 计算每一列的散粒度 +// 这个函数需要在补全列的库表信息之后再调用,否则无法确定要计算列的归属 +func (idxAdv *IndexAdvisor) calcCardinality(cols []*common.Column) []*common.Column { + common.Log.Debug("Enter: calcCardinality(), Caller: %s", common.Caller()) + tmpDB := *idxAdv.vEnv + for _, col := range cols { + // 补全对应列的库->表->索引信息到IndexMeta + // 这将在后面用于判断某一列是否为主键或单列唯一索引,快速返回散粒度 + if col.DB == "" { + col.DB = idxAdv.vEnv.Database + } + realDB := idxAdv.vEnv.DBHash(col.DB) + if idxAdv.IndexMeta[realDB] == nil { + idxAdv.IndexMeta[realDB] = make(map[string]*database.TableIndexInfo) + } + + if idxAdv.IndexMeta[realDB][col.Table] == nil { + tmpDB.Database = realDB + indexInfo, err := tmpDB.ShowIndex(col.Table) + if err != nil { + // 如果是不存在的表就会报错,报错的可能性有三个: + // 1.数据库错误 2.表不存在 3.临时表 + // 而这三种错误都是不需要在这一层关注的,直接跳过 + common.Log.Debug("calcCardinality error: %v", err) + continue + } + + // 将获取的索引信息以db.tb维度组织到IndexMeta中 + idxAdv.IndexMeta[realDB][col.Table] = indexInfo + } + + // 检查对应列是否为主键或单列唯一索引,如果满足直接返回1,不再重复计算,提高效率 + // 多列复合唯一索引不能跳过计算,单列普通索引不能跳过计算 + for _, index := range idxAdv.IndexMeta[realDB][col.Table].IdxRows { + // 根据索引的名称判断该索引包含的列数,列数大于1即为复合索引 + columnCount := len(idxAdv.IndexMeta[realDB][col.Table].FindIndex(database.IndexKeyName, index.KeyName)) + if col.Name == index.ColumnName { + // 主键、唯一键 无需计算散粒度 + if (index.KeyName == "PRIMARY" || index.NonUnique == 0) && columnCount == 1 { + common.Log.Debug("column '%s' is PK or UK, no need to calculate cardinality.", col.Name) + col.Cardinality = 1 + break + } + } + + } + + // 给非 PRIMARY、UNIQUE 的列计算散粒度 + if col.Cardinality != 1 { + col.Cardinality = idxAdv.vEnv.ColumnCardinality(col.Table, col.Name) + } + } + + return cols +} + +// Format 用于格式化输出索引建议 +func (idxAdvs IndexAdvises) Format() map[string]Rule { + rulesMap := make(map[string]Rule) + number := 1 + rules := make(map[string]*Rule) + sqls := make(map[string][]string) + + for _, advise := range idxAdvs { + advKey := advise.Database + advise.Table + + if _, ok := sqls[advKey]; !ok { + sqls[advKey] = make([]string, 0) + } + + sqls[advKey] = append(sqls[advKey], advise.DDL) + + if _, ok := rules[advKey]; !ok { + summary := fmt.Sprintf("为%s库的%s表添加索引", advise.Database, advise.Table) + if advise.Database == "" { + summary = fmt.Sprintf("为%s表添加索引", advise.Table) + } + + rules[advKey] = &Rule{ + Summary: summary, + Content: "", + Severity: "L2", + } + } + + for _, col := range advise.ColumnDetails { + // 为了更好地显示效果 + if common.Config.Sampling { + cardinal := fmt.Sprintf("%0.2f", col.Cardinality*100) + if cardinal != "0.00" { + rules[advKey].Content += fmt.Sprintf("为列%s添加索引,散粒度为: %s%%; ", + col.Name, cardinal) + } + } else { + rules[advKey].Content += fmt.Sprintf("为列%s添加索引;", col.Name) + } + } + // 清理多余的标点 + rules[advKey].Content = strings.Trim(rules[advKey].Content, common.Config.Delimiter) + } + + for adv := range rules { + key := fmt.Sprintf("IDX.%03d", number) + ddl := ast.MergeAlterTables(sqls[adv]...) + // 由于传入合并的SQL都是一张表的,所以一定只会输出一条ddl语句 + for _, v := range ddl { + rules[adv].Case = v + } + rulesMap[key] = *rules[adv] + + number++ + } + + return rulesMap +} + +// HeuristicCheck 依赖数据字典的启发式检查 +// IndexAdvisor会构建测试环境和数据字典,所以放在这里实现 +func (idxAdv *IndexAdvisor) HeuristicCheck(q Query4Audit) map[string]Rule { + var rule Rule + heuristicSuggest := make(map[string]Rule) + if common.Config.OnlineDSN.Disable && common.Config.TestDSN.Disable { + return heuristicSuggest + } + + ruleFuncs := []func(*IndexAdvisor) Rule{ + (*IndexAdvisor).RuleImplicitConversion, // ARG.003 + // (*IndexAdvisor).RuleImpossibleOuterJoin, // TODO: JOI.003, JOI.004 + (*IndexAdvisor).RuleGroupByConst, // CLA.004 + (*IndexAdvisor).RuleOrderByConst, // CLA.005 + (*IndexAdvisor).RuleUpdatePrimaryKey, // CLA.016 + } + + for _, f := range ruleFuncs { + rule = f(idxAdv) + if rule.Item != "OK" { + heuristicSuggest[rule.Item] = rule + } + } + return heuristicSuggest +} + +// DuplicateKeyChecker 对所有用到的库表检查是否存在重复索引 +func DuplicateKeyChecker(conn *database.Connector, databases ...string) map[string]Rule { + common.Log.Debug("Enter: DuplicateKeyChecker, Caller: %s", common.Caller()) + // 复制一份online connector,防止环境切换影响其他功能的使用 + tmpOnline := *conn + ruleMap := make(map[string]Rule) + number := 1 + + // 错误处理,用于汇总所有的错误 + funcErrCheck := func(err error) { + if err != nil { + if sug, ok := ruleMap["ERR.003"]; ok { + sug.Content += fmt.Sprintf("; %s", err.Error()) + } else { + ruleMap["ERR.003"] = Rule{ + Item: "ERR.003", + Severity: "L8", + Content: err.Error(), + } + } + } + } + + // 不指定DB的时候检查online dsn中的DB + if len(databases) == 0 { + databases = append(databases, tmpOnline.Database) + } + + for _, db := range databases { + // 获取所有的表 + tmpOnline.Database = db + tables, err := tmpOnline.ShowTables() + + if err != nil { + funcErrCheck(err) + if !common.Config.DryRun { + return ruleMap + } + } + + for _, tb := range tables { + // 获取表中所有的索引 + idxMap := make(map[string][]*common.Column) + idxInfo, err := tmpOnline.ShowIndex(tb) + if err != nil { + funcErrCheck(err) + if !common.Config.DryRun { + return ruleMap + } + } + + // 枚举所有的索引信息,提取用到的列 + for _, idx := range idxInfo.IdxRows { + if _, ok := idxMap[idx.KeyName]; !ok { + idxMap[idx.KeyName] = make([]*common.Column, 0) + for _, col := range idxInfo.FindIndex(database.IndexKeyName, idx.KeyName) { + idxMap[idx.KeyName] = append(idxMap[idx.KeyName], &common.Column{ + Name: col.ColumnName, + Table: tb, + DB: db, + }) + } + } + } + + // 对索引进行重复检查 + hasDup := false + content := "" + + for k1, cl1 := range idxMap { + for k2, cl2 := range idxMap { + if k1 != k2 && common.IsColsPart(cl1, cl2) { + hasDup = true + col1Str := common.JoinColumnsName(cl1, ", ") + col2Str := common.JoinColumnsName(cl2, ", ") + content += fmt.Sprintf("索引%s(%s)与%s(%s)重复;", k1, col1Str, k2, col2Str) + common.Log.Debug(" %s.%s has duplicate index %s(%s) <--> %s(%s)", db, tb, k1, col1Str, k2, col2Str) + } + } + delete(idxMap, k1) + } + + // TODO 重复索引检查添加对约束及索引的判断,提供重复索引的删除功能 + if hasDup { + tmpOnline.Database = db + ddl, _ := tmpOnline.ShowCreateTable(tb) + key := fmt.Sprintf("IDX.%03d", number) + ruleMap[key] = Rule{ + Item: key, + Severity: "L2", + Summary: fmt.Sprintf("%s.%s存在重复的索引", db, tb), + Content: content, + Case: ddl, + } + number++ + } + } + } + + return ruleMap +} diff --git a/advisor/index_test.go b/advisor/index_test.go new file mode 100644 index 00000000..a4bf8cc4 --- /dev/null +++ b/advisor/index_test.go @@ -0,0 +1,464 @@ +/* + * Copyright 2018 Xiaomi, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package advisor + +import ( + "fmt" + "os" + "testing" + + "github.com/XiaoMi/soar/common" + "github.com/XiaoMi/soar/env" + + "github.com/kr/pretty" + "vitess.io/vitess/go/vt/sqlparser" +) + +func init() { + common.BaseDir = common.DevPath + err := common.ParseConfig("") + if err != nil { + fmt.Println(err.Error()) + } + vEnv, rEnv := env.BuildEnv() + if _, err = vEnv.Version(); err != nil { + fmt.Println(err.Error(), ", By pass all advisor test cases") + os.Exit(0) + } + + if _, err := rEnv.Version(); err != nil { + fmt.Println(err.Error(), ", By pass all advisor test cases") + os.Exit(0) + } +} + +// ARG.003 +func TestRuleImplicitConversion(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + dsn := common.Config.OnlineDSN + common.Config.OnlineDSN = common.Config.TestDSN + vEnv, rEnv := env.BuildEnv() + defer vEnv.CleanUp() + + initSQLs := []string{ + `CREATE TABLE t1 (id int, title varchar(255) CHARSET utf8 COLLATE utf8_general_ci);`, + `CREATE TABLE t2 (id int, title varchar(255) CHARSET utf8mb4);`, + `CREATE TABLE t3 (id int, title varchar(255) CHARSET utf8 COLLATE utf8_bin);`, + } + for _, sql := range initSQLs { + vEnv.BuildVirtualEnv(rEnv, sql) + } + + sqls := []string{ + "SELECT * FROM t1 WHERE title >= 60;", + "SELECT * FROM t1, t2 WHERE t1.title = t2.title;", + "SELECT * FROM t1, t3 WHERE t1.title = t3.title;", + } + for _, sql := range sqls { + stmt, syntaxErr := sqlparser.Parse(sql) + if syntaxErr != nil { + common.Log.Critical("Syntax Error: %v, SQL: %s", syntaxErr, sql) + } + + q := &Query4Audit{Query: sql, Stmt: stmt} + + idxAdvisor, err := NewAdvisor(vEnv, *rEnv, *q) + if err != nil { + t.Error("NewAdvisor Error: ", err, "SQL: ", sql) + } + + if idxAdvisor != nil { + rule := idxAdvisor.RuleImplicitConversion() + if rule.Item != "ARG.003" { + t.Error("Rule not match:", rule, "Expect : ARG.003, SQL:", sql) + } + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) + common.Config.OnlineDSN = dsn +} + +// JOI.003 & JOI.004 +func TestRuleImpossibleOuterJoin(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := []string{ + `select city_id, city, country from city left outer join country using(country_id) WHERE city.city_id=59 and country.country='Algeria'`, + `select city_id, city, country from city left outer join country using(country_id) WHERE country.country='Algeria'`, + `select city_id, city, country from city left outer join country on city.country_id=country.country_id WHERE city.city_id IS NULL`, + } + + vEnv, rEnv := env.BuildEnv() + defer vEnv.CleanUp() + + for _, sql := range sqls { + stmt, syntaxErr := sqlparser.Parse(sql) + if syntaxErr != nil { + common.Log.Critical("Syntax Error: %v, SQL: %s", syntaxErr, sql) + } + + q := &Query4Audit{Query: sql, Stmt: stmt} + + if vEnv.BuildVirtualEnv(rEnv, q.Query) { + idxAdvisor, err := NewAdvisor(vEnv, *rEnv, *q) + if err != nil { + t.Error("NewAdvisor Error: ", err, "SQL: ", sql) + } + + if idxAdvisor != nil { + rule := idxAdvisor.RuleImpossibleOuterJoin() + if rule.Item != "JOI.003" && rule.Item != "JOI.004" { + t.Error("Rule not match:", rule, "Expect : JOI.003 || JOI.004") + } + } + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// GRP.001 +func TestIndexAdvisorRuleGroupByConst(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := [][]string{ + { + `select film_id, title from film where release_year='2006' group by release_year`, + `select film_id, title from film where release_year in ('2006') group by release_year`, + }, + { + // 反面的例子 + `select film_id, title from film where release_year in ('2006', '2007') group by release_year`, + }, + } + + vEnv, rEnv := env.BuildEnv() + defer vEnv.CleanUp() + + for _, sql := range sqls[0] { + stmt, syntaxErr := sqlparser.Parse(sql) + if syntaxErr != nil { + common.Log.Critical("Syntax Error: %v, SQL: %s", syntaxErr, sql) + } + + q := &Query4Audit{Query: sql, Stmt: stmt} + + if vEnv.BuildVirtualEnv(rEnv, q.Query) { + idxAdvisor, err := NewAdvisor(vEnv, *rEnv, *q) + if err != nil { + t.Error("NewAdvisor Error: ", err, "SQL: ", sql) + } + + if idxAdvisor != nil { + rule := idxAdvisor.RuleGroupByConst() + if rule.Item != "GRP.001" { + t.Error("Rule not match:", rule, "Expect : GRP.001") + } + } + } + } + + for _, sql := range sqls[1] { + stmt, syntaxErr := sqlparser.Parse(sql) + if syntaxErr != nil { + common.Log.Critical("Syntax Error: %v, SQL: %s", syntaxErr, sql) + } + + q := &Query4Audit{Query: sql, Stmt: stmt} + + if vEnv.BuildVirtualEnv(rEnv, q.Query) { + idxAdvisor, err := NewAdvisor(vEnv, *rEnv, *q) + if err != nil { + t.Error("NewAdvisor Error: ", err, "SQL: ", sql) + } + + if idxAdvisor != nil { + rule := idxAdvisor.RuleGroupByConst() + if rule.Item != "OK" { + t.Error("Rule not match:", rule, "Expect : OK") + } + } + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// CLA.005 +func TestIndexAdvisorRuleOrderByConst(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := [][]string{ + { + `select film_id, title from film where release_year='2006' order by release_year;`, + `select film_id, title from film where release_year in ('2006') order by release_year;`, + }, + { + // 反面的例子 + `select film_id, title from film where release_year in ('2006', '2007') order by release_year;`, + }, + } + + vEnv, rEnv := env.BuildEnv() + defer vEnv.CleanUp() + + for _, sql := range sqls[0] { + stmt, syntaxErr := sqlparser.Parse(sql) + if syntaxErr != nil { + common.Log.Critical("Syntax Error: %v, SQL: %s", syntaxErr, sql) + } + + q := &Query4Audit{Query: sql, Stmt: stmt} + + if vEnv.BuildVirtualEnv(rEnv, q.Query) { + idxAdvisor, err := NewAdvisor(vEnv, *rEnv, *q) + if err != nil { + t.Error("NewAdvisor Error: ", err, "SQL: ", sql) + } + + if idxAdvisor != nil { + rule := idxAdvisor.RuleOrderByConst() + if rule.Item != "CLA.005" { + t.Error("Rule not match:", rule, "Expect : CLA.005") + } + } + } + } + + for _, sql := range sqls[1] { + stmt, syntaxErr := sqlparser.Parse(sql) + if syntaxErr != nil { + common.Log.Critical("Syntax Error: %v, SQL: %s", syntaxErr, sql) + } + + q := &Query4Audit{Query: sql, Stmt: stmt} + + if vEnv.BuildVirtualEnv(rEnv, q.Query) { + idxAdvisor, err := NewAdvisor(vEnv, *rEnv, *q) + if err != nil { + t.Error("NewAdvisor Error: ", err, "SQL: ", sql) + } + + if idxAdvisor != nil { + rule := idxAdvisor.RuleOrderByConst() + if rule.Item != "OK" { + t.Error("Rule not match:", rule, "Expect : OK") + } + } + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +// CLA.016 +func TestRuleUpdatePrimaryKey(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := [][]string{ + { + `update film set film_id = 1 where title='a';`, + }, + { + // 反面的例子 + `select film_id, title from film where release_year in ('2006', '2007') order by release_year;`, + }, + } + + vEnv, rEnv := env.BuildEnv() + defer vEnv.CleanUp() + + for _, sql := range sqls[0] { + stmt, syntaxErr := sqlparser.Parse(sql) + if syntaxErr != nil { + common.Log.Critical("Syntax Error: %v, SQL: %s", syntaxErr, sql) + } + + q := &Query4Audit{Query: sql, Stmt: stmt} + + if vEnv.BuildVirtualEnv(rEnv, q.Query) { + idxAdvisor, err := NewAdvisor(vEnv, *rEnv, *q) + if err != nil { + t.Error("NewAdvisor Error: ", err, "SQL: ", sql) + } + + if idxAdvisor != nil { + rule := idxAdvisor.RuleUpdatePrimaryKey() + if rule.Item != "CLA.016" { + t.Error("Rule not match:", rule.Item, "Expect : CLA.016") + } + } + } + } + + for _, sql := range sqls[1] { + stmt, syntaxErr := sqlparser.Parse(sql) + if syntaxErr != nil { + common.Log.Critical("Syntax Error: %v, SQL: %s", syntaxErr, sql) + } + + q := &Query4Audit{Query: sql, Stmt: stmt} + + if vEnv.BuildVirtualEnv(rEnv, q.Query) { + idxAdvisor, err := NewAdvisor(vEnv, *rEnv, *q) + if err != nil { + t.Error("NewAdvisor Error: ", err, "SQL: ", sql) + } + + if idxAdvisor != nil { + rule := idxAdvisor.RuleUpdatePrimaryKey() + if rule.Item != "OK" { + t.Error("Rule not match:", rule, "Expect : OK") + } + } + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +func TestIndexAdvise(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + vEnv, rEnv := env.BuildEnv() + defer vEnv.CleanUp() + + for _, sql := range common.TestSQLs { + stmt, syntaxErr := sqlparser.Parse(sql) + if syntaxErr != nil { + common.Log.Critical("Syntax Error: %v, SQL: %s", syntaxErr, sql) + } + + q := &Query4Audit{Query: sql, Stmt: stmt} + + if vEnv.BuildVirtualEnv(rEnv, q.Query) { + idxAdvisor, err := NewAdvisor(vEnv, *rEnv, *q) + if err != nil { + t.Error("NewAdvisor Error: ", err, "SQL: ", sql) + } + + if idxAdvisor != nil { + rule := idxAdvisor.IndexAdvise().Format() + if len(rule) > 0 { + pretty.Println(rule) + } + } + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +func TestIndexAdviseNoEnv(t *testing.T) { + common.Config.OnlineDSN.Disable = true + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + vEnv, rEnv := env.BuildEnv() + defer vEnv.CleanUp() + + for _, sql := range common.TestSQLs { + stmt, syntaxErr := sqlparser.Parse(sql) + if syntaxErr != nil { + common.Log.Critical("Syntax Error: %v, SQL: %s", syntaxErr, sql) + } + + q := &Query4Audit{Query: sql, Stmt: stmt} + + if vEnv.BuildVirtualEnv(rEnv, q.Query) { + idxAdvisor, err := NewAdvisor(vEnv, *rEnv, *q) + if err != nil { + t.Error("NewAdvisor Error: ", err, "SQL: ", sql) + } + + if idxAdvisor != nil { + rule := idxAdvisor.IndexAdvise().Format() + if len(rule) > 0 { + pretty.Println(rule) + } + } + } + } + common.Log.Debug("Exiting function: %s", common.GetFunctionName()) +} + +func TestDuplicateKeyChecker(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + _, rEnv := env.BuildEnv() + rule := DuplicateKeyChecker(rEnv, "sakila") + if len(rule) != 0 { + t.Errorf("got rules: %s", pretty.Sprint(rule)) + } +} + +func TestMergeAdvices(t *testing.T) { + dst := []IndexInfo{ + { + Name: "test", + Database: "db", + Table: "tb", + ColumnDetails: []*common.Column{ + { + Name: "test", + }, + }, + }, + } + + src := dst[0] + + advise := mergeAdvices(dst, src) + if len(advise) != 1 { + t.Error(pretty.Sprint(advise)) + } +} + +func TestIdxColsTypeCheck(t *testing.T) { + common.Log.Debug("Entering function: %s", common.GetFunctionName()) + sqls := []string{ + `select city_id, city, country from city left outer join country using(country_id) WHERE city.city_id=59 and country.country='Algeria'`, + } + + vEnv, rEnv := env.BuildEnv() + defer vEnv.CleanUp() + + for _, sql := range sqls { + stmt, syntaxErr := sqlparser.Parse(sql) + if syntaxErr != nil { + common.Log.Critical("Syntax Error: %v, SQL: %s", syntaxErr, sql) + } + + q := &Query4Audit{Query: sql, Stmt: stmt} + + if vEnv.BuildVirtualEnv(rEnv, q.Query) { + idxAdvisor, err := NewAdvisor(vEnv, *rEnv, *q) + if err != nil { + t.Error("NewAdvisor Error: ", err, "SQL: ", sql) + } + + idxList := []IndexInfo{ + { + Name: "idx_fk_country_id", + Database: "sakila", + Table: "city", + ColumnDetails: []*common.Column{ + { + Name: "country_id", + Character: "utf8", + DataType: "varchar(3000)", + }, + }, + }, + } + + if idxAdvisor != nil { + rule := idxAdvisor.idxColsTypeCheck(idxList) + if !(len(rule) > 0 && rule[0].DDL == "alter table `sakila`.`city` add index `idx_country_id` (`country_id`(N))") { + t.Error(pretty.Sprint(rule)) + } + } + } + } +} diff --git a/advisor/rules.go b/advisor/rules.go new file mode 100644 index 00000000..97a60e8e --- /dev/null +++ b/advisor/rules.go @@ -0,0 +1,1372 @@ +/* + * Copyright 2018 Xiaomi, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package advisor + +import ( + "encoding/json" + "fmt" + "regexp" + "sort" + "strconv" + "strings" + + "github.com/XiaoMi/soar/ast" + "github.com/XiaoMi/soar/common" + + "github.com/kr/pretty" + "github.com/percona/go-mysql/query" + tidb "github.com/pingcap/tidb/ast" + "vitess.io/vitess/go/vt/sqlparser" +) + +// Query4Audit 待评审的SQL结构体,由原SQL和其对应的抽象语法树组成 +type Query4Audit struct { + Query string // 查询语句 + Stmt sqlparser.Statement // 通过Vitess解析出的抽象语法树 + TiStmt []tidb.StmtNode // 通过TiDB解析出的抽象语法树 +} + +// NewQuery4Audit return a struct for Query4Audit +func NewQuery4Audit(sql string, options ...string) (*Query4Audit, error) { + var err, tiErr error + var charset string + var collation string + + if len(options) > 0 { + charset = options[0] + } + + if len(options) > 1 { + collation = options[1] + } + + q := &Query4Audit{Query: sql} + // vitess语法解析 + q.Stmt, err = sqlparser.Parse(sql) + + // TiDB 语法解析仅作为补充,不检查语法错误 + // TODO: charset, collation + q.TiStmt, tiErr = ast.TiParse(sql, charset, collation) + if tiErr != nil { + common.Log.Warn("NewQuery4Audit ast.Tiparse Error: %s", tiErr.Error()) + } + return q, err +} + +// Rule 评审规则元数据结构 +type Rule struct { + Item string `json:"Item"` // 规则代号 + Severity string `json:"Severity"` // 危险等级:L[0-8], 数字越大表示级别越高 + Summary string `json:"Summary"` // 规则摘要 + Content string `json:"Content"` // 规则解释 + Case string `json:"Case"` // SQL示例 + Position int `json:"Position"` // 建议所处SQL字符位置,默认0表示全局建议 + Func func(*Query4Audit) Rule `json:"-"` // 函数名 +} + +/* + +## Item单词缩写含义 + +* ALI Alias(AS) +* ALT Alter +* ARG Argument +* CLA Classic +* COL Column +* DIS Distinct +* ERR Error, 特指MySQL执行返回的报错信息, ERR.000为vitess语法错误,ERR.001为执行错误,ERR.002为EXPLAIN错误 +* EXP Explain, 由explain模块给 +* FUN Function +* IDX Index, 由index模块给 +* JOI Join +* KEY Key +* KWR Keyword +* LCK Lock +* LIT Literal +* PRO Profiling, 由profiling模块给 +* RES Result +* SEC Security +* STA Standard +* SUB Subquery +* TBL Table +* TRA Trace, 由trace模块给 + +*/ + +// HeuristicRules 启发式规则列表 +var HeuristicRules map[string]Rule + +func init() { + HeuristicRules = map[string]Rule{ + "OK": { + Item: "OK", + Severity: "L0", + Summary: "✔️", // heavy check mark unicode + Content: `✔️`, + Case: "✔️", + Func: (*Query4Audit).RuleOK, + }, + "ALI.001": { + Item: "ALI.001", + Severity: "L0", + Summary: "建议使用AS关键字显示声明一个别名", + Content: `在列或表别名(如"tbl AS alias")中, 明确使用AS关键字比隐含别名(如"tbl alias")更易懂。`, + Case: "select name from tbl t1 where id < 1000", + Func: (*Query4Audit).RuleImplicitAlias, + }, + "ALI.002": { + Item: "ALI.002", + Severity: "L8", + Summary: "不建议给列通配符'*'设置别名", + Content: `例: "SELECT tbl.* col1, col2"上面这条SQL给列通配符设置了别名,这样的SQL可能存在逻辑错误。您可能意在查询col1, 但是代替它的是重命名的是tbl的最后一列。`, + Case: "select tbl.* as c1,c2,c3 from tbl where id < 1000", + Func: (*Query4Audit).RuleStarAlias, + }, + "ALI.003": { + Item: "ALI.003", + Severity: "L1", + Summary: "别名不要与表或列的名字相同", + Content: `表或列的别名与其真实名称相同, 这样的别名会使得查询更难去分辨。`, + Case: "select name from tbl as tbl where id < 1000", + Func: (*Query4Audit).RuleSameAlias, + }, + "ALT.001": { + Item: "ALT.001", + Severity: "L4", + Summary: "修改表的默认字符集不会改表各个字段的字符集", + Content: `很多初学者会将ALTER TABLE tbl_name [DEFAULT] CHARACTER SET 'UTF8'误认为会修改所有字段的字符集,但实际上它只会影响后续新增的字段不会改表已有字段的字符集。如果想修改整张表所有字段的字符集建议使用ALTER TABLE tbl_name CONVERT TO CHARACTER SET charset_name;`, + Case: "ALTER TABLE tbl_name CONVERT TO CHARACTER SET charset_name;", + Func: (*Query4Audit).RuleAlterCharset, + }, + "ALT.002": { + Item: "ALT.002", + Severity: "L2", + Summary: "同一张表的多条ALTER请求建议合为一条", + Content: `每次表结构变更对线上服务都会产生影响,即使是能够通过在线工具进行调整也请尽量通过合并ALTER请求的试减少操作次数。`, + Case: "ALTER TABLE tbl ADD COLUMN col int, ADD INDEX idx_col (`col`);", + Func: (*Query4Audit).RuleOK, // 该建议在indexAdvisor中给 + }, + "ALT.003": { + Item: "ALT.003", + Severity: "L0", + Summary: "删除列为高危操作,操作前请注意检查业务逻辑是否还有依赖", + Content: `如业务逻辑依赖未完全消除,列被删除后可能导致数据无法写入或无法查询到已删除列数据导致程序异常的情况。这种情况下即使通过备份数据回滚也会丢失用户请求写入的数据。`, + Case: "ALTER TABLE tbl DROP COLUMN col;", + Func: (*Query4Audit).RuleAlterDropColumn, + }, + "ALT.004": { + Item: "ALT.004", + Severity: "L0", + Summary: "删除主键和外键为高危操作,操作前请与DBA确认影响", + Content: `主键和外键为关系型数据库中两种重要约束,删除已有约束会打破已有业务逻辑,操作前请业务开发与DBA确认影响,三思而行。`, + Case: "ALTER TABLE tbl DROP PRIMARY KEY;", + Func: (*Query4Audit).RuleAlterDropKey, + }, + "ARG.001": { + Item: "ARG.001", + Severity: "L4", + Summary: "不建议使用前项通配符查找", + Content: `例如“%foo”,查询参数有一个前项通配符的情况无法使用已有索引。`, + Case: "select c1,c2,c3 from tbl where name like '%foo'", + Func: (*Query4Audit).RulePrefixLike, + }, + "ARG.002": { + Item: "ARG.002", + Severity: "L1", + Summary: "没有通配符的LIKE查询", + Content: `不包含通配符的LIKE查询可能存在逻辑错误,因为逻辑上它与等值查询相同。`, + Case: "select c1,c2,c3 from tbl where name like 'foo'", + Func: (*Query4Audit).RuleEqualLike, + }, + "ARG.003": { + Item: "ARG.003", + Severity: "L4", + Summary: "参数比较包含隐式转换,无法使用索引", + Content: "隐式类型转换有无法命中索引的风险,在高并发、大数据量的情况下,命不中索引带来的后果非常严重。", + Case: "SELECT * FROM sakila.film WHERE length >= '60';", + Func: (*Query4Audit).RuleOK, // 该建议在indexAdvisor中给 + }, + "ARG.004": { + Item: "ARG.004", + Severity: "L4", + Summary: "IN (NULL)/NOT IN (NULL)永远非真", + Content: "正确的作法是col IN ('val1', 'val2', 'val3') OR col IS NULL", + Case: "SELECT * FROM sakila.film WHERE length >= '60';", + Func: (*Query4Audit).RuleIn, + }, + "ARG.005": { + Item: "ARG.005", + Severity: "L1", + Summary: "IN要慎用,元素过多会导致全表扫描", + Content: ` 如:select id from t where num in(1,2,3)对于连续的数值,能用BETWEEN就不要用IN了:select id from t where num between 1 and 3。而当IN值过多时MySQL也可能会进入全表扫描导致性能急剧下降。`, + Case: "select id from t where num in(1,2,3)", + Func: (*Query4Audit).RuleIn, + }, + "ARG.006": { + Item: "ARG.006", + Severity: "L1", + Summary: "应尽量避免在WHERE子句中对字段进行NULL值判断", + Content: `使用IS NULL或IS NOT NULL将可能导致引擎放弃使用索引而进行全表扫描,如:select id from t where num is null;可以在num上设置默认值0,确保表中num列没有null值,然后这样查询: select id from t where num=0;`, + Case: "select id from t where num is null", + Func: (*Query4Audit).RuleIsNullIsNotNull, + }, + "ARG.007": { + Item: "ARG.007", + Severity: "L3", + Summary: "避免使用模式匹配", + Content: `性能问题是使用模式匹配操作符的最大缺点。使用LIKE或正则表达式进行模式匹配进行查询的另一个问题,是可能会返回意料之外的结果。最好的方案就是使用特殊的搜索引擎技术来替代SQL,比如Apache Lucene。另一个可选方案是将结果保存起来从而减少重复的搜索开销。如果一定要使用SQL,请考虑在MySQL中使用像FULLTEXT索引这样的第三方扩展。但更广泛地说,您不一定要使用SQL来解决所有问题。`, + Case: "select c_id,c2,c3 from tbl where c2 like 'test%'", + Func: (*Query4Audit).RulePatternMatchingUsage, + }, + "ARG.008": { + Item: "ARG.008", + Severity: "L1", + Summary: "OR查询索引列时请尽量使用IN谓词", + Content: `IN-list谓词可以用于索引检索,并且优化器可以对IN-list进行排序,以匹配索引的排序序列,从而获得更有效的检索。请注意,IN-list必须只包含常量,或在查询块执行期间保持常量的值,例如外引用。`, + Case: "SELECT c1,c2,c3 FROM tbl WHERE c1 = 14 OR c1 = 17", + Func: (*Query4Audit).RuleORUsage, + }, + "ARG.009": { + Item: "ARG.009", + Severity: "L1", + Summary: "引号中的字符串开头或结尾包含空格", + Content: `如果VARCHAR列的前后存在空格将可能引起逻辑问题,如在MySQL 5.5中'a'和'a '可能会在查询中被认为是相同的值。`, + Case: "SELECT 'abc '", + Func: (*Query4Audit).RuleSpaceWithQuote, + }, + "ARG.010": { + Item: "ARG.010", + Severity: "L1", + Summary: "不要使用hint,如sql_no_cache,force index,ignore key,straight join等", + Content: `hint是用来强制SQL按照某个执行计划来执行,但随着数据量变化我们无法保证自己当初的预判是正确的。`, + Case: "SELECT 'abc '", + Func: (*Query4Audit).RuleHint, + }, + "ARG.011": { + Item: "ARG.011", + Severity: "L3", + Summary: "不要使用负向查询,如:NOT IN/NOT LIKE", + Content: `请尽量不要使用负向查询,这将导致全表扫描,对查询性能影响较大。`, + Case: "select id from t where num not in(1,2,3);", + Func: (*Query4Audit).RuleNot, + }, + "CLA.001": { + Item: "CLA.001", + Severity: "L4", + Summary: "最外层SELECT未指定WHERE条件", + Content: `SELECT语句没有WHERE子句,可能检查比预期更多的行(全表扫描)。对于SELECT COUNT(*)类型的请求如果不要求精度,建议使用SHOW TABLE STATUS或EXPLAIN替代。`, + Case: "select id from tbl", + Func: (*Query4Audit).RuleNoWhere, + }, + "CLA.002": { + Item: "CLA.002", + Severity: "L3", + Summary: "不建议使用ORDER BY RAND()", + Content: `ORDER BY RAND()是从结果集中检索随机行的一种非常低效的方法,因为它会对整个结果进行排序并丢弃其大部分数据。`, + Case: "select name from tbl where id < 1000 order by rand(number)", + Func: (*Query4Audit).RuleOrderByRand, + }, + + "CLA.003": { + Item: "CLA.003", + Severity: "L2", + Summary: "不建议使用带OFFSET的LIMIT查询", + Content: `使用LIMIT和OFFSET对结果集分页的复杂度是O(n^2),并且会随着数据增大而导致性能问题。采用“书签”扫描的方法实现分页效率更高。`, + Case: "select c1,c2 from tbl where name=xx order by number limit 1 offset 20", + Func: (*Query4Audit).RuleOffsetLimit, + }, + "CLA.004": { + Item: "CLA.004", + Severity: "L2", + Summary: "不建议对常量进行GROUP BY", + Content: `GROUP BY 1 表示按第一列进行GROUP BY。如果在GROUP BY子句中使用数字,而不是表达式或列名称,当查询列顺序改变时,可能会导致问题。`, + Case: "select col1,col2 from tbl group by 1", + Func: (*Query4Audit).RuleGroupByConst, + }, + "CLA.005": { + Item: "CLA.005", + Severity: "L2", + Summary: "ORDER BY常数列没有任何意义", + Content: `SQL逻辑上可能存在错误; 最多只是一个无用的操作,不会更改查询结果。`, + Case: "select id from test where id=1 order by id", + Func: (*Query4Audit).RuleOrderByConst, + }, + "CLA.006": { + Item: "CLA.006", + Severity: "L4", + Summary: "在不同的表中GROUP BY或ORDER BY", + Content: `这将强制使用临时表和filesort,可能产生巨大性能隐患,并且可能消耗大量内存和磁盘上的临时空间。`, + Case: "select tb1.col, tb2.col from tb1, tb2 where id=1 group by tb1.col, tb2.col", + Func: (*Query4Audit).RuleDiffGroupByOrderBy, + }, + "CLA.007": { + Item: "CLA.007", + Severity: "L2", + Summary: "ORDER BY语句对多个不同条件使用不同方向的排序无法使用索引", + Content: `ORDER BY子句中的所有表达式必须按统一的ASC或DESC方向排序,以便利用索引。`, + Case: "select c1,c2,c3 from t1 where c1='foo' order by c2 desc, c3 asc", + Func: (*Query4Audit).RuleMixOrderBy, + }, + "CLA.008": { + Item: "CLA.008", + Severity: "L2", + Summary: "请为GROUP BY显示添加ORDER BY条件", + Content: `默认MySQL会对'GROUP BY col1, col2, ...'请求按如下顺序排序'ORDER BY col1, col2, ...'。如果GROUP BY语句不指定ORDER BY条件会导致无谓的排序产生,如果不需要排序建议添加'ORDER BY NULL'。`, + Case: "select c1,c2,c3 from t1 where c1='foo' group by c2", + Func: (*Query4Audit).RuleExplicitOrderBy, + }, + "CLA.009": { + Item: "CLA.009", + Severity: "L2", + Summary: "ORDER BY的条件为表达式", + Content: `当ORDER BY条件为表达式或函数时会使用到临时表,如果在未指定WHERE或WHERE条件返回的结果集较大时性能会很差。`, + Case: "select description from film where title ='ACADEMY DINOSAUR' order by length-language_id;", + Func: (*Query4Audit).RuleOrderByExpr, + }, + "CLA.010": { + Item: "CLA.010", + Severity: "L2", + Summary: "GROUP BY的条件为表达式", + Content: `当GROUP BY条件为表达式或函数时会使用到临时表,如果在未指定WHERE或WHERE条件返回的结果集较大时性能会很差。`, + Case: "select description from film where title ='ACADEMY DINOSAUR' GROUP BY length-language_id;", + Func: (*Query4Audit).RuleGroupByExpr, + }, + "CLA.011": { + Item: "CLA.011", + Severity: "L1", + Summary: "建议为表添加注释", + Content: `为表添加注释能够使得表的意义更明确,从而为日后的维护带来极大的便利。`, + Case: "CREATE TABLE `test1` (`ID` bigint(20) NOT NULL AUTO_INCREMENT,`c1` varchar(128) DEFAULT NULL,PRIMARY KEY (`ID`)) ENGINE=InnoDB DEFAULT CHARSET=utf8", + Func: (*Query4Audit).RuleTblCommentCheck, + }, + "CLA.012": { + Item: "CLA.012", + Severity: "L2", + Summary: "将复杂的裹脚布式查询分解成几个简单的查询", + Content: `SQL是一门极具表现力的语言,您可以在单个SQL查询或者单条语句中完成很多事情。但这并不意味着必须强制只使用一行代码,或者认为使用一行代码就搞定每个任务是个好主意。通过一个查询来获得所有结果的常见后果是得到了一个笛卡儿积。当查询中的两张表之间没有条件限制它们的关系时,就会发生这种情况。没有对应的限制而直接使用两张表进行联结查询,就会得到第一张表中的每一行和第二张表中的每一行的一个组合。每一个这样的组合就会成为结果集中的一行,最终您就会得到一个行数很多的结果集。重要的是要考虑这些查询很难编写、难以修改和难以调试。数据库查询请求的日益增加应该是预料之中的事。经理们想要更复杂的报告以及在用户界面上添加更多的字段。如果您的设计很复杂,并且是一个单一查询,要扩展它们就会很费时费力。不论对您还是项目来说,时间花在这些事情上面不值得。将复杂的意大利面条式查询分解成几个简单的查询。当您拆分一个复杂的SQL查询时,得到的结果可能是很多类似的查询,可能仅仅在数据类型上有所不同。编写所有的这些查询是很乏味的,因此,最好能够有个程序自动生成这些代码。SQL代码生成是一个很好的应用。尽管SQL支持用一行代码解决复杂的问题,但也别做不切实际的事情。`, + Case: "这是一条很长很长的SQL,案例略。", + Func: (*Query4Audit).RuleSpaghettiQueryAlert, + }, + /* + https://www.datacamp.com/community/tutorials/sql-tutorial-query + The HAVING Clause + The HAVING clause was originally added to SQL because the WHERE keyword could not be used with aggregate functions. HAVING is typically used with the GROUP BY clause to restrict the groups of returned rows to only those that meet certain conditions. However, if you use this clause in your query, the index is not used, which -as you already know- can result in a query that doesn't really perform all that well. + + If you’re looking for an alternative, consider using the WHERE clause. Consider the following queries: + + SELECT state, COUNT(*) + FROM Drivers + WHERE state IN ('GA', 'TX') + GROUP BY state + ORDER BY state + SELECT state, COUNT(*) + FROM Drivers + GROUP BY state + HAVING state IN ('GA', 'TX') + ORDER BY state + The first query uses the WHERE clause to restrict the number of rows that need to be summed, whereas the second query sums up all the rows in the table and then uses HAVING to throw away the sums it calculated. In these types of cases, the alternative with the WHERE clause is obviously the better one, as you don’t waste any resources. + + You see that this is not about limiting the result set, rather about limiting the intermediate number of records within a query. + + Note that the difference between these two clauses lies in the fact that the WHERE clause introduces a condition on individual rows, while the HAVING clause introduces a condition on aggregations or results of a selection where a single result, such as MIN, MAX, SUM,… has been produced from multiple rows. + */ + "CLA.013": { + Item: "CLA.013", + Severity: "L3", + Summary: "不建议使用HAVING子句", + Content: `将查询的HAVING子句改写为WHERE中的查询条件,可以在查询处理期间使用索引。`, + Case: "SELECT s.c_id,count(s.c_id) FROM s where c = test GROUP BY s.c_id HAVING s.c_id <> '1660' AND s.c_id <> '2' order by s.c_id", + Func: (*Query4Audit).RuleHavingClause, + }, + "CLA.014": { + Item: "CLA.014", + Severity: "L2", + Summary: "删除全表时建议使用TRUNCATE替代DELETE", + Content: `删除全表时建议使用TRUNCATE替代DELETE`, + Case: "delete from tbl", + Func: (*Query4Audit).RuleNoWhere, + }, + "CLA.015": { + Item: "CLA.015", + Severity: "L4", + Summary: "UPDATE未指定WHERE条件", + Content: `UPDATE不指定WHERE条件一般是致命的,请您三思后行`, + Case: "update tbl set col=1", + Func: (*Query4Audit).RuleNoWhere, + }, + "CLA.016": { + Item: "CLA.016", + Severity: "L2", + Summary: "不要UPDATE主键", + Content: `主键是数据表中记录的唯一标识符,不建议频繁更新主键列,这将影响元数据统计信息进而影响正常的查询。`, + Case: "update tbl set col=1", + Func: (*Query4Audit).RuleOK, // 该建议在indexAdvisor中给 + }, + "CLA.017": { + Item: "CLA.017", + Severity: "L2", + Summary: "不建议使用存储过程、视图、触发器、临时表等", + Content: `这些功能的使用在一定程度上会使得程序难以调试和拓展,更没有移植性,且会极大的增加出现BUG的概率。`, + Case: "CREATE VIEW v_today (today) AS SELECT CURRENT_DATE;", + Func: (*Query4Audit).RuleForbiddenSyntax, + }, + "COL.001": { + Item: "COL.001", + Severity: "L1", + Summary: "不建议使用SELECT * 类型查询", + Content: `当表结构变更时,使用*通配符选择所有列将导致查询的含义和行为会发生更改,可能导致查询返回更多的数据。`, + Case: "select * from tbl where id=1", + Func: (*Query4Audit).RuleSelectStar, + }, + "COL.002": { + Item: "COL.002", + Severity: "L2", + Summary: "INSERT未指定列名", + Content: `当表结构发生变更,如果INSERT或REPLACE请求不明确指定列名,请求的结果将会与预想的不同; 建议使用“INSERT INTO tbl(col1,col2)VALUES ...”代替。`, + Case: "insert into tbl values(1,'name')", + Func: (*Query4Audit).RuleInsertColDef, + }, + "COL.003": { + Item: "COL.003", + Severity: "L2", + Summary: "建议修改自增ID为无符号类型", + Content: `建议修改自增ID为无符号类型`, + Case: "create table test(`id` int(11) NOT NULL AUTO_INCREMENT)", + Func: (*Query4Audit).RuleAutoIncUnsigned, + }, + "COL.004": { + Item: "COL.004", + Severity: "L1", + Summary: "请为列添加默认值", + Content: `请为列添加默认值,如果是ALTER操作,请不要忘记将原字段的默认值写上。字段无默认值,当表较大时无法在线变更表结构。`, + Case: "CREATE TABLE tbl (col int) ENGINE=InnoDB;", + Func: (*Query4Audit).RuleAddDefaultValue, + }, + "COL.005": { + Item: "COL.005", + Severity: "L1", + Summary: "列未添加注释", + Content: `建议对表中每个列添加注释,来明确每个列在表中的含义及作用。`, + Case: "CREATE TABLE tbl (col int) ENGINE=InnoDB;", + Func: (*Query4Audit).RuleColCommentCheck, + }, + "COL.006": { + Item: "COL.006", + Severity: "L3", + Summary: "表中包含有太多的列", + Content: `表中包含有太多的列`, + Case: "CREATE TABLE tbl ( cols ....);", + Func: (*Query4Audit).RuleTooManyFields, + }, + "COL.008": { + Item: "COL.008", + Severity: "L1", + Summary: "可使用VARCHAR代替CHAR,VARBINARY代替BINARY", + Content: `为首先变长字段存储空间小,可以节省存储空间。其次对于查询来说,在一个相对较小的字段内搜索效率显然要高些。`, + Case: "create table t1(id int,name char(20),last_time date)", + Func: (*Query4Audit).RuleVarcharVSChar, + }, + "COL.009": { + Item: "COL.009", + Severity: "L2", + Summary: "建议使用精确的数据类型", + Content: `实际上,任何使用FLOAT、REAL或DOUBLE PRECISION数据类型的设计都有可能是反模式。大多数应用程序使用的浮点数的取值范围并不需要达到IEEE 754标准所定义的最大/最小区间。在计算总量时,非精确浮点数所积累的影响是严重的。使用SQL中的NUMERIC或DECIMAL类型来代替FLOAT及其类似的数据类型进行固定精度的小数存储。这些数据类型精确地根据您定义这一列时指定的精度来存储数据。尽可能不要使用浮点数。`, + Case: "CREATE TABLE tab2 (p_id BIGINT UNSIGNED NOT NULL,a_id BIGINT UNSIGNED NOT NULL,hours float not null,PRIMARY KEY (p_id, a_id))", + Func: (*Query4Audit).RuleImpreciseDataType, + }, + "COL.010": { + Item: "COL.010", + Severity: "L2", + Summary: "不建议使用ENUM数据类型", + Content: `ENUM定义了列中值的类型,使用字符串表示ENUM里的值时,实际存储在列中的数据是这些值在定义时的序数。因此,这列的数据是字节对齐的,当您进行一次排序查询时,结果是按照实际存储的序数值排序的,而不是按字符串值的字母顺序排序的。这可能不是您所希望的。没有什么语法支持从ENUM或者check约束中添加或删除一个值;您只能使用一个新的集合重新定义这一列。如果您打算废弃一个选项,您可能会为历史数据而烦恼。作为一种策略,改变元数据——也就是说,改变表和列的定义——应该是不常见的,并且要注意测试和质量保证。有一个更好的解决方案来约束一列中的可选值:创建一张检查表,每一行包含一个允许在列中出现的候选值;然后在引用新表的旧表上声明一个外键约束。`, + Case: "create table tab1(status ENUM('new','in progress','fixed'))", + Func: (*Query4Audit).RuleValuesInDefinition, + }, + // 这个建议从sqlcheck迁移来的,实际生产环境每条建表SQL都会给这条建议,看多了会不开心。 + "COL.011": { + Item: "COL.011", + Severity: "L0", + Summary: "当需要唯一约束时才使用NULL,仅当列不能有缺失值时才使用NOT NULL", + Content: `NULL和0是不同的,10乘以NULL还是NULL。NULL和空字符串是不一样的。将一个字符串和标准SQL中的NULL联合起来的结果还是NULL。NULL和FALSE也是不同的。AND、OR和NOT这三个布尔操作如果涉及NULL,其结果也让很多人感到困惑。当您将一列声明为NOT NULL时,也就是说这列中的每一个值都必须存在且是有意义的。使用NULL来表示任意类型不存在的空值。 当您将一列声明为NOT NULL时,也就是说这列中的每一个值都必须存在且是有意义的。`, + Case: "select c1,c2,c3 from tbl where c4 is null or c4 <> 1", + Func: (*Query4Audit).RuleNullUsage, + }, + "COL.012": { + Item: "COL.012", + Severity: "L5", + Summary: "BLOB和TEXT类型的字段不可设置为NULL", + Content: `BLOB和TEXT类型的字段不可设置为NULL`, + Case: "CREATE TABLE `tbl` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `c` longblob, PRIMARY KEY (`id`));", + Func: (*Query4Audit).RuleCantBeNull, + }, + "COL.013": { + Item: "COL.013", + Severity: "L4", + Summary: "TIMESTAMP类型未设置默认值", + Content: `TIMESTAMP类型未设置默认值`, + Case: "CREATE TABLE tbl( `id` bigint not null, `create_time` timestamp);", + Func: (*Query4Audit).RuleTimestampDefault, + }, + "COL.014": { + Item: "COL.014", + Severity: "L5", + Summary: "为列指定了字符集", + Content: `建议列与表使用同一个字符集,不要单独指定列的字符集。`, + Case: "CREATE TABLE `tb2` ( `id` int(11) DEFAULT NULL, `col` char(10) CHARACTER SET utf8 DEFAULT NULL)", + Func: (*Query4Audit).RuleColumnWithCharset, + }, + "COL.015": { + Item: "COL.015", + Severity: "L4", + Summary: "BLOB类型的字段不可指定默认值", + Content: `BLOB类型的字段不可指定默认值`, + Case: "CREATE TABLE `tbl` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `c` blob NOT NULL DEFAULT '', PRIMARY KEY (`id`));", + Func: (*Query4Audit).RuleBlobDefaultValue, + }, + "COL.016": { + Item: "COL.016", + Severity: "L1", + Summary: "整型定义建议采用INT(10)或BIGINT(20)", + Content: `INT(M) 在 integer 数据类型中,M 表示最大显示宽度。 在 INT(M) 中,M 的值跟 INT(M) 所占多少存储空间并无任何关系。 INT(3)、INT(4)、INT(8) 在磁盘上都是占用 4 bytes 的存储空间。`, + Case: "CREATE TABLE tab (a INT(1));", + Func: (*Query4Audit).RuleIntPrecision, + }, + "COL.017": { + Item: "COL.017", + Severity: "L2", + Summary: "varchar定义长度过长", + Content: fmt.Sprintf(`varchar 是可变长字符串,不预先分配存储空间,长度不要超过%d,如果存储长度过长MySQL将定义字段类型为text,独立出来一张表,用主键来对应,避免影响其它字段索引效率。`, common.Config.MaxVarcharLength), + Case: "CREATE TABLE tab (a varchar(3500));", + Func: (*Query4Audit).RuleVarcharLength, + }, + "DIS.001": { + Item: "DIS.001", + Severity: "L1", + Summary: "消除不必要的DISTINCT条件", + Content: `太多DISTINCT条件是复杂的裹脚布式查询的症状。考虑将复杂查询分解成许多简单的查询,并减少DISTINCT条件的数量。如果主键列是列的结果集的一部分,则DISTINCT条件可能没有影响。`, + Case: "SELECT DISTINCT c.c_id,count(DISTINCT c.c_name),count(DISTINCT c.c_e),count(DISTINCT c.c_n),count(DISTINCT c.c_me),c.c_d FROM (select distinct xing, name from B) as e WHERE e.country_id = c.country_id", + Func: (*Query4Audit).RuleDistinctUsage, + }, + "DIS.002": { + Item: "DIS.002", + Severity: "L3", + Summary: "COUNT(DISTINCT)多列时结果可能和你预想的不同", + Content: `COUNT(DISTINCT col)计算该列除NULL之外的不重复行数,注意COUNT(DISTINCT col, col2)如果其中一列全为NULL那么即使另一列有不同的值,也返回0。`, + Case: "SELECT COUNT(DISTINCT col, col2) FROM tbl;", + Func: (*Query4Audit).RuleCountDistinctMultiCol, + }, + // DIS.003灵感来源于如下链接 + // http://www.ijstr.org/final-print/oct2015/Query-Optimization-Techniques-Tips-For-Writing-Efficient-And-Faster-Sql-Queries.pdf + "DIS.003": { + Item: "DIS.003", + Severity: "L3", + Summary: "DISTINCT *对有主键的表没有意义", + Content: `当表已经有主键时,对所有列进行DISTINCT的输出结果与不进行DISTINCT操作的结果相同,请不要画蛇添足。`, + Case: "SELECT DISTINCT * FROM film;", + Func: (*Query4Audit).RuleDistinctStar, + }, + "FUN.001": { + Item: "FUN.001", + Severity: "L2", + Summary: "避免在WHERE条件中使用函数或其他运算符", + Content: `虽然在SQL中使用函数可以简化很多复杂的查询,但使用了函数的查询无法利用表中已经建立的索引,该查询将会是全表扫描,性能较差。通常建议将列名写在比较运算符左侧,将查询过滤条件放在比较运算符右侧。`, + Case: "select id from t where substring(name,1,3)='abc'", + Func: (*Query4Audit).RuleCompareWithFunction, + }, + "FUN.002": { + Item: "FUN.002", + Severity: "L1", + Summary: "指定了WHERE条件或非MyISAM引擎时使用COUNT(*)操作性能不佳", + Content: `COUNT(*)的作用是统计表行数,COUNT(COL)的作用是统计指定列非NULL的行数。MyISAM表对于COUNT(*)统计全表行数进行了特殊的优化,通常情况下非常快。但对于非MyISAM表或指定了某些WHERE条件,COUNT(*)操作需要扫描大量的行才能获取精确的结果,性能也因此不佳。有时候某些业务场景并不需要完全精确的COUNT值,此时可以用近似值来代替。EXPLAIN出来的优化器估算的行数就是一个不错的近似值,执行EXPLAIN并不需要真正去执行查询,所以成本很低。`, + Case: "SELECT c3, COUNT(*) AS accounts FROM tab where c2 < 10000 GROUP BY c3 ORDER BY num", + Func: (*Query4Audit).RuleCountStar, + }, + "FUN.003": { + Item: "FUN.003", + Severity: "L3", + Summary: "使用了合并为可空列的字符串连接", + Content: `在一些查询请求中,您需要强制让某一列或者某个表达式返回非NULL的值,从而让查询逻辑变得更简单,担忧不想将这个值存下来。使用COALESCE()函数来构造连接的表达式,这样即使是空值列也不会使整表达式变为NULL。`, + Case: "select c1 || coalesce(' ' || c2 || ' ', ' ') || c3 as c from tbl", + Func: (*Query4Audit).RuleStringConcatenation, + }, + "FUN.004": { + Item: "FUN.004", + Severity: "L4", + Summary: "不建议使用SYSDATE()函数", + Content: `SYSDATE()函数可能导致主从数据不一致,请使用NOW()函数替代SYSDATE()。`, + Case: "SELECT SYSDATE();", + Func: (*Query4Audit).RuleSysdate, + }, + "FUN.005": { + Item: "FUN.005", + Severity: "L1", + Summary: "不建议使用COUNT(col)或COUNT(常量)", + Content: `不要使用COUNT(col)或COUNT(常量)来替代COUNT(*),COUNT(*)是SQL92定义的标准统计行数的方法,跟数据无关,跟NULL和非NULL也无关。`, + Case: "SELECT COUNT(1) FROM tbl;", + Func: (*Query4Audit).RuleCountConst, + }, + "FUN.006": { + Item: "FUN.006", + Severity: "L1", + Summary: "使用SUM(COL)时需注意NPE问题", + Content: `当某一列的值全是NULL时,COUNT(COL)的返回结果为0,但SUM(COL)的返回结果为NULL,因此使用SUM()时需注意NPE问题。可以使用如下方式来避免SUM的NPE问题: SELECT IF(ISNULL(SUM(COL)), 0, SUM(COL)) FROM tbl`, + Case: "SELECT SUM(COL) FROM tbl;", + Func: (*Query4Audit).RuleSumNPE, + }, + "GRP.001": { + Item: "GRP.001", + Severity: "L2", + Summary: "不建议对等值查询列使用GROUP BY", + Content: `GROUP BY中的列在前面的WHERE条件中使用了等值查询,对这样的列进行GROUP BY意义不大。`, + Case: "select film_id, title from film where release_year='2006' group by release_year", + Func: (*Query4Audit).RuleOK, // 该建议在indexAdvisor中给 + }, + "JOI.001": { + Item: "JOI.001", + Severity: "L2", + Summary: "JOIN语句混用逗号和ANSI模式", + Content: `表连接的时候混用逗号和ANSI JOIN不便于人类理解,并且MySQL不同版本的表连接行为和优先级均有所不同,当MySQL版本变化后可能会引入错误。`, + Case: "select c1,c2,c3 from t1,t2 join t3 on t1.c1=t2.c1,t1.c3=t3,c1 where id>1000", + Func: (*Query4Audit).RuleCommaAnsiJoin, + }, + "JOI.002": { + Item: "JOI.002", + Severity: "L4", + Summary: "同一张表被连接两次", + Content: `相同的表在FROM子句中至少出现两次,可以简化为对该表的单次访问。`, + Case: "select tb1.col from (tb1, tb2) join tb2 on tb1.id=tb.id where tb1.id=1", + Func: (*Query4Audit).RuleDupJoin, + }, + "JOI.003": { + Item: "JOI.003", + Severity: "L4", + Summary: "OUTER JOIN失效", + Content: `由于WHERE条件错误使得OUTER JOIN的外部表无数据返回,这会将查询隐式转换为 INNER JOIN 。如:select c from L left join R using(c) where L.a=5 and R.b=10。这种SQL逻辑上可能存在错误或程序员对OUTER JOIN如何工作存在误解,因为LEFT/RIGHT JOIN是LEFT/RIGHT OUTER JOIN的缩写。`, + Case: "select c1,c2,c3 from t1 left outer join t2 using(c1) where t1.c2=2 and t2.c3=4", + Func: (*Query4Audit).RuleOK, // TODO + }, + "JOI.004": { + Item: "JOI.004", + Severity: "L4", + Summary: "不建议使用排它JOIN", + Content: `只在右侧表为NULL的带WHERE子句的LEFT OUTER JOIN语句,有可能是在WHERE子句中使用错误的列,如:“... FROM l LEFT OUTER JOIN r ON l.l = r.r WHERE r.z IS NULL”,这个查询正确的逻辑可能是 WHERE r.r IS NULL。`, + Case: "select c1,c2,c3 from t1 left outer join t2 on t1.c1=t2.c1 where t2.c2 is null", + Func: (*Query4Audit).RuleOK, // TODO + }, + "JOI.005": { + Item: "JOI.005", + Severity: "L2", + Summary: "减少JOIN的数量", + Content: `太多的JOIN是复杂的裹脚布式查询的症状。考虑将复杂查询分解成许多简单的查询,并减少JOIN的数量。`, + Case: "select bp1.p_id, b1.d_d as l, b1.b_id from b1 join bp1 on (b1.b_id = bp1.b_id) left outer join (b1 as b2 join bp2 on (b2.b_id = bp2.b_id)) on (bp1.p_id = bp2.p_id ) join bp21 on (b1.b_id = bp1.b_id) join bp31 on (b1.b_id = bp1.b_id) join bp41 on (b1.b_id = bp1.b_id) where b2.b_id = 0", + Func: (*Query4Audit).RuleReduceNumberOfJoin, + }, + "JOI.006": { + Item: "JOI.006", + Severity: "L4", + Summary: "将嵌套查询重写为JOIN通常会导致更高效的执行和更有效的优化", + Content: `一般来说,非嵌套子查询总是用于关联子查询,最多是来自FROM子句中的一个表,这些子查询用于ANY、ALL和EXISTS的谓词。如果可以根据查询语义决定子查询最多返回一个行,那么一个不相关的子查询或来自FROM子句中的多个表的子查询就被压平了。`, + Case: "SELECT s,p,d FROM tbl WHERE p.p_id = (SELECT s.p_id FROM tbl WHERE s.c_id = 100996 AND s.q = 1 )", + Func: (*Query4Audit).RuleNestedSubQueries, + }, + "JOI.007": { + Item: "JOI.007", + Severity: "L4", + Summary: "不建议使用联表更新", + Content: `当需要同时更新多张表时建议使用简单SQL,一条SQL只更新一张表,尽量不要将多张表的更新在同一条SQL中完成。`, + Case: "UPDATE users u LEFT JOIN hobby h ON u.id = h.uid SET u.name = 'pianoboy' WHERE h.hobby = 'piano';", + Func: (*Query4Audit).RuleMultiDeleteUpdate, + }, + "JOI.008": { + Item: "JOI.008", + Severity: "L4", + Summary: "不要使用跨DB的Join查询", + Content: `一般来说,跨DB的Join查询意味着查询语句跨越了两个不同的子系统,这可能意味着系统耦合度过高或库表结构设计不合理。`, + Case: "SELECT s,p,d FROM tbl WHERE p.p_id = (SELECT s.p_id FROM tbl WHERE s.c_id = 100996 AND s.q = 1 )", + Func: (*Query4Audit).RuleMultiDBJoin, + }, + // TODO: 跨库事务的检查,目前SOAR未对事务做处理 + "KEY.001": { + Item: "KEY.001", + Severity: "L2", + Summary: "建议使用自增列作为主键,如使用联合自增主键时请将自增键作为第一列", + Content: `建议使用自增列作为主键,如使用联合自增主键时请将自增键作为第一列`, + Case: "create table test(`id` int(11) NOT NULL PRIMARY KEY (`id`))", + Func: (*Query4Audit).RulePKNotInt, + }, + "KEY.002": { + Item: "KEY.002", + Severity: "L4", + Summary: "无主键或唯一键,无法在线变更表结构", + Content: `无主键或唯一键,无法在线变更表结构`, + Case: "create table test(col varchar(5000))", + Func: (*Query4Audit).RuleNoOSCKey, + }, + "KEY.003": { + Item: "KEY.003", + Severity: "L4", + Summary: "避免外键等递归关系", + Content: `存在递归关系的数据很常见,数据常会像树或者以层级方式组织。然而,创建一个外键约束来强制执行同一表中两列之间的关系,会导致笨拙的查询。树的每一层对应着另一个连接。您将需要发出递归查询,以获得节点的所有后代或所有祖先。解决方案是构造一个附加的闭包表。它记录了树中所有节点间的关系,而不仅仅是那些具有直接的父子关系。您也可以比较不同层次的数据设计:闭包表,路径枚举,嵌套集。然后根据应用程序的需要选择一个。`, + Case: "CREATE TABLE tab2 (p_id BIGINT UNSIGNED NOT NULL,a_id BIGINT UNSIGNED NOT NULL,PRIMARY KEY (p_id, a_id),FOREIGN KEY (p_id) REFERENCES tab1(p_id),FOREIGN KEY (a_id) REFERENCES tab3(a_id))", + Func: (*Query4Audit).RuleRecursiveDependency, + }, + // TODO: 新增复合索引,字段按散粒度是否由大到小排序,区分度最高的在最左边 + "KEY.004": { + Item: "KEY.004", + Severity: "L0", + Summary: "提醒:请将索引属性顺序与查询对齐", + Content: `如果为列创建复合索引,请确保查询属性与索引属性的顺序相同,以便DBMS在处理查询时使用索引。如果查询和索引属性订单没有对齐,那么DBMS可能无法在查询处理期间使用索引。`, + Case: "create index idx1 on tbl (last_name,first_name)", + Func: (*Query4Audit).RuleIndexAttributeOrder, + }, + "KEY.005": { + Item: "KEY.005", + Severity: "L2", + Summary: "表建的索引过多", + Content: `表建的索引过多`, + Case: "CREATE TABLE tbl ( a int, b int, c int, KEY idx_a (`a`),KEY idx_b(`b`),KEY idx_c(`c`));", + Func: (*Query4Audit).RuleTooManyKeys, + }, + "KEY.006": { + Item: "KEY.006", + Severity: "L4", + Summary: "主键中的列过多", + Content: `主键中的列过多`, + Case: "CREATE TABLE tbl ( a int, b int, c int, PRIMARY KEY(`a`,`b`,`c`));", + Func: (*Query4Audit).RuleTooManyKeyParts, + }, + "KEY.007": { + Item: "KEY.007", + Severity: "L4", + Summary: "未指定主键或主键非int或bigint", + Content: `未指定主键或主键非int或bigint,建议将主键设置为int unsigned或bigint unsigned。`, + Case: "CREATE TABLE tbl (a int);", + Func: (*Query4Audit).RulePKNotInt, + }, + "KEY.008": { + Item: "KEY.008", + Severity: "L4", + Summary: "ORDER BY多个列但排序方向不同时可能无法使用索引", + Content: `在MySQL 8.0之前当ORDER BY多个列指定的排序方向不同时将无法使用已经建立的索引。`, + Case: "SELECT * FROM tbl ORDER BY a DESC, b ASC;", + Func: (*Query4Audit).RuleOrderByMultiDirection, + }, + "KEY.009": { + Item: "KEY.009", + Severity: "L0", + Summary: "添加唯一索引前请注意检查数据唯一性", + Content: `请提前检查添加唯一索引列的数据唯一性,如果数据不唯一在线表结构调整时将有可能自动将重复列删除,这有可能导致数据丢失。`, + Case: "CREATE UNIQUE INDEX part_of_name ON customer (name(10));", + Func: (*Query4Audit).RuleUniqueKeyDup, + }, + "KWR.001": { + Item: "KWR.001", + Severity: "L2", + Summary: "SQL_CALC_FOUND_ROWS效率低下", + Content: `因为SQL_CALC_FOUND_ROWS不能很好地扩展,所以可能导致性能问题; 建议业务使用其他策略来替代SQL_CALC_FOUND_ROWS提供的计数功能,比如:分页结果展示等。`, + Case: "select SQL_CALC_FOUND_ROWS col from tbl where id>1000", + Func: (*Query4Audit).RuleSQLCalcFoundRows, + }, + "KWR.002": { + Item: "KWR.002", + Severity: "L2", + Summary: "不建议使用MySQL关键字做列名或表名", + Content: `当使用关键字做为列名或表名时程序需要对列名和表名进行转义,如果疏忽被将导致请求无法执行。`, + Case: "CREATE TABLE tbl ( `select` int )", + Func: (*Query4Audit).RuleUseKeyWord, + }, + "KWR.003": { + Item: "KWR.003", + Severity: "L1", + Summary: "不建议使用复数做列名或表名", + Content: `表名应该仅仅表示表里面的实体内容,不应该表示实体数量,对应于 DO 类名也是单数形式,符合表达习惯。`, + Case: "CREATE TABLE tbl ( `books` int )", + Func: (*Query4Audit).RulePluralWord, + }, + "LCK.001": { + Item: "LCK.001", + Severity: "L3", + Summary: "INSERT INTO xx SELECT加锁粒度较大请谨慎", + Content: `INSERT INTO xx SELECT加锁粒度较大请谨慎`, + Case: "INSERT INTO tbl SELECT * FROM tbl2;", + Func: (*Query4Audit).RuleInsertSelect, + }, + "LCK.002": { + Item: "LCK.002", + Severity: "L3", + Summary: "请慎用INSERT ON DUPLICATE KEY UPDATE", + Content: `当主键为自增键时使用INSERT ON DUPLICATE KEY UPDATE可能会导致主键出现大量不连续快速增长,导致主键快速溢出无法继续写入。极端情况下还有可能导致主从数据不一致。`, + Case: "INSERT INTO t1(a,b,c) VALUES (1,2,3) ON DUPLICATE KEY UPDATE c=c+1;", + Func: (*Query4Audit).RuleInsertOnDup, + }, + "LIT.001": { + Item: "LIT.001", + Severity: "L2", + Summary: "用字符类型存储IP地址", + Content: `字符串字面上看起来像IP地址,但不是INET_ATON()的参数,表示数据被存储为字符而不是整数。将IP地址存储为整数更为有效。`, + Case: "insert into tbl (IP,name) values('10.20.306.122','test')", + Func: (*Query4Audit).RuleIPString, + }, + "LIT.002": { + Item: "LIT.002", + Severity: "L4", + Summary: "日期/时间未使用引号括起", + Content: `诸如“WHERE col <2010-02-12”之类的查询是有效的SQL,但可能是一个错误,因为它将被解释为“WHERE col <1996”; 日期/时间文字应该加引号。`, + Case: "select col1,col2 from tbl where time < 2018-01-10", + Func: (*Query4Audit).RuleDataNotQuote, + }, + "LIT.003": { + Item: "LIT.003", + Severity: "L3", + Summary: "一列中存储一系列相关数据的集合", + Content: `将ID存储为一个列表,作为VARCHAR/TEXT列,这样能导致性能和数据完整性问题。查询这样的列需要使用模式匹配的表达式。使用逗号分隔的列表来做多表联结查询定位一行数据是极不优雅和耗时的。这将使验证ID更加困难。考虑一下,列表最多支持存放多少数据呢?将ID存储在一张单独的表中,代替使用多值属性,从而每个单独的属性值都可以占据一行。这样交叉表实现了两张表之间的多对多关系。这将更好地简化查询,也更有效地验证ID。`, + Case: "select c1,c2,c3,c4 from tab1 where col_id REGEXP '[[:<:]]12[[:>:]]'", + Func: (*Query4Audit).RuleMultiValueAttribute, + }, + "LIT.004": { + Item: "LIT.004", + Severity: "L1", + Summary: "请使用分号或已设定的DELIMITER结尾", + Content: `USE database, SHOW DATABASES等命令也需要使用使用分号或已设定的DELIMITER结尾。`, + Case: "USE db", + Func: (*Query4Audit).RuleOK, // TODO: RuleAddDelimiter + }, + "RES.001": { + Item: "RES.001", + Severity: "L4", + Summary: "非确定性的GROUP BY", + Content: `SQL返回的列既不在聚合函数中也不是GROUP BY表达式的列中,因此这些值的结果将是非确定性的。如:select a, b, c from tbl where foo="bar" group by a,该SQL返回的结果就是不确定的。`, + Case: "select c1,c2,c3 from t1 where c2='foo' group by c2", + Func: (*Query4Audit).RuleNoDeterministicGroupby, + }, + "RES.002": { + Item: "RES.002", + Severity: "L4", + Summary: "未使用ORDER BY的LIMIT查询", + Content: `没有ORDER BY的LIMIT会导致非确定性的结果,这取决于查询执行计划。`, + Case: "select col1,col2 from tbl where name=xx limit 10", + Func: (*Query4Audit).RuleNoDeterministicLimit, + }, + "RES.003": { + Item: "RES.003", + Severity: "L4", + Summary: "UPDATE/DELETE操作使用了LIMIT条件", + Content: `UPDATE/DELETE操作使用LIMIT条件和不添加WHERE条件一样危险,它可将会导致主从数据不一致或从库同步中断。`, + Case: "UPDATE film SET length = 120 WHERE title = 'abc' LIMIT 1;", + Func: (*Query4Audit).RuleUpdateDeleteWithLimit, + }, + "RES.004": { + Item: "RES.004", + Severity: "L4", + Summary: "UPDATE/DELETE操作指定了ORDER BY条件", + Content: `UPDATE/DELETE操作不要指定ORDER BY条件。`, + Case: "UPDATE film SET length = 120 WHERE title = 'abc' ORDER BY title", + Func: (*Query4Audit).RuleUpdateDeleteWithOrderby, + }, + "RES.005": { + Item: "RES.005", + Severity: "L4", + Summary: "UPDATE可能存在逻辑错误,导致数据损坏", + Content: "", + Case: "update tbl set col = 1 and cl = 2 where col=3;", + Func: (*Query4Audit).RuleUpdateSetAnd, + }, + "RES.006": { + Item: "RES.006", + Severity: "L4", + Summary: "永远不真的比较条件", + Content: "查询条件永远非真,这将导致查询无匹配到的结果。", + Case: "select * from tbl where 1 != 1;", + Func: (*Query4Audit).RuleImpossibleWhere, + }, + "RES.007": { + Item: "RES.007", + Severity: "L4", + Summary: "永远为真的比较条件", + Content: "查询条件永远为真,这将导致WHERE条件失效进行全表查询。", + Case: "select * from tbl where 1 = 1;", + Func: (*Query4Audit).RuleMeaninglessWhere, + }, + "RES.008": { + Item: "RES.008", + Severity: "L2", + Summary: "不建议使用LOAD DATA/SELECT ... INTO OUTFILE", + Content: "SELECT INTO OUTFILE需要授予FILE权限,这通过会引入安全问题。LOAD DATA虽然可以提高数据导入速度,但同时也可能导致从库同步延迟过大。", + Case: "LOAD DATA INFILE 'data.txt' INTO TABLE db2.my_table;", + Func: (*Query4Audit).RuleLoadFile, + }, + "SEC.001": { + Item: "SEC.001", + Severity: "L0", + Summary: "请谨慎使用TRUNCATE操作", + Content: `一般来说想清空一张表最快速的做法就是使用TRUNCATE TABLE tbl_name;语句。但TRUNCATE操作也并非是毫无代价的,TRUNCATE TABLE无法返回被删除的准确行数,如果需要返回被删除的行数建议使用DELETE语法。TRUNCATE操作还会重置AUTO_INCREMENT,如果不想重置该值建议使用DELETE FROM tbl_name WHERE 1;替代。TRUNCATE操作会对数据字典添加源数据锁(MDL),当一次需要TRUNCATE很多表时会影响整个实例的所有请求,因此如果要TRUNCATE多个表建议用DROP+CREATE的方式以减少锁时长。`, + Case: "TRUNCATE TABLE tbl_name", + Func: (*Query4Audit).RuleTruncateTable, + }, + "SEC.002": { + Item: "SEC.002", + Severity: "L0", + Summary: "不使用明文存储密码", + Content: `使用明文存储密码或者使用明文在网络上传递密码都是不安全的。如果攻击者能够截获您用来插入密码的SQL语句,他们就能直接读到密码。另外,将用户输入的字符串以明文的形式插入到纯SQL语句中,也会让攻击者发现它。如果您能够读取密码,黑客也可以。解决方案是使用单向哈希函数对原始密码进行加密编码。哈希是指将输入字符串转化成另一个新的、不可识别的字符串的函数。对密码加密表达式加点随机串来防御“字典攻击”。不要将明文密码输入到SQL查询语句中。在应用程序代码中计算哈希串,只在SQL查询中使用哈希串。`, + Case: "create table test(id int,name varchar(20) not null,password varchar(200)not null)", + Func: (*Query4Audit).RuleReadablePasswords, + }, + "SEC.003": { + Item: "SEC.003", + Severity: "L0", + Summary: "使用DELETE/DROP/TRUNCATE等操作时注意备份", + Content: `在执行高危操作之前对数据进行备份是十分有必要的。`, + Case: "delete from table where col = 'condition'", + Func: (*Query4Audit).RuleDataDrop, + }, + "STA.001": { + Item: "STA.001", + Severity: "L0", + Summary: "'!=' 运算符是非标准的", + Content: `"<>"才是标准SQL中的不等于运算符。`, + Case: "select col1,col2 from tbl where type!=0", + Func: (*Query4Audit).RuleStandardINEQ, + }, + "STA.002": { + Item: "STA.002", + Severity: "L1", + Summary: "库名或表名点后建议不要加空格", + Content: `当使用db.table或table.column格式访问表或字段时,请不要在点号后面添加空格,虽然这样语法正确。`, + Case: "select col from sakila. film", + Func: (*Query4Audit).RuleSpaceAfterDot, + }, + "STA.003": { + Item: "STA.003", + Severity: "L1", + Summary: "索引起名不规范", + Content: `建议普通二级索引以idx_为前缀,唯一索引以uk_为前缀。`, + Case: "select col from now where type!=0", + Func: (*Query4Audit).RuleIdxPrefix, + }, + "STA.004": { + Item: "STA.004", + Severity: "L1", + Summary: "起名时请不要使用字母、数字和下划线之外的字符", + Content: `以字母或下划线开头,名字只允许使用字母、数字和下划线。请统一大小写,不要使用驼峰命名法。不要在名字中出现连续下划线'__',这样很难辨认。`, + Case: "CREATE TABLE ` abc` (a int);", + Func: (*Query4Audit).RuleStandardName, + }, + "SUB.001": { + Item: "SUB.001", + Severity: "L4", + Summary: "MySQL对子查询的优化效果不佳", + Content: `MySQL将外部查询中的每一行作为依赖子查询执行子查询。 这是导致严重性能问题的常见原因。这可能会在 MySQL 5.6版本中得到改善, 但对于5.1及更早版本, 建议将该类查询分别重写为JOIN或LEFT OUTER JOIN。`, + Case: "select col1,col2,col3 from table1 where col2 in(select col from table2)", + Func: (*Query4Audit).RuleInSubquery, + }, + "SUB.002": { + Item: "SUB.002", + Severity: "L2", + Summary: "如果您不在乎重复的话,建议使用UNION ALL替代UNION", + Content: `与去除重复的UNION不同,UNION ALL允许重复元组。如果您不关心重复元组,那么使用UNION ALL将是一个更快的选项。`, + Case: "select teacher_id as id,people_name as name from t1,t2 where t1.teacher_id=t2.people_id union select student_id as id,people_name as name from t1,t2 where t1.student_id=t2.people_id", + Func: (*Query4Audit).RuleUNIONUsage, + }, + "SUB.003": { + Item: "SUB.003", + Severity: "L3", + Summary: "考虑使用EXISTS而不是DISTINCT子查询", + Content: `DISTINCT关键字在对元组排序后删除重复。相反,考虑使用一个带有EXISTS关键字的子查询,您可以避免返回整个表。`, + Case: "SELECT DISTINCT c.c_id, c.c_name FROM c,e WHERE e.c_id = c.c_id", + Func: (*Query4Audit).RuleDistinctJoinUsage, + }, + // TODO: 5.6有了semi join还要把in转成exists么? + // Use EXISTS instead of IN to check existence of data. + // http://www.winwire.com/25-tips-to-improve-sql-query-performance/ + "SUB.004": { + Item: "SUB.004", + Severity: "L3", + Summary: "执行计划中嵌套连接深度过深", + Content: `MySQL对子查询的优化效果不佳,MySQL将外部查询中的每一行作为依赖子查询执行子查询。 这是导致严重性能问题的常见原因。`, + Case: "SELECT * from tb where id in (select id from (select id from tb))", + Func: (*Query4Audit).RuleSubqueryDepth, + }, + // SUB.005灵感来自 https://blog.csdn.net/zhuocr/article/details/61192418 + "SUB.005": { + Item: "SUB.005", + Severity: "L8", + Summary: "子查询不支持LIMIT", + Content: `当前MySQL版本不支持在子查询中进行'LIMIT & IN/ALL/ANY/SOME'。`, + Case: "SELECT * FROM staff WHERE name IN (SELECT NAME FROM customer ORDER BY name LIMIT 1)", + Func: (*Query4Audit).RuleSubQueryLimit, + }, + "SUB.006": { + Item: "SUB.006", + Severity: "L2", + Summary: "不建议在子查询中使用函数", + Content: `MySQL将外部查询中的每一行作为依赖子查询执行子查询,如果在子查询中使用函数,即使是semi-join也很难进行高效的查询。可以将子查询重写为OUTER JOIN语句并用连接条件对数据进行过滤。`, + Case: "SELECT * FROM staff WHERE name IN (SELECT max(NAME) FROM customer)", + Func: (*Query4Audit).RuleSubQueryFunctions, + }, + "TBL.001": { + Item: "TBL.001", + Severity: "L4", + Summary: "不建议使用分区表", + Content: `不建议使用分区表`, + Case: "CREATE TABLE trb3(id INT, name VARCHAR(50), purchased DATE) PARTITION BY RANGE(YEAR(purchased)) (PARTITION p0 VALUES LESS THAN (1990), PARTITION p1 VALUES LESS THAN (1995), PARTITION p2 VALUES LESS THAN (2000), PARTITION p3 VALUES LESS THAN (2005) );", + Func: (*Query4Audit).RulePartitionNotAllowed, + }, + "TBL.002": { + Item: "TBL.002", + Severity: "L4", + Summary: "请为表选择合适的存储引擎", + Content: `建表或修改表的存储引擎时建议使用推荐的存储引擎,如:` + strings.Join(common.Config.TableAllowEngines, ","), + Case: "create table test(`id` int(11) NOT NULL AUTO_INCREMENT)", + Func: (*Query4Audit).RuleAllowEngine, + }, + "TBL.003": { + Item: "TBL.003", + Severity: "L8", + Summary: "以DUAL命名的表在数据库中有特殊含义", + Content: `DUAL表为虚拟表,不需要创建即可使用,也不建议服务以DUAL命名表。`, + Case: "create table dual(id int, primary key (id));", + Func: (*Query4Audit).RuleCreateDualTable, + }, + "TBL.004": { + Item: "TBL.004", + Severity: "L2", + Summary: "表的初始AUTO_INCREMENT值不为0", + Content: `AUTO_INCREMENT不为0会导致数据空洞。`, + Case: "CREATE TABLE tbl (a int) AUTO_INCREMENT = 10;", + Func: (*Query4Audit).RuleAutoIncrementInitNotZero, + }, + "TBL.005": { + Item: "TBL.005", + Severity: "L4", + Summary: "请使用推荐的字符集", + Content: `表字符集只允许设置为` + strings.Join(common.Config.TableAllowCharsets, ","), + Case: "CREATE TABLE tbl (a int) DEFAULT CHARSET = latin1;", + Func: (*Query4Audit).RuleTableCharsetCheck, + }, + } +} + +// IsIgnoreRule 判断是否是过滤规则 +// 支持XXX*前缀匹配,OK规则不可设置过滤 +func IsIgnoreRule(item string) bool { + + for _, ir := range common.Config.IgnoreRules { + ir = strings.Trim(ir, "*") + if strings.HasPrefix(item, ir) && ir != "OK" && ir != "" { + common.Log.Debug("IsIgnoreRule: %s", item) + return true + } + } + return false +} + +// InBlackList 判断一条请求是否在黑名单列表中 +// 如果在返回true,表示不需要评审 +// 注意这里没有做指纹判断,是否用指纹在这个函数的外面处理 +func InBlackList(sql string) bool { + in := false + for _, r := range common.BlackList { + if sql == r { + in = true + break + } + re, err := regexp.Compile("(?i)" + r) + if err == nil { + if re.FindString(sql) != "" { + common.Log.Debug("InBlackList: true, regexp: %s, sql: %s", "(?i)"+r, sql) + in = true + break + } + common.Log.Debug("InBlackList: false, regexp: %s, sql: %s", "(?i)"+r, sql) + } + } + return in +} + +// FormatSuggest 格式化输出优化建议 +// 目前支持:json, text两种形式,其他形式会给结构体的pretty.Println +func FormatSuggest(sql string, format string, suggests ...map[string]Rule) (map[string]Rule, string) { + var fingerprint, id string + var buf []string + var score = 100 + type Result struct { + ID string + Fingerprint string + Sample string + Suggest map[string]Rule + } + + // 生成指纹和ID + if sql != "" { + fingerprint = query.Fingerprint(sql) + id = query.Id(fingerprint) + } + + // 合并重复的建议 + suggest := make(map[string]Rule) + for _, s := range suggests { + for item, rule := range s { + suggest[item] = rule + } + } + suggest = MergeConflictHeuristicRules(suggest) + + // 是否忽略显示OK建议,测试的时候大家都喜欢看OK,线上跑起来的时候OK太多反而容易看花眼 + ignoreOK := false + for _, r := range common.Config.IgnoreRules { + if "OK" == r { + ignoreOK = true + } + } + + // 先保证suggest中有元素,然后再根据ignore配置删除不需要的项 + if len(suggest) < 1 { + suggest = map[string]Rule{"OK": HeuristicRules["OK"]} + } + if ignoreOK || len(suggest) > 1 { + delete(suggest, "OK") + } + for k := range suggest { + if IsIgnoreRule(k) { + delete(suggest, k) + } + } + + switch format { + case "json": + js, err := json.MarshalIndent(Result{ + ID: id, + Fingerprint: fingerprint, + Sample: sql, + Suggest: suggest, + }, "", " ") + if err == nil { + buf = append(buf, fmt.Sprintln(string(js))) + } else { + common.Log.Error("FormatSuggest json.Marshal Error: %v", err) + } + + case "text": + for item, rule := range suggest { + buf = append(buf, fmt.Sprintln("Query: ", sql)) + buf = append(buf, fmt.Sprintln("ID: ", id)) + buf = append(buf, fmt.Sprintln("Item: ", item)) + buf = append(buf, fmt.Sprintln("Severity: ", rule.Severity)) + buf = append(buf, fmt.Sprintln("Summary: ", rule.Summary)) + buf = append(buf, fmt.Sprintln("Content: ", rule.Content)) + } + case "lint": + for item, rule := range suggest { + // lint 中无需关注 OK 和 EXP + if item != "OK" && !strings.HasPrefix(item, "EXP") { + buf = append(buf, fmt.Sprintf("%s %s", item, rule.Summary)) + } + } + + case "markdown", "html", "explain-digest", "duplicate-key-checker": + if sql != "" && len(suggest) > 0 { + switch common.Config.ExplainSQLReportType { + case "fingerprint": + buf = append(buf, fmt.Sprintf("# Query: %s\n", id)) + buf = append(buf, fmt.Sprintf("```sql\n%s\n```\n", fingerprint)) + case "sample": + buf = append(buf, fmt.Sprintf("# Query: %s\n", id)) + buf = append(buf, fmt.Sprintf("```sql\n%s\n```\n", sql)) + default: + buf = append(buf, fmt.Sprintf("# Query: %s\n", id)) + buf = append(buf, fmt.Sprintf("```sql\n%s\n```\n", ast.Pretty(sql, format))) + } + } + // MySQL + var sortedMySQLSuggest []string + for item := range suggest { + if strings.HasPrefix(item, "ERR") { + if suggest[item].Content == "" { + delete(suggest, item) + } else { + sortedMySQLSuggest = append(sortedMySQLSuggest, item) + } + } + } + sort.Strings(sortedMySQLSuggest) + if len(sortedMySQLSuggest) > 0 { + buf = append(buf, "## MySQL执行出错\n") + } + for _, item := range sortedMySQLSuggest { + buf = append(buf, fmt.Sprintln(suggest[item].Content)) + score = 0 + delete(suggest, item) + } + + // Explain + if suggest["EXP.000"].Item != "" { + buf = append(buf, fmt.Sprintln("## ", suggest["EXP.000"].Summary)) + buf = append(buf, fmt.Sprintln(suggest["EXP.000"].Content)) + buf = append(buf, fmt.Sprint(suggest["EXP.000"].Case, "\n")) + delete(suggest, "EXP.000") + } + var sortedExplainSuggest []string + for item := range suggest { + if strings.HasPrefix(item, "EXP") { + sortedExplainSuggest = append(sortedExplainSuggest, item) + } + } + sort.Strings(sortedExplainSuggest) + for _, item := range sortedExplainSuggest { + buf = append(buf, fmt.Sprintln("### ", suggest[item].Summary)) + buf = append(buf, fmt.Sprintln(suggest[item].Content)) + buf = append(buf, fmt.Sprint(suggest[item].Case, "\n")) + } + + // Profiling + var sortedProfilingSuggest []string + for item := range suggest { + if strings.HasPrefix(item, "PRO") { + sortedProfilingSuggest = append(sortedProfilingSuggest, item) + } + } + sort.Strings(sortedProfilingSuggest) + if len(sortedProfilingSuggest) > 0 { + buf = append(buf, "## Profiling信息\n") + } + for _, item := range sortedProfilingSuggest { + buf = append(buf, fmt.Sprintln(suggest[item].Content)) + delete(suggest, item) + } + + // Trace + var sortedTraceSuggest []string + for item := range suggest { + if strings.HasPrefix(item, "TRA") { + sortedTraceSuggest = append(sortedTraceSuggest, item) + } + } + sort.Strings(sortedTraceSuggest) + if len(sortedTraceSuggest) > 0 { + buf = append(buf, "## Trace信息\n") + } + for _, item := range sortedTraceSuggest { + buf = append(buf, fmt.Sprintln(suggest[item].Content)) + delete(suggest, item) + } + + // Index + var sortedIdxSuggest []string + for item := range suggest { + if strings.HasPrefix(item, "IDX") { + sortedIdxSuggest = append(sortedIdxSuggest, item) + } + } + sort.Strings(sortedIdxSuggest) + for _, item := range sortedIdxSuggest { + buf = append(buf, fmt.Sprintln("## ", common.MarkdownEscape(suggest[item].Summary))) + buf = append(buf, fmt.Sprintln("* **Item:** ", item)) + buf = append(buf, fmt.Sprintln("* **Severity:** ", suggest[item].Severity)) + minus, err := strconv.Atoi(strings.Trim(suggest[item].Severity, "L")) + if err == nil { + score = score - minus*5 + } else { + common.Log.Debug("FormatSuggest, sortedIdxSuggest, strconv.Atoi, Error: ", err) + score = 0 + } + buf = append(buf, fmt.Sprintln("* **Content:** ", common.MarkdownEscape(suggest[item].Content))) + + if format == "duplicate-key-checker" { + buf = append(buf, fmt.Sprintf("* **原建表语句:** \n```sql\n%s\n```\n", suggest[item].Case), "\n\n") + } else { + buf = append(buf, fmt.Sprint("* **Case:** ", common.MarkdownEscape(suggest[item].Case), "\n\n")) + } + } + + // Heuristic + var sortedHeuristicSuggest []string + for item := range suggest { + if !strings.HasPrefix(item, "EXP") && + !strings.HasPrefix(item, "IDX") && + !strings.HasPrefix(item, "PRO") { + sortedHeuristicSuggest = append(sortedHeuristicSuggest, item) + } + } + sort.Strings(sortedHeuristicSuggest) + for _, item := range sortedHeuristicSuggest { + buf = append(buf, fmt.Sprintln("## ", suggest[item].Summary)) + if item == "OK" { + continue + } + buf = append(buf, fmt.Sprintln("* **Item:** ", item)) + buf = append(buf, fmt.Sprintln("* **Severity:** ", suggest[item].Severity)) + minus, err := strconv.Atoi(strings.Trim(suggest[item].Severity, "L")) + if err == nil { + score = score - minus*5 + } else { + common.Log.Debug("FormatSuggest, sortedHeuristicSuggest, strconv.Atoi, Error: ", err) + score = 0 + } + buf = append(buf, fmt.Sprintln("* **Content:** ", common.MarkdownEscape(suggest[item].Content))) + // buf = append(buf, fmt.Sprint("* **Case:** ", common.MarkdownEscape(suggest[item].Case), "\n\n")) + } + + default: + buf = append(buf, fmt.Sprintln("Query: ", sql)) + for _, rule := range suggest { + buf = append(buf, pretty.Sprint(rule)) + } + } + + // 打分 + var str string + switch common.Config.ReportType { + case "explain-digest", "lint": + str = strings.Join(buf, "\n") + default: + if len(buf) > 1 { + str = buf[0] + "\n" + common.Score(score) + "\n\n" + strings.Join(buf[1:], "\n") + } + } + + return suggest, str +} + +// ListHeuristicRules 打印支持的启发式规则,对应命令行参数-list-heuristic-rules +func ListHeuristicRules(rules ...map[string]Rule) { + switch common.Config.ReportType { + case "json": + js, err := json.MarshalIndent(rules, "", " ") + if err == nil { + fmt.Println(string(js)) + } + default: + fmt.Print("# 启发式规则建议\n\n[toc]\n\n") + for _, r := range rules { + delete(r, "OK") + for _, item := range common.SortedKey(r) { + fmt.Print("## ", common.MarkdownEscape(r[item].Summary), + "\n\n* **Item**:", r[item].Item, + "\n* **Severity**:", r[item].Severity, + "\n* **Content**:", common.MarkdownEscape(r[item].Content), + "\n* **Case**:\n\n```sql\n", r[item].Case, "\n```\n") + } + } + } +} + +// ListTestSQLs 打印测试用的SQL,方便测试,对应命令行参数-list-test-sqls +func ListTestSQLs() { + for _, sql := range common.TestSQLs { + fmt.Println(sql) + } +} diff --git a/advisor/rules_test.go b/advisor/rules_test.go new file mode 100644 index 00000000..938a5460 --- /dev/null +++ b/advisor/rules_test.go @@ -0,0 +1,54 @@ +/* + * Copyright 2018 Xiaomi, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package advisor + +import ( + "flag" + "testing" + + "github.com/XiaoMi/soar/common" +) + +var update = flag.Bool("update", false, "update .golden files") + +func TestListTestSQLs(t *testing.T) { + err := common.GoldenDiff(func() { ListTestSQLs() }, t.Name(), update) + if nil != err { + t.Fatal(err) + } +} + +func TestListHeuristicRules(t *testing.T) { + err := common.GoldenDiff(func() { ListHeuristicRules(HeuristicRules) }, t.Name(), update) + if nil != err { + t.Fatal(err) + } +} + +func TestInBlackList(t *testing.T) { + common.BlackList = []string{"select"} + if !InBlackList("select 1") { + t.Error("should be true") + } +} + +func TestIsIgnoreRule(t *testing.T) { + common.Config.IgnoreRules = []string{"test"} + if !IsIgnoreRule("test") { + t.Error("should be true") + } +} diff --git a/advisor/testdata/TestDigestExplainText.golden b/advisor/testdata/TestDigestExplainText.golden new file mode 100644 index 00000000..b0311993 --- /dev/null +++ b/advisor/testdata/TestDigestExplainText.golden @@ -0,0 +1,26 @@ +## Explain信息 + +| id | select\_type | table | partitions | type | possible_keys | key | key\_len | ref | rows | filtered | scalability | Extra | +|---|---|---|---|---|---|---|---|---|---|---|---|---| +| 1 | SIMPLE | *country* | NULL | index | PRIMARY,
country\_id | country | 152 | NULL | 0 | 0.00% | ☠️ **O(n)** | Using index | +| 1 | SIMPLE | *city* | NULL | ref | idx\_fk\_country\_id,
idx\_country\_id\_city,
idx\_all,
idx\_other | idx\_fk\_country\_id | 2 | sakila.country.country\_id | 0 | 0.00% | ☠️ **O(n)** | Using index | + + + +### Explain信息解读 + +#### SelectType信息解读 + +* **SIMPLE**: 简单SELECT(不使用UNION或子查询等). + +#### Type信息解读 + +* **index**: 全表扫描, 只是扫描表的时候按照索引次序进行而不是行. 主要优点就是避免了排序, 但是开销仍然非常大. + +* **ref**: 连接不能基于关键字选择单个行, 可能查找到多个符合条件的行. 叫做ref是因为索引要跟某个参考值相比较. 这个参考值或者是一个数, 或者是来自一个表里的多表查询的结果值. 例:'SELECT * FROM tbl WHERE idx_col=expr;'. + +#### Extra信息解读 + +* **Using index**: 只需通过索引就可以从表中获取列的信息, 无需额外去读取真实的行数据. 如果查询使用的列值仅仅是一个简单索引的部分值, 则会使用这种策略来优化查询. + + diff --git a/advisor/testdata/TestIndexAdviseNoEnv.golden b/advisor/testdata/TestIndexAdviseNoEnv.golden new file mode 100644 index 00000000..ac966f08 --- /dev/null +++ b/advisor/testdata/TestIndexAdviseNoEnv.golden @@ -0,0 +1,115 @@ +map[string]advisor.Rule{ + "IDX.001": {Item:"", Severity:"L2", Summary:"为sakila库的film表添加索引", Content:"为列length添加索引,散粒度为: 14.00%; ", Case:"ALTER TABLE `sakila`.`film` add index `idx_length` (`length`) ;\n", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}}, +} +map[string]advisor.Rule{ + "IDX.001": {Item:"", Severity:"L2", Summary:"为sakila库的film表添加索引", Content:"为列length添加索引,散粒度为: 14.00%; ", Case:"ALTER TABLE `sakila`.`film` add index `idx_length` (`length`) ;\n", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}}, +} +map[string]advisor.Rule{ + "IDX.001": {Item:"", Severity:"L2", Summary:"为sakila库的film表添加索引", Content:"为列length添加索引,散粒度为: 14.00%; ", Case:"ALTER TABLE `sakila`.`film` add index `idx_length` (`length`) ;\n", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}}, +} +map[string]advisor.Rule{ + "IDX.001": {Item:"", Severity:"L2", Summary:"为sakila库的film表添加索引", Content:"为列length添加索引,散粒度为: 14.00%; ", Case:"ALTER TABLE `sakila`.`film` add index `idx_length` (`length`) ;\n", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}}, +} +map[string]advisor.Rule{ + "IDX.001": {Item:"", Severity:"L2", Summary:"为sakila库的film表添加索引", Content:"为列length添加索引,散粒度为: 14.00%; ", Case:"ALTER TABLE `sakila`.`film` add index `idx_length` (`length`) ;\n", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}}, +} +map[string]advisor.Rule{ + "IDX.001": {Item:"", Severity:"L2", Summary:"为sakila库的film表添加索引", Content:"为列length添加索引,散粒度为: 14.00%; ", Case:"ALTER TABLE `sakila`.`film` add index `idx_length` (`length`) ;\n", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}}, +} +map[string]advisor.Rule{ + "IDX.001": {Item:"", Severity:"L2", Summary:"为sakila库的film表添加索引", Content:"为列length添加索引,散粒度为: 14.00%; ", Case:"ALTER TABLE `sakila`.`film` add index `idx_length` (`length`) ;\n", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}}, +} +map[string]advisor.Rule{ + "IDX.001": {Item:"", Severity:"L2", Summary:"为sakila库的film表添加索引", Content:"为列length添加索引,散粒度为: 14.00%; ", Case:"ALTER TABLE `sakila`.`film` add index `idx_length` (`length`) ;\n", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}}, +} +map[string]advisor.Rule{ + "IDX.001": {Item:"", Severity:"L2", Summary:"为sakila库的film表添加索引", Content:"为列length添加索引,散粒度为: 14.00%; ", Case:"ALTER TABLE `sakila`.`film` add index `idx_length` (`length`) ;\n", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}}, +} +map[string]advisor.Rule{ + "IDX.001": {Item:"", Severity:"L2", Summary:"为sakila库的film表添加索引", Content:"为列length添加索引,散粒度为: 14.00%; 为列release_year添加索引,散粒度为: 0.10%; ", Case:"ALTER TABLE `sakila`.`film` add index `idx_length` (`length`), add index `idx_release_year` (`release_year`) ;\n", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}}, +} +map[string]advisor.Rule{ + "IDX.001": {Item:"", Severity:"L2", Summary:"为sakila库的film表添加索引", Content:"为列length添加索引,散粒度为: 14.00%; 为列release_year添加索引,散粒度为: 0.10%; ", Case:"ALTER TABLE `sakila`.`film` add index `idx_length` (`length`), add index `idx_release_year` (`release_year`) ;\n", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}}, +} +map[string]advisor.Rule{ + "IDX.001": {Item:"", Severity:"L2", Summary:"为sakila库的film表添加索引", Content:"为列release_year添加索引,散粒度为: 0.10%; ", Case:"ALTER TABLE `sakila`.`film` add index `idx_release_year` (`release_year`) ;\n", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}}, +} +map[string]advisor.Rule{ + "IDX.001": {Item:"", Severity:"L2", Summary:"为sakila库的film表添加索引", Content:"为列length添加索引,散粒度为: 14.00%; ", Case:"ALTER TABLE `sakila`.`film` add index `idx_length` (`length`) ;\n", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}}, +} +map[string]advisor.Rule{ + "IDX.001": {Item:"", Severity:"L2", Summary:"为sakila库的film表添加索引", Content:"为列release_year添加索引,散粒度为: 0.10%; ", Case:"ALTER TABLE `sakila`.`film` add index `idx_release_year` (`release_year`) ;\n", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}}, +} +map[string]advisor.Rule{ + "IDX.001": {Item:"", Severity:"L2", Summary:"为sakila库的address表添加索引", Content:"为列address添加索引,散粒度为: 100.00%; 为列district添加索引,散粒度为: 100.00%; ", Case:"ALTER TABLE `sakila`.`address` add index `idx_address` (`address`), add index `idx_district` (`district`) ;\n", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}}, +} +map[string]advisor.Rule{ + "IDX.001": {Item:"", Severity:"L2", Summary:"为sakila库的film表添加索引", Content:"为列length添加索引,散粒度为: 14.00%; 为列release_year添加索引,散粒度为: 0.10%; ", Case:"ALTER TABLE `sakila`.`film` add index `idx_length` (`length`), add index `idx_release_year` (`release_year`) ;\n", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}}, +} +map[string]advisor.Rule{ + "IDX.001": {Item:"", Severity:"L2", Summary:"为sakila库的film表添加索引", Content:"为列length添加索引,散粒度为: 14.00%; 为列release_year添加索引,散粒度为: 0.10%; ", Case:"ALTER TABLE `sakila`.`film` add index `idx_length` (`length`), add index `idx_release_year` (`release_year`) ;\n", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}}, +} +map[string]advisor.Rule{ + "IDX.001": {Item:"", Severity:"L2", Summary:"为sakila库的film表添加索引", Content:"为列length添加索引,散粒度为: 14.00%; ", Case:"ALTER TABLE `sakila`.`film` add index `idx_length` (`length`) ;\n", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}}, +} +map[string]advisor.Rule{ + "IDX.001": {Item:"", Severity:"L2", Summary:"为sakila库的film表添加索引", Content:"为列length添加索引,散粒度为: 14.00%; 为列release_year添加索引,散粒度为: 0.10%; ", Case:"ALTER TABLE `sakila`.`film` add index `idx_length` (`length`), add index `idx_release_year` (`release_year`) ;\n", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}}, +} +map[string]advisor.Rule{ + "IDX.001": {Item:"", Severity:"L2", Summary:"为sakila库的film表添加索引", Content:"为列length添加索引,散粒度为: 14.00%; 为列release_year添加索引,散粒度为: 0.10%; ", Case:"ALTER TABLE `sakila`.`film` add index `idx_length` (`length`), add index `idx_release_year` (`release_year`) ;\n", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}}, +} +map[string]advisor.Rule{ + "IDX.001": {Item:"", Severity:"L2", Summary:"为sakila库的film表添加索引", Content:"为列release_year添加索引,散粒度为: 0.10%; ", Case:"ALTER TABLE `sakila`.`film` add index `idx_release_year` (`release_year`) ;\n", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}}, +} +map[string]advisor.Rule{ + "IDX.001": {Item:"", Severity:"L2", Summary:"为sakila库的film表添加索引", Content:"为列length添加索引,散粒度为: 14.00%; ", Case:"ALTER TABLE `sakila`.`film` add index `idx_length` (`length`) ;\n", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}}, +} +map[string]advisor.Rule{ + "IDX.001": {Item:"", Severity:"L2", Summary:"为sakila库的film表添加索引", Content:"为列length添加索引,散粒度为: 14.00%; ", Case:"ALTER TABLE `sakila`.`film` add index `idx_length` (`length`) ;\n", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}}, +} +map[string]advisor.Rule{ + "IDX.001": {Item:"", Severity:"L2", Summary:"为sakila库的film表添加索引", Content:"为列release_year添加索引,散粒度为: 0.10%; 为列length添加索引,散粒度为: 14.00%; ", Case:"ALTER TABLE `sakila`.`film` add index `idx_release_year` (`release_year`), add index `idx_length` (`length`) ;\n", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}}, +} +map[string]advisor.Rule{ + "IDX.001": {Item:"", Severity:"L2", Summary:"为sakila库的film表添加索引", Content:"为列release_year添加索引,散粒度为: 0.10%; ", Case:"ALTER TABLE `sakila`.`film` add index `idx_release_year` (`release_year`) ;\n", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}}, +} +map[string]advisor.Rule{ + "IDX.001": {Item:"", Severity:"L2", Summary:"为sakila库的film表添加索引", Content:"为列length添加索引,散粒度为: 14.00%; ", Case:"ALTER TABLE `sakila`.`film` add index `idx_length` (`length`) ;\n", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}}, +} +map[string]advisor.Rule{ + "IDX.001": {Item:"", Severity:"L2", Summary:"为sakila库的film表添加索引", Content:"为列length添加索引,散粒度为: 14.00%; ", Case:"ALTER TABLE `sakila`.`film` add index `idx_length` (`length`) ;\n", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}}, +} +map[string]advisor.Rule{ + "IDX.001": {Item:"", Severity:"L2", Summary:"为sakila库的film表添加索引", Content:"为列length添加索引,散粒度为: 14.00%; 为列release_year添加索引,散粒度为: 0.10%; ", Case:"ALTER TABLE `sakila`.`film` add index `idx_length` (`length`), add index `idx_release_year` (`release_year`) ;\n", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}}, +} +map[string]advisor.Rule{ + "IDX.001": {Item:"", Severity:"L2", Summary:"为sakila库的film表添加索引", Content:"为列length添加索引,散粒度为: 14.00%; 为列release_year添加索引,散粒度为: 0.10%; ", Case:"ALTER TABLE `sakila`.`film` add index `idx_length` (`length`), add index `idx_release_year` (`release_year`) ;\n", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}}, +} +map[string]advisor.Rule{ + "IDX.001": {Item:"", Severity:"L2", Summary:"为sakila库的film表添加索引", Content:"为列length添加索引,散粒度为: 14.00%; 为列release_year添加索引,散粒度为: 0.10%; ", Case:"ALTER TABLE `sakila`.`film` add index `idx_length` (`length`), add index `idx_release_year` (`release_year`) ;\n", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}}, +} +map[string]advisor.Rule{ + "IDX.001": {Item:"", Severity:"L2", Summary:"为sakila库的country表添加索引", Content:"为列last_update添加索引,散粒度为: 0.92%; ", Case:"ALTER TABLE `sakila`.`country` add index `idx_last_update` (`last_update`) ;\n", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}}, +} +map[string]advisor.Rule{ + "IDX.001": {Item:"", Severity:"L2", Summary:"为sakila库的city表添加索引", Content:"为列last_update添加索引,散粒度为: 0.17%; ", Case:"ALTER TABLE `sakila`.`city` add index `idx_last_update` (`last_update`) ;\n", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}}, +} +map[string]advisor.Rule{ + "IDX.001": {Item:"", Severity:"L2", Summary:"为sakila库的country表添加索引", Content:"为列last_update添加索引,散粒度为: 0.92%; ", Case:"ALTER TABLE `sakila`.`country` add index `idx_last_update` (`last_update`) ;\n", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}}, + "IDX.002": {Item:"", Severity:"L2", Summary:"为sakila库的city表添加索引", Content:"为列last_update添加索引,散粒度为: 0.17%; ", Case:"ALTER TABLE `sakila`.`city` add index `idx_last_update` (`last_update`) ;\n", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}}, +} +map[string]advisor.Rule{ + "IDX.001": {Item:"", Severity:"L2", Summary:"为sakila库的country表添加索引", Content:"为列country添加索引,散粒度为: 100.00%; ", Case:"ALTER TABLE `sakila`.`country` add index `idx_country` (`country`) ;\n", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}}, +} +map[string]advisor.Rule{ + "IDX.001": {Item:"", Severity:"L2", Summary:"为sakila库的film表添加索引", Content:"为列length添加索引,散粒度为: 14.00%; ", Case:"ALTER TABLE `sakila`.`film` add index `idx_length` (`length`) ;\n", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}}, +} +map[string]advisor.Rule{ + "IDX.001": {Item:"", Severity:"L2", Summary:"为sakila库的actor表添加索引", Content:"为列last_update添加索引,散粒度为: 0.50%; 为列first_name添加索引,散粒度为: 64.00%; ", Case:"ALTER TABLE `sakila`.`actor` add index `idx_last_update` (`last_update`), add index `idx_first_name` (`first_name`) ;\n", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}}, +} +map[string]advisor.Rule{ + "IDX.001": {Item:"", Severity:"L2", Summary:"为sakila库的city表添加索引", Content:"为列city添加索引,散粒度为: 99.83%; ", Case:"ALTER TABLE `sakila`.`city` add index `idx_city` (`city`) ;\n", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}}, +} +map[string]advisor.Rule{ + "IDX.001": {Item:"", Severity:"L2", Summary:"为sakila库的film表添加索引", Content:"为列description添加索引,散粒度为: 100.00%; ", Case:"ALTER TABLE `sakila`.`film` add index `idx_description` (`description`(255)) ;\n", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}}, +} diff --git a/advisor/testdata/TestListHeuristicRules.golden b/advisor/testdata/TestListHeuristicRules.golden new file mode 100644 index 00000000..1e93d48a --- /dev/null +++ b/advisor/testdata/TestListHeuristicRules.golden @@ -0,0 +1,1134 @@ +# 启发式规则建议 + +[toc] + +## 建议使用AS关键字显示声明一个别名 + +* **Item**:ALI.001 +* **Severity**:L0 +* **Content**:在列或表别名(如"tbl AS alias")中, 明确使用AS关键字比隐含别名(如"tbl alias")更易懂。 +* **Case**: + +```sql +select name from tbl t1 where id < 1000 +``` +## 不建议给列通配符'\*'设置别名 + +* **Item**:ALI.002 +* **Severity**:L8 +* **Content**:例: "SELECT tbl.\* col1, col2"上面这条SQL给列通配符设置了别名,这样的SQL可能存在逻辑错误。您可能意在查询col1, 但是代替它的是重命名的是tbl的最后一列。 +* **Case**: + +```sql +select tbl.* as c1,c2,c3 from tbl where id < 1000 +``` +## 别名不要与表或列的名字相同 + +* **Item**:ALI.003 +* **Severity**:L1 +* **Content**:表或列的别名与其真实名称相同, 这样的别名会使得查询更难去分辨。 +* **Case**: + +```sql +select name from tbl as tbl where id < 1000 +``` +## 修改表的默认字符集不会改表各个字段的字符集 + +* **Item**:ALT.001 +* **Severity**:L4 +* **Content**:很多初学者会将ALTER TABLE tbl\_name [DEFAULT] CHARACTER SET 'UTF8'误认为会修改所有字段的字符集,但实际上它只会影响后续新增的字段不会改表已有字段的字符集。如果想修改整张表所有字段的字符集建议使用ALTER TABLE tbl\_name CONVERT TO CHARACTER SET charset\_name; +* **Case**: + +```sql +ALTER TABLE tbl_name CONVERT TO CHARACTER SET charset_name; +``` +## 同一张表的多条ALTER请求建议合为一条 + +* **Item**:ALT.002 +* **Severity**:L2 +* **Content**:每次表结构变更对线上服务都会产生影响,即使是能够通过在线工具进行调整也请尽量通过合并ALTER请求的试减少操作次数。 +* **Case**: + +```sql +ALTER TABLE tbl ADD COLUMN col int, ADD INDEX idx_col (`col`); +``` +## 删除列为高危操作,操作前请注意检查业务逻辑是否还有依赖 + +* **Item**:ALT.003 +* **Severity**:L0 +* **Content**:如业务逻辑依赖未完全消除,列被删除后可能导致数据无法写入或无法查询到已删除列数据导致程序异常的情况。这种情况下即使通过备份数据回滚也会丢失用户请求写入的数据。 +* **Case**: + +```sql +ALTER TABLE tbl DROP COLUMN col; +``` +## 删除主键和外键为高危操作,操作前请与DBA确认影响 + +* **Item**:ALT.004 +* **Severity**:L0 +* **Content**:主键和外键为关系型数据库中两种重要约束,删除已有约束会打破已有业务逻辑,操作前请业务开发与DBA确认影响,三思而行。 +* **Case**: + +```sql +ALTER TABLE tbl DROP PRIMARY KEY; +``` +## 不建议使用前项通配符查找 + +* **Item**:ARG.001 +* **Severity**:L4 +* **Content**:例如“%foo”,查询参数有一个前项通配符的情况无法使用已有索引。 +* **Case**: + +```sql +select c1,c2,c3 from tbl where name like '%foo' +``` +## 没有通配符的LIKE查询 + +* **Item**:ARG.002 +* **Severity**:L1 +* **Content**:不包含通配符的LIKE查询可能存在逻辑错误,因为逻辑上它与等值查询相同。 +* **Case**: + +```sql +select c1,c2,c3 from tbl where name like 'foo' +``` +## 参数比较包含隐式转换,无法使用索引 + +* **Item**:ARG.003 +* **Severity**:L4 +* **Content**:隐式类型转换有无法命中索引的风险,在高并发、大数据量的情况下,命不中索引带来的后果非常严重。 +* **Case**: + +```sql +SELECT * FROM sakila.film WHERE length >= '60'; +``` +## IN (NULL)/NOT IN (NULL)永远非真 + +* **Item**:ARG.004 +* **Severity**:L4 +* **Content**:正确的作法是col IN ('val1', 'val2', 'val3') OR col IS NULL +* **Case**: + +```sql +SELECT * FROM sakila.film WHERE length >= '60'; +``` +## IN要慎用,元素过多会导致全表扫描 + +* **Item**:ARG.005 +* **Severity**:L1 +* **Content**: 如:select id from t where num in(1,2,3)对于连续的数值,能用BETWEEN就不要用IN了:select id from t where num between 1 and 3。而当IN值过多时MySQL也可能会进入全表扫描导致性能急剧下降。 +* **Case**: + +```sql +select id from t where num in(1,2,3) +``` +## 应尽量避免在WHERE子句中对字段进行NULL值判断 + +* **Item**:ARG.006 +* **Severity**:L1 +* **Content**:使用IS NULL或IS NOT NULL将可能导致引擎放弃使用索引而进行全表扫描,如:select id from t where num is null;可以在num上设置默认值0,确保表中num列没有null值,然后这样查询: select id from t where num=0; +* **Case**: + +```sql +select id from t where num is null +``` +## 避免使用模式匹配 + +* **Item**:ARG.007 +* **Severity**:L3 +* **Content**:性能问题是使用模式匹配操作符的最大缺点。使用LIKE或正则表达式进行模式匹配进行查询的另一个问题,是可能会返回意料之外的结果。最好的方案就是使用特殊的搜索引擎技术来替代SQL,比如Apache Lucene。另一个可选方案是将结果保存起来从而减少重复的搜索开销。如果一定要使用SQL,请考虑在MySQL中使用像FULLTEXT索引这样的第三方扩展。但更广泛地说,您不一定要使用SQL来解决所有问题。 +* **Case**: + +```sql +select c_id,c2,c3 from tbl where c2 like 'test%' +``` +## OR查询索引列时请尽量使用IN谓词 + +* **Item**:ARG.008 +* **Severity**:L1 +* **Content**:IN-list谓词可以用于索引检索,并且优化器可以对IN-list进行排序,以匹配索引的排序序列,从而获得更有效的检索。请注意,IN-list必须只包含常量,或在查询块执行期间保持常量的值,例如外引用。 +* **Case**: + +```sql +SELECT c1,c2,c3 FROM tbl WHERE c1 = 14 OR c1 = 17 +``` +## 引号中的字符串开头或结尾包含空格 + +* **Item**:ARG.009 +* **Severity**:L1 +* **Content**:如果VARCHAR列的前后存在空格将可能引起逻辑问题,如在MySQL 5.5中'a'和'a '可能会在查询中被认为是相同的值。 +* **Case**: + +```sql +SELECT 'abc ' +``` +## 不要使用hint,如sql\_no\_cache,force index,ignore key,straight join等 + +* **Item**:ARG.010 +* **Severity**:L1 +* **Content**:hint是用来强制SQL按照某个执行计划来执行,但随着数据量变化我们无法保证自己当初的预判是正确的。 +* **Case**: + +```sql +SELECT 'abc ' +``` +## 不要使用负向查询,如:NOT IN/NOT LIKE + +* **Item**:ARG.011 +* **Severity**:L3 +* **Content**:请尽量不要使用负向查询,这将导致全表扫描,对查询性能影响较大。 +* **Case**: + +```sql +select id from t where num not in(1,2,3); +``` +## 最外层SELECT未指定WHERE条件 + +* **Item**:CLA.001 +* **Severity**:L4 +* **Content**:SELECT语句没有WHERE子句,可能检查比预期更多的行(全表扫描)。对于SELECT COUNT(\*)类型的请求如果不要求精度,建议使用SHOW TABLE STATUS或EXPLAIN替代。 +* **Case**: + +```sql +select id from tbl +``` +## 不建议使用ORDER BY RAND() + +* **Item**:CLA.002 +* **Severity**:L3 +* **Content**:ORDER BY RAND()是从结果集中检索随机行的一种非常低效的方法,因为它会对整个结果进行排序并丢弃其大部分数据。 +* **Case**: + +```sql +select name from tbl where id < 1000 order by rand(number) +``` +## 不建议使用带OFFSET的LIMIT查询 + +* **Item**:CLA.003 +* **Severity**:L2 +* **Content**:使用LIMIT和OFFSET对结果集分页的复杂度是O(n^2),并且会随着数据增大而导致性能问题。采用“书签”扫描的方法实现分页效率更高。 +* **Case**: + +```sql +select c1,c2 from tbl where name=xx order by number limit 1 offset 20 +``` +## 不建议对常量进行GROUP BY + +* **Item**:CLA.004 +* **Severity**:L2 +* **Content**:GROUP BY 1 表示按第一列进行GROUP BY。如果在GROUP BY子句中使用数字,而不是表达式或列名称,当查询列顺序改变时,可能会导致问题。 +* **Case**: + +```sql +select col1,col2 from tbl group by 1 +``` +## ORDER BY常数列没有任何意义 + +* **Item**:CLA.005 +* **Severity**:L2 +* **Content**:SQL逻辑上可能存在错误; 最多只是一个无用的操作,不会更改查询结果。 +* **Case**: + +```sql +select id from test where id=1 order by id +``` +## 在不同的表中GROUP BY或ORDER BY + +* **Item**:CLA.006 +* **Severity**:L4 +* **Content**:这将强制使用临时表和filesort,可能产生巨大性能隐患,并且可能消耗大量内存和磁盘上的临时空间。 +* **Case**: + +```sql +select tb1.col, tb2.col from tb1, tb2 where id=1 group by tb1.col, tb2.col +``` +## ORDER BY语句对多个不同条件使用不同方向的排序无法使用索引 + +* **Item**:CLA.007 +* **Severity**:L2 +* **Content**:ORDER BY子句中的所有表达式必须按统一的ASC或DESC方向排序,以便利用索引。 +* **Case**: + +```sql +select c1,c2,c3 from t1 where c1='foo' order by c2 desc, c3 asc +``` +## 请为GROUP BY显示添加ORDER BY条件 + +* **Item**:CLA.008 +* **Severity**:L2 +* **Content**:默认MySQL会对'GROUP BY col1, col2, ...'请求按如下顺序排序'ORDER BY col1, col2, ...'。如果GROUP BY语句不指定ORDER BY条件会导致无谓的排序产生,如果不需要排序建议添加'ORDER BY NULL'。 +* **Case**: + +```sql +select c1,c2,c3 from t1 where c1='foo' group by c2 +``` +## ORDER BY的条件为表达式 + +* **Item**:CLA.009 +* **Severity**:L2 +* **Content**:当ORDER BY条件为表达式或函数时会使用到临时表,如果在未指定WHERE或WHERE条件返回的结果集较大时性能会很差。 +* **Case**: + +```sql +select description from film where title ='ACADEMY DINOSAUR' order by length-language_id; +``` +## GROUP BY的条件为表达式 + +* **Item**:CLA.010 +* **Severity**:L2 +* **Content**:当GROUP BY条件为表达式或函数时会使用到临时表,如果在未指定WHERE或WHERE条件返回的结果集较大时性能会很差。 +* **Case**: + +```sql +select description from film where title ='ACADEMY DINOSAUR' GROUP BY length-language_id; +``` +## 建议为表添加注释 + +* **Item**:CLA.011 +* **Severity**:L1 +* **Content**:为表添加注释能够使得表的意义更明确,从而为日后的维护带来极大的便利。 +* **Case**: + +```sql +CREATE TABLE `test1` (`ID` bigint(20) NOT NULL AUTO_INCREMENT,`c1` varchar(128) DEFAULT NULL,PRIMARY KEY (`ID`)) ENGINE=InnoDB DEFAULT CHARSET=utf8 +``` +## 将复杂的裹脚布式查询分解成几个简单的查询 + +* **Item**:CLA.012 +* **Severity**:L2 +* **Content**:SQL是一门极具表现力的语言,您可以在单个SQL查询或者单条语句中完成很多事情。但这并不意味着必须强制只使用一行代码,或者认为使用一行代码就搞定每个任务是个好主意。通过一个查询来获得所有结果的常见后果是得到了一个笛卡儿积。当查询中的两张表之间没有条件限制它们的关系时,就会发生这种情况。没有对应的限制而直接使用两张表进行联结查询,就会得到第一张表中的每一行和第二张表中的每一行的一个组合。每一个这样的组合就会成为结果集中的一行,最终您就会得到一个行数很多的结果集。重要的是要考虑这些查询很难编写、难以修改和难以调试。数据库查询请求的日益增加应该是预料之中的事。经理们想要更复杂的报告以及在用户界面上添加更多的字段。如果您的设计很复杂,并且是一个单一查询,要扩展它们就会很费时费力。不论对您还是项目来说,时间花在这些事情上面不值得。将复杂的意大利面条式查询分解成几个简单的查询。当您拆分一个复杂的SQL查询时,得到的结果可能是很多类似的查询,可能仅仅在数据类型上有所不同。编写所有的这些查询是很乏味的,因此,最好能够有个程序自动生成这些代码。SQL代码生成是一个很好的应用。尽管SQL支持用一行代码解决复杂的问题,但也别做不切实际的事情。 +* **Case**: + +```sql +这是一条很长很长的SQL,案例略。 +``` +## 不建议使用HAVING子句 + +* **Item**:CLA.013 +* **Severity**:L3 +* **Content**:将查询的HAVING子句改写为WHERE中的查询条件,可以在查询处理期间使用索引。 +* **Case**: + +```sql +SELECT s.c_id,count(s.c_id) FROM s where c = test GROUP BY s.c_id HAVING s.c_id <> '1660' AND s.c_id <> '2' order by s.c_id +``` +## 删除全表时建议使用TRUNCATE替代DELETE + +* **Item**:CLA.014 +* **Severity**:L2 +* **Content**:删除全表时建议使用TRUNCATE替代DELETE +* **Case**: + +```sql +delete from tbl +``` +## UPDATE未指定WHERE条件 + +* **Item**:CLA.015 +* **Severity**:L4 +* **Content**:UPDATE不指定WHERE条件一般是致命的,请您三思后行 +* **Case**: + +```sql +update tbl set col=1 +``` +## 不要UPDATE主键 + +* **Item**:CLA.016 +* **Severity**:L2 +* **Content**:主键是数据表中记录的唯一标识符,不建议频繁更新主键列,这将影响元数据统计信息进而影响正常的查询。 +* **Case**: + +```sql +update tbl set col=1 +``` +## 不建议使用存储过程、视图、触发器、临时表等 + +* **Item**:CLA.017 +* **Severity**:L2 +* **Content**:这些功能的使用在一定程度上会使得程序难以调试和拓展,更没有移植性,且会极大的增加出现BUG的概率。 +* **Case**: + +```sql +CREATE VIEW v_today (today) AS SELECT CURRENT_DATE; +``` +## 不建议使用SELECT \* 类型查询 + +* **Item**:COL.001 +* **Severity**:L1 +* **Content**:当表结构变更时,使用\*通配符选择所有列将导致查询的含义和行为会发生更改,可能导致查询返回更多的数据。 +* **Case**: + +```sql +select * from tbl where id=1 +``` +## INSERT未指定列名 + +* **Item**:COL.002 +* **Severity**:L2 +* **Content**:当表结构发生变更,如果INSERT或REPLACE请求不明确指定列名,请求的结果将会与预想的不同; 建议使用“INSERT INTO tbl(col1,col2)VALUES ...”代替。 +* **Case**: + +```sql +insert into tbl values(1,'name') +``` +## 建议修改自增ID为无符号类型 + +* **Item**:COL.003 +* **Severity**:L2 +* **Content**:建议修改自增ID为无符号类型 +* **Case**: + +```sql +create table test(`id` int(11) NOT NULL AUTO_INCREMENT) +``` +## 请为列添加默认值 + +* **Item**:COL.004 +* **Severity**:L1 +* **Content**:请为列添加默认值,如果是ALTER操作,请不要忘记将原字段的默认值写上。字段无默认值,当表较大时无法在线变更表结构。 +* **Case**: + +```sql +CREATE TABLE tbl (col int) ENGINE=InnoDB; +``` +## 列未添加注释 + +* **Item**:COL.005 +* **Severity**:L1 +* **Content**:建议对表中每个列添加注释,来明确每个列在表中的含义及作用。 +* **Case**: + +```sql +CREATE TABLE tbl (col int) ENGINE=InnoDB; +``` +## 表中包含有太多的列 + +* **Item**:COL.006 +* **Severity**:L3 +* **Content**:表中包含有太多的列 +* **Case**: + +```sql +CREATE TABLE tbl ( cols ....); +``` +## 可使用VARCHAR代替CHAR,VARBINARY代替BINARY + +* **Item**:COL.008 +* **Severity**:L1 +* **Content**:为首先变长字段存储空间小,可以节省存储空间。其次对于查询来说,在一个相对较小的字段内搜索效率显然要高些。 +* **Case**: + +```sql +create table t1(id int,name char(20),last_time date) +``` +## 建议使用精确的数据类型 + +* **Item**:COL.009 +* **Severity**:L2 +* **Content**:实际上,任何使用FLOAT、REAL或DOUBLE PRECISION数据类型的设计都有可能是反模式。大多数应用程序使用的浮点数的取值范围并不需要达到IEEE 754标准所定义的最大/最小区间。在计算总量时,非精确浮点数所积累的影响是严重的。使用SQL中的NUMERIC或DECIMAL类型来代替FLOAT及其类似的数据类型进行固定精度的小数存储。这些数据类型精确地根据您定义这一列时指定的精度来存储数据。尽可能不要使用浮点数。 +* **Case**: + +```sql +CREATE TABLE tab2 (p_id BIGINT UNSIGNED NOT NULL,a_id BIGINT UNSIGNED NOT NULL,hours float not null,PRIMARY KEY (p_id, a_id)) +``` +## 不建议使用ENUM数据类型 + +* **Item**:COL.010 +* **Severity**:L2 +* **Content**:ENUM定义了列中值的类型,使用字符串表示ENUM里的值时,实际存储在列中的数据是这些值在定义时的序数。因此,这列的数据是字节对齐的,当您进行一次排序查询时,结果是按照实际存储的序数值排序的,而不是按字符串值的字母顺序排序的。这可能不是您所希望的。没有什么语法支持从ENUM或者check约束中添加或删除一个值;您只能使用一个新的集合重新定义这一列。如果您打算废弃一个选项,您可能会为历史数据而烦恼。作为一种策略,改变元数据——也就是说,改变表和列的定义——应该是不常见的,并且要注意测试和质量保证。有一个更好的解决方案来约束一列中的可选值:创建一张检查表,每一行包含一个允许在列中出现的候选值;然后在引用新表的旧表上声明一个外键约束。 +* **Case**: + +```sql +create table tab1(status ENUM('new','in progress','fixed')) +``` +## 当需要唯一约束时才使用NULL,仅当列不能有缺失值时才使用NOT NULL + +* **Item**:COL.011 +* **Severity**:L0 +* **Content**:NULL和0是不同的,10乘以NULL还是NULL。NULL和空字符串是不一样的。将一个字符串和标准SQL中的NULL联合起来的结果还是NULL。NULL和FALSE也是不同的。AND、OR和NOT这三个布尔操作如果涉及NULL,其结果也让很多人感到困惑。当您将一列声明为NOT NULL时,也就是说这列中的每一个值都必须存在且是有意义的。使用NULL来表示任意类型不存在的空值。 当您将一列声明为NOT NULL时,也就是说这列中的每一个值都必须存在且是有意义的。 +* **Case**: + +```sql +select c1,c2,c3 from tbl where c4 is null or c4 <> 1 +``` +## BLOB和TEXT类型的字段不可设置为NULL + +* **Item**:COL.012 +* **Severity**:L5 +* **Content**:BLOB和TEXT类型的字段不可设置为NULL +* **Case**: + +```sql +CREATE TABLE `tbl` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `c` longblob, PRIMARY KEY (`id`)); +``` +## TIMESTAMP类型未设置默认值 + +* **Item**:COL.013 +* **Severity**:L4 +* **Content**:TIMESTAMP类型未设置默认值 +* **Case**: + +```sql +CREATE TABLE tbl( `id` bigint not null, `create_time` timestamp); +``` +## 为列指定了字符集 + +* **Item**:COL.014 +* **Severity**:L5 +* **Content**:建议列与表使用同一个字符集,不要单独指定列的字符集。 +* **Case**: + +```sql +CREATE TABLE `tb2` ( `id` int(11) DEFAULT NULL, `col` char(10) CHARACTER SET utf8 DEFAULT NULL) +``` +## BLOB类型的字段不可指定默认值 + +* **Item**:COL.015 +* **Severity**:L4 +* **Content**:BLOB类型的字段不可指定默认值 +* **Case**: + +```sql +CREATE TABLE `tbl` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `c` blob NOT NULL DEFAULT '', PRIMARY KEY (`id`)); +``` +## 整型定义建议采用INT(10)或BIGINT(20) + +* **Item**:COL.016 +* **Severity**:L1 +* **Content**:INT(M) 在 integer 数据类型中,M 表示最大显示宽度。 在 INT(M) 中,M 的值跟 INT(M) 所占多少存储空间并无任何关系。 INT(3)、INT(4)、INT(8) 在磁盘上都是占用 4 bytes 的存储空间。 +* **Case**: + +```sql +CREATE TABLE tab (a INT(1)); +``` +## varchar定义长度过长 + +* **Item**:COL.017 +* **Severity**:L2 +* **Content**:varchar 是可变长字符串,不预先分配存储空间,长度不要超过1024,如果存储长度过长MySQL将定义字段类型为text,独立出来一张表,用主键来对应,避免影响其它字段索引效率。 +* **Case**: + +```sql +CREATE TABLE tab (a varchar(3500)); +``` +## 消除不必要的DISTINCT条件 + +* **Item**:DIS.001 +* **Severity**:L1 +* **Content**:太多DISTINCT条件是复杂的裹脚布式查询的症状。考虑将复杂查询分解成许多简单的查询,并减少DISTINCT条件的数量。如果主键列是列的结果集的一部分,则DISTINCT条件可能没有影响。 +* **Case**: + +```sql +SELECT DISTINCT c.c_id,count(DISTINCT c.c_name),count(DISTINCT c.c_e),count(DISTINCT c.c_n),count(DISTINCT c.c_me),c.c_d FROM (select distinct xing, name from B) as e WHERE e.country_id = c.country_id +``` +## COUNT(DISTINCT)多列时结果可能和你预想的不同 + +* **Item**:DIS.002 +* **Severity**:L3 +* **Content**:COUNT(DISTINCT col)计算该列除NULL之外的不重复行数,注意COUNT(DISTINCT col, col2)如果其中一列全为NULL那么即使另一列有不同的值,也返回0。 +* **Case**: + +```sql +SELECT COUNT(DISTINCT col, col2) FROM tbl; +``` +## DISTINCT \*对有主键的表没有意义 + +* **Item**:DIS.003 +* **Severity**:L3 +* **Content**:当表已经有主键时,对所有列进行DISTINCT的输出结果与不进行DISTINCT操作的结果相同,请不要画蛇添足。 +* **Case**: + +```sql +SELECT DISTINCT * FROM film; +``` +## 避免在WHERE条件中使用函数或其他运算符 + +* **Item**:FUN.001 +* **Severity**:L2 +* **Content**:虽然在SQL中使用函数可以简化很多复杂的查询,但使用了函数的查询无法利用表中已经建立的索引,该查询将会是全表扫描,性能较差。通常建议将列名写在比较运算符左侧,将查询过滤条件放在比较运算符右侧。 +* **Case**: + +```sql +select id from t where substring(name,1,3)='abc' +``` +## 指定了WHERE条件或非MyISAM引擎时使用COUNT(\*)操作性能不佳 + +* **Item**:FUN.002 +* **Severity**:L1 +* **Content**:COUNT(\*)的作用是统计表行数,COUNT(COL)的作用是统计指定列非NULL的行数。MyISAM表对于COUNT(\*)统计全表行数进行了特殊的优化,通常情况下非常快。但对于非MyISAM表或指定了某些WHERE条件,COUNT(\*)操作需要扫描大量的行才能获取精确的结果,性能也因此不佳。有时候某些业务场景并不需要完全精确的COUNT值,此时可以用近似值来代替。EXPLAIN出来的优化器估算的行数就是一个不错的近似值,执行EXPLAIN并不需要真正去执行查询,所以成本很低。 +* **Case**: + +```sql +SELECT c3, COUNT(*) AS accounts FROM tab where c2 < 10000 GROUP BY c3 ORDER BY num +``` +## 使用了合并为可空列的字符串连接 + +* **Item**:FUN.003 +* **Severity**:L3 +* **Content**:在一些查询请求中,您需要强制让某一列或者某个表达式返回非NULL的值,从而让查询逻辑变得更简单,担忧不想将这个值存下来。使用COALESCE()函数来构造连接的表达式,这样即使是空值列也不会使整表达式变为NULL。 +* **Case**: + +```sql +select c1 || coalesce(' ' || c2 || ' ', ' ') || c3 as c from tbl +``` +## 不建议使用SYSDATE()函数 + +* **Item**:FUN.004 +* **Severity**:L4 +* **Content**:SYSDATE()函数可能导致主从数据不一致,请使用NOW()函数替代SYSDATE()。 +* **Case**: + +```sql +SELECT SYSDATE(); +``` +## 不建议使用COUNT(col)或COUNT(常量) + +* **Item**:FUN.005 +* **Severity**:L1 +* **Content**:不要使用COUNT(col)或COUNT(常量)来替代COUNT(\*),COUNT(\*)是SQL92定义的标准统计行数的方法,跟数据无关,跟NULL和非NULL也无关。 +* **Case**: + +```sql +SELECT COUNT(1) FROM tbl; +``` +## 使用SUM(COL)时需注意NPE问题 + +* **Item**:FUN.006 +* **Severity**:L1 +* **Content**:当某一列的值全是NULL时,COUNT(COL)的返回结果为0,但SUM(COL)的返回结果为NULL,因此使用SUM()时需注意NPE问题。可以使用如下方式来避免SUM的NPE问题: SELECT IF(ISNULL(SUM(COL)), 0, SUM(COL)) FROM tbl +* **Case**: + +```sql +SELECT SUM(COL) FROM tbl; +``` +## 不建议对等值查询列使用GROUP BY + +* **Item**:GRP.001 +* **Severity**:L2 +* **Content**:GROUP BY中的列在前面的WHERE条件中使用了等值查询,对这样的列进行GROUP BY意义不大。 +* **Case**: + +```sql +select film_id, title from film where release_year='2006' group by release_year +``` +## JOIN语句混用逗号和ANSI模式 + +* **Item**:JOI.001 +* **Severity**:L2 +* **Content**:表连接的时候混用逗号和ANSI JOIN不便于人类理解,并且MySQL不同版本的表连接行为和优先级均有所不同,当MySQL版本变化后可能会引入错误。 +* **Case**: + +```sql +select c1,c2,c3 from t1,t2 join t3 on t1.c1=t2.c1,t1.c3=t3,c1 where id>1000 +``` +## 同一张表被连接两次 + +* **Item**:JOI.002 +* **Severity**:L4 +* **Content**:相同的表在FROM子句中至少出现两次,可以简化为对该表的单次访问。 +* **Case**: + +```sql +select tb1.col from (tb1, tb2) join tb2 on tb1.id=tb.id where tb1.id=1 +``` +## OUTER JOIN失效 + +* **Item**:JOI.003 +* **Severity**:L4 +* **Content**:由于WHERE条件错误使得OUTER JOIN的外部表无数据返回,这会将查询隐式转换为 INNER JOIN 。如:select c from L left join R using(c) where L.a=5 and R.b=10。这种SQL逻辑上可能存在错误或程序员对OUTER JOIN如何工作存在误解,因为LEFT/RIGHT JOIN是LEFT/RIGHT OUTER JOIN的缩写。 +* **Case**: + +```sql +select c1,c2,c3 from t1 left outer join t2 using(c1) where t1.c2=2 and t2.c3=4 +``` +## 不建议使用排它JOIN + +* **Item**:JOI.004 +* **Severity**:L4 +* **Content**:只在右侧表为NULL的带WHERE子句的LEFT OUTER JOIN语句,有可能是在WHERE子句中使用错误的列,如:“... FROM l LEFT OUTER JOIN r ON l.l = r.r WHERE r.z IS NULL”,这个查询正确的逻辑可能是 WHERE r.r IS NULL。 +* **Case**: + +```sql +select c1,c2,c3 from t1 left outer join t2 on t1.c1=t2.c1 where t2.c2 is null +``` +## 减少JOIN的数量 + +* **Item**:JOI.005 +* **Severity**:L2 +* **Content**:太多的JOIN是复杂的裹脚布式查询的症状。考虑将复杂查询分解成许多简单的查询,并减少JOIN的数量。 +* **Case**: + +```sql +select bp1.p_id, b1.d_d as l, b1.b_id from b1 join bp1 on (b1.b_id = bp1.b_id) left outer join (b1 as b2 join bp2 on (b2.b_id = bp2.b_id)) on (bp1.p_id = bp2.p_id ) join bp21 on (b1.b_id = bp1.b_id) join bp31 on (b1.b_id = bp1.b_id) join bp41 on (b1.b_id = bp1.b_id) where b2.b_id = 0 +``` +## 将嵌套查询重写为JOIN通常会导致更高效的执行和更有效的优化 + +* **Item**:JOI.006 +* **Severity**:L4 +* **Content**:一般来说,非嵌套子查询总是用于关联子查询,最多是来自FROM子句中的一个表,这些子查询用于ANY、ALL和EXISTS的谓词。如果可以根据查询语义决定子查询最多返回一个行,那么一个不相关的子查询或来自FROM子句中的多个表的子查询就被压平了。 +* **Case**: + +```sql +SELECT s,p,d FROM tbl WHERE p.p_id = (SELECT s.p_id FROM tbl WHERE s.c_id = 100996 AND s.q = 1 ) +``` +## 不建议使用联表更新 + +* **Item**:JOI.007 +* **Severity**:L4 +* **Content**:当需要同时更新多张表时建议使用简单SQL,一条SQL只更新一张表,尽量不要将多张表的更新在同一条SQL中完成。 +* **Case**: + +```sql +UPDATE users u LEFT JOIN hobby h ON u.id = h.uid SET u.name = 'pianoboy' WHERE h.hobby = 'piano'; +``` +## 不要使用跨DB的Join查询 + +* **Item**:JOI.008 +* **Severity**:L4 +* **Content**:一般来说,跨DB的Join查询意味着查询语句跨越了两个不同的子系统,这可能意味着系统耦合度过高或库表结构设计不合理。 +* **Case**: + +```sql +SELECT s,p,d FROM tbl WHERE p.p_id = (SELECT s.p_id FROM tbl WHERE s.c_id = 100996 AND s.q = 1 ) +``` +## 建议使用自增列作为主键,如使用联合自增主键时请将自增键作为第一列 + +* **Item**:KEY.001 +* **Severity**:L2 +* **Content**:建议使用自增列作为主键,如使用联合自增主键时请将自增键作为第一列 +* **Case**: + +```sql +create table test(`id` int(11) NOT NULL PRIMARY KEY (`id`)) +``` +## 无主键或唯一键,无法在线变更表结构 + +* **Item**:KEY.002 +* **Severity**:L4 +* **Content**:无主键或唯一键,无法在线变更表结构 +* **Case**: + +```sql +create table test(col varchar(5000)) +``` +## 避免外键等递归关系 + +* **Item**:KEY.003 +* **Severity**:L4 +* **Content**:存在递归关系的数据很常见,数据常会像树或者以层级方式组织。然而,创建一个外键约束来强制执行同一表中两列之间的关系,会导致笨拙的查询。树的每一层对应着另一个连接。您将需要发出递归查询,以获得节点的所有后代或所有祖先。解决方案是构造一个附加的闭包表。它记录了树中所有节点间的关系,而不仅仅是那些具有直接的父子关系。您也可以比较不同层次的数据设计:闭包表,路径枚举,嵌套集。然后根据应用程序的需要选择一个。 +* **Case**: + +```sql +CREATE TABLE tab2 (p_id BIGINT UNSIGNED NOT NULL,a_id BIGINT UNSIGNED NOT NULL,PRIMARY KEY (p_id, a_id),FOREIGN KEY (p_id) REFERENCES tab1(p_id),FOREIGN KEY (a_id) REFERENCES tab3(a_id)) +``` +## 提醒:请将索引属性顺序与查询对齐 + +* **Item**:KEY.004 +* **Severity**:L0 +* **Content**:如果为列创建复合索引,请确保查询属性与索引属性的顺序相同,以便DBMS在处理查询时使用索引。如果查询和索引属性订单没有对齐,那么DBMS可能无法在查询处理期间使用索引。 +* **Case**: + +```sql +create index idx1 on tbl (last_name,first_name) +``` +## 表建的索引过多 + +* **Item**:KEY.005 +* **Severity**:L2 +* **Content**:表建的索引过多 +* **Case**: + +```sql +CREATE TABLE tbl ( a int, b int, c int, KEY idx_a (`a`),KEY idx_b(`b`),KEY idx_c(`c`)); +``` +## 主键中的列过多 + +* **Item**:KEY.006 +* **Severity**:L4 +* **Content**:主键中的列过多 +* **Case**: + +```sql +CREATE TABLE tbl ( a int, b int, c int, PRIMARY KEY(`a`,`b`,`c`)); +``` +## 未指定主键或主键非int或bigint + +* **Item**:KEY.007 +* **Severity**:L4 +* **Content**:未指定主键或主键非int或bigint,建议将主键设置为int unsigned或bigint unsigned。 +* **Case**: + +```sql +CREATE TABLE tbl (a int); +``` +## ORDER BY多个列但排序方向不同时可能无法使用索引 + +* **Item**:KEY.008 +* **Severity**:L4 +* **Content**:在MySQL 8.0之前当ORDER BY多个列指定的排序方向不同时将无法使用已经建立的索引。 +* **Case**: + +```sql +SELECT * FROM tbl ORDER BY a DESC, b ASC; +``` +## 添加唯一索引前请注意检查数据唯一性 + +* **Item**:KEY.009 +* **Severity**:L0 +* **Content**:请提前检查添加唯一索引列的数据唯一性,如果数据不唯一在线表结构调整时将有可能自动将重复列删除,这有可能导致数据丢失。 +* **Case**: + +```sql +CREATE UNIQUE INDEX part_of_name ON customer (name(10)); +``` +## SQL\_CALC\_FOUND\_ROWS效率低下 + +* **Item**:KWR.001 +* **Severity**:L2 +* **Content**:因为SQL\_CALC\_FOUND\_ROWS不能很好地扩展,所以可能导致性能问题; 建议业务使用其他策略来替代SQL\_CALC\_FOUND\_ROWS提供的计数功能,比如:分页结果展示等。 +* **Case**: + +```sql +select SQL_CALC_FOUND_ROWS col from tbl where id>1000 +``` +## 不建议使用MySQL关键字做列名或表名 + +* **Item**:KWR.002 +* **Severity**:L2 +* **Content**:当使用关键字做为列名或表名时程序需要对列名和表名进行转义,如果疏忽被将导致请求无法执行。 +* **Case**: + +```sql +CREATE TABLE tbl ( `select` int ) +``` +## 不建议使用复数做列名或表名 + +* **Item**:KWR.003 +* **Severity**:L1 +* **Content**:表名应该仅仅表示表里面的实体内容,不应该表示实体数量,对应于 DO 类名也是单数形式,符合表达习惯。 +* **Case**: + +```sql +CREATE TABLE tbl ( `books` int ) +``` +## INSERT INTO xx SELECT加锁粒度较大请谨慎 + +* **Item**:LCK.001 +* **Severity**:L3 +* **Content**:INSERT INTO xx SELECT加锁粒度较大请谨慎 +* **Case**: + +```sql +INSERT INTO tbl SELECT * FROM tbl2; +``` +## 请慎用INSERT ON DUPLICATE KEY UPDATE + +* **Item**:LCK.002 +* **Severity**:L3 +* **Content**:当主键为自增键时使用INSERT ON DUPLICATE KEY UPDATE可能会导致主键出现大量不连续快速增长,导致主键快速溢出无法继续写入。极端情况下还有可能导致主从数据不一致。 +* **Case**: + +```sql +INSERT INTO t1(a,b,c) VALUES (1,2,3) ON DUPLICATE KEY UPDATE c=c+1; +``` +## 用字符类型存储IP地址 + +* **Item**:LIT.001 +* **Severity**:L2 +* **Content**:字符串字面上看起来像IP地址,但不是INET\_ATON()的参数,表示数据被存储为字符而不是整数。将IP地址存储为整数更为有效。 +* **Case**: + +```sql +insert into tbl (IP,name) values('10.20.306.122','test') +``` +## 日期/时间未使用引号括起 + +* **Item**:LIT.002 +* **Severity**:L4 +* **Content**:诸如“WHERE col <2010-02-12”之类的查询是有效的SQL,但可能是一个错误,因为它将被解释为“WHERE col <1996”; 日期/时间文字应该加引号。 +* **Case**: + +```sql +select col1,col2 from tbl where time < 2018-01-10 +``` +## 一列中存储一系列相关数据的集合 + +* **Item**:LIT.003 +* **Severity**:L3 +* **Content**:将ID存储为一个列表,作为VARCHAR/TEXT列,这样能导致性能和数据完整性问题。查询这样的列需要使用模式匹配的表达式。使用逗号分隔的列表来做多表联结查询定位一行数据是极不优雅和耗时的。这将使验证ID更加困难。考虑一下,列表最多支持存放多少数据呢?将ID存储在一张单独的表中,代替使用多值属性,从而每个单独的属性值都可以占据一行。这样交叉表实现了两张表之间的多对多关系。这将更好地简化查询,也更有效地验证ID。 +* **Case**: + +```sql +select c1,c2,c3,c4 from tab1 where col_id REGEXP '[[:<:]]12[[:>:]]' +``` +## 请使用分号或已设定的DELIMITER结尾 + +* **Item**:LIT.004 +* **Severity**:L1 +* **Content**:USE database, SHOW DATABASES等命令也需要使用使用分号或已设定的DELIMITER结尾。 +* **Case**: + +```sql +USE db +``` +## 非确定性的GROUP BY + +* **Item**:RES.001 +* **Severity**:L4 +* **Content**:SQL返回的列既不在聚合函数中也不是GROUP BY表达式的列中,因此这些值的结果将是非确定性的。如:select a, b, c from tbl where foo="bar" group by a,该SQL返回的结果就是不确定的。 +* **Case**: + +```sql +select c1,c2,c3 from t1 where c2='foo' group by c2 +``` +## 未使用ORDER BY的LIMIT查询 + +* **Item**:RES.002 +* **Severity**:L4 +* **Content**:没有ORDER BY的LIMIT会导致非确定性的结果,这取决于查询执行计划。 +* **Case**: + +```sql +select col1,col2 from tbl where name=xx limit 10 +``` +## UPDATE/DELETE操作使用了LIMIT条件 + +* **Item**:RES.003 +* **Severity**:L4 +* **Content**:UPDATE/DELETE操作使用LIMIT条件和不添加WHERE条件一样危险,它可将会导致主从数据不一致或从库同步中断。 +* **Case**: + +```sql +UPDATE film SET length = 120 WHERE title = 'abc' LIMIT 1; +``` +## UPDATE/DELETE操作指定了ORDER BY条件 + +* **Item**:RES.004 +* **Severity**:L4 +* **Content**:UPDATE/DELETE操作不要指定ORDER BY条件。 +* **Case**: + +```sql +UPDATE film SET length = 120 WHERE title = 'abc' ORDER BY title +``` +## UPDATE可能存在逻辑错误,导致数据损坏 + +* **Item**:RES.005 +* **Severity**:L4 +* **Content**: +* **Case**: + +```sql +update tbl set col = 1 and cl = 2 where col=3; +``` +## 永远不真的比较条件 + +* **Item**:RES.006 +* **Severity**:L4 +* **Content**:查询条件永远非真,这将导致查询无匹配到的结果。 +* **Case**: + +```sql +select * from tbl where 1 != 1; +``` +## 永远为真的比较条件 + +* **Item**:RES.007 +* **Severity**:L4 +* **Content**:查询条件永远为真,这将导致WHERE条件失效进行全表查询。 +* **Case**: + +```sql +select * from tbl where 1 = 1; +``` +## 不建议使用LOAD DATA/SELECT ... INTO OUTFILE + +* **Item**:RES.008 +* **Severity**:L2 +* **Content**:SELECT INTO OUTFILE需要授予FILE权限,这通过会引入安全问题。LOAD DATA虽然可以提高数据导入速度,但同时也可能导致从库同步延迟过大。 +* **Case**: + +```sql +LOAD DATA INFILE 'data.txt' INTO TABLE db2.my_table; +``` +## 请谨慎使用TRUNCATE操作 + +* **Item**:SEC.001 +* **Severity**:L0 +* **Content**:一般来说想清空一张表最快速的做法就是使用TRUNCATE TABLE tbl\_name;语句。但TRUNCATE操作也并非是毫无代价的,TRUNCATE TABLE无法返回被删除的准确行数,如果需要返回被删除的行数建议使用DELETE语法。TRUNCATE操作还会重置AUTO\_INCREMENT,如果不想重置该值建议使用DELETE FROM tbl\_name WHERE 1;替代。TRUNCATE操作会对数据字典添加源数据锁(MDL),当一次需要TRUNCATE很多表时会影响整个实例的所有请求,因此如果要TRUNCATE多个表建议用DROP+CREATE的方式以减少锁时长。 +* **Case**: + +```sql +TRUNCATE TABLE tbl_name +``` +## 不使用明文存储密码 + +* **Item**:SEC.002 +* **Severity**:L0 +* **Content**:使用明文存储密码或者使用明文在网络上传递密码都是不安全的。如果攻击者能够截获您用来插入密码的SQL语句,他们就能直接读到密码。另外,将用户输入的字符串以明文的形式插入到纯SQL语句中,也会让攻击者发现它。如果您能够读取密码,黑客也可以。解决方案是使用单向哈希函数对原始密码进行加密编码。哈希是指将输入字符串转化成另一个新的、不可识别的字符串的函数。对密码加密表达式加点随机串来防御“字典攻击”。不要将明文密码输入到SQL查询语句中。在应用程序代码中计算哈希串,只在SQL查询中使用哈希串。 +* **Case**: + +```sql +create table test(id int,name varchar(20) not null,password varchar(200)not null) +``` +## 使用DELETE/DROP/TRUNCATE等操作时注意备份 + +* **Item**:SEC.003 +* **Severity**:L0 +* **Content**:在执行高危操作之前对数据进行备份是十分有必要的。 +* **Case**: + +```sql +delete from table where col = 'condition' +``` +## '!=' 运算符是非标准的 + +* **Item**:STA.001 +* **Severity**:L0 +* **Content**:"<>"才是标准SQL中的不等于运算符。 +* **Case**: + +```sql +select col1,col2 from tbl where type!=0 +``` +## 库名或表名点后建议不要加空格 + +* **Item**:STA.002 +* **Severity**:L1 +* **Content**:当使用db.table或table.column格式访问表或字段时,请不要在点号后面添加空格,虽然这样语法正确。 +* **Case**: + +```sql +select col from sakila. film +``` +## 索引起名不规范 + +* **Item**:STA.003 +* **Severity**:L1 +* **Content**:建议普通二级索引以idx\_为前缀,唯一索引以uk\_为前缀。 +* **Case**: + +```sql +select col from now where type!=0 +``` +## 起名时请不要使用字母、数字和下划线之外的字符 + +* **Item**:STA.004 +* **Severity**:L1 +* **Content**:以字母或下划线开头,名字只允许使用字母、数字和下划线。请统一大小写,不要使用驼峰命名法。不要在名字中出现连续下划线'\_\_',这样很难辨认。 +* **Case**: + +```sql +CREATE TABLE ` abc` (a int); +``` +## MySQL对子查询的优化效果不佳 + +* **Item**:SUB.001 +* **Severity**:L4 +* **Content**:MySQL将外部查询中的每一行作为依赖子查询执行子查询。 这是导致严重性能问题的常见原因。这可能会在 MySQL 5.6版本中得到改善, 但对于5.1及更早版本, 建议将该类查询分别重写为JOIN或LEFT OUTER JOIN。 +* **Case**: + +```sql +select col1,col2,col3 from table1 where col2 in(select col from table2) +``` +## 如果您不在乎重复的话,建议使用UNION ALL替代UNION + +* **Item**:SUB.002 +* **Severity**:L2 +* **Content**:与去除重复的UNION不同,UNION ALL允许重复元组。如果您不关心重复元组,那么使用UNION ALL将是一个更快的选项。 +* **Case**: + +```sql +select teacher_id as id,people_name as name from t1,t2 where t1.teacher_id=t2.people_id union select student_id as id,people_name as name from t1,t2 where t1.student_id=t2.people_id +``` +## 考虑使用EXISTS而不是DISTINCT子查询 + +* **Item**:SUB.003 +* **Severity**:L3 +* **Content**:DISTINCT关键字在对元组排序后删除重复。相反,考虑使用一个带有EXISTS关键字的子查询,您可以避免返回整个表。 +* **Case**: + +```sql +SELECT DISTINCT c.c_id, c.c_name FROM c,e WHERE e.c_id = c.c_id +``` +## 执行计划中嵌套连接深度过深 + +* **Item**:SUB.004 +* **Severity**:L3 +* **Content**:MySQL对子查询的优化效果不佳,MySQL将外部查询中的每一行作为依赖子查询执行子查询。 这是导致严重性能问题的常见原因。 +* **Case**: + +```sql +SELECT * from tb where id in (select id from (select id from tb)) +``` +## 子查询不支持LIMIT + +* **Item**:SUB.005 +* **Severity**:L8 +* **Content**:当前MySQL版本不支持在子查询中进行'LIMIT & IN/ALL/ANY/SOME'。 +* **Case**: + +```sql +SELECT * FROM staff WHERE name IN (SELECT NAME FROM customer ORDER BY name LIMIT 1) +``` +## 不建议在子查询中使用函数 + +* **Item**:SUB.006 +* **Severity**:L2 +* **Content**:MySQL将外部查询中的每一行作为依赖子查询执行子查询,如果在子查询中使用函数,即使是semi-join也很难进行高效的查询。可以将子查询重写为OUTER JOIN语句并用连接条件对数据进行过滤。 +* **Case**: + +```sql +SELECT * FROM staff WHERE name IN (SELECT max(NAME) FROM customer) +``` +## 不建议使用分区表 + +* **Item**:TBL.001 +* **Severity**:L4 +* **Content**:不建议使用分区表 +* **Case**: + +```sql +CREATE TABLE trb3(id INT, name VARCHAR(50), purchased DATE) PARTITION BY RANGE(YEAR(purchased)) (PARTITION p0 VALUES LESS THAN (1990), PARTITION p1 VALUES LESS THAN (1995), PARTITION p2 VALUES LESS THAN (2000), PARTITION p3 VALUES LESS THAN (2005) ); +``` +## 请为表选择合适的存储引擎 + +* **Item**:TBL.002 +* **Severity**:L4 +* **Content**:建表或修改表的存储引擎时建议使用推荐的存储引擎,如:innodb +* **Case**: + +```sql +create table test(`id` int(11) NOT NULL AUTO_INCREMENT) +``` +## 以DUAL命名的表在数据库中有特殊含义 + +* **Item**:TBL.003 +* **Severity**:L8 +* **Content**:DUAL表为虚拟表,不需要创建即可使用,也不建议服务以DUAL命名表。 +* **Case**: + +```sql +create table dual(id int, primary key (id)); +``` +## 表的初始AUTO\_INCREMENT值不为0 + +* **Item**:TBL.004 +* **Severity**:L2 +* **Content**:AUTO\_INCREMENT不为0会导致数据空洞。 +* **Case**: + +```sql +CREATE TABLE tbl (a int) AUTO_INCREMENT = 10; +``` +## 请使用推荐的字符集 + +* **Item**:TBL.005 +* **Severity**:L4 +* **Content**:表字符集只允许设置为utf8,utf8mb4 +* **Case**: + +```sql +CREATE TABLE tbl (a int) DEFAULT CHARSET = latin1; +``` diff --git a/advisor/testdata/TestListTestSQLs.golden b/advisor/testdata/TestListTestSQLs.golden new file mode 100644 index 00000000..4ac6be8c --- /dev/null +++ b/advisor/testdata/TestListTestSQLs.golden @@ -0,0 +1,80 @@ +SELECT * FROM film WHERE length = 86; +SELECT * FROM film WHERE length IS NULL; +SELECT * FROM film HAVING title = 'abc'; +SELECT * FROM sakila.film WHERE length >= 60; +SELECT * FROM sakila.film WHERE length >= '60'; +SELECT * FROM film WHERE length BETWEEN 60 AND 84; +SELECT * FROM film WHERE title LIKE 'AIR%'; +SELECT * FROM film WHERE title IS NOT NULL; +SELECT * FROM film WHERE length = 114 and title = 'ALABAMA DEVIL'; +SELECT * FROM film WHERE length > 100 and title = 'ALABAMA DEVIL'; +SELECT * FROM film WHERE length > 100 and language_id < 10 and title = 'xyz'; +SELECT * FROM film WHERE length > 100 and language_id < 10; +SELECT release_year, sum(length) FROM film WHERE length = 123 AND language_id = 1 GROUP BY release_year; +SELECT release_year, sum(length) FROM film WHERE length >= 123 GROUP BY release_year; +SELECT release_year, language_id, sum(length) FROM film GROUP BY release_year, language_id; +SELECT release_year, sum(length) FROM film WHERE length = 123 GROUP BY release_year,(length+language_id); +SELECT release_year, sum(film_id) FROM film GROUP BY release_year; +SELECT * FROM address GROUP BY address,district; +SELECT title FROM film WHERE ABS(language_id) = 3 GROUP BY title; +SELECT language_id FROM film WHERE length = 123 GROUP BY release_year ORDER BY language_id; +SELECT release_year FROM film WHERE length = 123 GROUP BY release_year ORDER BY release_year; +SELECT * FROM film WHERE length = 123 ORDER BY release_year ASC, language_id DESC; +SELECT release_year FROM film WHERE length = 123 GROUP BY release_year ORDER BY release_year LIMIT 10; +SELECT * FROM film WHERE length = 123 ORDER BY release_year LIMIT 10; +SELECT * FROM film ORDER BY release_year LIMIT 10; +SELECT * FROM film WHERE length > 100 ORDER BY length LIMIT 10; +SELECT * FROM film WHERE length < 100 ORDER BY length LIMIT 10; +SELECT * FROM customer WHERE address_id in (224,510) ORDER BY last_name; +SELECT * FROM film WHERE release_year = 2016 AND length != 1 ORDER BY title; +SELECT title FROM film WHERE release_year = 1995; +SELECT title, replacement_cost FROM film WHERE language_id = 5 AND length = 70; +SELECT title FROM film WHERE language_id > 5 AND length > 70; +SELECT * FROM film WHERE length = 100 and title = 'xyz' ORDER BY release_year; +SELECT * FROM film WHERE length > 100 and title = 'xyz' ORDER BY release_year; +SELECT * FROM film WHERE length > 100 ORDER BY release_year; +SELECT * FROM city a INNER JOIN country b ON a.country_id=b.country_id; +SELECT * FROM city a LEFT JOIN country b ON a.country_id=b.country_id; +SELECT * FROM city a RIGHT JOIN country b ON a.country_id=b.country_id; +SELECT * FROM city a LEFT JOIN country b ON a.country_id=b.country_id WHERE b.last_update IS NULL; +SELECT * FROM city a RIGHT JOIN country b ON a.country_id=b.country_id WHERE a.last_update IS NULL; +SELECT * FROM city a LEFT JOIN country b ON a.country_id=b.country_id UNION SELECT * FROM city a RIGHT JOIN country b ON a.country_id=b.country_id; +SELECT * FROM city a RIGHT JOIN country b ON a.country_id=b.country_id WHERE a.last_update IS NULL UNION SELECT * FROM city a LEFT JOIN country b ON a.country_id=b.country_id WHERE b.last_update IS NULL; +SELECT country_id, last_update FROM city NATURAL JOIN country; +SELECT country_id, last_update FROM city NATURAL LEFT JOIN country; +SELECT country_id, last_update FROM city NATURAL RIGHT JOIN country; +SELECT a.country_id, a.last_update FROM city a STRAIGHT_JOIN country b ON a.country_id=b.country_id; +SELECT d.deptno,d.dname,d.loc FROM scott.dept d WHERE d.deptno IN (SELECT e.deptno FROM scott.emp e); +SELECT visitor_id, url FROM (SELECT id FROM log WHERE ip="123.45.67.89" order by tsdesc limit 50, 10) I JOIN log ON (I.id=log.id) JOIN url ON (url.id=log.url_id) order by TS desc; +DELETE city, country FROM city INNER JOIN country using (country_id) WHERE city.city_id = 1; +DELETE city FROM city LEFT JOIN country ON city.country_id = country.country_id WHERE country.country IS NULL; +DELETE a1, a2 FROM city AS a1 INNER JOIN country AS a2 WHERE a1.country_id=a2.country_id; +DELETE FROM a1, a2 USING city AS a1 INNER JOIN country AS a2 WHERE a1.country_id=a2.country_id; +DELETE FROM film WHERE length > 100; +UPDATE city INNER JOIN country USING(country_id) SET city.city = 'Abha', city.last_update = '2006-02-15 04:45:25', country.country = 'Afghanistan' WHERE city.city_id=10; +UPDATE city INNER JOIN country ON city.country_id = country.country_id INNER JOIN address ON city.city_id = address.city_id SET city.city = 'Abha', city.last_update = '2006-02-15 04:45:25', country.country = 'Afghanistan' WHERE city.city_id=10; +UPDATE city, country SET city.city = 'Abha', city.last_update = '2006-02-15 04:45:25', country.country = 'Afghanistan' WHERE city.country_id = country.country_id AND city.city_id=10; +UPDATE film SET length = 10 WHERE language_id = 20; +INSERT INTO city (country_id) SELECT country_id FROM country; +INSERT INTO city (country_id) VALUES (1),(2),(3); +INSERT INTO city (country_id) VALUES (10); +INSERT INTO city (country_id) SELECT 10 FROM DUAL; +REPLACE INTO city (country_id) SELECT country_id FROM country; +REPLACE INTO city (country_id) VALUES (1),(2),(3); +REPLACE INTO city (country_id) VALUES (10); +REPLACE INTO city (country_id) SELECT 10 FROM DUAL; +SELECT film_id FROM ( SELECT film_id FROM ( SELECT film_id FROM ( SELECT film_id FROM ( SELECT film_id FROM ( SELECT film_id FROM ( SELECT film_id FROM ( SELECT film_id FROM ( SELECT film_id FROM ( SELECT film_id FROM ( SELECT film_id FROM ( SELECT film_id FROM ( SELECT film_id FROM ( SELECT film_id FROM ( SELECT film_id FROM ( SELECT film_id FROM ( SELECT film_id FROM film ) film ) film ) film ) film ) film ) film ) film ) film ) film ) film ) film ) film ) film ) film ) film ) film; +SELECT * FROM film WHERE language_id = (SELECT language_id FROM language LIMIT 1); +SELECT * FROM city i left JOIN country o ON i.city_id=o.country_id union SELECT * FROM city i right JOIN country o ON i.city_id=o.country_id; +SELECT * FROM (SELECT * FROM actor WHERE last_update='2006-02-15 04:34:33' and last_name='CHASE') t WHERE last_update='2006-02-15 04:34:33' and last_name='CHASE' GROUP BY first_name; +SELECT * FROM city i left JOIN country o ON i.city_id=o.country_id union SELECT * FROM city i right JOIN country o ON i.city_id=o.country_id; +SELECT * FROM city i left JOIN country o ON i.city_id=o.country_id WHERE o.country_id is null union SELECT * FROM city i right JOIN country o ON i.city_id=o.country_id WHERE i.city_id is null; +SELECT first_name,last_name,email FROM customer STRAIGHT_JOIN address ON customer.address_id=address.address_id; +SELECT ID,name FROM (SELECT address FROM customer_list WHERE SID=1 order by phone limit 50,10) a JOIN customer_list l ON (a.address=l.address) JOIN city c ON (c.city=l.city) order by phone desc; +SELECT * FROM film WHERE date(last_update)='2006-02-15'; +SELECT last_update FROM film GROUP BY date(last_update); +SELECT last_update FROM film order by date(last_update); +SELECT description FROM film WHERE description IN('NEWS','asd') GROUP BY description; +alter table address add index idx_city_id(city_id); +alter table inventory add index `idx_store_film` (`store_id`,`film_id`); +alter table inventory add index `idx_store_film` (`store_id`,`film_id`),add index `idx_store_film` (`store_id`,`film_id`),add index `idx_store_film` (`store_id`,`film_id`); diff --git a/advisor/testdata/TestMergeConflictHeuristicRules.golden b/advisor/testdata/TestMergeConflictHeuristicRules.golden new file mode 100644 index 00000000..90232655 --- /dev/null +++ b/advisor/testdata/TestMergeConflictHeuristicRules.golden @@ -0,0 +1,109 @@ +advisor.Rule{Item:"ALI.001", Severity:"L0", Summary:"建议使用AS关键字显示声明一个别名", Content:"在列或表别名(如\"tbl AS alias\")中, 明确使用AS关键字比隐含别名(如\"tbl alias\")更易懂。", Case:"select name from tbl t1 where id < 1000", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"ALI.002", Severity:"L8", Summary:"不建议给列通配符'*'设置别名", Content:"例: \"SELECT tbl.* col1, col2\"上面这条SQL给列通配符设置了别名,这样的SQL可能存在逻辑错误。您可能意在查询col1, 但是代替它的是重命名的是tbl的最后一列。", Case:"select tbl.* as c1,c2,c3 from tbl where id < 1000", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"ALI.003", Severity:"L1", Summary:"别名不要与表或列的名字相同", Content:"表或列的别名与其真实名称相同, 这样的别名会使得查询更难去分辨。", Case:"select name from tbl as tbl where id < 1000", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"ALT.001", Severity:"L4", Summary:"修改表的默认字符集不会改表各个字段的字符集", Content:"很多初学者会将ALTER TABLE tbl_name [DEFAULT] CHARACTER SET 'UTF8'误认为会修改所有字段的字符集,但实际上它只会影响后续新增的字段不会改表已有字段的字符集。如果想修改整张表所有字段的字符集建议使用ALTER TABLE tbl_name CONVERT TO CHARACTER SET charset_name;", Case:"ALTER TABLE tbl_name CONVERT TO CHARACTER SET charset_name;", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"ALT.002", Severity:"L2", Summary:"同一张表的多条ALTER请求建议合为一条", Content:"每次表结构变更对线上服务都会产生影响,即使是能够通过在线工具进行调整也请尽量通过合并ALTER请求的试减少操作次数。", Case:"ALTER TABLE tbl ADD COLUMN col int, ADD INDEX idx_col (`col`);", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"ALT.003", Severity:"L0", Summary:"删除列为高危操作,操作前请注意检查业务逻辑是否还有依赖", Content:"如业务逻辑依赖未完全消除,列被删除后可能导致数据无法写入或无法查询到已删除列数据导致程序异常的情况。这种情况下即使通过备份数据回滚也会丢失用户请求写入的数据。", Case:"ALTER TABLE tbl DROP COLUMN col;", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"ALT.004", Severity:"L0", Summary:"删除主键和外键为高危操作,操作前请与DBA确认影响", Content:"主键和外键为关系型数据库中两种重要约束,删除已有约束会打破已有业务逻辑,操作前请业务开发与DBA确认影响,三思而行。", Case:"ALTER TABLE tbl DROP PRIMARY KEY;", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"ARG.001", Severity:"L4", Summary:"不建议使用前项通配符查找", Content:"例如“%foo”,查询参数有一个前项通配符的情况无法使用已有索引。", Case:"select c1,c2,c3 from tbl where name like '%foo'", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"ARG.002", Severity:"L1", Summary:"没有通配符的LIKE查询", Content:"不包含通配符的LIKE查询可能存在逻辑错误,因为逻辑上它与等值查询相同。", Case:"select c1,c2,c3 from tbl where name like 'foo'", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"ARG.003", Severity:"L4", Summary:"参数比较包含隐式转换,无法使用索引", Content:"隐式类型转换有无法命中索引的风险,在高并发、大数据量的情况下,命不中索引带来的后果非常严重。", Case:"SELECT * FROM sakila.film WHERE length >= '60';", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"ARG.004", Severity:"L4", Summary:"IN (NULL)/NOT IN (NULL)永远非真", Content:"正确的作法是col IN ('val1', 'val2', 'val3') OR col IS NULL", Case:"SELECT * FROM sakila.film WHERE length >= '60';", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"ARG.006", Severity:"L1", Summary:"应尽量避免在WHERE子句中对字段进行NULL值判断", Content:"使用IS NULL或IS NOT NULL将可能导致引擎放弃使用索引而进行全表扫描,如:select id from t where num is null;可以在num上设置默认值0,确保表中num列没有null值,然后这样查询: select id from t where num=0;", Case:"select id from t where num is null", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"ARG.007", Severity:"L3", Summary:"避免使用模式匹配", Content:"性能问题是使用模式匹配操作符的最大缺点。使用LIKE或正则表达式进行模式匹配进行查询的另一个问题,是可能会返回意料之外的结果。最好的方案就是使用特殊的搜索引擎技术来替代SQL,比如Apache Lucene。另一个可选方案是将结果保存起来从而减少重复的搜索开销。如果一定要使用SQL,请考虑在MySQL中使用像FULLTEXT索引这样的第三方扩展。但更广泛地说,您不一定要使用SQL来解决所有问题。", Case:"select c_id,c2,c3 from tbl where c2 like 'test%'", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"ARG.008", Severity:"L1", Summary:"OR查询索引列时请尽量使用IN谓词", Content:"IN-list谓词可以用于索引检索,并且优化器可以对IN-list进行排序,以匹配索引的排序序列,从而获得更有效的检索。请注意,IN-list必须只包含常量,或在查询块执行期间保持常量的值,例如外引用。", Case:"SELECT c1,c2,c3 FROM tbl WHERE c1 = 14 OR c1 = 17", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"ARG.009", Severity:"L1", Summary:"引号中的字符串开头或结尾包含空格", Content:"如果VARCHAR列的前后存在空格将可能引起逻辑问题,如在MySQL 5.5中'a'和'a '可能会在查询中被认为是相同的值。", Case:"SELECT 'abc '", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"ARG.010", Severity:"L1", Summary:"不要使用hint,如sql_no_cache,force index,ignore key,straight join等", Content:"hint是用来强制SQL按照某个执行计划来执行,但随着数据量变化我们无法保证自己当初的预判是正确的。", Case:"SELECT 'abc '", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"ARG.011", Severity:"L3", Summary:"不要使用负向查询,如:NOT IN/NOT LIKE", Content:"请尽量不要使用负向查询,这将导致全表扫描,对查询性能影响较大。", Case:"select id from t where num not in(1,2,3);", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"CLA.001", Severity:"L4", Summary:"最外层SELECT未指定WHERE条件", Content:"SELECT语句没有WHERE子句,可能检查比预期更多的行(全表扫描)。对于SELECT COUNT(*)类型的请求如果不要求精度,建议使用SHOW TABLE STATUS或EXPLAIN替代。", Case:"select id from tbl", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"CLA.002", Severity:"L3", Summary:"不建议使用ORDER BY RAND()", Content:"ORDER BY RAND()是从结果集中检索随机行的一种非常低效的方法,因为它会对整个结果进行排序并丢弃其大部分数据。", Case:"select name from tbl where id < 1000 order by rand(number)", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"CLA.003", Severity:"L2", Summary:"不建议使用带OFFSET的LIMIT查询", Content:"使用LIMIT和OFFSET对结果集分页的复杂度是O(n^2),并且会随着数据增大而导致性能问题。采用“书签”扫描的方法实现分页效率更高。", Case:"select c1,c2 from tbl where name=xx order by number limit 1 offset 20", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"CLA.004", Severity:"L2", Summary:"不建议对常量进行GROUP BY", Content:"GROUP BY 1 表示按第一列进行GROUP BY。如果在GROUP BY子句中使用数字,而不是表达式或列名称,当查询列顺序改变时,可能会导致问题。", Case:"select col1,col2 from tbl group by 1", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"CLA.005", Severity:"L2", Summary:"ORDER BY常数列没有任何意义", Content:"SQL逻辑上可能存在错误; 最多只是一个无用的操作,不会更改查询结果。", Case:"select id from test where id=1 order by id", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"CLA.006", Severity:"L4", Summary:"在不同的表中GROUP BY或ORDER BY", Content:"这将强制使用临时表和filesort,可能产生巨大性能隐患,并且可能消耗大量内存和磁盘上的临时空间。", Case:"select tb1.col, tb2.col from tb1, tb2 where id=1 group by tb1.col, tb2.col", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"CLA.007", Severity:"L2", Summary:"ORDER BY语句对多个不同条件使用不同方向的排序无法使用索引", Content:"ORDER BY子句中的所有表达式必须按统一的ASC或DESC方向排序,以便利用索引。", Case:"select c1,c2,c3 from t1 where c1='foo' order by c2 desc, c3 asc", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"CLA.008", Severity:"L2", Summary:"请为GROUP BY显示添加ORDER BY条件", Content:"默认MySQL会对'GROUP BY col1, col2, ...'请求按如下顺序排序'ORDER BY col1, col2, ...'。如果GROUP BY语句不指定ORDER BY条件会导致无谓的排序产生,如果不需要排序建议添加'ORDER BY NULL'。", Case:"select c1,c2,c3 from t1 where c1='foo' group by c2", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"CLA.009", Severity:"L2", Summary:"ORDER BY的条件为表达式", Content:"当ORDER BY条件为表达式或函数时会使用到临时表,如果在未指定WHERE或WHERE条件返回的结果集较大时性能会很差。", Case:"select description from film where title ='ACADEMY DINOSAUR' order by length-language_id;", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"CLA.010", Severity:"L2", Summary:"GROUP BY的条件为表达式", Content:"当GROUP BY条件为表达式或函数时会使用到临时表,如果在未指定WHERE或WHERE条件返回的结果集较大时性能会很差。", Case:"select description from film where title ='ACADEMY DINOSAUR' GROUP BY length-language_id;", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"CLA.011", Severity:"L1", Summary:"建议为表添加注释", Content:"为表添加注释能够使得表的意义更明确,从而为日后的维护带来极大的便利。", Case:"CREATE TABLE `test1` (`ID` bigint(20) NOT NULL AUTO_INCREMENT,`c1` varchar(128) DEFAULT NULL,PRIMARY KEY (`ID`)) ENGINE=InnoDB DEFAULT CHARSET=utf8", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"CLA.012", Severity:"L2", Summary:"将复杂的裹脚布式查询分解成几个简单的查询", Content:"SQL是一门极具表现力的语言,您可以在单个SQL查询或者单条语句中完成很多事情。但这并不意味着必须强制只使用一行代码,或者认为使用一行代码就搞定每个任务是个好主意。通过一个查询来获得所有结果的常见后果是得到了一个笛卡儿积。当查询中的两张表之间没有条件限制它们的关系时,就会发生这种情况。没有对应的限制而直接使用两张表进行联结查询,就会得到第一张表中的每一行和第二张表中的每一行的一个组合。每一个这样的组合就会成为结果集中的一行,最终您就会得到一个行数很多的结果集。重要的是要考虑这些查询很难编写、难以修改和难以调试。数据库查询请求的日益增加应该是预料之中的事。经理们想要更复杂的报告以及在用户界面上添加更多的字段。如果您的设计很复杂,并且是一个单一查询,要扩展它们就会很费时费力。不论对您还是项目来说,时间花在这些事情上面不值得。将复杂的意大利面条式查询分解成几个简单的查询。当您拆分一个复杂的SQL查询时,得到的结果可能是很多类似的查询,可能仅仅在数据类型上有所不同。编写所有的这些查询是很乏味的,因此,最好能够有个程序自动生成这些代码。SQL代码生成是一个很好的应用。尽管SQL支持用一行代码解决复杂的问题,但也别做不切实际的事情。", Case:"这是一条很长很长的SQL,案例略。", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"CLA.013", Severity:"L3", Summary:"不建议使用HAVING子句", Content:"将查询的HAVING子句改写为WHERE中的查询条件,可以在查询处理期间使用索引。", Case:"SELECT s.c_id,count(s.c_id) FROM s where c = test GROUP BY s.c_id HAVING s.c_id <> '1660' AND s.c_id <> '2' order by s.c_id", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"CLA.014", Severity:"L2", Summary:"删除全表时建议使用TRUNCATE替代DELETE", Content:"删除全表时建议使用TRUNCATE替代DELETE", Case:"delete from tbl", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"CLA.015", Severity:"L4", Summary:"UPDATE未指定WHERE条件", Content:"UPDATE不指定WHERE条件一般是致命的,请您三思后行", Case:"update tbl set col=1", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"CLA.016", Severity:"L2", Summary:"不要UPDATE主键", Content:"主键是数据表中记录的唯一标识符,不建议频繁更新主键列,这将影响元数据统计信息进而影响正常的查询。", Case:"update tbl set col=1", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"CLA.017", Severity:"L2", Summary:"不建议使用存储过程、视图、触发器、临时表等", Content:"这些功能的使用在一定程度上会使得程序难以调试和拓展,更没有移植性,且会极大的增加出现BUG的概率。", Case:"CREATE VIEW v_today (today) AS SELECT CURRENT_DATE;", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"COL.001", Severity:"L1", Summary:"不建议使用SELECT * 类型查询", Content:"当表结构变更时,使用*通配符选择所有列将导致查询的含义和行为会发生更改,可能导致查询返回更多的数据。", Case:"select * from tbl where id=1", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"COL.002", Severity:"L2", Summary:"INSERT未指定列名", Content:"当表结构发生变更,如果INSERT或REPLACE请求不明确指定列名,请求的结果将会与预想的不同; 建议使用“INSERT INTO tbl(col1,col2)VALUES ...”代替。", Case:"insert into tbl values(1,'name')", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"COL.003", Severity:"L2", Summary:"建议修改自增ID为无符号类型", Content:"建议修改自增ID为无符号类型", Case:"create table test(`id` int(11) NOT NULL AUTO_INCREMENT)", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"COL.004", Severity:"L1", Summary:"请为列添加默认值", Content:"请为列添加默认值,如果是ALTER操作,请不要忘记将原字段的默认值写上。字段无默认值,当表较大时无法在线变更表结构。", Case:"CREATE TABLE tbl (col int) ENGINE=InnoDB;", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"COL.005", Severity:"L1", Summary:"列未添加注释", Content:"建议对表中每个列添加注释,来明确每个列在表中的含义及作用。", Case:"CREATE TABLE tbl (col int) ENGINE=InnoDB;", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"COL.006", Severity:"L3", Summary:"表中包含有太多的列", Content:"表中包含有太多的列", Case:"CREATE TABLE tbl ( cols ....);", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"COL.008", Severity:"L1", Summary:"可使用VARCHAR代替CHAR,VARBINARY代替BINARY", Content:"为首先变长字段存储空间小,可以节省存储空间。其次对于查询来说,在一个相对较小的字段内搜索效率显然要高些。", Case:"create table t1(id int,name char(20),last_time date)", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"COL.009", Severity:"L2", Summary:"建议使用精确的数据类型", Content:"实际上,任何使用FLOAT、REAL或DOUBLE PRECISION数据类型的设计都有可能是反模式。大多数应用程序使用的浮点数的取值范围并不需要达到IEEE 754标准所定义的最大/最小区间。在计算总量时,非精确浮点数所积累的影响是严重的。使用SQL中的NUMERIC或DECIMAL类型来代替FLOAT及其类似的数据类型进行固定精度的小数存储。这些数据类型精确地根据您定义这一列时指定的精度来存储数据。尽可能不要使用浮点数。", Case:"CREATE TABLE tab2 (p_id BIGINT UNSIGNED NOT NULL,a_id BIGINT UNSIGNED NOT NULL,hours float not null,PRIMARY KEY (p_id, a_id))", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"COL.010", Severity:"L2", Summary:"不建议使用ENUM数据类型", Content:"ENUM定义了列中值的类型,使用字符串表示ENUM里的值时,实际存储在列中的数据是这些值在定义时的序数。因此,这列的数据是字节对齐的,当您进行一次排序查询时,结果是按照实际存储的序数值排序的,而不是按字符串值的字母顺序排序的。这可能不是您所希望的。没有什么语法支持从ENUM或者check约束中添加或删除一个值;您只能使用一个新的集合重新定义这一列。如果您打算废弃一个选项,您可能会为历史数据而烦恼。作为一种策略,改变元数据——也就是说,改变表和列的定义——应该是不常见的,并且要注意测试和质量保证。有一个更好的解决方案来约束一列中的可选值:创建一张检查表,每一行包含一个允许在列中出现的候选值;然后在引用新表的旧表上声明一个外键约束。", Case:"create table tab1(status ENUM('new','in progress','fixed'))", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"COL.011", Severity:"L0", Summary:"当需要唯一约束时才使用NULL,仅当列不能有缺失值时才使用NOT NULL", Content:"NULL和0是不同的,10乘以NULL还是NULL。NULL和空字符串是不一样的。将一个字符串和标准SQL中的NULL联合起来的结果还是NULL。NULL和FALSE也是不同的。AND、OR和NOT这三个布尔操作如果涉及NULL,其结果也让很多人感到困惑。当您将一列声明为NOT NULL时,也就是说这列中的每一个值都必须存在且是有意义的。使用NULL来表示任意类型不存在的空值。 当您将一列声明为NOT NULL时,也就是说这列中的每一个值都必须存在且是有意义的。", Case:"select c1,c2,c3 from tbl where c4 is null or c4 <> 1", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"COL.012", Severity:"L5", Summary:"BLOB和TEXT类型的字段不可设置为NULL", Content:"BLOB和TEXT类型的字段不可设置为NULL", Case:"CREATE TABLE `tbl` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `c` longblob, PRIMARY KEY (`id`));", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"COL.013", Severity:"L4", Summary:"TIMESTAMP类型未设置默认值", Content:"TIMESTAMP类型未设置默认值", Case:"CREATE TABLE tbl( `id` bigint not null, `create_time` timestamp);", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"COL.014", Severity:"L5", Summary:"为列指定了字符集", Content:"建议列与表使用同一个字符集,不要单独指定列的字符集。", Case:"CREATE TABLE `tb2` ( `id` int(11) DEFAULT NULL, `col` char(10) CHARACTER SET utf8 DEFAULT NULL)", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"COL.015", Severity:"L4", Summary:"BLOB类型的字段不可指定默认值", Content:"BLOB类型的字段不可指定默认值", Case:"CREATE TABLE `tbl` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `c` blob NOT NULL DEFAULT '', PRIMARY KEY (`id`));", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"COL.016", Severity:"L1", Summary:"整型定义建议采用INT(10)或BIGINT(20)", Content:"INT(M) 在 integer 数据类型中,M 表示最大显示宽度。 在 INT(M) 中,M 的值跟 INT(M) 所占多少存储空间并无任何关系。 INT(3)、INT(4)、INT(8) 在磁盘上都是占用 4 bytes 的存储空间。", Case:"CREATE TABLE tab (a INT(1));", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"COL.017", Severity:"L2", Summary:"varchar定义长度过长", Content:"varchar 是可变长字符串,不预先分配存储空间,长度不要超过1024,如果存储长度过长MySQL将定义字段类型为text,独立出来一张表,用主键来对应,避免影响其它字段索引效率。", Case:"CREATE TABLE tab (a varchar(3500));", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"DIS.001", Severity:"L1", Summary:"消除不必要的DISTINCT条件", Content:"太多DISTINCT条件是复杂的裹脚布式查询的症状。考虑将复杂查询分解成许多简单的查询,并减少DISTINCT条件的数量。如果主键列是列的结果集的一部分,则DISTINCT条件可能没有影响。", Case:"SELECT DISTINCT c.c_id,count(DISTINCT c.c_name),count(DISTINCT c.c_e),count(DISTINCT c.c_n),count(DISTINCT c.c_me),c.c_d FROM (select distinct xing, name from B) as e WHERE e.country_id = c.country_id", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"DIS.002", Severity:"L3", Summary:"COUNT(DISTINCT)多列时结果可能和你预想的不同", Content:"COUNT(DISTINCT col)计算该列除NULL之外的不重复行数,注意COUNT(DISTINCT col, col2)如果其中一列全为NULL那么即使另一列有不同的值,也返回0。", Case:"SELECT COUNT(DISTINCT col, col2) FROM tbl;", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"DIS.003", Severity:"L3", Summary:"DISTINCT *对有主键的表没有意义", Content:"当表已经有主键时,对所有列进行DISTINCT的输出结果与不进行DISTINCT操作的结果相同,请不要画蛇添足。", Case:"SELECT DISTINCT * FROM film;", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"FUN.001", Severity:"L2", Summary:"避免在WHERE条件中使用函数或其他运算符", Content:"虽然在SQL中使用函数可以简化很多复杂的查询,但使用了函数的查询无法利用表中已经建立的索引,该查询将会是全表扫描,性能较差。通常建议将列名写在比较运算符左侧,将查询过滤条件放在比较运算符右侧。", Case:"select id from t where substring(name,1,3)='abc'", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"FUN.002", Severity:"L1", Summary:"指定了WHERE条件或非MyISAM引擎时使用COUNT(*)操作性能不佳", Content:"COUNT(*)的作用是统计表行数,COUNT(COL)的作用是统计指定列非NULL的行数。MyISAM表对于COUNT(*)统计全表行数进行了特殊的优化,通常情况下非常快。但对于非MyISAM表或指定了某些WHERE条件,COUNT(*)操作需要扫描大量的行才能获取精确的结果,性能也因此不佳。有时候某些业务场景并不需要完全精确的COUNT值,此时可以用近似值来代替。EXPLAIN出来的优化器估算的行数就是一个不错的近似值,执行EXPLAIN并不需要真正去执行查询,所以成本很低。", Case:"SELECT c3, COUNT(*) AS accounts FROM tab where c2 < 10000 GROUP BY c3 ORDER BY num", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"FUN.003", Severity:"L3", Summary:"使用了合并为可空列的字符串连接", Content:"在一些查询请求中,您需要强制让某一列或者某个表达式返回非NULL的值,从而让查询逻辑变得更简单,担忧不想将这个值存下来。使用COALESCE()函数来构造连接的表达式,这样即使是空值列也不会使整表达式变为NULL。", Case:"select c1 || coalesce(' ' || c2 || ' ', ' ') || c3 as c from tbl", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"FUN.004", Severity:"L4", Summary:"不建议使用SYSDATE()函数", Content:"SYSDATE()函数可能导致主从数据不一致,请使用NOW()函数替代SYSDATE()。", Case:"SELECT SYSDATE();", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"FUN.005", Severity:"L1", Summary:"不建议使用COUNT(col)或COUNT(常量)", Content:"不要使用COUNT(col)或COUNT(常量)来替代COUNT(*),COUNT(*)是SQL92定义的标准统计行数的方法,跟数据无关,跟NULL和非NULL也无关。", Case:"SELECT COUNT(1) FROM tbl;", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"FUN.006", Severity:"L1", Summary:"使用SUM(COL)时需注意NPE问题", Content:"当某一列的值全是NULL时,COUNT(COL)的返回结果为0,但SUM(COL)的返回结果为NULL,因此使用SUM()时需注意NPE问题。可以使用如下方式来避免SUM的NPE问题: SELECT IF(ISNULL(SUM(COL)), 0, SUM(COL)) FROM tbl", Case:"SELECT SUM(COL) FROM tbl;", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"GRP.001", Severity:"L2", Summary:"不建议对等值查询列使用GROUP BY", Content:"GROUP BY中的列在前面的WHERE条件中使用了等值查询,对这样的列进行GROUP BY意义不大。", Case:"select film_id, title from film where release_year='2006' group by release_year", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"JOI.001", Severity:"L2", Summary:"JOIN语句混用逗号和ANSI模式", Content:"表连接的时候混用逗号和ANSI JOIN不便于人类理解,并且MySQL不同版本的表连接行为和优先级均有所不同,当MySQL版本变化后可能会引入错误。", Case:"select c1,c2,c3 from t1,t2 join t3 on t1.c1=t2.c1,t1.c3=t3,c1 where id>1000", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"JOI.002", Severity:"L4", Summary:"同一张表被连接两次", Content:"相同的表在FROM子句中至少出现两次,可以简化为对该表的单次访问。", Case:"select tb1.col from (tb1, tb2) join tb2 on tb1.id=tb.id where tb1.id=1", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"JOI.003", Severity:"L4", Summary:"OUTER JOIN失效", Content:"由于WHERE条件错误使得OUTER JOIN的外部表无数据返回,这会将查询隐式转换为 INNER JOIN 。如:select c from L left join R using(c) where L.a=5 and R.b=10。这种SQL逻辑上可能存在错误或程序员对OUTER JOIN如何工作存在误解,因为LEFT/RIGHT JOIN是LEFT/RIGHT OUTER JOIN的缩写。", Case:"select c1,c2,c3 from t1 left outer join t2 using(c1) where t1.c2=2 and t2.c3=4", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"JOI.004", Severity:"L4", Summary:"不建议使用排它JOIN", Content:"只在右侧表为NULL的带WHERE子句的LEFT OUTER JOIN语句,有可能是在WHERE子句中使用错误的列,如:“... FROM l LEFT OUTER JOIN r ON l.l = r.r WHERE r.z IS NULL”,这个查询正确的逻辑可能是 WHERE r.r IS NULL。", Case:"select c1,c2,c3 from t1 left outer join t2 on t1.c1=t2.c1 where t2.c2 is null", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"JOI.005", Severity:"L2", Summary:"减少JOIN的数量", Content:"太多的JOIN是复杂的裹脚布式查询的症状。考虑将复杂查询分解成许多简单的查询,并减少JOIN的数量。", Case:"select bp1.p_id, b1.d_d as l, b1.b_id from b1 join bp1 on (b1.b_id = bp1.b_id) left outer join (b1 as b2 join bp2 on (b2.b_id = bp2.b_id)) on (bp1.p_id = bp2.p_id ) join bp21 on (b1.b_id = bp1.b_id) join bp31 on (b1.b_id = bp1.b_id) join bp41 on (b1.b_id = bp1.b_id) where b2.b_id = 0", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"JOI.008", Severity:"L4", Summary:"不要使用跨DB的Join查询", Content:"一般来说,跨DB的Join查询意味着查询语句跨越了两个不同的子系统,这可能意味着系统耦合度过高或库表结构设计不合理。", Case:"SELECT s,p,d FROM tbl WHERE p.p_id = (SELECT s.p_id FROM tbl WHERE s.c_id = 100996 AND s.q = 1 )", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"KEY.001", Severity:"L2", Summary:"建议使用自增列作为主键,如使用联合自增主键时请将自增键作为第一列", Content:"建议使用自增列作为主键,如使用联合自增主键时请将自增键作为第一列", Case:"create table test(`id` int(11) NOT NULL PRIMARY KEY (`id`))", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"KEY.003", Severity:"L4", Summary:"避免外键等递归关系", Content:"存在递归关系的数据很常见,数据常会像树或者以层级方式组织。然而,创建一个外键约束来强制执行同一表中两列之间的关系,会导致笨拙的查询。树的每一层对应着另一个连接。您将需要发出递归查询,以获得节点的所有后代或所有祖先。解决方案是构造一个附加的闭包表。它记录了树中所有节点间的关系,而不仅仅是那些具有直接的父子关系。您也可以比较不同层次的数据设计:闭包表,路径枚举,嵌套集。然后根据应用程序的需要选择一个。", Case:"CREATE TABLE tab2 (p_id BIGINT UNSIGNED NOT NULL,a_id BIGINT UNSIGNED NOT NULL,PRIMARY KEY (p_id, a_id),FOREIGN KEY (p_id) REFERENCES tab1(p_id),FOREIGN KEY (a_id) REFERENCES tab3(a_id))", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"KEY.004", Severity:"L0", Summary:"提醒:请将索引属性顺序与查询对齐", Content:"如果为列创建复合索引,请确保查询属性与索引属性的顺序相同,以便DBMS在处理查询时使用索引。如果查询和索引属性订单没有对齐,那么DBMS可能无法在查询处理期间使用索引。", Case:"create index idx1 on tbl (last_name,first_name)", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"KEY.005", Severity:"L2", Summary:"表建的索引过多", Content:"表建的索引过多", Case:"CREATE TABLE tbl ( a int, b int, c int, KEY idx_a (`a`),KEY idx_b(`b`),KEY idx_c(`c`));", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"KEY.006", Severity:"L4", Summary:"主键中的列过多", Content:"主键中的列过多", Case:"CREATE TABLE tbl ( a int, b int, c int, PRIMARY KEY(`a`,`b`,`c`));", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"KEY.007", Severity:"L4", Summary:"未指定主键或主键非int或bigint", Content:"未指定主键或主键非int或bigint,建议将主键设置为int unsigned或bigint unsigned。", Case:"CREATE TABLE tbl (a int);", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"KEY.008", Severity:"L4", Summary:"ORDER BY多个列但排序方向不同时可能无法使用索引", Content:"在MySQL 8.0之前当ORDER BY多个列指定的排序方向不同时将无法使用已经建立的索引。", Case:"SELECT * FROM tbl ORDER BY a DESC, b ASC;", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"KEY.009", Severity:"L0", Summary:"添加唯一索引前请注意检查数据唯一性", Content:"请提前检查添加唯一索引列的数据唯一性,如果数据不唯一在线表结构调整时将有可能自动将重复列删除,这有可能导致数据丢失。", Case:"CREATE UNIQUE INDEX part_of_name ON customer (name(10));", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"KWR.001", Severity:"L2", Summary:"SQL_CALC_FOUND_ROWS效率低下", Content:"因为SQL_CALC_FOUND_ROWS不能很好地扩展,所以可能导致性能问题; 建议业务使用其他策略来替代SQL_CALC_FOUND_ROWS提供的计数功能,比如:分页结果展示等。", Case:"select SQL_CALC_FOUND_ROWS col from tbl where id>1000", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"KWR.002", Severity:"L2", Summary:"不建议使用MySQL关键字做列名或表名", Content:"当使用关键字做为列名或表名时程序需要对列名和表名进行转义,如果疏忽被将导致请求无法执行。", Case:"CREATE TABLE tbl ( `select` int )", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"KWR.003", Severity:"L1", Summary:"不建议使用复数做列名或表名", Content:"表名应该仅仅表示表里面的实体内容,不应该表示实体数量,对应于 DO 类名也是单数形式,符合表达习惯。", Case:"CREATE TABLE tbl ( `books` int )", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"LCK.001", Severity:"L3", Summary:"INSERT INTO xx SELECT加锁粒度较大请谨慎", Content:"INSERT INTO xx SELECT加锁粒度较大请谨慎", Case:"INSERT INTO tbl SELECT * FROM tbl2;", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"LCK.002", Severity:"L3", Summary:"请慎用INSERT ON DUPLICATE KEY UPDATE", Content:"当主键为自增键时使用INSERT ON DUPLICATE KEY UPDATE可能会导致主键出现大量不连续快速增长,导致主键快速溢出无法继续写入。极端情况下还有可能导致主从数据不一致。", Case:"INSERT INTO t1(a,b,c) VALUES (1,2,3) ON DUPLICATE KEY UPDATE c=c+1;", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"LIT.001", Severity:"L2", Summary:"用字符类型存储IP地址", Content:"字符串字面上看起来像IP地址,但不是INET_ATON()的参数,表示数据被存储为字符而不是整数。将IP地址存储为整数更为有效。", Case:"insert into tbl (IP,name) values('10.20.306.122','test')", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"LIT.002", Severity:"L4", Summary:"日期/时间未使用引号括起", Content:"诸如“WHERE col <2010-02-12”之类的查询是有效的SQL,但可能是一个错误,因为它将被解释为“WHERE col <1996”; 日期/时间文字应该加引号。", Case:"select col1,col2 from tbl where time < 2018-01-10", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"LIT.003", Severity:"L3", Summary:"一列中存储一系列相关数据的集合", Content:"将ID存储为一个列表,作为VARCHAR/TEXT列,这样能导致性能和数据完整性问题。查询这样的列需要使用模式匹配的表达式。使用逗号分隔的列表来做多表联结查询定位一行数据是极不优雅和耗时的。这将使验证ID更加困难。考虑一下,列表最多支持存放多少数据呢?将ID存储在一张单独的表中,代替使用多值属性,从而每个单独的属性值都可以占据一行。这样交叉表实现了两张表之间的多对多关系。这将更好地简化查询,也更有效地验证ID。", Case:"select c1,c2,c3,c4 from tab1 where col_id REGEXP '[[:<:]]12[[:>:]]'", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"LIT.004", Severity:"L1", Summary:"请使用分号或已设定的DELIMITER结尾", Content:"USE database, SHOW DATABASES等命令也需要使用使用分号或已设定的DELIMITER结尾。", Case:"USE db", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"OK", Severity:"L0", Summary:"✔️", Content:"✔️", Case:"✔️", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"RES.001", Severity:"L4", Summary:"非确定性的GROUP BY", Content:"SQL返回的列既不在聚合函数中也不是GROUP BY表达式的列中,因此这些值的结果将是非确定性的。如:select a, b, c from tbl where foo=\"bar\" group by a,该SQL返回的结果就是不确定的。", Case:"select c1,c2,c3 from t1 where c2='foo' group by c2", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"RES.002", Severity:"L4", Summary:"未使用ORDER BY的LIMIT查询", Content:"没有ORDER BY的LIMIT会导致非确定性的结果,这取决于查询执行计划。", Case:"select col1,col2 from tbl where name=xx limit 10", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"RES.003", Severity:"L4", Summary:"UPDATE/DELETE操作使用了LIMIT条件", Content:"UPDATE/DELETE操作使用LIMIT条件和不添加WHERE条件一样危险,它可将会导致主从数据不一致或从库同步中断。", Case:"UPDATE film SET length = 120 WHERE title = 'abc' LIMIT 1;", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"RES.004", Severity:"L4", Summary:"UPDATE/DELETE操作指定了ORDER BY条件", Content:"UPDATE/DELETE操作不要指定ORDER BY条件。", Case:"UPDATE film SET length = 120 WHERE title = 'abc' ORDER BY title", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"RES.005", Severity:"L4", Summary:"UPDATE可能存在逻辑错误,导致数据损坏", Content:"", Case:"update tbl set col = 1 and cl = 2 where col=3;", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"RES.006", Severity:"L4", Summary:"永远不真的比较条件", Content:"查询条件永远非真,这将导致查询无匹配到的结果。", Case:"select * from tbl where 1 != 1;", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"RES.007", Severity:"L4", Summary:"永远为真的比较条件", Content:"查询条件永远为真,这将导致WHERE条件失效进行全表查询。", Case:"select * from tbl where 1 = 1;", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"RES.008", Severity:"L2", Summary:"不建议使用LOAD DATA/SELECT ... INTO OUTFILE", Content:"SELECT INTO OUTFILE需要授予FILE权限,这通过会引入安全问题。LOAD DATA虽然可以提高数据导入速度,但同时也可能导致从库同步延迟过大。", Case:"LOAD DATA INFILE 'data.txt' INTO TABLE db2.my_table;", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"SEC.001", Severity:"L0", Summary:"请谨慎使用TRUNCATE操作", Content:"一般来说想清空一张表最快速的做法就是使用TRUNCATE TABLE tbl_name;语句。但TRUNCATE操作也并非是毫无代价的,TRUNCATE TABLE无法返回被删除的准确行数,如果需要返回被删除的行数建议使用DELETE语法。TRUNCATE操作还会重置AUTO_INCREMENT,如果不想重置该值建议使用DELETE FROM tbl_name WHERE 1;替代。TRUNCATE操作会对数据字典添加源数据锁(MDL),当一次需要TRUNCATE很多表时会影响整个实例的所有请求,因此如果要TRUNCATE多个表建议用DROP+CREATE的方式以减少锁时长。", Case:"TRUNCATE TABLE tbl_name", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"SEC.002", Severity:"L0", Summary:"不使用明文存储密码", Content:"使用明文存储密码或者使用明文在网络上传递密码都是不安全的。如果攻击者能够截获您用来插入密码的SQL语句,他们就能直接读到密码。另外,将用户输入的字符串以明文的形式插入到纯SQL语句中,也会让攻击者发现它。如果您能够读取密码,黑客也可以。解决方案是使用单向哈希函数对原始密码进行加密编码。哈希是指将输入字符串转化成另一个新的、不可识别的字符串的函数。对密码加密表达式加点随机串来防御“字典攻击”。不要将明文密码输入到SQL查询语句中。在应用程序代码中计算哈希串,只在SQL查询中使用哈希串。", Case:"create table test(id int,name varchar(20) not null,password varchar(200)not null)", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"SEC.003", Severity:"L0", Summary:"使用DELETE/DROP/TRUNCATE等操作时注意备份", Content:"在执行高危操作之前对数据进行备份是十分有必要的。", Case:"delete from table where col = 'condition'", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"STA.001", Severity:"L0", Summary:"'!=' 运算符是非标准的", Content:"\"<>\"才是标准SQL中的不等于运算符。", Case:"select col1,col2 from tbl where type!=0", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"STA.002", Severity:"L1", Summary:"库名或表名点后建议不要加空格", Content:"当使用db.table或table.column格式访问表或字段时,请不要在点号后面添加空格,虽然这样语法正确。", Case:"select col from sakila. film", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"STA.003", Severity:"L1", Summary:"索引起名不规范", Content:"建议普通二级索引以idx_为前缀,唯一索引以uk_为前缀。", Case:"select col from now where type!=0", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"STA.004", Severity:"L1", Summary:"起名时请不要使用字母、数字和下划线之外的字符", Content:"以字母或下划线开头,名字只允许使用字母、数字和下划线。请统一大小写,不要使用驼峰命名法。不要在名字中出现连续下划线'__',这样很难辨认。", Case:"CREATE TABLE ` abc` (a int);", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"SUB.002", Severity:"L2", Summary:"如果您不在乎重复的话,建议使用UNION ALL替代UNION", Content:"与去除重复的UNION不同,UNION ALL允许重复元组。如果您不关心重复元组,那么使用UNION ALL将是一个更快的选项。", Case:"select teacher_id as id,people_name as name from t1,t2 where t1.teacher_id=t2.people_id union select student_id as id,people_name as name from t1,t2 where t1.student_id=t2.people_id", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"SUB.003", Severity:"L3", Summary:"考虑使用EXISTS而不是DISTINCT子查询", Content:"DISTINCT关键字在对元组排序后删除重复。相反,考虑使用一个带有EXISTS关键字的子查询,您可以避免返回整个表。", Case:"SELECT DISTINCT c.c_id, c.c_name FROM c,e WHERE e.c_id = c.c_id", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"SUB.004", Severity:"L3", Summary:"执行计划中嵌套连接深度过深", Content:"MySQL对子查询的优化效果不佳,MySQL将外部查询中的每一行作为依赖子查询执行子查询。 这是导致严重性能问题的常见原因。", Case:"SELECT * from tb where id in (select id from (select id from tb))", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"SUB.005", Severity:"L8", Summary:"子查询不支持LIMIT", Content:"当前MySQL版本不支持在子查询中进行'LIMIT & IN/ALL/ANY/SOME'。", Case:"SELECT * FROM staff WHERE name IN (SELECT NAME FROM customer ORDER BY name LIMIT 1)", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"SUB.006", Severity:"L2", Summary:"不建议在子查询中使用函数", Content:"MySQL将外部查询中的每一行作为依赖子查询执行子查询,如果在子查询中使用函数,即使是semi-join也很难进行高效的查询。可以将子查询重写为OUTER JOIN语句并用连接条件对数据进行过滤。", Case:"SELECT * FROM staff WHERE name IN (SELECT max(NAME) FROM customer)", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"TBL.001", Severity:"L4", Summary:"不建议使用分区表", Content:"不建议使用分区表", Case:"CREATE TABLE trb3(id INT, name VARCHAR(50), purchased DATE) PARTITION BY RANGE(YEAR(purchased)) (PARTITION p0 VALUES LESS THAN (1990), PARTITION p1 VALUES LESS THAN (1995), PARTITION p2 VALUES LESS THAN (2000), PARTITION p3 VALUES LESS THAN (2005) );", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"TBL.002", Severity:"L4", Summary:"请为表选择合适的存储引擎", Content:"建表或修改表的存储引擎时建议使用推荐的存储引擎,如:innodb", Case:"create table test(`id` int(11) NOT NULL AUTO_INCREMENT)", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"TBL.003", Severity:"L8", Summary:"以DUAL命名的表在数据库中有特殊含义", Content:"DUAL表为虚拟表,不需要创建即可使用,也不建议服务以DUAL命名表。", Case:"create table dual(id int, primary key (id));", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"TBL.004", Severity:"L2", Summary:"表的初始AUTO_INCREMENT值不为0", Content:"AUTO_INCREMENT不为0会导致数据空洞。", Case:"CREATE TABLE tbl (a int) AUTO_INCREMENT = 10;", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} +advisor.Rule{Item:"TBL.005", Severity:"L4", Summary:"请使用推荐的字符集", Content:"表字符集只允许设置为utf8,utf8mb4", Case:"CREATE TABLE tbl (a int) DEFAULT CHARSET = latin1;", Position:0, Func:func(*advisor.Query4Audit) advisor.Rule {...}} diff --git a/ast/doc.go b/ast/doc.go new file mode 100644 index 00000000..43bbb74f --- /dev/null +++ b/ast/doc.go @@ -0,0 +1,18 @@ +/* + * Copyright 2018 Xiaomi, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Package ast is an interface for Abstract Syntax Tree parser +package ast diff --git a/ast/meta.go b/ast/meta.go new file mode 100644 index 00000000..12a3f815 --- /dev/null +++ b/ast/meta.go @@ -0,0 +1,754 @@ +/* + * Copyright 2018 Xiaomi, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ast + +import ( + "fmt" + "strings" + + "github.com/XiaoMi/soar/common" + "vitess.io/vitess/go/vt/sqlparser" +) + +// GetTableFromExprs 从sqlparser.Exprs中获取所有的库表 +func GetTableFromExprs(exprs sqlparser.TableExprs, metas ...common.Meta) common.Meta { + meta := make(map[string]*common.DB) + if len(metas) >= 1 { + meta = metas[0] + } + + err := sqlparser.Walk(func(node sqlparser.SQLNode) (kontinue bool, err error) { + switch expr := node.(type) { + case *sqlparser.AliasedTableExpr: + + switch table := expr.Expr.(type) { + case sqlparser.TableName: + db := table.Qualifier.String() + tb := table.Name.String() + + if meta[db] == nil { + meta[db] = common.NewDB(db) + } + + meta[db].Table[tb] = common.NewTable(tb) + + // alias去重 + aliasExist := false + for _, existedAlias := range meta[db].Table[tb].TableAliases { + if existedAlias == expr.As.String() { + aliasExist = true + } + } + + if !aliasExist { + meta[db].Table[tb].TableAliases = append(meta[db].Table[tb].TableAliases, expr.As.String()) + } + } + } + return true, nil + }, exprs) + common.LogIfWarn(err, "") + return meta +} + +// GetMeta 获取元数据信息,构建到db->table层级。 +// 从 SQL 或 Statement 中获取表信息,并返回。当 meta 不为 nil 时,返回值会将新老 meta 合并去重 +func GetMeta(stmt sqlparser.Statement, meta common.Meta) common.Meta { + // 初始化meta + if meta == nil { + meta = make(map[string]*common.DB) + } + + err := sqlparser.Walk(func(node sqlparser.SQLNode) (kontinue bool, err error) { + switch expr := node.(type) { + case *sqlparser.DDL: + // 如果SQL是一个DDL,则不需要继续遍历语法树了 + db1 := expr.Table.Qualifier.String() + tb1 := expr.Table.Name.String() + db2 := expr.NewName.Qualifier.String() + tb2 := expr.NewName.Name.String() + + if tb1 != "" { + if _, ok := meta[db1]; !ok { + meta[db1] = common.NewDB(db1) + } + + meta[db1].Table[tb1] = common.NewTable(tb1) + } + + if tb2 != "" { + if _, ok := meta[db2]; !ok { + meta[db2] = common.NewDB(db2) + } + + meta[db1].Table[tb2] = common.NewTable(tb2) + } + + return false, nil + case *sqlparser.AliasedTableExpr: + // 非 DDL 情况下处理 TableExpr + // 在 sqlparser 中存在三种 TableExpr: AliasedTableExpr,ParenTableExpr 以及 JoinTableExpr。 + // 其中 AliasedTableExpr 是其他两种 TableExpr 的基础组成,SQL中的 表信息(别名、前缀)在这个结构体中。 + + switch table := expr.Expr.(type) { + + // 获取表名、别名与前缀名(数据库名) + // 表名存放在 AST 中 TableName 里,包含表名与表前缀名。 + // 当与 As 相对应的 Expr 为 TableName 的时候,别名才是一张实体表的别名,否则为结果集的别名。 + case sqlparser.TableName: + db := table.Qualifier.String() + tb := table.Name.String() + + if meta[db] == nil { + meta[db] = common.NewDB(db) + } + + meta[db].Table[tb] = common.NewTable(tb) + + // alias去重 + aliasExist := false + for _, existedAlias := range meta[db].Table[tb].TableAliases { + if existedAlias == expr.As.String() { + aliasExist = true + } + } + if !aliasExist { + meta[db].Table[tb].TableAliases = append(meta[db].Table[tb].TableAliases, expr.As.String()) + } + + default: + // 如果 AliasedTableExpr 中的 Expr 不是 TableName 结构体,则表示该表为一个查询结果集(子查询或临时表)。 + // 在这里记录一下别名,但将列名制空,用来保证在其他环节中判断列前缀的时候不会有遗漏 + // 最终结果为所有的子查询别名都会归于 ""(空) 数据库 ""(空) 表下,对于空数据库,空表后续在索引优化时直接PASS + if meta == nil { + meta = make(map[string]*common.DB) + } + + if meta[""] == nil { + meta[""] = common.NewDB("") + } + + meta[""].Table[""] = common.NewTable("") + meta[""].Table[""].TableAliases = append(meta[""].Table[""].TableAliases, expr.As.String()) + } + } + return true, nil + }, stmt) + common.LogIfWarn(err, "") + return meta +} + +// eqOperators 等值条件判断关键字 +var eqOperators = map[string]string{ + "=": "eq", + "<=>": "eq", + "is true": "eq", + "is false": "eq", + "is not true": "eq", + "is not false": "eq", + "is null": "eq", + "in": "eq", // 单值的in属于等值条件 +} + +// inEqOperators 非等值条件判断关键字 +var inEqOperators = map[string]string{ + "<": "inEq", + ">": "inEq", + "<=": "inEq", + ">=": "inEq", + "!=": "inEq", + "is not null": "inEq", + "like": "inEq", + "not like": "inEq", + "->": "inEq", + "->>": "inEq", + "between": "inEq", + "not between": "inEq", + "in": "inEq", // 多值in属于非等值条件 + + // 某些非等值条件无需添加索引,所以忽略即可 + // 比如"not in",比如"exists"、 "not exists"等 +} + +// FindColumn 从传入的node中获取所有可能加索引的的column信息 +func FindColumn(node sqlparser.SQLNode) []*common.Column { + common.Log.Debug("Enter: FindColumn, Caller: %s", common.Caller()) + var result []*common.Column + err := sqlparser.Walk(func(node sqlparser.SQLNode) (kontinue bool, err error) { + switch col := node.(type) { + case *sqlparser.FuncExpr: + // 忽略function + return false, nil + case *sqlparser.ColName: + result = common.MergeColumn(result, &common.Column{ + Name: col.Name.String(), + Table: col.Qualifier.Name.String(), + DB: col.Qualifier.Qualifier.String(), + Alias: make([]string, 0), + }) + } + + return true, nil + }, node) + common.LogIfWarn(err, "") + return result +} + +// inEqIndexAble 判断非等值查询条件是否可以复用到索引 +// Output: true 可以考虑添加索引, false 不需要添加索引 +func inEqIndexAble(node sqlparser.SQLNode) bool { + common.Log.Debug("Enter: inEqIndexAble(), Caller: %s", common.Caller()) + var indexAble bool + switch expr := node.(type) { + case *sqlparser.ComparisonExpr: + // like前百分号查询无法使用索引 + // TODO date类型的like属于隐式数据类型转换,会导致无法使用索引 + if expr.Operator == "like" || expr.Operator == "not like" { + switch right := expr.Right.(type) { + case *sqlparser.SQLVal: + return !(strings.HasPrefix(string(right.Val), "%")) + } + } + + // 如果是in查询,则需要判断in查询是否是多值查询 + if expr.Operator == "in" { + switch right := expr.Right.(type) { + case sqlparser.ValTuple: + // 若是单值查询则应该属于等值条件而非非等值条件 + return len(right) > 1 + } + } + + _, indexAble = inEqOperators[expr.Operator] + + case *sqlparser.IsExpr: + _, indexAble = inEqOperators[expr.Operator] + + case *sqlparser.RangeCond: + _, indexAble = inEqOperators[expr.Operator] + + default: + indexAble = false + } + return indexAble +} + +// FindWhereEQ 找到Where中的等值条件 +func FindWhereEQ(node sqlparser.SQLNode) []*common.Column { + common.Log.Debug("Enter: FindWhereEQ(), Caller: %s", common.Caller()) + return append(FindEQColsInWhere(node), FindEQColsInJoinCond(node)...) +} + +// FindWhereINEQ 找到Where条件中的非等值条件 +func FindWhereINEQ(node sqlparser.SQLNode) []*common.Column { + common.Log.Debug("Enter: FindWhereINEQ(), Caller: %s", common.Caller()) + return append(FindINEQColsInWhere(node), FindINEQColsInJoinCond(node)...) +} + +// FindEQColsInWhere 获取等值条件信息 +// 将所有值得加索引的condition条件信息进行过滤 +func FindEQColsInWhere(node sqlparser.SQLNode) []*common.Column { + common.Log.Debug("Enter: FindEQColsInWhere(), Caller: %s", common.Caller()) + var columns []*common.Column + err := sqlparser.Walk(func(node sqlparser.SQLNode) (kontinue bool, err error) { + switch node := node.(type) { + // 对AST中所有节点进行扫描 + case *sqlparser.Subquery, *sqlparser.JoinTableExpr, *sqlparser.BinaryExpr, *sqlparser.OrExpr: + // 忽略子查询,join condition,数值计算,or condition + return false, nil + + case *sqlparser.ComparisonExpr: + var newCols []*common.Column + // ComparisonExpr中可能含有等值查询列条件 + switch node.Operator { + case "in": + // 对in进行特别判断,只有单值的in条件才算做是等值查询 + switch right := node.Right.(type) { + case sqlparser.ValTuple: + if len(right) == 1 { + newCols = FindColumn(node) + } + } + + default: + if _, ok := eqOperators[node.Operator]; ok { + newCols = FindColumn(node) + } + } + + // operator两边都为列的情况不提供索引建议 + // 如果该列位于function中则不予提供索引建议 + if len(newCols) == 1 { + columns = common.MergeColumn(columns, newCols[0]) + } + + case *sqlparser.IsExpr: + // IsExpr中可能含有等值查询列条件 + if _, ok := eqOperators[node.Operator]; ok { + newCols := FindColumn(node) + if len(newCols) == 1 { + columns = common.MergeColumn(columns, newCols[0]) + } + } + } + return true, nil + + }, node) + common.LogIfWarn(err, "") + return columns +} + +// FindINEQColsInWhere 获取非等值条件中可能需要加索引的列 +// 将所有值得加索引的condition条件信息进行过滤 +// TODO: 将where条件中隐含的join条件合并到join condition中 +func FindINEQColsInWhere(node sqlparser.SQLNode) []*common.Column { + common.Log.Debug("Enter: FindINEQColsInWhere(), Caller: %s", common.Caller()) + var columns []*common.Column + err := sqlparser.Walk(func(node sqlparser.SQLNode) (kontinue bool, err error) { + switch node := node.(type) { + // 对AST中所有节点进行扫描 + case *sqlparser.Subquery, *sqlparser.JoinTableExpr, *sqlparser.BinaryExpr, *sqlparser.OrExpr: + // 忽略子查询,join condition,数值计算,or condition + return false, nil + + case *sqlparser.ComparisonExpr: + // ComparisonExpr中可能含有非等值查询列条件 + if inEqIndexAble(node) { + newCols := FindColumn(node) + // operator两边都为列的情况不提供索引建议 + if len(newCols) == 1 { + columns = common.MergeColumn(columns, newCols[0]) + } + } + case *sqlparser.IsExpr: + // IsExpr中可能含有非等值查询列条件 + if inEqIndexAble(node) { + newCols := FindColumn(node) + if len(newCols) == 1 { + columns = common.MergeColumn(columns, newCols[0]) + } + } + + case *sqlparser.RangeCond: + // RangeCond中只存在非等值条件查询 + if inEqIndexAble(node) { + columns = common.MergeColumn(columns, FindColumn(node)...) + } + } + + return true, nil + + }, node) + common.LogIfWarn(err, "") + return columns +} + +// FindGroupByCols 获取groupBy中可能需要加索引的列信息 +func FindGroupByCols(node sqlparser.SQLNode) []*common.Column { + common.Log.Debug("Enter: FindGroupByCols(), Caller: %s", common.Caller()) + isIgnore := false + var columns []*common.Column + err := sqlparser.Walk(func(node sqlparser.SQLNode) (kontinue bool, err error) { + switch expr := node.(type) { + case sqlparser.GroupBy: + err := sqlparser.Walk(func(node sqlparser.SQLNode) (kontinue bool, err error) { + switch node := node.(type) { + case *sqlparser.BinaryExpr, *sqlparser.FuncExpr: + // 如果group by中出现数值计算、函数等 + isIgnore = true + return false, nil + default: + columns = common.MergeColumn(columns, FindColumn(node)...) + } + return true, nil + }, expr) + common.LogIfWarn(err, "") + case *sqlparser.Subquery, *sqlparser.JoinTableExpr, *sqlparser.BinaryExpr: + // 忽略子查询,join condition以及数值计算 + return false, nil + } + return true, nil + }, node) + common.LogIfWarn(err, "") + if isIgnore { + return []*common.Column{} + } + + return columns +} + +// FindOrderByCols 为索引优化获取orderBy中可能添加索引的列信息 +func FindOrderByCols(node sqlparser.SQLNode) []*common.Column { + common.Log.Debug("Enter: FindOrderByCols(), Caller: %s", common.Caller()) + var columns []*common.Column + lastDirection := "" + directionNotEq := false + err := sqlparser.Walk(func(node sqlparser.SQLNode) (kontinue bool, err error) { + switch expr := node.(type) { + case *sqlparser.Order: + // MySQL对于排序顺序不同的查询无法使用索引(8.0后支持) + if lastDirection != "" && expr.Direction != lastDirection { + directionNotEq = true + return false, nil + } + lastDirection = expr.Direction + columns = common.MergeColumn(columns, FindColumn(expr)...) + case *sqlparser.Subquery, *sqlparser.JoinTableExpr, *sqlparser.BinaryExpr: + // 忽略子查询,join condition以及数值计算 + return false, nil + } + return true, nil + }, node) + common.LogIfWarn(err, "") + if directionNotEq { + // 当发现Order by中排序顺序不同时,即放弃Oder by条件中的字段 + return []*common.Column{} + } + + return columns +} + +// FindJoinTable 获取 Join 中需要添加索引的表 +// join 优化添加索引分为三种类型:1. inner join, 2. left join, 3.right join +// 针对三种优化类型,需要三种不同的索引添加方案: +// 1. inner join 需要对 join 左右的表添加索引 +// 2. left join 由于左表为全表扫描,需要对右表的关联列添加索引。 +// 3. right join 与 left join 相反,需要对左表的关联列添加索引。 +// 以上添加索引的策略前提为join的表为实体表而非临时表。 +func FindJoinTable(node sqlparser.SQLNode, meta common.Meta) common.Meta { + common.Log.Debug("Enter: FindJoinTable(), Caller: %s", common.Caller()) + if meta == nil { + meta = make(common.Meta) + } + err := sqlparser.Walk(func(node sqlparser.SQLNode) (kontinue bool, err error) { + switch expr := node.(type) { + case *sqlparser.JoinTableExpr: + switch expr.Join { + case "join", "natural join": + // 两边表都需要 + findJoinTable(expr.LeftExpr, meta) + findJoinTable(expr.RightExpr, meta) + case "left join", "natural left join", "straight_join": + // 只需要右表 + findJoinTable(expr.RightExpr, meta) + + case "right join", "natural right join": + // 只需要左表 + findJoinTable(expr.LeftExpr, meta) + } + } + return true, nil + }, node) + common.LogIfWarn(err, "") + return meta +} + +// findJoinTable 获取join table +func findJoinTable(expr sqlparser.TableExpr, meta common.Meta) { + common.Log.Debug("Enter: findJoinTable(), Caller: %s", common.Caller()) + if meta == nil { + meta = make(common.Meta) + } + switch tableExpr := expr.(type) { + case *sqlparser.AliasedTableExpr: + switch table := tableExpr.Expr.(type) { + // 获取表名、别名与前缀名(数据库名) + // 表名存放在 AST 中 TableName 里,包含表名与表前缀名。 + // 当与 As 相对应的 Expr 为 TableName 的时候,别名才是一张实体表的别名,否则为结果集的别名。 + case sqlparser.TableName: + db := table.Qualifier.String() + tb := table.Name.String() + + if meta == nil { + meta = make(map[string]*common.DB) + } + + if meta[db] == nil { + meta[db] = common.NewDB(db) + } + + meta[db].Table[tb] = common.NewTable(tb) + + // alias去重 + aliasExist := false + for _, existedAlias := range meta[db].Table[tb].TableAliases { + if existedAlias == tableExpr.As.String() { + aliasExist = true + } + } + if !aliasExist { + meta[db].Table[tb].TableAliases = append(meta[db].Table[tb].TableAliases, tableExpr.As.String()) + } + } + case *sqlparser.ParenTableExpr: + // join 时可能会同时 join 多张表 + for _, tbExpr := range tableExpr.Exprs { + findJoinTable(tbExpr, meta) + } + default: + // 如果是如上两种类型都没有命中,说明join的表为临时表,递归调用 FindJoinTable 继续下探查找。 + // NOTE: 这里需要注意的是,如果不递归寻找,如果存在子查询结果集的join表,subquery也会把这个查询提取出。 + // 所以针对default这一段理论上可以忽略处理(待测试) + FindJoinTable(tableExpr, meta) + } +} + +// FindJoinCols 获取 join condition 中使用到的列(必须是 `列 operator 列` 的情况。 +// 如果列对应的值或是function,则应该移到where condition中) +// 某些where条件隐含在Join条件中(INNER JOIN) +func FindJoinCols(node sqlparser.SQLNode) [][]*common.Column { + common.Log.Debug("Enter: FindJoinCols(), Caller: %s", common.Caller()) + var columns [][]*common.Column + err := sqlparser.Walk(func(node sqlparser.SQLNode) (kontinue bool, err error) { + switch expr := node.(type) { + case *sqlparser.JoinTableExpr: + // on + if on := expr.Condition.On; on != nil { + cols := FindColumn(expr.Condition.On) + if len(cols) > 1 { + columns = append(columns, cols) + } + } + + // using + if using := expr.Condition.Using; using != nil { + left := "" + right := "" + + switch tableExpr := expr.LeftExpr.(type) { + case *sqlparser.AliasedTableExpr: + switch table := tableExpr.Expr.(type) { + case sqlparser.TableName: + left = table.Name.String() + } + } + + switch tableExpr := expr.RightExpr.(type) { + case *sqlparser.AliasedTableExpr: + switch table := tableExpr.Expr.(type) { + case sqlparser.TableName: + right = table.Name.String() + } + } + + var cols []*common.Column + for _, col := range using { + if left != "" { + cols = append(cols, &common.Column{ + Name: col.String(), + Table: left, + Alias: make([]string, 0), + }) + } + + if right != "" { + cols = append(cols, &common.Column{ + Name: col.String(), + Table: right, + Alias: make([]string, 0), + }) + } + + } + columns = append(columns, cols) + + } + + } + return true, nil + }, node) + common.LogIfWarn(err, "") + return columns +} + +// FindEQColsInJoinCond 获取 join condition 中应转为whereEQ条件的列 +func FindEQColsInJoinCond(node sqlparser.SQLNode) []*common.Column { + common.Log.Debug("Enter: FindEQColsInJoinCond(), Caller: %s", common.Caller()) + var columns []*common.Column + err := sqlparser.Walk(func(node sqlparser.SQLNode) (kontinue bool, err error) { + switch expr := node.(type) { + case sqlparser.JoinCondition: + columns = append(columns, FindEQColsInWhere(expr)...) + } + return true, nil + }, node) + common.LogIfWarn(err, "") + return columns +} + +// FindINEQColsInJoinCond 获取 join condition 中应转为whereINEQ条件的列 +func FindINEQColsInJoinCond(node sqlparser.SQLNode) []*common.Column { + common.Log.Debug("Enter: FindINEQColsInJoinCond(), Caller: %s", common.Caller()) + var columns []*common.Column + err := sqlparser.Walk(func(node sqlparser.SQLNode) (kontinue bool, err error) { + switch expr := node.(type) { + case sqlparser.JoinCondition: + columns = append(columns, FindINEQColsInWhere(expr)...) + } + return true, nil + }, node) + common.LogIfWarn(err, "") + return columns +} + +// FindSubquery 拆分subquery,获取最深层的subquery +// 为索引优化获取subquery中包含的列信息 +func FindSubquery(depth int, node sqlparser.SQLNode, queries ...string) []string { + common.Log.Debug("Enter: FindSubquery(), Caller: %s", common.Caller()) + if queries == nil { + queries = make([]string, 0) + } + err := sqlparser.Walk(func(node sqlparser.SQLNode) (kontinue bool, err error) { + switch expr := node.(type) { + // 查找SQL中的子查询 + case *sqlparser.Subquery: + noSub := true + // 查看子查询中是否还包含子查询,如果包含,递归找到最深层的子查询 + err := sqlparser.Walk(func(node sqlparser.SQLNode) (kontinue bool, err error) { + switch sub := node.(type) { + case *sqlparser.Subquery: + noSub = false + // 查找深度depth,超过最大深度后不再向下查找 + depth = depth + 1 + if depth < common.Config.MaxSubqueryDepth { + queries = append(queries, FindSubquery(depth, sub.Select)...) + } + } + return true, nil + }, expr.Select) + common.LogIfWarn(err, "") + + // 如果没有嵌套的子查询了,返回子查询的SQL + if noSub { + sql := sqlparser.String(expr) + // 去除SQL前后的括号 + queries = append(queries, sql[1:len(sql)-1]) + } + + } + return true, nil + }, node) + common.LogIfWarn(err, "") + return queries +} + +// FindAllCondition 获取AST中所有的condition条件 +func FindAllCondition(node sqlparser.SQLNode) []interface{} { + common.Log.Debug("Enter: FindAllCondition(), Caller: %s", common.Caller()) + var conditions []interface{} + err := sqlparser.Walk(func(node sqlparser.SQLNode) (kontinue bool, err error) { + switch node := node.(type) { + case *sqlparser.ComparisonExpr, *sqlparser.RangeCond, *sqlparser.IsExpr: + conditions = append(conditions, node) + } + return true, nil + }, node) + common.LogIfWarn(err, "") + return conditions +} + +// FindAllCols 获取AST中某个节点下所有的columns +func FindAllCols(node sqlparser.SQLNode, targets ...string) []*common.Column { + var result []*common.Column + // 获取节点内所有的列 + f := func(node sqlparser.SQLNode) { + err := sqlparser.Walk(func(node sqlparser.SQLNode) (kontinue bool, err error) { + switch col := node.(type) { + case *sqlparser.ColName: + result = common.MergeColumn(result, &common.Column{ + Name: col.Name.String(), + Table: col.Qualifier.Name.String(), + DB: col.Qualifier.Qualifier.String(), + Alias: make([]string, 0), + }) + } + return true, nil + }, node) + common.LogIfWarn(err, "") + } + + if len(targets) == 0 { + // 如果不指定具体节点类型,则获取全部的column + f(node) + } else { + // 根据target获取所有的节点 + for _, target := range targets { + target = strings.Replace(strings.ToLower(target), " ", "", -1) + err := sqlparser.Walk(func(node sqlparser.SQLNode) (kontinue bool, err error) { + switch node := node.(type) { + case *sqlparser.Subquery: + // 忽略子查询 + case *sqlparser.JoinTableExpr: + if target == "join" { + f(node) + } + case *sqlparser.Where: + if target == "where" { + f(node) + } + case *sqlparser.GroupBy: + if target == "groupby" { + f(node) + } + case sqlparser.OrderBy: + if target == "orderby" { + f(node) + } + } + return true, nil + }, node) + common.LogIfWarn(err, "") + } + } + + return result +} + +// GetSubqueryDepth 获取一条SQL的嵌套深度 +func GetSubqueryDepth(node sqlparser.SQLNode) int { + depth := 1 + err := sqlparser.Walk(func(node sqlparser.SQLNode) (kontinue bool, err error) { + switch node.(type) { + case *sqlparser.Subquery: + depth++ + } + return true, nil + }, node) + common.LogIfWarn(err, "") + return depth +} + +// getColumnName 获取node中Column具体的定义以及名称 +func getColumnName(node sqlparser.SQLNode) (*sqlparser.ColName, string) { + var colName *sqlparser.ColName + str := "" + switch c := node.(type) { + case *sqlparser.ColName: + if c.Qualifier.Name.IsEmpty() { + str = fmt.Sprintf("`%s`", c.Name.String()) + } else { + if c.Qualifier.Qualifier.IsEmpty() { + str = fmt.Sprintf("`%s`.`%s`", c.Qualifier.Name.String(), c.Name.String()) + } else { + str = fmt.Sprintf("`%s`.`%s`.`%s`", + c.Qualifier.Qualifier.String(), c.Qualifier.Name.String(), c.Name.String()) + } + } + colName = c + } + return colName, str +} diff --git a/ast/meta_test.go b/ast/meta_test.go new file mode 100644 index 00000000..2da7a706 --- /dev/null +++ b/ast/meta_test.go @@ -0,0 +1,324 @@ +/* + * Copyright 2018 Xiaomi, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ast + +import ( + "fmt" + "testing" + + "github.com/XiaoMi/soar/common" + + "github.com/kr/pretty" + "vitess.io/vitess/go/vt/sqlparser" +) + +func TestGetTableFromExprs(t *testing.T) { + tbExprs := sqlparser.TableExprs{ + &sqlparser.AliasedTableExpr{ + Expr: sqlparser.TableName{ + Name: sqlparser.NewTableIdent("table"), + Qualifier: sqlparser.NewTableIdent("db"), + }, + As: sqlparser.NewTableIdent("as"), + }, + } + meta := GetTableFromExprs(tbExprs) + if tb, ok := meta["db"]; !ok { + t.Errorf("no table qualifier, meta: %s", pretty.Sprint(tb)) + } +} + +func TestGetParseTableWithStmt(t *testing.T) { + for _, sql := range common.TestSQLs { + fmt.Println(sql) + stmt, err := sqlparser.Parse(sql) + if err != nil { + t.Errorf("SQL Parsed error: %v", err) + } + meta := GetMeta(stmt, nil) + pretty.Println(meta) + fmt.Println() + } +} + +func TestFindCondition(t *testing.T) { + for _, sql := range common.TestSQLs { + fmt.Println(sql) + stmt, err := sqlparser.Parse(sql) + //pretty.Println(stmt) + if err != nil { + panic(err) + } + eq := FindEQColsInWhere(stmt) + inEq := FindINEQColsInWhere(stmt) + fmt.Println("WherEQ:") + pretty.Println(eq) + fmt.Println("WherINEQ:") + pretty.Println(inEq) + fmt.Println() + } +} + +func TestFindGroupBy(t *testing.T) { + sqlList := []string{ + "select a from t group by c", + } + + for _, sql := range sqlList { + fmt.Println(sql) + stmt, err := sqlparser.Parse(sql) + if err != nil { + panic(err) + } + res := FindGroupByCols(stmt) + pretty.Println(res) + fmt.Println() + } +} + +func TestFindOrderBy(t *testing.T) { + sqlList := []string{ + "select a from t group by c order by d, c desc", + "select a from t group by c order by d desc", + } + + for _, sql := range sqlList { + fmt.Println(sql) + stmt, err := sqlparser.Parse(sql) + if err != nil { + panic(err) + } + res := FindOrderByCols(stmt) + pretty.Println(res) + fmt.Println() + } +} + +func TestFindSubquery(t *testing.T) { + sqlList := []string{ + "SELECT * FROM t1 WHERE column1 = (SELECT column1 FROM (SELECT column1 FROM t2) a);", + "select column1 from t2", + "SELECT * FROM t1 WHERE column1 = (SELECT column1 FROM t2);", + "select ID,name from (select address from customer_list where SID=1 order by phone limit 50,10) a join customer_list l on (a.address=l.address) join city c on (c.city=l.city) order by phone desc;", + } + + for _, sql := range sqlList { + fmt.Println(sql) + stmt, err := sqlparser.Parse(sql) + if err != nil { + panic(err) + } + + subquery := FindSubquery(0, stmt) + fmt.Println(len(subquery)) + pretty.Println(subquery) + } + +} + +func TestFindJoinTable(t *testing.T) { + sqlList := []string{ + "SELECT * FROM t1 LEFT JOIN (t2 CROSS JOIN t3 CROSS JOIN t4) ON (t2.a = t1.a AND t3.b = t1.b AND t4.c = t1.c)", + "select ID,name from (select address from customer_list where SID=1 order by phone limit 50,10) a join customer_list l on (a.address=l.address) join city c on (c.city=l.city) order by phone desc;", + "SELECT * FROM t1 LEFT JOIN (t2, t3, t4) ON (t2.a = t1.a AND t3.b = t1.b AND t4.c = t1.c)", + "SELECT * FROM t1 RIGHT JOIN (t2, t3, t4) ON (t2.a = t1.a AND t3.b = t1.b AND t4.c = t1.c)", + "SELECT left_tbl.* FROM left_tbl LEFT JOIN right_tbl ON left_tbl.id = right_tbl.id WHERE right_tbl.id IS NULL;", + "SELECT left_tbl.* FROM left_tbl RIGHT JOIN right_tbl ON left_tbl.id = right_tbl.id WHERE right_tbl.id IS NULL;", + } + + for _, sql := range sqlList { + fmt.Println(sql) + stmt, err := sqlparser.Parse(sql) + //pretty.Println(stmt) + if err != nil { + panic(err) + } + + joinMeta := FindJoinTable(stmt, nil) + pretty.Println(joinMeta) + } +} + +func TestFindJoinCols(t *testing.T) { + sqlList := []string{ + "SELECT * FROM t1 LEFT JOIN (t2 CROSS JOIN t3 CROSS JOIN t4) ON (t2.a = t1.a AND t3.b = t1.b AND t4.c = t1.c)", + "select t from a LEFT JOIN b USING (c1, c2, c3)", + "select ID,name from (select address from customer_list where SID=1 order by phone limit 50,10) a join customer_list l on (a.address=l.address) join city c on (c.city=l.city) order by phone desc;", + "SELECT * FROM t1 LEFT JOIN (t2, t3, t4) ON (t2.a = t1.a AND t3.b = t1.b AND t4.c = t1.c)", + "SELECT * FROM t1 RIGHT JOIN (t2, t3, t4) ON (t2.a = t1.a AND t3.b = t1.b AND t4.c = t1.c)", + "SELECT left_tbl.* FROM left_tbl LEFT JOIN right_tbl ON left_tbl.id = right_tbl.id WHERE right_tbl.id IS NULL;", + "SELECT left_tbl.* FROM left_tbl RIGHT JOIN right_tbl ON left_tbl.id = right_tbl.id WHERE right_tbl.id IS NULL;", + } + + for _, sql := range sqlList { + fmt.Println(sql) + stmt, err := sqlparser.Parse(sql) + //pretty.Println(stmt) + if err != nil { + panic(err) + } + + columns := FindJoinCols(stmt) + pretty.Println(columns) + } +} + +func TestFindJoinColBeWhereEQ(t *testing.T) { + sqlList := []string{ + "select ID,name from (select address from customer_list where SID=1 order by phone limit 50,10) a join customer_list l on (a.address=l.address) join city c on (c.city=l.city) order by phone desc;", + "SELECT * FROM t1 LEFT JOIN (t2, t3, t4) ON (t2.a = t1.a AND t3.b = t1.b AND t4.c = t1.c)", + "SELECT * FROM t1 RIGHT JOIN (t2, t3, t4) ON (t2.a = t1.a AND t3.b = t1.b AND t4.c = t1.c)", + "SELECT left_tbl.* FROM left_tbl LEFT JOIN right_tbl ON left_tbl.id = right_tbl.id WHERE right_tbl.id IS NULL;", + "SELECT left_tbl.* FROM left_tbl RIGHT JOIN right_tbl ON left_tbl.id = right_tbl.id WHERE right_tbl.id IS NULL;", + } + + for _, sql := range sqlList { + fmt.Println(sql) + stmt, err := sqlparser.Parse(sql) + //pretty.Println(stmt) + if err != nil { + panic(err) + } + + columns := FindEQColsInJoinCond(stmt) + pretty.Println(columns) + } +} + +func TestFindJoinColBeWhereINEQ(t *testing.T) { + sqlList := []string{ + "select ID,name from (select address from customer_list where SID=1 order by phone limit 50,10) a join customer_list l on (a.address=l.address) join city c on (c.city=l.city) order by phone desc;", + "SELECT * FROM t1 LEFT JOIN (t2, t3, t4) ON (t2.a = t1.a AND t3.b = t1.b AND t4.c = t1.c)", + "SELECT * FROM t1 RIGHT JOIN (t2, t3, t4) ON (t2.a = t1.a AND t3.b > 'b' AND t4.c = t1.c)", + "SELECT left_tbl.* FROM left_tbl LEFT JOIN right_tbl ON left_tbl.id = right_tbl.id WHERE right_tbl.id IS NULL;", + "SELECT left_tbl.* FROM left_tbl RIGHT JOIN right_tbl ON left_tbl.id = right_tbl.id WHERE right_tbl.id IS NULL;", + } + + for _, sql := range sqlList { + fmt.Println(sql) + stmt, err := sqlparser.Parse(sql) + //pretty.Println(stmt) + if err != nil { + panic(err) + } + + columns := FindINEQColsInJoinCond(stmt) + pretty.Println(columns) + } +} + +func TestFindAllCondition(t *testing.T) { + sqlList := []string{ + "SELECT * FROM t1 LEFT JOIN (t2 CROSS JOIN t3 CROSS JOIN t4) ON (t2.a = t1.a AND t3.b = t1.b AND t4.c = t1.c)", + "select t from a LEFT JOIN b USING (c1, c2, c3)", + "select ID,name from (select address from customer_list where SID=1 order by phone limit 50,10) a join customer_list l on (a.address=l.address) join city c on (c.city=l.city) order by phone desc;", + "SELECT * FROM t1 LEFT JOIN (t2, t3, t4) ON (t2.a = t1.a AND t3.b = t1.b AND t4.c = t1.c)", + "SELECT * FROM t1 RIGHT JOIN (t2, t3, t4) ON (t2.a = t1.a AND t3.b = t1.b AND t4.c = t1.c)", + "SELECT left_tbl.* FROM left_tbl LEFT JOIN right_tbl ON left_tbl.id = right_tbl.id WHERE right_tbl.id IS NULL;", + "SELECT left_tbl.* FROM left_tbl RIGHT JOIN right_tbl ON left_tbl.id = right_tbl.id WHERE right_tbl.id IS NULL;", + "SELECT * FROM t1 where a in ('a','b')", + "SELECT * FROM t1 where a BETWEEN 'bar' AND 'foo'", + "SELECT * FROM t1 where a = sum(a,b)", + "SELECT distinct a FROM t1 where a = '2001-01-01 01:01:01'", + } + + for _, sql := range sqlList { + fmt.Println(sql) + stmt, err := sqlparser.Parse(sql) + //pretty.Println(stmt) + if err != nil { + panic(err) + } + + columns := FindAllCondition(stmt) + pretty.Println(columns) + } +} + +func TestFindColumn(t *testing.T) { + sqlList := []string{ + "select col, col2, sum(col1) from tb group by col", + "select col from tb group by col,sum(col1)", + "select col, sum(col1) from tb group by col", + } + for _, sql := range sqlList { + fmt.Println(sql) + stmt, err := sqlparser.Parse(sql) + //pretty.Println(stmt) + if err != nil { + panic(err) + } + + columns := FindColumn(stmt) + pretty.Println(columns) + } +} + +func TestFindAllCols(t *testing.T) { + sqlList := []string{ + "SELECT * FROM t1 LEFT JOIN (t2 CROSS JOIN t3 CROSS JOIN t4) ON (t2.a = t1.a AND t3.b = t1.b AND t4.c = t1.c)", + "select t from a LEFT JOIN b USING (c1, c2, c3)", + "select ID,name from (select address from customer_list where SID=1 order by phone limit 50,10) a join customer_list l on (a.address=l.address) join city c on (c.city=l.city) order by phone desc;", + "SELECT * FROM t1 LEFT JOIN (t2, t3, t4) ON (t2.a = t1.a AND t3.b = t1.b AND t4.c = t1.c)", + "SELECT * FROM t1 RIGHT JOIN (t2, t3, t4) ON (t2.a = t1.a AND t3.b = t1.b AND t4.c = t1.c)", + "SELECT left_tbl.* FROM left_tbl LEFT JOIN right_tbl ON left_tbl.id = right_tbl.id WHERE right_tbl.id IS NULL;", + "SELECT left_tbl.* FROM left_tbl RIGHT JOIN right_tbl ON left_tbl.id = right_tbl.id WHERE right_tbl.id IS NULL;", + "SELECT * FROM t1 where a in ('a','b')", + "SELECT * FROM t1 where a BETWEEN 'bar' AND 'foo'", + "SELECT * FROM t1 where a = sum(a,b)", + "SELECT distinct a FROM t1 where a = '2001-01-01 01:01:01'", + } + + for _, sql := range sqlList { + fmt.Println(sql) + stmt, err := sqlparser.Parse(sql) + //pretty.Println(stmt) + if err != nil { + panic(err) + } + + columns := FindAllCols(stmt, "order by") + pretty.Println(columns) + } +} + +func TestGetSubqueryDepth(t *testing.T) { + sqlList := []string{ + "SELECT * FROM t1 LEFT JOIN (t2 CROSS JOIN t3 CROSS JOIN t4) ON (t2.a = t1.a AND t3.b = t1.b AND t4.c = t1.c)", + "select t from a LEFT JOIN b USING (c1, c2, c3)", + "select ID,name from (select address from customer_list where SID=1 order by phone limit 50,10) a join customer_list l on (a.address=l.address) join city c on (c.city=l.city) order by phone desc;", + "SELECT * FROM t1 LEFT JOIN (t2, t3, t4) ON (t2.a = t1.a AND t3.b = t1.b AND t4.c = t1.c)", + "SELECT * FROM t1 RIGHT JOIN (t2, t3, t4) ON (t2.a = t1.a AND t3.b = t1.b AND t4.c = t1.c)", + "SELECT left_tbl.* FROM left_tbl LEFT JOIN right_tbl ON left_tbl.id = right_tbl.id WHERE right_tbl.id IS NULL;", + "SELECT left_tbl.* FROM left_tbl RIGHT JOIN right_tbl ON left_tbl.id = right_tbl.id WHERE right_tbl.id IS NULL;", + "SELECT * FROM t1 where a in ('a','b')", + "SELECT * FROM t1 where a BETWEEN 'bar' AND 'foo'", + "SELECT * FROM t1 where a = sum(a,b)", + "SELECT distinct a FROM t1 where a = '2001-01-01 01:01:01'", + } + + for _, sql := range sqlList { + fmt.Println(sql) + stmt, err := sqlparser.Parse(sql) + if err != nil { + t.Error("syntax check error.") + } + + dep := GetSubqueryDepth(stmt) + fmt.Println(dep) + } +} diff --git a/ast/node_array.go b/ast/node_array.go new file mode 100644 index 00000000..f2164f4a --- /dev/null +++ b/ast/node_array.go @@ -0,0 +1,123 @@ +/* + * Copyright 2018 Xiaomi, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ast + +import ( + "errors" + + "github.com/XiaoMi/soar/common" + "vitess.io/vitess/go/vt/sqlparser" +) + +// 该文件用于构造一个存储AST生成节点的链表 +// 以能够更好的对AST中的每个节点进行查询、跳转、重建等 + +// NodeItem 链表节点 +type NodeItem struct { + ID int // NodeItem在List中的编号,与顺序有关 + Prev *NodeItem // 前一个节点 + Self sqlparser.SQLNode // 自身指向的AST Node + Next *NodeItem // 后一个节点 + Array *NodeList // 指针指向所在的链表,用于快速跳转node +} + +// NodeList 链表结构体 +type NodeList struct { + Length int + Head *NodeItem + NodeMap map[int]*NodeItem +} + +// NewNodeList 从抽象语法树中构造一个链表 +func NewNodeList(statement sqlparser.Statement) *NodeList { + // 将AST构造成链表 + l := &NodeList{NodeMap: make(map[int]*NodeItem)} + err := sqlparser.Walk(func(node sqlparser.SQLNode) (kontinue bool, err error) { + l.Add(node) + return true, nil + }, statement) + common.LogIfWarn(err, "") + return l +} + +// Add 将会把一个sqlparser.SQLNode添加到节点中 +func (l *NodeList) Add(node sqlparser.SQLNode) *NodeItem { + if l.Length == 0 { + l.Head = &NodeItem{ + ID: 0, + Self: node, + Next: nil, + Prev: nil, + Array: l, + } + l.NodeMap[l.Length] = l.Head + } else { + if n, ok := l.NodeMap[l.Length-1]; ok { + n.Next = &NodeItem{ + ID: l.Length - 1, + Prev: n, + Self: node, + Next: nil, + Array: l, + } + l.NodeMap[l.Length] = n.Next + } + } + l.Length++ + + return l.NodeMap[l.Length-1] +} + +// Remove 从链表中移除一个节点 +func (l *NodeList) Remove(node *NodeItem) error { + var err error + defer func() { + err := recover() + if err != nil { + common.Log.Error("func (l *NodeList) Remove recovered: %v", err) + } + }() + + if node.Array != l { + return errors.New("node not belong to this array") + } + + if node.Prev == nil { + // 如果是头结点 + node.Next.Prev = nil + } else if node.Next == nil { + // 如果是尾节点 + node.Prev.Next = nil + } else { + // 删除节点,连接断开的链表 + node.Prev.Next = node.Next + node.Next.Prev = node.Prev + delete(l.NodeMap, node.ID) + } + + return err +} + +// First 返回链表头结点 +func (l *NodeList) First() *NodeItem { + return l.Head +} + +// Last 返回链表末尾节点 +func (l *NodeList) Last() *NodeItem { + return l.NodeMap[l.Length-1] +} diff --git a/ast/pretty.go b/ast/pretty.go new file mode 100644 index 00000000..8933513a --- /dev/null +++ b/ast/pretty.go @@ -0,0 +1,347 @@ +/* + * Copyright 2018 Xiaomi, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ast + +import ( + "container/list" + "regexp" + "strings" + + "github.com/XiaoMi/soar/common" + + "github.com/percona/go-mysql/query" +) + +// Pretty 格式化输出SQL +func Pretty(sql string, method string) (output string) { + // 超出 Config.MaxPrettySQLLength 长度的SQL会对其指纹进行pretty + if len(sql) > common.Config.MaxPrettySQLLength { + fingerprint := query.Fingerprint(sql) + // 超出 Config.MaxFpPrettySqlLength 长度的指纹不会进行pretty + if len(fingerprint) > common.Config.MaxPrettySQLLength { + return sql + } + sql = fingerprint + } + + switch method { + case "builtin", "markdown": + return format(sql) + default: + return sql + } +} + +// format the whitespace in a SQL string to make it easier to read. +// @param string $query The SQL string +// @return String The SQL string with HTML styles and formatting wrapped in a
 tag
+func format(query string) string {
+	// This variable will be populated with formatted html
+	result := ""
+	// Use an actual tab while formatting and then switch out with self::$tab at the end
+	tab := "  "
+	indentLevel := 0
+	var newline bool
+	var inlineParentheses bool
+	var increaseSpecialIndent bool
+	var increaseBlockIndent bool
+	var addedNewline bool
+	var inlineCount int
+	var inlineIndented bool
+	var clauseLimit bool
+	indentTypes := list.New()
+
+	// Tokenize String
+	originalTokens := Tokenize(query)
+
+	// Remove existing whitespace//
+	var tokens []Token
+	for i, token := range originalTokens {
+		if token.Type != TokenTypeWhitespace {
+			token.i = i
+			tokens = append(tokens, token)
+		}
+	}
+
+	for i, token := range tokens {
+		highlighted := token.Val
+
+		// If we are increasing the special indent level now
+		if increaseSpecialIndent {
+			indentLevel++
+			increaseSpecialIndent = false
+			indentTypes.PushFront("special")
+		}
+
+		// If we are increasing the block indent level now
+		if increaseBlockIndent {
+			indentLevel++
+			increaseBlockIndent = false
+			indentTypes.PushFront("block")
+		}
+
+		// If we need a new line before the token
+		if newline {
+			result += "\n" + strings.Repeat(tab, indentLevel)
+			newline = false
+			addedNewline = true
+		} else {
+			addedNewline = false
+		}
+
+		// Display comments directly where they appear in the source
+		if token.Type == TokenTypeComment || token.Type == TokenTypeBlockComment {
+			if token.Type == TokenTypeBlockComment {
+				indent := strings.Repeat(tab, indentLevel)
+				result += "\n" + indent
+				highlighted = strings.Replace(highlighted, "\n", "\n"+indent, -1)
+			}
+
+			result += highlighted
+			newline = true
+			continue
+		}
+
+		if inlineParentheses {
+			// End of inline parentheses
+			if token.Val == ")" {
+				result = strings.TrimRight(result, " ")
+
+				if inlineIndented {
+					indentTypes.Remove(indentTypes.Front())
+					if indentLevel > 0 {
+						indentLevel--
+					}
+					result += strings.Repeat(tab, indentLevel)
+				}
+
+				inlineParentheses = false
+
+				result += highlighted + " "
+				continue
+			}
+
+			if token.Val == "," {
+				if inlineCount >= 30 {
+					inlineCount = 0
+					newline = true
+				}
+			}
+
+			inlineCount += len(token.Val)
+		}
+
+		// Opening parentheses increase the block indent level and start a new line
+		if token.Val == "(" {
+			// First check if this should be an inline parentheses block
+			// Examples are "NOW()", "COUNT(*)", "int(10)", key(`somecolumn`), DECIMAL(7,2)
+			// Allow up to 3 non-whitespace tokens inside inline parentheses
+			length := 0
+			for j := 1; j <= 250; j++ {
+				// Reached end of string
+				if i+j >= len(tokens) {
+					break
+				}
+
+				next := tokens[i+j]
+
+				// Reached closing parentheses, able to inline it
+				if next.Val == ")" {
+					inlineParentheses = true
+					inlineCount = 0
+					inlineIndented = false
+					break
+				}
+
+				// Reached an invalid token for inline parentheses
+				if next.Val == ";" || next.Val == "(" {
+					break
+				}
+
+				// Reached an invalid token type for inline parentheses
+				if next.Type == TokenTypeReservedToplevel ||
+					next.Type == TokenTypeReservedNewline ||
+					next.Type == TokenTypeComment ||
+					next.Type == TokenTypeBlockComment {
+					break
+				}
+
+				length += len(next.Val)
+			}
+
+			if inlineParentheses && length > 30 {
+				increaseBlockIndent = true
+				inlineIndented = true
+				newline = true
+			}
+
+			// Take out the preceding space unless there was whitespace there in the original query
+			if token.i != 0 && (token.i-1) > len(originalTokens)-1 &&
+				originalTokens[token.i-1].Type != TokenTypeWhitespace {
+
+				result = strings.TrimRight(result, " ")
+			}
+
+			if inlineParentheses {
+				increaseBlockIndent = true
+				// Add a newline after the parentheses
+				newline = true
+			}
+
+		} else if token.Val == ")" {
+			// Closing parentheses decrease the block indent level
+			// Remove whitespace before the closing parentheses
+			result = strings.TrimRight(result, " ")
+
+			if indentLevel > 0 {
+				indentLevel--
+			}
+
+			// Reset indent level
+			for j := indentTypes.Front(); indentTypes.Len() > 0; indentTypes.Remove(j) {
+				if j.Value.(string) == "special" {
+					if indentLevel > 0 {
+						indentLevel--
+					}
+				} else {
+					break
+				}
+			}
+
+			if indentLevel < 0 {
+				// This is an error
+				indentLevel = 0
+			}
+
+			// Add a newline before the closing parentheses (if not already added)
+			if !addedNewline {
+				result += "\n" + strings.Repeat(tab, indentLevel)
+			}
+
+		} else if token.Type == TokenTypeReservedToplevel {
+			// Top level reserved words start a new line and increase the special indent level
+			increaseSpecialIndent = true
+
+			// If the last indent type was 'special', decrease the special indent for this round
+			if indentTypes.Len() > 0 && indentTypes.Front().Value.(string) == "special" {
+				if indentLevel > 0 {
+					indentLevel--
+				}
+				indentTypes.Remove(indentTypes.Front())
+			}
+
+			// Add a newline after the top level reserved word
+			newline = true
+			// Add a newline before the top level reserved word (if not already added)
+			if !addedNewline {
+				result += "\n" + strings.Repeat(tab, indentLevel)
+			} else {
+				// If we already added a newline, redo the indentation since it may be different now
+				result = strings.TrimSuffix(result, tab) + strings.Repeat(tab, indentLevel)
+			}
+
+			// If the token may have extra whitespace
+			if strings.Index(token.Val, " ") != 0 ||
+				strings.Index(token.Val, "\n") != 0 ||
+				strings.Index(token.Val, "\t") != 0 {
+
+				re, _ := regexp.Compile(`\s+`)
+				highlighted = re.ReplaceAllString(highlighted, " ")
+
+			}
+
+			//if SQL 'LIMIT' clause, start variable to reset newline
+			if token.Val == "LIMIT" && inlineParentheses {
+				clauseLimit = true
+			}
+
+		} else if clauseLimit && token.Val != "," &&
+			token.Type != TokenTypeNumber &&
+			token.Type != TokenTypeWhitespace {
+			// Checks if we are out of the limit clause
+
+			clauseLimit = false
+
+		} else if token.Val == "," && !inlineParentheses {
+			// Commas start a new line (unless within inline parentheses or SQL 'LIMIT' clause)
+			if clauseLimit {
+				newline = false
+				clauseLimit = false
+			} else {
+				// All other cases of commas
+				newline = true
+			}
+
+		} else if token.Type == TokenTypeReservedNewline {
+			// Newline reserved words start a new line
+			// Add a newline before the reserved word (if not already added)
+			if !addedNewline {
+				result += "\n" + strings.Repeat(tab, indentLevel)
+			}
+
+			// If the token may have extra whitespace
+			if strings.Index(token.Val, " ") != 0 ||
+				strings.Index(token.Val, "\n") != 0 ||
+				strings.Index(token.Val, "\t") != 0 {
+
+				re, _ := regexp.Compile(`\s+`)
+				highlighted = re.ReplaceAllString(highlighted, " ")
+			}
+
+		} else if token.Type == TokenTypeBoundary {
+			// Multiple boundary characters in a row should not have spaces between them (not including parentheses)
+			if i != 0 && i < len(tokens) &&
+				tokens[i-1].Type == TokenTypeBoundary {
+
+				if token.i != 0 && token.i < len(originalTokens) &&
+					originalTokens[token.i-1].Type != TokenTypeWhitespace {
+
+					result = strings.TrimRight(result, " ")
+				}
+			}
+		}
+
+		// If the token shouldn't have a space before it
+		if token.Val == "." || token.Val == "," || token.Val == ";" {
+			result = strings.TrimRight(result, " ")
+		}
+
+		result += highlighted + " "
+
+		// If the token shouldn't have a space after it
+		if token.Val == "(" || token.Val == "." {
+			result = strings.TrimRight(result, " ")
+		}
+
+		// If this is the "-" of a negative number, it shouldn't have a space after it
+		if token.Val == "-" && i+1 < len(tokens) && tokens[i+1].Type == TokenTypeNumber && i != 0 {
+			prev := tokens[i-1].Type
+			if prev != TokenTypeQuote &&
+				prev != TokenTypeBacktickQuote &&
+				prev != TokenTypeWord &&
+				prev != TokenTypeNumber {
+
+				result = strings.TrimRight(result, " ")
+			}
+		}
+	}
+
+	// Replace tab characters with the configuration tab character
+	result = strings.TrimRight(strings.Replace(result, "\t", tab, -1), " ")
+
+	return result
+}
diff --git a/ast/pretty_test.go b/ast/pretty_test.go
new file mode 100644
index 00000000..877c83ba
--- /dev/null
+++ b/ast/pretty_test.go
@@ -0,0 +1,198 @@
+/*
+ * Copyright 2018 Xiaomi, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package ast
+
+import (
+	"flag"
+	"fmt"
+	"testing"
+
+	"github.com/XiaoMi/soar/common"
+
+	"vitess.io/vitess/go/vt/sqlparser"
+)
+
+var update = flag.Bool("update", false, "update .golden files")
+
+var TestSqlsPretty = []string{
+	"select sourcetable, if(f.lastcontent = ?, f.lastupdate, f.lastcontent) as lastactivity, f.totalcount as activity, type.class as type, (f.nodeoptions & ?) as nounsubscribe from node as f inner join contenttype as type on type.contenttypeid = f.contenttypeid inner join subscribed as sd on sd.did = f.nodeid and sd.userid = ? union all select f.name as title, f.userid as keyval, ? as sourcetable, ifnull(f.lastpost, f.joindate) as lastactivity, f.posts as activity, ? as type, ? as nounsubscribe from user as f inner join userlist as ul on ul.relationid = f.userid and ul.userid = ? where ul.type = ? and ul.aq = ? order by title limit ?",
+	"administrator command: Init DB",
+	"CALL foo(1, 2, 3)",
+	"### Channels ###\n\u0009\u0009\u0009\u0009\u0009SELECT sourcetable, IF(f.lastcontent = 0, f.lastupdate, f.lastcontent) AS lastactivity,\n\u0009\u0009\u0009\u0009\u0009f.totalcount AS activity, type.class AS type,\n\u0009\u0009\u0009\u0009\u0009(f.nodeoptions \u0026 512) AS noUnsubscribe\n\u0009\u0009\u0009\u0009\u0009FROM node AS f\n\u0009\u0009\u0009\u0009\u0009INNER JOIN contenttype AS type ON type.contenttypeid = f.contenttypeid \n\n\u0009\u0009\u0009\u0009\u0009INNER JOIN subscribed AS sd ON sd.did = f.nodeid AND sd.userid = 15965\n UNION  ALL \n\n\u0009\u0009\u0009\u0009\u0009### Users ###\n\u0009\u0009\u0009\u0009\u0009SELECT f.name AS title, f.userid AS keyval, 'user' AS sourcetable, IFNULL(f.lastpost, f.joindate) AS lastactivity,\n\u0009\u0009\u0009\u0009\u0009f.posts as activity, 'Member' AS type,\n\u0009\u0009\u0009\u0009\u00090 AS noUnsubscribe\n\u0009\u0009\u0009\u0009\u0009FROM user AS f\n\u0009\u0009\u0009\u0009\u0009INNER JOIN userlist AS ul ON ul.relationid = f.userid AND ul.userid = 15965\n\u0009\u0009\u0009\u0009\u0009WHERE ul.type = 'f' AND ul.aq = 'yes'\n ORDER BY title ASC LIMIT 100",
+	"CREATE DATABASE org235_percona345 COLLATE 'utf8_general_ci'",
+	"insert into abtemp.coxed select foo.bar from foo",
+	"insert into foo(a, b, c) value(2, 4, 5)",
+	"insert into foo(a, b, c) values(2, 4, 5)",
+	"insert into foo(a, b, c) values(2, 4, 5) , (2,4,5)",
+	"insert into foo values (1, '(2)', 'This is a trick: ). More values.', 4)",
+	"insert into tb values (1)",
+	"INSERT INTO t (ts) VALUES ('()', '\\(', '\\)')",
+	"INSERT INTO t (ts) VALUES (NOW())",
+	"INSERT INTO t () VALUES ()",
+	"insert into t values (1), (2), (3)\n\n\ton duplicate key update query_count=1",
+	"insert into t values (1) on duplicate key update query_count=COALESCE(query_count, 0) + VALUES(query_count)",
+	"LOAD DATA INFILE '/tmp/foo.txt' INTO db.tbl",
+	"select 0e0, +6e-30, -6.00 from foo where a = 5.5 or b=0.5 or c=.5",
+	"select 0x0, x'123', 0b1010, b'10101' from foo",
+	"select 123_foo from 123_foo",
+	"select 123foo from 123foo",
+	`SELECT 	1 AS one FROM calls USE INDEX(index_name)`,
+	"SELECT /*!40001 SQL_NO_CACHE */ * FROM `film`",
+	"SELECT 'a' 'b' 'c' 'd' FROM kamil",
+	"SELECT BENCHMARK(100000000, pow(rand(), rand())), 1 FROM `-hj-7d6-shdj5-7jd-kf-g988h-`.`-aaahj-7d6-shdj5-7&^%$jd-kf-g988h-9+4-5*6ab-`",
+	"SELECT c FROM org235.t WHERE id=0xdeadbeaf",
+	"select c from t where i=1 order by c asc",
+	"SELECT c FROM t WHERE id=0xdeadbeaf",
+	"SELECT c FROM t WHERE id=1",
+	"select `col` from `table-1` where `id` = 5",
+	"SELECT `db`.*, (CASE WHEN (`date_start` <=  '2014-09-10 09:17:59' AND `date_end` >=  '2014-09-10 09:17:59') THEN 'open' WHEN (`date_start` >  '2014-09-10 09:17:59' AND `date_end` >  '2014-09-10 09:17:59') THEN 'tbd' ELSE 'none' END) AS `status` FROM `foo` AS `db` WHERE (a_b in ('1', '10101'))",
+	"select field from `-master-db-1`.`-table-1-` order by id, ?;",
+	"select   foo",
+	"select foo_1 from foo_2_3",
+	"select foo -- bar\n",
+	"select foo-- bar\n,foo",
+	"select '\\\\' from foo",
+	"select * from foo limit 5",
+	"select * from foo limit 5, 10",
+	"select * from foo limit 5 offset 10",
+	"SELECT * from foo where a = 5",
+	"select * from foo where a in (5) and b in (5, 8,9 ,9 , 10)",
+	"SELECT '' '' '' FROM kamil",
+	" select  * from\nfoo where a = 5",
+	"SELECT * FROM prices.rt_5min where id=1",
+	"SELECT * FROM table WHERE field = 'value' /*arbitrary/31*/ ",
+	"SELECT * FROM table WHERE field = 'value' /*arbitrary31*/ ",
+	"SELECT *    FROM t WHERE 1=1 AND id=1",
+	"select * from t where (base.nid IN  ('1412', '1410', '1411'))",
+	`select * from t where i=1      order            by
+             a,  b          ASC, d    DESC,
+
+                                    e asc`,
+	"select * from t where i=1 order by a, b ASC, d DESC, e asc",
+	"select 'hello'\n",
+	"select 'hello', '\nhello\n', \"hello\", '\\'' from foo",
+	"SELECT ID, name, parent, type FROM posts WHERE _name IN ('perf','caching') AND (type = 'page' OR type = 'attachment')",
+	"SELECT name, value FROM variable",
+	"select \n-- bar\n foo",
+	"select null, 5.001, 5001. from foo",
+	"select sleep(2) from test.n",
+	"SELECT t FROM field WHERE  (entity_type = 'node') AND (entity_id IN  ('609')) AND (language IN  ('und')) AND (deleted = '0') ORDER BY delta ASC",
+	"select  t.table_schema,t.table_name,engine  from information_schema.tables t  inner join information_schema.columns c  on t.table_schema=c.table_schema and t.table_name=c.table_name group by t.table_schema,t.table_name having  sum(if(column_key in ('PRI','UNI'),1,0))=0",
+	"/* -- S++ SU ABORTABLE -- spd_user: rspadim */SELECT SQL_SMALL_RESULT SQL_CACHE DISTINCT centro_atividade FROM est_dia WHERE unidade_id=1001 AND item_id=67 AND item_id_red=573",
+	`UPDATE groups_search SET  charter = '   -------3\'\' XXXXXXXXX.\n    \n    -----------------------------------------------------', show_in_list = 'Y' WHERE group_id='aaaaaaaa'`,
+	"use `foo`",
+	"select sourcetable, if(f.lastcontent = ?, f.lastupdate, f.lastcontent) as lastactivity, f.totalcount as activity, type.class as type, (f.nodeoptions & ?) as nounsubscribe from node as f inner join contenttype as type on type.contenttypeid = f.contenttypeid inner join subscribed as sd on sd.did = f.nodeid and sd.userid = ? union all select f.name as title, f.userid as keyval, ? as sourcetable, ifnull(f.lastpost, f.joindate) as lastactivity, f.posts as activity, ? as type, ? as nounsubscribe from user as f inner join userlist as ul on ul.relationid = f.userid and ul.userid = ? where ul.type = ? and ul.aq = ? order by title limit ?",
+	"CREATE INDEX part_of_name ON customer (name(10));",
+	"alter table `sakila`.`t1` add index `idx_col`(`col`)",
+	"alter table `sakila`.`t1` add UNIQUE index `idx_col`(`col`)",
+	"alter table `sakila`.`t1` add index `idx_ID`(`ID`)",
+
+	// ADD|DROP COLUMN
+	"ALTER TABLE t2 DROP COLUMN c, DROP COLUMN d;",
+	"ALTER TABLE T2 ADD COLUMN C int;",
+	"ALTER TABLE T2 ADD COLUMN D int FIRST;",
+	"ALTER TABLE T2 ADD COLUMN E int AFTER D;",
+
+	// RENMAE COLUMN
+	"ALTER TABLE t1 RENAME COLUMN a TO b",
+
+	// RENAME INDEX
+	"ALTER TABLE t1 RENAME INDEX idx_a TO idx_b",
+	"ALTER TABLE t1 RENAME KEY idx_a TO idx_b",
+
+	// RENAME TABLE
+	"ALTER TABLE db.old_table RENAME new_table;",
+	"ALTER TABLE old_table RENAME TO new_table;",
+	"ALTER TABLE old_table RENAME AS new_table;",
+
+	// MODIFY & CHANGE
+	"ALTER TABLE t1 MODIFY col1 BIGINT UNSIGNED DEFAULT 1 COMMENT 'my column';",
+	"ALTER TABLE t1 CHANGE b a INT NOT NULL;",
+}
+
+func TestPretty(t *testing.T) {
+	err := common.GoldenDiff(func() {
+		for _, sql := range append(TestSqlsPretty, common.TestSQLs...) {
+			fmt.Println(sql)
+			fmt.Println(Pretty(sql, "builtin"))
+		}
+	}, t.Name(), update)
+	if nil != err {
+		t.Fatal(err)
+	}
+}
+
+func TestIsKeyword(t *testing.T) {
+	tks := map[string]bool{
+		"AGAINST":        true,
+		"AUTO_INCREMENT": true,
+		"ADD":            true,
+		"BETWEEN":        true,
+		".":              false,
+		"actions":        false,
+		`"`:              false,
+		":":              false,
+	}
+	for tk, v := range tks {
+		if IsMysqlKeyword(tk) != v {
+			t.Error("isKeyword:", tk)
+		}
+	}
+}
+
+func TestRemoveComments(t *testing.T) {
+	for _, sql := range TestSqlsPretty {
+		stmt, _ := sqlparser.Parse(sql)
+		newSQL := sqlparser.String(stmt)
+		if newSQL != sql {
+			fmt.Print(newSQL)
+		}
+	}
+}
+
+func TestMysqlEscapeString(t *testing.T) {
+	var strs = []map[string]string{
+		{
+			"input":  "abc",
+			"output": "abc",
+		},
+		{
+			"input":  "'abc",
+			"output": "\\'abc",
+		},
+		{
+			"input": `
+abc`,
+			"output": `\
+abc`,
+		},
+		{
+			"input":  "\"abc",
+			"output": "\\\"abc",
+		},
+	}
+	for _, str := range strs {
+		output, err := MysqlEscapeString(str["input"])
+		if err != nil {
+			t.Error("TestMysqlEscapeString", err)
+		} else {
+			if output != str["output"] {
+				t.Error("TestMysqlEscapeString", output, str["output"])
+			}
+		}
+	}
+}
diff --git a/ast/rewrite.go b/ast/rewrite.go
new file mode 100644
index 00000000..a21bbcdd
--- /dev/null
+++ b/ast/rewrite.go
@@ -0,0 +1,1728 @@
+/*
+ * Copyright 2018 Xiaomi, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package ast
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"reflect"
+	"regexp"
+	"strings"
+
+	"github.com/XiaoMi/soar/common"
+
+	"github.com/kr/pretty"
+	"vitess.io/vitess/go/vt/sqlparser"
+)
+
+// Rule SQL重写规则
+type Rule struct {
+	Name        string                  `json:"Name"`
+	Description string                  `json:"Description"`
+	Original    string                  `json:"Original"` // 错误示范。为空或"暂不支持"不会出现在list-rewrite-rules中
+	Suggest     string                  `json:"Suggest"`  // 正确示范。
+	Func        func(*Rewrite) *Rewrite `json:"-"`        // 如果不定义Func需要多条SQL联动改写
+}
+
+// RewriteRules SQL重写规则,注意这个规则是有序的,先后顺序不能乱
+var RewriteRules = []Rule{
+	{
+		Name:        "dml2select",
+		Description: "将数据库更新请求转换为只读查询请求,便于执行EXPLAIN",
+		Original:    "DELETE FROM film WHERE length > 100",
+		Suggest:     "select * from film where length > 100",
+		Func:        (*Rewrite).RewriteDML2Select,
+	},
+	{
+		Name:        "star2columns",
+		Description: "为SELECT *补全表的列信息",
+		Original:    "SELECT * FROM film",
+		Suggest:     "select film.film_id, film.title from film",
+		Func:        (*Rewrite).RewriteStar2Columns,
+	},
+	{
+		Name:        "insertcolumns",
+		Description: "为INSERT补全表的列信息",
+		Original:    "insert into film values(1,2,3,4,5)",
+		Suggest:     "insert into film(film_id, title, description, release_year, language_id) values (1, 2, 3, 4, 5)",
+		Func:        (*Rewrite).RewriteInsertColumns,
+	},
+	{
+		Name:        "having",
+		Description: "将查询的HAVING子句改写为WHERE中的查询条件",
+		Original:    "SELECT state, COUNT(*) FROM Drivers GROUP BY state HAVING state IN ('GA', 'TX') ORDER BY state",
+		Suggest:     "select state, COUNT(*) from Drivers where state in ('GA', 'TX') group by state order by state asc",
+		Func:        (*Rewrite).RewriteHaving,
+	},
+	{
+		Name:        "orderbynull",
+		Description: "如果GROUP BY语句不指定ORDER BY条件会导致无谓的排序产生,如果不需要排序建议添加ORDER BY NULL",
+		Original:    "SELECT sum(col1) FROM tbl GROUP BY col",
+		Suggest:     "select sum(col1) from tbl group by col order by null",
+		Func:        (*Rewrite).RewriteAddOrderByNull,
+	},
+	{
+		Name:        "unionall",
+		Description: "可以接受重复的时间,使用UNION ALL替代UNION以提高查询效率",
+		Original:    "select country_id from city union select country_id from country",
+		Suggest:     "select country_id from city union all select country_id from country",
+		Func:        (*Rewrite).RewriteUnionAll,
+	},
+	{
+		Name:        "or2in",
+		Description: "将同一列不同条件的OR查询转写为IN查询",
+		Original:    "select country_id from city where col1 = 1 or (col2 = 1 or col2 = 2 ) or col1 = 3;",
+		Suggest:     "select country_id from city where (col2 in (1, 2)) or col1 in (1, 3);",
+		Func:        (*Rewrite).RewriteOr2In,
+	},
+	{
+		Name:        "innull",
+		Description: "如果IN条件中可能有NULL值而又想匹配NULL值时,建议添加OR col IS NULL",
+		Original:    "暂不支持",
+		Suggest:     "暂不支持",
+		Func:        (*Rewrite).RewriteInNull,
+	},
+	// 把所有跟or相关的重写完之后才进行or转union的重写
+	{
+		Name:        "or2union",
+		Description: "将不同列的OR查询转为UNION查询,建议结合unionall重写策略一起使用",
+		Original:    "暂不支持",
+		Suggest:     "暂不支持",
+		Func:        (*Rewrite).RewriteOr2Union,
+	},
+	{
+		Name:        "dmlorderby",
+		Description: "删除DML更新操作中无意义的ORDER BY",
+		Original:    "DELETE FROM tbl WHERE col1=1 ORDER BY col",
+		Suggest:     "delete from tbl where col1 = 1",
+		Func:        (*Rewrite).RewriteRemoveDMLOrderBy,
+	},
+	/*
+		{
+			Name:        "groupbyconst",
+			Description: "删除无意义的GROUP BY常量",
+			Original:    "SELECT sum(col1) FROM tbl GROUP BY 1;",
+			Suggest:     "select sum(col1) from tbl",
+			Func:        (*Rewrite).RewriteGroupByConst,
+		},
+	*/
+	{
+		Name:        "sub2join",
+		Description: "将子查询转换为JOIN查询",
+		Original:    "暂不支持",
+		Suggest:     "暂不支持",
+		Func:        (*Rewrite).RewriteSubQuery2Join,
+	},
+	{
+		Name:        "join2sub",
+		Description: "将JOIN查询转换为子查询",
+		Original:    "暂不支持",
+		Suggest:     "暂不支持",
+		Func:        (*Rewrite).RewriteJoin2SubQuery,
+	},
+	{
+		Name:        "distinctstar",
+		Description: "DISTINCT *对有主键的表没有意义,可以将DISTINCT删掉",
+		Original:    "SELECT DISTINCT * FROM film;",
+		Suggest:     "SELECT * FROM film",
+		Func:        (*Rewrite).RewriteDistinctStar,
+	},
+	{
+		Name:        "standard",
+		Description: "SQL标准化,如:关键字转换为小写",
+		Original:    "SELECT sum(col1) FROM tbl GROUP BY 1;",
+		Suggest:     "select sum(col1) from tbl group by 1",
+		Func:        (*Rewrite).RewriteStandard,
+	},
+	{
+		Name:        "mergealter",
+		Description: "合并同一张表的多条ALTER语句",
+		Original:    "ALTER TABLE t2 DROP COLUMN c;ALTER TABLE t2 DROP COLUMN d;",
+		Suggest:     "ALTER TABLE t2 DROP COLUMN c, DROP COLUMN d;",
+	},
+	{
+		Name:        "alwaystrue",
+		Description: "删除无用的恒真判断条件",
+		Original:    "SELECT count(col) FROM tbl where 'a'= 'a' or ('b' = 'b' and a = 'b');",
+		Suggest:     "select count(col) from tbl where (a = 'b');",
+		Func:        (*Rewrite).RewriteAlwaysTrue,
+	},
+	{
+		Name:        "countstar",
+		Description: "不建议使用COUNT(col)或COUNT(常量),建议改写为COUNT(*)",
+		Original:    "SELECT count(col) FROM tbl GROUP BY 1;",
+		Suggest:     "SELECT count(*) FROM tbl GROUP BY 1;",
+		Func:        (*Rewrite).RewriteCountStar,
+	},
+	{
+		Name:        "innodb",
+		Description: "建表时建议使用InnoDB引擎,非InnoDB引擎表自动转InnoDB",
+		Original:    "CREATE TABLE t1(id bigint(20) NOT NULL AUTO_INCREMENT);",
+		Suggest:     "create table t1 (\n\tid bigint(20) not null auto_increment\n) ENGINE=InnoDB;",
+		Func:        (*Rewrite).RewriteInnoDB,
+	},
+	{
+		Name:        "autoincrement",
+		Description: "将autoincrement初始化为1",
+		Original:    "CREATE TABLE t1(id bigint(20) NOT NULL AUTO_INCREMENT) ENGINE=InnoDB AUTO_INCREMENT=123802;",
+		Suggest:     "create table t1(id bigint(20) not null auto_increment) ENGINE=InnoDB auto_increment=1;",
+		Func:        (*Rewrite).RewriteAutoIncrement,
+	},
+	{
+		Name:        "intwidth",
+		Description: "整型数据类型修改默认显示宽度",
+		Original:    "create table t1 (id int(20) not null auto_increment) ENGINE=InnoDB;",
+		Suggest:     "create table t1 (id int(10) not null auto_increment) ENGINE=InnoDB;",
+		Func:        (*Rewrite).RewriteIntWidth,
+	},
+	{
+		Name:        "truncate",
+		Description: "不带WHERE条件的DELETE操作建议修改为TRUNCATE",
+		Original:    "DELETE FROM tbl",
+		Suggest:     "truncate table tbl",
+		Func:        (*Rewrite).RewriteTruncate,
+	},
+	{
+		Name:        "rmparenthesis",
+		Description: "去除没有意义的括号",
+		Original:    "select col from table where (col = 1);",
+		Suggest:     "select col from table where col = 1;",
+		Func:        (*Rewrite).RewriteRmParenthesis,
+	},
+	// delimiter要放在最后,不然补不上
+	{
+		Name:        "delimiter",
+		Description: "补全DELIMITER",
+		Original:    "use sakila",
+		Suggest:     "use sakila;",
+		Func:        (*Rewrite).RewriteDelimiter,
+	},
+	// TODO in to exists
+	// TODO exists to in
+}
+
+// ListRewriteRules 打印SQL重写规则
+func ListRewriteRules(rules []Rule) {
+	switch common.Config.ReportType {
+	case "json":
+		js, err := json.MarshalIndent(rules, "", "  ")
+		if err == nil {
+			fmt.Println(string(js))
+		}
+	default:
+
+		fmt.Print("# 重写规则\n\n[toc]\n\n")
+		for _, r := range rules {
+			if !common.Config.Verbose && (r.Original == "" || r.Original == "暂不支持") {
+				continue
+			}
+
+			fmt.Print("## ", common.MarkdownEscape(r.Name),
+				"\n* **Description**:", r.Description+"\n",
+				"\n* **Original**:\n\n```sql\n", r.Original, "\n```\n",
+				"\n* **Suggest**:\n\n```sql\n", r.Suggest, "\n```\n")
+
+		}
+	}
+}
+
+// Rewrite 用于重写SQL
+type Rewrite struct {
+	SQL     string
+	NewSQL  string
+	Stmt    sqlparser.Statement
+	Columns common.TableColumns
+}
+
+// NewRewrite 返回一个*Rewrite对象,如果SQL无法被正常解析,将错误输出到日志中,返回一个nil
+func NewRewrite(sql string) *Rewrite {
+	stmt, err := sqlparser.Parse(sql)
+	if err != nil {
+		common.Log.Error(err.Error(), sql)
+		return nil
+	}
+
+	return &Rewrite{
+		SQL:  sql,
+		Stmt: stmt,
+	}
+}
+
+// Rewrite 入口函数
+func (rw *Rewrite) Rewrite() *Rewrite {
+	defer func() {
+		if err := recover(); err != nil {
+			common.Log.Error("Query rewrite Error: %s, maybe hit a bug.\nQuery: %s \nAST: %s",
+				err, rw.SQL, pretty.Sprint(rw.Stmt))
+			return
+		}
+	}()
+
+	for _, rule := range RewriteRules {
+		if RewriteRuleMatch(rule.Name) && rule.Func != nil {
+			rule.Func(rw)
+			common.Log.Debug("Rewrite Rule:%s Output NewSQL: %s", rule.Name, rw.NewSQL)
+		}
+	}
+	if rw.NewSQL == "" {
+		rw.NewSQL = rw.SQL
+	}
+	rw.Stmt, _ = sqlparser.Parse(rw.NewSQL)
+
+	// TODO: 重新前后返回结果一致性对比
+
+	// TODO: 前后SQL性能对比
+	return rw
+}
+
+// RewriteDelimiter delimiter: 补分号,可以指定不同的DELIMITER
+func (rw *Rewrite) RewriteDelimiter() *Rewrite {
+	if rw.NewSQL != "" {
+		rw.NewSQL = strings.TrimSuffix(rw.NewSQL, common.Config.Delimiter) + common.Config.Delimiter
+	} else {
+		rw.NewSQL = strings.TrimSuffix(rw.SQL, common.Config.Delimiter) + common.Config.Delimiter
+	}
+	return rw
+}
+
+// RewriteStandard standard: 使用vitess提供的String功能将抽象语法树转写回SQL,注意:这可能转写失败。
+func (rw *Rewrite) RewriteStandard() *Rewrite {
+	if _, err := sqlparser.Parse(rw.SQL); err == nil {
+		rw.NewSQL = sqlparser.String(rw.Stmt)
+	}
+	return rw
+}
+
+// RewriteAlwaysTrue alwaystrue: 删除恒真条件
+func (rw *Rewrite) RewriteAlwaysTrue() (reWriter *Rewrite) {
+	array := NewNodeList(rw.Stmt)
+	tNode := array.Head
+	for {
+		omitAwaysTrue(tNode)
+		tNode = tNode.Next
+		if tNode == nil {
+			break
+		}
+	}
+
+	rw.NewSQL = sqlparser.String(rw.Stmt)
+	return rw
+}
+
+// isAlwaysTrue 用于判断ComparisonExpr是否是恒真
+func isAlwaysTrue(expr *sqlparser.ComparisonExpr) bool {
+	if expr == nil {
+		return true
+	}
+
+	var result bool
+	switch expr.Operator {
+	case "<>":
+		expr.Operator = "!="
+	case "<=>":
+		expr.Operator = "="
+	case ">=", "<=", "!=", "=":
+	default:
+		return false
+	}
+
+	var left []byte
+	var right []byte
+
+	// left
+	switch l := expr.Left.(type) {
+	case *sqlparser.SQLVal:
+		left = l.Val
+	default:
+		return false
+	}
+
+	// right
+	switch r := expr.Right.(type) {
+	case *sqlparser.SQLVal:
+		right = r.Val
+	default:
+		return false
+	}
+
+	switch expr.Operator {
+	case "=":
+		result = bytes.Equal(left, right)
+	case "!=":
+		result = !bytes.Equal(left, right)
+	case ">":
+		result = bytes.Compare(left, right) > 0
+	case ">=":
+		result = bytes.Compare(left, right) >= 0
+	case "<":
+		result = bytes.Compare(left, right) < 0
+	case "<=":
+		result = bytes.Compare(left, right) <= 0
+	default:
+		result = false
+	}
+
+	return result
+}
+
+// omitAwaysTrue 移除AST中的恒真条件
+func omitAwaysTrue(node *NodeItem) {
+	if node == nil {
+		return
+	}
+
+	switch self := node.Self.(type) {
+	case *sqlparser.Where:
+		if self != nil {
+			switch cond := self.Expr.(type) {
+			case *sqlparser.ComparisonExpr:
+				if isAlwaysTrue(cond) {
+					self.Expr = nil
+				}
+			case *sqlparser.ParenExpr:
+				if cond.Expr == nil {
+					self.Expr = nil
+				}
+			}
+		}
+	case *sqlparser.ParenExpr:
+		if self != nil {
+			switch cond := self.Expr.(type) {
+			case *sqlparser.ComparisonExpr:
+				if isAlwaysTrue(cond) {
+					self.Expr = nil
+				}
+			}
+		}
+	case *sqlparser.AndExpr:
+		if self != nil {
+			var tmp sqlparser.Expr
+			isRightTrue := false
+			isLeftTrue := false
+			tmp = nil
+
+			// 查看左树的情况
+			switch l := self.Left.(type) {
+			case *sqlparser.ComparisonExpr:
+				if isAlwaysTrue(l) {
+					self.Left = nil
+					isLeftTrue = true
+					tmp = self.Right
+				}
+			case *sqlparser.ParenExpr:
+				if l.Expr == nil {
+					self.Left = nil
+					isLeftTrue = true
+					tmp = self.Right
+				}
+			default:
+				if l == nil {
+					isLeftTrue = true
+					tmp = self.Right
+				}
+			}
+
+			// 查看右树的情况
+			switch r := self.Right.(type) {
+			case *sqlparser.ComparisonExpr:
+				if isAlwaysTrue(r) {
+					self.Right = nil
+					isRightTrue = true
+					tmp = self.Left
+				}
+			case *sqlparser.ParenExpr:
+				if r.Expr == nil {
+					self.Right = nil
+					isRightTrue = true
+					tmp = self.Left
+				}
+			default:
+				if r == nil {
+					isRightTrue = true
+					tmp = self.Left
+				}
+			}
+
+			if isRightTrue && isLeftTrue {
+				tmp = nil
+			} else if !isLeftTrue && !isRightTrue {
+				return
+			}
+
+			// 根据类型开始替换节点
+			switch l := node.Prev.Self.(type) {
+			case *sqlparser.Where:
+				l.Expr = tmp
+			case *sqlparser.ParenExpr:
+				l.Expr = tmp
+			case *sqlparser.AndExpr:
+				if l.Left == self {
+					l.Left = tmp
+				} else if l.Right == self {
+					l.Right = tmp
+				}
+			case *sqlparser.OrExpr:
+				if l.Left == self {
+					l.Left = tmp
+				} else if l.Right == self {
+					l.Right = tmp
+				}
+			default:
+				// 未匹配到对应数据类型则从链表中移除该节点
+				err := node.Array.Remove(node.Prev)
+				common.LogIfError(err, "")
+			}
+
+		}
+
+	case *sqlparser.OrExpr:
+		// 与AndExpr相同
+		if self != nil {
+			var tmp sqlparser.Expr
+			isRightTrue := false
+			isLeftTrue := false
+			tmp = nil
+
+			switch l := self.Left.(type) {
+			case *sqlparser.ComparisonExpr:
+				if isAlwaysTrue(l) {
+					self.Left = nil
+					isLeftTrue = true
+					tmp = self.Right
+				}
+			case *sqlparser.ParenExpr:
+				if l.Expr == nil {
+					self.Left = nil
+					isLeftTrue = true
+					tmp = self.Right
+				}
+			default:
+				if l == nil {
+					isLeftTrue = true
+					tmp = self.Right
+				}
+			}
+
+			switch r := self.Right.(type) {
+			case *sqlparser.ComparisonExpr:
+				if isAlwaysTrue(r) {
+					self.Right = nil
+					isRightTrue = true
+					tmp = self.Left
+				}
+			case *sqlparser.ParenExpr:
+				if r.Expr == nil {
+					self.Right = nil
+					isRightTrue = true
+					tmp = self.Left
+				}
+			default:
+				if r == nil {
+					isRightTrue = true
+					tmp = self.Left
+				}
+			}
+
+			if isRightTrue && isLeftTrue {
+				tmp = nil
+			} else if !isLeftTrue && !isRightTrue {
+				return
+			}
+
+			switch l := node.Prev.Self.(type) {
+			case *sqlparser.Where:
+				l.Expr = tmp
+			case *sqlparser.ParenExpr:
+				l.Expr = tmp
+			case *sqlparser.AndExpr:
+				if l.Left == self {
+					l.Left = tmp
+				} else if l.Right == self {
+					l.Right = tmp
+				}
+			case *sqlparser.OrExpr:
+				if l.Left == self {
+					l.Left = tmp
+				} else if l.Right == self {
+					l.Right = tmp
+				}
+			default:
+				err := node.Array.Remove(node.Prev)
+				common.LogIfError(err, "")
+			}
+		}
+	}
+
+	omitAwaysTrue(node.Prev)
+}
+
+// RewriteCountStar countstar: 将COUNT(col)改写为COUNT(*)
+// COUNT(DISTINCT col)不能替换为COUNT(*)
+func (rw *Rewrite) RewriteCountStar() *Rewrite {
+	err := sqlparser.Walk(func(node sqlparser.SQLNode) (kontinue bool, err error) {
+		switch f := node.(type) {
+		case *sqlparser.FuncExpr:
+			if strings.ToLower(f.Name.String()) == "count" && len(f.Exprs) > 0 {
+				switch colExpr := f.Exprs[0].(type) {
+				case *sqlparser.AliasedExpr:
+					switch col := colExpr.Expr.(type) {
+					case *sqlparser.ColName:
+						f.Exprs[0] = &sqlparser.StarExpr{TableName: col.Qualifier}
+					}
+				}
+			}
+		}
+		return true, nil
+	}, rw.Stmt)
+	common.LogIfError(err, "")
+	rw.NewSQL = sqlparser.String(rw.Stmt)
+	return rw
+}
+
+// RewriteInnoDB innodb: 为未指定Engine的表默认添加InnoDB引擎,将其他存储引擎转为InnoDB
+func (rw *Rewrite) RewriteInnoDB() *Rewrite {
+	switch create := rw.Stmt.(type) {
+	case *sqlparser.DDL:
+		if create.Action != "create" {
+			return rw
+		}
+
+		if strings.Contains(strings.ToLower(create.TableSpec.Options), "engine=") {
+			reg := regexp.MustCompile(`(?i)engine=[a-z]+`)
+			create.TableSpec.Options = reg.ReplaceAllString(create.TableSpec.Options, "ENGINE=InnoDB ")
+		} else {
+			create.TableSpec.Options = " ENGINE=InnoDB " + create.TableSpec.Options
+		}
+
+	}
+
+	rw.NewSQL = sqlparser.String(rw.Stmt)
+	return rw
+}
+
+// RewriteAutoIncrement autoincrement: 将auto_increment设置为1
+func (rw *Rewrite) RewriteAutoIncrement() *Rewrite {
+	switch create := rw.Stmt.(type) {
+	case *sqlparser.DDL:
+		if create.Action != "create" || create.TableSpec == nil {
+			return rw
+		}
+		if strings.Contains(strings.ToLower(create.TableSpec.Options), "auto_increment=") {
+			reg := regexp.MustCompile(`(?i)auto_increment=[0-9]+`)
+			create.TableSpec.Options = reg.ReplaceAllString(create.TableSpec.Options, "auto_increment=1 ")
+		}
+	}
+
+	rw.NewSQL = sqlparser.String(rw.Stmt)
+	return rw
+}
+
+// RewriteIntWidth intwidth: int类型转为int(10),bigint类型转为bigint(20)
+func (rw *Rewrite) RewriteIntWidth() *Rewrite {
+	switch create := rw.Stmt.(type) {
+	case *sqlparser.DDL:
+		if create.Action != "create" || create.TableSpec == nil {
+			return rw
+		}
+		for _, col := range create.TableSpec.Columns {
+			switch col.Type.Type {
+			case "int", "integer":
+				if col.Type.Length != nil &&
+					(string(col.Type.Length.Val) != "10" && string(col.Type.Length.Val) != "11") {
+					col.Type.Length = sqlparser.NewIntVal([]byte("10"))
+				}
+			case "bigint":
+				if col.Type.Length != nil && string(col.Type.Length.Val) != "20" || col.Type.Length == nil {
+					col.Type.Length = sqlparser.NewIntVal([]byte("20"))
+				}
+			default:
+			}
+		}
+	}
+
+	rw.NewSQL = sqlparser.String(rw.Stmt)
+	return rw
+}
+
+// RewriteStar2Columns star2columns: 对应COL.001,SELECT补全*指代的列名
+func (rw *Rewrite) RewriteStar2Columns() *Rewrite {
+	// 如果未配置mysql环境或从环境中获取失败,*不进行替换
+	if common.Config.TestDSN.Disable || len(rw.Columns) == 0 {
+		common.Log.Debug("(rw *Rewrite) RewriteStar2Columns(): Rewrite failed. TestDSN.Disable: %v, len(rw.Columns):%d",
+			common.Config.TestDSN.Disable, len(rw.Columns))
+		return rw
+	}
+
+	err := sqlparser.Walk(func(node sqlparser.SQLNode) (kontinue bool, err error) {
+		switch n := node.(type) {
+		case *sqlparser.Select:
+
+			// select * 可能出现的情况:
+			// 1. select * from tb;
+			// 2. select * from tb1,tb2;
+			// 3. select tb1.* from tb1;
+			// 4. select tb1.*,tb2.col from tb1,tb2;
+			// 5. select db.tb1.* from tb1;
+			// 6. select db.tb1.*,db.tb2.col from db.tb1,db.tb2;
+
+			newSelectExprs := make(sqlparser.SelectExprs, 0)
+			for _, expr := range n.SelectExprs {
+				switch e := expr.(type) {
+				case *sqlparser.StarExpr:
+					// 一般情况下最外层循环不会超过两层
+					for _, tables := range rw.Columns {
+						for _, cols := range tables {
+							for _, col := range cols {
+								newExpr := &sqlparser.AliasedExpr{
+									Expr: &sqlparser.ColName{
+										Metadata: nil,
+										Name:     sqlparser.NewColIdent(col.Name),
+										Qualifier: sqlparser.TableName{
+											Name: sqlparser.NewTableIdent(col.Table),
+											// 因为不建议跨DB的查询,所以这里的db前缀将不进行补齐
+											Qualifier: sqlparser.TableIdent{},
+										},
+									},
+									As: sqlparser.ColIdent{},
+								}
+
+								if e.TableName.Name.IsEmpty() {
+									// 情况1,2
+									newSelectExprs = append(newSelectExprs, newExpr)
+								} else {
+									// 其他情况下只有在匹配表名的时候才会进行替换
+									if e.TableName.Name.String() == col.Table {
+										newSelectExprs = append(newSelectExprs, newExpr)
+									}
+								}
+							}
+						}
+					}
+				default:
+					newSelectExprs = append(newSelectExprs, e)
+				}
+			}
+
+			n.SelectExprs = newSelectExprs
+		}
+		return true, nil
+	}, rw.Stmt)
+	common.LogIfError(err, "")
+	rw.NewSQL = sqlparser.String(rw.Stmt)
+	return rw
+}
+
+// RewriteInsertColumns insertcolumns: 对应COL.002,INSERT补全列名
+func (rw *Rewrite) RewriteInsertColumns() *Rewrite {
+
+	switch insert := rw.Stmt.(type) {
+	case *sqlparser.Insert:
+		switch insert.Action {
+		case "insert", "replace":
+			if insert.Columns != nil {
+				return rw
+			}
+
+			newColumns := make(sqlparser.Columns, 0)
+			db := insert.Table.Qualifier.String()
+			table := insert.Table.Name.String()
+			// 支持INSERT/REPLACE INTO VALUES形式,支持INSERT/REPLACE INTO SELECT
+			colCount := 0
+			switch v := insert.Rows.(type) {
+			case sqlparser.Values:
+				if len(v) > 0 {
+					colCount = len(v[0])
+				}
+
+			case *sqlparser.Select:
+				if l := len(v.SelectExprs); l > 0 {
+					colCount = l
+				}
+			}
+
+			// 开始对ast进行替换,补全前N列
+			counter := 0
+			for dbName, tb := range rw.Columns {
+				for tbName, cols := range tb {
+					for _, col := range cols {
+						// 只有全部列补全完成的时候才会替换ast
+						if counter == colCount {
+							insert.Columns = newColumns
+							rw.NewSQL = sqlparser.String(rw.Stmt)
+							return rw
+						}
+
+						if db != "" {
+							// 指定了DB的时候,只能怼指定DB的列
+							if db == dbName && table == tbName {
+								newColumns = append(newColumns, sqlparser.NewColIdent(col.Name))
+								counter++
+							}
+						} else {
+							// 没有指定DB的时候,将column中的列按顺序往里怼
+							if table == tbName {
+								newColumns = append(newColumns, sqlparser.NewColIdent(col.Name))
+								counter++
+							}
+						}
+					}
+				}
+			}
+		}
+	}
+	return rw
+}
+
+// RewriteHaving having: 对应CLA.013,使用WHERE过滤条件替代HAVING
+func (rw *Rewrite) RewriteHaving() *Rewrite {
+	err := sqlparser.Walk(func(node sqlparser.SQLNode) (kontinue bool, err error) {
+		switch n := node.(type) {
+		case *sqlparser.Select:
+			if n.Having != nil {
+				if n.Where == nil {
+					// WHERE条件为空直接用HAVING替代WHERE即可
+					n.Where = n.Having
+				} else {
+					// WHERE条件不为空,需要对已有的条件进行括号保护,然后再AND+HAVING
+					n.Where = &sqlparser.Where{
+						Expr: &sqlparser.AndExpr{
+							Left: &sqlparser.ParenExpr{
+								Expr: n.Where.Expr,
+							},
+							Right: n.Having.Expr,
+						},
+					}
+				}
+				// 别忘了重置HAVING和Where.Type
+				n.Where.Type = "where"
+				n.Having = nil
+			}
+		}
+		return true, nil
+	}, rw.Stmt)
+	common.LogIfError(err, "")
+	rw.NewSQL = sqlparser.String(rw.Stmt)
+	return rw
+}
+
+// RewriteAddOrderByNull orderbynull: 对应CLA.008,GROUP BY无排序要求时添加ORDER BY NULL
+func (rw *Rewrite) RewriteAddOrderByNull() *Rewrite {
+	err := sqlparser.Walk(func(node sqlparser.SQLNode) (kontinue bool, err error) {
+		switch n := node.(type) {
+		case *sqlparser.Select:
+			if n.GroupBy != nil && n.OrderBy == nil {
+				n.OrderBy = sqlparser.OrderBy{
+					&sqlparser.Order{
+						Expr:      &sqlparser.NullVal{},
+						Direction: "asc",
+					},
+				}
+			}
+		}
+		return true, nil
+	}, rw.Stmt)
+	common.LogIfError(err, "")
+	rw.NewSQL = sqlparser.String(rw.Stmt)
+	return rw
+}
+
+// RewriteOr2Union or2union: 将OR查询转写为UNION ALL TODO: 暂无对应HeuristicRules
+// https://sqlperformance.com/2014/09/sql-plan/rewriting-queries-improve-performance
+func (rw *Rewrite) RewriteOr2Union() *Rewrite {
+	return rw
+}
+
+// RewriteUnionAll unionall: 不介意重复数据的情况下使用union all替换union
+func (rw *Rewrite) RewriteUnionAll() *Rewrite {
+	err := sqlparser.Walk(func(node sqlparser.SQLNode) (kontinue bool, err error) {
+		switch n := node.(type) {
+		case *sqlparser.Union:
+			n.Type = "union all"
+		}
+		return true, nil
+	}, rw.Stmt)
+	common.LogIfError(err, "")
+	rw.NewSQL = sqlparser.String(rw.Stmt)
+	return rw
+}
+
+// RewriteOr2In or2in: 同一列的OR过滤条件使用IN()替代,如果值有相等的会进行合并
+func (rw *Rewrite) RewriteOr2In() *Rewrite {
+	// 通过AST生成node的双向链表,链表顺序为书写顺序
+	nodeList := NewNodeList(rw.Stmt)
+	tNode := nodeList.First()
+
+	for {
+		tNode.or2in()
+		if tNode.Next == nil {
+			break
+		}
+		tNode = tNode.Next
+	}
+
+	rw.NewSQL = sqlparser.String(rw.Stmt)
+	return rw
+}
+
+// or2in 用于将or转换成in
+func (node *NodeItem) or2in() {
+	if node == nil || node.Self == nil {
+		return
+	}
+
+	switch selfNode := node.Self.(type) {
+	case *sqlparser.OrExpr:
+		newExpr := mergeExprs(selfNode.Left, selfNode.Right)
+		if newExpr != nil {
+			// or 自身两个节点可以合并的情况下,将父节点中的expr替换成新的
+			switch pre := node.Prev.Self.(type) {
+			case *sqlparser.OrExpr:
+				if pre.Left == node.Self {
+					node.Self = newExpr
+					pre.Left = newExpr
+				} else if pre.Right == node.Self {
+					node.Self = newExpr
+					pre.Right = newExpr
+				}
+			case *sqlparser.AndExpr:
+				if pre.Left == node.Self {
+					node.Self = newExpr
+					pre.Left = newExpr
+				} else if pre.Right == node.Self {
+					node.Self = newExpr
+					pre.Right = newExpr
+				}
+			case *sqlparser.Where:
+				node.Self = newExpr
+				pre.Expr = newExpr
+			case *sqlparser.ParenExpr:
+				// 如果SQL书写中带了括号,暂不会进行跨括号的合并,TODO:无意义括号打平,加个rewrite rule
+				node.Self = newExpr
+				pre.Expr = newExpr
+			}
+		} else {
+			// or 自身两个节点如不可以合并,则检测是否可以与父节点合并
+			// 与父节点的合并不能跨越and、括号等,可能会改变语义
+			// 检查自身左右节点是否能与上层节点中合并,or只能与or合并
+			switch pre := node.Prev.Self.(type) {
+			case *sqlparser.OrExpr:
+				// AST中如果出现复合条件,则一定在左树,所以只需要判断左边就可以
+				if pre.Left == selfNode {
+					switch n := pre.Right.(type) {
+					case *sqlparser.ComparisonExpr:
+						newLeftExpr := mergeExprs(selfNode.Left, n)
+						newRightExpr := mergeExprs(selfNode.Right, n)
+
+						// newLeftExpr 与 newRightExpr 一定有一个是nil,
+						// 否则说明该orExpr下的两个节点可合并,可以通过最后的向前递归合并pre节点中的expr
+						if newLeftExpr == nil || newRightExpr == nil {
+							if newLeftExpr != nil {
+								pre.Right = newLeftExpr
+								pre.Left = selfNode.Right
+								err := node.Array.Remove(node)
+								common.LogIfError(err, "")
+							}
+
+							if newRightExpr != nil {
+								pre.Right = newRightExpr
+								pre.Left = selfNode.Left
+								err := node.Array.Remove(node)
+								common.LogIfError(err, "")
+							}
+						}
+					}
+				}
+			}
+		}
+	}
+
+	// 逆向合并由更改AST后产生的新的可合并节点
+	node.Prev.or2in()
+}
+
+// mergeExprs 将两个属于同一个列的ComparisonExpr合并成一个,如果不能合并则返回nil
+func mergeExprs(left, right sqlparser.Expr) *sqlparser.ComparisonExpr {
+	// 用于对比两个列是否相同
+	colInLeft := ""
+	colInRight := ""
+	lOperator := ""
+	rOperator := ""
+
+	// 用于存放expr左右子树中的值
+	var values []sqlparser.SQLNode
+
+	// SQL中使用到的列
+	var colName *sqlparser.ColName
+
+	// 左子树
+	switch l := left.(type) {
+	case *sqlparser.ComparisonExpr:
+		// 获取列名
+		colName, colInLeft = getColumnName(l.Left)
+		// 获取值
+		if colInLeft != "" {
+			switch v := l.Right.(type) {
+			case *sqlparser.SQLVal, sqlparser.ValTuple, *sqlparser.BoolVal, *sqlparser.NullVal:
+				values = append(values, v)
+			}
+		}
+		// 获取operator
+		lOperator = l.Operator
+	default:
+		return nil
+	}
+
+	// 右子树
+	switch r := right.(type) {
+	case *sqlparser.ComparisonExpr:
+		// 获取列名
+		if colName.Name.String() != "" {
+			common.Log.Warn("colName shouldn't has value, but now it's %s", colName.Name.String())
+		}
+		colName, colInRight = getColumnName(r.Left)
+		// 获取值
+		if colInRight != "" {
+			switch v := r.Right.(type) {
+			case *sqlparser.SQLVal, sqlparser.ValTuple, *sqlparser.BoolVal, *sqlparser.NullVal:
+				values = append(values, v)
+			}
+		}
+		// 获取operator
+		rOperator = r.Operator
+	default:
+		return nil
+	}
+
+	// operator替换,用于在之后判断是否可以合并
+	switch lOperator {
+	case "in", "=":
+		lOperator = "="
+	default:
+		return nil
+	}
+
+	switch rOperator {
+	case "in", "=":
+		rOperator = "="
+	default:
+		return nil
+	}
+
+	// 不匹配则返回
+	if colInLeft == "" || colInLeft != colInRight ||
+		lOperator == "" || lOperator != rOperator {
+		return nil
+	}
+
+	// 合并左右子树的值
+	newValTuple := make(sqlparser.ValTuple, 0)
+	for _, v := range values {
+		switch v := v.(type) {
+		case *sqlparser.SQLVal:
+			newValTuple = append(newValTuple, v)
+		case *sqlparser.BoolVal:
+			newValTuple = append(newValTuple, v)
+		case *sqlparser.NullVal:
+			newValTuple = append(newValTuple, v)
+		case sqlparser.ValTuple:
+			newValTuple = append(newValTuple, v...)
+		}
+	}
+
+	// 去expr中除重复的value,
+	newValTuple = removeDup(newValTuple...)
+	newExpr := &sqlparser.ComparisonExpr{
+		Operator: "in",
+		Left:     colName,
+		Right:    newValTuple,
+	}
+	// 如果只有一个值则是一个等式,没有必要转写成in
+	if len(newValTuple) == 1 {
+		newExpr = &sqlparser.ComparisonExpr{
+			Operator: lOperator,
+			Left:     colName,
+			Right:    newValTuple[0],
+		}
+	}
+
+	return newExpr
+}
+
+// removeDup 清除sqlparser.ValTuple中重复的值
+func removeDup(vt ...sqlparser.Expr) sqlparser.ValTuple {
+	uni := make(sqlparser.ValTuple, 0)
+	m := make(map[string]sqlparser.SQLNode)
+
+	for _, value := range vt {
+		switch v := value.(type) {
+		case *sqlparser.SQLVal:
+			// Type:Val, 冒号用于分隔Type和Val,防止两种不同类型拼接后出现同一个值
+			if _, ok := m[string(v.Type)+":"+sqlparser.String(v)]; !ok {
+				uni = append(uni, v)
+				m[string(v.Type)+":"+sqlparser.String(v)] = v
+			}
+		case *sqlparser.BoolVal:
+			if _, ok := m[sqlparser.String(v)]; !ok {
+				uni = append(uni, v)
+				m[sqlparser.String(v)] = v
+			}
+		case *sqlparser.NullVal:
+			if _, ok := m[sqlparser.String(v)]; !ok {
+				uni = append(uni, v)
+				m[sqlparser.String(v)] = v
+			}
+		case sqlparser.ValTuple:
+			for _, val := range removeDup(v...) {
+				switch v := val.(type) {
+				case *sqlparser.SQLVal:
+					if _, ok := m[string(v.Type)+":"+sqlparser.String(v)]; !ok {
+						uni = append(uni, v)
+						m[string(v.Type)+":"+sqlparser.String(v)] = v
+					}
+				case *sqlparser.BoolVal:
+					if _, ok := m[sqlparser.String(v)]; !ok {
+						uni = append(uni, v)
+						m[sqlparser.String(v)] = v
+					}
+				case *sqlparser.NullVal:
+					if _, ok := m[sqlparser.String(v)]; !ok {
+						uni = append(uni, v)
+						m[sqlparser.String(v)] = v
+					}
+				}
+			}
+		}
+	}
+
+	return uni
+}
+
+// RewriteInNull innull: TODO: 对应ARG.004
+func (rw *Rewrite) RewriteInNull() *Rewrite {
+	return rw
+}
+
+// RewriteRmParenthesis rmparenthesis: 去除无意义的括号
+func (rw *Rewrite) RewriteRmParenthesis() *Rewrite {
+	rw.rmParenthesis()
+	rw.NewSQL = sqlparser.String(rw.Stmt)
+	return rw
+}
+
+// rmParenthesis 用于语出无用的括号
+func (rw *Rewrite) rmParenthesis() {
+	continueFlag := false
+	err := sqlparser.Walk(func(node sqlparser.SQLNode) (kontinue bool, err error) {
+		switch node := node.(type) {
+		case *sqlparser.Where:
+			if node == nil {
+				return true, nil
+			}
+			switch paren := node.Expr.(type) {
+			case *sqlparser.ParenExpr:
+				switch paren.Expr.(type) {
+				case *sqlparser.ComparisonExpr:
+					node.Expr = paren.Expr
+					continueFlag = true
+				}
+			}
+
+		case *sqlparser.ParenExpr:
+			switch paren := node.Expr.(type) {
+			case *sqlparser.ParenExpr:
+				switch paren.Expr.(type) {
+				case *sqlparser.ComparisonExpr:
+					node.Expr = paren.Expr
+					continueFlag = true
+				}
+			}
+
+		case *sqlparser.AndExpr:
+			switch left := node.Left.(type) {
+			case *sqlparser.ParenExpr:
+				switch inner := left.Expr.(type) {
+				case *sqlparser.ComparisonExpr:
+					node.Left = inner
+					continueFlag = true
+				}
+			}
+
+			switch right := node.Right.(type) {
+			case *sqlparser.ParenExpr:
+				switch inner := right.Expr.(type) {
+				case *sqlparser.ComparisonExpr:
+					node.Right = inner
+					continueFlag = true
+				}
+			}
+
+		case *sqlparser.OrExpr:
+			switch left := node.Left.(type) {
+			case *sqlparser.ParenExpr:
+				switch inner := left.Expr.(type) {
+				case *sqlparser.ComparisonExpr:
+					node.Left = inner
+					continueFlag = true
+				}
+			}
+
+			switch right := node.Right.(type) {
+			case *sqlparser.ParenExpr:
+				switch inner := right.Expr.(type) {
+				case *sqlparser.ComparisonExpr:
+					node.Right = inner
+					continueFlag = true
+				}
+			}
+		}
+		return true, nil
+	}, rw.Stmt)
+	common.LogIfError(err, "")
+	// 本层的修改可能使得原本不符合条件的括号变为无意义括号
+	// 每次修改都需要再过滤一遍语法树
+	if continueFlag {
+		rw.rmParenthesis()
+	} else {
+		return
+	}
+}
+
+// RewriteRemoveDMLOrderBy dmlorderby: 对应RES.004,删除无LIMIT条件时UPDATE, DELETE中包含的ORDER BY
+func (rw *Rewrite) RewriteRemoveDMLOrderBy() *Rewrite {
+	switch st := rw.Stmt.(type) {
+	case *sqlparser.Update:
+		err := sqlparser.Walk(func(node sqlparser.SQLNode) (kontinue bool, err error) {
+			switch n := node.(type) {
+			case *sqlparser.Select:
+				if n.OrderBy != nil && n.Limit == nil {
+					n.OrderBy = nil
+				}
+				return false, nil
+			}
+			return true, nil
+		}, rw.Stmt)
+		common.LogIfError(err, "")
+		if st.OrderBy != nil && st.Limit == nil {
+			st.OrderBy = nil
+		}
+	case *sqlparser.Delete:
+		err := sqlparser.Walk(func(node sqlparser.SQLNode) (kontinue bool, err error) {
+			switch n := node.(type) {
+			case *sqlparser.Select:
+				if n.OrderBy != nil && n.Limit == nil {
+					n.OrderBy = nil
+				}
+				return false, nil
+			}
+			return true, nil
+		}, rw.Stmt)
+		common.LogIfError(err, "")
+		if st.OrderBy != nil && st.Limit == nil {
+			st.OrderBy = nil
+		}
+	}
+	rw.NewSQL = sqlparser.String(rw.Stmt)
+	return rw
+}
+
+// RewriteGroupByConst 对应CLA.004,将GROUP BY CONST替换为列名
+// TODO:
+func (rw *Rewrite) RewriteGroupByConst() *Rewrite {
+	err := sqlparser.Walk(func(node sqlparser.SQLNode) (kontinue bool, err error) {
+		switch n := node.(type) {
+		case *sqlparser.Select:
+			groupByCol := false
+			if n.GroupBy != nil {
+				for _, group := range n.GroupBy {
+					switch group.(type) {
+					case *sqlparser.SQLVal:
+					default:
+						groupByCol = true
+					}
+				}
+				if !groupByCol {
+					// TODO: 这里只是去掉了GROUP BY并没解决问题
+					n.GroupBy = nil
+				}
+			}
+		}
+		return true, nil
+	}, rw.Stmt)
+	common.LogIfError(err, "")
+	rw.NewSQL = sqlparser.String(rw.Stmt)
+	return rw
+}
+
+// RewriteSubQuery2Join 将subquery转写成join
+func (rw *Rewrite) RewriteSubQuery2Join() *Rewrite {
+	var err error
+	// 如果未配置mysql环境或从环境中获取失败
+	if common.Config.TestDSN.Disable || len(rw.Columns) == 0 {
+		common.Log.Debug("(rw *Rewrite) RewriteSubQuery2Join(): Rewrite failed. TestDSN.Disable: %v, len(rw.Columns):%d",
+			common.Config.TestDSN.Disable, len(rw.Columns))
+		return rw
+	}
+
+	if rw.NewSQL == "" {
+		rw.NewSQL = sqlparser.String(rw.Stmt)
+	}
+
+	// query backup
+	backup := rw.NewSQL
+	var subQueryList []string
+	err = sqlparser.Walk(func(node sqlparser.SQLNode) (kontinue bool, err error) {
+		switch sub := node.(type) {
+		case sqlparser.SelectStatement:
+			subStr := sqlparser.String(sub)
+			if strings.HasPrefix(subStr, "(") {
+				subStr = subStr[1 : len(subStr)-1]
+			}
+			subQueryList = append(subQueryList, subStr)
+		}
+		return true, nil
+	}, rw.Stmt)
+	common.LogIfError(err, "")
+	if length := len(subQueryList); length > 1 {
+		lastResult := ""
+		for i := length - 1; i > 0; i-- {
+			if lastResult == "" {
+				lastResult, err = rw.sub2Join(subQueryList[i-1], subQueryList[i])
+			} else {
+				// 将subquery的部分替换成上次合并的结果
+				subQueryList[i-1] = strings.Replace(subQueryList[i-1], subQueryList[i], lastResult, -1)
+				lastResult, err = rw.sub2Join(subQueryList[i-1], lastResult)
+			}
+
+			if err != nil {
+				common.Log.Error("RewriteSubQuery2Join Error: %v", err)
+				return rw
+			}
+		}
+		rw.NewSQL = lastResult
+	} else if length == 1 {
+		var newSQL string
+		newSQL, err = rw.sub2Join(rw.NewSQL, subQueryList[0])
+		if err == nil {
+			rw.NewSQL = newSQL
+		}
+	}
+
+	// 因为这个修改不会直接修改rw.stmt,所以需要将rw.stmt也更新一下
+	newStmt, err := sqlparser.Parse(rw.NewSQL)
+	if err != nil {
+		rw.NewSQL = backup
+		rw.Stmt, _ = sqlparser.Parse(backup)
+	} else {
+		rw.Stmt = newStmt
+	}
+
+	return rw
+}
+
+// sub2Join 将subquery转写成join
+func (rw *Rewrite) sub2Join(parent, sub string) (string, error) {
+	// 只处理SelectStatement
+	if sqlparser.Preview(parent) != sqlparser.StmtSelect || sqlparser.Preview(sub) != sqlparser.StmtSelect {
+		return "", nil
+	}
+
+	// 如果子查询不属于parent,则不处理
+	if !strings.Contains(parent, sub) {
+		return "", nil
+	}
+
+	// 解析外层SQL语法树
+	stmt, err := sqlparser.Parse(parent)
+	if err != nil {
+		common.Log.Warn("(rw *Rewrite) RewriteSubQuery2Join() sub2Join sql `%s` parsed error: %v", parent, err)
+		return "", err
+	}
+
+	switch stmt.(type) {
+	case sqlparser.SelectStatement:
+	default:
+		common.Log.Debug("Query `%s` not select statement.", parent)
+		return "", nil
+	}
+
+	// 解析子查询语法树
+	subStmt, err := sqlparser.Parse(sub)
+	if err != nil {
+		common.Log.Warn("(rw *Rewrite) RewriteSubQuery2Join() sub2Join sql `%s` parsed error: %v", sub, err)
+		return "", err
+	}
+
+	// 获取外部SQL用到的表
+	stmtMeta := GetTableFromExprs(stmt.(*sqlparser.Select).From)
+	// 获取内部SQL用到的表
+	subMeta := GetTableFromExprs(subStmt.(*sqlparser.Select).From)
+
+	// 处理关联条件
+	err = sqlparser.Walk(func(node sqlparser.SQLNode) (kontinue bool, err error) {
+		switch p := node.(type) {
+		case *sqlparser.ComparisonExpr:
+			// a in (select * from tb)
+			switch subquery := p.Right.(type) {
+			case *sqlparser.Subquery:
+
+				// 获取左边的列
+				var leftColumn *sqlparser.ColName
+
+				switch l := p.Left.(type) {
+				case *sqlparser.ColName:
+					leftColumn = l
+				default:
+					return false, nil
+				}
+
+				// 用于存放获取的subquery中的列,有且只有一个
+				var rightColumn sqlparser.SQLNode
+
+				// 对subquery中的列进行替换
+				switch subSelectStmt := subquery.Select.(type) {
+				case *sqlparser.Select:
+					cachingOperator := p.Operator
+
+					rightColumn = subSelectStmt.SelectExprs[0]
+
+					rightCol, _ := getColumnName(rightColumn.(*sqlparser.AliasedExpr).Expr)
+					if rightCol != nil {
+						// 将subquery替换为等值条件
+						p.Operator = "="
+
+						// selectExpr 信息补齐
+						var newExprs []sqlparser.SelectExpr
+						err = sqlparser.Walk(func(node sqlparser.SQLNode) (kontinue bool, err error) {
+							switch col := node.(type) {
+							case *sqlparser.StarExpr:
+								if col.TableName.Name.IsEmpty() {
+									for dbName, db := range stmtMeta {
+										for tbName := range db.Table {
+
+											col.TableName.Name = sqlparser.NewTableIdent(tbName)
+											if dbName != "" {
+												col.TableName.Qualifier = sqlparser.NewTableIdent(dbName)
+											}
+
+											newExprs = append(newExprs, col)
+										}
+									}
+								}
+							case *sqlparser.AliasedExpr:
+								switch n := col.Expr.(type) {
+								case *sqlparser.ColName:
+									col.Expr = columnFromWhere(n, stmtMeta, rw.Columns)
+								}
+							}
+							return true, nil
+						}, stmt.(*sqlparser.Select).SelectExprs)
+						common.LogIfError(err, "")
+
+						// 原节点列信息补齐
+						p.Left = columnFromWhere(leftColumn, stmtMeta, rw.Columns)
+
+						// 将子查询中的节点上提,补充前缀信息
+						p.Right = columnFromWhere(rightCol, subMeta, rw.Columns)
+
+						// subquery Where条件中的列信息补齐
+						subWhereExpr := subStmt.(*sqlparser.Select).Where
+						err = sqlparser.Walk(func(node sqlparser.SQLNode) (kontinue bool, err error) {
+							switch n := node.(type) {
+							case *sqlparser.ComparisonExpr:
+								switch left := n.Left.(type) {
+								case *sqlparser.ColName:
+									n.Left = columnFromWhere(left, subMeta, rw.Columns)
+								}
+
+								switch right := n.Right.(type) {
+								case *sqlparser.ColName:
+									n.Right = columnFromWhere(right, subMeta, rw.Columns)
+								}
+							}
+							return true, nil
+						}, subWhereExpr)
+						common.LogIfError(err, "")
+						// 如果subquery中存在Where条件,怼在parent的where中后面
+						if subWhereExpr != nil {
+							if stmt.(*sqlparser.Select).Where != nil {
+								stmt.(*sqlparser.Select).Where.Expr = &sqlparser.AndExpr{
+									Left:  stmt.(*sqlparser.Select).Where.Expr,
+									Right: subWhereExpr.Expr,
+								}
+							} else {
+								stmt.(*sqlparser.Select).Where = subWhereExpr
+							}
+						}
+
+						switch cachingOperator {
+						case "in":
+							// 将表以inner join的形式追加到parent的from中
+							var newTables []sqlparser.TableExpr
+							for _, subExpr := range subStmt.(*sqlparser.Select).From {
+								has := false
+								for _, expr := range stmt.(*sqlparser.Select).From {
+									if reflect.DeepEqual(expr, subExpr) {
+										has = true
+									}
+								}
+								if !has {
+									newTables = append(newTables, subExpr)
+								}
+							}
+							stmt.(*sqlparser.Select).From = append(stmt.(*sqlparser.Select).From, newTables...)
+						case "not in":
+							// 将表以left join 的形式 追加到parent的from中
+							// TODO
+						}
+					}
+
+				}
+			}
+		}
+		return true, nil
+	}, stmt)
+	common.LogIfError(err, "")
+	newSQL := sqlparser.String(stmt)
+	return newSQL, nil
+}
+
+// columnFromWhere 获取列是来自哪个表,并补充前缀
+func columnFromWhere(col *sqlparser.ColName, meta common.Meta, columns common.TableColumns) *sqlparser.ColName {
+
+	for dbName, db := range meta {
+		for tbName := range db.Table {
+			for _, tables := range columns {
+				for _, columns := range tables {
+					for _, column := range columns {
+						if strings.EqualFold(col.Name.String(), column.Name) {
+							if col.Qualifier.Name.IsEmpty() && tbName == column.Table {
+								col.Qualifier.Name = sqlparser.NewTableIdent(column.Table)
+								return col
+							}
+							if (dbName == "" && tbName == column.Table) || (tbName == column.Table && dbName == column.DB) {
+								col.Qualifier.Name = sqlparser.NewTableIdent(column.Table)
+								if dbName != "" {
+									col.Qualifier.Qualifier = sqlparser.NewTableIdent(column.DB)
+								}
+								return col
+							}
+						}
+					}
+				}
+			}
+
+		}
+	}
+	return col
+}
+
+// RewriteJoin2SubQuery join2sub: TODO:
+// https://mariadb.com/kb/en/library/subqueries-and-joins/
+func (rw *Rewrite) RewriteJoin2SubQuery() *Rewrite {
+	return rw
+}
+
+// RewriteDistinctStar distinctstar: 对应DIS.003,将多余的`DISTINCT *`删除
+func (rw *Rewrite) RewriteDistinctStar() *Rewrite {
+	// 注意:这里并未对表是否有主键做检查,按照我们的SQL编程规范,一张表必须有主键
+	switch rw.Stmt.(type) {
+	case *sqlparser.Select:
+		meta := GetMeta(rw.Stmt, nil)
+		for _, m := range meta {
+			if len(m.Table) == 1 {
+				// distinct tbl.*, distinct *, count(distinct *)
+				re := regexp.MustCompile(`(?i)((distinct\s*\*)|(distinct\s+[0-9a-z_` + "`" + `]*\.\*))`)
+				if re.MatchString(rw.SQL) {
+					rw.NewSQL = re.ReplaceAllString(rw.SQL, "*")
+				}
+			}
+			break
+		}
+	}
+	if rw.NewSQL == "" {
+		rw.NewSQL = rw.SQL
+	}
+	rw.Stmt, _ = sqlparser.Parse(rw.NewSQL)
+	return rw
+}
+
+// RewriteTruncate truncate: DELETE全表修改为TRUNCATE TABLE
+func (rw *Rewrite) RewriteTruncate() *Rewrite {
+	switch n := rw.Stmt.(type) {
+	case *sqlparser.Delete:
+		meta := GetMeta(rw.Stmt, nil)
+		if len(meta) == 1 && n.Where == nil {
+			for _, db := range meta {
+				for _, tbl := range db.Table {
+					rw.NewSQL = "truncate table " + tbl.TableName
+				}
+			}
+		}
+	}
+	return rw
+}
+
+// RewriteDML2Select dml2select: DML转成SELECT,兼容低版本的EXPLAIN
+func (rw *Rewrite) RewriteDML2Select() *Rewrite {
+	if rw.Stmt == nil {
+		return rw
+	}
+
+	switch stmt := rw.Stmt.(type) {
+	case *sqlparser.Select:
+		rw.NewSQL = rw.SQL
+	case *sqlparser.Delete: // Multi DELETE not support yet.
+		rw.NewSQL = delete2Select(stmt)
+	case *sqlparser.Insert:
+		rw.NewSQL = insert2Select(stmt)
+	case *sqlparser.Update: // Multi UPDATE not support yet.
+		rw.NewSQL = update2Select(stmt)
+	}
+	rw.Stmt, _ = sqlparser.Parse(rw.NewSQL)
+	return rw
+}
+
+// delete2Select 将Delete语句改写成Select
+func delete2Select(stmt *sqlparser.Delete) string {
+	newSQL := &sqlparser.Select{
+		SelectExprs: []sqlparser.SelectExpr{
+			new(sqlparser.StarExpr),
+		},
+		From:    stmt.TableExprs,
+		Where:   stmt.Where,
+		OrderBy: stmt.OrderBy,
+	}
+	return sqlparser.String(newSQL)
+}
+
+// update2Select 将Update语句改写成Select
+func update2Select(stmt *sqlparser.Update) string {
+	newSQL := &sqlparser.Select{
+		SelectExprs: []sqlparser.SelectExpr{
+			new(sqlparser.StarExpr),
+		},
+		From:    stmt.TableExprs,
+		Where:   stmt.Where,
+		OrderBy: stmt.OrderBy,
+		Limit:   stmt.Limit,
+	}
+	return sqlparser.String(newSQL)
+}
+
+// insert2Select 将Insert语句改写成Select
+func insert2Select(stmt *sqlparser.Insert) string {
+	switch row := stmt.Rows.(type) {
+	// 如果insert包含子查询,只需要explain该子树
+	case *sqlparser.Select, *sqlparser.Union, *sqlparser.ParenSelect:
+		return sqlparser.String(row)
+	}
+
+	return "select 1 from DUAL"
+}
+
+// AlterAffectTable 获取ALTER影响的库表名,返回:`db`.`table`
+func AlterAffectTable(stmt sqlparser.Statement) string {
+	switch n := stmt.(type) {
+	case *sqlparser.DDL:
+		tableName := strings.ToLower(n.Table.Name.String())
+		dbName := strings.ToLower(n.Table.Qualifier.String())
+		if tableName != "" && tableName != "dual" {
+			if dbName == "" {
+				return "`" + tableName + "`"
+			}
+
+			return "`" + dbName + "`.`" + tableName + "`"
+		}
+	}
+	return ""
+}
+
+// MergeAlterTables mergealter: 将同一张表的多条ALTER语句合成一条ALTER语句
+// @input: sql, alter string
+// @output: [[db.]table]sql, 如果找不到DB,key为表名;如果找得到DB,key为db.table
+func MergeAlterTables(sqls ...string) map[string]string {
+	alterStrs := make(map[string][]string)
+	mergedAlterStr := make(map[string]string)
+
+	alterExp := regexp.MustCompile(`(?i)alter\s*table\s*[^\s]*\s*`)   // ALTER TABLE
+	renameExp := regexp.MustCompile(`(?i)rename\s*table\s*[^\s]*\s*`) // RENAME TABLE
+	// CREATE [UNIQUE|FULLTEXT|SPATIAL|PRIMARY] [KEY|INDEX] idx_name ON tbl_name
+	createIndexExp := regexp.MustCompile(`(?i)create((unique)|(fulltext)|(spatial)|(primary)|(\s*)\s*)((index)|(key))\s*`)
+	indexNameExp := regexp.MustCompile(`(?i)[^\s]*\s*`)
+	indexColsExp := regexp.MustCompile(`(?i)[^\s]*\s*on\s*[^\s]*\s*`)
+
+	for _, sql := range sqls {
+		sql = strings.Trim(sql, common.Config.Delimiter)
+		stmt, _ := sqlparser.Parse(sql)
+		alterStr := ""
+		dbName := ""
+		tableName := ""
+		switch n := stmt.(type) {
+		case *sqlparser.DDL:
+			// 注意: 表名和库名不区分大小写
+			tableName = strings.ToLower(n.Table.Name.String())
+			dbName = strings.ToLower(n.Table.Qualifier.String())
+			switch n.Action {
+			case "rename":
+				if alterExp.MatchString(sql) {
+					common.Log.Debug("rename alterExp: ALTER %v %v", tableName, alterExp.ReplaceAllString(sql, ""))
+					alterStr = fmt.Sprint(alterExp.ReplaceAllString(sql, ""))
+				} else if renameExp.MatchString(sql) {
+					common.Log.Debug("rename renameExp: ALTER %v %v", tableName, alterExp.ReplaceAllString(sql, ""))
+					alterStr = fmt.Sprint(alterExp.ReplaceAllString(sql, ""))
+				} else {
+					common.Log.Warn("rename not match: ALTER %v %v", tableName, sql)
+				}
+			case "alter":
+				if alterExp.MatchString(sql) {
+					common.Log.Debug("rename alterExp: ALTER %v %v", tableName, alterExp.ReplaceAllString(sql, ""))
+					alterStr = fmt.Sprint(alterExp.ReplaceAllString(sql, ""))
+				} else if createIndexExp.MatchString(sql) {
+					buf := createIndexExp.ReplaceAllString(sql, "")
+					idxName := strings.TrimSpace(indexNameExp.FindString(buf))
+					buf = indexColsExp.ReplaceAllString(buf, "")
+					common.Log.Debug("alter createIndexExp: ALTER %v ADD INDEX %v %v", tableName, "ADD INDEX", idxName, buf)
+					alterStr = fmt.Sprint("ADD INDEX", " "+idxName+" ", buf)
+				}
+			default:
+
+			}
+		}
+		if alterStr != "" && tableName != "" && tableName != "dual" {
+			if dbName == "" {
+				alterStrs["`"+tableName+"`"] = append(alterStrs["`"+tableName+"`"], alterStr)
+			} else {
+				alterStrs["`"+dbName+"`.`"+tableName+"`"] = append(alterStrs["`"+dbName+"`.`"+tableName+"`"], alterStr)
+			}
+		}
+	}
+	for k, v := range alterStrs {
+		mergedAlterStr[k] = fmt.Sprintln("ALTER TABLE", k, strings.Join(v, ", "), common.Config.Delimiter)
+	}
+	return mergedAlterStr
+}
+
+// RewriteRuleMatch 检查重写规则是否生效
+func RewriteRuleMatch(name string) bool {
+	for _, r := range common.Config.RewriteRules {
+		if r == name {
+			return true
+		}
+	}
+	return false
+}
diff --git a/ast/rewrite_test.go b/ast/rewrite_test.go
new file mode 100644
index 00000000..f84250f3
--- /dev/null
+++ b/ast/rewrite_test.go
@@ -0,0 +1,685 @@
+/*
+ * Copyright 2018 Xiaomi, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package ast
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/XiaoMi/soar/common"
+)
+
+func TestRewrite(t *testing.T) {
+	common.Config.TestDSN.Disable = false
+	testSQL := []map[string]string{
+		{
+			"input":  `SELECT * FROM film`,
+			"output": `select film.film_id, film.title, film.description, film.release_year, film.language_id, film.original_language_id, film.rental_duration from film;`,
+		},
+		{
+			"input":  `SELECT film.*, actor.actor_id FROM film,actor`,
+			"output": `select film.film_id, film.title, film.description, film.release_year, film.language_id, film.original_language_id, film.rental_duration, actor.actor_id from film, actor;`,
+		},
+		{
+			"input":  `insert into film values(1,2,3,4,5)`,
+			"output": `insert into film(film_id, title, description, release_year, language_id) values (1, 2, 3, 4, 5);`,
+		},
+		{
+			"input":  `insert into sakila.film values(1,2)`,
+			"output": `insert into sakila.film(film_id, title) values (1, 2);`,
+		},
+		{
+			"input":  `replace into sakila.film select id from tb`,
+			"output": `replace into sakila.film(film_id) select id from tb;`,
+		},
+		{
+			"input":  `replace into sakila.film select id, title, description from tb`,
+			"output": `replace into sakila.film(film_id, title, description) select id, title, description from tb;`,
+		},
+		{
+			"input":  `insert into film values(1,2,3,4,5)`,
+			"output": `insert into film(film_id, title, description, release_year, language_id) values (1, 2, 3, 4, 5);`,
+		},
+		{
+			"input":  `insert into sakila.film values(1,2)`,
+			"output": `insert into sakila.film(film_id, title) values (1, 2);`,
+		},
+		{
+			"input":  `replace into sakila.film select id from tb`,
+			"output": `replace into sakila.film(film_id) select id from tb;`,
+		},
+		{
+			"input":  `replace into sakila.film select id, title, description from tb`,
+			"output": `replace into sakila.film(film_id, title, description) select id, title, description from tb;`,
+		},
+		{
+			"input":  "DELETE FROM tbl WHERE col1=1 ORDER BY col",
+			"output": "delete from tbl where col1 = 1;",
+		},
+		{
+			"input":  "UPDATE tbl SET col =1 WHERE col1=1 ORDER BY col",
+			"output": "update tbl set col = 1 where col1 = 1;",
+		},
+	}
+
+	for _, sql := range testSQL {
+		rw := NewRewrite(sql["input"])
+		rw.Columns = map[string]map[string][]*common.Column{
+			"sakila": {
+				"film": {
+					{Name: "film_id", Table: "film"},
+					{Name: "title", Table: "film"},
+					{Name: "description", Table: "film"},
+					{Name: "release_year", Table: "film"},
+					{Name: "language_id", Table: "film"},
+					{Name: "original_language_id", Table: "film"},
+					{Name: "rental_duration", Table: "film"},
+				},
+			},
+		}
+		rw.Rewrite()
+		if rw.NewSQL != sql["output"] {
+			t.Errorf("want: %s\ngot: %s", sql["output"], rw.NewSQL)
+		}
+	}
+}
+
+func TestRewriteStar2Columns(t *testing.T) {
+	common.Config.TestDSN.Disable = false
+	testSQL := []map[string]string{
+		{
+			"input":  `SELECT * FROM film`,
+			"output": `select film.film_id, film.title from film`,
+		},
+		{
+			"input":  `SELECT film.*, actor.actor_id FROM film,actor`,
+			"output": `select film.film_id, film.title, actor.actor_id from film, actor`,
+		},
+	}
+
+	for _, sql := range testSQL {
+		rw := NewRewrite(sql["input"])
+		rw.Columns = map[string]map[string][]*common.Column{
+			"sakila": {
+				"film": {
+					{Name: "film_id", Table: "film"},
+					{Name: "title", Table: "film"},
+				},
+			},
+		}
+		rw.RewriteStar2Columns()
+		if rw.NewSQL != sql["output"] {
+			t.Errorf("want: %s\ngot: %s", sql["output"], rw.NewSQL)
+		}
+	}
+}
+
+func TestRewriteInsertColumns(t *testing.T) {
+	testSQL := []map[string]string{
+		{
+			"input":  `insert into film values(1,2,3,4,5)`,
+			"output": `insert into film(film_id, title, description, release_year, language_id) values (1, 2, 3, 4, 5)`,
+		},
+		{
+			"input":  `insert into sakila.film values(1,2)`,
+			"output": `insert into sakila.film(film_id, title) values (1, 2)`,
+		},
+		{
+			"input":  `replace into sakila.film select id from tb`,
+			"output": `replace into sakila.film(film_id) select id from tb`,
+		},
+		{
+			"input":  `replace into sakila.film select id, title, description from tb`,
+			"output": `replace into sakila.film(film_id, title, description) select id, title, description from tb`,
+		},
+	}
+
+	for _, sql := range testSQL {
+		rw := NewRewrite(sql["input"])
+		rw.Columns = map[string]map[string][]*common.Column{
+			"sakila": {
+				"film": {
+					{Name: "film_id", Table: "film"},
+					{Name: "title", Table: "film"},
+					{Name: "description", Table: "film"},
+					{Name: "release_year", Table: "film"},
+					{Name: "language_id", Table: "film"},
+					{Name: "original_language_id", Table: "film"},
+					{Name: "rental_duration", Table: "film"},
+				},
+			},
+		}
+		rw.RewriteInsertColumns()
+		if rw.NewSQL != sql["output"] {
+			t.Errorf("want: %s\ngot: %s", sql["output"], rw.NewSQL)
+		}
+	}
+}
+
+func TestRewriteHaving(t *testing.T) {
+	testSQL := []map[string]string{
+		{
+			"input":  `SELECT state, COUNT(*) FROM Drivers GROUP BY state HAVING state IN ('GA', 'TX') ORDER BY state`,
+			"output": "select state, COUNT(*) from Drivers where state in ('GA', 'TX') group by state order by state asc",
+		},
+		{
+			"input":  `SELECT state, COUNT(*) FROM Drivers WHERE col =1 GROUP BY state HAVING state IN ('GA', 'TX') ORDER BY state`,
+			"output": "select state, COUNT(*) from Drivers where (col = 1) and state in ('GA', 'TX') group by state order by state asc",
+		},
+		{
+			"input":  `SELECT state, COUNT(*) FROM Drivers WHERE col =1 or col1 =2 GROUP BY state HAVING state IN ('GA', 'TX') ORDER BY state`,
+			"output": "select state, COUNT(*) from Drivers where (col = 1 or col1 = 2) and state in ('GA', 'TX') group by state order by state asc",
+		},
+	}
+	for _, sql := range testSQL {
+		rw := NewRewrite(sql["input"]).RewriteHaving()
+		if rw.NewSQL != sql["output"] {
+			t.Errorf("want: %s\ngot: %s", sql["output"], rw.NewSQL)
+		}
+	}
+}
+
+func TestRewriteAddOrderByNull(t *testing.T) {
+	testSQL := []map[string]string{
+		{
+			"input":  "SELECT sum(col1) FROM tbl GROUP BY col",
+			"output": "select sum(col1) from tbl group by col order by null",
+		},
+	}
+	for _, sql := range testSQL {
+		rw := NewRewrite(sql["input"]).RewriteAddOrderByNull()
+		if rw.NewSQL != sql["output"] {
+			t.Errorf("want: %s\ngot: %s", sql["output"], rw.NewSQL)
+		}
+	}
+}
+
+func TestRewriteRemoveDMLOrderBy(t *testing.T) {
+	testSQL := []map[string]string{
+		{
+			"input":  "DELETE FROM tbl WHERE col1=1 ORDER BY col",
+			"output": "delete from tbl where col1 = 1",
+		},
+		{
+			"input":  "UPDATE tbl SET col =1 WHERE col1=1 ORDER BY col",
+			"output": "update tbl set col = 1 where col1 = 1",
+		},
+	}
+	for _, sql := range testSQL {
+		rw := NewRewrite(sql["input"]).RewriteRemoveDMLOrderBy()
+		if rw.NewSQL != sql["output"] {
+			t.Errorf("want: %s\ngot: %s", sql["output"], rw.NewSQL)
+		}
+	}
+}
+
+func TestRewriteGroupByConst(t *testing.T) {
+	testSQL := []map[string]string{
+		{
+			"input":  "select 1;",
+			"output": "select 1 from dual",
+		},
+		/*
+				{
+					"input":  "SELECT col1 FROM tbl GROUP BY 1;",
+					"output": "select col1 from tbl GROUP BY col1",
+				},
+			    {
+					"input":  "SELECT col1, col2 FROM tbl GROUP BY 1, 2;",
+					"output": "select col1, col2 from tbl GROUP BY col1, col2",
+				},
+			    {
+					"input":  "SELECT col1, col2, col3 FROM tbl GROUP BY 1, 3;",
+					"output": "select col1, col2, col3 from tbl GROUP BY col1, col3",
+				},
+		*/
+	}
+	for _, sql := range testSQL {
+		rw := NewRewrite(sql["input"]).RewriteGroupByConst()
+		if rw.NewSQL != sql["output"] {
+			t.Errorf("want: %s\ngot: %s", sql["output"], rw.NewSQL)
+		}
+	}
+}
+
+func TestRewriteStandard(t *testing.T) {
+	testSQL := []map[string]string{
+		{
+			"input":  "SELECT sum(col1) FROM tbl GROUP BY 1;",
+			"output": "select sum(col1) from tbl group by 1",
+		},
+	}
+	for _, sql := range testSQL {
+		rw := NewRewrite(sql["input"]).RewriteStandard()
+		if rw.NewSQL != sql["output"] {
+			t.Errorf("want: %s\ngot: %s", sql["output"], rw.NewSQL)
+		}
+	}
+}
+
+func TestRewriteCountStar(t *testing.T) {
+	testSQL := []map[string]string{
+		{
+			"input":  "SELECT count(col) FROM tbl GROUP BY 1;",
+			"output": "select count(*) from tbl group by 1",
+		},
+		{
+			"input":  "SELECT COUNT(tb.col) FROM tbl GROUP BY 1;",
+			"output": "select COUNT(tb.*) from tbl group by 1",
+		},
+	}
+	for _, sql := range testSQL {
+		rw := NewRewrite(sql["input"]).RewriteCountStar()
+		if rw.NewSQL != sql["output"] {
+			t.Errorf("want: %s\ngot: %s", sql["output"], rw.NewSQL)
+		}
+	}
+}
+
+func TestRewriteInnoDB(t *testing.T) {
+	testSQL := []map[string]string{
+		{
+			"input":  "CREATE TABLE t1(id bigint(20) NOT NULL AUTO_INCREMENT);",
+			"output": "create table t1 (\n\tid bigint(20) not null auto_increment\n) ENGINE=InnoDB ",
+		},
+		{
+			"input":  "create table t1 (\n\tid bigint(20) not null auto_increment\n) ENGINE=memory ",
+			"output": "create table t1 (\n\tid bigint(20) not null auto_increment\n) ENGINE=InnoDB ",
+		},
+	}
+	for _, sql := range testSQL {
+		rw := NewRewrite(sql["input"]).RewriteInnoDB()
+		if rw.NewSQL != sql["output"] {
+			t.Errorf("want: %s\ngot: %s", sql["output"], rw.NewSQL)
+		}
+	}
+}
+
+func TestRewriteAutoIncrement(t *testing.T) {
+	testSQL := []map[string]string{
+		{
+			"input":  "CREATE TABLE t1(id bigint(20) NOT NULL AUTO_INCREMENT) ENGINE=InnoDB AUTO_INCREMENT=123802;",
+			"output": "create table t1 (\n\tid bigint(20) not null auto_increment\n) ENGINE=InnoDB auto_increment=1 ",
+		},
+		{
+			"input":  "create table t1 (\n\tid bigint(20) not null auto_increment\n) ENGINE=InnoDB",
+			"output": "create table t1 (\n\tid bigint(20) not null auto_increment\n) ENGINE=InnoDB",
+		},
+	}
+	for _, sql := range testSQL {
+		rw := NewRewrite(sql["input"]).RewriteAutoIncrement()
+		if rw.NewSQL != sql["output"] {
+			t.Errorf("want: %s\ngot: %s", sql["output"], rw.NewSQL)
+		}
+	}
+}
+
+func TestRewriteIntWidth(t *testing.T) {
+	testSQL := []map[string]string{
+		{
+			"input":  "CREATE TABLE t1(id bigint(10) NOT NULL AUTO_INCREMENT) ENGINE=InnoDB AUTO_INCREMENT=123802;",
+			"output": "create table t1 (\n\tid bigint(20) not null auto_increment\n) ENGINE=InnoDB auto_increment=123802",
+		},
+		{
+			"input":  "CREATE TABLE t1(id bigint NOT NULL AUTO_INCREMENT) ENGINE=InnoDB AUTO_INCREMENT=123802;",
+			"output": "create table t1 (\n\tid bigint(20) not null auto_increment\n) ENGINE=InnoDB auto_increment=123802",
+		},
+		{
+			"input":  "create table t1(id int(20) not null auto_increment) ENGINE=InnoDB;",
+			"output": "create table t1 (\n\tid int(10) not null auto_increment\n) ENGINE=InnoDB",
+		},
+		{
+			"input":  "create table t1(id int not null auto_increment) ENGINE=InnoDB;",
+			"output": "create table t1 (\n\tid int not null auto_increment\n) ENGINE=InnoDB",
+		},
+	}
+	for _, sql := range testSQL {
+		rw := NewRewrite(sql["input"]).RewriteIntWidth()
+		if rw.NewSQL != sql["output"] {
+			t.Errorf("want: %s\ngot: %s", sql["output"], rw.NewSQL)
+		}
+	}
+}
+
+func TestRewriteAlwaysTrue(t *testing.T) {
+	testSQL := []map[string]string{
+		{
+			"input":  "SELECT count(col) FROM tbl where 1=1;",
+			"output": "select count(col) from tbl",
+		},
+		{
+			"input":  "SELECT count(col) FROM tbl where col=col;",
+			"output": "select count(col) from tbl where col = col",
+		},
+		{
+			"input":  "SELECT count(col) FROM tbl where col=col2;",
+			"output": "select count(col) from tbl where col = col2",
+		},
+		{
+			"input":  "SELECT count(col) FROM tbl where 1>=1;",
+			"output": "select count(col) from tbl",
+		},
+		{
+			"input":  "SELECT count(col) FROM tbl where 1<=1;",
+			"output": "select count(col) from tbl",
+		},
+		{
+			"input":  "SELECT count(col) FROM tbl where 1=1 and 2=2;",
+			"output": "select count(col) from tbl",
+		},
+		{
+			"input":  "SELECT count(col) FROM tbl where 1=1 or 2=3;",
+			"output": "select count(col) from tbl where 2 = 3",
+		},
+		{
+			"input":  "SELECT count(col) FROM tbl where 1=1 and 3=3 or 2=3;",
+			"output": "select count(col) from tbl where 2 = 3",
+		},
+		{
+			"input":  "SELECT count(col) FROM tbl where 1=1 and 3=3 or 2!=3;",
+			"output": "select count(col) from tbl",
+		},
+		{
+			"input":  "SELECT count(col) FROM tbl where 1=1 or 2=3 and 3=3 ;",
+			"output": "select count(col) from tbl where 2 = 3",
+		},
+		{
+			"input":  "SELECT count(col) FROM tbl where (1=1);",
+			"output": "select count(col) from tbl",
+		},
+		{
+			"input":  "SELECT count(col) FROM tbl where ('a'= 'a' or 'b' = 'b') and a = 'b';",
+			"output": "select count(col) from tbl where a = 'b'",
+		},
+		{
+			"input":  "SELECT count(col) FROM tbl where (('a'= 'a' or 'b' = 'b') and a = 'b');",
+			"output": "select count(col) from tbl where (a = 'b')",
+		},
+		{
+			"input":  "SELECT count(col) FROM tbl where 'a'= 'a' or ('b' = 'b' and a = 'b');",
+			"output": "select count(col) from tbl where (a = 'b')",
+		},
+	}
+	for _, sql := range testSQL {
+		rw := NewRewrite(sql["input"]).RewriteAlwaysTrue()
+		if rw == nil {
+			t.Errorf("NoRw")
+		} else if rw.NewSQL != sql["output"] {
+			t.Errorf("want: %s\ngot: %s", sql["output"], rw.NewSQL)
+		}
+	}
+}
+
+// TODO:
+func TestRewriteSubQuery2Join(t *testing.T) {
+	common.Config.TestDSN.Disable = true
+	testSQL := []map[string]string{
+		{
+			// 这个case是官方文档给的,但不一定正确,需要视表结构的定义来进行判断
+			"input":  `SELECT * FROM t1 WHERE id IN (SELECT id FROM t2);`,
+			"output": "",
+			//"output": `SELECT DISTINCT t1.* FROM t1, t2 WHERE t1.id=t2.id;`,
+		},
+		{
+			"input":  `SELECT * FROM t1 WHERE id NOT IN (SELECT id FROM t2);`,
+			"output": "",
+			//"output": `SELECT table1.* FROM t1 LEFT JOIN t2 ON t1.id=t2.id WHERE t2.id IS NULL;`,
+		},
+		{
+			"input":  `SELECT * FROM t1 WHERE NOT EXISTS (SELECT id FROM t2 WHERE t1.id=t2.id);`,
+			"output": "",
+			//"output": `SELECT table1.* FROM table1 LEFT JOIN table2 ON table1.id=table2.id WHERE table2.id IS NULL;`,
+		},
+	}
+	for _, sql := range testSQL {
+		rw := NewRewrite(sql["input"]).RewriteSubQuery2Join()
+		if rw.NewSQL != sql["output"] {
+			t.Errorf("want: %s\ngot: %s", sql["output"], rw.NewSQL)
+		}
+	}
+}
+
+func TestRewriteDML2Select(t *testing.T) {
+	testSQL := []map[string]string{
+		{
+			"input":  "DELETE city, country FROM city INNER JOIN country using (country_id) WHERE city.city_id = 1;",
+			"output": "select * from city join country using (country_id) where city.city_id = 1",
+		}, {
+			"input":  "DELETE city FROM city LEFT JOIN country ON city.country_id = country.country_id WHERE country.country IS NULL;",
+			"output": "select * from city left join country on city.country_id = country.country_id where country.country is null",
+		}, {
+			"input":  "DELETE a1, a2 FROM city AS a1 INNER JOIN country AS a2 WHERE a1.country_id=a2.country_id",
+			"output": "select * from city as a1 join country as a2 where a1.country_id = a2.country_id",
+		}, {
+			"input":  "DELETE FROM a1, a2 USING city AS a1 INNER JOIN country AS a2 WHERE a1.country_id=a2.country_id",
+			"output": "select * from city as a1 join country as a2 where a1.country_id = a2.country_id",
+		}, {
+			"input":  "DELETE FROM film WHERE length > 100;",
+			"output": "select * from film where length > 100",
+		}, {
+			"input":  "UPDATE city INNER JOIN country USING(country_id) SET city.city = 'Abha', city.last_update = '2006-02-15 04:45:25', country.country = 'Afghanistan' WHERE city.city_id=10;",
+			"output": "select * from city join country using (country_id) where city.city_id = 10",
+		}, {
+			"input":  "UPDATE city INNER JOIN country ON city.country_id = country.country_id INNER JOIN address ON city.city_id = address.city_id SET city.city = 'Abha', city.last_update = '2006-02-15 04:45:25', country.country = 'Afghanistan' WHERE city.city_id=10;",
+			"output": "select * from city join country on city.country_id = country.country_id join address on city.city_id = address.city_id where city.city_id = 10",
+		}, {
+			"input":  "UPDATE city, country SET city.city = 'Abha', city.last_update = '2006-02-15 04:45:25', country.country = 'Afghanistan' WHERE city.country_id = country.country_id AND city.city_id=10;",
+			"output": "select * from city, country where city.country_id = country.country_id and city.city_id = 10",
+		}, {
+			"input":  "UPDATE film SET length = 10 WHERE language_id = 20;",
+			"output": "select * from film where language_id = 20",
+		}, {
+			"input":  "INSERT INTO city (country_id) SELECT country_id FROM country;",
+			"output": "select country_id from country",
+		}, {
+			"input":  "INSERT INTO city (country_id) VALUES (1),(2),(3);",
+			"output": "select 1 from DUAL",
+		}, {
+			"input":  "INSERT INTO city (country_id) VALUES (10);",
+			"output": "select 1 from DUAL",
+		}, {
+			"input":  "INSERT INTO city (country_id) SELECT 10 FROM DUAL;",
+			"output": "select 10 from dual",
+		}, {
+			"input":  "replace INTO city (country_id) SELECT 10 FROM DUAL;",
+			"output": "select 10 from dual",
+		},
+	}
+
+	for _, sql := range testSQL {
+		rw := NewRewrite(sql["input"]).RewriteDML2Select()
+		if rw.NewSQL != sql["output"] {
+			t.Errorf("want: %s\ngot: %s", sql["output"], rw.NewSQL)
+		}
+	}
+}
+
+func TestRewriteDistinctStar(t *testing.T) {
+	testSQL := []map[string]string{
+		{
+			"input":  `SELECT DISTINCT * FROM film;`,
+			"output": "SELECT * FROM film;",
+		},
+		{
+			"input":  `SELECT COUNT(DISTINCT *) FROM film;`,
+			"output": "SELECT COUNT(*) FROM film;",
+		},
+		{
+			"input":  `SELECT DISTINCT film.* FROM film;`,
+			"output": "SELECT * FROM film;",
+		},
+		{
+			"input":  "SELECT DISTINCT col FROM film;",
+			"output": "SELECT DISTINCT col FROM film;",
+		},
+		{
+			"input":  "SELECT DISTINCT film.* FROM film, tbl;",
+			"output": "SELECT DISTINCT film.* FROM film, tbl;",
+		},
+		{
+
+			"input":  "SELECT DISTINCT * FROM film, tbl;",
+			"output": "SELECT DISTINCT * FROM film, tbl;",
+		},
+	}
+	for _, sql := range testSQL {
+		rw := NewRewrite(sql["input"]).RewriteDistinctStar()
+		if rw.NewSQL != sql["output"] {
+			t.Errorf("want: %s\ngot: %s", sql["output"], rw.NewSQL)
+		}
+	}
+}
+
+func TestMergeAlterTables(t *testing.T) {
+	sqls := []string{
+		// ADD|DROP INDEX
+		// TODO: PRIMARY KEY, [UNIQUE|FULLTEXT|SPATIAL] INDEX
+		"CREATE INDEX part_of_name ON customer (name(10));",
+		"alter table `sakila`.`t1` add index `idx_col`(`col`)",
+		"alter table `sakila`.`t1` add UNIQUE index `idx_col`(`col`)",
+		"alter table `sakila`.`t1` add index `idx_ID`(`ID`)",
+
+		// ADD|DROP COLUMN
+		"ALTER TABLE t2 DROP COLUMN c, DROP COLUMN d;",
+		"ALTER TABLE T2 ADD COLUMN C int;",
+		"ALTER TABLE T2 ADD COLUMN D int FIRST;",
+		"ALTER TABLE T2 ADD COLUMN E int AFTER D;",
+
+		// RENAME COLUMN
+		"ALTER TABLE t1 RENAME COLUMN a TO b",
+
+		// RENAME INDEX
+		"ALTER TABLE t1 RENAME INDEX idx_a TO idx_b",
+		"ALTER TABLE t1 RENAME KEY idx_a TO idx_b",
+
+		// RENAME TABLE
+		"ALTER TABLE db.old_table RENAME new_table;",
+		"ALTER TABLE old_table RENAME TO new_table;",
+		"ALTER TABLE old_table RENAME AS new_table;",
+
+		// MODIFY & CHANGE
+		"ALTER TABLE t1 MODIFY col1 BIGINT UNSIGNED DEFAULT 1 COMMENT 'my column';",
+		"ALTER TABLE t1 CHANGE b a INT NOT NULL;",
+	}
+	fmt.Println(MergeAlterTables(sqls...))
+}
+
+func TestRewriteUnionAll(t *testing.T) {
+	testSQL := []map[string]string{
+		{
+			"input":  `select country_id from city union select country_id from country;`,
+			"output": "select country_id from city union all select country_id from country",
+		},
+	}
+	for _, sql := range testSQL {
+		rw := NewRewrite(sql["input"]).RewriteUnionAll()
+		if rw.NewSQL != sql["output"] {
+			t.Errorf("want: %s\ngot: %s", sql["output"], rw.NewSQL)
+		}
+	}
+}
+func TestRewriteTruncate(t *testing.T) {
+	testSQL := []map[string]string{
+		{
+			"input":  `delete from tbl;`,
+			"output": "truncate table tbl",
+		},
+	}
+	for _, sql := range testSQL {
+		rw := NewRewrite(sql["input"]).RewriteTruncate()
+		if rw.NewSQL != sql["output"] {
+			t.Errorf("want: %s\ngot: %s", sql["output"], rw.NewSQL)
+		}
+	}
+}
+
+func TestRewriteOr2In(t *testing.T) {
+	testSQL := []map[string]string{
+		{
+			"input":  `select country_id from city where country_id = 1 or country_id = 2 or country_id = 3;`,
+			"output": "select country_id from city where country_id in (1, 2, 3)",
+		},
+		// TODO or中的恒真条件
+		{
+			"input":  `select country_id from city where country_id != 1 or country_id != 2 or country_id = 3;`,
+			"output": "select country_id from city where country_id != 1 or country_id != 2 or country_id = 3",
+		},
+		// col = 1 or col is null不可转为IN
+		{
+			"input":  `select country_id from city where col = 1 or col is null;`,
+			"output": "select country_id from city where col = 1 or col is null",
+		},
+		{
+			"input":  `select country_id from city where col1 = 1 or col2 = 1 or col2 = 2;`,
+			"output": "select country_id from city where col1 = 1 or col2 in (1, 2)",
+		},
+		{
+			"input":  `select country_id from city where col1 = 1 or col2 = 1 or col2 = 2 or col1 = 3;`,
+			"output": "select country_id from city where col2 in (1, 2) or col1 in (1, 3)",
+		},
+		{
+			"input":  `select country_id from city where (col1 = 1 or col2 = 1 or col2 = 2 ) or col1 = 3;`,
+			"output": "select country_id from city where (col1 = 1 or col2 in (1, 2)) or col1 = 3",
+		},
+		{
+			"input":  `select country_id from city where col1 = 1 or (col2 = 1 or col2 = 2 ) or col1 = 3;`,
+			"output": "select country_id from city where (col2 in (1, 2)) or col1 in (1, 3)",
+		},
+	}
+	for _, sql := range testSQL {
+		rw := NewRewrite(sql["input"]).RewriteOr2In()
+		if rw.NewSQL != sql["output"] {
+			t.Errorf("want: %s\ngot: %s", sql["output"], rw.NewSQL)
+		}
+	}
+}
+
+func TestRmParenthesis(t *testing.T) {
+	testSQL := []map[string]string{
+		{
+			"input":  `select country_id from city where (country_id = 1);`,
+			"output": "select country_id from city where country_id = 1",
+		},
+		{
+			"input":  `select * from city where a = 1 and (country_id = 1);`,
+			"output": "select * from city where a = 1 and country_id = 1",
+		},
+		{
+			"input":  `select country_id from city where (country_id = 1) or country_id = 1 ;`,
+			"output": "select country_id from city where country_id = 1 or country_id = 1",
+		},
+		{
+			"input":  `select country_id from city where col = 1 or (country_id = 1) or country_id = 1 ;`,
+			"output": "select country_id from city where col = 1 or country_id = 1 or country_id = 1",
+		},
+	}
+	for _, sql := range testSQL {
+		rw := NewRewrite(sql["input"]).RewriteRmParenthesis()
+		if rw.NewSQL != sql["output"] {
+			t.Errorf("want: %s\ngot: %s", sql["output"], rw.NewSQL)
+		}
+	}
+}
+
+func TestListRewriteRules(t *testing.T) {
+	err := common.GoldenDiff(func() {
+		ListRewriteRules(RewriteRules)
+	}, t.Name(), update)
+	if err != nil {
+		t.Error(err)
+	}
+}
diff --git a/ast/testdata/TestListRewriteRules.golden b/ast/testdata/TestListRewriteRules.golden
new file mode 100644
index 00000000..68a821d5
--- /dev/null
+++ b/ast/testdata/TestListRewriteRules.golden
@@ -0,0 +1,272 @@
+# 重写规则
+
+[toc]
+
+## dml2select
+* **Description**:将数据库更新请求转换为只读查询请求,便于执行EXPLAIN
+
+* **Original**:
+
+```sql
+DELETE FROM film WHERE length > 100
+```
+
+* **Suggest**:
+
+```sql
+select * from film where length > 100
+```
+## star2columns
+* **Description**:为SELECT *补全表的列信息
+
+* **Original**:
+
+```sql
+SELECT * FROM film
+```
+
+* **Suggest**:
+
+```sql
+select film.film_id, film.title from film
+```
+## insertcolumns
+* **Description**:为INSERT补全表的列信息
+
+* **Original**:
+
+```sql
+insert into film values(1,2,3,4,5)
+```
+
+* **Suggest**:
+
+```sql
+insert into film(film_id, title, description, release_year, language_id) values (1, 2, 3, 4, 5)
+```
+## having
+* **Description**:将查询的HAVING子句改写为WHERE中的查询条件
+
+* **Original**:
+
+```sql
+SELECT state, COUNT(*) FROM Drivers GROUP BY state HAVING state IN ('GA', 'TX') ORDER BY state
+```
+
+* **Suggest**:
+
+```sql
+select state, COUNT(*) from Drivers where state in ('GA', 'TX') group by state order by state asc
+```
+## orderbynull
+* **Description**:如果GROUP BY语句不指定ORDER BY条件会导致无谓的排序产生,如果不需要排序建议添加ORDER BY NULL
+
+* **Original**:
+
+```sql
+SELECT sum(col1) FROM tbl GROUP BY col
+```
+
+* **Suggest**:
+
+```sql
+select sum(col1) from tbl group by col order by null
+```
+## unionall
+* **Description**:可以接受重复的时间,使用UNION ALL替代UNION以提高查询效率
+
+* **Original**:
+
+```sql
+select country_id from city union select country_id from country
+```
+
+* **Suggest**:
+
+```sql
+select country_id from city union all select country_id from country
+```
+## or2in
+* **Description**:将同一列不同条件的OR查询转写为IN查询
+
+* **Original**:
+
+```sql
+select country_id from city where col1 = 1 or (col2 = 1 or col2 = 2 ) or col1 = 3;
+```
+
+* **Suggest**:
+
+```sql
+select country_id from city where (col2 in (1, 2)) or col1 in (1, 3);
+```
+## dmlorderby
+* **Description**:删除DML更新操作中无意义的ORDER BY
+
+* **Original**:
+
+```sql
+DELETE FROM tbl WHERE col1=1 ORDER BY col
+```
+
+* **Suggest**:
+
+```sql
+delete from tbl where col1 = 1
+```
+## distinctstar
+* **Description**:DISTINCT *对有主键的表没有意义,可以将DISTINCT删掉
+
+* **Original**:
+
+```sql
+SELECT DISTINCT * FROM film;
+```
+
+* **Suggest**:
+
+```sql
+SELECT * FROM film
+```
+## standard
+* **Description**:SQL标准化,如:关键字转换为小写
+
+* **Original**:
+
+```sql
+SELECT sum(col1) FROM tbl GROUP BY 1;
+```
+
+* **Suggest**:
+
+```sql
+select sum(col1) from tbl group by 1
+```
+## mergealter
+* **Description**:合并同一张表的多条ALTER语句
+
+* **Original**:
+
+```sql
+ALTER TABLE t2 DROP COLUMN c;ALTER TABLE t2 DROP COLUMN d;
+```
+
+* **Suggest**:
+
+```sql
+ALTER TABLE t2 DROP COLUMN c, DROP COLUMN d;
+```
+## alwaystrue
+* **Description**:删除无用的恒真判断条件
+
+* **Original**:
+
+```sql
+SELECT count(col) FROM tbl where 'a'= 'a' or ('b' = 'b' and a = 'b');
+```
+
+* **Suggest**:
+
+```sql
+select count(col) from tbl where (a = 'b');
+```
+## countstar
+* **Description**:不建议使用COUNT(col)或COUNT(常量),建议改写为COUNT(*)
+
+* **Original**:
+
+```sql
+SELECT count(col) FROM tbl GROUP BY 1;
+```
+
+* **Suggest**:
+
+```sql
+SELECT count(*) FROM tbl GROUP BY 1;
+```
+## innodb
+* **Description**:建表时建议使用InnoDB引擎,非InnoDB引擎表自动转InnoDB
+
+* **Original**:
+
+```sql
+CREATE TABLE t1(id bigint(20) NOT NULL AUTO_INCREMENT);
+```
+
+* **Suggest**:
+
+```sql
+create table t1 (
+	id bigint(20) not null auto_increment
+) ENGINE=InnoDB;
+```
+## autoincrement
+* **Description**:将autoincrement初始化为1
+
+* **Original**:
+
+```sql
+CREATE TABLE t1(id bigint(20) NOT NULL AUTO_INCREMENT) ENGINE=InnoDB AUTO_INCREMENT=123802;
+```
+
+* **Suggest**:
+
+```sql
+create table t1(id bigint(20) not null auto_increment) ENGINE=InnoDB auto_increment=1;
+```
+## intwidth
+* **Description**:整型数据类型修改默认显示宽度
+
+* **Original**:
+
+```sql
+create table t1 (id int(20) not null auto_increment) ENGINE=InnoDB;
+```
+
+* **Suggest**:
+
+```sql
+create table t1 (id int(10) not null auto_increment) ENGINE=InnoDB;
+```
+## truncate
+* **Description**:不带WHERE条件的DELETE操作建议修改为TRUNCATE
+
+* **Original**:
+
+```sql
+DELETE FROM tbl
+```
+
+* **Suggest**:
+
+```sql
+truncate table tbl
+```
+## rmparenthesis
+* **Description**:去除没有意义的括号
+
+* **Original**:
+
+```sql
+select col from table where (col = 1);
+```
+
+* **Suggest**:
+
+```sql
+select col from table where col = 1;
+```
+## delimiter
+* **Description**:补全DELIMITER
+
+* **Original**:
+
+```sql
+use sakila
+```
+
+* **Suggest**:
+
+```sql
+use sakila;
+```
diff --git a/ast/testdata/TestPretty.golden b/ast/testdata/TestPretty.golden
new file mode 100644
index 00000000..b333aeae
--- /dev/null
+++ b/ast/testdata/TestPretty.golden
@@ -0,0 +1,1470 @@
+select sourcetable, if(f.lastcontent = ?, f.lastupdate, f.lastcontent) as lastactivity, f.totalcount as activity, type.class as type, (f.nodeoptions & ?) as nounsubscribe from node as f inner join contenttype as type on type.contenttypeid = f.contenttypeid inner join subscribed as sd on sd.did = f.nodeid and sd.userid = ? union all select f.name as title, f.userid as keyval, ? as sourcetable, ifnull(f.lastpost, f.joindate) as lastactivity, f.posts as activity, ? as type, ? as nounsubscribe from user as f inner join userlist as ul on ul.relationid = f.userid and ul.userid = ? where ul.type = ? and ul.aq = ? order by title limit ?
+
+SELECT  
+  sourcetable, IF( f. lastcontent  = ?, f. lastupdate, f. lastcontent) as  lastactivity, f. totalcount  as  activity, type. class  as  type, (f. nodeoptions  & ?) as  nounsubscribe  
+FROM  
+  node  as  f  
+  INNER JOIN  contenttype  as  type  on  type. contenttypeid  = f. contenttypeid  
+  INNER JOIN  subscribed  as  sd  on  sd. did  = f. nodeid  
+  AND  sd. userid  = ?  
+UNION ALL  
+SELECT  
+  f. name  as  title, f. userid  as  keyval, ?  as  sourcetable, IFNULL( f. lastpost, f. joindate) as  lastactivity, f. posts  as  activity, ?  as  type, ?  as  nounsubscribe  
+FROM  
+  USER  as  f  
+  INNER JOIN  userlist  as  ul  on  ul. relationid  = f. userid  
+  AND  ul. userid  = ?  
+WHERE  
+  ul. type  = ?  
+  AND  ul. aq  = ?  
+ORDER BY  
+  title  
+LIMIT  
+  ?
+administrator command: Init DB
+administrator  command: Init  DB
+CALL foo(1, 2, 3)
+CALL  foo( 1, 2, 3)
+### Channels ###
+					SELECT sourcetable, IF(f.lastcontent = 0, f.lastupdate, f.lastcontent) AS lastactivity,
+					f.totalcount AS activity, type.class AS type,
+					(f.nodeoptions & 512) AS noUnsubscribe
+					FROM node AS f
+					INNER JOIN contenttype AS type ON type.contenttypeid = f.contenttypeid 
+
+					INNER JOIN subscribed AS sd ON sd.did = f.nodeid AND sd.userid = 15965
+ UNION  ALL 
+
+					### Users ###
+					SELECT f.name AS title, f.userid AS keyval, 'user' AS sourcetable, IFNULL(f.lastpost, f.joindate) AS lastactivity,
+					f.posts as activity, 'Member' AS type,
+					0 AS noUnsubscribe
+					FROM user AS f
+					INNER JOIN userlist AS ul ON ul.relationid = f.userid AND ul.userid = 15965
+					WHERE ul.type = 'f' AND ul.aq = 'yes'
+ ORDER BY title ASC LIMIT 100
+### Channels ###
+SELECT  
+  sourcetable, IF( f. lastcontent  = 0, f. lastupdate, f. lastcontent) AS  lastactivity, f. totalcount  AS  activity, type. class  AS  type, (f. nodeoptions  & 512) AS  noUnsubscribe
+ 
+FROM  
+  node  AS  f
+ 
+  INNER JOIN  contenttype  AS  type  ON  type. contenttypeid  = f. contenttypeid  
+  INNER JOIN  subscribed  AS  sd  ON  sd. did  = f. nodeid  
+  AND  sd. userid  = 15965
+ 
+UNION  
+  ALL  ### Users ###
+SELECT  
+  f. name  AS  title, f. userid  AS  keyval, 'user' AS  sourcetable, IFNULL( f. lastpost, f. joindate) AS  lastactivity, f. posts  as  activity, 'Member' AS  type, 0  AS  noUnsubscribe
+ 
+FROM  
+  USER  AS  f
+ 
+  INNER JOIN  userlist  AS  ul  ON  ul. relationid  = f. userid  
+  AND  ul. userid  = 15965
+ 
+WHERE  
+  ul. type  = 'f' 
+  AND  ul. aq  = 'yes' 
+ORDER BY  
+  title  ASC  
+LIMIT  
+  100
+CREATE DATABASE org235_percona345 COLLATE 'utf8_general_ci'
+CREATE  DATABASE  org235_percona345  COLLATE  'utf8_general_ci'
+insert into abtemp.coxed select foo.bar from foo
+INSERT  into  abtemp. coxed  
+SELECT  
+  foo. bar  
+FROM  
+  foo
+insert into foo(a, b, c) value(2, 4, 5)
+INSERT  into  foo( a, b, c) value( 2, 4, 5)
+insert into foo(a, b, c) values(2, 4, 5)
+INSERT  into  foo( a, b, c) 
+VALUES( 
+  2, 4, 5)
+insert into foo(a, b, c) values(2, 4, 5) , (2,4,5)
+INSERT  into  foo( a, b, c) 
+VALUES( 
+  2, 4, 5), 
+  (2, 4, 5)
+insert into foo values (1, '(2)', 'This is a trick: ). More values.', 4)
+INSERT  into  foo  
+VALUES  
+  (1, '(2)', 
+  'This is a trick: ). More values.', 
+  4)
+insert into tb values (1)
+INSERT  into  tb  
+VALUES  
+  (1)
+INSERT INTO t (ts) VALUES ('()', '\(', '\)')
+INSERT  INTO  t  (ts) 
+VALUES  
+  (
+    '()', '\(', '\)')
+INSERT INTO t (ts) VALUES (NOW())
+INSERT  INTO  t  (ts) 
+VALUES  
+  (
+    NOW()
+  )
+INSERT INTO t () VALUES ()
+INSERT  INTO  t  (
+) 
+  VALUES  
+    (
+)
+insert into t values (1), (2), (3)
+
+	on duplicate key update query_count=1
+INSERT  into  t  
+VALUES  
+  (1), 
+  (2), 
+  (3) on  duplicate  key  
+UPDATE  
+  query_count= 1
+insert into t values (1) on duplicate key update query_count=COALESCE(query_count, 0) + VALUES(query_count)
+INSERT  into  t  
+VALUES  
+  (1) on  duplicate  key  
+UPDATE  
+  query_count= COALESCE( query_count, 0) + 
+VALUES( 
+  query_count)
+LOAD DATA INFILE '/tmp/foo.txt' INTO db.tbl
+LOAD  DATA  INFILE  '/tmp/foo.txt' INTO  db. tbl
+select 0e0, +6e-30, -6.00 from foo where a = 5.5 or b=0.5 or c=.5
+
+SELECT  
+  0e0, + 6e- 30, - 6.00  
+FROM  
+  foo  
+WHERE  
+  a  = 5.5  
+  OR  b= 0.5  
+  OR  c=.5
+select 0x0, x'123', 0b1010, b'10101' from foo
+
+SELECT  
+  0x0, x' 123', 
+  0b1010, b' 10101' 
+FROM  
+  foo
+select 123_foo from 123_foo
+
+SELECT  
+  123_foo  
+FROM  
+  123_foo
+select 123foo from 123foo
+
+SELECT  
+  123foo  
+FROM  
+  123foo
+SELECT 	1 AS one FROM calls USE INDEX(index_name)
+
+SELECT  
+  1  AS  one  
+FROM  
+  calls  USE  INDEX( index_name)
+SELECT /*!40001 SQL_NO_CACHE */ * FROM `film`
+
+SELECT  
+  */ * 
+FROM  
+  `film`
+SELECT 'a' 'b' 'c' 'd' FROM kamil
+
+SELECT  
+  'a' 'b' 'c' 'd' 
+FROM  
+  kamil
+SELECT BENCHMARK(100000000, pow(rand(), rand())), 1 FROM `-hj-7d6-shdj5-7jd-kf-g988h-`.`-aaahj-7d6-shdj5-7&^%$jd-kf-g988h-9+4-5*6ab-`
+
+SELECT  
+  BENCHMARK( 100000000, POW( RAND(
+), 
+RAND(
+)
+)
+), 
+1  
+FROM  
+  `-hj-7d6-shdj5-7jd-kf-g988h-`.`-aaahj-7d6-shdj5-7&^%$jd-kf-g988h-9+4-5*6ab-`
+SELECT c FROM org235.t WHERE id=0xdeadbeaf
+
+SELECT  
+  c  
+FROM  
+  org235. t  
+WHERE  
+  id= 0xdeadbeaf
+select c from t where i=1 order by c asc
+
+SELECT  
+  c  
+FROM  
+  t  
+WHERE  
+  i= 1  
+ORDER BY  
+  c  asc
+SELECT c FROM t WHERE id=0xdeadbeaf
+
+SELECT  
+  c  
+FROM  
+  t  
+WHERE  
+  id= 0xdeadbeaf
+SELECT c FROM t WHERE id=1
+
+SELECT  
+  c  
+FROM  
+  t  
+WHERE  
+  id= 1
+select `col` from `table-1` where `id` = 5
+
+SELECT  
+  `col` 
+FROM  
+  `table-1` 
+WHERE  
+  `id` = 5
+SELECT `db`.*, (CASE WHEN (`date_start` <=  '2014-09-10 09:17:59' AND `date_end` >=  '2014-09-10 09:17:59') THEN 'open' WHEN (`date_start` >  '2014-09-10 09:17:59' AND `date_end` >  '2014-09-10 09:17:59') THEN 'tbd' ELSE 'none' END) AS `status` FROM `foo` AS `db` WHERE (a_b in ('1', '10101'))
+
+SELECT  
+  `db`.*, 
+  (CASE  WHEN  (`date_start` <= '2014-09-10 09:17:59' 
+  AND  `date_end` >= '2014-09-10 09:17:59'
+) THEN  'open' WHEN  (`date_start` > '2014-09-10 09:17:59' 
+AND  `date_end` > '2014-09-10 09:17:59'
+) THEN  'tbd' ELSE  'none' END) AS  `status` 
+FROM  
+  `foo` AS  `db` 
+WHERE  
+  (a_b  in  (
+    '1', '10101')
+  )
+select field from `-master-db-1`.`-table-1-` order by id, ?;
+
+SELECT  
+  FIELD  
+FROM  
+  `-master-db-1`.`-table-1-` 
+ORDER BY  
+  id, ?;
+select   foo
+
+SELECT  
+  foo
+select foo_1 from foo_2_3
+
+SELECT  
+  foo_1  
+FROM  
+  foo_2_3
+select foo -- bar
+
+
+SELECT  
+  foo  -- bar
+select foo-- bar
+,foo
+
+SELECT  
+  foo- - bar
+, 
+  foo
+select '\\' from foo
+
+SELECT  
+  '\\' 
+FROM  
+  foo
+select * from foo limit 5
+
+SELECT  
+  * 
+FROM  
+  foo  
+LIMIT  
+  5
+select * from foo limit 5, 10
+
+SELECT  
+  * 
+FROM  
+  foo  
+LIMIT  
+  5, 10
+select * from foo limit 5 offset 10
+
+SELECT  
+  * 
+FROM  
+  foo  
+LIMIT  
+  5  offset  10
+SELECT * from foo where a = 5
+
+SELECT  
+  * 
+FROM  
+  foo  
+WHERE  
+  a  = 5
+select * from foo where a in (5) and b in (5, 8,9 ,9 , 10)
+
+SELECT  
+  * 
+FROM  
+  foo  
+WHERE  
+  a  in  (5) 
+  AND  b  in  (5, 8, 9, 
+  9, 
+  10)
+SELECT '' '' '' FROM kamil
+
+SELECT  
+  '' '' '' 
+FROM  
+  kamil
+ select  * from
+foo where a = 5
+
+SELECT  
+  * 
+FROM  
+  foo  
+WHERE  
+  a  = 5
+SELECT * FROM prices.rt_5min where id=1
+
+SELECT  
+  * 
+FROM  
+  prices. rt_5min  
+WHERE  
+  id= 1
+SELECT * FROM table WHERE field = 'value' /*arbitrary/31*/ 
+
+SELECT  
+  * 
+FROM  
+  table  
+WHERE  
+  FIELD  = 'value' */
+SELECT * FROM table WHERE field = 'value' /*arbitrary31*/ 
+
+SELECT  
+  * 
+FROM  
+  table  
+WHERE  
+  FIELD  = 'value' */
+SELECT *    FROM t WHERE 1=1 AND id=1
+
+SELECT  
+  * 
+FROM  
+  t  
+WHERE  
+  1= 1  
+  AND  id= 1
+select * from t where (base.nid IN  ('1412', '1410', '1411'))
+
+SELECT  
+  * 
+FROM  
+  t  
+WHERE  
+  (base. nid  IN  (
+    '1412', '1410', '1411')
+  )
+select * from t where i=1      order            by
+             a,  b          ASC, d    DESC,
+
+                                    e asc
+
+SELECT  
+  * 
+FROM  
+  t  
+WHERE  
+  i= 1  order  by
+ a, b  ASC, d  DESC, e  asc
+select * from t where i=1 order by a, b ASC, d DESC, e asc
+
+SELECT  
+  * 
+FROM  
+  t  
+WHERE  
+  i= 1  
+ORDER BY  
+  a, b  ASC, d  DESC, e  asc
+select 'hello'
+
+
+SELECT  
+  'hello'
+select 'hello', '
+hello
+', "hello", '\'' from foo
+
+SELECT  
+  'hello', 
+  '
+hello
+', 
+  "hello", 
+  '\'' 
+FROM  
+  foo
+SELECT ID, name, parent, type FROM posts WHERE _name IN ('perf','caching') AND (type = 'page' OR type = 'attachment')
+
+SELECT  
+  ID, name, parent, type  
+FROM  
+  posts  
+WHERE  
+  _name  IN  (
+    'perf', 'caching') 
+    AND  (type  = 'page' 
+    OR  type  = 'attachment'
+  )
+SELECT name, value FROM variable
+
+SELECT  
+  name, value  
+FROM  
+  variable
+select 
+-- bar
+ foo
+
+SELECT  
+  -- bar
+  foo
+select null, 5.001, 5001. from foo
+
+SELECT  
+  null, 5.001, 5001. 
+FROM  
+  foo
+select sleep(2) from test.n
+
+SELECT  
+  SLEEP( 2) 
+FROM  
+  test. n
+SELECT t FROM field WHERE  (entity_type = 'node') AND (entity_id IN  ('609')) AND (language IN  ('und')) AND (deleted = '0') ORDER BY delta ASC
+
+SELECT  
+  t  
+FROM  
+  FIELD  
+WHERE  
+  (
+    entity_type  = 'node') 
+    AND  (entity_id  IN  (
+      '609')
+    ) 
+    AND  (language  IN  (
+      'und')
+    ) 
+    AND  (
+      deleted  = '0') 
+      ORDER BY  
+        delta  ASC
+select  t.table_schema,t.table_name,engine  from information_schema.tables t  inner join information_schema.columns c  on t.table_schema=c.table_schema and t.table_name=c.table_name group by t.table_schema,t.table_name having  sum(if(column_key in ('PRI','UNI'),1,0))=0
+
+SELECT  
+  t. table_schema, t. table_name, engine  
+FROM  
+  information_schema. tables  t  
+  INNER JOIN  information_schema. columns  c  on  t. table_schema= c. table_schema  
+  AND  t. table_name= c. table_name  
+GROUP BY  
+  t. table_schema, t. table_name  
+HAVING  
+  SUM( IF( column_key  in  (
+    'PRI', 'UNI'), 
+    1, 0)
+  )= 0
+/* -- S++ SU ABORTABLE -- spd_user: rspadim */SELECT SQL_SMALL_RESULT SQL_CACHE DISTINCT centro_atividade FROM est_dia WHERE unidade_id=1001 AND item_id=67 AND item_id_red=573
+*/ 
+SELECT  
+  SQL_SMALL_RESULT  SQL_CACHE  DISTINCT  centro_atividade  
+FROM  
+  est_dia  
+WHERE  
+  unidade_id= 1001  
+  AND  item_id= 67  
+  AND  item_id_red= 573
+UPDATE groups_search SET  charter = '   -------3\'\' XXXXXXXXX.\n    \n    -----------------------------------------------------', show_in_list = 'Y' WHERE group_id='aaaaaaaa'
+
+UPDATE  
+  groups_search  
+SET  
+  charter  = '   -------3\'\' XXXXXXXXX.\n    \n    -----------------------------------------------------', 
+  show_in_list  = 'Y' 
+WHERE  
+  group_id= 'aaaaaaaa'
+use `foo`
+use  `foo`
+select sourcetable, if(f.lastcontent = ?, f.lastupdate, f.lastcontent) as lastactivity, f.totalcount as activity, type.class as type, (f.nodeoptions & ?) as nounsubscribe from node as f inner join contenttype as type on type.contenttypeid = f.contenttypeid inner join subscribed as sd on sd.did = f.nodeid and sd.userid = ? union all select f.name as title, f.userid as keyval, ? as sourcetable, ifnull(f.lastpost, f.joindate) as lastactivity, f.posts as activity, ? as type, ? as nounsubscribe from user as f inner join userlist as ul on ul.relationid = f.userid and ul.userid = ? where ul.type = ? and ul.aq = ? order by title limit ?
+
+SELECT  
+  sourcetable, IF( f. lastcontent  = ?, f. lastupdate, f. lastcontent) as  lastactivity, f. totalcount  as  activity, type. class  as  type, (f. nodeoptions  & ?) as  nounsubscribe  
+FROM  
+  node  as  f  
+  INNER JOIN  contenttype  as  type  on  type. contenttypeid  = f. contenttypeid  
+  INNER JOIN  subscribed  as  sd  on  sd. did  = f. nodeid  
+  AND  sd. userid  = ?  
+UNION ALL  
+SELECT  
+  f. name  as  title, f. userid  as  keyval, ?  as  sourcetable, IFNULL( f. lastpost, f. joindate) as  lastactivity, f. posts  as  activity, ?  as  type, ?  as  nounsubscribe  
+FROM  
+  USER  as  f  
+  INNER JOIN  userlist  as  ul  on  ul. relationid  = f. userid  
+  AND  ul. userid  = ?  
+WHERE  
+  ul. type  = ?  
+  AND  ul. aq  = ?  
+ORDER BY  
+  title  
+LIMIT  
+  ?
+CREATE INDEX part_of_name ON customer (name(10));
+CREATE  INDEX  part_of_name  ON  customer  (
+  name( 10));
+alter table `sakila`.`t1` add index `idx_col`(`col`)
+
+ALTER TABLE  
+  `sakila`.`t1` 
+ADD  
+  index  `idx_col` (
+    `col`)
+alter table `sakila`.`t1` add UNIQUE index `idx_col`(`col`)
+
+ALTER TABLE  
+  `sakila`.`t1` 
+ADD  
+  UNIQUE  index  `idx_col` (
+    `col`)
+alter table `sakila`.`t1` add index `idx_ID`(`ID`)
+
+ALTER TABLE  
+  `sakila`.`t1` 
+ADD  
+  index  `idx_ID` (
+    `ID`)
+ALTER TABLE t2 DROP COLUMN c, DROP COLUMN d;
+
+ALTER TABLE  
+  t2  
+DROP  
+  COLUMN  c, 
+DROP  
+  COLUMN  d;
+ALTER TABLE T2 ADD COLUMN C int;
+
+ALTER TABLE  
+  T2  
+ADD  
+  COLUMN  C  int;
+ALTER TABLE T2 ADD COLUMN D int FIRST;
+
+ALTER TABLE  
+  T2  
+ADD  
+  COLUMN  D  int  FIRST;
+ALTER TABLE T2 ADD COLUMN E int AFTER D;
+
+ALTER TABLE  
+  T2  
+ADD  
+  COLUMN  E  int  
+AFTER  
+  D;
+ALTER TABLE t1 RENAME COLUMN a TO b
+
+ALTER TABLE  
+  t1  RENAME  COLUMN  a  TO  b
+ALTER TABLE t1 RENAME INDEX idx_a TO idx_b
+
+ALTER TABLE  
+  t1  RENAME  INDEX  idx_a  TO  idx_b
+ALTER TABLE t1 RENAME KEY idx_a TO idx_b
+
+ALTER TABLE  
+  t1  RENAME  KEY  idx_a  TO  idx_b
+ALTER TABLE db.old_table RENAME new_table;
+
+ALTER TABLE  
+  db. old_table  RENAME  new_table;
+ALTER TABLE old_table RENAME TO new_table;
+
+ALTER TABLE  
+  old_table  RENAME  TO  new_table;
+ALTER TABLE old_table RENAME AS new_table;
+
+ALTER TABLE  
+  old_table  RENAME  AS  new_table;
+ALTER TABLE t1 MODIFY col1 BIGINT UNSIGNED DEFAULT 1 COMMENT 'my column';
+
+ALTER TABLE  
+  t1  MODIFY  col1  BIGINT  UNSIGNED  DEFAULT  1  COMMENT  'my column';
+ALTER TABLE t1 CHANGE b a INT NOT NULL;
+
+ALTER TABLE  
+  t1  CHANGE  b  a  INT  NOT  NULL;
+SELECT * FROM film WHERE length = 86;
+
+SELECT  
+  * 
+FROM  
+  film  
+WHERE  
+  LENGTH  = 86;
+SELECT * FROM film WHERE length IS NULL;
+
+SELECT  
+  * 
+FROM  
+  film  
+WHERE  
+  LENGTH  IS  NULL;
+SELECT * FROM film HAVING title = 'abc';
+
+SELECT  
+  * 
+FROM  
+  film  
+HAVING  
+  title  = 'abc';
+SELECT * FROM sakila.film WHERE length >= 60;
+
+SELECT  
+  * 
+FROM  
+  sakila. film  
+WHERE  
+  LENGTH  >= 60;
+SELECT * FROM sakila.film WHERE length >= '60';
+
+SELECT  
+  * 
+FROM  
+  sakila. film  
+WHERE  
+  LENGTH  >= '60';
+SELECT * FROM film WHERE length BETWEEN 60 AND 84;
+
+SELECT  
+  * 
+FROM  
+  film  
+WHERE  
+  LENGTH  BETWEEN  60  
+  AND  84;
+SELECT * FROM film WHERE title LIKE 'AIR%';
+
+SELECT  
+  * 
+FROM  
+  film  
+WHERE  
+  title  LIKE  'AIR%';
+SELECT * FROM film WHERE title IS NOT NULL;
+
+SELECT  
+  * 
+FROM  
+  film  
+WHERE  
+  title  IS  NOT  NULL;
+SELECT * FROM film WHERE length = 114 and title = 'ALABAMA DEVIL';
+
+SELECT  
+  * 
+FROM  
+  film  
+WHERE  
+  LENGTH  = 114  
+  AND  title  = 'ALABAMA DEVIL';
+SELECT * FROM film WHERE length > 100 and title = 'ALABAMA DEVIL';
+
+SELECT  
+  * 
+FROM  
+  film  
+WHERE  
+  LENGTH  > 100  
+  AND  title  = 'ALABAMA DEVIL';
+SELECT * FROM film WHERE length > 100 and language_id < 10 and title = 'xyz';
+
+SELECT  
+  * 
+FROM  
+  film  
+WHERE  
+  LENGTH  > 100  
+  AND  language_id  < 10  
+  AND  title  = 'xyz';
+SELECT * FROM film WHERE length > 100 and language_id < 10;
+
+SELECT  
+  * 
+FROM  
+  film  
+WHERE  
+  LENGTH  > 100  
+  AND  language_id  < 10;
+SELECT release_year, sum(length) FROM film WHERE length = 123 AND language_id = 1 GROUP BY release_year;
+
+SELECT  
+  release_year, SUM( LENGTH) 
+FROM  
+  film  
+WHERE  
+  LENGTH  = 123  
+  AND  language_id  = 1  
+GROUP BY  
+  release_year;
+SELECT release_year, sum(length) FROM film WHERE length >= 123 GROUP BY release_year;
+
+SELECT  
+  release_year, SUM( LENGTH) 
+FROM  
+  film  
+WHERE  
+  LENGTH  >= 123  
+GROUP BY  
+  release_year;
+SELECT release_year, language_id, sum(length) FROM film GROUP BY release_year, language_id;
+
+SELECT  
+  release_year, language_id, SUM( LENGTH) 
+FROM  
+  film  
+GROUP BY  
+  release_year, language_id;
+SELECT release_year, sum(length) FROM film WHERE length = 123 GROUP BY release_year,(length+language_id);
+
+SELECT  
+  release_year, SUM( LENGTH) 
+FROM  
+  film  
+WHERE  
+  LENGTH  = 123  
+GROUP BY  
+  release_year, (LENGTH+ language_id);
+SELECT release_year, sum(film_id) FROM film GROUP BY release_year;
+
+SELECT  
+  release_year, SUM( film_id) 
+FROM  
+  film  
+GROUP BY  
+  release_year;
+SELECT * FROM address GROUP BY address,district;
+
+SELECT  
+  * 
+FROM  
+  address  
+GROUP BY  
+  address, district;
+SELECT title FROM film WHERE ABS(language_id) = 3 GROUP BY title;
+
+SELECT  
+  title  
+FROM  
+  film  
+WHERE  
+  ABS( language_id) = 3  
+GROUP BY  
+  title;
+SELECT language_id FROM film WHERE length = 123 GROUP BY release_year ORDER BY language_id;
+
+SELECT  
+  language_id  
+FROM  
+  film  
+WHERE  
+  LENGTH  = 123  
+GROUP BY  
+  release_year  
+ORDER BY  
+  language_id;
+SELECT release_year FROM film WHERE length = 123 GROUP BY release_year ORDER BY release_year;
+
+SELECT  
+  release_year  
+FROM  
+  film  
+WHERE  
+  LENGTH  = 123  
+GROUP BY  
+  release_year  
+ORDER BY  
+  release_year;
+SELECT * FROM film WHERE length = 123 ORDER BY release_year ASC, language_id DESC;
+
+SELECT  
+  * 
+FROM  
+  film  
+WHERE  
+  LENGTH  = 123  
+ORDER BY  
+  release_year  ASC, language_id  DESC;
+SELECT release_year FROM film WHERE length = 123 GROUP BY release_year ORDER BY release_year LIMIT 10;
+
+SELECT  
+  release_year  
+FROM  
+  film  
+WHERE  
+  LENGTH  = 123  
+GROUP BY  
+  release_year  
+ORDER BY  
+  release_year  
+LIMIT  
+  10;
+SELECT * FROM film WHERE length = 123 ORDER BY release_year LIMIT 10;
+
+SELECT  
+  * 
+FROM  
+  film  
+WHERE  
+  LENGTH  = 123  
+ORDER BY  
+  release_year  
+LIMIT  
+  10;
+SELECT * FROM film ORDER BY release_year LIMIT 10;
+
+SELECT  
+  * 
+FROM  
+  film  
+ORDER BY  
+  release_year  
+LIMIT  
+  10;
+SELECT * FROM film WHERE length > 100 ORDER BY length LIMIT 10;
+
+SELECT  
+  * 
+FROM  
+  film  
+WHERE  
+  LENGTH  > 100  
+ORDER BY  
+  LENGTH  
+LIMIT  
+  10;
+SELECT * FROM film WHERE length < 100 ORDER BY length LIMIT 10;
+
+SELECT  
+  * 
+FROM  
+  film  
+WHERE  
+  LENGTH  < 100  
+ORDER BY  
+  LENGTH  
+LIMIT  
+  10;
+SELECT * FROM customer WHERE address_id in (224,510) ORDER BY last_name;
+
+SELECT  
+  * 
+FROM  
+  customer  
+WHERE  
+  address_id  in  (224, 510) 
+ORDER BY  
+  last_name;
+SELECT * FROM film WHERE release_year = 2016 AND length != 1 ORDER BY title;
+
+SELECT  
+  * 
+FROM  
+  film  
+WHERE  
+  release_year  = 2016  
+  AND  LENGTH  != 1  
+ORDER BY  
+  title;
+SELECT title FROM film WHERE release_year = 1995;
+
+SELECT  
+  title  
+FROM  
+  film  
+WHERE  
+  release_year  = 1995;
+SELECT title, replacement_cost FROM film WHERE language_id = 5 AND length = 70;
+
+SELECT  
+  title, replacement_cost  
+FROM  
+  film  
+WHERE  
+  language_id  = 5  
+  AND  LENGTH  = 70;
+SELECT title FROM film WHERE language_id > 5 AND length > 70;
+
+SELECT  
+  title  
+FROM  
+  film  
+WHERE  
+  language_id  > 5  
+  AND  LENGTH  > 70;
+SELECT * FROM film WHERE length = 100 and title = 'xyz' ORDER BY release_year;
+
+SELECT  
+  * 
+FROM  
+  film  
+WHERE  
+  LENGTH  = 100  
+  AND  title  = 'xyz' 
+ORDER BY  
+  release_year;
+SELECT * FROM film WHERE length > 100 and title = 'xyz' ORDER BY release_year;
+
+SELECT  
+  * 
+FROM  
+  film  
+WHERE  
+  LENGTH  > 100  
+  AND  title  = 'xyz' 
+ORDER BY  
+  release_year;
+SELECT * FROM film WHERE length > 100 ORDER BY release_year;
+
+SELECT  
+  * 
+FROM  
+  film  
+WHERE  
+  LENGTH  > 100  
+ORDER BY  
+  release_year;
+SELECT * FROM city a INNER JOIN country b ON a.country_id=b.country_id;
+
+SELECT  
+  * 
+FROM  
+  city  a  
+  INNER JOIN  country  b  ON  a. country_id= b. country_id;
+SELECT * FROM city a LEFT JOIN country b ON a.country_id=b.country_id;
+
+SELECT  
+  * 
+FROM  
+  city  a  
+  LEFT JOIN  country  b  ON  a. country_id= b. country_id;
+SELECT * FROM city a RIGHT JOIN country b ON a.country_id=b.country_id;
+
+SELECT  
+  * 
+FROM  
+  city  a  
+  RIGHT JOIN  country  b  ON  a. country_id= b. country_id;
+SELECT * FROM city a LEFT JOIN country b ON a.country_id=b.country_id WHERE b.last_update IS NULL;
+
+SELECT  
+  * 
+FROM  
+  city  a  
+  LEFT JOIN  country  b  ON  a. country_id= b. country_id  
+WHERE  
+  b. last_update  IS  NULL;
+SELECT * FROM city a RIGHT JOIN country b ON a.country_id=b.country_id WHERE a.last_update IS NULL;
+
+SELECT  
+  * 
+FROM  
+  city  a  
+  RIGHT JOIN  country  b  ON  a. country_id= b. country_id  
+WHERE  
+  a. last_update  IS  NULL;
+SELECT * FROM city a LEFT JOIN country b ON a.country_id=b.country_id UNION SELECT * FROM city a RIGHT JOIN country b ON a.country_id=b.country_id;
+
+SELECT  
+  * 
+FROM  
+  city  a  
+  LEFT JOIN  country  b  ON  a. country_id= b. country_id  
+UNION  
+SELECT  
+  * 
+FROM  
+  city  a  
+  RIGHT JOIN  country  b  ON  a. country_id= b. country_id;
+SELECT * FROM city a RIGHT JOIN country b ON a.country_id=b.country_id WHERE a.last_update IS NULL UNION SELECT * FROM city a LEFT JOIN country b ON a.country_id=b.country_id WHERE b.last_update IS NULL;
+
+SELECT  
+  * 
+FROM  
+  city  a  
+  RIGHT JOIN  country  b  ON  a. country_id= b. country_id  
+WHERE  
+  a. last_update  IS  NULL  
+UNION  
+SELECT  
+  * 
+FROM  
+  city  a  
+  LEFT JOIN  country  b  ON  a. country_id= b. country_id  
+WHERE  
+  b. last_update  IS  NULL;
+SELECT country_id, last_update FROM city NATURAL JOIN country;
+
+SELECT  
+  country_id, last_update  
+FROM  
+  city  NATURAL  
+  JOIN  country;
+SELECT country_id, last_update FROM city NATURAL LEFT JOIN country;
+
+SELECT  
+  country_id, last_update  
+FROM  
+  city  NATURAL  
+  LEFT JOIN  country;
+SELECT country_id, last_update FROM city NATURAL RIGHT JOIN country;
+
+SELECT  
+  country_id, last_update  
+FROM  
+  city  NATURAL  
+  RIGHT JOIN  country;
+SELECT a.country_id, a.last_update FROM city a STRAIGHT_JOIN country b ON a.country_id=b.country_id;
+
+SELECT  
+  a. country_id, a. last_update  
+FROM  
+  city  a  STRAIGHT_JOIN  country  b  ON  a. country_id= b. country_id;
+SELECT d.deptno,d.dname,d.loc FROM scott.dept d WHERE d.deptno IN  (SELECT e.deptno FROM scott.emp e);
+
+SELECT  
+  d. deptno, d. dname, d. loc  
+FROM  
+  scott. dept  d  
+WHERE  
+  d. deptno  IN  (
+SELECT  
+  e. deptno  
+FROM  
+  scott. emp  e);
+SELECT visitor_id, url FROM (SELECT id FROM log WHERE ip="123.45.67.89" order by tsdesc limit 50, 10) I JOIN log ON (I.id=log.id) JOIN url ON (url.id=log.url_id) order by TS desc;
+
+SELECT  
+  visitor_id, url  
+FROM  
+  (
+SELECT  
+  id  
+FROM  
+  LOG  
+WHERE  
+  ip= "123.45.67.89" 
+ORDER BY  
+  tsdesc  
+LIMIT  
+  50, 10) I  
+  JOIN  LOG  ON  (I. id= LOG. id) 
+  JOIN  url  ON  (url. id= LOG. url_id) 
+ORDER BY  
+  TS  desc;
+DELETE city, country FROM city INNER JOIN country using (country_id) WHERE city.city_id = 1;
+DELETE  city, country  
+FROM  
+  city  
+  INNER JOIN  country  using  (country_id) 
+WHERE  
+  city. city_id  = 1;
+DELETE city FROM city LEFT JOIN country ON city.country_id = country.country_id WHERE country.country IS NULL;
+DELETE  city  
+FROM  
+  city  
+  LEFT JOIN  country  ON  city. country_id  = country. country_id  
+WHERE  
+  country. country  IS  NULL;
+DELETE a1, a2 FROM city AS a1 INNER JOIN country AS a2 WHERE a1.country_id=a2.country_id;
+DELETE  a1, a2  
+FROM  
+  city  AS  a1  
+  INNER JOIN  country  AS  a2  
+WHERE  
+  a1. country_id= a2. country_id;
+DELETE FROM a1, a2 USING city AS a1 INNER JOIN country AS a2 WHERE a1.country_id=a2.country_id;
+
+DELETE FROM  
+  a1, a2  USING  city  AS  a1  
+  INNER JOIN  country  AS  a2  
+WHERE  
+  a1. country_id= a2. country_id;
+DELETE FROM film WHERE length > 100;
+
+DELETE FROM  
+  film  
+WHERE  
+  LENGTH  > 100;
+UPDATE city INNER JOIN country USING(country_id) SET city.city = 'Abha', city.last_update = '2006-02-15 04:45:25', country.country = 'Afghanistan' WHERE city.city_id=10;
+
+UPDATE  
+  city  
+  INNER JOIN  country  USING( country_id) 
+SET  
+  city. city  = 'Abha', 
+  city. last_update  = '2006-02-15 04:45:25', 
+  country. country  = 'Afghanistan' 
+WHERE  
+  city. city_id= 10;
+UPDATE city INNER JOIN country ON city.country_id = country.country_id INNER JOIN address ON city.city_id = address.city_id SET city.city = 'Abha', city.last_update = '2006-02-15 04:45:25', country.country = 'Afghanistan' WHERE city.city_id=10;
+
+UPDATE  
+  city  
+  INNER JOIN  country  ON  city. country_id  = country. country_id  
+  INNER JOIN  address  ON  city. city_id  = address. city_id  
+SET  
+  city. city  = 'Abha', 
+  city. last_update  = '2006-02-15 04:45:25', 
+  country. country  = 'Afghanistan' 
+WHERE  
+  city. city_id= 10;
+UPDATE city, country SET city.city = 'Abha', city.last_update = '2006-02-15 04:45:25', country.country = 'Afghanistan' WHERE city.country_id = country.country_id AND city.city_id=10;
+
+UPDATE  
+  city, country  
+SET  
+  city. city  = 'Abha', 
+  city. last_update  = '2006-02-15 04:45:25', 
+  country. country  = 'Afghanistan' 
+WHERE  
+  city. country_id  = country. country_id  
+  AND  city. city_id= 10;
+UPDATE film SET length = 10 WHERE language_id = 20;
+
+UPDATE  
+  film  
+SET  
+  LENGTH  = 10  
+WHERE  
+  language_id  = 20;
+INSERT INTO city (country_id) SELECT country_id FROM country;
+INSERT  INTO  city  (country_id) 
+SELECT  
+  country_id  
+FROM  
+  country;
+INSERT INTO city (country_id) VALUES (1),(2),(3);
+INSERT  INTO  city  (country_id) 
+VALUES  
+  (1), 
+  (2), 
+  (3);
+INSERT INTO city (country_id) VALUES (10);
+INSERT  INTO  city  (country_id) 
+VALUES  
+  (10);
+INSERT INTO city (country_id) SELECT 10 FROM DUAL;
+INSERT  INTO  city  (country_id) 
+SELECT  
+  10  
+FROM  
+  DUAL;
+REPLACE INTO city (country_id) SELECT country_id FROM country;
+REPLACE  INTO  city  (country_id) 
+SELECT  
+  country_id  
+FROM  
+  country;
+REPLACE INTO city (country_id) VALUES (1),(2),(3);
+REPLACE  INTO  city  (country_id) 
+VALUES  
+  (1), 
+  (2), 
+  (3);
+REPLACE INTO city (country_id) VALUES (10);
+REPLACE  INTO  city  (country_id) 
+VALUES  
+  (10);
+REPLACE INTO city (country_id) SELECT 10 FROM DUAL;
+REPLACE  INTO  city  (country_id) 
+SELECT  
+  10  
+FROM  
+  DUAL;
+SELECT film_id FROM ( SELECT film_id FROM ( SELECT film_id FROM ( SELECT film_id FROM ( SELECT film_id FROM ( SELECT film_id FROM ( SELECT film_id FROM ( SELECT film_id FROM ( SELECT film_id FROM ( SELECT film_id FROM ( SELECT film_id FROM ( SELECT film_id FROM ( SELECT film_id FROM ( SELECT film_id FROM ( SELECT film_id FROM ( SELECT film_id FROM ( SELECT film_id FROM  film ) film ) film ) film ) film ) film ) film ) film ) film ) film ) film ) film ) film ) film ) film ) film ) film;
+
+SELECT  
+  film_id  
+FROM  
+  (
+SELECT  
+  film_id  
+FROM  
+  (
+SELECT  
+  film_id  
+FROM  
+  (
+SELECT  
+  film_id  
+FROM  
+  (
+SELECT  
+  film_id  
+FROM  
+  (
+SELECT  
+  film_id  
+FROM  
+  (
+SELECT  
+  film_id  
+FROM  
+  (
+SELECT  
+  film_id  
+FROM  
+  (
+SELECT  
+  film_id  
+FROM  
+  (
+SELECT  
+  film_id  
+FROM  
+  (
+SELECT  
+  film_id  
+FROM  
+  (
+SELECT  
+  film_id  
+FROM  
+  (
+SELECT  
+  film_id  
+FROM  
+  (
+SELECT  
+  film_id  
+FROM  
+  (
+SELECT  
+  film_id  
+FROM  
+  (
+SELECT  
+  film_id  
+FROM  
+  (
+SELECT  
+  film_id  
+FROM  
+  film
+) film
+) film
+) film
+) film
+) film
+) film
+) film
+) film
+) film
+) film
+) film
+) film
+) film
+) film
+) film
+) film;
+SELECT * FROM film WHERE language_id = (SELECT language_id FROM language LIMIT 1);
+
+SELECT  
+  * 
+FROM  
+  film  
+WHERE  
+  language_id  = (
+SELECT  
+  language_id  
+FROM  
+  language  
+LIMIT  
+  1);
+SELECT * FROM city i left JOIN country o ON i.city_id=o.country_id union SELECT * FROM city i right JOIN country o ON i.city_id=o.country_id;
+
+SELECT  
+  * 
+FROM  
+  city  i  
+  LEFT JOIN  country  o  ON  i. city_id= o. country_id  
+UNION  
+SELECT  
+  * 
+FROM  
+  city  i  
+  RIGHT JOIN  country  o  ON  i. city_id= o. country_id;
+SELECT * FROM (SELECT * FROM actor WHERE last_update='2006-02-15 04:34:33' and last_name='CHASE') t WHERE last_update='2006-02-15 04:34:33' and last_name='CHASE' GROUP BY first_name;
+
+SELECT  
+  * 
+FROM  
+  (
+SELECT  
+  * 
+FROM  
+  actor  
+WHERE  
+  last_update= '2006-02-15 04:34:33' 
+  AND  last_name= 'CHASE'
+) t  
+WHERE  
+  last_update= '2006-02-15 04:34:33' 
+  AND  last_name= 'CHASE' 
+GROUP BY  
+  first_name;
+SELECT * FROM city i left JOIN country o ON i.city_id=o.country_id union SELECT * FROM city i right JOIN country o ON i.city_id=o.country_id;
+
+SELECT  
+  * 
+FROM  
+  city  i  
+  LEFT JOIN  country  o  ON  i. city_id= o. country_id  
+UNION  
+SELECT  
+  * 
+FROM  
+  city  i  
+  RIGHT JOIN  country  o  ON  i. city_id= o. country_id;
+SELECT * FROM city i left JOIN country o ON i.city_id=o.country_id WHERE o.country_id is null union SELECT * FROM city i right JOIN country o ON i.city_id=o.country_id WHERE i.city_id is null;
+
+SELECT  
+  * 
+FROM  
+  city  i  
+  LEFT JOIN  country  o  ON  i. city_id= o. country_id  
+WHERE  
+  o. country_id  is  null  
+UNION  
+SELECT  
+  * 
+FROM  
+  city  i  
+  RIGHT JOIN  country  o  ON  i. city_id= o. country_id  
+WHERE  
+  i. city_id  is  null;
+SELECT first_name,last_name,email FROM customer STRAIGHT_JOIN address ON customer.address_id=address.address_id;
+
+SELECT  
+  first_name, last_name, email  
+FROM  
+  customer  STRAIGHT_JOIN  address  ON  customer. address_id= address. address_id;
+SELECT ID,name FROM (SELECT address FROM customer_list WHERE SID=1 order by phone limit 50,10) a JOIN customer_list l ON (a.address=l.address) JOIN city c ON (c.city=l.city) order by phone desc;
+
+SELECT  
+  ID, name  
+FROM  
+  (
+SELECT  
+  address  
+FROM  
+  customer_list  
+WHERE  
+  SID= 1  
+ORDER BY  
+  phone  
+LIMIT  
+  50, 10) a  
+  JOIN  customer_list  l  ON  (a. address= l. address) 
+  JOIN  city  c  ON  (c. city= l. city) 
+ORDER BY  
+  phone  desc;
+SELECT * FROM film WHERE date(last_update)='2006-02-15';
+
+SELECT  
+  * 
+FROM  
+  film  
+WHERE  
+  DATE( last_update) = '2006-02-15';
+SELECT last_update FROM film GROUP BY date(last_update);
+
+SELECT  
+  last_update  
+FROM  
+  film  
+GROUP BY  
+  DATE( last_update);
+SELECT last_update FROM film order by date(last_update);
+
+SELECT  
+  last_update  
+FROM  
+  film  
+ORDER BY  
+  DATE( last_update);
+SELECT description FROM film WHERE description IN('NEWS','asd') GROUP BY description;
+
+SELECT  
+  description  
+FROM  
+  film  
+WHERE  
+  description  IN( 'NEWS', 
+  'asd'
+) 
+GROUP BY  
+  description;
+alter table address add index idx_city_id(city_id);
+
+ALTER TABLE  
+  address  
+ADD  
+  index  idx_city_id( city_id);
+alter table inventory add index `idx_store_film` (`store_id`,`film_id`);
+
+ALTER TABLE  
+  inventory  
+ADD  
+  index  `idx_store_film` (
+    `store_id`, `film_id`);
+alter table inventory add index `idx_store_film` (`store_id`,`film_id`),add index `idx_store_film` (`store_id`,`film_id`),add index `idx_store_film` (`store_id`,`film_id`);
+
+ALTER TABLE  
+  inventory  
+ADD  
+  index  `idx_store_film` (
+    `store_id`, `film_id`), 
+      ADD  
+      index  `idx_store_film` (
+        `store_id`, `film_id`), 
+              ADD  
+          index  `idx_store_film` (
+            `store_id`, `film_id`);
diff --git a/ast/tidb.go b/ast/tidb.go
new file mode 100644
index 00000000..d93e23b6
--- /dev/null
+++ b/ast/tidb.go
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2018 Xiaomi, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package ast
+
+import (
+	"github.com/XiaoMi/soar/common"
+
+	"github.com/kr/pretty"
+	"github.com/pingcap/tidb/ast"
+	"github.com/pingcap/tidb/parser"
+)
+
+// TiParse TiDB 语法解析
+func TiParse(sql, charset, collation string) ([]ast.StmtNode, error) {
+	p := parser.New()
+	return p.Parse(sql, charset, collation)
+}
+
+// PrintPrettyStmtNode 打印TiParse语法树
+func PrintPrettyStmtNode(sql, charset, collation string) {
+	tree, err := TiParse(sql, charset, collation)
+	if err != nil {
+		common.Log.Warning(err.Error())
+	} else {
+		_, err = pretty.Println(tree)
+		common.LogIfWarn(err, "")
+	}
+}
+
+// TiVisitor TODO
+type TiVisitor struct {
+	EnterFunc func(node ast.Node) bool
+	LeaveFunc func(node ast.Node) bool
+}
+
+// Enter TODO
+func (visitor *TiVisitor) Enter(n ast.Node) (node ast.Node, skip bool) {
+	skip = visitor.EnterFunc(n)
+	return
+}
+
+// Leave TODO
+func (visitor *TiVisitor) Leave(n ast.Node) (node ast.Node, ok bool) {
+	ok = visitor.LeaveFunc(n)
+	return
+}
diff --git a/ast/token.go b/ast/token.go
new file mode 100644
index 00000000..43aa7c45
--- /dev/null
+++ b/ast/token.go
@@ -0,0 +1,1009 @@
+/*
+ * Copyright 2018 Xiaomi, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package ast
+
+import (
+	"errors"
+	"fmt"
+	"regexp"
+	"strings"
+	"unicode"
+
+	"vitess.io/vitess/go/vt/sqlparser"
+)
+
+// TokenType
+const (
+	TokenTypeWhitespace       = 0
+	TokenTypeWord             = 1
+	TokenTypeQuote            = 2
+	TokenTypeBacktickQuote    = 3
+	TokenTypeReserved         = 4
+	TokenTypeReservedToplevel = 5
+	TokenTypeReservedNewline  = 6
+	TokenTypeBoundary         = 7
+	TokenTypeComment          = 8
+	TokenTypeBlockComment     = 9
+	TokenTypeNumber           = 10
+	TokenTypeError            = 11
+	TokenTypeVariable         = 12
+)
+
+var maxCachekeySize = 15
+var cacheHits int
+var cacheMisses int
+var tokenCache map[string]Token
+
+var tokenBoudaries = []string{",", ";", ":", ")", "(", ".", "=", "<", ">", "+", "-", "*", "/", "!", "^", "%", "|", "&", "#"}
+
+var tokenReserved = []string{
+	"ACCESSIBLE", "ACTION", "AGAINST", "AGGREGATE", "ALGORITHM", "ALL", "ALTER", "ANALYSE", "ANALYZE", "AS", "ASC",
+	"AUTOCOMMIT", "AUTO_INCREMENT", "BACKUP", "BEGIN", "BETWEEN", "BINLOG", "BOTH", "CASCADE", "CASE", "CHANGE", "CHANGED", "CHARACTER SET",
+	"CHARSET", "CHECK", "CHECKSUM", "COLLATE", "COLLATION", "COLUMN", "COLUMNS", "COMMENT", "COMMIT", "COMMITTED", "COMPRESSED", "CONCURRENT",
+	"CONSTRAINT", "CONTAINS", "CONVERT", "CREATE", "CROSS", "CURRENT_TIMESTAMP", "DATABASE", "DATABASES", "DAY", "DAY_HOUR", "DAY_MINUTE",
+	"DAY_SECOND", "DEFAULT", "DEFINER", "DELAYED", "DELETE", "DESC", "DESCRIBE", "DETERMINISTIC", "DISTINCT", "DISTINCTROW", "DIV",
+	"DO", "DUMPFILE", "DUPLICATE", "DYNAMIC", "ELSE", "ENCLOSED", "END", "ENGINE", "ENGINE_TYPE", "ENGINES", "ESCAPE", "ESCAPED", "EVENTS", "EXEC",
+	"EXECUTE", "EXISTS", "EXPLAIN", "EXTENDED", "FAST", "FIELDS", "FILE", "FIRST", "FIXED", "FLUSH", "FOR", "FORCE", "FOREIGN", "FULL", "FULLTEXT",
+	"FUNCTION", "GLOBAL", "GRANT", "GRANTS", "GROUP_CONCAT", "HEAP", "HIGH_PRIORITY", "HOSTS", "HOUR", "HOUR_MINUTE",
+	"HOUR_SECOND", "IDENTIFIED", "IF", "IFNULL", "IGNORE", "IN", "INDEX", "INDEXES", "INFILE", "INSERT", "INSERT_ID", "INSERT_METHOD", "INTERVAL",
+	"INTO", "INVOKER", "IS", "ISOLATION", "KEY", "KEYS", "KILL", "LAST_INSERT_ID", "LEADING", "LEVEL", "LIKE", "LINEAR",
+	"LINES", "LOAD", "LOCAL", "LOCK", "LOCKS", "LOGS", "LOW_PRIORITY", "MARIA", "MASTER", "MASTER_CONNECT_RETRY", "MASTER_HOST", "MASTER_LOG_FILE",
+	"MATCH", "MAX_CONNECTIONS_PER_HOUR", "MAX_QUERIES_PER_HOUR", "MAX_ROWS", "MAX_UPDATES_PER_HOUR", "MAX_USER_CONNECTIONS",
+	"MEDIUM", "MERGE", "MINUTE", "MINUTE_SECOND", "MIN_ROWS", "MODE", "MODIFY",
+	"MONTH", "MRG_MYISAM", "MYISAM", "NAMES", "NATURAL", "NOT", "NOW()", "NULL", "OFFSET", "ON", "OPEN", "OPTIMIZE", "OPTION", "OPTIONALLY",
+	"ON UPDATE", "ON DELETE", "OUTFILE", "PACK_KEYS", "PAGE", "PARTIAL", "PARTITION", "PARTITIONS", "PASSWORD", "PRIMARY", "PRIVILEGES", "PROCEDURE",
+	"PROCESS", "PROCESSLIST", "PURGE", "QUICK", "RANGE", "RAID0", "RAID_CHUNKS", "RAID_CHUNKSIZE", "RAID_TYPE", "READ", "READ_ONLY",
+	"READ_WRITE", "REFERENCES", "REGEXP", "RELOAD", "RENAME", "REPAIR", "REPEATABLE", "REPLACE", "REPLICATION", "RESET", "RESTORE", "RESTRICT",
+	"RETURN", "RETURNS", "REVOKE", "RLIKE", "ROLLBACK", "ROW", "ROWS", "ROW_FORMAT", "SECOND", "SECURITY", "SEPARATOR",
+	"SERIALIZABLE", "SESSION", "SHARE", "SHOW", "SHUTDOWN", "SLAVE", "SONAME", "SOUNDS", "SQL", "SQL_AUTO_IS_NULL", "SQL_BIG_RESULT",
+	"SQL_BIG_SELECTS", "SQL_BIG_TABLES", "SQL_BUFFER_RESULT", "SQL_CALC_FOUND_ROWS", "SQL_LOG_BIN", "SQL_LOG_OFF", "SQL_LOG_UPDATE",
+	"SQL_LOW_PRIORITY_UPDATES", "SQL_MAX_JOIN_SIZE", "SQL_QUOTE_SHOW_CREATE", "SQL_SAFE_UPDATES", "SQL_SELECT_LIMIT", "SQL_SLAVE_SKIP_COUNTER",
+	"SQL_SMALL_RESULT", "SQL_WARNINGS", "SQL_CACHE", "SQL_NO_CACHE", "START", "STARTING", "STATUS", "STOP", "STORAGE",
+	"STRAIGHT_JOIN", "STRING", "STRIPED", "SUPER", "TABLE", "TABLES", "TEMPORARY", "TERMINATED", "THEN", "TO", "TRAILING", "TRANSACTIONAL", "TRUE",
+	"TRUNCATE", "TYPE", "TYPES", "UNCOMMITTED", "UNIQUE", "UNLOCK", "UNSIGNED", "USAGE", "USE", "USING", "VARIABLES",
+	"VIEW", "WHEN", "WITH", "WORK", "WRITE", "YEAR_MONTH",
+}
+
+var tokenReservedTopLevel = []string{
+	"SELECT", "FROM", "WHERE", "SET", "ORDER BY", "GROUP BY", "LIMIT", "DROP",
+	"VALUES", "UPDATE", "HAVING", "ADD", "AFTER", "ALTER TABLE", "DELETE FROM", "UNION ALL", "UNION", "EXCEPT", "INTERSECT",
+}
+
+var tokenFunction = []string{
+	"ABS", "ACOS", "ADDDATE", "ADDTIME", "AES_DECRYPT", "AES_ENCRYPT", "AREA", "ASBINARY", "ASCII", "ASIN", "ASTEXT", "ATAN", "ATAN2",
+	"AVG", "BDMPOLYFROMTEXT", "BDMPOLYFROMWKB", "BDPOLYFROMTEXT", "BDPOLYFROMWKB", "BENCHMARK", "BIN", "BIT_AND", "BIT_COUNT", "BIT_LENGTH",
+	"BIT_OR", "BIT_XOR", "BOUNDARY", "BUFFER", "CAST", "CEIL", "CEILING", "CENTROID", "CHAR", "CHARACTER_LENGTH", "CHARSET", "CHAR_LENGTH",
+	"COALESCE", "COERCIBILITY", "COLLATION", "COMPRESS", "CONCAT", "CONCAT_WS", "CONNECTION_ID", "CONTAINS", "CONV", "CONVERT", "CONVERT_TZ",
+	"CONVEXHULL", "COS", "COT", "COUNT", "CRC32", "CROSSES", "CURDATE", "CURRENT_DATE", "CURRENT_TIME", "CURRENT_TIMESTAMP", "CURRENT_USER",
+	"CURTIME", "DATABASE", "DATE", "DATEDIFF", "DATE_ADD", "DATE_DIFF", "DATE_FORMAT", "DATE_SUB", "DAY", "DAYNAME", "DAYOFMONTH", "DAYOFWEEK",
+	"DAYOFYEAR", "DECODE", "DEFAULT", "DEGREES", "DES_DECRYPT", "DES_ENCRYPT", "DIFFERENCE", "DIMENSION", "DISJOINT", "DISTANCE", "ELT", "ENCODE",
+	"ENCRYPT", "ENDPOINT", "ENVELOPE", "EQUALS", "EXP", "EXPORT_SET", "EXTERIORRING", "EXTRACT", "EXTRACTVALUE", "FIELD", "FIND_IN_SET", "FLOOR",
+	"FORMAT", "FOUND_ROWS", "FROM_DAYS", "FROM_UNIXTIME", "GEOMCOLLFROMTEXT", "GEOMCOLLFROMWKB", "GEOMETRYCOLLECTION", "GEOMETRYCOLLECTIONFROMTEXT",
+	"GEOMETRYCOLLECTIONFROMWKB", "GEOMETRYFROMTEXT", "GEOMETRYFROMWKB", "GEOMETRYN", "GEOMETRYTYPE", "GEOMFROMTEXT", "GEOMFROMWKB", "GET_FORMAT",
+	"GET_LOCK", "GLENGTH", "GREATEST", "GROUP_CONCAT", "GROUP_UNIQUE_USERS", "HEX", "HOUR", "IF", "IFNULL", "INET_ATON", "INET_NTOA", "INSERT", "INSTR",
+	"INTERIORRINGN", "INTERSECTION", "INTERSECTS", "INTERVAL", "ISCLOSED", "ISEMPTY", "ISNULL", "ISRING", "ISSIMPLE", "IS_FREE_LOCK", "IS_USED_LOCK",
+	"LAST_DAY", "LAST_INSERT_ID", "LCASE", "LEAST", "LEFT", "LENGTH", "LINEFROMTEXT", "LINEFROMWKB", "LINESTRING", "LINESTRINGFROMTEXT", "LINESTRINGFROMWKB",
+	"LN", "LOAD_FILE", "LOCALTIME", "LOCALTIMESTAMP", "LOCATE", "LOG", "LOG10", "LOG2", "LOWER", "LPAD", "LTRIM", "MAKEDATE", "MAKETIME", "MAKE_SET",
+	"MASTER_POS_WAIT", "MAX", "MBRCONTAINS", "MBRDISJOINT", "MBREQUAL", "MBRINTERSECTS", "MBROVERLAPS", "MBRTOUCHES", "MBRWITHIN", "MD5", "MICROSECOND",
+	"MID", "MIN", "MINUTE", "MLINEFROMTEXT", "MLINEFROMWKB", "MOD", "MONTH", "MONTHNAME", "MPOINTFROMTEXT", "MPOINTFROMWKB", "MPOLYFROMTEXT", "MPOLYFROMWKB",
+	"MULTILINESTRING", "MULTILINESTRINGFROMTEXT", "MULTILINESTRINGFROMWKB", "MULTIPOINT", "MULTIPOINTFROMTEXT", "MULTIPOINTFROMWKB", "MULTIPOLYGON",
+	"MULTIPOLYGONFROMTEXT", "MULTIPOLYGONFROMWKB", "NAME_CONST", "NULLIF", "NUMGEOMETRIES", "NUMINTERIORRINGS", "NUMPOINTS", "OCT", "OCTET_LENGTH",
+	"OLD_PASSWORD", "ORD", "OVERLAPS", "PASSWORD", "PERIOD_ADD", "PERIOD_DIFF", "PI", "POINT", "POINTFROMTEXT", "POINTFROMWKB", "POINTN", "POINTONSURFACE",
+	"POLYFROMTEXT", "POLYFROMWKB", "POLYGON", "POLYGONFROMTEXT", "POLYGONFROMWKB", "POSITION", "POW", "POWER", "QUARTER", "QUOTE", "RADIANS", "RAND",
+	"RELATED", "RELEASE_LOCK", "REPEAT", "REPLACE", "REVERSE", "RIGHT", "ROUND", "ROW_COUNT", "RPAD", "RTRIM", "SCHEMA", "SECOND", "SEC_TO_TIME",
+	"SESSION_USER", "SHA", "SHA1", "SIGN", "SIN", "SLEEP", "SOUNDEX", "SPACE", "SQRT", "SRID", "STARTPOINT", "STD", "STDDEV", "STDDEV_POP", "STDDEV_SAMP",
+	"STRCMP", "STR_TO_DATE", "SUBDATE", "SUBSTR", "SUBSTRING", "SUBSTRING_INDEX", "SUBTIME", "SUM", "SYMDIFFERENCE", "SYSDATE", "SYSTEM_USER", "TAN",
+	"TIME", "TIMEDIFF", "TIMESTAMP", "TIMESTAMPADD", "TIMESTAMPDIFF", "TIME_FORMAT", "TIME_TO_SEC", "TOUCHES", "TO_DAYS", "TRIM", "TRUNCATE", "UCASE",
+	"UNCOMPRESS", "UNCOMPRESSED_LENGTH", "UNHEX", "UNIQUE_USERS", "UNIX_TIMESTAMP", "UPDATEXML", "UPPER", "USER", "UTC_DATE", "UTC_TIME", "UTC_TIMESTAMP",
+	"UUID", "VARIANCE", "VAR_POP", "VAR_SAMP", "VERSION", "WEEK", "WEEKDAY", "WEEKOFYEAR", "WITHIN", "X", "Y", "YEAR", "YEARWEEK",
+}
+
+var tokenReservedNewLine = []string{
+	"LEFT OUTER JOIN", "RIGHT OUTER JOIN", "LEFT JOIN", "RIGHT JOIN", "OUTER JOIN", "INNER JOIN", "JOIN", "XOR", "OR", "AND",
+}
+
+var regBoundariesString string
+var regResrvedToplevelString string
+var regReservedNewlineString string
+var regReservedString string
+var regFunctionString string
+
+func init() {
+	var regs []string
+	for _, reg := range tokenBoudaries {
+		regs = append(regs, regexp.QuoteMeta(reg))
+	}
+	regBoundariesString = "(" + strings.Join(regs, "|") + ")"
+
+	regs = make([]string, 0)
+	for _, reg := range tokenReservedTopLevel {
+		regs = append(regs, regexp.QuoteMeta(reg))
+	}
+	regResrvedToplevelString = "(" + strings.Join(regs, "|") + ")"
+
+	regs = make([]string, 0)
+	for _, reg := range tokenReservedNewLine {
+		regs = append(regs, regexp.QuoteMeta(reg))
+	}
+	regReservedNewlineString = "(" + strings.Join(regs, "|") + ")"
+
+	regs = make([]string, 0)
+	for _, reg := range tokenReserved {
+		regs = append(regs, regexp.QuoteMeta(reg))
+	}
+	regReservedString = "(" + strings.Join(regs, "|") + ")"
+
+	regs = make([]string, 0)
+	for _, reg := range tokenFunction {
+		regs = append(regs, regexp.QuoteMeta(reg))
+	}
+	regFunctionString = "(" + strings.Join(regs, "|") + ")"
+}
+
+// TokenString sqlparser tokens
+var TokenString = map[int]string{
+	sqlparser.LEX_ERROR:               "",
+	sqlparser.UNION:                   "union",
+	sqlparser.SELECT:                  "select",
+	sqlparser.STREAM:                  "stream",
+	sqlparser.INSERT:                  "insert",
+	sqlparser.UPDATE:                  "update",
+	sqlparser.DELETE:                  "delete",
+	sqlparser.FROM:                    "from",
+	sqlparser.WHERE:                   "where",
+	sqlparser.GROUP:                   "group",
+	sqlparser.HAVING:                  "having",
+	sqlparser.ORDER:                   "order",
+	sqlparser.BY:                      "by",
+	sqlparser.LIMIT:                   "limit",
+	sqlparser.OFFSET:                  "offset",
+	sqlparser.FOR:                     "for",
+	sqlparser.ALL:                     "all",
+	sqlparser.DISTINCT:                "distinct",
+	sqlparser.AS:                      "as",
+	sqlparser.EXISTS:                  "exists",
+	sqlparser.ASC:                     "asc",
+	sqlparser.DESC:                    "desc",
+	sqlparser.INTO:                    "into",
+	sqlparser.DUPLICATE:               "duplicate",
+	sqlparser.KEY:                     "key",
+	sqlparser.DEFAULT:                 "default",
+	sqlparser.SET:                     "set",
+	sqlparser.LOCK:                    "lock",
+	sqlparser.KEYS:                    "keys",
+	sqlparser.VALUES:                  "values",
+	sqlparser.LAST_INSERT_ID:          "last_insert_id",
+	sqlparser.NEXT:                    "next",
+	sqlparser.VALUE:                   "value",
+	sqlparser.SHARE:                   "share",
+	sqlparser.MODE:                    "mode",
+	sqlparser.SQL_NO_CACHE:            "sql_no_cache",
+	sqlparser.SQL_CACHE:               "sql_cache",
+	sqlparser.JOIN:                    "join",
+	sqlparser.STRAIGHT_JOIN:           "straight_join",
+	sqlparser.LEFT:                    "left",
+	sqlparser.RIGHT:                   "right",
+	sqlparser.INNER:                   "inner",
+	sqlparser.OUTER:                   "outer",
+	sqlparser.CROSS:                   "cross",
+	sqlparser.NATURAL:                 "natural",
+	sqlparser.USE:                     "use",
+	sqlparser.FORCE:                   "force",
+	sqlparser.ON:                      "on",
+	sqlparser.USING:                   "using",
+	sqlparser.ID:                      "id",
+	sqlparser.HEX:                     "hex",
+	sqlparser.STRING:                  "string",
+	sqlparser.INTEGRAL:                "integral",
+	sqlparser.FLOAT:                   "float",
+	sqlparser.HEXNUM:                  "hexnum",
+	sqlparser.VALUE_ARG:               "?",
+	sqlparser.LIST_ARG:                ":",
+	sqlparser.COMMENT:                 "",
+	sqlparser.COMMENT_KEYWORD:         "comment",
+	sqlparser.BIT_LITERAL:             "bit_literal",
+	sqlparser.NULL:                    "null",
+	sqlparser.TRUE:                    "true",
+	sqlparser.FALSE:                   "false",
+	sqlparser.OR:                      "||",
+	sqlparser.AND:                     "&&",
+	sqlparser.NOT:                     "not",
+	sqlparser.BETWEEN:                 "between",
+	sqlparser.CASE:                    "case",
+	sqlparser.WHEN:                    "when",
+	sqlparser.THEN:                    "then",
+	sqlparser.ELSE:                    "else",
+	sqlparser.END:                     "end",
+	sqlparser.LE:                      "<",
+	sqlparser.GE:                      ">=",
+	sqlparser.NE:                      "<>",
+	sqlparser.NULL_SAFE_EQUAL:         "<=>",
+	sqlparser.IS:                      "is",
+	sqlparser.LIKE:                    "like",
+	sqlparser.REGEXP:                  "regexp",
+	sqlparser.IN:                      "in",
+	sqlparser.SHIFT_LEFT:              "<<",
+	sqlparser.SHIFT_RIGHT:             ">>",
+	sqlparser.DIV:                     "div",
+	sqlparser.MOD:                     "mod",
+	sqlparser.UNARY:                   "unary",
+	sqlparser.COLLATE:                 "collate",
+	sqlparser.BINARY:                  "binary",
+	sqlparser.UNDERSCORE_BINARY:       "_binary",
+	sqlparser.INTERVAL:                "interval",
+	sqlparser.JSON_EXTRACT_OP:         "->>",
+	sqlparser.JSON_UNQUOTE_EXTRACT_OP: "->",
+	sqlparser.CREATE:                  "create",
+	sqlparser.ALTER:                   "alter",
+	sqlparser.DROP:                    "drop",
+	sqlparser.RENAME:                  "rename",
+	sqlparser.ANALYZE:                 "analyze",
+	sqlparser.ADD:                     "add",
+	sqlparser.SCHEMA:                  "schema",
+	sqlparser.TABLE:                   "table",
+	sqlparser.INDEX:                   "index",
+	sqlparser.VIEW:                    "view",
+	sqlparser.TO:                      "to",
+	sqlparser.IGNORE:                  "ignore",
+	sqlparser.IF:                      "if",
+	sqlparser.UNIQUE:                  "unique",
+	sqlparser.PRIMARY:                 "primary",
+	sqlparser.COLUMN:                  "column",
+	sqlparser.CONSTRAINT:              "constraint",
+	sqlparser.SPATIAL:                 "spatial",
+	sqlparser.FULLTEXT:                "fulltext",
+	sqlparser.FOREIGN:                 "foreign",
+	sqlparser.SHOW:                    "show",
+	sqlparser.DESCRIBE:                "describe",
+	sqlparser.EXPLAIN:                 "explain",
+	sqlparser.DATE:                    "date",
+	sqlparser.ESCAPE:                  "escape",
+	sqlparser.REPAIR:                  "repair",
+	sqlparser.OPTIMIZE:                "optimize",
+	sqlparser.TRUNCATE:                "truncate",
+	sqlparser.MAXVALUE:                "maxvalue",
+	sqlparser.PARTITION:               "partition",
+	sqlparser.REORGANIZE:              "reorganize",
+	sqlparser.LESS:                    "less",
+	sqlparser.THAN:                    "than",
+	sqlparser.PROCEDURE:               "procedure",
+	sqlparser.TRIGGER:                 "trigger",
+	sqlparser.VINDEX:                  "vindex",
+	sqlparser.VINDEXES:                "vindexes",
+	sqlparser.STATUS:                  "status",
+	sqlparser.VARIABLES:               "variables",
+	sqlparser.BEGIN:                   "begin",
+	sqlparser.START:                   "start",
+	sqlparser.TRANSACTION:             "transaction",
+	sqlparser.COMMIT:                  "commit",
+	sqlparser.ROLLBACK:                "rollback",
+	sqlparser.BIT:                     "bit",
+	sqlparser.TINYINT:                 "tinyint",
+	sqlparser.SMALLINT:                "smallint",
+	sqlparser.MEDIUMINT:               "mediumint",
+	sqlparser.INT:                     "int",
+	sqlparser.INTEGER:                 "integer",
+	sqlparser.BIGINT:                  "bigint",
+	sqlparser.INTNUM:                  "intnum",
+	sqlparser.REAL:                    "real",
+	sqlparser.DOUBLE:                  "bouble",
+	sqlparser.FLOAT_TYPE:              "float_type",
+	sqlparser.DECIMAL:                 "decimal",
+	sqlparser.NUMERIC:                 "numeric",
+	sqlparser.TIME:                    "time",
+	sqlparser.TIMESTAMP:               "timestamp",
+	sqlparser.DATETIME:                "datetime",
+	sqlparser.YEAR:                    "year",
+	sqlparser.CHAR:                    "char",
+	sqlparser.VARCHAR:                 "varchar",
+	sqlparser.BOOL:                    "bool",
+	sqlparser.CHARACTER:               "character",
+	sqlparser.VARBINARY:               "varbinary",
+	sqlparser.NCHAR:                   "nchar",
+	sqlparser.TEXT:                    "text",
+	sqlparser.TINYTEXT:                "tinytext",
+	sqlparser.MEDIUMTEXT:              "mediumtext",
+	sqlparser.LONGTEXT:                "longtext",
+	sqlparser.BLOB:                    "blob",
+	sqlparser.TINYBLOB:                "tinyblob",
+	sqlparser.MEDIUMBLOB:              "mediumblob",
+	sqlparser.LONGBLOB:                "longblob",
+	sqlparser.JSON:                    "json",
+	sqlparser.ENUM:                    "enum",
+	sqlparser.GEOMETRY:                "geometry",
+	sqlparser.POINT:                   "point",
+	sqlparser.LINESTRING:              "linestring",
+	sqlparser.POLYGON:                 "polygon",
+	sqlparser.GEOMETRYCOLLECTION:      "geometrycollection",
+	sqlparser.MULTIPOINT:              "multipoint",
+	sqlparser.MULTILINESTRING:         "multilinestring",
+	sqlparser.MULTIPOLYGON:            "multipolygon",
+	sqlparser.NULLX:                   "nullx",
+	sqlparser.AUTO_INCREMENT:          "auto_increment",
+	sqlparser.APPROXNUM:               "approxnum",
+	sqlparser.SIGNED:                  "signed",
+	sqlparser.UNSIGNED:                "unsigned",
+	sqlparser.ZEROFILL:                "zerofill",
+	sqlparser.DATABASES:               "databases",
+	sqlparser.TABLES:                  "tables",
+	sqlparser.VITESS_KEYSPACES:        "vitess_keyspaces",
+	sqlparser.VITESS_SHARDS:           "vitess_shards",
+	sqlparser.VITESS_TABLETS:          "vitess_tablets",
+	sqlparser.VSCHEMA_TABLES:          "vschema_tables",
+	sqlparser.NAMES:                   "names",
+	sqlparser.CHARSET:                 "charset",
+	sqlparser.GLOBAL:                  "global",
+	sqlparser.SESSION:                 "session",
+	sqlparser.CURRENT_TIMESTAMP:       "current_timestamp",
+	sqlparser.DATABASE:                "database",
+	sqlparser.CURRENT_DATE:            "current_date",
+	sqlparser.CURRENT_TIME:            "current_time",
+	sqlparser.LOCALTIME:               "localtime",
+	sqlparser.LOCALTIMESTAMP:          "localtimestamp",
+	sqlparser.UTC_DATE:                "utc_date",
+	sqlparser.UTC_TIME:                "utc_time",
+	sqlparser.UTC_TIMESTAMP:           "utc_timestamp",
+	sqlparser.REPLACE:                 "replace",
+	sqlparser.CONVERT:                 "convert",
+	sqlparser.CAST:                    "cast",
+	sqlparser.SUBSTR:                  "substr",
+	sqlparser.SUBSTRING:               "substring",
+	sqlparser.GROUP_CONCAT:            "group_concat",
+	sqlparser.SEPARATOR:               "separator",
+	sqlparser.MATCH:                   "match",
+	sqlparser.AGAINST:                 "against",
+	sqlparser.BOOLEAN:                 "boolean",
+	sqlparser.LANGUAGE:                "language",
+	sqlparser.WITH:                    "with",
+	sqlparser.QUERY:                   "query",
+	sqlparser.EXPANSION:               "expansion",
+	sqlparser.UNUSED:                  "",
+}
+
+// 这个变更从vitess更新过来,如果vitess新开了一个关键字这里也要同步开
+var mySQLKeywords = map[string]string{
+	"add":                "ADD",
+	"against":            "AGAINST",
+	"all":                "ALL",
+	"alter":              "ALTER",
+	"analyze":            "ANALYZE",
+	"and":                "AND",
+	"as":                 "AS",
+	"asc":                "ASC",
+	"auto_increment":     "AUTO_INCREMENT",
+	"begin":              "BEGIN",
+	"between":            "BETWEEN",
+	"bigint":             "BIGINT",
+	"binary":             "BINARY",
+	"_binary":            "UNDERSCORE_BINARY",
+	"bit":                "BIT",
+	"blob":               "BLOB",
+	"bool":               "BOOL",
+	"boolean":            "BOOLEAN",
+	"by":                 "BY",
+	"case":               "CASE",
+	"cast":               "CAST",
+	"char":               "CHAR",
+	"character":          "CHARACTER",
+	"charset":            "CHARSET",
+	"collate":            "COLLATE",
+	"column":             "COLUMN",
+	"comment":            "COMMENT_KEYWORD",
+	"commit":             "COMMIT",
+	"constraint":         "CONSTRAINT",
+	"convert":            "CONVERT",
+	"substr":             "SUBSTR",
+	"substring":          "SUBSTRING",
+	"create":             "CREATE",
+	"cross":              "CROSS",
+	"current_date":       "CURRENT_DATE",
+	"current_time":       "CURRENT_TIME",
+	"current_timestamp":  "CURRENT_TIMESTAMP",
+	"database":           "DATABASE",
+	"databases":          "DATABASES",
+	"date":               "DATE",
+	"datetime":           "DATETIME",
+	"decimal":            "DECIMAL",
+	"default":            "DEFAULT",
+	"delete":             "DELETE",
+	"desc":               "DESC",
+	"describe":           "DESCRIBE",
+	"distinct":           "DISTINCT",
+	"div":                "DIV",
+	"double":             "DOUBLE",
+	"drop":               "DROP",
+	"duplicate":          "DUPLICATE",
+	"else":               "ELSE",
+	"end":                "END",
+	"enum":               "ENUM",
+	"escape":             "ESCAPE",
+	"exists":             "EXISTS",
+	"explain":            "EXPLAIN",
+	"expansion":          "EXPANSION",
+	"false":              "FALSE",
+	"float":              "FLOAT_TYPE",
+	"for":                "FOR",
+	"force":              "FORCE",
+	"foreign":            "FOREIGN",
+	"from":               "FROM",
+	"fulltext":           "FULLTEXT",
+	"geometry":           "GEOMETRY",
+	"geometrycollection": "GEOMETRYCOLLECTION",
+	"global":             "GLOBAL",
+	"group":              "GROUP",
+	"group_concat":       "GROUP_CONCAT",
+	"having":             "HAVING",
+	"if":                 "IF",
+	"ignore":             "IGNORE",
+	"in":                 "IN",
+	"index":              "INDEX",
+	"inner":              "INNER",
+	"insert":             "INSERT",
+	"int":                "INT",
+	"integer":            "INTEGER",
+	"interval":           "INTERVAL",
+	"into":               "INTO",
+	"is":                 "IS",
+	"join":               "JOIN",
+	"json":               "JSON",
+	"key":                "KEY",
+	"keys":               "KEYS",
+	"key_block_size":     "KEY_BLOCK_SIZE",
+	"language":           "LANGUAGE",
+	"last_insert_id":     "LAST_INSERT_ID",
+	"left":               "LEFT",
+	"less":               "LESS",
+	"like":               "LIKE",
+	"limit":              "LIMIT",
+	"linestring":         "LINESTRING",
+	"localtime":          "LOCALTIME",
+	"localtimestamp":     "LOCALTIMESTAMP",
+	"lock":               "LOCK",
+	"longblob":           "LONGBLOB",
+	"longtext":           "LONGTEXT",
+	"match":              "MATCH",
+	"maxvalue":           "MAXVALUE",
+	"mediumblob":         "MEDIUMBLOB",
+	"mediumint":          "MEDIUMINT",
+	"mediumtext":         "MEDIUMTEXT",
+	"mod":                "MOD",
+	"mode":               "MODE",
+	"multilinestring":    "MULTILINESTRING",
+	"multipoint":         "MULTIPOINT",
+	"multipolygon":       "MULTIPOLYGON",
+	"names":              "NAMES",
+	"natural":            "NATURAL",
+	"nchar":              "NCHAR",
+	"next":               "NEXT",
+	"not":                "NOT",
+	"null":               "NULL",
+	"numeric":            "NUMERIC",
+	"offset":             "OFFSET",
+	"on":                 "ON",
+	"optimize":           "OPTIMIZE",
+	"or":                 "OR",
+	"order":              "ORDER",
+	"outer":              "OUTER",
+	"partition":          "PARTITION",
+	"point":              "POINT",
+	"polygon":            "POLYGON",
+	"primary":            "PRIMARY",
+	"procedure":          "PROCEDURE",
+	"query":              "QUERY",
+	"real":               "REAL",
+	"regexp":             "REGEXP",
+	"rename":             "RENAME",
+	"reorganize":         "REORGANIZE",
+	"repair":             "REPAIR",
+	"replace":            "REPLACE",
+	"right":              "RIGHT",
+	"rlike":              "REGEXP",
+	"rollback":           "ROLLBACK",
+	"schema":             "SCHEMA",
+	"select":             "SELECT",
+	"separator":          "SEPARATOR",
+	"session":            "SESSION",
+	"set":                "SET",
+	"share":              "SHARE",
+	"show":               "SHOW",
+	"signed":             "SIGNED",
+	"smallint":           "SMALLINT",
+	"spatial":            "SPATIAL",
+	"sql_cache":          "SQL_CACHE",
+	"sql_no_cache":       "SQL_NO_CACHE",
+	"start":              "START",
+	"status":             "STATUS",
+	"straight_join":      "STRAIGHT_JOIN",
+	"stream":             "STREAM",
+	"table":              "TABLE",
+	"tables":             "TABLES",
+	"text":               "TEXT",
+	"than":               "THAN",
+	"then":               "THEN",
+	"time":               "TIME",
+	"timestamp":          "TIMESTAMP",
+	"tinyblob":           "TINYBLOB",
+	"tinyint":            "TINYINT",
+	"tinytext":           "TINYTEXT",
+	"to":                 "TO",
+	"transaction":        "TRANSACTION",
+	"trigger":            "TRIGGER",
+	"true":               "TRUE",
+	"truncate":           "TRUNCATE",
+	"union":              "UNION",
+	"unique":             "UNIQUE",
+	"unsigned":           "UNSIGNED",
+	"update":             "UPDATE",
+	"use":                "USE",
+	"using":              "USING",
+	"utc_date":           "UTC_DATE",
+	"utc_time":           "UTC_TIME",
+	"utc_timestamp":      "UTC_TIMESTAMP",
+	"values":             "VALUES",
+	"variables":          "VARIABLES",
+	"varbinary":          "VARBINARY",
+	"varchar":            "VARCHAR",
+	"vindex":             "VINDEX",
+	"vindexes":           "VINDEXES",
+	"view":               "VIEW",
+	"vitess_keyspaces":   "VITESS_KEYSPACES",
+	"vitess_shards":      "VITESS_SHARDS",
+	"vitess_tablets":     "VITESS_TABLETS",
+	"vschema_tables":     "VSCHEMA_TABLES",
+	"when":               "WHEN",
+	"where":              "WHERE",
+	"with":               "WITH",
+	"year":               "YEAR",
+	"zerofill":           "ZEROFILL",
+}
+
+// Token 基本定义
+type Token struct {
+	Type int
+	Val  string
+	i    int
+}
+
+// Tokenizer 用于初始化token
+func Tokenizer(sql string) []Token {
+	var tokens []Token
+	tkn := sqlparser.NewStringTokenizer(sql)
+	typ, val := tkn.Scan()
+	for typ != 0 {
+		if val != nil {
+			tokens = append(tokens, Token{Type: typ, Val: string(val)})
+		} else {
+			if typ > 255 {
+				if v, ok := TokenString[typ]; ok {
+					tokens = append(tokens, Token{
+						Type: typ,
+						Val:  v,
+					})
+				} else {
+					tokens = append(tokens, Token{
+						Type: typ,
+						Val:  "",
+					})
+				}
+			} else {
+				tokens = append(tokens, Token{
+					Type: typ,
+					Val:  fmt.Sprintf("%c", typ),
+				})
+			}
+		}
+		typ, val = tkn.Scan()
+	}
+	return tokens
+}
+
+// MysqlEscapeString mysql_real_escape_string
+// https://github.com/liule/golang_escape
+func MysqlEscapeString(source string) (string, error) {
+	var j = 0
+	if len(source) == 0 {
+		return "", errors.New("source is null")
+	}
+	tempStr := source[:]
+	desc := make([]byte, len(tempStr)*2)
+	for i := 0; i < len(tempStr); i++ {
+		flag := false
+		var escape byte
+		switch tempStr[i] {
+		case '\r':
+			flag = true
+			escape = '\r'
+		case '\n':
+			flag = true
+			escape = '\n'
+		case '\\':
+			flag = true
+			escape = '\\'
+		case '\'':
+			flag = true
+			escape = '\''
+		case '"':
+			flag = true
+			escape = '"'
+		case '\032':
+			flag = true
+			escape = 'Z'
+		default:
+		}
+		if flag {
+			desc[j] = '\\'
+			desc[j+1] = escape
+			j = j + 2
+		} else {
+			desc[j] = tempStr[i]
+			j = j + 1
+		}
+	}
+	return string(desc[0:j]), nil
+}
+
+// IsMysqlKeyword 判断是否是关键字
+func IsMysqlKeyword(name string) bool {
+	_, ok := mySQLKeywords[strings.ToLower(strings.TrimSpace(name))]
+	return ok
+}
+
+// getNextToken 从buf中获取token
+func getNextToken(buf string, previous Token) Token {
+	var typ int // TOKEN_TYPE
+
+	// Whitespace
+	whiteSpaceReg := regexp.MustCompile(`^\s+`)
+	if whiteSpaceReg.MatchString(buf) {
+		return Token{
+			Type: TokenTypeWhitespace,
+			Val:  " ",
+		}
+	}
+
+	// Comment (#, --, /**/)
+	if buf[0] == '#' || (len(buf) > 1 && (buf[0] == '-' && buf[1] == '-')) || (buf[0] == '/' && buf[1] == '*') {
+		var last int
+		if buf[0] == '-' || buf[0] == '#' {
+			// Comment until end of line
+			last = strings.Index(buf, "\n")
+			typ = TokenTypeComment
+		} else {
+			// Comment until closing comment tag
+			last = strings.Index(buf[2:], "*/") + 2
+		}
+		if last == 0 {
+			last = len(buf)
+		}
+		return Token{
+			Type: typ,
+			Val:  buf[:last],
+		}
+	}
+
+	// Quoted String
+	if buf[0] == '"' || buf[0] == '\'' || buf[0] == '`' || buf[0] == '[' {
+		var typ int
+		switch buf[0] {
+		case '`', '[':
+			typ = TokenTypeBacktickQuote
+		default:
+			typ = TokenTypeQuote
+		}
+		return Token{
+			Type: typ,
+			Val:  getQuotedString(buf),
+		}
+	}
+
+	// User-defined Variable
+	if (buf[0] == '@' || buf[0] == ':') && len(buf) > 1 {
+		ret := Token{
+			Type: TokenTypeVariable,
+			Val:  "",
+		}
+
+		if buf[1] == '"' || buf[1] == '\'' || buf[1] == '`' {
+			// If the variable name is quoted
+			ret.Val = string(buf[0]) + getQuotedString(buf[1:])
+		} else {
+			// Non-quoted variable name
+			varReg := regexp.MustCompile(`^(` + string(buf[0]) + `[a-zA-Z0-9\._\$]+)`)
+			if varReg.MatchString(buf) {
+				ret.Val = varReg.FindString(buf)
+			}
+		}
+
+		if ret.Val != "" {
+			return ret
+		}
+	}
+
+	// Number(decimal, binary, hex...)
+	numReg := regexp.MustCompile(`^([0-9]+(\.[0-9]+)?|0x[0-9a-fA-F]+|0b[01]+)($|\s|"'` + "`|" + regBoundariesString + ")")
+	if numReg.MatchString(buf) {
+		return Token{
+			Type: TokenTypeNumber,
+			Val:  numReg.FindString(buf),
+		}
+	}
+
+	// Boundary Character(punctuation and symbols)
+	boundaryReg := regexp.MustCompile(`^(` + regBoundariesString + `)`)
+	if boundaryReg.MatchString(buf) {
+		return Token{
+			Type: TokenTypeBoundary,
+			Val:  boundaryReg.FindString(buf),
+		}
+	}
+	sqlUpper := strings.ToUpper(buf)
+	// A reserved word cannot be preceded by a '.'
+	// this makes it so in "mytable.from", "from" is not considered a reserved word
+	if previous.Val != "." {
+		// Top Level Reserved Word
+		reservedToplevelReg := regexp.MustCompile(`^(` + regResrvedToplevelString + `)($|\s|` + regBoundariesString + `)`)
+		if reservedToplevelReg.MatchString(sqlUpper) {
+			return Token{
+				Type: TokenTypeReservedToplevel,
+				Val:  reservedToplevelReg.FindString(sqlUpper),
+			}
+		}
+
+		// Newline Reserved Word
+		reservedNewlineReg := regexp.MustCompile(`^(` + regReservedNewlineString + `)($|\s|` + regBoundariesString + `)`)
+		if reservedNewlineReg.MatchString(sqlUpper) {
+			return Token{
+				Type: TokenTypeReservedNewline,
+				Val:  reservedNewlineReg.FindString(sqlUpper),
+			}
+		}
+
+		// Other Reserved Word
+		reservedReg := regexp.MustCompile(`^(` + regReservedString + `)($|\s|` + regBoundariesString + `)`)
+		if reservedNewlineReg.MatchString(sqlUpper) {
+			return Token{
+				Type: TokenTypeReserved,
+				Val:  reservedReg.FindString(sqlUpper),
+			}
+		}
+
+	}
+
+	// function
+	// A function must be succeeded by '('
+	// this makes it so "count(" is considered a function, but "count" alone is not
+	functionReg := regexp.MustCompile(`^(` + regFunctionString + `)($|\s|` + regBoundariesString + `)`)
+	if functionReg.MatchString(sqlUpper) {
+		return Token{
+			Type: TokenTypeReserved,
+			Val:  functionReg.FindString(sqlUpper),
+		}
+	}
+
+	// Non reserved word
+	noReservedReg := regexp.MustCompile(`(.*?)($|\s|["'` + "`]|" + regBoundariesString + `)`)
+	if noReservedReg.MatchString(buf) {
+		return Token{
+			Type: TokenTypeWord,
+			Val:  noReservedReg.FindString(buf),
+		}
+	}
+	return Token{}
+}
+
+// getQuotedString 获取quote
+func getQuotedString(buf string) string {
+	// This checks for the following patterns:
+	// 1. backtick quoted string using `` to escape
+	// 2. double quoted string using "" or \" to escape
+	// 3. single quoted string using '' or \' to escape
+	start := string(buf[0])
+	switch start {
+	case "\"", "`", "'":
+		reg := fmt.Sprintf(`(^%s[^%s\\]*(?:\\.[^%s\\]*)*(%s|$))+`, start, start, start, start)
+		quotedReg := regexp.MustCompile(reg)
+		if quotedReg.MatchString(buf) {
+			buf = quotedReg.FindString(buf)
+		} else {
+			buf = ""
+		}
+	default:
+		buf = ""
+	}
+	return buf
+}
+
+// Tokenize 序列化token
+func Tokenize(sql string) []Token {
+	var token Token
+	var tokenLength int
+	var tokens []Token
+	tokenCache = make(map[string]Token)
+
+	// Used to make sure the string keeps shrinking on each iteration
+	oldStringLen := len(sql) + 1
+
+	currentLength := len(sql)
+	for currentLength > 0 {
+		// If the string stopped shrinking, there was a problem
+		if oldStringLen <= currentLength {
+			tokens = []Token{
+				{
+					Type: TokenTypeError,
+					Val:  sql,
+				},
+			}
+			return tokens
+		}
+
+		oldStringLen = currentLength
+		cacheKey := ""
+		// Determine if we can use caching
+		if currentLength >= maxCachekeySize {
+			cacheKey = sql[:maxCachekeySize]
+		}
+
+		// See if the token is already cached
+		if _, ok := tokenCache[cacheKey]; ok {
+			// Retrieve from cache
+			token = tokenCache[cacheKey]
+			tokenLength = len(token.Val)
+			cacheHits = cacheHits + 1
+		} else {
+			// Get the next token and the token type
+			token = getNextToken(sql, token)
+			tokenLength = len(token.Val)
+			cacheMisses = cacheMisses + 1
+			// If the token is shorter than the max length, store it in cache
+			if cacheKey != "" && tokenLength < maxCachekeySize {
+				tokenCache[cacheKey] = token
+			}
+		}
+
+		tokens = append(tokens, token)
+
+		// Advance the string
+		sql = sql[tokenLength:]
+		currentLength = currentLength - tokenLength
+	}
+	return tokens
+}
+
+// Compress compress sql
+// this method is inspired by eversql.com
+func Compress(sql string) string {
+	regLineTab := regexp.MustCompile(`(?i)([\n\t])`)
+	regSpace := regexp.MustCompile(`\s\s+`)
+	sql = regSpace.ReplaceAllString(regLineTab.ReplaceAllString(sql, " "), " ")
+	return sql
+}
+
+// SplitStatement SQL切分
+func SplitStatement(buf []byte, delimiter []byte) (string, []byte) {
+	var singleLineComment bool
+	var multiLineComment bool
+	var quoted bool
+	var quoteRune byte
+	var sql string
+
+	for i := 0; i < len(buf); i++ {
+		b := buf[i]
+		// single line comment
+		if b == '-' {
+			if i+2 < len(buf) && buf[i+1] == '-' && buf[i+2] == ' ' {
+				singleLineComment = true
+				i = i + 2
+				continue
+			}
+			if i+2 < len(buf) && i == 0 && buf[i+1] == '-' && (buf[i+2] == '\n' || buf[i+2] == '\r') {
+				sql = "--\n"
+				break
+			}
+		}
+
+		if b == '#' {
+			if !multiLineComment && !quoted && !singleLineComment {
+				singleLineComment = true
+				continue
+			}
+		}
+
+		// new line end single line comment
+		if singleLineComment {
+			if b == '\r' || b == '\n' {
+				sql = string(buf[:i])
+				break
+			}
+		}
+
+		// multi line comment
+		if b == '/' && i+1 < len(buf) && buf[i+1] == '*' {
+			if !multiLineComment && !singleLineComment && !quoted && buf[i+2] != '!' {
+				i = i + 2
+				multiLineComment = true
+				continue
+			}
+		}
+
+		if b == '*' && i+1 < len(buf) && buf[i+1] == '/' {
+			if multiLineComment && !quoted && !singleLineComment {
+				i = i + 2
+				multiLineComment = false
+				continue
+			}
+		}
+
+		// quoted string
+		if b == '`' || b == '\'' || b == '"' {
+			if i > 1 && buf[i-1] != '\\' {
+				if quoted && b == quoteRune {
+					quoted = false
+				} else {
+					quoted = true
+					quoteRune = b
+				}
+			}
+		}
+
+		// delimiter
+		if !quoted && !singleLineComment && !multiLineComment {
+			eof := true
+			for k, c := range delimiter {
+				if len(buf) > i+k && buf[i+k] != c {
+					eof = false
+				}
+			}
+			if eof {
+				i = i + len(delimiter)
+				sql = string(buf[:i])
+				break
+			}
+		}
+
+		// ended of buf
+		if i == len(buf)-1 {
+			sql = string(buf)
+		}
+	}
+	buf = buf[len(sql):]
+	return strings.TrimSuffix(sql, string(delimiter)), buf
+}
+
+// LeftNewLines cal left new lines in space
+func LeftNewLines(buf []byte) int {
+	newLines := 0
+	for _, b := range buf {
+		if !unicode.IsSpace(rune(b)) {
+			break
+		}
+		if b == byte('\n') {
+			newLines++
+		}
+	}
+	return newLines
+}
+
+// NewLines cal all new lines
+func NewLines(buf []byte) int {
+	newLines := 0
+	for _, b := range buf {
+		if b == byte('\n') {
+			newLines++
+		}
+	}
+	return newLines
+}
diff --git a/ast/token_test.go b/ast/token_test.go
new file mode 100644
index 00000000..08ba3ef8
--- /dev/null
+++ b/ast/token_test.go
@@ -0,0 +1,144 @@
+/*
+ * Copyright 2018 Xiaomi, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package ast
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/XiaoMi/soar/common"
+
+	"github.com/kr/pretty"
+)
+
+func TestTokenizer(_ *testing.T) {
+	sqls := []string{
+		"select c1,c2,c3 from t1,t2 join t3 on t1.c1=t2.c1 and t1.c3=t3.c1 where id>1000",
+		"select sourcetable, if(f.lastcontent = ?, f.lastupdate, f.lastcontent) as lastactivity, f.totalcount as activity, type.class as type, (f.nodeoptions & ?) as nounsubscribe from node as f inner join contenttype as type on type.contenttypeid = f.contenttypeid inner join subscribed as sd on sd.did = f.nodeid and sd.userid = ? union all select f.name as title, f.userid as keyval, ? as sourcetable, ifnull(f.lastpost, f.joindate) as lastactivity, f.posts as activity, ? as type, ? as nounsubscribe from user as f inner join userlist as ul on ul.relationid = f.userid and ul.userid = ? where ul.type = ? and ul.aq = ? order by title limit ?",
+		"select c1 from t1 where id>=1000", // test ">="
+		"select SQL_CALC_FOUND_ROWS col from tbl where id>1000",
+		"SELECT * FROM tb WHERE id=?;",
+		"SELECT * FROM tb WHERE id is null;",
+		"SELECT * FROM tb WHERE id is not null;",
+		"SELECT * FROM tb WHERE id between 1 and 3;",
+		"alter table inventory add index idx_store_film` (`store_id`,`film_id`);",
+	}
+	for _, sql := range sqls {
+		pretty.Println(Tokenizer(sql))
+	}
+}
+
+func TestGetQuotedString(t *testing.T) {
+	var str = []string{
+		`"hello world"`,
+		"`hello world`",
+		`'hello world'`,
+		"hello world",
+		`'hello \'world'`,
+		`"hello \"wor\"ld"`,
+		`"hello \"world"`,
+		`""`,
+		`''`,
+		"``",
+		`'hello 'world'`,
+		`"hello "world"`,
+	}
+	for _, s := range str {
+		fmt.Printf("orignal: %s\nquoted: %s\n", s, getQuotedString(s))
+	}
+}
+
+func TestTokenizer2(t *testing.T) {
+	for _, sql := range common.TestSQLs {
+		fmt.Println(sql)
+		fmt.Println(Tokenize(sql))
+	}
+}
+
+func TestCompress(t *testing.T) {
+	for _, sql := range common.TestSQLs {
+		fmt.Println(sql)
+		fmt.Println(Compress(sql))
+	}
+}
+
+func TestFormat(t *testing.T) {
+	for _, sql := range common.TestSQLs {
+		fmt.Println(sql)
+		fmt.Println(format(sql))
+	}
+}
+
+func TestSplitStatement(t *testing.T) {
+	bufs := [][]byte{
+		[]byte("select * from test;hello"),
+		[]byte("select 'asd;fas', col from test;hello"),
+		[]byte("-- select * from test;hello"),
+		[]byte("#select * from test;hello"),
+		[]byte("select * /*comment*/from test;hello"),
+		[]byte("select * /*comment;*/from test;hello"),
+		[]byte(`select * /*comment
+        ;*/
+        from test;hello`),
+		[]byte(`select * from test`),
+	}
+	for _, buf := range bufs {
+		fmt.Println(SplitStatement(buf, []byte(common.Config.Delimiter)))
+	}
+	buf2s := [][]byte{
+		[]byte("select * from test\\Ghello"),
+		[]byte("select 'asd\\Gfas', col from test\\Ghello"),
+		[]byte("-- select * from test\\Ghello"),
+		[]byte("#select * from test\\Ghello"),
+		[]byte("select * /*comment*/from test\\Ghello"),
+		[]byte("select * /*comment;*/from test\\Ghello"),
+		[]byte(`select * /*comment
+        \\G*/
+        from test\\Ghello`),
+	}
+	for _, buf := range buf2s {
+		fmt.Println(SplitStatement(buf, []byte("\\G")))
+	}
+}
+
+func TestLeftNewLines(t *testing.T) {
+	bufs := [][]byte{
+		[]byte(`
+		select * from test;hello`),
+		[]byte(`select * /*comment
+        ;*/
+        from test;hello`),
+		[]byte(`select * from test`),
+	}
+	for _, buf := range bufs {
+		fmt.Println(LeftNewLines(buf))
+	}
+}
+
+func TestNewLines(t *testing.T) {
+	bufs := [][]byte{
+		[]byte(`
+		select * from test;hello`),
+		[]byte(`select * /*comment
+        ;*/
+        from test;hello`),
+		[]byte(`select * from test`),
+	}
+	for _, buf := range bufs {
+		fmt.Println(NewLines(buf))
+	}
+}
diff --git a/common/cases.go b/common/cases.go
new file mode 100644
index 00000000..6775f4c0
--- /dev/null
+++ b/common/cases.go
@@ -0,0 +1,197 @@
+/*
+ * Copyright 2018 Xiaomi, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package common
+
+// TestSQLs 测试SQL大集合
+var TestSQLs []string
+
+func init() {
+	// 所有的SQL都要以分号结尾,-list-test-sqls参数会打印这个list,以分号结尾可方便测试
+	// 如:./soar -list-test-sql | ./soar
+	TestSQLs = []string{
+		//  single equality
+		"SELECT * FROM film WHERE length = 86;",    // index(length)
+		"SELECT * FROM film WHERE length IS NULL;", // index(length)
+		"SELECT * FROM film HAVING title = 'abc';", // 无法使用索引
+
+		//single inequality
+		"SELECT * FROM sakila.film WHERE length >= 60;",   // any of <, <=, >=, >; but not <>, !=, IS NOT NULL"
+		"SELECT * FROM sakila.film WHERE length >= '60';", // Implicit Conversion
+		"SELECT * FROM film WHERE length BETWEEN 60 AND 84;",
+		"SELECT * FROM film WHERE title LIKE 'AIR%';", // but not LIKE '%blah'",
+		"SELECT * FROM film WHERE title IS NOT NULL;",
+
+		// multiple equalities
+		"SELECT * FROM film WHERE length = 114 and title = 'ALABAMA DEVIL';", // index(title,length) or index(length,title)",
+
+		// equality and inequality
+		"SELECT * FROM film WHERE length > 100 and title = 'ALABAMA DEVIL';", // index(title, length)",
+
+		// multiple inequality
+		"SELECT * FROM film WHERE length > 100 and language_id < 10 and title = 'xyz';", // index(d, b) or index(d, c) 依赖数据",
+		"SELECT * FROM film WHERE length > 100 and language_id < 10;",                   // index(b) or index(c)",
+
+		// GROUP BY
+		"SELECT release_year, sum(length) FROM film WHERE length = 123 AND language_id = 1 GROUP BY release_year;",  // INDEX(length, language_id, release_year) or INDEX(language_id, length, release_year)",
+		"SELECT release_year, sum(length) FROM film WHERE length >= 123 GROUP BY release_year;",                     // INDEX(length)",
+		"SELECT release_year, language_id, sum(length) FROM film GROUP BY release_year, language_id;",               // INDEX(release_year, language_id) (no WHERE)",
+		"SELECT release_year, sum(length) FROM film WHERE length = 123 GROUP BY release_year,(length+language_id);", // INDEX(length) expression in GROUP BY, so no use including even release_year.",
+		"SELECT release_year, sum(film_id) FROM film GROUP BY release_year;",                                        // INDEX(`release_year`)
+		"SELECT * FROM address GROUP BY address,district;",                                                          // INDEX(address, district)
+		"SELECT title FROM film WHERE ABS(language_id) = 3 GROUP BY title;",                                         // 无法使用索引
+
+		// ORDER BY
+		"SELECT language_id FROM film WHERE length = 123 GROUP BY release_year ORDER BY language_id;",            //  INDEX(length, release_year) should have stopped with Step 2b",
+		"SELECT release_year FROM film WHERE length = 123 GROUP BY release_year ORDER BY release_year;",          //  INDEX(length, release_year) the release_year will be used for both GROUP BY and ORDER BY",
+		"SELECT * FROM film WHERE length = 123 ORDER BY release_year ASC, language_id DESC;",                     //  INDEX(length) mixture of ASC and DESC.",
+		"SELECT release_year FROM film WHERE length = 123 GROUP BY release_year ORDER BY release_year LIMIT 10;", //  INDEX(length, release_year)",
+		"SELECT * FROM film WHERE length = 123 ORDER BY release_year LIMIT 10;",                                  //  INDEX(length, release_year)",
+		"SELECT * FROM film ORDER BY release_year LIMIT 10;",                                                     //  INDEX(release_year)",
+		"SELECT * FROM film WHERE length > 100 ORDER BY length LIMIT 10;",                                        //  INDEX(length) This "range" is compatible with ORDER BY
+		"SELECT * FROM film WHERE length < 100 ORDER BY length LIMIT 10;",                                        //  INDEX(length) also works
+		"SELECT * FROM customer WHERE address_id in (224,510) ORDER BY last_name;",                               // INDEX(address_id)
+		"SELECT * FROM film WHERE release_year = 2016 AND length != 1 ORDER BY title;",                           // INDEX(`release_year`, `length`, `title`)
+
+		//"Covering" IdxRows
+		"SELECT title FROM film WHERE release_year = 1995;",                               //  INDEX(release_year, title)",
+		"SELECT title, replacement_cost FROM film WHERE language_id = 5 AND length = 70;", //  INDEX(language_id, length, title, replacement_cos film ), title, replacement_cost顺序无关,language_id, length顺序视散粒度情况.
+		"SELECT title FROM film WHERE language_id > 5 AND length > 70;",                   //  INDEX(language_id, length, title) language_id or length first (that's as far as the Algorithm goes), then the other two fields afterwards.
+
+		// equalities and sort
+		"SELECT * FROM film WHERE length = 100 and title = 'xyz' ORDER BY release_year;", // 依赖数据特征,index(length, title, release_year) or index(title, length, release_year)需要评估
+
+		// inequality and sort
+		"SELECT * FROM film WHERE length > 100 and title = 'xyz' ORDER BY release_year;", // 依赖数据特征, index(title, release_year),index(title, length)需要评估
+		"SELECT * FROM film WHERE length > 100 ORDER BY release_year;",                   // 依赖数据特征, index(length),index(release_year)需要评估
+
+		// Join
+		// 内连接 INNER JOIN
+		// 在mysql中,inner join...on , join...on , 逗号...WHERE ,cross join...on是一样的含义。
+		// 但是在标准SQL中,它们并不等价,标准SQL中INNER JOIN与ON共同使用, CROSS JOIN用于其他情况。
+		// 逗号不支持on和using语法, 逗号的优先级要低于INNER JOIN, CROSS JOIN, LEFT JOIN
+		// ON子句的语法格式为:tb1.col1 = tb2.col2列名可以不同,筛选连接后的结果,两表的对应列值相同才在结果集中。
+		// 当模式设计对联接表的列采用了相同的命名样式时,就可以使用 USING 语法来简化 ON 语法
+
+		// join, inner join, cross join等价,优先选择小结果集条件表为驱动表
+		// left [outer] join左表为驱动表
+		// right [outer] join右表为驱动表
+		// 驱动表连接列如果没其他条件可以不考虑加索引,反正是需要foreach
+		// 被驱动表连接列需要加索引。即:left [outer] join的右表连接列需要加索引,right [outer] join的左表连接列需要加索引,inner join结果集较大表的连接列需要加索引
+		// 其他索引添加算法与单表索引优化算法相同
+		// 总结:被驱动表列需要添加索引
+		// 建议:将无索引的表通常作为驱动表
+
+		"SELECT * FROM city a INNER JOIN country b ON a.country_id=b.country_id;",
+
+		// 左外连接 LEFT [OUTER] JOIN
+		"SELECT * FROM city a LEFT JOIN country b ON a.country_id=b.country_id;",
+
+		// 右外连接 RIGHT [OUTER] JOIN
+		"SELECT * FROM city a RIGHT JOIN country b ON a.country_id=b.country_id;",
+
+		// 左连接
+		"SELECT * FROM city a LEFT JOIN country b ON a.country_id=b.country_id WHERE b.last_update IS NULL;",
+
+		// 右连接
+		"SELECT * FROM city a RIGHT JOIN country b ON a.country_id=b.country_id WHERE a.last_update IS NULL;",
+
+		// 全连接 FULL JOIN 因为在mysql中并不支持,所以我们用union实现
+		"SELECT * FROM city a LEFT JOIN country b ON a.country_id=b.country_id " +
+			"UNION " +
+			"SELECT * FROM city a RIGHT JOIN country b ON a.country_id=b.country_id;",
+
+		// 两张表中不共同满足的数据集
+		"SELECT * FROM city a RIGHT JOIN country b ON a.country_id=b.country_id WHERE a.last_update IS NULL " +
+			"UNION " +
+			"SELECT * FROM city a LEFT JOIN country b ON a.country_id=b.country_id WHERE b.last_update IS NULL;",
+
+		// NATURAL JOIN 默认是同名字段完全匹配的INNER JOIN
+		"SELECT country_id, last_update FROM city NATURAL JOIN country;",
+
+		// NATURAL LEFT JOIN
+		"SELECT country_id, last_update FROM city NATURAL LEFT JOIN country;",
+
+		// NATURAL RIGHT JOIN
+		"SELECT country_id, last_update FROM city NATURAL RIGHT JOIN country;",
+
+		// STRAIGHT_JOIN 实际上与内连接 INNER JOIN 表现完全一致,
+		// 不同的是使用了 STRAIGHT_JOIN后指定表载入的顺序,city先于country载入
+		"SELECT a.country_id, a.last_update FROM city a STRAIGHT_JOIN country b ON a.country_id=b.country_id;",
+
+		// SEMI JOIN
+		// 半连接: 当一张表在另一张表找到匹配的记录之后,半连接(semi-join)返回第一张表中的记录。
+		// 与条件连接相反,即使在右节点中找到几条匹配的记录,左节点的表也只会返回一条记录。
+		// 另外,右节点的表一条记录也不会返回。半连接通常使用IN  或 EXISTS 作为连接条件
+		"SELECT d.deptno,d.dname,d.loc FROM scott.dept d WHERE d.deptno IN  (SELECT e.deptno FROM scott.emp e);",
+
+		// Delayed Join
+		// https://www.percona.com/blog/2007/04/06/using-delayed-join-to-optimize-count-and-limit-queries/
+		`SELECT visitor_id, url FROM (SELECT id FROM log WHERE ip="123.45.67.89" order by tsdesc limit 50, 10) I JOIN log ON (I.id=log.id) JOIN url ON (url.id=log.url_id) order by TS desc;`,
+
+		// DELETE
+		"DELETE city, country FROM city INNER JOIN country using (country_id) WHERE city.city_id = 1;",
+		"DELETE city FROM city LEFT JOIN country ON city.country_id = country.country_id WHERE country.country IS NULL;",
+		"DELETE a1, a2 FROM city AS a1 INNER JOIN country AS a2 WHERE a1.country_id=a2.country_id;",
+		"DELETE FROM a1, a2 USING city AS a1 INNER JOIN country AS a2 WHERE a1.country_id=a2.country_id;",
+		"DELETE FROM film WHERE length > 100;",
+
+		// UPDATE
+		"UPDATE city INNER JOIN country USING(country_id) SET city.city = 'Abha', city.last_update = '2006-02-15 04:45:25', country.country = 'Afghanistan' WHERE city.city_id=10;",
+		"UPDATE city INNER JOIN country ON city.country_id = country.country_id INNER JOIN address ON city.city_id = address.city_id SET city.city = 'Abha', city.last_update = '2006-02-15 04:45:25', country.country = 'Afghanistan' WHERE city.city_id=10;",
+		"UPDATE city, country SET city.city = 'Abha', city.last_update = '2006-02-15 04:45:25', country.country = 'Afghanistan' WHERE city.country_id = country.country_id AND city.city_id=10;",
+		"UPDATE film SET length = 10 WHERE language_id = 20;",
+
+		// INSERT
+		"INSERT INTO city (country_id) SELECT country_id FROM country;",
+		"INSERT INTO city (country_id) VALUES (1),(2),(3);",
+		"INSERT INTO city (country_id) VALUES (10);",
+		"INSERT INTO city (country_id) SELECT 10 FROM DUAL;",
+
+		// REPLACE
+		"REPLACE INTO city (country_id) SELECT country_id FROM country;",
+		"REPLACE INTO city (country_id) VALUES (1),(2),(3);",
+		"REPLACE INTO city (country_id) VALUES (10);",
+		"REPLACE INTO city (country_id) SELECT 10 FROM DUAL;",
+
+		// DEPTH
+		"SELECT film_id FROM ( SELECT film_id FROM ( SELECT film_id FROM ( SELECT film_id FROM ( SELECT film_id FROM ( SELECT film_id FROM ( SELECT film_id FROM ( SELECT film_id FROM ( SELECT film_id FROM ( SELECT film_id FROM ( SELECT film_id FROM ( SELECT film_id FROM ( SELECT film_id FROM ( SELECT film_id FROM ( SELECT film_id FROM ( SELECT film_id FROM ( SELECT film_id FROM  film ) film ) film ) film ) film ) film ) film ) film ) film ) film ) film ) film ) film ) film ) film ) film ) film;",
+
+		// SUBQUERY
+		"SELECT * FROM film WHERE language_id = (SELECT language_id FROM language LIMIT 1);",
+		//"SELECT COUNT(*) /* no hint */ FROM t2 WHERE NOT EXISTS (SELECT * FROM t3 WHERE ROW(5 * t2.s1, 77) = (SELECT 50, 11 * s1 FROM t4 UNION SELECT 50, 77 FROM (SELECT * FROM t5) AS t5 ) ) ;",
+		"SELECT * FROM city i left JOIN country o ON i.city_id=o.country_id union SELECT * FROM city i right JOIN country o ON i.city_id=o.country_id;",
+		"SELECT * FROM (SELECT * FROM actor WHERE last_update='2006-02-15 04:34:33' and last_name='CHASE') t WHERE last_update='2006-02-15 04:34:33' and last_name='CHASE' GROUP BY first_name;",
+		"SELECT * FROM city i left JOIN country o ON i.city_id=o.country_id union SELECT * FROM city i right JOIN country o ON i.city_id=o.country_id;",
+		"SELECT * FROM city i left JOIN country o ON i.city_id=o.country_id WHERE o.country_id is null union SELECT * FROM city i right JOIN country o ON i.city_id=o.country_id WHERE i.city_id is null;",
+		"SELECT first_name,last_name,email FROM customer STRAIGHT_JOIN address ON customer.address_id=address.address_id;",
+		"SELECT ID,name FROM (SELECT address FROM customer_list WHERE SID=1 order by phone limit 50,10) a JOIN customer_list l ON (a.address=l.address) JOIN city c ON (c.city=l.city) order by phone desc;",
+
+		// function in conditions
+		"SELECT * FROM film WHERE date(last_update)='2006-02-15';",
+		"SELECT last_update FROM film GROUP BY date(last_update);",
+		"SELECT last_update FROM film order by date(last_update);",
+
+		// CLA.004
+		"SELECT description FROM film WHERE description IN('NEWS','asd') GROUP BY description;",
+
+		// ALTER TABLE ADD INDEX
+		// 已经存在索引的列应该提醒索引已存在
+		"alter table address add index idx_city_id(city_id);",
+		"alter table inventory add index `idx_store_film` (`store_id`,`film_id`);",
+		"alter table inventory add index `idx_store_film` (`store_id`,`film_id`),add index `idx_store_film` (`store_id`,`film_id`),add index `idx_store_film` (`store_id`,`film_id`);",
+	}
+}
diff --git a/common/config.go b/common/config.go
new file mode 100644
index 00000000..212bab4c
--- /dev/null
+++ b/common/config.go
@@ -0,0 +1,822 @@
+/*
+ * Copyright 2018 Xiaomi, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package common
+
+import (
+	"bufio"
+	"bytes"
+	"encoding/json"
+	"flag"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"os"
+	"regexp"
+	"strings"
+
+	"gopkg.in/yaml.v2"
+)
+
+// BlackList 黑名单中的SQL不会被评审
+var BlackList []string
+var hasParsed bool
+
+// Configration 配置文件定义结构体
+type Configration struct {
+	// +++++++++++++++测试环境+++++++++++++++++
+	OnlineDSN               *dsn   `yaml:"online-dsn"`                // 线上环境数据库配置
+	TestDSN                 *dsn   `yaml:"test-dsn"`                  // 测试环境数据库配置
+	AllowOnlineAsTest       bool   `yaml:"allow-online-as-test"`      // 允许Online环境也可以当作Test环境
+	DropTestTemporary       bool   `yaml:"drop-test-temporary"`       // 是否清理Test环境产生的临时库表
+	OnlySyntaxCheck         bool   `yaml:"only-syntax-check"`         // 只做语法检查不输出优化建议
+	SamplingStatisticTarget int    `yaml:"sampling-statistic-target"` // 数据采样因子,对应postgres的default_statistics_target
+	Sampling                bool   `yaml:"sampling"`                  // 数据采样开关
+	Profiling               bool   `yaml:"profiling"`                 // 在开启数据采样的情况下,在测试环境执行进行profile
+	Trace                   bool   `yaml:"trace"`                     // 在开启数据采样的情况下,在测试环境执行进行Trace
+	Explain                 bool   `yaml:"explain"`                   // Explain开关
+	ConnTimeOut             int    `yaml:"conn-time-out"`             // 数据库连接超时时间,单位秒
+	QueryTimeOut            int    `yaml:"query-time-out"`            // 数据库SQL执行超时时间,单位秒
+	Delimiter               string `yaml:"delimiter"`                 // SQL分隔符
+
+	// +++++++++++++++日志相关+++++++++++++++++
+	// 日志级别,这里使用了beego的log包
+	// [0:Emergency, 1:Alert, 2:Critical, 3:Error, 4:Warning, 5:Notice, 6:Informational, 7:Debug]
+	LogLevel int `yaml:"log-level"`
+	// 日志输出位置,默认日志输出到控制台
+	// 目前只支持['console', 'file']两种形式,如非console形式这里需要指定文件的路径,可以是相对路径
+	LogOutput string `yaml:"log-output"`
+	// 优化建议输出格式,目前支持: json, text, markdown格式,如指定其他格式会给pretty.Println的输出
+	ReportType string `yaml:"report-type"`
+	// 当ReportType为html格式时使用的css风格,如不指定会提供一个默认风格。CSS可以是本地文件,也可以是一个URL
+	ReportCSS string `yaml:"report-css"`
+	// 当ReportType为html格式时使用的javascript脚本,如不指定默认会加载SQL pretty使用的javascript。像CSS一样可以是本地文件,也可以是一个URL
+	ReportJavascript string `yaml:"report-javascript"`
+	// 当ReportType为html格式时,HTML的title
+	ReportTitle string `yaml:"report-title"`
+	// blackfriday markdown2html config
+	MarkdownExtensions int `yaml:"markdown-extensions"` // markdown转html支持的扩展包, 参考blackfriday
+	MarkdownHTMLFlags  int `yaml:"markdown-html-flags"` // markdown转html支持的flag, 参考blackfriday, default 0
+
+	// ++++++++++++++优化建议相关++++++++++++++
+	IgnoreRules          []string `yaml:"ignore-rules"`              // 忽略的优化建议规则
+	RewriteRules         []string `yaml:"rewrite-rules"`             // 生效的重写规则
+	BlackList            string   `yaml:"blacklist"`                 // blacklist中的SQL不会被评审,可以是指纹,也可以是正则
+	MaxJoinTableCount    int      `yaml:"max-join-table-count"`      // 单条SQL中JOIN表的最大数量
+	MaxGroupByColsCount  int      `yaml:"max-group-by-cols-count"`   // 单条SQL中GroupBy包含列的最大数量
+	MaxDistinctCount     int      `yaml:"max-distinct-count"`        // 单条SQL中Distinct的最大数量
+	MaxIdxColsCount      int      `yaml:"max-index-cols-count"`      // 复合索引中包含列的最大数量
+	MaxTotalRows         int64    `yaml:"max-total-rows"`            // 计算散粒度时,当数据行数大于 MaxTotalRows即开启数据库保护模式,散粒度返回结果可信度下降
+	MaxQueryCost         int64    `yaml:"max-query-cost"`            // last_query_cost 超过该值时将给予警告
+	SpaghettiQueryLength int      `yaml:"spaghetti-query-length"`    // SQL最大长度警告,超过该长度会给警告
+	AllowDropIndex       bool     `yaml:"allow-drop-index"`          // 允许输出删除重复索引的建议
+	MaxInCount           int      `yaml:"max-in-count"`              // IN()最大数量
+	MaxIdxBytesPerColumn int      `yaml:"max-index-bytes-percolumn"` // 索引中单列最大字节数,默认767
+	MaxIdxBytes          int      `yaml:"max-index-bytes"`           // 索引总长度限制,默认3072
+	TableAllowCharsets   []string `yaml:"table-allow-charsets"`      // Table允许使用的DEFAULT CHARSET
+	TableAllowEngines    []string `yaml:"table-allow-engines"`       // Table允许使用的Engine
+	MaxIdxCount          int      `yaml:"max-index-count"`           // 单张表允许最多索引数
+	MaxColCount          int      `yaml:"max-column-count"`          // 单张表允许最大列数
+	IdxPrefix            string   `yaml:"index-prefix"`              // 普通索引建议使用的前缀
+	UkPrefix             string   `yaml:"unique-key-prefix"`         // 唯一键建议使用的前缀
+	MaxSubqueryDepth     int      `yaml:"max-subquery-depth"`        // 子查询最大尝试
+	MaxVarcharLength     int      `yaml:"max-varchar-length"`        // varchar最大长度
+
+	// ++++++++++++++EXPLAIN检查项+++++++++++++
+	ExplainSQLReportType   string   `yaml:"explain-sql-report-type"`  // EXPLAIN markdown格式输出SQL样式,支持sample, fingerprint, pretty
+	ExplainType            string   `yaml:"explain-type"`             // EXPLAIN方式 [traditional, extended, partitions]
+	ExplainFormat          string   `yaml:"explain-format"`           // FORMAT=[json, traditional]
+	ExplainWarnSelectType  []string `yaml:"explain-warn-select-type"` // 哪些select_type不建议使用
+	ExplainWarnAccessType  []string `yaml:"explain-warn-access-type"` // 哪些access type不建议使用
+	ExplainMaxKeyLength    int      `yaml:"explain-max-keys"`         // 最大key_len
+	ExplainMinPossibleKeys int      `yaml:"explain-min-keys"`         // 最小possible_keys警告
+	ExplainMaxRows         int      `yaml:"explain-max-rows"`         // 最大扫描行数警告
+	ExplainWarnExtra       []string `yaml:"explain-warn-extra"`       // 哪些extra信息会给警告
+	ExplainMaxFiltered     float64  `yaml:"explain-max-filtered"`     // filtered大于该配置给出警告
+	ExplainWarnScalability []string `yaml:"explain-warn-scalability"` // 复杂度警告名单
+	ShowWarnings           bool     `yaml:"show-warnings"`            // explain extended with show warnings
+	ShowLastQueryCost      bool     `yaml:"show-last-query-cost"`     // switch with show status like 'last_query_cost'
+	// ++++++++++++++其他配置项+++++++++++++++
+	Query              string `yaml:"query"`                 // 需要进行调优的SQL
+	ListHeuristicRules bool   `yaml:"list-heuristic-rules"`  // 打印支持的评审规则列表
+	ListRewriteRules   bool   `yaml:"list-rewrite-rules"`    // 打印重写规则
+	ListTestSqls       bool   `yaml:"list-test-sqls"`        // 打印测试case用于测试
+	ListReportTypes    bool   `yaml:"list-report-types"`     // 打印支持的报告输出类型
+	Verbose            bool   `yaml:"verbose"`               // verbose模式,会多输出一些信息
+	DryRun             bool   `yaml:"dry-run"`               // 是否在预演环境执行
+	MaxPrettySQLLength int    `yaml:"max-pretty-sql-length"` // 超出该长度的SQL会转换成指纹输出
+}
+
+// Config 默认设置
+var Config = &Configration{
+	OnlineDSN: &dsn{
+		Schema:  "information_schema",
+		Charset: "utf8mb4",
+		Disable: true,
+		Version: 999,
+	},
+	TestDSN: &dsn{
+		Schema:  "information_schema",
+		Charset: "utf8mb4",
+		Disable: true,
+		Version: 999,
+	},
+	AllowOnlineAsTest:       false,
+	DropTestTemporary:       true,
+	DryRun:                  true,
+	OnlySyntaxCheck:         false,
+	SamplingStatisticTarget: 100,
+	Sampling:                false,
+	Profiling:               false,
+	Trace:                   false,
+	Explain:                 true,
+	ConnTimeOut:             3,
+	QueryTimeOut:            30,
+	Delimiter:               ";",
+
+	MaxJoinTableCount:    5,
+	MaxGroupByColsCount:  5,
+	MaxDistinctCount:     5,
+	MaxIdxColsCount:      5,
+	MaxIdxBytesPerColumn: 767,
+	MaxIdxBytes:          3072,
+	MaxTotalRows:         9999999,
+	MaxQueryCost:         9999,
+	SpaghettiQueryLength: 2048,
+	AllowDropIndex:       false,
+	LogLevel:             3,
+	LogOutput:            "/dev/stderr",
+	ReportType:           "markdown",
+	ReportCSS:            "",
+	ReportJavascript:     "",
+	ReportTitle:          "SQL优化分析报告",
+	BlackList:            "",
+	TableAllowCharsets:   []string{"utf8", "utf8mb4"},
+	TableAllowEngines:    []string{"innodb"},
+	MaxIdxCount:          10,
+	MaxColCount:          40,
+	MaxInCount:           10,
+	IdxPrefix:            "idx_",
+	UkPrefix:             "uk_",
+	MaxSubqueryDepth:     5,
+	MaxVarcharLength:     1024,
+
+	MarkdownExtensions: 94,
+	MarkdownHTMLFlags:  0,
+
+	ExplainSQLReportType:   "pretty",
+	ExplainType:            "extended",
+	ExplainFormat:          "traditional",
+	ExplainWarnSelectType:  []string{""},
+	ExplainWarnAccessType:  []string{"ALL"},
+	ExplainMaxKeyLength:    3,
+	ExplainMinPossibleKeys: 0,
+	ExplainMaxRows:         10000,
+	ExplainWarnExtra:       []string{"Using temporary", "Using filesort"},
+	ExplainMaxFiltered:     100.0,
+	ExplainWarnScalability: []string{"O(n)"},
+	ShowWarnings:           false,
+	ShowLastQueryCost:      false,
+
+	IgnoreRules: []string{
+		"COL.011",
+	},
+	RewriteRules: []string{
+		"delimiter",
+		"orderbynull",
+		"groupbyconst",
+		"dmlorderby",
+		"having",
+		"star2columns",
+		"insertcolumns",
+		"distinctstar",
+	},
+
+	ListHeuristicRules: false,
+	ListRewriteRules:   false,
+	ListTestSqls:       false,
+	ListReportTypes:    false,
+	MaxPrettySQLLength: 1024,
+}
+
+type dsn struct {
+	Addr   string `yaml:"addr"`
+	Schema string `yaml:"schema"`
+
+	// 数据库用户名和密码可以通过系统环境变量的形式赋值
+	User     string `yaml:"user"`
+	Password string `yaml:"password"`
+	Charset  string `yaml:"charset"`
+	Disable  bool   `yaml:"disable"`
+
+	Version int `yaml:"-"` // 版本自动检查,不可配置
+}
+
+// 解析命令行DSN输入
+func parseDSN(odbc string, d *dsn) *dsn {
+	var addr, user, password, schema, charset string
+	if d != nil {
+		addr = d.Addr
+		user = d.User
+		password = d.Password
+		schema = d.Schema
+		charset = d.Charset
+	}
+
+	// 设置为空表示禁用环境
+	odbc = strings.TrimSpace(odbc)
+	if odbc == "" {
+		return &dsn{Disable: true}
+	}
+
+	// username:password@ip:port/dbname
+	l1 := strings.Split(odbc, "@")
+	if len(l1) < 2 {
+		if strings.HasPrefix(l1[0], ":") {
+			// ":port/database"
+			l2 := strings.Split(strings.TrimLeft(l1[0], ":"), "/")
+			if l2[0] == "" {
+				addr = strings.Split(addr, ":")[0] + ":3306"
+				if len(l2) > 1 {
+					schema = strings.Split(l2[1], "?")[0]
+				}
+			} else {
+				addr = strings.Split(addr, ":")[0] + ":" + l2[0]
+				if len(l2) > 1 {
+					schema = strings.Split(l2[1], "?")[0]
+				}
+			}
+		} else if strings.HasPrefix(l1[0], "/") {
+			// "/database"
+			l2 := strings.TrimLeft(l1[0], "/")
+			schema = l2
+		} else {
+			// ip:port/dbname
+			l2 := strings.Split(l1[0], "/")
+			if len(l2) == 2 {
+				addr = l2[0]
+				schema = strings.Split(l2[1], "?")[0]
+			} else {
+				addr = l2[0]
+			}
+		}
+	} else {
+		// user:password
+		l2 := strings.Split(l1[0], ":")
+		if len(l2) == 2 {
+			user = l2[0]
+			password = l2[1]
+		} else {
+			user = l2[0]
+		}
+		// ip:port/dbname
+		l3 := strings.Split(l1[1], "/")
+		if len(l3) == 2 {
+			addr = l3[0]
+			schema = strings.Split(l3[1], "?")[0]
+		} else {
+			addr = l3[0]
+		}
+	}
+
+	// 其他flag参数,目前只支持charset :(
+	if len(strings.Split(odbc, "?")) > 1 {
+		flags := strings.Split(strings.Split(odbc, "?")[1], "&")
+		for _, f := range flags {
+			attr := strings.Split(f, "=")
+			if len(attr) > 1 {
+				arg := strings.TrimSpace(attr[0])
+				val := strings.TrimSpace(attr[1])
+				switch arg {
+				case "charset":
+					charset = val
+				default:
+				}
+			}
+		}
+	}
+
+	// 自动补端口
+	if !strings.Contains(addr, ":") {
+		addr = addr + ":3306"
+	} else {
+		if strings.HasSuffix(addr, ":") {
+			addr = addr + "3306"
+		}
+	}
+
+	// 默认走127.0.0.1
+	if strings.HasPrefix(addr, ":") {
+		addr = "127.0.0.1" + addr
+	}
+
+	// 默认用information_schema库
+	if schema == "" {
+		schema = "information_schema"
+	}
+
+	// 默认utf8mb4使用字符集
+	if charset == "" {
+		charset = "utf8mb4"
+	}
+
+	dsn := &dsn{
+		Addr:     addr,
+		User:     user,
+		Password: password,
+		Schema:   schema,
+		Charset:  charset,
+		Disable:  false,
+		Version:  999,
+	}
+	return dsn
+}
+
+// FormatDSN 格式化打印DSN
+func FormatDSN(env *dsn) string {
+	if env.Disable {
+		return ""
+	}
+	// username:password@ip:port/schema?charset=xxx
+	return fmt.Sprintf("%s:%s@%s/%s?charset=%s", env.User, env.Password, env.Addr, env.Schema, env.Charset)
+}
+
+func version() {
+	fmt.Println("Version:", Version)
+	fmt.Println("Branch:", Branch)
+	fmt.Println("Compile:", Compile)
+	fmt.Println("GitDirty:", GitDirty)
+}
+
+// 因为vitess sqlparser使用了glog中也会使用flag,为了不让用户困扰我们单独写一个usage
+func usage() {
+	regPwd := regexp.MustCompile(`:.*@`)
+	vitessHelp := []string{
+		"-alsologtostderr",
+		"log to standard error as well as files",
+		"-log_backtrace_at value",
+		"when logging hits line file:N, emit a stack trace",
+		"-log_dir string",
+		"If non-empty, write log files in this directory",
+		"-logtostderr",
+		"log to standard error instead of files",
+		"-sql-max-length-errors int",
+		"truncate queries in error logs to the given length (default unlimited)",
+		"-sql-max-length-ui int",
+		"truncate queries in debug UIs to the given length (default 512) (default 512)",
+		"-stderrthreshold value",
+		"logs at or above this threshold go to stderr",
+		"-v value",
+		"log level for V logs",
+		"-vmodule value",
+		"comma-separated list of pattern=N settings for file-filtered logging",
+	}
+
+	// io redirect
+	restoreStdout := os.Stdout
+	restoreStderr := os.Stderr
+	stdin, stdout, _ := os.Pipe()
+	os.Stderr = stdout
+	os.Stdout = stdout
+
+	flag.PrintDefaults()
+
+	// copy the output in a separate goroutine so printing can't block indefinitely
+	outC := make(chan string)
+	go func() {
+		var buf bytes.Buffer
+		_, err := io.Copy(&buf, stdin)
+		if err != nil {
+			fmt.Println(err.Error())
+		}
+		outC <- buf.String()
+	}()
+
+	// back to normal state
+	stdout.Close()
+	os.Stdout = restoreStdout // restoring the real stderr
+	os.Stderr = restoreStderr
+
+	fmt.Printf("Usage of %s:\n", os.Args[0])
+	// reading our temp stdout
+	out := <-outC
+	for _, line := range strings.Split(out, "\n") {
+		found := false
+		for _, ignore := range vitessHelp {
+			if strings.TrimSpace(line) == strings.TrimSpace(ignore) {
+				found = true
+			}
+			if regPwd.MatchString(line) && !Config.Verbose {
+				line = regPwd.ReplaceAllString(line, ":********@")
+			}
+		}
+		if !found {
+			fmt.Println(line)
+		}
+	}
+}
+
+// 加载配置文件
+func (conf *Configration) readConfigFile(path string) error {
+	configFile, err := os.Open(path)
+	if err != nil {
+		Log.Warning("readConfigFile(%s) os.Open failed: %v", path, err)
+		return err
+	}
+	defer configFile.Close()
+
+	content, err := ioutil.ReadAll(configFile)
+	if err != nil {
+		Log.Warning("readConfigFile(%s) ioutil.ReadAll failed: %v", path, err)
+		return err
+	}
+
+	err = yaml.Unmarshal(content, Config)
+	if err != nil {
+		Log.Warning("readConfigFile(%s) yaml.Unmarshal failed: %v", path, err)
+		return err
+	}
+	return nil
+}
+
+// 从命令行参数读配置
+func readCmdFlags() error {
+	if hasParsed {
+		Log.Debug("Skip read cmd flags.")
+		return nil
+	}
+
+	config := flag.String("config", "", "Config file path")
+	// +++++++++++++++测试环境+++++++++++++++++
+	onlineDSN := flag.String("online-dsn", FormatDSN(Config.OnlineDSN), "OnlineDSN, 线上环境数据库配置, username:password@ip:port/schema")
+	testDSN := flag.String("test-dsn", FormatDSN(Config.TestDSN), "TestDSN, 测试环境数据库配置, username:password@ip:port/schema")
+	allowOnlineAsTest := flag.Bool("allow-online-as-test", Config.AllowOnlineAsTest, "AllowOnlineAsTest, 允许线上环境也可以当作测试环境")
+	dropTestTemporary := flag.Bool("drop-test-temporary", Config.DropTestTemporary, "DropTestTemporary, 是否清理测试环境产生的临时库表")
+	onlySyntaxCheck := flag.Bool("only-syntax-check", Config.OnlySyntaxCheck, "OnlySyntaxCheck, 只做语法检查不输出优化建议")
+	profiling := flag.Bool("profiling", Config.Profiling, "Profiling, 开启数据采样的情况下在测试环境执行Profile")
+	trace := flag.Bool("trace", Config.Trace, "Trace, 开启数据采样的情况下在测试环境执行Trace")
+	explain := flag.Bool("explain", Config.Explain, "Explain, 是否开启Exaplin执行计划分析")
+	sampling := flag.Bool("sampling", Config.Sampling, "Sampling, 数据采样开关")
+	samplingStatisticTarget := flag.Int("sampling-statistic-target", Config.SamplingStatisticTarget, "SamplingStatisticTarget, 数据采样因子,对应postgres的default_statistics_target")
+	connTimeOut := flag.Int("conn-time-out", Config.ConnTimeOut, "ConnTimeOut, 数据库连接超时时间,单位秒")
+	queryTimeOut := flag.Int("query-time-out", Config.QueryTimeOut, "QueryTimeOut, 数据库SQL执行超时时间,单位秒")
+	delimiter := flag.String("delimiter", Config.Delimiter, "Delimiter, SQL分隔符")
+	// +++++++++++++++日志相关+++++++++++++++++
+	logLevel := flag.Int("log-level", Config.LogLevel, "LogLevel, 日志级别, [0:Emergency, 1:Alert, 2:Critical, 3:Error, 4:Warning, 5:Notice, 6:Informational, 7:Debug]")
+	logOutput := flag.String("log-output", Config.LogOutput, "LogOutput, 日志输出位置")
+	reportType := flag.String("report-type", Config.ReportType, "ReportType, 化建议输出格式,目前支持: json, text, markdown, html等")
+	reportCSS := flag.String("report-css", Config.ReportCSS, "ReportCSS, 当ReportType为html格式时使用的css风格,如不指定会提供一个默认风格。CSS可以是本地文件,也可以是一个URL")
+	reportJavascript := flag.String("report-javascript", Config.ReportJavascript, "ReportJavascript, 当ReportType为html格式时使用的javascript脚本,如不指定默认会加载SQL pretty使用的javascript。像CSS一样可以是本地文件,也可以是一个URL")
+	reportTitle := flag.String("report-title", Config.ReportTitle, "ReportTitle, 当ReportType为html格式时,HTML的title")
+	// +++++++++++++++markdown+++++++++++++++++
+	markdownExtensions := flag.Int("markdown-extensions", Config.MarkdownExtensions, "MarkdownExtensions, markdown转html支持的扩展包, 参考blackfriday")
+	markdownHTMLFlags := flag.Int("markdown-html-flags", Config.MarkdownHTMLFlags, "MarkdownHTMLFlags, markdown转html支持的flag, 参考blackfriday")
+	// ++++++++++++++优化建议相关++++++++++++++
+	ignoreRules := flag.String("ignore-rules", strings.Join(Config.IgnoreRules, ","), "IgnoreRules, 忽略的优化建议规则")
+	rewriteRules := flag.String("rewrite-rules", strings.Join(Config.RewriteRules, ","), "RewriteRules, 生效的重写规则")
+	blackList := flag.String("blacklist", Config.BlackList, "blacklist中的SQL不会被评审,可以是指纹,也可以是正则")
+	maxJoinTableCount := flag.Int("max-join-table-count", Config.MaxJoinTableCount, "MaxJoinTableCount, 单条SQL中JOIN表的最大数量")
+	maxGroupByColsCount := flag.Int("max-group-by-cols-count", Config.MaxGroupByColsCount, "MaxGroupByColsCount, 单条SQL中GroupBy包含列的最大数量")
+	maxDistinctCount := flag.Int("max-distinct-count", Config.MaxDistinctCount, "MaxDistinctCount, 单条SQL中Distinct的最大数量")
+	maxIdxColsCount := flag.Int("max-index-cols-count", Config.MaxIdxColsCount, "MaxIdxColsCount, 复合索引中包含列的最大数量")
+	maxTotalRows := flag.Int64("max-total-rows", Config.MaxTotalRows, "MaxTotalRows, 计算散粒度时,当数据行数大于MaxTotalRows即开启数据库保护模式,不计算散粒度")
+	maxQueryCost := flag.Int64("max-query-cost", Config.MaxQueryCost, "MaxQueryCost, last_query_cost 超过该值时将给予警告")
+	spaghettiQueryLength := flag.Int("spaghetti-query-length", Config.SpaghettiQueryLength, "SpaghettiQueryLength, SQL最大长度警告,超过该长度会给警告")
+	allowDropIdx := flag.Bool("allow-drop-index", Config.AllowDropIndex, "AllowDropIndex, 允许输出删除重复索引的建议")
+	maxInCount := flag.Int("max-in-count", Config.MaxInCount, "MaxInCount, IN()最大数量")
+	maxIdxBytesPerColumn := flag.Int("max-index-bytes-percolumn", Config.MaxIdxBytesPerColumn, "MaxIdxBytesPerColumn, 索引中单列最大字节数")
+	maxIdxBytes := flag.Int("max-index-bytes", Config.MaxIdxBytes, "MaxIdxBytes, 索引总长度限制")
+	tableAllowCharsets := flag.String("table-allow-charsets", strings.ToLower(strings.Join(Config.TableAllowCharsets, ",")), "TableAllowCharsets")
+	tableAllowEngines := flag.String("table-allow-engines", strings.ToLower(strings.Join(Config.TableAllowEngines, ",")), "TableAllowEngines")
+	maxIdxCount := flag.Int("max-index-count", Config.MaxIdxCount, "MaxIdxCount, 单表最大索引个数")
+	maxColCount := flag.Int("max-column-count", Config.MaxColCount, "MaxColCount, 单表允许的最大列数")
+	idxPrefix := flag.String("index-prefix", Config.IdxPrefix, "IdxPrefix")
+	ukPrefix := flag.String("unique-key-prefix", Config.UkPrefix, "UkPrefix")
+	maxSubqueryDepth := flag.Int("max-subquery-depth", Config.MaxSubqueryDepth, "MaxSubqueryDepth")
+	maxVarcharLength := flag.Int("max-varchar-length", Config.MaxVarcharLength, "MaxVarcharLength")
+	// ++++++++++++++EXPLAIN检查项+++++++++++++
+	explainSQLReportType := flag.String("explain-sql-report-type", strings.ToLower(Config.ExplainSQLReportType), "ExplainSQLReportType [pretty, sample, fingerprint]")
+	explainType := flag.String("explain-type", strings.ToLower(Config.ExplainType), "ExplainType [extended, partitions, traditional]")
+	explainFormat := flag.String("explain-format", strings.ToLower(Config.ExplainFormat), "ExplainFormat [json, traditional]")
+	explainWarnSelectType := flag.String("explain-warn-select-type", strings.Join(Config.ExplainWarnSelectType, ","), "ExplainWarnSelectType, 哪些select_type不建议使用")
+	explainWarnAccessType := flag.String("explain-warn-access-type", strings.Join(Config.ExplainWarnAccessType, ","), "ExplainWarnAccessType, 哪些access type不建议使用")
+	explainMaxKeyLength := flag.Int("explain-max-keys", Config.ExplainMaxKeyLength, "ExplainMaxKeyLength, 最大key_len")
+	explainMinPossibleKeys := flag.Int("explain-min-keys", Config.ExplainMinPossibleKeys, "ExplainMinPossibleKeys, 最小possible_keys警告")
+	explainMaxRows := flag.Int("explain-max-rows", Config.ExplainMaxRows, "ExplainMaxRows, 最大扫描行数警告")
+	explainWarnExtra := flag.String("explain-warn-extra", strings.Join(Config.ExplainWarnExtra, ","), "ExplainWarnExtra, 哪些extra信息会给警告")
+	explainMaxFiltered := flag.Float64("explain-max-filtered", Config.ExplainMaxFiltered, "ExplainMaxFiltered, filtered大于该配置给出警告")
+	explainWarnScalability := flag.String("explain-warn-scalability", strings.Join(Config.ExplainWarnScalability, ","), "ExplainWarnScalability, 复杂度警告名单, 支持O(n),O(log n),O(1),O(?)")
+	showWarnings := flag.Bool("show-warnings", Config.ShowWarnings, "ShowWarnings")
+	showLastQueryCost := flag.Bool("show-last-query-cost", Config.ShowLastQueryCost, "ShowLastQueryCost")
+	// +++++++++++++++++其他+++++++++++++++++++
+	printConfig := flag.Bool("print-config", false, "Print configs")
+	ver := flag.Bool("version", false, "Print version info")
+	query := flag.String("query", Config.Query, "Queries for analyzing")
+	listHeuristicRules := flag.Bool("list-heuristic-rules", Config.ListHeuristicRules, "ListHeuristicRules, 打印支持的评审规则列表")
+	listRewriteRules := flag.Bool("list-rewrite-rules", Config.ListRewriteRules, "ListRewriteRules, 打印支持的重写规则列表")
+	listTestSQLs := flag.Bool("list-test-sqls", Config.ListTestSqls, "ListTestSqls, 打印测试case用于测试")
+	listReportTypes := flag.Bool("list-report-types", Config.ListReportTypes, "ListReportTypes, 打印支持的报告输出类型")
+	verbose := flag.Bool("verbose", Config.Verbose, "Verbose")
+	dryrun := flag.Bool("dry-run", Config.DryRun, "是否在预演环境执行")
+	maxPrettySQLLength := flag.Int("max-pretty-sql-length", Config.MaxPrettySQLLength, "MaxPrettySQLLength, 超出该长度的SQL会转换成指纹输出")
+	// 一个不存在log-level,用于更新usage。
+	// 因为vitess里面也用了flag,这些vitess的参数我们不需要关注
+	if !Config.Verbose {
+		flag.Usage = usage
+	}
+	flag.Parse()
+
+	if *config != "" {
+		err := Config.readConfigFile(*config)
+		if err != nil {
+			fmt.Println(err.Error())
+		}
+	}
+
+	Config.OnlineDSN = parseDSN(*onlineDSN, Config.OnlineDSN)
+	Config.TestDSN = parseDSN(*testDSN, Config.OnlineDSN)
+	Config.AllowOnlineAsTest = *allowOnlineAsTest
+	Config.DropTestTemporary = *dropTestTemporary
+	Config.OnlySyntaxCheck = *onlySyntaxCheck
+	Config.Profiling = *profiling
+	Config.Trace = *trace
+	Config.Explain = *explain
+	Config.Sampling = *sampling
+	Config.SamplingStatisticTarget = *samplingStatisticTarget
+	Config.ConnTimeOut = *connTimeOut
+	Config.QueryTimeOut = *queryTimeOut
+
+	Config.LogLevel = *logLevel
+	if strings.HasPrefix(*logOutput, "/") {
+		Config.LogOutput = *logOutput
+	} else {
+		if BaseDir == "" {
+			Config.LogOutput = *logOutput
+		} else {
+			Config.LogOutput = BaseDir + "/" + *logOutput
+		}
+	}
+	Config.ReportType = strings.ToLower(*reportType)
+	Config.ReportCSS = *reportCSS
+	Config.ReportJavascript = *reportJavascript
+	Config.ReportTitle = *reportTitle
+	Config.MarkdownExtensions = *markdownExtensions
+	Config.MarkdownHTMLFlags = *markdownHTMLFlags
+	Config.IgnoreRules = strings.Split(*ignoreRules, ",")
+	Config.RewriteRules = strings.Split(*rewriteRules, ",")
+	*blackList = strings.TrimSpace(*blackList)
+	if strings.HasPrefix(*blackList, "/") || *blackList == "" {
+		Config.BlackList = *blackList
+	} else {
+		Config.BlackList = BaseDir + "/" + *blackList
+	}
+	Config.MaxJoinTableCount = *maxJoinTableCount
+	Config.MaxGroupByColsCount = *maxGroupByColsCount
+	Config.MaxDistinctCount = *maxDistinctCount
+
+	if *maxIdxColsCount < 16 {
+		Config.MaxIdxColsCount = *maxIdxColsCount
+	} else {
+		Config.MaxIdxColsCount = 16
+	}
+
+	Config.MaxIdxBytesPerColumn = *maxIdxBytesPerColumn
+	Config.MaxIdxBytes = *maxIdxBytes
+	Config.TableAllowCharsets = strings.Split(strings.ToLower(*tableAllowCharsets), ",")
+	Config.TableAllowEngines = strings.Split(strings.ToLower(*tableAllowEngines), ",")
+	Config.MaxIdxCount = *maxIdxCount
+	Config.MaxColCount = *maxColCount
+	Config.IdxPrefix = *idxPrefix
+	Config.UkPrefix = *ukPrefix
+	Config.MaxSubqueryDepth = *maxSubqueryDepth
+	Config.MaxTotalRows = *maxTotalRows
+	Config.MaxQueryCost = *maxQueryCost
+	Config.AllowDropIndex = *allowDropIdx
+	Config.MaxInCount = *maxInCount
+	Config.SpaghettiQueryLength = *spaghettiQueryLength
+	Config.Query = *query
+	Config.Delimiter = *delimiter
+
+	Config.ExplainSQLReportType = strings.ToLower(*explainSQLReportType)
+	Config.ExplainType = strings.ToLower(*explainType)
+	Config.ExplainFormat = strings.ToLower(*explainFormat)
+	Config.ExplainWarnSelectType = strings.Split(*explainWarnSelectType, ",")
+	Config.ExplainWarnAccessType = strings.Split(*explainWarnAccessType, ",")
+	Config.ExplainMaxKeyLength = *explainMaxKeyLength
+	Config.ExplainMinPossibleKeys = *explainMinPossibleKeys
+	Config.ExplainMaxRows = *explainMaxRows
+	Config.ExplainWarnExtra = strings.Split(*explainWarnExtra, ",")
+	Config.ExplainMaxFiltered = *explainMaxFiltered
+	Config.ExplainWarnScalability = strings.Split(*explainWarnScalability, ",")
+	Config.ShowWarnings = *showWarnings
+	Config.ShowLastQueryCost = *showLastQueryCost
+	Config.ListHeuristicRules = *listHeuristicRules
+	Config.ListRewriteRules = *listRewriteRules
+	Config.ListTestSqls = *listTestSQLs
+	Config.ListReportTypes = *listReportTypes
+	Config.Verbose = *verbose
+	Config.DryRun = *dryrun
+	Config.MaxPrettySQLLength = *maxPrettySQLLength
+	Config.MaxVarcharLength = *maxVarcharLength
+
+	if *ver {
+		version()
+		os.Exit(0)
+	}
+
+	if *printConfig {
+		// 打印配置的时候密码不显示
+		if !Config.Verbose {
+			Config.OnlineDSN.Password = "********"
+			Config.TestDSN.Password = "********"
+		}
+		data, _ := yaml.Marshal(Config)
+		fmt.Print(string(data))
+		os.Exit(0)
+	}
+
+	hasParsed = true
+	return nil
+}
+
+// ParseConfig 加载配置文件和命令行参数
+func ParseConfig(configFile string) error {
+	var err error
+	var configs []string
+	// 指定了配置文件优先读配置文件,未指定配置文件按如下顺序加载,先找到哪个加载哪个
+	if configFile == "" {
+		configs = []string{
+			"/etc/soar.yaml",
+			BaseDir + "/etc/soar.yaml",
+			BaseDir + "/soar.yaml",
+		}
+	} else {
+		configs = []string{
+			configFile,
+		}
+	}
+
+	for _, config := range configs {
+		if _, err = os.Stat(config); err == nil {
+			err = Config.readConfigFile(config)
+			if err != nil {
+				Log.Error("ParseConfig Config.readConfigFile Error: %v", err)
+			}
+			break
+		}
+	}
+
+	err = readCmdFlags()
+	if err != nil {
+		Log.Error("ParseConfig readCmdFlags Error: %v", err)
+	}
+
+	// parse blacklist & ignore blacklist file parse error
+	if _, e := os.Stat(Config.BlackList); e == nil {
+		var blFd *os.File
+		blFd, err = os.Open(Config.BlackList)
+		if err == nil {
+			bl := bufio.NewReader(blFd)
+			for {
+				rule, e := bl.ReadString('\n')
+				if e != nil {
+					break
+				}
+				rule = strings.TrimSpace(rule)
+				if strings.HasPrefix(rule, "#") || rule == "" {
+					continue
+				}
+				BlackList = append(BlackList, rule)
+			}
+		}
+		defer blFd.Close()
+	}
+	LoggerInit()
+	return err
+}
+
+// ReportType 元数据结构定义
+type ReportType struct {
+	Name        string `json:"Name"`
+	Description string `json:"Description"`
+	Example     string `json:"Example"`
+}
+
+// ReportTypes 命令行-report-type支持的形式
+var ReportTypes = []ReportType{
+	{
+		Name:        "lint",
+		Description: "参考sqlint格式,以插件形式集成到代码编辑器,显示输出更加友好",
+		Example:     `soar -report-type lint -query test.sql`,
+	},
+	{
+		Name:        "markdown",
+		Description: "该格式为默认输出格式,以markdown格式展现,可以用网页浏览器插件直接打开,也可以用markdown编辑器打开",
+		Example:     `echo "select * from film" | soar`,
+	},
+	{
+		Name:        "rewrite",
+		Description: "SQL重写功能,配合-rewrite-rules参数一起使用,可以通过-list-rewrite-rules查看所有支持的SQL重写规则",
+		Example:     `echo "select * from film" | soar -rewrite-rules star2columns,delimiter -report-type rewrite`,
+	},
+	{
+		Name:        "ast",
+		Description: "输出SQL的抽象语法树,主要用于测试",
+		Example:     `echo "select * from film" | soar -report-type ast`,
+	},
+	{
+		Name:        "tiast",
+		Description: "输出SQL的TiDB抽象语法树,主要用于测试",
+		Example:     `echo "select * from film" | soar -report-type tiast`,
+	},
+	{
+		Name:        "fingerprint",
+		Description: "输出SQL的指纹",
+		Example:     `echo "select * from film where language_id=1" | soar -report-type fingerprint`,
+	},
+	{
+		Name:        "md2html",
+		Description: "markdown格式转html格式小工具",
+		Example:     `soar -list-heuristic-rules | soar -report-type md2html > heuristic_rules.html`,
+	},
+	{
+		Name:        "explain-digest",
+		Description: "输入为EXPLAIN的表格,JSON或Vertical格式,对其进行分析,给出分析结果",
+		Example: `soar -report-type explain-digest << EOF
++----+-------------+-------+------+---------------+------+---------+------+------+-------+
+| id | select_type | table | type | possible_keys | key  | key_len | ref  | rows | Extra |
++----+-------------+-------+------+---------------+------+---------+------+------+-------+
+|  1 | SIMPLE      | film  | ALL  | NULL          | NULL | NULL    | NULL | 1131 |       |
++----+-------------+-------+------+---------------+------+---------+------+------+-------+
+EOF`,
+	},
+	{
+		Name:        "duplicate-key-checker",
+		Description: "对OnlineDsn中指定的DB进行索引重复检查",
+		Example:     `soar -report-type duplicate-key-checker -online-dsn user:passwd@127.0.0.1:3306/db`,
+	},
+	{
+		Name:        "html",
+		Description: "以HTML格式输出报表",
+		Example:     `echo "select * from film" | soar -report-type html`,
+	},
+	{
+		Name:        "json",
+		Description: "输出JSON格式报表,方便应用程序处理",
+		Example:     `echo "select * from film" | soar -report-type json`,
+	},
+	{
+		Name:        "tokenize",
+		Description: "对SQL进行切词,主要用于测试",
+		Example:     `echo "select * from film" | soar -report-type tokenize`,
+	},
+	{
+		Name:        "compress",
+		Description: "SQL压缩小工具,使用内置SQL压缩逻辑,测试中的功能",
+		Example: `echo "select
+*
+from
+  film" | soar -report-type compress`,
+	},
+	{
+		Name:        "pretty",
+		Description: "使用kr/pretty打印报告,主要用于测试",
+		Example:     `echo "select * from film" | soar -report-type pretty`,
+	},
+	{
+		Name:        "remove-comment",
+		Description: "去除SQL语句中的注释,支持单行多行注释的去除",
+		Example:     `echo "select/*comment*/ * from film" | soar -report-type remove-comment`,
+	},
+}
+
+// ListReportTypes 查看所有支持的report-type
+func ListReportTypes() {
+	switch Config.ReportType {
+	case "json":
+		js, err := json.MarshalIndent(ReportTypes, "", "  ")
+		if err == nil {
+			fmt.Println(string(js))
+		}
+	default:
+		fmt.Print("# 支持的报告类型\n\n[toc]\n\n")
+		for _, r := range ReportTypes {
+			fmt.Print("## ", MarkdownEscape(r.Name),
+				"\n* **Description**:", r.Description+"\n",
+				"\n* **Example**:\n\n```bash\n", r.Example, "\n```\n")
+		}
+	}
+}
diff --git a/common/config_test.go b/common/config_test.go
new file mode 100644
index 00000000..80dcf7d7
--- /dev/null
+++ b/common/config_test.go
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2018 Xiaomi, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package common
+
+import (
+	"flag"
+	"testing"
+
+	"github.com/kr/pretty"
+)
+
+var update = flag.Bool("update", false, "update .golden files")
+
+func TestParseConfig(t *testing.T) {
+	err := ParseConfig("")
+	if err != nil {
+		t.Error("sqlparser.Parse Error:", err)
+	}
+}
+
+func TestReadConfigFile(t *testing.T) {
+	if Config == nil {
+		Config = new(Configration)
+	}
+	Config.readConfigFile("../soar.yaml")
+}
+
+func TestParseDSN(t *testing.T) {
+	var dsns = []string{
+		"",
+		"user:password@hostname:3307/database",
+		"user:password@hostname:3307",
+		"user:password@hostname:/database",
+		"user:password@:3307/database",
+		"user:password@",
+		"hostname:3307/database",
+		"@hostname:3307/database",
+		"@hostname",
+		"hostname",
+		"@/database",
+		"@hostname:3307",
+		"@:3307/database",
+		":3307/database",
+		"/database",
+	}
+
+	GoldenDiff(func() {
+		for _, dsn := range dsns {
+			pretty.Println(parseDSN(dsn, nil))
+		}
+	}, t.Name(), update)
+}
+
+func TestListReportTypes(t *testing.T) {
+	err := GoldenDiff(func() { ListReportTypes() }, t.Name(), update)
+	if nil != err {
+		t.Fatal(err)
+	}
+}
diff --git a/common/doc.go b/common/doc.go
new file mode 100644
index 00000000..95b0973c
--- /dev/null
+++ b/common/doc.go
@@ -0,0 +1,18 @@
+/*
+ * Copyright 2018 Xiaomi, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Package common contain many useful functions for logging, formatting and so on.
+package common
diff --git a/common/example_test.go b/common/example_test.go
new file mode 100644
index 00000000..8baeb710
--- /dev/null
+++ b/common/example_test.go
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2018 Xiaomi, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package common
+
+import "fmt"
+
+func ExampleFormatDSN() {
+	dsxExp := &dsn{
+		Addr:     "127.0.0.1:3306",
+		Schema:   "mysql",
+		User:     "root",
+		Password: "1t'sB1g3rt",
+		Charset:  "utf8mb4",
+		Disable:  false,
+	}
+
+	// 根据 &dsn 生成 dsnStr
+	fmt.Println(FormatDSN(dsxExp))
+
+	// Output: root:1t'sB1g3rt@127.0.0.1:3306/mysql?charset=utf8mb4
+}
+
+func ExampleIsColsPart() {
+	// IsColsPart() 会 按照顺序 检查两个Column队列是否是包含(或相等)关系。
+	a := []*Column{{Name: "1"}, {Name: "2"}, {Name: "3"}}
+	b := []*Column{{Name: "1"}, {Name: "2"}}
+	c := []*Column{{Name: "1"}, {Name: "3"}}
+	d := []*Column{{Name: "1"}, {Name: "2"}, {Name: "3"}, {Name: "4"}}
+
+	ab := IsColsPart(a, b)
+	ac := IsColsPart(a, c)
+	ad := IsColsPart(a, d)
+
+	fmt.Println(ab, ac, ad)
+	// Output: true false true
+}
+
+func ExampleSortedKey() {
+	ages := map[string]int{
+		"a": 1,
+		"c": 3,
+		"d": 4,
+		"b": 2,
+	}
+	for _, name := range SortedKey(ages) {
+		fmt.Print(ages[name])
+	}
+	// Output: 1234
+}
diff --git a/common/logger.go b/common/logger.go
new file mode 100644
index 00000000..2d71f856
--- /dev/null
+++ b/common/logger.go
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2018 Xiaomi, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package common
+
+import (
+	"fmt"
+	"regexp"
+	"runtime"
+	"strings"
+
+	"github.com/astaxie/beego/logs"
+)
+
+// Log 使用beego的log库
+var Log *logs.BeeLogger
+
+// BaseDir 日志打印在binary的根路径
+var BaseDir string
+
+func init() {
+	Log = logs.NewLogger(0)
+	Log.EnableFuncCallDepth(true)
+}
+
+// LoggerInit Log配置初始化
+func LoggerInit() {
+	Log.SetLevel(Config.LogLevel)
+	if Config.LogOutput == "console" {
+		err := Log.SetLogger("console")
+		if err != nil {
+			fmt.Println(err.Error())
+		}
+	} else {
+		err := Log.SetLogger("file", fmt.Sprintf(`{"filename":"%s","level":7,"maxlines":0,"maxsize":0,"daily":false,"maxdays":0}`, Config.LogOutput))
+		if err != nil {
+			fmt.Println(err.Error())
+		}
+	}
+}
+
+// Caller returns the caller of the function that called it :)
+// https://stackoverflow.com/questions/35212985/is-it-possible-get-information-about-caller-function-in-golang
+func Caller() string {
+	// we get the callers as uintptrs - but we just need 1
+	fpcs := make([]uintptr, 1)
+
+	// skip 3 levels to get to the caller of whoever called Caller()
+	n := runtime.Callers(3, fpcs)
+	if n == 0 {
+		return "n/a" // proper error her would be better
+	}
+
+	// get the info of the actual function that's in the pointer
+	fun := runtime.FuncForPC(fpcs[0] - 1)
+	if fun == nil {
+		return "n/a"
+	}
+
+	// return its name
+	return fun.Name()
+}
+
+// GetFunctionName 获取调当前函数名
+func GetFunctionName() string {
+	// Skip this function, and fetch the PC and file for its parent
+	pc, _, _, _ := runtime.Caller(1)
+	// Retrieve a Function object this functions parent
+	functionObject := runtime.FuncForPC(pc)
+	// Regex to extract just the function name (and not the module path)
+	extractFnName := regexp.MustCompile(`^.*\.(.*)$`)
+	fnName := extractFnName.ReplaceAllString(functionObject.Name(), "$1")
+	return fnName
+}
+
+// fileName get filename from path
+func fileName(original string) string {
+	i := strings.LastIndex(original, "/")
+	if i == -1 {
+		return original
+	}
+	return original[i+1:]
+}
+
+// LogIfError 简化if err != nil打Error日志代码长度
+func LogIfError(err error, format string, v ...interface{}) {
+	if err != nil {
+		_, fn, line, _ := runtime.Caller(1)
+		if format == "" {
+			format = "[%s:%d] %s"
+			Log.Error(format, fileName(fn), line, err.Error())
+		} else {
+			format = "[%s:%d] " + format + " Error: %s"
+			Log.Error(format, fileName(fn), line, v, err.Error())
+		}
+	}
+}
+
+// LogIfWarn 简化if err != nil打Warn日志代码长度
+func LogIfWarn(err error, format string, v ...interface{}) {
+	if err != nil {
+		_, fn, line, _ := runtime.Caller(1)
+		if format == "" {
+			format = "[%s:%d] %s"
+			Log.Warn(format, fileName(fn), line, err.Error())
+		} else {
+			format = "[%s:%d] " + format + " Error: %s"
+			Log.Warn(format, fileName(fn), line, v, err.Error())
+		}
+	}
+}
diff --git a/common/logger_test.go b/common/logger_test.go
new file mode 100644
index 00000000..d2e3a122
--- /dev/null
+++ b/common/logger_test.go
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2018 Xiaomi, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package common
+
+import (
+	"errors"
+	"testing"
+)
+
+func init() {
+	BaseDir = DevPath
+}
+
+func TestLogger(t *testing.T) {
+	Log.Info("info")
+	Log.Debug("debug")
+	Log.Warning("warning")
+	Log.Error("error")
+}
+
+func TestCaller(t *testing.T) {
+	caller := Caller()
+	if caller != "testing.tRunner" {
+		t.Error("get caller failer")
+	}
+}
+
+func TestGetFunctionName(t *testing.T) {
+	f := GetFunctionName()
+	if f != "TestGetFunctionName" {
+		t.Error("get functionname failer")
+	}
+}
+
+func TestIfError(t *testing.T) {
+	err := errors.New("test")
+	LogIfError(err, "")
+	LogIfError(err, "func %s", "func_test")
+}
+
+func TestIfWarn(t *testing.T) {
+	err := errors.New("test")
+	LogIfWarn(err, "")
+	LogIfWarn(err, "func %s", "func_test")
+}
diff --git a/common/markdown.go b/common/markdown.go
new file mode 100644
index 00000000..dd107f8c
--- /dev/null
+++ b/common/markdown.go
@@ -0,0 +1,156 @@
+/*
+ * Copyright 2018 Xiaomi, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package common
+
+import (
+	"encoding/base64"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"os"
+	"strings"
+
+	"github.com/russross/blackfriday"
+)
+
+// BuiltinCSS 内置HTML风格
+var BuiltinCSS = `
+a:link,a:visited{text-decoration:none}h3,h4{margin-top:2em}h5,h6{margin-top:20px}h3,h4,h5,h6{margin-bottom:.5em;color:#000}body,h1,h2,h3,h4,h5,h6{color:#000}ol,ul{margin:0 0 0 30px;padding:0 0 12px 6px}ol,ol ol{list-style-position:outside}table td p,table th p{margin-bottom:0}input,select{vertical-align:middle;padding:0}h5,h6,input,select{padding:0}hr,table,textarea{width:100%}body{margin:20px auto;width:800px;background-color:#fff;font:13px "Myriad Pro","Lucida Grande",Lucida,Verdana,sans-serif}h1,table th p{font-weight:700}a:link{color:#00f}a:visited{color:#00a}a:active,a:hover{color:#f60;text-decoration:underline}* html code,* html pre{font-size:101%}code,pre{font-size:11px;font-family:monaco,courier,consolas,monospace}pre{border:1px solid #c7cfd5;background:#f1f5f9;margin:20px 0;padding:8px;text-align:left}hr{color:#919699;size:1;noshade:"noshade"}h1,h2,h3,h4,h5,h6{font-family:"Myriad Pro","Lucida Grande",Lucida,Verdana,sans-serif;font-weight:700}h1{margin-top:1em;margin-bottom:25px;font-size:30px}h2{margin-top:2.5em;font-size:24px;padding-bottom:2px;border-bottom:1px solid #919699}h3{font-size:17px}h4{font-size:15px}h5{font-size:13px}h6{font-size:11px}table td,table th{font-size:12px;border-bottom:1px solid #919699;border-right:1px solid #919699}p{margin-top:0;margin-bottom:10px}ul{list-style:square}li{margin-top:7px}ol{list-style-type:decimal}ol ol{list-style-type:lower-alpha;margin:7px 0 0 30px;padding:0 0 0 10px}ul ul{margin-left:40px;padding:0 0 0 6px}li>p{display:inline}li>a+p,li>p+p{display:block}table{border-top:1px solid #919699;border-left:1px solid #919699;border-spacing:0}table th{padding:4px 8px;background:#E2E2E2}table td{padding:8px;vertical-align:top}table td p+p,table td p+p+p{margin-top:5px}form{margin:0}button{margin:3px 0 10px}input{margin:0 0 5px}select{margin:0 0 3px}textarea{margin:0 0 10px}
+`
+
+// BuiltinJavascript 内置SQL美化Javascript脚本
+var BuiltinJavascript = `IWZ1bmN0aW9uKGUsRSl7Im9iamVjdCI9PXR5cGVvZiBleHBvcnRzJiYib2JqZWN0Ij09dHlwZW9mIG1vZHVsZT9tb2R1bGUuZXhwb3J0cz1FKCk6ImZ1bmN0aW9uIj09dHlwZW9mIGRlZmluZSYmZGVmaW5lLmFtZD9kZWZpbmUoW10sRSk6Im9iamVjdCI9PXR5cGVvZiBleHBvcnRzP2V4cG9ydHMuc3FsRm9ybWF0dGVyPUUoKTplLnNxbEZvcm1hdHRlcj1FKCl9KHRoaXMsZnVuY3Rpb24oKXtyZXR1cm4gZnVuY3Rpb24oZSl7ZnVuY3Rpb24gRShuKXtpZih0W25dKXJldHVybiB0W25dLmV4cG9ydHM7dmFyIHI9dFtuXT17ZXhwb3J0czp7fSxpZDpuLGxvYWRlZDohMX07cmV0dXJuIGVbbl0uY2FsbChyLmV4cG9ydHMscixyLmV4cG9ydHMsRSksci5sb2FkZWQ9ITAsci5leHBvcnRzfXZhciB0PXt9O3JldHVybiBFLm09ZSxFLmM9dCxFLnA9IiIsRSgwKX0oW2Z1bmN0aW9uKGUsRSx0KXsidXNlIHN0cmljdCI7ZnVuY3Rpb24gbihlKXtyZXR1cm4gZSYmZS5fX2VzTW9kdWxlP2U6eyJkZWZhdWx0IjplfX1FLl9fZXNNb2R1bGU9ITA7dmFyIHI9dCgxOCksVD1uKHIpLFI9dCgxOSksbz1uKFIpLE49dCgyMCksQT1uKE4pLEk9dCgyMSksTz1uKEkpO0VbImRlZmF1bHQiXT17Zm9ybWF0OmZ1bmN0aW9uKGUsRSl7c3dpdGNoKEU9RXx8e30sRS5sYW5ndWFnZSl7Y2FzZSJkYjIiOnJldHVybiBuZXcgVFsiZGVmYXVsdCJdKEUpLmZvcm1hdChlKTtjYXNlIm4xcWwiOnJldHVybiBuZXcgb1siZGVmYXVsdCJdKEUpLmZvcm1hdChlKTtjYXNlInBsL3NxbCI6cmV0dXJuIG5ldyBBWyJkZWZhdWx0Il0oRSkuZm9ybWF0KGUpO2Nhc2Uic3FsIjpjYXNlIHZvaWQgMDpyZXR1cm4gbmV3IE9bImRlZmF1bHQiXShFKS5mb3JtYXQoZSk7ZGVmYXVsdDp0aHJvdyBFcnJvcigiVW5zdXBwb3J0ZWQgU1FMIGRpYWxlY3Q6ICIrRS5sYW5ndWFnZSl9fX0sZS5leHBvcnRzPUVbImRlZmF1bHQiXX0sZnVuY3Rpb24oZSxFKXsidXNlIHN0cmljdCI7RS5fX2VzTW9kdWxlPSEwLEVbImRlZmF1bHQiXT1mdW5jdGlvbihlLEUpe2lmKCEoZSBpbnN0YW5jZW9mIEUpKXRocm93IG5ldyBUeXBlRXJyb3IoIkNhbm5vdCBjYWxsIGEgY2xhc3MgYXMgYSBmdW5jdGlvbiIpfX0sZnVuY3Rpb24oZSxFLHQpe3ZhciBuPXQoMzkpLHI9Im9iamVjdCI9PXR5cGVvZiBzZWxmJiZzZWxmJiZzZWxmLk9iamVjdD09PU9iamVjdCYmc2VsZixUPW58fHJ8fEZ1bmN0aW9uKCJyZXR1cm4gdGhpcyIpKCk7ZS5leHBvcnRzPVR9LGZ1bmN0aW9uKGUsRSx0KXtmdW5jdGlvbiBuKGUsRSl7dmFyIHQ9VChlLEUpO3JldHVybiByKHQpP3Q6dm9pZCAwfXZhciByPXQoMzMpLFQ9dCg0MSk7ZS5leHBvcnRzPW59LGZ1bmN0aW9uKGUsRSx0KXsidXNlIHN0cmljdCI7ZnVuY3Rpb24gbihlKXtyZXR1cm4gZSYmZS5fX2VzTW9kdWxlP2U6eyJkZWZhdWx0IjplfX1FLl9fZXNNb2R1bGU9ITA7dmFyIHI9dCgxKSxUPW4ociksUj10KDY2KSxvPW4oUiksTj10KDcpLEE9bihOKSxJPXQoMTUpLE89bihJKSxpPXQoMTYpLFM9bihpKSx1PXQoMTcpLEw9bih1KSxDPWZ1bmN0aW9uKCl7ZnVuY3Rpb24gZShFLHQpeygwLFRbImRlZmF1bHQiXSkodGhpcyxlKSx0aGlzLmNmZz1FfHx7fSx0aGlzLmluZGVudGF0aW9uPW5ldyBPWyJkZWZhdWx0Il0odGhpcy5jZmcuaW5kZW50KSx0aGlzLmlubGluZUJsb2NrPW5ldyBTWyJkZWZhdWx0Il0sdGhpcy5wYXJhbXM9bmV3IExbImRlZmF1bHQiXSh0aGlzLmNmZy5wYXJhbXMpLHRoaXMudG9rZW5pemVyPXQsdGhpcy5wcmV2aW91c1Jlc2VydmVkV29yZD17fX1yZXR1cm4gZS5wcm90b3R5cGUuZm9ybWF0PWZ1bmN0aW9uKGUpe3ZhciBFPXRoaXMudG9rZW5pemVyLnRva2VuaXplKGUpLHQ9dGhpcy5nZXRGb3JtYXR0ZWRRdWVyeUZyb21Ub2tlbnMoRSk7cmV0dXJuIHQudHJpbSgpfSxlLnByb3RvdHlwZS5nZXRGb3JtYXR0ZWRRdWVyeUZyb21Ub2tlbnM9ZnVuY3Rpb24oZSl7dmFyIEU9dGhpcyx0PSIiO3JldHVybiBlLmZvckVhY2goZnVuY3Rpb24obixyKXtuLnR5cGUhPT1BWyJkZWZhdWx0Il0uV0hJVEVTUEFDRSYmKG4udHlwZT09PUFbImRlZmF1bHQiXS5MSU5FX0NPTU1FTlQ/dD1FLmZvcm1hdExpbmVDb21tZW50KG4sdCk6bi50eXBlPT09QVsiZGVmYXVsdCJdLkJMT0NLX0NPTU1FTlQ/dD1FLmZvcm1hdEJsb2NrQ29tbWVudChuLHQpOm4udHlwZT09PUFbImRlZmF1bHQiXS5SRVNFUlZFRF9UT1BMRVZFTD8odD1FLmZvcm1hdFRvcGxldmVsUmVzZXJ2ZWRXb3JkKG4sdCksRS5wcmV2aW91c1Jlc2VydmVkV29yZD1uKTpuLnR5cGU9PT1BWyJkZWZhdWx0Il0uUkVTRVJWRURfTkVXTElORT8odD1FLmZvcm1hdE5ld2xpbmVSZXNlcnZlZFdvcmQobix0KSxFLnByZXZpb3VzUmVzZXJ2ZWRXb3JkPW4pOm4udHlwZT09PUFbImRlZmF1bHQiXS5SRVNFUlZFRD8odD1FLmZvcm1hdFdpdGhTcGFjZXMobix0KSxFLnByZXZpb3VzUmVzZXJ2ZWRXb3JkPW4pOnQ9bi50eXBlPT09QVsiZGVmYXVsdCJdLk9QRU5fUEFSRU4/RS5mb3JtYXRPcGVuaW5nUGFyZW50aGVzZXMoZSxyLHQpOm4udHlwZT09PUFbImRlZmF1bHQiXS5DTE9TRV9QQVJFTj9FLmZvcm1hdENsb3NpbmdQYXJlbnRoZXNlcyhuLHQpOm4udHlwZT09PUFbImRlZmF1bHQiXS5QTEFDRUhPTERFUj9FLmZvcm1hdFBsYWNlaG9sZGVyKG4sdCk6IiwiPT09bi52YWx1ZT9FLmZvcm1hdENvbW1hKG4sdCk6IjoiPT09bi52YWx1ZT9FLmZvcm1hdFdpdGhTcGFjZUFmdGVyKG4sdCk6Ii4iPT09bi52YWx1ZXx8IjsiPT09bi52YWx1ZT9FLmZvcm1hdFdpdGhvdXRTcGFjZXMobix0KTpFLmZvcm1hdFdpdGhTcGFjZXMobix0KSl9KSx0fSxlLnByb3RvdHlwZS5mb3JtYXRMaW5lQ29tbWVudD1mdW5jdGlvbihlLEUpe3JldHVybiB0aGlzLmFkZE5ld2xpbmUoRStlLnZhbHVlKX0sZS5wcm90b3R5cGUuZm9ybWF0QmxvY2tDb21tZW50PWZ1bmN0aW9uKGUsRSl7cmV0dXJuIHRoaXMuYWRkTmV3bGluZSh0aGlzLmFkZE5ld2xpbmUoRSkrdGhpcy5pbmRlbnRDb21tZW50KGUudmFsdWUpKX0sZS5wcm90b3R5cGUuaW5kZW50Q29tbWVudD1mdW5jdGlvbihlKXtyZXR1cm4gZS5yZXBsYWNlKC9cbi9nLCJcbiIrdGhpcy5pbmRlbnRhdGlvbi5nZXRJbmRlbnQoKSl9LGUucHJvdG90eXBlLmZvcm1hdFRvcGxldmVsUmVzZXJ2ZWRXb3JkPWZ1bmN0aW9uKGUsRSl7cmV0dXJuIHRoaXMuaW5kZW50YXRpb24uZGVjcmVhc2VUb3BMZXZlbCgpLEU9dGhpcy5hZGROZXdsaW5lKEUpLHRoaXMuaW5kZW50YXRpb24uaW5jcmVhc2VUb3BsZXZlbCgpLEUrPXRoaXMuZXF1YWxpemVXaGl0ZXNwYWNlKGUudmFsdWUpLHRoaXMuYWRkTmV3bGluZShFKX0sZS5wcm90b3R5cGUuZm9ybWF0TmV3bGluZVJlc2VydmVkV29yZD1mdW5jdGlvbihlLEUpe3JldHVybiB0aGlzLmFkZE5ld2xpbmUoRSkrdGhpcy5lcXVhbGl6ZVdoaXRlc3BhY2UoZS52YWx1ZSkrIiAifSxlLnByb3RvdHlwZS5lcXVhbGl6ZVdoaXRlc3BhY2U9ZnVuY3Rpb24oZSl7cmV0dXJuIGUucmVwbGFjZSgvXHMrL2csIiAiKX0sZS5wcm90b3R5cGUuZm9ybWF0T3BlbmluZ1BhcmVudGhlc2VzPWZ1bmN0aW9uKGUsRSx0KXt2YXIgbj1lW0UtMV07cmV0dXJuIG4mJm4udHlwZSE9PUFbImRlZmF1bHQiXS5XSElURVNQQUNFJiZuLnR5cGUhPT1BWyJkZWZhdWx0Il0uT1BFTl9QQVJFTiYmKHQ9KDAsb1siZGVmYXVsdCJdKSh0KSksdCs9ZVtFXS52YWx1ZSx0aGlzLmlubGluZUJsb2NrLmJlZ2luSWZQb3NzaWJsZShlLEUpLHRoaXMuaW5saW5lQmxvY2suaXNBY3RpdmUoKXx8KHRoaXMuaW5kZW50YXRpb24uaW5jcmVhc2VCbG9ja0xldmVsKCksdD10aGlzLmFkZE5ld2xpbmUodCkpLHR9LGUucHJvdG90eXBlLmZvcm1hdENsb3NpbmdQYXJlbnRoZXNlcz1mdW5jdGlvbihlLEUpe3JldHVybiB0aGlzLmlubGluZUJsb2NrLmlzQWN0aXZlKCk/KHRoaXMuaW5saW5lQmxvY2suZW5kKCksdGhpcy5mb3JtYXRXaXRoU3BhY2VBZnRlcihlLEUpKToodGhpcy5pbmRlbnRhdGlvbi5kZWNyZWFzZUJsb2NrTGV2ZWwoKSx0aGlzLmZvcm1hdFdpdGhTcGFjZXMoZSx0aGlzLmFkZE5ld2xpbmUoRSkpKX0sZS5wcm90b3R5cGUuZm9ybWF0UGxhY2Vob2xkZXI9ZnVuY3Rpb24oZSxFKXtyZXR1cm4gRSt0aGlzLnBhcmFtcy5nZXQoZSkrIiAifSxlLnByb3RvdHlwZS5mb3JtYXRDb21tYT1mdW5jdGlvbihlLEUpe3JldHVybiBFPSgwLG9bImRlZmF1bHQiXSkoRSkrZS52YWx1ZSsiICIsdGhpcy5pbmxpbmVCbG9jay5pc0FjdGl2ZSgpP0U6L15MSU1JVCQvaS50ZXN0KHRoaXMucHJldmlvdXNSZXNlcnZlZFdvcmQudmFsdWUpP0U6dGhpcy5hZGROZXdsaW5lKEUpfSxlLnByb3RvdHlwZS5mb3JtYXRXaXRoU3BhY2VBZnRlcj1mdW5jdGlvbihlLEUpe3JldHVybigwLG9bImRlZmF1bHQiXSkoRSkrZS52YWx1ZSsiICJ9LGUucHJvdG90eXBlLmZvcm1hdFdpdGhvdXRTcGFjZXM9ZnVuY3Rpb24oZSxFKXtyZXR1cm4oMCxvWyJkZWZhdWx0Il0pKEUpK2UudmFsdWV9LGUucHJvdG90eXBlLmZvcm1hdFdpdGhTcGFjZXM9ZnVuY3Rpb24oZSxFKXtyZXR1cm4gRStlLnZhbHVlKyIgIn0sZS5wcm90b3R5cGUuYWRkTmV3bGluZT1mdW5jdGlvbihlKXtyZXR1cm4oMCxvWyJkZWZhdWx0Il0pKGUpKyJcbiIrdGhpcy5pbmRlbnRhdGlvbi5nZXRJbmRlbnQoKX0sZX0oKTtFWyJkZWZhdWx0Il09QyxlLmV4cG9ydHM9RVsiZGVmYXVsdCJdfSxmdW5jdGlvbihlLEUsdCl7InVzZSBzdHJpY3QiO2Z1bmN0aW9uIG4oZSl7cmV0dXJuIGUmJmUuX19lc01vZHVsZT9lOnsiZGVmYXVsdCI6ZX19RS5fX2VzTW9kdWxlPSEwO3ZhciByPXQoMSksVD1uKHIpLFI9dCg1OCksbz1uKFIpLE49dCg1MyksQT1uKE4pLEk9dCg3KSxPPW4oSSksaT1mdW5jdGlvbigpe2Z1bmN0aW9uIGUoRSl7KDAsVFsiZGVmYXVsdCJdKSh0aGlzLGUpLHRoaXMuV0hJVEVTUEFDRV9SRUdFWD0vXihccyspLyx0aGlzLk5VTUJFUl9SRUdFWD0vXigoLVxzKik/WzAtOV0rKFwuWzAtOV0rKT98MHhbMC05YS1mQS1GXSt8MGJbMDFdKylcYi8sdGhpcy5PUEVSQVRPUl9SRUdFWD0vXighPXw8Pnw9PXw8PXw+PXwhPHwhPnxcfFx8fDo6fC0+PnwtPnx+flwqfH5+fCF+flwqfCF+fnx+XCp8IX5cKnwhfnwuKS8sdGhpcy5CTE9DS19DT01NRU5UX1JFR0VYPS9eKFwvXCpbXl0qPyg/OlwqXC98JCkpLyx0aGlzLkxJTkVfQ09NTUVOVF9SRUdFWD10aGlzLmNyZWF0ZUxpbmVDb21tZW50UmVnZXgoRS5saW5lQ29tbWVudFR5cGVzKSx0aGlzLlJFU0VSVkVEX1RPUExFVkVMX1JFR0VYPXRoaXMuY3JlYXRlUmVzZXJ2ZWRXb3JkUmVnZXgoRS5yZXNlcnZlZFRvcGxldmVsV29yZHMpLHRoaXMuUkVTRVJWRURfTkVXTElORV9SRUdFWD10aGlzLmNyZWF0ZVJlc2VydmVkV29yZFJlZ2V4KEUucmVzZXJ2ZWROZXdsaW5lV29yZHMpLHRoaXMuUkVTRVJWRURfUExBSU5fUkVHRVg9dGhpcy5jcmVhdGVSZXNlcnZlZFdvcmRSZWdleChFLnJlc2VydmVkV29yZHMpLHRoaXMuV09SRF9SRUdFWD10aGlzLmNyZWF0ZVdvcmRSZWdleChFLnNwZWNpYWxXb3JkQ2hhcnMpLHRoaXMuU1RSSU5HX1JFR0VYPXRoaXMuY3JlYXRlU3RyaW5nUmVnZXgoRS5zdHJpbmdUeXBlcyksdGhpcy5PUEVOX1BBUkVOX1JFR0VYPXRoaXMuY3JlYXRlUGFyZW5SZWdleChFLm9wZW5QYXJlbnMpLHRoaXMuQ0xPU0VfUEFSRU5fUkVHRVg9dGhpcy5jcmVhdGVQYXJlblJlZ2V4KEUuY2xvc2VQYXJlbnMpLHRoaXMuSU5ERVhFRF9QTEFDRUhPTERFUl9SRUdFWD10aGlzLmNyZWF0ZVBsYWNlaG9sZGVyUmVnZXgoRS5pbmRleGVkUGxhY2Vob2xkZXJUeXBlcywiWzAtOV0qIiksdGhpcy5JREVOVF9OQU1FRF9QTEFDRUhPTERFUl9SRUdFWD10aGlzLmNyZWF0ZVBsYWNlaG9sZGVyUmVnZXgoRS5uYW1lZFBsYWNlaG9sZGVyVHlwZXMsIlthLXpBLVowLTkuXyRdKyIpLHRoaXMuU1RSSU5HX05BTUVEX1BMQUNFSE9MREVSX1JFR0VYPXRoaXMuY3JlYXRlUGxhY2Vob2xkZXJSZWdleChFLm5hbWVkUGxhY2Vob2xkZXJUeXBlcyx0aGlzLmNyZWF0ZVN0cmluZ1BhdHRlcm4oRS5zdHJpbmdUeXBlcykpfXJldHVybiBlLnByb3RvdHlwZS5jcmVhdGVMaW5lQ29tbWVudFJlZ2V4PWZ1bmN0aW9uKGUpe3JldHVybiBSZWdFeHAoIl4oKD86IitlLm1hcChmdW5jdGlvbihlKXtyZXR1cm4oMCxBWyJkZWZhdWx0Il0pKGUpfSkuam9pbigifCIpKyIpLio/KD86XG58JCkpIil9LGUucHJvdG90eXBlLmNyZWF0ZVJlc2VydmVkV29yZFJlZ2V4PWZ1bmN0aW9uKGUpe3ZhciBFPWUuam9pbigifCIpLnJlcGxhY2UoLyAvZywiXFxzKyIpO3JldHVybiBSZWdFeHAoIl4oIitFKyIpXFxiIiwiaSIpfSxlLnByb3RvdHlwZS5jcmVhdGVXb3JkUmVnZXg9ZnVuY3Rpb24oKXt2YXIgZT1hcmd1bWVudHMubGVuZ3RoPjAmJnZvaWQgMCE9PWFyZ3VtZW50c1swXT9hcmd1bWVudHNbMF06W107cmV0dXJuIFJlZ0V4cCgiXihbXFx3IitlLmpvaW4oIiIpKyJdKykiKX0sZS5wcm90b3R5cGUuY3JlYXRlU3RyaW5nUmVnZXg9ZnVuY3Rpb24oZSl7cmV0dXJuIFJlZ0V4cCgiXigiK3RoaXMuY3JlYXRlU3RyaW5nUGF0dGVybihlKSsiKSIpfSxlLnByb3RvdHlwZS5jcmVhdGVTdHJpbmdQYXR0ZXJuPWZ1bmN0aW9uKGUpe3ZhciBFPXsiYGAiOiIoKGBbXmBdKigkfGApKSspIiwiW10iOiIoKFxcW1teXFxdXSooJHxcXF0pKShcXF1bXlxcXV0qKCR8XFxdKSkqKSIsJyIiJzonKCgiW14iXFxcXF0qKD86XFxcXC5bXiJcXFxcXSopKigifCQpKSspJywiJyciOiIoKCdbXidcXFxcXSooPzpcXFxcLlteJ1xcXFxdKikqKCd8JCkpKykiLCJOJyciOiIoKE4nW15OJ1xcXFxdKig/OlxcXFwuW15OJ1xcXFxdKikqKCd8JCkpKykifTtyZXR1cm4gZS5tYXAoZnVuY3Rpb24oZSl7cmV0dXJuIEVbZV19KS5qb2luKCJ8Iil9LGUucHJvdG90eXBlLmNyZWF0ZVBhcmVuUmVnZXg9ZnVuY3Rpb24oZSl7dmFyIEU9dGhpcztyZXR1cm4gUmVnRXhwKCJeKCIrZS5tYXAoZnVuY3Rpb24oZSl7cmV0dXJuIEUuZXNjYXBlUGFyZW4oZSl9KS5qb2luKCJ8IikrIikiLCJpIil9LGUucHJvdG90eXBlLmVzY2FwZVBhcmVuPWZ1bmN0aW9uKGUpe3JldHVybiAxPT09ZS5sZW5ndGg/KDAsQVsiZGVmYXVsdCJdKShlKToiXFxiIitlKyJcXGIifSxlLnByb3RvdHlwZS5jcmVhdGVQbGFjZWhvbGRlclJlZ2V4PWZ1bmN0aW9uKGUsRSl7aWYoKDAsb1siZGVmYXVsdCJdKShlKSlyZXR1cm4hMTt2YXIgdD1lLm1hcChBWyJkZWZhdWx0Il0pLmpvaW4oInwiKTtyZXR1cm4gUmVnRXhwKCJeKCg/OiIrdCsiKSg/OiIrRSsiKSkiKX0sZS5wcm90b3R5cGUudG9rZW5pemU9ZnVuY3Rpb24oZSl7Zm9yKHZhciBFPVtdLHQ9dm9pZCAwO2UubGVuZ3RoOyl0PXRoaXMuZ2V0TmV4dFRva2VuKGUsdCksZT1lLnN1YnN0cmluZyh0LnZhbHVlLmxlbmd0aCksRS5wdXNoKHQpO3JldHVybiBFfSxlLnByb3RvdHlwZS5nZXROZXh0VG9rZW49ZnVuY3Rpb24oZSxFKXtyZXR1cm4gdGhpcy5nZXRXaGl0ZXNwYWNlVG9rZW4oZSl8fHRoaXMuZ2V0Q29tbWVudFRva2VuKGUpfHx0aGlzLmdldFN0cmluZ1Rva2VuKGUpfHx0aGlzLmdldE9wZW5QYXJlblRva2VuKGUpfHx0aGlzLmdldENsb3NlUGFyZW5Ub2tlbihlKXx8dGhpcy5nZXRQbGFjZWhvbGRlclRva2VuKGUpfHx0aGlzLmdldE51bWJlclRva2VuKGUpfHx0aGlzLmdldFJlc2VydmVkV29yZFRva2VuKGUsRSl8fHRoaXMuZ2V0V29yZFRva2VuKGUpfHx0aGlzLmdldE9wZXJhdG9yVG9rZW4oZSl9LGUucHJvdG90eXBlLmdldFdoaXRlc3BhY2VUb2tlbj1mdW5jdGlvbihlKXtyZXR1cm4gdGhpcy5nZXRUb2tlbk9uRmlyc3RNYXRjaCh7aW5wdXQ6ZSx0eXBlOk9bImRlZmF1bHQiXS5XSElURVNQQUNFLHJlZ2V4OnRoaXMuV0hJVEVTUEFDRV9SRUdFWH0pfSxlLnByb3RvdHlwZS5nZXRDb21tZW50VG9rZW49ZnVuY3Rpb24oZSl7cmV0dXJuIHRoaXMuZ2V0TGluZUNvbW1lbnRUb2tlbihlKXx8dGhpcy5nZXRCbG9ja0NvbW1lbnRUb2tlbihlKX0sZS5wcm90b3R5cGUuZ2V0TGluZUNvbW1lbnRUb2tlbj1mdW5jdGlvbihlKXtyZXR1cm4gdGhpcy5nZXRUb2tlbk9uRmlyc3RNYXRjaCh7aW5wdXQ6ZSx0eXBlOk9bImRlZmF1bHQiXS5MSU5FX0NPTU1FTlQscmVnZXg6dGhpcy5MSU5FX0NPTU1FTlRfUkVHRVh9KX0sZS5wcm90b3R5cGUuZ2V0QmxvY2tDb21tZW50VG9rZW49ZnVuY3Rpb24oZSl7cmV0dXJuIHRoaXMuZ2V0VG9rZW5PbkZpcnN0TWF0Y2goe2lucHV0OmUsdHlwZTpPWyJkZWZhdWx0Il0uQkxPQ0tfQ09NTUVOVCxyZWdleDp0aGlzLkJMT0NLX0NPTU1FTlRfUkVHRVh9KX0sZS5wcm90b3R5cGUuZ2V0U3RyaW5nVG9rZW49ZnVuY3Rpb24oZSl7cmV0dXJuIHRoaXMuZ2V0VG9rZW5PbkZpcnN0TWF0Y2goe2lucHV0OmUsdHlwZTpPWyJkZWZhdWx0Il0uU1RSSU5HLHJlZ2V4OnRoaXMuU1RSSU5HX1JFR0VYfSl9LGUucHJvdG90eXBlLmdldE9wZW5QYXJlblRva2VuPWZ1bmN0aW9uKGUpe3JldHVybiB0aGlzLmdldFRva2VuT25GaXJzdE1hdGNoKHtpbnB1dDplLHR5cGU6T1siZGVmYXVsdCJdLk9QRU5fUEFSRU4scmVnZXg6dGhpcy5PUEVOX1BBUkVOX1JFR0VYfSl9LGUucHJvdG90eXBlLmdldENsb3NlUGFyZW5Ub2tlbj1mdW5jdGlvbihlKXtyZXR1cm4gdGhpcy5nZXRUb2tlbk9uRmlyc3RNYXRjaCh7aW5wdXQ6ZSx0eXBlOk9bImRlZmF1bHQiXS5DTE9TRV9QQVJFTixyZWdleDp0aGlzLkNMT1NFX1BBUkVOX1JFR0VYfSl9LGUucHJvdG90eXBlLmdldFBsYWNlaG9sZGVyVG9rZW49ZnVuY3Rpb24oZSl7cmV0dXJuIHRoaXMuZ2V0SWRlbnROYW1lZFBsYWNlaG9sZGVyVG9rZW4oZSl8fHRoaXMuZ2V0U3RyaW5nTmFtZWRQbGFjZWhvbGRlclRva2VuKGUpfHx0aGlzLmdldEluZGV4ZWRQbGFjZWhvbGRlclRva2VuKGUpfSxlLnByb3RvdHlwZS5nZXRJZGVudE5hbWVkUGxhY2Vob2xkZXJUb2tlbj1mdW5jdGlvbihlKXtyZXR1cm4gdGhpcy5nZXRQbGFjZWhvbGRlclRva2VuV2l0aEtleSh7aW5wdXQ6ZSxyZWdleDp0aGlzLklERU5UX05BTUVEX1BMQUNFSE9MREVSX1JFR0VYLHBhcnNlS2V5OmZ1bmN0aW9uKGUpe3JldHVybiBlLnNsaWNlKDEpfX0pfSxlLnByb3RvdHlwZS5nZXRTdHJpbmdOYW1lZFBsYWNlaG9sZGVyVG9rZW49ZnVuY3Rpb24oZSl7dmFyIEU9dGhpcztyZXR1cm4gdGhpcy5nZXRQbGFjZWhvbGRlclRva2VuV2l0aEtleSh7aW5wdXQ6ZSxyZWdleDp0aGlzLlNUUklOR19OQU1FRF9QTEFDRUhPTERFUl9SRUdFWCxwYXJzZUtleTpmdW5jdGlvbihlKXtyZXR1cm4gRS5nZXRFc2NhcGVkUGxhY2Vob2xkZXJLZXkoe2tleTplLnNsaWNlKDIsLTEpLHF1b3RlQ2hhcjplLnNsaWNlKC0xKX0pfX0pfSxlLnByb3RvdHlwZS5nZXRJbmRleGVkUGxhY2Vob2xkZXJUb2tlbj1mdW5jdGlvbihlKXtyZXR1cm4gdGhpcy5nZXRQbGFjZWhvbGRlclRva2VuV2l0aEtleSh7aW5wdXQ6ZSxyZWdleDp0aGlzLklOREVYRURfUExBQ0VIT0xERVJfUkVHRVgscGFyc2VLZXk6ZnVuY3Rpb24oZSl7cmV0dXJuIGUuc2xpY2UoMSl9fSl9LGUucHJvdG90eXBlLmdldFBsYWNlaG9sZGVyVG9rZW5XaXRoS2V5PWZ1bmN0aW9uKGUpe3ZhciBFPWUuaW5wdXQsdD1lLnJlZ2V4LG49ZS5wYXJzZUtleSxyPXRoaXMuZ2V0VG9rZW5PbkZpcnN0TWF0Y2goe2lucHV0OkUscmVnZXg6dCx0eXBlOk9bImRlZmF1bHQiXS5QTEFDRUhPTERFUn0pO3JldHVybiByJiYoci5rZXk9bihyLnZhbHVlKSkscn0sZS5wcm90b3R5cGUuZ2V0RXNjYXBlZFBsYWNlaG9sZGVyS2V5PWZ1bmN0aW9uKGUpe3ZhciBFPWUua2V5LHQ9ZS5xdW90ZUNoYXI7cmV0dXJuIEUucmVwbGFjZShSZWdFeHAoKDAsQVsiZGVmYXVsdCJdKSgiXFwiKSt0LCJnIiksdCl9LGUucHJvdG90eXBlLmdldE51bWJlclRva2VuPWZ1bmN0aW9uKGUpe3JldHVybiB0aGlzLmdldFRva2VuT25GaXJzdE1hdGNoKHtpbnB1dDplLHR5cGU6T1siZGVmYXVsdCJdLk5VTUJFUixyZWdleDp0aGlzLk5VTUJFUl9SRUdFWH0pfSxlLnByb3RvdHlwZS5nZXRPcGVyYXRvclRva2VuPWZ1bmN0aW9uKGUpe3JldHVybiB0aGlzLmdldFRva2VuT25GaXJzdE1hdGNoKHtpbnB1dDplLHR5cGU6T1siZGVmYXVsdCJdLk9QRVJBVE9SLHJlZ2V4OnRoaXMuT1BFUkFUT1JfUkVHRVh9KX0sZS5wcm90b3R5cGUuZ2V0UmVzZXJ2ZWRXb3JkVG9rZW49ZnVuY3Rpb24oZSxFKXtpZighRXx8IUUudmFsdWV8fCIuIiE9PUUudmFsdWUpcmV0dXJuIHRoaXMuZ2V0VG9wbGV2ZWxSZXNlcnZlZFRva2VuKGUpfHx0aGlzLmdldE5ld2xpbmVSZXNlcnZlZFRva2VuKGUpfHx0aGlzLmdldFBsYWluUmVzZXJ2ZWRUb2tlbihlKX0sZS5wcm90b3R5cGUuZ2V0VG9wbGV2ZWxSZXNlcnZlZFRva2VuPWZ1bmN0aW9uKGUpe3JldHVybiB0aGlzLmdldFRva2VuT25GaXJzdE1hdGNoKHtpbnB1dDplLHR5cGU6T1siZGVmYXVsdCJdLlJFU0VSVkVEX1RPUExFVkVMLHJlZ2V4OnRoaXMuUkVTRVJWRURfVE9QTEVWRUxfUkVHRVh9KX0sZS5wcm90b3R5cGUuZ2V0TmV3bGluZVJlc2VydmVkVG9rZW49ZnVuY3Rpb24oZSl7cmV0dXJuIHRoaXMuZ2V0VG9rZW5PbkZpcnN0TWF0Y2goe2lucHV0OmUsdHlwZTpPWyJkZWZhdWx0Il0uUkVTRVJWRURfTkVXTElORSxyZWdleDp0aGlzLlJFU0VSVkVEX05FV0xJTkVfUkVHRVh9KX0sZS5wcm90b3R5cGUuZ2V0UGxhaW5SZXNlcnZlZFRva2VuPWZ1bmN0aW9uKGUpe3JldHVybiB0aGlzLmdldFRva2VuT25GaXJzdE1hdGNoKHtpbnB1dDplLHR5cGU6T1siZGVmYXVsdCJdLlJFU0VSVkVELHJlZ2V4OnRoaXMuUkVTRVJWRURfUExBSU5fUkVHRVh9KX0sZS5wcm90b3R5cGUuZ2V0V29yZFRva2VuPWZ1bmN0aW9uKGUpe3JldHVybiB0aGlzLmdldFRva2VuT25GaXJzdE1hdGNoKHtpbnB1dDplLHR5cGU6T1siZGVmYXVsdCJdLldPUkQscmVnZXg6dGhpcy5XT1JEX1JFR0VYfSl9LGUucHJvdG90eXBlLmdldFRva2VuT25GaXJzdE1hdGNoPWZ1bmN0aW9uKGUpe3ZhciBFPWUuaW5wdXQsdD1lLnR5cGUsbj1lLnJlZ2V4LHI9RS5tYXRjaChuKTtpZihyKXJldHVybnt0eXBlOnQsdmFsdWU6clsxXX19LGV9KCk7RVsiZGVmYXVsdCJdPWksZS5leHBvcnRzPUVbImRlZmF1bHQiXX0sZnVuY3Rpb24oZSxFKXtmdW5jdGlvbiB0KGUpe3ZhciBFPXR5cGVvZiBlO3JldHVybiBudWxsIT1lJiYoIm9iamVjdCI9PUV8fCJmdW5jdGlvbiI9PUUpfWUuZXhwb3J0cz10fSxmdW5jdGlvbihlLEUpeyJ1c2Ugc3RyaWN0IjtFLl9fZXNNb2R1bGU9ITAsRVsiZGVmYXVsdCJdPXtXSElURVNQQUNFOiJ3aGl0ZXNwYWNlIixXT1JEOiJ3b3JkIixTVFJJTkc6InN0cmluZyIsUkVTRVJWRUQ6InJlc2VydmVkIixSRVNFUlZFRF9UT1BMRVZFTDoicmVzZXJ2ZWQtdG9wbGV2ZWwiLFJFU0VSVkVEX05FV0xJTkU6InJlc2VydmVkLW5ld2xpbmUiLE9QRVJBVE9SOiJvcGVyYXRvciIsT1BFTl9QQVJFTjoib3Blbi1wYXJlbiIsQ0xPU0VfUEFSRU46ImNsb3NlLXBhcmVuIixMSU5FX0NPTU1FTlQ6ImxpbmUtY29tbWVudCIsQkxPQ0tfQ09NTUVOVDoiYmxvY2stY29tbWVudCIsTlVNQkVSOiJudW1iZXIiLFBMQUNFSE9MREVSOiJwbGFjZWhvbGRlciJ9LGUuZXhwb3J0cz1FWyJkZWZhdWx0Il19LGZ1bmN0aW9uKGUsRSx0KXtmdW5jdGlvbiBuKGUpe3JldHVybiBudWxsIT1lJiZUKGUubGVuZ3RoKSYmIXIoZSl9dmFyIHI9dCgxMiksVD10KDU5KTtlLmV4cG9ydHM9bn0sZnVuY3Rpb24oZSxFLHQpe2Z1bmN0aW9uIG4oZSl7cmV0dXJuIG51bGw9PWU/IiI6cihlKX12YXIgcj10KDEwKTtlLmV4cG9ydHM9bn0sZnVuY3Rpb24oZSxFLHQpe2Z1bmN0aW9uIG4oZSl7aWYoInN0cmluZyI9PXR5cGVvZiBlKXJldHVybiBlO2lmKFQoZSkpcmV0dXJuIE4/Ti5jYWxsKGUpOiIiO3ZhciBFPWUrIiI7cmV0dXJuIjAiPT1FJiYxL2U9PS1SPyItMCI6RX12YXIgcj10KDI2KSxUPXQoMTQpLFI9MS8wLG89cj9yLnByb3RvdHlwZTp2b2lkIDAsTj1vP28udG9TdHJpbmc6dm9pZCAwO2UuZXhwb3J0cz1ufSxmdW5jdGlvbihlLEUpe2Z1bmN0aW9uIHQoZSl7aWYobnVsbCE9ZSl7dHJ5e3JldHVybiByLmNhbGwoZSl9Y2F0Y2goRSl7fXRyeXtyZXR1cm4gZSsiIn1jYXRjaChFKXt9fXJldHVybiIifXZhciBuPUZ1bmN0aW9uLnByb3RvdHlwZSxyPW4udG9TdHJpbmc7ZS5leHBvcnRzPXR9LGZ1bmN0aW9uKGUsRSx0KXtmdW5jdGlvbiBuKGUpe3ZhciBFPXIoZSk/Ti5jYWxsKGUpOiIiO3JldHVybiBFPT1UfHxFPT1SfXZhciByPXQoNiksVD0iW29iamVjdCBGdW5jdGlvbl0iLFI9IltvYmplY3QgR2VuZXJhdG9yRnVuY3Rpb25dIixvPU9iamVjdC5wcm90b3R5cGUsTj1vLnRvU3RyaW5nO2UuZXhwb3J0cz1ufSxmdW5jdGlvbihlLEUpe2Z1bmN0aW9uIHQoZSl7cmV0dXJuIG51bGwhPWUmJiJvYmplY3QiPT10eXBlb2YgZX1lLmV4cG9ydHM9dH0sZnVuY3Rpb24oZSxFLHQpe2Z1bmN0aW9uIG4oZSl7cmV0dXJuInN5bWJvbCI9PXR5cGVvZiBlfHxyKGUpJiZvLmNhbGwoZSk9PVR9dmFyIHI9dCgxMyksVD0iW29iamVjdCBTeW1ib2xdIixSPU9iamVjdC5wcm90b3R5cGUsbz1SLnRvU3RyaW5nO2UuZXhwb3J0cz1ufSxmdW5jdGlvbihlLEUsdCl7InVzZSBzdHJpY3QiO2Z1bmN0aW9uIG4oZSl7cmV0dXJuIGUmJmUuX19lc01vZHVsZT9lOnsiZGVmYXVsdCI6ZX19RS5fX2VzTW9kdWxlPSEwO3ZhciByPXQoMSksVD1uKHIpLFI9dCg2MSksbz1uKFIpLE49dCg2MCksQT1uKE4pLEk9InRvcC1sZXZlbCIsTz0iYmxvY2stbGV2ZWwiLGk9ZnVuY3Rpb24oKXtmdW5jdGlvbiBlKEUpeygwLFRbImRlZmF1bHQiXSkodGhpcyxlKSx0aGlzLmluZGVudD1FfHwiICAiLHRoaXMuaW5kZW50VHlwZXM9W119cmV0dXJuIGUucHJvdG90eXBlLmdldEluZGVudD1mdW5jdGlvbigpe3JldHVybigwLG9bImRlZmF1bHQiXSkodGhpcy5pbmRlbnQsdGhpcy5pbmRlbnRUeXBlcy5sZW5ndGgpfSxlLnByb3RvdHlwZS5pbmNyZWFzZVRvcGxldmVsPWZ1bmN0aW9uKCl7dGhpcy5pbmRlbnRUeXBlcy5wdXNoKEkpfSxlLnByb3RvdHlwZS5pbmNyZWFzZUJsb2NrTGV2ZWw9ZnVuY3Rpb24oKXt0aGlzLmluZGVudFR5cGVzLnB1c2goTyl9LGUucHJvdG90eXBlLmRlY3JlYXNlVG9wTGV2ZWw9ZnVuY3Rpb24oKXsoMCxBWyJkZWZhdWx0Il0pKHRoaXMuaW5kZW50VHlwZXMpPT09SSYmdGhpcy5pbmRlbnRUeXBlcy5wb3AoKX0sZS5wcm90b3R5cGUuZGVjcmVhc2VCbG9ja0xldmVsPWZ1bmN0aW9uKCl7Zm9yKDt0aGlzLmluZGVudFR5cGVzLmxlbmd0aD4wOyl7dmFyIGU9dGhpcy5pbmRlbnRUeXBlcy5wb3AoKTtpZihlIT09SSlicmVha319LGV9KCk7RVsiZGVmYXVsdCJdPWksZS5leHBvcnRzPUVbImRlZmF1bHQiXX0sZnVuY3Rpb24oZSxFLHQpeyJ1c2Ugc3RyaWN0IjtmdW5jdGlvbiBuKGUpe3JldHVybiBlJiZlLl9fZXNNb2R1bGU/ZTp7ImRlZmF1bHQiOmV9fUUuX19lc01vZHVsZT0hMDt2YXIgcj10KDEpLFQ9bihyKSxSPXQoNyksbz1uKFIpLE49NTAsQT1mdW5jdGlvbigpe2Z1bmN0aW9uIGUoKXsoMCxUWyJkZWZhdWx0Il0pKHRoaXMsZSksdGhpcy5sZXZlbD0wfXJldHVybiBlLnByb3RvdHlwZS5iZWdpbklmUG9zc2libGU9ZnVuY3Rpb24oZSxFKXswPT09dGhpcy5sZXZlbCYmdGhpcy5pc0lubGluZUJsb2NrKGUsRSk/dGhpcy5sZXZlbD0xOnRoaXMubGV2ZWw+MD90aGlzLmxldmVsKys6dGhpcy5sZXZlbD0wfSxlLnByb3RvdHlwZS5lbmQ9ZnVuY3Rpb24oKXt0aGlzLmxldmVsLS19LGUucHJvdG90eXBlLmlzQWN0aXZlPWZ1bmN0aW9uKCl7cmV0dXJuIHRoaXMubGV2ZWw+MH0sZS5wcm90b3R5cGUuaXNJbmxpbmVCbG9jaz1mdW5jdGlvbihlLEUpe2Zvcih2YXIgdD0wLG49MCxyPUU7ZS5sZW5ndGg+cjtyKyspe3ZhciBUPWVbcl07aWYodCs9VC52YWx1ZS5sZW5ndGgsdD5OKXJldHVybiExO2lmKFQudHlwZT09PW9bImRlZmF1bHQiXS5PUEVOX1BBUkVOKW4rKztlbHNlIGlmKFQudHlwZT09PW9bImRlZmF1bHQiXS5DTE9TRV9QQVJFTiYmKG4tLSwwPT09bikpcmV0dXJuITA7aWYodGhpcy5pc0ZvcmJpZGRlblRva2VuKFQpKXJldHVybiExfXJldHVybiExfSxlLnByb3RvdHlwZS5pc0ZvcmJpZGRlblRva2VuPWZ1bmN0aW9uKGUpe3ZhciBFPWUudHlwZSx0PWUudmFsdWU7cmV0dXJuIEU9PT1vWyJkZWZhdWx0Il0uUkVTRVJWRURfVE9QTEVWRUx8fEU9PT1vWyJkZWZhdWx0Il0uUkVTRVJWRURfTkVXTElORXx8RT09PW9bImRlZmF1bHQiXS5DT01NRU5UfHxFPT09b1siZGVmYXVsdCJdLkJMT0NLX0NPTU1FTlR8fCI7Ij09PXR9LGV9KCk7RVsiZGVmYXVsdCJdPUEsZS5leHBvcnRzPUVbImRlZmF1bHQiXX0sZnVuY3Rpb24oZSxFLHQpeyJ1c2Ugc3RyaWN0IjtmdW5jdGlvbiBuKGUpe3JldHVybiBlJiZlLl9fZXNNb2R1bGU/ZTp7ImRlZmF1bHQiOmV9fUUuX19lc01vZHVsZT0hMDt2YXIgcj10KDEpLFQ9bihyKSxSPWZ1bmN0aW9uKCl7ZnVuY3Rpb24gZShFKXsoMCxUWyJkZWZhdWx0Il0pKHRoaXMsZSksdGhpcy5wYXJhbXM9RSx0aGlzLmluZGV4PTB9cmV0dXJuIGUucHJvdG90eXBlLmdldD1mdW5jdGlvbihlKXt2YXIgRT1lLmtleSx0PWUudmFsdWU7cmV0dXJuIHRoaXMucGFyYW1zP0U/dGhpcy5wYXJhbXNbRV06dGhpcy5wYXJhbXNbdGhpcy5pbmRleCsrXTp0fSxlfSgpO0VbImRlZmF1bHQiXT1SLGUuZXhwb3J0cz1FWyJkZWZhdWx0Il19LGZ1bmN0aW9uKGUsRSx0KXsidXNlIHN0cmljdCI7ZnVuY3Rpb24gbihlKXtyZXR1cm4gZSYmZS5fX2VzTW9kdWxlP2U6eyJkZWZhdWx0IjplfX1FLl9fZXNNb2R1bGU9ITA7dmFyIHI9dCgxKSxUPW4ociksUj10KDQpLG89bihSKSxOPXQoNSksQT1uKE4pLEk9WyJBQlMiLCJBQ1RJVkFURSIsIkFMSUFTIiwiQUxMIiwiQUxMT0NBVEUiLCJBTExPVyIsIkFMVEVSIiwiQU5ZIiwiQVJFIiwiQVJSQVkiLCJBUyIsIkFTQyIsIkFTRU5TSVRJVkUiLCJBU1NPQ0lBVEUiLCJBU1VUSU1FIiwiQVNZTU1FVFJJQyIsIkFUIiwiQVRPTUlDIiwiQVRUUklCVVRFUyIsIkFVRElUIiwiQVVUSE9SSVpBVElPTiIsIkFVWCIsIkFVWElMSUFSWSIsIkFWRyIsIkJFRk9SRSIsIkJFR0lOIiwiQkVUV0VFTiIsIkJJR0lOVCIsIkJJTkFSWSIsIkJMT0IiLCJCT09MRUFOIiwiQk9USCIsIkJVRkZFUlBPT0wiLCJCWSIsIkNBQ0hFIiwiQ0FMTCIsIkNBTExFRCIsIkNBUFRVUkUiLCJDQVJESU5BTElUWSIsIkNBU0NBREVEIiwiQ0FTRSIsIkNBU1QiLCJDQ1NJRCIsIkNFSUwiLCJDRUlMSU5HIiwiQ0hBUiIsIkNIQVJBQ1RFUiIsIkNIQVJBQ1RFUl9MRU5HVEgiLCJDSEFSX0xFTkdUSCIsIkNIRUNLIiwiQ0xPQiIsIkNMT05FIiwiQ0xPU0UiLCJDTFVTVEVSIiwiQ09BTEVTQ0UiLCJDT0xMQVRFIiwiQ09MTEVDVCIsIkNPTExFQ1RJT04iLCJDT0xMSUQiLCJDT0xVTU4iLCJDT01NRU5UIiwiQ09NTUlUIiwiQ09OQ0FUIiwiQ09ORElUSU9OIiwiQ09OTkVDVCIsIkNPTk5FQ1RJT04iLCJDT05TVFJBSU5UIiwiQ09OVEFJTlMiLCJDT05USU5VRSIsIkNPTlZFUlQiLCJDT1JSIiwiQ09SUkVTUE9ORElORyIsIkNPVU5UIiwiQ09VTlRfQklHIiwiQ09WQVJfUE9QIiwiQ09WQVJfU0FNUCIsIkNSRUFURSIsIkNST1NTIiwiQ1VCRSIsIkNVTUVfRElTVCIsIkNVUlJFTlQiLCJDVVJSRU5UX0RBVEUiLCJDVVJSRU5UX0RFRkFVTFRfVFJBTlNGT1JNX0dST1VQIiwiQ1VSUkVOVF9MQ19DVFlQRSIsIkNVUlJFTlRfUEFUSCIsIkNVUlJFTlRfUk9MRSIsIkNVUlJFTlRfU0NIRU1BIiwiQ1VSUkVOVF9TRVJWRVIiLCJDVVJSRU5UX1RJTUUiLCJDVVJSRU5UX1RJTUVTVEFNUCIsIkNVUlJFTlRfVElNRVpPTkUiLCJDVVJSRU5UX1RSQU5TRk9STV9HUk9VUF9GT1JfVFlQRSIsIkNVUlJFTlRfVVNFUiIsIkNVUlNPUiIsIkNZQ0xFIiwiREFUQSIsIkRBVEFCQVNFIiwiREFUQVBBUlRJVElPTk5BTUUiLCJEQVRBUEFSVElUSU9OTlVNIiwiREFURSIsIkRBWSIsIkRBWVMiLCJEQjJHRU5FUkFMIiwiREIyR0VOUkwiLCJEQjJTUUwiLCJEQklORk8iLCJEQlBBUlRJVElPTk5BTUUiLCJEQlBBUlRJVElPTk5VTSIsIkRFQUxMT0NBVEUiLCJERUMiLCJERUNJTUFMIiwiREVDTEFSRSIsIkRFRkFVTFQiLCJERUZBVUxUUyIsIkRFRklOSVRJT04iLCJERUxFVEUiLCJERU5TRVJBTksiLCJERU5TRV9SQU5LIiwiREVSRUYiLCJERVNDUklCRSIsIkRFU0NSSVBUT1IiLCJERVRFUk1JTklTVElDIiwiRElBR05PU1RJQ1MiLCJESVNBQkxFIiwiRElTQUxMT1ciLCJESVNDT05ORUNUIiwiRElTVElOQ1QiLCJETyIsIkRPQ1VNRU5UIiwiRE9VQkxFIiwiRFJPUCIsIkRTU0laRSIsIkRZTkFNSUMiLCJFQUNIIiwiRURJVFBST0MiLCJFTEVNRU5UIiwiRUxTRSIsIkVMU0VJRiIsIkVOQUJMRSIsIkVOQ09ESU5HIiwiRU5DUllQVElPTiIsIkVORCIsIkVORC1FWEVDIiwiRU5ESU5HIiwiRVJBU0UiLCJFU0NBUEUiLCJFVkVSWSIsIkVYQ0VQVElPTiIsIkVYQ0xVRElORyIsIkVYQ0xVU0lWRSIsIkVYRUMiLCJFWEVDVVRFIiwiRVhJU1RTIiwiRVhJVCIsIkVYUCIsIkVYUExBSU4iLCJFWFRFTkRFRCIsIkVYVEVSTkFMIiwiRVhUUkFDVCIsIkZBTFNFIiwiRkVOQ0VEIiwiRkVUQ0giLCJGSUVMRFBST0MiLCJGSUxFIiwiRklMVEVSIiwiRklOQUwiLCJGSVJTVCIsIkZMT0FUIiwiRkxPT1IiLCJGT1IiLCJGT1JFSUdOIiwiRlJFRSIsIkZVTEwiLCJGVU5DVElPTiIsIkZVU0lPTiIsIkdFTkVSQUwiLCJHRU5FUkFURUQiLCJHRVQiLCJHTE9CQUwiLCJHT1RPIiwiR1JBTlQiLCJHUkFQSElDIiwiR1JPVVAiLCJHUk9VUElORyIsIkhBTkRMRVIiLCJIQVNIIiwiSEFTSEVEX1ZBTFVFIiwiSElOVCIsIkhPTEQiLCJIT1VSIiwiSE9VUlMiLCJJREVOVElUWSIsIklGIiwiSU1NRURJQVRFIiwiSU4iLCJJTkNMVURJTkciLCJJTkNMVVNJVkUiLCJJTkNSRU1FTlQiLCJJTkRFWCIsIklORElDQVRPUiIsIklORElDQVRPUlMiLCJJTkYiLCJJTkZJTklUWSIsIklOSEVSSVQiLCJJTk5FUiIsIklOT1VUIiwiSU5TRU5TSVRJVkUiLCJJTlNFUlQiLCJJTlQiLCJJTlRFR0VSIiwiSU5URUdSSVRZIiwiSU5URVJTRUNUSU9OIiwiSU5URVJWQUwiLCJJTlRPIiwiSVMiLCJJU09CSUQiLCJJU09MQVRJT04iLCJJVEVSQVRFIiwiSkFSIiwiSkFWQSIsIktFRVAiLCJLRVkiLCJMQUJFTCIsIkxBTkdVQUdFIiwiTEFSR0UiLCJMQVRFUkFMIiwiTENfQ1RZUEUiLCJMRUFESU5HIiwiTEVBVkUiLCJMRUZUIiwiTElLRSIsIkxJTktUWVBFIiwiTE4iLCJMT0NBTCIsIkxPQ0FMREFURSIsIkxPQ0FMRSIsIkxPQ0FMVElNRSIsIkxPQ0FMVElNRVNUQU1QIiwiTE9DQVRPUiIsIkxPQ0FUT1JTIiwiTE9DSyIsIkxPQ0tNQVgiLCJMT0NLU0laRSIsIkxPTkciLCJMT09QIiwiTE9XRVIiLCJNQUlOVEFJTkVEIiwiTUFUQ0giLCJNQVRFUklBTElaRUQiLCJNQVgiLCJNQVhWQUxVRSIsIk1FTUJFUiIsIk1FUkdFIiwiTUVUSE9EIiwiTUlDUk9TRUNPTkQiLCJNSUNST1NFQ09ORFMiLCJNSU4iLCJNSU5VVEUiLCJNSU5VVEVTIiwiTUlOVkFMVUUiLCJNT0QiLCJNT0RFIiwiTU9ESUZJRVMiLCJNT0RVTEUiLCJNT05USCIsIk1PTlRIUyIsIk1VTFRJU0VUIiwiTkFOIiwiTkFUSU9OQUwiLCJOQVRVUkFMIiwiTkNIQVIiLCJOQ0xPQiIsIk5FVyIsIk5FV19UQUJMRSIsIk5FWFRWQUwiLCJOTyIsIk5PQ0FDSEUiLCJOT0NZQ0xFIiwiTk9ERU5BTUUiLCJOT0RFTlVNQkVSIiwiTk9NQVhWQUxVRSIsIk5PTUlOVkFMVUUiLCJOT05FIiwiTk9PUkRFUiIsIk5PUk1BTElaRSIsIk5PUk1BTElaRUQiLCJOT1QiLCJOVUxMIiwiTlVMTElGIiwiTlVMTFMiLCJOVU1FUklDIiwiTlVNUEFSVFMiLCJPQklEIiwiT0NURVRfTEVOR1RIIiwiT0YiLCJPRkZTRVQiLCJPTEQiLCJPTERfVEFCTEUiLCJPTiIsIk9OTFkiLCJPUEVOIiwiT1BUSU1JWkFUSU9OIiwiT1BUSU1JWkUiLCJPUFRJT04iLCJPUkRFUiIsIk9VVCIsIk9VVEVSIiwiT1ZFUiIsIk9WRVJMQVBTIiwiT1ZFUkxBWSIsIk9WRVJSSURJTkciLCJQQUNLQUdFIiwiUEFEREVEIiwiUEFHRVNJWkUiLCJQQVJBTUVURVIiLCJQQVJUIiwiUEFSVElUSU9OIiwiUEFSVElUSU9ORUQiLCJQQVJUSVRJT05JTkciLCJQQVJUSVRJT05TIiwiUEFTU1dPUkQiLCJQQVRIIiwiUEVSQ0VOVElMRV9DT05UIiwiUEVSQ0VOVElMRV9ESVNDIiwiUEVSQ0VOVF9SQU5LIiwiUElFQ0VTSVpFIiwiUExBTiIsIlBPU0lUSU9OIiwiUE9XRVIiLCJQUkVDSVNJT04iLCJQUkVQQVJFIiwiUFJFVlZBTCIsIlBSSU1BUlkiLCJQUklRVFkiLCJQUklWSUxFR0VTIiwiUFJPQ0VEVVJFIiwiUFJPR1JBTSIsIlBTSUQiLCJQVUJMSUMiLCJRVUVSWSIsIlFVRVJZTk8iLCJSQU5HRSIsIlJBTksiLCJSRUFEIiwiUkVBRFMiLCJSRUFMIiwiUkVDT1ZFUlkiLCJSRUNVUlNJVkUiLCJSRUYiLCJSRUZFUkVOQ0VTIiwiUkVGRVJFTkNJTkciLCJSRUZSRVNIIiwiUkVHUl9BVkdYIiwiUkVHUl9BVkdZIiwiUkVHUl9DT1VOVCIsIlJFR1JfSU5URVJDRVBUIiwiUkVHUl9SMiIsIlJFR1JfU0xPUEUiLCJSRUdSX1NYWCIsIlJFR1JfU1hZIiwiUkVHUl9TWVkiLCJSRUxFQVNFIiwiUkVOQU1FIiwiUkVQRUFUIiwiUkVTRVQiLCJSRVNJR05BTCIsIlJFU1RBUlQiLCJSRVNUUklDVCIsIlJFU1VMVCIsIlJFU1VMVF9TRVRfTE9DQVRPUiIsIlJFVFVSTiIsIlJFVFVSTlMiLCJSRVZPS0UiLCJSSUdIVCIsIlJPTEUiLCJST0xMQkFDSyIsIlJPTExVUCIsIlJPVU5EX0NFSUxJTkciLCJST1VORF9ET1dOIiwiUk9VTkRfRkxPT1IiLCJST1VORF9IQUxGX0RPV04iLCJST1VORF9IQUxGX0VWRU4iLCJST1VORF9IQUxGX1VQIiwiUk9VTkRfVVAiLCJST1VUSU5FIiwiUk9XIiwiUk9XTlVNQkVSIiwiUk9XUyIsIlJPV1NFVCIsIlJPV19OVU1CRVIiLCJSUk4iLCJSVU4iLCJTQVZFUE9JTlQiLCJTQ0hFTUEiLCJTQ09QRSIsIlNDUkFUQ0hQQUQiLCJTQ1JPTEwiLCJTRUFSQ0giLCJTRUNPTkQiLCJTRUNPTkRTIiwiU0VDUVRZIiwiU0VDVVJJVFkiLCJTRU5TSVRJVkUiLCJTRVFVRU5DRSIsIlNFU1NJT04iLCJTRVNTSU9OX1VTRVIiLCJTSUdOQUwiLCJTSU1JTEFSIiwiU0lNUExFIiwiU01BTExJTlQiLCJTTkFOIiwiU09NRSIsIlNPVVJDRSIsIlNQRUNJRklDIiwiU1BFQ0lGSUNUWVBFIiwiU1FMIiwiU1FMRVhDRVBUSU9OIiwiU1FMSUQiLCJTUUxTVEFURSIsIlNRTFdBUk5JTkciLCJTUVJUIiwiU1RBQ0tFRCIsIlNUQU5EQVJEIiwiU1RBUlQiLCJTVEFSVElORyIsIlNUQVRFTUVOVCIsIlNUQVRJQyIsIlNUQVRNRU5UIiwiU1RBWSIsIlNURERFVl9QT1AiLCJTVERERVZfU0FNUCIsIlNUT0dST1VQIiwiU1RPUkVTIiwiU1RZTEUiLCJTVUJNVUxUSVNFVCIsIlNVQlNUUklORyIsIlNVTSIsIlNVTU1BUlkiLCJTWU1NRVRSSUMiLCJTWU5PTllNIiwiU1lTRlVOIiwiU1lTSUJNIiwiU1lTUFJPQyIsIlNZU1RFTSIsIlNZU1RFTV9VU0VSIiwiVEFCTEUiLCJUQUJMRVNBTVBMRSIsIlRBQkxFU1BBQ0UiLCJUSEVOIiwiVElNRSIsIlRJTUVTVEFNUCIsIlRJTUVaT05FX0hPVVIiLCJUSU1FWk9ORV9NSU5VVEUiLCJUTyIsIlRSQUlMSU5HIiwiVFJBTlNBQ1RJT04iLCJUUkFOU0xBVEUiLCJUUkFOU0xBVElPTiIsIlRSRUFUIiwiVFJJR0dFUiIsIlRSSU0iLCJUUlVFIiwiVFJVTkNBVEUiLCJUWVBFIiwiVUVTQ0FQRSIsIlVORE8iLCJVTklRVUUiLCJVTktOT1dOIiwiVU5ORVNUIiwiVU5USUwiLCJVUFBFUiIsIlVTQUdFIiwiVVNFUiIsIlVTSU5HIiwiVkFMSURQUk9DIiwiVkFMVUUiLCJWQVJDSEFSIiwiVkFSSUFCTEUiLCJWQVJJQU5UIiwiVkFSWUlORyIsIlZBUl9QT1AiLCJWQVJfU0FNUCIsIlZDQVQiLCJWRVJTSU9OIiwiVklFVyIsIlZPTEFUSUxFIiwiVk9MVU1FUyIsIldIRU4iLCJXSEVORVZFUiIsIldISUxFIiwiV0lEVEhfQlVDS0VUIiwiV0lORE9XIiwiV0lUSCIsIldJVEhJTiIsIldJVEhPVVQiLCJXTE0iLCJXUklURSIsIlhNTEVMRU1FTlQiLCJYTUxFWElTVFMiLCJYTUxOQU1FU1BBQ0VTIiwiWUVBUiIsIllFQVJTIl0sTz1bIkFERCIsIkFGVEVSIiwiQUxURVIgQ09MVU1OIiwiQUxURVIgVEFCTEUiLCJERUxFVEUgRlJPTSIsIkVYQ0VQVCIsIkZFVENIIEZJUlNUIiwiRlJPTSIsIkdST1VQIEJZIiwiR08iLCJIQVZJTkciLCJJTlNFUlQgSU5UTyIsIklOVEVSU0VDVCIsIkxJTUlUIiwiT1JERVIgQlkiLCJTRUxFQ1QiLCJTRVQgQ1VSUkVOVCBTQ0hFTUEiLCJTRVQgU0NIRU1BIiwiU0VUIiwiVU5JT04gQUxMIiwiVVBEQVRFIiwiVkFMVUVTIiwiV0hFUkUiXSxpPVsiQU5EIiwiQ1JPU1MgSk9JTiIsIklOTkVSIEpPSU4iLCJKT0lOIiwiTEVGVCBKT0lOIiwiTEVGVCBPVVRFUiBKT0lOIiwiT1IiLCJPVVRFUiBKT0lOIiwiUklHSFQgSk9JTiIsIlJJR0hUIE9VVEVSIEpPSU4iXSxTPXZvaWQgMCx1PWZ1bmN0aW9uKCl7ZnVuY3Rpb24gZShFKXsoMCxUWyJkZWZhdWx0Il0pKHRoaXMsZSksdGhpcy5jZmc9RX1yZXR1cm4gZS5wcm90b3R5cGUuZm9ybWF0PWZ1bmN0aW9uKGUpe3JldHVybiBTfHwoUz1uZXcgQVsiZGVmYXVsdCJdKHtyZXNlcnZlZFdvcmRzOkkscmVzZXJ2ZWRUb3BsZXZlbFdvcmRzOk8scmVzZXJ2ZWROZXdsaW5lV29yZHM6aSxzdHJpbmdUeXBlczpbJyIiJywiJyciLCJgYCIsIltdIl0sb3BlblBhcmVuczpbIigiXSxjbG9zZVBhcmVuczpbIikiXSxpbmRleGVkUGxhY2Vob2xkZXJUeXBlczpbIj8iXSxuYW1lZFBsYWNlaG9sZGVyVHlwZXM6WyI6Il0sbGluZUNvbW1lbnRUeXBlczpbIi0tIl0sc3BlY2lhbFdvcmRDaGFyczpbIiMiLCJAIl19KSksbmV3IG9bImRlZmF1bHQiXSh0aGlzLmNmZyxTKS5mb3JtYXQoZSl9LGV9KCk7RVsiZGVmYXVsdCJdPXUsZS5leHBvcnRzPUVbImRlZmF1bHQiXX0sZnVuY3Rpb24oZSxFLHQpeyJ1c2Ugc3RyaWN0IjtmdW5jdGlvbiBuKGUpe3JldHVybiBlJiZlLl9fZXNNb2R1bGU/ZTp7ImRlZmF1bHQiOmV9fUUuX19lc01vZHVsZT0hMDt2YXIgcj10KDEpLFQ9bihyKSxSPXQoNCksbz1uKFIpLE49dCg1KSxBPW4oTiksST1bIkFMTCIsIkFMVEVSIiwiQU5BTFlaRSIsIkFORCIsIkFOWSIsIkFSUkFZIiwiQVMiLCJBU0MiLCJCRUdJTiIsIkJFVFdFRU4iLCJCSU5BUlkiLCJCT09MRUFOIiwiQlJFQUsiLCJCVUNLRVQiLCJCVUlMRCIsIkJZIiwiQ0FMTCIsIkNBU0UiLCJDQVNUIiwiQ0xVU1RFUiIsIkNPTExBVEUiLCJDT0xMRUNUSU9OIiwiQ09NTUlUIiwiQ09OTkVDVCIsIkNPTlRJTlVFIiwiQ09SUkVMQVRFIiwiQ09WRVIiLCJDUkVBVEUiLCJEQVRBQkFTRSIsIkRBVEFTRVQiLCJEQVRBU1RPUkUiLCJERUNMQVJFIiwiREVDUkVNRU5UIiwiREVMRVRFIiwiREVSSVZFRCIsIkRFU0MiLCJERVNDUklCRSIsIkRJU1RJTkNUIiwiRE8iLCJEUk9QIiwiRUFDSCIsIkVMRU1FTlQiLCJFTFNFIiwiRU5EIiwiRVZFUlkiLCJFWENFUFQiLCJFWENMVURFIiwiRVhFQ1VURSIsIkVYSVNUUyIsIkVYUExBSU4iLCJGQUxTRSIsIkZFVENIIiwiRklSU1QiLCJGTEFUVEVOIiwiRk9SIiwiRk9SQ0UiLCJGUk9NIiwiRlVOQ1RJT04iLCJHUkFOVCIsIkdST1VQIiwiR1NJIiwiSEFWSU5HIiwiSUYiLCJJR05PUkUiLCJJTElLRSIsIklOIiwiSU5DTFVERSIsIklOQ1JFTUVOVCIsIklOREVYIiwiSU5GRVIiLCJJTkxJTkUiLCJJTk5FUiIsIklOU0VSVCIsIklOVEVSU0VDVCIsIklOVE8iLCJJUyIsIkpPSU4iLCJLRVkiLCJLRVlTIiwiS0VZU1BBQ0UiLCJLTk9XTiIsIkxBU1QiLCJMRUZUIiwiTEVUIiwiTEVUVElORyIsIkxJS0UiLCJMSU1JVCIsIkxTTSIsIk1BUCIsIk1BUFBJTkciLCJNQVRDSEVEIiwiTUFURVJJQUxJWkVEIiwiTUVSR0UiLCJNSU5VUyIsIk1JU1NJTkciLCJOQU1FU1BBQ0UiLCJORVNUIiwiTk9UIiwiTlVMTCIsIk5VTUJFUiIsIk9CSkVDVCIsIk9GRlNFVCIsIk9OIiwiT1BUSU9OIiwiT1IiLCJPUkRFUiIsIk9VVEVSIiwiT1ZFUiIsIlBBUlNFIiwiUEFSVElUSU9OIiwiUEFTU1dPUkQiLCJQQVRIIiwiUE9PTCIsIlBSRVBBUkUiLCJQUklNQVJZIiwiUFJJVkFURSIsIlBSSVZJTEVHRSIsIlBST0NFRFVSRSIsIlBVQkxJQyIsIlJBVyIsIlJFQUxNIiwiUkVEVUNFIiwiUkVOQU1FIiwiUkVUVVJOIiwiUkVUVVJOSU5HIiwiUkVWT0tFIiwiUklHSFQiLCJST0xFIiwiUk9MTEJBQ0siLCJTQVRJU0ZJRVMiLCJTQ0hFTUEiLCJTRUxFQ1QiLCJTRUxGIiwiU0VNSSIsIlNFVCIsIlNIT1ciLCJTT01FIiwiU1RBUlQiLCJTVEFUSVNUSUNTIiwiU1RSSU5HIiwiU1lTVEVNIiwiVEhFTiIsIlRPIiwiVFJBTlNBQ1RJT04iLCJUUklHR0VSIiwiVFJVRSIsIlRSVU5DQVRFIiwiVU5ERVIiLCJVTklPTiIsIlVOSVFVRSIsIlVOS05PV04iLCJVTk5FU1QiLCJVTlNFVCIsIlVQREFURSIsIlVQU0VSVCIsIlVTRSIsIlVTRVIiLCJVU0lORyIsIlZBTElEQVRFIiwiVkFMVUUiLCJWQUxVRUQiLCJWQUxVRVMiLCJWSUEiLCJWSUVXIiwiV0hFTiIsIldIRVJFIiwiV0hJTEUiLCJXSVRIIiwiV0lUSElOIiwiV09SSyIsIlhPUiJdLE89WyJERUxFVEUgRlJPTSIsIkVYQ0VQVCBBTEwiLCJFWENFUFQiLCJFWFBMQUlOIERFTEVURSBGUk9NIiwiRVhQTEFJTiBVUERBVEUiLCJFWFBMQUlOIFVQU0VSVCIsIkZST00iLCJHUk9VUCBCWSIsIkhBVklORyIsIklORkVSIiwiSU5TRVJUIElOVE8iLCJJTlRFUlNFQ1QgQUxMIiwiSU5URVJTRUNUIiwiTEVUIiwiTElNSVQiLCJNRVJHRSIsIk5FU1QiLCJPUkRFUiBCWSIsIlBSRVBBUkUiLCJTRUxFQ1QiLCJTRVQgQ1VSUkVOVCBTQ0hFTUEiLCJTRVQgU0NIRU1BIiwiU0VUIiwiVU5JT04gQUxMIiwiVU5JT04iLCJVTk5FU1QiLCJVUERBVEUiLCJVUFNFUlQiLCJVU0UgS0VZUyIsIlZBTFVFUyIsIldIRVJFIl0saT1bIkFORCIsIklOTkVSIEpPSU4iLCJKT0lOIiwiTEVGVCBKT0lOIiwiTEVGVCBPVVRFUiBKT0lOIiwiT1IiLCJPVVRFUiBKT0lOIiwiUklHSFQgSk9JTiIsIlJJR0hUIE9VVEVSIEpPSU4iLCJYT1IiXSxTPXZvaWQgMCx1PWZ1bmN0aW9uKCl7ZnVuY3Rpb24gZShFKXsoMCxUWyJkZWZhdWx0Il0pKHRoaXMsZSksdGhpcy5jZmc9RX1yZXR1cm4gZS5wcm90b3R5cGUuZm9ybWF0PWZ1bmN0aW9uKGUpe3JldHVybiBTfHwoUz1uZXcgQVsiZGVmYXVsdCJdKHtyZXNlcnZlZFdvcmRzOkkscmVzZXJ2ZWRUb3BsZXZlbFdvcmRzOk8scmVzZXJ2ZWROZXdsaW5lV29yZHM6aSxzdHJpbmdUeXBlczpbJyIiJywiJyciLCJgYCJdLG9wZW5QYXJlbnM6WyIoIiwiWyIsInsiXSxjbG9zZVBhcmVuczpbIikiLCJdIiwifSJdLG5hbWVkUGxhY2Vob2xkZXJUeXBlczpbIiQiXSxsaW5lQ29tbWVudFR5cGVzOlsiIyIsIi0tIl19KSksbmV3IG9bImRlZmF1bHQiXSh0aGlzLmNmZyxTKS5mb3JtYXQoZSl9LGV9KCk7RVsiZGVmYXVsdCJdPXUsZS5leHBvcnRzPUVbImRlZmF1bHQiXX0sZnVuY3Rpb24oZSxFLHQpeyJ1c2Ugc3RyaWN0IjtmdW5jdGlvbiBuKGUpe3JldHVybiBlJiZlLl9fZXNNb2R1bGU/ZTp7ImRlZmF1bHQiOmV9fUUuX19lc01vZHVsZT0hMDt2YXIgcj10KDEpLFQ9bihyKSxSPXQoNCksbz1uKFIpLE49dCg1KSxBPW4oTiksST1bIkEiLCJBQ0NFU1NJQkxFIiwiQUdFTlQiLCJBR0dSRUdBVEUiLCJBTEwiLCJBTFRFUiIsIkFOWSIsIkFSUkFZIiwiQVMiLCJBU0MiLCJBVCIsIkFUVFJJQlVURSIsIkFVVEhJRCIsIkFWRyIsIkJFVFdFRU4iLCJCRklMRV9CQVNFIiwiQklOQVJZX0lOVEVHRVIiLCJCSU5BUlkiLCJCTE9CX0JBU0UiLCJCTE9DSyIsIkJPRFkiLCJCT09MRUFOIiwiQk9USCIsIkJPVU5EIiwiQlVMSyIsIkJZIiwiQllURSIsIkMiLCJDQUxMIiwiQ0FMTElORyIsIkNBU0NBREUiLCJDQVNFIiwiQ0hBUl9CQVNFIiwiQ0hBUiIsIkNIQVJBQ1RFUiIsIkNIQVJTRVQiLCJDSEFSU0VURk9STSIsIkNIQVJTRVRJRCIsIkNIRUNLIiwiQ0xPQl9CQVNFIiwiQ0xPTkUiLCJDTE9TRSIsIkNMVVNURVIiLCJDTFVTVEVSUyIsIkNPQUxFU0NFIiwiQ09MQVVUSCIsIkNPTExFQ1QiLCJDT0xVTU5TIiwiQ09NTUVOVCIsIkNPTU1JVCIsIkNPTU1JVFRFRCIsIkNPTVBJTEVEIiwiQ09NUFJFU1MiLCJDT05ORUNUIiwiQ09OU1RBTlQiLCJDT05TVFJVQ1RPUiIsIkNPTlRFWFQiLCJDT05USU5VRSIsIkNPTlZFUlQiLCJDT1VOVCIsIkNSQVNIIiwiQ1JFQVRFIiwiQ1JFREVOVElBTCIsIkNVUlJFTlQiLCJDVVJSVkFMIiwiQ1VSU09SIiwiQ1VTVE9NREFUVU0iLCJEQU5HTElORyIsIkRBVEEiLCJEQVRFX0JBU0UiLCJEQVRFIiwiREFZIiwiREVDSU1BTCIsIkRFRkFVTFQiLCJERUZJTkUiLCJERUxFVEUiLCJERVNDIiwiREVURVJNSU5JU1RJQyIsIkRJUkVDVE9SWSIsIkRJU1RJTkNUIiwiRE8iLCJET1VCTEUiLCJEUk9QIiwiRFVSQVRJT04iLCJFTEVNRU5UIiwiRUxTSUYiLCJFTVBUWSIsIkVTQ0FQRSIsIkVYQ0VQVElPTlMiLCJFWENMVVNJVkUiLCJFWEVDVVRFIiwiRVhJU1RTIiwiRVhJVCIsIkVYVEVORFMiLCJFWFRFUk5BTCIsIkVYVFJBQ1QiLCJGQUxTRSIsIkZFVENIIiwiRklOQUwiLCJGSVJTVCIsIkZJWEVEIiwiRkxPQVQiLCJGT1IiLCJGT1JBTEwiLCJGT1JDRSIsIkZST00iLCJGVU5DVElPTiIsIkdFTkVSQUwiLCJHT1RPIiwiR1JBTlQiLCJHUk9VUCIsIkhBU0giLCJIRUFQIiwiSElEREVOIiwiSE9VUiIsIklERU5USUZJRUQiLCJJRiIsIklNTUVESUFURSIsIklOIiwiSU5DTFVESU5HIiwiSU5ERVgiLCJJTkRFWEVTIiwiSU5ESUNBVE9SIiwiSU5ESUNFUyIsIklORklOSVRFIiwiSU5TVEFOVElBQkxFIiwiSU5UIiwiSU5URUdFUiIsIklOVEVSRkFDRSIsIklOVEVSVkFMIiwiSU5UTyIsIklOVkFMSURBVEUiLCJJUyIsIklTT0xBVElPTiIsIkpBVkEiLCJMQU5HVUFHRSIsIkxBUkdFIiwiTEVBRElORyIsIkxFTkdUSCIsIkxFVkVMIiwiTElCUkFSWSIsIkxJS0UiLCJMSUtFMiIsIkxJS0U0IiwiTElLRUMiLCJMSU1JVEVEIiwiTE9DQUwiLCJMT0NLIiwiTE9ORyIsIk1BUCIsIk1BWCIsIk1BWExFTiIsIk1FTUJFUiIsIk1FUkdFIiwiTUlOIiwiTUlOVVMiLCJNSU5VVEUiLCJNTFNMQUJFTCIsIk1PRCIsIk1PREUiLCJNT05USCIsIk1VTFRJU0VUIiwiTkFNRSIsIk5BTiIsIk5BVElPTkFMIiwiTkFUSVZFIiwiTkFUVVJBTCIsIk5BVFVSQUxOIiwiTkNIQVIiLCJORVciLCJORVhUVkFMIiwiTk9DT01QUkVTUyIsIk5PQ09QWSIsIk5PVCIsIk5PV0FJVCIsIk5VTEwiLCJOVUxMSUYiLCJOVU1CRVJfQkFTRSIsIk5VTUJFUiIsIk9CSkVDVCIsIk9DSUNPTEwiLCJPQ0lEQVRFIiwiT0NJREFURVRJTUUiLCJPQ0lEVVJBVElPTiIsIk9DSUlOVEVSVkFMIiwiT0NJTE9CTE9DQVRPUiIsIk9DSU5VTUJFUiIsIk9DSVJBVyIsIk9DSVJFRiIsIk9DSVJFRkNVUlNPUiIsIk9DSVJPV0lEIiwiT0NJU1RSSU5HIiwiT0NJVFlQRSIsIk9GIiwiT0xEIiwiT04iLCJPTkxZIiwiT1BBUVVFIiwiT1BFTiIsIk9QRVJBVE9SIiwiT1BUSU9OIiwiT1JBQ0xFIiwiT1JBREFUQSIsIk9SREVSIiwiT1JHQU5JWkFUSU9OIiwiT1JMQU5ZIiwiT1JMVkFSWSIsIk9USEVSUyIsIk9VVCIsIk9WRVJMQVBTIiwiT1ZFUlJJRElORyIsIlBBQ0tBR0UiLCJQQVJBTExFTF9FTkFCTEUiLCJQQVJBTUVURVIiLCJQQVJBTUVURVJTIiwiUEFSRU5UIiwiUEFSVElUSU9OIiwiUEFTQ0FMIiwiUENURlJFRSIsIlBJUEUiLCJQSVBFTElORUQiLCJQTFNfSU5URUdFUiIsIlBMVUdHQUJMRSIsIlBPU0lUSVZFIiwiUE9TSVRJVkVOIiwiUFJBR01BIiwiUFJFQ0lTSU9OIiwiUFJJT1IiLCJQUklWQVRFIiwiUFJPQ0VEVVJFIiwiUFVCTElDIiwiUkFJU0UiLCJSQU5HRSIsIlJBVyIsIlJFQUQiLCJSRUFMIiwiUkVDT1JEIiwiUkVGIiwiUkVGRVJFTkNFIiwiUkVMRUFTRSIsIlJFTElFU19PTiIsIlJFTSIsIlJFTUFJTkRFUiIsIlJFTkFNRSIsIlJFU09VUkNFIiwiUkVTVUxUX0NBQ0hFIiwiUkVTVUxUIiwiUkVUVVJOIiwiUkVUVVJOSU5HIiwiUkVWRVJTRSIsIlJFVk9LRSIsIlJPTExCQUNLIiwiUk9XIiwiUk9XSUQiLCJST1dOVU0iLCJST1dUWVBFIiwiU0FNUExFIiwiU0FWRSIsIlNBVkVQT0lOVCIsIlNCMSIsIlNCMiIsIlNCNCIsIlNFQ09ORCIsIlNFR01FTlQiLCJTRUxGIiwiU0VQQVJBVEUiLCJTRVFVRU5DRSIsIlNFUklBTElaQUJMRSIsIlNIQVJFIiwiU0hPUlQiLCJTSVpFX1QiLCJTSVpFIiwiU01BTExJTlQiLCJTT01FIiwiU1BBQ0UiLCJTUEFSU0UiLCJTUUwiLCJTUUxDT0RFIiwiU1FMREFUQSIsIlNRTEVSUk0iLCJTUUxOQU1FIiwiU1FMU1RBVEUiLCJTVEFOREFSRCIsIlNUQVJUIiwiU1RBVElDIiwiU1REREVWIiwiU1RPUkVEIiwiU1RSSU5HIiwiU1RSVUNUIiwiU1RZTEUiLCJTVUJNVUxUSVNFVCIsIlNVQlBBUlRJVElPTiIsIlNVQlNUSVRVVEFCTEUiLCJTVUJUWVBFIiwiU1VDQ0VTU0ZVTCIsIlNVTSIsIlNZTk9OWU0iLCJTWVNEQVRFIiwiVEFCQVVUSCIsIlRBQkxFIiwiVERPIiwiVEhFIiwiVEhFTiIsIlRJTUUiLCJUSU1FU1RBTVAiLCJUSU1FWk9ORV9BQkJSIiwiVElNRVpPTkVfSE9VUiIsIlRJTUVaT05FX01JTlVURSIsIlRJTUVaT05FX1JFR0lPTiIsIlRPIiwiVFJBSUxJTkciLCJUUkFOU0FDVElPTiIsIlRSQU5TQUNUSU9OQUwiLCJUUklHR0VSIiwiVFJVRSIsIlRSVVNURUQiLCJUWVBFIiwiVUIxIiwiVUIyIiwiVUI0IiwiVUlEIiwiVU5ERVIiLCJVTklRVUUiLCJVTlBMVUciLCJVTlNJR05FRCIsIlVOVFJVU1RFRCIsIlVTRSIsIlVTRVIiLCJVU0lORyIsIlZBTElEQVRFIiwiVkFMSVNUIiwiVkFMVUUiLCJWQVJDSEFSIiwiVkFSQ0hBUjIiLCJWQVJJQUJMRSIsIlZBUklBTkNFIiwiVkFSUkFZIiwiVkFSWUlORyIsIlZJRVciLCJWSUVXUyIsIlZPSUQiLCJXSEVORVZFUiIsIldISUxFIiwiV0lUSCIsIldPUksiLCJXUkFQUEVEIiwiV1JJVEUiLCJZRUFSIiwiWk9ORSJdLE89WyJBREQiLCJBTFRFUiBDT0xVTU4iLCJBTFRFUiBUQUJMRSIsIkJFR0lOIiwiQ09OTkVDVCBCWSIsIkRFQ0xBUkUiLCJERUxFVEUgRlJPTSIsIkRFTEVURSIsIkVORCIsIkVYQ0VQVCIsIkVYQ0VQVElPTiIsIkZFVENIIEZJUlNUIiwiRlJPTSIsIkdST1VQIEJZIiwiSEFWSU5HIiwiSU5TRVJUIElOVE8iLCJJTlNFUlQiLCJJTlRFUlNFQ1QiLCJMSU1JVCIsIkxPT1AiLCJNT0RJRlkiLCJPUkRFUiBCWSIsIlNFTEVDVCIsIlNFVCBDVVJSRU5UIFNDSEVNQSIsIlNFVCBTQ0hFTUEiLCJTRVQiLCJTVEFSVCBXSVRIIiwiVU5JT04gQUxMIiwiVU5JT04iLCJVUERBVEUiLCJWQUxVRVMiLCJXSEVSRSJdLGk9WyJBTkQiLCJDUk9TUyBBUFBMWSIsIkNST1NTIEpPSU4iLCJFTFNFIiwiRU5EIiwiSU5ORVIgSk9JTiIsIkpPSU4iLCJMRUZUIEpPSU4iLCJMRUZUIE9VVEVSIEpPSU4iLCJPUiIsIk9VVEVSIEFQUExZIiwiT1VURVIgSk9JTiIsIlJJR0hUIEpPSU4iLCJSSUdIVCBPVVRFUiBKT0lOIiwiV0hFTiIsIlhPUiJdLFM9dm9pZCAwLHU9ZnVuY3Rpb24oKXtmdW5jdGlvbiBlKEUpeygwLFRbImRlZmF1bHQiXSkodGhpcyxlKSx0aGlzLmNmZz1FfXJldHVybiBlLnByb3RvdHlwZS5mb3JtYXQ9ZnVuY3Rpb24oZSl7cmV0dXJuIFN8fChTPW5ldyBBWyJkZWZhdWx0Il0oe3Jlc2VydmVkV29yZHM6SSxyZXNlcnZlZFRvcGxldmVsV29yZHM6TyxyZXNlcnZlZE5ld2xpbmVXb3JkczppLHN0cmluZ1R5cGVzOlsnIiInLCJOJyciLCInJyIsImBgIl0sb3BlblBhcmVuczpbIigiLCJDQVNFIl0sY2xvc2VQYXJlbnM6WyIpIiwiRU5EIl0saW5kZXhlZFBsYWNlaG9sZGVyVHlwZXM6WyI/Il0sbmFtZWRQbGFjZWhvbGRlclR5cGVzOlsiOiJdLGxpbmVDb21tZW50VHlwZXM6WyItLSJdLHNwZWNpYWxXb3JkQ2hhcnM6WyJfIiwiJCIsIiMiLCIuIiwiQCJdfSkpLG5ldyBvWyJkZWZhdWx0Il0odGhpcy5jZmcsUykuZm9ybWF0KGUpfSxlfSgpO0VbImRlZmF1bHQiXT11LGUuZXhwb3J0cz1FWyJkZWZhdWx0Il19LGZ1bmN0aW9uKGUsRSx0KXsidXNlIHN0cmljdCI7ZnVuY3Rpb24gbihlKXtyZXR1cm4gZSYmZS5fX2VzTW9kdWxlP2U6eyJkZWZhdWx0IjplfX1FLl9fZXNNb2R1bGU9ITA7dmFyIHI9dCgxKSxUPW4ociksUj10KDQpLG89bihSKSxOPXQoNSksQT1uKE4pLEk9WyJBQ0NFU1NJQkxFIiwiQUNUSU9OIiwiQUdBSU5TVCIsIkFHR1JFR0FURSIsIkFMR09SSVRITSIsIkFMTCIsIkFMVEVSIiwiQU5BTFlTRSIsIkFOQUxZWkUiLCJBUyIsIkFTQyIsIkFVVE9DT01NSVQiLCJBVVRPX0lOQ1JFTUVOVCIsIkJBQ0tVUCIsIkJFR0lOIiwiQkVUV0VFTiIsIkJJTkxPRyIsIkJPVEgiLCJDQVNDQURFIiwiQ0FTRSIsIkNIQU5HRSIsIkNIQU5HRUQiLCJDSEFSQUNURVIgU0VUIiwiQ0hBUlNFVCIsIkNIRUNLIiwiQ0hFQ0tTVU0iLCJDT0xMQVRFIiwiQ09MTEFUSU9OIiwiQ09MVU1OIiwiQ09MVU1OUyIsIkNPTU1FTlQiLCJDT01NSVQiLCJDT01NSVRURUQiLCJDT01QUkVTU0VEIiwiQ09OQ1VSUkVOVCIsIkNPTlNUUkFJTlQiLCJDT05UQUlOUyIsIkNPTlZFUlQiLCJDUkVBVEUiLCJDUk9TUyIsIkNVUlJFTlRfVElNRVNUQU1QIiwiREFUQUJBU0UiLCJEQVRBQkFTRVMiLCJEQVkiLCJEQVlfSE9VUiIsIkRBWV9NSU5VVEUiLCJEQVlfU0VDT05EIiwiREVGQVVMVCIsIkRFRklORVIiLCJERUxBWUVEIiwiREVMRVRFIiwiREVTQyIsIkRFU0NSSUJFIiwiREVURVJNSU5JU1RJQyIsIkRJU1RJTkNUIiwiRElTVElOQ1RST1ciLCJESVYiLCJETyIsIkRST1AiLCJEVU1QRklMRSIsIkRVUExJQ0FURSIsIkRZTkFNSUMiLCJFTFNFIiwiRU5DTE9TRUQiLCJFTkQiLCJFTkdJTkUiLCJFTkdJTkVTIiwiRU5HSU5FX1RZUEUiLCJFU0NBUEUiLCJFU0NBUEVEIiwiRVZFTlRTIiwiRVhFQyIsIkVYRUNVVEUiLCJFWElTVFMiLCJFWFBMQUlOIiwiRVhURU5ERUQiLCJGQVNUIiwiRkVUQ0giLCJGSUVMRFMiLCJGSUxFIiwiRklSU1QiLCJGSVhFRCIsIkZMVVNIIiwiRk9SIiwiRk9SQ0UiLCJGT1JFSUdOIiwiRlVMTCIsIkZVTExURVhUIiwiRlVOQ1RJT04iLCJHTE9CQUwiLCJHUkFOVCIsIkdSQU5UUyIsIkdST1VQX0NPTkNBVCIsIkhFQVAiLCJISUdIX1BSSU9SSVRZIiwiSE9TVFMiLCJIT1VSIiwiSE9VUl9NSU5VVEUiLCJIT1VSX1NFQ09ORCIsIklERU5USUZJRUQiLCJJRiIsIklGTlVMTCIsIklHTk9SRSIsIklOIiwiSU5ERVgiLCJJTkRFWEVTIiwiSU5GSUxFIiwiSU5TRVJUIiwiSU5TRVJUX0lEIiwiSU5TRVJUX01FVEhPRCIsIklOVEVSVkFMIiwiSU5UTyIsIklOVk9LRVIiLCJJUyIsIklTT0xBVElPTiIsIktFWSIsIktFWVMiLCJLSUxMIiwiTEFTVF9JTlNFUlRfSUQiLCJMRUFESU5HIiwiTEVWRUwiLCJMSUtFIiwiTElORUFSIiwiTElORVMiLCJMT0FEIiwiTE9DQUwiLCJMT0NLIiwiTE9DS1MiLCJMT0dTIiwiTE9XX1BSSU9SSVRZIiwiTUFSSUEiLCJNQVNURVIiLCJNQVNURVJfQ09OTkVDVF9SRVRSWSIsIk1BU1RFUl9IT1NUIiwiTUFTVEVSX0xPR19GSUxFIiwiTUFUQ0giLCJNQVhfQ09OTkVDVElPTlNfUEVSX0hPVVIiLCJNQVhfUVVFUklFU19QRVJfSE9VUiIsIk1BWF9ST1dTIiwiTUFYX1VQREFURVNfUEVSX0hPVVIiLCJNQVhfVVNFUl9DT05ORUNUSU9OUyIsIk1FRElVTSIsIk1FUkdFIiwiTUlOVVRFIiwiTUlOVVRFX1NFQ09ORCIsIk1JTl9ST1dTIiwiTU9ERSIsIk1PRElGWSIsIk1PTlRIIiwiTVJHX01ZSVNBTSIsIk1ZSVNBTSIsIk5BTUVTIiwiTkFUVVJBTCIsIk5PVCIsIk5PVygpIiwiTlVMTCIsIk9GRlNFVCIsIk9OIERFTEVURSIsIk9OIFVQREFURSIsIk9OIiwiT05MWSIsIk9QRU4iLCJPUFRJTUlaRSIsIk9QVElPTiIsIk9QVElPTkFMTFkiLCJPVVRGSUxFIiwiUEFDS19LRVlTIiwiUEFHRSIsIlBBUlRJQUwiLCJQQVJUSVRJT04iLCJQQVJUSVRJT05TIiwiUEFTU1dPUkQiLCJQUklNQVJZIiwiUFJJVklMRUdFUyIsIlBST0NFRFVSRSIsIlBST0NFU1MiLCJQUk9DRVNTTElTVCIsIlBVUkdFIiwiUVVJQ0siLCJSQUlEMCIsIlJBSURfQ0hVTktTIiwiUkFJRF9DSFVOS1NJWkUiLCJSQUlEX1RZUEUiLCJSQU5HRSIsIlJFQUQiLCJSRUFEX09OTFkiLCJSRUFEX1dSSVRFIiwiUkVGRVJFTkNFUyIsIlJFR0VYUCIsIlJFTE9BRCIsIlJFTkFNRSIsIlJFUEFJUiIsIlJFUEVBVEFCTEUiLCJSRVBMQUNFIiwiUkVQTElDQVRJT04iLCJSRVNFVCIsIlJFU1RPUkUiLCJSRVNUUklDVCIsIlJFVFVSTiIsIlJFVFVSTlMiLCJSRVZPS0UiLCJSTElLRSIsIlJPTExCQUNLIiwiUk9XIiwiUk9XUyIsIlJPV19GT1JNQVQiLCJTRUNPTkQiLCJTRUNVUklUWSIsIlNFUEFSQVRPUiIsIlNFUklBTElaQUJMRSIsIlNFU1NJT04iLCJTSEFSRSIsIlNIT1ciLCJTSFVURE9XTiIsIlNMQVZFIiwiU09OQU1FIiwiU09VTkRTIiwiU1FMIiwiU1FMX0FVVE9fSVNfTlVMTCIsIlNRTF9CSUdfUkVTVUxUIiwiU1FMX0JJR19TRUxFQ1RTIiwiU1FMX0JJR19UQUJMRVMiLCJTUUxfQlVGRkVSX1JFU1VMVCIsIlNRTF9DQUNIRSIsIlNRTF9DQUxDX0ZPVU5EX1JPV1MiLCJTUUxfTE9HX0JJTiIsIlNRTF9MT0dfT0ZGIiwiU1FMX0xPR19VUERBVEUiLCJTUUxfTE9XX1BSSU9SSVRZX1VQREFURVMiLCJTUUxfTUFYX0pPSU5fU0laRSIsIlNRTF9OT19DQUNIRSIsIlNRTF9RVU9URV9TSE9XX0NSRUFURSIsIlNRTF9TQUZFX1VQREFURVMiLCJTUUxfU0VMRUNUX0xJTUlUIiwiU1FMX1NMQVZFX1NLSVBfQ09VTlRFUiIsIlNRTF9TTUFMTF9SRVNVTFQiLCJTUUxfV0FSTklOR1MiLCJTVEFSVCIsIlNUQVJUSU5HIiwiU1RBVFVTIiwiU1RPUCIsIlNUT1JBR0UiLCJTVFJBSUdIVF9KT0lOIiwiU1RSSU5HIiwiU1RSSVBFRCIsIlNVUEVSIiwiVEFCTEUiLCJUQUJMRVMiLCJURU1QT1JBUlkiLCJURVJNSU5BVEVEIiwiVEhFTiIsIlRPIiwiVFJBSUxJTkciLCJUUkFOU0FDVElPTkFMIiwiVFJVRSIsIlRSVU5DQVRFIiwiVFlQRSIsIlRZUEVTIiwiVU5DT01NSVRURUQiLCJVTklRVUUiLCJVTkxPQ0siLCJVTlNJR05FRCIsIlVTQUdFIiwiVVNFIiwiVVNJTkciLCJWQVJJQUJMRVMiLCJWSUVXIiwiV0hFTiIsIldJVEgiLCJXT1JLIiwiV1JJVEUiLCJZRUFSX01PTlRIIl0sTz1bIkFERCIsIkFGVEVSIiwiQUxURVIgQ09MVU1OIiwiQUxURVIgVEFCTEUiLCJERUxFVEUgRlJPTSIsIkVYQ0VQVCIsIkZFVENIIEZJUlNUIiwiRlJPTSIsIkdST1VQIEJZIiwiR08iLCJIQVZJTkciLCJJTlNFUlQgSU5UTyIsIklOU0VSVCIsIklOVEVSU0VDVCIsIkxJTUlUIiwiTU9ESUZZIiwiT1JERVIgQlkiLCJTRUxFQ1QiLCJTRVQgQ1VSUkVOVCBTQ0hFTUEiLCJTRVQgU0NIRU1BIiwiU0VUIiwiVU5JT04gQUxMIiwiVU5JT04iLCJVUERBVEUiLCJWQUxVRVMiLCJXSEVSRSJdLGk9WyJBTkQiLCJDUk9TUyBBUFBMWSIsIkNST1NTIEpPSU4iLCJFTFNFIiwiSU5ORVIgSk9JTiIsIkpPSU4iLCJMRUZUIEpPSU4iLCJMRUZUIE9VVEVSIEpPSU4iLCJPUiIsIk9VVEVSIEFQUExZIiwiT1VURVIgSk9JTiIsIlJJR0hUIEpPSU4iLCJSSUdIVCBPVVRFUiBKT0lOIiwiV0hFTiIsIlhPUiJdLFM9dm9pZCAwLHU9ZnVuY3Rpb24oKXtmdW5jdGlvbiBlKEUpeygwLFRbImRlZmF1bHQiXSkodGhpcyxlKSx0aGlzLmNmZz1FfXJldHVybiBlLnByb3RvdHlwZS5mb3JtYXQ9ZnVuY3Rpb24oZSl7cmV0dXJuIFN8fChTPW5ldyBBWyJkZWZhdWx0Il0oe3Jlc2VydmVkV29yZHM6SSxyZXNlcnZlZFRvcGxldmVsV29yZHM6TyxyZXNlcnZlZE5ld2xpbmVXb3JkczppLHN0cmluZ1R5cGVzOlsnIiInLCJOJyciLCInJyIsImBgIiwiW10iXSxvcGVuUGFyZW5zOlsiKCIsIkNBU0UiXSxjbG9zZVBhcmVuczpbIikiLCJFTkQiXSxpbmRleGVkUGxhY2Vob2xkZXJUeXBlczpbIj8iXSxuYW1lZFBsYWNlaG9sZGVyVHlwZXM6WyJAIiwiOiJdLGxpbmVDb21tZW50VHlwZXM6WyIjIiwiLS0iXX0pKSxuZXcgb1siZGVmYXVsdCJdKHRoaXMuY2ZnLFMpLmZvcm1hdChlKX0sZX0oKTtFWyJkZWZhdWx0Il09dSxlLmV4cG9ydHM9RVsiZGVmYXVsdCJdfSxmdW5jdGlvbihlLEUsdCl7dmFyIG49dCgzKSxyPXQoMiksVD1uKHIsIkRhdGFWaWV3Iik7ZS5leHBvcnRzPVR9LGZ1bmN0aW9uKGUsRSx0KXt2YXIgbj10KDMpLHI9dCgyKSxUPW4ociwiTWFwIik7ZS5leHBvcnRzPVR9LGZ1bmN0aW9uKGUsRSx0KXt2YXIgbj10KDMpLHI9dCgyKSxUPW4ociwiUHJvbWlzZSIpO2UuZXhwb3J0cz1UfSxmdW5jdGlvbihlLEUsdCl7dmFyIG49dCgzKSxyPXQoMiksVD1uKHIsIlNldCIpO2UuZXhwb3J0cz1UfSxmdW5jdGlvbihlLEUsdCl7dmFyIG49dCgyKSxyPW4uU3ltYm9sO2UuZXhwb3J0cz1yfSxmdW5jdGlvbihlLEUsdCl7dmFyIG49dCgzKSxyPXQoMiksVD1uKHIsIldlYWtNYXAiKTtlLmV4cG9ydHM9VH0sZnVuY3Rpb24oZSxFKXtmdW5jdGlvbiB0KGUpe3JldHVybiBlLnNwbGl0KCIiKX1lLmV4cG9ydHM9dH0sZnVuY3Rpb24oZSxFKXtmdW5jdGlvbiB0KGUsRSx0LG4pe2Zvcih2YXIgcj1lLmxlbmd0aCxUPXQrKG4/MTotMSk7bj9ULS06KytUPHI7KWlmKEUoZVtUXSxULGUpKXJldHVybiBUOwpyZXR1cm4tMX1lLmV4cG9ydHM9dH0sZnVuY3Rpb24oZSxFKXtmdW5jdGlvbiB0KGUpe3JldHVybiByLmNhbGwoZSl9dmFyIG49T2JqZWN0LnByb3RvdHlwZSxyPW4udG9TdHJpbmc7ZS5leHBvcnRzPXR9LGZ1bmN0aW9uKGUsRSx0KXtmdW5jdGlvbiBuKGUsRSx0KXtyZXR1cm4gRT09PUU/UihlLEUsdCk6cihlLFQsdCl9dmFyIHI9dCgyOSksVD10KDMyKSxSPXQoNDkpO2UuZXhwb3J0cz1ufSxmdW5jdGlvbihlLEUpe2Z1bmN0aW9uIHQoZSl7cmV0dXJuIGUhPT1lfWUuZXhwb3J0cz10fSxmdW5jdGlvbihlLEUsdCl7ZnVuY3Rpb24gbihlKXtpZighUihlKXx8VChlKSlyZXR1cm4hMTt2YXIgRT1yKGUpP3U6QTtyZXR1cm4gRS50ZXN0KG8oZSkpfXZhciByPXQoMTIpLFQ9dCg0NSksUj10KDYpLG89dCgxMSksTj0vW1xcXiQuKis/KClbXF17fXxdL2csQT0vXlxbb2JqZWN0IC4rP0NvbnN0cnVjdG9yXF0kLyxJPUZ1bmN0aW9uLnByb3RvdHlwZSxPPU9iamVjdC5wcm90b3R5cGUsaT1JLnRvU3RyaW5nLFM9Ty5oYXNPd25Qcm9wZXJ0eSx1PVJlZ0V4cCgiXiIraS5jYWxsKFMpLnJlcGxhY2UoTiwiXFwkJiIpLnJlcGxhY2UoL2hhc093blByb3BlcnR5fChmdW5jdGlvbikuKj8oPz1cXFwoKXwgZm9yIC4rPyg/PVxcXF0pL2csIiQxLio/IikrIiQiKTtlLmV4cG9ydHM9bn0sZnVuY3Rpb24oZSxFKXtmdW5jdGlvbiB0KGUsRSl7dmFyIHQ9IiI7aWYoIWV8fDE+RXx8RT5uKXJldHVybiB0O2RvIEUlMiYmKHQrPWUpLEU9cihFLzIpLEUmJihlKz1lKTt3aGlsZShFKTtyZXR1cm4gdH12YXIgbj05MDA3MTk5MjU0NzQwOTkxLHI9TWF0aC5mbG9vcjtlLmV4cG9ydHM9dH0sZnVuY3Rpb24oZSxFKXtmdW5jdGlvbiB0KGUsRSx0KXt2YXIgbj0tMSxyPWUubGVuZ3RoOzA+RSYmKEU9LUU+cj8wOnIrRSksdD10PnI/cjp0LDA+dCYmKHQrPXIpLHI9RT50PzA6dC1FPj4+MCxFPj4+PTA7Zm9yKHZhciBUPUFycmF5KHIpOysrbjxyOylUW25dPWVbbitFXTtyZXR1cm4gVH1lLmV4cG9ydHM9dH0sZnVuY3Rpb24oZSxFLHQpe2Z1bmN0aW9uIG4oZSxFLHQpe3ZhciBuPWUubGVuZ3RoO3JldHVybiB0PXZvaWQgMD09PXQ/bjp0LEV8fG4+dD9yKGUsRSx0KTplfXZhciByPXQoMzUpO2UuZXhwb3J0cz1ufSxmdW5jdGlvbihlLEUsdCl7ZnVuY3Rpb24gbihlLEUpe2Zvcih2YXIgdD1lLmxlbmd0aDt0LS0mJnIoRSxlW3RdLDApPi0xOyk7cmV0dXJuIHR9dmFyIHI9dCgzMSk7ZS5leHBvcnRzPW59LGZ1bmN0aW9uKGUsRSx0KXt2YXIgbj10KDIpLHI9blsiX19jb3JlLWpzX3NoYXJlZF9fIl07ZS5leHBvcnRzPXJ9LGZ1bmN0aW9uKGUsRSl7KGZ1bmN0aW9uKEUpe3ZhciB0PSJvYmplY3QiPT10eXBlb2YgRSYmRSYmRS5PYmplY3Q9PT1PYmplY3QmJkU7ZS5leHBvcnRzPXR9KS5jYWxsKEUsZnVuY3Rpb24oKXtyZXR1cm4gdGhpc30oKSl9LGZ1bmN0aW9uKGUsRSx0KXt2YXIgbj10KDIyKSxyPXQoMjMpLFQ9dCgyNCksUj10KDI1KSxvPXQoMjcpLE49dCgzMCksQT10KDExKSxJPSJbb2JqZWN0IE1hcF0iLE89IltvYmplY3QgT2JqZWN0XSIsaT0iW29iamVjdCBQcm9taXNlXSIsUz0iW29iamVjdCBTZXRdIix1PSJbb2JqZWN0IFdlYWtNYXBdIixMPSJbb2JqZWN0IERhdGFWaWV3XSIsQz1PYmplY3QucHJvdG90eXBlLHM9Qy50b1N0cmluZyxhPUEobiksZj1BKHIpLGM9QShUKSxwPUEoUiksbD1BKG8pLEQ9TjsobiYmRChuZXcgbihuZXcgQXJyYXlCdWZmZXIoMSkpKSE9THx8ciYmRChuZXcgcikhPUl8fFQmJkQoVC5yZXNvbHZlKCkpIT1pfHxSJiZEKG5ldyBSKSE9U3x8byYmRChuZXcgbykhPXUpJiYoRD1mdW5jdGlvbihlKXt2YXIgRT1zLmNhbGwoZSksdD1FPT1PP2UuY29uc3RydWN0b3I6dm9pZCAwLG49dD9BKHQpOnZvaWQgMDtpZihuKXN3aXRjaChuKXtjYXNlIGE6cmV0dXJuIEw7Y2FzZSBmOnJldHVybiBJO2Nhc2UgYzpyZXR1cm4gaTtjYXNlIHA6cmV0dXJuIFM7Y2FzZSBsOnJldHVybiB1fXJldHVybiBFfSksZS5leHBvcnRzPUR9LGZ1bmN0aW9uKGUsRSl7ZnVuY3Rpb24gdChlLEUpe3JldHVybiBudWxsPT1lP3ZvaWQgMDplW0VdfWUuZXhwb3J0cz10fSxmdW5jdGlvbihlLEUpe2Z1bmN0aW9uIHQoZSl7cmV0dXJuIE4udGVzdChlKX12YXIgbj0iXFx1ZDgwMC1cXHVkZmZmIixyPSJcXHUwMzAwLVxcdTAzNmZcXHVmZTIwLVxcdWZlMjMiLFQ9IlxcdTIwZDAtXFx1MjBmMCIsUj0iXFx1ZmUwZVxcdWZlMGYiLG89IlxcdTIwMGQiLE49UmVnRXhwKCJbIitvK24rcitUK1IrIl0iKTtlLmV4cG9ydHM9dH0sZnVuY3Rpb24oZSxFKXtmdW5jdGlvbiB0KGUsRSl7cmV0dXJuIEU9bnVsbD09RT9uOkUsISFFJiYoIm51bWJlciI9PXR5cGVvZiBlfHxyLnRlc3QoZSkpJiZlPi0xJiZlJTE9PTAmJkU+ZX12YXIgbj05MDA3MTk5MjU0NzQwOTkxLHI9L14oPzowfFsxLTldXGQqKSQvO2UuZXhwb3J0cz10fSxmdW5jdGlvbihlLEUsdCl7ZnVuY3Rpb24gbihlLEUsdCl7aWYoIW8odCkpcmV0dXJuITE7dmFyIG49dHlwZW9mIEU7cmV0dXJuISEoIm51bWJlciI9PW4/VCh0KSYmUihFLHQubGVuZ3RoKToic3RyaW5nIj09biYmRSBpbiB0KSYmcih0W0VdLGUpfXZhciByPXQoNTIpLFQ9dCg4KSxSPXQoNDMpLG89dCg2KTtlLmV4cG9ydHM9bn0sZnVuY3Rpb24oZSxFLHQpe2Z1bmN0aW9uIG4oZSl7cmV0dXJuISFUJiZUIGluIGV9dmFyIHI9dCgzOCksVD1mdW5jdGlvbigpe3ZhciBlPS9bXi5dKyQvLmV4ZWMociYmci5rZXlzJiZyLmtleXMuSUVfUFJPVE98fCIiKTtyZXR1cm4gZT8iU3ltYm9sKHNyYylfMS4iK2U6IiJ9KCk7ZS5leHBvcnRzPW59LGZ1bmN0aW9uKGUsRSl7ZnVuY3Rpb24gdChlKXt2YXIgRT1lJiZlLmNvbnN0cnVjdG9yLHQ9ImZ1bmN0aW9uIj09dHlwZW9mIEUmJkUucHJvdG90eXBlfHxuO3JldHVybiBlPT09dH12YXIgbj1PYmplY3QucHJvdG90eXBlO2UuZXhwb3J0cz10fSxmdW5jdGlvbihlLEUsdCl7dmFyIG49dCg0OCkscj1uKE9iamVjdC5rZXlzLE9iamVjdCk7ZS5leHBvcnRzPXJ9LGZ1bmN0aW9uKGUsRSl7ZnVuY3Rpb24gdChlLEUpe3JldHVybiBmdW5jdGlvbih0KXtyZXR1cm4gZShFKHQpKX19ZS5leHBvcnRzPXR9LGZ1bmN0aW9uKGUsRSl7ZnVuY3Rpb24gdChlLEUsdCl7Zm9yKHZhciBuPXQtMSxyPWUubGVuZ3RoOysrbjxyOylpZihlW25dPT09RSlyZXR1cm4gbjtyZXR1cm4tMX1lLmV4cG9ydHM9dH0sZnVuY3Rpb24oZSxFLHQpe2Z1bmN0aW9uIG4oZSl7cmV0dXJuIFQoZSk/UihlKTpyKGUpfXZhciByPXQoMjgpLFQ9dCg0MiksUj10KDUxKTtlLmV4cG9ydHM9bn0sZnVuY3Rpb24oZSxFKXtmdW5jdGlvbiB0KGUpe3JldHVybiBlLm1hdGNoKGMpfHxbXX12YXIgbj0iXFx1ZDgwMC1cXHVkZmZmIixyPSJcXHUwMzAwLVxcdTAzNmZcXHVmZTIwLVxcdWZlMjMiLFQ9IlxcdTIwZDAtXFx1MjBmMCIsUj0iXFx1ZmUwZVxcdWZlMGYiLG89IlsiK24rIl0iLE49IlsiK3IrVCsiXSIsQT0iXFx1ZDgzY1tcXHVkZmZiLVxcdWRmZmZdIixJPSIoPzoiK04rInwiK0ErIikiLE89IlteIituKyJdIixpPSIoPzpcXHVkODNjW1xcdWRkZTYtXFx1ZGRmZl0pezJ9IixTPSJbXFx1ZDgwMC1cXHVkYmZmXVtcXHVkYzAwLVxcdWRmZmZdIix1PSJcXHUyMDBkIixMPUkrIj8iLEM9IlsiK1IrIl0/IixzPSIoPzoiK3UrIig/OiIrW08saSxTXS5qb2luKCJ8IikrIikiK0MrTCsiKSoiLGE9QytMK3MsZj0iKD86IitbTytOKyI/IixOLGksUyxvXS5qb2luKCJ8IikrIikiLGM9UmVnRXhwKEErIig/PSIrQSsiKXwiK2YrYSwiZyIpO2UuZXhwb3J0cz10fSxmdW5jdGlvbihlLEUpe2Z1bmN0aW9uIHQoZSxFKXtyZXR1cm4gZT09PUV8fGUhPT1lJiZFIT09RX1lLmV4cG9ydHM9dH0sZnVuY3Rpb24oZSxFLHQpe2Z1bmN0aW9uIG4oZSl7cmV0dXJuIGU9cihlKSxlJiZSLnRlc3QoZSk/ZS5yZXBsYWNlKFQsIlxcJCYiKTplfXZhciByPXQoOSksVD0vW1xcXiQuKis/KClbXF17fXxdL2csUj1SZWdFeHAoVC5zb3VyY2UpO2UuZXhwb3J0cz1ufSxmdW5jdGlvbihlLEUsdCl7ZnVuY3Rpb24gbihlKXtyZXR1cm4gcihlKSYmby5jYWxsKGUsImNhbGxlZSIpJiYoIUEuY2FsbChlLCJjYWxsZWUiKXx8Ti5jYWxsKGUpPT1UKX12YXIgcj10KDU2KSxUPSJbb2JqZWN0IEFyZ3VtZW50c10iLFI9T2JqZWN0LnByb3RvdHlwZSxvPVIuaGFzT3duUHJvcGVydHksTj1SLnRvU3RyaW5nLEE9Ui5wcm9wZXJ0eUlzRW51bWVyYWJsZTtlLmV4cG9ydHM9bn0sZnVuY3Rpb24oZSxFKXt2YXIgdD1BcnJheS5pc0FycmF5O2UuZXhwb3J0cz10fSxmdW5jdGlvbihlLEUsdCl7ZnVuY3Rpb24gbihlKXtyZXR1cm4gVChlKSYmcihlKX12YXIgcj10KDgpLFQ9dCgxMyk7ZS5leHBvcnRzPW59LGZ1bmN0aW9uKGUsRSx0KXsoZnVuY3Rpb24oZSl7dmFyIG49dCgyKSxyPXQoNjIpLFQ9Im9iamVjdCI9PXR5cGVvZiBFJiZFJiYhRS5ub2RlVHlwZSYmRSxSPVQmJiJvYmplY3QiPT10eXBlb2YgZSYmZSYmIWUubm9kZVR5cGUmJmUsbz1SJiZSLmV4cG9ydHM9PT1ULE49bz9uLkJ1ZmZlcjp2b2lkIDAsQT1OP04uaXNCdWZmZXI6dm9pZCAwLEk9QXx8cjtlLmV4cG9ydHM9SX0pLmNhbGwoRSx0KDY3KShlKSl9LGZ1bmN0aW9uKGUsRSx0KXtmdW5jdGlvbiBuKGUpe2lmKG8oZSkmJihSKGUpfHwic3RyaW5nIj09dHlwZW9mIGV8fCJmdW5jdGlvbiI9PXR5cGVvZiBlLnNwbGljZXx8TihlKXx8VChlKSkpcmV0dXJuIWUubGVuZ3RoO3ZhciBFPXIoZSk7aWYoRT09T3x8RT09aSlyZXR1cm4hZS5zaXplO2lmKEEoZSkpcmV0dXJuIUkoZSkubGVuZ3RoO2Zvcih2YXIgdCBpbiBlKWlmKHUuY2FsbChlLHQpKXJldHVybiExO3JldHVybiEwfXZhciByPXQoNDApLFQ9dCg1NCksUj10KDU1KSxvPXQoOCksTj10KDU3KSxBPXQoNDYpLEk9dCg0NyksTz0iW29iamVjdCBNYXBdIixpPSJbb2JqZWN0IFNldF0iLFM9T2JqZWN0LnByb3RvdHlwZSx1PVMuaGFzT3duUHJvcGVydHk7ZS5leHBvcnRzPW59LGZ1bmN0aW9uKGUsRSl7ZnVuY3Rpb24gdChlKXtyZXR1cm4ibnVtYmVyIj09dHlwZW9mIGUmJmU+LTEmJmUlMT09MCYmbj49ZX12YXIgbj05MDA3MTk5MjU0NzQwOTkxO2UuZXhwb3J0cz10fSxmdW5jdGlvbihlLEUpe2Z1bmN0aW9uIHQoZSl7dmFyIEU9ZT9lLmxlbmd0aDowO3JldHVybiBFP2VbRS0xXTp2b2lkIDB9ZS5leHBvcnRzPXR9LGZ1bmN0aW9uKGUsRSx0KXtmdW5jdGlvbiBuKGUsRSx0KXtyZXR1cm4gRT0odD9UKGUsRSx0KTp2b2lkIDA9PT1FKT8xOlIoRSkscihvKGUpLEUpfXZhciByPXQoMzQpLFQ9dCg0NCksUj10KDY0KSxvPXQoOSk7ZS5leHBvcnRzPW59LGZ1bmN0aW9uKGUsRSl7ZnVuY3Rpb24gdCgpe3JldHVybiExfWUuZXhwb3J0cz10fSxmdW5jdGlvbihlLEUsdCl7ZnVuY3Rpb24gbihlKXtpZighZSlyZXR1cm4gMD09PWU/ZTowO2lmKGU9cihlKSxlPT09VHx8ZT09PS1UKXt2YXIgRT0wPmU/LTE6MTtyZXR1cm4gRSpSfXJldHVybiBlPT09ZT9lOjB9dmFyIHI9dCg2NSksVD0xLzAsUj0xLjc5NzY5MzEzNDg2MjMxNTdlMzA4O2UuZXhwb3J0cz1ufSxmdW5jdGlvbihlLEUsdCl7ZnVuY3Rpb24gbihlKXt2YXIgRT1yKGUpLHQ9RSUxO3JldHVybiBFPT09RT90P0UtdDpFOjB9dmFyIHI9dCg2Myk7ZS5leHBvcnRzPW59LGZ1bmN0aW9uKGUsRSx0KXtmdW5jdGlvbiBuKGUpe2lmKCJudW1iZXIiPT10eXBlb2YgZSlyZXR1cm4gZTtpZihUKGUpKXJldHVybiBSO2lmKHIoZSkpe3ZhciBFPSJmdW5jdGlvbiI9PXR5cGVvZiBlLnZhbHVlT2Y/ZS52YWx1ZU9mKCk6ZTtlPXIoRSk/RSsiIjpFfWlmKCJzdHJpbmciIT10eXBlb2YgZSlyZXR1cm4gMD09PWU/ZTorZTtlPWUucmVwbGFjZShvLCIiKTt2YXIgdD1BLnRlc3QoZSk7cmV0dXJuIHR8fEkudGVzdChlKT9PKGUuc2xpY2UoMiksdD8yOjgpOk4udGVzdChlKT9SOitlfXZhciByPXQoNiksVD10KDE0KSxSPU5hTixvPS9eXHMrfFxzKyQvZyxOPS9eWy0rXTB4WzAtOWEtZl0rJC9pLEE9L14wYlswMV0rJC9pLEk9L14wb1swLTddKyQvaSxPPXBhcnNlSW50O2UuZXhwb3J0cz1ufSxmdW5jdGlvbihlLEUsdCl7ZnVuY3Rpb24gbihlLEUsdCl7aWYoZT1OKGUpLGUmJih0fHx2b2lkIDA9PT1FKSlyZXR1cm4gZS5yZXBsYWNlKEEsIiIpO2lmKCFlfHwhKEU9cihFKSkpcmV0dXJuIGU7dmFyIG49byhlKSxJPVIobixvKEUpKSsxO3JldHVybiBUKG4sMCxJKS5qb2luKCIiKX12YXIgcj10KDEwKSxUPXQoMzYpLFI9dCgzNyksbz10KDUwKSxOPXQoOSksQT0vXHMrJC87ZS5leHBvcnRzPW59LGZ1bmN0aW9uKGUsRSl7ZS5leHBvcnRzPWZ1bmN0aW9uKGUpe3JldHVybiBlLndlYnBhY2tQb2x5ZmlsbHx8KGUuZGVwcmVjYXRlPWZ1bmN0aW9uKCl7fSxlLnBhdGhzPVtdLGUuY2hpbGRyZW49W10sZS53ZWJwYWNrUG9seWZpbGw9MSksZX19XSl9KTsKCgkJZnVuY3Rpb24gZXNjYXBlMkh0bWwoc3RyKSB7CiAgICAJICAgIHZhciBhcnJFbnRpdGllcyA9IHsnbHQnOiAnPCcsICdndCc6ICc+JywgJ25ic3AnOiAnJywgJ2FtcCc6ICcmJywgJ3F1b3QnOiAnIid9OwogICAgCSAgICByZXR1cm4gc3RyLnJlcGxhY2UoLyYobHR8Z3R8bmJzcHxhbXB8cXVvdCk7L2lnLCBmdW5jdGlvbiAoYWxsLCB0KSB7CiAgICAJICAgICAgICByZXR1cm4gYXJyRW50aXRpZXNbdF07CiAgICAJICAgIH0pOwogICAgCX0KCQogICAgCWZ1bmN0aW9uIGxvYWQoKSB7CiAgICAJICAgIGxldCBjb2RlTGlzdCA9IGRvY3VtZW50LmdldEVsZW1lbnRzQnlDbGFzc05hbWUoJ2xhbmd1YWdlLXNxbCcpOwoJCiAgICAJICAgIGZvciAobGV0IGkgPSAwIDtpPGNvZGVMaXN0Lmxlbmd0aDtpKyspIHsKICAgIAkgICAgICAgIGNvZGVMaXN0W2ldLmlubmVySFRNTCA9IHdpbmRvdy5zcWxGb3JtYXR0ZXIuZm9ybWF0KGVzY2FwZTJIdG1sKGNvZGVMaXN0W2ldLmlubmVySFRNTCkpCiAgICAJICAgIH0KICAgIAl9Owo=
+`
+
+// MarkdownEscape markdown格式转义,原样输出
+func MarkdownEscape(str string) string {
+	for _, b := range "_`*" {
+		str = strings.Replace(str, string(b), "\\"+string(b), -1)
+	}
+	return str
+}
+
+//
+func loadExternalResource(resource string) string {
+	var content string
+	var body []byte
+	if strings.HasPrefix(resource, "http") {
+		resp, err := http.Get(resource)
+		if err == nil {
+			body, err = ioutil.ReadAll(resp.Body)
+			if err == nil {
+				content = string(body)
+			} else {
+				Log.Debug("ioutil.ReadAll %s Error: %v", resource, err)
+			}
+		} else {
+			Log.Debug("http.Get %s Error: %v", resource, err)
+		}
+		defer resp.Body.Close()
+	} else {
+		fd, err := os.Open(resource)
+		defer func() {
+			err = fd.Close()
+			if err != nil {
+				Log.Error("loadExternalResource(%s) fd.Close failed: %s", resource, err.Error())
+			}
+		}()
+		if err == nil {
+			body, err = ioutil.ReadAll(fd)
+			if err != nil {
+				Log.Debug("ioutil.ReadAll %s Error: %v", resource, err)
+			} else {
+				content = string(body)
+			}
+		} else {
+			Log.Debug("os.Open %s Error: %v", resource, err)
+		}
+	}
+	return content
+}
+
+// MarkdownHTMLHeader markdown转HTML输出时添加HTML头
+func MarkdownHTMLHeader() string {
+	// load css
+	var css string
+	if Config.ReportCSS == "" {
+		css = BuiltinCSS
+	} else {
+		css = loadExternalResource(Config.ReportCSS)
+	}
+
+	// load javascript
+	var js string
+	if Config.ReportJavascript == "" {
+		decode, _ := base64.StdEncoding.DecodeString(BuiltinJavascript)
+		js = string(decode)
+	} else {
+		js = loadExternalResource(Config.ReportJavascript)
+	}
+
+	header := `
+
+` + Config.ReportTitle + `
+
+
+
+
+`
+	return header
+}
+
+// Markdown2HTML markdown转HTML输出
+func Markdown2HTML(buf string) string {
+	// extensions default: 94
+	// extensions |= blackfriday.EXTENSION_TABLES
+	// extensions |= blackfriday.EXTENSION_FENCED_CODE
+	// extensions |= blackfriday.EXTENSION_AUTOLINK
+	// extensions |= blackfriday.EXTENSION_STRIKETHROUGH
+	// extensions |= blackfriday.EXTENSION_SPACE_HEADERS
+	extensions := Config.MarkdownExtensions
+
+	// htmlFlags
+	htmlFlags := Config.MarkdownHTMLFlags
+
+	renderer := blackfriday.HtmlRenderer(htmlFlags, "", "")
+	buf = string(blackfriday.Markdown([]byte(buf), renderer, extensions))
+	return buf
+}
+
+// Score SQL评审打分
+func Score(score int) string {
+	// 不需要打分的功能
+	switch Config.ReportType {
+	case "duplicate-key-checker", "explain-digest":
+		return ""
+	}
+	s1, s2 := "★ ", "☆ "
+	if score > 100 {
+		score = 100
+		Log.Debug("Score Error: score larger than 100, %d", score)
+	}
+	if score < 0 {
+		score = 0
+		Log.Debug("Score Warn: score less than 0, %d", score)
+	}
+	s1Count := score / 20
+	s2Count := 5 - s1Count
+	str := fmt.Sprintf("%s %d分", strings.TrimSpace(strings.Repeat(s1, s1Count)+strings.Repeat(s2, s2Count)), score)
+	return str
+}
diff --git a/common/markdown_test.go b/common/markdown_test.go
new file mode 100644
index 00000000..b5fd65bc
--- /dev/null
+++ b/common/markdown_test.go
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2018 Xiaomi, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package common
+
+import (
+	"fmt"
+	"io"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"testing"
+)
+
+func TestMarkdownEscape(_ *testing.T) {
+	var strs = []string{
+		"a`bc",
+		"abc",
+		"a'bc",
+		"a\"bc",
+	}
+	for _, str := range strs {
+		fmt.Println(MarkdownEscape(str))
+	}
+}
+
+func TestMarkdown2Html(t *testing.T) {
+	md := filepath.Join("testdata", t.Name()+".md")
+	buf, err := ioutil.ReadFile(md)
+	if err != nil {
+		t.Error(err.Error())
+	}
+	err = GoldenDiff(func() {
+		fmt.Println(Markdown2HTML(string(buf)))
+	}, t.Name(), update)
+	if nil != err {
+		t.Fatal(err)
+	}
+
+	// golden文件拷贝成html文件,这步是给人看的
+	gd, err := os.OpenFile("testdata/"+t.Name()+".golden", os.O_RDONLY, 0666)
+	if nil != err {
+		t.Fatal(err)
+	}
+	html, err := os.OpenFile("testdata/"+t.Name()+".html", os.O_CREATE|os.O_RDWR, 0666)
+	if nil != err {
+		t.Fatal(err)
+	}
+	io.Copy(html, gd)
+}
+
+func TestScore(t *testing.T) {
+	score := Score(50)
+	if score != "★ ★ ☆ ☆ ☆ 50分" {
+		t.Error(score)
+	}
+}
+
+func TestLoadExternalResource(t *testing.T) {
+	buf := loadExternalResource("../doc/themes/github.css")
+	if buf == "" {
+		t.Error("loadExternalResource local error")
+	}
+	buf = loadExternalResource("http://www.baidu.com")
+	if buf == "" {
+		t.Error("loadExternalResource http error")
+	}
+}
+
+func TestMarkdownHTMLHeader(t *testing.T) {
+	err := GoldenDiff(func() {
+		MarkdownHTMLHeader()
+	}, t.Name(), update)
+	if err != nil {
+		t.Error(err)
+	}
+}
diff --git a/common/meta.go b/common/meta.go
new file mode 100644
index 00000000..bfa18e9f
--- /dev/null
+++ b/common/meta.go
@@ -0,0 +1,495 @@
+/*
+ * Copyright 2018 Xiaomi, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package common
+
+import (
+	"strconv"
+	"strings"
+)
+
+// Meta 以'database'为key, DB的map,按db->table->column组织的元数据
+type Meta map[string]*DB
+
+// DB 数据库相关的结构体
+type DB struct {
+	Name  string
+	Table map[string]*Table // ['table_name']*Table
+}
+
+// NewDB 用于初始化*DB
+func NewDB(db string) *DB {
+	return &DB{
+		Name:  db,
+		Table: make(map[string]*Table),
+	}
+}
+
+// Table 含有表的属性
+type Table struct {
+	TableName    string
+	TableAliases []string
+	Column       map[string]*Column
+}
+
+// NewTable 初始化*Table
+func NewTable(tb string) *Table {
+	return &Table{
+		TableName:    tb,
+		TableAliases: make([]string, 0),
+		Column:       make(map[string]*Column),
+	}
+}
+
+// KeyType 用于标志每个Key的类别
+type KeyType int
+
+// Column 含有列的定义属性
+type Column struct {
+	Name        string   `json:"col_name"`    // 列名
+	Alias       []string `json:"alias"`       // 别名
+	Table       string   `json:"tb_name"`     // 表名
+	DB          string   `json:"db_name"`     // 数据库名称
+	DataType    string   `json:"data_type"`   // 数据类型
+	Character   string   `json:"character"`   // 字符集
+	Collation   string   `json:"collation"`   // collation
+	Cardinality float64  `json:"cardinality"` // 散粒度
+	Null        string   `json:"null"`        // 是否为空: YES/NO
+	Key         string   `json:"key"`         // 键类型
+	Default     string   `json:"default"`     // 默认值
+	Extra       string   `json:"extra"`       // 其他
+	Comment     string   `json:"comment"`     // 备注
+	Privileges  string   `json:"privileges"`  // 权限
+}
+
+// TableColumns 这个结构体中的元素是有序的  map[db]map[table][]columns
+type TableColumns map[string]map[string][]*Column
+
+// Equal 判断两个column是否相等
+func (col *Column) Equal(column *Column) bool {
+	return col.Name == column.Name &&
+		col.Table == column.Table &&
+		col.DB == column.DB
+}
+
+// IsColsPart 判断两个column队列是否是包含关系(包括相等)
+func IsColsPart(a, b []*Column) bool {
+	times := len(a)
+	if len(b) < times {
+		times = len(b)
+	}
+
+	for i := 0; i < times; i++ {
+		if a[i].DB != b[i].DB || a[i].Table != b[i].Table || a[i].Name != b[i].Name {
+			return false
+		}
+	}
+
+	return true
+}
+
+// JoinColumnsName 将所有的列合并
+func JoinColumnsName(cols []*Column, sep string) string {
+	name := ""
+	for _, col := range cols {
+		name += col.Name + sep
+	}
+	return strings.Trim(name, sep)
+}
+
+// Tables 获取Meta中指定db的所有表名
+// Input:数据库名
+// Output:表名组成的list
+func (b Meta) Tables(db string) []string {
+	var result []string
+	if b[db] != nil {
+		for tb := range b[db].Table {
+			result = append(result, tb)
+		}
+
+	}
+	return result
+}
+
+// SetDefault 设置默认值
+func (b Meta) SetDefault(defaultDB string) Meta {
+	if defaultDB == "" {
+		return b
+	}
+
+	for db := range b {
+		if db == "" {
+			// 当获取到的join中的DB为空的时候,说明SQL未显示的指定DB,即使用的是rEnv默认DB,需要将表合并到原DB中
+			if _, ok := b[defaultDB]; ok {
+				for tbName, table := range b[""].Table {
+					if _, ok := b[defaultDB].Table[tbName]; ok {
+						b[defaultDB].Table[tbName].TableAliases = append(
+							b[defaultDB].Table[tbName].TableAliases,
+							table.TableAliases...,
+						)
+						continue
+					}
+					b[defaultDB].Table[tbName] = table
+				}
+				delete(b, "")
+			}
+
+			// 如果没有出现DB指定不一致的情况,直接进行合并
+			b[defaultDB] = b[""]
+			delete(b, "")
+		}
+	}
+
+	return b
+}
+
+// MergeColumn 将使用到的列按db->table组织去重
+// 注意:Column中的db, table信息可能为空,需要提前通过env环境补齐再调用该函数。
+// @input: 目标列list, 源列list(可以将多个源合并到一个目标列list)
+// @output: 合并后的列list
+func MergeColumn(dst []*Column, src ...*Column) []*Column {
+	var tmp []*Column
+	for _, newCol := range src {
+		if len(dst) == 0 {
+			tmp = append(tmp, newCol)
+			continue
+		}
+
+		has := false
+		for _, oldCol := range dst {
+			if (newCol.Name == oldCol.Name) && (newCol.Table == oldCol.Table) && (newCol.DB == oldCol.DB) {
+				has = true
+			}
+		}
+
+		if !has {
+			tmp = append(tmp, newCol)
+		}
+
+	}
+	return append(dst, tmp...)
+}
+
+// ColumnSort 通过散粒度对 colList 进行排序, 散粒度排序由大到小
+func ColumnSort(colList []*Column) []*Column {
+	// 使用冒泡排序保持相等情况下左右两边顺序不变
+	if len(colList) < 2 {
+		return colList
+	}
+
+	for i := 0; i < len(colList)-1; i++ {
+		for j := i + 1; j < len(colList); j++ {
+			if colList[i].Cardinality < colList[j].Cardinality {
+				colList[i], colList[j] = colList[j], colList[i]
+			}
+		}
+	}
+
+	return colList
+}
+
+// GetDataTypeBase 获取dataType中的数据类型,忽略长度
+func GetDataTypeBase(dataType string) string {
+	if i := strings.Index(dataType, "("); i > 0 {
+		return dataType[0:i]
+	}
+
+	return dataType
+}
+
+// GetDataTypeLength 获取dataType中的数据类型长度
+func GetDataTypeLength(dataType string) []int {
+	var length []int
+	if si := strings.Index(dataType, "("); si > 0 {
+		dataLength := dataType[si+1:]
+		if ei := strings.Index(dataLength, ")"); ei > 0 {
+			dataLength = dataLength[:ei]
+			for _, l := range strings.Split(dataLength, ",") {
+				v, err := strconv.Atoi(l)
+				if err != nil {
+					Log.Debug("GetDataTypeLength() Error: %v", err)
+					return []int{-1}
+				}
+				length = append(length, v)
+			}
+		}
+	}
+
+	if len(length) == 0 {
+		length = []int{-1}
+	}
+
+	return length
+}
+
+// GetDataBytes 计算数据类型字节数
+// https://dev.mysql.com/doc/refman/8.0/en/storage-requirements.html
+// return -1 表示该列无法计算数据大小
+func (col *Column) GetDataBytes(dbVersion int) int {
+	if col.DataType == "" {
+		Log.Warning("Can't get %s.%s data type", col.Table, col.Name)
+		return -1
+	}
+	switch strings.ToLower(GetDataTypeBase(col.DataType)) {
+	case "tinyint", "smallint", "mediumint",
+		"int", "integer", "bigint",
+		"double", "real", "float", "decimal",
+		"numeric", "bit":
+		// numeric
+		return numericStorageReq(col.DataType)
+
+	case "year", "date", "time", "datetime", "timestamp":
+		// date & time
+		return timeStorageReq(col.DataType, dbVersion)
+
+	case "char", "binary", "varchar", "varbinary", "enum", "set":
+		// string
+		return StringStorageReq(col.DataType, col.Character)
+	case "tinyblob", "tinytext", "blob", "text", "mediumblob", "mediumtext",
+		"longblob", "longtext":
+		// strings length depend on it's values
+		// 这些字段为不定长字段,添加索引时必须指定前缀,索引前缀与字符集相关
+		return Config.MaxIdxBytesPerColumn + 1
+	default:
+		Log.Warning("Type %s not support:", col.DataType)
+		return -1
+	}
+}
+
+// Numeric Type Storage Requirements
+// return bytes count
+func numericStorageReq(dataType string) int {
+	typeLength := GetDataTypeLength(dataType)
+	baseType := strings.ToLower(GetDataTypeBase(dataType))
+
+	switch baseType {
+	case "tinyint":
+		return 1
+	case "smallint":
+		return 2
+	case "mediumint":
+		return 3
+	case "int", "integer":
+		return 4
+	case "bigint", "double", "real":
+		return 8
+	case "float":
+		if typeLength[0] == -1 || typeLength[0] >= 0 && typeLength[0] <= 24 {
+			// 4 bytes if 0 <= p <= 24
+			return 4
+		}
+		// 8 bytes if no p || 25 <= p <= 53
+		return 8
+	case "decimal", "numeric":
+		// Values for DECIMAL (and NUMERIC) columns are represented using a binary format
+		// that packs nine decimal (base 10) digits into four bytes. Storage for the integer
+		// and fractional parts of each value are determined separately. Each multiple of nine
+		// digits requires four bytes, and the “leftover” digits require some fraction of four bytes.
+
+		if typeLength[0] == -1 {
+			return 4
+		}
+
+		leftover := func(leftover int) int {
+			if leftover > 0 && leftover <= 2 {
+				return 1
+			} else if leftover > 2 && leftover <= 4 {
+				return 2
+			} else if leftover > 4 && leftover <= 6 {
+				return 3
+			} else if leftover > 6 && leftover <= 8 {
+				return 4
+			} else {
+				return 4
+			}
+		}
+
+		integer := typeLength[0]/9*4 + leftover(typeLength[0]%9)
+		fractional := typeLength[1]/9*4 + leftover(typeLength[1]%9)
+
+		return integer + fractional
+
+	case "bit":
+		// approximately (M+7)/8 bytes
+		if typeLength[0] == -1 {
+			return 1
+		}
+		return (typeLength[0] + 7) / 8
+
+	default:
+		Log.Error("No such numeric type: %s", baseType)
+		return 8
+	}
+}
+
+// Date and Time Type Storage Requirements
+// return bytes count
+func timeStorageReq(dataType string, version int) int {
+	/*
+			https://dev.mysql.com/doc/refman/8.0/en/storage-requirements.html
+			*   ============================================================================================
+			*   |	Data Type |	Storage Required Before MySQL 5.6.4	| Storage Required as of MySQL 5.6.4   |
+			*   | ---------------------------------------------------------------------------------------- |
+			*   |	YEAR	  |	1 byte	                            | 1 byte                               |
+			*   |	DATE	  | 3 bytes	                            | 3 bytes                              |
+			*   |	TIME	  | 3 bytes	                            | 3 bytes + fractional seconds storage |
+			*   |	DATETIME  | 8 bytes	                            | 5 bytes + fractional seconds storage |
+			*   |	TIMESTAMP |	4 bytes	                            | 4 bytes + fractional seconds storage |
+			*   ============================================================================================
+			*	|  Fractional Seconds Precision |Storage Required  |
+			*   | ------------------------------------------------ |
+			*	|  0	    					|0 bytes		   |
+			*	|  1, 2						    |1 byte            |
+			*	|  3, 4						    |2 bytes           |
+			*	|  5, 6						    |3 bytes           |
+		    *   ====================================================
+	*/
+
+	typeLength := GetDataTypeLength(dataType)
+
+	extr := func(length int) int {
+		if length > 0 && length <= 2 {
+			return 1
+		} else if length > 2 && length <= 4 {
+			return 2
+		} else if length > 4 && length <= 6 || length > 6 {
+			return 3
+		}
+		return 0
+	}
+
+	switch strings.ToLower(GetDataTypeBase(dataType)) {
+	case "year":
+		return 1
+	case "date":
+		return 3
+	case "time":
+		if version < 564 {
+			return 3
+		}
+		// 3 bytes + fractional seconds storage
+		return 3 + extr(typeLength[0])
+	case "datetime":
+		if version < 564 {
+			return 8
+		}
+		// 5 bytes + fractional seconds storage
+		return 5 + extr(typeLength[0])
+	case "timestamp":
+		if version < 564 {
+			return 4
+		}
+		// 4 bytes + fractional seconds storage
+		return 4 + extr(typeLength[0])
+	default:
+		return 8
+	}
+}
+
+// SHOW CHARACTER SET
+
+// CharSets character bytes per charcharacter bytes per char
+var CharSets = map[string]int{
+	"armscii8": 1,
+	"ascii":    1,
+	"big5":     2,
+	"binary":   1,
+	"cp1250":   1,
+	"cp1251":   1,
+	"cp1256":   1,
+	"cp1257":   1,
+	"cp850":    1,
+	"cp852":    1,
+	"cp866":    1,
+	"cp932":    2,
+	"dec8":     1,
+	"eucjpms":  3,
+	"euckr":    2,
+	"gb18030":  4,
+	"gb2312":   2,
+	"gbk":      2,
+	"geostd8":  1,
+	"greek":    1,
+	"hebrew":   1,
+	"hp8":      1,
+	"keybcs2":  1,
+	"koi8r":    1,
+	"koi8u":    1,
+	"latin1":   1,
+	"latin2":   1,
+	"latin5":   1,
+	"latin7":   1,
+	"macce":    1,
+	"macroman": 1,
+	"sjis":     2,
+	"swe7":     1,
+	"tis620":   1,
+	"ucs2":     2,
+	"ujis":     3,
+	"utf16":    4,
+	"utf16le":  4,
+	"utf32":    4,
+	"utf8":     3,
+	"utf8mb4":  4,
+}
+
+// StringStorageReq String Type Storage Requirements return bytes count
+func StringStorageReq(dataType string, charset string) int {
+	// get bytes per character, default 1
+	bysPerChar := 1
+	if _, ok := CharSets[strings.ToLower(charset)]; ok {
+		bysPerChar = CharSets[strings.ToLower(charset)]
+	}
+
+	// get length
+	typeLength := GetDataTypeLength(dataType)
+	if typeLength[0] == -1 {
+		return 0
+	}
+
+	// get type
+	baseType := strings.ToLower(GetDataTypeBase(dataType))
+
+	switch baseType {
+	case "char":
+		// Otherwise, M × w bytes, <= M <= 255,
+		// where w is the number of bytes required for the maximum-length character in the character set.
+		if typeLength[0] > 255 {
+			typeLength[0] = 255
+		}
+		return typeLength[0] * bysPerChar
+	case "binary":
+		// M bytes, 0 <= M <= 255
+		if typeLength[0] > 255 {
+			typeLength[0] = 255
+		}
+		return typeLength[0]
+	case "varchar", "varbinary":
+		if typeLength[0] < 255 {
+			return typeLength[0]*bysPerChar + 1
+		}
+		return typeLength[0]*bysPerChar + 2
+
+	case "enum":
+		// 1 or 2 bytes, depending on the number of enumeration values (65,535 values maximum)
+		return 2
+	case "set":
+		// 1, 2, 3, 4, or 8 bytes, depending on the number of set members (64 members maximum)
+		return 8
+	default:
+		return 0
+	}
+}
diff --git a/common/meta_test.go b/common/meta_test.go
new file mode 100644
index 00000000..e1114ed4
--- /dev/null
+++ b/common/meta_test.go
@@ -0,0 +1,140 @@
+/*
+ * Copyright 2018 Xiaomi, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package common
+
+import (
+	"testing"
+)
+
+func TestGetDataTypeLength(t *testing.T) {
+	typeList := map[string][]int{
+		"varchar(20)":  {20},
+		"int(2)":       {2},
+		"int(2000000)": {2000000},
+		"DECIMAL(1,2)": {1, 2},
+		"int":          {-1},
+	}
+
+	for typ, want := range typeList {
+		got := GetDataTypeLength(typ)
+		for i := 0; i < len(want); i++ {
+			if want[i] != got[i] {
+				t.Errorf("Not match, want %v, got %v", want, got)
+			}
+		}
+	}
+
+}
+
+func TestGetDataTypeBase(t *testing.T) {
+	typeList := map[string]string{
+		"varchar(20)":  "varchar",
+		"int(2)":       "int",
+		"int(2000000)": "int",
+	}
+
+	for typ := range typeList {
+		if got := GetDataTypeBase(typ); got != typeList[typ] {
+			t.Errorf("Not match, want %s, got %s", typeList[typ], got)
+		}
+	}
+
+}
+
+func TestGetDataBytes(t *testing.T) {
+	cols564 := map[*Column]int{
+		// numeric type
+		{Name: "col000", DataType: "tinyint", Character: "utf8"}:        1,
+		{Name: "col001", DataType: "SMALLINT", Character: "utf8"}:       2,
+		{Name: "col002", DataType: "MEDIUMINT", Character: "utf8"}:      3,
+		{Name: "col003", DataType: "int(32)", Character: "utf8"}:        4,
+		{Name: "col004", DataType: "integer(32)", Character: "utf8"}:    4,
+		{Name: "col005", DataType: "bigint(10)", Character: "utf8"}:     8,
+		{Name: "col006", DataType: "float(12)", Character: "utf8"}:      4,
+		{Name: "col007", DataType: "float(50)", Character: "utf8"}:      8,
+		{Name: "col008", DataType: "float(100)", Character: "utf8"}:     8,
+		{Name: "col009", DataType: "float", Character: "utf8"}:          4,
+		{Name: "col010", DataType: "double", Character: "utf8"}:         8,
+		{Name: "col011", DataType: "real", Character: "utf8"}:           8,
+		{Name: "col012", DataType: "BIT(32)", Character: "utf8"}:        4,
+		{Name: "col013", DataType: "numeric(32,32)", Character: "utf8"}: 30,
+		{Name: "col013", DataType: "decimal(2,32)", Character: "utf8"}:  16,
+		{Name: "col014", DataType: "BIT(32)", Character: "utf8"}:        4,
+
+		// date & time
+		{Name: "col015", DataType: "year(32)", Character: "utf8mb4"}:      1,
+		{Name: "col016", DataType: "date", Character: "utf8mb4"}:          3,
+		{Name: "col017", DataType: "time", Character: "utf8mb4"}:          3,
+		{Name: "col018", DataType: "time(0)", Character: "utf8mb4"}:       3,
+		{Name: "col019", DataType: "time(2)", Character: "utf8mb4"}:       4,
+		{Name: "col020", DataType: "time(4)", Character: "utf8mb4"}:       5,
+		{Name: "col021", DataType: "time(6)", Character: "utf8mb4"}:       6,
+		{Name: "col022", DataType: "datetime", Character: "utf8mb4"}:      5,
+		{Name: "col023", DataType: "timestamp(32)", Character: "utf8mb4"}: 7,
+
+		// string
+		{Name: "col024", DataType: "varchar(255)", Character: "utf8"}:    767,
+		{Name: "col025", DataType: "varchar(191)", Character: "utf8mb4"}: 765,
+	}
+
+	for col, bytes := range cols564 {
+		if got := col.GetDataBytes(564); got != bytes {
+			t.Errorf("Version 564, %s Not match, want %d, got %d", col.Name, bytes, got)
+		}
+	}
+
+	cols550 := map[*Column]int{
+		// numeric type
+		{Name: "col000", DataType: "tinyint", Character: "utf8"}:        1,
+		{Name: "col001", DataType: "SMALLINT", Character: "utf8"}:       2,
+		{Name: "col002", DataType: "MEDIUMINT", Character: "utf8"}:      3,
+		{Name: "col003", DataType: "int(32)", Character: "utf8"}:        4,
+		{Name: "col004", DataType: "integer(32)", Character: "utf8"}:    4,
+		{Name: "col005", DataType: "bigint(10)", Character: "utf8"}:     8,
+		{Name: "col006", DataType: "float(12)", Character: "utf8"}:      4,
+		{Name: "col007", DataType: "float(50)", Character: "utf8"}:      8,
+		{Name: "col008", DataType: "float(100)", Character: "utf8"}:     8,
+		{Name: "col009", DataType: "float", Character: "utf8"}:          4,
+		{Name: "col010", DataType: "double", Character: "utf8"}:         8,
+		{Name: "col011", DataType: "real", Character: "utf8"}:           8,
+		{Name: "col012", DataType: "BIT(32)", Character: "utf8"}:        4,
+		{Name: "col013", DataType: "numeric(32,32)", Character: "utf8"}: 30,
+		{Name: "col013", DataType: "decimal(2,32)", Character: "utf8"}:  16,
+		{Name: "col014", DataType: "BIT(32)", Character: "utf8"}:        4,
+
+		// date & time
+		{Name: "col015", DataType: "year(32)", Character: "utf8mb4"}:      1,
+		{Name: "col016", DataType: "date", Character: "utf8mb4"}:          3,
+		{Name: "col017", DataType: "time", Character: "utf8mb4"}:          3,
+		{Name: "col018", DataType: "time(0)", Character: "utf8mb4"}:       3,
+		{Name: "col019", DataType: "time(2)", Character: "utf8mb4"}:       3,
+		{Name: "col020", DataType: "time(4)", Character: "utf8mb4"}:       3,
+		{Name: "col021", DataType: "time(6)", Character: "utf8mb4"}:       3,
+		{Name: "col022", DataType: "datetime", Character: "utf8mb4"}:      8,
+		{Name: "col023", DataType: "timestamp(32)", Character: "utf8mb4"}: 4,
+
+		// string
+		{Name: "col024", DataType: "varchar(255)", Character: "utf8"}:    767,
+		{Name: "col025", DataType: "varchar(191)", Character: "utf8mb4"}: 765,
+	}
+
+	for col, bytes := range cols550 {
+		if got := col.GetDataBytes(550); got != bytes {
+			t.Errorf("Version: 550, %s Not match, want %d, got %d", col.Name, bytes, got)
+		}
+	}
+}
diff --git a/common/testdata/TestListReportTypes.golden b/common/testdata/TestListReportTypes.golden
new file mode 100644
index 00000000..86c81bb6
--- /dev/null
+++ b/common/testdata/TestListReportTypes.golden
@@ -0,0 +1,133 @@
+# 支持的报告类型
+
+[toc]
+
+## lint
+* **Description**:参考sqlint格式,以插件形式集成到代码编辑器,显示输出更加友好
+
+* **Example**:
+
+```bash
+soar -report-type lint -query test.sql
+```
+## markdown
+* **Description**:该格式为默认输出格式,以markdown格式展现,可以用网页浏览器插件直接打开,也可以用markdown编辑器打开
+
+* **Example**:
+
+```bash
+echo "select * from film" | soar
+```
+## rewrite
+* **Description**:SQL重写功能,配合-rewrite-rules参数一起使用,可以通过-list-rewrite-rules查看所有支持的SQL重写规则
+
+* **Example**:
+
+```bash
+echo "select * from film" | soar -rewrite-rules star2columns,delimiter -report-type rewrite
+```
+## ast
+* **Description**:输出SQL的抽象语法树,主要用于测试
+
+* **Example**:
+
+```bash
+echo "select * from film" | soar -report-type ast
+```
+## tiast
+* **Description**:输出SQL的TiDB抽象语法树,主要用于测试
+
+* **Example**:
+
+```bash
+echo "select * from film" | soar -report-type tiast
+```
+## fingerprint
+* **Description**:输出SQL的指纹
+
+* **Example**:
+
+```bash
+echo "select * from film where language_id=1" | soar -report-type fingerprint
+```
+## md2html
+* **Description**:markdown格式转html格式小工具
+
+* **Example**:
+
+```bash
+soar -list-heuristic-rules | soar -report-type md2html > heuristic_rules.html
+```
+## explain-digest
+* **Description**:输入为EXPLAIN的表格,JSON或Vertical格式,对其进行分析,给出分析结果
+
+* **Example**:
+
+```bash
+soar -report-type explain-digest << EOF
++----+-------------+-------+------+---------------+------+---------+------+------+-------+
+| id | select_type | table | type | possible_keys | key  | key_len | ref  | rows | Extra |
++----+-------------+-------+------+---------------+------+---------+------+------+-------+
+|  1 | SIMPLE      | film  | ALL  | NULL          | NULL | NULL    | NULL | 1131 |       |
++----+-------------+-------+------+---------------+------+---------+------+------+-------+
+EOF
+```
+## duplicate-key-checker
+* **Description**:对OnlineDsn中指定的DB进行索引重复检查
+
+* **Example**:
+
+```bash
+soar -report-type duplicate-key-checker -online-dsn user:passwd@127.0.0.1:3306/db
+```
+## html
+* **Description**:以HTML格式输出报表
+
+* **Example**:
+
+```bash
+echo "select * from film" | soar -report-type html
+```
+## json
+* **Description**:输出JSON格式报表,方便应用程序处理
+
+* **Example**:
+
+```bash
+echo "select * from film" | soar -report-type json
+```
+## tokenize
+* **Description**:对SQL进行切词,主要用于测试
+
+* **Example**:
+
+```bash
+echo "select * from film" | soar -report-type tokenize
+```
+## compress
+* **Description**:SQL压缩小工具,使用内置SQL压缩逻辑,测试中的功能
+
+* **Example**:
+
+```bash
+echo "select
+*
+from
+  film" | soar -report-type compress
+```
+## pretty
+* **Description**:使用kr/pretty打印报告,主要用于测试
+
+* **Example**:
+
+```bash
+echo "select * from film" | soar -report-type pretty
+```
+## remove-comment
+* **Description**:去除SQL语句中的注释,支持单行多行注释的去除
+
+* **Example**:
+
+```bash
+echo "select/*comment*/ * from film" | soar -report-type remove-comment
+```
diff --git a/common/testdata/TestMarkdown2Html.golden b/common/testdata/TestMarkdown2Html.golden
new file mode 100644
index 00000000..8e3b056b
--- /dev/null
+++ b/common/testdata/TestMarkdown2Html.golden
@@ -0,0 +1,374 @@
+

Markdown For Typora

+ +

Overview

+ +

Markdown is created by Daring Fireball, the original guideline is here. Its syntax, however, varies between different parsers or editors. Typora is using GitHub Flavored Markdown.

+ +

Please note that HTML fragments in markdown source will be recognized but not parsed or rendered. Also, there may be small reformatting on the original markdown source code after saving.

+ +

Outline

+ +

[TOC]

+ +

Block Elements

+ +

Paragraph and line breaks

+ +

A paragraph is simply one or more consecutive lines of text. In markdown source code, paragraphs are separated by more than one blank lines. In Typora, you only need to press Return to create a new paragraph.

+ +

Press Shift + Return to create a single line break. However, most markdown parser will ignore single line break, to make other markdown parsers recognize your line break, you can leave two whitespace at the end of the line, or insert <br/>.

+ +

Headers

+ +

Headers use 1-6 hash characters at the start of the line, corresponding to header levels 1-6. For example:

+ +
# This is an H1
+
+## This is an H2
+
+###### This is an H6
+
+ +

In typora, input ‘#’s followed by title content, and press Return key will create a header.

+ +

Blockquotes

+ +

Markdown uses email-style > characters for block quoting. They are presented as:

+ +
> This is a blockquote with two paragraphs. This is first paragraph.
+>
+> This is second pragraph.Vestibulum enim wisi, viverra nec, fringilla in, laoreet vitae, risus.
+
+
+
+> This is another blockquote with one paragraph. There is three empty line to seperate two blockquote.
+
+ +

In typora, just input ‘>’ followed by quote contents a block quote is generated. Typora will insert proper ‘>’ or line break for you. Block quote inside anther block quote is allowed by adding additional levels of ‘>’.

+ +

Lists

+ +

Input * list item 1 will create an un-ordered list, the * symbol can be replace with + or -.

+ +

Input 1. list item 1 will create an ordered list, their markdown source code is like:

+ +
## un-ordered list
+*   Red
+*   Green
+*   Blue
+
+## ordered list
+1.  Red
+2. 	Green
+3.	Blue
+
+ +

Task List

+ +

Task lists are lists with items marked as either [ ] or x. For example:

+ +
- [ ] a task list item
+- [ ] list syntax required
+- [ ] normal **formatting**, @mentions, #1234 refs
+- [ ] incomplete
+- [x] completed
+
+ +

You can change the complete/incomplete state by click the checkbox before the item.

+ +

(Fenced) Code Blocks

+ +

Typora only support fences in Github Flavored Markdown. Original code blocks in markdown is not supported.

+ +

Using fences is easy: Input ``` and press return. Add an optional language identifier after ``` and we'll run it through syntax highlighting:

+ +
Here's an example:
+
+​```
+function test() {
+  console.log("notice the blank line before this function?");
+}
+​```
+
+syntax highlighting:
+​```ruby
+require 'redcarpet'
+markdown = Redcarpet.new("Hello World!")
+puts markdown.to_html
+​```
+
+ +

Math Blocks

+ +

You can render LaTeX mathematical expressions using MathJax.

+ +

Input $$, then press 'Return' key will trigger an input field which accept Tex/LaTex source. Following is an example: +$$ +\mathbf{V}1 \times \mathbf{V}2 = \begin{vmatrix} +\mathbf{i} & \mathbf{j} & \mathbf{k} \ +\frac{\partial X}{\partial u} & \frac{\partial Y}{\partial u} & 0 \ +\frac{\partial X}{\partial v} & \frac{\partial Y}{\partial v} & 0 \ +\end{vmatrix} +$$

+ +

In markdown source file, math block is LaTeX expression wrapped by ‘$$’ mark:

+ +
$$
+\mathbf{V}_1 \times \mathbf{V}_2 =  \begin{vmatrix} 
+\mathbf{i} & \mathbf{j} & \mathbf{k} \\
+\frac{\partial X}{\partial u} &  \frac{\partial Y}{\partial u} & 0 \\
+\frac{\partial X}{\partial v} &  \frac{\partial Y}{\partial v} & 0 \\
+\end{vmatrix}
+$$
+
+ +

Tables

+ +

Input | First Header | Second Header | and press return key will create a table with two column.

+ +

After table is created, focus on that table will pop up a toolbar for table, where you can resize, align, or delete table. You can also use context menu to copy and add/delete column/row.

+ +

Following descriptions can be skipped, as markdown source code for tables are generated by typora automatically.

+ +

In markdown source code, they look like:

+ +
| First Header  | Second Header |
+| ------------- | ------------- |
+| Content Cell  | Content Cell  |
+| Content Cell  | Content Cell  |
+
+ +

You can also include inline Markdown such as links, bold, italics, or strikethrough.

+ +

Finally, by including colons : within the header row, you can define text to be left-aligned, right-aligned, or center-aligned:

+ +
| Left-Aligned  | Center Aligned  | Right Aligned |
+| :------------ |:---------------:| -----:|
+| col 3 is      | some wordy text | $1600 |
+| col 2 is      | centered        |   $12 |
+| zebra stripes | are neat        |    $1 |
+
+ +

A colon on the left-most side indicates a left-aligned column; a colon on the right-most side indicates a right-aligned column; a colon on both sides indicates a center-aligned column.

+ +

Footnotes

+ +
You can create footnotes like this[^footnote].
+
+[^footnote]: Here is the *text* of the **footnote**.
+
+ +

will produce:

+ +

You can create footnotes like this[^footnote].

+ +

[^footnote]: Here is the text of the footnote.

+ +

Mouse on the ‘footnote’ superscript to see content of the footnote.

+ +

Horizontal Rules

+ +

Input *** or --- on a blank line and press return will draw a horizontal line.

+ +
+ +

YAML Front Matter

+ +

Typora support YAML Front Matter now. Input --- at the top of the article and then press Enter will introduce one. Or insert one metadata block from the menu.

+ +

Table of Contents (TOC)

+ +

Input [toc] then press Return key will create a section for “Table of Contents” extracting all headers from one’s writing, its contents will be updated automatically.

+ +

Diagrams (Sequence, Flowchart and Mermaid)

+ +

Typora supports, sequence, flowchart and mermaid, after this feature is enabled from preference panel.

+ +

See this document for detail.

+ +

Span Elements

+ +

Span elements will be parsed and rendered right after your typing. Moving cursor in middle of those span elements will expand those elements into markdown source. Following will explain the syntax of those span element.

+ +

Links

+ +

Markdown supports two style of links: inline and reference.

+ +

In both styles, the link text is delimited by [square brackets].

+ +

To create an inline link, use a set of regular parentheses immediately after the link text’s closing square bracket. Inside the parentheses, put the URL where you want the link to point, along with an optional title for the link, surrounded in quotes. For example:

+ +
This is [an example](http://example.com/ "Title") inline link.
+
+[This link](http://example.net/) has no title attribute.
+
+ +

will produce:

+ +

This is an example inline link. (<p>This is <a href="http://example.com/" title="Title">)

+ +

This link has no title attribute. (<p><a href="http://example.net/">This link</a> has no)

+ +

Internal Links

+ +

You can set the href to headers, which will create a bookmark that allow you to jump to that section after clicking. For example:

+ +

Command(on Windows: Ctrl) + Click This link will jump to header Block Elements. To see how to write that, please move cursor or click that link with key pressed to expand the element into markdown source.

+ +

Reference Links

+ +

Reference-style links use a second set of square brackets, inside which you place a label of your choosing to identify the link:

+ +
This is [an example][id] reference-style link.
+
+Then, anywhere in the document, you define your link label like this, on a line by itself:
+
+[id]: http://example.com/  "Optional Title Here"
+
+ +

In typora, they will be rendered like:

+ +

This is an example reference-style link.

+ +

The implicit link name shortcut allows you to omit the name of the link, in which case the link text itself is used as the name. Just use an empty set of square brackets — e.g., to link the word “Google” to the google.com web site, you could simply write:

+ +
[Google][]
+And then define the link:
+
+[Google]: http://google.com/
+
+ +

In typora click link will expand it for editing, command+click will open the hyperlink in web browser.

+ +

URLs

+ +

Typora allows you to insert urls as links, wrapped by <brackets>.

+ +

<i@typora.io> becomes i@typora.io.

+ +

Typora will aslo auto link standard URLs. e.g: www.google.com.

+ +

Images

+ +

Image looks similar with links, but it requires an additional ! char before the start of link. Image syntax looks like this:

+ +
![Alt text](/path/to/img.jpg)
+
+![Alt text](/path/to/img.jpg "Optional title")
+
+ +

You are able to use drag & drop to insert image from image file or we browser. And modify the markdown source code by clicking on the image. Relative path will be used if image is in same directory or sub-directory with current editing document when drag & drop.

+ +

For more tips on images, please read http://support.typora.io//Images/

+ +

Emphasis

+ +

Markdown treats asterisks (*) and underscores (_) as indicators of emphasis. Text wrapped with one * or _ will be wrapped with an HTML <em> tag. E.g:

+ +
*single asterisks*
+
+_single underscores_
+
+ +

output:

+ +

single asterisks

+ +

single underscores

+ +

GFM will ignores underscores in words, which is commonly used in code and names, like this:

+ +
+

wowgreatstuff

+ +

dothisanddothatandanother_thing.

+
+ +

To produce a literal asterisk or underscore at a position where it would otherwise be used as an emphasis delimiter, you can backslash escape it:

+ +
\*this text is surrounded by literal asterisks\*
+
+ +

Typora recommends to use * symbol.

+ +

Strong

+ +

double *’s or _’s will be wrapped with an HTML <strong> tag, e.g:

+ +
**double asterisks**
+
+__double underscores__
+
+ +

output:

+ +

double asterisks

+ +

double underscores

+ +

Typora recommends to use ** symbol.

+ +

Code

+ +

To indicate a span of code, wrap it with backtick quotes (`). Unlike a pre-formatted code block, a code span indicates code within a normal paragraph. For example:

+ +
Use the `printf()` function.
+
+ +

will produce:

+ +

Use the printf() function.

+ +

Strikethrough

+ +

GFM adds syntax to create strikethrough text, which is missing from standard Markdown.

+ +

~~Mistaken text.~~ becomes Mistaken text.

+ +

Underline

+ +

Underline is powered by raw HTML.

+ +

<u>Underline</u> becomes Underline.

+ +

Emoji :happy:

+ +

Input emoji with syntax :smile:.

+ +

User can trigger auto-complete suggestions for emoji by pressing ESC key, or trigger it automatically after enable it on preference panel. Also, input UTF8 emoji char directly from Edit -> Emoji & Symbols from menu bar is also supported.

+ +

HTML

+ +

Typora cannot render html fragments. But typora can parse and render very limited HTML fragments, as an extension of Markdown, including:

+ + + +

Most of their attributes, styles, or classes will be ignored. For other tags, typora will render them as raw HTML snippets.

+ +

But those HTML will be exported on print or export.

+ +

Inline Math

+ +

To use this feature, first, please enable it in Preference Panel -> Markdown Tab. Then use $ to wrap TeX command, for example: $\lim_{x \to \infty} \exp(-x) = 0$ will be rendered as LaTeX command.

+ +

To trigger inline preview for inline math: input “$”, then press ESC key, then input TeX command, a preview tooltip will be visible like below:

+ +

+ +

Subscript

+ +

To use this feature, first, please enable it in Preference Panel -> Markdown Tab. Then use ~ to wrap subscript content, for example: H~2~O, X~long\ text~/

+ +

Superscript

+ +

To use this feature, first, please enable it in Preference Panel -> Markdown Tab. Then use ^ to wrap superscript content, for example: X^2^.

+ +

Highlight

+ +

To use this feature, first, please enable it in Preference Panel -> Markdown Tab. Then use == to wrap superscript content, for example: ==highlight==.

+ diff --git a/common/testdata/TestMarkdown2Html.md b/common/testdata/TestMarkdown2Html.md new file mode 100644 index 00000000..654b7f3f --- /dev/null +++ b/common/testdata/TestMarkdown2Html.md @@ -0,0 +1,391 @@ +# Markdown For Typora + +## Overview + +**Markdown** is created by [Daring Fireball](http://daringfireball.net/), the original guideline is [here](http://daringfireball.net/projects/markdown/syntax). Its syntax, however, varies between different parsers or editors. **Typora** is using [GitHub Flavored Markdown][GFM]. + +Please note that HTML fragments in markdown source will be recognized but not parsed or rendered. Also, there may be small reformatting on the original markdown source code after saving. + +*Outline* + +[TOC] + +## Block Elements + +### Paragraph and line breaks + +A paragraph is simply one or more consecutive lines of text. In markdown source code, paragraphs are separated by more than one blank lines. In Typora, you only need to press `Return` to create a new paragraph. + +Press `Shift` + `Return` to create a single line break. However, most markdown parser will ignore single line break, to make other markdown parsers recognize your line break, you can leave two whitespace at the end of the line, or insert `
`. + +### Headers + +Headers use 1-6 hash characters at the start of the line, corresponding to header levels 1-6. For example: + +``` markdown +# This is an H1 + +## This is an H2 + +###### This is an H6 +``` + +In typora, input ‘#’s followed by title content, and press `Return` key will create a header. + +### Blockquotes + +Markdown uses email-style > characters for block quoting. They are presented as: + +``` markdown +> This is a blockquote with two paragraphs. This is first paragraph. +> +> This is second pragraph.Vestibulum enim wisi, viverra nec, fringilla in, laoreet vitae, risus. + + + +> This is another blockquote with one paragraph. There is three empty line to seperate two blockquote. +``` + +In typora, just input ‘>’ followed by quote contents a block quote is generated. Typora will insert proper ‘>’ or line break for you. Block quote inside anther block quote is allowed by adding additional levels of ‘>’. + +### Lists + +Input `* list item 1` will create an un-ordered list, the `*` symbol can be replace with `+` or `-`. + +Input `1. list item 1` will create an ordered list, their markdown source code is like: + +``` markdown +## un-ordered list +* Red +* Green +* Blue + +## ordered list +1. Red +2. Green +3. Blue +``` + +### Task List + +Task lists are lists with items marked as either [ ] or [x] (incomplete or complete). For example: + +``` markdown +- [ ] a task list item +- [ ] list syntax required +- [ ] normal **formatting**, @mentions, #1234 refs +- [ ] incomplete +- [x] completed +``` + +You can change the complete/incomplete state by click the checkbox before the item. + +### (Fenced) Code Blocks + +Typora only support fences in Github Flavored Markdown. Original code blocks in markdown is not supported. + +Using fences is easy: Input \`\`\` and press `return`. Add an optional language identifier after \`\`\` and we'll run it through syntax highlighting: + +``` gfm +Here's an example: + +​``` +function test() { + console.log("notice the blank line before this function?"); +} +​``` + +syntax highlighting: +​```ruby +require 'redcarpet' +markdown = Redcarpet.new("Hello World!") +puts markdown.to_html +​``` +``` + +### Math Blocks + +You can render *LaTeX* mathematical expressions using **MathJax**. + +Input `$$`, then press 'Return' key will trigger an input field which accept *Tex/LaTex* source. Following is an example: +$$ +\mathbf{V}_1 \times \mathbf{V}_2 = \begin{vmatrix} +\mathbf{i} & \mathbf{j} & \mathbf{k} \\ +\frac{\partial X}{\partial u} & \frac{\partial Y}{\partial u} & 0 \\ +\frac{\partial X}{\partial v} & \frac{\partial Y}{\partial v} & 0 \\ +\end{vmatrix} +$$ + + +In markdown source file, math block is *LaTeX* expression wrapped by ‘$$’ mark: + +``` markdown +$$ +\mathbf{V}_1 \times \mathbf{V}_2 = \begin{vmatrix} +\mathbf{i} & \mathbf{j} & \mathbf{k} \\ +\frac{\partial X}{\partial u} & \frac{\partial Y}{\partial u} & 0 \\ +\frac{\partial X}{\partial v} & \frac{\partial Y}{\partial v} & 0 \\ +\end{vmatrix} +$$ +``` + +### Tables + +Input `| First Header | Second Header |` and press `return` key will create a table with two column. + +After table is created, focus on that table will pop up a toolbar for table, where you can resize, align, or delete table. You can also use context menu to copy and add/delete column/row. + +Following descriptions can be skipped, as markdown source code for tables are generated by typora automatically. + +In markdown source code, they look like: + +``` markdown +| First Header | Second Header | +| ------------- | ------------- | +| Content Cell | Content Cell | +| Content Cell | Content Cell | +``` + +You can also include inline Markdown such as links, bold, italics, or strikethrough. + +Finally, by including colons : within the header row, you can define text to be left-aligned, right-aligned, or center-aligned: + +``` markdown +| Left-Aligned | Center Aligned | Right Aligned | +| :------------ |:---------------:| -----:| +| col 3 is | some wordy text | $1600 | +| col 2 is | centered | $12 | +| zebra stripes | are neat | $1 | +``` + +A colon on the left-most side indicates a left-aligned column; a colon on the right-most side indicates a right-aligned column; a colon on both sides indicates a center-aligned column. + +### Footnotes + +``` markdown +You can create footnotes like this[^footnote]. + +[^footnote]: Here is the *text* of the **footnote**. +``` + +will produce: + +You can create footnotes like this[^footnote]. + +[^footnote]: Here is the *text* of the **footnote**. + +Mouse on the ‘footnote’ superscript to see content of the footnote. + +### Horizontal Rules + +Input `***` or `---` on a blank line and press `return` will draw a horizontal line. + +------ + +### YAML Front Matter + +Typora support [YAML Front Matter](http://jekyllrb.com/docs/frontmatter/) now. Input `---` at the top of the article and then press `Enter` will introduce one. Or insert one metadata block from the menu. + +### Table of Contents (TOC) + +Input `[toc]` then press `Return` key will create a section for “Table of Contents” extracting all headers from one’s writing, its contents will be updated automatically. + +### Diagrams (Sequence, Flowchart and Mermaid) + +Typora supports, [sequence](https://bramp.github.io/js-sequence-diagrams/), [flowchart](http://flowchart.js.org/) and [mermaid](https://knsv.github.io/mermaid/#mermaid), after this feature is enabled from preference panel. + +See this [document](http://support.typora.io/Draw-Diagrams-With-Markdown/) for detail. + +## Span Elements + +Span elements will be parsed and rendered right after your typing. Moving cursor in middle of those span elements will expand those elements into markdown source. Following will explain the syntax of those span element. + +### Links + +Markdown supports two style of links: inline and reference. + +In both styles, the link text is delimited by [square brackets]. + +To create an inline link, use a set of regular parentheses immediately after the link text’s closing square bracket. Inside the parentheses, put the URL where you want the link to point, along with an optional title for the link, surrounded in quotes. For example: + +``` markdown +This is [an example](http://example.com/ "Title") inline link. + +[This link](http://example.net/) has no title attribute. +``` + +will produce: + +This is [an example](http://example.com/"Title") inline link. (`

This is `) + +[This link](http://example.net/) has no title attribute. (`

This link has no`) + +#### Internal Links + +**You can set the href to headers**, which will create a bookmark that allow you to jump to that section after clicking. For example: + +Command(on Windows: Ctrl) + Click [This link](#block-elements) will jump to header `Block Elements`. To see how to write that, please move cursor or click that link with `⌘` key pressed to expand the element into markdown source. + +#### Reference Links + +Reference-style links use a second set of square brackets, inside which you place a label of your choosing to identify the link: + +``` markdown +This is [an example][id] reference-style link. + +Then, anywhere in the document, you define your link label like this, on a line by itself: + +[id]: http://example.com/ "Optional Title Here" +``` + +In typora, they will be rendered like: + +This is [an example][id] reference-style link. + +[id]: http://example.com/ "Optional Title Here" + +The implicit link name shortcut allows you to omit the name of the link, in which case the link text itself is used as the name. Just use an empty set of square brackets — e.g., to link the word “Google” to the google.com web site, you could simply write: + +``` markdown +[Google][] +And then define the link: + +[Google]: http://google.com/ +``` + +In typora click link will expand it for editing, command+click will open the hyperlink in web browser. + +### URLs + +Typora allows you to insert urls as links, wrapped by `<`brackets`>`. + +`` becomes . + +Typora will aslo auto link standard URLs. e.g: www.google.com. + +### Images + +Image looks similar with links, but it requires an additional `!` char before the start of link. Image syntax looks like this: + +``` markdown +![Alt text](/path/to/img.jpg) + +![Alt text](/path/to/img.jpg "Optional title") +``` + +You are able to use drag & drop to insert image from image file or we browser. And modify the markdown source code by clicking on the image. Relative path will be used if image is in same directory or sub-directory with current editing document when drag & drop. + +For more tips on images, please read + +### Emphasis + +Markdown treats asterisks (`*`) and underscores (`_`) as indicators of emphasis. Text wrapped with one `*` or `_` will be wrapped with an HTML `` tag. E.g: + +``` markdown +*single asterisks* + +_single underscores_ +``` + +output: + +*single asterisks* + +_single underscores_ + +GFM will ignores underscores in words, which is commonly used in code and names, like this: + +> wow_great_stuff +> +> do_this_and_do_that_and_another_thing. + +To produce a literal asterisk or underscore at a position where it would otherwise be used as an emphasis delimiter, you can backslash escape it: + +``` markdown +\*this text is surrounded by literal asterisks\* +``` + +Typora recommends to use `*` symbol. + +### Strong + +double *’s or _’s will be wrapped with an HTML `` tag, e.g: + +``` markdown +**double asterisks** + +__double underscores__ +``` + +output: + +**double asterisks** + +__double underscores__ + +Typora recommends to use `**` symbol. + +### Code + +To indicate a span of code, wrap it with backtick quotes (`). Unlike a pre-formatted code block, a code span indicates code within a normal paragraph. For example: + +``` markdown +Use the `printf()` function. +``` + +will produce: + +Use the `printf()` function. + +### Strikethrough + +GFM adds syntax to create strikethrough text, which is missing from standard Markdown. + +`~~Mistaken text.~~` becomes ~~Mistaken text.~~ + +### Underline + +Underline is powered by raw HTML. + +`Underline` becomes Underline. + +### Emoji :happy: + +Input emoji with syntax `:smile:`. + +User can trigger auto-complete suggestions for emoji by pressing `ESC` key, or trigger it automatically after enable it on preference panel. Also, input UTF8 emoji char directly from `Edit` -> `Emoji & Symbols` from menu bar is also supported. + +### HTML + +Typora cannot render html fragments. But typora can parse and render very limited HTML fragments, as an extension of Markdown, including: + +- Underline: `underline` +- Image: `` (And `width`, `height` attribute in HTML tag, and `width`, `height`, `zoom` style in `style` attribute will be applied.) +- Comments: `` +- Hyperlink: `link`. + +Most of their attributes, styles, or classes will be ignored. For other tags, typora will render them as raw HTML snippets. + +But those HTML will be exported on print or export. + +### Inline Math + +To use this feature, first, please enable it in `Preference` Panel -> `Markdown` Tab. Then use `$` to wrap TeX command, for example: `$\lim_{x \to \infty} \exp(-x) = 0$` will be rendered as LaTeX command. + +To trigger inline preview for inline math: input “$”, then press `ESC` key, then input TeX command, a preview tooltip will be visible like below: + + + +### Subscript + +To use this feature, first, please enable it in `Preference` Panel -> `Markdown` Tab. Then use `~` to wrap subscript content, for example: `H~2~O`, `X~long\ text~`/ + +### Superscript + +To use this feature, first, please enable it in `Preference` Panel -> `Markdown` Tab. Then use `^` to wrap superscript content, for example: `X^2^`. + +### Highlight + +To use this feature, first, please enable it in `Preference` Panel -> `Markdown` Tab. Then use `==` to wrap superscript content, for example: `==highlight==`. + +[GFM]: https://help.github.com/articles/github-flavored-markdown/ diff --git a/common/testdata/TestMarkdownHTMLHeader.golden b/common/testdata/TestMarkdownHTMLHeader.golden new file mode 100644 index 00000000..e69de29b diff --git a/common/testdata/TestParseDSN.golden b/common/testdata/TestParseDSN.golden new file mode 100644 index 00000000..af08f5e1 --- /dev/null +++ b/common/testdata/TestParseDSN.golden @@ -0,0 +1,15 @@ +&common.dsn{Addr:"", Schema:"", User:"", Password:"", Charset:"", Disable:true, Version:0} +&common.dsn{Addr:"hostname:3307", Schema:"database", User:"user", Password:"password", Charset:"utf8mb4", Disable:false, Version:999} +&common.dsn{Addr:"hostname:3307", Schema:"information_schema", User:"user", Password:"password", Charset:"utf8mb4", Disable:false, Version:999} +&common.dsn{Addr:"hostname:3306", Schema:"database", User:"user", Password:"password", Charset:"utf8mb4", Disable:false, Version:999} +&common.dsn{Addr:"127.0.0.1:3307", Schema:"database", User:"user", Password:"password", Charset:"utf8mb4", Disable:false, Version:999} +&common.dsn{Addr:"127.0.0.1:3306", Schema:"information_schema", User:"user", Password:"password", Charset:"utf8mb4", Disable:false, Version:999} +&common.dsn{Addr:"hostname:3307", Schema:"database", User:"", Password:"", Charset:"utf8mb4", Disable:false, Version:999} +&common.dsn{Addr:"hostname:3307", Schema:"database", User:"", Password:"", Charset:"utf8mb4", Disable:false, Version:999} +&common.dsn{Addr:"hostname:3306", Schema:"information_schema", User:"", Password:"", Charset:"utf8mb4", Disable:false, Version:999} +&common.dsn{Addr:"hostname:3306", Schema:"information_schema", User:"", Password:"", Charset:"utf8mb4", Disable:false, Version:999} +&common.dsn{Addr:"127.0.0.1:3306", Schema:"database", User:"", Password:"", Charset:"utf8mb4", Disable:false, Version:999} +&common.dsn{Addr:"hostname:3307", Schema:"information_schema", User:"", Password:"", Charset:"utf8mb4", Disable:false, Version:999} +&common.dsn{Addr:"127.0.0.1:3307", Schema:"database", User:"", Password:"", Charset:"utf8mb4", Disable:false, Version:999} +&common.dsn{Addr:"127.0.0.1:3307", Schema:"database", User:"", Password:"", Charset:"utf8mb4", Disable:false, Version:999} +&common.dsn{Addr:"127.0.0.1:3306", Schema:"database", User:"", Password:"", Charset:"utf8mb4", Disable:false, Version:999} diff --git a/common/tricks.go b/common/tricks.go new file mode 100644 index 00000000..2a462482 --- /dev/null +++ b/common/tricks.go @@ -0,0 +1,109 @@ +/* + * Copyright 2018 Xiaomi, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package common + +import ( + "bufio" + "bytes" + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + "reflect" + "sort" +) + +// GoldenDiff 从gofmt学来的测试方法 +// https://medium.com/soon-london/testing-with-golden-files-in-go-7fccc71c43d3 +func GoldenDiff(f func(), name string, update *bool) error { + var b bytes.Buffer + w := bufio.NewWriter(&b) + str := captureOutput(f) + _, err := w.WriteString(str) + if err != nil { + Log.Warning(err.Error()) + } + err = w.Flush() + if err != nil { + Log.Warning(err.Error()) + } + + gp := filepath.Join("testdata", name+".golden") + if *update { + if err = ioutil.WriteFile(gp, b.Bytes(), 0644); err != nil { + err = fmt.Errorf("%s failed to update golden file: %s", name, err) + return err + } + } + g, err := ioutil.ReadFile(gp) + if err != nil { + err = fmt.Errorf("%s failed reading .golden: %s", name, err) + } + if !bytes.Equal(b.Bytes(), g) { + err = fmt.Errorf("%s does not match .golden file", name) + } + return err +} + +// captureOutput 获取函数标准输出 +func captureOutput(f func()) string { + // keep backup of the real stdout + oldStdout := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + + // execute function + f() + + outC := make(chan string) + // copy the output in a separate goroutine so printing can't block indefinitely + go func() { + var buf bytes.Buffer + _, err := io.Copy(&buf, r) + if err != nil { + Log.Warning(err.Error()) + } + outC <- buf.String() + }() + + // back to normal state + err := w.Close() + if err != nil { + Log.Warning(err.Error()) + } + os.Stdout = oldStdout // restoring the real stdout + out := <-outC + os.Stdout = oldStdout + return out +} + +// SortedKey sort map[string]interface{}, use in range clause +func SortedKey(m interface{}) []string { + var keys []string + switch reflect.TypeOf(m).Kind() { + case reflect.Map: + switch reflect.TypeOf(m).Key().Kind() { + case reflect.String: + for _, k := range reflect.ValueOf(m).MapKeys() { + keys = append(keys, k.String()) + } + } + } + sort.Strings(keys) + return keys +} diff --git a/database/doc.go b/database/doc.go new file mode 100644 index 00000000..9deb6a09 --- /dev/null +++ b/database/doc.go @@ -0,0 +1,18 @@ +/* + * Copyright 2018 Xiaomi, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Package database will take cover of communicate with mysql database. +package database diff --git a/database/explain.go b/database/explain.go new file mode 100644 index 00000000..ecaed35f --- /dev/null +++ b/database/explain.go @@ -0,0 +1,1069 @@ +/* + * Copyright 2018 Xiaomi, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package database + +import ( + "encoding/json" + "errors" + "fmt" + "regexp" + "runtime" + "strconv" + "strings" + + "github.com/XiaoMi/soar/ast" + "github.com/XiaoMi/soar/common" + + "github.com/tidwall/gjson" + "vitess.io/vitess/go/vt/sqlparser" +) + +// format_type 支持的输出格式 +// https://dev.mysql.com/doc/refman/5.7/en/explain-output.html +const ( + TraditionalFormatExplain = iota // 默认输出 + JSONFormatExplain // JSON格式输出 +) + +// ExplainFormatType EXPLAIN支持的FORMAT_TYPE +var ExplainFormatType = map[string]int{ + "traditional": 0, + "json": 1, +} + +// explain_type +const ( + TraditionalExplainType = iota // 默认转出 + ExtendedExplainType // EXTENDED输出 + PartitionsExplainType // PARTITIONS输出 +) + +// ExplainType EXPLAIN命令支持的参数 +var ExplainType = map[string]int{ + "traditional": 0, + "extended": 1, + "partitions": 2, +} + +// 为TraditionalFormatExplain准备的结构体 { start + +// ExplainInfo 用于存放Explain信息 +type ExplainInfo struct { + SQL string + ExplainFormat int + ExplainRows []*ExplainRow + ExplainJSON *ExplainJSON + Warnings []*ExplainWarning + QueryCost float64 +} + +// ExplainRow 单行Explain +type ExplainRow struct { + ID int + SelectType string + TableName string + Partitions string // explain partitions + AccessType string + PossibleKeys []string + Key string + KeyLen string // 索引长度,如果发生了index_merge, KeyLen格式为N,N,所以不能定义为整型 + Ref []string + Rows int + Filtered float64 // 5.6 JSON, 5.7+, 5.5 EXTENDED + Scalability string // O(1), O(n), O(log n), O(log n)+ + Extra string +} + +// ExplainWarning explain extended后SHOW WARNINGS输出的结果 +type ExplainWarning struct { + Level string + Code int + Message string +} + +// 为TraditionalFormatExplain准备的结构体 end } + +// 为JSONFormatExplain准备的结构体 { start + +// ExplainJSONCostInfo JSON +type ExplainJSONCostInfo struct { + ReadCost string `json:"read_cost"` + EvalCost string `json:"eval_cost"` + PrefixCost string `json:"prefix_cost"` + DataReadPerJoin string `json:"data_read_per_join"` + QueryCost string `json:"query_cost"` + SortCost string `json:"sort_cost"` +} + +// ExplainJSONMaterializedFromSubquery JSON +type ExplainJSONMaterializedFromSubquery struct { + UsingTemporaryTable bool `json:"using_temporary_table"` + Dependent bool `json:"dependent"` + Cacheable bool `json:"cacheable"` + QueryBlock *ExplainJSONQueryBlock `json:"query_block"` +} + +// 该变量用于存放JSON到Traditional模式的所有ExplainJSONTable +var explainJSONTables []*ExplainJSONTable + +// ExplainJSONTable JSON +type ExplainJSONTable struct { + TableName string `json:"table_name"` + AccessType string `json:"access_type"` + PossibleKeys []string `json:"possible_keys"` + Key string `json:"key"` + UsedKeyParts []string `json:"used_key_parts"` + KeyLength string `json:"key_length"` + Ref []string `json:"ref"` + RowsExaminedPerScan int `json:"rows_examined_per_scan"` + RowsProducedPerJoin int `json:"rows_produced_per_join"` + Filtered string `json:"filtered"` + UsingIndex bool `json:"using_index"` + UsingIndexForGroupBy bool `json:"using_index_for_group_by"` + CostInfo ExplainJSONCostInfo `json:"cost_info"` + UsedColumns []string `json:"used_columns"` + AttachedCondition string `json:"attached_condition"` + AttachedSubqueries []ExplainJSONSubqueries `json:"attached_subqueries"` + MaterializedFromSubquery ExplainJSONMaterializedFromSubquery `json:"materialized_from_subquery"` +} + +// ExplainJSONNestedLoop JSON +type ExplainJSONNestedLoop struct { + Table ExplainJSONTable `json:"table"` +} + +// ExplainJSONBufferResult JSON +type ExplainJSONBufferResult struct { + UsingTemporaryTable bool `json:"using_temporary_table"` + NestedLoop []ExplainJSONNestedLoop `json:"nested_loop"` +} + +// ExplainJSONSubqueries JSON +type ExplainJSONSubqueries struct { + Dependent bool `json:"dependent"` + Cacheable bool `json:"cacheable"` + QueryBlock ExplainJSONQueryBlock `json:"query_block"` +} + +// ExplainJSONGroupingOperation JSON +type ExplainJSONGroupingOperation struct { + UsingTemporaryTable bool `json:"using_temporary_table"` + UsingFilesort bool `json:"using_filesort"` + Table ExplainJSONTable `json:"table"` + CostInfo ExplainJSONCostInfo `json:"cost_info"` + NestedLoop []ExplainJSONNestedLoop `json:"nested_loop"` + GroupBySubqueries []ExplainJSONSubqueries `json:"group_by_subqueries"` +} + +// ExplainJSONDuplicatesRemoval JSON +type ExplainJSONDuplicatesRemoval struct { + UsingTemporaryTable bool `json:"using_temporary_table"` + UsingFilesort bool `json:"using_filesort"` + BufferResult ExplainJSONBufferResult `json:"buffer_result"` + GroupingOperation ExplainJSONGroupingOperation `json:"grouping_operation"` +} + +// ExplainJSONOrderingOperation JSON +type ExplainJSONOrderingOperation struct { + UsingFilesort bool `json:"using_filesort"` + Table ExplainJSONTable `json:"table"` + DuplicatesRemoval ExplainJSONDuplicatesRemoval `json:"duplicates_removal"` + GroupingOperation ExplainJSONGroupingOperation `json:"grouping_operation"` + OderbySubqueries []ExplainJSONSubqueries `json:"order_by_subqueries"` +} + +// ExplainJSONQueryBlock JSON +type ExplainJSONQueryBlock struct { + SelectID int `json:"select_id"` + CostInfo ExplainJSONCostInfo `json:"cost_info"` + Table ExplainJSONTable `json:"table"` + NestedLoop []ExplainJSONNestedLoop `json:"nested_loop"` + OrderingOperation ExplainJSONOrderingOperation `json:"ordering_operation"` + GroupingOperation ExplainJSONGroupingOperation `json:"grouping_operation"` + OptimizedAwaySubqueries []ExplainJSONSubqueries `json:"optimized_away_subqueries"` + HavingSubqueries []ExplainJSONSubqueries `json:"having_subqueries"` + SelectListSubqueries []ExplainJSONSubqueries `json:"select_list_subqueries"` + UpdateValueSubqueries []ExplainJSONSubqueries `json:"update_value_subqueries"` + QuerySpecifications []ExplainJSONSubqueries `json:"query_specifications"` + UnionResult ExplainJSONUnionResult `json:"union_result"` + Message string `json:"message"` +} + +// ExplainJSONUnionResult JSON +type ExplainJSONUnionResult struct { + UsingTemporaryTable bool `json:"using_temporary_table"` + TableName string `json:"table_name"` + AccessType string `json:"access_type"` + QuerySpecifications []ExplainJSONSubqueries `json:"query_specifications"` +} + +// ExplainJSON 根结点 +type ExplainJSON struct { + QueryBlock ExplainJSONQueryBlock `json:"query_block"` +} + +// 为JSONFormatExplain准备的结构体 end } + +// ExplainKeyWords 需要解释的关键字 +var ExplainKeyWords = []string{ + "access_type", + "attached_condition", + "attached_subqueries", + "buffer_result", + "cacheable", + "cost_info", + "data_read_per_join", + "dependent", + "duplicates_removal", + "eval_cost", + "filtered", + "group_by_subqueries", + "grouping_operation", + "having_subqueries", + "key", + "key_length", + "materialized_from_subquery", + "message", + "nested_loop", + "optimized_away_subqueries", + "order_by_subqueries", + "ordering_operation", + "possible_keys", + "prefix_cost", + "query_block", + "query_cost", + "query_specifications", + "read_cost", + "ref", + "rows_examined_per_scan", + "rows_produced_per_join", + "select_id", + "select_list_subqueries", + "sort_cost", + "table", + "table_name", + "union_result", + "update_value_subqueries", + "used_columns", + "used_key_parts", + "using_filesort", + "using_index", + "using_index_for_group_by", + "using_temporary_table", +} + +// ExplainColumnIndent EXPLAIN表头 +var ExplainColumnIndent = map[string]string{ + "id": "id为SELECT的标识符. 它是在SELECT查询中的顺序编号. 如果这一行表示其他行的union结果, 这个值可以为空. 在这种情况下, table列会显示为形如, 表示它是id为M和N的查询行的联合结果.", + "select_type": "表示查询的类型. ", + "table": "输出行所引用的表.", + "type": "type显示连接使用的类型, 有关不同类型的描述, 请参见解释连接类型.", + "possible_keys": "指出MySQL能在该表中使用哪些索引有助于查询. 如果为空, 说明没有可用的索引.", + "key": "MySQL实际从possible_keys选择使用的索引. 如果为NULL, 则没有使用索引. 很少情况下, MySQL会选择优化不足的索引. 这种情况下, 可以在select语句中使用USE INDEX (indexname)来强制使用一个索引或者用IGNORE INDEX (indexname)来强制MySQL忽略索引.", + "key_len": "显示MySQL使用索引键的长度. 如果key是NULL, 则key_len为NULL. 使用的索引的长度. 在不损失精确性的情况下, 长度越短越好.", + "ref": "显示索引的哪一列被使用了.", + "rows": "表示MySQL认为必须检查的用来返回请求数据的行数.", + "filtered": "表示返回结果的行占需要读到的行(rows列的值)的百分比.", + "Extra": "该列显示MySQL在查询过程中的一些详细信息, MySQL查询优化器执行查询的过程中对查询计划的重要补充信息.", +} + +// ExplainSelectType EXPLAIN中SELECT TYPE会出现的类型 +var ExplainSelectType = map[string]string{ + "SIMPLE": "简单SELECT(不使用UNION或子查询等).", + "PRIMARY": "最外层的select.", + "UNION": "UNION中的第二个或后面的SELECT查询, 不依赖于外部查询的结果集.", + "DEPENDENT": "UNION中的第二个或后面的SELECT查询, 依赖于外部查询的结果集.", + "UNION RESULT": "UNION查询的结果集.", + "SUBQUERY": "子查询中的第一个SELECT查询, 不依赖于外部查询的结果集.", + "DEPENDENT SUBQUERY": "子查询中的第一个SELECT查询, 依赖于外部查询的结果集.", + "DERIVED": "用于from子句里有子查询的情况. MySQL会递归执行这些子查询, 把结果放在临时表里.", + "MATERIALIZED": "Materialized subquery.", + "UNCACHEABLE SUBQUERY": "结果集不能被缓存的子查询, 必须重新为外层查询的每一行进行评估.", + "UNCACHEABLE UNION": "UNION中的第二个或后面的select查询, 属于不可缓存的子查询(类似于UNCACHEABLE SUBQUERY).", +} + +// ExplainAccessType EXPLAIN中ACCESS TYPE会出现的类型 +var ExplainAccessType = map[string]string{ + "system": "这是const连接类型的一种特例, 该表仅有一行数据(=系统表).", + "const": `const用于使用常数值比较PRIMARY KEY时, 当查询的表仅有一行时, 使用system. 例:SELECT * FROM tbl WHERE col =1.`, + "eq_ref": `除const类型外最好的可能实现的连接类型. 它用在一个索引的所有部分被连接使用并且索引是UNIQUE或PRIMARY KEY, 对于每个索引键, 表中只有一条记录与之匹配. 例:'SELECT * FROM ref_table,tbl WHERE ref_table.key_column=tbl.column;'.`, + "ref": `连接不能基于关键字选择单个行, 可能查找到多个符合条件的行. 叫做ref是因为索引要跟某个参考值相比较. 这个参考值或者是一个数, 或者是来自一个表里的多表查询的结果值. 例:'SELECT * FROM tbl WHERE idx_col=expr;'.`, + "fulltext": "查询时使用 FULLTEXT 索引.", + "ref_or_null": "如同ref, 但是MySQL必须在初次查找的结果里找出null条目, 然后进行二次查找.", + "index_merge": `表示使用了索引合并优化方法. 在这种情况下. key列包含了使用的索引的清单, key_len包含了使用的索引的最长的关键元素. 详情请见 8.2.1.4, “Index Merge Optimization”.`, + "unique_subquery": `在某些IN查询中使用此种类型,而不是常规的ref:'value IN (SELECT primary_key FROM single_table WHERE some_expr)'.`, + "index_subquery": "在某些IN查询中使用此种类型, 与unique_subquery类似, 但是查询的是非唯一索引性索引.", + "range": `只检索给定范围的行, 使用一个索引来选择行. key列显示使用了哪个索引. key_len包含所使用索引的最长关键元素.`, + "index": "全表扫描, 只是扫描表的时候按照索引次序进行而不是行. 主要优点就是避免了排序, 但是开销仍然非常大.", + "ALL": `最坏的情况, 从头到尾全表扫描.`, +} + +// ExplainScalability ACCESS TYPE对应的运算复杂度 [AccessType]scalability map +var ExplainScalability = map[string]string{ + "ALL": "O(n)", + "index": "O(n)", + "range": "O(log n)+", + "index_subquery": "O(log n)+", + "unique_subquery": "O(log n)+", + "index_merge": "O(log n)+", + "ref_or_null": "O(log n)+", + "fulltext": "O(log n)+", + "ref": "O(log n)", + "eq_ref": "O(log n)", + "const": "O(1)", + "system": "O(1)", +} + +// ExplainExtra Extra信息解读 +// https://dev.mysql.com/doc/refman/8.0/en/explain-output.html +// sql/opt_explain_traditional.cc:traditional_extra_tags +var ExplainExtra = map[string]string{ + "Using temporary": "表示MySQL在对查询结果排序时使用临时表. 常见于排序order by和分组查询group by.", + "Using filesort": "MySQL会对结果使用一个外部索引排序,而不是从表里按照索引次序读到相关内容. 可能在内存或者磁盘上进行排序. MySQL中无法利用索引完成的排序操作称为'文件排序'.", + "Using index condition": "在5.6版本后加入的新特性(Index Condition Pushdown)。Using index condition 会先条件过滤索引,过滤完索引后找到所有符合索引条件的数据行,随后用 WHERE 子句中的其他条件去过滤这些数据行。", + "Range checked for each record": "MySQL没有发现好的可以使用的索引,但发现如果来自前面的表的列值已知,可能部分索引可以使用。", + "Using where with pushed condition": "这是一个仅仅在NDBCluster存储引擎中才会出现的信息,打开condition pushdown优化功能才可能被使用。", + "Using MRR": "使用了 MRR Optimization IO 层面进行了优化,减少 IO 方面的开销。", + "Skip_open_table": "Tables are read using the Multi-Range Read optimization strategy.", + "Open_frm_only": "Table files do not need to be opened. The information is already available from the data dictionary.", + "Open_full_table": "Unoptimized information lookup. Table information must be read from the data dictionary and by reading table files.", + "Scanned": "This indicates how many directory scans the server performs when processing a query for INFORMATION_SCHEMA tables.", + "Using index for group-by": "Similar to the Using index table access method, Using index for group-by indicates that MySQL found an index that can be used to retrieve all columns of a GROUP BY or DISTINCT query without any extra disk access to the actual table. Additionally, the index is used in the most efficient way so that for each group, only a few index entries are read.", + "Start temporary": "This indicates temporary table use for the semi-join Duplicate Weedout strategy.Start", + "End temporary": "This indicates temporary table use for the semi-join Duplicate Weedout strategy.End", + "FirstMatch": "The semi-join FirstMatch join shortcutting strategy is used for tbl_name.", + "Materialize": "Materialized subquery", + "Start materialize": "Materialized subquery Start", + "End materialize": "Materialized subquery End", + "unique row not found": "For a query such as SELECT ... FROM tbl_name, no rows satisfy the condition for a UNIQUE index or PRIMARY KEY on the table.", + //"Scan": "", + //"Impossible ON condition": "", + //"Ft_hints:": "", + //"Backward index scan": "", + //"Recursive": "", + //"Table function:": "", + "Index dive skipped due to FORCE": "This item applies to NDB tables only. It means that MySQL Cluster is using the Condition Pushdown optimization to improve the efficiency of a direct comparison between a nonindexed column and a constant. In such cases, the condition is “pushed down” to the cluster's data nodes and is evaluated on all data nodes simultaneously. This eliminates the need to send nonmatching rows over the network, and can speed up such queries by a factor of 5 to 10 times over cases where Condition Pushdown could be but is not used.", + "Impossible WHERE noticed after reading const tables": "查询了所有const(和system)表, 但发现WHERE查询条件不起作用.", + "Using where": "WHERE条件用于筛选出与下一个表匹配的数据然后返回给客户端. 除非故意做的全表扫描, 否则连接类型是ALL或者是index, 且在Extra列的值中没有Using Where, 则该查询可能是有问题的.", + "Using join buffer": "从已有连接中找被读入缓存的数据, 并且通过缓存来完成与当前表的连接.", + "Using index": "只需通过索引就可以从表中获取列的信息, 无需额外去读取真实的行数据. 如果查询使用的列值仅仅是一个简单索引的部分值, 则会使用这种策略来优化查询.", + "const row not found": "空表做类似 SELECT ... FROM tbl_name 的查询操作.", + "Distinct": "MySQL is looking for distinct values, so it stops searching for more rows for the current row combination after it has found the first matching row.", + "Full scan on NULL key": "子查询中的一种优化方式, 常见于无法通过索引访问null值.", + "Impossible HAVING": "HAVING条件过滤没有效果, 返回已有查询的结果集.", + "Impossible WHERE": "WHERE条件过滤没有效果, 最终是全表扫描.", + "LooseScan": "使用半连接LooseScan策略.", + "No matching min/max row": "没有行满足查询的条件, 如 SELECT MIN(...) FROM ... WHERE condition.", + "no matching row in const table": "对于连接查询, 列未满足唯一索引的条件或表为空.", + "No matching rows after partition pruning": "对于DELETE 或 UPDATE, 优化器在分区之后, 未发现任何要删除或更新的内容. 类似查询 Impossible WHERE.", + "No tables used": "查询没有FROM子句, 或者有一个 FROM DUAL子句.", + "Not exists": "MySQL能够对LEFT JOIN查询进行优化, 并且在查找到符合LEFT JOIN条件的行后, 则不再查找更多的行.", + "Plan isn't ready yet": "This value occurs with EXPLAIN FOR CONNECTION when the optimizer has not finished creating the execution plan for the statement executing in the named connection. If execution plan output comprises multiple lines, any or all of them could have this Extra value, depending on the progress of the optimizer in determining the full execution plan.", + "Using intersect": "开启了index merge,即:对多个索引分别进行条件扫描,然后将它们各自的结果进行合并,使用的算法为:index_merge_intersection", + "Using union": "开启了index merge,即:对多个索引分别进行条件扫描,然后将它们各自的结果进行合并,使用的算法为:index_merge_union", + "Using sort_union": "开启了index merge,即:对多个索引分别进行条件扫描,然后将它们各自的结果进行合并,使用的算法为:index_merge_sort_union", +} + +// 提取ExplainJSON中所有的ExplainJSONTable, 将其写入全局变量explainJSONTables +// depth只是用于debug,逻辑上并未使用 +func findTablesInJSON(explainJSON string, depth int) { + common.Log.Debug("findTablesInJSON Enter: depth(%d), json(%s)", depth, explainJSON) + // 去除注释,语法检查 + explainJSON = string(RemoveSQLComments([]byte(explainJSON))) + if !gjson.Valid(explainJSON) { + return + } + // 提取所有ExplainJSONTable struct + for _, key := range ExplainKeyWords { + result := gjson.Get(explainJSON, key) + if result.String() == "" { + continue + } + + if key == "table" { + table := new(ExplainJSONTable) + common.Log.Debug("findTablesInJSON FindTable: depth(%d), table(%s)", depth, result.String) + err := json.Unmarshal([]byte(result.Raw), table) + common.LogIfError(err, "") + if table.TableName != "" { + explainJSONTables = append(explainJSONTables, table) + } + findTablesInJSON(result.String(), depth+1) + } else { + common.Log.Debug("findTablesInJSON ScanOther: depth(%d), key(%s), array_len(%d), json(%s)", depth, key, len(result.Array()), result.String) + for _, val := range result.Array() { + if val.String() != "" { + findTablesInJSON(val.String(), depth+1) + } + } + findTablesInJSON(result.String(), depth+1) + } + } +} + +// FormatJSONIntoTraditional 将JSON形式转换为TRADITIONAL形式,方便前端展现 +func FormatJSONIntoTraditional(explainJSON string) []*ExplainRow { + // 查找JSON中的所有ExplainJSONTable + explainJSONTables = []*ExplainJSONTable{} + findTablesInJSON(explainJSON, 0) + + var explainRows []*ExplainRow + id := -1 + for _, table := range explainJSONTables { + keyLen := table.KeyLength + filtered, err := strconv.ParseFloat(table.Filtered, 64) + if err != nil { + filtered = 0.00 + } + if filtered > 100.00 { + filtered = 100.00 + } + explainRows = append(explainRows, &ExplainRow{ + ID: id + 1, + SelectType: "", + TableName: table.TableName, + Partitions: "NULL", + AccessType: table.AccessType, + PossibleKeys: table.PossibleKeys, + Key: table.Key, + KeyLen: keyLen, + Ref: table.Ref, + Rows: table.RowsExaminedPerScan, + Filtered: filtered, + Scalability: ExplainScalability[table.AccessType], + Extra: "", + }) + } + return explainRows +} + +// ConvertExplainJSON2Row 将JSON格式转成ROW格式,为方便统一做优化建议 +// 但是会损失一些JSON特有的分析结果 +func ConvertExplainJSON2Row(explainJSON *ExplainJSON) []*ExplainRow { + buf, err := json.Marshal(explainJSON) + if err != nil { + return nil + } + return FormatJSONIntoTraditional(string(buf)) +} + +// 用于检测MySQL版本是否低于MySQL5.6 +// 低于5.6 返回 true, 表示需要改写非SELECT的SQL --> SELECT +func (db *Connector) supportExplainWrite() (bool, error) { + defer func() { + err := recover() + if err != nil { + common.Log.Error("Recover supportExplainWrite() Error:", err) + } + }() + + // 5.6以上版本支持EXPLAIN UPDATE/DELETE等语句,但需要开启写入 + // 如开启了read_only,EXPLAIN UPDATE/DELETE也会受限制 + if common.Config.TestDSN.Version >= 560 { + readOnly, err := db.SingleIntValue("read_only") + if err != nil { + return false, err + } + superReadOnly, err := db.SingleIntValue("super_read_only") + // Percona, MariaDB 5.6就已经有super_read_only了,但社区版5.6还没有这个参数 + if strings.Contains(err.Error(), "Unknown system variable") { + superReadOnly = readOnly + } else if err != nil { + return false, err + } + + if readOnly == 1 || superReadOnly == 1 { + return true, nil + } + + return false, nil + } + + return true, nil +} + +// 将SQL语句转换为可以被Explain的语句,如:写转读 +// 当输出为空时,表示语法错误或不支持EXPLAIN +func (db *Connector) explainAbleSQL(sql string) (string, error) { + stmt, err := sqlparser.Parse(sql) + if err != nil { + common.Log.Error("explainAbleSQL sqlparser.Parse Error: %v", err) + return sql, err + } + + switch stmt.(type) { + case *sqlparser.Insert, *sqlparser.Update, *sqlparser.Delete: // REPLACE和INSERT的AST基本相同,只是Action不同 + // 判断Explain的SQL是否需要被改写 + need, err := db.supportExplainWrite() + if err != nil { + common.Log.Error("explainAbleSQL db.supportExplainWrite Error: %v", err) + return "", err + } + if need { + rw := ast.NewRewrite(sql) + if rw != nil { + return rw.RewriteDML2Select().NewSQL, nil + } + } + return sql, nil + + case *sqlparser.Union, *sqlparser.ParenSelect, *sqlparser.Select, sqlparser.SelectStatement: + return sql, nil + default: + } + return "", nil +} + +// 执行explain请求,返回mysql.Result执行结果 +func (db *Connector) executeExplain(sql string, explainType int, formatType int) (*QueryResult, error) { + var err error + sql, _ = db.explainAbleSQL(sql) + if sql == "" { + return nil, err + } + + // 5.6以上支持FORMAT=JSON + explainFormat := "" + switch formatType { + case JSONFormatExplain: + if common.Config.TestDSN.Version >= 560 { + explainFormat = "FORMAT=JSON" + } + } + // 执行explain + var res *QueryResult + switch explainType { + case ExtendedExplainType: + // 5.6以上extended关键字已经不推荐使用,8.0废弃了这个关键字 + if common.Config.TestDSN.Version >= 560 { + res, err = db.Query("explain %s", sql) + } else { + res, err = db.Query("explain extended %s", sql) + } + case PartitionsExplainType: + res, err = db.Query("explain partitions %s", sql) + + default: + res, err = db.Query("explain %s %s", explainFormat, sql) + } + return res, err +} + +// MySQLExplainWarnings WARNINGS信息中包含的优化器信息 +func MySQLExplainWarnings(exp *ExplainInfo) string { + content := "## MySQL优化器调优结果\n\n```sql\n" + for _, row := range exp.Warnings { + content += "\n" + row.Message + "\n" + } + content += "\n```" + return content +} + +// MySQLExplainQueryCost 将last_query_cost信息补充到评审结果中 +func MySQLExplainQueryCost(exp *ExplainInfo) string { + var content string + if exp.QueryCost > 0 { + + tmp := fmt.Sprintf("%.3f\n", exp.QueryCost) + + content = "Query cost: " + if exp.QueryCost > float64(common.Config.MaxQueryCost) { + content += fmt.Sprintf("☠️ **%s**", tmp) + } else { + content += tmp + } + + } + return content +} + +// ExplainInfoTranslator 将explain信息翻译成人能读懂的 +func ExplainInfoTranslator(exp *ExplainInfo) string { + var buf []string + var selectTypeBuf []string + var accessTypeBuf []string + var extraTypeBuf []string + buf = append(buf, fmt.Sprint("### Explain信息解读\n")) + rows := exp.ExplainRows + if exp.ExplainFormat == JSONFormatExplain { + // JSON形式遍历分析不方便,转成Row格式统一处理 + rows = ConvertExplainJSON2Row(exp.ExplainJSON) + } + if len(rows) == 0 { + return "" + } + + // SelectType信息解读 + explainSelectType := make(map[string]string) + for k, v := range ExplainSelectType { + explainSelectType[k] = v + } + for _, row := range rows { + if _, ok := explainSelectType[row.SelectType]; ok { + desc := fmt.Sprintf("* **%s**: %s\n", row.SelectType, explainSelectType[row.SelectType]) + selectTypeBuf = append(selectTypeBuf, desc) + delete(explainSelectType, row.SelectType) + } + } + if len(selectTypeBuf) > 0 { + buf = append(buf, fmt.Sprint("#### SelectType信息解读\n")) + buf = append(buf, strings.Join(selectTypeBuf, "\n")) + } + + // #### Type信息解读 + explainAccessType := make(map[string]string) + for k, v := range ExplainAccessType { + explainAccessType[k] = v + } + for _, row := range rows { + if _, ok := explainAccessType[row.AccessType]; ok { + var warn bool + var desc string + for _, t := range common.Config.ExplainWarnAccessType { + if row.AccessType == t { + warn = true + } + } + if warn { + desc = fmt.Sprintf("* ☠️ **%s**: %s\n", row.AccessType, explainAccessType[row.AccessType]) + } else { + desc = fmt.Sprintf("* **%s**: %s\n", row.AccessType, explainAccessType[row.AccessType]) + } + + accessTypeBuf = append(accessTypeBuf, desc) + delete(explainAccessType, row.AccessType) + } + } + if len(accessTypeBuf) > 0 { + buf = append(buf, fmt.Sprint("#### Type信息解读\n")) + buf = append(buf, strings.Join(accessTypeBuf, "\n")) + } + + // #### Extra信息解读 + if exp.ExplainFormat != JSONFormatExplain { + explainExtra := make(map[string]string) + for k, v := range ExplainExtra { + explainExtra[k] = v + } + for _, row := range rows { + for k, c := range explainExtra { + if strings.Contains(row.Extra, k) { + if k == "Impossible WHERE" { + if strings.Contains(row.Extra, "Impossible WHERE noticed after reading const tables") { + continue + } + } + warn := false + for _, w := range common.Config.ExplainWarnExtra { + if k == w { + warn = true + } + } + if warn { + extraTypeBuf = append(extraTypeBuf, fmt.Sprintf("* ☠️ **%s**: %s\n", k, c)) + } else { + extraTypeBuf = append(extraTypeBuf, fmt.Sprintf("* **%s**: %s\n", k, c)) + } + delete(explainExtra, k) + } + } + } + } + if len(extraTypeBuf) > 0 { + buf = append(buf, fmt.Sprint("#### Extra信息解读\n")) + buf = append(buf, strings.Join(extraTypeBuf, "\n")) + } + + return strings.Join(buf, "\n") +} + +// ParseExplainText 解析explain文本信息(很可能是用户复制粘贴得到),返回格式化数据 +func ParseExplainText(content string) (exp *ExplainInfo, err error) { + exp = &ExplainInfo{ExplainFormat: TraditionalFormatExplain} + + content = strings.TrimSpace(content) + verticalFormat := strings.HasPrefix(content, "*") + jsonFormat := strings.HasPrefix(content, "{") + traditionalFormat := strings.HasPrefix(content, "+") + + if verticalFormat && traditionalFormat && jsonFormat { + return nil, errors.New("not supported explain type") + } + + if verticalFormat { + exp.ExplainRows, err = parseVerticalExplainText(content) + } + + if jsonFormat { + exp.ExplainFormat = JSONFormatExplain + exp.ExplainJSON, err = parseJSONExplainText(content) + } + + if traditionalFormat { + exp.ExplainRows, err = parseTraditionalExplainText(content) + } + return exp, err +} + +// 解析文本形式传统形式Explain信息 +func parseTraditionalExplainText(content string) (explainRows []*ExplainRow, err error) { + LS := regexp.MustCompile(`^\+`) // 华丽的分隔线:) + + // 格式正确性检查 + lines := strings.Split(content, "\n") + if len(lines) < 3 { + return nil, errors.New("explain Rows less than 3") + } + + // 提取头部,用于后续list到map的转换 + var header []string + for _, h := range strings.Split(strings.Trim(lines[1], "|"), "|") { + header = append(header, strings.TrimSpace(h)) + } + colIdx := make(map[string]int) + for i, item := range header { + colIdx[strings.ToLower(item)] = i + } + + // explain format=json未把外面的框去了 + if strings.ToLower(header[0]) == "explain" { + return nil, errors.New("json format explain need remove") + } + + // 将每一列填充至ExplainRow结构体 + colsMap := make(map[string]string) + for _, l := range lines[3:] { + var keylen string + var rows int + var filtered float64 + var partitions string + // 跳过分割线 + if LS.MatchString(l) || strings.TrimSpace(l) == "" { + continue + } + + // list到map的转换 + var cols []string + for _, c := range strings.Split(strings.Trim(l, "|"), "|") { + cols = append(cols, strings.TrimSpace(c)) + } + for item, i := range colIdx { + colsMap[item] = cols[i] + } + + // 值类型转换 + id, err := strconv.Atoi(colsMap["id"]) + if err != nil { + return nil, err + } + + // 不存在字段给默认值 + if colsMap["partitions"] == "" { + partitions = "NULL" + } else { + partitions = colsMap["partitions"] + } + + keylen = colsMap["key_len"] + + rows, err = strconv.Atoi(colsMap["Rows"]) + if err != nil { + rows = 0 + } + + filtered, err = strconv.ParseFloat(colsMap["filtered"], 64) + if err != nil { + filtered = 0.00 + } + if filtered > 100.00 { + filtered = 100.00 + } + + // 拼接结构体 + explainRows = append(explainRows, &ExplainRow{ + ID: id, + SelectType: colsMap["select_type"], + TableName: colsMap["table"], + Partitions: partitions, + AccessType: colsMap["type"], + PossibleKeys: strings.Split(colsMap["possible_keys"], ","), + Key: colsMap["key"], + KeyLen: keylen, + Ref: strings.Split(colsMap["ref"], ","), + Rows: rows, + Filtered: filtered, + Scalability: ExplainScalability[colsMap["type"]], + Extra: colsMap["extra"], + }) + } + return explainRows, nil +} + +// 解析文本形式竖排版 Explain信息 +func parseVerticalExplainText(content string) (explainRows []*ExplainRow, err error) { + var lines []string + explainRow := &ExplainRow{ + Partitions: "NULL", + Filtered: 0.00, + } + LS := regexp.MustCompile(`^\*.*\*$`) // 华丽的分隔线:) + + // 格式正确性检查 + for _, l := range strings.Split(content, "\n") { + lines = append(lines, strings.TrimSpace(l)) + } + if len(lines) < 11 { + return nil, errors.New("explain rows less than 11") + } + + // 将每一行填充至ExplainRow结构体 + for _, l := range lines { + if LS.MatchString(l) || strings.TrimSpace(l) == "" { + continue + } + if strings.HasPrefix(l, "id:") { + id := strings.TrimPrefix(l, "id: ") + explainRow.ID, err = strconv.Atoi(id) + if err != nil { + return nil, err + } + } + if strings.HasPrefix(l, "select_type:") { + explainRow.SelectType = strings.TrimPrefix(l, "select_type: ") + } + if strings.HasPrefix(l, "table:") { + explainRow.TableName = strings.TrimPrefix(l, "table: ") + } + if strings.HasPrefix(l, "partitions:") { + explainRow.AccessType = strings.TrimPrefix(l, "partitions: ") + } + if strings.HasPrefix(l, "type:") { + explainRow.AccessType = strings.TrimPrefix(l, "type: ") + explainRow.Scalability = ExplainScalability[explainRow.AccessType] + } + if strings.HasPrefix(l, "possible_keys:") { + explainRow.PossibleKeys = strings.Split(strings.TrimPrefix(l, "possible_keys: "), ",") + } + if strings.HasPrefix(l, "key:") { + explainRow.Key = strings.TrimPrefix(l, "key: ") + } + if strings.HasPrefix(l, "key_len:") { + keyLen := strings.TrimPrefix(l, "key_len: ") + explainRow.KeyLen = keyLen + } + if strings.HasPrefix(l, "ref:") { + explainRow.Ref = strings.Split(strings.TrimPrefix(l, "ref: "), ",") + } + if strings.HasPrefix(l, "Rows:") { + rows := strings.TrimPrefix(l, "Rows: ") + explainRow.Rows, err = strconv.Atoi(rows) + if err != nil { + explainRow.Rows = 0 + } + } + if strings.HasPrefix(l, "filtered:") { + filtered := strings.TrimPrefix(l, "filtered: ") + explainRow.Filtered, err = strconv.ParseFloat(filtered, 64) + if err != nil { + return nil, err + } else if explainRow.Filtered > 100.00 { + explainRow.Filtered = 100.00 + } + } + if strings.HasPrefix(l, "Extra:") { + explainRow.Extra = strings.TrimPrefix(l, "Extra: ") + explainRows = append(explainRows, explainRow) + } + } + return explainRows, err +} + +// 解析文本形式JSON Explain信息 +func parseJSONExplainText(content string) (*ExplainJSON, error) { + explainJSON := new(ExplainJSON) + err := json.Unmarshal(RemoveSQLComments([]byte(content)), explainJSON) + return explainJSON, err +} + +// ParseExplainResult 分析mysql执行explain的结果,返回ExplainInfo结构化数据 +func ParseExplainResult(res *QueryResult, formatType int) (exp *ExplainInfo, err error) { + exp = &ExplainInfo{ + ExplainFormat: formatType, + } + // JSON格式直接调用文本方式解析 + if formatType == JSONFormatExplain { + exp.ExplainJSON, err = parseJSONExplainText(res.Rows[0].Str(0)) + return exp, err + } + + // 生成表头 + colIdx := make(map[int]string) + for i, f := range res.Result.Fields() { + colIdx[i] = strings.ToLower(f.Name) + } + // 补全ExplainRows + var explainrows []*ExplainRow + for _, row := range res.Rows { + expRow := &ExplainRow{Partitions: "NULL", Filtered: 0.00} + // list到map的转换 + for i := range row { + switch colIdx[i] { + case "id": + expRow.ID = row.ForceInt(i) + case "select_type": + expRow.SelectType = row.Str(i) + case "table": + expRow.TableName = row.Str(i) + if expRow.TableName == "" { + expRow.TableName = "NULL" + } + case "type": + expRow.AccessType = row.Str(i) + if expRow.AccessType == "" { + expRow.AccessType = "NULL" + } + expRow.Scalability = ExplainScalability[expRow.AccessType] + case "possible_keys": + expRow.PossibleKeys = strings.Split(row.Str(i), ",") + case "key": + expRow.Key = row.Str(i) + if expRow.Key == "" { + expRow.Key = "NULL" + } + case "key_len": + expRow.KeyLen = row.Str(i) + case "ref": + expRow.Ref = strings.Split(row.Str(i), ",") + case "rows": + expRow.Rows = row.ForceInt(i) + case "extra": + expRow.Extra = row.Str(i) + if expRow.Extra == "" { + expRow.Extra = "NULL" + } + case "filtered": + expRow.Filtered = row.ForceFloat(i) + // MySQL bug: https://bugs.mysql.com/bug.php?id=34124 + if expRow.Filtered > 100.00 { + expRow.Filtered = 100.00 + } + } + } + explainrows = append(explainrows, expRow) + } + exp.ExplainRows = explainrows + for _, w := range res.Warning { + // 'EXTENDED' is deprecated and will be removed in a future release. + if w.Int(1) != 1681 { + exp.Warnings = append(exp.Warnings, &ExplainWarning{Level: w.Str(0), Code: w.Int(1), Message: w.Str(2)}) + } + } + + // 添加 last_query_cost + exp.QueryCost = res.QueryCost + + return exp, err +} + +// Explain 获取SQL的explain信息 +func (db *Connector) Explain(sql string, explainType int, formatType int) (exp *ExplainInfo, err error) { + exp = &ExplainInfo{} + if explainType != TraditionalExplainType { + formatType = TraditionalFormatExplain + } + defer func() { + if e := recover(); e != nil { + const size = 4096 + buf := make([]byte, size) + buf = buf[:runtime.Stack(buf, false)] + common.Log.Error("Recover Explain() Error: %v\n%v", e, string(buf)) + err = errors.New(fmt.Sprint(e)) + } + }() + + // 执行EXPLAIN请求 + res, err := db.executeExplain(sql, explainType, formatType) + if err != nil || res == nil { + return exp, err + } + + // 解析mysql结果,输出ExplainInfo + exp, err = ParseExplainResult(res, formatType) + + // 补全SQL + exp.SQL = sql + return exp, err +} + +// PrintMarkdownExplainTable 打印markdown格式的explain table +func PrintMarkdownExplainTable(exp *ExplainInfo) string { + var buf []string + rows := exp.ExplainRows + // JSON转换为TRADITIONAL格式 + if exp.ExplainFormat == JSONFormatExplain { + buf = append(buf, fmt.Sprint("以下为JSON格式转为传统格式EXPLAIN表格", "\n\n")) + rows = ConvertExplainJSON2Row(exp.ExplainJSON) + } + + // explain出错 + if len(rows) == 0 { + return "" + } + if exp.ExplainFormat == JSONFormatExplain { + buf = append(buf, fmt.Sprintln("| table | partitions | type | possible\\_keys | key | key\\_len | ref | rows | filtered | scalability | Extra |")) + buf = append(buf, fmt.Sprintln("|---|---|---|---|---|---|---|---|---|---|---|")) + for _, row := range rows { + buf = append(buf, fmt.Sprintln("|", row.TableName, "|", row.Partitions, "|", row.AccessType, + "|", strings.Join(row.PossibleKeys, ","), "|", row.Key, "|", row.KeyLen, "|", + strings.Join(row.Ref, ","), "|", row.Rows, "|", fmt.Sprintf("%.2f%s", row.Filtered, "%"), + "|", row.Scalability, "|", row.Extra, "|")) + } + } else { + buf = append(buf, fmt.Sprintln("| id | select\\_type | table | partitions | type | possible_keys | key | key\\_len | ref | rows | filtered | scalability | Extra |")) + buf = append(buf, fmt.Sprintln("|---|---|---|---|---|---|---|---|---|---|---|---|---|")) + for _, row := range rows { + // 加粗 + rows := fmt.Sprint(row.Rows) + if row.Rows >= common.Config.ExplainMaxRows { + rows = "☠️ **" + rows + "**" + } + filtered := fmt.Sprintf("%.2f%s", row.Filtered, "%") + if row.Filtered >= common.Config.ExplainMaxFiltered { + filtered = "☠️ **" + filtered + "**" + } + scalability := row.Scalability + for _, s := range common.Config.ExplainWarnScalability { + scalability = "☠️ **" + s + "**" + } + buf = append(buf, fmt.Sprintln("|", row.ID, " |", + common.MarkdownEscape(row.SelectType), + "| *"+common.MarkdownEscape(row.TableName)+"* |", + common.MarkdownEscape(row.Partitions), "|", + common.MarkdownEscape(row.AccessType), "|", + common.MarkdownEscape(strings.Join(row.PossibleKeys, ",
")), "|", + common.MarkdownEscape(row.Key), "|", + row.KeyLen, "|", + common.MarkdownEscape(strings.Join(row.Ref, ",
")), + "|", rows, "|", + filtered, "|", scalability, "|", + strings.Replace(common.MarkdownEscape(row.Extra), ",", ",
", -1), + "|")) + } + } + buf = append(buf, "\n") + return strings.Join(buf, "") +} diff --git a/database/explain_test.go b/database/explain_test.go new file mode 100644 index 00000000..6cd6e60b --- /dev/null +++ b/database/explain_test.go @@ -0,0 +1,2454 @@ +/* + * Copyright 2018 Xiaomi, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package database + +import ( + "fmt" + "os" + "testing" + + "github.com/XiaoMi/soar/common" + + "github.com/kr/pretty" +) + +var connTest *Connector + +func init() { + common.BaseDir = common.DevPath + common.ParseConfig("") + connTest = &Connector{ + Addr: common.Config.OnlineDSN.Addr, + User: common.Config.OnlineDSN.User, + Pass: common.Config.OnlineDSN.Password, + Database: common.Config.OnlineDSN.Schema, + } + if _, err := connTest.Version(); err != nil { + common.Log.Critical("Test env Error: %v", err) + os.Exit(0) + } +} + +var sqls = []string{ + `select * from city where country_id = 44;`, + `select * from address where address2 is not null;`, + `select * from address where address2 is null;`, + `select * from address where address2 >= 44;`, + `select * from city where country_id between 44 and 107;`, + `select * from city where city like 'Ad%';`, + `select * from city where city = 'Aden' and country_id = 107;`, + `select * from city where country_id > 31 and city = 'Aden';`, + `select * from address where address_id > 8 and city_id < 400 and district = 'Nantou';`, + `select * from address where address_id > 8 and city_id < 400;`, + `select * from actor where last_update='2006-02-15 04:34:33' and last_name='CHASE' group by first_name;`, + `select * from address where last_update >='2014-09-25 22:33:47' group by district;`, + `select * from address group by address,district;`, + `select * from address where last_update='2014-09-25 22:30:27' group by district,(address_id+city_id);`, + `select * from customer where active=1 order by last_name limit 10;`, + `select * from customer order by last_name limit 10;`, + `select * from customer where address_id > 224 order by address_id limit 10;`, + `select * from customer where address_id < 224 order by address_id limit 10;`, + `select * from customer where active=1 order by last_name;`, + `select * from customer where address_id > 224 order by address_id;`, + `select * from customer where address_id in (224,510) order by last_name;`, + `select city from city where country_id = 44;`, + `select city,city_id from city where country_id = 44 and last_update='2006-02-15 04:45:25';`, + `select city from city where country_id > 44 and last_update > '2006-02-15 04:45:25';`, + `select * from city where country_id=1 and city='Kabul' order by last_update;`, + `select * from city where country_id>1 and city='Kabul' order by last_update;`, + `select * from city where city_id>251 order by last_update;`, + `select * from city i inner join country o on i.country_id=o.country_id;`, + `select * from city i left join country o on i.city_id=o.country_id;`, + `select * from city i right join country o on i.city_id=o.country_id;`, + `select * from city i left join country o on i.city_id=o.country_id where o.country_id is null;`, + `select * from city i right join country o on i.city_id=o.country_id where i.city_id is null;`, + `select * from city i left join country o on i.city_id=o.country_id union select * from city i right join country o on i.city_id=o.country_id;`, + `select * from city i left join country o on i.city_id=o.country_id where o.country_id is null union select * from city i right join country o on i.city_id=o.country_id where i.city_id is null;`, + `select first_name,last_name,email from customer natural left join address;`, + `select first_name,last_name,email from customer natural left join address;`, + `select first_name,last_name,email from customer natural right join address;`, + `select first_name,last_name,email from customer STRAIGHT_JOIN address on customer.address_id=address.address_id;`, + `select ID,name from (select address from customer_list where SID=1 order by phone limit 50,10) a join customer_list l on (a.address=l.address) join city c on (c.city=l.city) order by phone desc;`, +} + +var exp = []string{ + `+----+-------------+---------+-------+---------------------------------------------------------+-------------------+---------+---------------------------+------+-------------+ +| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | ++----+-------------+---------+-------+---------------------------------------------------------+-------------------+---------+---------------------------+------+-------------+ +| 1 | SIMPLE | country | index | PRIMARY,country_id | country | 152 | NULL | 109 | Using index | +| 1 | SIMPLE | city | ref | idx_fk_country_id,idx_country_id_city,idx_all,idx_other | idx_fk_country_id | 2 | sakila.country.country_id | 2 | Using index | ++----+-------------+---------+-------+---------------------------------------------------------+-------------------+---------+---------------------------+------+-------------+`, + `+----+-------------+---------+------------+-------+-------------------+-------------------+---------+---------------------------+------+----------+-------------+ +| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | ++----+-------------+---------+------------+-------+-------------------+-------------------+---------+---------------------------+------+----------+-------------+ +| 1 | SIMPLE | country | NULL | index | PRIMARY | PRIMARY | 2 | NULL | 109 | 100.00 | Using index | +| 1 | SIMPLE | city | NULL | ref | idx_fk_country_id | idx_fk_country_id | 2 | sakila.country.country_id | 5 | 100.00 | Using index | ++----+-------------+---------+------------+-------+-------------------+-------------------+---------+---------------------------+------+----------+-------------+`, + `*************************** 1. row *************************** + id: 1 + select_type: SIMPLE + table: country + type: index +possible_keys: PRIMARY,country_id + key: country + key_len: 152 + ref: NULL + rows: 109 + Extra: Using index +*************************** 2. row *************************** + id: 1 + select_type: SIMPLE + table: city + type: ref +possible_keys: idx_fk_country_id,idx_country_id_city,idx_all,idx_other + key: idx_fk_country_id + key_len: 2 + ref: sakila.country.country_id + rows: 2 + Extra: Using index`, + `+----+-------------+---------+------------+-------+---------------------------------------------------------+-------------------+---------+---------------------------+------+-------------+ +| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | Extra | ++----+-------------+---------+------------+-------+---------------------------------------------------------+-------------------+---------+---------------------------+------+-------------+ +| 1 | SIMPLE | country | NULL | index | PRIMARY,country_id | country | 152 | NULL | 109 | Using index | +| 1 | SIMPLE | city | NULL | ref | idx_fk_country_id,idx_country_id_city,idx_all,idx_other | idx_fk_country_id | 2 | sakila.country.country_id | 2 | Using index | ++----+-------------+---------+------------+-------+---------------------------------------------------------+-------------------+---------+---------------------------+------+-------------+`, + `{ + "query_block": { + "select_id": 1, + "message": "No tables used" + } /* query_block */ +}`, + `{ + "query_block": { + "select_id": 1, + "message": "no matching row in const table" + } +}`, + `{ + "query_block": { + "select_id": 1, + "table": { + "insert": true, + "table_name": "t1", + "access_type": "ALL" + } /* table */ + } /* query_block */ +}`, + `{ + "query_block": { + "select_id": 1, + "message": "no matching row in const table" + } /* query_block */ +}`, + `{ + "query_block": { + "select_id": 1, + "message": "no matching row in const table" + } /* query_block */ +}`, + `{ + "query_block": { + "select_id": 1, + "cost_info": { + "query_cost": "13.50" + } /* cost_info */, + "table": { + "table_name": "a4", + "access_type": "ALL", + "rows_examined_per_scan": 14, + "rows_produced_per_join": 14, + "filtered": "100.00", + "cost_info": { + "read_cost": "10.70", + "eval_cost": "2.80", + "prefix_cost": "13.50", + "data_read_per_join": "224" + } /* cost_info */, + "used_columns": [ + "i" + ] /* used_columns */, + "materialized_from_subquery": { + "using_temporary_table": true, + "dependent": false, + "cacheable": true, + "query_block": { + "select_id": 2, + "cost_info": { + "query_cost": "13.50" + } /* cost_info */, + "table": { + "table_name": "a3", + "access_type": "ALL", + "rows_examined_per_scan": 14, + "rows_produced_per_join": 14, + "filtered": "100.00", + "cost_info": { + "read_cost": "10.70", + "eval_cost": "2.80", + "prefix_cost": "13.50", + "data_read_per_join": "224" + } /* cost_info */, + "used_columns": [ + "i" + ] /* used_columns */, + "materialized_from_subquery": { + "using_temporary_table": true, + "dependent": false, + "cacheable": true, + "query_block": { + "select_id": 3, + "cost_info": { + "query_cost": "13.50" + } /* cost_info */, + "table": { + "table_name": "a2", + "access_type": "ALL", + "rows_examined_per_scan": 14, + "rows_produced_per_join": 14, + "filtered": "100.00", + "cost_info": { + "read_cost": "10.70", + "eval_cost": "2.80", + "prefix_cost": "13.50", + "data_read_per_join": "224" + } /* cost_info */, + "used_columns": [ + "i" + ] /* used_columns */, + "materialized_from_subquery": { + "using_temporary_table": true, + "dependent": false, + "cacheable": true, + "query_block": { + "select_id": 4, + "cost_info": { + "query_cost": "15.55" + } /* cost_info */, + "nested_loop": [ + { + "table": { + "table_name": "t2", + "access_type": "ALL", + "rows_examined_per_scan": 2, + "rows_produced_per_join": 2, + "filtered": "100.00", + "cost_info": { + "read_cost": "2.00", + "eval_cost": "0.40", + "prefix_cost": "2.40", + "data_read_per_join": "16" + } /* cost_info */ + } /* table */ + }, + { + "table": { + "table_name": "a1", + "access_type": "ALL", + "rows_examined_per_scan": 7, + "rows_produced_per_join": 14, + "filtered": "100.00", + "using_join_buffer": "Block Nested Loop", + "cost_info": { + "read_cost": "10.35", + "eval_cost": "2.80", + "prefix_cost": "15.55", + "data_read_per_join": "224" + } /* cost_info */, + "used_columns": [ + "i" + ] /* used_columns */, + "materialized_from_subquery": { + "using_temporary_table": true, + "dependent": false, + "cacheable": true, + "query_block": { + "select_id": 5, + "cost_info": { + "query_cost": "3.41" + } /* cost_info */, + "table": { + "table_name": "t1", + "access_type": "ALL", + "rows_examined_per_scan": 7, + "rows_produced_per_join": 7, + "filtered": "100.00", + "cost_info": { + "read_cost": "2.01", + "eval_cost": "1.40", + "prefix_cost": "3.41", + "data_read_per_join": "56" + } /* cost_info */, + "used_columns": [ + "i" + ] /* used_columns */ + } /* table */ + } /* query_block */ + } /* materialized_from_subquery */ + } /* table */ + } + ] /* nested_loop */ + } /* query_block */ + } /* materialized_from_subquery */ + } /* table */ + } /* query_block */ + } /* materialized_from_subquery */ + } /* table */ + } /* query_block */ + } /* materialized_from_subquery */ + } /* table */ + } /* query_block */ +}`, + `{ + "query_block": { + "select_id": 1, + "cost_info": { + "query_cost": "5.81" + } /* cost_info */, + "nested_loop": [ + { + "table": { + "table_name": "t1", + "access_type": "ALL", + "rows_examined_per_scan": 7, + "rows_produced_per_join": 0, + "filtered": "14.29", + "cost_info": { + "read_cost": "3.21", + "eval_cost": "0.20", + "prefix_cost": "3.41", + "data_read_per_join": "7" + } /* cost_info */, + "used_columns": [ + "i" + ] /* used_columns */, + "attached_condition": "(test.t1.i = 10)" + } /* table */ + }, + { + "table": { + "table_name": "t2", + "access_type": "ALL", + "rows_examined_per_scan": 2, + "rows_produced_per_join": 0, + "filtered": "50.00", + "first_match": "t1", + "using_join_buffer": "Block Nested Loop", + "cost_info": { + "read_cost": "2.20", + "eval_cost": "0.20", + "prefix_cost": "5.82", + "data_read_per_join": "7" + } /* cost_info */, + "used_columns": [ + "i" + ] /* used_columns */, + "attached_condition": "(test.t2.i = 10)" + } /* table */ + } + ] /* nested_loop */ + } /* query_block */ +}`, + `{ + "query_block": { + "select_id": 1, + "cost_info": { + "query_cost": "3.41" + } /* cost_info */, + "table": { + "table_name": "t1", + "access_type": "ALL", + "rows_examined_per_scan": 7, + "rows_produced_per_join": 7, + "filtered": "100.00", + "cost_info": { + "read_cost": "2.01", + "eval_cost": "1.40", + "prefix_cost": "3.41", + "data_read_per_join": "56" + } /* cost_info */, + "used_columns": [ + "i" + ] /* used_columns */, + "attached_condition": "((test.t1.i ,(/* select#2 */ select 1 from test.t2 where ((test.t1.i = 10) and ((test.t1.i) = test.t2.i)))) or (test.t1.i.test.t1.i in ( (/* select#3 */ select NULL from test.t4 where 1 ), (test.t1.i in on where ((test.t1.i = materialized-subquery.i))))))", + "attached_subqueries": [ + { + "table": { + "table_name": "", + "access_type": "eq_ref", + "key": "", + "key_length": "5", + "rows_examined_per_scan": 1, + "materialized_from_subquery": { + "using_temporary_table": true, + "dependent": true, + "cacheable": false, + "query_block": { + "select_id": 3, + "message": "no matching row in const table" + } /* query_block */ + } /* materialized_from_subquery */ + } /* table */ + }, + { + "dependent": true, + "cacheable": false, + "query_block": { + "select_id": 2, + "cost_info": { + "query_cost": "2.40" + } /* cost_info */, + "table": { + "table_name": "t2", + "access_type": "ALL", + "rows_examined_per_scan": 2, + "rows_produced_per_join": 1, + "filtered": "50.00", + "cost_info": { + "read_cost": "2.00", + "eval_cost": "0.20", + "prefix_cost": "2.40", + "data_read_per_join": "8" + } /* cost_info */, + "used_columns": [ + "i" + ] /* used_columns */, + "attached_condition": "((test.t1.i = 10) and ((test.t1.i) = test.t2.i))" + } /* table */ + } /* query_block */ + } + ] /* attached_subqueries */ + } /* table */ + } /* query_block */ +}`, + `{ + "query_block": { + "union_result": { + "using_temporary_table": true, + "table_name": "", + "access_type": "ALL", + "query_specifications": [ + { + "dependent": false, + "cacheable": true, + "query_block": { + "select_id": 1, + "cost_info": { + "query_cost": "3.41" + } /* cost_info */, + "table": { + "table_name": "t1", + "access_type": "ALL", + "rows_examined_per_scan": 7, + "rows_produced_per_join": 7, + "filtered": "100.00", + "cost_info": { + "read_cost": "2.01", + "eval_cost": "1.40", + "prefix_cost": "3.41", + "data_read_per_join": "56" + } /* cost_info */, + "used_columns": [ + "i" + ] /* used_columns */ + } /* table */ + } /* query_block */ + }, + { + "dependent": false, + "cacheable": true, + "query_block": { + "select_id": 2, + "cost_info": { + "query_cost": "2.40" + } /* cost_info */, + "table": { + "table_name": "t2", + "access_type": "ALL", + "rows_examined_per_scan": 2, + "rows_produced_per_join": 2, + "filtered": "100.00", + "cost_info": { + "read_cost": "2.00", + "eval_cost": "0.40", + "prefix_cost": "2.40", + "data_read_per_join": "16" + } /* cost_info */, + "used_columns": [ + "i" + ] /* used_columns */ + } /* table */ + } /* query_block */ + }, + { + "dependent": false, + "cacheable": true, + "query_block": { + "select_id": 3, + "message": "no matching row in const table" + } /* query_block */ + } + ] /* query_specifications */ + } /* union_result */ + } /* query_block */ +}`, + `{ + "query_block": { + "union_result": { + "using_temporary_table": false, + "query_specifications": [ + { + "dependent": false, + "cacheable": true, + "query_block": { + "select_id": 1, + "cost_info": { + "query_cost": "7.21" + } /* cost_info */, + "nested_loop": [ + { + "table": { + "table_name": "t2", + "access_type": "ALL", + "rows_examined_per_scan": 2, + "rows_produced_per_join": 2, + "filtered": "100.00", + "cost_info": { + "read_cost": "2.00", + "eval_cost": "0.40", + "prefix_cost": "2.40", + "data_read_per_join": "16" + } /* cost_info */ + } /* table */ + }, + { + "table": { + "table_name": "t1", + "access_type": "ALL", + "rows_examined_per_scan": 7, + "rows_produced_per_join": 14, + "filtered": "100.00", + "using_join_buffer": "Block Nested Loop", + "cost_info": { + "read_cost": "2.01", + "eval_cost": "2.80", + "prefix_cost": "7.22", + "data_read_per_join": "112" + } /* cost_info */, + "used_columns": [ + "i" + ] /* used_columns */ + } /* table */ + } + ] /* nested_loop */ + } /* query_block */ + }, + { + "dependent": false, + "cacheable": true, + "query_block": { + "select_id": 2, + "message": "no matching row in const table" + } /* query_block */ + } + ] /* query_specifications */ + } /* union_result */ + } /* query_block */ +}`, + `{ + "query_block": { + "ordering_operation": { + "using_filesort": true, + "union_result": { + "using_temporary_table": true, + "table_name": "", + "access_type": "ALL", + "query_specifications": [ + { + "dependent": false, + "cacheable": true, + "query_block": { + "select_id": 1, + "cost_info": { + "query_cost": "3.41" + } /* cost_info */, + "table": { + "table_name": "t1", + "access_type": "ALL", + "rows_examined_per_scan": 7, + "rows_produced_per_join": 7, + "filtered": "100.00", + "cost_info": { + "read_cost": "2.01", + "eval_cost": "1.40", + "prefix_cost": "3.41", + "data_read_per_join": "56" + } /* cost_info */, + "used_columns": [ + "i" + ] /* used_columns */ + } /* table */ + } /* query_block */ + }, + { + "dependent": false, + "cacheable": true, + "query_block": { + "select_id": 2, + "cost_info": { + "query_cost": "2.40" + } /* cost_info */, + "table": { + "table_name": "t2", + "access_type": "ALL", + "rows_examined_per_scan": 2, + "rows_produced_per_join": 2, + "filtered": "100.00", + "cost_info": { + "read_cost": "2.00", + "eval_cost": "0.40", + "prefix_cost": "2.40", + "data_read_per_join": "16" + } /* cost_info */, + "used_columns": [ + "i" + ] /* used_columns */ + } /* table */ + } /* query_block */ + } + ] /* query_specifications */ + } /* union_result */, + "order_by_subqueries": [ + { + "dependent": true, + "cacheable": false, + "query_block": { + "select_id": 3, + "message": "No tables used" + } /* query_block */ + } + ] /* order_by_subqueries */ + } /* ordering_operation */ + } /* query_block */ +}`, + `{ + "query_block": { + "select_id": 1, + "cost_info": { + "query_cost": "3.41" + } /* cost_info */, + "ordering_operation": { + "using_filesort": false, + "table": { + "table_name": "t1", + "access_type": "ALL", + "rows_examined_per_scan": 7, + "rows_produced_per_join": 7, + "filtered": "100.00", + "cost_info": { + "read_cost": "2.01", + "eval_cost": "1.40", + "prefix_cost": "3.41", + "data_read_per_join": "56" + } /* cost_info */, + "used_columns": [ + "i" + ] /* used_columns */ + } /* table */, + "optimized_away_subqueries": [ + { + "dependent": false, + "cacheable": true, + "query_block": { + "select_id": 2, + "cost_info": { + "query_cost": "2.40" + } /* cost_info */, + "table": { + "table_name": "t2", + "access_type": "ALL", + "rows_examined_per_scan": 2, + "rows_produced_per_join": 2, + "filtered": "100.00", + "cost_info": { + "read_cost": "2.00", + "eval_cost": "0.40", + "prefix_cost": "2.40", + "data_read_per_join": "16" + } /* cost_info */ + } /* table */ + } /* query_block */ + } + ] /* optimized_away_subqueries */ + } /* ordering_operation */ + } /* query_block */ +}`, + `{ + "query_block": { + "select_id": 1, + "cost_info": { + "query_cost": "3.41" + } /* cost_info */, + "table": { + "table_name": "t1", + "access_type": "ALL", + "rows_examined_per_scan": 7, + "rows_produced_per_join": 7, + "filtered": "100.00", + "cost_info": { + "read_cost": "2.01", + "eval_cost": "1.40", + "prefix_cost": "3.41", + "data_read_per_join": "56" + } /* cost_info */, + "used_columns": [ + "i" + ] /* used_columns */ + } /* table */, + "having_subqueries": [ + { + "dependent": false, + "cacheable": true, + "query_block": { + "select_id": 3, + "cost_info": { + "query_cost": "2.40" + } /* cost_info */, + "table": { + "table_name": "t2", + "access_type": "ALL", + "rows_examined_per_scan": 2, + "rows_produced_per_join": 2, + "filtered": "100.00", + "cost_info": { + "read_cost": "2.00", + "eval_cost": "0.40", + "prefix_cost": "2.40", + "data_read_per_join": "16" + } /* cost_info */, + "used_columns": [ + "i" + ] /* used_columns */ + } /* table */ + } /* query_block */ + }, + { + "dependent": false, + "cacheable": true, + "query_block": { + "select_id": 2, + "cost_info": { + "query_cost": "2.40" + } /* cost_info */, + "table": { + "table_name": "t2", + "access_type": "ALL", + "rows_examined_per_scan": 2, + "rows_produced_per_join": 2, + "filtered": "100.00", + "cost_info": { + "read_cost": "2.00", + "eval_cost": "0.40", + "prefix_cost": "2.40", + "data_read_per_join": "16" + } /* cost_info */, + "used_columns": [ + "i" + ] /* used_columns */ + } /* table */ + } /* query_block */ + } + ] /* having_subqueries */ + } /* query_block */ +}`, + `{ + "query_block": { + "select_id": 1, + "cost_info": { + "query_cost": "10.41" + } /* cost_info */, + "grouping_operation": { + "using_temporary_table": true, + "using_filesort": true, + "cost_info": { + "sort_cost": "7.00" + } /* cost_info */, + "table": { + "table_name": "t1", + "access_type": "ALL", + "rows_examined_per_scan": 7, + "rows_produced_per_join": 7, + "filtered": "100.00", + "cost_info": { + "read_cost": "2.01", + "eval_cost": "1.40", + "prefix_cost": "3.41", + "data_read_per_join": "56" + } /* cost_info */, + "used_columns": [ + "i" + ] /* used_columns */ + } /* table */, + "group_by_subqueries": [ + { + "dependent": true, + "cacheable": false, + "query_block": { + "select_id": 3, + "cost_info": { + "query_cost": "2.40" + } /* cost_info */, + "table": { + "table_name": "t2", + "access_type": "ALL", + "rows_examined_per_scan": 2, + "rows_produced_per_join": 2, + "filtered": "100.00", + "cost_info": { + "read_cost": "2.00", + "eval_cost": "0.40", + "prefix_cost": "2.40", + "data_read_per_join": "16" + } /* cost_info */, + "used_columns": [ + "i" + ] /* used_columns */, + "attached_condition": "(outer_field_is_not_null, (((test.t1.i) >= test.t2.i) or isnull(test.t2.i)), true)" + } /* table */ + } /* query_block */ + }, + { + "dependent": true, + "cacheable": false, + "query_block": { + "select_id": 2, + "cost_info": { + "query_cost": "2.40" + } /* cost_info */, + "table": { + "table_name": "t2", + "access_type": "ALL", + "rows_examined_per_scan": 2, + "rows_produced_per_join": 2, + "filtered": "100.00", + "cost_info": { + "read_cost": "2.00", + "eval_cost": "0.40", + "prefix_cost": "2.40", + "data_read_per_join": "16" + } /* cost_info */, + "used_columns": [ + "i" + ] /* used_columns */, + "attached_condition": "(outer_field_is_not_null, (((test.t1.i) <= test.t2.i) or isnull(test.t2.i)), true)" + } /* table */ + } /* query_block */ + } + ] /* group_by_subqueries */ + } /* grouping_operation */ + } /* query_block */ +}`, + `{ + "query_block": { + "select_id": 1, + "cost_info": { + "query_cost": "3.41" + } /* cost_info */, + "table": { + "table_name": "t1", + "access_type": "ALL", + "rows_examined_per_scan": 7, + "rows_produced_per_join": 7, + "filtered": "100.00", + "cost_info": { + "read_cost": "2.01", + "eval_cost": "1.40", + "prefix_cost": "3.41", + "data_read_per_join": "56" + } /* cost_info */, + "used_columns": [ + "i" + ] /* used_columns */ + } /* table */, + "select_list_subqueries": [ + { + "dependent": false, + "cacheable": false, + "query_block": { + "select_id": 2, + "cost_info": { + "query_cost": "3.41" + } /* cost_info */, + "ordering_operation": { + "using_temporary_table": true, + "using_filesort": true, + "table": { + "table_name": "t1", + "access_type": "ALL", + "rows_examined_per_scan": 7, + "rows_produced_per_join": 7, + "filtered": "100.00", + "cost_info": { + "read_cost": "2.01", + "eval_cost": "1.40", + "prefix_cost": "3.41", + "data_read_per_join": "56" + } /* cost_info */, + "used_columns": [ + "i" + ] /* used_columns */ + } /* table */ + } /* ordering_operation */ + } /* query_block */ + } + ] /* select_list_subqueries */ + } /* query_block */ +}`, + `{ + "query_block": { + "select_id": 1, + "message": "no matching row in const table" + } /* query_block */ +}`, + `{ + "query_block": { + "select_id": 1, + "cost_info": { + "query_cost": "5.21" + } /* cost_info */, + "nested_loop": [ + { + "table": { + "table_name": "t1", + "access_type": "ALL", + "rows_examined_per_scan": 2, + "rows_produced_per_join": 2, + "filtered": "100.00", + "cost_info": { + "read_cost": "2.00", + "eval_cost": "0.40", + "prefix_cost": "2.40", + "data_read_per_join": "32" + } /* cost_info */, + "used_columns": [ + "a", + "b" + ] /* used_columns */, + "attached_condition": "(((/* select#3 */ select test.t3.e from test.t3),(/* select#4 */ select 1 from test.t3 where (test.t1.b and (outer_field_is_not_null, ((((/* select#3 */ select test.t3.e from test.t3)) < test.t3.e) or isnull(test.t3.e)), true)) having (outer_field_is_not_null, (test.t3.e), true))))", + "attached_subqueries": [ + { + "dependent": true, + "cacheable": false, + "query_block": { + "select_id": 4, + "cost_info": { + "query_cost": "2.40" + } /* cost_info */, + "table": { + "table_name": "t3", + "access_type": "ALL", + "rows_examined_per_scan": 2, + "rows_produced_per_join": 2, + "filtered": "100.00", + "cost_info": { + "read_cost": "2.00", + "eval_cost": "0.40", + "prefix_cost": "2.40", + "data_read_per_join": "16" + } /* cost_info */, + "used_columns": [ + "e" + ] /* used_columns */, + "attached_condition": "(test.t1.b and (outer_field_is_not_null, ((((/* select#3 */ select test.t3.e from test.t3)) < test.t3.e) or isnull(test.t3.e)), true))" + } /* table */ + } /* query_block */ + }, + { + "dependent": false, + "cacheable": true, + "query_block": { + "select_id": 3, + "cost_info": { + "query_cost": "2.40" + } /* cost_info */, + "table": { + "table_name": "t3", + "access_type": "ALL", + "rows_examined_per_scan": 2, + "rows_produced_per_join": 2, + "filtered": "100.00", + "cost_info": { + "read_cost": "2.00", + "eval_cost": "0.40", + "prefix_cost": "2.40", + "data_read_per_join": "16" + } /* cost_info */, + "used_columns": [ + "e" + ] /* used_columns */ + } /* table */ + } /* query_block */ + } + ] /* attached_subqueries */ + } /* table */ + }, + { + "table": { + "table_name": "t2", + "access_type": "ALL", + "rows_examined_per_scan": 2, + "rows_produced_per_join": 2, + "filtered": "50.00", + "first_match": "t1", + "using_join_buffer": "Block Nested Loop", + "cost_info": { + "read_cost": "2.00", + "eval_cost": "0.40", + "prefix_cost": "5.21", + "data_read_per_join": "32" + } /* cost_info */, + "used_columns": [ + "c" + ] /* used_columns */, + "attached_condition": "(test.t2.c = test.t1.a)" + } /* table */ + } + ] /* nested_loop */ + } /* query_block */ +}`, + `{ + "query_block": { + "select_id": 1, + "cost_info": { + "query_cost": "35.44" + } /* cost_info */, + "nested_loop": [ + { + "table": { + "table_name": "t1", + "access_type": "ALL", + "rows_examined_per_scan": 12, + "rows_produced_per_join": 12, + "filtered": "100.00", + "cost_info": { + "read_cost": "2.02", + "eval_cost": "2.40", + "prefix_cost": "4.42", + "data_read_per_join": "96" + } /* cost_info */, + "used_columns": [ + "a" + ] /* used_columns */, + "attached_condition": "((test.t1.a is not null) and (test.t1.a is not null))" + } /* table */ + }, + { + "table": { + "table_name": "", + "access_type": "eq_ref", + "key": "", + "key_length": "5", + "ref": [ + "test.t1.a" + ] /* ref */, + "rows_examined_per_scan": 1, + "materialized_from_subquery": { + "using_temporary_table": true, + "query_block": { + "nested_loop": [ + { + "table": { + "table_name": "t4", + "access_type": "ALL", + "rows_examined_per_scan": 12, + "rows_produced_per_join": 3, + "filtered": "33.33", + "cost_info": { + "read_cost": "3.62", + "eval_cost": "0.80", + "prefix_cost": "4.42", + "data_read_per_join": "31" + } /* cost_info */, + "used_columns": [ + "a" + ] /* used_columns */, + "attached_condition": "(test.t4.a > 0)" + } /* table */ + }, + { + "table": { + "table_name": "t3", + "access_type": "ALL", + "rows_examined_per_scan": 12, + "rows_produced_per_join": 4, + "filtered": "10.00", + "using_join_buffer": "Block Nested Loop", + "cost_info": { + "read_cost": "2.02", + "eval_cost": "0.96", + "prefix_cost": "16.04", + "data_read_per_join": "38" + } /* cost_info */, + "used_columns": [ + "a" + ] /* used_columns */, + "attached_condition": "(test.t3.a = test.t4.a)" + } /* table */ + } + ] /* nested_loop */ + } /* query_block */ + } /* materialized_from_subquery */ + } /* table */ + }, + { + "table": { + "table_name": "", + "access_type": "eq_ref", + "key": "", + "key_length": "5", + "ref": [ + "test.t1.a" + ] /* ref */, + "rows_examined_per_scan": 1, + "materialized_from_subquery": { + "using_temporary_table": true, + "query_block": { + "table": { + "table_name": "t2", + "access_type": "ALL", + "rows_examined_per_scan": 12, + "rows_produced_per_join": 3, + "filtered": "33.33", + "cost_info": { + "read_cost": "3.62", + "eval_cost": "0.80", + "prefix_cost": "4.42", + "data_read_per_join": "31" + } /* cost_info */, + "used_columns": [ + "a" + ] /* used_columns */, + "attached_condition": "(test.t2.a > 0)" + } /* table */ + } /* query_block */ + } /* materialized_from_subquery */ + } /* table */ + } + ] /* nested_loop */ + } /* query_block */ +}`, + `{ + "query_block": { + "select_id": 1, + "cost_info": { + "query_cost": "1.20" + } /* cost_info */, + "table": { + "table_name": "t1", + "access_type": "ALL", + "rows_examined_per_scan": 1, + "rows_produced_per_join": 1, + "filtered": "100.00", + "cost_info": { + "read_cost": "1.00", + "eval_cost": "0.20", + "prefix_cost": "1.20", + "data_read_per_join": "8" + } /* cost_info */, + "used_columns": [ + "i1", + "c1" + ] /* used_columns */, + "attached_condition": "exists(/* select#2 */ select test.t2.c1 from test.t2 join test.t3 where ((test.t2.c1 = test.t3.c1) and (test.t2.c2 = (/* select#3 */ select min(test.t3.c1) from test.t3)) and ((/* select#3 */ select min(test.t3.c1) from test.t3) <> test.t1.c1)))", + "attached_subqueries": [ + { + "dependent": true, + "cacheable": false, + "query_block": { + "select_id": 2, + "cost_info": { + "query_cost": "2.40" + } /* cost_info */, + "nested_loop": [ + { + "table": { + "table_name": "t3", + "access_type": "ALL", + "rows_examined_per_scan": 1, + "rows_produced_per_join": 1, + "filtered": "100.00", + "cost_info": { + "read_cost": "1.00", + "eval_cost": "0.20", + "prefix_cost": "1.20", + "data_read_per_join": "8" + } /* cost_info */, + "used_columns": [ + "c1" + ] /* used_columns */, + "attached_condition": "((/* select#3 */ select min(test.t3.c1) from test.t3) <> test.t1.c1)", + "attached_subqueries": [ + { + "dependent": false, + "cacheable": true, + "query_block": { + "select_id": 3, + "cost_info": { + "query_cost": "1.20" + } /* cost_info */, + "table": { + "table_name": "t3", + "access_type": "ALL", + "rows_examined_per_scan": 1, + "rows_produced_per_join": 1, + "filtered": "100.00", + "cost_info": { + "read_cost": "1.00", + "eval_cost": "0.20", + "prefix_cost": "1.20", + "data_read_per_join": "8" + } /* cost_info */, + "used_columns": [ + "c1" + ] /* used_columns */ + } /* table */ + } /* query_block */ + } + ] /* attached_subqueries */ + } /* table */ + }, + { + "table": { + "table_name": "t2", + "access_type": "ref", + "possible_keys": [ + "c1" + ] /* possible_keys */, + "key": "c1", + "used_key_parts": [ + "c1" + ] /* used_key_parts */, + "key_length": "3", + "ref": [ + "test.t3.c1" + ] /* ref */, + "rows_examined_per_scan": 1, + "rows_produced_per_join": 0, + "filtered": "50.00", + "cost_info": { + "read_cost": "1.00", + "eval_cost": "0.10", + "prefix_cost": "2.40", + "data_read_per_join": "8" + } /* cost_info */, + "used_columns": [ + "c1", + "c2" + ] /* used_columns */, + "attached_condition": "(test.t2.c2 = (/* select#3 */ select min(test.t3.c1) from test.t3))", + "attached_subqueries": [ + { + "dependent": false, + "cacheable": true, + "query_block": { + "select_id": 3, + "cost_info": { + "query_cost": "1.20" + } /* cost_info */, + "table": { + "table_name": "t3", + "access_type": "ALL", + "rows_examined_per_scan": 1, + "rows_produced_per_join": 1, + "filtered": "100.00", + "cost_info": { + "read_cost": "1.00", + "eval_cost": "0.20", + "prefix_cost": "1.20", + "data_read_per_join": "8" + } /* cost_info */, + "used_columns": [ + "c1" + ] /* used_columns */ + } /* table */ + } /* query_block */ + } + ] /* attached_subqueries */ + } /* table */ + } + ] /* nested_loop */ + } /* query_block */ + } + ] /* attached_subqueries */ + } /* table */ + } /* query_block */ +}`, + `{ + "query_block": { + "select_id": 1, + "cost_info": { + "query_cost": "20.82" + } /* cost_info */, + "duplicates_removal": { + "using_temporary_table": true, + "nested_loop": [ + { + "table": { + "table_name": "t5", + "access_type": "ALL", + "rows_examined_per_scan": 3, + "rows_produced_per_join": 3, + "filtered": "100.00", + "cost_info": { + "read_cost": "2.01", + "eval_cost": "0.60", + "prefix_cost": "2.61", + "data_read_per_join": "24" + } /* cost_info */, + "used_columns": [ + "c" + ] /* used_columns */ + } /* table */ + }, + { + "table": { + "table_name": "t2", + "access_type": "ALL", + "rows_examined_per_scan": 3, + "rows_produced_per_join": 3, + "filtered": "33.33", + "using_join_buffer": "Block Nested Loop", + "cost_info": { + "read_cost": "2.01", + "eval_cost": "0.60", + "prefix_cost": "6.41", + "data_read_per_join": "48" + } /* cost_info */, + "used_columns": [ + "c", + "c_key" + ] /* used_columns */, + "attached_condition": "(test.t2.c = test.t5.c)" + } /* table */ + }, + { + "table": { + "table_name": "t1", + "access_type": "index", + "possible_keys": [ + "c_key" + ] /* possible_keys */, + "key": "c_key", + "used_key_parts": [ + "c_key" + ] /* used_key_parts */, + "key_length": "5", + "rows_examined_per_scan": 3, + "rows_produced_per_join": 3, + "filtered": "33.33", + "using_index": true, + "using_join_buffer": "Block Nested Loop", + "cost_info": { + "read_cost": "2.01", + "eval_cost": "0.60", + "prefix_cost": "12.22", + "data_read_per_join": "24" + } /* cost_info */, + "used_columns": [ + "c_key" + ] /* used_columns */, + "attached_condition": "(test.t1.c_key = test.t2.c_key)" + } /* table */ + }, + { + "table": { + "table_name": "t4", + "access_type": "ALL", + "rows_examined_per_scan": 3, + "rows_produced_per_join": 3, + "filtered": "33.33", + "using_join_buffer": "Block Nested Loop", + "cost_info": { + "read_cost": "2.01", + "eval_cost": "0.60", + "prefix_cost": "16.02", + "data_read_per_join": "48" + } /* cost_info */, + "used_columns": [ + "c", + "c_key" + ] /* used_columns */, + "attached_condition": "((test.t4.c = test.t5.c) and (test.t4.c_key is not null))" + } /* table */ + }, + { + "table": { + "table_name": "t3", + "access_type": "ref", + "possible_keys": [ + "c_key" + ] /* possible_keys */, + "key": "c_key", + "used_key_parts": [ + "c_key" + ] /* used_key_parts */, + "key_length": "5", + "ref": [ + "test.t4.c_key" + ] /* ref */, + "rows_examined_per_scan": 1, + "rows_produced_per_join": 3, + "filtered": "100.00", + "using_index": true, + "cost_info": { + "read_cost": "3.00", + "eval_cost": "0.60", + "prefix_cost": "20.82", + "data_read_per_join": "24" + } /* cost_info */, + "used_columns": [ + "c_key" + ] /* used_columns */ + } /* table */ + } + ] /* nested_loop */ + } /* duplicates_removal */ + } /* query_block */ +}`, + `{ + "query_block": { + "select_id": 1, + "table": { + "update": true, + "table_name": "t1", + "access_type": "ALL", + "rows_examined_per_scan": 1, + "filtered": "100.00" + } /* table */, + "update_value_subqueries": [ + { + "dependent": false, + "cacheable": true, + "query_block": { + "select_id": 2, + "cost_info": { + "query_cost": "1.00" + } /* cost_info */, + "table": { + "table_name": "t2", + "access_type": "system", + "rows_examined_per_scan": 1, + "rows_produced_per_join": 1, + "filtered": "100.00", + "cost_info": { + "read_cost": "0.00", + "eval_cost": "0.20", + "prefix_cost": "0.00", + "data_read_per_join": "8" + } /* cost_info */, + "used_columns": [ + "i" + ] /* used_columns */ + } /* table */ + } /* query_block */ + } + ] /* update_value_subqueries */ + } /* query_block */ +}`, + `{ + "query_block": { + "select_id": 1, + "cost_info": { + "query_cost": "1.00" + } /* cost_info */, + "nested_loop": [ + { + "table": { + "update": true, + "table_name": "t1", + "access_type": "system", + "rows_examined_per_scan": 1, + "rows_produced_per_join": 1, + "filtered": "100.00", + "cost_info": { + "read_cost": "0.00", + "eval_cost": "0.20", + "prefix_cost": "0.00", + "data_read_per_join": "8" + } /* cost_info */, + "used_columns": [ + "i" + ] /* used_columns */ + } /* table */ + }, + { + "table": { + "table_name": "t2", + "access_type": "system", + "rows_examined_per_scan": 1, + "rows_produced_per_join": 1, + "filtered": "100.00", + "cost_info": { + "read_cost": "0.00", + "eval_cost": "0.20", + "prefix_cost": "0.00", + "data_read_per_join": "8" + } /* cost_info */ + } /* table */ + } + ] /* nested_loop */, + "update_value_subqueries": [ + { + "dependent": false, + "cacheable": true, + "query_block": { + "select_id": 2, + "cost_info": { + "query_cost": "1.00" + } /* cost_info */, + "table": { + "table_name": "t3", + "access_type": "system", + "rows_examined_per_scan": 1, + "rows_produced_per_join": 1, + "filtered": "100.00", + "cost_info": { + "read_cost": "0.00", + "eval_cost": "0.20", + "prefix_cost": "0.00", + "data_read_per_join": "8" + } /* cost_info */, + "used_columns": [ + "i" + ] /* used_columns */ + } /* table */ + } /* query_block */ + } + ] /* update_value_subqueries */ + } /* query_block */ +}`, + `{ + "query_block": { + "select_id": 1, + "cost_info": { + "query_cost": "1.00" + } /* cost_info */, + "table": { + "insert": true, + "table_name": "t1", + "access_type": "ALL" + } /* table */, + "insert_from": { + "table": { + "table_name": "t2", + "access_type": "system", + "rows_examined_per_scan": 1, + "rows_produced_per_join": 1, + "filtered": "100.00", + "cost_info": { + "read_cost": "0.00", + "eval_cost": "0.20", + "prefix_cost": "0.00", + "data_read_per_join": "8" + } /* cost_info */, + "used_columns": [ + "i" + ] /* used_columns */ + } /* table */ + } /* insert_from */, + "update_value_subqueries": [ + { + "dependent": false, + "cacheable": true, + "query_block": { + "select_id": 2, + "cost_info": { + "query_cost": "1.00" + } /* cost_info */, + "table": { + "table_name": "t2", + "access_type": "system", + "rows_examined_per_scan": 1, + "rows_produced_per_join": 1, + "filtered": "100.00", + "cost_info": { + "read_cost": "0.00", + "eval_cost": "0.20", + "prefix_cost": "0.00", + "data_read_per_join": "8" + } /* cost_info */, + "used_columns": [ + "i" + ] /* used_columns */ + } /* table */ + } /* query_block */ + } + ] /* update_value_subqueries */ + } /* query_block */ +}`, + `{ + "query_block": { + "select_id": 1, + "table": { + "insert": true, + "table_name": "t1", + "access_type": "ALL" + } /* table */, + "update_value_subqueries": [ + { + "dependent": false, + "cacheable": true, + "query_block": { + "select_id": 2, + "cost_info": { + "query_cost": "1.00" + } /* cost_info */, + "table": { + "table_name": "t2", + "access_type": "system", + "rows_examined_per_scan": 1, + "rows_produced_per_join": 1, + "filtered": "100.00", + "cost_info": { + "read_cost": "0.00", + "eval_cost": "0.20", + "prefix_cost": "0.00", + "data_read_per_join": "8" + } /* cost_info */, + "used_columns": [ + "i" + ] /* used_columns */ + } /* table */ + } /* query_block */ + } + ] /* update_value_subqueries */ + } /* query_block */ +}`, + `{ + "query_block": { + "select_id": 1, + "table": { + "insert": true, + "table_name": "t3", + "access_type": "ALL" + } /* table */, + "optimized_away_subqueries": [ + { + "dependent": false, + "cacheable": true, + "query_block": { + "select_id": 3, + "cost_info": { + "query_cost": "1.00" + } /* cost_info */, + "table": { + "table_name": "t2", + "access_type": "system", + "rows_examined_per_scan": 1, + "rows_produced_per_join": 1, + "filtered": "100.00", + "cost_info": { + "read_cost": "0.00", + "eval_cost": "0.20", + "prefix_cost": "0.00", + "data_read_per_join": "8" + } /* cost_info */, + "used_columns": [ + "i" + ] /* used_columns */ + } /* table */ + } /* query_block */ + }, + { + "dependent": false, + "cacheable": true, + "query_block": { + "select_id": 2, + "cost_info": { + "query_cost": "1.00" + } /* cost_info */, + "table": { + "table_name": "t1", + "access_type": "system", + "rows_examined_per_scan": 1, + "rows_produced_per_join": 1, + "filtered": "100.00", + "cost_info": { + "read_cost": "0.00", + "eval_cost": "0.20", + "prefix_cost": "0.00", + "data_read_per_join": "8" + } /* cost_info */, + "used_columns": [ + "i" + ] /* used_columns */ + } /* table */ + } /* query_block */ + } + ] /* optimized_away_subqueries */ + } /* query_block */ +}`, + `{ + "query_block": { + "select_id": 1, + "cost_info": { + "query_cost": "10.50" + } /* cost_info */, + "ordering_operation": { + "using_filesort": true, + "grouping_operation": { + "using_temporary_table": true, + "using_filesort": false, + "table": { + "table_name": "t1", + "access_type": "ALL", + "rows_examined_per_scan": 2, + "rows_produced_per_join": 2, + "filtered": "100.00", + "cost_info": { + "read_cost": "10.10", + "eval_cost": "0.40", + "prefix_cost": "10.50", + "data_read_per_join": "48" + } /* cost_info */, + "used_columns": [ + "a", + "b" + ] /* used_columns */, + "materialized_from_subquery": { + "using_temporary_table": true, + "dependent": false, + "cacheable": true, + "query_block": { + "union_result": { + "using_temporary_table": false, + "query_specifications": [ + { + "dependent": false, + "cacheable": true, + "query_block": { + "select_id": 2, + "message": "No tables used" + } /* query_block */ + }, + { + "dependent": false, + "cacheable": true, + "query_block": { + "select_id": 3, + "message": "No tables used" + } /* query_block */ + } + ] /* query_specifications */ + } /* union_result */ + } /* query_block */ + } /* materialized_from_subquery */ + } /* table */ + } /* grouping_operation */ + } /* ordering_operation */ + } /* query_block */ +}`, + `{ + "query_block": { + "select_id": 1, + "cost_info": { + "query_cost": "4.40" + } /* cost_info */, + "grouping_operation": { + "using_temporary_table": true, + "using_filesort": true, + "cost_info": { + "sort_cost": "2.00" + } /* cost_info */, + "table": { + "table_name": "t1", + "access_type": "ALL", + "rows_examined_per_scan": 2, + "rows_produced_per_join": 2, + "filtered": "100.00", + "cost_info": { + "read_cost": "2.00", + "eval_cost": "0.40", + "prefix_cost": "2.40", + "data_read_per_join": "32" + } /* cost_info */, + "used_columns": [ + "a" + ] /* used_columns */ + } /* table */, + "group_by_subqueries": [ + { + "dependent": false, + "cacheable": true, + "query_block": { + "select_id": 2, + "cost_info": { + "query_cost": "1.00" + } /* cost_info */, + "table": { + "table_name": "d", + "access_type": "system", + "rows_examined_per_scan": 1, + "rows_produced_per_join": 1, + "filtered": "100.00", + "cost_info": { + "read_cost": "0.00", + "eval_cost": "0.20", + "prefix_cost": "0.00", + "data_read_per_join": "16" + } /* cost_info */, + "used_columns": [ + "b" + ] /* used_columns */, + "materialized_from_subquery": { + "using_temporary_table": true, + "dependent": false, + "cacheable": true, + "query_block": { + "select_id": 3, + "cost_info": { + "query_cost": "5.21" + } /* cost_info */, + "ordering_operation": { + "using_temporary_table": true, + "using_filesort": true, + "nested_loop": [ + { + "table": { + "table_name": "t1", + "access_type": "ALL", + "rows_examined_per_scan": 2, + "rows_produced_per_join": 2, + "filtered": "100.00", + "cost_info": { + "read_cost": "2.00", + "eval_cost": "0.40", + "prefix_cost": "2.40", + "data_read_per_join": "32" + } /* cost_info */, + "used_columns": [ + "a", + "b" + ] /* used_columns */ + } /* table */ + }, + { + "table": { + "table_name": "t2", + "access_type": "ALL", + "rows_examined_per_scan": 2, + "rows_produced_per_join": 4, + "filtered": "100.00", + "using_join_buffer": "Block Nested Loop", + "cost_info": { + "read_cost": "2.00", + "eval_cost": "0.80", + "prefix_cost": "5.21", + "data_read_per_join": "64" + } /* cost_info */ + } /* table */ + } + ] /* nested_loop */ + } /* ordering_operation */ + } /* query_block */ + } /* materialized_from_subquery */ + } /* table */ + } /* query_block */ + } + ] /* group_by_subqueries */ + } /* grouping_operation */ + } /* query_block */ +}`, + `{ + "query_block": { + "select_id": 1, + "cost_info": { + "query_cost": "2.40" + } /* cost_info */, + "table": { + "table_name": "t1", + "access_type": "ALL", + "rows_examined_per_scan": 2, + "rows_produced_per_join": 2, + "filtered": "100.00", + "cost_info": { + "read_cost": "2.00", + "eval_cost": "0.40", + "prefix_cost": "2.40", + "data_read_per_join": "16" + } /* cost_info */ + } /* table */, + "optimized_away_subqueries": [ + { + "dependent": false, + "cacheable": true, + "query_block": { + "select_id": 3, + "cost_info": { + "query_cost": "4.40" + } /* cost_info */, + "grouping_operation": { + "using_temporary_table": true, + "using_filesort": true, + "cost_info": { + "sort_cost": "2.00" + } /* cost_info */, + "table": { + "table_name": "t1", + "access_type": "ALL", + "rows_examined_per_scan": 2, + "rows_produced_per_join": 2, + "filtered": "100.00", + "cost_info": { + "read_cost": "2.00", + "eval_cost": "0.40", + "prefix_cost": "2.40", + "data_read_per_join": "16" + } /* cost_info */, + "used_columns": [ + "f1" + ] /* used_columns */ + } /* table */ + } /* grouping_operation */ + } /* query_block */ + } + ] /* optimized_away_subqueries */ + } /* query_block */ +}`, + `{ + "query_block": { + "select_id": 1, + "cost_info": { + "query_cost": "4.02" + } /* cost_info */, + "ordering_operation": { + "using_filesort": true, + "table": { + "table_name": "t1", + "access_type": "ALL", + "rows_examined_per_scan": 10, + "rows_produced_per_join": 10, + "filtered": "100.00", + "cost_info": { + "read_cost": "2.02", + "eval_cost": "2.00", + "prefix_cost": "4.02", + "data_read_per_join": "80" + } /* cost_info */, + "used_columns": [ + "i" + ] /* used_columns */ + } /* table */, + "order_by_subqueries": [ + { + "dependent": true, + "cacheable": false, + "query_block": { + "select_id": 2, + "cost_info": { + "query_cost": "4.02" + } /* cost_info */, + "table": { + "table_name": "t2", + "access_type": "ALL", + "rows_examined_per_scan": 10, + "rows_produced_per_join": 1, + "filtered": "10.00", + "cost_info": { + "read_cost": "2.02", + "eval_cost": "0.20", + "prefix_cost": "4.02", + "data_read_per_join": "16" + } /* cost_info */, + "used_columns": [ + "i", + "j" + ] /* used_columns */, + "attached_condition": "(test.t2.i = test.t1.i)" + } /* table */ + } /* query_block */ + } + ] /* order_by_subqueries */ + } /* ordering_operation */ + } /* query_block */ +}`, + `{ + "query_block": { + "select_id": 1, + "cost_info": { + "query_cost": "4.02" + } /* cost_info */, + "grouping_operation": { + "using_temporary_table": true, + "using_filesort": true, + "table": { + "table_name": "t1", + "access_type": "ALL", + "rows_examined_per_scan": 10, + "rows_produced_per_join": 10, + "filtered": "100.00", + "cost_info": { + "read_cost": "2.02", + "eval_cost": "2.00", + "prefix_cost": "4.02", + "data_read_per_join": "80" + } /* cost_info */, + "used_columns": [ + "i" + ] /* used_columns */ + } /* table */, + "group_by_subqueries": [ + { + "dependent": true, + "cacheable": false, + "query_block": { + "select_id": 2, + "cost_info": { + "query_cost": "4.02" + } /* cost_info */, + "table": { + "table_name": "t2", + "access_type": "ALL", + "rows_examined_per_scan": 10, + "rows_produced_per_join": 1, + "filtered": "10.00", + "cost_info": { + "read_cost": "2.02", + "eval_cost": "0.20", + "prefix_cost": "4.02", + "data_read_per_join": "16" + } /* cost_info */, + "used_columns": [ + "i", + "j" + ] /* used_columns */, + "attached_condition": "(test.t2.i = test.t1.i)" + } /* table */ + } /* query_block */ + } + ] /* group_by_subqueries */ + } /* grouping_operation */ + } /* query_block */ +}`, + `{ + "query_block": { + "select_id": 1, + "cost_info": { + "query_cost": "6.50" + } /* cost_info */, + "ordering_operation": { + "using_temporary_table": true, + "using_filesort": true, + "grouping_operation": { + "using_filesort": false, + "table": { + "table_name": "t1", + "access_type": "range", + "possible_keys": [ + "k1" + ] /* possible_keys */, + "key": "k1", + "used_key_parts": [ + "a" + ] /* used_key_parts */, + "key_length": "4", + "rows_examined_per_scan": 11, + "rows_produced_per_join": 11, + "filtered": "100.00", + "using_index_for_group_by": true, + "cost_info": { + "read_cost": "4.30", + "eval_cost": "2.20", + "prefix_cost": "6.50", + "data_read_per_join": "176" + } /* cost_info */, + "used_columns": [ + "a", + "b" + ] /* used_columns */ + } /* table */ + } /* grouping_operation */ + } /* ordering_operation */ + } /* query_block */ +}`, + `{ + "query_block": { + "select_id": 1, + "cost_info": { + "query_cost": "6.20" + } /* cost_info */, + "ordering_operation": { + "using_temporary_table": true, + "using_filesort": true, + "grouping_operation": { + "using_filesort": true, + "nested_loop": [ + { + "table": { + "table_name": "t1", + "access_type": "ALL", + "possible_keys": [ + "PRIMARY" + ] /* possible_keys */, + "rows_examined_per_scan": 3, + "rows_produced_per_join": 3, + "filtered": "100.00", + "cost_info": { + "read_cost": "2.01", + "eval_cost": "0.60", + "prefix_cost": "2.61", + "data_read_per_join": "48" + } /* cost_info */, + "used_columns": [ + "a", + "b" + ] /* used_columns */ + } /* table */ + }, + { + "table": { + "table_name": "t2", + "access_type": "ref", + "possible_keys": [ + "PRIMARY" + ] /* possible_keys */, + "key": "PRIMARY", + "used_key_parts": [ + "a" + ] /* used_key_parts */, + "key_length": "4", + "ref": [ + "test.t1.a" + ] /* ref */, + "rows_examined_per_scan": 1, + "rows_produced_per_join": 3, + "filtered": "100.00", + "using_index": true, + "cost_info": { + "read_cost": "3.00", + "eval_cost": "0.60", + "prefix_cost": "6.21", + "data_read_per_join": "48" + } /* cost_info */, + "used_columns": [ + "a", + "b" + ] /* used_columns */ + } /* table */ + } + ] /* nested_loop */ + } /* grouping_operation */ + } /* ordering_operation */ + } /* query_block */ +}`, + `{ + "query_block": { + "select_id": 1, + "cost_info": { + "query_cost": "12.82" + } /* cost_info */, + "grouping_operation": { + "using_filesort": true, + "cost_info": { + "sort_cost": "9.00" + } /* cost_info */, + "table": { + "table_name": "t1", + "access_type": "ALL", + "rows_examined_per_scan": 9, + "rows_produced_per_join": 9, + "filtered": "100.00", + "cost_info": { + "read_cost": "2.02", + "eval_cost": "1.80", + "prefix_cost": "3.82", + "data_read_per_join": "144" + } /* cost_info */, + "used_columns": [ + "a", + "b" + ] /* used_columns */ + } /* table */ + } /* grouping_operation */ + } /* query_block */ +}`, + `{ + "query_block": { + "select_id": 1, + "cost_info": { + "query_cost": "3.01" + } /* cost_info */, + "ordering_operation": { + "using_filesort": true, + "duplicates_removal": { + "using_temporary_table": true, + "using_filesort": false, + "grouping_operation": { + "using_temporary_table": true, + "using_filesort": false, + "table": { + "table_name": "t1", + "access_type": "ALL", + "rows_examined_per_scan": 5, + "rows_produced_per_join": 5, + "filtered": "100.00", + "cost_info": { + "read_cost": "2.01", + "eval_cost": "1.00", + "prefix_cost": "3.01", + "data_read_per_join": "80" + } /* cost_info */, + "used_columns": [ + "a", + "b" + ] /* used_columns */ + } /* table */ + } /* grouping_operation */ + } /* duplicates_removal */ + } /* ordering_operation */ + } /* query_block */ +}`, + `{ + "query_block": { + "select_id": 1, + "cost_info": { + "query_cost": "2.40" + } /* cost_info */, + "ordering_operation": { + "using_filesort": false, + "duplicates_removal": { + "using_temporary_table": true, + "using_filesort": false, + "buffer_result": { + "using_temporary_table": true, + "nested_loop": [ + { + "table": { + "table_name": "t1", + "access_type": "system", + "rows_examined_per_scan": 1, + "rows_produced_per_join": 1, + "filtered": "100.00", + "cost_info": { + "read_cost": "0.00", + "eval_cost": "0.20", + "prefix_cost": "0.00", + "data_read_per_join": "8" + } /* cost_info */, + "used_columns": [ + "a" + ] /* used_columns */ + } /* table */ + }, + { + "table": { + "table_name": "t2", + "access_type": "index", + "key": "PRIMARY", + "used_key_parts": [ + "a" + ] /* used_key_parts */, + "key_length": "4", + "rows_examined_per_scan": 2, + "rows_produced_per_join": 2, + "filtered": "100.00", + "using_index": true, + "distinct": true, + "cost_info": { + "read_cost": "2.00", + "eval_cost": "0.40", + "prefix_cost": "2.40", + "data_read_per_join": "16" + } /* cost_info */, + "used_columns": [ + "a" + ] /* used_columns */ + } /* table */ + } + ] /* nested_loop */ + } /* buffer_result */ + } /* duplicates_removal */ + } /* ordering_operation */ + } /* query_block */ +}`, + `{ + "query_block": { + "select_id": 1, + "cost_info": { + "query_cost": "6.41" + } /* cost_info */, + "nested_loop": [ + { + "table": { + "table_name": "t1", + "access_type": "ALL", + "possible_keys": [ + "PRIMARY" + ] /* possible_keys */, + "rows_examined_per_scan": 4, + "rows_produced_per_join": 3, + "filtered": "75.00", + "cost_info": { + "read_cost": "2.21", + "eval_cost": "0.60", + "prefix_cost": "2.81", + "data_read_per_join": "48" + } /* cost_info */, + "used_columns": [ + "a", + "b" + ] /* used_columns */, + "attached_condition": "(test.t1.b <> 30)" + } /* table */ + }, + { + "table": { + "table_name": "t2", + "access_type": "eq_ref", + "possible_keys": [ + "PRIMARY" + ] /* possible_keys */, + "key": "PRIMARY", + "used_key_parts": [ + "a" + ] /* used_key_parts */, + "key_length": "4", + "ref": [ + "test.t1.a" + ] /* ref */, + "rows_examined_per_scan": 1, + "rows_produced_per_join": 3, + "filtered": "100.00", + "using_index": true, + "cost_info": { + "read_cost": "3.00", + "eval_cost": "0.60", + "prefix_cost": "6.41", + "data_read_per_join": "24" + } /* cost_info */, + "used_columns": [ + "a" + ] /* used_columns */ + } /* table */ + } + ] /* nested_loop */ + } /* query_block */ +}`, +} + +func TestExplain(t *testing.T) { + for _, sql := range sqls { + exp, err := connTest.Explain(sql, TraditionalExplainType, TraditionalFormatExplain) + //exp, err := conn.Explain(sql, TraditionalExplainType, JSONFormatExplain) + fmt.Println("Old: ", sql) + fmt.Println("New: ", exp.SQL) + if err != nil { + fmt.Println(err) + } + pretty.Println(exp) + fmt.Println() + } +} + +func TestParseExplainText(t *testing.T) { + for _, content := range exp { + pretty.Println(string(RemoveSQLComments([]byte(content)))) + pretty.Println(ParseExplainText(content)) + } + /* + //length := len(exp) + pretty.Println(string(RemoveSQLComments([]byte(exp[9])))) + explainInfo, err := ParseExplainText(exp[9]) + pretty.Println(explainInfo) + fmt.Println(err) + */ +} + +func TestFindTablesInJson(t *testing.T) { + idx := 9 + for _, j := range exp[idx : idx+1] { + pretty.Println(j) + findTablesInJSON(j, 0) + } + pretty.Println(len(explainJSONTables), explainJSONTables) +} + +func TestFormatJsonIntoTraditional(t *testing.T) { + idx := 11 + for _, j := range exp[idx : idx+1] { + pretty.Println(j) + pretty.Println(FormatJSONIntoTraditional(j)) + } +} + +func TestPrintMarkdownExplainTable(t *testing.T) { + expInfo, err := connTest.Explain("select 1", TraditionalExplainType, TraditionalFormatExplain) + if err != nil { + t.Error(err) + } + err = common.GoldenDiff(func() { + PrintMarkdownExplainTable(expInfo) + }, t.Name(), update) + if err != nil { + t.Error(err) + } +} + +func TestExplainInfoTranslator(t *testing.T) { + expInfo, err := connTest.Explain("select 1", TraditionalExplainType, TraditionalFormatExplain) + if err != nil { + t.Error(err) + } + err = common.GoldenDiff(func() { + ExplainInfoTranslator(expInfo) + }, t.Name(), update) + if err != nil { + t.Error(err) + } +} + +func TestMySQLExplainWarnings(t *testing.T) { + expInfo, err := connTest.Explain("select 1", TraditionalExplainType, TraditionalFormatExplain) + if err != nil { + t.Error(err) + } + err = common.GoldenDiff(func() { + MySQLExplainWarnings(expInfo) + }, t.Name(), update) + if err != nil { + t.Error(err) + } +} + +func TestMySQLExplainQueryCost(t *testing.T) { + expInfo, err := connTest.Explain("select 1", TraditionalExplainType, TraditionalFormatExplain) + if err != nil { + t.Error(err) + } + err = common.GoldenDiff(func() { + MySQLExplainQueryCost(expInfo) + }, t.Name(), update) + if err != nil { + t.Error(err) + } +} + +func TestSupportExplainWrite(t *testing.T) { + _, err := connTest.supportExplainWrite() + if err != nil { + t.Error(err) + } +} diff --git a/database/mysql.go b/database/mysql.go new file mode 100644 index 00000000..8695453d --- /dev/null +++ b/database/mysql.go @@ -0,0 +1,310 @@ +/* + * Copyright 2018 Xiaomi, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package database + +import ( + "errors" + "fmt" + "io/ioutil" + "os" + "regexp" + "strconv" + "strings" + "time" + + "github.com/XiaoMi/soar/ast" + "github.com/XiaoMi/soar/common" + + "github.com/ziutek/mymysql/mysql" + // mymysql driver + _ "github.com/ziutek/mymysql/native" + "vitess.io/vitess/go/vt/sqlparser" +) + +// Connector 数据库连接基本对象 +type Connector struct { + Addr string + User string + Pass string + Database string + Charset string +} + +// QueryResult 数据库查询返回值 +type QueryResult struct { + Rows []mysql.Row + Result mysql.Result + Error error + Warning []mysql.Row + QueryCost float64 +} + +// NewConnection 创建新连接 +func (db *Connector) NewConnection() mysql.Conn { + return mysql.New("tcp", "", db.Addr, db.User, db.Pass, db.Database) +} + +// Query 执行SQL +func (db *Connector) Query(sql string, params ...interface{}) (*QueryResult, error) { + // 测试环境如果检查是关闭的,则SQL不会被执行 + if common.Config.TestDSN.Disable { + return nil, errors.New("TestDsn Disable") + } + + // 数据库安全性检查:如果Connector的IP端口与TEST环境不一致,则启用SQL白名单 + // 不在白名单中的SQL不允许执行 + // 执行环境与test环境不相同 + if db.Addr != common.Config.TestDSN.Addr && db.dangerousQuery(sql) { + return nil, fmt.Errorf("query execution deny: execute SQL with DSN(%s/%s) '%s'", + db.Addr, db.Database, fmt.Sprintf(sql, params...)) + } + + common.Log.Debug("Execute SQL with DSN(%s/%s) : %s", db.Addr, db.Database, fmt.Sprintf(sql, params...)) + conn := db.NewConnection() + + // 设置SQL连接超时时间 + conn.SetTimeout(time.Duration(common.Config.ConnTimeOut) * time.Second) + defer conn.Close() + err := conn.Connect() + if err != nil { + return nil, err + } + + // 添加SQL执行超时限制 + ch := make(chan QueryResult, 1) + go func() { + res := QueryResult{} + res.Rows, res.Result, res.Error = conn.Query(sql, params...) + + if common.Config.ShowWarnings { + warning, _, err := conn.Query("SHOW WARNINGS") + if err == nil { + res.Warning = warning + } + } + + // SHOW WARNINGS并不会影响last_query_cost + if common.Config.ShowLastQueryCost { + cost, _, err := conn.Query("SHOW SESSION STATUS LIKE 'last_query_cost'") + if err == nil { + if len(cost) > 0 { + res.QueryCost = cost[0].Float(1) + } + } + } + + ch <- res + }() + + select { + case res := <-ch: + return &res, res.Error + case <-time.After(time.Duration(common.Config.QueryTimeOut) * time.Second): + return nil, errors.New("query execution timeout") + } + +} + +// Version 获取MySQL数据库版本 +func (db *Connector) Version() (int, error) { + // 从数据库中获取版本信息 + res, err := db.Query("select @@version") + if err != nil { + common.Log.Warn("(db *Connector) Version() Error: %v", err) + return -1, err + } + + // 从MySQL版本中获取版本号 + var reg *regexp.Regexp + var v int + reg, err = regexp.Compile(`[^0-9]+`) + if err != nil { + // 如果获取不到version信息,则以最新版本为准 + v = 999 + return v, err + } + version := reg.ReplaceAllString(res.Rows[0].Str(0), "")[:3] + v, err = strconv.Atoi(version) + if err != nil { + // 如果获取不到version信息,则以最新版本为准 + v = 999 + } + return v, err +} + +// Source execute sql from file +func (db *Connector) Source(file string) ([]*QueryResult, error) { + var sqlCounter int // SQL 计数器 + var result []*QueryResult + + fd, err := os.Open(file) + defer func() { + err = fd.Close() + if err != nil { + common.Log.Error("(db *Connector) Source(%s) fd.Close failed: %s", file, err.Error()) + } + }() + if err != nil { + common.Log.Warning("(db *Connector) Source(%s) os.Open failed: %s", file, err.Error()) + return nil, err + } + data, err := ioutil.ReadAll(fd) + if err != nil { + common.Log.Critical("ioutil.ReadAll Error: %s", err.Error()) + return nil, err + } + + sql := strings.TrimSpace(string(data)) + buf := strings.TrimSpace(sql) + for ; ; sqlCounter++ { + if buf == "" { + break + } + + // 查询请求切分 + sql, bufBytes := ast.SplitStatement([]byte(buf), []byte(common.Config.Delimiter)) + buf = string(bufBytes) + sql = strings.TrimSpace(sql) + common.Log.Debug("Source Query SQL: %s", sql) + + res, e := db.Query(sql) + if e != nil { + common.Log.Error("(db *Connector) Source Filename: %s, SQLCounter.: %d", file, sqlCounter) + return result, e + } + result = append(result, res) + } + return result, nil +} + +// SingleIntValue 获取某个int型变量的值 +func (db *Connector) SingleIntValue(option string) (int, error) { + // 从数据库中获取信息 + res, err := db.Query("select @@%s", option) + if err != nil { + common.Log.Warn("(db *Connector) SingleIntValue() Error: %v", err) + return -1, err + } + + return res.Rows[0].Int(0), err +} + +// ColumnCardinality 粒度计算 +func (db *Connector) ColumnCardinality(tb, col string) float64 { + // 获取该表上的已有的索引 + + // show table status 获取总行数(近似) + tbStatus, err := db.ShowTableStatus(tb) + if err != nil { + common.Log.Warn("(db *Connector) ColumnCardinality() ShowTableStatus Error: %v", err) + return 0 + } + + // 如果是视图或表中无数据,rowTotal 都为0 + // 视图不需要加索引,无数据相当于散粒度为1 + if len(tbStatus.Rows) == 0 { + common.Log.Debug("(db *Connector) ColumnCardinality() No table status: %s", tb) + return 1 + } + rowTotal := tbStatus.Rows[0].Rows + if rowTotal == 0 { + if common.Config.Sampling { + common.Log.Debug("ColumnCardinality, %s rowTotal == 0", tb) + } + return 1 + } + + // rowTotal > xxx 时保护数据库,不对该值计算散粒度,xxx可以在配置中设置 + if rowTotal > common.Config.MaxTotalRows { + return 0.5 + } + + // 计算该列散粒度 + res, err := db.Query("select count(distinct `%s`) from `%s`.`%s`", col, db.Database, tb) + if err != nil { + common.Log.Warn("(db *Connector) ColumnCardinality() Query Error: %v", err) + return 0 + } + + colNum := res.Rows[0].Float(0) + + // 散粒度区间:[0,1] + return colNum / float64(rowTotal) +} + +// IsView 判断表是否是视图 +func (db *Connector) IsView(tbName string) bool { + tbStatus, err := db.ShowTableStatus(tbName) + if err != nil { + common.Log.Error("(db *Connector) IsView Error: %v:", err) + return false + } + + if len(tbStatus.Rows) > 0 { + if tbStatus.Rows[0].Comment == "VIEW" { + return true + } + } + + return false + +} + +// RemoveSQLComments 去除SQL中的注释 +func RemoveSQLComments(sql []byte) []byte { + cmtReg := regexp.MustCompile(`("(""|[^"])*")|('(''|[^'])*')|(--[^\n\r]*)|(#.*)|(/\*([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*+/)`) + + return cmtReg.ReplaceAllFunc(sql, func(s []byte) []byte { + if (s[0] == '"' && s[len(s)-1] == '"') || + (s[0] == '\'' && s[len(s)-1] == '\'') || + (string(s[:3]) == "/*!") { + return s + } + return []byte("") + }) +} + +// 为了防止在Online环境进行误操作,通过dangerousQuery来判断能否在Online执行 +func (db *Connector) dangerousQuery(query string) bool { + queries, err := sqlparser.SplitStatementToPieces(strings.TrimSpace(strings.ToLower(query))) + if err != nil { + return true + } + + for _, sql := range queries { + dangerous := true + whiteList := []string{ + "select", + "show", + "explain", + "describe", + } + + for _, prefix := range whiteList { + if strings.HasPrefix(sql, prefix) { + dangerous = false + break + } + } + + if dangerous { + return true + } + } + + return false +} diff --git a/database/mysql_test.go b/database/mysql_test.go new file mode 100644 index 00000000..2914c889 --- /dev/null +++ b/database/mysql_test.go @@ -0,0 +1,90 @@ +/* + * Copyright 2018 Xiaomi, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package database + +import ( + "fmt" + "testing" + + "github.com/XiaoMi/soar/common" + "github.com/kr/pretty" +) + +// TODO: go test -race不通过待解决 +func TestQuery(t *testing.T) { + common.Config.QueryTimeOut = 1 + _, err := connTest.Query("select sleep(2)") + if err == nil { + t.Error("connTest.Query not timeout") + } +} + +func TestColumnCardinality(_ *testing.T) { + connTest.Database = "information_schema" + a := connTest.ColumnCardinality("TABLES", "TABLE_SCHEMA") + fmt.Println("TABLES.TABLE_SCHEMA:", a) +} + +func TestDangerousSQL(t *testing.T) { + testCase := map[string]bool{ + "select * from tb;delete from tb;": true, + "show database;": false, + "select * from t;": false, + "explain delete from t;": false, + } + + db := Connector{} + for sql, want := range testCase { + got := db.dangerousQuery(sql) + if got != want { + t.Errorf("SQL:%s got:%v want:%v", sql, got, want) + } + } +} + +func TestWarningsAndQueryCost(t *testing.T) { + common.Config.ShowWarnings = true + common.Config.ShowLastQueryCost = true + res, err := connTest.Query("explain select * from sakila.film") + if err != nil { + t.Error("Query Error: ", err) + } else { + for _, w := range res.Warning { + pretty.Println(w.Str(2)) + } + fmt.Println(res.QueryCost) + pretty.Println(err) + } +} + +func TestVersion(t *testing.T) { + version, err := connTest.Version() + if err != nil { + t.Error(err.Error()) + } + fmt.Println(version) +} + +func TestSource(t *testing.T) { + res, err := connTest.Source("testdata/" + t.Name() + ".sql") + if err != nil { + t.Error("Query Error: ", err) + } + if res[0].Rows[0].Int(0) != 1 || res[1].Rows[0].Int(0) != 1 { + t.Error("Source result not match, expect 1, 1") + } +} diff --git a/database/profiling.go b/database/profiling.go new file mode 100644 index 00000000..3de00dde --- /dev/null +++ b/database/profiling.go @@ -0,0 +1,135 @@ +/* + * Copyright 2018 Xiaomi, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package database + +import ( + "errors" + "fmt" + "io" + "strings" + "time" + + "github.com/XiaoMi/soar/common" + + "vitess.io/vitess/go/vt/sqlparser" +) + +// Profiling show profile输出的结果 +type Profiling struct { + Rows []ProfilingRow +} + +// ProfilingRow show profile每一行信息 +type ProfilingRow struct { + Status string + Duration float64 + // TODO: 支持show profile all,不过目前看all的信息过多有点眼花缭乱 +} + +// Profiling 执行SQL,并对其Profiling +func (db *Connector) Profiling(sql string, params ...interface{}) (*QueryResult, error) { + // 过滤不需要profiling的SQL + switch sqlparser.Preview(sql) { + case sqlparser.StmtSelect, sqlparser.StmtUpdate, sqlparser.StmtDelete: + default: + return nil, errors.New("no need profiling") + } + + // 测试环境如果检查是关闭的,则SQL不会被执行 + if common.Config.TestDSN.Disable { + return nil, errors.New("TestDsn Disable") + } + + // 数据库安全性检查:如果Connector的IP端口与TEST环境不一致,则启用SQL白名单 + // 不在白名单中的SQL不允许执行 + // 执行环境与test环境不相同 + if db.Addr != common.Config.TestDSN.Addr && db.dangerousQuery(sql) { + return nil, fmt.Errorf("query execution deny: Execute SQL with DSN(%s/%s) '%s'", + db.Addr, db.Database, fmt.Sprintf(sql, params...)) + } + + common.Log.Debug("Execute SQL with DSN(%s/%s) : %s", db.Addr, db.Database, sql) + conn := db.NewConnection() + + // 设置SQL连接超时时间 + conn.SetTimeout(time.Duration(common.Config.ConnTimeOut) * time.Second) + defer conn.Close() + err := conn.Connect() + if err != nil { + return nil, err + } + + // 添加SQL执行超时限制 + ch := make(chan QueryResult, 1) + go func() { + // 开启Profiling + _, _, err = conn.Query("set @@profiling=1") + common.LogIfError(err, "") + + // 执行SQL,抛弃返回结果 + result, err := conn.Start(sql, params...) + if err != nil { + ch <- QueryResult{ + Error: err, + } + return + } + row := result.MakeRow() + for { + err = result.ScanRow(row) + if err == io.EOF { + break + } + } + + // 返回Profiling结果 + res := QueryResult{} + res.Rows, res.Result, res.Error = conn.Query("show profile") + _, _, err = conn.Query("set @@profiling=0") + common.LogIfError(err, "") + ch <- res + }() + + select { + case res := <-ch: + return &res, res.Error + case <-time.After(time.Duration(common.Config.QueryTimeOut) * time.Second): + return nil, errors.New("query execution timeout") + } +} + +func getProfiling(res *QueryResult) Profiling { + var rows []ProfilingRow + for _, row := range res.Rows { + rows = append(rows, ProfilingRow{ + Status: row.Str(0), + Duration: row.Float(1), + }) + } + return Profiling{Rows: rows} +} + +// FormatProfiling 格式化输出Profiling信息 +func FormatProfiling(res *QueryResult) string { + profiling := getProfiling(res) + str := []string{"| Status | Duration |"} + str = append(str, "| --- | --- |") + for _, row := range profiling.Rows { + str = append(str, fmt.Sprintf("| %s | %f |", row.Status, row.Duration)) + } + return strings.Join(str, "\n") +} diff --git a/database/profiling_test.go b/database/profiling_test.go new file mode 100644 index 00000000..4b226ba9 --- /dev/null +++ b/database/profiling_test.go @@ -0,0 +1,53 @@ +/* + * Copyright 2018 Xiaomi, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package database + +import ( + "testing" + + "github.com/XiaoMi/soar/common" + + "github.com/kr/pretty" +) + +func TestProfiling(t *testing.T) { + common.Config.QueryTimeOut = 1 + res, err := connTest.Profiling("select 1") + if err == nil { + pretty.Println(res) + } else { + t.Error(err) + } +} + +func TestFormatProfiling(t *testing.T) { + res, err := connTest.Profiling("select 1") + if err == nil { + pretty.Println(FormatProfiling(res)) + } else { + t.Error(err) + } +} + +func TestGetProfiling(t *testing.T) { + res, err := connTest.Profiling("select 1") + if err == nil { + pretty.Println(getProfiling(res)) + } else { + t.Error(err) + } +} diff --git a/database/sampling.go b/database/sampling.go new file mode 100644 index 00000000..6190c18e --- /dev/null +++ b/database/sampling.go @@ -0,0 +1,230 @@ +/* + * Copyright 2018 Xiaomi, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package database + +import ( + "fmt" + "io" + "strconv" + "strings" + "time" + + "github.com/XiaoMi/soar/common" + "github.com/ziutek/mymysql/mysql" +) + +/*-------------------- +* The following choice of minrows is based on the paper +* "Random sampling for histogram construction: how much is enough?" +* by Surajit Chaudhuri, Rajeev Motwani and Vivek Narasayya, in +* Proceedings of ACM SIGMOD International Conference on Management +* of Data, 1998, Pages 436-447. Their Corollary 1 to Theorem 5 +* says that for table size n, histogram size k, maximum relative +* error in bin size f, and error probability gamma, the minimum +* random sample size is +* r = 4 * k * ln(2*n/gamma) / f^2 +* Taking f = 0.5, gamma = 0.01, n = 10^6 rows, we obtain +* r = 305.82 * k +* Note that because of the log function, the dependence on n is +* quite weak; even at n = 10^12, a 300*k sample gives <= 0.66 +* bin size error with probability 0.99. So there's no real need to +* scale for n, which is a good thing because we don't necessarily +* know it at this point. +*-------------------- + */ + +// SamplingData 将数据从Remote拉取到 db 中 +func (db *Connector) SamplingData(remote Connector, tables ...string) error { + // 计算需要泵取的数据量 + wantRowsCount := 300 * common.Config.SamplingStatisticTarget + + // 设置数据采样单条SQL中value的数量 + // 该数值越大,在内存中缓存的data就越多,但相对的,插入时速度就越快 + maxValCount := 200 + + // 获取数据库连接对象 + conn := remote.NewConnection() + localConn := db.NewConnection() + + // 连接数据库 + err := conn.Connect() + defer conn.Close() + if err != nil { + return err + } + + err = localConn.Connect() + defer localConn.Close() + if err != nil { + return err + } + + for _, table := range tables { + // 表类型检查 + if remote.IsView(table) { + return nil + } + + tableStatus, err := remote.ShowTableStatus(table) + if err != nil { + return err + } + + if len(tableStatus.Rows) == 0 { + common.Log.Info("SamplingData, Table %s with no data, stop sampling", table) + return nil + } + + tableRows := tableStatus.Rows[0].Rows + if tableRows == 0 { + common.Log.Info("SamplingData, Table %s with no data, stop sampling", table) + return nil + } + + factor := float64(wantRowsCount) / float64(tableRows) + common.Log.Debug("SamplingData, tableRows: %d, wantRowsCount: %d, factor: %f", tableRows, wantRowsCount, factor) + + err = startSampling(conn, localConn, db.Database, table, factor, wantRowsCount, maxValCount) + if err != nil { + common.Log.Error("(db *Connector) SamplingData Error : %v", err) + } + } + return nil +} + +// 开始从环境中泵取数据 +// 因为涉及到的数据量问题,所以泵取与插入时同时进行的 +// TODO 加 ref link +func startSampling(conn, localConn mysql.Conn, database, table string, factor float64, wants, maxValCount int) error { + // 从线上数据库获取所需dump的表中所有列的数据类型,备用 + // 由于测试库中的库表为刚建立的,所以在information_schema中很可能没有这个表的信息 + var dataTypes []string + q := fmt.Sprintf("select DATA_TYPE from information_schema.COLUMNS where TABLE_SCHEMA='%s' and TABLE_NAME = '%s'", + database, table) + common.Log.Debug("Sampling data execute: %s", q) + rs, _, err := localConn.Query(q) + if err != nil { + common.Log.Debug("Sampling data got data type Err: %v", err) + } else { + for _, r := range rs { + dataTypes = append(dataTypes, r.Str(0)) + } + } + + // 生成where条件 + where := fmt.Sprintf("where RAND()<=%f", factor) + if factor >= 1 { + where = "" + } + + sql := fmt.Sprintf("select * from `%s` %s limit %d;", table, where, wants) + res, err := conn.Start(sql) + if err != nil { + return err + } + + // GetRow method allocates a new chunk of memory for every received row. + row := res.MakeRow() + rowCount := 0 + valCount := 0 + + // 获取所有的列名 + columns := make([]string, len(res.Fields())) + for i, filed := range res.Fields() { + columns[i] = filed.Name + } + colDef := strings.Join(columns, ",") + + // 开始填充数据 + var valList []string + for { + err := res.ScanRow(row) + if err == io.EOF { + // 扫描结束 + if len(valList) > 0 { + // 如果缓存中还存在未插入的数据,则把缓存中的数据刷新到DB中 + doSampling(localConn, database, table, colDef, strings.Join(valList, ",")) + } + break + } + + if err != nil { + return err + } + + values := make([]string, len(columns)) + for i := range row { + // TODO 不支持坐标类型的导出 + switch data := row[i].(type) { + case nil: + // str = "" + case []byte: + // 先尝试转成数字,如果报错则转换成string + v, err := row.Int64Err(i) + values[i] = strconv.FormatInt(v, 10) + if err != nil { + values[i] = string(data) + } + case time.Time: + values[i] = mysql.TimeString(data) + case time.Duration: + values[i] = mysql.DurationString(data) + default: + values[i] = fmt.Sprint(data) + } + + // 非text/varchar类的数据类型,如果dump出的数据为空,则说明该值为null值 + // 应转换其value为null,如果用空('')进行替代,会导致出现语法错误。 + if len(dataTypes) == len(res.Fields()) && values[i] == "" && + (!strings.Contains(dataTypes[i], "char") || + !strings.Contains(dataTypes[i], "text")) { + values[i] = "null" + } else { + values[i] = "'" + values[i] + "'" + } + } + + valuesStr := fmt.Sprintf(`(%s)`, strings.Join(values, `,`)) + valList = append(valList, valuesStr) + + rowCount++ + valCount++ + + if rowCount%maxValCount == 0 { + doSampling(localConn, database, table, colDef, strings.Join(valList, ",")) + valCount = 0 + valList = make([]string, 0) + + } + } + + common.Log.Debug("%d rows sampling out", rowCount) + return nil +} + +// 将泵取的数据转换成Insert语句并在数据库中执行 +func doSampling(conn mysql.Conn, dbName, table, colDef, values string) { + sql := fmt.Sprintf("Insert into `%s`.`%s`(%s) values%s;", dbName, table, + colDef, values) + + _, _, err := conn.Query(sql) + + if err != nil { + common.Log.Error("doSampling Error from %s.%s: %v", dbName, table, err) + } + +} diff --git a/database/sampling_test.go b/database/sampling_test.go new file mode 100644 index 00000000..082d5e9f --- /dev/null +++ b/database/sampling_test.go @@ -0,0 +1,50 @@ +/* + * Copyright 2018 Xiaomi, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package database + +import ( + "testing" + + "github.com/XiaoMi/soar/common" +) + +func init() { + common.BaseDir = common.DevPath +} + +func TestSamplingData(t *testing.T) { + online := &Connector{ + Addr: common.Config.OnlineDSN.Addr, + User: common.Config.OnlineDSN.User, + Pass: common.Config.OnlineDSN.Password, + Database: common.Config.OnlineDSN.Schema, + } + + offline := &Connector{ + Addr: common.Config.TestDSN.Addr, + User: common.Config.TestDSN.User, + Pass: common.Config.TestDSN.Password, + Database: common.Config.TestDSN.Schema, + } + + offline.Database = "test" + + err := connTest.SamplingData(*online, "film") + if err != nil { + t.Error(err) + } +} diff --git a/database/show.go b/database/show.go new file mode 100644 index 00000000..40440189 --- /dev/null +++ b/database/show.go @@ -0,0 +1,584 @@ +/* + * Copyright 2018 Xiaomi, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package database + +import ( + "fmt" + "regexp" + "strconv" + "strings" + "time" + + "github.com/XiaoMi/soar/common" +) + +// SHOW TABLE STATUS Syntax +// https://dev.mysql.com/doc/refman/5.7/en/show-table-status.html + +// TableStatInfo 用以保存 show table status 之后获取的table信息 +type TableStatInfo struct { + Name string + Rows []tableStatusRow +} + +// tableStatusRow 用于 show table status value +type tableStatusRow struct { + Name string // 表名 + Engine string // 该表使用的存储引擎 + Version int // 该表的 .frm 文件版本号 + RowFormat string // 该表使用的行存储格式 + Rows int64 // 表行数,InnoDB 引擎中为预估值,甚至可能会有40%~50%的数值偏差 + AvgRowLength int // 平均行长度 + + // MyISAM: Data_length 为数据文件的大小,单位为 bytes + // InnoDB: Data_length 为聚簇索引分配的近似内存量,单位为 bytes, 计算方式为聚簇索引数量乘以 InnoDB 页面大小 + // 其他不同的存储引擎中该值的意义可能不尽相同 + DataLength int + + // MyISAM: Max_data_length 为数据文件长度的最大值。这是在给定使用的数据指针大小的情况下,可以存储在表中的数据的最大字节数 + // InnoDB: 未使用 + // 其他不同的存储引擎中该值的意义可能不尽相同 + MaxDataLength int + + // MyISAM: Index_length 为 index 文件的大小,单位为 bytes + // InnoDB: Index_length 为非聚簇索引分配的近似内存量,单位为 bytes,计算方式为非聚簇索引数量乘以 InnoDB 页面大小 + // 其他不同的存储引擎中该值的意义可能不尽相同 + IndexLength int + + DataFree int // 已分配但未使用的字节数 + AutoIncrement int // 下一个自增值 + CreateTime time.Time // 创建时间 + UpdateTime time.Time // 最近一次更新时间,该值不准确 + CheckTime time.Time // 上次检查时间 + Collation string // 字符集及排序规则信息 + Checksum string // 校验和 + CreateOptions string // 创建表的时候的时候一切其他属性 + Comment string // 注释 +} + +// newTableStat 构造 table Stat 对象 +func newTableStat(tableName string) *TableStatInfo { + return &TableStatInfo{ + Name: tableName, + Rows: make([]tableStatusRow, 0), + } +} + +// ShowTables 执行 show tables +func (db *Connector) ShowTables() ([]string, error) { + defer func() { + err := recover() + if err != nil { + common.Log.Error("recover ShowTableStatus()", err) + } + }() + + // 执行 show table status + res, err := db.Query("show tables") + if err != nil { + return []string{}, err + } + + // 获取值 + var tables []string + for _, row := range res.Rows { + tables = append(tables, row.Str(0)) + } + + return tables, err +} + +// ShowTableStatus 执行 show table status +func (db *Connector) ShowTableStatus(tableName string) (*TableStatInfo, error) { + defer func() { + err := recover() + if err != nil { + common.Log.Error("recover ShowTableStatus()", err) + } + }() + + // 初始化struct + ts := newTableStat(tableName) + + // 执行 show table status + res, err := db.Query("show table status where name = '%s'", ts.Name) + if err != nil { + return ts, err + } + + rs := res.Result.Map("Rows") + name := res.Result.Map("Name") + df := res.Result.Map("Data_free") + sum := res.Result.Map("Checksum") + engine := res.Result.Map("Engine") + version := res.Result.Map("Version") + comment := res.Result.Map("Comment") + ai := res.Result.Map("Auto_increment") + collation := res.Result.Map("Collation") + rowFormat := res.Result.Map("Row_format") + checkTime := res.Result.Map("Check_time") + dataLength := res.Result.Map("Data_length") + idxLength := res.Result.Map("Index_length") + createTime := res.Result.Map("Create_time") + updateTime := res.Result.Map("Update_time") + options := res.Result.Map("Create_options") + avgRowLength := res.Result.Map("Avg_row_length") + maxDataLength := res.Result.Map("Max_data_length") + + // 获取值 + for _, row := range res.Rows { + value := tableStatusRow{ + Name: row.Str(name), + Engine: row.Str(engine), + Version: row.Int(version), + Rows: row.Int64(rs), + RowFormat: row.Str(rowFormat), + AvgRowLength: row.Int(avgRowLength), + DataLength: row.Int(dataLength), + MaxDataLength: row.Int(maxDataLength), + IndexLength: row.Int(idxLength), + DataFree: row.Int(df), + AutoIncrement: row.Int(ai), + CreateTime: row.Time(createTime, time.Local), + UpdateTime: row.Time(updateTime, time.Local), + CheckTime: row.Time(checkTime, time.Local), + Collation: row.Str(collation), + Checksum: row.Str(sum), + CreateOptions: row.Str(options), + Comment: row.Str(comment), + } + ts.Rows = append(ts.Rows, value) + } + + return ts, err +} + +// https://dev.mysql.com/doc/refman/5.7/en/show-index.html + +// TableIndexInfo 用以保存 show index 之后获取的 index 信息 +type TableIndexInfo struct { + TableName string + IdxRows []TableIndexRow +} + +// TableIndexRow 用以存放show index之后获取的每一条index信息 +type TableIndexRow struct { + Table string // 表名 + NonUnique int // 0:unique key,1:not unique + KeyName string // index的名称,如果是主键则为 "PRIMARY" + SeqInIndex int // 该列在索引中的位置。计数从 1 开始 + ColumnName string // 列名 + Collation string // A or Null + Cardinality int // 索引中唯一值的数量,"ANALYZE TABLE" 可更新该值 + SubPart int // 索引前缀字节数 + Packed int + Null string // 表示该列是否可以为空,如果可以为 'YES',反之'' + IndexType string // BTREE, FULLTEXT, HASH, RTREE + Comment string + IndexComment string +} + +// NewTableIndexInfo 构造 TableIndexInfo +func NewTableIndexInfo(tableName string) *TableIndexInfo { + return &TableIndexInfo{ + TableName: tableName, + IdxRows: make([]TableIndexRow, 0), + } +} + +// ShowIndex show Index +func (db *Connector) ShowIndex(tableName string) (*TableIndexInfo, error) { + tbIndex := NewTableIndexInfo(tableName) + + // 执行 show create table + res, err := db.Query("show index from `%s`.`%s`", db.Database, tableName) + if err != nil { + return nil, err + } + + table := res.Result.Map("Table") + unique := res.Result.Map("Non_unique") + keyName := res.Result.Map("Key_name") + seq := res.Result.Map("Seq_in_index") + cName := res.Result.Map("Column_name") + collation := res.Result.Map("Collation") + cardinality := res.Result.Map("Cardinality") + subPart := res.Result.Map("Sub_part") + packed := res.Result.Map("Packed") + null := res.Result.Map("Null") + idxType := res.Result.Map("Index_type") + comment := res.Result.Map("Comment") + idxComment := res.Result.Map("Index_comment") + + // 获取值 + for _, row := range res.Rows { + value := TableIndexRow{ + Table: row.Str(table), + NonUnique: row.Int(unique), + KeyName: row.Str(keyName), + SeqInIndex: row.Int(seq), + ColumnName: row.Str(cName), + Collation: row.Str(collation), + Cardinality: row.Int(cardinality), + SubPart: row.Int(subPart), + Packed: row.Int(packed), + Null: row.Str(null), + IndexType: row.Str(idxType), + Comment: row.Str(comment), + IndexComment: row.Str(idxComment), + } + tbIndex.IdxRows = append(tbIndex.IdxRows, value) + } + return tbIndex, err +} + +// IndexSelectKey 用以对 TableIndexInfo 进行查询 +type IndexSelectKey string + +// 索引相关 +const ( + IndexKeyName = IndexSelectKey("KeyName") // 索引名称 + IndexColumnName = IndexSelectKey("ColumnName") // 索引列名称 + IndexIndexType = IndexSelectKey("IndexType") // 索引类型 + IndexNonUnique = IndexSelectKey("NonUnique") // 唯一索引 +) + +// FindIndex 获取TableIndexInfo中需要的索引 +func (tbIndex *TableIndexInfo) FindIndex(arg IndexSelectKey, value string) []TableIndexRow { + var result []TableIndexRow + if tbIndex == nil { + return result + } + + value = strings.ToLower(value) + + switch arg { + case IndexKeyName: + for _, index := range tbIndex.IdxRows { + if strings.ToLower(index.KeyName) == value { + result = append(result, index) + } + } + + case IndexColumnName: + for _, index := range tbIndex.IdxRows { + if strings.ToLower(index.ColumnName) == value { + result = append(result, index) + } + } + + case IndexIndexType: + for _, index := range tbIndex.IdxRows { + if strings.ToLower(index.IndexType) == value { + result = append(result, index) + } + } + + case IndexNonUnique: + for _, index := range tbIndex.IdxRows { + unique := strconv.Itoa(index.NonUnique) + if unique == value { + result = append(result, index) + } + } + + default: + common.Log.Error("no such args: TableIndexRow") + } + + return result +} + +// desc table +// https://dev.mysql.com/doc/refman/5.7/en/show-columns.html + +// TableDesc show columns from rental; +type TableDesc struct { + Name string + DescValues []TableDescValue +} + +// TableDescValue 含有每一列的属性 +type TableDescValue struct { + Field string // 列名 + Type string // 数据类型 + Null string // 是否有NULL(NO、YES) + Collation string // 字符集 + Privileges string // 权限s + Key string // 键类型 + Default string // 默认值 + Extra string // 其他 + Comment string // 备注 +} + +// NewTableDesc 初始化一个*TableDesc +func NewTableDesc(tableName string) *TableDesc { + return &TableDesc{ + Name: tableName, + DescValues: make([]TableDescValue, 0), + } +} + +// ShowColumns 获取DB中所有的columns +func (db *Connector) ShowColumns(tableName string) (*TableDesc, error) { + tbDesc := NewTableDesc(tableName) + + // 执行 show create table + res, err := db.Query("show full columns from `%s`.`%s`", db.Database, tableName) + if err != nil { + return nil, err + } + + field := res.Result.Map("Field") + tp := res.Result.Map("Type") + null := res.Result.Map("Null") + key := res.Result.Map("Key") + def := res.Result.Map("Default") + extra := res.Result.Map("Extra") + collation := res.Result.Map("Collation") + privileges := res.Result.Map("Privileges") + comm := res.Result.Map("Comment") + + // 获取值 + for _, row := range res.Rows { + value := TableDescValue{ + Field: row.Str(field), + Type: row.Str(tp), + Null: row.Str(null), + Key: row.Str(key), + Default: row.Str(def), + Extra: row.Str(extra), + Privileges: row.Str(privileges), + Collation: row.Str(collation), + Comment: row.Str(comm), + } + tbDesc.DescValues = append(tbDesc.DescValues, value) + } + return tbDesc, err +} + +// Columns 用于获取TableDesc中所有列的名称 +func (td TableDesc) Columns() []string { + var cols []string + for _, col := range td.DescValues { + cols = append(cols, col.Field) + } + return cols +} + +// showCreate show create +func (db *Connector) showCreate(createType, name string) (string, error) { + // 执行 show create table + res, err := db.Query("show create %s `%s`", createType, name) + if err != nil { + return "", err + } + + // 获取ddl + var ddl string + for _, row := range res.Rows { + ddl = row.Str(1) + } + + return ddl, err +} + +// ShowCreateDatabase show create database +func (db *Connector) ShowCreateDatabase(dbName string) (string, error) { + defer func() { + err := recover() + if err != nil { + common.Log.Error("recover ShowCreateDatabase()", err) + } + }() + return db.showCreate("database", dbName) +} + +// ShowCreateTable show create table +func (db *Connector) ShowCreateTable(tableName string) (string, error) { + defer func() { + err := recover() + if err != nil { + common.Log.Error("recover ShowCreateTable()", err) + } + }() + + ddl, err := db.showCreate("table", tableName) + + // 去除外键关联条件 + var noConstraint []string + relationReg, _ := regexp.Compile("CONSTRAINT") + for _, line := range strings.Split(ddl, "\n") { + + if relationReg.Match([]byte(line)) { + continue + } + + // 去除外键语句会使DDL中多一个','导致语法错误,要把多余的逗号去除 + if strings.Index(line, ")") == 0 { + lineWrongSyntax := noConstraint[len(noConstraint)-1] + // 如果')'前一句的末尾是',' 删除 ',' 保证语法正确性 + if strings.Index(lineWrongSyntax, ",") == len(lineWrongSyntax)-1 { + noConstraint[len(noConstraint)-1] = lineWrongSyntax[:len(lineWrongSyntax)-1] + } + } + + noConstraint = append(noConstraint, line) + } + + return strings.Join(noConstraint, "\n"), err +} + +// FindColumn find column +func (db *Connector) FindColumn(name, dbName string, tables ...string) ([]*common.Column, error) { + // 执行 show create table + var columns []*common.Column + sql := fmt.Sprintf("SELECT "+ + "c.TABLE_NAME,c.TABLE_SCHEMA,c.COLUMN_TYPE,c.CHARACTER_SET_NAME, c.COLLATION_NAME "+ + "FROM `INFORMATION_SCHEMA`.`COLUMNS` as c where c.COLUMN_NAME = '%s' ", name) + + if len(tables) > 0 { + var tmp []string + for _, table := range tables { + tmp = append(tmp, "'"+table+"'") + } + sql += fmt.Sprintf(" and c.table_name in (%s)", strings.Join(tmp, ",")) + } + + if dbName != "" { + sql += fmt.Sprintf(" and c.table_schema = '%s'", dbName) + } + + res, err := db.Query(sql) + if err != nil { + common.Log.Error("(db *Connector) FindColumn Error : ", err) + return columns, err + } + + tbName := res.Result.Map("TABLE_NAME") + schema := res.Result.Map("TABLE_SCHEMA") + colTyp := res.Result.Map("COLUMN_TYPE") + colCharset := res.Result.Map("CHARACTER_SET_NAME") + collation := res.Result.Map("COLLATION_NAME") + + // 获取ddl + for _, row := range res.Rows { + col := &common.Column{ + Name: name, + Table: row.Str(tbName), + DB: row.Str(schema), + DataType: row.Str(colTyp), + Character: row.Str(colCharset), + Collation: row.Str(collation), + } + + // 填充字符集和排序规则 + if col.Character == "" { + // 当从`INFORMATION_SCHEMA`.`COLUMNS`表中查询不到相关列的character和collation的信息时 + // 认为该列使用的character和collation与其所处的表一致 + // 由于`INFORMATION_SCHEMA`.`TABLES`表中未找到表的character,所以从按照MySQL中collation的规则从中截取character + + sql = fmt.Sprintf("SELECT `t`.`TABLE_COLLATION` FROM `INFORMATION_SCHEMA`.`TABLES` AS `t` "+ + "WHERE `t`.`TABLE_NAME`='%s' AND `t`.`TABLE_SCHEMA` = '%s'", col.Table, col.DB) + var newRes *QueryResult + newRes, err = db.Query(sql) + if err != nil { + common.Log.Error("(db *Connector) FindColumn Error : ", err) + return columns, err + } + + tbCollation := newRes.Rows[0].Str(0) + if tbCollation != "" { + col.Character = strings.Split(tbCollation, "_")[0] + col.Collation = tbCollation + } + } + + columns = append(columns, col) + } + + return columns, err +} + +// IsFKey 判断列是否是外键 +func (db *Connector) IsFKey(dbName, tbName, column string) bool { + sql := fmt.Sprintf("SELECT REFERENCED_COLUMN_NAME FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE C "+ + "WHERE REFERENCED_TABLE_SCHEMA <> 'NULL' AND"+ + " TABLE_NAME='%s' AND"+ + " TABLE_SCHEMA='%s' AND"+ + " COLUMN_NAME='%s'", tbName, dbName, column) + + res, err := db.Query(sql) + if err == nil && len(res.Rows) == 0 { + return false + } + + return true +} + +// Reference 用于存储关系 +type Reference map[string][]ReferenceValue + +// ReferenceValue 用于处理表之间的关系 +type ReferenceValue struct { + RefDBName string // 夫表所属数据库 + RefTable string // 父表 + DBName string // 子表所属数据库 + Table string // 子表 + ConstraintName string // 关系名称 +} + +// ShowReference 查找所有的外键信息 +func (db *Connector) ShowReference(dbName string, tbName ...string) ([]ReferenceValue, error) { + var referenceValues []ReferenceValue + sql := `SELECT C.REFERENCED_TABLE_SCHEMA,C.REFERENCED_TABLE_NAME,C.TABLE_SCHEMA,C.TABLE_NAME,C.CONSTRAINT_NAME +FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE C JOIN INFORMATION_SCHEMA. TABLES T ON T.TABLE_NAME = C.TABLE_NAME +WHERE C.REFERENCED_TABLE_NAME IS NOT NULL` + sql = sql + fmt.Sprintf(` AND C.TABLE_SCHEMA = "%s"`, dbName) + + if len(tbName) > 0 { + extra := fmt.Sprintf(` AND C.TABLE_NAME IN ("%s")`, strings.Join(tbName, `","`)) + sql = sql + extra + } + + // 执行SQL查找外键关联关系 + res, err := db.Query(sql) + if err != nil { + return referenceValues, err + } + + refDb := res.Result.Map("REFERENCED_TABLE_SCHEMA") + refTb := res.Result.Map("REFERENCED_TABLE_NAME") + schema := res.Result.Map("TABLE_SCHEMA") + tb := res.Result.Map("TABLE_NAME") + cName := res.Result.Map("CONSTRAINT_NAME") + + // 获取值 + for _, row := range res.Rows { + value := ReferenceValue{ + RefDBName: row.Str(refDb), + RefTable: row.Str(refTb), + DBName: row.Str(schema), + Table: row.Str(tb), + ConstraintName: row.Str(cName), + } + referenceValues = append(referenceValues, value) + } + + return referenceValues, err + +} diff --git a/database/show_test.go b/database/show_test.go new file mode 100644 index 00000000..66f98d47 --- /dev/null +++ b/database/show_test.go @@ -0,0 +1,94 @@ +/* + * Copyright 2018 Xiaomi, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package database + +import ( + "fmt" + "testing" + + "github.com/kr/pretty" + "vitess.io/vitess/go/vt/sqlparser" +) + +func TestShowTableStatus(t *testing.T) { + connTest.Database = "information_schema" + ts, err := connTest.ShowTableStatus("TABLES") + if err != nil { + t.Error("ShowTableStatus Error: ", err) + } + pretty.Println(ts) +} + +func TestShowTables(t *testing.T) { + connTest.Database = "information_schema" + ts, err := connTest.ShowTables() + if err != nil { + t.Error("ShowTableStatus Error: ", err) + } + pretty.Println(ts) +} + +func TestShowCreateTable(t *testing.T) { + connTest.Database = "information_schema" + ts, err := connTest.ShowCreateTable("TABLES") + if err != nil { + t.Error("ShowCreateTable Error: ", err) + } + fmt.Println(ts) + stmt, err := sqlparser.Parse(ts) + pretty.Println(stmt, err) +} + +func TestShowIndex(t *testing.T) { + connTest.Database = "information_schema" + ti, err := connTest.ShowIndex("TABLES") + if err != nil { + t.Error("ShowIndex Error: ", err) + } + pretty.Println(ti.FindIndex(IndexKeyName, "idx_store_id_film_id")) +} + +func TestShowColumns(t *testing.T) { + connTest.Database = "information_schema" + ti, err := connTest.ShowColumns("TABLES") + if err != nil { + t.Error("ShowColumns Error: ", err) + } + pretty.Println(ti) +} + +func TestFindColumn(t *testing.T) { + ti, err := connTest.FindColumn("id", "") + if err != nil { + t.Error("FindColumn Error: ", err) + } + pretty.Println(ti) +} + +func TestShowReference(t *testing.T) { + rv, err := connTest.ShowReference("test2", "homeImg") + if err != nil { + t.Error("ShowReference Error: ", err) + } + pretty.Println(rv) +} + +func TestIsFKey(t *testing.T) { + if !connTest.IsFKey("sakila", "film", "language_id") { + t.Error("want True. got false") + } +} diff --git a/database/testdata/TestExplain.golden b/database/testdata/TestExplain.golden new file mode 100644 index 00000000..4238525f --- /dev/null +++ b/database/testdata/TestExplain.golden @@ -0,0 +1,159 @@ +&database.ExplainInfo{ + SQL: "select ID,name from (select address from customer_list where SID=1 order by phone limit 50,10) a join customer_list l on (a.address=l.address) join city c on (c.city=l.city) order by phone desc;", + ExplainFormat: 0, + ExplainRows: { + &database.ExplainRow{ + ID: 1, + SelectType: "PRIMARY", + TableName: "country", + Partitions: "NULL", + AccessType: "index", + PossibleKeys: {"PRIMARY"}, + Key: "PRIMARY", + KeyLen: "2", + Ref: {""}, + Rows: 109, + Filtered: 100, + Scalability: "O(n)", + Extra: "Using index; Using temporary; Using filesort", + }, + &database.ExplainRow{ + ID: 1, + SelectType: "PRIMARY", + TableName: "city", + Partitions: "NULL", + AccessType: "ref", + PossibleKeys: {"PRIMARY", "idx_fk_country_id"}, + Key: "idx_fk_country_id", + KeyLen: "2", + Ref: {"sakila.country.country_id"}, + Rows: 5, + Filtered: 100, + Scalability: "O(log n)", + Extra: "NULL", + }, + &database.ExplainRow{ + ID: 1, + SelectType: "PRIMARY", + TableName: "c", + Partitions: "NULL", + AccessType: "ALL", + PossibleKeys: {""}, + Key: "NULL", + KeyLen: "", + Ref: {""}, + Rows: 600, + Filtered: 10, + Scalability: "O(n)", + Extra: "Using where; Using join buffer (Block Nested Loop)", + }, + &database.ExplainRow{ + ID: 1, + SelectType: "PRIMARY", + TableName: "a", + Partitions: "NULL", + AccessType: "ref", + PossibleKeys: {"PRIMARY", "idx_fk_city_id"}, + Key: "idx_fk_city_id", + KeyLen: "2", + Ref: {"sakila.city.city_id"}, + Rows: 1, + Filtered: 100, + Scalability: "O(log n)", + Extra: "NULL", + }, + &database.ExplainRow{ + ID: 1, + SelectType: "PRIMARY", + TableName: "cu", + Partitions: "NULL", + AccessType: "ref", + PossibleKeys: {"idx_fk_address_id"}, + Key: "idx_fk_address_id", + KeyLen: "2", + Ref: {"sakila.a.address_id"}, + Rows: 1, + Filtered: 100, + Scalability: "O(log n)", + Extra: "NULL", + }, + &database.ExplainRow{ + ID: 1, + SelectType: "PRIMARY", + TableName: "", + Partitions: "NULL", + AccessType: "ref", + PossibleKeys: {""}, + Key: "", + KeyLen: "152", + Ref: {"sakila.a.address"}, + Rows: 6, + Filtered: 100, + Scalability: "O(log n)", + Extra: "Using index", + }, + &database.ExplainRow{ + ID: 2, + SelectType: "DERIVED", + TableName: "a", + Partitions: "NULL", + AccessType: "ALL", + PossibleKeys: {"PRIMARY", "idx_fk_city_id"}, + Key: "NULL", + KeyLen: "", + Ref: {""}, + Rows: 603, + Filtered: 100, + Scalability: "O(n)", + Extra: "Using filesort", + }, + &database.ExplainRow{ + ID: 2, + SelectType: "DERIVED", + TableName: "cu", + Partitions: "NULL", + AccessType: "ref", + PossibleKeys: {"idx_fk_store_id", "idx_fk_address_id"}, + Key: "idx_fk_address_id", + KeyLen: "2", + Ref: {"sakila.a.address_id"}, + Rows: 1, + Filtered: 54.42, + Scalability: "O(log n)", + Extra: "Using where", + }, + &database.ExplainRow{ + ID: 2, + SelectType: "DERIVED", + TableName: "city", + Partitions: "NULL", + AccessType: "eq_ref", + PossibleKeys: {"PRIMARY", "idx_fk_country_id"}, + Key: "PRIMARY", + KeyLen: "2", + Ref: {"sakila.a.city_id"}, + Rows: 1, + Filtered: 100, + Scalability: "O(log n)", + Extra: "NULL", + }, + &database.ExplainRow{ + ID: 2, + SelectType: "DERIVED", + TableName: "country", + Partitions: "NULL", + AccessType: "eq_ref", + PossibleKeys: {"PRIMARY"}, + Key: "PRIMARY", + KeyLen: "2", + Ref: {"sakila.city.country_id"}, + Rows: 1, + Filtered: 100, + Scalability: "O(log n)", + Extra: "Using index", + }, + }, + ExplainJSON: (*database.ExplainJSON)(nil), + Warnings: nil, + QueryCost: 0, +} diff --git a/database/testdata/TestExplainInfoTranslator.golden b/database/testdata/TestExplainInfoTranslator.golden new file mode 100644 index 00000000..e69de29b diff --git a/database/testdata/TestFormatProfiling.golden b/database/testdata/TestFormatProfiling.golden new file mode 100644 index 00000000..e69de29b diff --git a/database/testdata/TestMySQLExplainQueryCost.golden b/database/testdata/TestMySQLExplainQueryCost.golden new file mode 100644 index 00000000..e69de29b diff --git a/database/testdata/TestMySQLExplainWarnings.golden b/database/testdata/TestMySQLExplainWarnings.golden new file mode 100644 index 00000000..e69de29b diff --git a/database/testdata/TestPrintMarkdownExplainTable.golden b/database/testdata/TestPrintMarkdownExplainTable.golden new file mode 100644 index 00000000..e69de29b diff --git a/database/testdata/TestSource.sql b/database/testdata/TestSource.sql new file mode 100644 index 00000000..2bba4d12 --- /dev/null +++ b/database/testdata/TestSource.sql @@ -0,0 +1,2 @@ +select 1; +select 1; diff --git a/database/trace.go b/database/trace.go new file mode 100644 index 00000000..83e2c75b --- /dev/null +++ b/database/trace.go @@ -0,0 +1,159 @@ +/* + * Copyright 2018 Xiaomi, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package database + +import ( + "errors" + "fmt" + "io" + "regexp" + "strings" + "time" + + "github.com/XiaoMi/soar/common" + + "vitess.io/vitess/go/vt/sqlparser" +) + +// Trace 用于存放 Select * From Information_Schema.Optimizer_Trace;输出的结果 +type Trace struct { + Rows []TraceRow +} + +// TraceRow 中含有trace的基本信息 +type TraceRow struct { + Query string + Trace string + MissingBytesBeyondMaxMemSize int + InsufficientPrivileges int +} + +// Trace 执行SQL,并对其Trace +func (db *Connector) Trace(sql string, params ...interface{}) (*QueryResult, error) { + common.Log.Debug("Trace SQL: %s", sql) + if common.Config.TestDSN.Version < 560 { + return nil, errors.New("version < 5.6, not support trace") + } + + // 过滤不需要Trace的SQL + switch sqlparser.Preview(sql) { + case sqlparser.StmtSelect, sqlparser.StmtUpdate, sqlparser.StmtDelete: + sql = "explain " + sql + case sqlparser.EXPLAIN: + default: + return nil, errors.New("no need trace") + } + + // 测试环境如果检查是关闭的,则SQL不会被执行 + if common.Config.TestDSN.Disable { + return nil, errors.New("TestDsn Disable") + } + + // 数据库安全性检查:如果Connector的IP端口与TEST环境不一致,则启用SQL白名单 + // 不在白名单中的SQL不允许执行 + // 执行环境与test环境不相同 + if db.Addr != common.Config.TestDSN.Addr && db.dangerousQuery(sql) { + return nil, fmt.Errorf("query Execution Deny: Execute SQL with DSN(%s/%s) '%s'", + db.Addr, db.Database, fmt.Sprintf(sql, params...)) + } + + common.Log.Debug("Execute SQL with DSN(%s/%s) : %s", db.Addr, db.Database, sql) + conn := db.NewConnection() + + // 设置SQL连接超时时间 + conn.SetTimeout(time.Duration(common.Config.ConnTimeOut) * time.Second) + defer conn.Close() + err := conn.Connect() + if err != nil { + return nil, err + } + + // 添加SQL执行超时限制 + ch := make(chan QueryResult, 1) + go func() { + // 开启Trace + common.Log.Debug("SET SESSION OPTIMIZER_TRACE='enabled=on'") + _, _, err = conn.Query("SET SESSION OPTIMIZER_TRACE='enabled=on'") + common.LogIfError(err, "") + + // 执行SQL,抛弃返回结果 + result, err := conn.Start(sql, params...) + if err != nil { + ch <- QueryResult{ + Error: err, + } + return + } + row := result.MakeRow() + for { + err = result.ScanRow(row) + if err == io.EOF { + break + } + } + + // 返回Trace结果 + res := QueryResult{} + res.Rows, res.Result, res.Error = conn.Query("SELECT * FROM information_schema.OPTIMIZER_TRACE") + + // 关闭Trace + common.Log.Debug("SET SESSION OPTIMIZER_TRACE='enabled=off'") + _, _, err = conn.Query("SET SESSION OPTIMIZER_TRACE='enabled=off'") + if err != nil { + fmt.Println(err.Error()) + } + ch <- res + }() + + select { + case res := <-ch: + return &res, res.Error + case <-time.After(time.Duration(common.Config.QueryTimeOut) * time.Second): + return nil, errors.New("query execution timeout") + } +} + +// getTrace 获取trace信息 +func getTrace(res *QueryResult) Trace { + var rows []TraceRow + for _, row := range res.Rows { + rows = append(rows, TraceRow{ + Query: row.Str(0), + Trace: row.Str(1), + MissingBytesBeyondMaxMemSize: row.Int(2), + InsufficientPrivileges: row.Int(3), + }) + } + return Trace{Rows: rows} +} + +// FormatTrace 格式化输出Trace信息 +func FormatTrace(res *QueryResult) string { + explainReg := regexp.MustCompile(`(?i)^explain\s+`) + trace := getTrace(res) + str := []string{""} + for _, row := range trace.Rows { + str = append(str, "```sql") + sql := explainReg.ReplaceAllString(row.Query, "") + str = append(str, sql) + str = append(str, "```\n") + str = append(str, "```json") + str = append(str, row.Trace) + str = append(str, "```\n") + } + return strings.Join(str, "\n") +} diff --git a/database/trace_test.go b/database/trace_test.go new file mode 100644 index 00000000..8dea2d7f --- /dev/null +++ b/database/trace_test.go @@ -0,0 +1,58 @@ +/* + * Copyright 2018 Xiaomi, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package database + +import ( + "flag" + "testing" + + "github.com/XiaoMi/soar/common" + + "github.com/kr/pretty" +) + +var update = flag.Bool("update", false, "update .golden files") + +func TestTrace(t *testing.T) { + common.Config.QueryTimeOut = 1 + res, err := connTest.Trace("select 1") + if err == nil { + common.GoldenDiff(func() { + pretty.Println(res) + }, t.Name(), update) + } else { + t.Error(err) + } +} + +func TestFormatTrace(t *testing.T) { + res, err := connTest.Trace("select 1") + if err == nil { + pretty.Println(FormatTrace(res)) + } else { + t.Error(err) + } +} + +func TestGetTrace(t *testing.T) { + res, err := connTest.Trace("select 1") + if err == nil { + pretty.Println(getTrace(res)) + } else { + t.Error(err) + } +} diff --git a/deps.sh b/deps.sh new file mode 100755 index 00000000..afeb3e08 --- /dev/null +++ b/deps.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +NEEDED_COMMANDS="mysql docker git go govendor retool" + +for cmd in ${NEEDED_COMMANDS} ; do + if ! command -v "${cmd}" &> /dev/null ; then + echo -e "\033[91m${cmd} missing\033[0m" + exit 1 + else + echo "${cmd} found" + fi +done diff --git a/doc/FAQ.md b/doc/FAQ.md new file mode 100644 index 00000000..a9648b1b --- /dev/null +++ b/doc/FAQ.md @@ -0,0 +1,71 @@ +## 常见问题 + +### 软件依赖 + +* [git](https://git-scm.co) 项目代码管理工具 +* [go](https://golang.org/) 源码编译依赖 +* [govendor](https://github.com/kardianos/govendor) 管理第三方包 +* [docker](https://www.docker.com) 主要用于构建测试环境 +* [mysql](https://www.mysql.com/) 测试时用来连接测试环境 +* [retool](https://github.com/twitchtv/retool): 管理测试开发工具,首次安装耗时会比较长,如:`gometalinter.v2`, `revive`, `golangci-lint` + +### 提示语法错误 + +* 请检查SQL语句中是否出现了不配对的引号,如 `, ", ' + +### 输出结果返回慢 + +* 如果配置了online-dsn或test-dsn SOAR会请求这些数据库以支持更多的功能,这时评审一条SQL就会耗时变长。 +* 如果又开启了`-sampling=true`的话会将线上的数据导入到测试环境,数据采样也会消耗一些时间。 + +## 如何搭建测试环境 + +```bash +# 创建测试数据库 +wget http://downloads.mysql.com/doc/sakila-db.tar.gz +tar zxf sakila-db.tar.gz && cd sakila-db +mysql -u root -p -f < sakila-schema.sql +mysql -u root -p -f < sakila-data.sql + +# 创建测试用户 +CREATE USER root@'hostname' IDENTIFIED BY "1t'sB1g3rt"; +GRANT ALL ON *.* TO root@'hostname'; +``` + +## 更新vitess依赖 + +使用`govendor fetch`或`git clone` [vitess](https://github.com/vitessio/vitess) 在某些地区更新vitess可能会比较慢,导致项目编译不过,所以将vitess整个代码库加到了代码仓库。 + +如属更新vitess仓库可以使用如下命令。 + +```bash +$ make vitess +``` + +## 生成报告并发邮件 + +```bash +#!/bin/bash + +soar -query "select * from film" > ./index.html + +( + echo To: youmail@example.com + echo From: robot@example.com + echo "Content-Type: text/html; " + echo Subject: SQL Analyze Report + echo + cat ./index.html +) | sendmail -t + +``` + +## 如何新增一条启发式建议 + +```bash +advisor/rules.go HeuristicRules 加一个条新的规则 +advisor/heuristic.go 实现一个规则函数 +advisor/heuristic_test.go 添加相应规则函数的测试用例 +make heuristic +make daily +``` diff --git a/doc/FAQ_en.md b/doc/FAQ_en.md new file mode 100644 index 00000000..dde04c96 --- /dev/null +++ b/doc/FAQ_en.md @@ -0,0 +1,74 @@ +## FAQ + +### Dependency Tools + +* [git](https://git-scm.co): clone code from git repository +* [go](https://golang.org/): build source +* [govendor](https://github.com/kardianos/govendor): manager third party dependency +* [docker](https://www.docker.com): manager test envirment +* [mysql](https://www.mysql.com/): connect test envirment +* [retool](https://github.com/twitchtv/retool): manager test tools such as `gometalinter.v2`, `revive`, `golangci-lint` + +### Syntax Error + +* Unexpected quote, like `, ", ' +* vitess syntax not supported yet + +### Program running slowly + +* SOAR will use online-dsn, test-dsn for data sampling and testing if they are on a different host to access these instance will cost much time. This may cause analyze slowly, especially when you are optimizing lots of queries. +* As mentioned above, if you set `-sampling=true`(by default), data sampling will take some time for more accurate suggestions. + +## build test env + +```bash +# create test database +wget http://downloads.mysql.com/doc/sakila-db.tar.gz +tar zxf sakila-db.tar.gz && cd sakila-db +mysql -u root -p -f < sakila-schema.sql +mysql -u root -p -f < sakila-data.sql + +# create test user +CREATE USER root@'hostname' IDENTIFIED BY "1t'sB1g3rt"; +GRANT ALL ON *.* TO root@'hostname'; +``` + +## update vitess in vendor + +`govendor fetch` or `git clone` [vitess](https://github.com/vitessio/vitess) in somewhere maybe very slow or be blocked, so we add vitess source code in vendor directory. + +If you what to update vitess package, you should bypass that block using yourself method. + +```bash +$ make vitess +``` + +## HTML Format Report + +```bash +#!/bin/bash + +soar -query "select * from film" > ./index.html + +( + echo To: youmail@example.com + echo From: robot@example.com + echo "Content-Type: text/html; " + echo Subject: SQL Analyze Report + echo + cat ./index.html +) | sendmail -t + +``` + +## Add a new heuristict rule + +```bash +advisor/rules.go HeuristicRules add a new item +advisor/heuristic.go add a new rule function +advisor/heuristic_test.go add a new test function +make doc +go test github.com/XiaoMi/soar/advisor -v -update -run TestListHeuristicRules +go test github.com/XiaoMi/soar/advisor -v -update -run TestMergeConflictHeuristicRules +make daily +``` diff --git a/doc/cheatsheet.md b/doc/cheatsheet.md new file mode 100644 index 00000000..f1145879 --- /dev/null +++ b/doc/cheatsheet.md @@ -0,0 +1,174 @@ +[toc] + +# 常用命令 + +## 基本用法 + +```bash +echo "select title from sakila.film" | ./soar -log-output=soar.log +``` + +## 指定配置文件 + +```bash +vi soar.yaml +# yaml format config file +online-dsn: + addr: 127.0.0.1:3306 + schema: sakila + user: root + password: "1t'sB1g3rt" + disable: false + +test-dsn: + addr: 127.0.0.1:3306 + schema: sakila + user: root + password: "1t'sB1g3rt" + disable: false +``` + +```bash +echo "select title from sakila.film" | ./soar -test-dsn="root:1t'sB1g3rt@127.0.0.1:3306/sakila" -allow-online-as-test -log-output=soar.log +``` + +## 打印所有的启发式规则 + +```bash +$ soar -list-heuristic-rules +``` + +## 忽略某些规则 + +```bash +$ soar -ignore-rules "ALI.001,IDX.*" +``` + +## 打印支持的报告格式 + +```bash +$ soar -list-report-types +``` + +## 以指定格式输出报告 + +```bash +$ soar -report-type json +``` + +## 语法检查工具 + +```bash +$ echo "select * from tb" | soar -only-syntax-check +$ echo $? +0 + +$ echo "select * fromtb" | soar -only-syntax-check +At SQL 0 : syntax error at position 16 near 'fromtb' +$ echo $? +1 + +``` + +## 慢日志进行分析示例 + +```bash +$ pt-query-digest slow.log > slow.log.digest +# parse pt-query-digest's output which example script +$ python2.7 doc/example/digest_pt.py slow.log.digest > slow.md +``` + + +## SQL指纹 + +```bash +$ echo "select * from film where col='abc'" | soar -report-type=fingerprint +``` + +输出 + +```sql +select * from film where col=? +``` + +## 将UPDATE/DELETE/INSERT语法转为SELECT + +```bash +$ echo "update film set title = 'abc'" | soar -rewrite-rules dml2select,delimiter -report-type rewrite +``` + +输出 + +```sql +select * from film; +``` + + +## 合并多条ALTER语句 + +```bash +$ echo "alter table tb add column a int; alter table tb add column b int;" | soar -report-type rewrite -rewrite-rules mergealter +``` + +输出 + +```sql +ALTER TABLE `tb` add column a int, add column b int ; +``` + +## SQL美化 + +```bash +$ echo "select * from tbl where col = 'val'" | ./soar -report-type=pretty +``` + +输出 + +```sql +SELECT + * +FROM + tbl +WHERE + col = 'val'; +``` + +## EXPLAIN信息分析报告 + +```bash +$ soar -report-type explain-digest << EOF ++----+-------------+-------+------+---------------+------+---------+------+------+-------+ +| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | ++----+-------------+-------+------+---------------+------+---------+------+------+-------+ +| 1 | SIMPLE | film | ALL | NULL | NULL | NULL | NULL | 1131 | | ++----+-------------+-------+------+---------------+------+---------+------+------+-------+ +EOF +``` + +```text +## Explain信息 + +| id | select\_type | table | partitions | type | possible_keys | key | key\_len | ref | rows | filtered | scalability | Extra | +|---|---|---|---|---|---|---|---|---|---|---|---|---| +| 1 | SIMPLE | *film* | NULL | ALL | NULL | NULL | NULL | NULL | 0 | 0.00% | ☠️ **O(n)** | | + + +### Explain信息解读 + +#### SelectType信息解读 + +* **SIMPLE**: 简单SELECT(不使用UNION或子查询等). + +#### Type信息解读 + +* ☠️ **ALL**: 最坏的情况, 从头到尾全表扫描. +``` + +## markdown转HTML + +通过指定-report-css, -report-javascript, -markdown-extensions, -markdown-html-flags这些参数,你还可以控制HTML的显示格式。 + +```bash +$ cat test.md | soar -report-type md2html > test.html +``` + diff --git a/doc/cheatsheet_en.md b/doc/cheatsheet_en.md new file mode 100644 index 00000000..dc9ba765 --- /dev/null +++ b/doc/cheatsheet_en.md @@ -0,0 +1,143 @@ +[toc] + +# Useful Commands + +## Basic suggest + +```bash +echo "select title from sakila.film" | ./soar -log-output=soar.log +``` + +## Analyze SQL with test environment + +```bash +vi soar.yaml +# yaml format config file +online-dsn: + addr: 127.0.0.1:3306 + schema: sakila + user: root + password: "1t'sB1g3rt" + disable: false + +test-dsn: + addr: 127.0.0.1:3306 + schema: sakila + user: root + password: "1t'sB1g3rt" + disable: false +``` + +```bash +echo "select title from sakila.film" | ./soar -test-dsn="root:1t'sB1g3rt@127.0.0.1:3306/sakila" -allow-online-as-test -log-output=soar.log +``` + +## List supported heuristic rules + +```bash +$ soar -list-heuristic-rules +``` + +## Ignore Rules + +```bash +$ soar -ignore-rules "ALI.001,IDX.*" +``` + +## List supported report-type + +```bash +$ soar -list-report-types +``` + +## Set report-type for output + +```bash +$ soar -report-type json +``` + +## Syntax Check + +```bash +$ echo "select * from tb" | soar -only-syntax-check +$ echo $? +0 + +$ echo "select * fromtb" | soar -only-syntax-check +At SQL 0 : syntax error at position 16 near 'fromtb' +$ echo $? +1 + +``` + +## Slow log analyzing + +```bash +$ pt-query-digest slow.log > slow.log.digest +# parse pt-query-digest's output which example script +$ python2.7 doc/example/digest_pt.py slow.log.digest > slow.md +``` + + +## SQL FingerPrint + +```bash +$ echo "select * from film where col='abc'" | soar -report-type=fingerprint +``` + +Output + +```sql +select * from film where col=? +``` + +## Convert UPDATE/DELETE/INSERT into SELECT + +```bash +$ echo "update film set title = 'abc'" | soar -rewrite-rules dml2select,delimiter -report-type rewrite +``` + +Output + +```sql +select * from film; +``` + + +## Merge ALTER SQLs + +```bash +$ echo "alter table tb add column a int; alter table tb add column b int;" | soar -report-type rewrite -rewrite-rules mergealter +``` + +Output + +```sql +ALTER TABLE `tb` add column a int, add column b int ; +``` + +## SQL Pretty + +```bash +$ echo "select * from tbl where col = 'val'" | ./soar -report-type=pretty +``` + +Output + +```sql +SELECT + * +FROM + tbl +WHERE + col = 'val'; +``` + +## Convert markdown to HTML + +md2html comes with other flags, such as `-report-css`, `-report-javascript`, `-markdown-extensions`, `-markdown-html-flags`, you can get more self control HTML report. + +```bash +$ cat test.md | soar -report-type md2html > test.html +``` + diff --git a/doc/comparison.md b/doc/comparison.md new file mode 100644 index 00000000..90f70878 --- /dev/null +++ b/doc/comparison.md @@ -0,0 +1,13 @@ +## 业内其他优秀产品对比 + +| | SOAR | sqlcheck | pt-query-advisor | SQL Advisor | Inception | sqlautoreview | +| --- | --- | --- | --- | --- | --- | --- | +| 启发式建议 | ✔️ | ✔️ | ✔️ | ❌ | ✔️ | ✔️ | +| 索引建议 | ✔️ | ❌ | ❌ | ✔️ | ❌ | ✔️ | +| 查询重写 | ✔️ | ❌ | ❌ | ❌ | ❌ | ❌ | +| 执行计划展示 | ✔️ | ❌ | ❌ | ❌ | ❌ | ❌ | +| Profiling | ✔️ | ❌ | ❌ | ❌ | ❌ | ❌ | +| Trace | ✔️ | ❌ | ❌ | ❌ | ❌ | ❌ | +| SQL在线执行 | ❌ | ❌ | ❌ | ❌ | ✔️ | ❌ | +| 数据备份 | ❌ | ❌ | ❌ | ❌ | ✔️ | ❌ | + diff --git a/doc/comparison_en.md b/doc/comparison_en.md new file mode 100644 index 00000000..1aced8c9 --- /dev/null +++ b/doc/comparison_en.md @@ -0,0 +1,13 @@ +## Compare with other wonderful product + +| | SOAR | sqlcheck | pt-query-advisor | SQL Advisor | Inception | sqlautoreview | +| --- | --- | --- | --- | --- | --- | --- | +| Heuristic Rules | ✔️ | ✔️ | ✔️ | ❌ | ✔️ | ✔️ | +| Index Suggest | ✔️ | ❌ | ❌ | ✔️ | ❌ | ✔️ | +| Rewrite Query | ✔️ | ❌ | ❌ | ❌ | ❌ | ❌ | +| Explain | ✔️ | ❌ | ❌ | ❌ | ❌ | ❌ | +| Profiling | ✔️ | ❌ | ❌ | ❌ | ❌ | ❌ | +| Trace | ✔️ | ❌ | ❌ | ❌ | ❌ | ❌ | +| Execute SQL Online | ❌ | ❌ | ❌ | ❌ | ✔️ | ❌ | +| Backup Data | ❌ | ❌ | ❌ | ❌ | ✔️ | ❌ | + diff --git a/doc/config.md b/doc/config.md new file mode 100644 index 00000000..65f8ef99 --- /dev/null +++ b/doc/config.md @@ -0,0 +1,102 @@ +## 配置文件说明 + +配置文件为[yaml](https://en.wikipedia.org/wiki/YAML)格式。一般情况下只需要配置online-dsn, test-dsn, log-output等少数几个参数。即使不创建配置文件SOAR仍然会给出基本的启发式建议。 + +```text +# 线上环境配置 +online-dsn: + addr: 127.0.0.1:3306 + schema: sakila + user: root + password: 1t'sB1g3rt + disable: false +# 测试环境配置 +test-dsn: + addr: 127.0.0.1:3307 + schema: test + user: root + password: 1t'sB1g3rt + disable: false +# 是否允许测试环境与线上环境配置相同 +allow-online-as-test: true +# 是否清理测试时产生的临时文件 +drop-test-temporary: true +# 语法检查小工具 +only-syntax-check: false +sampling-data-factor: 100 +sampling: true +# 日志级别,[0:Emergency, 1:Alert, 2:Critical, 3:Error, 4:Warning, 5:Notice, 6:Informational, 7:Debug] +log-level: 7 +log-output: ${BASE_DIR}/soar.log +# 优化建议输出格式 +report-type: markdown +ignore-rules: +- "" +blacklist: ${BASE_DIR}/soar.blacklist +# 启发式算法相关配置 +max-join-table-count: 5 +max-group-by-cols-count: 5 +max-distinct-count: 5 +max-index-cols-count: 5 +max-total-rows: 9999999 +spaghetti-query-length: 2048 +allow-drop-index: false +# EXPLAIN相关配置 +explain-sql-report-type: pretty +explain-type: extended +explain-format: traditional +explain-warn-select-type: +- "" +explain-warn-access-type: +- ALL +explain-max-keys: 3 +explain-min-keys: 0 +explain-max-rows: 10000 +explain-warn-extra: +- "" +explain-max-filtered: 100 +explain-warn-scalability: +- O(n) +query: "" +list-heuristic-rules: false +list-test-sqls: false +verbose: true +``` + +## 命令行参数 + +几乎所有配置文件中指定的参数都通通过命令行参数进行修改,且命令行参数优先级较配置文件优先级高。 + +```bash +$ soar -h +``` + +### 命令行参数配置DSN + +```bash +$ soar -online-dsn "user:password@hostname:port/database" + +$ soar -test-dsn "user:password@hostname:port/database" +``` + +#### DSN格式支持 +* "user:password@hostname:3307/database" +* "user:password@hostname:3307" +* "user:password@hostname:/database" +* "user:password@:3307/database" +* "user:password@" +* "hostname:3307/database" +* "@hostname:3307/database" +* "@hostname" +* "hostname" +* "@/database" +* "@hostname:3307" +* "@:3307/database" +* ":3307/database" +* "/database" + +### SQL评分 + +不同类型的建议指定的Severity不同,严重程度数字由低到高依次排序。满分100分,扣到0分为止。L0不扣分只给出建议,L1扣5分,L2扣10分,每级多扣5分以此类推。当由时给出L1, L2两要建议时扣分叠加,即扣15分。 + +如果您想给出不同的扣分建议或者对指引中的文字内容不满意可以为在git中提ISSUE,也可直接修改rules.go的相应配置然后重新编译自己的版本。 diff --git a/doc/editor_plugin.md b/doc/editor_plugin.md new file mode 100644 index 00000000..8324d340 --- /dev/null +++ b/doc/editor_plugin.md @@ -0,0 +1,34 @@ +## Vim插件安装 + +* 首先安装Syntastic,安装方法参见[官方文档](https://github.com/vim-syntastic/syntastic#installation) +* 将`soar`二进制文件拷贝到可执行文件的查找路径($PATH)下,添加可执行权限`chmod a+x soar` +* 将doc/example/[soar.vim](http://github.com/XiaoMi/soar/raw/master/doc/example/soar.vim)文件拷贝至${SyntasticInstalledPath}/syntax_checkers/sql目录下 +* 修改${SyntasticInstalledPath}/plugin/syntastic/registry.vim文件,增加sql文件的检查工具,`'sql':['soar', 'sqlint']` + +### 插件演示 + +![Vim插件示例](http://github.com/XiaoMi/soar/raw/master/doc/images/vim_plugin.png) + +### 常见问题 + +#### 安装插件后无任何变化 + +安装了Syntastic没有任何显示,官方推荐通过如下配置来开启自动提示,不然用户无法看到SOAR给出的建议。 + +```vim +set statusline+=%#warningmsg# +set statusline+=%{SyntasticStatuslineFlag()} +set statusline+=%* + +let g:syntastic_always_populate_loc_list = 1 +let g:syntastic_auto_loc_list = 1 +let g:syntastic_check_on_open = 1 +let g:syntastic_check_on_wq = 0 +``` + +如果soar二进制未在可执行文件查找路径下,或未添加可执行文件也会导致无法提供建议,可通过如下命令确认。 + +```bash +$ which soar +/usr/local/bin/soar +``` diff --git a/doc/enviorment.md b/doc/enviorment.md new file mode 100644 index 00000000..a53a4863 --- /dev/null +++ b/doc/enviorment.md @@ -0,0 +1,23 @@ +## 集成环境 + +![集成环境](http://github.com/XiaoMi/soar/raw/master/doc/images/env.png) + +| 线上环境 | 测试环境 | 场景 | +| --- | --- | --- | +| 有 | 有 | 日常优化,完整的建议,推荐 | +| 无 | 有 | 新申请资源,环境初始化测试 | +| 无 | 无 | 盲测,试用,无EXPLAIN和索引建议 | +| 有 | 无 | 用线上环境当测试环境,不推荐 | + +## 线上环境 + +* 数据字典 +* 数据采样 +* EXPLAIN + +## 测试环境 + +* 库表映射 +* 语法检查 +* 模拟执行 +* 索引建议/去重 diff --git a/doc/example/digest_pt.py b/doc/example/digest_pt.py new file mode 100755 index 00000000..d6b0c88f --- /dev/null +++ b/doc/example/digest_pt.py @@ -0,0 +1,94 @@ +#!/usr/bin/python -u +#-*- coding: utf-8 -*- + +import sys, re, subprocess +import os.path +reload(sys) +sys.setdefaultencoding("utf-8") + +SOAR_ARGS=["-ignore-rules=OK"] +USE_DATABASE="" + +# 打印pt-query-digest的统计信息 +def printStatInfo(buf): + if buf.strip() == "": + return + if re.match("^# Query [0-9]", buf): + sys.stdout.write(buf.split(":", 1)[0] + "\n") + sys.stdout.write("\n```text\n") + sys.stdout.write(buf) + sys.stdout.write("```\n") + +# 打印每条SQL的SOAR结果 +def printSqlAdvisor(buf): + global USE_DATABASE + buf = re.sub("\\\G$", "", USE_DATABASE + buf) + if buf.strip() == "": + return + + cmd = ["soar"] + if len(SOAR_ARGS) > 0: + cmd = cmd + SOAR_ARGS + + p = subprocess.Popen(["soar"], stdout=subprocess.PIPE, stdin=subprocess.PIPE) + adv = p.communicate(input=buf)[0] + + # 清理环境 + USE_DATABASE = "" + + # 删除第一行"# Query: xxxxx" + try: + adv = adv.split('\n', 1)[1] + except: + pass + sys.stdout.write(adv + "\n") + +# 从统计信息中获取database信息 +def getUseDB(line): + global USE_DATABASE + USE_DATABASE = "USE " + re.sub(' +', " ", line).split(" ")[2] + ";" + +def parsePtQueryDisget(f): + statBuf = "" + sqlBuf = "" + for line in f: + if line.strip() == "": + continue + + if line.startswith("#"): + if line.startswith("# Databases ") and not line.strip().endswith("more"): + getUseDB(line) + if re.match("^# Query [0-9]", line): + # pt-query-digest的头部统计信息 + if line.startswith("# Query 1:"): + sys.stdout.write("# pt-query-digest统计信息" + "\n") + printStatInfo(statBuf) + statBuf = line + else: + statBuf += line + else: + if not line.strip().endswith("\G"): + sqlBuf += line + else: + sqlBuf += line + printStatInfo(statBuf) + statBuf = "" + printSqlAdvisor(sqlBuf) + sqlBuf = "" + +def main(): + global SOAR_ARGS + if len(sys.argv) == 1: + f = sys.stdin + parsePtQueryDisget(f) + else: + if os.path.isfile(sys.argv[-1]): + SOAR_ARGS = sys.argv[1:-1] + f = open(sys.argv[-1]) + else: + SOAR_ARGS = sys.argv[1:] + f = sys.stdin + parsePtQueryDisget(f) + +if __name__ == '__main__': + main() diff --git a/doc/example/main_test.md b/doc/example/main_test.md new file mode 100644 index 00000000..149da347 --- /dev/null +++ b/doc/example/main_test.md @@ -0,0 +1,4438 @@ +# Query: C3FAEDA6AD6D762B + +★ ★ ★ ★ ☆ 85分 + +```sql + +SELECT + * +FROM + film +WHERE + LENGTH = 86 +``` + +## Explain信息 + +| id | select\_type | table | partitions | type | possible_keys | key | key\_len | ref | rows | filtered | scalability | Extra | +|---|---|---|---|---|---|---|---|---|---|---|---|---| +| 1 | SIMPLE | *film* | NULL | ALL | | NULL | | | 1000 | 10.00% | ☠️ **O(n)** | Using where | + + + +### Explain信息解读 + +#### SelectType信息解读 + +* **SIMPLE**: 简单SELECT(不使用UNION或子查询等). + +#### Type信息解读 + +* ☠️ **ALL**: 最坏的情况, 从头到尾全表扫描. + +#### Extra信息解读 + +* **Using where**: WHERE条件用于筛选出与下一个表匹配的数据然后返回给客户端. 除非故意做的全表扫描, 否则连接类型是ALL或者是index, 且在Extra列的值中没有Using Where, 则该查询可能是有问题的. + + +## 为sakila库的film表添加索引 + +* **Item:** IDX.001 + +* **Severity:** L2 + +* **Content:** 为列length添加索引,散粒度为: 14.00%; + +* **Case:** ALTER TABLE \`sakila\`.\`film\` add index \`idx\_length\` (\`length\`) ; + + + +## 不建议使用SELECT * 类型查询 + +* **Item:** COL.001 + +* **Severity:** L1 + +* **Content:** 当表结构变更时,使用\*通配符选择所有列将导致查询的含义和行为会发生更改,可能导致查询返回更多的数据。 + +# Query: E969B9297DA79BA6 + +★ ★ ★ ★ ☆ 80分 + +```sql + +SELECT + * +FROM + film +WHERE + LENGTH IS NULL +``` + +## Explain信息 + +| id | select\_type | table | partitions | type | possible_keys | key | key\_len | ref | rows | filtered | scalability | Extra | +|---|---|---|---|---|---|---|---|---|---|---|---|---| +| 1 | SIMPLE | *film* | NULL | ALL | | NULL | | | 1000 | 10.00% | ☠️ **O(n)** | Using where | + + + +### Explain信息解读 + +#### SelectType信息解读 + +* **SIMPLE**: 简单SELECT(不使用UNION或子查询等). + +#### Type信息解读 + +* ☠️ **ALL**: 最坏的情况, 从头到尾全表扫描. + +#### Extra信息解读 + +* **Using where**: WHERE条件用于筛选出与下一个表匹配的数据然后返回给客户端. 除非故意做的全表扫描, 否则连接类型是ALL或者是index, 且在Extra列的值中没有Using Where, 则该查询可能是有问题的. + + +## 为sakila库的film表添加索引 + +* **Item:** IDX.001 + +* **Severity:** L2 + +* **Content:** 为列length添加索引,散粒度为: 14.00%; + +* **Case:** ALTER TABLE \`sakila\`.\`film\` add index \`idx\_length\` (\`length\`) ; + + + +## 应尽量避免在WHERE子句中对字段进行NULL值判断 + +* **Item:** ARG.006 + +* **Severity:** L1 + +* **Content:** 使用IS NULL或IS NOT NULL将可能导致引擎放弃使用索引而进行全表扫描,如:select id from t where num is null;可以在num上设置默认值0,确保表中num列没有null值,然后这样查询: select id from t where num=0; + +## 不建议使用SELECT * 类型查询 + +* **Item:** COL.001 + +* **Severity:** L1 + +* **Content:** 当表结构变更时,使用\*通配符选择所有列将导致查询的含义和行为会发生更改,可能导致查询返回更多的数据。 + +# Query: 8A106444D14B9880 + +★ ★ ★ ☆ ☆ 60分 + +```sql + +SELECT + * +FROM + film +HAVING + title = 'abc' +``` + +## Explain信息 + +| id | select\_type | table | partitions | type | possible_keys | key | key\_len | ref | rows | filtered | scalability | Extra | +|---|---|---|---|---|---|---|---|---|---|---|---|---| +| 1 | SIMPLE | *film* | NULL | ALL | | NULL | | | 1000 | ☠️ **100.00%** | ☠️ **O(n)** | NULL | + + + +### Explain信息解读 + +#### SelectType信息解读 + +* **SIMPLE**: 简单SELECT(不使用UNION或子查询等). + +#### Type信息解读 + +* ☠️ **ALL**: 最坏的情况, 从头到尾全表扫描. + + +## SELECT未指定WHERE条件 + +* **Item:** CLA.001 + +* **Severity:** L4 + +* **Content:** SELECT语句没有WHERE子句,可能检查比预期更多的行(全表扫描)。对于SELECT COUNT(\*)类型的请求如果不要求精度,建议使用SHOW TABLE STATUS或EXPLAIN替代。 + +## 不建议使用HAVING子句 + +* **Item:** CLA.013 + +* **Severity:** L3 + +* **Content:** 将查询的HAVING子句改写为WHERE中的查询条件,可以在查询处理期间使用索引。 + +## 不建议使用SELECT * 类型查询 + +* **Item:** COL.001 + +* **Severity:** L1 + +* **Content:** 当表结构变更时,使用\*通配符选择所有列将导致查询的含义和行为会发生更改,可能导致查询返回更多的数据。 + +# Query: A0C5E62C724A121A + +★ ★ ★ ★ ☆ 85分 + +```sql + +SELECT + * +FROM + sakila. film +WHERE + LENGTH >= 60 +``` + +## Explain信息 + +| id | select\_type | table | partitions | type | possible_keys | key | key\_len | ref | rows | filtered | scalability | Extra | +|---|---|---|---|---|---|---|---|---|---|---|---|---| +| 1 | SIMPLE | *film* | NULL | ALL | | NULL | | | 1000 | 33.33% | ☠️ **O(n)** | Using where | + + + +### Explain信息解读 + +#### SelectType信息解读 + +* **SIMPLE**: 简单SELECT(不使用UNION或子查询等). + +#### Type信息解读 + +* ☠️ **ALL**: 最坏的情况, 从头到尾全表扫描. + +#### Extra信息解读 + +* **Using where**: WHERE条件用于筛选出与下一个表匹配的数据然后返回给客户端. 除非故意做的全表扫描, 否则连接类型是ALL或者是index, 且在Extra列的值中没有Using Where, 则该查询可能是有问题的. + + +## 为sakila库的film表添加索引 + +* **Item:** IDX.001 + +* **Severity:** L2 + +* **Content:** 为列length添加索引,散粒度为: 14.00%; + +* **Case:** ALTER TABLE \`sakila\`.\`film\` add index \`idx\_length\` (\`length\`) ; + + + +## 不建议使用SELECT * 类型查询 + +* **Item:** COL.001 + +* **Severity:** L1 + +* **Content:** 当表结构变更时,使用\*通配符选择所有列将导致查询的含义和行为会发生更改,可能导致查询返回更多的数据。 + +# Query: 868317D1973FD1B0 + +★ ★ ★ ★ ☆ 85分 + +```sql + +SELECT + * +FROM + film +WHERE + LENGTH BETWEEN 60 + AND 84 +``` + +## Explain信息 + +| id | select\_type | table | partitions | type | possible_keys | key | key\_len | ref | rows | filtered | scalability | Extra | +|---|---|---|---|---|---|---|---|---|---|---|---|---| +| 1 | SIMPLE | *film* | NULL | ALL | | NULL | | | 1000 | 11.11% | ☠️ **O(n)** | Using where | + + + +### Explain信息解读 + +#### SelectType信息解读 + +* **SIMPLE**: 简单SELECT(不使用UNION或子查询等). + +#### Type信息解读 + +* ☠️ **ALL**: 最坏的情况, 从头到尾全表扫描. + +#### Extra信息解读 + +* **Using where**: WHERE条件用于筛选出与下一个表匹配的数据然后返回给客户端. 除非故意做的全表扫描, 否则连接类型是ALL或者是index, 且在Extra列的值中没有Using Where, 则该查询可能是有问题的. + + +## 为sakila库的film表添加索引 + +* **Item:** IDX.001 + +* **Severity:** L2 + +* **Content:** 为列length添加索引,散粒度为: 14.00%; + +* **Case:** ALTER TABLE \`sakila\`.\`film\` add index \`idx\_length\` (\`length\`) ; + + + +## 不建议使用SELECT * 类型查询 + +* **Item:** COL.001 + +* **Severity:** L1 + +* **Content:** 当表结构变更时,使用\*通配符选择所有列将导致查询的含义和行为会发生更改,可能导致查询返回更多的数据。 + +# Query: 707FE669669FA075 + +★ ★ ★ ★ ☆ 95分 + +```sql + +SELECT + * +FROM + film +WHERE + title LIKE 'AIR%' +``` + +## Explain信息 + +| id | select\_type | table | partitions | type | possible_keys | key | key\_len | ref | rows | filtered | scalability | Extra | +|---|---|---|---|---|---|---|---|---|---|---|---|---| +| 1 | SIMPLE | *film* | NULL | range | idx\_title | idx\_title | 767 | | 2 | ☠️ **100.00%** | ☠️ **O(n)** | Using index condition | + + + +### Explain信息解读 + +#### SelectType信息解读 + +* **SIMPLE**: 简单SELECT(不使用UNION或子查询等). + +#### Type信息解读 + +* **range**: 只检索给定范围的行, 使用一个索引来选择行. key列显示使用了哪个索引. key_len包含所使用索引的最长关键元素. + +#### Extra信息解读 + +* **Using index**: 只需通过索引就可以从表中获取列的信息, 无需额外去读取真实的行数据. 如果查询使用的列值仅仅是一个简单索引的部分值, 则会使用这种策略来优化查询. + +* **Using index condition**: 在5.6版本后加入的新特性(Index Condition Pushdown)。Using index condition 会先条件过滤索引,过滤完索引后找到所有符合索引条件的数据行,随后用 WHERE 子句中的其他条件去过滤这些数据行。 + + +## 不建议使用SELECT * 类型查询 + +* **Item:** COL.001 + +* **Severity:** L1 + +* **Content:** 当表结构变更时,使用\*通配符选择所有列将导致查询的含义和行为会发生更改,可能导致查询返回更多的数据。 + +# Query: DF916439ABD07664 + +★ ★ ★ ★ ☆ 90分 + +```sql + +SELECT + * +FROM + film +WHERE + title IS NOT NULL +``` + +## Explain信息 + +| id | select\_type | table | partitions | type | possible_keys | key | key\_len | ref | rows | filtered | scalability | Extra | +|---|---|---|---|---|---|---|---|---|---|---|---|---| +| 1 | SIMPLE | *film* | NULL | ALL | idx\_title | NULL | | | 1000 | 90.00% | ☠️ **O(n)** | Using where | + + + +### Explain信息解读 + +#### SelectType信息解读 + +* **SIMPLE**: 简单SELECT(不使用UNION或子查询等). + +#### Type信息解读 + +* ☠️ **ALL**: 最坏的情况, 从头到尾全表扫描. + +#### Extra信息解读 + +* **Using where**: WHERE条件用于筛选出与下一个表匹配的数据然后返回给客户端. 除非故意做的全表扫描, 否则连接类型是ALL或者是index, 且在Extra列的值中没有Using Where, 则该查询可能是有问题的. + + +## 应尽量避免在WHERE子句中对字段进行NULL值判断 + +* **Item:** ARG.006 + +* **Severity:** L1 + +* **Content:** 使用IS NULL或IS NOT NULL将可能导致引擎放弃使用索引而进行全表扫描,如:select id from t where num is null;可以在num上设置默认值0,确保表中num列没有null值,然后这样查询: select id from t where num=0; + +## 不建议使用SELECT * 类型查询 + +* **Item:** COL.001 + +* **Severity:** L1 + +* **Content:** 当表结构变更时,使用\*通配符选择所有列将导致查询的含义和行为会发生更改,可能导致查询返回更多的数据。 + +# Query: B9336971FF3D3792 + +★ ★ ★ ★ ☆ 85分 + +```sql + +SELECT + * +FROM + film +WHERE + LENGTH = 114 + AND title = 'ALABAMA DEVIL' +``` + +## Explain信息 + +| id | select\_type | table | partitions | type | possible_keys | key | key\_len | ref | rows | filtered | scalability | Extra | +|---|---|---|---|---|---|---|---|---|---|---|---|---| +| 1 | SIMPLE | *film* | NULL | ref | idx\_title | idx\_title | 767 | const | 1 | 10.00% | ☠️ **O(n)** | Using where | + + + +### Explain信息解读 + +#### SelectType信息解读 + +* **SIMPLE**: 简单SELECT(不使用UNION或子查询等). + +#### Type信息解读 + +* **ref**: 连接不能基于关键字选择单个行, 可能查找到多个符合条件的行. 叫做ref是因为索引要跟某个参考值相比较. 这个参考值或者是一个数, 或者是来自一个表里的多表查询的结果值. 例:'SELECT * FROM tbl WHERE idx_col=expr;'. + +#### Extra信息解读 + +* **Using where**: WHERE条件用于筛选出与下一个表匹配的数据然后返回给客户端. 除非故意做的全表扫描, 否则连接类型是ALL或者是index, 且在Extra列的值中没有Using Where, 则该查询可能是有问题的. + + +## 为sakila库的film表添加索引 + +* **Item:** IDX.001 + +* **Severity:** L2 + +* **Content:** 为列title添加索引,散粒度为: 100.00%; 为列length添加索引,散粒度为: 14.00%; + +* **Case:** ALTER TABLE \`sakila\`.\`film\` add index \`idx\_title\_length\` (\`title\`,\`length\`) ; + + + +## 不建议使用SELECT * 类型查询 + +* **Item:** COL.001 + +* **Severity:** L1 + +* **Content:** 当表结构变更时,使用\*通配符选择所有列将导致查询的含义和行为会发生更改,可能导致查询返回更多的数据。 + +# Query: 68E48001ECD53152 + +★ ★ ★ ★ ☆ 85分 + +```sql + +SELECT + * +FROM + film +WHERE + LENGTH > 100 + AND title = 'ALABAMA DEVIL' +``` + +## Explain信息 + +| id | select\_type | table | partitions | type | possible_keys | key | key\_len | ref | rows | filtered | scalability | Extra | +|---|---|---|---|---|---|---|---|---|---|---|---|---| +| 1 | SIMPLE | *film* | NULL | ref | idx\_title | idx\_title | 767 | const | 1 | 33.33% | ☠️ **O(n)** | Using where | + + + +### Explain信息解读 + +#### SelectType信息解读 + +* **SIMPLE**: 简单SELECT(不使用UNION或子查询等). + +#### Type信息解读 + +* **ref**: 连接不能基于关键字选择单个行, 可能查找到多个符合条件的行. 叫做ref是因为索引要跟某个参考值相比较. 这个参考值或者是一个数, 或者是来自一个表里的多表查询的结果值. 例:'SELECT * FROM tbl WHERE idx_col=expr;'. + +#### Extra信息解读 + +* **Using where**: WHERE条件用于筛选出与下一个表匹配的数据然后返回给客户端. 除非故意做的全表扫描, 否则连接类型是ALL或者是index, 且在Extra列的值中没有Using Where, 则该查询可能是有问题的. + + +## 为sakila库的film表添加索引 + +* **Item:** IDX.001 + +* **Severity:** L2 + +* **Content:** 为列title添加索引,散粒度为: 100.00%; 为列length添加索引,散粒度为: 14.00%; + +* **Case:** ALTER TABLE \`sakila\`.\`film\` add index \`idx\_title\_length\` (\`title\`,\`length\`) ; + + + +## 不建议使用SELECT * 类型查询 + +* **Item:** COL.001 + +* **Severity:** L1 + +* **Content:** 当表结构变更时,使用\*通配符选择所有列将导致查询的含义和行为会发生更改,可能导致查询返回更多的数据。 + +# Query: 12FF1DAA3D425FA9 + +★ ★ ★ ★ ☆ 85分 + +```sql + +SELECT + * +FROM + film +WHERE + LENGTH > 100 + AND language_id < 10 + AND title = 'xyz' +``` + +## Explain信息 + +| id | select\_type | table | partitions | type | possible_keys | key | key\_len | ref | rows | filtered | scalability | Extra | +|---|---|---|---|---|---|---|---|---|---|---|---|---| +| 1 | SIMPLE | *film* | NULL | ref | idx\_title,
idx\_fk\_language\_id | idx\_title | 767 | const | 1 | 33.33% | ☠️ **O(n)** | Using where | + + + +### Explain信息解读 + +#### SelectType信息解读 + +* **SIMPLE**: 简单SELECT(不使用UNION或子查询等). + +#### Type信息解读 + +* **ref**: 连接不能基于关键字选择单个行, 可能查找到多个符合条件的行. 叫做ref是因为索引要跟某个参考值相比较. 这个参考值或者是一个数, 或者是来自一个表里的多表查询的结果值. 例:'SELECT * FROM tbl WHERE idx_col=expr;'. + +#### Extra信息解读 + +* **Using where**: WHERE条件用于筛选出与下一个表匹配的数据然后返回给客户端. 除非故意做的全表扫描, 否则连接类型是ALL或者是index, 且在Extra列的值中没有Using Where, 则该查询可能是有问题的. + + +## 为sakila库的film表添加索引 + +* **Item:** IDX.001 + +* **Severity:** L2 + +* **Content:** 为列title添加索引,散粒度为: 100.00%; 为列length添加索引,散粒度为: 14.00%; + +* **Case:** ALTER TABLE \`sakila\`.\`film\` add index \`idx\_title\_length\` (\`title\`,\`length\`) ; + + + +## 不建议使用SELECT * 类型查询 + +* **Item:** COL.001 + +* **Severity:** L1 + +* **Content:** 当表结构变更时,使用\*通配符选择所有列将导致查询的含义和行为会发生更改,可能导致查询返回更多的数据。 + +# Query: E84CBAAC2E12BDEA + +★ ★ ★ ★ ☆ 85分 + +```sql + +SELECT + * +FROM + film +WHERE + LENGTH > 100 + AND language_id < 10 +``` + +## Explain信息 + +| id | select\_type | table | partitions | type | possible_keys | key | key\_len | ref | rows | filtered | scalability | Extra | +|---|---|---|---|---|---|---|---|---|---|---|---|---| +| 1 | SIMPLE | *film* | NULL | ALL | idx\_fk\_language\_id | NULL | | | 1000 | 33.33% | ☠️ **O(n)** | Using where | + + + +### Explain信息解读 + +#### SelectType信息解读 + +* **SIMPLE**: 简单SELECT(不使用UNION或子查询等). + +#### Type信息解读 + +* ☠️ **ALL**: 最坏的情况, 从头到尾全表扫描. + +#### Extra信息解读 + +* **Using where**: WHERE条件用于筛选出与下一个表匹配的数据然后返回给客户端. 除非故意做的全表扫描, 否则连接类型是ALL或者是index, 且在Extra列的值中没有Using Where, 则该查询可能是有问题的. + + +## 为sakila库的film表添加索引 + +* **Item:** IDX.001 + +* **Severity:** L2 + +* **Content:** 为列length添加索引,散粒度为: 14.00%; + +* **Case:** ALTER TABLE \`sakila\`.\`film\` add index \`idx\_length\` (\`length\`) ; + + + +## 不建议使用SELECT * 类型查询 + +* **Item:** COL.001 + +* **Severity:** L1 + +* **Content:** 当表结构变更时,使用\*通配符选择所有列将导致查询的含义和行为会发生更改,可能导致查询返回更多的数据。 + +# Query: 6A0F035BD4E01018 + +★ ★ ★ ★ ☆ 80分 + +```sql + +SELECT + release_year, SUM( LENGTH) +FROM + film +WHERE + LENGTH = 123 + AND language_id = 1 +GROUP BY + release_year +``` + +## Explain信息 + +| id | select\_type | table | partitions | type | possible_keys | key | key\_len | ref | rows | filtered | scalability | Extra | +|---|---|---|---|---|---|---|---|---|---|---|---|---| +| 1 | SIMPLE | *film* | NULL | ALL | idx\_fk\_language\_id | NULL | | | 1000 | 10.00% | ☠️ **O(n)** | Using where; Using temporary | + + + +### Explain信息解读 + +#### SelectType信息解读 + +* **SIMPLE**: 简单SELECT(不使用UNION或子查询等). + +#### Type信息解读 + +* ☠️ **ALL**: 最坏的情况, 从头到尾全表扫描. + +#### Extra信息解读 + +* ☠️ **Using temporary**: 表示MySQL在对查询结果排序时使用临时表. 常见于排序order by和分组查询group by. + +* **Using where**: WHERE条件用于筛选出与下一个表匹配的数据然后返回给客户端. 除非故意做的全表扫描, 否则连接类型是ALL或者是index, 且在Extra列的值中没有Using Where, 则该查询可能是有问题的. + + +## 为sakila库的film表添加索引 + +* **Item:** IDX.001 + +* **Severity:** L2 + +* **Content:** 为列length添加索引,散粒度为: 14.00%; 为列language\_id添加索引,散粒度为: 0.10%; 为列release\_year添加索引,散粒度为: 0.10%; + +* **Case:** ALTER TABLE \`sakila\`.\`film\` add index \`idx\_length\_language\_id\_release\_year\` (\`length\`,\`language\_id\`,\`release\_year\`) ; + + + +## 请为GROUP BY显示添加ORDER BY条件 + +* **Item:** CLA.008 + +* **Severity:** L2 + +* **Content:** 默认MySQL会对'GROUP BY col1, col2, ...'请求按如下顺序排序'ORDER BY col1, col2, ...'。如果GROUP BY语句不指定ORDER BY条件会导致无谓的排序产生,如果不需要排序建议添加'ORDER BY NULL'。 + +# Query: 23D176AEA2947002 + +★ ★ ★ ★ ☆ 80分 + +```sql + +SELECT + release_year, SUM( LENGTH) +FROM + film +WHERE + LENGTH >= 123 +GROUP BY + release_year +``` + +## Explain信息 + +| id | select\_type | table | partitions | type | possible_keys | key | key\_len | ref | rows | filtered | scalability | Extra | +|---|---|---|---|---|---|---|---|---|---|---|---|---| +| 1 | SIMPLE | *film* | NULL | ALL | | NULL | | | 1000 | 33.33% | ☠️ **O(n)** | Using where; Using temporary | + + + +### Explain信息解读 + +#### SelectType信息解读 + +* **SIMPLE**: 简单SELECT(不使用UNION或子查询等). + +#### Type信息解读 + +* ☠️ **ALL**: 最坏的情况, 从头到尾全表扫描. + +#### Extra信息解读 + +* ☠️ **Using temporary**: 表示MySQL在对查询结果排序时使用临时表. 常见于排序order by和分组查询group by. + +* **Using where**: WHERE条件用于筛选出与下一个表匹配的数据然后返回给客户端. 除非故意做的全表扫描, 否则连接类型是ALL或者是index, 且在Extra列的值中没有Using Where, 则该查询可能是有问题的. + + +## 为sakila库的film表添加索引 + +* **Item:** IDX.001 + +* **Severity:** L2 + +* **Content:** 为列length添加索引,散粒度为: 14.00%; 为列release\_year添加索引,散粒度为: 0.10%; + +* **Case:** ALTER TABLE \`sakila\`.\`film\` add index \`idx\_length\_release\_year\` (\`length\`,\`release\_year\`) ; + + + +## 请为GROUP BY显示添加ORDER BY条件 + +* **Item:** CLA.008 + +* **Severity:** L2 + +* **Content:** 默认MySQL会对'GROUP BY col1, col2, ...'请求按如下顺序排序'ORDER BY col1, col2, ...'。如果GROUP BY语句不指定ORDER BY条件会导致无谓的排序产生,如果不需要排序建议添加'ORDER BY NULL'。 + +# Query: 73DDF6E6D9E40384 + +★ ★ ★ ☆ ☆ 60分 + +```sql + +SELECT + release_year, language_id, SUM( LENGTH) +FROM + film +GROUP BY + release_year, language_id +``` + +## Explain信息 + +| id | select\_type | table | partitions | type | possible_keys | key | key\_len | ref | rows | filtered | scalability | Extra | +|---|---|---|---|---|---|---|---|---|---|---|---|---| +| 1 | SIMPLE | *film* | NULL | ALL | | NULL | | | 1000 | ☠️ **100.00%** | ☠️ **O(n)** | Using temporary | + + + +### Explain信息解读 + +#### SelectType信息解读 + +* **SIMPLE**: 简单SELECT(不使用UNION或子查询等). + +#### Type信息解读 + +* ☠️ **ALL**: 最坏的情况, 从头到尾全表扫描. + +#### Extra信息解读 + +* ☠️ **Using temporary**: 表示MySQL在对查询结果排序时使用临时表. 常见于排序order by和分组查询group by. + + +## 为sakila库的film表添加索引 + +* **Item:** IDX.001 + +* **Severity:** L2 + +* **Content:** 为列release\_year添加索引,散粒度为: 0.10%; 为列language\_id添加索引,散粒度为: 0.10%; + +* **Case:** ALTER TABLE \`sakila\`.\`film\` add index \`idx\_release\_year\_language\_id\` (\`release\_year\`,\`language\_id\`) ; + + + +## SELECT未指定WHERE条件 + +* **Item:** CLA.001 + +* **Severity:** L4 + +* **Content:** SELECT语句没有WHERE子句,可能检查比预期更多的行(全表扫描)。对于SELECT COUNT(\*)类型的请求如果不要求精度,建议使用SHOW TABLE STATUS或EXPLAIN替代。 + +## 请为GROUP BY显示添加ORDER BY条件 + +* **Item:** CLA.008 + +* **Severity:** L2 + +* **Content:** 默认MySQL会对'GROUP BY col1, col2, ...'请求按如下顺序排序'ORDER BY col1, col2, ...'。如果GROUP BY语句不指定ORDER BY条件会导致无谓的排序产生,如果不需要排序建议添加'ORDER BY NULL'。 + +# Query: B3C502B4AA344196 + +★ ★ ★ ☆ ☆ 70分 + +```sql + +SELECT + release_year, SUM( LENGTH) +FROM + film +WHERE + LENGTH = 123 +GROUP BY + release_year, (LENGTH+ language_id) +``` + +## Explain信息 + +| id | select\_type | table | partitions | type | possible_keys | key | key\_len | ref | rows | filtered | scalability | Extra | +|---|---|---|---|---|---|---|---|---|---|---|---|---| +| 1 | SIMPLE | *film* | NULL | ALL | | NULL | | | 1000 | 10.00% | ☠️ **O(n)** | Using where; Using temporary | + + + +### Explain信息解读 + +#### SelectType信息解读 + +* **SIMPLE**: 简单SELECT(不使用UNION或子查询等). + +#### Type信息解读 + +* ☠️ **ALL**: 最坏的情况, 从头到尾全表扫描. + +#### Extra信息解读 + +* ☠️ **Using temporary**: 表示MySQL在对查询结果排序时使用临时表. 常见于排序order by和分组查询group by. + +* **Using where**: WHERE条件用于筛选出与下一个表匹配的数据然后返回给客户端. 除非故意做的全表扫描, 否则连接类型是ALL或者是index, 且在Extra列的值中没有Using Where, 则该查询可能是有问题的. + + +## 为sakila库的film表添加索引 + +* **Item:** IDX.001 + +* **Severity:** L2 + +* **Content:** 为列length添加索引,散粒度为: 14.00%; + +* **Case:** ALTER TABLE \`sakila\`.\`film\` add index \`idx\_length\` (\`length\`) ; + + + +## 请为GROUP BY显示添加ORDER BY条件 + +* **Item:** CLA.008 + +* **Severity:** L2 + +* **Content:** 默认MySQL会对'GROUP BY col1, col2, ...'请求按如下顺序排序'ORDER BY col1, col2, ...'。如果GROUP BY语句不指定ORDER BY条件会导致无谓的排序产生,如果不需要排序建议添加'ORDER BY NULL'。 + +## GROUP BY的条件为表达式 + +* **Item:** CLA.010 + +* **Severity:** L2 + +* **Content:** 当GROUP BY条件为表达式或函数时会使用到临时表,如果在未指定WHERE或WHERE条件返回的结果集较大时性能会很差。 + +# Query: 47044E1FE1A965A5 + +★ ★ ★ ☆ ☆ 60分 + +```sql + +SELECT + release_year, SUM( film_id) +FROM + film +GROUP BY + release_year +``` + +## Explain信息 + +| id | select\_type | table | partitions | type | possible_keys | key | key\_len | ref | rows | filtered | scalability | Extra | +|---|---|---|---|---|---|---|---|---|---|---|---|---| +| 1 | SIMPLE | *film* | NULL | ALL | | NULL | | | 1000 | ☠️ **100.00%** | ☠️ **O(n)** | Using temporary | + + + +### Explain信息解读 + +#### SelectType信息解读 + +* **SIMPLE**: 简单SELECT(不使用UNION或子查询等). + +#### Type信息解读 + +* ☠️ **ALL**: 最坏的情况, 从头到尾全表扫描. + +#### Extra信息解读 + +* ☠️ **Using temporary**: 表示MySQL在对查询结果排序时使用临时表. 常见于排序order by和分组查询group by. + + +## 为sakila库的film表添加索引 + +* **Item:** IDX.001 + +* **Severity:** L2 + +* **Content:** 为列release\_year添加索引,散粒度为: 0.10%; + +* **Case:** ALTER TABLE \`sakila\`.\`film\` add index \`idx\_release\_year\` (\`release\_year\`) ; + + + +## SELECT未指定WHERE条件 + +* **Item:** CLA.001 + +* **Severity:** L4 + +* **Content:** SELECT语句没有WHERE子句,可能检查比预期更多的行(全表扫描)。对于SELECT COUNT(\*)类型的请求如果不要求精度,建议使用SHOW TABLE STATUS或EXPLAIN替代。 + +## 请为GROUP BY显示添加ORDER BY条件 + +* **Item:** CLA.008 + +* **Severity:** L2 + +* **Content:** 默认MySQL会对'GROUP BY col1, col2, ...'请求按如下顺序排序'ORDER BY col1, col2, ...'。如果GROUP BY语句不指定ORDER BY条件会导致无谓的排序产生,如果不需要排序建议添加'ORDER BY NULL'。 + +# Query: 2BA1217F6C8CF0AB + +☆ ☆ ☆ ☆ ☆ 0分 + +```sql + +SELECT + * +FROM + address +GROUP BY + address, district +``` + +## MySQL返回信息 + +Expression #1 of SELECT list is not in GROUP BY clause and contains nonaggregated column 'optimizer_RSq3xBEF0TXgZsHj.address.address_id' which is not functionally dependent on columns in GROUP BY clause; this is incompatible with sql_mode=only_full_group_by + +## 为sakila库的address表添加索引 + +* **Item:** IDX.001 + +* **Severity:** L2 + +* **Content:** 为列address添加索引,散粒度为: 100.00%; 为列district添加索引,散粒度为: 100.00%; + +* **Case:** ALTER TABLE \`sakila\`.\`address\` add index \`idx\_address\_district\` (\`address\`,\`district\`) ; + + + +## SELECT未指定WHERE条件 + +* **Item:** CLA.001 + +* **Severity:** L4 + +* **Content:** SELECT语句没有WHERE子句,可能检查比预期更多的行(全表扫描)。对于SELECT COUNT(\*)类型的请求如果不要求精度,建议使用SHOW TABLE STATUS或EXPLAIN替代。 + +## 请为GROUP BY显示添加ORDER BY条件 + +* **Item:** CLA.008 + +* **Severity:** L2 + +* **Content:** 默认MySQL会对'GROUP BY col1, col2, ...'请求按如下顺序排序'ORDER BY col1, col2, ...'。如果GROUP BY语句不指定ORDER BY条件会导致无谓的排序产生,如果不需要排序建议添加'ORDER BY NULL'。 + +## 不建议使用SELECT * 类型查询 + +* **Item:** COL.001 + +* **Severity:** L1 + +* **Content:** 当表结构变更时,使用\*通配符选择所有列将导致查询的含义和行为会发生更改,可能导致查询返回更多的数据。 + +## 非确定性的GROUP BY + +* **Item:** RES.001 + +* **Severity:** L4 + +* **Content:** SQL返回的列既不在聚合函数中也不是GROUP BY表达式的列中,因此这些值的结果将是非确定性的。如:select a, b, c from tbl where foo="bar" group by a,该SQL返回的结果就是不确定的。 + +# Query: 863A85207E4F410D + +★ ★ ★ ★ ☆ 80分 + +```sql + +SELECT + title +FROM + film +WHERE + ABS( language_id) = 3 +GROUP BY + title +``` + +## Explain信息 + +| id | select\_type | table | partitions | type | possible_keys | key | key\_len | ref | rows | filtered | scalability | Extra | +|---|---|---|---|---|---|---|---|---|---|---|---|---| +| 1 | SIMPLE | *film* | NULL | index | idx\_title | idx\_title | 767 | | 1000 | ☠️ **100.00%** | ☠️ **O(n)** | Using where | + + + +### Explain信息解读 + +#### SelectType信息解读 + +* **SIMPLE**: 简单SELECT(不使用UNION或子查询等). + +#### Type信息解读 + +* **index**: 全表扫描, 只是扫描表的时候按照索引次序进行而不是行. 主要优点就是避免了排序, 但是开销仍然非常大. + +#### Extra信息解读 + +* **Using where**: WHERE条件用于筛选出与下一个表匹配的数据然后返回给客户端. 除非故意做的全表扫描, 否则连接类型是ALL或者是index, 且在Extra列的值中没有Using Where, 则该查询可能是有问题的. + + +## 请为GROUP BY显示添加ORDER BY条件 + +* **Item:** CLA.008 + +* **Severity:** L2 + +* **Content:** 默认MySQL会对'GROUP BY col1, col2, ...'请求按如下顺序排序'ORDER BY col1, col2, ...'。如果GROUP BY语句不指定ORDER BY条件会导致无谓的排序产生,如果不需要排序建议添加'ORDER BY NULL'。 + +## 避免在WHERE条件中使用函数或其他运算符 + +* **Item:** FUN.001 + +* **Severity:** L2 + +* **Content:** 虽然在SQL中使用函数可以简化很多复杂的查询,但使用了函数的查询无法利用表中已经建立的索引,该查询将会是全表扫描,性能较差。通常建议将列名写在比较运算符左侧,将查询过滤条件放在比较运算符右侧。 + +# Query: DF59FD602E4AA368 + +☆ ☆ ☆ ☆ ☆ 0分 + +```sql + +SELECT + language_id +FROM + film +WHERE + LENGTH = 123 +GROUP BY + release_year +ORDER BY + language_id +``` + +## MySQL返回信息 + +Expression #1 of SELECT list is not in GROUP BY clause and contains nonaggregated column 'optimizer_RSq3xBEF0TXgZsHj.film.language_id' which is not functionally dependent on columns in GROUP BY clause; this is incompatible with sql_mode=only_full_group_by + +## 为sakila库的film表添加索引 + +* **Item:** IDX.001 + +* **Severity:** L2 + +* **Content:** 为列length添加索引,散粒度为: 14.00%; 为列release\_year添加索引,散粒度为: 0.10%; + +* **Case:** ALTER TABLE \`sakila\`.\`film\` add index \`idx\_length\_release\_year\` (\`length\`,\`release\_year\`) ; + + + +## 非确定性的GROUP BY + +* **Item:** RES.001 + +* **Severity:** L4 + +* **Content:** SQL返回的列既不在聚合函数中也不是GROUP BY表达式的列中,因此这些值的结果将是非确定性的。如:select a, b, c from tbl where foo="bar" group by a,该SQL返回的结果就是不确定的。 + +# Query: F6DBEAA606D800FC + +★ ★ ★ ★ ☆ 90分 + +```sql + +SELECT + release_year +FROM + film +WHERE + LENGTH = 123 +GROUP BY + release_year +ORDER BY + release_year +``` + +## Explain信息 + +| id | select\_type | table | partitions | type | possible_keys | key | key\_len | ref | rows | filtered | scalability | Extra | +|---|---|---|---|---|---|---|---|---|---|---|---|---| +| 1 | SIMPLE | *film* | NULL | ALL | | NULL | | | 1000 | 10.00% | ☠️ **O(n)** | Using where; Using temporary; Using filesort | + + + +### Explain信息解读 + +#### SelectType信息解读 + +* **SIMPLE**: 简单SELECT(不使用UNION或子查询等). + +#### Type信息解读 + +* ☠️ **ALL**: 最坏的情况, 从头到尾全表扫描. + +#### Extra信息解读 + +* ☠️ **Using filesort**: MySQL会对结果使用一个外部索引排序,而不是从表里按照索引次序读到相关内容. 可能在内存或者磁盘上进行排序. MySQL中无法利用索引完成的排序操作称为'文件排序'. + +* ☠️ **Using temporary**: 表示MySQL在对查询结果排序时使用临时表. 常见于排序order by和分组查询group by. + +* **Using where**: WHERE条件用于筛选出与下一个表匹配的数据然后返回给客户端. 除非故意做的全表扫描, 否则连接类型是ALL或者是index, 且在Extra列的值中没有Using Where, 则该查询可能是有问题的. + + +## 为sakila库的film表添加索引 + +* **Item:** IDX.001 + +* **Severity:** L2 + +* **Content:** 为列length添加索引,散粒度为: 14.00%; 为列release\_year添加索引,散粒度为: 0.10%; + +* **Case:** ALTER TABLE \`sakila\`.\`film\` add index \`idx\_length\_release\_year\` (\`length\`,\`release\_year\`) ; + + + +# Query: 6E9B96CA3F0E6BDA + +★ ★ ☆ ☆ ☆ 55分 + +```sql + +SELECT + * +FROM + film +WHERE + LENGTH = 123 +ORDER BY + release_year ASC, language_id DESC +``` + +## Explain信息 + +| id | select\_type | table | partitions | type | possible_keys | key | key\_len | ref | rows | filtered | scalability | Extra | +|---|---|---|---|---|---|---|---|---|---|---|---|---| +| 1 | SIMPLE | *film* | NULL | ALL | | NULL | | | 1000 | 10.00% | ☠️ **O(n)** | Using where; Using filesort | + + + +### Explain信息解读 + +#### SelectType信息解读 + +* **SIMPLE**: 简单SELECT(不使用UNION或子查询等). + +#### Type信息解读 + +* ☠️ **ALL**: 最坏的情况, 从头到尾全表扫描. + +#### Extra信息解读 + +* ☠️ **Using filesort**: MySQL会对结果使用一个外部索引排序,而不是从表里按照索引次序读到相关内容. 可能在内存或者磁盘上进行排序. MySQL中无法利用索引完成的排序操作称为'文件排序'. + +* **Using where**: WHERE条件用于筛选出与下一个表匹配的数据然后返回给客户端. 除非故意做的全表扫描, 否则连接类型是ALL或者是index, 且在Extra列的值中没有Using Where, 则该查询可能是有问题的. + + +## 为sakila库的film表添加索引 + +* **Item:** IDX.001 + +* **Severity:** L2 + +* **Content:** 为列length添加索引,散粒度为: 14.00%; + +* **Case:** ALTER TABLE \`sakila\`.\`film\` add index \`idx\_length\` (\`length\`) ; + + + +## ORDER BY语句对多个不同条件使用不同方向的排序无法使用索引 + +* **Item:** CLA.007 + +* **Severity:** L2 + +* **Content:** ORDER BY子句中的所有表达式必须按统一的ASC或DESC方向排序,以便利用索引。 + +## 不建议使用SELECT * 类型查询 + +* **Item:** COL.001 + +* **Severity:** L1 + +* **Content:** 当表结构变更时,使用\*通配符选择所有列将导致查询的含义和行为会发生更改,可能导致查询返回更多的数据。 + +## ORDER BY多个列但排序方向不同时可能无法使用索引 + +* **Item:** KEY.008 + +* **Severity:** L4 + +* **Content:** 在MySQL 8.0之前当ORDER BY多个列指定的排序方向不同时将无法使用已经建立的索引。 + +# Query: 2EAACFD7030EA528 + +★ ★ ★ ★ ☆ 90分 + +```sql + +SELECT + release_year +FROM + film +WHERE + LENGTH = 123 +GROUP BY + release_year +ORDER BY + release_year +LIMIT + 10 +``` + +## Explain信息 + +| id | select\_type | table | partitions | type | possible_keys | key | key\_len | ref | rows | filtered | scalability | Extra | +|---|---|---|---|---|---|---|---|---|---|---|---|---| +| 1 | SIMPLE | *film* | NULL | ALL | | NULL | | | 1000 | 10.00% | ☠️ **O(n)** | Using where; Using temporary; Using filesort | + + + +### Explain信息解读 + +#### SelectType信息解读 + +* **SIMPLE**: 简单SELECT(不使用UNION或子查询等). + +#### Type信息解读 + +* ☠️ **ALL**: 最坏的情况, 从头到尾全表扫描. + +#### Extra信息解读 + +* ☠️ **Using filesort**: MySQL会对结果使用一个外部索引排序,而不是从表里按照索引次序读到相关内容. 可能在内存或者磁盘上进行排序. MySQL中无法利用索引完成的排序操作称为'文件排序'. + +* ☠️ **Using temporary**: 表示MySQL在对查询结果排序时使用临时表. 常见于排序order by和分组查询group by. + +* **Using where**: WHERE条件用于筛选出与下一个表匹配的数据然后返回给客户端. 除非故意做的全表扫描, 否则连接类型是ALL或者是index, 且在Extra列的值中没有Using Where, 则该查询可能是有问题的. + + +## 为sakila库的film表添加索引 + +* **Item:** IDX.001 + +* **Severity:** L2 + +* **Content:** 为列length添加索引,散粒度为: 14.00%; 为列release\_year添加索引,散粒度为: 0.10%; + +* **Case:** ALTER TABLE \`sakila\`.\`film\` add index \`idx\_length\_release\_year\` (\`length\`,\`release\_year\`) ; + + + +# Query: 5CE2F187DBF2A710 + +★ ★ ★ ★ ☆ 85分 + +```sql + +SELECT + * +FROM + film +WHERE + LENGTH = 123 +ORDER BY + release_year +LIMIT + 10 +``` + +## Explain信息 + +| id | select\_type | table | partitions | type | possible_keys | key | key\_len | ref | rows | filtered | scalability | Extra | +|---|---|---|---|---|---|---|---|---|---|---|---|---| +| 1 | SIMPLE | *film* | NULL | ALL | | NULL | | | 1000 | 10.00% | ☠️ **O(n)** | Using where; Using filesort | + + + +### Explain信息解读 + +#### SelectType信息解读 + +* **SIMPLE**: 简单SELECT(不使用UNION或子查询等). + +#### Type信息解读 + +* ☠️ **ALL**: 最坏的情况, 从头到尾全表扫描. + +#### Extra信息解读 + +* ☠️ **Using filesort**: MySQL会对结果使用一个外部索引排序,而不是从表里按照索引次序读到相关内容. 可能在内存或者磁盘上进行排序. MySQL中无法利用索引完成的排序操作称为'文件排序'. + +* **Using where**: WHERE条件用于筛选出与下一个表匹配的数据然后返回给客户端. 除非故意做的全表扫描, 否则连接类型是ALL或者是index, 且在Extra列的值中没有Using Where, 则该查询可能是有问题的. + + +## 为sakila库的film表添加索引 + +* **Item:** IDX.001 + +* **Severity:** L2 + +* **Content:** 为列length添加索引,散粒度为: 14.00%; 为列release\_year添加索引,散粒度为: 0.10%; + +* **Case:** ALTER TABLE \`sakila\`.\`film\` add index \`idx\_length\_release\_year\` (\`length\`,\`release\_year\`) ; + + + +## 不建议使用SELECT * 类型查询 + +* **Item:** COL.001 + +* **Severity:** L1 + +* **Content:** 当表结构变更时,使用\*通配符选择所有列将导致查询的含义和行为会发生更改,可能导致查询返回更多的数据。 + +# Query: E75234155B5E2E14 + +★ ★ ★ ☆ ☆ 65分 + +```sql + +SELECT + * +FROM + film +ORDER BY + release_year +LIMIT + 10 +``` + +## Explain信息 + +| id | select\_type | table | partitions | type | possible_keys | key | key\_len | ref | rows | filtered | scalability | Extra | +|---|---|---|---|---|---|---|---|---|---|---|---|---| +| 1 | SIMPLE | *film* | NULL | ALL | | NULL | | | 1000 | ☠️ **100.00%** | ☠️ **O(n)** | Using filesort | + + + +### Explain信息解读 + +#### SelectType信息解读 + +* **SIMPLE**: 简单SELECT(不使用UNION或子查询等). + +#### Type信息解读 + +* ☠️ **ALL**: 最坏的情况, 从头到尾全表扫描. + +#### Extra信息解读 + +* ☠️ **Using filesort**: MySQL会对结果使用一个外部索引排序,而不是从表里按照索引次序读到相关内容. 可能在内存或者磁盘上进行排序. MySQL中无法利用索引完成的排序操作称为'文件排序'. + + +## 为sakila库的film表添加索引 + +* **Item:** IDX.001 + +* **Severity:** L2 + +* **Content:** 为列release\_year添加索引,散粒度为: 0.10%; + +* **Case:** ALTER TABLE \`sakila\`.\`film\` add index \`idx\_release\_year\` (\`release\_year\`) ; + + + +## SELECT未指定WHERE条件 + +* **Item:** CLA.001 + +* **Severity:** L4 + +* **Content:** SELECT语句没有WHERE子句,可能检查比预期更多的行(全表扫描)。对于SELECT COUNT(\*)类型的请求如果不要求精度,建议使用SHOW TABLE STATUS或EXPLAIN替代。 + +## 不建议使用SELECT * 类型查询 + +* **Item:** COL.001 + +* **Severity:** L1 + +* **Content:** 当表结构变更时,使用\*通配符选择所有列将导致查询的含义和行为会发生更改,可能导致查询返回更多的数据。 + +# Query: 965D5AC955824512 + +★ ★ ★ ★ ☆ 85分 + +```sql + +SELECT + * +FROM + film +WHERE + LENGTH > 100 +ORDER BY + LENGTH +LIMIT + 10 +``` + +## Explain信息 + +| id | select\_type | table | partitions | type | possible_keys | key | key\_len | ref | rows | filtered | scalability | Extra | +|---|---|---|---|---|---|---|---|---|---|---|---|---| +| 1 | SIMPLE | *film* | NULL | ALL | | NULL | | | 1000 | 33.33% | ☠️ **O(n)** | Using where; Using filesort | + + + +### Explain信息解读 + +#### SelectType信息解读 + +* **SIMPLE**: 简单SELECT(不使用UNION或子查询等). + +#### Type信息解读 + +* ☠️ **ALL**: 最坏的情况, 从头到尾全表扫描. + +#### Extra信息解读 + +* ☠️ **Using filesort**: MySQL会对结果使用一个外部索引排序,而不是从表里按照索引次序读到相关内容. 可能在内存或者磁盘上进行排序. MySQL中无法利用索引完成的排序操作称为'文件排序'. + +* **Using where**: WHERE条件用于筛选出与下一个表匹配的数据然后返回给客户端. 除非故意做的全表扫描, 否则连接类型是ALL或者是index, 且在Extra列的值中没有Using Where, 则该查询可能是有问题的. + + +## 为sakila库的film表添加索引 + +* **Item:** IDX.001 + +* **Severity:** L2 + +* **Content:** 为列length添加索引,散粒度为: 14.00%; + +* **Case:** ALTER TABLE \`sakila\`.\`film\` add index \`idx\_length\` (\`length\`) ; + + + +## 不建议使用SELECT * 类型查询 + +* **Item:** COL.001 + +* **Severity:** L1 + +* **Content:** 当表结构变更时,使用\*通配符选择所有列将导致查询的含义和行为会发生更改,可能导致查询返回更多的数据。 + +# Query: 1E2CF4145EE706A5 + +★ ★ ★ ★ ☆ 85分 + +```sql + +SELECT + * +FROM + film +WHERE + LENGTH < 100 +ORDER BY + LENGTH +LIMIT + 10 +``` + +## Explain信息 + +| id | select\_type | table | partitions | type | possible_keys | key | key\_len | ref | rows | filtered | scalability | Extra | +|---|---|---|---|---|---|---|---|---|---|---|---|---| +| 1 | SIMPLE | *film* | NULL | ALL | | NULL | | | 1000 | 33.33% | ☠️ **O(n)** | Using where; Using filesort | + + + +### Explain信息解读 + +#### SelectType信息解读 + +* **SIMPLE**: 简单SELECT(不使用UNION或子查询等). + +#### Type信息解读 + +* ☠️ **ALL**: 最坏的情况, 从头到尾全表扫描. + +#### Extra信息解读 + +* ☠️ **Using filesort**: MySQL会对结果使用一个外部索引排序,而不是从表里按照索引次序读到相关内容. 可能在内存或者磁盘上进行排序. MySQL中无法利用索引完成的排序操作称为'文件排序'. + +* **Using where**: WHERE条件用于筛选出与下一个表匹配的数据然后返回给客户端. 除非故意做的全表扫描, 否则连接类型是ALL或者是index, 且在Extra列的值中没有Using Where, 则该查询可能是有问题的. + + +## 为sakila库的film表添加索引 + +* **Item:** IDX.001 + +* **Severity:** L2 + +* **Content:** 为列length添加索引,散粒度为: 14.00%; + +* **Case:** ALTER TABLE \`sakila\`.\`film\` add index \`idx\_length\` (\`length\`) ; + + + +## 不建议使用SELECT * 类型查询 + +* **Item:** COL.001 + +* **Severity:** L1 + +* **Content:** 当表结构变更时,使用\*通配符选择所有列将导致查询的含义和行为会发生更改,可能导致查询返回更多的数据。 + +# Query: A314542EEE8571EE + +★ ★ ★ ★ ☆ 85分 + +```sql + +SELECT + * +FROM + customer +WHERE + address_id in (224, 510) +ORDER BY + last_name +``` + +## Explain信息 + +| id | select\_type | table | partitions | type | possible_keys | key | key\_len | ref | rows | filtered | scalability | Extra | +|---|---|---|---|---|---|---|---|---|---|---|---|---| +| 1 | SIMPLE | *customer* | NULL | range | idx\_fk\_address\_id | idx\_fk\_address\_id | 2 | | 2 | ☠️ **100.00%** | ☠️ **O(n)** | Using index condition; Using filesort | + + + +### Explain信息解读 + +#### SelectType信息解读 + +* **SIMPLE**: 简单SELECT(不使用UNION或子查询等). + +#### Type信息解读 + +* **range**: 只检索给定范围的行, 使用一个索引来选择行. key列显示使用了哪个索引. key_len包含所使用索引的最长关键元素. + +#### Extra信息解读 + +* ☠️ **Using filesort**: MySQL会对结果使用一个外部索引排序,而不是从表里按照索引次序读到相关内容. 可能在内存或者磁盘上进行排序. MySQL中无法利用索引完成的排序操作称为'文件排序'. + +* **Using index**: 只需通过索引就可以从表中获取列的信息, 无需额外去读取真实的行数据. 如果查询使用的列值仅仅是一个简单索引的部分值, 则会使用这种策略来优化查询. + +* **Using index condition**: 在5.6版本后加入的新特性(Index Condition Pushdown)。Using index condition 会先条件过滤索引,过滤完索引后找到所有符合索引条件的数据行,随后用 WHERE 子句中的其他条件去过滤这些数据行。 + + +## 为sakila库的customer表添加索引 + +* **Item:** IDX.001 + +* **Severity:** L2 + +* **Content:** 为列address\_id添加索引,散粒度为: 100.00%; 为列last\_name添加索引,散粒度为: 100.00%; + +* **Case:** ALTER TABLE \`sakila\`.\`customer\` add index \`idx\_address\_id\_last\_name\` (\`address\_id\`,\`last\_name\`) ; + + + +## 不建议使用SELECT * 类型查询 + +* **Item:** COL.001 + +* **Severity:** L1 + +* **Content:** 当表结构变更时,使用\*通配符选择所有列将导致查询的含义和行为会发生更改,可能导致查询返回更多的数据。 + +# Query: 0BE2D79E2F1E7CB0 + +★ ★ ★ ★ ☆ 85分 + +```sql + +SELECT + * +FROM + film +WHERE + release_year = 2016 + AND LENGTH != 1 +ORDER BY + title +``` + +## Explain信息 + +| id | select\_type | table | partitions | type | possible_keys | key | key\_len | ref | rows | filtered | scalability | Extra | +|---|---|---|---|---|---|---|---|---|---|---|---|---| +| 1 | SIMPLE | *film* | NULL | ALL | | NULL | | | 1000 | 9.00% | ☠️ **O(n)** | Using where; Using filesort | + + + +### Explain信息解读 + +#### SelectType信息解读 + +* **SIMPLE**: 简单SELECT(不使用UNION或子查询等). + +#### Type信息解读 + +* ☠️ **ALL**: 最坏的情况, 从头到尾全表扫描. + +#### Extra信息解读 + +* ☠️ **Using filesort**: MySQL会对结果使用一个外部索引排序,而不是从表里按照索引次序读到相关内容. 可能在内存或者磁盘上进行排序. MySQL中无法利用索引完成的排序操作称为'文件排序'. + +* **Using where**: WHERE条件用于筛选出与下一个表匹配的数据然后返回给客户端. 除非故意做的全表扫描, 否则连接类型是ALL或者是index, 且在Extra列的值中没有Using Where, 则该查询可能是有问题的. + + +## 为sakila库的film表添加索引 + +* **Item:** IDX.001 + +* **Severity:** L2 + +* **Content:** 为列release\_year添加索引,散粒度为: 0.10%; 为列length添加索引,散粒度为: 14.00%; 为列title添加索引,散粒度为: 100.00%; + +* **Case:** ALTER TABLE \`sakila\`.\`film\` add index \`idx\_release\_year\_length\_title\` (\`release\_year\`,\`length\`,\`title\`) ; + + + +## 不建议使用SELECT * 类型查询 + +* **Item:** COL.001 + +* **Severity:** L1 + +* **Content:** 当表结构变更时,使用\*通配符选择所有列将导致查询的含义和行为会发生更改,可能导致查询返回更多的数据。 + +## '!=' 运算符是非标准的 + +* **Item:** STA.001 + +* **Severity:** L0 + +* **Content:** "<>"才是标准SQL中的不等于运算符。 + +# Query: 4E73AA068370E6A8 + +★ ★ ★ ★ ☆ 90分 + +```sql + +SELECT + title +FROM + film +WHERE + release_year = 1995 +``` + +## Explain信息 + +| id | select\_type | table | partitions | type | possible_keys | key | key\_len | ref | rows | filtered | scalability | Extra | +|---|---|---|---|---|---|---|---|---|---|---|---|---| +| 1 | SIMPLE | *film* | NULL | ALL | | NULL | | | 1000 | 10.00% | ☠️ **O(n)** | Using where | + + + +### Explain信息解读 + +#### SelectType信息解读 + +* **SIMPLE**: 简单SELECT(不使用UNION或子查询等). + +#### Type信息解读 + +* ☠️ **ALL**: 最坏的情况, 从头到尾全表扫描. + +#### Extra信息解读 + +* **Using where**: WHERE条件用于筛选出与下一个表匹配的数据然后返回给客户端. 除非故意做的全表扫描, 否则连接类型是ALL或者是index, 且在Extra列的值中没有Using Where, 则该查询可能是有问题的. + + +## 为sakila库的film表添加索引 + +* **Item:** IDX.001 + +* **Severity:** L2 + +* **Content:** 为列release\_year添加索引,散粒度为: 0.10%; + +* **Case:** ALTER TABLE \`sakila\`.\`film\` add index \`idx\_release\_year\` (\`release\_year\`) ; + + + +# Query: BA7111449E4F1122 + +★ ★ ★ ★ ☆ 90分 + +```sql + +SELECT + title, replacement_cost +FROM + film +WHERE + language_id = 5 + AND LENGTH = 70 +``` + +## Explain信息 + +| id | select\_type | table | partitions | type | possible_keys | key | key\_len | ref | rows | filtered | scalability | Extra | +|---|---|---|---|---|---|---|---|---|---|---|---|---| +| 1 | SIMPLE | *film* | NULL | ref | idx\_fk\_language\_id | idx\_fk\_language\_id | 1 | const | 1 | 10.00% | ☠️ **O(n)** | Using where | + + + +### Explain信息解读 + +#### SelectType信息解读 + +* **SIMPLE**: 简单SELECT(不使用UNION或子查询等). + +#### Type信息解读 + +* **ref**: 连接不能基于关键字选择单个行, 可能查找到多个符合条件的行. 叫做ref是因为索引要跟某个参考值相比较. 这个参考值或者是一个数, 或者是来自一个表里的多表查询的结果值. 例:'SELECT * FROM tbl WHERE idx_col=expr;'. + +#### Extra信息解读 + +* **Using where**: WHERE条件用于筛选出与下一个表匹配的数据然后返回给客户端. 除非故意做的全表扫描, 否则连接类型是ALL或者是index, 且在Extra列的值中没有Using Where, 则该查询可能是有问题的. + + +## 为sakila库的film表添加索引 + +* **Item:** IDX.001 + +* **Severity:** L2 + +* **Content:** 为列length添加索引,散粒度为: 14.00%; 为列language\_id添加索引,散粒度为: 0.10%; + +* **Case:** ALTER TABLE \`sakila\`.\`film\` add index \`idx\_length\_language\_id\` (\`length\`,\`language\_id\`) ; + + + +# Query: B13E0ACEAF8F3119 + +★ ★ ★ ★ ☆ 90分 + +```sql + +SELECT + title +FROM + film +WHERE + language_id > 5 + AND LENGTH > 70 +``` + +## Explain信息 + +| id | select\_type | table | partitions | type | possible_keys | key | key\_len | ref | rows | filtered | scalability | Extra | +|---|---|---|---|---|---|---|---|---|---|---|---|---| +| 1 | SIMPLE | *film* | NULL | range | idx\_fk\_language\_id | idx\_fk\_language\_id | 1 | | 1 | 33.33% | ☠️ **O(n)** | Using index condition; Using where | + + + +### Explain信息解读 + +#### SelectType信息解读 + +* **SIMPLE**: 简单SELECT(不使用UNION或子查询等). + +#### Type信息解读 + +* **range**: 只检索给定范围的行, 使用一个索引来选择行. key列显示使用了哪个索引. key_len包含所使用索引的最长关键元素. + +#### Extra信息解读 + +* **Using index**: 只需通过索引就可以从表中获取列的信息, 无需额外去读取真实的行数据. 如果查询使用的列值仅仅是一个简单索引的部分值, 则会使用这种策略来优化查询. + +* **Using index condition**: 在5.6版本后加入的新特性(Index Condition Pushdown)。Using index condition 会先条件过滤索引,过滤完索引后找到所有符合索引条件的数据行,随后用 WHERE 子句中的其他条件去过滤这些数据行。 + +* **Using where**: WHERE条件用于筛选出与下一个表匹配的数据然后返回给客户端. 除非故意做的全表扫描, 否则连接类型是ALL或者是index, 且在Extra列的值中没有Using Where, 则该查询可能是有问题的. + + +## 为sakila库的film表添加索引 + +* **Item:** IDX.001 + +* **Severity:** L2 + +* **Content:** 为列length添加索引,散粒度为: 14.00%; + +* **Case:** ALTER TABLE \`sakila\`.\`film\` add index \`idx\_length\` (\`length\`) ; + + + +# Query: A3FAB6027484B88B + +★ ★ ★ ★ ☆ 85分 + +```sql + +SELECT + * +FROM + film +WHERE + LENGTH = 100 + AND title = 'xyz' +ORDER BY + release_year +``` + +## Explain信息 + +| id | select\_type | table | partitions | type | possible_keys | key | key\_len | ref | rows | filtered | scalability | Extra | +|---|---|---|---|---|---|---|---|---|---|---|---|---| +| 1 | SIMPLE | *film* | NULL | ref | idx\_title | idx\_title | 767 | const | 1 | 10.00% | ☠️ **O(n)** | Using index condition; Using where; Using filesort | + + + +### Explain信息解读 + +#### SelectType信息解读 + +* **SIMPLE**: 简单SELECT(不使用UNION或子查询等). + +#### Type信息解读 + +* **ref**: 连接不能基于关键字选择单个行, 可能查找到多个符合条件的行. 叫做ref是因为索引要跟某个参考值相比较. 这个参考值或者是一个数, 或者是来自一个表里的多表查询的结果值. 例:'SELECT * FROM tbl WHERE idx_col=expr;'. + +#### Extra信息解读 + +* ☠️ **Using filesort**: MySQL会对结果使用一个外部索引排序,而不是从表里按照索引次序读到相关内容. 可能在内存或者磁盘上进行排序. MySQL中无法利用索引完成的排序操作称为'文件排序'. + +* **Using index**: 只需通过索引就可以从表中获取列的信息, 无需额外去读取真实的行数据. 如果查询使用的列值仅仅是一个简单索引的部分值, 则会使用这种策略来优化查询. + +* **Using index condition**: 在5.6版本后加入的新特性(Index Condition Pushdown)。Using index condition 会先条件过滤索引,过滤完索引后找到所有符合索引条件的数据行,随后用 WHERE 子句中的其他条件去过滤这些数据行。 + +* **Using where**: WHERE条件用于筛选出与下一个表匹配的数据然后返回给客户端. 除非故意做的全表扫描, 否则连接类型是ALL或者是index, 且在Extra列的值中没有Using Where, 则该查询可能是有问题的. + + +## 为sakila库的film表添加索引 + +* **Item:** IDX.001 + +* **Severity:** L2 + +* **Content:** 为列title添加索引,散粒度为: 100.00%; 为列length添加索引,散粒度为: 14.00%; 为列release\_year添加索引,散粒度为: 0.10%; + +* **Case:** ALTER TABLE \`sakila\`.\`film\` add index \`idx\_title\_length\_release\_year\` (\`title\`,\`length\`,\`release\_year\`) ; + + + +## 不建议使用SELECT * 类型查询 + +* **Item:** COL.001 + +* **Severity:** L1 + +* **Content:** 当表结构变更时,使用\*通配符选择所有列将导致查询的含义和行为会发生更改,可能导致查询返回更多的数据。 + +# Query: CB42080E9F35AB07 + +★ ★ ★ ★ ☆ 85分 + +```sql + +SELECT + * +FROM + film +WHERE + LENGTH > 100 + AND title = 'xyz' +ORDER BY + release_year +``` + +## Explain信息 + +| id | select\_type | table | partitions | type | possible_keys | key | key\_len | ref | rows | filtered | scalability | Extra | +|---|---|---|---|---|---|---|---|---|---|---|---|---| +| 1 | SIMPLE | *film* | NULL | ref | idx\_title | idx\_title | 767 | const | 1 | 33.33% | ☠️ **O(n)** | Using index condition; Using where; Using filesort | + + + +### Explain信息解读 + +#### SelectType信息解读 + +* **SIMPLE**: 简单SELECT(不使用UNION或子查询等). + +#### Type信息解读 + +* **ref**: 连接不能基于关键字选择单个行, 可能查找到多个符合条件的行. 叫做ref是因为索引要跟某个参考值相比较. 这个参考值或者是一个数, 或者是来自一个表里的多表查询的结果值. 例:'SELECT * FROM tbl WHERE idx_col=expr;'. + +#### Extra信息解读 + +* ☠️ **Using filesort**: MySQL会对结果使用一个外部索引排序,而不是从表里按照索引次序读到相关内容. 可能在内存或者磁盘上进行排序. MySQL中无法利用索引完成的排序操作称为'文件排序'. + +* **Using index**: 只需通过索引就可以从表中获取列的信息, 无需额外去读取真实的行数据. 如果查询使用的列值仅仅是一个简单索引的部分值, 则会使用这种策略来优化查询. + +* **Using index condition**: 在5.6版本后加入的新特性(Index Condition Pushdown)。Using index condition 会先条件过滤索引,过滤完索引后找到所有符合索引条件的数据行,随后用 WHERE 子句中的其他条件去过滤这些数据行。 + +* **Using where**: WHERE条件用于筛选出与下一个表匹配的数据然后返回给客户端. 除非故意做的全表扫描, 否则连接类型是ALL或者是index, 且在Extra列的值中没有Using Where, 则该查询可能是有问题的. + + +## 为sakila库的film表添加索引 + +* **Item:** IDX.001 + +* **Severity:** L2 + +* **Content:** 为列title添加索引,散粒度为: 100.00%; 为列length添加索引,散粒度为: 14.00%; 为列release\_year添加索引,散粒度为: 0.10%; + +* **Case:** ALTER TABLE \`sakila\`.\`film\` add index \`idx\_title\_length\_release\_year\` (\`title\`,\`length\`,\`release\_year\`) ; + + + +## 不建议使用SELECT * 类型查询 + +* **Item:** COL.001 + +* **Severity:** L1 + +* **Content:** 当表结构变更时,使用\*通配符选择所有列将导致查询的含义和行为会发生更改,可能导致查询返回更多的数据。 + +# Query: C4A212A42400411D + +★ ★ ★ ★ ☆ 85分 + +```sql + +SELECT + * +FROM + film +WHERE + LENGTH > 100 +ORDER BY + release_year +``` + +## Explain信息 + +| id | select\_type | table | partitions | type | possible_keys | key | key\_len | ref | rows | filtered | scalability | Extra | +|---|---|---|---|---|---|---|---|---|---|---|---|---| +| 1 | SIMPLE | *film* | NULL | ALL | | NULL | | | 1000 | 33.33% | ☠️ **O(n)** | Using where; Using filesort | + + + +### Explain信息解读 + +#### SelectType信息解读 + +* **SIMPLE**: 简单SELECT(不使用UNION或子查询等). + +#### Type信息解读 + +* ☠️ **ALL**: 最坏的情况, 从头到尾全表扫描. + +#### Extra信息解读 + +* ☠️ **Using filesort**: MySQL会对结果使用一个外部索引排序,而不是从表里按照索引次序读到相关内容. 可能在内存或者磁盘上进行排序. MySQL中无法利用索引完成的排序操作称为'文件排序'. + +* **Using where**: WHERE条件用于筛选出与下一个表匹配的数据然后返回给客户端. 除非故意做的全表扫描, 否则连接类型是ALL或者是index, 且在Extra列的值中没有Using Where, 则该查询可能是有问题的. + + +## 为sakila库的film表添加索引 + +* **Item:** IDX.001 + +* **Severity:** L2 + +* **Content:** 为列length添加索引,散粒度为: 14.00%; 为列release\_year添加索引,散粒度为: 0.10%; + +* **Case:** ALTER TABLE \`sakila\`.\`film\` add index \`idx\_length\_release\_year\` (\`length\`,\`release\_year\`) ; + + + +## 不建议使用SELECT * 类型查询 + +* **Item:** COL.001 + +* **Severity:** L1 + +* **Content:** 当表结构变更时,使用\*通配符选择所有列将导致查询的含义和行为会发生更改,可能导致查询返回更多的数据。 + +# Query: 4ECCA9568BE69E68 + +★ ★ ★ ☆ ☆ 75分 + +```sql + +SELECT + * +FROM + city a + INNER JOIN country b ON a. country_id= b. country_id +``` + +## Explain信息 + +| id | select\_type | table | partitions | type | possible_keys | key | key\_len | ref | rows | filtered | scalability | Extra | +|---|---|---|---|---|---|---|---|---|---|---|---|---| +| 1 | SIMPLE | *b* | NULL | ALL | PRIMARY | NULL | | | 109 | ☠️ **100.00%** | ☠️ **O(n)** | NULL | +| 1 | SIMPLE | *a* | NULL | ref | idx\_fk\_country\_id | idx\_fk\_country\_id | 2 | sakila.b.country\_id | 5 | ☠️ **100.00%** | ☠️ **O(n)** | NULL | + + + +### Explain信息解读 + +#### SelectType信息解读 + +* **SIMPLE**: 简单SELECT(不使用UNION或子查询等). + +#### Type信息解读 + +* ☠️ **ALL**: 最坏的情况, 从头到尾全表扫描. + +* **ref**: 连接不能基于关键字选择单个行, 可能查找到多个符合条件的行. 叫做ref是因为索引要跟某个参考值相比较. 这个参考值或者是一个数, 或者是来自一个表里的多表查询的结果值. 例:'SELECT * FROM tbl WHERE idx_col=expr;'. + + +## 建议使用AS关键字显示声明一个别名 + +* **Item:** ALI.001 + +* **Severity:** L0 + +* **Content:** 在列或表别名(如"tbl AS alias")中, 明确使用AS关键字比隐含别名(如"tbl alias")更易懂。 + +## SELECT未指定WHERE条件 + +* **Item:** CLA.001 + +* **Severity:** L4 + +* **Content:** SELECT语句没有WHERE子句,可能检查比预期更多的行(全表扫描)。对于SELECT COUNT(\*)类型的请求如果不要求精度,建议使用SHOW TABLE STATUS或EXPLAIN替代。 + +## 不建议使用SELECT * 类型查询 + +* **Item:** COL.001 + +* **Severity:** L1 + +* **Content:** 当表结构变更时,使用\*通配符选择所有列将导致查询的含义和行为会发生更改,可能导致查询返回更多的数据。 + +# Query: 485D56FC88BBBDB9 + +★ ★ ★ ☆ ☆ 75分 + +```sql + +SELECT + * +FROM + city a + LEFT JOIN country b ON a. country_id= b. country_id +``` + +## Explain信息 + +| id | select\_type | table | partitions | type | possible_keys | key | key\_len | ref | rows | filtered | scalability | Extra | +|---|---|---|---|---|---|---|---|---|---|---|---|---| +| 1 | SIMPLE | *a* | NULL | ALL | | NULL | | | 600 | ☠️ **100.00%** | ☠️ **O(n)** | NULL | +| 1 | SIMPLE | *b* | NULL | eq\_ref | PRIMARY | PRIMARY | 2 | sakila.a.country\_id | 1 | ☠️ **100.00%** | ☠️ **O(n)** | NULL | + + + +### Explain信息解读 + +#### SelectType信息解读 + +* **SIMPLE**: 简单SELECT(不使用UNION或子查询等). + +#### Type信息解读 + +* ☠️ **ALL**: 最坏的情况, 从头到尾全表扫描. + +* **eq_ref**: 除const类型外最好的可能实现的连接类型. 它用在一个索引的所有部分被连接使用并且索引是UNIQUE或PRIMARY KEY, 对于每个索引键, 表中只有一条记录与之匹配. 例:'SELECT * FROM ref_table,tbl WHERE ref_table.key_column=tbl.column;'. + + +## 建议使用AS关键字显示声明一个别名 + +* **Item:** ALI.001 + +* **Severity:** L0 + +* **Content:** 在列或表别名(如"tbl AS alias")中, 明确使用AS关键字比隐含别名(如"tbl alias")更易懂。 + +## SELECT未指定WHERE条件 + +* **Item:** CLA.001 + +* **Severity:** L4 + +* **Content:** SELECT语句没有WHERE子句,可能检查比预期更多的行(全表扫描)。对于SELECT COUNT(\*)类型的请求如果不要求精度,建议使用SHOW TABLE STATUS或EXPLAIN替代。 + +## 不建议使用SELECT * 类型查询 + +* **Item:** COL.001 + +* **Severity:** L1 + +* **Content:** 当表结构变更时,使用\*通配符选择所有列将导致查询的含义和行为会发生更改,可能导致查询返回更多的数据。 + +# Query: 0D0DABACEDFF5765 + +★ ★ ★ ☆ ☆ 75分 + +```sql + +SELECT + * +FROM + city a + RIGHT JOIN country b ON a. country_id= b. country_id +``` + +## Explain信息 + +| id | select\_type | table | partitions | type | possible_keys | key | key\_len | ref | rows | filtered | scalability | Extra | +|---|---|---|---|---|---|---|---|---|---|---|---|---| +| 1 | SIMPLE | *b* | NULL | ALL | | NULL | | | 109 | ☠️ **100.00%** | ☠️ **O(n)** | NULL | +| 1 | SIMPLE | *a* | NULL | ref | idx\_fk\_country\_id | idx\_fk\_country\_id | 2 | sakila.b.country\_id | 5 | ☠️ **100.00%** | ☠️ **O(n)** | NULL | + + + +### Explain信息解读 + +#### SelectType信息解读 + +* **SIMPLE**: 简单SELECT(不使用UNION或子查询等). + +#### Type信息解读 + +* ☠️ **ALL**: 最坏的情况, 从头到尾全表扫描. + +* **ref**: 连接不能基于关键字选择单个行, 可能查找到多个符合条件的行. 叫做ref是因为索引要跟某个参考值相比较. 这个参考值或者是一个数, 或者是来自一个表里的多表查询的结果值. 例:'SELECT * FROM tbl WHERE idx_col=expr;'. + + +## 建议使用AS关键字显示声明一个别名 + +* **Item:** ALI.001 + +* **Severity:** L0 + +* **Content:** 在列或表别名(如"tbl AS alias")中, 明确使用AS关键字比隐含别名(如"tbl alias")更易懂。 + +## SELECT未指定WHERE条件 + +* **Item:** CLA.001 + +* **Severity:** L4 + +* **Content:** SELECT语句没有WHERE子句,可能检查比预期更多的行(全表扫描)。对于SELECT COUNT(\*)类型的请求如果不要求精度,建议使用SHOW TABLE STATUS或EXPLAIN替代。 + +## 不建议使用SELECT * 类型查询 + +* **Item:** COL.001 + +* **Severity:** L1 + +* **Content:** 当表结构变更时,使用\*通配符选择所有列将导致查询的含义和行为会发生更改,可能导致查询返回更多的数据。 + +# Query: 1E56C6CCEA2131CC + +★ ★ ★ ★ ☆ 80分 + +```sql + +SELECT + * +FROM + city a + LEFT JOIN country b ON a. country_id= b. country_id +WHERE + b. last_update IS NULL +``` + +## Explain信息 + +| id | select\_type | table | partitions | type | possible_keys | key | key\_len | ref | rows | filtered | scalability | Extra | +|---|---|---|---|---|---|---|---|---|---|---|---|---| +| 1 | SIMPLE | *a* | NULL | ALL | | NULL | | | 600 | ☠️ **100.00%** | ☠️ **O(n)** | NULL | +| 1 | SIMPLE | *b* | NULL | eq\_ref | PRIMARY | PRIMARY | 2 | sakila.a.country\_id | 1 | 10.00% | ☠️ **O(n)** | Using where; Not exists | + + + +### Explain信息解读 + +#### SelectType信息解读 + +* **SIMPLE**: 简单SELECT(不使用UNION或子查询等). + +#### Type信息解读 + +* ☠️ **ALL**: 最坏的情况, 从头到尾全表扫描. + +* **eq_ref**: 除const类型外最好的可能实现的连接类型. 它用在一个索引的所有部分被连接使用并且索引是UNIQUE或PRIMARY KEY, 对于每个索引键, 表中只有一条记录与之匹配. 例:'SELECT * FROM ref_table,tbl WHERE ref_table.key_column=tbl.column;'. + +#### Extra信息解读 + +* **Not exists**: MySQL能够对LEFT JOIN查询进行优化, 并且在查找到符合LEFT JOIN条件的行后, 则不再查找更多的行. + +* **Using where**: WHERE条件用于筛选出与下一个表匹配的数据然后返回给客户端. 除非故意做的全表扫描, 否则连接类型是ALL或者是index, 且在Extra列的值中没有Using Where, 则该查询可能是有问题的. + + +## 为sakila库的country表添加索引 + +* **Item:** IDX.001 + +* **Severity:** L2 + +* **Content:** 为列last\_update添加索引,散粒度为: 0.92%; + +* **Case:** ALTER TABLE \`sakila\`.\`country\` add index \`idx\_last\_update\` (\`last\_update\`) ; + + + +## 建议使用AS关键字显示声明一个别名 + +* **Item:** ALI.001 + +* **Severity:** L0 + +* **Content:** 在列或表别名(如"tbl AS alias")中, 明确使用AS关键字比隐含别名(如"tbl alias")更易懂。 + +## 应尽量避免在WHERE子句中对字段进行NULL值判断 + +* **Item:** ARG.006 + +* **Severity:** L1 + +* **Content:** 使用IS NULL或IS NOT NULL将可能导致引擎放弃使用索引而进行全表扫描,如:select id from t where num is null;可以在num上设置默认值0,确保表中num列没有null值,然后这样查询: select id from t where num=0; + +## 不建议使用SELECT * 类型查询 + +* **Item:** COL.001 + +* **Severity:** L1 + +* **Content:** 当表结构变更时,使用\*通配符选择所有列将导致查询的含义和行为会发生更改,可能导致查询返回更多的数据。 + +# Query: F5D30BCAC1E206A1 + +★ ★ ★ ★ ☆ 80分 + +```sql + +SELECT + * +FROM + city a + RIGHT JOIN country b ON a. country_id= b. country_id +WHERE + a. last_update IS NULL +``` + +## Explain信息 + +| id | select\_type | table | partitions | type | possible_keys | key | key\_len | ref | rows | filtered | scalability | Extra | +|---|---|---|---|---|---|---|---|---|---|---|---|---| +| 1 | SIMPLE | *b* | NULL | ALL | | NULL | | | 109 | ☠️ **100.00%** | ☠️ **O(n)** | NULL | +| 1 | SIMPLE | *a* | NULL | ref | idx\_fk\_country\_id | idx\_fk\_country\_id | 2 | sakila.b.country\_id | 5 | 10.00% | ☠️ **O(n)** | Using where; Not exists | + + + +### Explain信息解读 + +#### SelectType信息解读 + +* **SIMPLE**: 简单SELECT(不使用UNION或子查询等). + +#### Type信息解读 + +* ☠️ **ALL**: 最坏的情况, 从头到尾全表扫描. + +* **ref**: 连接不能基于关键字选择单个行, 可能查找到多个符合条件的行. 叫做ref是因为索引要跟某个参考值相比较. 这个参考值或者是一个数, 或者是来自一个表里的多表查询的结果值. 例:'SELECT * FROM tbl WHERE idx_col=expr;'. + +#### Extra信息解读 + +* **Not exists**: MySQL能够对LEFT JOIN查询进行优化, 并且在查找到符合LEFT JOIN条件的行后, 则不再查找更多的行. + +* **Using where**: WHERE条件用于筛选出与下一个表匹配的数据然后返回给客户端. 除非故意做的全表扫描, 否则连接类型是ALL或者是index, 且在Extra列的值中没有Using Where, 则该查询可能是有问题的. + + +## 为sakila库的city表添加索引 + +* **Item:** IDX.001 + +* **Severity:** L2 + +* **Content:** 为列last\_update添加索引,散粒度为: 0.17%; + +* **Case:** ALTER TABLE \`sakila\`.\`city\` add index \`idx\_last\_update\` (\`last\_update\`) ; + + + +## 建议使用AS关键字显示声明一个别名 + +* **Item:** ALI.001 + +* **Severity:** L0 + +* **Content:** 在列或表别名(如"tbl AS alias")中, 明确使用AS关键字比隐含别名(如"tbl alias")更易懂。 + +## 应尽量避免在WHERE子句中对字段进行NULL值判断 + +* **Item:** ARG.006 + +* **Severity:** L1 + +* **Content:** 使用IS NULL或IS NOT NULL将可能导致引擎放弃使用索引而进行全表扫描,如:select id from t where num is null;可以在num上设置默认值0,确保表中num列没有null值,然后这样查询: select id from t where num=0; + +## 不建议使用SELECT * 类型查询 + +* **Item:** COL.001 + +* **Severity:** L1 + +* **Content:** 当表结构变更时,使用\*通配符选择所有列将导致查询的含义和行为会发生更改,可能导致查询返回更多的数据。 + +# Query: 17D5BCF21DC2364C + +★ ★ ★ ☆ ☆ 65分 + +```sql + +SELECT + * +FROM + city a + LEFT JOIN country b ON a. country_id= b. country_id +UNION +SELECT + * +FROM + city a + RIGHT JOIN country b ON a. country_id= b. country_id +``` + +## Explain信息 + +| id | select\_type | table | partitions | type | possible_keys | key | key\_len | ref | rows | filtered | scalability | Extra | +|---|---|---|---|---|---|---|---|---|---|---|---|---| +| 1 | PRIMARY | *a* | NULL | ALL | | NULL | | | 600 | ☠️ **100.00%** | ☠️ **O(n)** | NULL | +| 1 | PRIMARY | *b* | NULL | eq\_ref | PRIMARY | PRIMARY | 2 | sakila.a.country\_id | 1 | ☠️ **100.00%** | ☠️ **O(n)** | NULL | +| 2 | UNION | *b* | NULL | ALL | | NULL | | | 109 | ☠️ **100.00%** | ☠️ **O(n)** | NULL | +| 2 | UNION | *a* | NULL | ref | idx\_fk\_country\_id | idx\_fk\_country\_id | 2 | sakila.b.country\_id | 5 | ☠️ **100.00%** | ☠️ **O(n)** | NULL | +| 0 | UNION RESULT | ** | NULL | ALL | | NULL | | | 0 | 0.00% | ☠️ **O(n)** | Using temporary | + + + +### Explain信息解读 + +#### SelectType信息解读 + +* **PRIMARY**: 最外层的select. + +* **UNION**: UNION中的第二个或后面的SELECT查询, 不依赖于外部查询的结果集. + +* **UNION RESULT**: UNION查询的结果集. + +#### Type信息解读 + +* ☠️ **ALL**: 最坏的情况, 从头到尾全表扫描. + +* **eq_ref**: 除const类型外最好的可能实现的连接类型. 它用在一个索引的所有部分被连接使用并且索引是UNIQUE或PRIMARY KEY, 对于每个索引键, 表中只有一条记录与之匹配. 例:'SELECT * FROM ref_table,tbl WHERE ref_table.key_column=tbl.column;'. + +* **ref**: 连接不能基于关键字选择单个行, 可能查找到多个符合条件的行. 叫做ref是因为索引要跟某个参考值相比较. 这个参考值或者是一个数, 或者是来自一个表里的多表查询的结果值. 例:'SELECT * FROM tbl WHERE idx_col=expr;'. + +#### Extra信息解读 + +* ☠️ **Using temporary**: 表示MySQL在对查询结果排序时使用临时表. 常见于排序order by和分组查询group by. + + +## 建议使用AS关键字显示声明一个别名 + +* **Item:** ALI.001 + +* **Severity:** L0 + +* **Content:** 在列或表别名(如"tbl AS alias")中, 明确使用AS关键字比隐含别名(如"tbl alias")更易懂。 + +## SELECT未指定WHERE条件 + +* **Item:** CLA.001 + +* **Severity:** L4 + +* **Content:** SELECT语句没有WHERE子句,可能检查比预期更多的行(全表扫描)。对于SELECT COUNT(\*)类型的请求如果不要求精度,建议使用SHOW TABLE STATUS或EXPLAIN替代。 + +## 不建议使用SELECT * 类型查询 + +* **Item:** COL.001 + +* **Severity:** L1 + +* **Content:** 当表结构变更时,使用\*通配符选择所有列将导致查询的含义和行为会发生更改,可能导致查询返回更多的数据。 + +## 如果您不在乎重复的话,建议使用UNION ALL替代UNION + +* **Item:** SUB.002 + +* **Severity:** L2 + +* **Content:** 与去除重复的UNION不同,UNION ALL允许重复元组。如果您不关心重复元组,那么使用UNION ALL将是一个更快的选项。 + +# Query: A4911095C201896F + +★ ★ ★ ☆ ☆ 65分 + +```sql + +SELECT + * +FROM + city a + RIGHT JOIN country b ON a. country_id= b. country_id +WHERE + a. last_update IS NULL +UNION +SELECT + * +FROM + city a + LEFT JOIN country b ON a. country_id= b. country_id +WHERE + b. last_update IS NULL +``` + +## Explain信息 + +| id | select\_type | table | partitions | type | possible_keys | key | key\_len | ref | rows | filtered | scalability | Extra | +|---|---|---|---|---|---|---|---|---|---|---|---|---| +| 1 | PRIMARY | *b* | NULL | ALL | | NULL | | | 109 | ☠️ **100.00%** | ☠️ **O(n)** | NULL | +| 1 | PRIMARY | *a* | NULL | ref | idx\_fk\_country\_id | idx\_fk\_country\_id | 2 | sakila.b.country\_id | 5 | 10.00% | ☠️ **O(n)** | Using where; Not exists | +| 2 | UNION | *a* | NULL | ALL | | NULL | | | 600 | ☠️ **100.00%** | ☠️ **O(n)** | NULL | +| 2 | UNION | *b* | NULL | eq\_ref | PRIMARY | PRIMARY | 2 | sakila.a.country\_id | 1 | 10.00% | ☠️ **O(n)** | Using where; Not exists | +| 0 | UNION RESULT | ** | NULL | ALL | | NULL | | | 0 | 0.00% | ☠️ **O(n)** | Using temporary | + + + +### Explain信息解读 + +#### SelectType信息解读 + +* **PRIMARY**: 最外层的select. + +* **UNION**: UNION中的第二个或后面的SELECT查询, 不依赖于外部查询的结果集. + +* **UNION RESULT**: UNION查询的结果集. + +#### Type信息解读 + +* ☠️ **ALL**: 最坏的情况, 从头到尾全表扫描. + +* **ref**: 连接不能基于关键字选择单个行, 可能查找到多个符合条件的行. 叫做ref是因为索引要跟某个参考值相比较. 这个参考值或者是一个数, 或者是来自一个表里的多表查询的结果值. 例:'SELECT * FROM tbl WHERE idx_col=expr;'. + +* **eq_ref**: 除const类型外最好的可能实现的连接类型. 它用在一个索引的所有部分被连接使用并且索引是UNIQUE或PRIMARY KEY, 对于每个索引键, 表中只有一条记录与之匹配. 例:'SELECT * FROM ref_table,tbl WHERE ref_table.key_column=tbl.column;'. + +#### Extra信息解读 + +* **Not exists**: MySQL能够对LEFT JOIN查询进行优化, 并且在查找到符合LEFT JOIN条件的行后, 则不再查找更多的行. + +* **Using where**: WHERE条件用于筛选出与下一个表匹配的数据然后返回给客户端. 除非故意做的全表扫描, 否则连接类型是ALL或者是index, 且在Extra列的值中没有Using Where, 则该查询可能是有问题的. + +* ☠️ **Using temporary**: 表示MySQL在对查询结果排序时使用临时表. 常见于排序order by和分组查询group by. + + +## 为sakila库的city表添加索引 + +* **Item:** IDX.001 + +* **Severity:** L2 + +* **Content:** 为列last\_update添加索引,散粒度为: 0.17%; + +* **Case:** ALTER TABLE \`sakila\`.\`city\` add index \`idx\_last\_update\` (\`last\_update\`) ; + + + +## 为sakila库的country表添加索引 + +* **Item:** IDX.002 + +* **Severity:** L2 + +* **Content:** 为列last\_update添加索引,散粒度为: 0.92%; + +* **Case:** ALTER TABLE \`sakila\`.\`country\` add index \`idx\_last\_update\` (\`last\_update\`) ; + + + +## 建议使用AS关键字显示声明一个别名 + +* **Item:** ALI.001 + +* **Severity:** L0 + +* **Content:** 在列或表别名(如"tbl AS alias")中, 明确使用AS关键字比隐含别名(如"tbl alias")更易懂。 + +## 不建议使用SELECT * 类型查询 + +* **Item:** COL.001 + +* **Severity:** L1 + +* **Content:** 当表结构变更时,使用\*通配符选择所有列将导致查询的含义和行为会发生更改,可能导致查询返回更多的数据。 + +## 如果您不在乎重复的话,建议使用UNION ALL替代UNION + +* **Item:** SUB.002 + +* **Severity:** L2 + +* **Content:** 与去除重复的UNION不同,UNION ALL允许重复元组。如果您不关心重复元组,那么使用UNION ALL将是一个更快的选项。 + +# Query: 3FF20E28EC9CBEF9 + +★ ★ ★ ★ ☆ 80分 + +```sql + +SELECT + country_id, last_update +FROM + city NATURAL + JOIN country +``` + +## Explain信息 + +| id | select\_type | table | partitions | type | possible_keys | key | key\_len | ref | rows | filtered | scalability | Extra | +|---|---|---|---|---|---|---|---|---|---|---|---|---| +| 1 | SIMPLE | *country* | NULL | ALL | PRIMARY | NULL | | | 109 | ☠️ **100.00%** | ☠️ **O(n)** | NULL | +| 1 | SIMPLE | *city* | NULL | ref | idx\_fk\_country\_id | idx\_fk\_country\_id | 2 | sakila.country.country\_id | 5 | 10.00% | ☠️ **O(n)** | Using where | + + + +### Explain信息解读 + +#### SelectType信息解读 + +* **SIMPLE**: 简单SELECT(不使用UNION或子查询等). + +#### Type信息解读 + +* ☠️ **ALL**: 最坏的情况, 从头到尾全表扫描. + +* **ref**: 连接不能基于关键字选择单个行, 可能查找到多个符合条件的行. 叫做ref是因为索引要跟某个参考值相比较. 这个参考值或者是一个数, 或者是来自一个表里的多表查询的结果值. 例:'SELECT * FROM tbl WHERE idx_col=expr;'. + +#### Extra信息解读 + +* **Using where**: WHERE条件用于筛选出与下一个表匹配的数据然后返回给客户端. 除非故意做的全表扫描, 否则连接类型是ALL或者是index, 且在Extra列的值中没有Using Where, 则该查询可能是有问题的. + + +## SELECT未指定WHERE条件 + +* **Item:** CLA.001 + +* **Severity:** L4 + +* **Content:** SELECT语句没有WHERE子句,可能检查比预期更多的行(全表扫描)。对于SELECT COUNT(\*)类型的请求如果不要求精度,建议使用SHOW TABLE STATUS或EXPLAIN替代。 + +# Query: 5C547F08EADBB131 + +★ ★ ★ ★ ☆ 80分 + +```sql + +SELECT + country_id, last_update +FROM + city NATURAL + LEFT JOIN country +``` + +## Explain信息 + +| id | select\_type | table | partitions | type | possible_keys | key | key\_len | ref | rows | filtered | scalability | Extra | +|---|---|---|---|---|---|---|---|---|---|---|---|---| +| 1 | SIMPLE | *city* | NULL | ALL | | NULL | | | 600 | ☠️ **100.00%** | ☠️ **O(n)** | NULL | +| 1 | SIMPLE | *country* | NULL | eq\_ref | PRIMARY | PRIMARY | 2 | sakila.city.country\_id | 1 | ☠️ **100.00%** | ☠️ **O(n)** | Using where | + + + +### Explain信息解读 + +#### SelectType信息解读 + +* **SIMPLE**: 简单SELECT(不使用UNION或子查询等). + +#### Type信息解读 + +* ☠️ **ALL**: 最坏的情况, 从头到尾全表扫描. + +* **eq_ref**: 除const类型外最好的可能实现的连接类型. 它用在一个索引的所有部分被连接使用并且索引是UNIQUE或PRIMARY KEY, 对于每个索引键, 表中只有一条记录与之匹配. 例:'SELECT * FROM ref_table,tbl WHERE ref_table.key_column=tbl.column;'. + +#### Extra信息解读 + +* **Using where**: WHERE条件用于筛选出与下一个表匹配的数据然后返回给客户端. 除非故意做的全表扫描, 否则连接类型是ALL或者是index, 且在Extra列的值中没有Using Where, 则该查询可能是有问题的. + + +## SELECT未指定WHERE条件 + +* **Item:** CLA.001 + +* **Severity:** L4 + +* **Content:** SELECT语句没有WHERE子句,可能检查比预期更多的行(全表扫描)。对于SELECT COUNT(\*)类型的请求如果不要求精度,建议使用SHOW TABLE STATUS或EXPLAIN替代。 + +# Query: AF0C1EB58B23D2FA + +★ ★ ★ ★ ☆ 80分 + +```sql + +SELECT + country_id, last_update +FROM + city NATURAL + RIGHT JOIN country +``` + +## Explain信息 + +| id | select\_type | table | partitions | type | possible_keys | key | key\_len | ref | rows | filtered | scalability | Extra | +|---|---|---|---|---|---|---|---|---|---|---|---|---| +| 1 | SIMPLE | *country* | NULL | ALL | | NULL | | | 109 | ☠️ **100.00%** | ☠️ **O(n)** | NULL | +| 1 | SIMPLE | *city* | NULL | ref | idx\_fk\_country\_id | idx\_fk\_country\_id | 2 | sakila.country.country\_id | 5 | ☠️ **100.00%** | ☠️ **O(n)** | Using where | + + + +### Explain信息解读 + +#### SelectType信息解读 + +* **SIMPLE**: 简单SELECT(不使用UNION或子查询等). + +#### Type信息解读 + +* ☠️ **ALL**: 最坏的情况, 从头到尾全表扫描. + +* **ref**: 连接不能基于关键字选择单个行, 可能查找到多个符合条件的行. 叫做ref是因为索引要跟某个参考值相比较. 这个参考值或者是一个数, 或者是来自一个表里的多表查询的结果值. 例:'SELECT * FROM tbl WHERE idx_col=expr;'. + +#### Extra信息解读 + +* **Using where**: WHERE条件用于筛选出与下一个表匹配的数据然后返回给客户端. 除非故意做的全表扫描, 否则连接类型是ALL或者是index, 且在Extra列的值中没有Using Where, 则该查询可能是有问题的. + + +## SELECT未指定WHERE条件 + +* **Item:** CLA.001 + +* **Severity:** L4 + +* **Content:** SELECT语句没有WHERE子句,可能检查比预期更多的行(全表扫描)。对于SELECT COUNT(\*)类型的请求如果不要求精度,建议使用SHOW TABLE STATUS或EXPLAIN替代。 + +# Query: 626571EAE84E2C8A + +★ ★ ★ ★ ☆ 80分 + +```sql + +SELECT + a. country_id, a. last_update +FROM + city a STRAIGHT_JOIN country b ON a. country_id= b. country_id +``` + +## Explain信息 + +| id | select\_type | table | partitions | type | possible_keys | key | key\_len | ref | rows | filtered | scalability | Extra | +|---|---|---|---|---|---|---|---|---|---|---|---|---| +| 1 | SIMPLE | *a* | NULL | ALL | idx\_fk\_country\_id | NULL | | | 600 | ☠️ **100.00%** | ☠️ **O(n)** | NULL | +| 1 | SIMPLE | *b* | NULL | eq\_ref | PRIMARY | PRIMARY | 2 | sakila.a.country\_id | 1 | ☠️ **100.00%** | ☠️ **O(n)** | Using index | + + + +### Explain信息解读 + +#### SelectType信息解读 + +* **SIMPLE**: 简单SELECT(不使用UNION或子查询等). + +#### Type信息解读 + +* ☠️ **ALL**: 最坏的情况, 从头到尾全表扫描. + +* **eq_ref**: 除const类型外最好的可能实现的连接类型. 它用在一个索引的所有部分被连接使用并且索引是UNIQUE或PRIMARY KEY, 对于每个索引键, 表中只有一条记录与之匹配. 例:'SELECT * FROM ref_table,tbl WHERE ref_table.key_column=tbl.column;'. + +#### Extra信息解读 + +* **Using index**: 只需通过索引就可以从表中获取列的信息, 无需额外去读取真实的行数据. 如果查询使用的列值仅仅是一个简单索引的部分值, 则会使用这种策略来优化查询. + + +## 建议使用AS关键字显示声明一个别名 + +* **Item:** ALI.001 + +* **Severity:** L0 + +* **Content:** 在列或表别名(如"tbl AS alias")中, 明确使用AS关键字比隐含别名(如"tbl alias")更易懂。 + +## SELECT未指定WHERE条件 + +* **Item:** CLA.001 + +* **Severity:** L4 + +* **Content:** SELECT语句没有WHERE子句,可能检查比预期更多的行(全表扫描)。对于SELECT COUNT(\*)类型的请求如果不要求精度,建议使用SHOW TABLE STATUS或EXPLAIN替代。 + +# Query: F76BFFC87914E3D5 + +☆ ☆ ☆ ☆ ☆ 0分 + +```sql + +SELECT + d. deptno, d. dname, d. loc +FROM + scott. dept d +WHERE + d. deptno IN ( +SELECT + e. deptno +FROM + scott. emp e) +``` + +## MySQL返回信息 + +Unknown database 'scott' + +## 建议使用AS关键字显示声明一个别名 + +* **Item:** ALI.001 + +* **Severity:** L0 + +* **Content:** 在列或表别名(如"tbl AS alias")中, 明确使用AS关键字比隐含别名(如"tbl alias")更易懂。 + +## SELECT未指定WHERE条件 + +* **Item:** CLA.001 + +* **Severity:** L4 + +* **Content:** SELECT语句没有WHERE子句,可能检查比预期更多的行(全表扫描)。对于SELECT COUNT(\*)类型的请求如果不要求精度,建议使用SHOW TABLE STATUS或EXPLAIN替代。 + +## MySQL对子查询的优化效果不佳 + +* **Item:** SUB.001 + +* **Severity:** L4 + +* **Content:** MySQL将外部查询中的每一行作为依赖子查询执行子查询。 这是导致严重性能问题的常见原因。这可能会在 MySQL 5.6版本中得到改善, 但对于5.1及更早版本, 建议将该类查询分别重写为JOIN或LEFT OUTER JOIN。 + +# Query: 18D2299710570E81 + +☆ ☆ ☆ ☆ ☆ 10分 + +```sql + +SELECT + visitor_id, url +FROM + ( +SELECT + id +FROM + LOG +WHERE + ip= "123.45.67.89" +ORDER BY + tsdesc +LIMIT + 50, 10) I + JOIN LOG ON (I. id= LOG. id) + JOIN url ON (url. id= LOG. url_id) +ORDER BY + TS desc +``` + +## SELECT未指定WHERE条件 + +* **Item:** CLA.001 + +* **Severity:** L4 + +* **Content:** SELECT语句没有WHERE子句,可能检查比预期更多的行(全表扫描)。对于SELECT COUNT(\*)类型的请求如果不要求精度,建议使用SHOW TABLE STATUS或EXPLAIN替代。 + +## ORDER BY语句对多个不同条件使用不同方向的排序无法使用索引 + +* **Item:** CLA.007 + +* **Severity:** L2 + +* **Content:** ORDER BY子句中的所有表达式必须按统一的ASC或DESC方向排序,以便利用索引。 + +## ORDER BY的条件为表达式 + +* **Item:** CLA.009 + +* **Severity:** L2 + +* **Content:** 当ORDER BY条件为表达式或函数时会使用到临时表,如果在未指定WHERE或WHERE条件返回的结果集较大时性能会很差。 + +## 同一张表被连接两次 + +* **Item:** JOI.002 + +* **Severity:** L4 + +* **Content:** 相同的表在FROM子句中至少出现两次,可以简化为对该表的单次访问。 + +## 用字符类型存储IP地址 + +* **Item:** LIT.001 + +* **Severity:** L2 + +* **Content:** 字符串字面上看起来像IP地址,但不是INET\_ATON()的参数,表示数据被存储为字符而不是整数。将IP地址存储为整数更为有效。 + +## MySQL对子查询的优化效果不佳 + +* **Item:** SUB.001 + +* **Severity:** L4 + +* **Content:** MySQL将外部查询中的每一行作为依赖子查询执行子查询。 这是导致严重性能问题的常见原因。这可能会在 MySQL 5.6版本中得到改善, 但对于5.1及更早版本, 建议将该类查询分别重写为JOIN或LEFT OUTER JOIN。 + +# Query: 7F02E23D44A38A6D + +★ ★ ★ ★ ☆ 80分 + +```sql +DELETE city, country +FROM + city + INNER JOIN country using (country_id) +WHERE + city. city_id = 1 +``` + +## Explain信息 + +| id | select\_type | table | partitions | type | possible_keys | key | key\_len | ref | rows | filtered | scalability | Extra | +|---|---|---|---|---|---|---|---|---|---|---|---|---| +| 1 | DELETE | *city* | NULL | const | PRIMARY,
idx\_fk\_country\_id | PRIMARY | 2 | const | 1 | ☠️ **100.00%** | ☠️ **O(n)** | NULL | +| 1 | DELETE | *country* | NULL | const | PRIMARY | PRIMARY | 2 | const | 1 | ☠️ **100.00%** | ☠️ **O(n)** | NULL | + + + +### Explain信息解读 + +#### Type信息解读 + +* **const**: const用于使用常数值比较PRIMARY KEY时, 当查询的表仅有一行时, 使用system. 例:SELECT * FROM tbl WHERE col =1. + + +## 不建议使用联表更新 + +* **Item:** JOI.007 + +* **Severity:** L4 + +* **Content:** 当需要同时更新多张表时建议使用简单SQL,一条SQL只更新一张表,尽量不要将多张表的更新在同一条SQL中完成。 + +## 使用DELETE/DROP/TRUNCATE等操作时注意备份 + +* **Item:** SEC.003 + +* **Severity:** L0 + +* **Content:** 在执行高危操作之前对数据进行备份是十分有必要的。 + +# Query: F8314ABD1CBF2FF1 + +★ ★ ★ ☆ ☆ 70分 + +```sql +DELETE city +FROM + city + LEFT JOIN country ON city. country_id = country. country_id +WHERE + country. country IS NULL +``` + +## Explain信息 + +| id | select\_type | table | partitions | type | possible_keys | key | key\_len | ref | rows | filtered | scalability | Extra | +|---|---|---|---|---|---|---|---|---|---|---|---|---| +| 1 | DELETE | *city* | NULL | ALL | | NULL | | | 600 | ☠️ **100.00%** | ☠️ **O(n)** | NULL | +| 1 | SIMPLE | *country* | NULL | eq\_ref | PRIMARY | PRIMARY | 2 | sakila.city.country\_id | 1 | 10.00% | ☠️ **O(n)** | Using where; Not exists | + + + +### Explain信息解读 + +#### SelectType信息解读 + +* **SIMPLE**: 简单SELECT(不使用UNION或子查询等). + +#### Type信息解读 + +* ☠️ **ALL**: 最坏的情况, 从头到尾全表扫描. + +* **eq_ref**: 除const类型外最好的可能实现的连接类型. 它用在一个索引的所有部分被连接使用并且索引是UNIQUE或PRIMARY KEY, 对于每个索引键, 表中只有一条记录与之匹配. 例:'SELECT * FROM ref_table,tbl WHERE ref_table.key_column=tbl.column;'. + +#### Extra信息解读 + +* **Not exists**: MySQL能够对LEFT JOIN查询进行优化, 并且在查找到符合LEFT JOIN条件的行后, 则不再查找更多的行. + +* **Using where**: WHERE条件用于筛选出与下一个表匹配的数据然后返回给客户端. 除非故意做的全表扫描, 否则连接类型是ALL或者是index, 且在Extra列的值中没有Using Where, 则该查询可能是有问题的. + + +## 为sakila库的country表添加索引 + +* **Item:** IDX.001 + +* **Severity:** L2 + +* **Content:** 为列country添加索引,散粒度为: 100.00%; + +* **Case:** ALTER TABLE \`sakila\`.\`country\` add index \`idx\_country\` (\`country\`) ; + + + +## 不建议使用联表更新 + +* **Item:** JOI.007 + +* **Severity:** L4 + +* **Content:** 当需要同时更新多张表时建议使用简单SQL,一条SQL只更新一张表,尽量不要将多张表的更新在同一条SQL中完成。 + +## 使用DELETE/DROP/TRUNCATE等操作时注意备份 + +* **Item:** SEC.003 + +* **Severity:** L0 + +* **Content:** 在执行高危操作之前对数据进行备份是十分有必要的。 + +# Query: 1A53649C43122975 + +★ ★ ★ ★ ☆ 80分 + +```sql +DELETE a1, a2 +FROM + city AS a1 + INNER JOIN country AS a2 +WHERE + a1. country_id= a2. country_id +``` + +## Explain信息 + +| id | select\_type | table | partitions | type | possible_keys | key | key\_len | ref | rows | filtered | scalability | Extra | +|---|---|---|---|---|---|---|---|---|---|---|---|---| +| 1 | DELETE | *a2* | NULL | ALL | PRIMARY | NULL | | | 109 | ☠️ **100.00%** | ☠️ **O(n)** | NULL | +| 1 | DELETE | *a1* | NULL | ref | idx\_fk\_country\_id | idx\_fk\_country\_id | 2 | sakila.a2.country\_id | 5 | ☠️ **100.00%** | ☠️ **O(n)** | NULL | + + + +### Explain信息解读 + +#### Type信息解读 + +* ☠️ **ALL**: 最坏的情况, 从头到尾全表扫描. + +* **ref**: 连接不能基于关键字选择单个行, 可能查找到多个符合条件的行. 叫做ref是因为索引要跟某个参考值相比较. 这个参考值或者是一个数, 或者是来自一个表里的多表查询的结果值. 例:'SELECT * FROM tbl WHERE idx_col=expr;'. + + +## 不建议使用联表更新 + +* **Item:** JOI.007 + +* **Severity:** L4 + +* **Content:** 当需要同时更新多张表时建议使用简单SQL,一条SQL只更新一张表,尽量不要将多张表的更新在同一条SQL中完成。 + +## 使用DELETE/DROP/TRUNCATE等操作时注意备份 + +* **Item:** SEC.003 + +* **Severity:** L0 + +* **Content:** 在执行高危操作之前对数据进行备份是十分有必要的。 + +# Query: B862978586C6338B + +★ ★ ★ ★ ☆ 80分 + +```sql + +DELETE FROM + a1, a2 USING city AS a1 + INNER JOIN country AS a2 +WHERE + a1. country_id= a2. country_id +``` + +## Explain信息 + +| id | select\_type | table | partitions | type | possible_keys | key | key\_len | ref | rows | filtered | scalability | Extra | +|---|---|---|---|---|---|---|---|---|---|---|---|---| +| 1 | DELETE | *a2* | NULL | ALL | PRIMARY | NULL | | | 109 | ☠️ **100.00%** | ☠️ **O(n)** | NULL | +| 1 | DELETE | *a1* | NULL | ref | idx\_fk\_country\_id | idx\_fk\_country\_id | 2 | sakila.a2.country\_id | 5 | ☠️ **100.00%** | ☠️ **O(n)** | NULL | + + + +### Explain信息解读 + +#### Type信息解读 + +* ☠️ **ALL**: 最坏的情况, 从头到尾全表扫描. + +* **ref**: 连接不能基于关键字选择单个行, 可能查找到多个符合条件的行. 叫做ref是因为索引要跟某个参考值相比较. 这个参考值或者是一个数, 或者是来自一个表里的多表查询的结果值. 例:'SELECT * FROM tbl WHERE idx_col=expr;'. + + +## 不建议使用联表更新 + +* **Item:** JOI.007 + +* **Severity:** L4 + +* **Content:** 当需要同时更新多张表时建议使用简单SQL,一条SQL只更新一张表,尽量不要将多张表的更新在同一条SQL中完成。 + +## 使用DELETE/DROP/TRUNCATE等操作时注意备份 + +* **Item:** SEC.003 + +* **Severity:** L0 + +* **Content:** 在执行高危操作之前对数据进行备份是十分有必要的。 + +# Query: F16FD63381EF8299 + +★ ★ ★ ★ ☆ 90分 + +```sql + +DELETE FROM + film +WHERE + LENGTH > 100 +``` + +## Explain信息 + +| id | select\_type | table | partitions | type | possible_keys | key | key\_len | ref | rows | filtered | scalability | Extra | +|---|---|---|---|---|---|---|---|---|---|---|---|---| +| 1 | DELETE | *film* | NULL | ALL | | NULL | | | 1000 | ☠️ **100.00%** | ☠️ **O(n)** | Using where | + + + +### Explain信息解读 + +#### Type信息解读 + +* ☠️ **ALL**: 最坏的情况, 从头到尾全表扫描. + +#### Extra信息解读 + +* **Using where**: WHERE条件用于筛选出与下一个表匹配的数据然后返回给客户端. 除非故意做的全表扫描, 否则连接类型是ALL或者是index, 且在Extra列的值中没有Using Where, 则该查询可能是有问题的. + + +## 为sakila库的film表添加索引 + +* **Item:** IDX.001 + +* **Severity:** L2 + +* **Content:** 为列length添加索引,散粒度为: 14.00%; + +* **Case:** ALTER TABLE \`sakila\`.\`film\` add index \`idx\_length\` (\`length\`) ; + + + +## 使用DELETE/DROP/TRUNCATE等操作时注意备份 + +* **Item:** SEC.003 + +* **Severity:** L0 + +* **Content:** 在执行高危操作之前对数据进行备份是十分有必要的。 + +# Query: 08CFE41C7D20AAC8 + +★ ★ ★ ★ ☆ 80分 + +```sql + +UPDATE + city + INNER JOIN country USING( country_id) +SET + city. city = 'Abha', + city. last_update = '2006-02-15 04:45:25', + country. country = 'Afghanistan' +WHERE + city. city_id= 10 +``` + +## Explain信息 + +| id | select\_type | table | partitions | type | possible_keys | key | key\_len | ref | rows | filtered | scalability | Extra | +|---|---|---|---|---|---|---|---|---|---|---|---|---| +| 1 | UPDATE | *city* | NULL | const | PRIMARY,
idx\_fk\_country\_id | PRIMARY | 2 | const | 1 | ☠️ **100.00%** | ☠️ **O(n)** | NULL | +| 1 | UPDATE | *country* | NULL | const | PRIMARY | PRIMARY | 2 | const | 1 | ☠️ **100.00%** | ☠️ **O(n)** | NULL | + + + +### Explain信息解读 + +#### Type信息解读 + +* **const**: const用于使用常数值比较PRIMARY KEY时, 当查询的表仅有一行时, 使用system. 例:SELECT * FROM tbl WHERE col =1. + + +## 不建议使用联表更新 + +* **Item:** JOI.007 + +* **Severity:** L4 + +* **Content:** 当需要同时更新多张表时建议使用简单SQL,一条SQL只更新一张表,尽量不要将多张表的更新在同一条SQL中完成。 + +# Query: C15BDF2C73B5B7ED + +★ ★ ★ ★ ☆ 80分 + +```sql + +UPDATE + city + INNER JOIN country ON city. country_id = country. country_id + INNER JOIN address ON city. city_id = address. city_id +SET + city. city = 'Abha', + city. last_update = '2006-02-15 04:45:25', + country. country = 'Afghanistan' +WHERE + city. city_id= 10 +``` + +## Explain信息 + +| id | select\_type | table | partitions | type | possible_keys | key | key\_len | ref | rows | filtered | scalability | Extra | +|---|---|---|---|---|---|---|---|---|---|---|---|---| +| 1 | UPDATE | *city* | NULL | const | PRIMARY,
idx\_fk\_country\_id | PRIMARY | 2 | const | 1 | ☠️ **100.00%** | ☠️ **O(n)** | NULL | +| 1 | UPDATE | *country* | NULL | const | PRIMARY | PRIMARY | 2 | const | 1 | ☠️ **100.00%** | ☠️ **O(n)** | NULL | +| 1 | SIMPLE | *address* | NULL | ref | idx\_fk\_city\_id | idx\_fk\_city\_id | 2 | const | 1 | ☠️ **100.00%** | ☠️ **O(n)** | Using index | + + + +### Explain信息解读 + +#### SelectType信息解读 + +* **SIMPLE**: 简单SELECT(不使用UNION或子查询等). + +#### Type信息解读 + +* **const**: const用于使用常数值比较PRIMARY KEY时, 当查询的表仅有一行时, 使用system. 例:SELECT * FROM tbl WHERE col =1. + +* **ref**: 连接不能基于关键字选择单个行, 可能查找到多个符合条件的行. 叫做ref是因为索引要跟某个参考值相比较. 这个参考值或者是一个数, 或者是来自一个表里的多表查询的结果值. 例:'SELECT * FROM tbl WHERE idx_col=expr;'. + +#### Extra信息解读 + +* **Using index**: 只需通过索引就可以从表中获取列的信息, 无需额外去读取真实的行数据. 如果查询使用的列值仅仅是一个简单索引的部分值, 则会使用这种策略来优化查询. + + +## 不建议使用联表更新 + +* **Item:** JOI.007 + +* **Severity:** L4 + +* **Content:** 当需要同时更新多张表时建议使用简单SQL,一条SQL只更新一张表,尽量不要将多张表的更新在同一条SQL中完成。 + +# Query: FCD1ABF36F8CDAD7 + +★ ★ ★ ★ ★ 100分 + +```sql + +UPDATE + city, country +SET + city. city = 'Abha', + city. last_update = '2006-02-15 04:45:25', + country. country = 'Afghanistan' +WHERE + city. country_id = country. country_id + AND city. city_id= 10 +``` + +## Explain信息 + +| id | select\_type | table | partitions | type | possible_keys | key | key\_len | ref | rows | filtered | scalability | Extra | +|---|---|---|---|---|---|---|---|---|---|---|---|---| +| 1 | UPDATE | *city* | NULL | const | PRIMARY,
idx\_fk\_country\_id | PRIMARY | 2 | const | 1 | ☠️ **100.00%** | ☠️ **O(n)** | NULL | +| 1 | UPDATE | *country* | NULL | const | PRIMARY | PRIMARY | 2 | const | 1 | ☠️ **100.00%** | ☠️ **O(n)** | NULL | + + + +### Explain信息解读 + +#### Type信息解读 + +* **const**: const用于使用常数值比较PRIMARY KEY时, 当查询的表仅有一行时, 使用system. 例:SELECT * FROM tbl WHERE col =1. + + +# Query: FE409EB794EE91CF + +★ ★ ★ ★ ★ 100分 + +```sql + +UPDATE + film +SET + LENGTH = 10 +WHERE + language_id = 20 +``` + +## Explain信息 + +| id | select\_type | table | partitions | type | possible_keys | key | key\_len | ref | rows | filtered | scalability | Extra | +|---|---|---|---|---|---|---|---|---|---|---|---|---| +| 1 | UPDATE | *film* | NULL | range | idx\_fk\_language\_id | idx\_fk\_language\_id | 1 | const | 1 | ☠️ **100.00%** | ☠️ **O(n)** | Using where | + + + +### Explain信息解读 + +#### Type信息解读 + +* **range**: 只检索给定范围的行, 使用一个索引来选择行. key列显示使用了哪个索引. key_len包含所使用索引的最长关键元素. + +#### Extra信息解读 + +* **Using where**: WHERE条件用于筛选出与下一个表匹配的数据然后返回给客户端. 除非故意做的全表扫描, 否则连接类型是ALL或者是index, 且在Extra列的值中没有Using Where, 则该查询可能是有问题的. + + +# Query: 3656B13CC4F888E2 + +★ ★ ★ ☆ ☆ 65分 + +```sql +INSERT INTO city (country_id) +SELECT + country_id +FROM + country +``` + +## Explain信息 + +| id | select\_type | table | partitions | type | possible_keys | key | key\_len | ref | rows | filtered | scalability | Extra | +|---|---|---|---|---|---|---|---|---|---|---|---|---| +| 1 | INSERT | *city* | NULL | ALL | | NULL | | | 0 | 0.00% | ☠️ **O(n)** | NULL | +| 1 | SIMPLE | *country* | NULL | index | | PRIMARY | 2 | | 109 | ☠️ **100.00%** | ☠️ **O(n)** | Using index | + + + +### Explain信息解读 + +#### SelectType信息解读 + +* **SIMPLE**: 简单SELECT(不使用UNION或子查询等). + +#### Type信息解读 + +* ☠️ **ALL**: 最坏的情况, 从头到尾全表扫描. + +* **index**: 全表扫描, 只是扫描表的时候按照索引次序进行而不是行. 主要优点就是避免了排序, 但是开销仍然非常大. + +#### Extra信息解读 + +* **Using index**: 只需通过索引就可以从表中获取列的信息, 无需额外去读取真实的行数据. 如果查询使用的列值仅仅是一个简单索引的部分值, 则会使用这种策略来优化查询. + + +## SELECT未指定WHERE条件 + +* **Item:** CLA.001 + +* **Severity:** L4 + +* **Content:** SELECT语句没有WHERE子句,可能检查比预期更多的行(全表扫描)。对于SELECT COUNT(\*)类型的请求如果不要求精度,建议使用SHOW TABLE STATUS或EXPLAIN替代。 + +## INSERT INTO xx SELECT加锁粒度较大请谨慎 + +* **Item:** LCK.001 + +* **Severity:** L3 + +* **Content:** INSERT INTO xx SELECT加锁粒度较大请谨慎 + +# Query: 2F7439623B712317 + +★ ★ ★ ★ ★ 100分 + +```sql +INSERT INTO city (country_id) +VALUES + (1), + (2), + (3) +``` + +## Explain信息 + +| id | select\_type | table | partitions | type | possible_keys | key | key\_len | ref | rows | filtered | scalability | Extra | +|---|---|---|---|---|---|---|---|---|---|---|---|---| +| 1 | INSERT | *city* | NULL | ALL | | NULL | | | 0 | 0.00% | ☠️ **O(n)** | NULL | + + + +### Explain信息解读 + +#### Type信息解读 + +* ☠️ **ALL**: 最坏的情况, 从头到尾全表扫描. + + +# Query: 11EC7AAACC97DC0F + +★ ★ ★ ★ ☆ 85分 + +```sql +INSERT INTO city (country_id) +SELECT + 10 +FROM + DUAL +``` + +## Explain信息 + +| id | select\_type | table | partitions | type | possible_keys | key | key\_len | ref | rows | filtered | scalability | Extra | +|---|---|---|---|---|---|---|---|---|---|---|---|---| +| 1 | INSERT | *city* | NULL | ALL | | NULL | | | 0 | 0.00% | ☠️ **O(n)** | NULL | + + + +### Explain信息解读 + +#### Type信息解读 + +* ☠️ **ALL**: 最坏的情况, 从头到尾全表扫描. + + +## INSERT INTO xx SELECT加锁粒度较大请谨慎 + +* **Item:** LCK.001 + +* **Severity:** L3 + +* **Content:** INSERT INTO xx SELECT加锁粒度较大请谨慎 + +# Query: E3DDA1A929236E72 + +★ ★ ★ ☆ ☆ 65分 + +```sql +REPLACE INTO city (country_id) +SELECT + country_id +FROM + country +``` + +## Explain信息 + +| id | select\_type | table | partitions | type | possible_keys | key | key\_len | ref | rows | filtered | scalability | Extra | +|---|---|---|---|---|---|---|---|---|---|---|---|---| +| 1 | REPLACE | *city* | NULL | ALL | | NULL | | | 0 | 0.00% | ☠️ **O(n)** | NULL | +| 1 | SIMPLE | *country* | NULL | index | | PRIMARY | 2 | | 109 | ☠️ **100.00%** | ☠️ **O(n)** | Using index | + + + +### Explain信息解读 + +#### SelectType信息解读 + +* **SIMPLE**: 简单SELECT(不使用UNION或子查询等). + +#### Type信息解读 + +* ☠️ **ALL**: 最坏的情况, 从头到尾全表扫描. + +* **index**: 全表扫描, 只是扫描表的时候按照索引次序进行而不是行. 主要优点就是避免了排序, 但是开销仍然非常大. + +#### Extra信息解读 + +* **Using index**: 只需通过索引就可以从表中获取列的信息, 无需额外去读取真实的行数据. 如果查询使用的列值仅仅是一个简单索引的部分值, 则会使用这种策略来优化查询. + + +## SELECT未指定WHERE条件 + +* **Item:** CLA.001 + +* **Severity:** L4 + +* **Content:** SELECT语句没有WHERE子句,可能检查比预期更多的行(全表扫描)。对于SELECT COUNT(\*)类型的请求如果不要求精度,建议使用SHOW TABLE STATUS或EXPLAIN替代。 + +## INSERT INTO xx SELECT加锁粒度较大请谨慎 + +* **Item:** LCK.001 + +* **Severity:** L3 + +* **Content:** INSERT INTO xx SELECT加锁粒度较大请谨慎 + +# Query: 466F1AC2F5851149 + +★ ★ ★ ★ ★ 100分 + +```sql +REPLACE INTO city (country_id) +VALUES + (1), + (2), + (3) +``` + +## Explain信息 + +| id | select\_type | table | partitions | type | possible_keys | key | key\_len | ref | rows | filtered | scalability | Extra | +|---|---|---|---|---|---|---|---|---|---|---|---|---| +| 1 | REPLACE | *city* | NULL | ALL | | NULL | | | 0 | 0.00% | ☠️ **O(n)** | NULL | + + + +### Explain信息解读 + +#### Type信息解读 + +* ☠️ **ALL**: 最坏的情况, 从头到尾全表扫描. + + +# Query: A7973BDD268F926E + +★ ★ ★ ★ ☆ 85分 + +```sql +REPLACE INTO city (country_id) +SELECT + 10 +FROM + DUAL +``` + +## Explain信息 + +| id | select\_type | table | partitions | type | possible_keys | key | key\_len | ref | rows | filtered | scalability | Extra | +|---|---|---|---|---|---|---|---|---|---|---|---|---| +| 1 | REPLACE | *city* | NULL | ALL | | NULL | | | 0 | 0.00% | ☠️ **O(n)** | NULL | + + + +### Explain信息解读 + +#### Type信息解读 + +* ☠️ **ALL**: 最坏的情况, 从头到尾全表扫描. + + +## INSERT INTO xx SELECT加锁粒度较大请谨慎 + +* **Item:** LCK.001 + +* **Severity:** L3 + +* **Content:** INSERT INTO xx SELECT加锁粒度较大请谨慎 + +# Query: 105C870D5DFB6710 + +★ ★ ★ ☆ ☆ 65分 + +```sql + +SELECT + film_id +FROM + ( +SELECT + film_id +FROM + ( +SELECT + film_id +FROM + ( +SELECT + film_id +FROM + ( +SELECT + film_id +FROM + ( +SELECT + film_id +FROM + ( +SELECT + film_id +FROM + ( +SELECT + film_id +FROM + ( +SELECT + film_id +FROM + ( +SELECT + film_id +FROM + ( +SELECT + film_id +FROM + ( +SELECT + film_id +FROM + ( +SELECT + film_id +FROM + ( +SELECT + film_id +FROM + ( +SELECT + film_id +FROM + ( +SELECT + film_id +FROM + ( +SELECT + film_id +FROM + film +) film +) film +) film +) film +) film +) film +) film +) film +) film +) film +) film +) film +) film +) film +) film +) film +``` + +## Explain信息 + +| id | select\_type | table | partitions | type | possible_keys | key | key\_len | ref | rows | filtered | scalability | Extra | +|---|---|---|---|---|---|---|---|---|---|---|---|---| +| 1 | SIMPLE | *film* | NULL | index | | idx\_fk\_language\_id | 1 | | 1000 | ☠️ **100.00%** | ☠️ **O(n)** | Using index | + + + +### Explain信息解读 + +#### SelectType信息解读 + +* **SIMPLE**: 简单SELECT(不使用UNION或子查询等). + +#### Type信息解读 + +* **index**: 全表扫描, 只是扫描表的时候按照索引次序进行而不是行. 主要优点就是避免了排序, 但是开销仍然非常大. + +#### Extra信息解读 + +* **Using index**: 只需通过索引就可以从表中获取列的信息, 无需额外去读取真实的行数据. 如果查询使用的列值仅仅是一个简单索引的部分值, 则会使用这种策略来优化查询. + + +## SELECT未指定WHERE条件 + +* **Item:** CLA.001 + +* **Severity:** L4 + +* **Content:** SELECT语句没有WHERE子句,可能检查比预期更多的行(全表扫描)。对于SELECT COUNT(\*)类型的请求如果不要求精度,建议使用SHOW TABLE STATUS或EXPLAIN替代。 + +## 执行计划中嵌套连接深度过深 + +* **Item:** SUB.004 + +* **Severity:** L3 + +* **Content:** MySQL对子查询的优化效果不佳,MySQL将外部查询中的每一行作为依赖子查询执行子查询。 这是导致严重性能问题的常见原因。 + +# Query: 16C2B14E7DAA9906 + +★ ☆ ☆ ☆ ☆ 35分 + +```sql + +SELECT + * +FROM + film +WHERE + language_id = ( +SELECT + language_id +FROM + language +LIMIT + 1) +``` + +## Explain信息 + +| id | select\_type | table | partitions | type | possible_keys | key | key\_len | ref | rows | filtered | scalability | Extra | +|---|---|---|---|---|---|---|---|---|---|---|---|---| +| 1 | PRIMARY | *film* | NULL | ALL | idx\_fk\_language\_id | NULL | | | 1000 | ☠️ **100.00%** | ☠️ **O(n)** | Using where | +| 2 | SUBQUERY | *language* | NULL | index | | PRIMARY | 1 | | 6 | ☠️ **100.00%** | ☠️ **O(n)** | Using index | + + + +### Explain信息解读 + +#### SelectType信息解读 + +* **PRIMARY**: 最外层的select. + +* **SUBQUERY**: 子查询中的第一个SELECT查询, 不依赖于外部查询的结果集. + +#### Type信息解读 + +* ☠️ **ALL**: 最坏的情况, 从头到尾全表扫描. + +* **index**: 全表扫描, 只是扫描表的时候按照索引次序进行而不是行. 主要优点就是避免了排序, 但是开销仍然非常大. + +#### Extra信息解读 + +* **Using where**: WHERE条件用于筛选出与下一个表匹配的数据然后返回给客户端. 除非故意做的全表扫描, 否则连接类型是ALL或者是index, 且在Extra列的值中没有Using Where, 则该查询可能是有问题的. + +* **Using index**: 只需通过索引就可以从表中获取列的信息, 无需额外去读取真实的行数据. 如果查询使用的列值仅仅是一个简单索引的部分值, 则会使用这种策略来优化查询. + + +## SELECT未指定WHERE条件 + +* **Item:** CLA.001 + +* **Severity:** L4 + +* **Content:** SELECT语句没有WHERE子句,可能检查比预期更多的行(全表扫描)。对于SELECT COUNT(\*)类型的请求如果不要求精度,建议使用SHOW TABLE STATUS或EXPLAIN替代。 + +## 不建议使用SELECT * 类型查询 + +* **Item:** COL.001 + +* **Severity:** L1 + +* **Content:** 当表结构变更时,使用\*通配符选择所有列将导致查询的含义和行为会发生更改,可能导致查询返回更多的数据。 + +## 未使用ORDER BY的LIMIT查询 + +* **Item:** RES.002 + +* **Severity:** L4 + +* **Content:** 没有ORDER BY的LIMIT会导致非确定性的结果,这取决于查询执行计划。 + +## MySQL对子查询的优化效果不佳 + +* **Item:** SUB.001 + +* **Severity:** L4 + +* **Content:** MySQL将外部查询中的每一行作为依赖子查询执行子查询。 这是导致严重性能问题的常见原因。这可能会在 MySQL 5.6版本中得到改善, 但对于5.1及更早版本, 建议将该类查询分别重写为JOIN或LEFT OUTER JOIN。 + +# Query: 16CB4628D2597D40 + +★ ★ ★ ☆ ☆ 65分 + +```sql + +SELECT + * +FROM + city i + LEFT JOIN country o ON i. city_id= o. country_id +UNION +SELECT + * +FROM + city i + RIGHT JOIN country o ON i. city_id= o. country_id +``` + +## Explain信息 + +| id | select\_type | table | partitions | type | possible_keys | key | key\_len | ref | rows | filtered | scalability | Extra | +|---|---|---|---|---|---|---|---|---|---|---|---|---| +| 1 | PRIMARY | *i* | NULL | ALL | | NULL | | | 600 | ☠️ **100.00%** | ☠️ **O(n)** | NULL | +| 1 | PRIMARY | *o* | NULL | eq\_ref | PRIMARY | PRIMARY | 2 | sakila.i.city\_id | 1 | ☠️ **100.00%** | ☠️ **O(n)** | NULL | +| 2 | UNION | *o* | NULL | ALL | | NULL | | | 109 | ☠️ **100.00%** | ☠️ **O(n)** | NULL | +| 2 | UNION | *i* | NULL | eq\_ref | PRIMARY | PRIMARY | 2 | sakila.o.country\_id | 1 | ☠️ **100.00%** | ☠️ **O(n)** | NULL | +| 0 | UNION RESULT | ** | NULL | ALL | | NULL | | | 0 | 0.00% | ☠️ **O(n)** | Using temporary | + + + +### Explain信息解读 + +#### SelectType信息解读 + +* **PRIMARY**: 最外层的select. + +* **UNION**: UNION中的第二个或后面的SELECT查询, 不依赖于外部查询的结果集. + +* **UNION RESULT**: UNION查询的结果集. + +#### Type信息解读 + +* ☠️ **ALL**: 最坏的情况, 从头到尾全表扫描. + +* **eq_ref**: 除const类型外最好的可能实现的连接类型. 它用在一个索引的所有部分被连接使用并且索引是UNIQUE或PRIMARY KEY, 对于每个索引键, 表中只有一条记录与之匹配. 例:'SELECT * FROM ref_table,tbl WHERE ref_table.key_column=tbl.column;'. + +#### Extra信息解读 + +* ☠️ **Using temporary**: 表示MySQL在对查询结果排序时使用临时表. 常见于排序order by和分组查询group by. + + +## 建议使用AS关键字显示声明一个别名 + +* **Item:** ALI.001 + +* **Severity:** L0 + +* **Content:** 在列或表别名(如"tbl AS alias")中, 明确使用AS关键字比隐含别名(如"tbl alias")更易懂。 + +## SELECT未指定WHERE条件 + +* **Item:** CLA.001 + +* **Severity:** L4 + +* **Content:** SELECT语句没有WHERE子句,可能检查比预期更多的行(全表扫描)。对于SELECT COUNT(\*)类型的请求如果不要求精度,建议使用SHOW TABLE STATUS或EXPLAIN替代。 + +## 不建议使用SELECT * 类型查询 + +* **Item:** COL.001 + +* **Severity:** L1 + +* **Content:** 当表结构变更时,使用\*通配符选择所有列将导致查询的含义和行为会发生更改,可能导致查询返回更多的数据。 + +## 如果您不在乎重复的话,建议使用UNION ALL替代UNION + +* **Item:** SUB.002 + +* **Severity:** L2 + +* **Content:** 与去除重复的UNION不同,UNION ALL允许重复元组。如果您不关心重复元组,那么使用UNION ALL将是一个更快的选项。 + +# Query: EA50643B01E139A8 + +☆ ☆ ☆ ☆ ☆ 0分 + +```sql + +SELECT + * +FROM + ( +SELECT + * +FROM + actor +WHERE + last_update= '2006-02-15 04:34:33' + AND last_name= 'CHASE' +) t +WHERE + last_update= '2006-02-15 04:34:33' + AND last_name= 'CHASE' +GROUP BY + first_name +``` + +## MySQL返回信息 + +Expression #1 of SELECT list is not in GROUP BY clause and contains nonaggregated column 't.actor_id' which is not functionally dependent on columns in GROUP BY clause; this is incompatible with sql_mode=only_full_group_by + +## 为sakila库的actor表添加索引 + +* **Item:** IDX.001 + +* **Severity:** L2 + +* **Content:** 为列last\_name添加索引,散粒度为: 60.50%; 为列last\_update添加索引,散粒度为: 0.50%; 为列first\_name添加索引,散粒度为: 64.00%; + +* **Case:** ALTER TABLE \`sakila\`.\`actor\` add index \`idx\_last\_name\_last\_update\_first\_name\` (\`last\_name\`,\`last\_update\`,\`first\_name\`) ; + + + +## 请为GROUP BY显示添加ORDER BY条件 + +* **Item:** CLA.008 + +* **Severity:** L2 + +* **Content:** 默认MySQL会对'GROUP BY col1, col2, ...'请求按如下顺序排序'ORDER BY col1, col2, ...'。如果GROUP BY语句不指定ORDER BY条件会导致无谓的排序产生,如果不需要排序建议添加'ORDER BY NULL'。 + +## 不建议使用SELECT * 类型查询 + +* **Item:** COL.001 + +* **Severity:** L1 + +* **Content:** 当表结构变更时,使用\*通配符选择所有列将导致查询的含义和行为会发生更改,可能导致查询返回更多的数据。 + +## 非确定性的GROUP BY + +* **Item:** RES.001 + +* **Severity:** L4 + +* **Content:** SQL返回的列既不在聚合函数中也不是GROUP BY表达式的列中,因此这些值的结果将是非确定性的。如:select a, b, c from tbl where foo="bar" group by a,该SQL返回的结果就是不确定的。 + +## MySQL对子查询的优化效果不佳 + +* **Item:** SUB.001 + +* **Severity:** L4 + +* **Content:** MySQL将外部查询中的每一行作为依赖子查询执行子查询。 这是导致严重性能问题的常见原因。这可能会在 MySQL 5.6版本中得到改善, 但对于5.1及更早版本, 建议将该类查询分别重写为JOIN或LEFT OUTER JOIN。 + +# Query: 7598A4EDE6CFA6BE + +★ ★ ★ ★ ☆ 85分 + +```sql + +SELECT + * +FROM + city i + LEFT JOIN country o ON i. city_id= o. country_id +WHERE + o. country_id is null +UNION +SELECT + * +FROM + city i + RIGHT JOIN country o ON i. city_id= o. country_id +WHERE + i. city_id is null +``` + +## Explain信息 + +| id | select\_type | table | partitions | type | possible_keys | key | key\_len | ref | rows | filtered | scalability | Extra | +|---|---|---|---|---|---|---|---|---|---|---|---|---| +| 1 | PRIMARY | *i* | NULL | ALL | | NULL | | | 600 | ☠️ **100.00%** | ☠️ **O(n)** | NULL | +| 1 | PRIMARY | *o* | NULL | eq\_ref | PRIMARY | PRIMARY | 2 | sakila.i.city\_id | 1 | ☠️ **100.00%** | ☠️ **O(n)** | Using where; Not exists | +| 2 | UNION | *o* | NULL | ALL | | NULL | | | 109 | ☠️ **100.00%** | ☠️ **O(n)** | NULL | +| 2 | UNION | *i* | NULL | eq\_ref | PRIMARY | PRIMARY | 2 | sakila.o.country\_id | 1 | ☠️ **100.00%** | ☠️ **O(n)** | Using where; Not exists | +| 0 | UNION RESULT | ** | NULL | ALL | | NULL | | | 0 | 0.00% | ☠️ **O(n)** | Using temporary | + + + +### Explain信息解读 + +#### SelectType信息解读 + +* **PRIMARY**: 最外层的select. + +* **UNION**: UNION中的第二个或后面的SELECT查询, 不依赖于外部查询的结果集. + +* **UNION RESULT**: UNION查询的结果集. + +#### Type信息解读 + +* ☠️ **ALL**: 最坏的情况, 从头到尾全表扫描. + +* **eq_ref**: 除const类型外最好的可能实现的连接类型. 它用在一个索引的所有部分被连接使用并且索引是UNIQUE或PRIMARY KEY, 对于每个索引键, 表中只有一条记录与之匹配. 例:'SELECT * FROM ref_table,tbl WHERE ref_table.key_column=tbl.column;'. + +#### Extra信息解读 + +* **Not exists**: MySQL能够对LEFT JOIN查询进行优化, 并且在查找到符合LEFT JOIN条件的行后, 则不再查找更多的行. + +* **Using where**: WHERE条件用于筛选出与下一个表匹配的数据然后返回给客户端. 除非故意做的全表扫描, 否则连接类型是ALL或者是index, 且在Extra列的值中没有Using Where, 则该查询可能是有问题的. + +* ☠️ **Using temporary**: 表示MySQL在对查询结果排序时使用临时表. 常见于排序order by和分组查询group by. + + +## 建议使用AS关键字显示声明一个别名 + +* **Item:** ALI.001 + +* **Severity:** L0 + +* **Content:** 在列或表别名(如"tbl AS alias")中, 明确使用AS关键字比隐含别名(如"tbl alias")更易懂。 + +## 不建议使用SELECT * 类型查询 + +* **Item:** COL.001 + +* **Severity:** L1 + +* **Content:** 当表结构变更时,使用\*通配符选择所有列将导致查询的含义和行为会发生更改,可能导致查询返回更多的数据。 + +## 如果您不在乎重复的话,建议使用UNION ALL替代UNION + +* **Item:** SUB.002 + +* **Severity:** L2 + +* **Content:** 与去除重复的UNION不同,UNION ALL允许重复元组。如果您不关心重复元组,那么使用UNION ALL将是一个更快的选项。 + +# Query: 1E8B70E30062FD13 + +★ ★ ★ ★ ☆ 80分 + +```sql + +SELECT + first_name, last_name, email +FROM + customer STRAIGHT_JOIN address ON customer. address_id= address. address_id +``` + +## Explain信息 + +| id | select\_type | table | partitions | type | possible_keys | key | key\_len | ref | rows | filtered | scalability | Extra | +|---|---|---|---|---|---|---|---|---|---|---|---|---| +| 1 | SIMPLE | *customer* | NULL | ALL | idx\_fk\_address\_id | NULL | | | 599 | ☠️ **100.00%** | ☠️ **O(n)** | NULL | +| 1 | SIMPLE | *address* | NULL | eq\_ref | PRIMARY | PRIMARY | 2 | sakila.customer.address\_id | 1 | ☠️ **100.00%** | ☠️ **O(n)** | Using index | + + + +### Explain信息解读 + +#### SelectType信息解读 + +* **SIMPLE**: 简单SELECT(不使用UNION或子查询等). + +#### Type信息解读 + +* ☠️ **ALL**: 最坏的情况, 从头到尾全表扫描. + +* **eq_ref**: 除const类型外最好的可能实现的连接类型. 它用在一个索引的所有部分被连接使用并且索引是UNIQUE或PRIMARY KEY, 对于每个索引键, 表中只有一条记录与之匹配. 例:'SELECT * FROM ref_table,tbl WHERE ref_table.key_column=tbl.column;'. + +#### Extra信息解读 + +* **Using index**: 只需通过索引就可以从表中获取列的信息, 无需额外去读取真实的行数据. 如果查询使用的列值仅仅是一个简单索引的部分值, 则会使用这种策略来优化查询. + + +## SELECT未指定WHERE条件 + +* **Item:** CLA.001 + +* **Severity:** L4 + +* **Content:** SELECT语句没有WHERE子句,可能检查比预期更多的行(全表扫描)。对于SELECT COUNT(\*)类型的请求如果不要求精度,建议使用SHOW TABLE STATUS或EXPLAIN替代。 + +# Query: E48A20D0413512DA + +★ ☆ ☆ ☆ ☆ 20分 + +```sql + +SELECT + ID, name +FROM + ( +SELECT + address +FROM + customer_list +WHERE + SID= 1 +ORDER BY + phone +LIMIT + 50, 10) a + JOIN customer_list l ON (a. address= l. address) + JOIN city c ON (c. city= l. city) +ORDER BY + phone desc +``` + +## Explain信息 + +| id | select\_type | table | partitions | type | possible_keys | key | key\_len | ref | rows | filtered | scalability | Extra | +|---|---|---|---|---|---|---|---|---|---|---|---|---| +| 1 | PRIMARY | *country* | NULL | index | PRIMARY | PRIMARY | 2 | | 109 | ☠️ **100.00%** | ☠️ **O(n)** | Using index; Using temporary; Using filesort | +| 1 | PRIMARY | *city* | NULL | ref | PRIMARY,
idx\_fk\_country\_id | idx\_fk\_country\_id | 2 | sakila.country.country\_id | 5 | ☠️ **100.00%** | ☠️ **O(n)** | NULL | +| 1 | PRIMARY | *c* | NULL | ALL | | NULL | | | 600 | 10.00% | ☠️ **O(n)** | Using where; Using join buffer (Block Nested Loop) | +| 1 | PRIMARY | *a* | NULL | ref | PRIMARY,
idx\_fk\_city\_id | idx\_fk\_city\_id | 2 | sakila.city.city\_id | 1 | ☠️ **100.00%** | ☠️ **O(n)** | NULL | +| 1 | PRIMARY | *cu* | NULL | ref | idx\_fk\_address\_id | idx\_fk\_address\_id | 2 | sakila.a.address\_id | 1 | ☠️ **100.00%** | ☠️ **O(n)** | NULL | +| 1 | PRIMARY | ** | NULL | ref | | | 152 | sakila.a.address | 6 | ☠️ **100.00%** | ☠️ **O(n)** | Using index | +| 2 | DERIVED | *a* | NULL | ALL | PRIMARY,
idx\_fk\_city\_id | NULL | | | 603 | ☠️ **100.00%** | ☠️ **O(n)** | Using filesort | +| 2 | DERIVED | *cu* | NULL | ref | idx\_fk\_store\_id,
idx\_fk\_address\_id | idx\_fk\_address\_id | 2 | sakila.a.address\_id | 1 | 54.42% | ☠️ **O(n)** | Using where | +| 2 | DERIVED | *city* | NULL | eq\_ref | PRIMARY,
idx\_fk\_country\_id | PRIMARY | 2 | sakila.a.city\_id | 1 | ☠️ **100.00%** | ☠️ **O(n)** | NULL | +| 2 | DERIVED | *country* | NULL | eq\_ref | PRIMARY | PRIMARY | 2 | sakila.city.country\_id | 1 | ☠️ **100.00%** | ☠️ **O(n)** | Using index | + + + +### Explain信息解读 + +#### SelectType信息解读 + +* **PRIMARY**: 最外层的select. + +* **DERIVED**: 用于from子句里有子查询的情况. MySQL会递归执行这些子查询, 把结果放在临时表里. + +#### Type信息解读 + +* **index**: 全表扫描, 只是扫描表的时候按照索引次序进行而不是行. 主要优点就是避免了排序, 但是开销仍然非常大. + +* **ref**: 连接不能基于关键字选择单个行, 可能查找到多个符合条件的行. 叫做ref是因为索引要跟某个参考值相比较. 这个参考值或者是一个数, 或者是来自一个表里的多表查询的结果值. 例:'SELECT * FROM tbl WHERE idx_col=expr;'. + +* ☠️ **ALL**: 最坏的情况, 从头到尾全表扫描. + +* **eq_ref**: 除const类型外最好的可能实现的连接类型. 它用在一个索引的所有部分被连接使用并且索引是UNIQUE或PRIMARY KEY, 对于每个索引键, 表中只有一条记录与之匹配. 例:'SELECT * FROM ref_table,tbl WHERE ref_table.key_column=tbl.column;'. + +#### Extra信息解读 + +* ☠️ **Using filesort**: MySQL会对结果使用一个外部索引排序,而不是从表里按照索引次序读到相关内容. 可能在内存或者磁盘上进行排序. MySQL中无法利用索引完成的排序操作称为'文件排序'. + +* **Using index**: 只需通过索引就可以从表中获取列的信息, 无需额外去读取真实的行数据. 如果查询使用的列值仅仅是一个简单索引的部分值, 则会使用这种策略来优化查询. + +* ☠️ **Using temporary**: 表示MySQL在对查询结果排序时使用临时表. 常见于排序order by和分组查询group by. + +* **Using join buffer**: 从已有连接中找被读入缓存的数据, 并且通过缓存来完成与当前表的连接. + +* **Using where**: WHERE条件用于筛选出与下一个表匹配的数据然后返回给客户端. 除非故意做的全表扫描, 否则连接类型是ALL或者是index, 且在Extra列的值中没有Using Where, 则该查询可能是有问题的. + + +## 为sakila库的city表添加索引 + +* **Item:** IDX.001 + +* **Severity:** L2 + +* **Content:** 为列city添加索引,散粒度为: 99.83%; + +* **Case:** ALTER TABLE \`sakila\`.\`city\` add index \`idx\_city\` (\`city\`) ; + + + +## 建议使用AS关键字显示声明一个别名 + +* **Item:** ALI.001 + +* **Severity:** L0 + +* **Content:** 在列或表别名(如"tbl AS alias")中, 明确使用AS关键字比隐含别名(如"tbl alias")更易懂。 + +## SELECT未指定WHERE条件 + +* **Item:** CLA.001 + +* **Severity:** L4 + +* **Content:** SELECT语句没有WHERE子句,可能检查比预期更多的行(全表扫描)。对于SELECT COUNT(\*)类型的请求如果不要求精度,建议使用SHOW TABLE STATUS或EXPLAIN替代。 + +## ORDER BY语句对多个不同条件使用不同方向的排序无法使用索引 + +* **Item:** CLA.007 + +* **Severity:** L2 + +* **Content:** ORDER BY子句中的所有表达式必须按统一的ASC或DESC方向排序,以便利用索引。 + +## 同一张表被连接两次 + +* **Item:** JOI.002 + +* **Severity:** L4 + +* **Content:** 相同的表在FROM子句中至少出现两次,可以简化为对该表的单次访问。 + +## MySQL对子查询的优化效果不佳 + +* **Item:** SUB.001 + +* **Severity:** L4 + +* **Content:** MySQL将外部查询中的每一行作为依赖子查询执行子查询。 这是导致严重性能问题的常见原因。这可能会在 MySQL 5.6版本中得到改善, 但对于5.1及更早版本, 建议将该类查询分别重写为JOIN或LEFT OUTER JOIN。 + +# Query: B0BA5A7079EA16B3 + +★ ★ ★ ★ ☆ 85分 + +```sql + +SELECT + * +FROM + film +WHERE + DATE( last_update) = '2006-02-15' +``` + +## Explain信息 + +| id | select\_type | table | partitions | type | possible_keys | key | key\_len | ref | rows | filtered | scalability | Extra | +|---|---|---|---|---|---|---|---|---|---|---|---|---| +| 1 | SIMPLE | *film* | NULL | ALL | | NULL | | | 1000 | ☠️ **100.00%** | ☠️ **O(n)** | Using where | + + + +### Explain信息解读 + +#### SelectType信息解读 + +* **SIMPLE**: 简单SELECT(不使用UNION或子查询等). + +#### Type信息解读 + +* ☠️ **ALL**: 最坏的情况, 从头到尾全表扫描. + +#### Extra信息解读 + +* **Using where**: WHERE条件用于筛选出与下一个表匹配的数据然后返回给客户端. 除非故意做的全表扫描, 否则连接类型是ALL或者是index, 且在Extra列的值中没有Using Where, 则该查询可能是有问题的. + + +## 不建议使用SELECT * 类型查询 + +* **Item:** COL.001 + +* **Severity:** L1 + +* **Content:** 当表结构变更时,使用\*通配符选择所有列将导致查询的含义和行为会发生更改,可能导致查询返回更多的数据。 + +## 避免在WHERE条件中使用函数或其他运算符 + +* **Item:** FUN.001 + +* **Severity:** L2 + +* **Content:** 虽然在SQL中使用函数可以简化很多复杂的查询,但使用了函数的查询无法利用表中已经建立的索引,该查询将会是全表扫描,性能较差。通常建议将列名写在比较运算符左侧,将查询过滤条件放在比较运算符右侧。 + +# Query: 18A2AD1395A58EAE + +☆ ☆ ☆ ☆ ☆ 0分 + +```sql + +SELECT + last_update +FROM + film +GROUP BY + DATE( last_update) +``` + +## MySQL返回信息 + +Expression #1 of SELECT list is not in GROUP BY clause and contains nonaggregated column 'optimizer_RSq3xBEF0TXgZsHj.film.last_update' which is not functionally dependent on columns in GROUP BY clause; this is incompatible with sql_mode=only_full_group_by + +## SELECT未指定WHERE条件 + +* **Item:** CLA.001 + +* **Severity:** L4 + +* **Content:** SELECT语句没有WHERE子句,可能检查比预期更多的行(全表扫描)。对于SELECT COUNT(\*)类型的请求如果不要求精度,建议使用SHOW TABLE STATUS或EXPLAIN替代。 + +## 请为GROUP BY显示添加ORDER BY条件 + +* **Item:** CLA.008 + +* **Severity:** L2 + +* **Content:** 默认MySQL会对'GROUP BY col1, col2, ...'请求按如下顺序排序'ORDER BY col1, col2, ...'。如果GROUP BY语句不指定ORDER BY条件会导致无谓的排序产生,如果不需要排序建议添加'ORDER BY NULL'。 + +## GROUP BY的条件为表达式 + +* **Item:** CLA.010 + +* **Severity:** L2 + +* **Content:** 当GROUP BY条件为表达式或函数时会使用到临时表,如果在未指定WHERE或WHERE条件返回的结果集较大时性能会很差。 + +# Query: 60F234BA33AAC132 + +★ ★ ★ ☆ ☆ 70分 + +```sql + +SELECT + last_update +FROM + film +ORDER BY + DATE( last_update) +``` + +## Explain信息 + +| id | select\_type | table | partitions | type | possible_keys | key | key\_len | ref | rows | filtered | scalability | Extra | +|---|---|---|---|---|---|---|---|---|---|---|---|---| +| 1 | SIMPLE | *film* | NULL | ALL | | NULL | | | 1000 | ☠️ **100.00%** | ☠️ **O(n)** | Using filesort | + + + +### Explain信息解读 + +#### SelectType信息解读 + +* **SIMPLE**: 简单SELECT(不使用UNION或子查询等). + +#### Type信息解读 + +* ☠️ **ALL**: 最坏的情况, 从头到尾全表扫描. + +#### Extra信息解读 + +* ☠️ **Using filesort**: MySQL会对结果使用一个外部索引排序,而不是从表里按照索引次序读到相关内容. 可能在内存或者磁盘上进行排序. MySQL中无法利用索引完成的排序操作称为'文件排序'. + + +## SELECT未指定WHERE条件 + +* **Item:** CLA.001 + +* **Severity:** L4 + +* **Content:** SELECT语句没有WHERE子句,可能检查比预期更多的行(全表扫描)。对于SELECT COUNT(\*)类型的请求如果不要求精度,建议使用SHOW TABLE STATUS或EXPLAIN替代。 + +## ORDER BY的条件为表达式 + +* **Item:** CLA.009 + +* **Severity:** L2 + +* **Content:** 当ORDER BY条件为表达式或函数时会使用到临时表,如果在未指定WHERE或WHERE条件返回的结果集较大时性能会很差。 + +# Query: 1ED2B7ECBA4215E1 + +★ ★ ★ ★ ☆ 80分 + +```sql + +SELECT + description +FROM + film +WHERE + description IN( 'NEWS', + 'asd' +) +GROUP BY + description +``` + +## Explain信息 + +| id | select\_type | table | partitions | type | possible_keys | key | key\_len | ref | rows | filtered | scalability | Extra | +|---|---|---|---|---|---|---|---|---|---|---|---|---| +| 1 | SIMPLE | *film* | NULL | ALL | | NULL | | | 1000 | 20.00% | ☠️ **O(n)** | Using where; Using temporary | + + + +### Explain信息解读 + +#### SelectType信息解读 + +* **SIMPLE**: 简单SELECT(不使用UNION或子查询等). + +#### Type信息解读 + +* ☠️ **ALL**: 最坏的情况, 从头到尾全表扫描. + +#### Extra信息解读 + +* ☠️ **Using temporary**: 表示MySQL在对查询结果排序时使用临时表. 常见于排序order by和分组查询group by. + +* **Using where**: WHERE条件用于筛选出与下一个表匹配的数据然后返回给客户端. 除非故意做的全表扫描, 否则连接类型是ALL或者是index, 且在Extra列的值中没有Using Where, 则该查询可能是有问题的. + + +## 为sakila库的film表添加索引 + +* **Item:** IDX.001 + +* **Severity:** L2 + +* **Content:** 为列description添加索引,散粒度为: 100.00%; + +* **Case:** ALTER TABLE \`sakila\`.\`film\` add index \`idx\_description\` (\`description\`(255)) ; + + + +## 请为GROUP BY显示添加ORDER BY条件 + +* **Item:** CLA.008 + +* **Severity:** L2 + +* **Content:** 默认MySQL会对'GROUP BY col1, col2, ...'请求按如下顺序排序'ORDER BY col1, col2, ...'。如果GROUP BY语句不指定ORDER BY条件会导致无谓的排序产生,如果不需要排序建议添加'ORDER BY NULL'。 + +# Query: 255BAC03F56CDBC7 + +★ ★ ★ ★ ★ 100分 + +```sql + +ALTER TABLE + address +ADD + index idx_city_id( city_id) +``` + +## 提醒:请将索引属性顺序与查询对齐 + +* **Item:** KEY.004 + +* **Severity:** L0 + +* **Content:** 如果为列创建复合索引,请确保查询属性与索引属性的顺序相同,以便DBMS在处理查询时使用索引。如果查询和索引属性订单没有对齐,那么DBMS可能无法在查询处理期间使用索引。 + +# Query: C315BC4EE0F4E523 + +★ ★ ★ ★ ★ 100分 + +```sql + +ALTER TABLE + inventory +ADD + index `idx_store_film` ( + `store_id`, `film_id` ) +``` + +## 提醒:请将索引属性顺序与查询对齐 + +* **Item:** KEY.004 + +* **Severity:** L0 + +* **Content:** 如果为列创建复合索引,请确保查询属性与索引属性的顺序相同,以便DBMS在处理查询时使用索引。如果查询和索引属性订单没有对齐,那么DBMS可能无法在查询处理期间使用索引。 + +# Query: 9BB74D074BA0727C + +★ ★ ★ ★ ★ 100分 + +```sql + +ALTER TABLE + inventory +ADD + index `idx_store_film` ( + `store_id`, `film_id` ), + ADD + index `idx_store_film` ( + `store_id`, `film_id` ), + ADD + index `idx_store_film` ( + `store_id`, `film_id` ) +``` + +## 提醒:请将索引属性顺序与查询对齐 + +* **Item:** KEY.004 + +* **Severity:** L0 + +* **Content:** 如果为列创建复合索引,请确保查询属性与索引属性的顺序相同,以便DBMS在处理查询时使用索引。如果查询和索引属性订单没有对齐,那么DBMS可能无法在查询处理期间使用索引。 + diff --git a/doc/example/main_test.sh b/doc/example/main_test.sh new file mode 100755 index 00000000..96f72b5a --- /dev/null +++ b/doc/example/main_test.sh @@ -0,0 +1,17 @@ +#!/bin/bash + + +PROJECT_PATH=${GOPATH}/src/github.com/XiaoMi/soar/ + +if [ "$1x" == "-updatex" ]; then + cd "${PROJECT_PATH}" && ./soar -list-test-sqls | ./soar -config=./etc/soar.yaml > ./doc/example/main_test.md +else + cd "${PROJECT_PATH}" && ./soar -list-test-sqls | ./soar -config=./etc/soar.yaml > ./doc/example/main_test.log + # optimizer_XXX 库名,散粒度,以及索引先后顺序每次可能会不一致 + DIFF_LINES=$(cat ./doc/example/main_test.log ./doc/example/main_test.md | grep -v "optimizer\|散粒度" | sort | uniq -u | wc -l) + if [ "${DIFF_LINES}" -gt 0 ]; then + git diff ./doc/example/main_test.log ./doc/example/main_test.md + fi +fi + + diff --git a/doc/example/metalinter.json b/doc/example/metalinter.json new file mode 100644 index 00000000..6564bf67 --- /dev/null +++ b/doc/example/metalinter.json @@ -0,0 +1,23 @@ +{ + "Vendor": true, + "DisableAll": true, + "Enable": [ + "gofmt", + "goimports", + "interfacer", + "misspell", + "unconvert", + "gosimple", + "golint", + "structcheck", + "deadcode", + "ineffassign", + "varcheck", + "gas", + "vet" + ], + "Exclude": [ + "MagicWordSZjYPIDgod1M8XqYEwhsdlzv2SyAtjy8" + ], + "Deadline": "5m" +} diff --git a/doc/example/metalinter.sh b/doc/example/metalinter.sh new file mode 100755 index 00000000..7e3a067a --- /dev/null +++ b/doc/example/metalinter.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +METABIN=$(which gometalinter.v1) +PROJECT_PATH=${GOPATH}/src/github.com/XiaoMi/soar/ + +if [ "x$METABIN" == "x" ]; then + go get -u gopkg.in/alecthomas/gometalinter.v1 + ${GOPATH}/bin/gometalinter.v1 --install +fi + +UPDATE=$1 + +if [ "${UPDATE}X" != "X" ]; then + ${GOPATH}/bin/gometalinter.v1 --config ${PROJECT_PATH}/doc/example/metalinter.json ./... | tr -d [0-9] | sort > ${PROJECT_PATH}/doc/example/metalinter.txt +else + cd ${PROJECT_PATH} && diff <(${GOPATH}/bin/gometalinter.v1 --config ${PROJECT_PATH}/doc/example/metalinter.json ./... | tr -d [0-9] | sort) <(cat ${PROJECT_PATH}/doc/example/metalinter.txt) +fi + diff --git a/doc/example/metalinter.txt b/doc/example/metalinter.txt new file mode 100644 index 00000000..e69de29b diff --git a/doc/example/revive.toml b/doc/example/revive.toml new file mode 100644 index 00000000..4cf298a8 --- /dev/null +++ b/doc/example/revive.toml @@ -0,0 +1,51 @@ +ignoreGeneratedHeader = false +severity = "error" +confidence = 0.8 +errorCode = 0 +warningCode = 0 + +[rule.blank-imports] +[rule.context-as-argument] +[rule.dot-imports] +[rule.error-return] +[rule.error-strings] +[rule.error-naming] +[rule.exported] +[rule.if-return] +[rule.var-naming] +[rule.package-comments] +[rule.range] +[rule.receiver-naming] +[rule.indent-error-flow] +[rule.superfluous-else] +[rule.modifies-parameter] + +# This can be checked by other tools like megacheck +[rule.unreachable-code] + + +# Currently this makes too much noise, but should add it in +# and perhaps ignore it in a few files +#[rule.confusing-naming] +# severity = "warning" +#[rule.confusing-results] +# severity = "warning" +#[rule.unused-parameter] +# severity = "warning" +#[rule.deep-exit] +# severity = "warning" +#[rule.flag-parameter] +# severity = "warning" + + + +# Adding these will slow down the linter +# They are already provided by megacheck +# [rule.unexported-return] +# [rule.time-naming] +# [rule.errorf] + +# Adding these will slow down the linter +# Not sure if they are already provided by megacheck +# [rule.var-declaration] +# [rule.context-keys-type] diff --git a/doc/example/sakila.sql.gz b/doc/example/sakila.sql.gz new file mode 100644 index 0000000000000000000000000000000000000000..5cdf8c7798a5220a6388cc2e4a50620fef4ad311 GIT binary patch literal 690810 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%0D=${?#u7JKZGe&;!BDT|@kI!m1;a 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%0A5lypjni`@JW;7i>J=3y) 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 z zMA!?Ktuea1!nGm|AvKB>R(JiigLQQSjarS`5#-TI*F`~TS7^%`7<~jn?r4ZB=(Hh$ zS@N(4dfU*deI!x>GNL|qSwQY1M+`yGK|}~Y%#IAo`h8@QS~k)ut&xzg=Ee2u6pb&; zDq7SC1$;I(K{y$*dF_JbJE(q8D%qs&I9z3>vTn++F_E$upzW8F-rETbvceI{EV5#t z#tx&@*gK~1lGrFZ!cbVm4DU)XzA&@Cjq|95WtMBms>NhBvdg)U2k-ibit_Yv)`7p^ z*MiB|DRf{kaprU*D!+(s7w(+g0$>%T-@ZvgGE6(vVY-bOGhP8okKG`6^Ao;adBY)( z%-O*_!jPd&AA@wUdOyo{PW|S}&oIxiGi$lS_aR{kuqE#@adM9MF9{&Vj^4-K;eddp zvYZh#wfMey13m1dZMRdsLYzbl?Ed7O*-UYZoRg3As;KU2F`hCsp>i~ReneY6nL4CZy~Zv z_HSfy1vcG7hCGLL_eD4WeYG+N&;^e)l3o<;JA!wFm z#1iV2DXRnMPd-vTJT^F8MW_ecZ{&;$<7EqRL@z|+i~~Y&1{VXP8i{(eLcvA8Co>QQ zfR24idOI?XN04iZmWVgj&g|XqAQYRIJ8cMAky*q~E+i=nyyKGh49z+uO?tYhY~3`H ziDF$4jMy@9Ta{5?EPGHGxIvt57ywviEq7L!CrcLK7$OTkm@$`H$=hQ1HnWr`2nNMR zo{$RQwXS=y$Wk_u(u@oq@gc>4O6()+h1dm{l`2cJY%;Mi3&BDL7LG8o$YK+D7;k2p zK8c>s**vG7pE+d=@+NnN-I4Yk%mXi-B04w@W$M6ZrRfR@6TDmS>PJ?bHNAH@%1_v| z68z*xps-U&EWAuAW8sR*cX$WoDk$Elv+rgH&fx}Fwg354eo8oG%v zL7L3bPQoMfjTq5(%cQnJkgdPphQwKSNB#=D%Wg8NFt%gWS$6|Q2==4RM!&$^=mK<) z;8el2v91o*NbrS#*A0aGbv#tM=5c!#g@>{xseEQ*({xiq>&1L zGi;_Hq$QhsGd||n>0c}3_AuYed9v~dg_g$*Oe`|}4+AT06u9bPwt@{G_G#d`akmX% zzT2kNOqPTEDz{LwcRKc-TEGJ1^R_T?SF*8I1U6yWT@oFJ3qt*g1 z_`>Lvf=@OlGC(T{l5vB47GTW5xS(uQ24xNLr!2mrRa-H}kirQDh8|i63uUdHoznRM z#Z2Y1=r*LLN`+iDf{{Y4yzkJh9$N!P1y>d8LX=I(G*?0ixo)c2ML#bQ15vWV)(0pK z!6=NzjODR%o76~|9)-J3VJzm~!$eDqJ^(D(hry**L*huw?T7)&7WgCeHh0+n0W=1- z%E<|W61066dfi83<%W_CIn(49Lg$737l~n(1`jH`mgG?tW>>eeBrSvRYf?tAlL}63 zcOYa(v};(f#<0c_ZKsQ_kEXy;*|W!x+Xn7SE+@jz#m>->*hrbc$0;@JOwmVOMqgON zyJ2W6-0pf#(Ekd*SXtY5nET{XePdMnYBkfgD@q=LC) ztp@0a6;eg2fl@<#KiEWwMgrVCG8x_SFug&UaF(ELklezp3Y_qS6KtxioHc6(c= z24)`n!%TalVFKAkfvpdDhm{o_IjB!Wg_sJZ57}Rt$P6)q(x*kz$f(i^B@T>@ev2pl z40&l5ffPx$u|CO2OZqabMn>Rx(y#$h$QB?-W1{sd88>AsEUO6#?&Y`ljn0pf775M5 zz-v@=x7nPjEzyx+c?(D65>Ai=-NDoB0Aqj;RQRD+wA#8(-5TGohhWuIcZz9bKr0N` z`g39uPO^0XK?xuV4hi?zOwxV&!o(|Nc zbguP>1qAD(LFg>M7p> zSy>olZMGSZm3@t^D@s5lQqZ(3UXoqtg%&lOrpnC}?6NJ!cj7$7BHneybX^}A2~8u_ zWd$n3?68J_N|28Ya3!y7tqOWi9-b2jjTbumu%0A#VR)8~c;f7xX$6JTvg9^p$q-7W zhn#F1Hf(hA!wfwS_M=gw5RaRzDqG0u`ltu;<6qQNk3X2?r~~RkE$_8tUAxl$wn8}=axro~SzFj6cZKer)q@NkcAptfb5vm^DP2;q z#SWfW0?Lw6sXKMSorMdOb!9qMRvjuuaxiaT+VveUMj0gxgkr`tEKi|pb1BK=Qt{ae z+)f@gn+@LOgPA@Bw!7ctzi1{0=s*hgRBmdekyyt-R8oaW?-^f^}j?zT~afj$YQULCZ_E4|vS z6CBc=rSB1ySL&Vb(BA}G-@-`rPO%3#o-%OSiy0dN2+Q38x$oM4k+cu!$8!roL;Z|5_vmd^YOt`b1RgnXtiM2Rfju z0f>@mLIhR_6$*q$f3?-i@{7eLrikbPU@&a<@;M8np$x-%fWkWo*mvqkN2k!Y1u|Rc zk@R8+trGi1SOTrv%t}n9NHQ|v^Urz&fPRO0{e!d)ucjcB&2{hwmPN53%!ylC9T$&lxH@A|vLmQtNfFwzQXaVBiou!suzJlaw$Ko`Ib`*1Y zmN6HJv4@{nmZX^;?NGExz+_mEoGoDk;SvTDyg<0G&RzXG@f+BqyU}uU_ z_**6Vhu*%DsU3#|%nL?fw822OI}gMt#tQdr1gf^KxI;*nB*ZlHaZed)D#v)d^cCs| z#&FroB-zAgDXd-0Mny83O_BtMhV6COpdI6<+52am53i68nkNj9VhAWXqHPh};W1G# zFI~r+O2zc1OQ!VStazk|hw;C5c)r4*Q>5fR%IpVGy^*R!YH1K;ZdL<>FHm7imn_Au zpbb_*);sP24}Dmu(E;Wd*ur=St@pd(ZGfBWtt<4=C&2^{;>JhD_h4b z4wfFmB3j!+k^`&h~X4H{HoBw%cgVzceO8~rtT zyUex{h|XY-#BB%ltFXjJb_vPRAmPbDH_YsWt2>y8E=mo3W#1$aI-0m{Fq-0hP#%S0!!35@xDobWP#DbL)yrSKeYPEuzEg6?)A0;h0 z(NLSUl6~k_OxmOtavx6N1uT{9I#ymL9Z9fP=y&VB6wep-`N>2F*%KBv>gKqFP1et| z??-RNgV?fhC9mHo4Rj&PM7N^9%mmib*VVQW8{Zz@$emVsl9e15m(Cg+1b$Y(^sA3v zElV_JGrAtD*&cWbjNxpt%qrWXWH1ZM1fJ!VpzLV5&$MTv$5Ri%`n-bw_kIlt+<0Y zqjHv&9|78u{ycdGAHdg#i@>fx$??!76sIsl-%w7}Ws_TX7=oya^0%(xqf(`1e* zbxWmeMGL)P_~00zSXpB%LD20`-=&!om6ft_njnTARG0N~Jw)hmDR?c`nbL2okZj2m z5t;{rk;a_39$-mqO{GK(8=}JWqzu9_5=#ntP!WW4-ZOQ{B0d<-tD%r(nh4nx0d(tW z?DV=%tC3dyB1MOi^`Fs1<^v$0l**)#Ade{3(t(KZbdg7ztO6>olwcLQus0Pz=xY5W z84Ovqg*c}(T9cW~qc6D9Nl-|2?20QItcmTVtA3aRKsiKYB-?U9JrD?|-dBcbDP%IX&1Aya<=mCP5a28QE15mr6qZ0g z%Zh+vVLIPa5mM9(6$N{hiI{tLFugX~NI-LRH?VSheb_C)?`ON$yr%>RMm>0z*_^;P zrH-s+0{&-fb&Q%$0lfBlm;eI>i6P2AVpwt!P zdDzeyeLZS|97tgl9kR?GO!kD(6yKG!cNxamXuwM|6i5Pe-FRCPK{%{xiEwmRBY|dp z)C$KCtwE}RiV*aTYz5qiiec{F<*@b9D3r1$tl6esNTs$ESfQn~8g@@fN_56GMjP2? z#PkPb7p^yX5M9(UBvTqsgug-HExX53_qk7LAj8|2X!t5%GS5^gZGV#v%!wqvpZ5^uj!VRl6VKjvpBm`nKrNv;sbuNE)e$zp0Hmqz9jjDFFJ2jgfyusxF$VvH2XpHpk6mRu9c;)Q2jjkzmbhdk%ElG;v z;5OB>=ftD{Vy;J(_m8k7Fo-Hi)FCSzuaK;I$YCKdTUg{!p&oM!3)pl)toxD82fku} zWK!u9k7@r@I-&ok(==xDPr!0Xkr|a2C3%dKE0Uvr%8Qf5kwH<4135Bt7&<>%L?av8 zh7EPBq0!$SvW=j}TvWh!vf>g-Y&mOJmToah&Wyos4Q3K)n-T@)o@Doo@>KUvy)+r} zxmq>^Hm90(H9}geoyf?-hlfp740F&|t%L~&!RrP}-zEz58VWmUHyGjkY4{0Q@r{{Q zGDH@Au>{KztQ!~f%HCMnSE{Xq2nsknDUZ#BU!lcVXdB4o!tdg3^_|yi#N;xS zNO?g&w_5G19Qq)VXBr56L$)bX=tLQO z6uLHeb!|bYuAg~;M)$$ev94CKy1LvH3}7RH>Y7 zG8jIc!DKm;Z}2xLE|6!}nYQM82~Or)KVxirjJKrhEstUWr~)f*3;$$Tp$Oh=lZCQ- z56N(bI~bJjE^7jjnY>tosY^D_CuY4QnF?Y+4>nk;88SbirPPy0ka|sd84T4B0V{H# zZ=!^-NC_8uDf;)2q~cIyu_Rr_K)HG+`7eMV6r-Hk~vT`*RhDOS2Bwb9> zQX$dDVi*K$aW*yrw>Hw3O@XIlyl9o4%Cs{f3O5|1NXil;D^hDrEb!P^6w~kkJwU?0 zF^cuJ4MZ~A16Gi$3I-KfY{Waj3hOCADC zJRqp2tck%PfT9i{i)}+&t*Wyy z+Xi50-9Ft_4+I@{Svjo2OtQWe(&4@ukrZ?(vZgx)LKY-a0EE*zqaa73)JATNzhvKf zWnDO&x42C(!_;36g=k8gD?l=RToARAiUMX0x(ry$l(2f%onav*G^|ph>kRiwh0~EG z!VLcPh*Ot%$TMYzVfTf#WVDBEX&cm+D{S0ZE_Y+WP|^outB52?Mr(!pb>T+YNC(|K zdrv461UA()x-6jl*T%}vOuh7|Ffy}QC@`(Ugd_&Uc8LYv1gp}s65nCTpp&x>=?OiM ztd)KzEyuxLz%E91x_DgyDTCRVVB5%Gk%|l~17HDeYlFwElOfn-=#tfikm@TAkSsOH zNr0o;EBi{j@#e2=%iW-XHZn>l$E4gwijlz>5FfGKgT^}qY*IF}VsOOeKrRTAhCXTp zvRVioq7hwnz1|bdk}d&CB*%J0_$}7{th@T(3NV>c6~i zv?0!JU?(tPOcTn(DRSRwuR^g8eNw_bz|dy&C#x~@v5-*@4#ry%L#~Nzg@v(fd{OEm zO9$M}Hm@8&X<55WGRlSl>2Fmeva@~7whGClC|mIq`W1&2>3a+W^}tLv#s%dHA^9W; zD3L4Ghr;nlXba?nq0o<~=k9m?FKDZin&Zaz>&+-yV(b}<0F#w6I3zPI^2-uj*~$!U zE%Wjgfhv@QAKErBWQHmm@ezEslA{cTueTgytFm?n{oKTc5cY(m17@TIgaG-|sqmlM zDlc|Bc53xLz+ftGYF^$1RH+2Zm=!t6)MZ3_uwNP(gN!32%zEZ(JizikX|rL)0IiTj zwHv$T4z&;2PBTW;5Tf9U4knZfJAb^QLg`Vs|mZxLQ zhD1X)L3Y^f>|jtVpwF_i>6W=YP!Ar>K_sCpvD~e964eNA?QU#ljJIs8Fq8#_h}g>v zW9Am)HUt^H+Z+@~H~@&;->_5LLez)tOP|{e!!sshTnGo_jX*!-VI*ga@H2L;Acct* znx7=c2lE#C*Tx=nGGiKaeP^8{X+l!o%fP!dR(>2?nUj7_QUsPC;VU?0d{$tSAsl}e z4K~)p$;bix2C?Q8yeS$oq{bYLvltXECcIO;2aq0bnGM5zwOe-;MlO+A@fnX;2?Zh zXeKJi`&9n;li9Y_rg{N-Q-74u)JQLx-H8yguy2xmg$zLLHV~&9-B+|R5Ej87o|j;B zMDnGb0{0Fbh=MxhB<;}u(gtKu5W-uynFvMlz0+une>6JwsN7htL834*tvKyn`X9UIgvFeAZfr~8)i zf!UDl63UpYRxL}ya7b4UbjTMedda*meP2Ct93EH#h>*jZrw@EWo$5h4EU;ubdiPk=YuIuR{*e2nZ5;mOaA+do|FzV*a4a&r{k@Yy*I!Nj^q-o~?37us5^H z9YUE1Dp2QmI5M(R5WVq3dP#1rXi6ofg`iz}ESr8}Ob>Q@<((~mWX?n0YJ)I*WvA6i z*IAlh^%@SN*^3DS;7{WPDz*_;&tAy;0cs~I$}U5P3%LwUVtdiLI0|YVG>h{2lMNWM zNVL*6Xtvn~v>kMPgMP?E@WV79E!>qwenf%=;(3|9UB+cmK0?&F8TW*ZmrGQ4QV8T% zLa`kyJsN3(hF^xF|8N6bY zz12LULMoX}HZIE;AJm`nCZA%KNMdpDUDBckrwOLhg5}C3t3iBLB~xdyWg{*LXh7XB+>V~jj`nUa7K zwG0c)Lg}X<|5RM@^ z0!dqC50sQ>JHXU1q2BJCfj5hCB%_wa{K3IgRVS*F~$_|??CiU zneQ+Pe3o$)3$j+iKz(|XLwyEALXv{WFfsAtSJnl$9Uk4)Z%Ec58H&!Xuf%6-DHr=uOu5L3@qF0e60tA*D{KT zJQZ|Xq64Qi5DJ+U-Mr(ClZb@0C=$%WC(C*FX=c8%alhpb{Hs6pt#%gQG*ds zt)CdJt&SyJekbSy5~Zk3AE3#M_7N*|8#6LeM#-@Jk&K#BDWjC_r-ZLBCUu^0!Vjs_ zWhio%vZX{-um@J!9T^m*!Yj}>OE?*4TOon43^lV$m&k3W>bua6UTsk_Rba&{E3=oF zf~qHHdYI{Yb6rptQ$%aV@MCIQ& zVqyg-6sf@HN(KZ#hGErXQy`jH@zJ0RXl(QVI>54Y)810d>XFrv!RXzBL^y)Ih0016 zSW^P?4D!gs9}jDo=_lE{I>O%JSeATR=yzBNG1z`S#EBIwC7TD1Yz^M=*sJ+svx=iG zqZeqI3KLUoUa^dT08H)&HCkv6AB+Vl$xvj_;VSU7>sa+`S!v-+2&8;y%EAJ~%j{^B z+-~gYRQ`jdN86yHJn8`xpj(v4N-iTyy=A@kX34yAL-wgkSXb`gv`E)`__^)V;X01S z@(g;hBX?lnUS8GF+22;f`8Ip8V8oNMRz2aVm%|G71Muo5fmB+@ejB$his=&kkBUC! zFd4|9OOhpNG+!2?xC7J-%S=~eXEWxkx-sY0fw(Z710_w~7vio=pJKyPGoUZ)#2OPN zM`@Q8iW$1&VA~OyfY=!jKVTkJSfdvwgK#jk%SgD-;0w(9ZBSR9O7WGrx|2LgP98cZ zuNTM81-MNSh}|)@zj#6|y%$-)61AKaVO6ITwppus4}*Qo$V(FExr;4tDB5 z6OZ9WiNAcDQbJ2>;Ee<|e&QeeN8*i=q-@((ccyu)^E2Awe-D7zMi=iqj3xEd|r*Qem@Ddl>@*+Kkn(& z*etL3wK` z(8Eh&TfvsLLg59v3K{dL%n_+!Pp>|Ox9pO-rz0w~?py-8L9EtUd!V#{Ft9Tp0aukG z28%OCtg6UW;^Y~kP|=5=-s{AmaV$N}B?M6}%47{Ito+~nFs^oN zE6HqL+gZ4C5>yoip7o|Y`iTSZY1>AFKrwmZfolf2}jPax4`u1Icp%@B&kV!JOH`%RshMXOZGd+8@CBg%zXzDtF= zKBb}&MO+NDMyq7d3pKHjn~+$@sO(l{GjTD`O0jgYXs!~?Nv5IIldL(savz&6^|s`Q zsm!dIDSlD{YBAKVoz?#ivviA00+o!6!`Zw|S|jRP^ECjU&N0cwL!s?AU-9&|2aV!{ zRj>)$Hhy4XZL5+!R^x0mL`Lk!N7aE`wv0dYD}xMj1&S}^KlDo^E1bjV3lTp2 z7;NZ-#G1w8yZ(axH>sAhM1pXmSb@K%+R!Ub;9Nc6FCZ?7RvN&(z=$mXV&ziJwnQ6*Nj@frSe(2qikHVg1{N+x@#*GHc=V9V@3WOP&(Mx;p_7LEtV~V1C&{bg@)cv$CO1c*#VJO|p0PMCD_JHob8Hpb#8c;mMlaS$_ z$EKLqF?a}0`Gnba0~n_efiNe+zu8znwQ{_FYK`X!tyir!2>gpv_rMF!~D_-_dh%eep5(*QA!aDOWR9Ul>BwWPs%hHeqix| zSz>vH0Dy$np~A^bsW{RvlY^#?NzQ59&P-o-dTfX7`eql7o*y5HC+XX1U6DflMv(`e zF~!70C(z?cXRRrgf^8FO;JRnl#n-WiLg}6HXT`=kS`P;YD^XZXFQL;5E%LlnZWYY{ z$J?Y`1nvb)6uhGBuD98LNfd=v7DTW#ZI4tYnPi0iqbKBvc`dVjw*L}1K@CHmnqtCo zu(p+9=!FqG+1*p3PS>KX5RL49cXTZj04-JwWTuJIaD^*K7A&li74~E5(K0b8(63yJ zyyQxjxfzs7!LfsuqqD)f1eTRcS6~8_Y>`;(dK+|rBP*zn!^u1w)_-H;kbZ51IAsTT zv6><_glsb?s06H1XHeX-Nj-^?WvjS@T~2+++nu0Wow6Ob!iY`VmnN$ekNS#iW3nn- zr2~d7Qig!LW|U{4DmzFLNKjoGaaa!Q8+QspDSK>GN?JSV+NXmpEAnmGNT2m|dRdf{ zp_$MxQ=GtNTq(?wWx2M_I0q{=;exT;QZY>;?2_rOmWi7MN27R$g^B)7Z73L%0^2J# zr;ld)Jw3cM?V9>kTa^mV~1%Fy}*+nF-rxEu_Kp5RvDTxE5RC$?2= zC(C7hOlT@uRMa49i?$LQ)2iI_6I1I~>6RsZvW<%bpb)*W?KrK6WFIQ)Y`o9V#Oq_^ zQ4N(U#cVRCXHnoKqul5}0K)?gDWeGyGVW}n224GW zBuzageHln$20XIqdV#yDt_OR`(PlWZB75;lB)s1!wGy4fT_jOZj97=-9qDP-G3M^ z63J^+;MF0dTn!Cu(g+(De5W|249cs>AY>Q}8-W||NHV7XMbcPGYn^xKY}h}t!@($- zAqbO#bY|=_JH`~YHG&LDZOaU~DIS$XQaozO+YSN6Y*4hdW99{rLza~(E;@K=PKG6< zo(U3eP!y8EY<()7&;d6ww%lY_7hp~We5HQdwiARFi~lm@*GaO*W6BhlXcZN(TMMH; z5OZ|!d!0pR3Jkn_U|V!ythLUO1QaIt#zJ68l)UGX!L%qw)U#N08Ai@o!Eiz&@~N;L z^d4l{l=CG$s*@j)y;W*vJ5P6O=%w9yE_yTtVF+P>{$05MxrwAtmLTR0UWf&%h}HOM z;7Tx_5ZD)(?4eqCqNZiv5QNsM5~~#=Vm7-1y2#3!DZcEh%_m5nS@%|PPV82ZASiJ; zn%>z-Q-L@(vLFa{FBsz!`6w*m5ys$EN;A$`kip~UWIZ@m=tY*{))|>!ZKUU5jrj_I z(cl2J(w_xSAXmA_%1E-##NyLarbRKfY?)i3Ifvbbai+z2+4l|PJRQM&HTWt4S{tP+(+fn82UM3PUAsobv~5rP$4YXtI1(O@+m zzhKCmGXcA!hXxLS4NSVNXlpK@(xy5ic~YRV5DOweKMvt zwsOlegS=urJ)n42VM@(948pJxIb&EonNW~2=pT>*9^+TY3d&=dwW6N9%g4enDXCPB z45_wWN8ai1L!#mcv5f&3WJhFmpl$nNn>%h5{Dv|@{+9P^5g}mEZ1Qrhi+6@dn&`s+sJeNYc zgMB!>M3&wuK9l4I3~Kru%@)1R(&$qe%Fcv7(3;1@KOgAwbP(s9;V3(t=+n2Pl^vps)O1A&A}!aP<6D}k;Fzvl)Xa={t6r> z%<6)e4eTRkX*9H8mO)gdn#y7_)G_^_uSWfrW}9q_JYKL!FUSHXFp2>2LRR4M$!1X* zcvtnPw#v82JMVPlfI?F33La`^{Zg4wZV}R8eU{zp$JqK#$I5uxpaRvcfk5k6>0DVv zf>Tx)%s})4-2z}L&jMuPGJbcpfo%r%^eNFTn&*Xj8#|n3LZOQ(1Q)hj6C_0k@^#2y zFKIB5F)+Opx6n?i2`~l zQ-B9ojstl?JT~g|kT>eTVPR=7O5ONd1wkGr_^bdrBaI%oI-cy5KrpWt__}l-l1B$I zvV)NdUv9sF1!M<*&slQ4?7d+20yHPo>G0W^%x>bl%9C1xr9<-0qn_)ExQF5tJ_!%xD1F^jfCMmb+F~5 zx?ikdC-Q1GTEdLafxOa?JfV-1jbu?5NDKKpB?_x;TYznx85xL7SzahPWUc@=v(O@< zJF5SH|E;u4J98DsrbN3ds0Sv*M(cFSAhy$1ICuzEWWPEa-z1dO_n1$|r=}0@jih{& zf6axN+Kz!M7{(P&9^_hrtPlxaVcAMLu0g@yLfD547X5Rj)>e)NKkc5_BQ2fY!KCbp z?l;yqfP#`0EFTYxKSU7e_9Bjo%}p*5JWSJ~l!~%~LP1ck3qtCc>37fx>@@ie0+M0s zvDcYME}276%xLWzP`#et0+H4@KfuHY<1ENF78SB&W<>`epF6GjNvbCQ331>`2m91* zd0qHMjws71xn__`dVKz0SLLQ(haJ@tf>vQ5suw* zIN2eKRwpKiq{_umCs_#7=Wwdfu$NKUmD)-;tJQ|U!0U8!m1-!!g}xnNTq%_7iq*`EM{9c5f5ihr&1IMhX=mE;#^`=off6$7Pz_!CD91y3)LZ z9i%D4Nw~m=p4i|6n+}6~k}9S$BrOF|f@DFmri4-nxB*W0{E1{4n zmxNLTC$BV{SzsOB^*jf5O-U6z+$`WWSxd^l*r|vWCL?Y;+bA1mkP+jxgkVSnt1!w} zgM*8XsxX@103FlMW_7e9zM-~Kp$u$n^W8i{7}mCxZD2dRUJAQDHffikoT*I1lL?h$ zWjk`ifygVBSeIiEwq5q4_nb48E7~W;>P4+uWr+;L-%i0M}POk=z zQv#}kJMYbiyOv_>()oo4{9T95_Ty)nhwZeP6I|4hsMd&?)KGaPZ3lvtS>wPIfq?DTA=-8Mq(0wCw z2w$g-<4{iO9Ze=OJ3?7d1iVn?hJG1LkPF=O0n`n2q{uWy`Xw;&&g&w8*Uu}5R}eYA zLSBQVR7$c`>>+(yXJsu~evBqE%hX*%(%;ou0vTXWWWV zeqj1!0*R5@W|_hs2e&GydRhxSMyV_GMxHeKbj>GZsew&rX<~Dg zS_%0kLu4`|k|@PwLw2O^d_`ILGGvW~9`c#{0=`h0E7KvhuX0^X2?WS=7UyJTn0#^= zjpW8Tazi}Ag2i%)C8oRqMR6y-6&U0(BpKQW!|oDi!viLh?ik|z*u7wg!?1)^20@{$ z-a30vSr|d2uu^J;W<=rlA*o`}4hC3fS!V~T#DYODSyFNe798@PZG#_0i39*SvWtZ* zb?YDq2-)OFEw_jjd{ba$42#96t}N-aE#LAc6f*RlWUCu_AJL3*%mje284!K`Gef#4`GOc93?{tetc+T5v&YUa-U?FsQ^ueu z#AQLEBuB67R^kZ_U0G$7@PP2Z4SF|L5fsMf*%7bA{E%s_Q2q78cG<{!0tW@r#y)!~ z4{Kw9X%!Yivg;F~FcL?p8{}ArWvY9TFY-h-hmn6Ii-!v2&g_d*_@X#AVXyEvr(^vi zkGGs7_9E7b9f@hKI5wf`+kxHM;!K?-I~I_w!q+3qtXY00+47F4ti@UoJY$X7jM_Xy zB+wRovh?fFZ5?dzaJIHu=B%w-bgX{Vvq?+X%HntV?Ug@|N+K;c=kuVlBNwar*`lyw|C#6*>K zh;T`y!fYM`n~Q_uh1Dupz!M&wGx6#ZQ~PBjh*i{V5h$CHNTpdeX63_kgT4TQTtxLO zj;UrCqAf6o2$kdxrmAB697Z&{kSnmyhcfvgvvM%#l}}b67uH@Zo~aMk!GSnFp*B)@ zq^p8)QSvSyFAH`ACLuEWte9=16~;ajNHQa6V2_|5M*~Z&7=$&IyDTIeIR~7IQUwy0 z&awj}in>Pqs|RFPS>RnN5S1;!KDmvCHL|gj{BG9Pmx_DY0p^ht{M(7_64DOGLSA@c zm*juu0a6q;b|+{%Ix(tz39G~oGWo^~04eTCjMMQ@kEfYaGpAh#<;hh>Gi zF~fetSxD5oj~u_i3eDmQIO;Yk0!rWi|83n{uPs+`1>koi{=>&=#>gE`_+Qug<~T`B>RylP(9M$P zq1VjqF7P)G&&v&98P`0CR84GJ`kNcs-NcHH=)+sp=LwE?PTo8Wg^g;zDF^4yN$B*c zZ1`}6)LKlu*GoP2#>Q`;7MbtLfhL$thJ!KE{Hny3trglQRtPVhd74uY7FE9 z`HcG4WV<6^ojy406^2MT%xs==Xy_hu;-MOwN8S>W&)0Sua71C<#AXPxC`;SD^o7R} zPpeneAPge~?$mHjRH_l9*^&4%9YaaR7+YnFuyuDagii$q8kQNRBw^ z438uC2Qz3)nPKyF4CEcyf#t1EmY8#~Tz=*O9lHLKKYi5i=hIuMukd!COEa8Zs!J?0 z;NOPfPR_@z&g;RwQJ6jH_7Wu1YHRZHKM!A!EOKKtoPRfqc z{BXTcpOTwvGQEng^M$czNL68m~C8Tt$p%i z2n^1Vw9lU4n&aE&WY>c4LPr^IW9ET*S_i=WAMDCi#9Fy4UDUx2m;Kp@?aua-n0Ia5 zLPu?S9XX3Eqga?4a_PB38rQAEL*RcLkO{CNf)!RGRof#(+<~nLTO6RydbT--G(<`C z=Ygy_vRytU_2ET)vVZFpv}*A_^XMg{GV?-wO*6?gUCk@v z@VO2|W67@QLv%SHD?9g@_iM*I>uoZYr?9tOKT$#c+w*9d97u3bX$K>r~) z{i*FPvK!-80v5?0c}-;Q7E4Ku?@lUIY}TnEJU1I-3$3!0Xt%vk)EF(#T3xhDyD_-f zsDjRhgJr>xL>DYf!jtFoQaewaHu8AZu1y$T_h@hOCI>*T0kM7KJu)VmO zKERDbV3L#%$ky~Y=2><_hglmO({Y%>tVn7tAG+6RBFYv>^sq5KEYp*>N*dp*yIQ?` z*|qjM9?l8L&?LcgV{?3j3e7<^X%qJ29JlKM&T5f}KXL{o_;| zBggdD`AXsc+F(zRpkU*X%)N?lNg|w=oL92cGY_DJ)HByyB+NN~VE&*TN*e8ij0YsX zPA{$t>+Ga>J&#;6%F<`@8pqGIMDriq5=C_IHTRgM{N;1X$J-?K<`vUO^-)5l3tfIi zP-=o`GUv)cM`A1&)f}>&9rd;c(2Tmx^pl>xHiGg~XjVTm*|V3It%ZZ&Af43|n^!4T z?qfNoH_V;9N_uO-gPJWI*OInxJXU+NH@)c~uPu)-h3PSr;jr-6(RtaHd<+3e<&48d zyq1>+R@n{hmTuTQ5JIq5k}Bsokq}b2U03e7Sn+kO71*>wV{&C4QGSTXWA-aI&iYZC zRTe$v9epv_6ZM`zI3I7c$RvoXwi;6oq=Y2dSP^ebI6?xb6B}II^XXiIF3pI6vn5tdmEDAxq=me&(Pr!kS0&Fp`;!ubZfK%|ee#of z0m2})m%yX)jpF=VYOJ^Yc9z1Y;UOB4HD9Rl;cNeJUeA}jTrRYP?jZM+gelji+>9e9 zWj>c0PA-Y93$6lR!ga99-MYOGmZAagYvyUKt*B&~*+vQF;3WD)uf6Gk=!XW~1{};w>%8bySwRfCQK2Z+~};bihhnm0ezLl=S|lHdMj@f zr%A~_trs2ZMQOnCGeGpI&MAfIIUf#vwuMn>Um8Zc{d8)T6j9qbSB_(~(v|3IK8cOO z%k6!vG)-wb1kolOF>58Oo;!1~&Kr}?kz4N)j!Y=eHEt<}@)_iXb+;DB7|58%`LI6T zSzr@oNCkt@r(B#E03BWOTd^@Zxc`vBzOEy30e2e715tXkAKH8^@rUeC@@k&leNr&x ziV_5z!gXuxYI@@{4U1H%9oz;F3PWF4Adur!6M&6>sbP3PiZd8AlK`R*M;~;l)0s4< zZ9P51f|GbS4WgqM_kSnOR8ucj&t>g`J*yVUr#$xuKfwj_O&QG8avz-QmKTCZo6PCB zP0Q>5UCOu9s5-fZaoitZKZh9DeBeH zPVM?EgEhDdLSy7ieLnfCBX>o;H~4^}(XmVNoryXH(Vf#w$}sxMY>1!nqIt}b^cl73 zo|`6l0GHM0%J6xqumYp6ciZDbOJCSH zoLofm8Sg{2`V$9fhTjz^mpryh`+T>WU(2CdGixq&K09X8tzTdhjvE{^dJawbgAL8D zR}VrDRzJ!za-erGH<2{sqU3ZYDztQ*$*`R~P);MQ$k*S0JFhg@j z6C6|NGid0NOUZ#Q>IMV8bt}7r)B(Lb6!h>mv7%@&6st({XdSDWcjsl4PeFn+1AdlSPoNEqR)-;S>QrM;n z!LHBa*@t9~7RZ_(yx79&gQ=U-HKp=;Am+R;7K+FD+CLAdY7b(%WEb#YtTZJmPpDIg zWBuN?8X6@sPOo_<(@vOgJ{Aa^FZ$kNm{sXLb1d2?xly-_6Voz3HHAC1lO4rd?)<;^YzI~OhY zHCL$U*IZnZQYH-zl+SHFP#+v7y`D{Bt1yVgc*sBq&qv| zoyYHD>jwFodA1&7S0c#f8E1Kp->gPAi~n-)mXb8tyhm=@EoNCQ4b(p<>gOLPE|f_f zd27?Op98uWo2=6eKH-az9h^2}hocjWsiwP}%Ptx6(y+>{yZ4z_guMz>>~ZDXKB)S; z%}&X|kc8y!RF8ehDIrI|+#9J;QaQT;U3|jB8iguaCCNqolhn}qS_SRR-5yqYYrqyh zf8JNBDh-?|3Flh)^)Y+Kia&UP8_O{CuHkW&x)ZoWB1TwYYqYKw9 zvGo`atH#I-Ij@^30a7T}y=3j9JgV_@juP5Bdvdl*!uyKJlZ3C$v=b4?c~?jHo>P%d zZa)989$NBwMJNaxymsTefF!DnNRINu~@5-hB8bZqq=i0#Dd z3BXSXd1%^KWnG@9R5Z`+O*14HW-ZwyD0a_r^t&b=#ic7v^!4rEVcZSn@G84Za8tlo zZ6uc;7_S#?i3I0_mW6j9pHX>|YQjPZe>TqVtkx#m&z+ltdSAPbyUlOnZVDd=_8hMc7ou#`!i z1bWBauB}^p){JK)x1a6{86rcH8m-a3$G8H_XvI zo{5#xt%mC_nUWmxZRu^aAvm`R#%3gZ-N)(R_ z|B{YUYfpV*mBrCipR_xUcIe*H4N1CR%YnR#^5vzg5$&#HqWYC_c5)68K(jsyZ);*ui5*YW7K%T`WlJjNJ#Ll0+eL0*{?%Vljyj zLo!^j#;nB#02e=zSH=h@yC6~3hVKsQ0Kr?=JH zTY734=RQ+!tEwNXf;s=gz52T2nYd(AUFwV@1Nf7Mx{5XQ8A)*8G7`@tDIew)XV}6} zO!JGV{#E%Ycy>pP7IG4Sg1`E%%?VV+yUi2s>E@5!LjHK!l7O}(wu6egg~jBoprI-0 z$PRhsyT^(_^4)rBsLFXmFa*Fj_W4k92U2J~JxAI+^(%ODBx06_J5zk7a2?pTjH&bz z?Q-4+hL|byXEuQg?>-yfbWOOu+S8sjkQ40#k%EL8x)5seBJ(9is0ovE79A{h!aZlw zIRWwYS9H=?<9|ho(>BY8`1aT^c`3#sfbI*-SL?BhRUOCpalA4b_m@wCmN^MW& zi~&_`{q};_MWk(3ikK|qa0LGFa-m^0CvK8gPRzB(*;8jfNvf_)my_M@K<5X2J^2Ii za%bpJw3_@uEe|L7C80ra7=VnK+*>D_@#E&Q&PdIZuOP2E&qN7KtP@QFQ)e2U`D7Lv zD0_p9=3-SpJ0>#LjiAtvLPT53^j4LTDv$M7sb(P484Mi^x)xi{saAJwu2sn{Y zPduP}|6l_ihb1x6&U|v)RqF1wCY$6r&b(gt+)M6)Q684?Wm5vG1UYpr@4#!FR`D6)|JHO%2ll-hoJ zRBdabR_;{34P<`ilp1FNOemBp()) z$y`F!yVL_T{%Vu{B4o5Ac}_$8Vd7NaM6@ zR02(;Q|H(Q3jWVsy<5p%+gBU|2!dEgcw(M&>Nt+2wl-56uJX?a2npQS**73TCGPI| zfCy3Ce8u-YWnmtxVG+`0U;E_FNWejG1@1(0stxGWbG6`j)EPU7*OGVKefX;J0Ezyh zBu*?juwb)-HgDa_*x{=fXlsnx* zzE~+Di0gE_U~hu$<4$<$i%F8wBAD^A^4|qa^uw9Fhx01oJ%c2lFU&bz)%yx9s^N*4 zb~zG>J3A7~WHg)*7F&3V1KcO3_5EDWoI#^*Q^Sef%86UHhB z2E&#yi@GK-eyoo*{~@8@@6ul9O?0ZdofaBA2mKEejg&X$ON7pGCHYxjM9)is>r|gx z+<#>45&z5h-S$;pQP8zjKnuc&Q{?u?wRZjHT0OsMkkYmaVx~s55|YLcZAXn7@=E5G zYsHReM7!TiZ&K~q@Zw6Cr?qamzLy@!^>Py5lM4bLAo}snr4J6-z4J+%f^kVWf`yLz1S_gM;O#yQQJ0dwL0dP1&sj>St4Xa%ni7b~@@)<0lPD#Jxkdw0$ z51e)r9z%$PrUg7ikws&x%|*&#Dq)l|7=)B#xc~$-tm|F1_P~blo|uQj>fXfE=%`Tx zDv9Y7DtUF|VC&A>d`iI7o(I=!%7(e?%fo@&?Loi=POcLP$OI~d&0>!}sfAo&fcBP* z+JyGN-Qdg=nAbHGt!Qf2^?US`F^_AU&OYxd>8Z1av}wkkEC#4)1rVf6IC3cx$2J?U z`3se3BwP~s#*2~$|bV|T+$9~HY%CKCr)2)T{{hO_ItA%@B}|5T;64U&gEncj2JCD zaKK^>=Xh{$p~12!aueW90BEQ$^Fiow0OSVy4pf>Zj@#SFc|8E#B1;MsJ+(gvg+7_S_SC*-K$Dk6`Z~L$?%Z^GIz!&zu^2iusYc;AX45trq3a&2Awv z%)6Lo1J5vt9k{_CD6;+M}Q2g=rR?#=}q=2NJs$=*1meSmgEPQs$kuC z+ZuV41VV$La}MQ~+F3fr8hqu0-Dp&&V$PlEr_^k0Et0%#%x#*%x5+WAioB|Y zEl@6eKHp&1i&{@!h&W1oEI(t)t3ticnP*25B(;hsFMnqv8oYk_HI>zmv-5dTPIPOQBd#i!3F9 zX*9khU;El%$>B&2Q5dZWPl?mXq$v1-&exu+`IHF%MN|4H>1)tC@Te1t+H@FxuLgQZ zA6sBhSF$R7W$kNt^uH3;tQ@-3P$W8L;yQD`qIs^UOGym)(svE#drq#q2@R>lf#La1 zX?3e!$W^_IsT)UuxlFM-=zxP$B3s@ACyQY0z7m*G5`t-IzmLNXe z!O@Jwq=!RsyVjfDSYG_mgf1{NXbsCOVE}9DnR!So%;aCMoM8vj~i`m%u_AQ zy6N{Bq>r9AC-}{EG?GSht~kJ-ofYEIuUf;(Rkhi8VJAG7^~wDCSGT6pR}5|=9ZGXv z^(erKMyMz93@6v*G8YWC+87ushcB|c!3g`L!==t3e1z0!uwhnk`{ulu&1pPF*1c}>G0N_g$6Rqa<*t%E(>XFGM(7ldC_3bG+bO?Jd}QzrtbwMAN{wje zEr{{19G$yx<=tBgIIW8tV)j%ZlX%V~kQtGrv(1f9o-7zBYNxU*wcFE}%WVZg^4yX% zH{RsA$@kgsyRoy*dI#i~G;d_PlZFDgPCk(QeNYpN_KL~C9@pj?p`Lv1Eof}dlS~ln zs^PTJ&bfkfYV#H^CrCP_!XlI1e55V`@K!x;K~>2Plyr3-VPkrhwz4g^5Q(QD%d>JB zG=g$SF7nPBp*$qA5z~Cp0@te93p?j!pswD@Xv`(jg8KcIJ|-Yg#wXw-mI#p1DNA(eF5P9IpPP9k$jIP7rw>9e8G3pOhHLK9(bW z=LFaty;!lNGETbPlelwwQ3*(d?HW6l1`v8mV6G;?AlpKrx|1Lqt*Dv4B%jedrIbcY z8!k4yHvXG&I+06@ev0&&9IP!fgr#=8lnwD3J>9PKsqeytwWz((*AiZ9y&9gNK zP@OBxt?n!1<9gL}HmEZC6FDMRwJqzLKsC07$GW=xz<0KtP1 zZ8BvdVFKbF^qBH=9bWl+VB1M=N<)kg1e>aSIJsClFLsHZomDdcy*#x z+S%DYPJot+@@b>*1U0mZ)j0$t+!ExDeicUW(UYp_j%@a1%65Rm0b3SWz1#EOa_Cb! zxBjvIH}o}DpfbJo1-Mcj8nGI>ruqu(s!94ha5}g0)&~NN<5HZv-UPEt+4s=V-@Fwo z3MJ8526^PJ_L1np6P%L-iT7L0G|t4csW7X7!)_@$;9MeAn{Rzs(dMSv`OZ6_)@?>X>yJFSLS*PY(N}%~D*_bKc;!zQUiTLn^)mjT|#O0mR`!sbwKFDwBe zD8h49gNYgDVua5dspcfOqn_o%c`&b!1P9GEM4&a*)1BC3_i0;6ck1(LrYwyLs;;0p z&}ANw5N)c%4ySkXPDRl{dv$IBosG+4fXPB*Wb5oL_E1^wtT8SH!Nj&?ycOYN9GUn5 z5_yB1byhf>8bhArV)cw!3G>Yjb@%}*PwDquNr)td=2v?u9wo>RE8R88H}z!03r)Pm z3`~N`WzKU5)Jd#=&U3KQX9xN;=gkXc62sm$Pu3FKrX!D}<>f!aPG-0-ESxfhZuN-? zxDPMBCDAJ)EW^M-VHy%{ZKLR?bOl`azor@hT9%pnFm}GD!)QcLSo?1&%a1GDZ6q?i_=l-aE&Y^WNNM&3avu3OkmIRD zbt`eiBfp>Am}hrbT6{);WbJBZM65oQRx^)33H}BEqzWjqVM2{vUjv)DuBH;piK9=s z1(|I1KbIQ$kf$>Nyh}xf_g>Qs-{!7%9e!lXufxyCr$tXO@oXaA zo)>nrW==p$q)wi&2y-(-u0gVOIV+rr>kFzD;VZTb34r_y5?VX>kztx~9I3rS1(kSu z+16!q*;!wNA)zUXtOt?5r2ho=P6$gfbtL*@E(Ply)4xL^qwn1F;my@c?G+R>-Hh<) zj%frUl@Zz)YpZixOOrW=tWQzbwi(jmk8<)INOsWMI9m%sar3kS zD5^7Ja=r1Hwf2h8$?+pdB9DGPWt_bA$A0h~V0wubM7gf3mPF`fCF#z|=noHy{44c4 z14Zy^$DcbdGK$Ap?#Ovt38`#6aW9jWH;MJ6$~&p30o?VfeoxcnR8Onwo`QtE&Ilz3 zKHxgGJ1NC!S}9=+v}V+~m_)>pMq}7c?UR>dT4OoYtajY0M`ZtwG|?~x*zP4&Qyh5L6yUfCpq`gC&%v`RM%TDW+HZ?x{`-j%-rY#1+ZDU%PZle24@lORNdmn47kT- zw|oaQAaUksT4o6ZzV87_7;hWF^w4$-R*7i*7?!uPq{@?C&x*R`x|85_A)@sDR*(~8B8ZqX`uH$ zmo54>*`G_6z+B!#AXOt7j?uYWjy&Yq)J!Q{OQCQG0K3$FJnB>$H&@!! zvb(m>bW=1sjD~J}q_Lk<0TkF7OTk$Fq%|50U{U#Axz-)?2rJFGf2Z{^_BqFjCKynB zM;^2((2^=}cRZ6zxl3ej3Zc6RvHeXkgXKK%T!ddEzD3#j4+8b{ENkiIzVW9!M zyJ059px3dRxuHGW#V%W9?UI&BuU}^`53ePLX;1V@WZc8J{=jYuF z_v{kb$xXuJW-`!(x~+I`b!}<_kn8X)jIjPD zRnq?J_uKZHUvb&_^8K%u?lmw*1xFaP-04?dhe{N#&|Kd#^Z?$^Kj_b+7dLh4&zU;Ph%!VkXr zjqiQ?pC)hg-SP3~_s`#bew-iQ?|xZ-&@bQJpVYtp$ zmVa-b{`99GeV%{#<~P3c9lzdP|NUQo{@ow{FaCX{oC>J7x!oHzSX}z z)Bpear+WRv*PoET{_%hR+gIqve)hAEfAP~#ult8T`t;-L7hnI|4_^QA`Sj=aNB{E4 uFY + +# Query 1: 1.22 QPS, 0.21x concurrency, ID 0x6F837C9DA962A07D at byte 6821409 +# This item is included in the report because it matches --limit. +# Scores: V/M = 0.27 +# Time range: 2018-05-17 00:01:47 to 15:24:47 +# Attribute pct total min max avg 95% stddev median +# ============ === ======= ======= ======= ======= ======= ======= ======= +# Count 89 67535 +# Exec time 91 11375s 20ms 6s 168ms 501ms 212ms 100ms +# Lock time 88 2s 20us 221us 29us 38us 5us 27us +# Rows sent 0 65.95k 1 1 1 1 0 1 +# Rows examine 72 75.13M 0 31.41k 1.14k 3.52k 1.89k 592.07 +# Query size 88 7.61M 114 119 118.23 118.34 2.50 112.70 +# String: +# Databases test... (50646/74%)... 2 more +# Hosts 127.0.0.1 (13617/20%)... 4 more +# Users test_r +# Query_time distribution +# 1us +# 10us +# 100us +# 1ms +# 10ms ############################################################ +# 100ms ################################################################ +# 1s # +# 10s+ +# Tables +# SHOW TABLE STATUS FROM `test` LIKE 'table_78'\G +# SHOW CREATE TABLE `test`.`table_78`\G +# EXPLAIN /*!50100 PARTITIONS*/ +SELECT COUNT(*) AS `count` FROM test.table_78 WHERE `id` = 824076488 AND `last_modify` > 1526044213 AND `type` = 6\G + +# Query 2: 0.11 QPS, 0.01x concurrency, ID 0x0B991403AD4E8932 at byte 1691609 +# This item is included in the report because it matches --limit. +# Scores: V/M = 0.24 +# Time range: 2018-05-17 00:01:54 to 15:24:43 +# Attribute pct total min max avg 95% stddev median +# ============ === ======= ======= ======= ======= ======= ======= ======= +# Count 7 5993 +# Exec time 6 803s 20ms 1s 134ms 552ms 181ms 56ms +# Lock time 9 206ms 26us 179us 34us 44us 5us 31us +# Rows sent 1 290.64k 7 50 49.66 49.17 2.98 49.17 +# Rows examine 6 7.13M 7 5.79k 1.22k 4.27k 1.40k 563.87 +# Query size 9 850.97k 142 154 145.40 143.84 1.98 143.84 +# String: +# Databases test... (4280/71%)... 2 more +# Hosts 127.0.0.1 (1246/20%), 127.0.0.2 (1229/20%)... 3 more +# Users test_r +# Query_time distribution +# 1us +# 10us +# 100us +# 1ms +# 10ms ################################################################ +# 100ms #################################### +# 1s # +# 10s+ +# Tables +# SHOW TABLE STATUS FROM `test` LIKE 'table_83'\G +# SHOW CREATE TABLE `test`.`table_83`\G +# EXPLAIN /*!50100 PARTITIONS*/ +SELECT * FROM test.table_83 WHERE `id` = 68211602 AND `last_modify` < 1526495341 AND `type` in ('6') order by `last_modify` desc LIMIT 0,50\G diff --git a/doc/example/soar.vim b/doc/example/soar.vim new file mode 100644 index 00000000..f4b2eaf6 --- /dev/null +++ b/doc/example/soar.vim @@ -0,0 +1,37 @@ +"============================================================================ +"File: soar.vim +"Description: Syntax checking plugin for syntastic +"Maintainer: Pengxiang Li +"License: MIT +"============================================================================ + +if exists('g:loaded_syntastic_sql_soar_checker') + finish +endif +let g:loaded_syntastic_sql_soar_checker= 1 + +let s:save_cpo = &cpo +set cpo&vim + +function! SyntaxCheckers_sql_soar_GetLocList() dict + let makeprg = self.makeprgBuild({ + \ 'args_after': '-report-type lint -query '}) + + let errorformat = '%f:%l:%m' + + return SyntasticMake({ + \ 'makeprg': makeprg, + \ 'errorformat': errorformat, + \ 'defaults': {'type': 'W'}, + \ 'subtype': 'Style', + \ 'returns': [0, 1] }) +endfunction + +call g:SyntasticRegistry.CreateAndRegisterChecker({ + \ 'filetype': 'sql', + \ 'name': 'soar'}) + +let &cpo = s:save_cpo +unlet s:save_cpo + +" vim: set sw=4 sts=4 et fdm=marker: diff --git a/doc/explain.md b/doc/explain.md new file mode 100644 index 00000000..5caa9081 --- /dev/null +++ b/doc/explain.md @@ -0,0 +1,42 @@ + +## EXPLAIN信息解读 + +* [EXPLAIN语法](https://dev.mysql.com/doc/refman/5.7/en/explain.html) +* [EXPLAIN输出信息](https://dev.mysql.com/doc/refman/5.7/en/explain-output.html) + +### SELECT转换 + +指定了线上环境时SOAR会到线上环境进行EXPLAIN,然后对线上执行EXPLAIN的结果进行分析。由于低版本的MySQL不支持对INSERT, UPDATE, DELETE, REPLACE进行分析,SOAR会自动将这些类型的查询请求转换为SELECT请求再执行EXPLAIN信息。 + +另外当线上环境设置了read\_only或super\_readonly时即使是高版本的MySQL也无法对更新请求执行EXPLAIN。需要进行SELECT转换。 + +### 文本格式 + +SOAR也支持用户直接拷贝粘贴已有的EXPLAIN文本信息,格式可以是传统格式,\G输出的Verical格式,也可以是JSON格式。 + +JSON格式的EXPLAIN包含的内容很丰富,但不便于人查看,信息解读的时候会将JSON和Vertical格式统一转换成传统格式。Golang处理JSON格式需要提前定义结构体,这里不得不向[gojson](https://github.com/ChimeraCoder/gojson)献出膝盖,要是没有这个工具也许我们暂时会放弃对JSON格式的支持。 + +### Filtered + +表示此查询条件所过滤的数据的百分比。低版本的MySQL EXPLAIN信息不包含Filtered字段,SOAR会按 `filtered = rows/total_rows` 计算补充。 + +5.7之前的版本Filtered计算可能出现大于100%的[BUG](https//bugs.mysql.com/bug.php?id=34124),为了不对用户产生困扰,soar会将大于100%的Filered化整为100%。 + +### Scalability + +Scalability表示单表查询的运算复杂度,是参考[explain-analyzer](https//github.com/Preetam/explain-analyzer)项目添加的。Scalability是对access\_type的映射表,由于是单表查询,所以最大复杂度为O(n)。 + +| Access Type | Scalability | +| --- | --- | +| ALL | O(n) | +| index | O(n) | +| range | O(log n)+ | +| index\_subquery | O(log n)+ | +| unique\_subquery | O(log n)+ | +| index\_merge | O(log n)+ | +| ref\_or\_null | O(log n)+ | +| fulltext | O(log n)+ | +| ref | O(log n) | +| eq\_ref | O(log n) | +| const | O(1) | +| system | O(1) | diff --git a/doc/heuristic.md b/doc/heuristic.md new file mode 100644 index 00000000..1e93d48a --- /dev/null +++ b/doc/heuristic.md @@ -0,0 +1,1134 @@ +# 启发式规则建议 + +[toc] + +## 建议使用AS关键字显示声明一个别名 + +* **Item**:ALI.001 +* **Severity**:L0 +* **Content**:在列或表别名(如"tbl AS alias")中, 明确使用AS关键字比隐含别名(如"tbl alias")更易懂。 +* **Case**: + +```sql +select name from tbl t1 where id < 1000 +``` +## 不建议给列通配符'\*'设置别名 + +* **Item**:ALI.002 +* **Severity**:L8 +* **Content**:例: "SELECT tbl.\* col1, col2"上面这条SQL给列通配符设置了别名,这样的SQL可能存在逻辑错误。您可能意在查询col1, 但是代替它的是重命名的是tbl的最后一列。 +* **Case**: + +```sql +select tbl.* as c1,c2,c3 from tbl where id < 1000 +``` +## 别名不要与表或列的名字相同 + +* **Item**:ALI.003 +* **Severity**:L1 +* **Content**:表或列的别名与其真实名称相同, 这样的别名会使得查询更难去分辨。 +* **Case**: + +```sql +select name from tbl as tbl where id < 1000 +``` +## 修改表的默认字符集不会改表各个字段的字符集 + +* **Item**:ALT.001 +* **Severity**:L4 +* **Content**:很多初学者会将ALTER TABLE tbl\_name [DEFAULT] CHARACTER SET 'UTF8'误认为会修改所有字段的字符集,但实际上它只会影响后续新增的字段不会改表已有字段的字符集。如果想修改整张表所有字段的字符集建议使用ALTER TABLE tbl\_name CONVERT TO CHARACTER SET charset\_name; +* **Case**: + +```sql +ALTER TABLE tbl_name CONVERT TO CHARACTER SET charset_name; +``` +## 同一张表的多条ALTER请求建议合为一条 + +* **Item**:ALT.002 +* **Severity**:L2 +* **Content**:每次表结构变更对线上服务都会产生影响,即使是能够通过在线工具进行调整也请尽量通过合并ALTER请求的试减少操作次数。 +* **Case**: + +```sql +ALTER TABLE tbl ADD COLUMN col int, ADD INDEX idx_col (`col`); +``` +## 删除列为高危操作,操作前请注意检查业务逻辑是否还有依赖 + +* **Item**:ALT.003 +* **Severity**:L0 +* **Content**:如业务逻辑依赖未完全消除,列被删除后可能导致数据无法写入或无法查询到已删除列数据导致程序异常的情况。这种情况下即使通过备份数据回滚也会丢失用户请求写入的数据。 +* **Case**: + +```sql +ALTER TABLE tbl DROP COLUMN col; +``` +## 删除主键和外键为高危操作,操作前请与DBA确认影响 + +* **Item**:ALT.004 +* **Severity**:L0 +* **Content**:主键和外键为关系型数据库中两种重要约束,删除已有约束会打破已有业务逻辑,操作前请业务开发与DBA确认影响,三思而行。 +* **Case**: + +```sql +ALTER TABLE tbl DROP PRIMARY KEY; +``` +## 不建议使用前项通配符查找 + +* **Item**:ARG.001 +* **Severity**:L4 +* **Content**:例如“%foo”,查询参数有一个前项通配符的情况无法使用已有索引。 +* **Case**: + +```sql +select c1,c2,c3 from tbl where name like '%foo' +``` +## 没有通配符的LIKE查询 + +* **Item**:ARG.002 +* **Severity**:L1 +* **Content**:不包含通配符的LIKE查询可能存在逻辑错误,因为逻辑上它与等值查询相同。 +* **Case**: + +```sql +select c1,c2,c3 from tbl where name like 'foo' +``` +## 参数比较包含隐式转换,无法使用索引 + +* **Item**:ARG.003 +* **Severity**:L4 +* **Content**:隐式类型转换有无法命中索引的风险,在高并发、大数据量的情况下,命不中索引带来的后果非常严重。 +* **Case**: + +```sql +SELECT * FROM sakila.film WHERE length >= '60'; +``` +## IN (NULL)/NOT IN (NULL)永远非真 + +* **Item**:ARG.004 +* **Severity**:L4 +* **Content**:正确的作法是col IN ('val1', 'val2', 'val3') OR col IS NULL +* **Case**: + +```sql +SELECT * FROM sakila.film WHERE length >= '60'; +``` +## IN要慎用,元素过多会导致全表扫描 + +* **Item**:ARG.005 +* **Severity**:L1 +* **Content**: 如:select id from t where num in(1,2,3)对于连续的数值,能用BETWEEN就不要用IN了:select id from t where num between 1 and 3。而当IN值过多时MySQL也可能会进入全表扫描导致性能急剧下降。 +* **Case**: + +```sql +select id from t where num in(1,2,3) +``` +## 应尽量避免在WHERE子句中对字段进行NULL值判断 + +* **Item**:ARG.006 +* **Severity**:L1 +* **Content**:使用IS NULL或IS NOT NULL将可能导致引擎放弃使用索引而进行全表扫描,如:select id from t where num is null;可以在num上设置默认值0,确保表中num列没有null值,然后这样查询: select id from t where num=0; +* **Case**: + +```sql +select id from t where num is null +``` +## 避免使用模式匹配 + +* **Item**:ARG.007 +* **Severity**:L3 +* **Content**:性能问题是使用模式匹配操作符的最大缺点。使用LIKE或正则表达式进行模式匹配进行查询的另一个问题,是可能会返回意料之外的结果。最好的方案就是使用特殊的搜索引擎技术来替代SQL,比如Apache Lucene。另一个可选方案是将结果保存起来从而减少重复的搜索开销。如果一定要使用SQL,请考虑在MySQL中使用像FULLTEXT索引这样的第三方扩展。但更广泛地说,您不一定要使用SQL来解决所有问题。 +* **Case**: + +```sql +select c_id,c2,c3 from tbl where c2 like 'test%' +``` +## OR查询索引列时请尽量使用IN谓词 + +* **Item**:ARG.008 +* **Severity**:L1 +* **Content**:IN-list谓词可以用于索引检索,并且优化器可以对IN-list进行排序,以匹配索引的排序序列,从而获得更有效的检索。请注意,IN-list必须只包含常量,或在查询块执行期间保持常量的值,例如外引用。 +* **Case**: + +```sql +SELECT c1,c2,c3 FROM tbl WHERE c1 = 14 OR c1 = 17 +``` +## 引号中的字符串开头或结尾包含空格 + +* **Item**:ARG.009 +* **Severity**:L1 +* **Content**:如果VARCHAR列的前后存在空格将可能引起逻辑问题,如在MySQL 5.5中'a'和'a '可能会在查询中被认为是相同的值。 +* **Case**: + +```sql +SELECT 'abc ' +``` +## 不要使用hint,如sql\_no\_cache,force index,ignore key,straight join等 + +* **Item**:ARG.010 +* **Severity**:L1 +* **Content**:hint是用来强制SQL按照某个执行计划来执行,但随着数据量变化我们无法保证自己当初的预判是正确的。 +* **Case**: + +```sql +SELECT 'abc ' +``` +## 不要使用负向查询,如:NOT IN/NOT LIKE + +* **Item**:ARG.011 +* **Severity**:L3 +* **Content**:请尽量不要使用负向查询,这将导致全表扫描,对查询性能影响较大。 +* **Case**: + +```sql +select id from t where num not in(1,2,3); +``` +## 最外层SELECT未指定WHERE条件 + +* **Item**:CLA.001 +* **Severity**:L4 +* **Content**:SELECT语句没有WHERE子句,可能检查比预期更多的行(全表扫描)。对于SELECT COUNT(\*)类型的请求如果不要求精度,建议使用SHOW TABLE STATUS或EXPLAIN替代。 +* **Case**: + +```sql +select id from tbl +``` +## 不建议使用ORDER BY RAND() + +* **Item**:CLA.002 +* **Severity**:L3 +* **Content**:ORDER BY RAND()是从结果集中检索随机行的一种非常低效的方法,因为它会对整个结果进行排序并丢弃其大部分数据。 +* **Case**: + +```sql +select name from tbl where id < 1000 order by rand(number) +``` +## 不建议使用带OFFSET的LIMIT查询 + +* **Item**:CLA.003 +* **Severity**:L2 +* **Content**:使用LIMIT和OFFSET对结果集分页的复杂度是O(n^2),并且会随着数据增大而导致性能问题。采用“书签”扫描的方法实现分页效率更高。 +* **Case**: + +```sql +select c1,c2 from tbl where name=xx order by number limit 1 offset 20 +``` +## 不建议对常量进行GROUP BY + +* **Item**:CLA.004 +* **Severity**:L2 +* **Content**:GROUP BY 1 表示按第一列进行GROUP BY。如果在GROUP BY子句中使用数字,而不是表达式或列名称,当查询列顺序改变时,可能会导致问题。 +* **Case**: + +```sql +select col1,col2 from tbl group by 1 +``` +## ORDER BY常数列没有任何意义 + +* **Item**:CLA.005 +* **Severity**:L2 +* **Content**:SQL逻辑上可能存在错误; 最多只是一个无用的操作,不会更改查询结果。 +* **Case**: + +```sql +select id from test where id=1 order by id +``` +## 在不同的表中GROUP BY或ORDER BY + +* **Item**:CLA.006 +* **Severity**:L4 +* **Content**:这将强制使用临时表和filesort,可能产生巨大性能隐患,并且可能消耗大量内存和磁盘上的临时空间。 +* **Case**: + +```sql +select tb1.col, tb2.col from tb1, tb2 where id=1 group by tb1.col, tb2.col +``` +## ORDER BY语句对多个不同条件使用不同方向的排序无法使用索引 + +* **Item**:CLA.007 +* **Severity**:L2 +* **Content**:ORDER BY子句中的所有表达式必须按统一的ASC或DESC方向排序,以便利用索引。 +* **Case**: + +```sql +select c1,c2,c3 from t1 where c1='foo' order by c2 desc, c3 asc +``` +## 请为GROUP BY显示添加ORDER BY条件 + +* **Item**:CLA.008 +* **Severity**:L2 +* **Content**:默认MySQL会对'GROUP BY col1, col2, ...'请求按如下顺序排序'ORDER BY col1, col2, ...'。如果GROUP BY语句不指定ORDER BY条件会导致无谓的排序产生,如果不需要排序建议添加'ORDER BY NULL'。 +* **Case**: + +```sql +select c1,c2,c3 from t1 where c1='foo' group by c2 +``` +## ORDER BY的条件为表达式 + +* **Item**:CLA.009 +* **Severity**:L2 +* **Content**:当ORDER BY条件为表达式或函数时会使用到临时表,如果在未指定WHERE或WHERE条件返回的结果集较大时性能会很差。 +* **Case**: + +```sql +select description from film where title ='ACADEMY DINOSAUR' order by length-language_id; +``` +## GROUP BY的条件为表达式 + +* **Item**:CLA.010 +* **Severity**:L2 +* **Content**:当GROUP BY条件为表达式或函数时会使用到临时表,如果在未指定WHERE或WHERE条件返回的结果集较大时性能会很差。 +* **Case**: + +```sql +select description from film where title ='ACADEMY DINOSAUR' GROUP BY length-language_id; +``` +## 建议为表添加注释 + +* **Item**:CLA.011 +* **Severity**:L1 +* **Content**:为表添加注释能够使得表的意义更明确,从而为日后的维护带来极大的便利。 +* **Case**: + +```sql +CREATE TABLE `test1` (`ID` bigint(20) NOT NULL AUTO_INCREMENT,`c1` varchar(128) DEFAULT NULL,PRIMARY KEY (`ID`)) ENGINE=InnoDB DEFAULT CHARSET=utf8 +``` +## 将复杂的裹脚布式查询分解成几个简单的查询 + +* **Item**:CLA.012 +* **Severity**:L2 +* **Content**:SQL是一门极具表现力的语言,您可以在单个SQL查询或者单条语句中完成很多事情。但这并不意味着必须强制只使用一行代码,或者认为使用一行代码就搞定每个任务是个好主意。通过一个查询来获得所有结果的常见后果是得到了一个笛卡儿积。当查询中的两张表之间没有条件限制它们的关系时,就会发生这种情况。没有对应的限制而直接使用两张表进行联结查询,就会得到第一张表中的每一行和第二张表中的每一行的一个组合。每一个这样的组合就会成为结果集中的一行,最终您就会得到一个行数很多的结果集。重要的是要考虑这些查询很难编写、难以修改和难以调试。数据库查询请求的日益增加应该是预料之中的事。经理们想要更复杂的报告以及在用户界面上添加更多的字段。如果您的设计很复杂,并且是一个单一查询,要扩展它们就会很费时费力。不论对您还是项目来说,时间花在这些事情上面不值得。将复杂的意大利面条式查询分解成几个简单的查询。当您拆分一个复杂的SQL查询时,得到的结果可能是很多类似的查询,可能仅仅在数据类型上有所不同。编写所有的这些查询是很乏味的,因此,最好能够有个程序自动生成这些代码。SQL代码生成是一个很好的应用。尽管SQL支持用一行代码解决复杂的问题,但也别做不切实际的事情。 +* **Case**: + +```sql +这是一条很长很长的SQL,案例略。 +``` +## 不建议使用HAVING子句 + +* **Item**:CLA.013 +* **Severity**:L3 +* **Content**:将查询的HAVING子句改写为WHERE中的查询条件,可以在查询处理期间使用索引。 +* **Case**: + +```sql +SELECT s.c_id,count(s.c_id) FROM s where c = test GROUP BY s.c_id HAVING s.c_id <> '1660' AND s.c_id <> '2' order by s.c_id +``` +## 删除全表时建议使用TRUNCATE替代DELETE + +* **Item**:CLA.014 +* **Severity**:L2 +* **Content**:删除全表时建议使用TRUNCATE替代DELETE +* **Case**: + +```sql +delete from tbl +``` +## UPDATE未指定WHERE条件 + +* **Item**:CLA.015 +* **Severity**:L4 +* **Content**:UPDATE不指定WHERE条件一般是致命的,请您三思后行 +* **Case**: + +```sql +update tbl set col=1 +``` +## 不要UPDATE主键 + +* **Item**:CLA.016 +* **Severity**:L2 +* **Content**:主键是数据表中记录的唯一标识符,不建议频繁更新主键列,这将影响元数据统计信息进而影响正常的查询。 +* **Case**: + +```sql +update tbl set col=1 +``` +## 不建议使用存储过程、视图、触发器、临时表等 + +* **Item**:CLA.017 +* **Severity**:L2 +* **Content**:这些功能的使用在一定程度上会使得程序难以调试和拓展,更没有移植性,且会极大的增加出现BUG的概率。 +* **Case**: + +```sql +CREATE VIEW v_today (today) AS SELECT CURRENT_DATE; +``` +## 不建议使用SELECT \* 类型查询 + +* **Item**:COL.001 +* **Severity**:L1 +* **Content**:当表结构变更时,使用\*通配符选择所有列将导致查询的含义和行为会发生更改,可能导致查询返回更多的数据。 +* **Case**: + +```sql +select * from tbl where id=1 +``` +## INSERT未指定列名 + +* **Item**:COL.002 +* **Severity**:L2 +* **Content**:当表结构发生变更,如果INSERT或REPLACE请求不明确指定列名,请求的结果将会与预想的不同; 建议使用“INSERT INTO tbl(col1,col2)VALUES ...”代替。 +* **Case**: + +```sql +insert into tbl values(1,'name') +``` +## 建议修改自增ID为无符号类型 + +* **Item**:COL.003 +* **Severity**:L2 +* **Content**:建议修改自增ID为无符号类型 +* **Case**: + +```sql +create table test(`id` int(11) NOT NULL AUTO_INCREMENT) +``` +## 请为列添加默认值 + +* **Item**:COL.004 +* **Severity**:L1 +* **Content**:请为列添加默认值,如果是ALTER操作,请不要忘记将原字段的默认值写上。字段无默认值,当表较大时无法在线变更表结构。 +* **Case**: + +```sql +CREATE TABLE tbl (col int) ENGINE=InnoDB; +``` +## 列未添加注释 + +* **Item**:COL.005 +* **Severity**:L1 +* **Content**:建议对表中每个列添加注释,来明确每个列在表中的含义及作用。 +* **Case**: + +```sql +CREATE TABLE tbl (col int) ENGINE=InnoDB; +``` +## 表中包含有太多的列 + +* **Item**:COL.006 +* **Severity**:L3 +* **Content**:表中包含有太多的列 +* **Case**: + +```sql +CREATE TABLE tbl ( cols ....); +``` +## 可使用VARCHAR代替CHAR,VARBINARY代替BINARY + +* **Item**:COL.008 +* **Severity**:L1 +* **Content**:为首先变长字段存储空间小,可以节省存储空间。其次对于查询来说,在一个相对较小的字段内搜索效率显然要高些。 +* **Case**: + +```sql +create table t1(id int,name char(20),last_time date) +``` +## 建议使用精确的数据类型 + +* **Item**:COL.009 +* **Severity**:L2 +* **Content**:实际上,任何使用FLOAT、REAL或DOUBLE PRECISION数据类型的设计都有可能是反模式。大多数应用程序使用的浮点数的取值范围并不需要达到IEEE 754标准所定义的最大/最小区间。在计算总量时,非精确浮点数所积累的影响是严重的。使用SQL中的NUMERIC或DECIMAL类型来代替FLOAT及其类似的数据类型进行固定精度的小数存储。这些数据类型精确地根据您定义这一列时指定的精度来存储数据。尽可能不要使用浮点数。 +* **Case**: + +```sql +CREATE TABLE tab2 (p_id BIGINT UNSIGNED NOT NULL,a_id BIGINT UNSIGNED NOT NULL,hours float not null,PRIMARY KEY (p_id, a_id)) +``` +## 不建议使用ENUM数据类型 + +* **Item**:COL.010 +* **Severity**:L2 +* **Content**:ENUM定义了列中值的类型,使用字符串表示ENUM里的值时,实际存储在列中的数据是这些值在定义时的序数。因此,这列的数据是字节对齐的,当您进行一次排序查询时,结果是按照实际存储的序数值排序的,而不是按字符串值的字母顺序排序的。这可能不是您所希望的。没有什么语法支持从ENUM或者check约束中添加或删除一个值;您只能使用一个新的集合重新定义这一列。如果您打算废弃一个选项,您可能会为历史数据而烦恼。作为一种策略,改变元数据——也就是说,改变表和列的定义——应该是不常见的,并且要注意测试和质量保证。有一个更好的解决方案来约束一列中的可选值:创建一张检查表,每一行包含一个允许在列中出现的候选值;然后在引用新表的旧表上声明一个外键约束。 +* **Case**: + +```sql +create table tab1(status ENUM('new','in progress','fixed')) +``` +## 当需要唯一约束时才使用NULL,仅当列不能有缺失值时才使用NOT NULL + +* **Item**:COL.011 +* **Severity**:L0 +* **Content**:NULL和0是不同的,10乘以NULL还是NULL。NULL和空字符串是不一样的。将一个字符串和标准SQL中的NULL联合起来的结果还是NULL。NULL和FALSE也是不同的。AND、OR和NOT这三个布尔操作如果涉及NULL,其结果也让很多人感到困惑。当您将一列声明为NOT NULL时,也就是说这列中的每一个值都必须存在且是有意义的。使用NULL来表示任意类型不存在的空值。 当您将一列声明为NOT NULL时,也就是说这列中的每一个值都必须存在且是有意义的。 +* **Case**: + +```sql +select c1,c2,c3 from tbl where c4 is null or c4 <> 1 +``` +## BLOB和TEXT类型的字段不可设置为NULL + +* **Item**:COL.012 +* **Severity**:L5 +* **Content**:BLOB和TEXT类型的字段不可设置为NULL +* **Case**: + +```sql +CREATE TABLE `tbl` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `c` longblob, PRIMARY KEY (`id`)); +``` +## TIMESTAMP类型未设置默认值 + +* **Item**:COL.013 +* **Severity**:L4 +* **Content**:TIMESTAMP类型未设置默认值 +* **Case**: + +```sql +CREATE TABLE tbl( `id` bigint not null, `create_time` timestamp); +``` +## 为列指定了字符集 + +* **Item**:COL.014 +* **Severity**:L5 +* **Content**:建议列与表使用同一个字符集,不要单独指定列的字符集。 +* **Case**: + +```sql +CREATE TABLE `tb2` ( `id` int(11) DEFAULT NULL, `col` char(10) CHARACTER SET utf8 DEFAULT NULL) +``` +## BLOB类型的字段不可指定默认值 + +* **Item**:COL.015 +* **Severity**:L4 +* **Content**:BLOB类型的字段不可指定默认值 +* **Case**: + +```sql +CREATE TABLE `tbl` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `c` blob NOT NULL DEFAULT '', PRIMARY KEY (`id`)); +``` +## 整型定义建议采用INT(10)或BIGINT(20) + +* **Item**:COL.016 +* **Severity**:L1 +* **Content**:INT(M) 在 integer 数据类型中,M 表示最大显示宽度。 在 INT(M) 中,M 的值跟 INT(M) 所占多少存储空间并无任何关系。 INT(3)、INT(4)、INT(8) 在磁盘上都是占用 4 bytes 的存储空间。 +* **Case**: + +```sql +CREATE TABLE tab (a INT(1)); +``` +## varchar定义长度过长 + +* **Item**:COL.017 +* **Severity**:L2 +* **Content**:varchar 是可变长字符串,不预先分配存储空间,长度不要超过1024,如果存储长度过长MySQL将定义字段类型为text,独立出来一张表,用主键来对应,避免影响其它字段索引效率。 +* **Case**: + +```sql +CREATE TABLE tab (a varchar(3500)); +``` +## 消除不必要的DISTINCT条件 + +* **Item**:DIS.001 +* **Severity**:L1 +* **Content**:太多DISTINCT条件是复杂的裹脚布式查询的症状。考虑将复杂查询分解成许多简单的查询,并减少DISTINCT条件的数量。如果主键列是列的结果集的一部分,则DISTINCT条件可能没有影响。 +* **Case**: + +```sql +SELECT DISTINCT c.c_id,count(DISTINCT c.c_name),count(DISTINCT c.c_e),count(DISTINCT c.c_n),count(DISTINCT c.c_me),c.c_d FROM (select distinct xing, name from B) as e WHERE e.country_id = c.country_id +``` +## COUNT(DISTINCT)多列时结果可能和你预想的不同 + +* **Item**:DIS.002 +* **Severity**:L3 +* **Content**:COUNT(DISTINCT col)计算该列除NULL之外的不重复行数,注意COUNT(DISTINCT col, col2)如果其中一列全为NULL那么即使另一列有不同的值,也返回0。 +* **Case**: + +```sql +SELECT COUNT(DISTINCT col, col2) FROM tbl; +``` +## DISTINCT \*对有主键的表没有意义 + +* **Item**:DIS.003 +* **Severity**:L3 +* **Content**:当表已经有主键时,对所有列进行DISTINCT的输出结果与不进行DISTINCT操作的结果相同,请不要画蛇添足。 +* **Case**: + +```sql +SELECT DISTINCT * FROM film; +``` +## 避免在WHERE条件中使用函数或其他运算符 + +* **Item**:FUN.001 +* **Severity**:L2 +* **Content**:虽然在SQL中使用函数可以简化很多复杂的查询,但使用了函数的查询无法利用表中已经建立的索引,该查询将会是全表扫描,性能较差。通常建议将列名写在比较运算符左侧,将查询过滤条件放在比较运算符右侧。 +* **Case**: + +```sql +select id from t where substring(name,1,3)='abc' +``` +## 指定了WHERE条件或非MyISAM引擎时使用COUNT(\*)操作性能不佳 + +* **Item**:FUN.002 +* **Severity**:L1 +* **Content**:COUNT(\*)的作用是统计表行数,COUNT(COL)的作用是统计指定列非NULL的行数。MyISAM表对于COUNT(\*)统计全表行数进行了特殊的优化,通常情况下非常快。但对于非MyISAM表或指定了某些WHERE条件,COUNT(\*)操作需要扫描大量的行才能获取精确的结果,性能也因此不佳。有时候某些业务场景并不需要完全精确的COUNT值,此时可以用近似值来代替。EXPLAIN出来的优化器估算的行数就是一个不错的近似值,执行EXPLAIN并不需要真正去执行查询,所以成本很低。 +* **Case**: + +```sql +SELECT c3, COUNT(*) AS accounts FROM tab where c2 < 10000 GROUP BY c3 ORDER BY num +``` +## 使用了合并为可空列的字符串连接 + +* **Item**:FUN.003 +* **Severity**:L3 +* **Content**:在一些查询请求中,您需要强制让某一列或者某个表达式返回非NULL的值,从而让查询逻辑变得更简单,担忧不想将这个值存下来。使用COALESCE()函数来构造连接的表达式,这样即使是空值列也不会使整表达式变为NULL。 +* **Case**: + +```sql +select c1 || coalesce(' ' || c2 || ' ', ' ') || c3 as c from tbl +``` +## 不建议使用SYSDATE()函数 + +* **Item**:FUN.004 +* **Severity**:L4 +* **Content**:SYSDATE()函数可能导致主从数据不一致,请使用NOW()函数替代SYSDATE()。 +* **Case**: + +```sql +SELECT SYSDATE(); +``` +## 不建议使用COUNT(col)或COUNT(常量) + +* **Item**:FUN.005 +* **Severity**:L1 +* **Content**:不要使用COUNT(col)或COUNT(常量)来替代COUNT(\*),COUNT(\*)是SQL92定义的标准统计行数的方法,跟数据无关,跟NULL和非NULL也无关。 +* **Case**: + +```sql +SELECT COUNT(1) FROM tbl; +``` +## 使用SUM(COL)时需注意NPE问题 + +* **Item**:FUN.006 +* **Severity**:L1 +* **Content**:当某一列的值全是NULL时,COUNT(COL)的返回结果为0,但SUM(COL)的返回结果为NULL,因此使用SUM()时需注意NPE问题。可以使用如下方式来避免SUM的NPE问题: SELECT IF(ISNULL(SUM(COL)), 0, SUM(COL)) FROM tbl +* **Case**: + +```sql +SELECT SUM(COL) FROM tbl; +``` +## 不建议对等值查询列使用GROUP BY + +* **Item**:GRP.001 +* **Severity**:L2 +* **Content**:GROUP BY中的列在前面的WHERE条件中使用了等值查询,对这样的列进行GROUP BY意义不大。 +* **Case**: + +```sql +select film_id, title from film where release_year='2006' group by release_year +``` +## JOIN语句混用逗号和ANSI模式 + +* **Item**:JOI.001 +* **Severity**:L2 +* **Content**:表连接的时候混用逗号和ANSI JOIN不便于人类理解,并且MySQL不同版本的表连接行为和优先级均有所不同,当MySQL版本变化后可能会引入错误。 +* **Case**: + +```sql +select c1,c2,c3 from t1,t2 join t3 on t1.c1=t2.c1,t1.c3=t3,c1 where id>1000 +``` +## 同一张表被连接两次 + +* **Item**:JOI.002 +* **Severity**:L4 +* **Content**:相同的表在FROM子句中至少出现两次,可以简化为对该表的单次访问。 +* **Case**: + +```sql +select tb1.col from (tb1, tb2) join tb2 on tb1.id=tb.id where tb1.id=1 +``` +## OUTER JOIN失效 + +* **Item**:JOI.003 +* **Severity**:L4 +* **Content**:由于WHERE条件错误使得OUTER JOIN的外部表无数据返回,这会将查询隐式转换为 INNER JOIN 。如:select c from L left join R using(c) where L.a=5 and R.b=10。这种SQL逻辑上可能存在错误或程序员对OUTER JOIN如何工作存在误解,因为LEFT/RIGHT JOIN是LEFT/RIGHT OUTER JOIN的缩写。 +* **Case**: + +```sql +select c1,c2,c3 from t1 left outer join t2 using(c1) where t1.c2=2 and t2.c3=4 +``` +## 不建议使用排它JOIN + +* **Item**:JOI.004 +* **Severity**:L4 +* **Content**:只在右侧表为NULL的带WHERE子句的LEFT OUTER JOIN语句,有可能是在WHERE子句中使用错误的列,如:“... FROM l LEFT OUTER JOIN r ON l.l = r.r WHERE r.z IS NULL”,这个查询正确的逻辑可能是 WHERE r.r IS NULL。 +* **Case**: + +```sql +select c1,c2,c3 from t1 left outer join t2 on t1.c1=t2.c1 where t2.c2 is null +``` +## 减少JOIN的数量 + +* **Item**:JOI.005 +* **Severity**:L2 +* **Content**:太多的JOIN是复杂的裹脚布式查询的症状。考虑将复杂查询分解成许多简单的查询,并减少JOIN的数量。 +* **Case**: + +```sql +select bp1.p_id, b1.d_d as l, b1.b_id from b1 join bp1 on (b1.b_id = bp1.b_id) left outer join (b1 as b2 join bp2 on (b2.b_id = bp2.b_id)) on (bp1.p_id = bp2.p_id ) join bp21 on (b1.b_id = bp1.b_id) join bp31 on (b1.b_id = bp1.b_id) join bp41 on (b1.b_id = bp1.b_id) where b2.b_id = 0 +``` +## 将嵌套查询重写为JOIN通常会导致更高效的执行和更有效的优化 + +* **Item**:JOI.006 +* **Severity**:L4 +* **Content**:一般来说,非嵌套子查询总是用于关联子查询,最多是来自FROM子句中的一个表,这些子查询用于ANY、ALL和EXISTS的谓词。如果可以根据查询语义决定子查询最多返回一个行,那么一个不相关的子查询或来自FROM子句中的多个表的子查询就被压平了。 +* **Case**: + +```sql +SELECT s,p,d FROM tbl WHERE p.p_id = (SELECT s.p_id FROM tbl WHERE s.c_id = 100996 AND s.q = 1 ) +``` +## 不建议使用联表更新 + +* **Item**:JOI.007 +* **Severity**:L4 +* **Content**:当需要同时更新多张表时建议使用简单SQL,一条SQL只更新一张表,尽量不要将多张表的更新在同一条SQL中完成。 +* **Case**: + +```sql +UPDATE users u LEFT JOIN hobby h ON u.id = h.uid SET u.name = 'pianoboy' WHERE h.hobby = 'piano'; +``` +## 不要使用跨DB的Join查询 + +* **Item**:JOI.008 +* **Severity**:L4 +* **Content**:一般来说,跨DB的Join查询意味着查询语句跨越了两个不同的子系统,这可能意味着系统耦合度过高或库表结构设计不合理。 +* **Case**: + +```sql +SELECT s,p,d FROM tbl WHERE p.p_id = (SELECT s.p_id FROM tbl WHERE s.c_id = 100996 AND s.q = 1 ) +``` +## 建议使用自增列作为主键,如使用联合自增主键时请将自增键作为第一列 + +* **Item**:KEY.001 +* **Severity**:L2 +* **Content**:建议使用自增列作为主键,如使用联合自增主键时请将自增键作为第一列 +* **Case**: + +```sql +create table test(`id` int(11) NOT NULL PRIMARY KEY (`id`)) +``` +## 无主键或唯一键,无法在线变更表结构 + +* **Item**:KEY.002 +* **Severity**:L4 +* **Content**:无主键或唯一键,无法在线变更表结构 +* **Case**: + +```sql +create table test(col varchar(5000)) +``` +## 避免外键等递归关系 + +* **Item**:KEY.003 +* **Severity**:L4 +* **Content**:存在递归关系的数据很常见,数据常会像树或者以层级方式组织。然而,创建一个外键约束来强制执行同一表中两列之间的关系,会导致笨拙的查询。树的每一层对应着另一个连接。您将需要发出递归查询,以获得节点的所有后代或所有祖先。解决方案是构造一个附加的闭包表。它记录了树中所有节点间的关系,而不仅仅是那些具有直接的父子关系。您也可以比较不同层次的数据设计:闭包表,路径枚举,嵌套集。然后根据应用程序的需要选择一个。 +* **Case**: + +```sql +CREATE TABLE tab2 (p_id BIGINT UNSIGNED NOT NULL,a_id BIGINT UNSIGNED NOT NULL,PRIMARY KEY (p_id, a_id),FOREIGN KEY (p_id) REFERENCES tab1(p_id),FOREIGN KEY (a_id) REFERENCES tab3(a_id)) +``` +## 提醒:请将索引属性顺序与查询对齐 + +* **Item**:KEY.004 +* **Severity**:L0 +* **Content**:如果为列创建复合索引,请确保查询属性与索引属性的顺序相同,以便DBMS在处理查询时使用索引。如果查询和索引属性订单没有对齐,那么DBMS可能无法在查询处理期间使用索引。 +* **Case**: + +```sql +create index idx1 on tbl (last_name,first_name) +``` +## 表建的索引过多 + +* **Item**:KEY.005 +* **Severity**:L2 +* **Content**:表建的索引过多 +* **Case**: + +```sql +CREATE TABLE tbl ( a int, b int, c int, KEY idx_a (`a`),KEY idx_b(`b`),KEY idx_c(`c`)); +``` +## 主键中的列过多 + +* **Item**:KEY.006 +* **Severity**:L4 +* **Content**:主键中的列过多 +* **Case**: + +```sql +CREATE TABLE tbl ( a int, b int, c int, PRIMARY KEY(`a`,`b`,`c`)); +``` +## 未指定主键或主键非int或bigint + +* **Item**:KEY.007 +* **Severity**:L4 +* **Content**:未指定主键或主键非int或bigint,建议将主键设置为int unsigned或bigint unsigned。 +* **Case**: + +```sql +CREATE TABLE tbl (a int); +``` +## ORDER BY多个列但排序方向不同时可能无法使用索引 + +* **Item**:KEY.008 +* **Severity**:L4 +* **Content**:在MySQL 8.0之前当ORDER BY多个列指定的排序方向不同时将无法使用已经建立的索引。 +* **Case**: + +```sql +SELECT * FROM tbl ORDER BY a DESC, b ASC; +``` +## 添加唯一索引前请注意检查数据唯一性 + +* **Item**:KEY.009 +* **Severity**:L0 +* **Content**:请提前检查添加唯一索引列的数据唯一性,如果数据不唯一在线表结构调整时将有可能自动将重复列删除,这有可能导致数据丢失。 +* **Case**: + +```sql +CREATE UNIQUE INDEX part_of_name ON customer (name(10)); +``` +## SQL\_CALC\_FOUND\_ROWS效率低下 + +* **Item**:KWR.001 +* **Severity**:L2 +* **Content**:因为SQL\_CALC\_FOUND\_ROWS不能很好地扩展,所以可能导致性能问题; 建议业务使用其他策略来替代SQL\_CALC\_FOUND\_ROWS提供的计数功能,比如:分页结果展示等。 +* **Case**: + +```sql +select SQL_CALC_FOUND_ROWS col from tbl where id>1000 +``` +## 不建议使用MySQL关键字做列名或表名 + +* **Item**:KWR.002 +* **Severity**:L2 +* **Content**:当使用关键字做为列名或表名时程序需要对列名和表名进行转义,如果疏忽被将导致请求无法执行。 +* **Case**: + +```sql +CREATE TABLE tbl ( `select` int ) +``` +## 不建议使用复数做列名或表名 + +* **Item**:KWR.003 +* **Severity**:L1 +* **Content**:表名应该仅仅表示表里面的实体内容,不应该表示实体数量,对应于 DO 类名也是单数形式,符合表达习惯。 +* **Case**: + +```sql +CREATE TABLE tbl ( `books` int ) +``` +## INSERT INTO xx SELECT加锁粒度较大请谨慎 + +* **Item**:LCK.001 +* **Severity**:L3 +* **Content**:INSERT INTO xx SELECT加锁粒度较大请谨慎 +* **Case**: + +```sql +INSERT INTO tbl SELECT * FROM tbl2; +``` +## 请慎用INSERT ON DUPLICATE KEY UPDATE + +* **Item**:LCK.002 +* **Severity**:L3 +* **Content**:当主键为自增键时使用INSERT ON DUPLICATE KEY UPDATE可能会导致主键出现大量不连续快速增长,导致主键快速溢出无法继续写入。极端情况下还有可能导致主从数据不一致。 +* **Case**: + +```sql +INSERT INTO t1(a,b,c) VALUES (1,2,3) ON DUPLICATE KEY UPDATE c=c+1; +``` +## 用字符类型存储IP地址 + +* **Item**:LIT.001 +* **Severity**:L2 +* **Content**:字符串字面上看起来像IP地址,但不是INET\_ATON()的参数,表示数据被存储为字符而不是整数。将IP地址存储为整数更为有效。 +* **Case**: + +```sql +insert into tbl (IP,name) values('10.20.306.122','test') +``` +## 日期/时间未使用引号括起 + +* **Item**:LIT.002 +* **Severity**:L4 +* **Content**:诸如“WHERE col <2010-02-12”之类的查询是有效的SQL,但可能是一个错误,因为它将被解释为“WHERE col <1996”; 日期/时间文字应该加引号。 +* **Case**: + +```sql +select col1,col2 from tbl where time < 2018-01-10 +``` +## 一列中存储一系列相关数据的集合 + +* **Item**:LIT.003 +* **Severity**:L3 +* **Content**:将ID存储为一个列表,作为VARCHAR/TEXT列,这样能导致性能和数据完整性问题。查询这样的列需要使用模式匹配的表达式。使用逗号分隔的列表来做多表联结查询定位一行数据是极不优雅和耗时的。这将使验证ID更加困难。考虑一下,列表最多支持存放多少数据呢?将ID存储在一张单独的表中,代替使用多值属性,从而每个单独的属性值都可以占据一行。这样交叉表实现了两张表之间的多对多关系。这将更好地简化查询,也更有效地验证ID。 +* **Case**: + +```sql +select c1,c2,c3,c4 from tab1 where col_id REGEXP '[[:<:]]12[[:>:]]' +``` +## 请使用分号或已设定的DELIMITER结尾 + +* **Item**:LIT.004 +* **Severity**:L1 +* **Content**:USE database, SHOW DATABASES等命令也需要使用使用分号或已设定的DELIMITER结尾。 +* **Case**: + +```sql +USE db +``` +## 非确定性的GROUP BY + +* **Item**:RES.001 +* **Severity**:L4 +* **Content**:SQL返回的列既不在聚合函数中也不是GROUP BY表达式的列中,因此这些值的结果将是非确定性的。如:select a, b, c from tbl where foo="bar" group by a,该SQL返回的结果就是不确定的。 +* **Case**: + +```sql +select c1,c2,c3 from t1 where c2='foo' group by c2 +``` +## 未使用ORDER BY的LIMIT查询 + +* **Item**:RES.002 +* **Severity**:L4 +* **Content**:没有ORDER BY的LIMIT会导致非确定性的结果,这取决于查询执行计划。 +* **Case**: + +```sql +select col1,col2 from tbl where name=xx limit 10 +``` +## UPDATE/DELETE操作使用了LIMIT条件 + +* **Item**:RES.003 +* **Severity**:L4 +* **Content**:UPDATE/DELETE操作使用LIMIT条件和不添加WHERE条件一样危险,它可将会导致主从数据不一致或从库同步中断。 +* **Case**: + +```sql +UPDATE film SET length = 120 WHERE title = 'abc' LIMIT 1; +``` +## UPDATE/DELETE操作指定了ORDER BY条件 + +* **Item**:RES.004 +* **Severity**:L4 +* **Content**:UPDATE/DELETE操作不要指定ORDER BY条件。 +* **Case**: + +```sql +UPDATE film SET length = 120 WHERE title = 'abc' ORDER BY title +``` +## UPDATE可能存在逻辑错误,导致数据损坏 + +* **Item**:RES.005 +* **Severity**:L4 +* **Content**: +* **Case**: + +```sql +update tbl set col = 1 and cl = 2 where col=3; +``` +## 永远不真的比较条件 + +* **Item**:RES.006 +* **Severity**:L4 +* **Content**:查询条件永远非真,这将导致查询无匹配到的结果。 +* **Case**: + +```sql +select * from tbl where 1 != 1; +``` +## 永远为真的比较条件 + +* **Item**:RES.007 +* **Severity**:L4 +* **Content**:查询条件永远为真,这将导致WHERE条件失效进行全表查询。 +* **Case**: + +```sql +select * from tbl where 1 = 1; +``` +## 不建议使用LOAD DATA/SELECT ... INTO OUTFILE + +* **Item**:RES.008 +* **Severity**:L2 +* **Content**:SELECT INTO OUTFILE需要授予FILE权限,这通过会引入安全问题。LOAD DATA虽然可以提高数据导入速度,但同时也可能导致从库同步延迟过大。 +* **Case**: + +```sql +LOAD DATA INFILE 'data.txt' INTO TABLE db2.my_table; +``` +## 请谨慎使用TRUNCATE操作 + +* **Item**:SEC.001 +* **Severity**:L0 +* **Content**:一般来说想清空一张表最快速的做法就是使用TRUNCATE TABLE tbl\_name;语句。但TRUNCATE操作也并非是毫无代价的,TRUNCATE TABLE无法返回被删除的准确行数,如果需要返回被删除的行数建议使用DELETE语法。TRUNCATE操作还会重置AUTO\_INCREMENT,如果不想重置该值建议使用DELETE FROM tbl\_name WHERE 1;替代。TRUNCATE操作会对数据字典添加源数据锁(MDL),当一次需要TRUNCATE很多表时会影响整个实例的所有请求,因此如果要TRUNCATE多个表建议用DROP+CREATE的方式以减少锁时长。 +* **Case**: + +```sql +TRUNCATE TABLE tbl_name +``` +## 不使用明文存储密码 + +* **Item**:SEC.002 +* **Severity**:L0 +* **Content**:使用明文存储密码或者使用明文在网络上传递密码都是不安全的。如果攻击者能够截获您用来插入密码的SQL语句,他们就能直接读到密码。另外,将用户输入的字符串以明文的形式插入到纯SQL语句中,也会让攻击者发现它。如果您能够读取密码,黑客也可以。解决方案是使用单向哈希函数对原始密码进行加密编码。哈希是指将输入字符串转化成另一个新的、不可识别的字符串的函数。对密码加密表达式加点随机串来防御“字典攻击”。不要将明文密码输入到SQL查询语句中。在应用程序代码中计算哈希串,只在SQL查询中使用哈希串。 +* **Case**: + +```sql +create table test(id int,name varchar(20) not null,password varchar(200)not null) +``` +## 使用DELETE/DROP/TRUNCATE等操作时注意备份 + +* **Item**:SEC.003 +* **Severity**:L0 +* **Content**:在执行高危操作之前对数据进行备份是十分有必要的。 +* **Case**: + +```sql +delete from table where col = 'condition' +``` +## '!=' 运算符是非标准的 + +* **Item**:STA.001 +* **Severity**:L0 +* **Content**:"<>"才是标准SQL中的不等于运算符。 +* **Case**: + +```sql +select col1,col2 from tbl where type!=0 +``` +## 库名或表名点后建议不要加空格 + +* **Item**:STA.002 +* **Severity**:L1 +* **Content**:当使用db.table或table.column格式访问表或字段时,请不要在点号后面添加空格,虽然这样语法正确。 +* **Case**: + +```sql +select col from sakila. film +``` +## 索引起名不规范 + +* **Item**:STA.003 +* **Severity**:L1 +* **Content**:建议普通二级索引以idx\_为前缀,唯一索引以uk\_为前缀。 +* **Case**: + +```sql +select col from now where type!=0 +``` +## 起名时请不要使用字母、数字和下划线之外的字符 + +* **Item**:STA.004 +* **Severity**:L1 +* **Content**:以字母或下划线开头,名字只允许使用字母、数字和下划线。请统一大小写,不要使用驼峰命名法。不要在名字中出现连续下划线'\_\_',这样很难辨认。 +* **Case**: + +```sql +CREATE TABLE ` abc` (a int); +``` +## MySQL对子查询的优化效果不佳 + +* **Item**:SUB.001 +* **Severity**:L4 +* **Content**:MySQL将外部查询中的每一行作为依赖子查询执行子查询。 这是导致严重性能问题的常见原因。这可能会在 MySQL 5.6版本中得到改善, 但对于5.1及更早版本, 建议将该类查询分别重写为JOIN或LEFT OUTER JOIN。 +* **Case**: + +```sql +select col1,col2,col3 from table1 where col2 in(select col from table2) +``` +## 如果您不在乎重复的话,建议使用UNION ALL替代UNION + +* **Item**:SUB.002 +* **Severity**:L2 +* **Content**:与去除重复的UNION不同,UNION ALL允许重复元组。如果您不关心重复元组,那么使用UNION ALL将是一个更快的选项。 +* **Case**: + +```sql +select teacher_id as id,people_name as name from t1,t2 where t1.teacher_id=t2.people_id union select student_id as id,people_name as name from t1,t2 where t1.student_id=t2.people_id +``` +## 考虑使用EXISTS而不是DISTINCT子查询 + +* **Item**:SUB.003 +* **Severity**:L3 +* **Content**:DISTINCT关键字在对元组排序后删除重复。相反,考虑使用一个带有EXISTS关键字的子查询,您可以避免返回整个表。 +* **Case**: + +```sql +SELECT DISTINCT c.c_id, c.c_name FROM c,e WHERE e.c_id = c.c_id +``` +## 执行计划中嵌套连接深度过深 + +* **Item**:SUB.004 +* **Severity**:L3 +* **Content**:MySQL对子查询的优化效果不佳,MySQL将外部查询中的每一行作为依赖子查询执行子查询。 这是导致严重性能问题的常见原因。 +* **Case**: + +```sql +SELECT * from tb where id in (select id from (select id from tb)) +``` +## 子查询不支持LIMIT + +* **Item**:SUB.005 +* **Severity**:L8 +* **Content**:当前MySQL版本不支持在子查询中进行'LIMIT & IN/ALL/ANY/SOME'。 +* **Case**: + +```sql +SELECT * FROM staff WHERE name IN (SELECT NAME FROM customer ORDER BY name LIMIT 1) +``` +## 不建议在子查询中使用函数 + +* **Item**:SUB.006 +* **Severity**:L2 +* **Content**:MySQL将外部查询中的每一行作为依赖子查询执行子查询,如果在子查询中使用函数,即使是semi-join也很难进行高效的查询。可以将子查询重写为OUTER JOIN语句并用连接条件对数据进行过滤。 +* **Case**: + +```sql +SELECT * FROM staff WHERE name IN (SELECT max(NAME) FROM customer) +``` +## 不建议使用分区表 + +* **Item**:TBL.001 +* **Severity**:L4 +* **Content**:不建议使用分区表 +* **Case**: + +```sql +CREATE TABLE trb3(id INT, name VARCHAR(50), purchased DATE) PARTITION BY RANGE(YEAR(purchased)) (PARTITION p0 VALUES LESS THAN (1990), PARTITION p1 VALUES LESS THAN (1995), PARTITION p2 VALUES LESS THAN (2000), PARTITION p3 VALUES LESS THAN (2005) ); +``` +## 请为表选择合适的存储引擎 + +* **Item**:TBL.002 +* **Severity**:L4 +* **Content**:建表或修改表的存储引擎时建议使用推荐的存储引擎,如:innodb +* **Case**: + +```sql +create table test(`id` int(11) NOT NULL AUTO_INCREMENT) +``` +## 以DUAL命名的表在数据库中有特殊含义 + +* **Item**:TBL.003 +* **Severity**:L8 +* **Content**:DUAL表为虚拟表,不需要创建即可使用,也不建议服务以DUAL命名表。 +* **Case**: + +```sql +create table dual(id int, primary key (id)); +``` +## 表的初始AUTO\_INCREMENT值不为0 + +* **Item**:TBL.004 +* **Severity**:L2 +* **Content**:AUTO\_INCREMENT不为0会导致数据空洞。 +* **Case**: + +```sql +CREATE TABLE tbl (a int) AUTO_INCREMENT = 10; +``` +## 请使用推荐的字符集 + +* **Item**:TBL.005 +* **Severity**:L4 +* **Content**:表字符集只允许设置为utf8,utf8mb4 +* **Case**: + +```sql +CREATE TABLE tbl (a int) DEFAULT CHARSET = latin1; +``` diff --git a/doc/images/env.png b/doc/images/env.png new file mode 100644 index 0000000000000000000000000000000000000000..8b6fb4714c1f7f277402ed5684b3dd3295a8b27a GIT binary patch literal 60038 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 Jl*(DX|1S*|=;;6e literal 0 HcmV?d00001 diff --git a/doc/images/logo.ascii b/doc/images/logo.ascii new file mode 100644 index 00000000..5dfb75d0 --- /dev/null +++ b/doc/images/logo.ascii @@ -0,0 +1,5 @@ +,adPPYba, ,adPPYba, ,adPPYYba, 8b,dPPYba, +I8[ "" a8" "8a "" `Y8 88P' "Y8 + `"Y8ba, 8b d8 ,adPPPPP88 88 +aa ]8I "8a, ,a8" 88, ,88 88 +`"YbbdP"' `"YbbdP"' `"8bbdP"Y8 88 diff --git a/doc/images/logo.png b/doc/images/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..7d62ffc3377874f5e67e727424069625c32ac700 GIT binary patch literal 63912 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%0Dv*@(iqM}TZ-rNIo-kPZxOYrwa6sKC*U34q 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 _Dwv9v8Ln@%5DaxCKP$UD z1we`#Qv6wi2Y|puz(MKTZGyjz06VSMo}W&&lMnjU4EYc`fD*K8x8D zcUu_3C8&kS(RvSPB@lO+MUJ4iGLhu|DIN~3AyNOg{^xTa9sJUhFJAXXByu^}MA8}b zct;7JGzm5ega;c5A>#Ke>p6}3yod*bNXFAkBjTcgq-}isSVm^ntT)CZzxXjd-qTg? znli78xov{N476AdeNl-eyQJ)RMbnyD6MhCAHg_WE|FyzkYK^(fMM4e?iGY*x+N=$h zp@F(sz}F98@yTWh4>sUp*q~cJw};o1LFQs}K-^E!P~=wd6}Q~ht25}n8w&@9OB&Y9 ziN&8e57ta`TIOnVTn<1>t_F%5QpxZ|O+fcsDbNz_1#Hg&o-SZ)J5YCfDnDznLT!Wx zVj;jX0Q9^Kj2s4n?WxIBt^rDR0`)tAyqcvS+MExNz5=e6v}}9$(IkM$H=p>Pl@?1+ z2a1YYeJK|JV3NFrI*WxKHihiEWJB_rvD2nieJ zyI%VB@W_cHvkpVC(U{6JHF3MW=aPXWg#kTDhLM%Zqz6ed-d0qs z-Wd!8F9&VreAXub=n)>S-ir#2F5@1JR{J&p`_i$yOVo%`43v2leD%&dhicZX|Gqve ztGm2u?VK5j#Y>z?H$1o$liURzZA5@t4^(dia>{|>H9*gsfbAF%>jp;0Q*C#~b^!Ie zfV@gTzCa*`5Fil)TvI^bF`(}d;5iF$c0jfY$lVIm-VT&C0~*8fO3r^j&|L@}UFZmZQg)GE2J7O83~B|PUjZ!YWXewr;oo} zy>aUb@#s|@S7OMsisF^qZy&1Nu<^gM3JPB?s9H6r6_~RFSl45`zTWfc*tcc9X;CR= zcYPkP`i@k^UH>|u?;zkl34~4q-2=c>J5X~cP`xRYpEaLhI4%MB?7+yyE3^ym0Er2J zF#$OrdoO%xEJ(EsTJG(CW=f@St%l?u2C4|I@8w;Qd6l)h?@j{n9R2l^UZo+cn@iTb z6bl6#MmsN8CgQQyXgE^MB@$Jxfi5W<3Q3YtKLwtn2;t@)bP*nLpIzxTEAG`7R}e)` z!9SaOyG9CD)nD~Oxit+4`Fxlj>;oSX9`cjv3?`k?up#2}90sr~H&!u2Ym7NLcTi$k zTBkWhWBHB;9ju^e`vgEv5Xk&T0^G7 zqW$HKtACZ5UvR9TYSo-~K$zPQ;e3V-$qnEdpeR5h2Xd=`jNDZCuxrhgklZD}eE>M$ z2TZg9H9LT+O+ZHG+yWdf2}A?HMAwy&<=a5`B0yzg5riCNTudh z7eDNKR>0JflWm{;909-raCE)%=8@devSXJ1o)?phI5+HemFjbIi^USjp79Ik^4bV-wP3#Lw`FDCR1hNO%qryIE-;8@o%bH=JQZfhk<0F>l5C&2U()@HpHgkp{qu zxu$7LU$>kj*kv2TA89{JdmVNOA5UkmqoBzv=JM;C_^iA<08h;L`cplf#;K8!mhiM$ zCXAYafx?s%$}TQ>%aEIIoy)N(U9*n7@#F9Ih(+{^5~aE#8u6;He*hqedF-Oe&dc?D zERz4mlAHlVNp$S|dV7EPx=D1-+%F2!eM z8wLhXr$|u)mjDt56iq?dYnf7|RcSR_#yc-(iR7xy_JQ8)cr2P9^SUw;;gFb*`l)o9E`PXZAr5C> zhemBQe3B;oJI9~><-pd59{Eu1D6DPblZ?#C$dw5If)^qiaML2SuF+IlkvZPc9x85Z zURK(7e&0)!d1z2?9XKja3t&+|BFT+23e`kbS>=p3%Cj#%E$(kScU!zJn#-C?HRB+l-9?Q?a zTb`jC0uZ^mS3d<>B!xtmmC)oBSoAr0Zx__mzLtzdj${^5N9woTCg#}qFXb|61|t?V`#s(@ zY&5#TYn>_z+iixJ$I1|nrDhsNOom*vwd-=NLaqKtW>LxaueR~U;)&=)cgHb4?E4HT zDqHCF_+)|#yB!s_!CpheWglH`^OA_iXo*;~nG#9!2@&3SmXRnBcUjGo7tT%H^~JBw z2w;s}YNN(_x^km&c5f`;H3_BpplBLgJVLo9Mm`Hd=BH|p5V=$)Rnd$XoDj+gLJ;#g zm632{Pn=EucILwwX?-T)!*m$qmooqmNdRLJpfLdjwLsrmpy%~e+nvY-V5A$E_${EW2J}@a zt}Sgopw32fJg-JTLGUH@Qee#ZXiAh@cvYum2o#vcTXzc%UH0) z+S|-EzM^KXqvV1^%MqI_qHH>UgN= zZlBds823!exp*jjphbAZJPx(bYTaSiXr2Ku`C)If^>>lXoa|tkF)+f9%FrSRmuCd{DKC8S1)I3=LLa1akO!JATU<1uX1606auMPUVw>T|R z{kX~6=g?v~vK!Y&^BdN5h{d7xoP0IQ{5POR!d1oSE>k?y=?o`7dQa^;kMHO*VNI^T3 z1OAVid%AA(Pfb+E+%}mo63P#3e6-(TFD9#Mt4=@vEAz$&{^$>^+O9AC8EHTCTbJA8 z8==Lrd@dOm$c04^^SKq)!M=@3mFg(~%Z3!oF+NHSw4clOSj>%V$R|#Rq>kdbV8rb@ zDN|{jGrfMu@7J)2?7|3Sh}p z#8^^hF08KpmWW~A&Mq#Em9B2#7WzKszuEH#JPB!f3jW|TORD4ZJZT04sB}QhrWDX> z&jq@l0peW%c!01#avH#NDt-4uLxdkTm<%9a4K#idSi2W676KyiO|aCc4<@+_(a%}w z=!z%nMw0TDjqm1eC5}J)^Yc2Bsh1+;)mTaS7o8T%efG;|lzbXwhlJ;lj79T8Ue5+* zRow|(CE49R+;1WA$Yr@HcS~r}FW94!4Eq_6)mk4c_N={h==CG3cip`#dJ_gbE=f2T zyv1jmF6P2s!A72<8L0D$W5sn1u87w&qsqynPyK|l^mkVdUua(ybDD3M(`GX0qDRjj zuM|;S%i*8>(7N-{FE2R2plA_fnZ-nA$Rsm6$P*-^VMHvW6c-B!_uz%Sl)1YzUnCZ7 zF0QFPUAnp@yclyZ^D4r0HQL@(nHn!geulfeMWb->z3 zfabj^;C8clt;js-8d=fZn?<%h^ceymc=EYlo)N3GZnxb*x<%sqt>@oS@@!bJ!o>Yv zDJ@lOl*wc{06w%SaQT{bi6kW&v5k%e!c&Y+2nP}X5H3zdJnj;Y!@gH-wOqh5@nX3w zQ~A?lqj&fwN3`j*Iif@|B9e%PMRLXY+dlV|8Lc{*HbWU59=gDG|6TLl6 zQ$3yaaknKc<0RJT64~WdT@s0SMy%yS=T1?!@v&;3)4n_Ib16V>q>YUr5%x*^PUl9S z)73KEe)d3l^ZEtV1&R?7E*iKf2LHGDWo2ozz!E|rS11f2zyIrOB>Jh~c#m9YMK8}r zDOX=d4g>PYs#|Uy88~~gt!n+|1?7hmEz-RIZtqVUEZmGBOP2$*+yzA3K=020E)BXB zaVb#qAh2f7O$x541=i6OxfvwCx{kkec>fUVbNq+eU?__^r`8K4rX-0Ng0?BjF1xz< zEv#)K{bx^(TF1tEY^sbZE+RZHiDx6Q_jMIyWMx9njUecB*@D?>001BWNklby;^Lf^LCpfgllf zi~KI<)?gs`7ZZIwf7-V1C0ol~_b(WVPJg%k{Se`YJuu7HulKo|5oDPPftLG$+&uu) z!p4iz0EK&ib@!&|9d26B6?Jtznrj_h(bt{`-@8W6Ew##oHTC#kH2Y&D!vDao@Bwhh7U*t;mYZLIQ4g*fH35 zh$czl9uz)_h}#a=#K^T*u!!N3@!0;%qT)VAp-oF$c{WVhdph$+F0_4S^kQ3C$Km}; z?sV06@+iZ|RqJ?K^o@wyQAh|UeDkL$C^GZIDy{bQL?~cs-kZKx;>^p>ieqfDHSF`R z=OX?a)-pR#DN<^;N5*6fL|!i*RvxNBZ@a z;o(PyF1D?G^MAg(U~|2=i*6&BG(LsY}xSNP+J}TE{5TOQ*03civT;keJuiDMq2}!b( zGKK6n%B=iAdb&&ycbmnQ?v7idG4?MBBQBmE9bPgP_t5`*m%4o9VCF<`&!g_Csg21< zAT2K&Bh#Q@O{=Z6Wn+78Rqc#e5?D5&Pq2ymL(`V5bQ0^A-vN^#>b9#xfxz7?n<(sh z=g_j;d2`Dv{S__iUoNO?I3d+%v*`ecWY|O5yDwA*Jf6Q$X5}?Z_4O=>?U>y~J}e|6 zG;|{5{fp^sAbhHCUm})zaETN<`eN>y6a}dDK>ZFtw;mw$RmM#MWNu7RJt(y+NcR09 zLZLcJW~iep-m!U;-~7X0irNmmx~}`wiN^;owEyE|Ur)vJ|M*u+? zk3Txmc4|*>tXqO~S`kq+)J0XSsi@>=UU|jv`j0%2_G;@pdy*C_RGXbsQ(NL`h9;Mz za1lS{Hc!?kIDQ9D(W2$}8!B2h@DiHpp%T%5(KKCER2;#!of+KSgS)#E+=4p<5AG1$ z86dbraCdhdoZt=#?gV#-;E#LndcXbAtEvvybD>=4?;+?OTv!FSDDX()p*e z&KKj&Wh?@u@6#@VGgOlg#RdY(lOkGV(BWagS`;U}z&u8ns-NDXrn1yol^pvmKo?yZ zUW|BkL=6Wxp@YDp$NnyD%~kN}?+_FeOc2$MCcoSgzFPOG0`F10TT%ne{H>l^y(_R3+O-t4qeOgM(A0 zJ#Wdcx&IzWnHVZtH}^O`8e`Kw`Z=}I`YjW#T{6)W!T3>;GIumP@@naGpObvjj=PK4 z(9j&e-+LUv(@p@Tqi|+^E?gt3<0>;d3p0`n1@RyeXB-xXu)SglBWxamoo|0Zh)3(* zVF0mvR&WdFU94Wasml5qmU~@uI8iv>VI9zT(&)kn7sjQ&ax{KA)qn1K9{-Sj_ItJ@ z&D1^)O$Sz;B(SN0M0(~vG5&z_bFD?+$jfPqQQf|j1ENFM zg(JwEmHO$o~MEA~sCCc%H?s##!Jb zyQ*S0mqFJE4Xc`FI55u?wtOjZ7JC`n-(1Y3g|}jaR)9$N7@C zUtC=XX*M{R8U&e>!pA7NLfu5FEg zH+M3U%S0y!Z^C5xz_h(!qeFh0CEz9c2GZ$gn%EePsN+sUN!a(+4+o0kC4|x^#9ubjGk*9Z%EX&kNJR|@^ z#-iW^l%L3zZi(EE;iC5fuOiZXvE)o&n>4Jh3xjQU!q!#5lM*Ru&2pQY;l9h8X}yd0 z>dg`ZEd~^{GOz_8PqKZgkTP55*{F*Mos=WSJYqa|6 z`>VfNz8}H;`tk$rq=tK`L`YntV%TNIb<(tdCT~}QAUhgR3rThCUrr+3lP|?GrPJ{ zSvSY4ARKRA<-h@tC^@fSwiioJ9m~KK(*~u;l^h zk^wsn<2XqH6>#J0VMjLMt(*hX(41UBQx0T2$ZcqVQrf_HBCBrNJx$%RELv&kUCp9j zDGBo0`gUB#6)*}?QKI9bYUB$Z9~F0C4pTY3;f8?s&FG-S^Q{Elst6Wh=o^zsE|&rG zVvuG3&wwHc{;e#B`g`}?-I|I`e3URF{_AmCr4ciZixM~y?gP=2+o9Gcg$j)+2W`$`11t7}=v~3x3pv7-71l?_v(7@`H2K4GDJ&#eM}lCLdz@{t84* z=^`nMbtQDC53je7fVU?xX{zghTgP`dCEnFoO1MFX%=s-B0r%A*Y0~eCiq&YkGQ~gS zi_e||o` zVYP83ST``)vacG!Xd`7{jo>W2jtxH$LkUb^pS!mTac(C7lAyyP#w9f)2k2r22yf z3VVm(0w-WVyx7Sm7K;Z^sMg2b^z`wXxz`TNh@bn^Bv`f2!&F>K3$>BF)ERT?AU~%} zWw4bh_khno_ojWZD7}3D3ViO@ z8Zc67U+i{XbJOOTy6$#&qWw(7d3_&$^($4L+1BKDgYuEj0ljptl9}0@Vw|^wDIFpy zxOtEp#(>e*vw-GQi!|DP674|FQ`X&`z9u~4=}-*(3)RN$8TYUam4fNYCJX(ZG&zAN~V0yD7mJ zpB{<1m+6tkT2|#d#XfVEar8o+*hzkPcZRS$;hjHFU=ovn&BYz}v$g8ttH%dND0Di% z)7m}=a5gns?;oeJPFF?1A-41_<%t-^r%`_FzWL1Od){769E;xgN4&mz!(i)At!2aW zKpJZo>BB^kADR~RM6Y)+sFoq`Z#}uL15WZlO(x{{0MVd7dhhQ2g?9gMpf(XGaK@5$I(JGD@s^xWfet83{`BZ<7g{st%a-8#0CQH4SUE9WxYqv_1|;%*f`s3tA5?0|&t1Z0DFjSAC=zWk z+t;$xSCH>o6(3;=TLbo^ICVAcpN-5<_eW7?jh$pT{^GSfs3L1Xq{x${yNqa(Vpxma z)E3spa(&OWyAYe7m?IWcaAIKg#9b4pOk78H?9G-3h$+f_KZ)8wRw50IUa*}1^7ed+ zkrIDT)@&76^d?tJKt^)qzyvMWCjHa*@(Bve8|M*tQ3jm4cm;4OI~yUB-!w(Id_s2x zW%1s%Hhn5vMlee)#h(iZ=*Rs6kO0p3YL5lG4gnOp5MAE|k}o&BMe=widhotb0g;R8 zMd1O7cA~|4LJRks?_E*re95|hw;ZM3MD2pRZfEwQ?skGDHrZNp%Pn8sy=m46sW7D0 z>=XDp?}eoEUq8x+W~ucBD@oy!O6Dmkt1{ltQ=hMfbF*r6dP1=0xTQ*mXuUUIrZyWn zAlUjG5ust-16&SEIu1u?a~`ChCy@2qn^w5lSMcK`d7f!xT)y^5h_~G`eDuwN4iTeh zcb%8J^IQbD{;*fr8D2arl?J4R58c{Pxwe)*bPBEec%j8NXsp>LHTcU&Em^a=eC)(; zbk%A5%l{rH5;^2O^n9M*{g1Gweznt>?e2;fa{>V3BzT=XNE4q8=E`568QOH37JhFQ z(sDH}aL=WFpoHc|>vF$@xQ#~R)yWngxQ3Y9%f;;H7LW|5&kH_GECo|(mUD{xZ$fpK zBNWbDnNT?!mc)B-{tzTf8cosXi@DVAO-l_}O3!{s_-L>wC9;#}iY@V1-*>EgNKqtc z1EN_iGwb(u;;6ipNkztG_gG;{nT;+{;BLjsMc{LP1T<+c2Z%RB!A@nrpD|8XulihM z0jHKj;J;){ML!($p%;L@TN<3EO=MBTLIk zsfqO}Li7cFE?=5HzZQ)M3%4*}ToPtW1Y?T`nV5C4Xj1H!SPncXs;Zdb3~)3w80J}5 zn68rg`41sc)i#8Z3i%XlrddB(_$#7(`4Rj8P5ZQW#3lFE|6NY4Dj?$<6l$|$kqonz z`Ol8YqzFl>-Jf#Q^~O}F=~{3R6mDsz{bBT)T3O;Ay_ zv8f37i>&@sPmS%Tl=it;diZ^8)qN&`hilHCkkUf`f*m!}33Du5=>obD1ur`XH^ofd zQlg5r4_3`%VXM%swev^0ustYn2E1v*5+O(nP4NC>Q%TRKqoRpmWx=%PE#fgn+28*w zuL;#v8Pe4y+Pt*7^jy-j?8IkkJi^$06QJjCXyjzc>2%myY|GB{OJ~4Ie{On`%J!NT z2I9J2EuM)!GWXm15Cf14hJMJEJhNC57JD8l^9?@y1ISOl^>-Xhjz(wGhH023^NkLi zP;#ED1;X6Jv^nm?KE=4(-m>g=b)}}_V;n8lA2j0or|UD{ez#eIKbs1-8Cf0CUY(I1 z9QF_h&k|@hrwk)4D4AE51n` zB+!NMSToeN$LkqFL>kEg7BsDrPgNVN(uuD3&5j2Rb3dn|*SDD)XOqDW0LZ(dAL(T5EVcIxQ%` ztI*|{I1Ymmau_sooQAy^up-GtPvMkdS4(5C@lCGIR6bHjm**kKqUe-YNW$CAh<2;8 z9i!S66>ozE?Xw5F(Ya)*RSST6o3qZO&+ZL&Kln&i)vGN#gAS2~wq4jdj8zRB)KwoQ z6I{_^8JOj4(me8M-uG_II;l+#SKC4~5iNl$lG|Ryl2`ZD znM4Cr2pB&x=k1`QJ52?oMHg(`=%~6LzegvU?e~@fMj53<0r+;h= znxE|9S*U$rcRpLY_Cp7K;2R5sse(}>ovP9?Qf^jy-ye9}oku@CE84;M+?g@TjL z>f-{@c(=jKluXY=5|s}_YsFJURSAl)Zk9B3#AUd53C_pu+&%iX0r#XtoQ;)J2yt=K zx4)qPbpc85A468)=@H|{fO0v6n7W8Xt2JLrpet&)OiBiNw}g#%#Vgz+$da(6eMDBXmu82%v-TwABm@-MaqSP-I+H*A5mn= zDocSVbKo`kZx0D=czL%P5A9W}^AZrHZ`t4H#zR4__`P=d)Qbcejv4fm20RPe0-I53 zeod=Mm9$q#h$YkulogrrLwC}{g#|FTScdBU;@4UH-RSD}>@C0mT~!@U26;GnmqXN^ zSH^F2A*o$%IIA7!brg(OtzThOI~;MAbvHG$!LJ}%AH0hZnaxy)OY)o$CS9~%st+4y zVK^FRW`6ee_gdb>b68=onyB4Y-yIp3z=x!iIm@5zWjc7(TkNFtjF(sOzm_}tc_{TB z&Op@LBc|kA{|$xhird%gm{KgyyY)XNbAN9A=**243;T0Gm?#p$ABZByhpZ!<4&+BT zXgEAP90(y!EYCCV{*KF*@hLT5WvEPsbbHQP4x;pU?;@MikG@Yz`Tp$cDXnkcy4f&mvu)Zjhd^QE7}IkJB?Sr>$Lmtd^6ZP zCZk+TEIyqqu)^5=T0B~yC`ilDvi8&#UT5?NU(r(J{^4DiR?&0w=`oHVx4(O6#Sg+j z;5rL(M6AgAC9Cvxusm|#t!1xH$)PW5yVzD`X^6Rf9=U*_7r;|e`I+xeRC%o!5{5pFw z7>E;9ID!}V$Awg}^SA-WLS(YwcpP{*z~|qso0w7+4NiGDb!SkaN}#<-Q498cb55P# zneUi#o=R&Z()%0!Z2DWhl7V+YE-)B6`vQS;o+A*Ee&aY z;sdm%b8386DoyxmrvBndNvj5pk12~kosY2Ol5MH~oNHRdxvYCH0YIr_PGqCtE^fVm%T6vE(UTwe~ zoYGJuCMQ7MIWcWblXuo}F-R9sAy${f<&TW}XHqxmFN9B-Azq>=+Rr)-9Kd^H2O*Nc z1Bp&V)ek(uSS$FjoW~pJu{*2kWyk9tR+YEu#A;dDS0@=xtodRBz$l~OC5aUaL zek3YQBz2^~Q#|0f$}xY!Zs;((Jc(I6!SVT>q0u+zT#xeAmLvdt=T-lErxhI?No0sb zX~FDLVeoLAM%d>XwS@~Un!>k1pe{B{f|{}hf5`dEQwdi4#02TFV)_9NgH+kwMi}mG zsLg_-#q;#13nVzurU_VrE46d@pzZ=;h2{oREZ0|hpvVQb3P0QpBAJ|wH^ESQI?h7x zM?-)hR6B3Ur+4pbrc@3JU74EM&ku|-T)mZExWGgzwum=NGhyg@BO9cJ%W&W9veUY2 zQGRC+@PDtw%SuXG&-tdPMS+fmO}YO|SYXSUTT*=c4A^RmFZx%y4sTg%Fwjis&CFfO zjh_g06RRGTL);z;52bh3gy_R6y#@X;ZZfRwa#N)siyXf^HByCFDV8L5lt0!-u2Fd9I5g3a;5J)k8#3xNrjV9EKM$K2@8}VBUz&o8xju_eD}W^o#3VG=^mT3rC0GXD4dHyp_& ziw9So$1VayQJD*;_4f~Rp+Y&G;j833JM7;%{ub{4Py-i;&YG}s;IJk#epPfxP|i&D zGBjs98)stZ*4O#^S<;hbrfw&e8VG6iJH8PD_pXXO&t_I2uRqGty<|u>o_#PPws4;> zjSUC*tG^6UV6q&S!lz~x-pK*6QKTz=$l7m7nW(Cmk2QTjfL1hMcM^V=aIf1mdyAH6 ze9XG2$xZz(@OV01c9oMuaj-NJS~gCDleJdE`Pc2BcdkdF{5j~UjEqjw$YUzVsAg6Jgs)gfHPBJQ%Sq#r!uSJw>K zz;UD4?m|!IAhM-^>umXj`UIB9Z}&%yjW;{nGYiISO*E(On#vFE3|5c| zvi*z{nROmSigb7(nItph9|wS6f#`|464)q_C1SG_D$)4HRC#n6No8|c9D*W4QvK=# z{1At7wRtU##if+!;aI2vO%2VYs?`>kFAig^!cX{Uy)OTOmZ%&mrfS$|S{O?0g&tki zr+CuMou+5ZU>hn=!#@YCH=Bf=ySeE}D?@s$fl^=(x1BV^1bjz22(0{j8f)2j8jhuWAM!Qg! z1uOydp~<+&a{%~Ibyf;jHtJebXmi3mxaCO86U^UhH9Z~wn0?CGHvH|QkZGi)o!RIF zu%*cyQL`LOVW|0mzzb?w1P2z1e#eS8@OE>*^-z}XL1Qr^6g3CY(f0}f571S*1^oP( z$%CKX{t!TLGrAtLw5`M7wbNEpt2G=ql;X!ozZ-bpw4OVW&1h=t)?^-bmZ{TWS45v? zp{Qr$-Nhn=wAHztc=K#c-YLdX9J^3rpn_w5^iu1D?=g&UBFCR7s^BZA5Gwpg92quM z(vuN0C}hNh&q|iJvXoZpPQv{1OONE=Zo`%9fABA;;GUfkB4E!S_6!4!+YA?ziJy93 zlT)LmG7MUdIzMW@3t)5c9sE75H1 zD04{-JofX;oOibUBHkMnM1L_*n2{D6RmPv{Lzs$KX~(5%6K2sVmv_I(Foq-;>XHLo zXicCQL^p*__&vGl zamr^QP#&Ms5Y40yfFA+vT#d@noZ_~6w%*m_=g-ko^%J|+K}l|YJPT z1k*4Mj$A;~0$zVHE<)kErzC4cUAhXv#_7KhGQjSg0a$^lz)pNHb^EKDO+`BEwZRq z`6iS=1eIuFc`yYLFp(YLvX-y~HL)#U9CYdZFacGMZcG5}9S4$t3+&y)PU@D0J#+?| z4}FToXl5ht#&K$<;7!AxpThuLB`4@4;CwMk!ApAX_)>P$l@L)Zx~!XbE-YFEC2a(p z>H)P)$|iF|8K+_6NDQ)&9l<`KLbW737%EGG>(I!q=BDIzL`8oc#!!+5^&$Y5=!}Vm z-YqXWc6K`}8gp9}E*d#I8vR;qDw~cc6RE#C_n{Cu^#Ub?$>QU;c9HUTDS?T;KN5Yh z;Xv%h$SmLc)}0f~?%G#2#!t_0H=aB9`~#w?^jgf^z7E~I_&=|%r?;;8Y~F0#thr2m zh6~F98NQOUU1E{l!;(n@*r6`h_NXH%fMh8fx0rHt`9Cmg7+BV&JWF(b-i+@0U-KP* zTJ*A|u@^?W)Ry?F23`R$ZNN%q_1<29uZDqIpsFW$;~M7nIxd<#Q=~0IKoA`GY7bmq zl6Jvx3M;M-PGdR$WVQ=n>`xxuSK3&UlC7yH1k;i-838cGj9c*Vy{g;P8`<<+bAy1p z#G>mwd1nHm`Q5%dY9m1Lx9n@TZVvD62Bqu}y_70l>Ypm&2-HDt-D^8`u#w{_qpx;S z1&`vhXYSlhAEfm?x8CD8&@!EI04DyEV!ibCwP4hD@&EWVyiOAep8%pl7pzVyh}#}q zjTVlakH0_$Ra8;%d=d-_DqvJW2iuk5TXMG`)pa474FeNdFd185*&G1^BG64WuoHKs zj<{`eJi86xv@W*vj=7MbLBqx$|NUcVY<(1bJq_gAAn}$qmpE4J(aB_3N}$bZpj!Z< zlXY(+G5#|OdOg%SDT9Ug;!mKxZQvsm_yA|?UlP~_gY612UIc?qz7lBaHCydjJTk#g zIyFzD14D)b+J6pA-|0P7B6G(_FO&l2Pl6rjXS_LQAdt2M(Ha3Cf#|5fk=!Nycputr zNa8cKoFKgXe@rL`ebQhUBM{}xpL%kspPTz)1P>)f6hEy`90UWvu*?JyWzl8N2E#B47i$(NS_F2<5U9oj z_5h&+eVo)dh}=}OMqH9 zH+cfMK(Rdupb4(S3((K$zrn9B_RaR54us__<+o|*L^ zJm<-zo)9)RAAFq_lrlA=&%E38dvZ_=P&3`#ZNW1>P+RHHFQ$W4yCArM(!f(>V;h(% zc>v^TtmQ}}1Gdw{nvf20h5+crO(qHa3T3>G=(-L+=YhuyH8U}9jf4U0fpS6xi0}S3 zZ)!5F``?DBl&jl7LPv#<2K*V0au83y?y`oX1m32Ygg%5)4^Hd~8BytAJ%fYaQK6-y zzHrx?Rn0boB3X2g8v=koaKU5ldDdU+FRHM$=grdti^ga3Cr$$1)~GdV{b zWR2o@c7H&=021pDa<|s^mQT-3H^0Fr05-2GtLnBC(OGbQ!y(}xcc?FD@n7`CobMdOzFz=G6U!q7 zg{ZG9m21F%b>c_>LU2(Fl~Kv6&Ji5CALrLkm0xg#c%-}j<@AJ8Zl{ouukN1?12D?0 zVRTar)`>$7f+S&^sz8U&QRnQYcgbCwKEwfT)nHa^s8u*4XvC?gq>gd=Lh1s||GtYt z=YoH#h3KTJ1I??q746U-E*iI2|AwR;aTB!yAAZwIqW)Do`NdDPxt$IKgqwNBMCXq* zi6~%X%Ke|8{&d3C7ohawZ5=Oex0t%?*@7D~0zA0`Mh}~%Sm-KrK4m26dsX||*p0v? z8DW41f_v+=1CLUbm2bTv=C{7J|N9K_2xEhW4}ZIGN{0yn$MiKI$p-{X44crylpAJPnT0|Jq2hj-}(8Z}aTbQs7=ceSO+B zHVx`U5KL^sF4dbD10Hfh88kEaCgq@Zlh-)PHz;Q=nLnFYOxk|ZAb);^ms8+ePK5&CXqMLZ8XlH&9ESOu z|12VbC6-4QNfB`0doh5f`wnO5A1XIY!3E5lu^NpOJohq+>61cdLOh73Z(lO_dhq;8 zATkc@%kG^LLcf6;Zw2l(5Av0viJNgef~`EnBRgQaTzan-1K(%^2kHx`Z^T2l)Nb0c z7n|v>`mZy*wP6 zuFsHvtEfz;-#-kD6hHgBXQdD6MsJ#?b$Gwl-b@JOvC1{=Y_YZ_9}CvF%sACWWiO_t~8T2o)maA`zt%(J$w8fvgBE+Cb>H)cFQz8 zwK_hNj8T-zBGQ(Lqi*G94M+e*(KuqS{dx)8^X}Bl6A`%^2=d_At_s1}K;+PZZG#A& zQXRH5nG8VA{p>DuH*mzkI2Z160#NA)N7w;q-abyY_L?5SdHct#145&s?42NUI`uO4 zr;X2raYA9SfmRY_Ie?**Pc8c$@~!5*%-dmN*W1xRK!<7O-;1kV<^uP9q zMXqJhbpl?%byxlB#ru;rqUCfU+N=v@@Ev)pj}!x8w=e-B^JMWKY*BJT2W3iK)33&>xtjrUo`}l|UK~09EJjZBC=h)kEz%r6-*q{XJ})iy_1rZzVoUXCC~t1b2IDV$(-`%Cq#Cq*BDEynK(?4E509xh-V6Wi)P4`IK+fe z1^@x=9Q{xX&F!SAem)n;eSNe6!|j%qEmT;eJ%61EKH{uyu-{u4AWM7aEh0WQn}lTJ zFvI9yzUfO|Ajn+>yBV!l5I)7xJyMDGFHynv$b0Pb=XMXK8!OmRSC%6Koh6oh$&SsV zjO_EzLaXi0_UF3(u<9}ooI=CvxVKLta3C#59^oR5@~2qGfLLgQDQ<{eiCT@?O{XZV zZ(|awyvr(gDLhM-F~Kpw5RvVevs9JrfX&}PR{FFx$D`&XS3nxzjMdwQcNqg*Zw0Iq z1!mxi*1S-<5haQ^AmL&}5#fJQ@4XZ@KBXq(0mKhHj&2N&94=3N{_|*=s63TEyfi+n zx{gVwT5>ZSCGtqZ?fox)o>TDVAwLAtVd9=?$Yf^y>i|PPEzHjee0ZGUpzuSyYkqRF z=PckZ#4>f=R|XUC39GOoJ+QmEv1|BEn+d;dS>$&q?)6u#iBbf>02FWu4o+*FJL9Cy z*I#?2{xbL7Ym;~<;I0D?8X!AdAlLH;V&XJx%lN^n8>@@XDX~5_F=Bc?BukQ`!n^Bw zyADgLAF%u^(KWy9BuatX09GLzRSCbIQosnwx63YBbLPrb625H|>39xd{d}u%uQqgv zCi2hH#-S`gf+PKfSEZ@D1Y#cc!~6nn3jkK2k}Xc6_gdRi`z~N02FlqsPxa`_iU^I5 z9V|x3HG%=x{YX*^Iei|=V$?igf`HyFr>-J08_0PJ9qk;=%&z-=NMG;%EWRIELMmsV zBa>xae^^Xd_edzI#;aShhgtqrr7D=sfXvP|Bf+^_N6>)_HVY2u5cBZG|E)<&nChjd z+Jt!pzRz#fXy=|ow)bsQ%*+eOogRa@w!c@vlUM!tTRc+9F2a>Zhu!w9UcNgi;3Ad7 zirtNv^ZeS}_Pg6D$lQizL>|$YYd34`c%6wE&orO^!c?|?>w~Rt{7z+j0)y)a)n3cE z!1{CK%`<3gGr{^LF5ColbIz$+fl?piM;d< zD(WNt)Xn}@QTrggc)2y)Kpv-MYp)DuRk%!-)KXQ|%Z&|A73NebYke%9V|%;tNfvx4%%6Y*5-Y8b3z zlmgn3b;dRd^E!eqMf0#_D2UKjhB;Mfu2=}wfalX%1_!>)y!Yqlj#|H?<>&7AMr44e zB@G<~c2~Nly#X`;Rrn1(8U9HHQik=Jcl`?Wh;#mH`Hu4E3D{G27&Dt>{Th{2JqG;g zUAkn&V&fB$l<9*88TBhq6^5kZIROJ%ie$|9#rI@PPgJtqxh_~kznp8O81Z7({=3|_ zzt#_gn;l92c|b+MkhwbR*3Vqp?Wo(RI#JB|=Sb3o76w$TB}l1V0WI{8N+_xDLf}yi zJ7!FZ_~LMJDQB@f1dDDUJYA{$5T;&AWKHFGc({lS4BHFHMNz&#Pu^Ns6~WaA1m85t&l63Nc3e2NyZAMwlB!Htf~!(hi0YnE~OgqK-qtk}pJl1hoKUOpDM z5ek(Q9Ut1_8EnoAsvZY+9mO>QI3FpGpr?VVB5ccWw~lb1o&$rb@kjh8gJHSsdkf~l z(r{Z=5&YS-CjO3o~$vxz$z&SOY>Y5{<$D-3J$+!J^AT7B&6 zC#s;2LyR7ZIk`GERJ<8*#l>W>jRkX6ZpUD4T-*>OUTjGAW9G`KEP1ExV@4;jNSkFz z{J5%WZ3lU>tPM2VOJFI98wch>Vf^@G-YPZHqtJpGg{qQd0^*bJ_Ililw-`-Q$)ew> zdXd+jE=%qx$yl+v7$ClKj+5+1^y#uHF-l0rB@2BJT_mnWEa~^LWI0RwVUs?~%x`B& zrYlZlOKEZ?8rV|7sL;P^MQ-JNPR8a5Y_;P6fJQ%=bR9l39KM=R^j|R-o1b%#pE=Vt zRuW~YY*0VnwAK2pONaHTz1BF$4KIZ=9O^7#W@R&2a?=#F%w1kV*uMzmEQzxxx|u$E zh%7?B-IU{ZFLxKZGJUGR@!XJ;OvvSHa%9Mq+b`@hmgmTsa{`UjY`%_(sb4z0%^fJY z0F{aPMWup)QY_gF%0EE!%aVyG&#&JsoPWhcA&`{NQK*+&2rkwSfI^q-IYC`abh$ zG2Ap)F%9_;dtYN%FI^Z(az?9CB$wE|b|KBGlzrLqf&tI`>o#c2S+?dOMCQS3AngV@#G?thiD`B^6vhDb7}^lN%24CZ5~72Qvg)%PK6>dVZFJAmz7K`nE=c>l+At#va0ob@dN6dw+_F&(@`Qm+N3e+Qef=$Dj@bG6_x zI2r@Y%@OY^gUZTidPP#w{&<-j_Yx^Ed`bwkD8F8ab#<9;kkEB`v^xl8-aX`ud%liX z5^FYW?J6j59@WL@ULPz)){<#ww!_FoDCk(MGCBSzCE$>Ny}WsDWO<>#5C1jv81C#Y9fhjskFYv2f=76AMy*F7^v@F zzs)P-i5L-M7#F(@*XM3oA0=JuY9L>i{@uEB-0!^;r!Xc5hiF{qeFTm9H%o!{ZPOie z$+B-aq9}?FcQy^nYjK`pR$OTc!H6M$F%IVDOk#IhBK7kH2<2T8kJ8Bzwn_Y+;ND-H zrx!Al?yoQ;c(UId7?Qs-UrG*|7hEz=cmyX4_sUXbO&KrQGVs3hkwp1X?xMNUCy%w| zPk06hq%WmT@4Yi*j&FWEQqx>`q78Ovo#p)O<;SqgD~~-U&W3(mSyt|sHeC`c^U|${ z1QJe&3eiI#|G9Xkr&9KlRTlo?*`Vi5+2bRNQyjqZkP%N1fg_(uB?u`0IqshjKK#x? z7_liKI#uDLld|V3u-*?_xDDq+`sn8FUKUX8XY!KcD%4GX!u0#YYcsV$=yd$HT zc4sDIJO-JP{QL9iCa&6dsR8y)i&Anp@M6HD=?c%%;|wU`qiEj4@e3dGI8?gUI1ig8}7m3`SxJ2Zk7H^@o!Qknm?NW za?EP62P6r|o(vxb?|x4DA@)9=ziho-X`dNk3gfN_^5e@UJ$9RPvy$M)b?kp=kajX^ z;Y_MYG``DS@UIK(l0SIh8W0M=EG$1|5B(M^)4zSO$3H7B85j#W{psiZ)R-@NX&9lA z!KTNw$;z4h>OXxE0P{SsWN|QY`+3yK%3$o^Hz#JfEPLV?7l}U@p%yt8epg4`PGn>X z_ap}TbWT0!lB(6bgdTDP#{sxBL#+rPNOgNPr3!*C|M7QJ!U`K!9f%=YUxDxov$%Ke zc#uGbsBsz?t+Qhc5)U6b4rx45I73$#ePNpsL}9uG8mvar6IWya(*Fi0ENo{`M^MBo zLe&r$JMpr0Bf*mb2M&{4_YE8f%w3_Y_`+PPQf<0RcPTmnRui~RS>XyyL9R?%xCmjq zvuY5(cdVX=LuSp#d5-`JG~Ak9tVD{KOeH; zA>t&u&O7#{IZGcCU=Dz7arOak0iLl&39c(GA+%vQ2~$&gMaXxVD+?{-zr#So_1{`u z}-1oc#b`s5deiPgU%R9bcqWNM0W+2}y;mNq`PUijY>t}}xV;|p%GjrM6 zhNM>}ad5_mi~^=GzITjYr&ks4NO)}aHY5&ilq2}@oLYv{fla8?bQx-Y>oBYmX;(Xr z7yo%bBOxZMcUt0G{OB#mdDBx|JCO>K-Nm6~;zKYE^c;`XMvF@zrf`!trg!~(@Zw#zV?5(Z4%<+mPUebG4HE2b#|68^qAOpELf#EMWfb^ z4ea2uB%>ot*OQ5dOs%Zw&i`g$-9-|)R*b=UVQF>ux2g+*7djT`m{XM3rb|pXOzbWt z;dF`^vGxu&(Mbs}fR+T8w0yPu+FT+&yIkxvt-u@u9lVLa!c;X0-N~d_NOlbq{^MsQ z!?AapzPsN^1I}iSfVT?o{$;Yq5JN}Aye@-7IpN|H3;*@I>Uszm4HyUBO|3Y zG46EvF?`5s;*?j5t{3s7!#-hk7_L)Ycea?0w=mDg#0&$93iIp6xVa%i zoWD0^CB?ZNyB{TD*2fJ2Z0tTSb*D4(tX0_?ug=QCB(n?EI=0%Cnpcme&*O;+I#R>c zq_cPoWA9$yJjn{4WQ+pa_P7T3qGr_6F%Y8ARK$)X%l(8i8?SvzX^ zn=!-3yW74_gLL(=*Q5Bzb8SMx<`i;PqbHSfQjd1qT=MD=GP_bl3pT`Nbs+3%6Vd@W zLJfYf{@pPWfuqmA%KM@+VFOF1vnKeF6lQ zIIcj`u)w!eKmcnmztg-4F6&9WDtJHz++Bf&SY9ic&Vp>9=P)%H6C+5G!@Q<%ullAaH}+du)|_Kg zZ*<-onl3eabXdG6p?uC(&A)WR-KkdCt~SY#B}miIkvWqnS4T^>bXP6=3m1NjOcCb( z$ZsM?!9rWP;`!nv;54Ngy$cdd6^e4)Sm;2IG}Erw56}<0PmRy+az!@PU({9k&C48x z7$PZHHR*VtN{2EFTOwe?G~NhOhRhU)6F(2>JEV-v;kZ0|Xk0CGskgeTR}_>FN^#mL zSjYS?bmY~a zO*f4-J<){B{VvV0n(OL z$G+o}{3i0lq*qXCRz-(!{@8hZVdTY*H#b7UoXg7jD=TDQZb z+pBYma26_@C!Jd>bjgRz_Uo{A%KXBnk(Rok6uX@cwr99L3B(DU!@psB7mbPu>6;jx z*Tw5+bI-8Y;2z3T8EN&jxac*)l`GHcf4u32E_Wp+yEDwqnX*_tb(x5#a@5cCnoL;8od-(byKblsyM zUHDKejWu=kZH0%UDq&ci z(K(`@sc`1k+!BozFAqiTbZS)9t!>S0zF?bKP_yfOG$B(8C4DWW!tU|iw^Z90+q_my zQIj*LatAG#Mh9RzjB%3CaGL%w-j| z$-_DSe9d{RL01<&UUY{TOkd8M3HOR6!A{}ye}%ncd|ci4K0I-nq)B7jwrw;v8{4++ zw6Qs9Cz#l5(%7~dTaES1_x}CgKl5VVo%7jy?X?%Kv(~k~+|oxJINqc<`uUDtLTq!6 z1@#ZToDxYXDlMsPaR~My<~j5?tGU@$g39LZGts==3C{N@?j_$efxw7#g;b;_E4FM! zPiwbVQN9<{2f6f;xPA2}PG7u59h7Jtv}Hl9-V%n)+zPt=E=e!e9RuBg4$T z@E8D=6;uZdA_~l$LHxDe#8*K6F$1nmw;_u}XAkrS9BK@F2q?&8+tVaShyA=S`)}3e z`*=;6gJFzf_gk<6^br9DTNWC5p?o^sTx4x|$oI9`rhoSDu{-cvz2rtN0WPVd z>K)1@s*fHSqYyTMbm*bW^{3Oq#32}w0+%Oc843_AIe@Z`U411@y)+0RJDaaHGs!QH z3Ktol5M^%~!Dz?>8G8jUu7G{LE&7f9;*mF&W zr;exoYv%8fD|ViGD?8Eg&M}1N88>H3O_YUK)7!`&eX5+4l9ds{^YX2P&dr!P_m# zpPff&&9{&eNSx`)Fsnz3dbCT`lzQGOKt-%d^qy#HSafkEWG!5~Ni{(lkm0o#_@W-$ z{Sk*T?VY2gy!gRY3#SEWe8h$BDoS7q=qawMmG++Q(pyr7s>IUJHd_gsdp7ieU^kML zHOEuoSqNO9&4ICEPis7C*o(T95_4i{51qakWi7Ss8yk^dj(1Q+bG^~?gfAF)Har{R z*!=y+u-ZzSMLXF2T9=39+HPW>m0l@>P#wyddnn#}&o}60ekV{eezAaLSZM}jW{HPy zZL#VF*Iwr#)R5x$cp^$*80%gS5y>Sl6VYb%$YXw7hUXG1s_#0f^2@qF+i#*8r^{Ez zwG(bM$B$ z?81SMOl^s)Jb~MDr~=1x?-ljs92^^)>t@>1=`>M({XBW$+z8fp_qo67P8d{g3TUsn zG~jf0*g7M?H!Pd6TRdT8*ksex@-1n06&{M;kirs_7DrS^X`r#l<+{d^~3?YFIY;>^V$oDcg)N^)%K@8QC3-s3^<8XiHSiAyuwXs zR=Tsb&+YjTcp;!;T%DWvPGBV<^zzUxJNOEX)Y(gE-pPz^7A2<`5HcmTL(Hd`-L8E= zkWsSM4)qPF(o-<0-0&PY?##fHd^bLb!5Nn_;}Cm)1zA}1tm8W@2dSK7z#2Ha|3+g#HB&g z(dU`eq1k%w`9o1s?};^dV$P|E{A++7g60(Zr1~%D_Cd zs$HXU*K=RbyXW>QZp-Gt^e4bB3ghrkzyU8ocyW*Gu|oZ2UG(O!W5b)LUxB}KZdU2Z zu}jCf7p&Q|F$4B4-={#^SJzwbx&)u$MfURY-{PDDmMhq_q&zJL5o|*JhkMv$OG39t zmtWKRmggrk^_URMC!dOTmxGAu#K1FXhZ37hbbT$bv{9OvTnwW*t65T4f8a&qC~`(u z#Wbd75$tkuf~iwSBpgq9yDH^{2vBn&@YWLT2C|=$XW*cwR+BMs3E{l!h5qYd{f-Ai z7aQR}Erdg>HN7Z(EdAQVzmC3LR0QERb&JW1EGNsGo*M@M07}>O>V+!VCGn3@ zT52|9$|_iI_^^$_C`n!R{VF1M%92qXtOx2dOt;r>xz5se_F3jx&P?<%tc1pt)c4Jf zOj}w&3m;=8dfi~fCM%&haBZLcHsy|mo+)K5o5;yzuIIWI9UgVEN?9e*M7QHf2peZK z!H^~?KGY|}lCN%nQ-f}sA#LL%lMy48aUG!vG^>+t^YK6*sLfloAele$RQ}Ftqf)J= z2QsnMn_p@R>ThWjmlST}YE{7)4VK6loRyK1z zz@hn_D&@m2##HM(}fy&2ZCme}GVZ?p2$g_XoPL)NiSV zAL}Erh9&HKrve?uP6lny###gmQZLUFnQ1nUe`CKZoQ5^|3sMw#UOgp{JymkT7}KXy z(}Wy-+de{J^G4BiPBPFwQQkA8?JpoJX<0xl)kC+!}&$z~C*LdWJ}AN+{u zJ{379Nq~azzT|5zO+f16&>yFbgSq+JormVQYIU4hoKuXw->RE8`xiC27W-TS3})F& zuTdVVQ*Ax#<1(6ks~1z!=@y8@LV+`B$U0?b6uc?^RpGx>eYia1@z5Fwn9Ok> zb}qCIXvWtcrV)n~>0n#$F;J8#0f;ql=nD8FF~{Q~U^6Vxht*%F7VbuG`7o!A-#iVk zqtqW)Nvthfee1U6yuGp6l5R29(}vRUG-@6I&0=Iv|c#25P@y& zd^bfPvukfoI0aZT%^xigS6o#8;+=IRU#(Y@4dlU%$faQ*^J9wbC8$$Yp6GUVgF)9# zk(3?rd72cMHsmcGDG8#wE7L>_&NhBa@QG(DzgKC8mXIjgzN-vTnKh4J<`NYX4c%|b zBa(w2N%(0~@T6f`>WG+077N$P;ukp@~|}xOA{*xevCWnHsQ)W|32gFDatm zyNw57mU|Yb{N(6I=>3)OOTUiX3fv%Ke|{2FTLKO`sH=I8z_{TTAVpP`GFJAbm1!2+ zTO*Hy22ho;|JGl{HP+EK^`&BSUaPFMf3;cPpN7m|)UpFQu;nYVk^{6h z7C1*0fF3qZ&B8kmUU28P#7N=SNfy&J8OI6q=`w+3DPsdrHu0xsNa{d_-_(?%z?jyb z?z@XZ0HK#I=Yok|zr}8qvsLBmCn8FV)Zqx2xToFd9x^PqhY+kLNeYZ|DG_6FC=9v1 zlR-;HXT2{^OS>Xybo$V-5lxn1o1~RZ?K91ugb`o!ky&y~)EGtkzUu_vLs#H9N{)Lm z)^pB21SbKYubg^M5&9@0MV`8TnA6QAc!i$TnH%&6l5IR|1GphYgm|LRD4j^H_t1GS zWvHFc5&!B%Wsp=o<7>epNvlcyY0&9-mI5px|wZWS-7;YgHCE;ek_U*ka#mWT_&M)47r4kY}!Aw&K zeT8>QDopU`YoD7wBWYRYb6t3XY+|9gjlN<;+VCNY`SGu^YE^VmOnsh3SxZfRESsz= zh9~$_f9Q=y!@KeDKZ?f+umo5!M!1x{VeNn7|i7pPF@w)^?Fef`b zV|th+wX^=q6g$HC(?;-|$eN;k@9vwQiIp2+B|G9`fu(;1VbxaC{KBgCmLI~?vl}BU z7!X5oQ}^X>qYX59P76L)5r+YWTYdqXl*#QemHUlRn%$V3r-4qG3hm!R3LU_g`*!YF zxyPjG@iM)=TR8+_tqX~JLM-V*flSste38_YFQKx3cE1@pIqG^Ip(S^DY`1(o9W2kF zl)3Sj-SC#>lSAajPeSip0U*-ao2J2BsQ}c-;rVsORZIS{$0v60G5!2#6UlK zgvI$A3B*y%rb^xK?WUcg*yf~{Q#TG{amXtGmy$;`D{RSg4R;E29j6J#oK&8>u;c1) zpp_y-e>D+kJF#&?7;(8RwH7L&ZyW9mDHJQKnOUma{?bjbIV-<%AqNJgnt(xfPqfi2 zBW8ar)xYqel0m!8d?7B4$&LxTICm=r|4JIW6F@gqR)+1)%KZwK(KG+rT-8U^Nwjw| z%cKh^;(SD(6Arn#kZDbg|LqInJkCcxFq+3_!Z>_cS-J2RKi>|0C1N{;?v(8PM`QA& zBsTZg4E@-iS>)90hxs>V#DF@wTMyCc9+<4PK#KD|>8s=h%YQwZE3K+ed>Vqw9MBrO zOcf`=iS3hc6{dP4exB&dFGn?0=q(LhDHGd|m8_Ge;|H2!H{5UlID@U3U6B`wX-;^Yr*1Q2FfGM*SGFMzsZ)JJ;v_(%8gXo+a&Y^jL zBmDpexQXYHfHrh0fK#X+hIa0kemy?5P^J!Iwff7>I58SqJ}v}e=6t{qpN0bLAjj|> zV8=#95tw^2hhsW{GT(QNe_g|L0E<`XqxKq1ZmtiZrvNjLnn)U)ZkD_XYmn^DS1~c=WL#kG<&59KK-?qBa5JIAwa#IqdVys(9 zSKDd2+nero6sKPl$WsM?A^6%7W1p=+s>9e=3esdaSA_~aWD5jpO}{V+x-Rk@SMR1- ze}!ypnvfFdW+cPuf)>ZWO9-TX@>3?|iYw7=Q=o{G%1)GK?vx9ZsL&)W2HyNJsOQNO zx}Pt6O6lU$NYTg)1(n0QCS%Ljq#_C6{9CAYOb1Z>+%r35ROBLB{F8tC1KAKABj%Op zbAW+0N92h%SmzVg9jMj(Vyvz$7HRD}47V9eM~UM^yDuPn5VDamiyb^6)L0qTMP`Gh z)USgQDs-!ra*>d*@GzN54LB}IM)vM84=Jbcluod!y zWXd%!)NDLE%d5^*-k>tM(fHcV?Rm9|$S zL%!no(U?kzDZc_jJn!!0q)Y#B&LbNcX*742?q;eDt+RBe2uf$+ffBUw6xCT$C%;+Ra!q zpniN!%ZwL(HyoPv>#b)GZT_qx1`JHM)72LHgq!fbWNaou@{m&``k1dtS2nxo>s^)_ z`}0Bcy0)~lTsgsam=p^dq_b+Dz)`VyQHuHQ^HfyM+G4vu%;MF!Zp*06 zbEvoJ4x|k5`R#D5(!QQ(NZ7f`o-1DH1Gm7u<1Y3IeHsI99{#Psy77`X=#{hYLY8z@t;Oef<5P>d{i;e@5aY)suF#drlH$f!QF1vp$gQgi904&gkaD}N2{j#x9(pr zx<;}O$?Y{;9F_An3*9U7s#wxtPPm*K!RsWh{%*ECvNT3@?cJ^e1O_ZbSz77o@dV*& zri5qqbd}ES%ERTxxHrd%-R3Pl5PCh>4qiv4$aUqB;fp1K0G_q;nd9%sOEV?Iv~S>L(esmJ)b)pf z<@uTL9u7^d?Awt)xR_3|M&(^Em!-uifm^$1mzfOMWbu$uyH%I!bVXcUoFm)n?j+5~y|YwkWiqy&(J!_&lFRlv?rRlGJ17MN?G)P%sY?)l zx3&&hF|ig}1J%Lp4gRB|JT^@aBo$a~(3%-JC27e47 zQl1zfN1Qxgf&oL_BEfINzUP(wXZ0@&L!Pw&mrY2q<|ldGrf$3AXi;`-?@4}!HQaF4 zy>u`r7RO!kHYY&nMq>Q*2ImJmCjinl7;EVI&9PMghY2trC%C&8Tk}q!|2f>_$(ZuA zJ_K=|bgEGWKn@+@4HHch(ze?>YY+nV6q6<X$i<(fQHmuEuCm?&Gq zt1&MK*CK{zCP)2XJqHp6_{g4%fi(Yq7ke$}u2&}+0rE;&4V?_qKO-YGRdlVdZpTHK zQ=J(W!MHa}tXP;S4IZZ0RiCdMq*r?D3U(OJ5TLXbXBO}^=}*Z9R&_m0GY>}L;8zRr zR7~4he*0O;z31UHIYh}I>8{gQRepi*ne7WD0aa0tL|!nQ^2?E^V5S%+R`dRItV98q z_QLb{V!2Kr@~uwrg!IhlN@%zLVHEsjlJe(@e~I~>o&)+IaK&>$WOjDPR-fZ7M$`&a zf&Z`Z)#YPid|lB%lglU{wyVT|<&P36@OXNQC#!oRiV^SJ){;@zuWJA6JA-bO2G+<; zyPYDslRWoZhfDHNZqAL2DclPNs$B+24Sz9nw)a_GQ(xb|*blZ&6iP}kOifMu_ZEE$ zU&{)S6q9lWwc_@lk;a0y9wTkA6xcL1i59XU^C}olMKSY<7hGe94^MQ!&a7u^_Ph1? z=ELVk>4=XRp(Qqk65TmK-Ys!d;Wyo7RgDKM9Xh5v6rwJtovUsf^4#tk^EY^e{@U9o zj}uo(OD!E1l9xXcmpk_XG`HMwV{hW))~_QL<-#V$y1X0y4%NV>>K4cS6K7u<+31SZ z_K3{j_fnCme~pyKE-Eltek9JkwBY|=3i5o zQ}9B9LS6_nnwqTMsCQ0z#7EBcEEgchBfY2H<3PbF8{bYEL>z!>@J%?|Q*t$$L^ZA$ zdvJKd7Bst3{@j2Y`SZPEWXaS4MtjO>VT-*SEFapoA3S0rG-qxI&>;S)0mUjLy}M=g zxJ!9TOgXJ3)+cnPzjs@?w~cU^HLqYWGH>YCr)L=69H3aKZgb*`WNEn-osj+W1UJ(# z99Iy(&f&MJ(~9xjN|(a0mG;nqb>nwXv6fq@WxA$wgzhlqxo3!3nb}19XLS3Azds)h z{mRTYWYEQ4aSMA6cTSR6sO$(X(ALT;VJL^}s2Mg2A!7NWCdP6Wo-+0&vW;g#qo6Mg!rsyt1kXH?UHs<{JF>rz92+jP?*3r^#aQi{E^`<2+aYLlbtT`q z^YV6Ypzz&9)s}1G&xW1kI=7DS7;-R$T7~=)&z!EF^uWy5EK8PE+MRp?jVR%Z#88kL zFl&lz-@5@5;epk>Y}V3QShrmBCf&0s0a*Sl=$UWSQ?A!M%FC|b<6<)<%=L%RMwa(LHL2`RZ5$?@QXKqfa6$?`uw9U_jmlk>(opctY#Lu4NtJ6og5sCCY z9j@R9m+(Ljs;_D(DH2IshArHr$naE?yaioUf*q>#iepvF2$rx zCcO~)>$_;;_ggHvJquRvlcMz^>o>SLrq6}iFJ*E}wP(4~k)I^=S#lR=xr|IKzA9q0 z83wso?IH8$f;I*4^|H?Y{xFp(pYicLxOu~fjo@6GSDP_0flxB6+QgPwGPm=<3$(M25W4sya6qrB( z9u@HhE4IXFi_u%&xhdw-x7X+K*8=FB`fpt^jp8~E^M;Vcyr8q(aK(?2lU2O1KxHRmEwhB&xg&^w&1-j5b)8 zT0CFH!P@y0yv6}nBS-dVB3EBZRin;J@4O?3kn>?ZJ{OJ++hperCmyg3Se|DbEK)M$ zzq<&eceC#4>Z(BoDCb_S563>aU;w0J5Lqxq?G4%jn0*So_;chX>p8v3VeG~Lk*~H4_WZz^vkJrc3kcGlj6XVN%+jC<@8{J1)@ZK| z$yK$qdUdT{Bc;I0S;|!YQfl)f+|2B;nr1b&jvMqAbE2^V4i|&0n<{{!s&=bpqhddG zFz2uOW5v{AY#R!sAdy_((v*o-D8(YYv2@rkqNdb(fa&CtntA`6{RlF66T}dtUM%G_ zQT`ELm4I?SaUS!sR{w#|MIV3IFAOOLEh&SdhxZWxVG@5LRsY;2xoRvm|2CP1M>H;| zygc&LHfK;7(K_)jh5CSGN&6H!MP+tI;_6z$+S*!OUG1QK`J58s02XGbZ2?ym^pIOa z=Kz*;wu4(Zdme&<@3#No%q=Gv?}+oe?@ycat~(($L%grwaSM-;{R2C8??E);R9H6U z1sQVT0C?qf=XXbKTIgQa5+g1#Uz?~MU4n23_V!v@xA=T`0gCm+q6A^XeUq4LRMD|o z-e}tKySxg0%T*o*e!lFcqzm}_Lv&A*^xXFr8L0C5&L6&F@riz+h5Br+QN2=6 zV{Sb_45*!`VBv|N9xTD-CpMhN+onBV~osm7$6U?-L8*#0ND%4M|Ej)rM0>fld}bI)^MTZ{(X>o zjKVoJ*A+mIbxKa7QfNu<_SY8`U?TSWli0MDrmK3QDQ)F!)r01e(G)Hc4MiM(R5=Y& zn_|tPUE%_Z-AkaJBZMcHp^Y(BvF3~Kq#`VddZGv`2!zj6S>X#V2kx&((H&7r{o-f$ zTE)B!zR01}onJyl2aw&7btsv31YR?RKi>hQzI#|-c>4q>si;^M8K{()y%f%7| z%i)z|r)Nx|lL0I4jnk=;Ti!PqS z?iX`Gn!4ECKl@At^6J^K?w5Po8wyHo2P_(#yc*n`S`Ukwg(Io^?pnVPTvXzKIerOkWTSW@xOoCvWHg! zVrgqRBfrpcCN9EG4fpnf_Vz4vl;v%EnECm24IB2`lkTSrUwsTdn!g;MT0{w+^Ra{w>{N>gEL|bobcHUqxB3&3@vb zqulC^3mDlD|5db>=4Yo}t!@hfZNSrFT4QnD&hJuRYT!mJJ!rF{qsOI*rCId7dgK|V z=Oa^7_COwJ3nAR3MN!k-b#dQSQ&GvpeMIi%2%Pf@MFLDgPZ_)Sqe2V)v!0SNemb>2 z2_^jl4XsgREEqc$KKYS@sylP+a)9tnwN|Ic4aH~Xsq%RDpmIyCSLY$UMq1?Y{KLQN zm)k~cT)U$IO!v&__IyM}#$ldnj+dEP(WQfgSy;`lt|}K12jEhL zlmiS#D{?EN|yOac{#_HjfzQ*IuuLj7>xd{_n2wdTURL{U+qvavda>#;t{TChSc zp06oC4KQo&)^4{n__yo5B?4RF`n)V}ayGaNZ@`MGg8J|=Zqd|NvxEzJ-p;}j?G$4DY$w~e4F;q=4K)w@U*<8 zr~Ss*dmGN=a^W8KV^LV;D3O%JL0(QdsQi~(|Hg0mErK*29+AKAE`1Agy7&2)2%+E6 zz6D%7)eG`;6{c8iT|6YEMM^@YtEmd%AU%ry^WR8}Z7u_~Z}|V~@ilRWx%wGm+P=lI zf=V>T;VUQl5yU`zc=?C(pLJtlS8{0TNJKQs_~Mc^VO+NJuyy%Q6gzYkNgVa0Q#)lC z4UxU`kQ{_!KSB%au#x)C+Xf1B8@^9dfoRZ~WV`WKe~cyWHpX7cchk=u4f96hf z3}wmJKjh(#3&nGp?6=rL=@})|Hvt0%Xd2!7(p3i!uG%kY4f;`Vnet<@Lun+5cr-%& z4D8%SDsC3i5JG@Kt*tJl&vt#gJgxo$S`zy3D?s~_w*6BPy`!5eC~h9EU!NPfq#uNC z=df-2CmVjE^cxYcMl31mJ3$@$!tL}`2wJ_K^=x!`t{qKJyN8bSt}3Ynv^3a_<*Tf^ zyQ@f1p!Yvs6)B8OE-=f}fA5T2IW_-Ps-)y2n-H{x*X-e6=JH0|auy6LFj>HX;@xZu zwZ)fBZ7(;)h+>VCzK-*8ALTEQ*3mI{L`q0a%_*p^-f_oi{}b$P&2=eOXga?Tzo8e` zRZI25NMFRNB5bL5#kQgJel|#WZnnwQd9h3f+fjV$J-2qTj_iFA6{nPv z!QU0Mh$J*Hd$%9#f`!^Q3@{YC8Xvt}FL&>Ek?p);tUMHIu^VE^$Nm;meU!xf-A8mT&17yOR-+xP zs1`o+i&3ex%ll6DnCcKp*P72g~8ND+N~*F7530?y9-Zx4a`V>yM$)$jt7EEDbtEW z1Go1p0sShxK~doF3z$n+z8NG((%*v!#a0M=napp^kgk_auxw@!Y6eLpJ?X#3!NmrF zV8A-*KnD1+YR#hUIf^5!i7%nlE#<6!NnSZ9biDI+nKRJtO&K2*i;;r5e-mi?T9^{7 z@KsSUCWjg4)4jyHgtiA1?c32;0?IC1*p<-AyO)35&O`3(`Krmy3@tu-81QVklD~PD zI|+H#;qKRg*@f7szmFxaUX#g|kw6AJr@?T!J*|(Hl;H6Of|F2p+3j(`>0lHIVWwTo zt!)1z1<%jG&=pXsCYxf=zAFdvNBZB-vawMJ^Y^qC7#&|EVp^+@3Rvx4Qq@ePoP9o+ z>u;t~PnMOuM@vxzPp&*^(61}7yL@Y6=C=(vocO{7QFY=m z%Fd0%1aKd)Qn-52J70X@&DYlQRuQTpeGzW%Yimpb={2fxb3Z)Q8Q-v+Y$=AGko2!w zsMcNDf)b$z-ZDvcgSFVk zjB-yGp-2Ctje}7|E|LR!7G8Mst;-3(&<{;!Du82(q2M{un~tU6cGou5r-svDrHQIm z0ERDa&4tgn#9>Y(j)&`e3Kuw!(1K4Dk^E0JW-WO0;o_s%SoGP9vbn0~&lg`}gB#u> zr8kOQNO73aU~55^VVmz{%ko};xCs~t_y;(2467kHaJS zb;&&5--~x>ac?%X7Qv4t%o(ey8P1Rq>0SQFG5lFl^#iq4MbhW!nH6KoWh2r3Cl03C zUq?)*5~Z|H<87w(c2V?T{xJ5(a2e?t8bXB`N10Goh3#^Rz9U9S- zdf@cnQVfJ{r%S=X^<5JK8g(r&xoCfyTe9uxGkl`uaOY_L%vh}}@?+us@NfWXq@5r2 z*#+RF`oTn(8ZQVloa#Z8PIuL?fdCoEORULNSjV<}L51fVH&f?46I>nwLgiHZ7~e7} zZxe0U*tX6M=U#gy4=lK1+dA2~HF_8|CYUAfg69zea6gEDqhD4R@!h>zE$)=85qtak z#inDY9jOYtcAtLv8gGy9_m40BlFG@01-ef%qMvM(5KOC}C^jGc;z_JGFlFB+muTGY zmLt<7^~j6qNx{pK|W#X~M8t zYvdh#JZE@FeX6`3+~TL~S9y^!(QB6z6O0Qc01dB*JOS2hxEEKfR{}=aVWvBfqslO> z7et{7r$RirZ36#QgkES1wqG;03KW!dm~v8DJ4(qkt?>{Qi8X|_0hsXX_KEt)uh7JP z33gLsrs0n(*IErsCP(yMCgqcM$`@(wdCt9+C0spvGwF!~~a zNDg({UgUW|jNBgxY}LP?_IpMT*wi1)sg{yCc-Ow31jbn}t4z2~1~dt-sqQ9@i7{C{ zvKQbiTGOu?Iv-7NpLuicPw&zhcjj^%`&a#jm$!ejsBtuXOM;1@EuiLa(>Q-NRef(m zCXYx7;Wii+c*-%WU63GahuV59oL+jS_7fTH-*OT|W3-)}vR7#qlW>iolEs8uKycB1 z7o4nU$Wq+3d&B+D3`D?IwT3NxK4)882eiJ^0Rt| zWGK4^E%}(_{VL~PMvvU_;Dh|su^QoJrX_B~7YVsu{U1f1*K!oqchvt{L}ebHYPK(a zSyc$KHzy_t4YyXeS6RWW<&d=z>JQWVVsl>$fmZU<98fBASihS8C#|M3^3zAgxMHx> zdbpNN^G{phC$ey*6<9yTBaWe`#mj1(TE2e1QKK^JF2$MWt~6(nA>BgRjh8f7Mu_rY z)Mjwo5C(3wYL!>5u?v!Nw7_)7-(eKD_8?^1G8jp05!?3pIRDzxe z`P_mfdjplveQC(jDIrpZUbOq9FKevn!Aom4ww`4DXv;!eV}2CC^ZOg-vk;o3l|dF~ zf&P4EYlgKyrb_knA*(lk8al>kY1!VL$y8u&z0yaN@Ff<|JE5}gj)REqzgJeE=zqQ> z#B;ZvS!qojqwS7xw%{$z$B%%=lG+Phw3dO9bW5ueAmz7|9bxo``?#{-OHTqBi6$t# zA^uOxJsDxEVWx79&L;`;x|W2QOzg)<#O zX58qsR4kulKBND48?}-3tREO@0zUZ|p4tjN(1DYLecBf}7A%t{ru2{C^d1RAIrDpt zSeu7WnJoDaPAo@JJ^{t+7roTf8&%ym#QkYQ+{*An0!ly;^hS!R7k z*|H~{cdGm&Yfr1G!}5Rr7;+KU7;Lx;*=AkIqYE8}_<;s5U0D z(5ZnA3A2KPbh)TLs6mS}nxT*4I;39iaABcb)QD%BI|_)9Io~hMKH|2Vu3X zW2!tisy2e~I(C;L0)}dV@bh3&xmt|>&cGeN@m{zR@1ZOu<%z7zajSg)56?R8c?3Ml z5l02L{}e}98<~RN_gt=_+`Y7Jg?%H^)B?dWhL*1b5D%9ULM`SjU{?O3&QndgHK961 zy6fpYv8iKmQI)A{0D~>cM-{(yB?k9 z|AfrME;;!)@xd0xAg=Prl8;MLe5lBuYNTC~S_oUcq8(IdAASHq_;+2OF5ug| zKZS~i`f@K;@L>GO*atO}FyvJb`P6xc+h0zNFT3%~t^7mmV%{p+wGnv_Vw|D(G)g?J zEZcW3C(`Fwjtk$bvZv+VHy)w)jpuHvw!l%YD6#v$1yPwWOAgzK+7~@}P>?KHtxg7} zk8ESM64Ym%a0S=}Eix}?pqNY|@9HikH=MxmT=-zBJP7JCi`-2oQ|bOMhQ_GXCbKzz zL-;(7Z}Lunt0E$?wSV49IE(1tbb4UT?C}H_n62-$=w@T$c4Xm5FqBc7oHv_EUm+wR zK9I8NQaIl5Kb50034|A~d=JT9YZH-d}8dc-3I7nVLc{xkLvJIH{j&-&4XZJ>-@utPfSvnR5CZmHO)uf1Pe%ps}Z znKgfAbrZKm#k~v1z~i0rowIn+DfPb_^uK43^hkRu*?ICL)}HibgtttK4!@#sSzsP5 zPV4+0^B#pg$U{5^vW#8`g;Aa^@!hQ4oQ zgBx=3&*do37RfL^Qg#9u>!&=<5=57J(}1aMrWn>*!cj+}s?FC_zLsw%NiUWwa1cR4 z*1?R`DB`y!aTqD#w?<;&c4GUi&v3=18FzCM3@6R=yp1!RyqF>sd-5(H+d-l{E8>=u={;Iyj^UJ)&{k zoQ^Q&s%v?VxfK0h2*iV^f4HgP6HR2u*9$4YxwWte>eX#AIQNzL?_xj#DIOHa86<|Td&0E}kQU$GU^eFKCfD#n zQV!$s_b7s_TMv#=T6s?*8o>wZ`Z%-{9S*4{V$*ajnjMAz^BjcHRT&y_1y-1pZ^@4U%Dj{hf% zLq?nP59@(nL*tLSpNfxQB7D=j$I^YG&lElEBKqOe$eZDkN#IfH+O(u7@D$OR?#bxA zc=dpH%eQD6nyv|r3Gioam=$F*1^-XKMTW%fw?r8gN3{FU5rTsGrAQ9ep$Oq$2oh*$ zH7E(7NV6PMAm;vy1B^56z=AMrID(wQB?bLP|LSTHLuH~uBGP}?3C3Lohc0S zU0i!Ka<+>#=X&*yyQDh&a=w~Fl1PrIm+4F&UN!pN723Y-|6;TM?dSjJug>O!RBgxg UDVsQf4*>beODju(B}~HpKeW_v4gdfE literal 0 HcmV?d00001 diff --git a/doc/images/qq.jpg b/doc/images/qq.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2638081c72d5c892df77205385eb27d6fe926919 GIT binary patch literal 116720 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�Re2}k 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 literal 0 HcmV?d00001 diff --git a/doc/images/qq.png b/doc/images/qq.png new file mode 100644 index 0000000000000000000000000000000000000000..d326c5e7889b1e6d62faff61bc57a9bc7eaf935b GIT binary patch literal 88017 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

    UYHz@2LoT^93V^gm%Icqc+tMXtgXC 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 P_P~WTzH*U?agF{T!&tyy literal 0 HcmV?d00001 diff --git a/doc/images/structure.png b/doc/images/structure.png new file mode 100644 index 0000000000000000000000000000000000000000..4fc61c7bb38def91cc0b3b2aa96bf0f6c6e24913 GIT binary patch literal 35279 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 CE(-<# literal 0 HcmV?d00001 diff --git a/doc/images/vim_plugin.png b/doc/images/vim_plugin.png new file mode 100644 index 0000000000000000000000000000000000000000..cf5c5b961088879b7a9469223039599d98f61a17 GIT binary patch literal 38799 zcmeEtQ*A|5(s`Id_<9;O`50@VNgO{86fwcN9j`YLBa4FWmi{R016KuhH$rmw=(QfBuL*Xb} zA;-)qo8yMfwtWbAz17K&@8rStEy=O+2+*>(%f;yDRI!qwCcmZ@+YSJf%b~Fmf{thK6~k zvKV~|4qmdv7k^30B>F1>W$FH`-DHh53k*%f%9Rp0<|O<1FN*sekc&eVjV)c z!r~}^R|;0q(+5~{f1ewMDFkmUhY9YcmnNf%Ia;o&!U4vdGx(8VbopqBC2E1nSeQj% zyUP@&p<2X7f**maHm$`C8WBgl8z9ls?d-HaLtZIOJdK3In=H~t-l0LOgo!LQEnD;$}bI0>b4tRr|bz;L@j zlr+}jbL8aSH9Kquvo$60>O`|Bjhgjmip+uCAII#4iQyy`f?Mak{p6YJG9k?GBU``- zymhd#9Wvo20f-cqNa|?(m~%6N90M*+`a2jwn2X_AIkPF2tnop&k}c9QKO)mP1;Goy z76?;+ine+lV~(4RTrIe|0K`q^InHKt?6kJlKq*lc#-fapSEa`UF0Z4=U_uqmuCuMKG9OvZZ zJak4+2_OP}`saES0}c(#`-%PhXeXcA{E0x>8FCAM9wb!&2$cUn*9O(6)8DuIM42PkZ7?mXBE@QUfa+0FY|7gO_ zRz-wD1b&EaU3|+Ei{V42a`mSX3z<78GXFdnD&IA2kz@N?1wej^pW1RkR9wBDxED*> z$Aa-BVQ+mEJQjU4`AWRLZPLii;ts;Q$}b-HpN4&CI1E^QO*bh-RiO|3vCau^HB+7% zT}z>c8aZQIGE}J*uw>UvXQ(Yi~0*=pmtCxWUxs4bl5DnncC4rU8S3@n}I2WatHp+xiz%r%O5}R?SoL5 zB@&fOT^22I^V~X}hPVjTU7W^7UMOR*)r*A8b<@i=V!@b`aM`)9;N>wH?U<`+_2+`7 z4}diXqX4&_(BOVyfG?d@!U&&iq^vH`OZhi2{!yGZ@O`yY&iv6VSwJYk=eC2T`MU7a z*@3s~vup(1Y;L_8Pp>7|?-znXpxO8+71N`8se#WsQ~sB)j*4{~yd`jLwm+k^ZYHpV z&*aiYEf@`v_U(DLRr~rfO@ZlI-yH)z2Z{f$Z;f8Vz$zvF{li`TLXjSlbNlY=zo=iU3RcXS$I zP|J3Qp0L=YjS9bMD6Bf!lI_mU|J24-sN<) zixAZ6Zy^N*NdHwCJ=E)>POA(;YG+sEX%otZ?9m{$t5*2x_3*K8W_~+|xfHasq5~5ioGM3piuB*z$e8Hxc0cyIK?NGNv}% zA^;i9MjfvVAz~Moz_t64>A#Mtm-MGxY z8EkDCxqTnOx0vb06YXK}+`0%&f@~d9kk~eKgFEeK@&|8L(H_b2;g$Jc+Cf}&`thhB z>D+y`|s(DLt3-^$=G6olwBSh%mis}e8lmykW) zEugAWxZ}QhU;$hVjv8xZ?Dt-uuIMc&Z{}h1)8~pEUVT2(Xntu`+-V;FsPwt*5Rc0e3D;|v4AHVL5 zQ$GOMnvM&kIGc%C)q5&f=da?uly}__6;na%{Oig;C;e;L#~Lt7RL(CyM?OW9K{9Z5 zB4r5>#eUI|0)zxv1Men#KR`M-C(|~xJ{w=W-u5`fGEi)j=-gGC(5%+d;mdP|;Nt8N zp3GxR1%7V(d65{5Iv~(vC0a-F94JK> z#k1cRsM{EyTE+S%duTjB=izb<2lFm-IIXF99d19Ky{%|da|~N}EpaH$@RxD-6k8UZ zU6}Sh+%|C7Qo{c?;QWx@vigPj52w?PZeu6WlSLb^o3jlbS<1Lq({s~V+_3r>giOe6 zvfi<(SQPT^rT)!5OWz<`$ioDOZ?k#uAo4NqYx)0%0FZlmuK#qpEh_6zGRq}8LWj); z4s1N#_X42@e#W~{@y$M-l+xz6dS++xI%mg%tlqCUSV6D{{+;<+fpRb+!s7AH8Q+Ey zSgQ@D%waK-Cwo5ED(R=LY=iuXG5#Mw+vn#xP!UrA2eim!=WmtpH>7q+1k8%drI7^t zgj4`ettQ&Kci-w8SsFR%X+nPuG70n{Z3&NrNpoIe_5kFURZFmPnLE5}w>Ek3h>2_H zn-I&v*!~fzYR4&L0-iF~GRcr%xrLM08(g6XvQ+8pfY8Ax>@wyY#J>+TBySq5^ZVBD zj+O|`EHpRQ{q+rrGE)C_^ev2FUWDK&tI!|p@bm@7i9A+_fV`_1XgK%Y7lbIYxM;WG zQwesDXAhz;BFG72?GJ8l4o^0?q3SGMd43BX$?9K7=75iG?hclpzsZRXbZ_Yxb5Yy47EaXGS{y zd6?%&kvxmcyoQBa=J*qPp-Piq7()mz^Yb@eqC7cdV}~`Yhq8sIx~*X>*y*?1CC3-5 zCTD+?-vq?f1`H<{{&ME*!FfPv=r^Uxu?PMo8Ep}u{XG!^R$HCC3}OXLx|zM1Kcf^uA+WmH&z+uvtZ>vjk3p`cpa z2+Sp7IejT)b9r+c<4?<4%SbIj42Xt>zaGRN#pNBvM5ibBO2i{_@gCT>|FO@NhE_zf z*-6(Oi?hLh_pHX1@aM~HL~|5#K?D>0s5y9g9JlEu)H_Rvdt{~HQj3#l`F2%JcLuNU z+hX(TGzTlgX&o^vmnb0P{^sL%7C#^NRN`&Cdvi6P;|YidXewx2M8ThzKllLeVNUud z1rCof%U#kX3fy0=tDW#E!T-=8R~A3XC{7fW-pkSr8#AKn{*p{n2bRgX@{v~W9Xj=& zBgKFb{Kd;}Vx}dTBVc2{vk?$vI}`BNLd{;-3ML~nT-ou4b+TDv*$fv*1cYiw?U>KM z;hTS}5(@NqJ^WuGgL~uiqa}WJh}}W;5la#Xb1zi|wYsh@9=FR4A<= zf3mmgKiT^frtoj}CjCVcJQ@Xax&Dv@6RJI9M{rGs8g^#zL$}Z>hGt^~fdGxn03#m=c9J8B?Iyp>eMi&bd{^ za^h&AU6{s~9ceCtiP`kg=|QBvJHDGkuOD8$cvt;?xz+e1`UcK2mrgC?gO2-qT*-pd zlib840v5jJqkCS`qRy7)CQ^>z?~t~Y{72iyIiF7+m-y&LoTT-IPlv1gYkKDwvpBEt zq`AKkhh)0E9ri8acV-zjDs=<|{?;lGs7^fp{$_^y43Ne|k^^{)&Zg^$U}L^r@TEp} zBxh4eTlW4u>a*$C(8QYgco0;X3*KUf$Zib~pPHYq0L8!$?1Q%&vOab~f#yYg7Ek%E zpq}#t_Vf2;xVGpGa{4dA!nYX>ud^ZiQM7NzXR9YQw6Pm${dF*EZ|)*{CAjq~4geY~ zts+e2?cBi1L*6-p09%1KKV8|Q)g0Lnn8ajT^NZ@S`+ii^V7v0$R8~*d;!8cXRvm&d znf6+<{;F^G0XXO(h`m}X6T0>7T+SwgYk}w_Q7ftj(8zyOegV}U%VwI^=FbqIBZK|2 z({e?3L&ZsYqWj9=__`b-cUV>N;JUJ72!MlBNPBJ1vrG{?ll4!_>Em*#03eOPId&oz z?xjr{*;>fyvcJIe-8Ep}lj!wyydbY#V)z^-gcG|?V=UB zq|@umUTCs8^US7Wo4sj%7>rF_K@jn-x7O;+)emsVOwfzNNOPpzky0;l@Vhv!CuJL` zoXdDfe>k7p5A4~%AI64+WY9b47gtnDC27ejeD-YG;`a(0Rc_4(mVrU+l+grlg_JKZ z^6nQ2^)FIqLsR?{DcZ;-YfvzTbo9T$T{UY;Ke(X*K&;rfYZ zPk(-$E)RW}o>eE_KIjqK@p!Nv5JkrlN(*k1EUVGfyFz+CTs5^)YUs}Mc0id)K!Nyc zJ6afJUdNm{b28TK&Vv)Qje6^$ZQSW72Q{Y3d_SKR7s7T8RQHWSF7*KsY(_)tL-?Pl zXu@0%^#zu3d5A&VkP5`1wm5x=WoD2C#MJ|!mF{;dmQ|7t189j{6` zzGYTwI$kV1r)TqKB089j27tKqlQgVQUaDY$j{QckZXdxd`~Y~gHzx1XAu8T}%{^P^ znadsHyeh9V($0ROT__mg$9eOULJu?>H*aAdui2^RL15inCag+u!<$Frl?tDCLfJ*$ zT2i8$3)fY)@FNlcZI+w!)wrCByEPZ5qeb=@h>H=b7-l0h^)WG3j&tyHfBa2!I9)V( z>ZiZz*F|;>%(pUMq<3}=x3He+?Qp5dsC%xkpQZINqQuG_E zkKo?Qa;oFHl$Z>zkNq6h36BH*`G(H8o%yfDs1)Z_nJzPDtCQj0@33ln130fwkIN_& zh2AyJ?=|C}b|G$crqqTBR5(KAoHt&ujj<312{B%a-a0W)0H$9miHV->0p+S05)%njD-q_vv)edPU&zd!K`N|8cDD7drm%hzQRQkEiN;`P8I5 zVOaR!tLjEz%HKuxr(!T19`(;XP%y=~`T~FXB4{p%CFp+{6CqGOe03h!Utfiw`={LQ zf&%)JklVw60{$PY;J+zdr(_fk`PR=-Dj{LsA}MQTl)`%M{Jmh$YX-mlGtgg7lK?qo z;Vy-r)kSg$7O)Uvrcn1;pwz)I&j^t%0kY>`)&AP7Zy?aw^?+GeUJNJ2v-`CYTCzkb z3YPx>Fg?^CnhFG52+VA_CjTmQJa6yXk*^No{hL7iV!(*LW5iZD{78SOvMS9OCYF4G$|n?DLJNMFG$o^Z`XuOOmeZ)7z-2kms9Y{f6|b# zIqth@+H3cn!um{O!AeN(WG$TlyAy=q=;3SqTZjB$!1ZJ3`4nM-Ck02RBXa6mq=kxR zu&74g|FkcFfbq+KCNS7gKIGjSv~1pi{#4b!ddl4zK(z7otbTvWAD~ZdzgXW=p&#a7 zUtRbm_?rIT){w9EKNgzlhW+c?-+l=yWWIp?;kn&azkJu(To3%OQ5${<7D!`&|G0#n z8jv7F8#6t~-&-_(3BDnV|LsWDPyyxoYh$H+`g;q_FF^xT!CwbmLj6@M(ZY@YZ4uV| zQ#`GJ{z>6Lpg*DckMxqk|8$LjAiu&)d0I4u!G7MsvXiw?RMe=7L&m}^ifqgLLg9wq zG!_7-4E0 z7TLt)89EY77Q`C2>7B+CNRy-|RbSr0eC;6^4Zuz?FgFm*&Laykh{y)L={iG4=pLNVRzRB`gA0;Yz5H6w0P+$Y2v@FeUZnxf*)h9i9=xGNh@s_AQZa?+< z#xLXR&L#)10;r7+eYg~6>%LJ$%{)}24z?M+S-m@_`)sa%ZWg;ulDLjk^mS_cx=xmd zx-r5@W16XCzF@z4Iq@7+moI*Ep`AZJ4W`XW0?0XS-}5LMkf- zRcC{k4}w;aX$(_y@guA-!umXI%&4mHAHyK=|Lz>WieKW3Z@78=DOGt1Ly8} zM}`a$+=@6Vg*X1B4Pq)RFo9QAX6GJ^rw4a#B6`7Tclww_|7XP z=JK`PM?MKvzjt7>4r@Mt;xSh99HBM8_$pOb4a0dap^5ZJvi9vhdO}k42$WzHqJH=GK1m@ZJ=lM8y`ETiPwXgS=}a6>Hdqz+tU!WO zw5ArgNsP7hXelA=&+Q&YT+5Qi<p?-M+Yzejx38Wk(BV0KNau;M;au4G;uM@PS#k>CY_;v(DTkK z6p{#TrC;6;S}2~RmRga*#3c)Y!ec5$!Yn}3xhI3*qWagwy`TC>6Lu~pfkPMar6nYT z**^yLW8tdUz)k88oPPC$zt9fNy0(%0P9>qI&lUU4I4?tA_mv$NCb z9hNu!03!4g$S*bKX)vCQz%~T>?hzXKJz=yv&z)SxkVU=wpq&}7prnk_7&oJ9c+saI zdl8@cZc%+1#HnbXej%wl=Dd>@6XPw$#&$6#riy^zZGBl}5m}!7v_AdevAes|uk&=t z2#`@>XJObu9NE<^RuXISK|Vbb)2V4~t+r-WBqp)ui&$t?Hr0$#U#UEh{EdC{$zDnk z)JN79%~pG6tlu^Q61giB(KbIB`9OJruW`Ex#Jd=%lj2yA~2!NujS1} znTn=~tD-W0JMRm?@S?ait9YGKa2%V3#CQ8p@U6`mJ5v*ekNqlmV%g zA1g>3t;H(4We=AnB;I%RJgnMCPVV3rs#lw018)`4G#OZ0^=em^L?NM$60G1lc7)EF z(ECyVXmZeMSf(?Ii9|iF_6Vd#OBv+;D&5Ly7btyxMmSDcwxkMU=idXejbMf$SL(nf z{|^3>6K-hE-35AI^M11RIH55JZ2%?(>s0*!rB(J>D2s4){-b7}1y01l?f@CwqHr2% z*bIj6H%q@P`)8~`c)McL(?9w&L7ODbPkm%J^?dWWb)6}b3$S}-bFl9idwSnGqn$W< zg5iO`I(U^te00oSW=+2pV#^ILhoS6$4f9OS;tft*yln|>teEhG5jN2BRH0bpNmpdR z{oRFOV&tInpf2!q`2fLYzsb1EHnNP>+;&1DVv?c?JD)rw_a;w1sip8uBAwcI9bphn2rz4XmVm9M9p+B*si_H-gLF~C^lv*L?{kaP{d#0!1EIOH^gTk z*`jcwpnfTKfOoTfcZ{u)7*p36K0X+k!3L;bzV97WCXEON^qxqfj@*Y)w%}AtcMK|B z9DtZz!DJB6NL&#bF{RGuij}njmzfOjDl_(OouD;1qxtZ7EEFCiSr^ zp-DrnX146Df7{3V-j-T8CXbAU5g@T z(2OI*3yvh#d{^^HPP>^Se=6~UsXsp1s*OBOjsO*2O;}@0@ZX~_UnSs4yns8$Ng43# zY4eF8vDYy9n8Mh51&S4_X&T<9snQd~UV1n}w|#i`grd$g_wAi4m~7lJBVK(bCK?2f z1QR1?YoVe;U;b+H4l}n-MfpZrghW>Zzvdsi`jNHjh`sd~z2b37yto7br3?X>;+>J| ztZ}!nsyIc_b1|u~qIplcNSQV5LDQ{)C_Uc8{IOv>oqFKLQY?}Nb=0&~^$N)hP=?&5 zIrI&K?Szu(eRs9^j4LhJ$1EzEoJfH(QLV!e z>fZa;k}_AwLPw`vC|IoAxQJLvjgdl-y`CzK-9%PPV)N2kIY!zk8nlvqiK@Wi|e&YtDmJJ_zR3#NsPg1?c@49uF z837xvZ8P#ED=aQ2SvgF7&r@GARIA2KYxm|zeY4f52wybE>+}5a>3(kBBc9Y$-1@+j zokn%bDQ<`vP^(V;-c6*3QK{E1T+eT?^@TgQXJf>ff=9uyI53r#H@~!fIgmpWIx;#y zg6|MN+2a{U?KxlY6G=zNnE?^%wdJpKbMm1UiQ(@jxi5sx68a7VHdHV+RZ>?Y@ zh<1bcZmr*VKg~YfUgsk?Q_VI_NNj#7rh~iY)AKq66AUQY`daog?@3_pa>lAH=HGU~ zhKFDO&gsvlhpYPYZzE7JiY1F4M{wsx$ECohh2e_yis-oubE=XDrl_u0Joo%-dU7W+ zg@z-7n-^M_@XNRp{F|Rs8RplN1OO% z{8vygX`=OP)9f{8p{Yb3?a zSVBe&9N1viZA+?Zh4Q1zBZbvnoTg-a#D5w#vA^_#_wG#1j-%ci`y(KiR zK+or`Zgc#g$`Q1OD>$}J=^RayaAY68ts831@0v~)rr=fR@_T%6GbsAgJj=SgC20Y$)qne#%QYRY}6Rcg6<^R%P zbqIjHaJluKj?H33(Ryn*YAXRXn)wNhtf{$vO979YP^)nem=)63p?F>BS|+nVIm z#WagitlBj_R?K`m06huNWqdJmo=)=hU`u_tSsI*vab9cs%=Sfk$<0VC`vn4?he_`h zE3y|=uF>SR6goIyc!_M!4*>TGy@>RdAt<+&Je?ZMy>&H8`iDQbR6I9x&`+&6Uk+MHW@w#$(@BxE|)fV5kWGqv#06U&pVa}Wcm z-%gpY1uJ1r0dCa(2(b$a3XMl9VdaFj>+*WwI%GeP_$E#vt3gWNz7qLtZ}gy{j|ZDs z@P*;vylTHoceDtRaRd-i(~O@q%J*v6M8eRTr{1U*=C3uVVO3=;^CF?9#!BUq&s-Ss zT(vHT9yx>~nStm+){{4_waq{C#p~*J5_h#IsK8+seA9Txdi_Ao;>ql0&5)z)Roco6 zB#O?OeE#?QkMP?yX-Q!T2KGjW$CO4qN zISih1L&U77E^gf309+4|6WTM(2eCA~ZGGK{)ID?-)3?67k}#A)&Zj0z&mS_cBQ}GX z5{&W*qULoP;q%9~Gs|xTI)#R_w#)w67aA2f!C+mJsjXyGeAqY0L7#KiecnqqPN^|R z-9dm8VQ|SGAfJ*Ap@W>K?ClBwj*Z$0H9@Z3AsK#FES75qrYDy7YG0!g%Io$aZxG+} z`Pj**h#$4l7ObWAGjve1mHF*Jw0oOd^Sn?DS{*gDnpjf33`WK*zi(Q8Jt`sT9}I<( zXawh`u^o;7I9lpxGGyVfSc4z+%3y*HBnw5?vLDn!un^L))& zcCFg2dIiw*D*bX2kEY`N!PK*`U2iEnUVOL&x2gJz-Yg~zk=t)|mtqf+gI93sbADaP z4y0V9Gn{W-D%1w2p;?6-$`09r@8jA|K3t=Q4>ap(DBq>?iB47;oceTQ$??J8KpL(_ z%blujjom{p>F()P3>G~u#xQjEB_Je?s7sSIhO^D>H0?-mbfC*xsIlPjw27jc)qSGI zi!D4n|2B@h7#`2repz#@Rr3Y>o+r#OWYy%6E`PeQ+C@8wXf%f$&#Jlxc>$(GB-D;D zfC`IJLPf`2ON%oU5%1-ZmBV29Dalv}STgaptCTIg(L(6eh6;kQU=+NssnU9crcFzN zWCdHZ4X{eTmC<@Mh@L3vZw9(#c)=dklQ)wI<4Zj(uVI z6wD`kj$rozj|)PghOnXZ*7r=AtX)l>K#oYbHZS|;r(A_Je3u6LYkcYwwXV_h&Q^qy zMAklIvkK}Cy{;~e*lj$uGQC!K#}gtY*SkX$V!nLxfPuV0Io;#xsi+c6M4#dOV8vQ^ z9!=)ST1%QrVZVKxr-Sh-wQoq2OSd{+%D|d7*N@!#=|+l8S~ol^)Nq2stx|9WboAq4 z;^x#GMR2EIy4O@n{E*drb&``i4>?!YlOtzO!#M)J;o$+D zhWmhLL@#J2DV#-VC%5@rR7oDgVfuq?;(;A~pxW6&*-4+0QuOQz!o`!s1TwY(6_xy=ygv6NM7Fiu?>M@A6W+Wlt9Xp+ZGg>O3@C|-tS`%;UG|y4=b&F0YZm>kCAV8^_g@NA%kdB zW&yJ1{2@0Zt=Z^JKa_s#vwUinS{K(nIXEcnT4y=4N5<2lTSPWi71Z!-fh=4jWaV8M z-Pfa`f<{7yM(hP;Q9%hjBrP0Dm*e>ofh$kdpRUpf-1Eo*~iO({FoOTJ2Sr=l#K zkDR2=6_Y-BgP_ceSrQjfge(y(mI@eyv%(c?j1u1PP`acrjYFZVq+OQs`$ zY%;#cD4*Ek-1&3X()jbEJVOBoO^o0=v+JWbhny4VC|gljGD?*j=W7sk-lafzbfpp z011As7V5sPovE@WIG8!0`I=i0Rsme0 zqlurLj=5~?>?AxdD(G%tFD+{4F;+UH~;o$g%q5X0H1#gu+tc0_N6lHt% zh{y;1lf}kxax)>#iirMqcl;*3t|jk5S9+g9e8L(-F~Cb3o|H&`p;%v#6XY^Z`j%@` za7*AKOdj3`7S*HW3cg`VSJXSj3DkU}P70zFMLl1+Z&`_MW17r?PR81?N%!IAnT7MW zt@?68=S%PI_QpG;4ReN^j#@J$|H!u^E1>6)0 z()AxDnu!o%lvr|P+L`St&^)L z8(PlwfbSSlz5=C*adCa&6kO?j%96}Ury$4yUavpIu0O1YIIa%GDODu<3qM1my9U1~ z9@E)VUKQsmJhpLUoQ6)YvLpBwT&ccz>ST`2^*hKT@~g zCHmfBep)14q4B_LxBzg*WwS{%Z~q=w^xEO|l4==>w7YmT{ju5-pCB>2D1zNYK3=JM z;T>6G=%HpaNh=@2oCTAVWZJUaPD@IDCBzqn4}vF$Xu!^kZ@KO~+TPTVWy}_+ z2MH?fCiY6GLCI!q7#GK81-TMX4=ta8jqfnX+%LQP5<7g%K4|S#%_~I2*>VYH>b_LZ z<1P0-+3d}EXdFfHJcxnE+e{!vLisfNMa69yr7xYPJ7yO`Nddm_md@wRyQvkk)#*}X z1Rn3tQe56t1t<+r@9{^iP!y@>ue;7!$C1TZkOlC%m#5*Cmm&xQMK9H$T#F@YaGC% z^{UG(+Wz?ad4jnf#v!zJ4702(p=4~j_A9JQIvkbE+{T;v1Z7W(#LJ?W`~GMw5&C;; zy4peas9EE+bKTBBgNsR{_Dd5JNN=OPiaibX&f*q%io=C#GEc`NK0!K{pVkqpa&lOm3*yAt^Vh$*oRM)OVRRw{p2279Y~zGXsaGYp z@iZkfEs%zgAS<4;l|SyEg0KsH!iH688EKe~N--ji4^DIGwM7Ie?D4gm zFOR+-TjSvjNz6LDjPyddARef$*E8!_-NtQpK{Ynxp%N(n8VVPDZW|-@a;T{>D%P7; zHYNQBiP2_C^W}Q>*U{7rk*dx;iQR6uWY>`6wdz~^QcQ2Qy*zbZ86CFYVL=3&&P}F- zym2_%m(O34Bh>7V>n<|JXf;^R!SWT7C1UT^uFQ5(#ftE)UiYaE^Ki}$!jc1YFRJ+W z1%sQG@nw2+HSI+sqqU}#alAMVH(D~9nLEr80$=90kXx4RC=O_@ts2WLwNH|dySy{c z)Hlo3n|!OCm1REOMylQnNP=cl#j7c9R1q_SgsEpGw8J|rmwPiFg;BZLo1O%g!6Zf> zl~^Z2<%GY!h;}mgtTRWd?5%sE@HNP+S_B(pwl*#!HtoNhi(P)V^orNnFSDmFUyGac zp5x=?eqE&sEK&%b;5t1ve8*^Gylj$<4w7jxf=#xK?&N!$dJ~a+5`&Mu7Hd-l*lPcH zbC}NML#}jIYYp%}B^$$vQ=4eBm)_bAgBGF9hkt#PxvPEou%~Hwbe5%vgP+W@qpWd0 z)H&<=`osRVZnw@9&qJQ^H9u0FYo}>dB$!AsRo%p>Oe+@+-|FBOQZ|w=h|il1UgOa& zT8lP?CuF-KvYX5_LGe*X*R~qWmG{KZogQe1hhN=0@^#*)pACM)ZC;NdxZ8^X+z%yW z%@JBeXg8L>8e64!sQ4tm?ir=J#rHS!jIKeJ`PA5tmb?v=hbN2~6Gu*#E|fMrufVy# zOsYhgASDRvWUR*c7`&&w4FG()g&?HWr-t*ySaTz8%qI>k+gY%Sr)`B~Z|RhpUSmyu zl-FcR(a|uwKBRlpjweP`r>K!X`GSoxJ4w6gU}UF5gc z5pWSxJy@NW&k{7OJhAE#FsGX=PA=pcFHkASjkp&zQuFuI)!`C&F9Z%oAk^77=ii+z zeoe+(<9di%u!IZ}Qg>n+N~1;1frr1W=%^VEueDo(v{{ruShsVHALFkzsYvn3-c?Vo zVOfv*&mM&=^tyx}r?Gn!#(?bzhV5?m^oxtgHeMZo$;qLqkkb;!`m5hdIIDu)ImC@;Q?r2(^gh3@pri<}JJpF`-3wu;H#rH$6Yj zB%7HCX1Jl=wUNYhc-AlnUS9_YB`3$*>W$WSMd~uURTjx)YY|xw>}}el!=q5+(|HS6 zXTcMUei{1#D?%=!F`kWWD2r#_VR^B*iQqG1x-NBNDRK~=aER8nH*ykfa$!a6jcM`_fRbn9QZ&Tu1(C zo0&9ygf@^g#kk6bnI`NCo2^_D-9l)sMr@GF@N=H>;v)6teF-r(34ppXDo%U=rKfVU zzG3IS{wzX-kGb3>K{S-)Kmk}=U-HCZcZ76Tq_n=5(H|Ic*_V;V+_m0mdgmss>}`!*7-Z#iOYT-hsk=yI(u7O9JF^JF$~dayBwo_M?$pT6Ow zrZZ=0nyk;Ws|NUF@GkQ?pFZCyj6h3&br~P^K0hGYdhj69v-|q;@Dgx}?U}`M57W}7 zQVxscHXiz@OSY-isr{M#OMPlJgT-)^T8vJwyp1Ndor3AF|K0>)qpn~NSD$l*5roxI z#%2-35=i;diC@_Z*L#*X-cv}W)UUnj<0^aCupM!j8kImEW}*~k$kgzUzj>_`5>zw) z=-sjV7+z3edXPz*uiliN?NYCc4j0s1xS`SpX*{9yd}2o}U(IXmRz@W)S?30pa>Jo3 zLNxYK4UDvF2kJOhGqpZz@$vvn^u2vcC)8oQdG4gb^CxS%HhjFxeEzXIuq#$6mSe`7 zJ&8ZFVf$?YAeX`M!Vd}&%vg&QHy>9~v9~v_^P1e<#KFPDG1Rb_5o(xj zXg-oULcuYxc#>q*#LIQy41A3I^Nn;eQ^lGl9XrIX+~w=@SU+vptWr^yyN;*qFWTcU zKj+_yM|DW^PTf2X_qjm!Ta?#035E~yCvBgWMlQ;-z0IigzSprxfL`w~d?bBK?%<_@ zQ<(LfSO`iX@nC;{`UeRyYC@9ih%pjRx&9*f_1J?BZ-;~WV)HkzAZp#~(d}Q=`{%+* zfP}1UM~(HVjzs(gsc@R6q6xt%LV;jHRu&b)v+MH7xr$NCF{S~k1jV)etOG~pfn4eo zBtQlrLHDSZP4%|&5PVxBd$#UCyAef!%#mVwJAm?MR0hi!Lq?Y@u=>gwNY{v8A~=xl|6H#F(RK6*=&v{vbq4u!r#sc@^a82*r zU@geWm~7lzj1und^Xnaq%O?JVA(M*b z=2TGCm8SDh%1{JHXRe?AfwuouV~5e_CUv9c*Zm}};@_V zJ+RL|yzc^2cBu9zU=rTW`k`RlD+Simsca_GVtH5z~fA$(rY~_BESQP;eko5n0sx?5t3LCjNn)GUhw}W z_#Gox+goc*vE?Or6$v^~YdKe@&6e;qT%D;)jVA5G#e3vYLW0M}cqj5F0_I&>!_6q4 z3-3m|2l)wf{&oz(#5{vb_MwE6V|A-a$5E^D54%mN{q;4cg4Lqfc(kLa*Tj%OuNhOL zhK&4O>r`6DVb^=b&C+kx-gJ|EqtjdMh&DlPy6>vKIdM%y@-0(-NI_JqE>oz%8J<>k z_Zo^39}3gyjwMrm5*}hD;k97Y?(a|%eB6x-ajOHGNd$@LiC-@nNSl*2S}XObBS>wn z7#OA^qlqG`=p}9+;vPMU(p$;R-*eUFR15`Mr62UmR7Mui*G>!3S&JSc?=HHgbd%{K zHL`JUNChFlC2F--BVcZA3qNyezmI*Ny+NkMz8Dm~{w8tg!Ft3<_x+iNEO?6z6LIgJ z+YS&pbAwthI$xxj(WsK?!|mY)FDPub=;Yyn&Nob_H|{4VnMGu@LJ7P4kjLom6|vr$ zY~1oO)1Qg)q0&>rpv{-LX33l!cUZ8&k=oxQZ+FyP!sYZ47FQV-r<*=whfpm-?(Y1S zIsh+pce^juZWB)0-d8OBWVt9}jK9ek%i_8+75lAAFc5H2mD#*_D-W^TUH_(zdRJax zmxK(vcoM5}GKV+?j2)eUMtOOE2?Yyj+A_~UuP|pkkLgd&tNt+}#`~l;X=*X1#T(tG zip}h^6QEkpRh~rKwzI~LaK2EaUGjQf%FZJQTV3Tabi2#*^+~MJ#psNlHyCgT{fN^k zpBc)A$JFw)kEw8A7LZG(4)=n9jX}QmnbS^GaV|-P@6E)nE49HhctR}JqqJ2JvMo=8z+7C~0&yT{J;GgexZ`1gyudY3wSHUeY zGn=THHF}Fbc`LgP<7vLp@r{S34ru<+Tgb*gG&6f-;`{ib`$L#Iz6=&6XglRNnDCA` zaQOSU7LM9xM}ir*DU#2-?1~!CO>Kl#3Azp%OJ(TI&v4UC5O4zs*vF%+W%tj zuDarCwnhObB*ERCph1JX1$TFMcb7(jyGw9_yIY`v#tH83?oQ)0hds`__xJsQb8*Jl zeX*|UVvSl|v#M6j^-PfPoci|}>26&Dzh+x6#37L2RW&0EKF40n;ejrihxc&s%h!^>GSi&+B?~) z+{EMcwj!0nph16b?`%5SaQ}9j;m&NaxS+2MDC28i)2^4g9$Bdn4xv+&6MuPJXqP%WP%=_OYShuv$fmIhmI=iVLt5ievU(t zAz3iF_r38{K%=kP$9eW>iOAt|yOKjH8B3e7(I(iSi=bAnNs%!%PLQQfjp$RZ6Q2BC zvGM9)0defuwK)cMm@{=ti0}edpH5~Q@+($ltM;08J&e*CPaCiujVv#^n;QIi(ahSx z$@|+EJKa}Lr-M>p`3d~`5nMzkpf6x*iwOIVMwA&S#tIea`{40wMz;MA)9H*7KcKNv zi!NRRk`F8G=C!_CR+9b4_r~tyPLAC?G4VdYeWY;DqyCm_Q0^}dTZaE;!zs-U&Kmu> zLKq1)100CMXOhGTo0)Te-hQVn&LngU(mERC7V)%EwWJJw_*VR}LE|X+N@y0Sv+?A- zneYo+Bx<11;y`6|W?e%4dxg+hdx3Czg$`Z^mLLDo&$&U+rxiDGrY&$&*-tKU^;02K4w=SJ z87}?a@}l_cnNgP}k^oh>kPGs6m|a5M*w#aVg!6mch%xi%L`iXS+0^_=o{!ANz6Hz0 zQye*l>eW#(9I-7orok4u3%}EPa99AoZ z0MTD=?5A-S`cEYof*Lo^>;mL|n}BYs2e+|_HaN@HM7j!+>E7CCbhr7SkneZ4m-MKwJlEL zMzV=Z$;@*-$V-ZXIZ0hFDRUwDnvv*^Of^7Aq z*(ToO_us{WgLjVNmC=XYa^h1Ifn!9Qh&;)mO3{P%dE)iZt-*n-R*Zi~o7q!+)wlXT zO>M5$c@Hi%L6ZTO_R9@o>ql$f&}HE3GP*jRs7|;|$1h*{5ExMz+r>l``Tl$sI=3MJUI0#(!I~Ew zLl-I4XUj!L(1`v|YZh;8UO9XUi5~U4N#7I_nxPn_|cQKROOs+Nt;5jGecdhkZKcg*Xm802`OhCr9*+`$xyySl{42hZ5GH%U*L53*82!iTktBE$NJpMc zOxA=5EI@v zQeqGY@wtoEpPX+}XZj5F=J@d<@0O%9tJd>X?Wfh7_rPoou!cX!a}XgDyWiH=5HbR@ zM#6QU+m~l&J55G^UcE#<2P9rJa$E)hX>NGk@yV>li&K_z*92`E(4?rUA1hXP8}WJ* zCUSk~UNc)cW7n^Y^o3Husg0z&)gl8)xqd4Ty=O@*pw;*|N<=BMVZU34Fzh1!%iaFw zm+J#S#se#!)HTfofF}q#zYXWS9k~AL_~9HXTw&|C*Ae37LywdD-dJ6Z#U1IjI{lA0R=LQ z@eP?M64lPC%zDZ#XWQ;66kjDUAYal=&W${hT{-!8QE}ONz~V$T%nODoS<45Re=C8KzgUE0-U zeL;@=FGxBQG}Ie*z}wU$>25|=(G9sSokXs;*M$V2bqzfvDOaEkU5Cki@BN@-{4^9H z5$v#3{kAe9MIrC!_G9_|CMZX!T{(6Y+Vn$+>X4i9SIxU2`fJ;uj0Mi9T2Bw5>CjWbXd~a z=Cv`J7iqYVI$Lb7>?w3hZ3R)rprZHT;|?!rYb&o!5Fqho2ESqz#ddfRI=u{vn00tB z8+b3}FR1IUV+rW(@rQw4+76JGk}i(30(Nd?lCXWdJEx*xBk+G?CMh`we(l9TH<|o^ zCy%NG#|)?5AbkMj66n1vKp0s6)0On2PyceeZEzqti%B=d?xmGtFLAO;p}l@zvIR#Q zP%It|nfx{b30kp>Hl4LYD29c2J}%wMKls8Rg+v4F&qOmlByO;gn0}a!16+2pL>FGM z#ou~IaQI!KU4FwW(mN_M#)f5oi4al&t+5mtpNqV`_WBQfT39J53!ECc7Zhx|nWs>D z>#n+4KionJ^5yX;Jw^O7&f*&1S7hMVA75#D7j^oyUtF*fcV2aB+j`;!k(~xtk+hpG zdH7#n@A%yR1XNQ#U>5sQ(nU(?qPY#yNp+W;Ch0nF{;VL8C*~9;rZpb5D?avR-|(L; zcI(iCibKr3b+kU`YR#(SB(i()va^GG9%c__n(C+fwrWYOdzm;4BANw?ZWl%wJZvUh zJ*Ud*pLdvp&7p}=2VycF7iTjW<)c!T+2_o7K`-88#qf7f>bNfbS?O`BTx1M{O&Yte z59=itXSPm65w&SYjAy!*p3bsAQE4yvcvP4l!-403{dI&;G2`QHk=*ZZhc|o*I6Ysr zs_QN4^!;SWq&nVGfZu|-{8ut@Zsimfy9kQqiro2b8|LL}fX1hTo(Jop-vloE6Dt`> z06v-TW4-d_bvYlPuW7LB&sEY0mOJIMa@$H+#=|)f_fcIF=Y;V5;ukVv)-Wq+&h?v{ z6Pjo;O!FDduHj>o3~Xx1RYfypTyMHX1}zjkuh!O*0QU2L`Cn?;N5e`Hre-YRX~jFC z3Cj}yN=!fM#za&_|9gNe0HmqOa=AFA(K=EhE0Nbr@NirkA#ks+X?C?6#tD61r88RY z^t-C>_lg~_?9$6gLX$>;K;gLD;L12ynGp5cc$T)@rr?~Y8#hGqX@?inHA_fJK$d@SmsSX}=iy(QdKkQ6 z4|_hNE)iBnB5o_5Jpb3YB>s+upGwHA>-;h<6IjSJ;!i%#r5BWz1x@|owUYhZuf1UB z*6{ctnfmW$+ASUGN>xUta7I=vVb77OnC@pIOE@#{(qKvpDk;vWtgj>9)%fbL^&1A3 zg+Q?L=h=bAJGy)C2d(Y;s2wXm&-JS^t_@$R_x=fo-w{gD?Ml)EI@i@IrAY(+p zjkc#cK`__2dlCg9W)O8ruerkyAmcTo5fKvw}1+BYHr&=_~_C^Cv(u_-WWTb zyp^Sx&p21Q{J{-8+w+iy!<#7x&|;D0-T9}nLfk#P1apIU@8Kk=Y{7rfkMJ}yOu!!# zaG%q}%Zr*HBiEg&&h)4@F+O6q4(UG{Xrofn8BKfMH{dqh$VdTtw~`o7&#t!05dTs0 zvYiSFS+H-{xtXmm;P#k!JAK6iw6tICr>@Nj9GJH4&F~icN%sj{f(dZ!a(piq&P>lw z1@$rb09Pyi^_=un>t`sNyi~CSPA5Svf<7j$LjXoO9fLv)IoG}A3XZBAb(fopnvRDV zH;K-d!@(NAic=q>76pG<+P9`wlw4u~Lf)n^F7SQ+(z-xKEIElj`_2J&on*AF`UJwB zuLZv6qSbq{37?VlCVGGhz>ALpy*eYS@OdYSyu%s^p0C11(hZ-Thg?{+Fy?xxU?NYL z+MD>xIIZh%BpAUZpYVJ=kB=GUvUx!ylG4!w3iz!pUwm+~Yd0SApL%HfXB2pF+UHf$ zwp0|8@p<>Q`p`ruoJmdC7uA3=8+s>@YgFRu_EU&Z>yAi|S(=`M6@!MIj+z?!>D2r5 z{A`JefFdLJqn}Rt?P0_XigpUi+d5T2wv(d!VWo2V^%PL%rY0vk`u_0rltK1@NyZ-3 zBaiV6f1}<7g&N{oLkNX}nI1@U(TuUZahvD#S4dRq>78zuvC#q_`wjZZxRwV9RKsfb zwnd_6cKK(7q?7q{Q)Z%idDJgz%T%8~;rhG*#h}xELw%zL)njgB2Q|Uxq=S6wZXws_ znUN^y?m&eGB*=V|y31fwUiMY1v;=(Y)Y+3}gDnW!C40qDaFEmW`c_b}TDQ>XJv4mr z&F5)$dIO$+?bGU<;vC7^{b30%^JzDZK84&oT*Lk2dV}VjRX58*@c6Y#&ZDv%z#vVD zc5v_4ch74U1%`fre4_&Bp*(;WC@!7wQj;74d@QFERm|3ugeo@Oj+sb%Th9c(M@3GiCw=R*q%Uc#s2-Ws6R370b$uM zzFxj&jY<2Iy1h{lucW6`Ee?w?R?mdEuC1kUzJ&DpYrr271&d`o`Gn-*K_Fc=wCcgq z2jDxlwUfui@??b)`uQ>GsKGr|@pO>yC^o~chGG5n><~|3 z4QMVs?=2a8Tvs=7_0qkKsnVfuSa;_ zDE?d@9~>Y{G(t8@TJ*k+=Df}O`|0sNKAIp0)0EJ!vZQA-QlUr2uf6+iw+CmQl8ECS zJY%)bO*1*Z1V;7AWeOXBi^6Ux>42Hpyz=Ge>DS!T6FzqgG#q}4>Lc6vv#;M~fWytW%-cmyJrvxF0>I5`5$iuW#@Heai2 z$@RAsF+t(|U=jB-_{}4r0iI|P1$k!uk0K@&))dk4_}!x+ooK{5kbh4P8cplCBuMTc zS0YsJxn~k>j6G9dwI!WdDwu8B%AEwSD3U;ZSC`CwqptaPdrCsIT)I~ji+Es)D{t7t z!oq<`boXp!x414ohD&W-e+_p4E}J-MX*rFhtIibI&P$CZNjwa$8Mx7b<7rfyc#p0B z_`Mg^>gJO$+NVK-WK`@(s@OP6cOa-JBqZi(qJKK^zJ>VE4|Q<2pq`~JQtg?~fbusx zz+H{6D+-r~B|@OJ^XqA|n9f1u{Df`?V{WJMt|6s*)z*YED}R;7 zWUH|e9!f`@qrLod=wNE)Fu~*Z=RnKe2=GYpzMNj&aX<|RlKGts}T-Uj&17fQD2!GnTn9@aByLpRv#k5|vvI>o*hepdw);BHDz@e7^% zpovVzwT8TmyB9au;?k)3UDls`R6_h6erEYcc4>toulIi=o3?MG8h3OYWWFo9=YM_V z6Wo|7T=x;i#g817a5Z~`USR*(Ihus!Ep^@JITI^^;O9g{@Hbu)KRb?*2(UkRZ!-gF zER=2gdUk|VSK{xm3aV;qzv=dq#l;I!sYnQTB39-jtJ zR5t7y!*tSL>GyBfeZu&>|FVrO{}L-E8kTf7uwRYT*T~tj6drsCi};vD?u8SY6d*xM z&U_QW0ratT4OYg*FLHjhI2eLfMPQp|p5x5@KfdMzJ z>cGwT-9BrzBu@Y=x)E|2@wgngGe^vgofam%RGYn~#I3B+)gDbMuABRLaV$LUntB}x z8CDc$myUmyzAPNb8Eo`9-HZm7KSDCM0TvsckD;8+D`69VSev|Oo(Fy7AoHR5(z9S+ zG57u96~T2^UPnpfw%iJkD$zMN zympcfN6;hJaxh8e&Yk`HW?t|2OhX24_rmemhx94)#^!+}6OJ0ER zVT}m5Cz;biF!_0FipUO8czj<3#O@)ltx1OemRllmAiDichcAZj?phD%w{pPm`bASp z^63B}OXcuuG#-oHKw4?}ClUt@w=)Od_Gyl>nu`{J2g~v8tF{;ZszLWg>;(_~qOCd+ z5m+@Drz(rM(;d8AA-DPK&X^R2J09bB2y)#x@jjseT zeV?DPzP^b}cdd<#up@m;!&ZMhVcS-0p@JhRK1Pm9yiMqR>hzc@E>f zvRoKS#lF0h%r}olZd5d8+ozqGVtL6}r2gnL^@z_l=pJvET3v2Ief|kuOJT_Q!CUvE zxZfEbTwWEx**M+@R!vrOcRd;?3&=nU^Vz+j`wgDb^OKnq!G#RmzB@18k&(rl+S2HT zGtn7^Zy9eH$>80W=n8qlr@G+g6;v83pB%V8QDZ`_vg#W9`=!9is&exZSH-6fFBA48 zBo=H{+9gcJhFqbG&rPTO3J+b9XhkYs0{Fhg2tpN2*5W*dKXD798OC6ZNR^a~#S7*p zsU_#r$NTv^I(iQ$?G3KaBQI!ye3xF-FBE;%+GoZVxEzA6N!&&8M#ys*>UT2dj71-4-ns?u|pCm2;F41G$ zmpvuK%Jb`wM@n0Vw|`>uG3M-SK``ee#F@Bze0OJ+bKll9)=j6^DlX!lo%$mM`CbKv zNKrtXF)E47^ZRx_h@6f^nm5F{GSG96_xc)?VR_M5wlQBG2e)>{>17R zO%s)LE>14B?#_lzw0Fn`rcgq!J{*^qNO%GW-Q{?1UKh;WV_HK?Gd%2bK*hm!!p1zV z`!+cHAm%)P%xcSzHH!68mnVSX;4icx+}0Qme1C*j+}_UcJ>hv$4oCNbUOL!Ze8 zp_3aCoDY9bbD@}m&g)BH@Qa6f)v|_JUo!bYbt~O8@RsQkMCO>9_X#`EXXe)S@u=gZ zg~Kl*8IE+5k>_bW;U^dg>r2TJ>WI}lif^+l&5zj&YC^D8(od(eZOnE{LHNm|UB zO?~G`c2k1@5Tn40N0FR}du)AjDj-J^$GVKMY)<9;gFZIW?if2mGEsc(Oq@kGf3ixFzrjbQ@qX{1 zemS{VvQLDK`*R!VT6|c6acau{UHrF~m9)bAc8kI&@>B!-7JV5O<|8vJ-C^xNKF-goHSK-&GSo@y`DO>i_#&}|}{gq?R}IEv_3 z91&+cSO0qL57EnT`Q$XRpVb z5f~C?MsAsLctuL^Wfg=btQBtVZx{k4!b-v9%vn_EG^7L6=%+`}vKwD-*DEL`8bQ~A z9B6ojltVRHmsvBS!*u2$D+<;G+Jwo9$t;?p%VXjTH=AwzZi6Le2CqKQU^N>8Dk+dU z=?_0ncf6!Bv6wymOg{C3x==|)F%v%qi}Z2sj|g#X=l1FQY1OS`QZhwB4wAA(h=M6o zu*`E3O?NWT(XwTB?%)B`La_r1rJ{{L0FL^FH|47?)7FF09wu>{iVBQiYI+(aQVQdq z5ZtnGc?C{wa5;X;K@J!9E<}U4lM!w0YgOk{im`{4_Uq1ZjEX=eWE^_wn*kAxWuv6= z%mD4kLZzUY<%7wZ!UE`KH%WoJ^R*+@m&*m>**g(;cJAE{l{8TNtq4%lZfT;DGT^&J z;Ha+(9#D{9ue%LcD36$s&&$HQ*AJ2JtSt-W5C1%5e9;G^SL3pd3Bj7IDxqViR*UCj zTWJK}jgFnKeqEzjQ`!6+5E7eycQi(BEMK*D#a&;b-(h|BDcuanfq^sn^g#KjGNN(n z3Q?Fdd9*jaL7!;sTMEMG+Y%EuktAT1+=H0!x*a(Aa1E8B2dn%zN!OAL zq3XhMpDy#+p8`1#nrAeasqQhbZ3T83GcEZznc96cU<)LQB@j;8weJ>3O7E&(-G`r+5K>n_kFDq=d}hn1qzoHtZ#V#* zSZiu}7x_;6(dXP@zC&{N63TPPtNH1FOMzu za?I5o>Nh?lxhw1pXx)t$4+dj3saRg0JgZ=nI9_O}R=N78Z1GU=>#j7_7#T++s_l=< z!J>30j7{lRz4~hPGcTMx;Rbo{u!^NF4JG09g;%C#*du{Y)zXa?c$?~6w(Skj4-^TI z+vk)W1%zQp3Gcnz_p+v|8XY&GN=dmBrk(b7l)?jR3Ap;& zO{w^xJIJ%@@ia@uSOibhC&aU$wlv$iBk=dDbd)#d3HmsCb z33FY81fy^5X^VH_xH0%@#F6nO8 zpGL*k>AT>z0(u5#GDzGU%jHlf_%03fg*p5!Ujt8$E(NRpNCJ2?s2&bPm_B^7l3Y^xJ5=e4iZ4w!utIs8E#7TtOj`z8x=@|2t!(2Dah zu(g=fs2MliuhMEXK5+vIycs{I3ewK4SJA2#7D~A4&$*V>JFE&{2MndBZ$WIj(zK7Q z+4Kkk%U_Q=qkPtjr_{b|pcc1pE)#^J&ga`9?EBqGuvRttU$>VkFBqM-W%QUpm&mAo zQ`K`KHZT*Ko*21O5q6#M6DH+Q^!yyxO_ca;YLebZ0ZMu=t z7)eNB9G%(_d|Pk52xC32$2c+fub&5de#y`1^zl|(?6JCkFQ+qO1T}0sT~2#92R`02 z5Sk~bhgd=B9UR_tm>#?qtnBvpY6y_8^4#?0sK4z5Ze(}~vJmXr%?Q=M96QI{UgGJ> z5_@m)2UTYg5=&(wv@E<{d#U)M)HGgxC-u;e-^PwNc!C6Q&zF=4UIE|C5@BHD+8)Nn zTpHp)9upYA6KWRchX)!?eZHff+uK4%?t9*pXmCxx3-QB%8z&5@)%j_zIu+!4^th>t z+(482MzHSKVB^iP-=buNC$2)YFXVpwL3AoYcb{7f`G)J(f{;z zsU>y+1AQR;KdH`+y_bjtBSM{my4j*7v(;E>;t5=0Aoax0IM#j^csnbZ*96&ZA@KqF z$wVkDpy{T&4f(p*x9&`Hm8t2dX?MP^dCZ@38cK2c&YfYaF|i)hnKfLj|ISaiJZ=H` z*f=l9)C6H&``L`KC%CRga~5-SHhWsM8jO4sPE1oOFp-Q3iEQ;5ij1nj7T|KwanKLw z?+kxB8M)hF0k5#+VtAuS5N!0Hy*@hcz^wZd`UlCIsOXq)7NVTuw_5_h}ahh-Ek@K+m;pD;15|DK-`Y-zV|#WFQ3h zd>u-$S=hcNdYKT1lk%Cdt6U~)E9&p%D?0Tq{Q)l~l=w3DcJej~Z*|_|IZbdP0|s0^ zt4zm^0xMYqv-an0Vs6kjTZ^EVh?99&a4f8Pa&z<3d;X%&ZbMky=ZsY}_a%Rvb*wN} zd6(CV-0G#J%s<^KC^Sl`SY%yV-xw4a$ajpZO`|%#x~yc&oJ_{eC-Yt6O$$W z_T!3Ei}h`INCA*XRzOiXb4XDTId^Oug$3KcZ^BtlH|48zYEK;%3a`qOuOB?IQ|=DP zQfF*PP$1#&H6SLEbV&z{<{Q)4R}n#&z$vUv=Fq@64V;HPPjPC!y;P6Y8cfF$?x^(~ zr9R|b{rh^0`f7uLDSfrZf^y5jmUT9rxiagEaEq?>_01)IcPhuIIXxoy<=*@642lZcJrtdNJ3w?8oJFCc)OvxBa%6Ic%*ZP z`3T*&Q-fDg3VHQdFroQ){nkv;9d6-&^zm8?jJ;&3n4%<=y~V%qy%{rJ4_v>ddN#A@0O^@|E{}0>%i_~p7B2o z50!pFg`!vq3@t^d(UfBVD!IGhx0X04?B2d%zMcu#Ed{R}tv4>? zC?h?SIn=PSJn)mih6&`phOF}Azgzy;fQtvXAFv|6XXHIT+eG%UpqVf)K|rA2^Bxs2 z?Efj)a$EIbXaMP4aKMV4hoAfs;onU52PoZ-hg3uC|4a1Nf9JV9t-k)p+Wz@YYkyk! zUrx5?I}@~76!1Tw_rLsZ-T#GgZ@yF80l%PG{zJL`dGB$@|0RC^{FmpgkN@{S&t&tR z-tPK;eiNp$^L<&SU+ia^=G8P7Dn_Sgv)BMvThbl(I;*R~|HF+>8hrU1ZP`1N9Tt84 zFq@a&ie~~lP)^G=`O!(le~t8A4*kC%@z*~eS}i;TdYS%eXM;e+lJpqN*hw+)`q0k* zH2CoO>A(H{2kO85{@?%mf8)%55hjVbX8J*D9fA8@FoM1@qZ zvJ=`_F+xU6Lgq8DE#g$jf`)V=d4D)3JvqQr$pQ_3B(i<@U~0LMRj}7EV|d{P#RF5;wB>-Z<$oB>?T86+A5c&yb?vl0n$4!aq_bEK>pB zj-^w=lM=(-Q}i#=(UU@=iAeCbxdWJy0sRrpJSG|uKSfY^9%`9l2ZWnKe@N02acEAe zmpZuoalsq567CT)>7{fl?@>o-Eup4LiNhtqKhC?SAWz1^m|^ch)*_gNk+$Ox13-!l z5&zkTa6zTH{$`>3X!CtOudfyj?>2%gihU9e8YdC`BOtPe-n~H*AIHqSo?#nt=Qlkp z>9V??@P<5j_lN%Q&%@SxqhU$!RdE>MOqdtxVIxdQp(>h+#`f*Z4Zrok3ja(B;0a?Y zjx7MbUdW;*ZejEiK2}kl1#lEx@EJX{!y4w*<$ZFJ(F*E_hD~*;Y+0V+%`Y7*cZo^4T8JIY8Q?wQ=1Tmr5QqhU_+*TLFo}aauJSsk!u;uWXu`wg&<6XS9Q42T=^XUC) z(c6=QR~0iTGGckG-P&&62@r;1ra{|GPVP!5;Iwg*kD|bo2-^`R+pgDCU`JH*uWyn) zz(cx?qUrfgNQ2?lk6ay5592QGo=u4x1es(mSON-J=`7{#!v`BVKmJAA{?U|UgfEXQ z2`3keHYA3-^IO*2MlQXx3P(;s$Q;A8;4=*t1)XF_pO&4OF1B zsdfE5nI&?4^jm}0^uy(-W@XV2y>fxDKOaeVYm*7}XVL>jOn-ND`Utd^y`K;<{rDF2shG<2z9Cb9fkt;`MeqN67RbNbYL z6IEmLhuXUQLwr50?6DTSpDa9t@M6w|MYS>F!csnn_TwItuwE0|`P5QrOS=0Di{mz1 ztw!&qzIolCfrt(v2b8_*tA2};Ec-VkoL?zo6KM(e^D)lVVPxsY{-Q5?ALn*ix;=Y5 zf5z!(GwSBj+fG|6q1h&U5l-xFKv%RWsuosYhGLP8;N|mWAJ3Zy)f{Tsn#q!l;a92h zj5cANK@*@4iNSw-(t%n7#TC`;2S*eh1qIJ*yDf1KAe|B^FI(rMiBms7c`YyLRwCgM zBxKMk|IQIW?fM*88XTYexh}cO_N=4o@>Ck!?4A}Y@qzry6pc~Y6XSvSL~&_) z(DodUt(zXf)dWbwLz4+cO6bjlKYML8=8owy^rk*g;q#HQbh~Y3vvE}4@C>XMK2|JB z-_NoQH{g2qeKnI#{z2CBg{wZ(c1*3Ue4UWl$;c;ga(Z^&*l`1ov?zG-hz+{ceemxt zvr^ed9w{<57LIY8n2rKdM&3nrlEP;D85rVz@-R{4D4}v;w}@qz{JwCeFA;3Z)80`h zp7sk?d?8<^Pjy2YJI@@=4y26|dX4Kv!=h} zF9SardTQz({^oKj#k8PcXz4(~(71 zOKC3ub^kT%-7&ElW%7!iRlausP3+`7<-Vl!(GhNu=4?xJF1DP5b1xy zg-oGo0$R1nRm}H9i=cDhNr#o4r%Q1O(J|y3aV@345;-NXOs^?9beYj)_hYvHqQed2 zek%l~q#6;BOF}7iST*qjCqJqbi^YbUXw$i+P`!_57E95>%j9y|5opeQ>i#r(xEcF$ zI`%YM#P1YRQQyiPxwI?XsyK!t!s7$-f>?WK8p?$yz7FWS5x=8I5zvi zOMQoXP>V^@oPdi)^xG=gpP5Ov95Y0@xMxNkhOB2>M5R9YuJ`yUXIZ|;DbGn*$17@P zrsE@m8hpZf*WLX!$BFBTc65V4P190lRMON@r0N&7jlMo)Q(E~y;~CXtxUr-e?pe>2 zxZ279uBc?F}|NR1k{wc8+lr~ zr$(2}>}uR&a(Eo7{4U8d;^-7=qe`(*+5>BZPol;TL$h4;sm z17k4FG@0}E)t$C)K&r8%l60yrO>gbpx;u$e=@>q(&yUYa8<~`dwUhC;9iC-8^6%u zsWOvLOUW5}^}}0^Y}J?S;v#?t4?7LBxz%{z=c*lkO{D5a8^BkqJJBxDJLxJ?8j_mU zt{MtONSZD*1)e+R4Bdo>SJ?>Rm8j3*O>*%>OX3k#!;&*~B*hTX%ZL5UL))q4lI>*7 zH?+)5Z#F_a(1y$V8nu-{Y0}hUwFtS?CNIuH#m0+W;B*IVA8T{mWs%{cGB7PzRbG7> zh>$Ibk`t$hjOas}8s#nSWCZS6!HKE!Ud8+2MtX=RkIyPMWIFn$J9QXr`g-^zs z!G=!xaqPxc92a1h5DPDH<`;*;v@9)$S0+n9KvcoO$&sPmbY3JJKHs>M)}07jH)Si7 zrdf*0+III{iI^G^)5D>4V<%04x&7B@&dLs2BffCKTO$F~^HnF^Kdz^*5Z8i-$5SCI zdTD-o;QRv*Ms<7q+DH71#Xmfh36sf7mGSD5kS( zT2?CJu-_fu;nj#=cKpg+C>p|@Gfx?~VA@hc=KGo8OvA$7(82+Hy<*Ix;ErXRYsOM- zmEQ8C4_bXKYg6myl;%aG!u*b7816xEg%#rZzSMBRy7a@z%wrHg2Xz9yW%r2(2^Vq|By7_A z#rUK1G9Hw~{ErEq>+hBR;_>~5^Q-~Bt|`LK9|3bDr;a0B$JeS1m|`KCwo66@5(#Fs z&L52E&id)vm=dF-5u~J2hiPQCnC<8YIG2u(33n2E^OqZmq{Z$O>k9OBmIi>YFB;Wh z%)oKgy_tp{{-&8J#Zv2p@O)#ltKI?_H`gqIwLBNSgq3CbWi~NYiY3JhOlmRb<@rr( zvSW_Sb>X^S)IOPsQ>rIA4OrY5eQg{vHc_ojYfCOB-Yt8Vc>RH=N{G+W)rJJKDkvr; zm6(ZjHD~$EtxTAMU+|cJnR(VTBR0ef2ipqm?`2>kpb)TsIHA_awm;qDxw8Je0yz5G zo?|93V~}N+E6ftbg0j_+aME>k{h3m9QZq-oXGj^SvM+R1Hi_d1HYc(H_|gNzBJZi&1G z%c9XDjjYnDqzaJzq5?4cC_*UuWlS4IEb{OMjBZ80?~CiXsqStnDM|il!7Qg;(QPpd z9PMPR&%%D6xT;!}GT7A<5=?njx7rGfS~x1wM{y$>yD8#O$+B}w5^@o+!UYbC3&-|* zdgqs6rDPk~y#DyacYC&V{XT0eK`reW$2u6=S}DNIoEbJ`!3(hR=1X90+(6#6rM0cO zZ>{Q`bhRMI+h|`!uxw;w-VVkzi&Ro6+RF+3WDCk+gTfRN^7@2zD26Y`YehlkWdG^I z1$rCA$g^3BasO}gl;LTN$^=8xdP+dq``LmwHtJxgLHzey;4Lx!8dwQ#IPuK_aF3ui zu%tM-zG*c(*f%|D`^L%yfG#A${f_z7^*#AvdH3*l5xm(!2lSnulr*If0TC5GGlxVN zQB(wal+I`l0XFd$+4t`YWZgowQz{yWV|6TdTQ*N!^0FC)H;HR-o3FC_zQB52_~$E^L$&&u!uN9A-|V2xgkMhCjCR-2Pdor-MSetfXNJ zOHvSTzmW4ck?Jq<^v` zx&GyV;BeKhN4S;+s{}0YcYQ@Gdre)-k_|ED@84Joc<*uEPJT zhU@0(XL>g~|5>ld>sz-=Q-D8SSZjsg2XU1g0+@9;CZjaP&_m)(LR4Y?W*$T1^dINk zDXga=T9{_63mn5PiM_0cvFaF!{lOF?WyLDycZKpR$K~`&c#%rvpTT1sN!i_!!hxqT zcJiT4?zl>-jeMQGg$>NOG^o!Zvf*`H(U{C%xkba4PH{|0Q-^)%rZjb` znV)_c#)_u|!PVpHM>Y_B?G+BCObVrSTTnSdEUISyTpCxek!yV-`^&@3M7WijC9#$* z4IUwUs~5{eJfXh?i8d;%-e#DUq$gOa7c&AKQ-ljS6;OS|L}slRK59LOXZ0;es@E(6 z-n8`ec=E@I91^sav^Z{$EoFwj9&>{pK9qg3VJ5l?RcOczhaJqGVLY|$*Pi&LDYsl| z#}NCVA3EHN#A56ku6@z)W5gi~?3zgGp{NL$Pyn?m2Fe`)f`(vmEr+!uQEo;6qG5aw#^Ng zJqK_wZ+^zi`|M~wt(YGFqcNMM@la zvJ)O7bf!H~)J_*=&Sch3CKQ;2>#TX2K`U2u!3v0OD4S1AV-S-wZ`v5@)p7V8@RNXY zvABB6|*|a+3AnhYsG=xf1LY(P>rDu2B7=s~LPi7Drg~ zl8~VuFs8(K!$4f4NWcm~E+Ppfh7wkEMYfqIyX;13{_D+gd0^hMGWj6rUK%sP>qc%)SZ z00>p5HgX4LNC+Z^47GsAf$SI-_F@sqQKZa~wTzX10>j`LzX_dgmI5M%R;0CX~k} z2d`qYX{rL=S&grrI%*OIY<{=79ozn9az$4OCeTS|A$B2ES;fP|3-1~Kw3U|pN*hu2 zxM#O$?-n|fuu*f}om2K|HORIRl_%QlrN|)SX zo4-rIjmuCbamIz`kHz=>tTpwSq!`4b+`2JSaZy++k&4$M0N-)99A5(`mum5h>S9AnZw!n@bx_i?{A?;{^OKm7>Uwetc;;bHp(W5ZioUla57 z+vUVMwpHDd5b%}U`s`Xz?q>5T;$4&3IeXrm{x#=U*^`~0?>hJ1%PD`+aPsM5y`=Y7 z{@2(2JGDFDe?2o_@4F9-^VD5#EKS<7RJXn&{>a5z%a`AN8UJh$)sw1my#9Rsox7^L zuHR&TT+yLZ+~RX6pe?L6Zs*m@k8ZG=I*a}{V~h%3c5ZXqi8FR@6KnVJ<@9R+mz4BX z#cqGF>-=*T(Rt;JZ7NT9?0wa;aqI7;mn!F*o>5r9kXkCioW|l`5~c0;bN{g$d22Oa z`fQx?j9p(wL0sCVqM#wgg9$PQ%qZ|;JyYkoj&q^>6^9p&d~Jj|a3#a8 y#seB~0Rx~w(@O_AxWGtK@g<$nSg0WU|9^(NFSkqEwPl(!0D-5gpUXO@geCy2g5C50 literal 0 HcmV?d00001 diff --git a/doc/indexing.md b/doc/indexing.md new file mode 100644 index 00000000..ec228bb2 --- /dev/null +++ b/doc/indexing.md @@ -0,0 +1,218 @@ + +# 索引优化建议 + +以下优化算法基于个人当前理解,能力有限,如有偏颇还请斧正。 + +## 简单查询索引优化 + +### 等值查询优化 + +* 单列等值查询,为该等值列加索引 +* 多列等值查询,每列求取散粒度,按从大到小排序取前N列添加到索引(N可配置) + +```sql +SELECT * FROM tbl WHERE a = 123; +SELECT * FROM tbl WHERE a = 123 AND b = 456; +SELECT * FROM tbl WHERE a IS NULL; +SELECT * FROM tbl WHERE a <=> 123; +SELECT * FROM tbl WHERE a IS TRUE; +SELECT * FROM tbl WHERE a IS FALSE; +SELECT * FROM tbl WHERE a IS NOT TRUE; +SELECT * FROM tbl WHERE a IS NOT FALSE; +SELECT * FROM tbl WHERE a IN ("xxx"); -- IN单值 +``` + +### 非等值查询优化 + +* 单列非等值查询,为该非等值列加索引 +* 多列非等值查询,每列求取散粒度,为散粒度最大的列加索引。 + +思考:对于多列非等值,为filtered最小列加索引可能比较好。因为输入可变,所以现在只按散粒度排序。对于高版本MySQL如果开启了Index Merge,考虑为非等值列加单列索引可能会比较好。 + +```sql +SELECT * FROM tbl WHERE a >= 123 -- <, <=, >=, >, !=, <> +SELECT * FROM tbl WHERE a BETWEEN 22 AND 44; -- NOT BETWEEN +SELECT * FROM tbl WHERE a LIKE 'blah%'; -- NOT LIKE +SELECT * FROM tbl WHERE a IS NOT NULL; +SELECT * FROM tbl WHERE a IN ("xxx"); -- IN多值 +``` + +### 等值 & 非等值组合查询优化 + +1. 先按`等值查询优化`为等值列添加索引 +2. 再将`非等值查询优化`的列追加在等值列索引后 + +```sql +SELECT * FROM tbl WHERE c = 9 AND a > 12 AND b > 345; -- INDEX(c, a)或INDEX(c, b) +``` + +### OR操作符 + +如果使用了OR操作符,即使OR两边是简单的查询条件也会对优化器带来很大的困难。一般对OR的优化需要依赖UNION ALL或Index Merge等多索引访问技术来实现。SOAR目前不会对使用OR操作符连接的字段进行索引优化。 + +### GROUP BY子句 + +GROUP BY相关字段能否加入索引列表需要依赖WHERE子句中的条件。当查询指定了WHERE条件,在满足WHERE子句只有等值查询时,可以对GROUP BY字段添加索引。当查询未指定WHERE条件,可以直接对GROUP BY字段添加索引。 + +* 按照GROPU BY的先后顺序添加索引 +* GROUP BY字段出现常量,数学运算或函数运算时会给出警告 + +### ORDER BY子句 + +ORDER BY相关字段能否加入索引列表需要依赖WHERE子句和GROUP BY子句中的条件。当查询指定了WHERE条件,在满足WHERE子句只有等值查询且无GROUP BY子句时,可以对ORDER BY字段添加索引。当查询未指定WHERE条件,在满足无GROUP BY子句时,可以对ORDER BY字段添加索引。 + +* 多个字段之间如果指定顺序相同,按照ORDER BY的先后顺序添加索引 +* 多个字段之间如果指定顺序不同,所有ORDER BY字段都不添加索引 +* ORDER BY字段出现常量,数学运算或函数运算时会给出警告 + +## 复杂查询索引优化 + +### JOIN索引优化算法 + +* LEFT JOIN为右表加索引 +* RIGHT JOIN为左表加索引 +* INNER JOIN两张表都加索引 +* NATURAL的处理方法参考前三条 +* STRAIGHT_JOIN为后面的表加索引 + +### SUBQUERY和UNION的复杂查询 + +对于使用了IN,EXIST等词的SUBQUERY或UNION类型的SQL,先将其拆成多条独立的SELECT语句。然后基于上面简单查询索引优化算法,对单条SELECT查询进行优化。SUBQUERY的连接列暂不考虑添加索引。 + + +```sql +SELECT * FROM film WHERE language_id = (SELECT language_id FROM language LIMIT 1); + +1. SELECT * FROM film; +2. SELECT language_id FROM language LIMIT 1; +``` + +```sql +SELECT * FROM city a LEFT JOIN country b ON a.country_id=b.country_id +UNION +SELECT * FROM city a RIGHT JOIN country b ON a.country_id=b.country_id; + +1. SELECT * FROM city a LEFT JOIN country b ON a.country_id=b.country_id; +2. SELECT * FROM city a RIGHT JOIN country b ON a.country_id=b.country_id; +``` + +## 无法使用索引的情况 + +如下类型的查询条件无法使用索引或SOAR无法给出正确的索引建议。 + +```sql +-- MySQL无法使用索引 +SELECT * FROM tbl WHERE a LIKE '%blah%'; +SELECT * FROM tbl WHERE a IN (SELECT...) +SELECT * FROM tbl WHERE DATE(dt) = 'xxx' +SELECT * FROM tbl WHERE LOWER(s) = 'xxx' +SELECT * FROM tbl WHERE CAST(s …) = 'xxx' +SELECT * FROM tbl where a NOT IN() +-- SOAR不支持的索引建议 +SELECT * FROM tbl WHERE a = 'xxx' COLLATE xxx -- vitess语法暂不支持 +SELECT * FROM tbl ORDER BY a ASC, b DESC -- 8.0+支持 +SELECT * FROM tbl WHERE `date` LIKE '2016-12%' -- 时间数据类型隐式类型转换 +``` + +## 索引长度限制 + +由于索引长度受数据库版本及不同配置参数影响,参考[InnoDB限制](https://dev.mysql.com/doc/refman/8.0/en/innodb-restrictions.html)。这里将索引长度限制定义为可配置值,用户可以根据实际情况进行设置。 + +* 通过-max-index-bytes配置每列索引最大长度,默认为767 Bytes +* 超过单列索引最大长度限制后程序会自动添加该列的前缀索引(max-index-bytes/CHARSET_Maxlen) +* 通过-max-index-bytes-percolumn配置多列索引加各最大长度,默认为3072 Bytes +* 超过多列索引最大长度限制后,由程序生成的ALTER语句会将每列前缀索引长度指定为N,用户自行调整 + +```sql +ALTER TABLE `sakila`.`film_text` add index `idx_description` (`description`(255)) ; + +``` + +## 更新语句转换为只读查询 + +SOAR支持将DELETE, UPDATE, INSERT, REPLACE四种类型语句转换为SELECT查询。对转换后的SELECT查询进行索引优化。以下为转换示例。 + +```sql +UPDATE film SET length = 10 WHERE language_id = 20; + +SELECT * FROM film WHERE language_id = 20; +``` + +```sql +DELETE FROM film WHERE length > 100; + +SELECT * FROM film WHERE length > 100; +``` + +```sql +INSERT INTO city (country_id) SELECT country_id FROM country; + +SELECT country_id FROM country; +``` + +```sql +REPLACE INTO city (country_id) SELECT country_id FROM country; + +SELECT country_id FROM country; +``` + +## 散粒度计算 + +### 计算公式 + +`Cardinality = ColumnDistinctCount/TableTotalRows * 100%` + +由于直接对线上表进行COUNT(DISTINCT)操作会影响数据库请求执行效率,因此默认各列的散粒度均为1。用户可以通过指定`-sampling`参数开启数据采样。SOAR会将线上数据随机采样至测试环境求取散粒度。 + +### 数据采样算法 + +以下说明摘抄自PostgreSQL数据直方图采样算法。默认k(-sampling-statistic-target)设置为100,即最多采样3万行记录。 + +```text + The following choice of minrows is based on the paper + "Random sampling for histogram construction: how much is enough?" + by Surajit Chaudhuri, Rajeev Motwani and Vivek Narasayya, in + Proceedings of ACM SIGMOD International Conference on Management + of Data, 1998, Pages 436-447. Their Corollary 1 to Theorem 5 + says that for table size n, histogram size k, maximum relative + error in bin size f, and error probability gamma, the minimum + random sample size is + r = 4 * k * ln(2*n/gamma) / f^2 + Taking f = 0.5, gamma = 0.01, n = 10^6 rows, we obtain + r = 305.82 * k + Note that because of the log function, the dependence on n is + quite weak; even at n = 10^12, a 300*k sample gives <= 0.66 + bin size error with probability 0.99. So there's no real need to + scale for n, which is a good thing because we don't necessarily + know it at this point. +``` + +### 随机采样 + +随机采样使用的SQL如下,其中变量`r`, `n`的含义见上面的说明。 + +```sql +SELECT * FROM `tbl` WHERE RAND() < r LIMIT n; +``` + +## 索引去重 + +### 检查步骤 +1. 为查询语句可能使用索引的字段添加索引 +2. 枚举用到的所有库表的已知索引 +3. 判断所有新加的索引是否与已知索引重复 +4. 判断所有新加的索引之间是否存在索引重复 + + +### 检查规则 + +* PRIMARY > UNIQUE > KEY +* 索引名称相同,即: idxA == idxA +* (a, b) > (a) +* (a, b), (b, a) 会给出警告,用户自行判断是否重复 + +## 不足 + +* 目前只支持针对InnoDB引擎添加索引建议,不支持FULLTEXT, SPATIAL等其他类型索引 +* 暂不支持索引覆盖(Covering) +* 暂不支持Index Merge情况下的索引建议 diff --git a/doc/install.md b/doc/install.md new file mode 100644 index 00000000..3c8e6b1c --- /dev/null +++ b/doc/install.md @@ -0,0 +1,52 @@ +## 下载二进制安装包 + +```bash +TODO: 开源后补充下载release版本链接 +wget https://github.com/XiaoMi/soar/archive/v0.7.0.zip +``` + +## 源码安装 + +### 依赖软件 + +一般依赖 + +* Go 1.10+ +* git + +高级依赖(仅面向开发人员) + +* [mysql](https://dev.mysql.com/doc/refman/8.0/en/mysql.html) 客户端版本需要与容器中MySQL版本相同,避免出现由于认证原因导致无法连接问题 +* [docker](https://docs.docker.com/engine/reference/commandline/cli/) MySQL Server测试容器管理 +* [govendor](https://github.com/kardianos/govendor) Go包管理 +* [retool](https://github.com/twitchtv/retool) 依赖外部代码质量静态检查工具二进制文件管理 + +### 生成二进制文件 + +```bash +TODO: 开源后可直接从github执行go get下载,未开源前需要git clone到指定路径 +go get github.com/XiaoMi/soar +cd ${GOPATH}/github.com/XiaoMi/soar && make +``` + +### 开发调试 + +如下指令如果您没有精力参与SOAR的开发可以跳过。 + +* make deps 依赖检查 +* make vitess 升级Vitess Parser依赖 +* make tidb 升级TiDB Parser依赖 +* make fmt 代码格式化,统一风格 +* make lint 代码质量检查 +* make docker 启动一个MySQL测试容器,可用于测试依赖元数据检查的功能或不同版本MySQL差异 +* make test 运行所有的测试用例 +* make cover 代码测试覆盖度检查 +* make doc 自动生成命令行参数中-list-XX相关文档 +* make daily 每日构建,时刻跟进Vitess, TiDB依赖变化 +* make release 生成Linux, Windoes, Mac发布版本 + +## 安装验证 + +```bash +echo 'select * from film' | ./soar +``` diff --git a/doc/install_en.md b/doc/install_en.md new file mode 100644 index 00000000..e6ea5b11 --- /dev/null +++ b/doc/install_en.md @@ -0,0 +1,19 @@ +## Get Released Binary + +```bash +TODO: +wget http://... +``` + +## Build From Source + +```bash +go get github.com/XiaoMi/soar +cd $GOPATH/github.com/XiaoMi/soar && make +``` + +## Simple Test Case + +```bash +echo 'select * from film' | ./soar +``` diff --git a/doc/js/pretty.js b/doc/js/pretty.js new file mode 100644 index 00000000..8abbe459 --- /dev/null +++ b/doc/js/pretty.js @@ -0,0 +1,1110 @@ +! function(e, E) { + "object" == typeof exports && "object" == typeof module ? module.exports = E() : "function" == typeof define && define.amd ? define([], E) : "object" == typeof exports ? exports.sqlFormatter = E() : e.sqlFormatter = E() +}(this, function() { + return function(e) { + function E(n) { + if (t[n]) return t[n].exports; + var r = t[n] = { + exports: {}, + id: n, + loaded: !1 + }; + return e[n].call(r.exports, r, r.exports, E), r.loaded = !0, r.exports + } + var t = {}; + return E.m = e, E.c = t, E.p = "", E(0) + }([function(e, E, t) { + "use strict"; + + function n(e) { + return e && e.__esModule ? e : { + "default": e + } + } + E.__esModule = !0; + var r = t(18), + T = n(r), + R = t(19), + o = n(R), + N = t(20), + A = n(N), + I = t(21), + O = n(I); + E["default"] = { + format: function(e, E) { + switch (E = E || {}, E.language) { + case "db2": + return new T["default"](E).format(e); + case "n1ql": + return new o["default"](E).format(e); + case "pl/sql": + return new A["default"](E).format(e); + case "sql": + case void 0: + return new O["default"](E).format(e); + default: + throw Error("Unsupported SQL dialect: " + E.language) + } + } + }, e.exports = E["default"] + }, function(e, E) { + "use strict"; + E.__esModule = !0, E["default"] = function(e, E) { + if (!(e instanceof E)) throw new TypeError("Cannot call a class as a function") + } + }, function(e, E, t) { + var n = t(39), + r = "object" == typeof self && self && self.Object === Object && self, + T = n || r || Function("return this")(); + e.exports = T + }, function(e, E, t) { + function n(e, E) { + var t = T(e, E); + return r(t) ? t : void 0 + } + var r = t(33), + T = t(41); + e.exports = n + }, function(e, E, t) { + "use strict"; + + function n(e) { + return e && e.__esModule ? e : { + "default": e + } + } + E.__esModule = !0; + var r = t(1), + T = n(r), + R = t(66), + o = n(R), + N = t(7), + A = n(N), + I = t(15), + O = n(I), + i = t(16), + S = n(i), + u = t(17), + L = n(u), + C = function() { + function e(E, t) { + (0, T["default"])(this, e), this.cfg = E || {}, this.indentation = new O["default"](this.cfg.indent), this.inlineBlock = new S["default"], this.params = new L["default"](this.cfg.params), this.tokenizer = t, this.previousReservedWord = {} + } + return e.prototype.format = function(e) { + var E = this.tokenizer.tokenize(e), + t = this.getFormattedQueryFromTokens(E); + return t.trim() + }, e.prototype.getFormattedQueryFromTokens = function(e) { + var E = this, + t = ""; + return e.forEach(function(n, r) { + n.type !== A["default"].WHITESPACE && (n.type === A["default"].LINE_COMMENT ? t = E.formatLineComment(n, t) : n.type === A["default"].BLOCK_COMMENT ? t = E.formatBlockComment(n, t) : n.type === A["default"].RESERVED_TOPLEVEL ? (t = E.formatToplevelReservedWord(n, t), E.previousReservedWord = n) : n.type === A["default"].RESERVED_NEWLINE ? (t = E.formatNewlineReservedWord(n, t), E.previousReservedWord = n) : n.type === A["default"].RESERVED ? (t = E.formatWithSpaces(n, t), E.previousReservedWord = n) : t = n.type === A["default"].OPEN_PAREN ? E.formatOpeningParentheses(e, r, t) : n.type === A["default"].CLOSE_PAREN ? E.formatClosingParentheses(n, t) : n.type === A["default"].PLACEHOLDER ? E.formatPlaceholder(n, t) : "," === n.value ? E.formatComma(n, t) : ":" === n.value ? E.formatWithSpaceAfter(n, t) : "." === n.value || ";" === n.value ? E.formatWithoutSpaces(n, t) : E.formatWithSpaces(n, t)) + }), t + }, e.prototype.formatLineComment = function(e, E) { + return this.addNewline(E + e.value) + }, e.prototype.formatBlockComment = function(e, E) { + return this.addNewline(this.addNewline(E) + this.indentComment(e.value)) + }, e.prototype.indentComment = function(e) { + return e.replace(/\n/g, "\n" + this.indentation.getIndent()) + }, e.prototype.formatToplevelReservedWord = function(e, E) { + return this.indentation.decreaseTopLevel(), E = this.addNewline(E), this.indentation.increaseToplevel(), E += this.equalizeWhitespace(e.value), this.addNewline(E) + }, e.prototype.formatNewlineReservedWord = function(e, E) { + return this.addNewline(E) + this.equalizeWhitespace(e.value) + " " + }, e.prototype.equalizeWhitespace = function(e) { + return e.replace(/\s+/g, " ") + }, e.prototype.formatOpeningParentheses = function(e, E, t) { + var n = e[E - 1]; + return n && n.type !== A["default"].WHITESPACE && n.type !== A["default"].OPEN_PAREN && (t = (0, o["default"])(t)), t += e[E].value, this.inlineBlock.beginIfPossible(e, E), this.inlineBlock.isActive() || (this.indentation.increaseBlockLevel(), t = this.addNewline(t)), t + }, e.prototype.formatClosingParentheses = function(e, E) { + return this.inlineBlock.isActive() ? (this.inlineBlock.end(), this.formatWithSpaceAfter(e, E)) : (this.indentation.decreaseBlockLevel(), this.formatWithSpaces(e, this.addNewline(E))) + }, e.prototype.formatPlaceholder = function(e, E) { + return E + this.params.get(e) + " " + }, e.prototype.formatComma = function(e, E) { + return E = (0, o["default"])(E) + e.value + " ", this.inlineBlock.isActive() ? E : /^LIMIT$/i.test(this.previousReservedWord.value) ? E : this.addNewline(E) + }, e.prototype.formatWithSpaceAfter = function(e, E) { + return (0, o["default"])(E) + e.value + " " + }, e.prototype.formatWithoutSpaces = function(e, E) { + return (0, o["default"])(E) + e.value + }, e.prototype.formatWithSpaces = function(e, E) { + return E + e.value + " " + }, e.prototype.addNewline = function(e) { + return (0, o["default"])(e) + "\n" + this.indentation.getIndent() + }, e + }(); + E["default"] = C, e.exports = E["default"] + }, function(e, E, t) { + "use strict"; + + function n(e) { + return e && e.__esModule ? e : { + "default": e + } + } + E.__esModule = !0; + var r = t(1), + T = n(r), + R = t(58), + o = n(R), + N = t(53), + A = n(N), + I = t(7), + O = n(I), + i = function() { + function e(E) { + (0, T["default"])(this, e), this.WHITESPACE_REGEX = /^(\s+)/, this.NUMBER_REGEX = /^((-\s*)?[0-9]+(\.[0-9]+)?|0x[0-9a-fA-F]+|0b[01]+)\b/, this.OPERATOR_REGEX = /^(!=|<>|==|<=|>=|!<|!>|\|\||::|->>|->|~~\*|~~|!~~\*|!~~|~\*|!~\*|!~|.)/, this.BLOCK_COMMENT_REGEX = /^(\/\*[^]*?(?:\*\/|$))/, this.LINE_COMMENT_REGEX = this.createLineCommentRegex(E.lineCommentTypes), this.RESERVED_TOPLEVEL_REGEX = this.createReservedWordRegex(E.reservedToplevelWords), this.RESERVED_NEWLINE_REGEX = this.createReservedWordRegex(E.reservedNewlineWords), this.RESERVED_PLAIN_REGEX = this.createReservedWordRegex(E.reservedWords), this.WORD_REGEX = this.createWordRegex(E.specialWordChars), this.STRING_REGEX = this.createStringRegex(E.stringTypes), this.OPEN_PAREN_REGEX = this.createParenRegex(E.openParens), this.CLOSE_PAREN_REGEX = this.createParenRegex(E.closeParens), this.INDEXED_PLACEHOLDER_REGEX = this.createPlaceholderRegex(E.indexedPlaceholderTypes, "[0-9]*"), this.IDENT_NAMED_PLACEHOLDER_REGEX = this.createPlaceholderRegex(E.namedPlaceholderTypes, "[a-zA-Z0-9._$]+"), this.STRING_NAMED_PLACEHOLDER_REGEX = this.createPlaceholderRegex(E.namedPlaceholderTypes, this.createStringPattern(E.stringTypes)) + } + return e.prototype.createLineCommentRegex = function(e) { + return RegExp("^((?:" + e.map(function(e) { + return (0, A["default"])(e) + }).join("|") + ").*?(?:\n|$))") + }, e.prototype.createReservedWordRegex = function(e) { + var E = e.join("|").replace(/ /g, "\\s+"); + return RegExp("^(" + E + ")\\b", "i") + }, e.prototype.createWordRegex = function() { + var e = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : []; + return RegExp("^([\\w" + e.join("") + "]+)") + }, e.prototype.createStringRegex = function(e) { + return RegExp("^(" + this.createStringPattern(e) + ")") + }, e.prototype.createStringPattern = function(e) { + var E = { + "``": "((`[^`]*($|`))+)", + "[]": "((\\[[^\\]]*($|\\]))(\\][^\\]]*($|\\]))*)", + '""': '(("[^"\\\\]*(?:\\\\.[^"\\\\]*)*("|$))+)', + "''": "(('[^'\\\\]*(?:\\\\.[^'\\\\]*)*('|$))+)", + "N''": "((N'[^N'\\\\]*(?:\\\\.[^N'\\\\]*)*('|$))+)" + }; + return e.map(function(e) { + return E[e] + }).join("|") + }, e.prototype.createParenRegex = function(e) { + var E = this; + return RegExp("^(" + e.map(function(e) { + return E.escapeParen(e) + }).join("|") + ")", "i") + }, e.prototype.escapeParen = function(e) { + return 1 === e.length ? (0, A["default"])(e) : "\\b" + e + "\\b" + }, e.prototype.createPlaceholderRegex = function(e, E) { + if ((0, o["default"])(e)) return !1; + var t = e.map(A["default"]).join("|"); + return RegExp("^((?:" + t + ")(?:" + E + "))") + }, e.prototype.tokenize = function(e) { + for (var E = [], t = void 0; e.length;) t = this.getNextToken(e, t), e = e.substring(t.value.length), E.push(t); + return E + }, e.prototype.getNextToken = function(e, E) { + return this.getWhitespaceToken(e) || this.getCommentToken(e) || this.getStringToken(e) || this.getOpenParenToken(e) || this.getCloseParenToken(e) || this.getPlaceholderToken(e) || this.getNumberToken(e) || this.getReservedWordToken(e, E) || this.getWordToken(e) || this.getOperatorToken(e) + }, e.prototype.getWhitespaceToken = function(e) { + return this.getTokenOnFirstMatch({ + input: e, + type: O["default"].WHITESPACE, + regex: this.WHITESPACE_REGEX + }) + }, e.prototype.getCommentToken = function(e) { + return this.getLineCommentToken(e) || this.getBlockCommentToken(e) + }, e.prototype.getLineCommentToken = function(e) { + return this.getTokenOnFirstMatch({ + input: e, + type: O["default"].LINE_COMMENT, + regex: this.LINE_COMMENT_REGEX + }) + }, e.prototype.getBlockCommentToken = function(e) { + return this.getTokenOnFirstMatch({ + input: e, + type: O["default"].BLOCK_COMMENT, + regex: this.BLOCK_COMMENT_REGEX + }) + }, e.prototype.getStringToken = function(e) { + return this.getTokenOnFirstMatch({ + input: e, + type: O["default"].STRING, + regex: this.STRING_REGEX + }) + }, e.prototype.getOpenParenToken = function(e) { + return this.getTokenOnFirstMatch({ + input: e, + type: O["default"].OPEN_PAREN, + regex: this.OPEN_PAREN_REGEX + }) + }, e.prototype.getCloseParenToken = function(e) { + return this.getTokenOnFirstMatch({ + input: e, + type: O["default"].CLOSE_PAREN, + regex: this.CLOSE_PAREN_REGEX + }) + }, e.prototype.getPlaceholderToken = function(e) { + return this.getIdentNamedPlaceholderToken(e) || this.getStringNamedPlaceholderToken(e) || this.getIndexedPlaceholderToken(e) + }, e.prototype.getIdentNamedPlaceholderToken = function(e) { + return this.getPlaceholderTokenWithKey({ + input: e, + regex: this.IDENT_NAMED_PLACEHOLDER_REGEX, + parseKey: function(e) { + return e.slice(1) + } + }) + }, e.prototype.getStringNamedPlaceholderToken = function(e) { + var E = this; + return this.getPlaceholderTokenWithKey({ + input: e, + regex: this.STRING_NAMED_PLACEHOLDER_REGEX, + parseKey: function(e) { + return E.getEscapedPlaceholderKey({ + key: e.slice(2, -1), + quoteChar: e.slice(-1) + }) + } + }) + }, e.prototype.getIndexedPlaceholderToken = function(e) { + return this.getPlaceholderTokenWithKey({ + input: e, + regex: this.INDEXED_PLACEHOLDER_REGEX, + parseKey: function(e) { + return e.slice(1) + } + }) + }, e.prototype.getPlaceholderTokenWithKey = function(e) { + var E = e.input, + t = e.regex, + n = e.parseKey, + r = this.getTokenOnFirstMatch({ + input: E, + regex: t, + type: O["default"].PLACEHOLDER + }); + return r && (r.key = n(r.value)), r + }, e.prototype.getEscapedPlaceholderKey = function(e) { + var E = e.key, + t = e.quoteChar; + return E.replace(RegExp((0, A["default"])("\\") + t, "g"), t) + }, e.prototype.getNumberToken = function(e) { + return this.getTokenOnFirstMatch({ + input: e, + type: O["default"].NUMBER, + regex: this.NUMBER_REGEX + }) + }, e.prototype.getOperatorToken = function(e) { + return this.getTokenOnFirstMatch({ + input: e, + type: O["default"].OPERATOR, + regex: this.OPERATOR_REGEX + }) + }, e.prototype.getReservedWordToken = function(e, E) { + if (!E || !E.value || "." !== E.value) return this.getToplevelReservedToken(e) || this.getNewlineReservedToken(e) || this.getPlainReservedToken(e) + }, e.prototype.getToplevelReservedToken = function(e) { + return this.getTokenOnFirstMatch({ + input: e, + type: O["default"].RESERVED_TOPLEVEL, + regex: this.RESERVED_TOPLEVEL_REGEX + }) + }, e.prototype.getNewlineReservedToken = function(e) { + return this.getTokenOnFirstMatch({ + input: e, + type: O["default"].RESERVED_NEWLINE, + regex: this.RESERVED_NEWLINE_REGEX + }) + }, e.prototype.getPlainReservedToken = function(e) { + return this.getTokenOnFirstMatch({ + input: e, + type: O["default"].RESERVED, + regex: this.RESERVED_PLAIN_REGEX + }) + }, e.prototype.getWordToken = function(e) { + return this.getTokenOnFirstMatch({ + input: e, + type: O["default"].WORD, + regex: this.WORD_REGEX + }) + }, e.prototype.getTokenOnFirstMatch = function(e) { + var E = e.input, + t = e.type, + n = e.regex, + r = E.match(n); + if (r) return { + type: t, + value: r[1] + } + }, e + }(); + E["default"] = i, e.exports = E["default"] + }, function(e, E) { + function t(e) { + var E = typeof e; + return null != e && ("object" == E || "function" == E) + } + e.exports = t + }, function(e, E) { + "use strict"; + E.__esModule = !0, E["default"] = { + WHITESPACE: "whitespace", + WORD: "word", + STRING: "string", + RESERVED: "reserved", + RESERVED_TOPLEVEL: "reserved-toplevel", + RESERVED_NEWLINE: "reserved-newline", + OPERATOR: "operator", + OPEN_PAREN: "open-paren", + CLOSE_PAREN: "close-paren", + LINE_COMMENT: "line-comment", + BLOCK_COMMENT: "block-comment", + NUMBER: "number", + PLACEHOLDER: "placeholder" + }, e.exports = E["default"] + }, function(e, E, t) { + function n(e) { + return null != e && T(e.length) && !r(e) + } + var r = t(12), + T = t(59); + e.exports = n + }, function(e, E, t) { + function n(e) { + return null == e ? "" : r(e) + } + var r = t(10); + e.exports = n + }, function(e, E, t) { + function n(e) { + if ("string" == typeof e) return e; + if (T(e)) return N ? N.call(e) : ""; + var E = e + ""; + return "0" == E && 1 / e == -R ? "-0" : E + } + var r = t(26), + T = t(14), + R = 1 / 0, + o = r ? r.prototype : void 0, + N = o ? o.toString : void 0; + e.exports = n + }, function(e, E) { + function t(e) { + if (null != e) { + try { + return r.call(e) + } catch (E) {} + try { + return e + "" + } catch (E) {} + } + return "" + } + var n = Function.prototype, + r = n.toString; + e.exports = t + }, function(e, E, t) { + function n(e) { + var E = r(e) ? N.call(e) : ""; + return E == T || E == R + } + var r = t(6), + T = "[object Function]", + R = "[object GeneratorFunction]", + o = Object.prototype, + N = o.toString; + e.exports = n + }, function(e, E) { + function t(e) { + return null != e && "object" == typeof e + } + e.exports = t + }, function(e, E, t) { + function n(e) { + return "symbol" == typeof e || r(e) && o.call(e) == T + } + var r = t(13), + T = "[object Symbol]", + R = Object.prototype, + o = R.toString; + e.exports = n + }, function(e, E, t) { + "use strict"; + + function n(e) { + return e && e.__esModule ? e : { + "default": e + } + } + E.__esModule = !0; + var r = t(1), + T = n(r), + R = t(61), + o = n(R), + N = t(60), + A = n(N), + I = "top-level", + O = "block-level", + i = function() { + function e(E) { + (0, T["default"])(this, e), this.indent = E || " ", this.indentTypes = [] + } + return e.prototype.getIndent = function() { + return (0, o["default"])(this.indent, this.indentTypes.length) + }, e.prototype.increaseToplevel = function() { + this.indentTypes.push(I) + }, e.prototype.increaseBlockLevel = function() { + this.indentTypes.push(O) + }, e.prototype.decreaseTopLevel = function() { + (0, A["default"])(this.indentTypes) === I && this.indentTypes.pop() + }, e.prototype.decreaseBlockLevel = function() { + for (; this.indentTypes.length > 0;) { + var e = this.indentTypes.pop(); + if (e !== I) break + } + }, e + }(); + E["default"] = i, e.exports = E["default"] + }, function(e, E, t) { + "use strict"; + + function n(e) { + return e && e.__esModule ? e : { + "default": e + } + } + E.__esModule = !0; + var r = t(1), + T = n(r), + R = t(7), + o = n(R), + N = 50, + A = function() { + function e() { + (0, T["default"])(this, e), this.level = 0 + } + return e.prototype.beginIfPossible = function(e, E) { + 0 === this.level && this.isInlineBlock(e, E) ? this.level = 1 : this.level > 0 ? this.level++ : this.level = 0 + }, e.prototype.end = function() { + this.level-- + }, e.prototype.isActive = function() { + return this.level > 0 + }, e.prototype.isInlineBlock = function(e, E) { + for (var t = 0, n = 0, r = E; e.length > r; r++) { + var T = e[r]; + if (t += T.value.length, t > N) return !1; + if (T.type === o["default"].OPEN_PAREN) n++; + else if (T.type === o["default"].CLOSE_PAREN && (n--, 0 === n)) return !0; + if (this.isForbiddenToken(T)) return !1 + } + return !1 + }, e.prototype.isForbiddenToken = function(e) { + var E = e.type, + t = e.value; + return E === o["default"].RESERVED_TOPLEVEL || E === o["default"].RESERVED_NEWLINE || E === o["default"].COMMENT || E === o["default"].BLOCK_COMMENT || ";" === t + }, e + }(); + E["default"] = A, e.exports = E["default"] + }, function(e, E, t) { + "use strict"; + + function n(e) { + return e && e.__esModule ? e : { + "default": e + } + } + E.__esModule = !0; + var r = t(1), + T = n(r), + R = function() { + function e(E) { + (0, T["default"])(this, e), this.params = E, this.index = 0 + } + return e.prototype.get = function(e) { + var E = e.key, + t = e.value; + return this.params ? E ? this.params[E] : this.params[this.index++] : t + }, e + }(); + E["default"] = R, e.exports = E["default"] + }, function(e, E, t) { + "use strict"; + + function n(e) { + return e && e.__esModule ? e : { + "default": e + } + } + E.__esModule = !0; + var r = t(1), + T = n(r), + R = t(4), + o = n(R), + N = t(5), + A = n(N), + I = ["ABS", "ACTIVATE", "ALIAS", "ALL", "ALLOCATE", "ALLOW", "ALTER", "ANY", "ARE", "ARRAY", "AS", "ASC", "ASENSITIVE", "ASSOCIATE", "ASUTIME", "ASYMMETRIC", "AT", "ATOMIC", "ATTRIBUTES", "AUDIT", "AUTHORIZATION", "AUX", "AUXILIARY", "AVG", "BEFORE", "BEGIN", "BETWEEN", "BIGINT", "BINARY", "BLOB", "BOOLEAN", "BOTH", "BUFFERPOOL", "BY", "CACHE", "CALL", "CALLED", "CAPTURE", "CARDINALITY", "CASCADED", "CASE", "CAST", "CCSID", "CEIL", "CEILING", "CHAR", "CHARACTER", "CHARACTER_LENGTH", "CHAR_LENGTH", "CHECK", "CLOB", "CLONE", "CLOSE", "CLUSTER", "COALESCE", "COLLATE", "COLLECT", "COLLECTION", "COLLID", "COLUMN", "COMMENT", "COMMIT", "CONCAT", "CONDITION", "CONNECT", "CONNECTION", "CONSTRAINT", "CONTAINS", "CONTINUE", "CONVERT", "CORR", "CORRESPONDING", "COUNT", "COUNT_BIG", "COVAR_POP", "COVAR_SAMP", "CREATE", "CROSS", "CUBE", "CUME_DIST", "CURRENT", "CURRENT_DATE", "CURRENT_DEFAULT_TRANSFORM_GROUP", "CURRENT_LC_CTYPE", "CURRENT_PATH", "CURRENT_ROLE", "CURRENT_SCHEMA", "CURRENT_SERVER", "CURRENT_TIME", "CURRENT_TIMESTAMP", "CURRENT_TIMEZONE", "CURRENT_TRANSFORM_GROUP_FOR_TYPE", "CURRENT_USER", "CURSOR", "CYCLE", "DATA", "DATABASE", "DATAPARTITIONNAME", "DATAPARTITIONNUM", "DATE", "DAY", "DAYS", "DB2GENERAL", "DB2GENRL", "DB2SQL", "DBINFO", "DBPARTITIONNAME", "DBPARTITIONNUM", "DEALLOCATE", "DEC", "DECIMAL", "DECLARE", "DEFAULT", "DEFAULTS", "DEFINITION", "DELETE", "DENSERANK", "DENSE_RANK", "DEREF", "DESCRIBE", "DESCRIPTOR", "DETERMINISTIC", "DIAGNOSTICS", "DISABLE", "DISALLOW", "DISCONNECT", "DISTINCT", "DO", "DOCUMENT", "DOUBLE", "DROP", "DSSIZE", "DYNAMIC", "EACH", "EDITPROC", "ELEMENT", "ELSE", "ELSEIF", "ENABLE", "ENCODING", "ENCRYPTION", "END", "END-EXEC", "ENDING", "ERASE", "ESCAPE", "EVERY", "EXCEPTION", "EXCLUDING", "EXCLUSIVE", "EXEC", "EXECUTE", "EXISTS", "EXIT", "EXP", "EXPLAIN", "EXTENDED", "EXTERNAL", "EXTRACT", "FALSE", "FENCED", "FETCH", "FIELDPROC", "FILE", "FILTER", "FINAL", "FIRST", "FLOAT", "FLOOR", "FOR", "FOREIGN", "FREE", "FULL", "FUNCTION", "FUSION", "GENERAL", "GENERATED", "GET", "GLOBAL", "GOTO", "GRANT", "GRAPHIC", "GROUP", "GROUPING", "HANDLER", "HASH", "HASHED_VALUE", "HINT", "HOLD", "HOUR", "HOURS", "IDENTITY", "IF", "IMMEDIATE", "IN", "INCLUDING", "INCLUSIVE", "INCREMENT", "INDEX", "INDICATOR", "INDICATORS", "INF", "INFINITY", "INHERIT", "INNER", "INOUT", "INSENSITIVE", "INSERT", "INT", "INTEGER", "INTEGRITY", "INTERSECTION", "INTERVAL", "INTO", "IS", "ISOBID", "ISOLATION", "ITERATE", "JAR", "JAVA", "KEEP", "KEY", "LABEL", "LANGUAGE", "LARGE", "LATERAL", "LC_CTYPE", "LEADING", "LEAVE", "LEFT", "LIKE", "LINKTYPE", "LN", "LOCAL", "LOCALDATE", "LOCALE", "LOCALTIME", "LOCALTIMESTAMP", "LOCATOR", "LOCATORS", "LOCK", "LOCKMAX", "LOCKSIZE", "LONG", "LOOP", "LOWER", "MAINTAINED", "MATCH", "MATERIALIZED", "MAX", "MAXVALUE", "MEMBER", "MERGE", "METHOD", "MICROSECOND", "MICROSECONDS", "MIN", "MINUTE", "MINUTES", "MINVALUE", "MOD", "MODE", "MODIFIES", "MODULE", "MONTH", "MONTHS", "MULTISET", "NAN", "NATIONAL", "NATURAL", "NCHAR", "NCLOB", "NEW", "NEW_TABLE", "NEXTVAL", "NO", "NOCACHE", "NOCYCLE", "NODENAME", "NODENUMBER", "NOMAXVALUE", "NOMINVALUE", "NONE", "NOORDER", "NORMALIZE", "NORMALIZED", "NOT", "NULL", "NULLIF", "NULLS", "NUMERIC", "NUMPARTS", "OBID", "OCTET_LENGTH", "OF", "OFFSET", "OLD", "OLD_TABLE", "ON", "ONLY", "OPEN", "OPTIMIZATION", "OPTIMIZE", "OPTION", "ORDER", "OUT", "OUTER", "OVER", "OVERLAPS", "OVERLAY", "OVERRIDING", "PACKAGE", "PADDED", "PAGESIZE", "PARAMETER", "PART", "PARTITION", "PARTITIONED", "PARTITIONING", "PARTITIONS", "PASSWORD", "PATH", "PERCENTILE_CONT", "PERCENTILE_DISC", "PERCENT_RANK", "PIECESIZE", "PLAN", "POSITION", "POWER", "PRECISION", "PREPARE", "PREVVAL", "PRIMARY", "PRIQTY", "PRIVILEGES", "PROCEDURE", "PROGRAM", "PSID", "PUBLIC", "QUERY", "QUERYNO", "RANGE", "RANK", "READ", "READS", "REAL", "RECOVERY", "RECURSIVE", "REF", "REFERENCES", "REFERENCING", "REFRESH", "REGR_AVGX", "REGR_AVGY", "REGR_COUNT", "REGR_INTERCEPT", "REGR_R2", "REGR_SLOPE", "REGR_SXX", "REGR_SXY", "REGR_SYY", "RELEASE", "RENAME", "REPEAT", "RESET", "RESIGNAL", "RESTART", "RESTRICT", + O = ["ADD", "AFTER", "ALTER COLUMN", "ALTER TABLE", "DELETE FROM", "EXCEPT", "FETCH FIRST", "FROM", "GROUP BY", "GO", "HAVING", "INSERT INTO", "INTERSECT", "LIMIT", "ORDER BY", "SELECT", "SET CURRENT SCHEMA", "SET SCHEMA", "SET", "UNION ALL", "UPDATE", "VALUES", "WHERE"], + i = ["AND", "CROSS JOIN", "INNER JOIN", "JOIN", "LEFT JOIN", "LEFT OUTER JOIN", "OR", "OUTER JOIN", "RIGHT JOIN", "RIGHT OUTER JOIN"], + S = void 0, + u = function() { + function e(E) { + (0, T["default"])(this, e), this.cfg = E + } + return e.prototype.format = function(e) { + return S || (S = new A["default"]({ + reservedWords: I, + reservedToplevelWords: O, + reservedNewlineWords: i, + stringTypes: ['""', "''", "``", "[]"], + openParens: ["("], + closeParens: [")"], + indexedPlaceholderTypes: ["?"], + namedPlaceholderTypes: [":"], + lineCommentTypes: ["--"], + specialWordChars: ["#", "@"] + })), new o["default"](this.cfg, S).format(e) + }, e + }(); + E["default"] = u, e.exports = E["default"] + }, function(e, E, t) { + "use strict"; + + function n(e) { + return e && e.__esModule ? e : { + "default": e + } + } + E.__esModule = !0; + var r = t(1), + T = n(r), + R = t(4), + o = n(R), + N = t(5), + A = n(N), + I = ["ALL", "ALTER", "ANALYZE", "AND", "ANY", "ARRAY", "AS", "ASC", "BEGIN", "BETWEEN", "BINARY", "BOOLEAN", "BREAK", "BUCKET", "BUILD", "BY", "CALL", "CASE", "CAST", "CLUSTER", "COLLATE", "COLLECTION", "COMMIT", "CONNECT", "CONTINUE", "CORRELATE", "COVER", "CREATE", "DATABASE", "DATASET", "DATASTORE", "DECLARE", "DECREMENT", "DELETE", "DERIVED", "DESC", "DESCRIBE", "DISTINCT", "DO", "DROP", "EACH", "ELEMENT", "ELSE", "END", "EVERY", "EXCEPT", "EXCLUDE", "EXECUTE", "EXISTS", "EXPLAIN", "FALSE", "FETCH", "FIRST", "FLATTEN", "FOR", "FORCE", "FROM", "FUNCTION", "GRANT", "GROUP", "GSI", "HAVING", "IF", "IGNORE", "ILIKE", "IN", "INCLUDE", "INCREMENT", "INDEX", "INFER", "INLINE", "INNER", "INSERT", "INTERSECT", "INTO", "IS", "JOIN", "KEY", "KEYS", "KEYSPACE", "KNOWN", "LAST", "LEFT", "LET", "LETTING", "LIKE", "LIMIT", "LSM", "MAP", "MAPPING", "MATCHED", "MATERIALIZED", "MERGE", "MINUS", "MISSING", "NAMESPACE", "NEST", "NOT", "NULL", "NUMBER", "OBJECT", "OFFSET", "ON", "OPTION", "OR", "ORDER", "OUTER", "OVER", "PARSE", "PARTITION", "PASSWORD", "PATH", "POOL", "PREPARE", "PRIMARY", "PRIVATE", "PRIVILEGE", "PROCEDURE", "PUBLIC", "RAW", "REALM", "REDUCE", "RENAME", "RETURN", "RETURNING", "REVOKE", "RIGHT", "ROLE", "ROLLBACK", "SATISFIES", "SCHEMA", "SELECT", "SELF", "SEMI", "SET", "SHOW", "SOME", "START", "STATISTICS", "STRING", "SYSTEM", "THEN", "TO", "TRANSACTION", "TRIGGER", "TRUE", "TRUNCATE", "UNDER", "UNION", "UNIQUE", "UNKNOWN", "UNNEST", "UNSET", "UPDATE", "UPSERT", "USE", "USER", "USING", "VALIDATE", "VALUE", "VALUED", "VALUES", "VIA", "VIEW", "WHEN", "WHERE", "WHILE", "WITH", "WITHIN", "WORK", "XOR"], + O = ["DELETE FROM", "EXCEPT ALL", "EXCEPT", "EXPLAIN DELETE FROM", "EXPLAIN UPDATE", "EXPLAIN UPSERT", "FROM", "GROUP BY", "HAVING", "INFER", "INSERT INTO", "INTERSECT ALL", "INTERSECT", "LET", "LIMIT", "MERGE", "NEST", "ORDER BY", "PREPARE", "SELECT", "SET CURRENT SCHEMA", "SET SCHEMA", "SET", "UNION ALL", "UNION", "UNNEST", "UPDATE", "UPSERT", "USE KEYS", "VALUES", "WHERE"], + i = ["AND", "INNER JOIN", "JOIN", "LEFT JOIN", "LEFT OUTER JOIN", "OR", "OUTER JOIN", "RIGHT JOIN", "RIGHT OUTER JOIN", "XOR"], + S = void 0, + u = function() { + function e(E) { + (0, T["default"])(this, e), this.cfg = E + } + return e.prototype.format = function(e) { + return S || (S = new A["default"]({ + reservedWords: I, + reservedToplevelWords: O, + reservedNewlineWords: i, + stringTypes: ['""', "''", "``"], + openParens: ["(", "[", "{"], + closeParens: [")", "]", "}"], + namedPlaceholderTypes: ["$"], + lineCommentTypes: ["#", "--"] + })), new o["default"](this.cfg, S).format(e) + }, e + }(); + E["default"] = u, e.exports = E["default"] + }, function(e, E, t) { + "use strict"; + + function n(e) { + return e && e.__esModule ? e : { + "default": e + } + } + E.__esModule = !0; + var r = t(1), + T = n(r), + R = t(4), + o = n(R), + N = t(5), + A = n(N), + I = ["A", "ACCESSIBLE", "AGENT", "AGGREGATE", "ALL", "ALTER", "ANY", "ARRAY", "AS", "ASC", "AT", "ATTRIBUTE", "AUTHID", "AVG", "BETWEEN", "BFILE_BASE", "BINARY_INTEGER", "BINARY", "BLOB_BASE", "BLOCK", "BODY", "BOOLEAN", "BOTH", "BOUND", "BULK", "BY", "BYTE", "C", "CALL", "CALLING", "CASCADE", "CASE", "CHAR_BASE", "CHAR", "CHARACTER", "CHARSET", "CHARSETFORM", "CHARSETID", "CHECK", "CLOB_BASE", "CLONE", "CLOSE", "CLUSTER", "CLUSTERS", "COALESCE", "COLAUTH", "COLLECT", "COLUMNS", "COMMENT", "COMMIT", "COMMITTED", "COMPILED", "COMPRESS", "CONNECT", "CONSTANT", "CONSTRUCTOR", "CONTEXT", "CONTINUE", "CONVERT", "COUNT", "CRASH", "CREATE", "CREDENTIAL", "CURRENT", "CURRVAL", "CURSOR", "CUSTOMDATUM", "DANGLING", "DATA", "DATE_BASE", "DATE", "DAY", "DECIMAL", "DEFAULT", "DEFINE", "DELETE", "DESC", "DETERMINISTIC", "DIRECTORY", "DISTINCT", "DO", "DOUBLE", "DROP", "DURATION", "ELEMENT", "ELSIF", "EMPTY", "ESCAPE", "EXCEPTIONS", "EXCLUSIVE", "EXECUTE", "EXISTS", "EXIT", "EXTENDS", "EXTERNAL", "EXTRACT", "FALSE", "FETCH", "FINAL", "FIRST", "FIXED", "FLOAT", "FOR", "FORALL", "FORCE", "FROM", "FUNCTION", "GENERAL", "GOTO", "GRANT", "GROUP", "HASH", "HEAP", "HIDDEN", "HOUR", "IDENTIFIED", "IF", "IMMEDIATE", "IN", "INCLUDING", "INDEX", "INDEXES", "INDICATOR", "INDICES", "INFINITE", "INSTANTIABLE", "INT", "INTEGER", "INTERFACE", "INTERVAL", "INTO", "INVALIDATE", "IS", "ISOLATION", "JAVA", "LANGUAGE", "LARGE", "LEADING", "LENGTH", "LEVEL", "LIBRARY", "LIKE", "LIKE2", "LIKE4", "LIKEC", "LIMITED", "LOCAL", "LOCK", "LONG", "MAP", "MAX", "MAXLEN", "MEMBER", "MERGE", "MIN", "MINUS", "MINUTE", "MLSLABEL", "MOD", "MODE", "MONTH", "MULTISET", "NAME", "NAN", "NATIONAL", "NATIVE", "NATURAL", "NATURALN", "NCHAR", "NEW", "NEXTVAL", "NOCOMPRESS", "NOCOPY", "NOT", "NOWAIT", "NULL", "NULLIF", "NUMBER_BASE", "NUMBER", "OBJECT", "OCICOLL", "OCIDATE", "OCIDATETIME", "OCIDURATION", "OCIINTERVAL", "OCILOBLOCATOR", "OCINUMBER", "OCIRAW", "OCIREF", "OCIREFCURSOR", "OCIROWID", "OCISTRING", "OCITYPE", "OF", "OLD", "ON", "ONLY", "OPAQUE", "OPEN", "OPERATOR", "OPTION", "ORACLE", "ORADATA", "ORDER", "ORGANIZATION", "ORLANY", "ORLVARY", "OTHERS", "OUT", "OVERLAPS", "OVERRIDING", "PACKAGE", "PARALLEL_ENABLE", "PARAMETER", "PARAMETERS", "PARENT", "PARTITION", "PASCAL", "PCTFREE", "PIPE", "PIPELINED", "PLS_INTEGER", "PLUGGABLE", "POSITIVE", "POSITIVEN", "PRAGMA", "PRECISION", "PRIOR", "PRIVATE", "PROCEDURE", "PUBLIC", "RAISE", "RANGE", "RAW", "READ", "REAL", "RECORD", "REF", "REFERENCE", "RELEASE", "RELIES_ON", "REM", "REMAINDER", "RENAME", "RESOURCE", "RESULT_CACHE", "RESULT", "RETURN", "RETURNING", "REVERSE", "REVOKE", "ROLLBACK", "ROW", "ROWID", "ROWNUM", "ROWTYPE", "SAMPLE", "SAVE", "SAVEPOINT", "SB1", "SB2", "SB4", "SECOND", "SEGMENT", "SELF", "SEPARATE", "SEQUENCE", "SERIALIZABLE", "SHARE", "SHORT", "SIZE_T", "SIZE", "SMALLINT", "SOME", "SPACE", "SPARSE", "SQL", "SQLCODE", "SQLDATA", "SQLERRM", "SQLNAME", "SQLSTATE", "STANDARD", "START", "STATIC", "STDDEV", "STORED", "STRING", "STRUCT", "STYLE", "SUBMULTISET", "SUBPARTITION", "SUBSTITUTABLE", "SUBTYPE", "SUCCESSFUL", "SUM", "SYNONYM", "SYSDATE", "TABAUTH", "TABLE", "TDO", "THE", "THEN", "TIME", "TIMESTAMP", "TIMEZONE_ABBR", "TIMEZONE_HOUR", "TIMEZONE_MINUTE", "TIMEZONE_REGION", "TO", "TRAILING", "TRANSACTION", "TRANSACTIONAL", "TRIGGER", "TRUE", "TRUSTED", "TYPE", "UB1", "UB2", "UB4", "UID", "UNDER", "UNIQUE", "UNPLUG", "UNSIGNED", "UNTRUSTED", "USE", "USER", "USING", "VALIDATE", "VALIST", "VALUE", "VARCHAR", "VARCHAR2", "VARIABLE", "VARIANCE", "VARRAY", "VARYING", "VIEW", "VIEWS", "VOID", "WHENEVER", "WHILE", "WITH", "WORK", "WRAPPED", "WRITE", "YEAR", "ZONE"], + O = ["ADD", "ALTER COLUMN", "ALTER TABLE", "BEGIN", "CONNECT BY", "DECLARE", "DELETE FROM", "DELETE", "END", "EXCEPT", "EXCEPTION", "FETCH FIRST", "FROM", "GROUP BY", "HAVING", "INSERT INTO", "INSERT", "INTERSECT", "LIMIT", "LOOP", "MODIFY", "ORDER BY", "SELECT", "SET CURRENT SCHEMA", "SET SCHEMA", "SET", "START WITH", "UNION ALL", "UNION", "UPDATE", "VALUES", "WHERE"], + i = ["AND", "CROSS APPLY", "CROSS JOIN", "ELSE", "END", "INNER JOIN", "JOIN", "LEFT JOIN", "LEFT OUTER JOIN", "OR", "OUTER APPLY", "OUTER JOIN", "RIGHT JOIN", "RIGHT OUTER JOIN", "WHEN", "XOR"], + S = void 0, + u = function() { + function e(E) { + (0, T["default"])(this, e), this.cfg = E + } + return e.prototype.format = function(e) { + return S || (S = new A["default"]({ + reservedWords: I, + reservedToplevelWords: O, + reservedNewlineWords: i, + stringTypes: ['""', "N''", "''", "``"], + openParens: ["(", "CASE"], + closeParens: [")", "END"], + indexedPlaceholderTypes: ["?"], + namedPlaceholderTypes: [":"], + lineCommentTypes: ["--"], + specialWordChars: ["_", "$", "#", ".", "@"] + })), new o["default"](this.cfg, S).format(e) + }, e + }(); + E["default"] = u, e.exports = E["default"] + }, function(e, E, t) { + "use strict"; + + function n(e) { + return e && e.__esModule ? e : { + "default": e + } + } + E.__esModule = !0; + var r = t(1), + T = n(r), + R = t(4), + o = n(R), + N = t(5), + A = n(N), + I = ["ACCESSIBLE", "ACTION", "AGAINST", "AGGREGATE", "ALGORITHM", "ALL", "ALTER", "ANALYSE", "ANALYZE", "AS", "ASC", "AUTOCOMMIT", "AUTO_INCREMENT", "BACKUP", "BEGIN", "BETWEEN", "BINLOG", "BOTH", "CASCADE", "CASE", "CHANGE", "CHANGED", "CHARACTER SET", "CHARSET", "CHECK", "CHECKSUM", "COLLATE", "COLLATION", "COLUMN", "COLUMNS", "COMMENT", "COMMIT", "COMMITTED", "COMPRESSED", "CONCURRENT", "CONSTRAINT", "CONTAINS", "CONVERT", "CREATE", "CROSS", "CURRENT_TIMESTAMP", "DATABASE", "DATABASES", "DAY", "DAY_HOUR", "DAY_MINUTE", "DAY_SECOND", "DEFAULT", "DEFINER", "DELAYED", "DELETE", "DESC", "DESCRIBE", "DETERMINISTIC", "DISTINCT", "DISTINCTROW", "DIV", "DO", "DROP", "DUMPFILE", "DUPLICATE", "DYNAMIC", "ELSE", "ENCLOSED", "END", "ENGINE", "ENGINES", "ENGINE_TYPE", "ESCAPE", "ESCAPED", "EVENTS", "EXEC", "EXECUTE", "EXISTS", "EXPLAIN", "EXTENDED", "FAST", "FETCH", "FIELDS", "FILE", "FIRST", "FIXED", "FLUSH", "FOR", "FORCE", "FOREIGN", "FULL", "FULLTEXT", "FUNCTION", "GLOBAL", "GRANT", "GRANTS", "GROUP_CONCAT", "HEAP", "HIGH_PRIORITY", "HOSTS", "HOUR", "HOUR_MINUTE", "HOUR_SECOND", "IDENTIFIED", "IF", "IFNULL", "IGNORE", "IN", "INDEX", "INDEXES", "INFILE", "INSERT", "INSERT_ID", "INSERT_METHOD", "INTERVAL", "INTO", "INVOKER", "IS", "ISOLATION", "KEY", "KEYS", "KILL", "LAST_INSERT_ID", "LEADING", "LEVEL", "LIKE", "LINEAR", "LINES", "LOAD", "LOCAL", "LOCK", "LOCKS", "LOGS", "LOW_PRIORITY", "MARIA", "MASTER", "MASTER_CONNECT_RETRY", "MASTER_HOST", "MASTER_LOG_FILE", "MATCH", "MAX_CONNECTIONS_PER_HOUR", "MAX_QUERIES_PER_HOUR", "MAX_ROWS", "MAX_UPDATES_PER_HOUR", "MAX_USER_CONNECTIONS", "MEDIUM", "MERGE", "MINUTE", "MINUTE_SECOND", "MIN_ROWS", "MODE", "MODIFY", "MONTH", "MRG_MYISAM", "MYISAM", "NAMES", "NATURAL", "NOT", "NOW()", "NULL", "OFFSET", "ON DELETE", "ON UPDATE", "ON", "ONLY", "OPEN", "OPTIMIZE", "OPTION", "OPTIONALLY", "OUTFILE", "PACK_KEYS", "PAGE", "PARTIAL", "PARTITION", "PARTITIONS", "PASSWORD", "PRIMARY", "PRIVILEGES", "PROCEDURE", "PROCESS", "PROCESSLIST", "PURGE", "QUICK", "RAID0", "RAID_CHUNKS", "RAID_CHUNKSIZE", "RAID_TYPE", "RANGE", "READ", "READ_ONLY", "READ_WRITE", "REFERENCES", "REGEXP", "RELOAD", "RENAME", "REPAIR", "REPEATABLE", "REPLACE", "REPLICATION", "RESET", "RESTORE", "RESTRICT", "RETURN", "RETURNS", "REVOKE", "RLIKE", "ROLLBACK", "ROW", "ROWS", "ROW_FORMAT", "SECOND", "SECURITY", "SEPARATOR", "SERIALIZABLE", "SESSION", "SHARE", "SHOW", "SHUTDOWN", "SLAVE", "SONAME", "SOUNDS", "SQL", "SQL_AUTO_IS_NULL", "SQL_BIG_RESULT", "SQL_BIG_SELECTS", "SQL_BIG_TABLES", "SQL_BUFFER_RESULT", "SQL_CACHE", "SQL_CALC_FOUND_ROWS", "SQL_LOG_BIN", "SQL_LOG_OFF", "SQL_LOG_UPDATE", "SQL_LOW_PRIORITY_UPDATES", "SQL_MAX_JOIN_SIZE", "SQL_NO_CACHE", "SQL_QUOTE_SHOW_CREATE", "SQL_SAFE_UPDATES", "SQL_SELECT_LIMIT", "SQL_SLAVE_SKIP_COUNTER", "SQL_SMALL_RESULT", "SQL_WARNINGS", "START", "STARTING", "STATUS", "STOP", "STORAGE", "STRAIGHT_JOIN", "STRING", "STRIPED", "SUPER", "TABLE", "TABLES", "TEMPORARY", "TERMINATED", "THEN", "TO", "TRAILING", "TRANSACTIONAL", "TRUE", "TRUNCATE", "TYPE", "TYPES", "UNCOMMITTED", "UNIQUE", "UNLOCK", "UNSIGNED", "USAGE", "USE", "USING", "VARIABLES", "VIEW", "WHEN", "WITH", "WORK", "WRITE", "YEAR_MONTH"], + O = ["ADD", "AFTER", "ALTER COLUMN", "ALTER TABLE", "DELETE FROM", "EXCEPT", "FETCH FIRST", "FROM", "GROUP BY", "GO", "HAVING", "INSERT INTO", "INSERT", "INTERSECT", "LIMIT", "MODIFY", "ORDER BY", "SELECT", "SET CURRENT SCHEMA", "SET SCHEMA", "SET", "UNION ALL", "UNION", "UPDATE", "VALUES", "WHERE"], + i = ["AND", "CROSS APPLY", "CROSS JOIN", "ELSE", "INNER JOIN", "JOIN", "LEFT JOIN", "LEFT OUTER JOIN", "OR", "OUTER APPLY", "OUTER JOIN", "RIGHT JOIN", "RIGHT OUTER JOIN", "WHEN", "XOR"], + S = void 0, + u = function() { + function e(E) { + (0, T["default"])(this, e), this.cfg = E + } + return e.prototype.format = function(e) { + return S || (S = new A["default"]({ + reservedWords: I, + reservedToplevelWords: O, + reservedNewlineWords: i, + stringTypes: ['""', "N''", "''", "``", "[]"], + openParens: ["(", "CASE"], + closeParens: [")", "END"], + indexedPlaceholderTypes: ["?"], + namedPlaceholderTypes: ["@", ":"], + lineCommentTypes: ["#", "--"] + })), new o["default"](this.cfg, S).format(e) + }, e + }(); + E["default"] = u, e.exports = E["default"] + }, function(e, E, t) { + var n = t(3), + r = t(2), + T = n(r, "DataView"); + e.exports = T + }, function(e, E, t) { + var n = t(3), + r = t(2), + T = n(r, "Map"); + e.exports = T + }, function(e, E, t) { + var n = t(3), + r = t(2), + T = n(r, "Promise"); + e.exports = T + }, function(e, E, t) { + var n = t(3), + r = t(2), + T = n(r, "Set"); + e.exports = T + }, function(e, E, t) { + var n = t(2), + r = n.Symbol; + e.exports = r + }, function(e, E, t) { + var n = t(3), + r = t(2), + T = n(r, "WeakMap"); + e.exports = T + }, function(e, E) { + function t(e) { + return e.split("") + } + e.exports = t + }, function(e, E) { + function t(e, E, t, n) { + for (var r = e.length, T = t + (n ? 1 : -1); n ? T-- : ++T < r;) + if (E(e[T], T, e)) return T; + return -1 + } + e.exports = t + }, function(e, E) { + function t(e) { + return r.call(e) + } + var n = Object.prototype, + r = n.toString; + e.exports = t + }, function(e, E, t) { + function n(e, E, t) { + return E === E ? R(e, E, t) : r(e, T, t) + } + var r = t(29), + T = t(32), + R = t(49); + e.exports = n + }, function(e, E) { + function t(e) { + return e !== e + } + e.exports = t + }, function(e, E, t) { + function n(e) { + if (!R(e) || T(e)) return !1; + var E = r(e) ? u : A; + return E.test(o(e)) + } + var r = t(12), + T = t(45), + R = t(6), + o = t(11), + N = /[\\^$.*+?()[\]{}|]/g, + A = /^\[object .+?Constructor\]$/, + I = Function.prototype, + O = Object.prototype, + i = I.toString, + S = O.hasOwnProperty, + u = RegExp("^" + i.call(S).replace(N, "\\$&").replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g, "$1.*?") + "$"); + e.exports = n + }, function(e, E) { + function t(e, E) { + var t = ""; + if (!e || 1 > E || E > n) return t; + do E % 2 && (t += e), E = r(E / 2), E && (e += e); while (E); + return t + } + var n = 9007199254740991, + r = Math.floor; + e.exports = t + }, function(e, E) { + function t(e, E, t) { + var n = -1, + r = e.length; + 0 > E && (E = -E > r ? 0 : r + E), t = t > r ? r : t, 0 > t && (t += r), r = E > t ? 0 : t - E >>> 0, E >>>= 0; + for (var T = Array(r); ++n < r;) T[n] = e[n + E]; + return T + } + e.exports = t + }, function(e, E, t) { + function n(e, E, t) { + var n = e.length; + return t = void 0 === t ? n : t, E || n > t ? r(e, E, t) : e + } + var r = t(35); + e.exports = n + }, function(e, E, t) { + function n(e, E) { + for (var t = e.length; t-- && r(E, e[t], 0) > -1;); + return t + } + var r = t(31); + e.exports = n + }, function(e, E, t) { + var n = t(2), + r = n["__core-js_shared__"]; + e.exports = r + }, function(e, E) { + (function(E) { + var t = "object" == typeof E && E && E.Object === Object && E; + e.exports = t + }).call(E, function() { + return this + }()) + }, function(e, E, t) { + var n = t(22), + r = t(23), + T = t(24), + R = t(25), + o = t(27), + N = t(30), + A = t(11), + I = "[object Map]", + O = "[object Object]", + i = "[object Promise]", + S = "[object Set]", + u = "[object WeakMap]", + L = "[object DataView]", + C = Object.prototype, + s = C.toString, + a = A(n), + f = A(r), + c = A(T), + p = A(R), + l = A(o), + D = N; + (n && D(new n(new ArrayBuffer(1))) != L || r && D(new r) != I || T && D(T.resolve()) != i || R && D(new R) != S || o && D(new o) != u) && (D = function(e) { + var E = s.call(e), + t = E == O ? e.constructor : void 0, + n = t ? A(t) : void 0; + if (n) switch (n) { + case a: + return L; + case f: + return I; + case c: + return i; + case p: + return S; + case l: + return u + } + return E + }), e.exports = D + }, function(e, E) { + function t(e, E) { + return null == e ? void 0 : e[E] + } + e.exports = t + }, function(e, E) { + function t(e) { + return N.test(e) + } + var n = "\\ud800-\\udfff", + r = "\\u0300-\\u036f\\ufe20-\\ufe23", + T = "\\u20d0-\\u20f0", + R = "\\ufe0e\\ufe0f", + o = "\\u200d", + N = RegExp("[" + o + n + r + T + R + "]"); + e.exports = t + }, function(e, E) { + function t(e, E) { + return E = null == E ? n : E, !!E && ("number" == typeof e || r.test(e)) && e > -1 && e % 1 == 0 && E > e + } + var n = 9007199254740991, + r = /^(?:0|[1-9]\d*)$/; + e.exports = t + }, function(e, E, t) { + function n(e, E, t) { + if (!o(t)) return !1; + var n = typeof E; + return !!("number" == n ? T(t) && R(E, t.length) : "string" == n && E in t) && r(t[E], e) + } + var r = t(52), + T = t(8), + R = t(43), + o = t(6); + e.exports = n + }, function(e, E, t) { + function n(e) { + return !!T && T in e + } + var r = t(38), + T = function() { + var e = /[^.]+$/.exec(r && r.keys && r.keys.IE_PROTO || ""); + return e ? "Symbol(src)_1." + e : "" + }(); + e.exports = n + }, function(e, E) { + function t(e) { + var E = e && e.constructor, + t = "function" == typeof E && E.prototype || n; + return e === t + } + var n = Object.prototype; + e.exports = t + }, function(e, E, t) { + var n = t(48), + r = n(Object.keys, Object); + e.exports = r + }, function(e, E) { + function t(e, E) { + return function(t) { + return e(E(t)) + } + } + e.exports = t + }, function(e, E) { + function t(e, E, t) { + for (var n = t - 1, r = e.length; ++n < r;) + if (e[n] === E) return n; + return -1 + } + e.exports = t + }, function(e, E, t) { + function n(e) { + return T(e) ? R(e) : r(e) + } + var r = t(28), + T = t(42), + R = t(51); + e.exports = n + }, function(e, E) { + function t(e) { + return e.match(c) || [] + } + var n = "\\ud800-\\udfff", + r = "\\u0300-\\u036f\\ufe20-\\ufe23", + T = "\\u20d0-\\u20f0", + R = "\\ufe0e\\ufe0f", + o = "[" + n + "]", + N = "[" + r + T + "]", + A = "\\ud83c[\\udffb-\\udfff]", + I = "(?:" + N + "|" + A + ")", + O = "[^" + n + "]", + i = "(?:\\ud83c[\\udde6-\\uddff]){2}", + S = "[\\ud800-\\udbff][\\udc00-\\udfff]", + u = "\\u200d", + L = I + "?", + C = "[" + R + "]?", + s = "(?:" + u + "(?:" + [O, i, S].join("|") + ")" + C + L + ")*", + a = C + L + s, + f = "(?:" + [O + N + "?", N, i, S, o].join("|") + ")", + c = RegExp(A + "(?=" + A + ")|" + f + a, "g"); + e.exports = t + }, function(e, E) { + function t(e, E) { + return e === E || e !== e && E !== E + } + e.exports = t + }, function(e, E, t) { + function n(e) { + return e = r(e), e && R.test(e) ? e.replace(T, "\\$&") : e + } + var r = t(9), + T = /[\\^$.*+?()[\]{}|]/g, + R = RegExp(T.source); + e.exports = n + }, function(e, E, t) { + function n(e) { + return r(e) && o.call(e, "callee") && (!A.call(e, "callee") || N.call(e) == T) + } + var r = t(56), + T = "[object Arguments]", + R = Object.prototype, + o = R.hasOwnProperty, + N = R.toString, + A = R.propertyIsEnumerable; + e.exports = n + }, function(e, E) { + var t = Array.isArray; + e.exports = t + }, function(e, E, t) { + function n(e) { + return T(e) && r(e) + } + var r = t(8), + T = t(13); + e.exports = n + }, function(e, E, t) { + (function(e) { + var n = t(2), + r = t(62), + T = "object" == typeof E && E && !E.nodeType && E, + R = T && "object" == typeof e && e && !e.nodeType && e, + o = R && R.exports === T, + N = o ? n.Buffer : void 0, + A = N ? N.isBuffer : void 0, + I = A || r; + e.exports = I + }).call(E, t(67)(e)) + }, function(e, E, t) { + function n(e) { + if (o(e) && (R(e) || "string" == typeof e || "function" == typeof e.splice || N(e) || T(e))) return !e.length; + var E = r(e); + if (E == O || E == i) return !e.size; + if (A(e)) return !I(e).length; + for (var t in e) + if (u.call(e, t)) return !1; + return !0 + } + var r = t(40), + T = t(54), + R = t(55), + o = t(8), + N = t(57), + A = t(46), + I = t(47), + O = "[object Map]", + i = "[object Set]", + S = Object.prototype, + u = S.hasOwnProperty; + e.exports = n + }, function(e, E) { + function t(e) { + return "number" == typeof e && e > -1 && e % 1 == 0 && n >= e + } + var n = 9007199254740991; + e.exports = t + }, function(e, E) { + function t(e) { + var E = e ? e.length : 0; + return E ? e[E - 1] : void 0 + } + e.exports = t + }, function(e, E, t) { + function n(e, E, t) { + return E = (t ? T(e, E, t) : void 0 === E) ? 1 : R(E), r(o(e), E) + } + var r = t(34), + T = t(44), + R = t(64), + o = t(9); + e.exports = n + }, function(e, E) { + function t() { + return !1 + } + e.exports = t + }, function(e, E, t) { + function n(e) { + if (!e) return 0 === e ? e : 0; + if (e = r(e), e === T || e === -T) { + var E = 0 > e ? -1 : 1; + return E * R + } + return e === e ? e : 0 + } + var r = t(65), + T = 1 / 0, + R = 1.7976931348623157e308; + e.exports = n + }, function(e, E, t) { + function n(e) { + var E = r(e), + t = E % 1; + return E === E ? t ? E - t : E : 0 + } + var r = t(63); + e.exports = n + }, function(e, E, t) { + function n(e) { + if ("number" == typeof e) return e; + if (T(e)) return R; + if (r(e)) { + var E = "function" == typeof e.valueOf ? e.valueOf() : e; + e = r(E) ? E + "" : E + } + if ("string" != typeof e) return 0 === e ? e : +e; + e = e.replace(o, ""); + var t = A.test(e); + return t || I.test(e) ? O(e.slice(2), t ? 2 : 8) : N.test(e) ? R : +e + } + var r = t(6), + T = t(14), + R = NaN, + o = /^\s+|\s+$/g, + N = /^[-+]0x[0-9a-f]+$/i, + A = /^0b[01]+$/i, + I = /^0o[0-7]+$/i, + O = parseInt; + e.exports = n + }, function(e, E, t) { + function n(e, E, t) { + if (e = N(e), e && (t || void 0 === E)) return e.replace(A, ""); + if (!e || !(E = r(E))) return e; + var n = o(e), + I = R(n, o(E)) + 1; + return T(n, 0, I).join("") + } + var r = t(10), + T = t(36), + R = t(37), + o = t(50), + N = t(9), + A = /\s+$/; + e.exports = n + }, function(e, E) { + e.exports = function(e) { + return e.webpackPolyfill || (e.deprecate = function() {}, e.paths = [], e.children = [], e.webpackPolyfill = 1), e + } + }]) +}); + +function escape2Html(str) { + var arrEntities = { + 'lt': '<', + 'gt': '>', + 'nbsp': '', + 'amp': '&', + 'quot': '"' + }; + return str.replace(/&(lt|gt|nbsp|amp|quot);/ig, function(all, t) { + return arrEntities[t]; + }); +} + +function load() { + let codeList = document.getElementsByTagName('code'); + + for (let i = 0; i < codeList.length; i++) { + codeList[i].innerHTML = window.sqlFormatter.format(escape2Html(codeList[i].innerHTML)) + } +}; diff --git a/doc/report_type.md b/doc/report_type.md new file mode 100644 index 00000000..86c81bb6 --- /dev/null +++ b/doc/report_type.md @@ -0,0 +1,133 @@ +# 支持的报告类型 + +[toc] + +## lint +* **Description**:参考sqlint格式,以插件形式集成到代码编辑器,显示输出更加友好 + +* **Example**: + +```bash +soar -report-type lint -query test.sql +``` +## markdown +* **Description**:该格式为默认输出格式,以markdown格式展现,可以用网页浏览器插件直接打开,也可以用markdown编辑器打开 + +* **Example**: + +```bash +echo "select * from film" | soar +``` +## rewrite +* **Description**:SQL重写功能,配合-rewrite-rules参数一起使用,可以通过-list-rewrite-rules查看所有支持的SQL重写规则 + +* **Example**: + +```bash +echo "select * from film" | soar -rewrite-rules star2columns,delimiter -report-type rewrite +``` +## ast +* **Description**:输出SQL的抽象语法树,主要用于测试 + +* **Example**: + +```bash +echo "select * from film" | soar -report-type ast +``` +## tiast +* **Description**:输出SQL的TiDB抽象语法树,主要用于测试 + +* **Example**: + +```bash +echo "select * from film" | soar -report-type tiast +``` +## fingerprint +* **Description**:输出SQL的指纹 + +* **Example**: + +```bash +echo "select * from film where language_id=1" | soar -report-type fingerprint +``` +## md2html +* **Description**:markdown格式转html格式小工具 + +* **Example**: + +```bash +soar -list-heuristic-rules | soar -report-type md2html > heuristic_rules.html +``` +## explain-digest +* **Description**:输入为EXPLAIN的表格,JSON或Vertical格式,对其进行分析,给出分析结果 + +* **Example**: + +```bash +soar -report-type explain-digest << EOF ++----+-------------+-------+------+---------------+------+---------+------+------+-------+ +| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | ++----+-------------+-------+------+---------------+------+---------+------+------+-------+ +| 1 | SIMPLE | film | ALL | NULL | NULL | NULL | NULL | 1131 | | ++----+-------------+-------+------+---------------+------+---------+------+------+-------+ +EOF +``` +## duplicate-key-checker +* **Description**:对OnlineDsn中指定的DB进行索引重复检查 + +* **Example**: + +```bash +soar -report-type duplicate-key-checker -online-dsn user:passwd@127.0.0.1:3306/db +``` +## html +* **Description**:以HTML格式输出报表 + +* **Example**: + +```bash +echo "select * from film" | soar -report-type html +``` +## json +* **Description**:输出JSON格式报表,方便应用程序处理 + +* **Example**: + +```bash +echo "select * from film" | soar -report-type json +``` +## tokenize +* **Description**:对SQL进行切词,主要用于测试 + +* **Example**: + +```bash +echo "select * from film" | soar -report-type tokenize +``` +## compress +* **Description**:SQL压缩小工具,使用内置SQL压缩逻辑,测试中的功能 + +* **Example**: + +```bash +echo "select +* +from + film" | soar -report-type compress +``` +## pretty +* **Description**:使用kr/pretty打印报告,主要用于测试 + +* **Example**: + +```bash +echo "select * from film" | soar -report-type pretty +``` +## remove-comment +* **Description**:去除SQL语句中的注释,支持单行多行注释的去除 + +* **Example**: + +```bash +echo "select/*comment*/ * from film" | soar -report-type remove-comment +``` diff --git a/doc/rewrite.md b/doc/rewrite.md new file mode 100644 index 00000000..68a821d5 --- /dev/null +++ b/doc/rewrite.md @@ -0,0 +1,272 @@ +# 重写规则 + +[toc] + +## dml2select +* **Description**:将数据库更新请求转换为只读查询请求,便于执行EXPLAIN + +* **Original**: + +```sql +DELETE FROM film WHERE length > 100 +``` + +* **Suggest**: + +```sql +select * from film where length > 100 +``` +## star2columns +* **Description**:为SELECT *补全表的列信息 + +* **Original**: + +```sql +SELECT * FROM film +``` + +* **Suggest**: + +```sql +select film.film_id, film.title from film +``` +## insertcolumns +* **Description**:为INSERT补全表的列信息 + +* **Original**: + +```sql +insert into film values(1,2,3,4,5) +``` + +* **Suggest**: + +```sql +insert into film(film_id, title, description, release_year, language_id) values (1, 2, 3, 4, 5) +``` +## having +* **Description**:将查询的HAVING子句改写为WHERE中的查询条件 + +* **Original**: + +```sql +SELECT state, COUNT(*) FROM Drivers GROUP BY state HAVING state IN ('GA', 'TX') ORDER BY state +``` + +* **Suggest**: + +```sql +select state, COUNT(*) from Drivers where state in ('GA', 'TX') group by state order by state asc +``` +## orderbynull +* **Description**:如果GROUP BY语句不指定ORDER BY条件会导致无谓的排序产生,如果不需要排序建议添加ORDER BY NULL + +* **Original**: + +```sql +SELECT sum(col1) FROM tbl GROUP BY col +``` + +* **Suggest**: + +```sql +select sum(col1) from tbl group by col order by null +``` +## unionall +* **Description**:可以接受重复的时间,使用UNION ALL替代UNION以提高查询效率 + +* **Original**: + +```sql +select country_id from city union select country_id from country +``` + +* **Suggest**: + +```sql +select country_id from city union all select country_id from country +``` +## or2in +* **Description**:将同一列不同条件的OR查询转写为IN查询 + +* **Original**: + +```sql +select country_id from city where col1 = 1 or (col2 = 1 or col2 = 2 ) or col1 = 3; +``` + +* **Suggest**: + +```sql +select country_id from city where (col2 in (1, 2)) or col1 in (1, 3); +``` +## dmlorderby +* **Description**:删除DML更新操作中无意义的ORDER BY + +* **Original**: + +```sql +DELETE FROM tbl WHERE col1=1 ORDER BY col +``` + +* **Suggest**: + +```sql +delete from tbl where col1 = 1 +``` +## distinctstar +* **Description**:DISTINCT *对有主键的表没有意义,可以将DISTINCT删掉 + +* **Original**: + +```sql +SELECT DISTINCT * FROM film; +``` + +* **Suggest**: + +```sql +SELECT * FROM film +``` +## standard +* **Description**:SQL标准化,如:关键字转换为小写 + +* **Original**: + +```sql +SELECT sum(col1) FROM tbl GROUP BY 1; +``` + +* **Suggest**: + +```sql +select sum(col1) from tbl group by 1 +``` +## mergealter +* **Description**:合并同一张表的多条ALTER语句 + +* **Original**: + +```sql +ALTER TABLE t2 DROP COLUMN c;ALTER TABLE t2 DROP COLUMN d; +``` + +* **Suggest**: + +```sql +ALTER TABLE t2 DROP COLUMN c, DROP COLUMN d; +``` +## alwaystrue +* **Description**:删除无用的恒真判断条件 + +* **Original**: + +```sql +SELECT count(col) FROM tbl where 'a'= 'a' or ('b' = 'b' and a = 'b'); +``` + +* **Suggest**: + +```sql +select count(col) from tbl where (a = 'b'); +``` +## countstar +* **Description**:不建议使用COUNT(col)或COUNT(常量),建议改写为COUNT(*) + +* **Original**: + +```sql +SELECT count(col) FROM tbl GROUP BY 1; +``` + +* **Suggest**: + +```sql +SELECT count(*) FROM tbl GROUP BY 1; +``` +## innodb +* **Description**:建表时建议使用InnoDB引擎,非InnoDB引擎表自动转InnoDB + +* **Original**: + +```sql +CREATE TABLE t1(id bigint(20) NOT NULL AUTO_INCREMENT); +``` + +* **Suggest**: + +```sql +create table t1 ( + id bigint(20) not null auto_increment +) ENGINE=InnoDB; +``` +## autoincrement +* **Description**:将autoincrement初始化为1 + +* **Original**: + +```sql +CREATE TABLE t1(id bigint(20) NOT NULL AUTO_INCREMENT) ENGINE=InnoDB AUTO_INCREMENT=123802; +``` + +* **Suggest**: + +```sql +create table t1(id bigint(20) not null auto_increment) ENGINE=InnoDB auto_increment=1; +``` +## intwidth +* **Description**:整型数据类型修改默认显示宽度 + +* **Original**: + +```sql +create table t1 (id int(20) not null auto_increment) ENGINE=InnoDB; +``` + +* **Suggest**: + +```sql +create table t1 (id int(10) not null auto_increment) ENGINE=InnoDB; +``` +## truncate +* **Description**:不带WHERE条件的DELETE操作建议修改为TRUNCATE + +* **Original**: + +```sql +DELETE FROM tbl +``` + +* **Suggest**: + +```sql +truncate table tbl +``` +## rmparenthesis +* **Description**:去除没有意义的括号 + +* **Original**: + +```sql +select col from table where (col = 1); +``` + +* **Suggest**: + +```sql +select col from table where col = 1; +``` +## delimiter +* **Description**:补全DELIMITER + +* **Original**: + +```sql +use sakila +``` + +* **Suggest**: + +```sql +use sakila; +``` diff --git a/doc/roadmap.md b/doc/roadmap.md new file mode 100644 index 00000000..0390824b --- /dev/null +++ b/doc/roadmap.md @@ -0,0 +1,9 @@ +## 路线图 + +* 语法支持方面,目前主要依赖vitess,TiDB对SQL语法的支持。 +* 目前仅针对MySQL语法族进行开发和测试,其他使用SQL的数据库产品暂不支持。 +* Profiling和Trace功能有待深入挖掘,供经验丰富的DBA分析使用。 +* 目前尚不支持直接线上自动执行评审通过的SQL,后续会努力支持。 +* 由于暂不支持线上自动执行,因此数据备份功能也未提供。 +* Vim, Sublime, Emacs等编辑器插件支持。 +* Currently, only support Chinese suggestion, if you can help us add multi-language support, it will be greatly appreciated. diff --git a/doc/structure.md b/doc/structure.md new file mode 100644 index 00000000..02be0bf9 --- /dev/null +++ b/doc/structure.md @@ -0,0 +1,51 @@ + +# 体系架构 + +![架构图](http://github.com/XiaoMi/soar/raw/master/doc/images/structure.png) + +SOAR主要由语法解析器,集成环境,优化建议,重写逻辑,工具集五大模块组成。下面将对每个模块的作用及设计实现进行简述,更详细的算法及逻辑会在各个独立章节中详细讲解。 + +## 语法解析和语法检查 + +一条SQL从文件,标准输入或命令行参数等形式传递给SOAR后首先进入语法解析器,这里一开始我们选用了vitess的语法解析库作为SOAR的语法解析库,但随时需求的不断增加我们发现有些复杂需求使用vitess的语法解析实现起来比较逻辑比较复杂。于是参考业办其他数据库产品,我们引入了TiDB的语法解析器做为补充。我们发现这两个解析库还存在一定的盲区,于是又引入了MySQL执行返回结果作为多多版本SQL方言的补充。大家也可以看到在语法解析器这里,SOAR的实现方案是松散的、可插拔的。SOAR并不直接维护庞大的语法解析库,它把各种优秀的语法解析库集成在一起,各取所长。 + +## 集成环境 + +集成环境区分`线上环境`和`测试环境`两种,分别用于解决不同场景下用户的SQL优化需求。一种常见的情况是已有表结构需要优化查询SQL的场景,可以从线上环境导出表结构和足够的采样数据到测试环境,在测试环境上就可以放心的执行各种高危操作而不用担心数据被损坏。另一种常见的情况是建一套全新的数据库,需要验证提供的数据字典中是否存在优化的可能。对于这种情况,很有可能你不需要知道线上环境在哪儿,完全只是想先试试看,如果报错了马上改对就是了。当然还有更多种组合的场景需求,将在[集成环境](http://github.com/XiaoMi/soar/raw/master/doc/enviorment.md)一单分类说明。 + +## 优化建议 + +目前SOAR可以提供的优化建议有基于启发式规则(通常也称之为经验)的优化建议,基于索引优化算法给出的索引优化建议,以及基于EXPLAIN信息给出的解读。 + +### 启发式规则建议 + +下面这段代码是启发式规则的的元数据结构,它由规则代号,危险等级,规则摘要,规则解释,SQL示例,建议位置,规则函数等7部分组成。每一条SQL经过语法解析后会经过数百个启发式规则的逐一检查,命中了的规则将会保存在一个叫heuristicSuggest的变量中传递下去,与其他优化建议合并输出。这里最核心的部分,也是代码最多的部分在heuristic.go,里面包含了所有的启发式规则实现的函数。所有的启发式规则列表保存在rules.go文件中。 + +```Golang +// Rule 评审规则元数据结构 +type Rule struct { + Item string `json:"Item"` // 规则代号 + Severity string `json:"Severity"` // 危险等级:L[0-8], 数字越大表示级别越高 + Summary string `json:"Summary"` // 规则摘要 + Content string `json:"Content"` // 规则解释 + Case string `json:"Case"` // SQL示例 + Position int `json:"Position"` // 建议所处SQL字符位置,默认0表示全局建议 + Func func(*Query4Audit) Rule `json:"-"` // 函数名 +} +``` + +### 索引优化 + +关于索引优化,数据库经过几十年的发展,DBA沉淀了很多宝贵的经验,怎样把这些感性的经验转化为覆盖全面、逻辑可推导的算法是这种模块最大的挑战。很幸运的是SOAR并不是第一个尝试做这类算法整理的产品,有很多前人的著作、论文、博客等的知识储备。毫不夸张的说,为了写成这个模块我们读了不下5百万字的著作和论文,还不包括网络上各种大神的博客,这些老师们的知识结晶收集整理在[鸣谢](http://github.com/XiaoMi/soar/raw/master/doc/thanks.md)章节。使用到的算法在[索引优化](http://github.com/XiaoMi/soar/raw/master/doc/indexing.md)章节有详细的描述,虽然在某些算法理解上可能还存在一定争议,很希望与同行们共同讨论,共同进步,不断完善SOAR的算法。 + +### EXPLAIN解读 + +做过SQL优化的人对EXPLAIN应该都不陌生,但对于新手来说要记住每一个列代表什么含义,每个关键字背后的奥秘是什么需要足够的脑容量来记忆才行。统计了一下SOAR只在EXPLAIN信息的注解一项差不多写了200行代码,按平均行长度120计算,算下来一个DBA要精通EXPLAIN优化就要记住不下2万字的文档。SOAR能帮每为DBA节约了这部分脑容量。不过关于EXPLAIN解读还远不止这些,想了解更多可以参考[EXPLAIN信息解读](http://github.com/XiaoMi/soar/raw/master/doc/explain.md)章节。 + +## 重写逻辑 + +上面提到的优化建议是我们早期实现的主要功能,早期的功能还只是停留在建议上,对于一些初级用户看到建议也不一定会改写。为了进一步简化SQL优化的成本,SOAR又进一步挖掘了自动SQL重写的功能。现在提供几十种常见场景下的SQL等价转写,不过相比SQL优化建议还有很大的改进空间。这部分的功能和逻辑将在[重写逻辑](http://github.com/XiaoMi/soar/raw/master/doc/rewrite.md)一章中详细说明。 + +## 工具集 + +除了SQL优化和改写以外,为了方便用户使用以及美化输出展现形式,SOAR还提供了一些辅助的小工具,比如markdown转HTML工具,SQL格式化输出工具等等。你可以在[常用命令](http://github.com/XiaoMi/soar/raw/master/doc/cheatsheet.md)中找到这些小工具的使用方法。 diff --git a/doc/thanks.md b/doc/thanks.md new file mode 100644 index 00000000..a02440db --- /dev/null +++ b/doc/thanks.md @@ -0,0 +1,39 @@ +## 鸣谢 + +以下为SOAR的灵感及代码来源,我们站在伟人的肩膀上,让DBA的工作和生活更美好。 + +* [vitess](https://github.com/vitessio/vitess) +* [SQLAdvisor](https://github.com/Meituan-Dianping/SQLAdvisor) +* [pt-query-advisor](https://www.percona.com/doc/percona-toolkit/2.1/pt-query-advisor.html) +* [sqlcheck](https://github.com/jarulraj/sqlcheck) +* [pg_idx_advisor](https://github.com/cohenjo/pg_idx_advisor) +* [mysql-xplain-xplain](https://github.com/rap2hpoutre/mysql-xplain-xplain) +* [explain-analyzer](https://github.com/Preetam/explain-analyzer) +* [Explain](https://github.com/goghcrow/explain/blob/master/Explain.php) +* [sql-beautify](https://github.com/jkramer/sql-beautify) +* [go-mysql](https://github.com/percona/go-mysql) +* [pretty](https://github.com/kr/pretty) +* [golang_escape](https://github.com/liule/golang_escape) +* [mymysql](https://github.com/ziutek/mymysql) +* [beego/logs](https://github.com/astaxie/beego/logs) +* [uniuri](https://github.com/dchest/uniuri) +* [gjson](https://github.com/tidwall/gjson) + +## 参考博文 + +* [MySQL Reference Manual Chapter 8 Optimization](https://dev.mysql.com/doc/refman/8.0/en/optimization.html) +* [Indexing 101: Optimizing MySQL queries on a single table](https://www.percona.com/blog/2015/04/27/indexing-101-optimizing-mysql-queries-on-a-single-table/) +* [MySQL: Building the best INDEX for a given SELECT](http://mysql.rjweb.org/doc.php/index_cookbook_mysql) +* [MySQL INDEX Cookbook](http://mysql.rjweb.org/slides/cook.pdf) +* [Random Sampling for Histogram Construction: How much is enough?](http://www.mathcs.emory.edu/~cheung/papers/StreamDB/Histogram/1998-Chaudhuri-Histo.pdf) +* [10 Cool SQL Optimisations That do not Depend on the Cost Model](https://blog.jooq.org/2017/09/28/10-cool-sql-optimisations-that-do-not-depend-on-the-cost-model/) + +## 参考书目 + +* 《高性能MySQL》/《High Performance MySQL》 +* 《数据库索引设计与优化》/《Relational Database Index Design and the Optimizers》 +* 《数据库系统概论》/《Database System Concepts》 +* 《SQL反模式》/《SQL Antipatterns》 +* 《数据库查询优化器的艺术》/《The Art of Database Query Optimizer》 +* 《SQL优化最佳实践》/《SQL Optimization Best Practice》 +* 《SQL编程风格》/《Sql Programming Style》 diff --git a/doc/thanks_en.md b/doc/thanks_en.md new file mode 100644 index 00000000..06ae1255 --- /dev/null +++ b/doc/thanks_en.md @@ -0,0 +1,39 @@ +## Thanks + +以下为SOAR的灵感及代码来源,我们站在伟人的肩膀上,让DBA的工作和生活更美好。 + +* [vitess](https://github.com/vitessio/vitess) +* [SQLAdvisor](https://github.com/Meituan-Dianping/SQLAdvisor) +* [pt-query-advisor](https://www.percona.com/doc/percona-toolkit/2.1/pt-query-advisor.html) +* [sqlcheck](https://github.com/jarulraj/sqlcheck) +* [pg_idx_advisor](https://github.com/cohenjo/pg_idx_advisor) +* [mysql-xplain-xplain](https://github.com/rap2hpoutre/mysql-xplain-xplain) +* [explain-analyzer](https://github.com/Preetam/explain-analyzer) +* [Explain](https://github.com/goghcrow/explain/blob/master/Explain.php) +* [sql-beautify](https://github.com/jkramer/sql-beautify) +* [go-mysql](https://github.com/percona/go-mysql) +* [pretty](https://github.com/kr/pretty) +* [golang_escape](https://github.com/liule/golang_escape) +* [mymysql](https://github.com/ziutek/mymysql) +* [beego/logs](https://github.com/astaxie/beego/logs) +* [uniuri](https://github.com/dchest/uniuri) +* [gjson](https://github.com/tidwall/gjson) + +## Reference Articles + +* [MySQL Reference Manual Chapter 8 Optimization](https://dev.mysql.com/doc/refman/8.0/en/optimization.html) +* [Indexing 101: Optimizing MySQL queries on a single table](https://www.percona.com/blog/2015/04/27/indexing-101-optimizing-mysql-queries-on-a-single-table/) +* [MySQL: Building the best INDEX for a given SELECT](http://mysql.rjweb.org/doc.php/index_cookbook_mysql) +* [MySQL INDEX Cookbook](http://mysql.rjweb.org/slides/cook.pdf) +* [Random Sampling for Histogram Construction: How much is enough?](http://www.mathcs.emory.edu/~cheung/papers/StreamDB/Histogram/1998-Chaudhuri-Histo.pdf) +* [10 Cool SQL Optimisations That do not Depend on the Cost Model](https://blog.jooq.org/2017/09/28/10-cool-sql-optimisations-that-do-not-depend-on-the-cost-model/) + +## Books + +* 《高性能MySQL》/《High Performance MySQL》 +* 《数据库索引设计与优化》/《Relational Database Index Design and the Optimizers》 +* 《数据库系统概论》/《Database System Concepts》 +* 《SQL反模式》/《SQL Antipatterns》 +* 《数据库查询优化器的艺术》/《The Art of Database Query Optimizer》 +* 《SQL优化最佳实践》/《SQL Optimization Best Practice》 +* 《SQL编程风格》/《Sql Programming Style》 diff --git a/doc/themes/foghorn.css b/doc/themes/foghorn.css new file mode 100644 index 00000000..f8945572 --- /dev/null +++ b/doc/themes/foghorn.css @@ -0,0 +1,141 @@ + +html, body { + padding:1em; + margin:auto; + max-width:42em; + background:#fefefe; + } +body { + font: 1.3em "Vollkorn", Palatino, Times; + color: #333; + line-height: 1.4; + text-align: justify; + } +header, nav, article, footer { + width: 700px; + margin:0 auto; + } +article { + margin-top: 4em; + margin-bottom: 4em; + min-height: 400px; + } +footer { + margin-bottom:50px; + } +video { + margin: 2em 0; + border:1px solid #ddd; + } + +nav { + font-size: .9em; + font-style: italic; + border-bottom: 1px solid #ddd; + padding: 1em 0; + } +nav p { + margin: 0; + } + +/* Typography +-------------------------------------------------------- */ + +h1 { + margin-top: 0; + font-weight: normal; + } +h2 { + font-weight: normal; + } +h3 { + font-weight: normal; + font-style: italic; + margin-top:3em; + } +p { + margin-top:0; + -webkit-hypens:auto; + -moz-hypens:auto; + hyphens:auto; + } +ul { + list-style: square; + padding-left:1.2em; + } +ol { + padding-left:1.2em; + } +blockquote { + margin-left: 1em; + padding-left: 1em; + border-left: 1px solid #ddd; + } +code { + font-family: "Consolas", "Menlo", "Monaco", monospace, serif; + font-size: .9em; + background: white; + } +a { + color: #2484c1; + text-decoration: none; + } +a:hover { + text-decoration: underline; + } +a img { + border:none; + } +h1 a, h1 a:hover { + color: #333; + text-decoration: none; + } +hr { + color : #ddd; + height : 1px; + margin: 2em 0; + border-top : solid 1px #ddd; + border-bottom : none; + border-left: 0; + border-right: 0; + } +p#heart{ + font-size: 2em; + line-height: 1; + text-align: center; + color: #ccc; + } +.red { + color:#B50000; + } + +/* Home Page +--------------------------- */ + +body#index li { + margin-bottom: 1em; + } + + +/* iPad +-------------------------------------------------------- */ +@media only screen and (max-device-width: 1024px) { +body { + font-size: 120%; + line-height: 1.4; + } +} /* @iPad */ + +/* iPhone +-------------------------------------------------------- */ +@media only screen and (max-device-width: 480px) { +body { + text-align: left; + } +article, footer { + width: auto; + } +article { + padding: 0 10px; + } +} /* @iPhone */ diff --git a/doc/themes/ghostwriter.css b/doc/themes/ghostwriter.css new file mode 100644 index 00000000..0e41dffe --- /dev/null +++ b/doc/themes/ghostwriter.css @@ -0,0 +1,413 @@ +/* ============================================================ */ +/* Base */ +/* ============================================================ */ +html, body { + height: 100%; +} + +body { + background: #fefefe; + color: #424242; + font-family: "Open Sans", arial, sans-serif; + font-size: 18px; +} + +h1, h2, h3, h4, h5, h6 { + margin-bottom: 33px; + text-transform: none; +} + +h1 { + font-size: 26px; +} + +h2 { + font-size: 24px; +} + +h3 { + font-size: 20px; + margin-bottom: 20px; +} + +h4 { + font-size: 18px; + margin-bottom: 18px; +} + +h5 { + font-size: 16px; + margin-bottom: 15px; +} + +h6 { + font-size: 14px; + margin-bottom: 12px; +} + +p { + line-height: 1.8; + margin: 0 0 30px; +} + +a { + color: #f03838; + text-decoration: none; +} + +ul, ol { + list-style-position: inside; + line-height: 1.8; + margin: 0 0 40px; + padding: 0; +} +ul li, ol li { + margin: 0 0 10px; +} + +blockquote { + border-left: 1px dotted #303030; + margin: 40px 0; + padding: 5px 30px; +} +blockquote p { + color: #AEADAD; + display: block; + font-style: italic; + margin: 0; + width: 100%; +} + +img { + display: block; + margin: 40px 0; + width: auto; + max-width: 100%; +} + +pre { + background: #F1F0EA; + border: 1px solid #DDDBCC; + border-radius: 3px; + margin: 0 0 40px; + padding: 15px 20px; +} + +::selection { + background: #FFF5B8; + color: #000; + display: block; +} + +::-moz-selection { + background: #FFF5B8; + color: #000; + display: block; +} + +/* ============================================================ */ +/* General Appearance */ +/* ============================================================ */ +.container { + margin: 0 auto; + position: relative; + width: 100%; + max-width: 889px; +} + +#wrapper { + height: auto; + min-height: 100%; + /* This must be the same as the height of the footer */ + margin-bottom: -265px; +} +#wrapper:after { + content: ""; + display: block; + /* This must be the same as the height of the footer */ + height: 265px; +} + +.button { + background: #303030; + border: none; + border-radius: 3px; + color: #FEFEFE; + font-size: 14px; + font-weight: 700; + padding: 10px 12px; + text-transform: uppercase; +} +.button:hover { + background: #f03838; +} + +.button-square { + background: #f03838; + float: left; + margin: 0 0 0 10px; + padding: 8px; +} +.button-square:hover { + background: #303030; +} + +/* ============================================================ */ +/* Site Header */ +/* ============================================================ */ +.site-header { + padding: 100px 0 35px; + overflow: auto; + text-align: center; + text-transform: uppercase; +} + +.site-title-wrapper { + display: table; + margin: 0 auto; +} + +.site-title { + float: left; + font-size: 14px; + font-weight: 600; + margin: 0; + text-transform: uppercase; +} +.site-title a { + float: left; + background: #f03838; + color: #FEFEFE; + padding: 5px 10px 6px; +} +.site-title a:hover { + background: #303030; +} + +/* ============================================================ */ +/* Post */ +/* ============================================================ */ +.post { + margin: 0 40px; +} + +.post-header { + border-bottom: 6px solid #303030; + margin: 0 0 50px; + padding: 0 0 80px; + text-align: center; + text-transform: uppercase; +} + +.post-title { + font-size: 52px; + font-weight: 700; + margin: 15px 0; + text-transform: uppercase; +} + +.post-date { + color: #AEADAD; + font-size: 14px; + font-weight: 600; + line-height: 1; + margin: 25px 0 0; +} +.post-date:after { + border-bottom: 1px dotted #303030; + content: ""; + display: block; + margin: 40px auto 0; + width: 100px; +} + +.post-content { + margin: 0 0 92px; +} +.post-content a:hover { + border-bottom: 1px dotted #f03838; + padding: 0 0 2px; +} + +.post-tags { + color: #AEADAD; + font-size: 14px; +} +.post-tags span { + font-weight: 600; +} + +.post-navigation { + display: table; + margin: 70px auto 100px; +} + +.newer-posts, +.older-posts { + float: left; + background: #f03838; + color: #FEFEFE; + font-size: 14px; + font-weight: 600; + margin: 0 5px; + padding: 5px 10px 6px; + text-transform: uppercase; +} +.newer-posts:hover, +.older-posts:hover { + background: #303030; +} + +.page-number { + display: none; +} + +/* ============================================================ */ +/* Post Index */ +/* ============================================================ */ +.post-list { + border-top: 6px solid #303030; + list-style: none; + margin: 80px 40px 0; + padding: 35px 0 0; +} + +.post-stub { + border-bottom: 1px dotted #303030; + margin: 0; +} +.post-stub:first-child { + padding-top: 0; +} +.post-stub a { + -webkit-transition: all 0.2s ease-in-out; + -moz-transition: all 0.2s ease-in-out; + transition: all 0.2s ease-in-out; + display: block; + color: #424242; + padding: 20px 5px; +} +.post-stub a:hover { + background: #FCF5F5; + color: #f03838; + padding: 20px 12px; +} + +.post-stub-title { + display: inline-block; + margin: 0; + text-transform: none; +} + +.post-stub-date { + display: inline-block; +} +.post-stub-date:before { + content: "/ "; +} + +.next-posts-link a, +.previous-posts-link a { + display: block; + padding: 8px 11px; +} + +/* ============================================================ */ +/* Icons */ +/* ============================================================ */ +.icon { + background-size: 14px 38px; + display: block; + height: 38px; + width: 14px; +} + +.icon-menu { + background-position: 0 0; + height: 14px; + width: 14px; +} + +.icon-up { + background-position: 0 -15px; + height: 8px; + width: 14px; +} + +.icon-rss { + background-position: 0 -24px; + height: 14px; + width: 14px; +} + +/* ============================================================ */ +/* Footer */ +/* ============================================================ */ +.footer { + background: #303030; + color: #D3D3D3; + height: 265px; + overflow: auto; +} +.footer .site-title-wrapper { + margin: 80px auto 35px; +} +.footer .site-title a:hover, +.footer .button-square:hover { + background: #121212; +} + +.button-jump-top { + padding-top: 11px; + padding-bottom: 11px; +} + +.footer-copyright { + color: #656565; + font-size: 14px; + margin: 0; + text-align: center; + text-transform: uppercase; +} +.footer-copyright a { + color: #656565; + font-weight: 700; +} +.footer-copyright a:hover { + color: #FEFEFE; +} + +/* ============================================================ */ +/* NProgress */ +/* ============================================================ */ +#nprogress .bar { + background: #f03838; +} + +#nprogress .peg { + box-shadow: 0 0 10px #f03838, 0 0 5px #f03838; +} + +#nprogress .spinner-icon { + border-top-color: #f03838; + border-left-color: #f03838; +} + +/* ============================================================ */ +/* Media Queries */ +/* ============================================================ */ +@media only screen and (max-width: 600px) { + .post-stub-title { + display: block; + } + + .post-stub-date:before { + content: ""; + display: block; + } +} +@media only screen and (max-width: 400px) { + .post-title { + font-size: 32px; + } +} diff --git a/doc/themes/github-dark.css b/doc/themes/github-dark.css new file mode 100644 index 00000000..f41386ea --- /dev/null +++ b/doc/themes/github-dark.css @@ -0,0 +1,765 @@ +@font-face { + font-family: octicons-link; + src: url(data:font/woff;charset=utf-8;base64,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) format('woff'); +} + +.markdown-body { + -ms-text-size-adjust: 100%; + -webkit-text-size-adjust: 100%; + line-height: 1.5; + color: #24292e; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; + font-size: 16px; + line-height: 1.5; + word-wrap: break-word; +} + +.markdown-body .pl-c { + color: #6a737d; +} + +.markdown-body .pl-c1, +.markdown-body .pl-s .pl-v { + color: #005cc5; +} + +.markdown-body .pl-e, +.markdown-body .pl-en { + color: #6f42c1; +} + +.markdown-body .pl-smi, +.markdown-body .pl-s .pl-s1 { + color: #24292e; +} + +.markdown-body .pl-ent { + color: #22863a; +} + +.markdown-body .pl-k { + color: #d73a49; +} + +.markdown-body .pl-s, +.markdown-body .pl-pds, +.markdown-body .pl-s .pl-pse .pl-s1, +.markdown-body .pl-sr, +.markdown-body .pl-sr .pl-cce, +.markdown-body .pl-sr .pl-sre, +.markdown-body .pl-sr .pl-sra { + color: #032f62; +} + +.markdown-body .pl-v, +.markdown-body .pl-smw { + color: #e36209; +} + +.markdown-body .pl-bu { + color: #b31d28; +} + +.markdown-body .pl-ii { + color: #fafbfc; + background-color: #b31d28; +} + +.markdown-body .pl-c2 { + color: #fafbfc; + background-color: #d73a49; +} + +.markdown-body .pl-c2::before { + content: "^M"; +} + +.markdown-body .pl-sr .pl-cce { + font-weight: bold; + color: #22863a; +} + +.markdown-body .pl-ml { + color: #735c0f; +} + +.markdown-body .pl-mh, +.markdown-body .pl-mh .pl-en, +.markdown-body .pl-ms { + font-weight: bold; + color: #005cc5; +} + +.markdown-body .pl-mi { + font-style: italic; + color: #24292e; +} + +.markdown-body .pl-mb { + font-weight: bold; + color: #24292e; +} + +.markdown-body .pl-md { + color: #b31d28; + background-color: #ffeef0; +} + +.markdown-body .pl-mi1 { + color: #22863a; + background-color: #f0fff4; +} + +.markdown-body .pl-mc { + color: #e36209; + background-color: #ffebda; +} + +.markdown-body .pl-mi2 { + color: #f6f8fa; + background-color: #005cc5; +} + +.markdown-body .pl-mdr { + font-weight: bold; + color: #6f42c1; +} + +.markdown-body .pl-ba { + color: #586069; +} + +.markdown-body .pl-sg { + color: #959da5; +} + +.markdown-body .pl-corl { + text-decoration: underline; + color: #032f62; +} + +.markdown-body .octicon { + display: inline-block; + vertical-align: text-top; + fill: currentColor; +} + +.markdown-body a { + background-color: transparent; + -webkit-text-decoration-skip: objects; +} + +.markdown-body a:active, +.markdown-body a:hover { + outline-width: 0; +} + +.markdown-body strong { + font-weight: inherit; +} + +.markdown-body strong { + font-weight: bolder; +} + +.markdown-body h1 { + font-size: 2em; + margin: 0.67em 0; +} + +.markdown-body img { + border-style: none; +} + +.markdown-body svg:not(:root) { + overflow: hidden; +} + +.markdown-body code, +.markdown-body kbd, +.markdown-body pre { + font-family: monospace, monospace; + font-size: 1em; +} + +.markdown-body hr { + box-sizing: content-box; + height: 0; + overflow: visible; +} + +.markdown-body input { + font: inherit; + margin: 0; +} + +.markdown-body input { + overflow: visible; +} + +.markdown-body [type="checkbox"] { + box-sizing: border-box; + padding: 0; +} + +.markdown-body * { + box-sizing: border-box; +} + +.markdown-body input { + font-family: inherit; + font-size: inherit; + line-height: inherit; +} + +.markdown-body a { + color: #0366d6; + text-decoration: none; +} + +.markdown-body a:hover { + text-decoration: underline; +} + +.markdown-body strong { + font-weight: 600; +} + +.markdown-body hr { + height: 0; + margin: 15px 0; + overflow: hidden; + background: transparent; + border: 0; + border-bottom: 1px solid #dfe2e5; +} + +.markdown-body hr::before { + display: table; + content: ""; +} + +.markdown-body hr::after { + display: table; + clear: both; + content: ""; +} + +.markdown-body table { + border-spacing: 0; + border-collapse: collapse; +} + +.markdown-body td, +.markdown-body th { + padding: 0; +} + +.markdown-body h1, +.markdown-body h2, +.markdown-body h3, +.markdown-body h4, +.markdown-body h5, +.markdown-body h6 { + margin-top: 0; + margin-bottom: 0; +} + +.markdown-body h1 { + font-size: 32px; + font-weight: 600; +} + +.markdown-body h2 { + font-size: 24px; + font-weight: 600; +} + +.markdown-body h3 { + font-size: 20px; + font-weight: 600; +} + +.markdown-body h4 { + font-size: 16px; + font-weight: 600; +} + +.markdown-body h5 { + font-size: 14px; + font-weight: 600; +} + +.markdown-body h6 { + font-size: 12px; + font-weight: 600; +} + +.markdown-body p { + margin-top: 0; + margin-bottom: 10px; +} + +.markdown-body blockquote { + margin: 0; +} + +.markdown-body ul, +.markdown-body ol { + padding-left: 0; + margin-top: 0; + margin-bottom: 0; +} + +.markdown-body ol ol, +.markdown-body ul ol { + list-style-type: lower-roman; +} + +.markdown-body ul ul ol, +.markdown-body ul ol ol, +.markdown-body ol ul ol, +.markdown-body ol ol ol { + list-style-type: lower-alpha; +} + +.markdown-body dd { + margin-left: 0; +} + +.markdown-body code { + font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace; + font-size: 12px; +} + +.markdown-body pre { + margin-top: 0; + margin-bottom: 0; + font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace; + font-size: 12px; +} + +.markdown-body .octicon { + vertical-align: text-bottom; +} + +.markdown-body .pl-0 { + padding-left: 0 !important; +} + +.markdown-body .pl-1 { + padding-left: 4px !important; +} + +.markdown-body .pl-2 { + padding-left: 8px !important; +} + +.markdown-body .pl-3 { + padding-left: 16px !important; +} + +.markdown-body .pl-4 { + padding-left: 24px !important; +} + +.markdown-body .pl-5 { + padding-left: 32px !important; +} + +.markdown-body .pl-6 { + padding-left: 40px !important; +} + +.markdown-body::before { + display: table; + content: ""; +} + +.markdown-body::after { + display: table; + clear: both; + content: ""; +} + +.markdown-body>*:first-child { + margin-top: 0 !important; +} + +.markdown-body>*:last-child { + margin-bottom: 0 !important; +} + +.markdown-body a:not([href]) { + color: inherit; + text-decoration: none; +} + +.markdown-body .anchor { + float: left; + padding-right: 4px; + margin-left: -20px; + line-height: 1; +} + +.markdown-body .anchor:focus { + outline: none; +} + +.markdown-body p, +.markdown-body blockquote, +.markdown-body ul, +.markdown-body ol, +.markdown-body dl, +.markdown-body table, +.markdown-body pre { + margin-top: 0; + margin-bottom: 16px; +} + +.markdown-body hr { + height: 0.25em; + padding: 0; + margin: 24px 0; + background-color: #e1e4e8; + border: 0; +} + +.markdown-body blockquote { + padding: 0 1em; + color: #6a737d; + border-left: 0.25em solid #dfe2e5; +} + +.markdown-body blockquote>:first-child { + margin-top: 0; +} + +.markdown-body blockquote>:last-child { + margin-bottom: 0; +} + +.markdown-body kbd { + display: inline-block; + padding: 3px 5px; + font-size: 11px; + line-height: 10px; + color: #444d56; + vertical-align: middle; + background-color: #fafbfc; + border: solid 1px #c6cbd1; + border-bottom-color: #959da5; + border-radius: 3px; + box-shadow: inset 0 -1px 0 #959da5; +} + +.markdown-body h1, +.markdown-body h2, +.markdown-body h3, +.markdown-body h4, +.markdown-body h5, +.markdown-body h6 { + margin-top: 24px; + margin-bottom: 16px; + font-weight: 600; + line-height: 1.25; +} + +.markdown-body h1 .octicon-link, +.markdown-body h2 .octicon-link, +.markdown-body h3 .octicon-link, +.markdown-body h4 .octicon-link, +.markdown-body h5 .octicon-link, +.markdown-body h6 .octicon-link { + color: #1b1f23; + vertical-align: middle; + visibility: hidden; +} + +.markdown-body h1:hover .anchor, +.markdown-body h2:hover .anchor, +.markdown-body h3:hover .anchor, +.markdown-body h4:hover .anchor, +.markdown-body h5:hover .anchor, +.markdown-body h6:hover .anchor { + text-decoration: none; +} + +.markdown-body h1:hover .anchor .octicon-link, +.markdown-body h2:hover .anchor .octicon-link, +.markdown-body h3:hover .anchor .octicon-link, +.markdown-body h4:hover .anchor .octicon-link, +.markdown-body h5:hover .anchor .octicon-link, +.markdown-body h6:hover .anchor .octicon-link { + visibility: visible; +} + +.markdown-body h1 { + padding-bottom: 0.3em; + font-size: 2em; + border-bottom: 1px solid #eaecef; +} + +.markdown-body h2 { + padding-bottom: 0.3em; + font-size: 1.5em; + border-bottom: 1px solid #eaecef; +} + +.markdown-body h3 { + font-size: 1.25em; +} + +.markdown-body h4 { + font-size: 1em; +} + +.markdown-body h5 { + font-size: 0.875em; +} + +.markdown-body h6 { + font-size: 0.85em; + color: #6a737d; +} + +.markdown-body ul, +.markdown-body ol { + padding-left: 2em; +} + +.markdown-body ul ul, +.markdown-body ul ol, +.markdown-body ol ol, +.markdown-body ol ul { + margin-top: 0; + margin-bottom: 0; +} + +.markdown-body li>p { + margin-top: 16px; +} + +.markdown-body li+li { + margin-top: 0.25em; +} + +.markdown-body dl { + padding: 0; +} + +.markdown-body dl dt { + padding: 0; + margin-top: 16px; + font-size: 1em; + font-style: italic; + font-weight: 600; +} + +.markdown-body dl dd { + padding: 0 16px; + margin-bottom: 16px; +} + +.markdown-body table { + display: block; + width: 100%; + overflow: auto; +} + +.markdown-body table th { + font-weight: 600; +} + +.markdown-body table th, +.markdown-body table td { + padding: 6px 13px; + border: 1px solid #dfe2e5; +} + +.markdown-body table tr { + background-color: #fff; + border-top: 1px solid #c6cbd1; +} + +.markdown-body table tr:nth-child(2n) { + background-color: #f6f8fa; +} + +.markdown-body img { + max-width: 100%; + box-sizing: content-box; + background-color: #fff; +} + +.markdown-body img[align=right] { + padding-left: 20px; +} + +.markdown-body img[align=left] { + padding-right: 20px; +} + +.markdown-body code { + padding: 0; + padding-top: 0.2em; + padding-bottom: 0.2em; + margin: 0; + font-size: 85%; + background-color: rgba(27,31,35,0.05); + border-radius: 3px; +} + +.markdown-body code::before, +.markdown-body code::after { + letter-spacing: -0.2em; + content: "\00a0"; +} + +.markdown-body pre { + word-wrap: normal; +} + +.markdown-body pre>code { + padding: 0; + margin: 0; + font-size: 100%; + word-break: normal; + white-space: pre; + background: transparent; + border: 0; +} + +.markdown-body .highlight { + margin-bottom: 16px; +} + +.markdown-body .highlight pre { + margin-bottom: 0; + word-break: normal; +} + +.markdown-body .highlight pre, +.markdown-body pre { + padding: 16px; + overflow: auto; + font-size: 85%; + line-height: 1.45; + background-color: #f6f8fa; + border-radius: 3px; +} + +.markdown-body pre code { + display: inline; + max-width: auto; + padding: 0; + margin: 0; + overflow: visible; + line-height: inherit; + word-wrap: normal; + background-color: transparent; + border: 0; +} + +.markdown-body pre code::before, +.markdown-body pre code::after { + content: normal; +} + +.markdown-body .full-commit .btn-outline:not(:disabled):hover { + color: #005cc5; + border-color: #005cc5; +} + +.markdown-body kbd { + display: inline-block; + padding: 3px 5px; + font: 11px "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace; + line-height: 10px; + color: #444d56; + vertical-align: middle; + background-color: #fafbfc; + border: solid 1px #d1d5da; + border-bottom-color: #c6cbd1; + border-radius: 3px; + box-shadow: inset 0 -1px 0 #c6cbd1; +} + +.markdown-body :checked+.radio-label { + position: relative; + z-index: 1; + border-color: #0366d6; +} + +.markdown-body .task-list-item { + list-style-type: none; +} + +.markdown-body .task-list-item+.task-list-item { + margin-top: 3px; +} + +.markdown-body .task-list-item input { + margin: 0 0.2em 0.25em -1.6em; + vertical-align: middle; +} + +.markdown-body hr { + border-bottom-color: #eee; +} + +/*Markdown Viewer*/ +.markdown-body summary:hover { cursor: pointer; } +.markdown-body ul li p { margin: 0; } + +/*GitHub Dark*/ + +body { + background: #181818; +} + +.markdown-body { + color: #c0c0c0 !important; + background: #181818 !important; + border-color: #484848 !important; +} +.markdown-body table { color: #c0c0c0 !important; } +.markdown-body table th { border-color: #343434 !important; } +.markdown-body table td { border-color: #343434 !important; } +.markdown-body table tr { + background: #141414 !important; + border-color: #343434 !important; +} +.markdown-body table tr:nth-child(2n) { background: #181818 !important; } +.markdown-body hr { background: #383838 !important; } +.markdown-body h1, +.markdown-body h2 { border-color: #343434 !important; } +.markdown-body h1, .markdown-body h2, +.markdown-body h3, .markdown-body h4, +.markdown-body .octicon-link { color: #e0e0e0 !important; } +.markdown-body blockquote strong { color: #808080 !important; } +.markdown-body blockquote { border-color: #343434 !important; } +.markdown-body blockquote, +.markdown-body blockquote code { color: #666 !important; } +.markdown-body code, .markdown-body tt, .markdown-body pre, +.markdown-body .highlight pre, body.blog pre { + border: 1px solid rgba(255,255,255,.1) !important; +} +.markdown-body code, .markdown-body tt { background: #202020 !important; } +.markdown-body pre { + background: #141414 !important; color: #ccc !important; +} +.markdown-body pre code { background: none !important; border: 0 !important; } +.markdown-body code[class*="language-"] { + color: #c0c0c0 !important; + text-shadow: none !important; +} +.markdown-body code[class*="language-"] .operator, +.markdown-body code[class*="language-"] .string { + background: none !important; +} +.markdown-body a[href*="/labels/"], +.markdown-body a:not([href*="/labels/"]), +.markdown-body blockquote a code { color: #4183C4 !important; } + +.markdown-body summary:hover { cursor: pointer; } diff --git a/doc/themes/github.css b/doc/themes/github.css new file mode 100644 index 00000000..697938e9 --- /dev/null +++ b/doc/themes/github.css @@ -0,0 +1,713 @@ +@font-face { + font-family: octicons-link; + src: url(data:font/woff;charset=utf-8;base64,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) format('woff'); +} + +.markdown-body { + -ms-text-size-adjust: 100%; + -webkit-text-size-adjust: 100%; + line-height: 1.5; + color: #24292e; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; + font-size: 16px; + line-height: 1.5; + word-wrap: break-word; +} + +.markdown-body .pl-c { + color: #6a737d; +} + +.markdown-body .pl-c1, +.markdown-body .pl-s .pl-v { + color: #005cc5; +} + +.markdown-body .pl-e, +.markdown-body .pl-en { + color: #6f42c1; +} + +.markdown-body .pl-smi, +.markdown-body .pl-s .pl-s1 { + color: #24292e; +} + +.markdown-body .pl-ent { + color: #22863a; +} + +.markdown-body .pl-k { + color: #d73a49; +} + +.markdown-body .pl-s, +.markdown-body .pl-pds, +.markdown-body .pl-s .pl-pse .pl-s1, +.markdown-body .pl-sr, +.markdown-body .pl-sr .pl-cce, +.markdown-body .pl-sr .pl-sre, +.markdown-body .pl-sr .pl-sra { + color: #032f62; +} + +.markdown-body .pl-v, +.markdown-body .pl-smw { + color: #e36209; +} + +.markdown-body .pl-bu { + color: #b31d28; +} + +.markdown-body .pl-ii { + color: #fafbfc; + background-color: #b31d28; +} + +.markdown-body .pl-c2 { + color: #fafbfc; + background-color: #d73a49; +} + +.markdown-body .pl-c2::before { + content: "^M"; +} + +.markdown-body .pl-sr .pl-cce { + font-weight: bold; + color: #22863a; +} + +.markdown-body .pl-ml { + color: #735c0f; +} + +.markdown-body .pl-mh, +.markdown-body .pl-mh .pl-en, +.markdown-body .pl-ms { + font-weight: bold; + color: #005cc5; +} + +.markdown-body .pl-mi { + font-style: italic; + color: #24292e; +} + +.markdown-body .pl-mb { + font-weight: bold; + color: #24292e; +} + +.markdown-body .pl-md { + color: #b31d28; + background-color: #ffeef0; +} + +.markdown-body .pl-mi1 { + color: #22863a; + background-color: #f0fff4; +} + +.markdown-body .pl-mc { + color: #e36209; + background-color: #ffebda; +} + +.markdown-body .pl-mi2 { + color: #f6f8fa; + background-color: #005cc5; +} + +.markdown-body .pl-mdr { + font-weight: bold; + color: #6f42c1; +} + +.markdown-body .pl-ba { + color: #586069; +} + +.markdown-body .pl-sg { + color: #959da5; +} + +.markdown-body .pl-corl { + text-decoration: underline; + color: #032f62; +} + +.markdown-body .octicon { + display: inline-block; + vertical-align: text-top; + fill: currentColor; +} + +.markdown-body a { + background-color: transparent; + -webkit-text-decoration-skip: objects; +} + +.markdown-body a:active, +.markdown-body a:hover { + outline-width: 0; +} + +.markdown-body strong { + font-weight: inherit; +} + +.markdown-body strong { + font-weight: bolder; +} + +.markdown-body h1 { + font-size: 2em; + margin: 0.67em 0; +} + +.markdown-body img { + border-style: none; +} + +.markdown-body svg:not(:root) { + overflow: hidden; +} + +.markdown-body code, +.markdown-body kbd, +.markdown-body pre { + font-family: monospace, monospace; + font-size: 1em; +} + +.markdown-body hr { + box-sizing: content-box; + height: 0; + overflow: visible; +} + +.markdown-body input { + font: inherit; + margin: 0; +} + +.markdown-body input { + overflow: visible; +} + +.markdown-body [type="checkbox"] { + box-sizing: border-box; + padding: 0; +} + +.markdown-body * { + box-sizing: border-box; +} + +.markdown-body input { + font-family: inherit; + font-size: inherit; + line-height: inherit; +} + +.markdown-body a { + color: #0366d6; + text-decoration: none; +} + +.markdown-body a:hover { + text-decoration: underline; +} + +.markdown-body strong { + font-weight: 600; +} + +.markdown-body hr { + height: 0; + margin: 15px 0; + overflow: hidden; + background: transparent; + border: 0; + border-bottom: 1px solid #dfe2e5; +} + +.markdown-body hr::before { + display: table; + content: ""; +} + +.markdown-body hr::after { + display: table; + clear: both; + content: ""; +} + +.markdown-body table { + border-spacing: 0; + border-collapse: collapse; +} + +.markdown-body td, +.markdown-body th { + padding: 0; +} + +.markdown-body h1, +.markdown-body h2, +.markdown-body h3, +.markdown-body h4, +.markdown-body h5, +.markdown-body h6 { + margin-top: 0; + margin-bottom: 0; +} + +.markdown-body h1 { + font-size: 32px; + font-weight: 600; +} + +.markdown-body h2 { + font-size: 24px; + font-weight: 600; +} + +.markdown-body h3 { + font-size: 20px; + font-weight: 600; +} + +.markdown-body h4 { + font-size: 16px; + font-weight: 600; +} + +.markdown-body h5 { + font-size: 14px; + font-weight: 600; +} + +.markdown-body h6 { + font-size: 12px; + font-weight: 600; +} + +.markdown-body p { + margin-top: 0; + margin-bottom: 10px; +} + +.markdown-body blockquote { + margin: 0; +} + +.markdown-body ul, +.markdown-body ol { + padding-left: 0; + margin-top: 0; + margin-bottom: 0; +} + +.markdown-body ol ol, +.markdown-body ul ol { + list-style-type: lower-roman; +} + +.markdown-body ul ul ol, +.markdown-body ul ol ol, +.markdown-body ol ul ol, +.markdown-body ol ol ol { + list-style-type: lower-alpha; +} + +.markdown-body dd { + margin-left: 0; +} + +.markdown-body code { + font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace; + font-size: 12px; +} + +.markdown-body pre { + margin-top: 0; + margin-bottom: 0; + font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace; + font-size: 12px; +} + +.markdown-body .octicon { + vertical-align: text-bottom; +} + +.markdown-body .pl-0 { + padding-left: 0 !important; +} + +.markdown-body .pl-1 { + padding-left: 4px !important; +} + +.markdown-body .pl-2 { + padding-left: 8px !important; +} + +.markdown-body .pl-3 { + padding-left: 16px !important; +} + +.markdown-body .pl-4 { + padding-left: 24px !important; +} + +.markdown-body .pl-5 { + padding-left: 32px !important; +} + +.markdown-body .pl-6 { + padding-left: 40px !important; +} + +.markdown-body::before { + display: table; + content: ""; +} + +.markdown-body::after { + display: table; + clear: both; + content: ""; +} + +.markdown-body>*:first-child { + margin-top: 0 !important; +} + +.markdown-body>*:last-child { + margin-bottom: 0 !important; +} + +.markdown-body a:not([href]) { + color: inherit; + text-decoration: none; +} + +.markdown-body .anchor { + float: left; + padding-right: 4px; + margin-left: -20px; + line-height: 1; +} + +.markdown-body .anchor:focus { + outline: none; +} + +.markdown-body p, +.markdown-body blockquote, +.markdown-body ul, +.markdown-body ol, +.markdown-body dl, +.markdown-body table, +.markdown-body pre { + margin-top: 0; + margin-bottom: 16px; +} + +.markdown-body hr { + height: 0.25em; + padding: 0; + margin: 24px 0; + background-color: #e1e4e8; + border: 0; +} + +.markdown-body blockquote { + padding: 0 1em; + color: #6a737d; + border-left: 0.25em solid #dfe2e5; +} + +.markdown-body blockquote>:first-child { + margin-top: 0; +} + +.markdown-body blockquote>:last-child { + margin-bottom: 0; +} + +.markdown-body kbd { + display: inline-block; + padding: 3px 5px; + font-size: 11px; + line-height: 10px; + color: #444d56; + vertical-align: middle; + background-color: #fafbfc; + border: solid 1px #c6cbd1; + border-bottom-color: #959da5; + border-radius: 3px; + box-shadow: inset 0 -1px 0 #959da5; +} + +.markdown-body h1, +.markdown-body h2, +.markdown-body h3, +.markdown-body h4, +.markdown-body h5, +.markdown-body h6 { + margin-top: 24px; + margin-bottom: 16px; + font-weight: 600; + line-height: 1.25; +} + +.markdown-body h1 .octicon-link, +.markdown-body h2 .octicon-link, +.markdown-body h3 .octicon-link, +.markdown-body h4 .octicon-link, +.markdown-body h5 .octicon-link, +.markdown-body h6 .octicon-link { + color: #1b1f23; + vertical-align: middle; + visibility: hidden; +} + +.markdown-body h1:hover .anchor, +.markdown-body h2:hover .anchor, +.markdown-body h3:hover .anchor, +.markdown-body h4:hover .anchor, +.markdown-body h5:hover .anchor, +.markdown-body h6:hover .anchor { + text-decoration: none; +} + +.markdown-body h1:hover .anchor .octicon-link, +.markdown-body h2:hover .anchor .octicon-link, +.markdown-body h3:hover .anchor .octicon-link, +.markdown-body h4:hover .anchor .octicon-link, +.markdown-body h5:hover .anchor .octicon-link, +.markdown-body h6:hover .anchor .octicon-link { + visibility: visible; +} + +.markdown-body h1 { + padding-bottom: 0.3em; + font-size: 2em; + border-bottom: 1px solid #eaecef; +} + +.markdown-body h2 { + padding-bottom: 0.3em; + font-size: 1.5em; + border-bottom: 1px solid #eaecef; +} + +.markdown-body h3 { + font-size: 1.25em; +} + +.markdown-body h4 { + font-size: 1em; +} + +.markdown-body h5 { + font-size: 0.875em; +} + +.markdown-body h6 { + font-size: 0.85em; + color: #6a737d; +} + +.markdown-body ul, +.markdown-body ol { + padding-left: 2em; +} + +.markdown-body ul ul, +.markdown-body ul ol, +.markdown-body ol ol, +.markdown-body ol ul { + margin-top: 0; + margin-bottom: 0; +} + +.markdown-body li>p { + margin-top: 16px; +} + +.markdown-body li+li { + margin-top: 0.25em; +} + +.markdown-body dl { + padding: 0; +} + +.markdown-body dl dt { + padding: 0; + margin-top: 16px; + font-size: 1em; + font-style: italic; + font-weight: 600; +} + +.markdown-body dl dd { + padding: 0 16px; + margin-bottom: 16px; +} + +.markdown-body table { + display: block; + width: 100%; + overflow: auto; +} + +.markdown-body table th { + font-weight: 600; +} + +.markdown-body table th, +.markdown-body table td { + padding: 6px 13px; + border: 1px solid #dfe2e5; +} + +.markdown-body table tr { + background-color: #fff; + border-top: 1px solid #c6cbd1; +} + +.markdown-body table tr:nth-child(2n) { + background-color: #f6f8fa; +} + +.markdown-body img { + max-width: 100%; + box-sizing: content-box; + background-color: #fff; +} + +.markdown-body img[align=right] { + padding-left: 20px; +} + +.markdown-body img[align=left] { + padding-right: 20px; +} + +.markdown-body code { + padding: 0; + padding-top: 0.2em; + padding-bottom: 0.2em; + margin: 0; + font-size: 85%; + background-color: rgba(27,31,35,0.05); + border-radius: 3px; +} + +.markdown-body code::before, +.markdown-body code::after { + letter-spacing: -0.2em; + content: "\00a0"; +} + +.markdown-body pre { + word-wrap: normal; +} + +.markdown-body pre>code { + padding: 0; + margin: 0; + font-size: 100%; + word-break: normal; + white-space: pre; + background: transparent; + border: 0; +} + +.markdown-body .highlight { + margin-bottom: 16px; +} + +.markdown-body .highlight pre { + margin-bottom: 0; + word-break: normal; +} + +.markdown-body .highlight pre, +.markdown-body pre { + padding: 16px; + overflow: auto; + font-size: 85%; + line-height: 1.45; + background-color: #f6f8fa; + border-radius: 3px; +} + +.markdown-body pre code { + display: inline; + max-width: auto; + padding: 0; + margin: 0; + overflow: visible; + line-height: inherit; + word-wrap: normal; + background-color: transparent; + border: 0; +} + +.markdown-body pre code::before, +.markdown-body pre code::after { + content: normal; +} + +.markdown-body .full-commit .btn-outline:not(:disabled):hover { + color: #005cc5; + border-color: #005cc5; +} + +.markdown-body kbd { + display: inline-block; + padding: 3px 5px; + font: 11px "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace; + line-height: 10px; + color: #444d56; + vertical-align: middle; + background-color: #fafbfc; + border: solid 1px #d1d5da; + border-bottom-color: #c6cbd1; + border-radius: 3px; + box-shadow: inset 0 -1px 0 #c6cbd1; +} + +.markdown-body :checked+.radio-label { + position: relative; + z-index: 1; + border-color: #0366d6; +} + +.markdown-body .task-list-item { + list-style-type: none; +} + +.markdown-body .task-list-item+.task-list-item { + margin-top: 3px; +} + +.markdown-body .task-list-item input { + margin: 0 0.2em 0.25em -1.6em; + vertical-align: middle; +} + +.markdown-body hr { + border-bottom-color: #eee; +} + +/*Markdown Viewer*/ +.markdown-body summary:hover { cursor: pointer; } +.markdown-body ul li p { margin: 0; } diff --git a/doc/themes/godspeed.css b/doc/themes/godspeed.css new file mode 100644 index 00000000..233815a8 --- /dev/null +++ b/doc/themes/godspeed.css @@ -0,0 +1,626 @@ +/* Title: Godspeed */ +/* Author: Jocelyn Richard http://jocelynrichard.com/ */ +/* Description: A quirky, low-contrast theme. Works best with Brush Up: http://www.myfonts.com/fonts/pintassilgo/brush-up/ */ + +/* ================================================ */ +/* 1. Reset */ +/* 2. Skeleton */ +/* 3. Media Queries */ +/* 4. Print Styles */ +/* 5. Godspeed Overrides */ +/* ================================================ */ + + + +/* ================================================ */ +/* 1. Reset */ +/* ================================================ */ + +html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, +b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas, details, embed, figure, figcaption, footer, header, hgroup, menu, nav, output, ruby, section, summary, time, mark, audio, video {margin: 0; padding: 0; border: 0;} /* Edited from http://www.cssreset.com/scripts/eric-meyer-reset-css/ */ + +article, aside, details, figcaption, figure, footer, header, hgroup, nav, section, summary {display: block;} /* Semantic tags definition for IE 6/7/8/9 and Firefox 3 */ + +html {-webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%;} /* Prevents iOS text size adjust after orientation change, without disabling user zoom */ + + + +/* ================================================ */ +/* 2. Skeleton */ +/* ================================================ */ + +/* ------------------------------------------------- */ +/* General */ +/* ------------------------------------------------- */ + +html { + font-size: 14px; +} + +body { + font-family: 'Open Sans', sans-serif; + margin: 1.71rem 1.71rem 3rem 1.71rem ; /* Get margins even if the Markdown rendering app doesn't include any */ + background-color: white; + color: #222; +} + +#wrapper { /* #wrapper: ID added by Marked */ + max-width: 42rem; + margin: 0 auto; + margin-left: auto !important; /* Countering toc.css added by Marked */ + padding: 1.71rem 0 !important; /* Countering toc.css added by Marked */ +} + +/* ------------------------------------------------- */ +/* Typography */ +/* ------------------------------------------------- */ + +h1, +h2, +h3, +h4, +h5, +h6 { + margin-bottom: 1.6rem; +} + +h1, +h2 { + margin-top: 3.2rem; +} + +h1 { + font-size: 2.82rem; /* 42.3px @15px */ + line-height: 3.2rem; /* 48px @15px */ +} + +h2 { + font-size: 1.99rem; /* 29.9px @15px */ + line-height: 2.4rem; /* 36px @15px */ +} + +h3 { + font-size: 1.41rem; /* 21.2px @15px */ + line-height: 2rem; /* 30px @15px */ +} + +h4 { + font-size: 1rem; /* 15px @15px */ + line-height: 1.6rem; /* 24px @15px */ +} + +h5, h6 { + font-size: 0.8rem; + line-height: 1.2rem; + text-transform: uppercase; +} + +h6 { + margin-left: 1.6rem; +} + +p, +ol, +ul, +blockquote { + font-size: 1rem; + line-height: 1.6rem; + margin-bottom: 1.6rem; +} + +ul ul, +ul ol, +ol ul, +ol ol { + margin-left: 1.6rem; + margin-top: 1.6rem; +} + +#generated-toc ul ul, /* #generated-toc: added by Marked for its table of contents */ +#generated-toc ul ol, +#generated-toc ol ul, +#generated-toc ol ol { + margin-top: 0; + margin-bottom: 0; + padding-top: 0; + padding-bottom: 0; +} + +blockquote { + margin: 0 0 1.6rem 2.4rem; + padding-left: 0.8rem; /* Voire */ + border-left: 4px solid rgba(0,0,0,0.08); + font-style: normal; +} + +blockquote ul { + margin-left: 0.8rem; /* Pour ne pas que les hanging bullets mordent sur le blockquote */ +} + +ol li blockquote, /* So that blockquote work in lists */ +ul li blockquote { + margin-left: 0; +} + +a:link { + text-decoration: none; + color: #165bd4; + border-bottom: 1px solid #ccc; +} + +a:visited { + color: #7697cf; + border-bottom: 1px solid #ccc; +} + +a:hover { + border-color: #165bd4; +} + +a:active { + background-color: #e6e6e6; +} + +/* ------------------------------------------------- */ +/* Tables */ +/* ------------------------------------------------- */ + +table { + font-size: 0.85rem; + margin: 0 0 1.6rem 0; + border-collapse: collapse; + border: 1px solid #ccc; +} + +th, +td { + padding: 0.5rem 0.75rem; + max-width: 20rem; /* Avoid dropping lines for nothing without having ridiculously wide tables */ +} + +th { + border-bottom: 2px solid #222; +} + +tr { + border-bottom: 1px solid #ccc; +} + +tbody tr:nth-child(odd) { + background-color: #f9f9f9; +} + +table code { + font-size: 85%; +} + +/* ------------------------------------------------- */ +/* Misc */ +/* ------------------------------------------------- */ + +img { + max-width: 100% +} + +caption, +figcaption { + font-size: 0.85rem; + line-height: 1.6rem; + margin: 0 1.6rem; + text-align: left; +} + +figcaption { + margin-bottom: 1.6rem; +} + +h1, /* White-space mentions in order to force wrapping */ +h2, +a:link { + white-space: pre; /* CSS 2.0 */ + white-space: pre-wrap; /* CSS 2.1 */ + white-space: pre-line; /* CSS 3.0 */ + white-space: -pre-wrap; /* Opera 4-6 */ + white-space: -o-pre-wrap; /* Opera 7 */ + white-space: -moz-pre-wrap; /* Mozilla */ + white-space: -hp-pre-wrap; /* HP Printers */ + word-wrap: break-word; /* IE 5+ */ +} + +code { + font-family: "Menlo", "Courier New", "Courier", monospace; + font-size: 85%; + color: #666; + background-color: rgba(0,0,0,0.08); + padding: 2px 4px; + border-radius: 2px; +} + +pre { + background-color: rgba(0,0,0,0.08); + border-radius: 8px; + padding: 0.4rem; + margin-bottom: 1.6rem; +} + +pre code { /* Counter the code mentions */ + background-color: transparent; + padding: 0; +} + +sup, +sub, +a.footnote { /* Keep line-height from being affected by sub, cf https://gist.github.com/unruthless/413930 */ + font-size: 75%; + height: 0; + line-height: 1; + position: relative; +} + +sup, +a.footnote { + vertical-align:super; +} + +sub { + vertical-align: sub; +} + +dt { + font-weight: 600; +} + +dd { + font-size: 1rem; + line-height: 1.6rem; + margin-bottom: 1.6rem; +} + +hr { + clear: none; + height: 0.2rem; + border: none; + margin: 0 auto 1.4rem auto; /* 2.4rem auto 2.2rem auto; */ + width: 100%; + color: #ccc; + background-color: #ccc; +} + +::selection { + background-color: #f8dc77; +} + +::-moz-selection { + background-color: #f8dc77; +} + +a:focus { + outline: 2px solid; + outline-color: #165bd4; +} + +/* ------------------------------------------------- */ +/* Animations */ +/* ------------------------------------------------- */ + +a:hover { + -moz-transition: all 0.2s ease-in-out; + -webkit-transition: all 0.2s ease-in-out; +} + +h1, +h2, +h3, +h4, +h5, +h6, +p, +blockquote { + -moz-transition: all 0.2s ease; + -webkit-transition: all 0.2s ease; +} + + + +/* ================================================ */ +/* 3. Media Queries */ +/* ================================================ */ + +/* Base styles are for smartphones; elements are then tweaked as the viewport grows. */ + +/* ------------------------------------------------- */ +/* iPad and desktop */ +/* ------------------------------------------------- */ + +@media only screen and (min-width: 641px) { + + html { + font-size: 15px; + } + + body { + margin: 2.4rem 2.4rem 3.2rem 2.4rem; + } + + h1 { + font-size: 3.57rem; /* 53.2px @15px */ + line-height: 4rem; /* 60px @15px */ + } + + h2 { + font-size: 2.24rem; /* 33.6px @15px */ + line-height: 2.8rem; /* 42px @15px */ + } + +} + +/* ------------------------------------------------- */ +/* Widescreens */ +/* ------------------------------------------------- */ + +@media only screen and (min-width: 1441px) { + + html { + font-size: 22px; + } + +} + + + +/* ================================================ */ +/* 4. Print Styles */ +/* ================================================ */ + +/* Inconsistent and buggy across browsers */ + +@media print { + + * { + background: transparent !important; + color: #000 !important; /* Black text prints faster and browsers are inconsistent in color reproduction anyway: h5bp.com/s */ + } + + @page { + margin: 1cm; /* Added to any #wrapper margin*/ + } + + html { + font-size: 15px; + } + + body { + margin: 1rem !important; /* Security margins for browser without @page support */ + } + + #wrapper { + max-width: none; + } + + h1, + h2, + h3, + h4, + h5, + h6, + p { + orphans: 3; + widows: 3; + page-break-after: avoid; + } + + ul, + ol { + list-style-position: inside !important; + padding-right: 0 !important; + margin-left: 0 !important; + } + + ul ul, + ul ol, + ol ul, + ol ol, + ul p:not(:first-child), + ol p:not(:first-child) { + margin-left: 2rem !important; + } + + a:link, + a:visited { + text-decoration: underline !important; + font-weight: normal !important; + } + + a[href]:after { + content: " (" attr(href) ")"; + } + + a[href^="javascript:"]:after, + a[href^="#"]:after { + content: ""; /* Do not show javascript and internal links */ + } + + a[href^="#"] { + text-decoration: none !important; + } + + th { + background-color: rgba(0,0,0,0.2) !important; + border-bottom: none !important; + } + + tr { + page-break-inside: avoid; + } + + tbody tr:nth-child(even) { + background-color: rgba(0,0,0,0.1) !important; + } + + pre { + border: 1px solid rgba(0,0,0,0.2); + page-break-inside: avoid; + } + + img { + max-width: 100% !important; + page-break-inside: avoid; + } + + /* #generated-toc: added by Marked for its table of contents */ + + #wrapper #generated-toc ul, /* Table of contents printing in Marked */ + #wrapper #generated-toc ol { + list-style-type: decimal; + } + + #wrapper #generated-toc ul li, + #wrapper #generated-toc ol li { + margin: 1rem 0; + } + +} + + + +/* ================================================ */ +/* 5. Godspeed Overrides */ +/* ================================================ */ + +/* ------------------------------------------------- */ +/* General */ +/* ------------------------------------------------- */ + +body { + font-family: 'Source Sans Pro', Avenir, sans-serif; + background-color: #3c3d46; + color: #7d7d7a; + margin-bottom: 2.4rem; /* Visual tweak */ +} + +/* ------------------------------------------------- */ +/* Typography */ +/* ------------------------------------------------- */ + +h1 { + font-family: 'Brush Up Too', 'Source Sans Pro', Avenir, sans-serif; + color: #e6ceaa; +} + +h2, +h3 { + color: #b98552; + text-transform: uppercase; +} + +h4, +h5, +h6 { + text-transform: uppercase; +} + +blockquote { + border-color: rgba(0,0,0,0.1); /* Pour correspondre à l'opacité des bordures ajoutées au #wrapper */ +} + +a:link { + color: #6190d2; + text-decoration: none; + border-bottom: 2px solid rgba(0,0,0,0.2); +} + +a:hover { + color: #6190d2; + text-decoration: none; + border-bottom: 1px solid #6190d2; +} + +/* ------------------------------------------------- */ +/* Tables */ +/* ------------------------------------------------- */ + +table { + border: 1px solid #7d7d7a; + border-radius: 8px; +} + +th { + color: #b98552; + border-bottom: 1px solid #7d7d7a; +} + +tr { + border-bottom: 1px solid #7d7d7a; +} + +tbody tr:nth-child(odd) { + background-color: rgba(60,75,94,0.5); +} + +/* ------------------------------------------------- */ +/* Misc */ +/* ------------------------------------------------- */ + +code { + font-size: 75%; /* Matching better Source Sans */ + color: #3c3d46; + background-color: #7d7d7a; +} + +pre { + background-color: rgba(60,75,94,0.5); +} + +pre code { + color: #7d7d7a; +} + +hr { + color: rgba(0,0,0,0.2); + background-color: rgba(0,0,0,0.2); +} + +::selection { + background-color: rgba(0,0,0,0.2); +} + +::-moz-selection { + background-color: rgba(0,0,0,0.2); +} + +a:focus { + outline: 2px dotted; + outline-color: #7d7d7a; +} + +/* ------------------------------------------------- */ +/* iPad and desktop */ +/* ------------------------------------------------- */ + +@media only screen and (min-width: 641px) { + + html { + font-size: 16px; + } + + #wrapper { + padding: 0 2.4rem; + border: 1px solid rgba(0,0,0,0.2) !important; /* !important otherwise doesn't show up in Marked */ + box-shadow: 0 0 0 6px rgba(0,0,0,0.1); + padding: 2.4rem !important; + border-radius: 4px; + } + +} + +/* ------------------------------------------------- */ +/* Widescreens */ +/* ------------------------------------------------- */ + +@media only screen and (min-width: 1441px) { + + html { + font-size: 22px; + } + +} diff --git a/doc/themes/markdown-alt.css b/doc/themes/markdown-alt.css new file mode 100644 index 00000000..480eea3f --- /dev/null +++ b/doc/themes/markdown-alt.css @@ -0,0 +1,75 @@ +body { + line-height: 1.4em; + color: black; + padding:1em; + margin:auto; + max-width:42em; +} + +li { + color: black; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + border: 0 none !important; +} + +h1 { + margin-top: 0.5em; + margin-bottom: 0.5em; + border-bottom: 2px solid #000080 !important; +} + +h2 { + margin-top: 1em; + margin-bottom: 0.5em; + border-bottom: 2px solid #000080 !important; +} + +pre { + background-color: #f8f8f8; + border: 1px solid #2f6fab; + border-radius: 3px; + overflow: auto; + padding: 5px; +} + +pre code { + background-color: inherit; + border: none; + padding: 0; +} + +code { + background-color: #ffffe0; + border: 1px solid orange; + border-radius: 3px; + padding: 0 0.2em; +} + +a { + text-decoration: underline; +} + +ul, ol { + padding-left: 30px; +} + +li { + margin: 0.2em 0 0 0em; padding: 0px; +} + +em { + color: #b05000; +} + +table.text th, table.text td { + vertical-align: top; + border-top: 1px solid #ccc; + padding:5px; +} diff --git a/doc/themes/markdown.css b/doc/themes/markdown.css new file mode 100644 index 00000000..7dd96522 --- /dev/null +++ b/doc/themes/markdown.css @@ -0,0 +1,102 @@ +html { font-size: 100%; overflow-y: scroll; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; } + +body{ +color:#444; +font-family:Georgia, Palatino, 'Palatino Linotype', Times, 'Times New Roman', serif; +font-size:12px; +line-height:1.5em; +padding:1em; +margin:auto; +max-width:42em; +background:#fefefe; +} + +a{ color: #0645ad; text-decoration:none;} +a:visited{ color: #0b0080; } +a:hover{ color: #06e; } +a:active{ color:#faa700; } +a:focus{ outline: thin dotted; } +a:hover, a:active{ outline: 0; } + +::-moz-selection{background:rgba(255,255,0,0.3);color:#000} +::selection{background:rgba(255,255,0,0.3);color:#000} + +a::-moz-selection{background:rgba(255,255,0,0.3);color:#0645ad} +a::selection{background:rgba(255,255,0,0.3);color:#0645ad} + +p{ +margin:1em 0; +} + +img{ +max-width:100%; +} + +h1,h2,h3,h4,h5,h6{ +font-weight:normal; +color:#111; +line-height:1em; +} +h4,h5,h6{ font-weight: bold; } +h1{ font-size:2.5em; } +h2{ font-size:2em; } +h3{ font-size:1.5em; } +h4{ font-size:1.2em; } +h5{ font-size:1em; } +h6{ font-size:0.9em; } + +blockquote{ +color:#666666; +margin:0; +padding-left: 3em; +border-left: 0.5em #EEE solid; +} +hr { display: block; height: 2px; border: 0; border-top: 1px solid #aaa;border-bottom: 1px solid #eee; margin: 1em 0; padding: 0; } +pre, code, kbd, samp { color: #000; font-family: monospace, monospace; _font-family: 'courier new', monospace; font-size: 0.98em; } +pre { white-space: pre; white-space: pre-wrap; word-wrap: break-word; } + +b, strong { font-weight: bold; } + +dfn { font-style: italic; } + +ins { background: #ff9; color: #000; text-decoration: none; } + +mark { background: #ff0; color: #000; font-style: italic; font-weight: bold; } + +sub, sup { font-size: 75%; line-height: 0; position: relative; vertical-align: baseline; } +sup { top: -0.5em; } +sub { bottom: -0.25em; } + +ul, ol { margin: 1em 0; padding: 0 0 0 2em; } +li p:last-child { margin:0 } +dd { margin: 0 0 0 2em; } + +img { border: 0; -ms-interpolation-mode: bicubic; vertical-align: middle; } + +table { border-collapse: collapse; border-spacing: 0; } +td { vertical-align: top; } + +@media only screen and (min-width: 480px) { +body{font-size:14px;} +} + +@media only screen and (min-width: 768px) { +body{font-size:16px;} +} + +@media print { + * { background: transparent !important; color: black !important; filter:none !important; -ms-filter: none !important; } + body{font-size:12pt; max-width:100%;} + a, a:visited { text-decoration: underline; } + hr { height: 1px; border:0; border-bottom:1px solid black; } + a[href]:after { content: " (" attr(href) ")"; } + abbr[title]:after { content: " (" attr(title) ")"; } + .ir a:after, a[href^="javascript:"]:after, a[href^="#"]:after { content: ""; } + pre, blockquote { border: 1px solid #999; padding-right: 1em; page-break-inside: avoid; } + tr, img { page-break-inside: avoid; } + img { max-width: 100% !important; } + @page :left { margin: 15mm 20mm 15mm 10mm; } + @page :right { margin: 15mm 10mm 15mm 20mm; } + p, h2, h3 { orphans: 3; widows: 3; } + h2, h3 { page-break-after: avoid; } +} diff --git a/doc/themes/markdown5.css b/doc/themes/markdown5.css new file mode 100644 index 00000000..9c5d03ce --- /dev/null +++ b/doc/themes/markdown5.css @@ -0,0 +1,139 @@ +body{ + margin: 0 auto; + background-color:white; + +/* --------- FONT FAMILY -------- + following are some optional font families. Usually a family + is safer to choose than a specific font, + which may not be on the users computer */ + font-family:Georgia, Palatino, serif; + +/* -------------- COLOR OPTIONS ------------ + following are additional color options for base font + you could uncomment another one to easily change the base color + or add one to a specific element style below */ + color: #333333; /* dark gray not black */ + + line-height: 1; + max-width: 800px; + padding: 30px; + font-size: 18px; +} + + +p { + line-height: 150%; + max-width: 960px; + font-weight: 400; + color: #333333 +} + + +h1, h2, h3, h4 { + font-weight: 400; +} + +h2, h3, h4, h5, p { + margin-bottom: 25px; + padding: 0; +} + +h1 { + margin-bottom: 10px; + font-size:300%; + padding: 0px; + font-variant:small-caps; +} + +h2 { + font-size:150% +} + +h3 { + font-size:120% +} +h4 { + font-size:100% + font-variant:small-caps; + +} +h5 { + font-size:80% + font-weight: 100; +} + +h6 { + font-size:80% + font-weight: 100; + color:red; + font-variant:small-caps; +} +a { + color: grey; + margin: 0; + padding: 0; + vertical-align: baseline; +} +a:hover { + text-decoration: blink; + color: green; +} +a:visited { + color: black; +} +ul, ol { + padding: 0; + margin: 0px 0px 0px 50px; +} +ul { + list-style-type: square; + list-style-position: inside; + +} + +li { + line-height:150% +} +li ul, li ul { + margin-left: 24px; +} + +pre { + padding: 0px 24px; + max-width: 800px; + white-space: pre-wrap; +} +code { + font-family: Consolas, Monaco, Andale Mono, monospace; + line-height: 1.5; + font-size: 13px; +} +aside { + display: block; + float: right; + width: 390px; +} +blockquote { + border-left:.5em solid #eee; + padding: 0 1em; + margin-left:0; + max-width: 476px; +} +blockquote cite { + line-height:20px; + color:#bfbfbf; +} +blockquote cite:before { + content: '\2014 \00A0'; +} + +blockquote p { + color: #666; + max-width: 460px; +} +hr { + text-align: left; + margin: 0 auto 0 0; + color: #999; +} + diff --git a/doc/themes/markdown6.css b/doc/themes/markdown6.css new file mode 100644 index 00000000..8c6d0631 --- /dev/null +++ b/doc/themes/markdown6.css @@ -0,0 +1,222 @@ +/* Extracted and interpreted from adcstyle.css and frameset_styles.css */ + +/* body */ +body { + margin: 20px auto; + width: 800px; + background-color: #fff; + color: #000; + font: 13px "Myriad Pro", "Lucida Grande", Lucida, Verdana, sans-serif; +} + +/* links */ +a:link { + color: #00f; + text-decoration: none; +} + +a:visited { + color: #00a; + text-decoration: none; +} + +a:hover { + color: #f60; + text-decoration: underline; +} + +a:active { + color: #f60; + text-decoration: underline; +} + + +/* html tags */ + +/* Work around IE/Win code size bug - courtesy Jesper, waffle.wootest.net */ + +* html code { + font-size: 101%; +} + +* html pre { + font-size: 101%; +} + +/* code */ + +pre, code { + font-size: 11px; font-family: monaco, courier, consolas, monospace; +} + +pre { + margin-top: 5px; + margin-bottom: 10px; + border: 1px solid #c7cfd5; + background: #f1f5f9; + margin: 20px 0; + padding: 8px; + text-align: left; +} + +hr { + color: #919699; + size: 1; + width: 100%; + noshade: "noshade" +} + +/* headers */ + + +h1, h2, h3, h4, h5, h6 { + font-family: "Myriad Pro", "Lucida Grande", Lucida, Verdana, sans-serif; + font-weight: bold; +} + +h1 { + margin-top: 1em; + margin-bottom: 25px; + color: #000; + font-weight: bold; + font-size: 30px; +} +h2 { + margin-top: 2.5em; + font-size: 24px; + color: #000; + padding-bottom: 2px; + border-bottom: 1px solid #919699; +} +h3 { + margin-top: 2em; + margin-bottom: .5em; + font-size: 17px; + color: #000; +} +h4 { + margin-top: 2em; + margin-bottom: .5em; + font-size: 15px; + color: #000; +} +h5 { + margin-top: 20px; + margin-bottom: .5em; + padding: 0; + font-size: 13px; + color: #000; +} + +h6 { + margin-top: 20px; + margin-bottom: .5em; + padding: 0; + font-size: 11px; + color: #000; +} + +p { + margin-top: 0px; + margin-bottom: 10px; +} + +/* lists */ + +ul { + list-style: square outside; + margin: 0 0 0 30px; + padding: 0 0 12px 6px; +} + +li { + margin-top: 7px; +} + +ol { + list-style-type: decimal; + list-style-position: outside; + margin: 0 0 0 30px; + padding: 0 0 12px 6px; +} + +ol ol { + list-style-type: lower-alpha; + list-style-position: outside; + margin: 7px 0 0 30px; + padding: 0 0 0 10px; + } + +ul ul { + margin-left: 40px; + padding: 0 0 0 6px; +} + +li>p { display: inline } +li>p+p { display: block } +li>a+p { display: block } + + +/* table */ + +table { + width: 100%; + border-top: 1px solid #919699; + border-left: 1px solid #919699; + border-spacing: 0; +} + +table th { + padding: 4px 8px 4px 8px; + background: #E2E2E2; + font-size: 12px; + border-bottom: 1px solid #919699; + border-right: 1px solid #919699; +} +table th p { + font-weight: bold; + margin-bottom: 0px; +} + +table td { + padding: 8px; + font-size: 12px; + vertical-align: top; + border-bottom: 1px solid #919699; + border-right: 1px solid #919699; +} +table td p { + margin-bottom: 0px; +} +table td p + p { + margin-top: 5px; +} +table td p + p + p { + margin-top: 5px; +} + +/* forms */ + +form { + margin: 0; +} + +button { + margin: 3px 0 10px 0; +} +input { + vertical-align: middle; + padding: 0; + margin: 0 0 5px 0; +} + +select { + vertical-align: middle; + padding: 0; + margin: 0 0 3px 0; +} + +textarea { + margin: 0 0 10px 0; + width: 100%; +} \ No newline at end of file diff --git a/doc/themes/markdown7.css b/doc/themes/markdown7.css new file mode 100644 index 00000000..85355624 --- /dev/null +++ b/doc/themes/markdown7.css @@ -0,0 +1,295 @@ +body { + font-family: Helvetica, arial, sans-serif; + font-size: 14px; + line-height: 1.6; + padding-top: 10px; + padding-bottom: 10px; + background-color: white; + padding: 30px; } + +body > *:first-child { + margin-top: 0 !important; } +body > *:last-child { + margin-bottom: 0 !important; } + +a { + color: #4183C4; } +a.absent { + color: #cc0000; } +a.anchor { + display: block; + padding-left: 30px; + margin-left: -30px; + cursor: pointer; + position: absolute; + top: 0; + left: 0; + bottom: 0; } + +h1, h2, h3, h4, h5, h6 { + margin: 20px 0 10px; + padding: 0; + font-weight: bold; + -webkit-font-smoothing: antialiased; + cursor: text; + position: relative; } + +h1:hover a.anchor, h2:hover a.anchor, h3:hover a.anchor, h4:hover a.anchor, h5:hover a.anchor, h6:hover a.anchor { + text-decoration: none; } + +h1 tt, h1 code { + font-size: inherit; } + +h2 tt, h2 code { + font-size: inherit; } + +h3 tt, h3 code { + font-size: inherit; } + +h4 tt, h4 code { + font-size: inherit; } + +h5 tt, h5 code { + font-size: inherit; } + +h6 tt, h6 code { + font-size: inherit; } + +h1 { + font-size: 28px; + color: black; } + +h2 { + font-size: 24px; + border-bottom: 1px solid #cccccc; + color: black; } + +h3 { + font-size: 18px; } + +h4 { + font-size: 16px; } + +h5 { + font-size: 14px; } + +h6 { + color: #777777; + font-size: 14px; } + +p, blockquote, ul, ol, dl, li, table, pre { + margin: 15px 0; } + +hr { + border: 0 none; + color: #cccccc; + height: 4px; + padding: 0; +} + +body > h2:first-child { + margin-top: 0; + padding-top: 0; } +body > h1:first-child { + margin-top: 0; + padding-top: 0; } +body > h1:first-child + h2 { + margin-top: 0; + padding-top: 0; } +body > h3:first-child, body > h4:first-child, body > h5:first-child, body > h6:first-child { + margin-top: 0; + padding-top: 0; } + +a:first-child h1, a:first-child h2, a:first-child h3, a:first-child h4, a:first-child h5, a:first-child h6 { + margin-top: 0; + padding-top: 0; } + +h1 p, h2 p, h3 p, h4 p, h5 p, h6 p { + margin-top: 0; } + +li p.first { + display: inline-block; } +li { + margin: 0; } +ul, ol { + padding-left: 30px; } + +ul :first-child, ol :first-child { + margin-top: 0; } + +dl { + padding: 0; } +dl dt { + font-size: 14px; + font-weight: bold; + font-style: italic; + padding: 0; + margin: 15px 0 5px; } +dl dt:first-child { + padding: 0; } +dl dt > :first-child { + margin-top: 0; } +dl dt > :last-child { + margin-bottom: 0; } +dl dd { + margin: 0 0 15px; + padding: 0 15px; } +dl dd > :first-child { + margin-top: 0; } +dl dd > :last-child { + margin-bottom: 0; } + +blockquote { + border-left: 4px solid #dddddd; + padding: 0 15px; + color: #777777; } +blockquote > :first-child { + margin-top: 0; } +blockquote > :last-child { + margin-bottom: 0; } + +table { + padding: 0;border-collapse: collapse; } +table tr { + border-top: 1px solid #cccccc; + background-color: white; + margin: 0; + padding: 0; } +table tr:nth-child(2n) { + background-color: #f8f8f8; } +table tr th { + font-weight: bold; + border: 1px solid #cccccc; + margin: 0; + padding: 6px 13px; } +table tr td { + border: 1px solid #cccccc; + margin: 0; + padding: 6px 13px; } +table tr th :first-child, table tr td :first-child { + margin-top: 0; } +table tr th :last-child, table tr td :last-child { + margin-bottom: 0; } + +img { + max-width: 100%; } + +span.frame { + display: block; + overflow: hidden; } +span.frame > span { + border: 1px solid #dddddd; + display: block; + float: left; + overflow: hidden; + margin: 13px 0 0; + padding: 7px; + width: auto; } +span.frame span img { + display: block; + float: left; } +span.frame span span { + clear: both; + color: #333333; + display: block; + padding: 5px 0 0; } +span.align-center { + display: block; + overflow: hidden; + clear: both; } +span.align-center > span { + display: block; + overflow: hidden; + margin: 13px auto 0; + text-align: center; } +span.align-center span img { + margin: 0 auto; + text-align: center; } +span.align-right { + display: block; + overflow: hidden; + clear: both; } +span.align-right > span { + display: block; + overflow: hidden; + margin: 13px 0 0; + text-align: right; } +span.align-right span img { + margin: 0; + text-align: right; } +span.float-left { + display: block; + margin-right: 13px; + overflow: hidden; + float: left; } +span.float-left span { + margin: 13px 0 0; } +span.float-right { + display: block; + margin-left: 13px; + overflow: hidden; + float: right; } +span.float-right > span { + display: block; + overflow: hidden; + margin: 13px auto 0; + text-align: right; } + +code, tt { + margin: 0 2px; + padding: 0 5px; + white-space: nowrap; + border: 1px solid #eaeaea; + background-color: #f8f8f8; + border-radius: 3px; } + +pre code { + margin: 0; + padding: 0; + white-space: pre; + border: none; + background: transparent; } + +.highlight pre { + background-color: #f8f8f8; + border: 1px solid #cccccc; + font-size: 13px; + line-height: 19px; + overflow: auto; + padding: 6px 10px; + border-radius: 3px; } + +pre { + background-color: #f8f8f8; + border: 1px solid #cccccc; + font-size: 13px; + line-height: 19px; + overflow: auto; + padding: 6px 10px; + border-radius: 3px; } +pre code, pre tt { + background-color: transparent; + border: none; } + +sup { + font-size: 0.83em; + vertical-align: super; + line-height: 0; +} +* { + -webkit-print-color-adjust: exact; +} +@media screen and (min-width: 914px) { + body { + width: 854px; + margin:0 auto; + } +} +@media print { + table, pre { + page-break-inside: avoid; + } + pre { + word-wrap: break-word; + } +} diff --git a/doc/themes/markdown8.css b/doc/themes/markdown8.css new file mode 100644 index 00000000..90c1820c --- /dev/null +++ b/doc/themes/markdown8.css @@ -0,0 +1,136 @@ +h1, h2, h3, h4, h5, h6, p, blockquote { + margin: 0; + padding: 0; +} +body { + font-family: "Helvetica Neue", Helvetica, "Hiragino Sans GB", Arial, sans-serif; + font-size: 13px; + line-height: 18px; + color: #737373; + background-color: white; + margin: 10px 13px 10px 13px; +} +table { + margin: 10px 0 15px 0; + border-collapse: collapse; +} +td,th { + border: 1px solid #ddd; + padding: 3px 10px; +} +th { + padding: 5px 10px; +} + +a { + color: #0069d6; +} +a:hover { + color: #0050a3; + text-decoration: none; +} +a img { + border: none; +} +p { + margin-bottom: 9px; +} + +h1, h2, h3, h4, h5, h6 { + color: #404040; + line-height: 36px; +} +h1 { + margin-bottom: 18px; + font-size: 30px; +} +h2 { + font-size: 24px; +} +h3 { + font-size: 18px; +} +h4 { + font-size: 16px; +} +h5 { + font-size: 14px; +} +h6 { + font-size: 13px; +} +hr { + margin: 0 0 19px; + border: 0; + border-bottom: 1px solid #ccc; +} +blockquote { + padding: 13px 13px 21px 15px; + margin-bottom: 18px; + font-family:georgia,serif; + font-style: italic; +} +blockquote:before { + content:"\201C"; + font-size:40px; + margin-left:-10px; + font-family:georgia,serif; + color:#eee; +} +blockquote p { + font-size: 14px; + font-weight: 300; + line-height: 18px; + margin-bottom: 0; + font-style: italic; +} +code, pre { + font-family: Monaco, Andale Mono, Courier New, monospace; +} +code { + background-color: #fee9cc; + color: rgba(0, 0, 0, 0.75); + padding: 1px 3px; + font-size: 12px; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; +} +pre { + display: block; + padding: 14px; + margin: 0 0 18px; + line-height: 16px; + font-size: 11px; + border: 1px solid #d9d9d9; + white-space: pre-wrap; + word-wrap: break-word; +} +pre code { + background-color: #fff; + color:#737373; + font-size: 11px; + padding: 0; +} +sup { + font-size: 0.83em; + vertical-align: super; + line-height: 0; +} +* { + -webkit-print-color-adjust: exact; +} +@media screen and (min-width: 914px) { + body { + width: 854px; + margin:10px auto; + } +} +@media print { + body,code,pre code,h1,h2,h3,h4,h5,h6 { + color: black; + } + table, pre { + page-break-inside: avoid; + } +} diff --git a/doc/themes/markdown9.css b/doc/themes/markdown9.css new file mode 100644 index 00000000..42d55eb7 --- /dev/null +++ b/doc/themes/markdown9.css @@ -0,0 +1,138 @@ +h1, h2, h3, h4, h5, h6, p, blockquote { + margin: 0; + padding: 0; +} +body { + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 13px; + line-height: 18px; + color: #fff; + background-color: #110F14; + margin: 10px 13px 10px 13px; +} +table { + margin: 10px 0 15px 0; + border-collapse: collapse; +} +td,th { + border: 1px solid #ddd; + padding: 3px 10px; +} +th { + padding: 5px 10px; +} +a { + color: #59acf3; +} +a:hover { + color: #a7d8ff; + text-decoration: none; +} +a img { + border: none; +} +p { + margin-bottom: 9px; +} +h1, h2, h3, h4, h5, h6 { + color: #fff; + line-height: 36px; +} +h1 { + margin-bottom: 18px; + font-size: 30px; +} +h2 { + font-size: 24px; +} +h3 { + font-size: 18px; +} +h4 { + font-size: 16px; +} +h5 { + font-size: 14px; +} +h6 { + font-size: 13px; +} +hr { + margin: 0 0 19px; + border: 0; + border-bottom: 1px solid #ccc; +} +blockquote { + padding: 13px 13px 21px 15px; + margin-bottom: 18px; + font-family:georgia,serif; + font-style: italic; +} +blockquote:before { + content:"\201C"; + font-size:40px; + margin-left:-10px; + font-family:georgia,serif; + color:#eee; +} +blockquote p { + font-size: 14px; + font-weight: 300; + line-height: 18px; + margin-bottom: 0; + font-style: italic; +} + +code, pre { + font-family: Menlo, Monaco, Andale Mono, Courier New, monospace; +} + +code { + padding: 1px 3px; + font-size: 12px; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; + background: #334; +} + +pre { + display: block; + padding: 14px; + margin: 0 0 18px; + line-height: 16px; + font-size: 11px; + border: 1px solid #334; + white-space: pre; + white-space: pre-wrap; + word-wrap: break-word; + background-color: #282a36; + border-radius: 6px; +} +pre code { + font-size: 11px; + padding: 0; + background: transparent; +} +sup { + font-size: 0.83em; + vertical-align: super; + line-height: 0; +} +* { + -webkit-print-color-adjust: exact; +} +@media screen and (min-width: 914px) { + body { + width: 854px; + margin:10px auto; + } +} +@media print { + body,code,pre code,h1,h2,h3,h4,h5,h6 { + color: black; + } + table, pre { + page-break-inside: avoid; + } +} diff --git a/doc/themes/markedapp-byword.css b/doc/themes/markedapp-byword.css new file mode 100644 index 00000000..643aa9ab --- /dev/null +++ b/doc/themes/markedapp-byword.css @@ -0,0 +1,314 @@ +/* + * This document has been created with Marked.app . + * Copyright 2011 Brett Terpstra + * --------------------------------------------------------------------------- + * Please leave this notice in place, along with any additional credits below. + * + * Byword.css theme is based on Byword.app + * Authors: @brunodecarvalho, @jpedroso, @rcabaco + * Copyright 2011 Metaclassy, Lda. + */ + +html { + font-size: 62.5%; /* base font-size: 10px */ +} + +body { + background-color: #f2f2f2; + color: #3c3c3c; + + /* Change font size below */ + font-size: 1.7em; + line-height: 1.4em; + + /* Change font below */ + + /* Sans-serif fonts */ + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + -webkit-font-smoothing: antialiased; + + /* Serif fonts */ + /* + font-family: "Cochin", "Baskerville", "Georgia", serif; + -webkit-font-smoothing: subpixel-antialiased; + */ + + /* Monospaced fonts */ + /* + font-family: "Courier New", Menlo, Monaco, mono; + -webkit-font-smoothing: antialiased; + */ + + margin: auto; + max-width: 42em; +} +a { + color: #308bd8; + text-decoration:none; +} +a:hover { + text-decoration: underline; +} +/* headings */ +h1, h2 { + line-height:1.2em; + margin-top:32px; + margin-bottom:12px; +} +h1:first-child { + margin-top:0; +} +h3, h4, h5, h6 { + margin-top:12px; + margin-bottom:0; +} +h5, h6 { + font-size:0.9em; + line-height:1.0em; +} +/* end of headings */ +p { + margin:0 0 24px 0; +} +p:last-child { + margin:0; +} +#wrapper hr { + width: 100%; + margin: 3em auto; + border: 0; + color: #eee; + background-color: #ccc; + height: 1px; + -webkit-box-shadow:0px 1px 0px rgba(255, 255, 255, 0.75); +} +/* lists */ +ol { + list-style: outside decimal; +} +ul { + list-style: outside disc; +} +ol, ul { + padding-left:0; + margin-bottom:24px; +} +ol li { + margin-left:28px; +} +ul li { + margin-bottom:8px; + margin-left:16px; +} +ol:last-child, ul:last-child { + margin:0; +} +li > ol, li > ul { + padding-left:12px; +} +dl { + margin-bottom:24px; +} +dl dt { + font-weight:bold; + margin-bottom:8px; +} +dl dd { + margin-left:0; + margin-bottom:12px; +} +dl dd:last-child, dl:last-child { + margin-bottom:0; +} +/* end of lists */ +pre { + white-space: pre-wrap; + width: 96%; + margin-bottom: 24px; + overflow: hidden; + padding: 3px 10px; + -webkit-border-radius: 3px; + background-color: #eee; + border: 1px solid #ddd; +} +code { + white-space: nowrap; + font-size: 1.1em; + padding: 2px; + -webkit-border-radius: 3px; + background-color: #eee; + border: 1px solid #ddd; +} +pre code { + white-space: pre-wrap; + border: none; + padding: 0; + background-color: transparent; + -webkit-border-radius: 0; +} +blockquote { + margin-left: 0; + margin-right: 0; + width: 96%; + padding: 0 10px; + border-left: 3px solid #ddd; + color: #777; +} +table { + margin-left: auto; + margin-right: auto; + margin-bottom: 24px; + border-bottom: 1px solid #ddd; + border-right: 1px solid #ddd; + border-spacing: 0; +} +table th { + padding: 3px 10px; + background-color: #eee; + border-top: 1px solid #ddd; + border-left: 1px solid #ddd; +} +table tr { +} +table td { + padding: 3px 10px; + border-top: 1px solid #ddd; + border-left: 1px solid #ddd; +} +caption { + font-size: 1.2em; + font-weight: bold; + margin-bottom: 5px; +} +figure { + display: block; + text-align: center; +} +#wrapper img { + border: none; + display: block; + margin: 1em auto; + max-width: 100%; +} +figcaption { + font-size: 0.8em; + font-style: italic; +} +mark { + background: #fefec0; + padding:1px 3px; +} + + +/* classes */ + +.markdowncitation { +} +.footnote { + font-size: 0.8em; + vertical-align: super; +} +.footnotes ol { + font-weight: bold; +} +.footnotes ol li p { + font-weight: normal; +} + +/* custom formatting classes */ + +.shadow { + -webkit-box-shadow: 0 2px 4px #999; +} + +.source { + text-align: center; + font-size: 0.8em; + color: #777; + margin: -40px; +} + +@media screen { + .inverted, .inverted #wrapper { + background-color: #1a1a1a !important; + color: #bebebe !important; + + /* SANS-SERIF */ + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif !important; + -webkit-font-smoothing: antialiased !important; + + /* SERIF */ + /* + font-family: "Cochin", "Baskerville", "Georgia", serif !important; + -webkit-font-smoothing: subpixel-antialiased !important; + */ + /* MONO */ + /* + font-family: "Courier", mono !important; + -webkit-font-smoothing: antialiased !important; + */ + } + .inverted a { + color: #308bd8 !important; + } + .inverted hr { + color: #666 !important; + border: 0; + background-color: #666 !important; + -webkit-box-shadow: none !important; + } + .inverted pre { + background-color: #222 !important; + border-color: #3c3c3c !important; + } + .inverted code { + background-color: #222 !important; + border-color: #3c3c3c !important; + } + .inverted blockquote { + border-color: #333 !important; + color: #999 !important; + } + .inverted table { + border-color: #3c3c3c !important; + } + .inverted table th { + background-color: #222 !important; + border-color: #3c3c3c !important; + } + .inverted table td { + border-color: #3c3c3c !important; + } + .inverted mark { + background: #bc990b !important; + color:#000 !important; + } + .inverted .shadow { -webkit-box-shadow: 0 2px 4px #000 !important; } + #wrapper { + background: transparent; + margin: 40px; + } +} + +/* Printing support */ +@media print { + body { + overflow: auto; + } + img, pre, blockquote, table, figure { + page-break-inside: avoid; + } + pre, code { + border: none !important; + } + #wrapper { + background: #fff; + position: relative; + text-indent: 0px; + padding: 10px; + font-size:85%; + } + .footnotes { + page-break-before: always; + } +} diff --git a/doc/themes/new-modern.css b/doc/themes/new-modern.css new file mode 100644 index 00000000..071d0d47 --- /dev/null +++ b/doc/themes/new-modern.css @@ -0,0 +1,482 @@ +/* Title: New Modern */ +/* Author: Jocelyn Richard http://jocelynrichard.com/ */ +/* Description: Baseline style, meant to be used on its own or to serve as development basis. */ + +/* ================================================ */ +/* 1. Reset */ +/* 2. Skeleton */ +/* 3. Media Queries */ +/* 4. Print Styles */ +/* ================================================ */ + + + +/* ================================================ */ +/* 1. Reset */ +/* ================================================ */ + +html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, +b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas, details, embed, figure, figcaption, footer, header, hgroup, menu, nav, output, ruby, section, summary, time, mark, audio, video {margin: 0; padding: 0; border: 0;} /* Edited from http://www.cssreset.com/scripts/eric-meyer-reset-css/ */ + +article, aside, details, figcaption, figure, footer, header, hgroup, nav, section, summary {display: block;} /* Semantic tags definition for IE 6/7/8/9 and Firefox 3 */ + +html {-webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%;} /* Prevents iOS text size adjust after orientation change, without disabling user zoom */ + + + +/* ================================================ */ +/* 2. Skeleton */ +/* ================================================ */ + +/* ------------------------------------------------- */ +/* General */ +/* ------------------------------------------------- */ + +html { + font-size: 14px; +} + +body { + font-family: 'Open Sans', sans-serif; + /*margin: 3.42rem 1.71rem !important;*/ /* Get margins even if the Markdown rendering app doesn't include any */ + background-color: white; + color: #222; +} + +#wrapper { /* #wrapper: ID added by Marked */ + max-width: 42rem; + margin: 0 auto; + margin-left: auto !important; /* Countering toc.css added by Marked */ + padding: 1.71rem 0 !important; /* Countering toc.css added by Marked */ +} + +/* ------------------------------------------------- */ +/* Typography */ +/* ------------------------------------------------- */ + +h1, +h2, +h3, +h4, +h5, +h6 { + margin-bottom: 1.6rem; +} + +h1, +h2 { + margin-top: 3.2rem; +} + +h1 { + font-size: 2.82rem; /* 42.3px @15px */ + line-height: 3.2rem; /* 48px @15px */ +} + +h2 { + font-size: 1.99rem; /* 29.9px @15px */ + line-height: 2.4rem; /* 36px @15px */ +} + +h3 { + font-size: 1.41rem; /* 21.2px @15px */ + line-height: 2rem; /* 30px @15px */ +} + +h4 { + font-size: 1rem; /* 15px @15px */ + line-height: 1.6rem; /* 24px @15px */ +} + +h5, h6 { + font-size: 0.8rem; + line-height: 1.2rem; + text-transform: uppercase; +} + +h6 { + margin-left: 1.6rem; +} + +p, +ol, +ul, +blockquote { + font-size: 1rem; + line-height: 1.6rem; + margin-bottom: 1.6rem; +} + +ul ul, +ul ol, +ol ul, +ol ol { + margin-left: 1.6rem; + margin-top: 1.6rem; +} + +#generated-toc ul ul, /* #generated-toc: added by Marked for its table of contents */ +#generated-toc ul ol, +#generated-toc ol ul, +#generated-toc ol ol { + margin-top: 0; + margin-bottom: 0; + padding-top: 0; + padding-bottom: 0; +} + +blockquote { + margin: 0 0 1.6rem 2.4rem; + padding-left: 0.8rem; /* Voire */ + border-left: 4px solid rgba(0,0,0,0.08); + font-style: normal; +} + +blockquote ul { + margin-left: 0.8rem; /* Pour ne pas que les hanging bullets mordent sur le blockquote */ +} + +ol li blockquote, /* So that blockquote work in lists */ +ul li blockquote { + margin-left: 0; +} + +a:link { + text-decoration: none; + color: #165bd4; + border-bottom: 1px solid #ccc; +} + +a:visited { + color: #7697cf; + border-bottom: 1px solid #ccc; +} + +a:hover { + border-color: #165bd4; +} + +a:active { + background-color: #e6e6e6; +} + +/* ------------------------------------------------- */ +/* Tables */ +/* ------------------------------------------------- */ + +table { + font-size: 0.85rem; + margin: 0 0 1.6rem 0; + border-collapse: collapse; + border: 1px solid #ccc; +} + +th, +td { + padding: 0.5rem 0.75rem; + max-width: 20rem; /* Avoid dropping lines for nothing without having ridiculously wide tables */ +} + +th { + border-bottom: 2px solid #222; +} + +tr { + border-bottom: 1px solid #ccc; +} + +tbody tr:nth-child(odd) { + background-color: #f9f9f9; +} + +table code { + font-size: 85%; +} + +/* ------------------------------------------------- */ +/* Misc */ +/* ------------------------------------------------- */ + +img { + max-width: 100% +} + +caption, +figcaption { + font-size: 0.85rem; + line-height: 1.6rem; + margin: 0 1.6rem; + text-align: left; +} + +figcaption { + margin-bottom: 1.6rem; +} + +h1, /* White-space mentions in order to force wrapping */ +h2, +a:link, +pre { + white-space: pre; /* CSS 2.0 */ + white-space: pre-wrap; /* CSS 2.1 */ + white-space: pre-line; /* CSS 3.0 */ + white-space: -pre-wrap; /* Opera 4-6 */ + white-space: -o-pre-wrap; /* Opera 7 */ + white-space: -moz-pre-wrap; /* Mozilla */ + white-space: -hp-pre-wrap; /* HP Printers */ + word-wrap: break-word; /* IE 5+ */ +} + +code { + font-family: "Menlo", "Courier New", "Courier", monospace; + font-size: 85%; + color: #666; + background-color: rgba(0,0,0,0.08); + padding: 2px 4px; + border-radius: 2px; +} + +pre { + background-color: rgba(0,0,0,0.08); + border-radius: 8px; + padding: 0.4rem; + margin-bottom: 1.6rem; +} + +pre code { /* Counter the code mentions */ + background-color: transparent; + padding: 0; +} + +sup, +sub, +a.footnote { /* Keep line-height from being affected by sub, cf https://gist.github.com/unruthless/413930 */ + font-size: 75%; + height: 0; + line-height: 1; + position: relative; +} + +sup, +a.footnote { + vertical-align:super; +} + +sub { + vertical-align: sub; +} + +dt { + font-weight: 600; +} + +dd { + font-size: 1rem; + line-height: 1.6rem; + margin-bottom: 1.6rem; +} + +hr { + clear: none; + height: 0.2rem; + border: none; + margin: 0 auto 1.4rem auto; /* 2.4rem auto 2.2rem auto; */ + width: 100%; + color: #ccc; + background-color: #ccc; +} + +::selection { + background-color: #f8dc77; +} + +::-moz-selection { + background-color: #f8dc77; +} + +a:focus { + outline: 2px solid; + outline-color: #165bd4; +} + +/* ------------------------------------------------- */ +/* Animations */ +/* ------------------------------------------------- */ + +a:hover { + -moz-transition: all 0.2s ease-in-out; + -webkit-transition: all 0.2s ease-in-out; +} + +h1, +h2, +h3, +h4, +h5, +h6, +p, +blockquote { + -moz-transition: all 0.2s ease; + -webkit-transition: all 0.2s ease; +} + + + +/* ================================================ */ +/* 3. Media Queries */ +/* ================================================ */ + +/* Base styles are for smartphones; elements are then tweaked as the viewport grows. */ + +/* ------------------------------------------------- */ +/* iPad and desktop */ +/* ------------------------------------------------- */ + +@media only screen and (min-width: 641px) { + + html { + font-size: 15px; + } + + body { + /*margin: 4.8em 2.4rem 3.2rem 2.4rem !important;*/ + } + + h1 { + font-size: 3.57rem; /* 53.2px @15px */ + line-height: 4rem; /* 60px @15px */ + } + + h2 { + font-size: 2.24rem; /* 33.6px @15px */ + line-height: 2.8rem; /* 42px @15px */ + } + +} + +/* ------------------------------------------------- */ +/* Widescreens */ +/* ------------------------------------------------- */ + +@media only screen and (min-width: 1441px) { + + html { + font-size: 22px; + } + +} + + + +/* ================================================ */ +/* 4. Print Styles */ +/* ================================================ */ + +/* Inconsistent and buggy across browsers */ + +@media print { + + * { + background: transparent !important; + color: #000 !important; /* Black text prints faster and browsers are inconsistent in color reproduction anyway: h5bp.com/s */ + } + + @page { + margin: 1cm; /* Added to any #wrapper margin*/ + } + + html { + font-size: 15px; + } + + body { + margin: 1rem !important; /* Security margins for browser without @page support */ + } + + #wrapper { + max-width: none; + } + + h1, + h2, + h3, + h4, + h5, + h6, + p { + orphans: 3; + widows: 3; + page-break-after: avoid; + } + + ul, + ol { + list-style-position: inside !important; + padding-right: 0 !important; + margin-left: 0 !important; + } + + ul ul, + ul ol, + ol ul, + ol ol, + ul p:not(:first-child), + ol p:not(:first-child) { + margin-left: 2rem !important; + } + + a:link, + a:visited { + text-decoration: underline !important; + font-weight: normal !important; + } + + a[href]:after { + content: " (" attr(href) ")"; + } + + a[href^="javascript:"]:after, + a[href^="#"]:after { + content: ""; /* Do not show javascript and internal links */ + } + + a[href^="#"] { + text-decoration: none !important; + } + + th { + background-color: rgba(0,0,0,0.2) !important; + border-bottom: none !important; + } + + tr { + page-break-inside: avoid; + } + + tbody tr:nth-child(even) { + background-color: rgba(0,0,0,0.1) !important; + } + + pre { + border: 1px solid rgba(0,0,0,0.2); + page-break-inside: avoid; + } + + img { + max-width: 100% !important; + page-break-inside: avoid; + } + + /* #generated-toc: added by Marked for its table of contents */ + + #wrapper #generated-toc ul, /* Table of contents printing in Marked */ + #wrapper #generated-toc ol { + list-style-type: decimal; + } + + #wrapper #generated-toc ul li, + #wrapper #generated-toc ol li { + margin: 1rem 0; + } + +} diff --git a/doc/themes/radar.css b/doc/themes/radar.css new file mode 100644 index 00000000..be0e86c9 --- /dev/null +++ b/doc/themes/radar.css @@ -0,0 +1,355 @@ + +body { + margin: 0px; + + font-family: 'PT Sans', Helvetica, 'Helvetica Neuve', Arial, Tahoma, sans-serif; + font-size: 17px; + + color: #333; +} + +h1, h2, h3, h4, h5, h6 { + color:#222; + margin:0 0 20px; +} + +p, ul, ol, table, pre, dl { + margin:0 0 20px; +} + +h1, h2, h3 { + line-height:1.1; +} + +h1 { + font-size:28px; +} + +h2 { + color:#393939; +} + +h3, h4, h5, h6 { + color:#494949; +} + +a { + color:#39c; + font-weight:400; + text-decoration:none; +} + +a small { + font-size:11px; + color:#777; + margin-top:-0.6em; + display:block; +} + +.wrapper { + width:860px; + margin:0 auto; +} + +blockquote { + border-left:1px solid #e5e5e5; + margin:0; + padding:0 0 0 20px; + font-style:italic; +} + +code, pre { + color:#333; + font-size:12px; +} + +pre { + padding:8px 15px; + background: #f8f8f8; + border-radius:5px; + border:1px solid #e5e5e5; + overflow-x: auto; +} + +table { + width:100%; + border-collapse:collapse; +} + +th, td { + text-align:left; + padding:5px 10px; + border-bottom:1px solid #e5e5e5; +} + +dt { + color:#444; + font-weight:700; +} + +th { + color:#444; +} + +img { + max-width:100%; +} + +header { + width:270px; + float:left; + position:fixed; +} + +header ul { + list-style:none; + height:40px; + + padding:0; + + background: #eee; + background: -moz-linear-gradient(top, #f8f8f8 0%, #dddddd 100%); + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#f8f8f8), color-stop(100%,#dddddd)); + background: -webkit-linear-gradient(top, #f8f8f8 0%,#dddddd 100%); + background: -o-linear-gradient(top, #f8f8f8 0%,#dddddd 100%); + background: -ms-linear-gradient(top, #f8f8f8 0%,#dddddd 100%); + background: linear-gradient(top, #f8f8f8 0%,#dddddd 100%); + + border-radius:5px; + border:1px solid #d2d2d2; + box-shadow:inset #fff 0 1px 0, inset rgba(0,0,0,0.03) 0 -1px 0; + width:270px; +} + +header li { + width:89px; + float:left; + border-right:1px solid #d2d2d2; + height:40px; +} + +header ul a { + line-height:1; + font-size:11px; + color:#999; + display:block; + text-align:center; + padding-top:6px; + height:40px; +} + +strong { + color:#222; + font-weight:700; +} + +header ul li + li { + width:88px; + border-left:1px solid #fff; +} + +header ul li + li + li { + border-right:none; + width:89px; +} + +header ul a strong { + font-size:14px; + display:block; + color:#222; +} + +section { + width:500px; + float:right; + padding-bottom:50px; +} + +small { + font-size:11px; +} + +hr { + border:0; + background:#e5e5e5; + height:1px; + margin:0 0 20px; +} + +footer { + width:270px; + float:left; + position:fixed; + bottom:50px; +} + +@media print, screen and (max-width: 960px) { + + div.wrapper { + width:auto; + margin:0; + } + + header, section, footer { + float:none; + position:static; + width:auto; + } + + header { + padding-right:320px; + } + + section { + border:1px solid #e5e5e5; + border-width:1px 0; + padding:20px 0; + margin:0 0 20px; + } + + header a small { + display:inline; + } + + header ul { + position:absolute; + right:50px; + top:52px; + } +} + +@media print, screen and (max-width: 720px) { + body { + word-wrap:break-word; + } + + header { + padding:0; + } + + header ul, header p.view { + position:static; + } + + pre, code { + word-wrap:normal; + } +} + +@media print, screen and (max-width: 480px) { + body { + padding:15px; + } + + header ul { + display:none; + } +} + +@media print { + body { + padding:0.4in; + font-size:12pt; + color:#444; + } +} + + +#wrapper { + padding: 1em; +} + +.ca-menu { + list-style: none; + padding: 0; + margin: 20px auto; +} + +#toc { + top: 0; + right: 0; + bottom: 0; + left: auto; + width: 20%; + background-color: #fff; + padding: 20px; + position: fixed; + z-index: 1; + display: none; + height: 100%; +} + + +#toc::before { + content: ""; + position: absolute; + + top: 15%; + bottom: 15%; + left: -1px; + width: 1px; + background: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, rgba(227,224,216,0)), color-stop(20%, #e3e0d8), color-stop(80%, #e3e0d8), color-stop(100%, rgba(227,224,216,0))); + background: -webkit-linear-gradient(top, rgba(227,224,216,0) 0%,#e3e0d8 20%,#e3e0d8 80%,rgba(227,224,216,0) 100%); + background: -moz-linear-gradient(top, rgba(227,224,216,0) 0%,#e3e0d8 20%,#e3e0d8 80%,rgba(227,224,216,0) 100%); + background: -o-linear-gradient(top, rgba(227,224,216,0) 0%,#e3e0d8 20%,#e3e0d8 80%,rgba(227,224,216,0) 100%); + background: linear-gradient(top, rgba(227,224,216,0) 0%,#e3e0d8 20%,#e3e0d8 80%,rgba(227,224,216,0) 100%); +} + +#toc-inner { + display: table-cell; + vertical-align: middle; +} + +.nav-list { + height: 50%; + margin: auto 0; +} + +div.clear { + clear: both; +} + +h1 { font-size: 2.5em; line-height: 1; } +h2 { font-size: 2em; line-height: 1; } +h3 { font-size: 1.5em; line-height: 1; } +h4 { font-size: 1.2em; line-height: 1.25; } +h5 { font-size: 1em; line-height: 1; font-weight: bold; } +h6 { font-size: 1em; line-height: 1; font-weight: bold; } + +h1, h2, h3, h4, h5, h6 { font-weight: normal; margin-top: 1em; margin-bottom: 0.5em; } +h1, h2 { margin-bottom: 0.5em; } + +.post p { + max-width: 580px; +} + +ul.list, ol.list { + padding-left: 3.333em; + max-width: 580px; +} + +.post h2 { + border-bottom: 1px solid #EDEDED; +} + +h1:nth-child(1), +h2:nth-child(1), +h3:nth-child(1), +h4:nth-child(1), +h5:nth-child(1), +h6:nth-child(1) { + margin-top: 0; +} + +@media (min-width: 43.75em) { + #wrapper { + width: 650px; + padding: 20px 50px; + } +} + +@media (min-width: 62em) { + #toc { + display: table; + } +} diff --git a/doc/themes/screen.css b/doc/themes/screen.css new file mode 100644 index 00000000..89debb6c --- /dev/null +++ b/doc/themes/screen.css @@ -0,0 +1,77 @@ +html { font-size: 62.5%; } +html, body { height: 100%; } + +body { + font-family: Helvetica, Arial, sans-serif; + font-size: 150%; + line-height: 1.3; + color: #f6e6cc; + width: 700px; + margin: auto; + background: #27221a; + position: relative; + padding: 0 30px; +} + +p,ul,ol,dl,table,pre { margin-bottom: 1em; } +ul { margin-left: 20px; } +a { text-decoration: none; cursor: pointer; color: #ba832c; font-weight: bold; } +a:focus { outline: 1px dotted; } +a:visited { } +a:hover, a:focus { color: #d3a459; text-decoration: none; } +a *, button * { cursor: pointer; } +hr { display: none; } +small { font-size: 90%; } +input, select, button, textarea, option { font-family: Arial, "Lucida Grande", "Lucida Sans Unicode", Arial, Verdana, sans-serif; font-size: 100%; } +button, label, select, option, input[type=submit] { cursor: pointer; } +.group:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } .group {display: inline-block;} +/* Hides from IE-mac \*/ * html .group {height: 1%;} .group {display: block;} /* End hide from IE-mac */ +sup { font-size: 80%; line-height: 1; vertical-align: super; } +button::-moz-focus-inner { border: 0; padding: 1px; } +span.amp { font-family: Baskerville, "Goudy Old Style", "Palatino", "Book Antiqua", serif; font-weight: normal; font-style: italic; font-size: 1.2em; line-height: 0.8; } + +h1,h2,h3,h4,h5,h6 { + line-height: 1.1; + font-family: Baskerville, "Goudy Old Style", "Palatino", "Book Antiqua", serif; +} + +h2 { font-size: 22pt; } +h3 { font-size: 20pt; } +h4 { font-size: 18pt; } +h5 { font-size: 16pt; } +h6 { font-size: 14pt; } + +::selection { background: #745626; } +::-moz-selection { background: #745626; } + +h1 { + font-size: 420%; + margin: 0 0 0.1em; + font-family: Baskerville, "Goudy Old Style", "Palatino", "Book Antiqua", serif; +} + +h1 a, +h1 a:hover { + color: #d7af72; + font-weight: normal; + text-decoration: none; +} + +pre { + background: rgba(0,0,0,0.3); + color: #fff; + padding: 8px 10px; + border-radius: 0.4em; + -moz-border-radius: 0.4em; + -webkit-border-radius: 0.4em; + overflow-x: hidden; +} + +pre code { + font-size: 10pt; +} + +.thumb { + float:left; + margin: 10px; +} diff --git a/doc/themes/solarized-dark.css b/doc/themes/solarized-dark.css new file mode 100644 index 00000000..d6ca5c71 --- /dev/null +++ b/doc/themes/solarized-dark.css @@ -0,0 +1,294 @@ +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +nav, +section, +summary { + display: block; +} +audio, +canvas, +video { + display: inline-block; +} +audio:not([controls]) { + display: none; + height: 0; +} +[hidden] { + display: none; +} +html { + font-family: sans-serif; + -webkit-text-size-adjust: 100%; + -ms-text-size-adjust: 100%; +} +body { + margin: 0; +} +a:focus { + outline: thin dotted; +} +a:active, +a:hover { + outline: 0; +} +h1 { + font-size: 2em; +} +abbr[title] { + border-bottom: 1px dotted; +} +b, +strong { + font-weight: bold; +} +dfn { + font-style: italic; +} +mark { + background: #ff0; + color: #000; +} +code, +kbd, +pre, +samp { + font-family: monospace, serif; + font-size: 1em; +} +pre { + white-space: pre-wrap; + word-wrap: break-word; +} +q { + quotes: "\201C" "\201D" "\2018" "\2019"; +} +small { + font-size: 80%; +} +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} +sup { + top: -0.5em; +} +sub { + bottom: -0.25em; +} +img { + border: 0; +} +svg:not(:root) { + overflow: hidden; +} +figure { + margin: 0; +} +fieldset { + border: 1px solid #c0c0c0; + margin: 0 2px; + padding: 0.35em 0.625em 0.75em; +} +legend { + border: 0; + padding: 0; +} +button, +input, +select, +textarea { + font-family: inherit; + font-size: 100%; + margin: 0; +} +button, +input { + line-height: normal; +} +button, +html input[type="button"], +input[type="reset"], +input[type="submit"] { + -webkit-appearance: button; + cursor: pointer; +} +button[disabled], +input[disabled] { + cursor: default; +} +input[type="checkbox"], +input[type="radio"] { + box-sizing: border-box; + padding: 0; +} +input[type="search"] { + -webkit-appearance: textfield; + -moz-box-sizing: content-box; + -webkit-box-sizing: content-box; + box-sizing: content-box; +} +input[type="search"]::-webkit-search-cancel-button, +input[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; +} +button::-moz-focus-inner, +input::-moz-focus-inner { + border: 0; + padding: 0; +} +textarea { + overflow: auto; + vertical-align: top; +} +table { + border-collapse: collapse; + border-spacing: 0; +} + +html { + font-family: 'PT Sans', sans-serif; +} +pre, +code { + font-family: 'Inconsolata', sans-serif; +} +h1, +h2, +h3, +h4, +h5, +h6 { + font-family: 'PT Sans Narrow', sans-serif; + font-weight: 700; +} +html { + background-color: #002b36; + color: #839496; + margin: 1em; +} +code { + /*background-color: #073642;*/ + padding: 2px; +} +a { + color: #b58900; +} +a:visited { + color: #cb4b16; +} +a:hover { + color: #cb4b16; +} +h1 { + color: #d33682; +} +h2, +h3, +h4, +h5, +h6 { + color: #859900; +} +pre { + /*background-color: #002b36;*/ + color: #839496; + border: 1pt solid #586e75; + padding: 1em; + box-shadow: 5pt 5pt 8pt #073642; +} +pre code { + /*background-color: #002b36;*/ +} +h1 { + font-size: 2.8em; +} +h2 { + font-size: 2.4em; +} +h3 { + font-size: 1.8em; +} +h4 { + font-size: 1.4em; +} +h5 { + font-size: 1.3em; +} +h6 { + font-size: 1.15em; +} +.tag { + /*background-color: #073642;*/ + color: #d33682; + padding: 0 0.2em; +} +.todo, +.next, +.done { + color: #002b36; + background-color: #dc322f; + padding: 0 0.2em; +} +.tag { + -webkit-border-radius: 0.35em; + -moz-border-radius: 0.35em; + border-radius: 0.35em; +} +.TODO { + -webkit-border-radius: 0.2em; + -moz-border-radius: 0.2em; + border-radius: 0.2em; + background-color: #2aa198; +} +.NEXT { + -webkit-border-radius: 0.2em; + -moz-border-radius: 0.2em; + border-radius: 0.2em; + background-color: #268bd2; +} +.ACTIVE { + -webkit-border-radius: 0.2em; + -moz-border-radius: 0.2em; + border-radius: 0.2em; + background-color: #268bd2; +} +.DONE { + -webkit-border-radius: 0.2em; + -moz-border-radius: 0.2em; + border-radius: 0.2em; + background-color: #859900; +} +.WAITING { + -webkit-border-radius: 0.2em; + -moz-border-radius: 0.2em; + border-radius: 0.2em; + foreground-color: #cb4b16; +} +.HOLD { + -webkit-border-radius: 0.2em; + -moz-border-radius: 0.2em; + border-radius: 0.2em; + foreground-color: #d33682; +} +.NOTE { + -webkit-border-radius: 0.2em; + -moz-border-radius: 0.2em; + border-radius: 0.2em; + foreground-color: #d33682; +} +.CANCELLED { + -webkit-border-radius: 0.2em; + -moz-border-radius: 0.2em; + border-radius: 0.2em; + foreground-color: #859900; +} diff --git a/doc/themes/solarized-light.css b/doc/themes/solarized-light.css new file mode 100644 index 00000000..87f26725 --- /dev/null +++ b/doc/themes/solarized-light.css @@ -0,0 +1,294 @@ +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +nav, +section, +summary { + display: block; +} +audio, +canvas, +video { + display: inline-block; +} +audio:not([controls]) { + display: none; + height: 0; +} +[hidden] { + display: none; +} +html { + font-family: sans-serif; + -webkit-text-size-adjust: 100%; + -ms-text-size-adjust: 100%; +} +body { + margin: 0; +} +a:focus { + outline: thin dotted; +} +a:active, +a:hover { + outline: 0; +} +h1 { + font-size: 2em; +} +abbr[title] { + border-bottom: 1px dotted; +} +b, +strong { + font-weight: bold; +} +dfn { + font-style: italic; +} +mark { + background: #ff0; + color: #000; +} +code, +kbd, +pre, +samp { + font-family: monospace, serif; + font-size: 1em; +} +pre { + white-space: pre-wrap; + word-wrap: break-word; +} +q { + quotes: "\201C" "\201D" "\2018" "\2019"; +} +small { + font-size: 80%; +} +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} +sup { + top: -0.5em; +} +sub { + bottom: -0.25em; +} +img { + border: 0; +} +svg:not(:root) { + overflow: hidden; +} +figure { + margin: 0; +} +fieldset { + border: 1px solid #c0c0c0; + margin: 0 2px; + padding: 0.35em 0.625em 0.75em; +} +legend { + border: 0; + padding: 0; +} +button, +input, +select, +textarea { + font-family: inherit; + font-size: 100%; + margin: 0; +} +button, +input { + line-height: normal; +} +button, +html input[type="button"], +input[type="reset"], +input[type="submit"] { + -webkit-appearance: button; + cursor: pointer; +} +button[disabled], +input[disabled] { + cursor: default; +} +input[type="checkbox"], +input[type="radio"] { + box-sizing: border-box; + padding: 0; +} +input[type="search"] { + -webkit-appearance: textfield; + -moz-box-sizing: content-box; + -webkit-box-sizing: content-box; + box-sizing: content-box; +} +input[type="search"]::-webkit-search-cancel-button, +input[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; +} +button::-moz-focus-inner, +input::-moz-focus-inner { + border: 0; + padding: 0; +} +textarea { + overflow: auto; + vertical-align: top; +} +table { + border-collapse: collapse; + border-spacing: 0; +} + +html { + font-family: 'PT Sans', sans-serif; +} +pre, +code { + font-family: 'Inconsolata', sans-serif; +} +h1, +h2, +h3, +h4, +h5, +h6 { + font-family: 'PT Sans Narrow', sans-serif; + font-weight: 700; +} +html { + background-color: #fdf6e3; + color: #657b83; + margin: 1em; +} +code { + background-color: #eee8d5; + padding: 2px; +} +a { + color: #b58900; +} +a:visited { + color: #cb4b16; +} +a:hover { + color: #cb4b16; +} +h1 { + color: #d33682; +} +h2, +h3, +h4, +h5, +h6 { + color: #859900; +} +pre { + background-color: #fdf6e3; + color: #657b83; + border: 1pt solid #93a1a1; + padding: 1em; + box-shadow: 5pt 5pt 8pt #eee8d5; +} +pre code { + background-color: #fdf6e3; +} +h1 { + font-size: 2.8em; +} +h2 { + font-size: 2.4em; +} +h3 { + font-size: 1.8em; +} +h4 { + font-size: 1.4em; +} +h5 { + font-size: 1.3em; +} +h6 { + font-size: 1.15em; +} +.tag { + background-color: #eee8d5; + color: #d33682; + padding: 0 0.2em; +} +.todo, +.next, +.done { + color: #fdf6e3; + background-color: #dc322f; + padding: 0 0.2em; +} +.tag { + -webkit-border-radius: 0.35em; + -moz-border-radius: 0.35em; + border-radius: 0.35em; +} +.TODO { + -webkit-border-radius: 0.2em; + -moz-border-radius: 0.2em; + border-radius: 0.2em; + background-color: #2aa198; +} +.NEXT { + -webkit-border-radius: 0.2em; + -moz-border-radius: 0.2em; + border-radius: 0.2em; + background-color: #268bd2; +} +.ACTIVE { + -webkit-border-radius: 0.2em; + -moz-border-radius: 0.2em; + border-radius: 0.2em; + background-color: #268bd2; +} +.DONE { + -webkit-border-radius: 0.2em; + -moz-border-radius: 0.2em; + border-radius: 0.2em; + background-color: #859900; +} +.WAITING { + -webkit-border-radius: 0.2em; + -moz-border-radius: 0.2em; + border-radius: 0.2em; + foreground-color: #cb4b16; +} +.HOLD { + -webkit-border-radius: 0.2em; + -moz-border-radius: 0.2em; + border-radius: 0.2em; + foreground-color: #d33682; +} +.NOTE { + -webkit-border-radius: 0.2em; + -moz-border-radius: 0.2em; + border-radius: 0.2em; + foreground-color: #d33682; +} +.CANCELLED { + -webkit-border-radius: 0.2em; + -moz-border-radius: 0.2em; + border-radius: 0.2em; + foreground-color: #859900; +} diff --git a/doc/themes/torpedo.css b/doc/themes/torpedo.css new file mode 100644 index 00000000..dcd92188 --- /dev/null +++ b/doc/themes/torpedo.css @@ -0,0 +1,666 @@ +/* Title: Torpedo */ +/* Author: Jocelyn Richard http://jocelynrichard.com/ */ +/* Description: A muted color palette for long-form writing or reading, suited for technical documentation. Works best with Cinta: http://www.myfonts.com/fonts/tipo-pepel/cinta/ */ + +/* ================================================ */ +/* 1. Reset */ +/* 2. Skeleton */ +/* 3. Media Queries */ +/* 4. Print Styles */ +/* 5. Torpedo Overrides */ +/* ================================================ */ + + + +/* ================================================ */ +/* 1. Reset */ +/* ================================================ */ + +html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, +b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas, details, embed, figure, figcaption, footer, header, hgroup, menu, nav, output, ruby, section, summary, time, mark, audio, video {margin: 0; padding: 0; border: 0;} /* Edited from http://www.cssreset.com/scripts/eric-meyer-reset-css/ */ + +article, aside, details, figcaption, figure, footer, header, hgroup, nav, section, summary {display: block;} /* Semantic tags definition for IE 6/7/8/9 and Firefox 3 */ + +html {-webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%;} /* Prevents iOS text size adjust after orientation change, without disabling user zoom */ + + + +/* ================================================ */ +/* 2. Skeleton */ +/* ================================================ */ + +/* ------------------------------------------------- */ +/* General */ +/* ------------------------------------------------- */ + +html { + font-size: 14px; +} + +body { + font-family: 'Open Sans', sans-serif; + margin: 1.71rem 1.71rem 3rem 1.71rem ; /* Get margins even if the Markdown rendering app doesn't include any */ + background-color: white; + color: #222; +} + +#wrapper { /* #wrapper: ID added by Marked */ + max-width: 42rem; + margin: 0 auto; + margin-left: auto !important; /* Countering toc.css added by Marked */ + padding: 1.71rem 0 !important; /* Countering toc.css added by Marked */ +} + +/* ------------------------------------------------- */ +/* Typography */ +/* ------------------------------------------------- */ + +h1, +h2, +h3, +h4, +h5, +h6 { + margin-bottom: 1.6rem; +} + +h1, +h2 { + margin-top: 3.2rem; +} + +h1 { + font-size: 2.82rem; /* 42.3px @15px */ + line-height: 3.2rem; /* 48px @15px */ +} + +h2 { + font-size: 1.99rem; /* 29.9px @15px */ + line-height: 2.4rem; /* 36px @15px */ +} + +h3 { + font-size: 1.41rem; /* 21.2px @15px */ + line-height: 2rem; /* 30px @15px */ +} + +h4 { + font-size: 1rem; /* 15px @15px */ + line-height: 1.6rem; /* 24px @15px */ +} + +h5, h6 { + font-size: 0.8rem; + line-height: 1.2rem; + text-transform: uppercase; +} + +h6 { + margin-left: 1.6rem; +} + +p, +ol, +ul, +blockquote { + font-size: 1rem; + line-height: 1.6rem; + margin-bottom: 1.6rem; +} + +ul ul, +ul ol, +ol ul, +ol ol { + margin-left: 1.6rem; + margin-top: 1.6rem; +} + +#generated-toc ul ul, /* #generated-toc: added by Marked for its table of contents */ +#generated-toc ul ol, +#generated-toc ol ul, +#generated-toc ol ol { + margin-top: 0; + margin-bottom: 0; + padding-top: 0; + padding-bottom: 0; +} + +blockquote { + margin: 0 0 1.6rem 2.4rem; + padding-left: 0.8rem; /* Voire */ + border-left: 4px solid rgba(0,0,0,0.08); + font-style: normal; +} + +blockquote ul { + margin-left: 0.8rem; /* Pour ne pas que les hanging bullets mordent sur le blockquote */ +} + +ol li blockquote, /* So that blockquote work in lists */ +ul li blockquote { + margin-left: 0; +} + +a:link { + text-decoration: none; + color: #165bd4; + border-bottom: 1px solid #ccc; +} + +a:visited { + color: #7697cf; + border-bottom: 1px solid #ccc; +} + +a:hover { + border-color: #165bd4; +} + +a:active { + background-color: #e6e6e6; +} + +/* ------------------------------------------------- */ +/* Tables */ +/* ------------------------------------------------- */ + +table { + font-size: 0.85rem; + margin: 0 0 1.6rem 0; + border-collapse: collapse; + border: 1px solid #ccc; +} + +th, +td { + padding: 0.5rem 0.75rem; + max-width: 20rem; /* Avoid dropping lines for nothing without having ridiculously wide tables */ +} + +th { + border-bottom: 2px solid #222; +} + +tr { + border-bottom: 1px solid #ccc; +} + +tbody tr:nth-child(odd) { + background-color: #f9f9f9; +} + +table code { + font-size: 85%; +} + +/* ------------------------------------------------- */ +/* Misc */ +/* ------------------------------------------------- */ + +img { + max-width: 100% +} + +caption, +figcaption { + font-size: 0.85rem; + line-height: 1.6rem; + margin: 0 1.6rem; + text-align: left; +} + +figcaption { + margin-bottom: 1.6rem; +} + +h1, /* White-space mentions in order to force wrapping */ +h2, +a:link, +pre { + white-space: pre; /* CSS 2.0 */ + white-space: pre-wrap; /* CSS 2.1 */ + white-space: pre-line; /* CSS 3.0 */ + white-space: -pre-wrap; /* Opera 4-6 */ + white-space: -o-pre-wrap; /* Opera 7 */ + white-space: -moz-pre-wrap; /* Mozilla */ + white-space: -hp-pre-wrap; /* HP Printers */ + word-wrap: break-word; /* IE 5+ */ +} + +code { + font-family: "Menlo", "Courier New", "Courier", monospace; + font-size: 85%; + color: #666; + background-color: rgba(0,0,0,0.08); + padding: 2px 4px; + border-radius: 2px; +} + +pre { + background-color: rgba(0,0,0,0.08); + border-radius: 8px; + padding: 0.4rem; + margin-bottom: 1.6rem; +} + +pre code { /* Counter the code mentions */ + background-color: transparent; + padding: 0; +} + +sup, +sub, +a.footnote { /* Keep line-height from being affected by sub, cf https://gist.github.com/unruthless/413930 */ + font-size: 75%; + height: 0; + line-height: 1; + position: relative; +} + +sup, +a.footnote { + vertical-align:super; +} + +sub { + vertical-align: sub; +} + +dt { + font-weight: 600; +} + +dd { + font-size: 1rem; + line-height: 1.6rem; + margin-bottom: 1.6rem; +} + +hr { + clear: none; + height: 0.2rem; + border: none; + margin: 0 auto 1.4rem auto; /* 2.4rem auto 2.2rem auto; */ + width: 100%; + color: #ccc; + background-color: #ccc; +} + +::selection { + background-color: #f8dc77; +} + +::-moz-selection { + background-color: #f8dc77; +} + +a:focus { + outline: 2px solid; + outline-color: #165bd4; +} + +/* ------------------------------------------------- */ +/* Animations */ +/* ------------------------------------------------- */ + +a:hover { + -moz-transition: all 0.2s ease-in-out; + -webkit-transition: all 0.2s ease-in-out; +} + +h1, +h2, +h3, +h4, +h5, +h6, +p, +blockquote { + -moz-transition: all 0.2s ease; + -webkit-transition: all 0.2s ease; +} + + + +/* ================================================ */ +/* 3. Media Queries */ +/* ================================================ */ + +/* Base styles are for smartphones; elements are then tweaked as the viewport grows. */ + +/* ------------------------------------------------- */ +/* iPad and desktop */ +/* ------------------------------------------------- */ + +@media only screen and (min-width: 641px) { + + html { + font-size: 15px; + } + + body { + margin: 2.4rem 2.4rem 3.2rem 2.4rem; + } + + h1 { + font-size: 3.57rem; /* 53.2px @15px */ + line-height: 4rem; /* 60px @15px */ + } + + h2 { + font-size: 2.24rem; /* 33.6px @15px */ + line-height: 2.8rem; /* 42px @15px */ + } + +} + +/* ------------------------------------------------- */ +/* Widescreens */ +/* ------------------------------------------------- */ + +@media only screen and (min-width: 1441px) { + + html { + font-size: 22px; + } + +} + + + +/* ================================================ */ +/* 4. Print Styles */ +/* ================================================ */ + +/* Inconsistent and buggy across browsers */ + +@media print { + + * { + background: transparent !important; + color: #000 !important; /* Black text prints faster and browsers are inconsistent in color reproduction anyway: h5bp.com/s */ + } + + @page { + margin: 1cm; /* Added to any #wrapper margin*/ + } + + html { + font-size: 15px; + } + + body { + margin: 1rem !important; /* Security margins for browser without @page support */ + } + + #wrapper { + max-width: none; + } + + h1, + h2, + h3, + h4, + h5, + h6, + p { + orphans: 3; + widows: 3; + page-break-after: avoid; + } + + ul, + ol { + list-style-position: inside !important; + padding-right: 0 !important; + margin-left: 0 !important; + } + + ul ul, + ul ol, + ol ul, + ol ol, + ul p:not(:first-child), + ol p:not(:first-child) { + margin-left: 2rem !important; + } + + a:link, + a:visited { + text-decoration: underline !important; + font-weight: normal !important; + } + + a[href]:after { + content: " (" attr(href) ")"; + } + + a[href^="javascript:"]:after, + a[href^="#"]:after { + content: ""; /* Do not show javascript and internal links */ + } + + a[href^="#"] { + text-decoration: none !important; + } + + th { + background-color: rgba(0,0,0,0.2) !important; + border-bottom: none !important; + } + + tr { + page-break-inside: avoid; + } + + tbody tr:nth-child(even) { + background-color: rgba(0,0,0,0.1) !important; + } + + pre { + border: 1px solid rgba(0,0,0,0.2); + page-break-inside: avoid; + } + + img { + max-width: 100% !important; + page-break-inside: avoid; + } + + /* #generated-toc: added by Marked for its table of contents */ + + #wrapper #generated-toc ul, /* Table of contents printing in Marked */ + #wrapper #generated-toc ol { + list-style-type: decimal; + } + + #wrapper #generated-toc ul li, + #wrapper #generated-toc ol li { + margin: 1rem 0; + } + +} + + + +/* ================================================ */ +/* 5. Torpedo Overrides */ +/* ================================================ */ + +/* ------------------------------------------------- */ +/* General */ +/* ------------------------------------------------- */ + +html { + font-size: 16px; +} + +body { + font-family: 'Cinta', 'Source Sans Pro', Avenir, sans-serif; + background-color: #F9F9F9; + color: #636463; +} + +#wrapper { + max-width: 40rem; +} + +/* ------------------------------------------------- */ +/* Typography */ +/* ------------------------------------------------- */ + +h1, +h2 { + color: #febf60; +} + +h3 { + color: #b46864; +} + +h4, +h5, +h6 { + font-size: 1rem; + color: #b46864; + text-transform: none; +} + +h5 { + margin-left: 1.6rem; +} + +h6 { + margin-left: 3.2rem; +} + +p, +ol, +ul, +dd, +figcaption { + font-weight: 300; +} + +blockquote { + border-color: #efefef; +} + +a:link, +a:visited, +a:hover, +a:visited { + color: #636463; +} + +a:link { + font-weight: 600; + text-decoration: none; + border-bottom: none; + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMEAAAAJCAYAAACL+UhFAAAEJGlDQ1BJQ0MgUHJvZmlsZQAAOBGFVd9v21QUPolvUqQWPyBYR4eKxa9VU1u5GxqtxgZJk6XtShal6dgqJOQ6N4mpGwfb6baqT3uBNwb8AUDZAw9IPCENBmJ72fbAtElThyqqSUh76MQPISbtBVXhu3ZiJ1PEXPX6yznfOec7517bRD1fabWaGVWIlquunc8klZOnFpSeTYrSs9RLA9Sr6U4tkcvNEi7BFffO6+EdigjL7ZHu/k72I796i9zRiSJPwG4VHX0Z+AxRzNRrtksUvwf7+Gm3BtzzHPDTNgQCqwKXfZwSeNHHJz1OIT8JjtAq6xWtCLwGPLzYZi+3YV8DGMiT4VVuG7oiZpGzrZJhcs/hL49xtzH/Dy6bdfTsXYNY+5yluWO4D4neK/ZUvok/17X0HPBLsF+vuUlhfwX4j/rSfAJ4H1H0qZJ9dN7nR19frRTeBt4Fe9FwpwtN+2p1MXscGLHR9SXrmMgjONd1ZxKzpBeA71b4tNhj6JGoyFNp4GHgwUp9qplfmnFW5oTdy7NamcwCI49kv6fN5IAHgD+0rbyoBc3SOjczohbyS1drbq6pQdqumllRC/0ymTtej8gpbbuVwpQfyw66dqEZyxZKxtHpJn+tZnpnEdrYBbueF9qQn93S7HQGGHnYP7w6L+YGHNtd1FJitqPAR+hERCNOFi1i1alKO6RQnjKUxL1GNjwlMsiEhcPLYTEiT9ISbN15OY/jx4SMshe9LaJRpTvHr3C/ybFYP1PZAfwfYrPsMBtnE6SwN9ib7AhLwTrBDgUKcm06FSrTfSj187xPdVQWOk5Q8vxAfSiIUc7Z7xr6zY/+hpqwSyv0I0/QMTRb7RMgBxNodTfSPqdraz/sDjzKBrv4zu2+a2t0/HHzjd2Lbcc2sG7GtsL42K+xLfxtUgI7YHqKlqHK8HbCCXgjHT1cAdMlDetv4FnQ2lLasaOl6vmB0CMmwT/IPszSueHQqv6i/qluqF+oF9TfO2qEGTumJH0qfSv9KH0nfS/9TIp0Wboi/SRdlb6RLgU5u++9nyXYe69fYRPdil1o1WufNSdTTsp75BfllPy8/LI8G7AUuV8ek6fkvfDsCfbNDP0dvRh0CrNqTbV7LfEEGDQPJQadBtfGVMWEq3QWWdufk6ZSNsjG2PQjp3ZcnOWWing6noonSInvi0/Ex+IzAreevPhe+CawpgP1/pMTMDo64G0sTCXIM+KdOnFWRfQKdJvQzV1+Bt8OokmrdtY2yhVX2a+qrykJfMq4Ml3VR4cVzTQVz+UoNne4vcKLoyS+gyKO6EHe+75Fdt0Mbe5bRIf/wjvrVmhbqBN97RD1vxrahvBOfOYzoosH9bq94uejSOQGkVM6sN/7HelL4t10t9F4gPdVzydEOx83Gv+uNxo7XyL/FtFl8z9ZAHF4bBsrEwAAAE9JREFUWAnt0wENACEQBLHn/SEGK1jCGAk2pufgmp2x9pmfIxAW+MO/e53AExCBIeQFRJCfAAAR2EBeQAT5CQAQgQ3kBUSQnwAAEdhAXuACbF4CJXG10MIAAAAASUVORK5CYII=); + background-repeat: repeat-x; + background-position: 0 0.7rem; +} + +a:visited { /* Doesn't work? */ + background-position: 0 1rem; +} + +a:hover { + font-weight: 600; + text-decoration: none; + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMEAAAAJCAYAAACL+UhFAAAEJGlDQ1BJQ0MgUHJvZmlsZQAAOBGFVd9v21QUPolvUqQWPyBYR4eKxa9VU1u5GxqtxgZJk6XtShal6dgqJOQ6N4mpGwfb6baqT3uBNwb8AUDZAw9IPCENBmJ72fbAtElThyqqSUh76MQPISbtBVXhu3ZiJ1PEXPX6yznfOec7517bRD1fabWaGVWIlquunc8klZOnFpSeTYrSs9RLA9Sr6U4tkcvNEi7BFffO6+EdigjL7ZHu/k72I796i9zRiSJPwG4VHX0Z+AxRzNRrtksUvwf7+Gm3BtzzHPDTNgQCqwKXfZwSeNHHJz1OIT8JjtAq6xWtCLwGPLzYZi+3YV8DGMiT4VVuG7oiZpGzrZJhcs/hL49xtzH/Dy6bdfTsXYNY+5yluWO4D4neK/ZUvok/17X0HPBLsF+vuUlhfwX4j/rSfAJ4H1H0qZJ9dN7nR19frRTeBt4Fe9FwpwtN+2p1MXscGLHR9SXrmMgjONd1ZxKzpBeA71b4tNhj6JGoyFNp4GHgwUp9qplfmnFW5oTdy7NamcwCI49kv6fN5IAHgD+0rbyoBc3SOjczohbyS1drbq6pQdqumllRC/0ymTtej8gpbbuVwpQfyw66dqEZyxZKxtHpJn+tZnpnEdrYBbueF9qQn93S7HQGGHnYP7w6L+YGHNtd1FJitqPAR+hERCNOFi1i1alKO6RQnjKUxL1GNjwlMsiEhcPLYTEiT9ISbN15OY/jx4SMshe9LaJRpTvHr3C/ybFYP1PZAfwfYrPsMBtnE6SwN9ib7AhLwTrBDgUKcm06FSrTfSj187xPdVQWOk5Q8vxAfSiIUc7Z7xr6zY/+hpqwSyv0I0/QMTRb7RMgBxNodTfSPqdraz/sDjzKBrv4zu2+a2t0/HHzjd2Lbcc2sG7GtsL42K+xLfxtUgI7YHqKlqHK8HbCCXgjHT1cAdMlDetv4FnQ2lLasaOl6vmB0CMmwT/IPszSueHQqv6i/qluqF+oF9TfO2qEGTumJH0qfSv9KH0nfS/9TIp0Wboi/SRdlb6RLgU5u++9nyXYe69fYRPdil1o1WufNSdTTsp75BfllPy8/LI8G7AUuV8ek6fkvfDsCfbNDP0dvRh0CrNqTbV7LfEEGDQPJQadBtfGVMWEq3QWWdufk6ZSNsjG2PQjp3ZcnOWWing6noonSInvi0/Ex+IzAreevPhe+CawpgP1/pMTMDo64G0sTCXIM+KdOnFWRfQKdJvQzV1+Bt8OokmrdtY2yhVX2a+qrykJfMq4Ml3VR4cVzTQVz+UoNne4vcKLoyS+gyKO6EHe+75Fdt0Mbe5bRIf/wjvrVmhbqBN97RD1vxrahvBOfOYzoosH9bq94uejSOQGkVM6sN/7HelL4t10t9F4gPdVzydEOx83Gv+uNxo7XyL/FtFl8z9ZAHF4bBsrEwAAAE9JREFUWAnt0wENACEQBLHn/SEGK1jCGAk2pufgmp2x9pmfIxAW+MO/e53AExCBIeQFRJCfAAAR2EBeQAT5CQAQgQ3kBUSQnwAAEdhAXuACbF4CJXG10MIAAAAASUVORK5CYII=); + background-repeat: repeat; + background-position-y: 0 0; +} + +/* ------------------------------------------------- */ +/* Tables */ +/* ------------------------------------------------- */ + +table { + font-weight: 300; +} + +table, +th, +tr { + border: none; +} + +th { + border-bottom: 1px solid #da9e1a; + background-color: #ffd18c; +} + +tbody tr:nth-child(even) { + background-color: #efefef; +} + +/* ------------------------------------------------- */ +/* Misc */ +/* ------------------------------------------------- */ + +code { + color: #5597e7; + padding: 0; /* PLus besoin car plus de risque de collision avec l'arrière-plan coloré */ + background-color: transparent; +} + +pre { + background-color: transparent; +} + +hr { + clear: none; + height: 1rem; + width: 14rem; + margin: 2.5rem auto; + border: none; + color: none; + background-color: transparent; + background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+Cjxzdmcgd2lkdGg9IjEyNnB4IiBoZWlnaHQ9IjE2cHgiIHZpZXdCb3g9IjAgMCAxMjYgMTYiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeG1sbnM6c2tldGNoPSJodHRwOi8vd3d3LmJvaGVtaWFuY29kaW5nLmNvbS9za2V0Y2gvbnMiPgogICAgPHRpdGxlPmhyPC90aXRsZT4KICAgIDxkZXNjcmlwdGlvbj5DcmVhdGVkIHdpdGggU2tldGNoIChodHRwOi8vd3d3LmJvaGVtaWFuY29kaW5nLmNvbS9za2V0Y2gpPC9kZXNjcmlwdGlvbj4KICAgIDxkZWZzPjwvZGVmcz4KICAgIDxnIGlkPSJQYWdlLTEiIHN0cm9rZT0ibm9uZSIgc3Ryb2tlLXdpZHRoPSIxIiBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHNrZXRjaDp0eXBlPSJNU1BhZ2UiPgogICAgICAgIDxwYXRoIGQ9Ik0xMywzIEwzLDEzIiBpZD0iTGluZSIgc3Ryb2tlPSIjRDRERUVCIiBzdHJva2Utd2lkdGg9IjQiIHN0cm9rZS1saW5lY2FwPSJzcXVhcmUiIHNrZXRjaDp0eXBlPSJNU1NoYXBlR3JvdXAiPjwvcGF0aD4KICAgICAgICA8cGF0aCBkPSJNMTMsMyBMMjMuMDQ5ODc1MywxMy4wNDk4NzUzIiBpZD0iTGluZSIgc3Ryb2tlPSIjRDRERUVCIiBzdHJva2Utd2lkdGg9IjQiIHN0cm9rZS1saW5lY2FwPSJzcXVhcmUiIHNrZXRjaDp0eXBlPSJNU1NoYXBlR3JvdXAiPjwvcGF0aD4KICAgICAgICA8cGF0aCBkPSJNMzMsMyBMMjMsMTMiIGlkPSJMaW5lIiBzdHJva2U9IiNENERFRUIiIHN0cm9rZS13aWR0aD0iNCIgc3Ryb2tlLWxpbmVjYXA9InNxdWFyZSIgc2tldGNoOnR5cGU9Ik1TU2hhcGVHcm91cCI+PC9wYXRoPgogICAgICAgIDxwYXRoIGQ9Ik0zMywzIEw0My4wNDk4NzUzLDEzLjA0OTg3NTMiIGlkPSJMaW5lIiBzdHJva2U9IiNENERFRUIiIHN0cm9rZS13aWR0aD0iNCIgc3Ryb2tlLWxpbmVjYXA9InNxdWFyZSIgc2tldGNoOnR5cGU9Ik1TU2hhcGVHcm91cCI+PC9wYXRoPgogICAgICAgIDxwYXRoIGQ9Ik01MywzIEw0MywxMyIgaWQ9IkxpbmUiIHN0cm9rZT0iI0Q0REVFQiIgc3Ryb2tlLXdpZHRoPSI0IiBzdHJva2UtbGluZWNhcD0ic3F1YXJlIiBza2V0Y2g6dHlwZT0iTVNTaGFwZUdyb3VwIj48L3BhdGg+CiAgICAgICAgPHBhdGggZD0iTTUzLDMgTDYzLjA0OTg3NTMsMTMuMDQ5ODc1MyIgaWQ9IkxpbmUiIHN0cm9rZT0iI0Q0REVFQiIgc3Ryb2tlLXdpZHRoPSI0IiBzdHJva2UtbGluZWNhcD0ic3F1YXJlIiBza2V0Y2g6dHlwZT0iTVNTaGFwZUdyb3VwIj48L3BhdGg+CiAgICAgICAgPHBhdGggZD0iTTczLDMgTDYzLDEzIiBpZD0iTGluZSIgc3Ryb2tlPSIjRDRERUVCIiBzdHJva2Utd2lkdGg9IjQiIHN0cm9rZS1saW5lY2FwPSJzcXVhcmUiIHNrZXRjaDp0eXBlPSJNU1NoYXBlR3JvdXAiPjwvcGF0aD4KICAgICAgICA8cGF0aCBkPSJNNzMsMyBMODMuMDQ5ODc1MywxMy4wNDk4NzUzIiBpZD0iTGluZSIgc3Ryb2tlPSIjRDRERUVCIiBzdHJva2Utd2lkdGg9IjQiIHN0cm9rZS1saW5lY2FwPSJzcXVhcmUiIHNrZXRjaDp0eXBlPSJNU1NoYXBlR3JvdXAiPjwvcGF0aD4KICAgICAgICA8cGF0aCBkPSJNOTMsMyBMODMsMTMiIGlkPSJMaW5lIiBzdHJva2U9IiNENERFRUIiIHN0cm9rZS13aWR0aD0iNCIgc3Ryb2tlLWxpbmVjYXA9InNxdWFyZSIgc2tldGNoOnR5cGU9Ik1TU2hhcGVHcm91cCI+PC9wYXRoPgogICAgICAgIDxwYXRoIGQ9Ik05MywzIEwxMDMuMDQ5ODc1LDEzLjA0OTg3NTMiIGlkPSJMaW5lIiBzdHJva2U9IiNENERFRUIiIHN0cm9rZS13aWR0aD0iNCIgc3Ryb2tlLWxpbmVjYXA9InNxdWFyZSIgc2tldGNoOnR5cGU9Ik1TU2hhcGVHcm91cCI+PC9wYXRoPgogICAgICAgIDxwYXRoIGQ9Ik0xMTMsMyBMMTAzLDEzIiBpZD0iTGluZSIgc3Ryb2tlPSIjRDRERUVCIiBzdHJva2Utd2lkdGg9IjQiIHN0cm9rZS1saW5lY2FwPSJzcXVhcmUiIHNrZXRjaDp0eXBlPSJNU1NoYXBlR3JvdXAiPjwvcGF0aD4KICAgICAgICA8cGF0aCBkPSJNMTEzLDMgTDEyMy4wNDk4NzUsMTMuMDQ5ODc1MyIgaWQ9IkxpbmUiIHN0cm9rZT0iI0Q0REVFQiIgc3Ryb2tlLXdpZHRoPSI0IiBzdHJva2UtbGluZWNhcD0ic3F1YXJlIiBza2V0Y2g6dHlwZT0iTVNTaGFwZUdyb3VwIj48L3BhdGg+CiAgICA8L2c+Cjwvc3ZnPg==); + background-repeat: no-repeat; + background-position-x: 50%; +} + +::selection { + background-color: #D4DEEB; + color: #b46864; + } + +::-moz-selection { + background-color: #D4DEEB; + color: #b46864; +} + +/* #generated-toc: added by Marked for its table of contents */ + +#generated-toc ul li a { + font-weight: normal; +} + +/* ------------------------------------------------- */ +/* iPad and desktop */ +/* ------------------------------------------------- */ + +@media only screen and (min-width: 641px) { + + html { + font-size: 17px; + } + +} + +/* ------------------------------------------------- */ +/* Widescreens */ +/* ------------------------------------------------- */ + +@media only screen and (min-width: 1441px) { + + html { + font-size: 22px; + } + +} diff --git a/doc/themes/vostok.css b/doc/themes/vostok.css new file mode 100644 index 00000000..6f3e7b30 --- /dev/null +++ b/doc/themes/vostok.css @@ -0,0 +1,712 @@ +/* Title: Vostok */ +/* Author: Jocelyn Richard http://jocelynrichard.com/ */ +/* Description: Generous x-height and contrasted colors make for highly legible documents. Works best with the free PT fonts: http://www.paratype.com/public/ */ + +/* ================================================ */ +/* 1. Reset */ +/* 2. Skeleton */ +/* 3. Media Queries */ +/* 4. Print Styles */ +/* 5. Vostok Overrides */ +/* ================================================ */ + + + +/* ================================================ */ +/* 1. Reset */ +/* ================================================ */ + +html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, +b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas, details, embed, figure, figcaption, footer, header, hgroup, menu, nav, output, ruby, section, summary, time, mark, audio, video {margin: 0; padding: 0; border: 0;} /* Edited from http://www.cssreset.com/scripts/eric-meyer-reset-css/ */ + +article, aside, details, figcaption, figure, footer, header, hgroup, nav, section, summary {display: block;} /* Semantic tags definition for IE 6/7/8/9 and Firefox 3 */ + +html {-webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%;} /* Prevents iOS text size adjust after orientation change, without disabling user zoom */ + + + +/* ================================================ */ +/* 2. Skeleton */ +/* ================================================ */ + +/* ------------------------------------------------- */ +/* General */ +/* ------------------------------------------------- */ + +html { + font-size: 14px; +} + +body { + font-family: 'Open Sans', sans-serif; + margin: 1.71rem 1.71rem 3rem 1.71rem ; /* Get margins even if the Markdown rendering app doesn't include any */ + background-color: white; + color: #222; +} + +#wrapper { /* #wrapper: ID added by Marked */ + max-width: 42rem; + margin: 0 auto; + margin-left: auto !important; /* Countering toc.css added by Marked */ + padding: 1.71rem 0 !important; /* Countering toc.css added by Marked */ +} + +/* ------------------------------------------------- */ +/* Typography */ +/* ------------------------------------------------- */ + +h1, +h2, +h3, +h4, +h5, +h6 { + margin-bottom: 1.6rem; +} + +h1, +h2 { + margin-top: 3.2rem; +} + +h1 { + font-size: 2.82rem; /* 42.3px @15px */ + line-height: 3.2rem; /* 48px @15px */ +} + +h2 { + font-size: 1.99rem; /* 29.9px @15px */ + line-height: 2.4rem; /* 36px @15px */ +} + +h3 { + font-size: 1.41rem; /* 21.2px @15px */ + line-height: 2rem; /* 30px @15px */ +} + +h4 { + font-size: 1rem; /* 15px @15px */ + line-height: 1.6rem; /* 24px @15px */ +} + +h5, h6 { + font-size: 0.8rem; + line-height: 1.2rem; + text-transform: uppercase; +} + +h6 { + margin-left: 1.6rem; +} + +p, +ol, +ul, +blockquote { + font-size: 1rem; + line-height: 1.6rem; + margin-bottom: 1.6rem; +} + +ul ul, +ul ol, +ol ul, +ol ol { + margin-left: 1.6rem; + margin-top: 1.6rem; +} + +#generated-toc ul ul, /* #generated-toc: added by Marked for its table of contents */ +#generated-toc ul ol, +#generated-toc ol ul, +#generated-toc ol ol { + margin-top: 0; + margin-bottom: 0; + padding-top: 0; + padding-bottom: 0; +} + +blockquote { + margin: 0 0 1.6rem 2.4rem; + padding-left: 0.8rem; /* Voire */ + border-left: 4px solid rgba(0,0,0,0.08); + font-style: normal; +} + +blockquote ul { + margin-left: 0.8rem; /* Pour ne pas que les hanging bullets mordent sur le blockquote */ +} + +ol li blockquote, /* So that blockquote work in lists */ +ul li blockquote { + margin-left: 0; +} + +a:link { + text-decoration: none; + color: #165bd4; + border-bottom: 1px solid #ccc; +} + +a:visited { + color: #7697cf; + border-bottom: 1px solid #ccc; +} + +a:hover { + border-color: #165bd4; +} + +a:active { + background-color: #e6e6e6; +} + +/* ------------------------------------------------- */ +/* Tables */ +/* ------------------------------------------------- */ + +table { + font-size: 0.85rem; + margin: 0 0 1.6rem 0; + border-collapse: collapse; + border: 1px solid #ccc; +} + +th, +td { + padding: 0.5rem 0.75rem; + max-width: 20rem; /* Avoid dropping lines for nothing without having ridiculously wide tables */ +} + +th { + border-bottom: 2px solid #222; +} + +tr { + border-bottom: 1px solid #ccc; +} + +tbody tr:nth-child(odd) { + background-color: #f9f9f9; +} + +table code { + font-size: 85%; +} + +/* ------------------------------------------------- */ +/* Misc */ +/* ------------------------------------------------- */ + +img { + max-width: 100% +} + +caption, +figcaption { + font-size: 0.85rem; + line-height: 1.6rem; + margin: 0 1.6rem; + text-align: left; +} + +figcaption { + margin-bottom: 1.6rem; +} + +h1, /* White-space mentions in order to force wrapping */ +h2, +a:link, +pre { + white-space: pre; /* CSS 2.0 */ + white-space: pre-wrap; /* CSS 2.1 */ + white-space: pre-line; /* CSS 3.0 */ + white-space: -pre-wrap; /* Opera 4-6 */ + white-space: -o-pre-wrap; /* Opera 7 */ + white-space: -moz-pre-wrap; /* Mozilla */ + white-space: -hp-pre-wrap; /* HP Printers */ + word-wrap: break-word; /* IE 5+ */ +} + +code { + font-family: "Menlo", "Courier New", "Courier", monospace; + font-size: 85%; + color: #666; + background-color: rgba(0,0,0,0.08); + padding: 2px 4px; + border-radius: 2px; +} + +pre { + background-color: rgba(0,0,0,0.08); + border-radius: 8px; + padding: 0.4rem; + margin-bottom: 1.6rem; +} + +pre code { /* Counter the code mentions */ + background-color: transparent; + padding: 0; +} + +sup, +sub, +a.footnote { /* Keep line-height from being affected by sub, cf https://gist.github.com/unruthless/413930 */ + font-size: 75%; + height: 0; + line-height: 1; + position: relative; +} + +sup, +a.footnote { + vertical-align:super; +} + +sub { + vertical-align: sub; +} + +dt { + font-weight: 600; +} + +dd { + font-size: 1rem; + line-height: 1.6rem; + margin-bottom: 1.6rem; +} + +hr { + clear: none; + height: 0.2rem; + border: none; + margin: 0 auto 1.4rem auto; /* 2.4rem auto 2.2rem auto; */ + width: 100%; + color: #ccc; + background-color: #ccc; +} + +::selection { + background-color: #f8dc77; +} + +::-moz-selection { + background-color: #f8dc77; +} + +a:focus { + outline: 2px solid; + outline-color: #165bd4; +} + +/* ------------------------------------------------- */ +/* Animations */ +/* ------------------------------------------------- */ + +a:hover { + -moz-transition: all 0.2s ease-in-out; + -webkit-transition: all 0.2s ease-in-out; +} + +h1, +h2, +h3, +h4, +h5, +h6, +p, +blockquote { + -moz-transition: all 0.2s ease; + -webkit-transition: all 0.2s ease; +} + + + +/* ================================================ */ +/* 3. Media Queries */ +/* ================================================ */ + +/* Base styles are for smartphones; elements are then tweaked as the viewport grows. */ + +/* ------------------------------------------------- */ +/* iPad and desktop */ +/* ------------------------------------------------- */ + +@media only screen and (min-width: 641px) { + + html { + font-size: 15px; + } + + body { + margin: 2.4rem 2.4rem 3.2rem 2.4rem; + } + + h1 { + font-size: 3.57rem; /* 53.2px @15px */ + line-height: 4rem; /* 60px @15px */ + } + + h2 { + font-size: 2.24rem; /* 33.6px @15px */ + line-height: 2.8rem; /* 42px @15px */ + } + +} + +/* ------------------------------------------------- */ +/* Widescreens */ +/* ------------------------------------------------- */ + +@media only screen and (min-width: 1441px) { + + html { + font-size: 22px; + } + +} + + + +/* ================================================ */ +/* 4. Print Styles */ +/* ================================================ */ + +/* Inconsistent and buggy across browsers */ + +@media print { + + * { + background: transparent !important; + color: #000 !important; /* Black text prints faster and browsers are inconsistent in color reproduction anyway: h5bp.com/s */ + } + + @page { + margin: 1cm; /* Added to any #wrapper margin*/ + } + + html { + font-size: 15px; + } + + body { + margin: 1rem !important; /* Security margins for browser without @page support */ + } + + #wrapper { + max-width: none; + } + + h1, + h2, + h3, + h4, + h5, + h6, + p { + orphans: 3; + widows: 3; + page-break-after: avoid; + } + + ul, + ol { + list-style-position: inside !important; + padding-right: 0 !important; + margin-left: 0 !important; + } + + ul ul, + ul ol, + ol ul, + ol ol, + ul p:not(:first-child), + ol p:not(:first-child) { + margin-left: 2rem !important; + } + + a:link, + a:visited { + text-decoration: underline !important; + font-weight: normal !important; + } + + a[href]:after { + content: " (" attr(href) ")"; + } + + a[href^="javascript:"]:after, + a[href^="#"]:after { + content: ""; /* Do not show javascript and internal links */ + } + + a[href^="#"] { + text-decoration: none !important; + } + + th { + background-color: rgba(0,0,0,0.2) !important; + border-bottom: none !important; + } + + tr { + page-break-inside: avoid; + } + + tbody tr:nth-child(even) { + background-color: rgba(0,0,0,0.1) !important; + } + + pre { + border: 1px solid rgba(0,0,0,0.2); + page-break-inside: avoid; + } + + img { + max-width: 100% !important; + page-break-inside: avoid; + } + + /* #generated-toc: added by Marked for its table of contents */ + + #wrapper #generated-toc ul, /* Table of contents printing in Marked */ + #wrapper #generated-toc ol { + list-style-type: decimal; + } + + #wrapper #generated-toc ul li, + #wrapper #generated-toc ol li { + margin: 1rem 0; + } + +} + + + +/* ================================================ */ +/* 5. Vostok Overrides */ +/* ================================================ */ + +/* ------------------------------------------------- */ +/* General */ +/* ------------------------------------------------- */ + +html { + font-size: 15px; +} + +body { + font-family: "pt serif", Georgia, serif; + color: rgba(0,0,0,0.7); + text-shadow: 0 1px 0 white; + background-color: #ececec; +} + +#wrapper { + max-width: 40rem; +} + +/* ------------------------------------------------- */ +/* Typography */ +/* ------------------------------------------------- */ + +h1, +h2, +h3, +h4, +h5, +h6 { + font-family: "pt sans", "avenir next", sans-serif; + font-weight: 700; + color: rgba(0,0,0,1); +} + +h2, +h3 { + font-family: "pt sans narrow", "avenir next condensed", sans-serif; +} + +h5 { + color: rgba(0,0,0,0.7); +} + +h6 { + color: rgba(0,0,0,0.7); +} + +ul { + list-style-type: none; +} + +ul > li:before { + content: "\2022"; + float: left; + margin-left: -1.2rem; + padding-right: 0.6rem; /* Empirically chosen to align horizontally with the position of standard bullet points */ +} + +ul li, +ol li { + margin: 0; +} + +ul ul, +ul ol, +ol ul, +ol ol, +ul p:not(:first-child), +ol p:not(:first-child) { + margin-left: 1.6rem; +} + +blockquote { + border-left: 2px solid rgba(0,0,0,0.5); +} + +a:link { + text-decoration: none; + border-bottom: none; + font-weight: 700; + color: rgba(25,107,240,1); +} + +a:visited { + color: #2c508a; +} + +a:hover { + color: #2DAB5F; +} + +a:focus { + outline: 0.125rem solid; +} + +/* ------------------------------------------------- */ +/* Tables */ +/* ------------------------------------------------- */ + +thead { + border: 1px solid #4C4C4C; /* In hex (= rgba(0,0,0,0.7)) otherwise the alpha gets the border of the tr below */ +} + +th { + background-color: #4C4C4C; /* = rgba(0,0,0,0.7) */ + border-bottom: 1px solid #4C4C4C; /* = rgba(0,0,0,0.7) ; mentionned otherwise the stroke is of the tr below */ + color: white; + text-shadow: 0px -1px 0px black; +} + +tbody tr:nth-child(even) { + background-color: rgba(255,255,255,0.5); +} + +tbody tr:nth-child(odd) { + background-color: transparent; +} + +/* ------------------------------------------------- */ +/* Misc */ +/* ------------------------------------------------- */ + + +table, +caption, +figcaption { + font-family: "pt sans", "avenir next", sans-serif; +} + +code { + border: 1px solid rgba(255,255,255,0.7); + background-color: rgba(255,255,255,0.5); + border-radius: 2px; + color: #2DAB5F; + text-shadow: none; +} + +pre { + border: 1px solid rgba(255,255,255,0.7); + background-color: rgba(255,255,255,0.5); + border-radius: 2px; +} + +pre code { + color: #81A181; + border: none; +} + +hr { + clear: none; + height: 2px; /* height: 0.125rem; */ + border: none; + margin: 1.5rem auto; + width: 14rem; + color: rgba(0,0,0,0.5); + background-color: rgba(0,0,0,0.5); +} + +::selection { + background-color: #fbfb48; +} + +::-moz-selection { + background-color: #fbfb48; +} + +/* #generated-toc: added by Marked for its table of contents */ + +#generated-toc { + text-shadow: none; +} + +#generated-toc ul { /* Compensate for the earlier custom bullet point */ + margin: 0; + list-style-type: none; +} + +#generated-toc ul li { /* Compensate for the earlier custom bullet point */ + margin: 0; +} + +#generated-toc ul > li:before { /* Compensate for the earlier custom bullet point */ + content: none; + margin-left: 0; + padding-right: 0; +} + +#generated-toc ul li a { + font-weight: normal; + display: inline; +} + +#generated-toc ul li ul li ul li a { + text-transform: lowercase; + +} + +/* ------------------------------------------------- */ +/* iPad and desktop */ +/* ------------------------------------------------- */ + +@media only screen and (min-width: 641px) { + + html { + font-size: 17px; + } + + h1 { + font-size: 2.81rem; + } + + h2 { + font-size: 1.78rem; + line-height: 2.21rem; + } + + ul, + ol { + margin-left: 0; + } + +} diff --git a/env/doc.go b/env/doc.go new file mode 100644 index 00000000..e0b20d0f --- /dev/null +++ b/env/doc.go @@ -0,0 +1,18 @@ +/* + * Copyright 2018 Xiaomi, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Package env contain virtual database build, rehash, cleanup. +package env diff --git a/env/env.go b/env/env.go new file mode 100644 index 00000000..f2a2474b --- /dev/null +++ b/env/env.go @@ -0,0 +1,485 @@ +/* + * Copyright 2018 Xiaomi, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package env + +import ( + "fmt" + "strings" + + "github.com/XiaoMi/soar/ast" + "github.com/XiaoMi/soar/common" + "github.com/XiaoMi/soar/database" + + "github.com/dchest/uniuri" + "vitess.io/vitess/go/vt/sqlparser" +) + +// VirtualEnv SQL优化评审 测试环境 +// DB使用的信息从配置文件中获取 +type VirtualEnv struct { + *database.Connector + + // 保存DB测试环境映射关系,防止vEnv环境冲突。 + DBRef map[string]string + hash2Db map[string]string + // 保存Table创建关系,防止重复创建表 + TableMap map[string]map[string]string + // 错误 + Error error +} + +// NewVirtualEnv 初始化一个新的测试环境 +func NewVirtualEnv(vEnv *database.Connector) *VirtualEnv { + return &VirtualEnv{ + Connector: vEnv, + DBRef: make(map[string]string), + hash2Db: make(map[string]string), + TableMap: make(map[string]map[string]string), + } +} + +// BuildEnv 测试环境初始化&连接线上环境检查 +// @output *VirtualEnv 测试环境 +// @output *database.Connector 线上环境连接句柄 +func BuildEnv() (*VirtualEnv, *database.Connector) { + // 生成测试环境 + vEnv := NewVirtualEnv(&database.Connector{ + Addr: common.Config.TestDSN.Addr, + User: common.Config.TestDSN.User, + Pass: common.Config.TestDSN.Password, + Database: common.Config.TestDSN.Schema, + Charset: common.Config.TestDSN.Charset, + }) + + // 检查测试环境可用性,并记录数据库版本 + vEnvVersion, err := vEnv.Version() + common.Config.TestDSN.Version = vEnvVersion + if err != nil { + common.Log.Warn("BuildEnv TestDSN: %s:********@%s/%s not available , Error: %s", + vEnv.User, vEnv.Addr, vEnv.Database, err.Error()) + common.Config.TestDSN.Disable = true + } + + // 连接线上环境 + // 如果未配置线上环境线测试环境配置为线上环境 + if common.Config.OnlineDSN.Addr == "" { + common.Log.Warn("BuildEnv AllowOnlineAsTest: OnlineDSN not config, use TestDSN: %s:********@%s/%s as OnlineDSN", + vEnv.User, vEnv.Addr, vEnv.Database) + common.Config.OnlineDSN = common.Config.TestDSN + } + conn := &database.Connector{ + Addr: common.Config.OnlineDSN.Addr, + User: common.Config.OnlineDSN.User, + Pass: common.Config.OnlineDSN.Password, + Database: common.Config.OnlineDSN.Schema, + Charset: common.Config.OnlineDSN.Charset, + } + + // 检查线上环境可用性版本 + rEnvVersion, err := vEnv.Version() + common.Config.OnlineDSN.Version = rEnvVersion + if err != nil { + common.Log.Warn("BuildEnv OnlineDSN: %s:********@%s/%s not available , Error: %s", + vEnv.User, vEnv.Addr, vEnv.Database, err.Error()) + common.Config.TestDSN.Disable = true + } + + // 检查是否允许Online和Test一致,防止误操作 + if common.FormatDSN(common.Config.OnlineDSN) == common.FormatDSN(common.Config.TestDSN) && + !common.Config.AllowOnlineAsTest { + common.Log.Warn("BuildEnv AllowOnlineAsTest: %s:********@%s/%s OnlineDSN can't config as TestDSN", + vEnv.User, vEnv.Addr, vEnv.Database) + common.Config.TestDSN.Disable = true + common.Config.OnlineDSN.Disable = true + } + + // 判断测试环境与remote环境版本是否一致 + if vEnvVersion < rEnvVersion { + common.Log.Warning("TestDSN MySQL version older than OnlineDSN, TestDSN will not be used", vEnvVersion, rEnvVersion) + common.Config.TestDSN.Disable = true + } + + return vEnv, conn +} + +// RealDB 从测试环境中获取通过hash后的DB +func (ve VirtualEnv) RealDB(hash string) string { + if _, ok := ve.hash2Db[hash]; ok { + return ve.hash2Db[hash] + } + return hash +} + +// DBHash 从测试环境中根据DB找到对应的hash值 +func (ve VirtualEnv) DBHash(db string) string { + if _, ok := ve.DBRef[db]; ok { + return ve.DBRef[db] + } + return db +} + +// CleanUp 环境清理 +func (ve VirtualEnv) CleanUp() bool { + if !common.Config.TestDSN.Disable && common.Config.DropTestTemporary { + common.Log.Debug("CleanUp ...") + for db := range ve.hash2Db { + ve.Database = db + _, err := ve.Query("drop database %s", db) + if err != nil { + common.Log.Error("CleanUp failed Error: %s", err) + return false + } + } + common.Log.Debug("CleanUp, done") + } + return true +} + +// BuildVirtualEnv rEnv为SQL源环境,DB使用的信息从接口获取 +// 注意:如果是USE,DDL等语句,执行完第一条就会返回,后面的SQL不会执行 +func (ve *VirtualEnv) BuildVirtualEnv(rEnv *database.Connector, SQLs ...string) bool { + var stmt sqlparser.Statement + var err error + + // 置空错误信息 + ve.Error = nil + // 检测是否已经创建初始数据库,如果未创建则创建一个名称hash过的映射数据库 + err = ve.createDatabase(*rEnv, rEnv.Database) + common.LogIfWarn(err, "") + + // 测试环境检测 + if common.Config.TestDSN.Disable { + common.Log.Info("BuildVirtualEnv TestDSN not config") + return true + } + + // 判断rEnv中是否指定了DB + if rEnv.Database == "" { + common.Log.Error("BuildVirtualEnv no database specified, TestDSN init failed") + return false + } + + // 库表提取 + meta := make(map[string]*common.DB) + for _, sql := range SQLs { + + common.Log.Debug("BuildVirtualEnv Database&Table Mapping, SQL: %s", sql) + + stmt, err = sqlparser.Parse(sql) + if err != nil { + common.Log.Error("BuildVirtualEnv Error : %v", err) + return false + } + + // 语句类型判断 + switch stmt := stmt.(type) { + case *sqlparser.Use: + // 如果是use语句,则更改基础环配置 + if _, ok := meta[stmt.DBName.String()]; !ok { + // 如果USE了一个线上环境不存在的数据库,将创建该数据库,字符集默认utf8mb4 + meta[stmt.DBName.String()] = common.NewDB(stmt.DBName.String()) + rEnv.Database = stmt.DBName.String() + + // use DB 后检查 DB是否已经创建,如果没有创建则创建DB + err = ve.createDatabase(*rEnv, rEnv.Database) + common.LogIfWarn(err, "") + } + return true + case *sqlparser.DDL: + // 如果是DDL,则先获取DDL对应的表结构,然后直接在测试环境接执行SQL + // 为不影响其他SQL操作,复制一个Connector对象,将数据库切换到对应的DB上直接执行 + tmpDB := *ve.Connector + tmpDB.Database = ve.DBRef[rEnv.Database] + + // 为了支持并发,需要将DB进行映射,但db.table这种形式无法保证DB的映射是正确的 + // TODO:暂不支持 create db.tableName (id int) 形式的建表语句 + if stmt.Table.Qualifier.String() != "" || stmt.NewName.Qualifier.String() != "" { + common.Log.Error("BuildVirtualEnv DDL Not support '.'") + return false + } + + // 拉取表结构 + table := stmt.Table.Name.String() + if table != "" { + err = ve.createTable(*rEnv, rEnv.Database, table) + if err != nil { + common.Log.Error("BuildVirtualEnv Error : %v", err) + return false + } + } + + _, err = tmpDB.Query(sql) + if err != nil { + switch stmt.Action { + case "create", "alter": + // 如果是创建或者修改语句,且报错信息为如重复建表、重复索引等信息,将错误反馈到上一次层输出建议 + ve.Error = err + default: + common.Log.Error("BuildVirtualEnv DDL Execute Error : %v", err) + } + } + return true + } + + meta := ast.GetMeta(stmt, nil) + + // 由于DB环境可能是变的,所以需要每一次都单独的提取库表结构,整体随着rEnv的变动而发生变化 + for db, table := range meta { + if db == "" { + db = rEnv.Database + } + tmpEnv := *rEnv + tmpEnv.Database = db + + // 创建数据库环境 + for _, tb := range table.Table { + if tb.TableName == "" { + continue + } + + // 视图检查 + common.Log.Debug("BuildVirtualEnv Checking view -- %s.%s", tmpEnv.Database, tb.TableName) + tbStatus, err := tmpEnv.ShowTableStatus(tb.TableName) + if err != nil { + common.Log.Error("BuildVirtualEnv ShowTableStatus Error : %v", err) + return false + } + + // 如果是视图,解析语句 + if len(tbStatus.Rows) > 0 && tbStatus.Rows[0].Comment == "VIEW" { + tmpEnv.Database = db + var viewDDL string + viewDDL, err = tmpEnv.ShowCreateTable(tb.TableName) + if err != nil { + common.Log.Error("BuildVirtualEnv create view failed: %v", err) + return false + } + + startIdx := strings.Index(viewDDL, "AS") + viewDDL = viewDDL[startIdx+2:] + if !ve.BuildVirtualEnv(&tmpEnv, viewDDL) { + return false + } + } + + err = ve.createTable(tmpEnv, db, tb.TableName) + if err != nil { + common.Log.Error("BuildVirtualEnv Error : %v", err) + return false + } + } + } + } + return true +} + +func (ve VirtualEnv) createDatabase(rEnv database.Connector, dbName string) error { + // 生成映射关系 + if _, ok := ve.DBRef[dbName]; ok { + common.Log.Debug("createDatabase, Database `%s` created", dbName) + return nil + } + + dbHash := "optimizer_" + uniuri.New() + common.Log.Debug("createDatabase, mapping `%s` :`%s`-->`%s`", dbName, dbName, dbHash) + ddl, err := rEnv.ShowCreateDatabase(dbName) + if err != nil { + common.Log.Warning("createDatabase, rEnv.ShowCreateDatabase Error : %v", err) + ddl = fmt.Sprintf("create database `%s` character set utf8mb4", dbName) + } + + ddl = strings.Replace(ddl, dbName, dbHash, -1) + _, err = ve.Query(ddl) + if err != nil { + common.Log.Warning("createDatabase, Error : %v", err) + return err + } + + // 创建成功,添加映射记录 + ve.DBRef[dbName] = dbHash + ve.hash2Db[dbHash] = dbName + return nil +} + +/* + @input: + database.Connector 为一个线上环境数据库连接句柄的复制,因为在处理SQL时需要对上下文进行关联处理, + 所以存在修改DB连接参数(主要是数据库名称变更)的可能性,为了不影响整体上下文的环境,所以需要一个镜像句柄来做当前环境的操作。 + + dbName, tbName: 需要在环境中操作的库表名称, + + @output: + return 执行过程中的错误 + + NOTE: + 该函数会将线上环境中使用到的库表结构复制到测试环境中,为后续操作提供基础环境。 + 传入的库表名称均来自于对AST的解析,库表名称的获取遵循以下原则: + 如果未在SQL中指定数据库名称,则数据库一定是配置文件(或命令行参数传入DSN)中指定的数据库 + 如果一个SQL中存在多个数据库,则只能有一个数据库是没有在SQL中被显示指定的(即DSN中指定的数据库) + TODO: + 在一些可能的情况下,由于数据库配置的不一致(如SQL_MODE不同)导致remote环境的库表无法正确的在测试环境进行同步, + soar能够做出判断并进行session级别的修改,但是这一阶段可用性保证应该是由用户提供两个完全相同(或测试环境兼容线上环境) + 的数据库环境来实现的。 +*/ +func (ve VirtualEnv) createTable(rEnv database.Connector, dbName, tbName string) error { + + if dbName == "" { + dbName = rEnv.Database + } + // 如果 dbName 不为空,说明指定了DB,临时修改rEnv中DB参数,来确保执行正确性 + rEnv.Database = dbName + + if ve.TableMap[dbName] == nil { + ve.TableMap[dbName] = make(map[string]string) + } + + if strings.ToLower(tbName) == "dual" { + common.Log.Debug("createTable, %s no need create", tbName) + return nil + } + + if ve.TableMap[dbName][tbName] != "" { + common.Log.Debug("createTable, `%s`.`%s` created", dbName, tbName) + return nil + } + + common.Log.Debug("createTable, Database: %s, Table: %s", dbName, tbName) + + // TODO:查看是否有外键关联(done),对外键的支持 (未解决循环依赖的问题) + + // 判断数据库是否已经创建 + if ve.DBRef[dbName] == "" { + // 若没创建,则创建数据库 + err := ve.createDatabase(rEnv, dbName) + if err != nil { + return err + } + } + + // 记录Table创建信息 + ve.TableMap[dbName][tbName] = tbName + + // 生成建表语句 + common.Log.Debug("createTable DSN(%s/%s): generate ddl", rEnv.Addr, rEnv.Database) + + ddl, err := rEnv.ShowCreateTable(tbName) + if err != nil { + // 有可能是用户新建表,因此线上环境查不到 + common.Log.Error("createTable, %s DDL Error : %v", tbName, err) + return err + } + + // 改变数据环境 + ve.Database = ve.DBRef[dbName] + _, err = ve.Query(ddl) + if err != nil { + // 有可能是用户新建表,因此线上环境查不到 + common.Log.Error("createTable, %s Error : %v", tbName, err) + return err + } + + // 泵取数据 + if common.Config.Sampling { + common.Log.Debug("createTable, Start Sampling data from %s.%s to %s.%s ...", dbName, tbName, ve.DBRef[dbName], tbName) + err := ve.SamplingData(rEnv, tbName) + if err != nil { + common.Log.Error(" (ve VirtualEnv) createTable SamplingData Error: %v", err) + return err + } + } + return nil +} + +// GenTableColumns 为Rewrite提供的结构体初始化 +func (ve *VirtualEnv) GenTableColumns(meta common.Meta) common.TableColumns { + tableColumns := make(common.TableColumns) + for dbName, db := range meta { + for _, tb := range db.Table { + // 防止传入非预期值 + if tb == nil { + break + } + td, err := ve.Connector.ShowColumns(tb.TableName) + if err != nil { + common.Log.Warn("GenTableColumns, ShowColumns Error: " + err.Error()) + break + } + + // tableColumns 初始化 + if dbName == "" { + dbName = ve.RealDB(ve.Connector.Database) + } + + if _, ok := tableColumns[dbName]; !ok { + tableColumns[dbName] = make(map[string][]*common.Column) + } + + if _, ok := tableColumns[dbName][tb.TableName]; !ok { + tableColumns[dbName][tb.TableName] = make([]*common.Column, 0) + } + + if len(tb.Column) == 0 { + // tb.column为空说明SQL里这个表是用的*来查询 + if err != nil { + common.Log.Error("ast.Rewrite ShowColumns, Error: %v", err) + break + } + + for _, colInfo := range td.DescValues { + tableColumns[dbName][tb.TableName] = append(tableColumns[dbName][tb.TableName], &common.Column{ + Name: colInfo.Field, + DB: dbName, + Table: tb.TableName, + DataType: colInfo.Type, + Character: colInfo.Collation, + Key: colInfo.Key, + Default: colInfo.Default, + Extra: colInfo.Extra, + Comment: colInfo.Comment, + Privileges: colInfo.Privileges, + Null: colInfo.Null, + }) + } + } else { + // tb.column如果不为空则需要把使用到的列填写进去 + var columns []*common.Column + for _, col := range tb.Column { + for _, colInfo := range td.DescValues { + if col.Name == colInfo.Field { + // 根据获取的信息将列的信息补全 + col.DB = dbName + col.Table = tb.TableName + col.DataType = colInfo.Type + col.Character = colInfo.Collation + col.Key = colInfo.Key + col.Default = colInfo.Default + col.Extra = colInfo.Extra + col.Comment = colInfo.Comment + col.Privileges = colInfo.Privileges + col.Null = colInfo.Null + + columns = append(columns, col) + break + } + } + } + tableColumns[dbName][tb.TableName] = columns + } + } + } + return tableColumns +} diff --git a/env/env_test.go b/env/env_test.go new file mode 100644 index 00000000..ad511ae7 --- /dev/null +++ b/env/env_test.go @@ -0,0 +1,182 @@ +/* + * Copyright 2018 Xiaomi, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package env + +import ( + "flag" + "testing" + + "github.com/XiaoMi/soar/common" + "github.com/XiaoMi/soar/database" + "github.com/kr/pretty" + "github.com/ziutek/mymysql/mysql" +) + +var connTest *database.Connector +var update = flag.Bool("update", false, "update .golden files") + +func init() { + common.BaseDir = common.DevPath + err := common.ParseConfig("") + common.LogIfError(err, "init ParseConfig") + connTest = &database.Connector{ + Addr: common.Config.TestDSN.Addr, + User: common.Config.TestDSN.User, + Pass: common.Config.TestDSN.Password, + Database: common.Config.TestDSN.Schema, + Charset: common.Config.TestDSN.Charset, + } +} + +func TestNewVirtualEnv(t *testing.T) { + testSQL := []string{ + "create table t(id int,c1 varchar(20),PRIMARY KEY (id));", + "alter table t add index `idx_c1`(c1);", + "alter table t add index `idx_c1`(c1);", + "select * from city where country_id = 44;", + "select * from address where address2 is not null;", + "select * from address where address2 is null;", + "select * from address where address2 >= 44;", + "select * from city where country_id between 44 and 107;", + "select * from city where city like 'Ad%';", + "select * from city where city = 'Aden' and country_id = 107;", + "select * from city where country_id > 31 and city = 'Aden';", + "select * from address where address_id > 8 and city_id < 400 and district = 'Nantou';", + "select * from address where address_id > 8 and city_id < 400;", + "select * from actor where last_update='2006-02-15 04:34:33' and last_name='CHASE' group by first_name;", + "select * from address where last_update >='2014-09-25 22:33:47' group by district;", + "select * from address group by address,district;", + "select * from address where last_update='2014-09-25 22:30:27' group by district,(address_id+city_id);", + "select * from customer where active=1 order by last_name limit 10;", + "select * from customer order by last_name limit 10;", + "select * from customer where address_id > 224 order by address_id limit 10;", + "select * from customer where address_id < 224 order by address_id limit 10;", + "select * from customer where active=1 order by last_name;", + "select * from customer where address_id > 224 order by address_id;", + "select * from customer where address_id in (224,510) order by last_name;", + "select city from city where country_id = 44;", + "select city,city_id from city where country_id = 44 and last_update='2006-02-15 04:45:25';", + "select city from city where country_id > 44 and last_update > '2006-02-15 04:45:25';", + "select * from city where country_id=1 and city='Kabul' order by last_update;", + "select * from city where country_id>1 and city='Kabul' order by last_update;", + "select * from city where city_id>251 order by last_update; ", + "select * from city i inner join country o on i.country_id=o.country_id;", + "select * from city i left join country o on i.city_id=o.country_id;", + "select * from city i right join country o on i.city_id=o.country_id;", + "select * from city i left join country o on i.city_id=o.country_id where o.country_id is null;", + "select * from city i right join country o on i.city_id=o.country_id where i.city_id is null;", + "select * from city i left join country o on i.city_id=o.country_id union select * from city i right join country o on i.city_id=o.country_id;", + "select * from city i left join country o on i.city_id=o.country_id where o.country_id is null union select * from city i right join country o on i.city_id=o.country_id where i.city_id is null;", + "select first_name,last_name,email from customer natural left join address;", + "select first_name,last_name,email from customer natural left join address;", + "select first_name,last_name,email from customer natural right join address;", + "select first_name,last_name,email from customer STRAIGHT_JOIN address on customer.address_id=address.address_id;", + "select ID,name from (select address from customer_list where SID=1 order by phone limit 50,10) a join customer_list l on (a.address=l.address) join city c on (c.city=l.city) order by phone desc;", + } + + rEnv := connTest + + env := NewVirtualEnv(connTest) + defer env.CleanUp() + common.GoldenDiff(func() { + for _, sql := range testSQL { + env.BuildVirtualEnv(rEnv, sql) + switch err := env.Error.(type) { + case nil: + pretty.Println(sql, "OK") + case error: + // unexpected EOF + // 测试环境无法访问,或者被Disable的时候会进入这个分支 + pretty.Println(sql, err) + case *mysql.Error: + if err.Code != 1061 { + t.Error(err) + } + default: + t.Error(err) + } + } + }, t.Name(), update) +} + +func TestGenTableColumns(t *testing.T) { + vEnv, rEnv := BuildEnv() + defer vEnv.CleanUp() + + pretty.Println(common.Config.TestDSN.Disable) + if common.Config.TestDSN.Disable { + common.Log.Warn("common.Config.TestDSN.Disable=true, by pass TestGenTableColumns") + return + } + + // 只能对sakila数据库进行测试 + if rEnv.Database == "sakila" { + testSQL := []string{ + "select * from city where country_id = 44;", + "select country_id from city where country_id = 44;", + "select country_id from city where country_id > 44;", + } + + metaList := []common.Meta{ + { + "": &common.DB{ + Table: map[string]*common.Table{ + "city": common.NewTable("city"), + }, + }, + }, + { + "sakila": &common.DB{ + Table: map[string]*common.Table{ + "city": common.NewTable("city"), + }, + }, + }, + { + "sakila": &common.DB{ + Table: map[string]*common.Table{ + "city": { + TableName: "city", + Column: map[string]*common.Column{ + "country_id": { + Name: "country_id", + }, + }, + }, + }, + }, + }, + } + + for i, sql := range testSQL { + vEnv.BuildVirtualEnv(rEnv, sql) + tFlag := false + columns := vEnv.GenTableColumns(metaList[i]) + if _, ok := columns["sakila"]; ok { + if _, okk := columns["sakila"]["city"]; okk { + if length := len(columns["sakila"]["city"]); length >= 1 { + tFlag = true + } + } + } + + if !tFlag { + t.Errorf("columns: \n%s", pretty.Sprint(columns)) + } + } + } +} diff --git a/env/testdata/TestNewVirtualEnv.golden b/env/testdata/TestNewVirtualEnv.golden new file mode 100644 index 00000000..65edf3fd --- /dev/null +++ b/env/testdata/TestNewVirtualEnv.golden @@ -0,0 +1,42 @@ +create table t(id int,c1 varchar(20),PRIMARY KEY (id)); OK +alter table t add index `idx_c1`(c1); OK +alter table t add index `idx_c1`(c1); OK +select * from city where country_id = 44; OK +select * from address where address2 is not null; OK +select * from address where address2 is null; OK +select * from address where address2 >= 44; OK +select * from city where country_id between 44 and 107; OK +select * from city where city like 'Ad%'; OK +select * from city where city = 'Aden' and country_id = 107; OK +select * from city where country_id > 31 and city = 'Aden'; OK +select * from address where address_id > 8 and city_id < 400 and district = 'Nantou'; OK +select * from address where address_id > 8 and city_id < 400; OK +select * from actor where last_update='2006-02-15 04:34:33' and last_name='CHASE' group by first_name; OK +select * from address where last_update >='2014-09-25 22:33:47' group by district; OK +select * from address group by address,district; OK +select * from address where last_update='2014-09-25 22:30:27' group by district,(address_id+city_id); OK +select * from customer where active=1 order by last_name limit 10; OK +select * from customer order by last_name limit 10; OK +select * from customer where address_id > 224 order by address_id limit 10; OK +select * from customer where address_id < 224 order by address_id limit 10; OK +select * from customer where active=1 order by last_name; OK +select * from customer where address_id > 224 order by address_id; OK +select * from customer where address_id in (224,510) order by last_name; OK +select city from city where country_id = 44; OK +select city,city_id from city where country_id = 44 and last_update='2006-02-15 04:45:25'; OK +select city from city where country_id > 44 and last_update > '2006-02-15 04:45:25'; OK +select * from city where country_id=1 and city='Kabul' order by last_update; OK +select * from city where country_id>1 and city='Kabul' order by last_update; OK +select * from city where city_id>251 order by last_update; OK +select * from city i inner join country o on i.country_id=o.country_id; OK +select * from city i left join country o on i.city_id=o.country_id; OK +select * from city i right join country o on i.city_id=o.country_id; OK +select * from city i left join country o on i.city_id=o.country_id where o.country_id is null; OK +select * from city i right join country o on i.city_id=o.country_id where i.city_id is null; OK +select * from city i left join country o on i.city_id=o.country_id union select * from city i right join country o on i.city_id=o.country_id; OK +select * from city i left join country o on i.city_id=o.country_id where o.country_id is null union select * from city i right join country o on i.city_id=o.country_id where i.city_id is null; OK +select first_name,last_name,email from customer natural left join address; OK +select first_name,last_name,email from customer natural left join address; OK +select first_name,last_name,email from customer natural right join address; OK +select first_name,last_name,email from customer STRAIGHT_JOIN address on customer.address_id=address.address_id; OK +select ID,name from (select address from customer_list where SID=1 order by phone limit 50,10) a join customer_list l on (a.address=l.address) join city c on (c.city=l.city) order by phone desc; OK diff --git a/etc/soar.blacklist b/etc/soar.blacklist new file mode 100644 index 00000000..270f88de --- /dev/null +++ b/etc/soar.blacklist @@ -0,0 +1,9 @@ +# 这是一个黑名单例子 +## 不评审常见的SET, SHOW, SELECT CONST等完美请求 +^set.* +^show.* +^select \?$ +^\/\*.*\*\/$ +^drop.* +^lock.* +^unlock.* diff --git a/etc/soar.yaml b/etc/soar.yaml new file mode 100644 index 00000000..04cdcdfa --- /dev/null +++ b/etc/soar.yaml @@ -0,0 +1,20 @@ +# 这是一个配置文件例子 +online-dsn: + addr: 127.0.0.1:3306 + schema: sakila + user: root + password: "1tIsB1g3rt" + disable: false + +test-dsn: + addr: 127.0.0.1:3306 + schema: sakila + user: root + password: "1tIsB1g3rt" + disable: false + +allow-online-as-test: true + +log-level: 7 +log-output: soar.log +sampling: true diff --git a/genver.sh b/genver.sh new file mode 100755 index 00000000..5c199f40 --- /dev/null +++ b/genver.sh @@ -0,0 +1,46 @@ +#!/bin/bash + +## Go version check +GO_VERSION_MIN=$1 +echo "==> Checking that build is using go version >= ${GO_VERSION_MIN}..." + +GO_VERSION=$(go version | grep -o 'go[0-9]\+\.[0-9]\+\(\.[0-9]\+\)\?' | tr -d 'go') + +IFS="." read -r -a GO_VERSION_ARR <<<"$GO_VERSION" +IFS="." read -r -a GO_VERSION_REQ <<<"$GO_VERSION_MIN" + +if [[ ${GO_VERSION_ARR[0]} -lt ${GO_VERSION_REQ[0]} || (${GO_VERSION_ARR[0]} -eq ${GO_VERSION_REQ[0]} && (${GO_VERSION_ARR[1]} -lt ${GO_VERSION_REQ[1]} || (${GO_VERSION_ARR[1]} -eq ${GO_VERSION_REQ[1]} && ${GO_VERSION_ARR[2]} -lt ${GO_VERSION_REQ[2]}))) ]] \ + ; then + echo "requires go $GO_VERSION_MIN to build; found $GO_VERSION." + exit 1 +fi + +## Generate Repository Version +version=$(git log --date=iso --pretty=format:"%cd @%h" -1) +if [ "X${version}" == "X" ]; then + version="not a git repo" +fi + +git_dirty=$(git diff --no-ext-diff 2>/dev/null | wc -l) + +compile="$(date +"%F %T %z") by $(go version)" + +branch=$(git rev-parse --abbrev-ref HEAD) + +dev_path=$( + cd "$(dirname "$0")" || exit + pwd +) + +cat <common/version.go +package common + +// -version输出信息 +const ( + Version = "${version}" + Compile = "${compile}" + Branch = "${branch}" + GitDirty= ${git_dirty} + DevPath = "${dev_path}" +) +EOF diff --git a/retool-install.sh b/retool-install.sh new file mode 100755 index 00000000..23e441e3 --- /dev/null +++ b/retool-install.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +set -euo pipefail + +# This script generates tools.json +# It helps record what releases/branches are being used +which retool >/dev/null || go get -u github.com/twitchtv/retool + +# This tool can run other checks in a standardized way +retool add gopkg.in/alecthomas/gometalinter.v2 v2.0.5 + +# check spelling +# misspell works with gometalinter +retool add github.com/client9/misspell/cmd/misspell v0.3.4 +# goword adds additional capability to check comments +retool add github.com/chzchzchz/goword a9744cb52b033fe5c269df48eeef2c954526cd79 + +# checks correctness +retool add github.com/gordonklaus/ineffassign 7bae11eba15a3285c75e388f77eb6357a2d73ee2 +retool add honnef.co/go/tools/cmd/megacheck master +retool add github.com/dnephin/govet 4a96d43e39d340b63daa8bc5576985aa599885f6 + +# slow checks +retool add github.com/kisielk/errcheck v1.1.0 +retool add github.com/securego/gosec/cmd/gosec 1.0.0 + +# linter +retool add github.com/mgechev/revive 7773f47324c2bf1c8f7a5500aff2b6c01d3ed73b +retool add github.com/golangci/golangci-lint/cmd/golangci-lint v1.10 diff --git a/revive.toml b/revive.toml new file mode 100644 index 00000000..ebe13ddc --- /dev/null +++ b/revive.toml @@ -0,0 +1,51 @@ +ignoreGeneratedHeader = false +severity = "error" +confidence = 0.8 +errorCode = 0 +warningCode = 0 + +[rule.blank-imports] +[rule.context-as-argument] +[rule.dot-imports] +[rule.error-return] +[rule.error-strings] +[rule.error-naming] +[rule.exported] +[rule.if-return] +[rule.var-naming] +[rule.package-comments] +[rule.range] +[rule.receiver-naming] +[rule.indent-error-flow] +[rule.superfluous-else] +[rule.modifies-parameter] + +# This can be checked by other tools like megacheck +#[rule.unreachable-code] + + +# Currently this makes too much noise, but should add it in +# and perhaps ignore it in a few files +#[rule.confusing-naming] +# severity = "warning" +#[rule.confusing-results] +# severity = "warning" +#[rule.unused-parameter] +# severity = "warning" +#[rule.deep-exit] +# severity = "warning" +#[rule.flag-parameter] +# severity = "warning" + + + +# Adding these will slow down the linter +# They are already provided by megacheck +#[rule.unexported-return] +#[rule.time-naming] +#[rule.errorf] + +# Adding these will slow down the linter +# Not sure if they are already provided by megacheck +#[rule.var-declaration] +#[rule.context-keys-type] diff --git a/tools.json b/tools.json new file mode 100644 index 00000000..b9e5b2df --- /dev/null +++ b/tools.json @@ -0,0 +1,45 @@ +{ + "Tools": [ + { + "Repository": "gopkg.in/alecthomas/gometalinter.v2", + "Commit": "46cc1ea3778b247666c2949669a3333c532fa9c6" + }, + { + "Repository": "github.com/client9/misspell/cmd/misspell", + "Commit": "7888c6b6ce89353cd98e196bce3c3f9e4cdf31f6" + }, + { + "Repository": "github.com/chzchzchz/goword", + "Commit": "a9744cb52b033fe5c269df48eeef2c954526cd79" + }, + { + "Repository": "github.com/gordonklaus/ineffassign", + "Commit": "7bae11eba15a3285c75e388f77eb6357a2d73ee2" + }, + { + "Repository": "github.com/dnephin/govet", + "Commit": "4a96d43e39d340b63daa8bc5576985aa599885f6" + }, + { + "Repository": "github.com/securego/gosec/cmd/gosec", + "Commit": "5fb530cda357c16175f2c049577d2030de735b28" + }, + { + "Repository": "github.com/kisielk/errcheck", + "Commit": "55d8f507faff4d6eddd0c41a3e713e2567fca4e5" + }, + { + "Repository": "github.com/mgechev/revive", + "Commit": "7773f47324c2bf1c8f7a5500aff2b6c01d3ed73b" + }, + { + "Repository": "github.com/golangci/golangci-lint/cmd/golangci-lint", + "Commit": "a2b901227c37337bce9860499a413db2b464481b" + }, + { + "Repository": "honnef.co/go/tools/cmd/megacheck", + "Commit": "88497007e8588ea5b6baee991f74a1607e809487" + } + ], + "RetoolVersion": "1.3.7" +} \ No newline at end of file diff --git a/vendor/github.com/astaxie/beego/LICENSE b/vendor/github.com/astaxie/beego/LICENSE new file mode 100644 index 00000000..5dbd4243 --- /dev/null +++ b/vendor/github.com/astaxie/beego/LICENSE @@ -0,0 +1,13 @@ +Copyright 2014 astaxie + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. \ No newline at end of file diff --git a/vendor/github.com/astaxie/beego/logs/README.md b/vendor/github.com/astaxie/beego/logs/README.md new file mode 100644 index 00000000..57d7abc3 --- /dev/null +++ b/vendor/github.com/astaxie/beego/logs/README.md @@ -0,0 +1,63 @@ +## logs +logs is a Go logs manager. It can use many logs adapters. The repo is inspired by `database/sql` . + + +## How to install? + + go get github.com/astaxie/beego/logs + + +## What adapters are supported? + +As of now this logs support console, file,smtp and conn. + + +## How to use it? + +First you must import it + + import ( + "github.com/astaxie/beego/logs" + ) + +Then init a Log (example with console adapter) + + log := NewLogger(10000) + log.SetLogger("console", "") + +> the first params stand for how many channel + +Use it like this: + + log.Trace("trace") + log.Info("info") + log.Warn("warning") + log.Debug("debug") + log.Critical("critical") + + +## File adapter + +Configure file adapter like this: + + log := NewLogger(10000) + log.SetLogger("file", `{"filename":"test.log"}`) + + +## Conn adapter + +Configure like this: + + log := NewLogger(1000) + log.SetLogger("conn", `{"net":"tcp","addr":":7020"}`) + log.Info("info") + + +## Smtp adapter + +Configure like this: + + log := NewLogger(10000) + log.SetLogger("smtp", `{"username":"beegotest@gmail.com","password":"xxxxxxxx","host":"smtp.gmail.com:587","sendTos":["xiemengjun@gmail.com"]}`) + log.Critical("sendmail critical") + time.Sleep(time.Second * 30) diff --git a/vendor/github.com/astaxie/beego/logs/accesslog.go b/vendor/github.com/astaxie/beego/logs/accesslog.go new file mode 100644 index 00000000..cf799dc1 --- /dev/null +++ b/vendor/github.com/astaxie/beego/logs/accesslog.go @@ -0,0 +1,86 @@ +// Copyright 2014 beego Author. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package logs + +import ( + "bytes" + "encoding/json" + "time" + "fmt" +) + +const ( + apacheFormatPattern = "%s - - [%s] \"%s %d %d\" %f %s %s\n" + apacheFormat = "APACHE_FORMAT" + jsonFormat = "JSON_FORMAT" +) + +// AccessLogRecord struct for holding access log data. +type AccessLogRecord struct { + RemoteAddr string `json:"remote_addr"` + RequestTime time.Time `json:"request_time"` + RequestMethod string `json:"request_method"` + Request string `json:"request"` + ServerProtocol string `json:"server_protocol"` + Host string `json:"host"` + Status int `json:"status"` + BodyBytesSent int64 `json:"body_bytes_sent"` + ElapsedTime time.Duration `json:"elapsed_time"` + HTTPReferrer string `json:"http_referrer"` + HTTPUserAgent string `json:"http_user_agent"` + RemoteUser string `json:"remote_user"` +} + +func (r *AccessLogRecord) json() ([]byte, error) { + buffer := &bytes.Buffer{} + encoder := json.NewEncoder(buffer) + disableEscapeHTML(encoder) + + err := encoder.Encode(r) + return buffer.Bytes(), err +} + +func disableEscapeHTML(i interface{}) { + e, ok := i.(interface { + SetEscapeHTML(bool) + }); + if ok { + e.SetEscapeHTML(false) + } +} + +// AccessLog - Format and print access log. +func AccessLog(r *AccessLogRecord, format string) { + var msg string + + switch format { + + case apacheFormat: + timeFormatted := r.RequestTime.Format("02/Jan/2006 03:04:05") + msg = fmt.Sprintf(apacheFormatPattern, r.RemoteAddr, timeFormatted, r.Request, r.Status, r.BodyBytesSent, + r.ElapsedTime.Seconds(), r.HTTPReferrer, r.HTTPUserAgent) + case jsonFormat: + fallthrough + default: + jsonData, err := r.json() + if err != nil { + msg = fmt.Sprintf(`{"Error": "%s"}`, err) + } else { + msg = string(jsonData) + } + } + + beeLogger.Debug(msg) +} diff --git a/vendor/github.com/astaxie/beego/logs/color.go b/vendor/github.com/astaxie/beego/logs/color.go new file mode 100644 index 00000000..41d23638 --- /dev/null +++ b/vendor/github.com/astaxie/beego/logs/color.go @@ -0,0 +1,28 @@ +// Copyright 2014 beego Author. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// +build !windows + +package logs + +import "io" + +type ansiColorWriter struct { + w io.Writer + mode outputMode +} + +func (cw *ansiColorWriter) Write(p []byte) (int, error) { + return cw.w.Write(p) +} diff --git a/vendor/github.com/astaxie/beego/logs/color_windows.go b/vendor/github.com/astaxie/beego/logs/color_windows.go new file mode 100644 index 00000000..4e28f188 --- /dev/null +++ b/vendor/github.com/astaxie/beego/logs/color_windows.go @@ -0,0 +1,428 @@ +// Copyright 2014 beego Author. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// +build windows + +package logs + +import ( + "bytes" + "io" + "strings" + "syscall" + "unsafe" +) + +type ( + csiState int + parseResult int +) + +const ( + outsideCsiCode csiState = iota + firstCsiCode + secondCsiCode +) + +const ( + noConsole parseResult = iota + changedColor + unknown +) + +type ansiColorWriter struct { + w io.Writer + mode outputMode + state csiState + paramStartBuf bytes.Buffer + paramBuf bytes.Buffer +} + +const ( + firstCsiChar byte = '\x1b' + secondeCsiChar byte = '[' + separatorChar byte = ';' + sgrCode byte = 'm' +) + +const ( + foregroundBlue = uint16(0x0001) + foregroundGreen = uint16(0x0002) + foregroundRed = uint16(0x0004) + foregroundIntensity = uint16(0x0008) + backgroundBlue = uint16(0x0010) + backgroundGreen = uint16(0x0020) + backgroundRed = uint16(0x0040) + backgroundIntensity = uint16(0x0080) + underscore = uint16(0x8000) + + foregroundMask = foregroundBlue | foregroundGreen | foregroundRed | foregroundIntensity + backgroundMask = backgroundBlue | backgroundGreen | backgroundRed | backgroundIntensity +) + +const ( + ansiReset = "0" + ansiIntensityOn = "1" + ansiIntensityOff = "21" + ansiUnderlineOn = "4" + ansiUnderlineOff = "24" + ansiBlinkOn = "5" + ansiBlinkOff = "25" + + ansiForegroundBlack = "30" + ansiForegroundRed = "31" + ansiForegroundGreen = "32" + ansiForegroundYellow = "33" + ansiForegroundBlue = "34" + ansiForegroundMagenta = "35" + ansiForegroundCyan = "36" + ansiForegroundWhite = "37" + ansiForegroundDefault = "39" + + ansiBackgroundBlack = "40" + ansiBackgroundRed = "41" + ansiBackgroundGreen = "42" + ansiBackgroundYellow = "43" + ansiBackgroundBlue = "44" + ansiBackgroundMagenta = "45" + ansiBackgroundCyan = "46" + ansiBackgroundWhite = "47" + ansiBackgroundDefault = "49" + + ansiLightForegroundGray = "90" + ansiLightForegroundRed = "91" + ansiLightForegroundGreen = "92" + ansiLightForegroundYellow = "93" + ansiLightForegroundBlue = "94" + ansiLightForegroundMagenta = "95" + ansiLightForegroundCyan = "96" + ansiLightForegroundWhite = "97" + + ansiLightBackgroundGray = "100" + ansiLightBackgroundRed = "101" + ansiLightBackgroundGreen = "102" + ansiLightBackgroundYellow = "103" + ansiLightBackgroundBlue = "104" + ansiLightBackgroundMagenta = "105" + ansiLightBackgroundCyan = "106" + ansiLightBackgroundWhite = "107" +) + +type drawType int + +const ( + foreground drawType = iota + background +) + +type winColor struct { + code uint16 + drawType drawType +} + +var colorMap = map[string]winColor{ + ansiForegroundBlack: {0, foreground}, + ansiForegroundRed: {foregroundRed, foreground}, + ansiForegroundGreen: {foregroundGreen, foreground}, + ansiForegroundYellow: {foregroundRed | foregroundGreen, foreground}, + ansiForegroundBlue: {foregroundBlue, foreground}, + ansiForegroundMagenta: {foregroundRed | foregroundBlue, foreground}, + ansiForegroundCyan: {foregroundGreen | foregroundBlue, foreground}, + ansiForegroundWhite: {foregroundRed | foregroundGreen | foregroundBlue, foreground}, + ansiForegroundDefault: {foregroundRed | foregroundGreen | foregroundBlue, foreground}, + + ansiBackgroundBlack: {0, background}, + ansiBackgroundRed: {backgroundRed, background}, + ansiBackgroundGreen: {backgroundGreen, background}, + ansiBackgroundYellow: {backgroundRed | backgroundGreen, background}, + ansiBackgroundBlue: {backgroundBlue, background}, + ansiBackgroundMagenta: {backgroundRed | backgroundBlue, background}, + ansiBackgroundCyan: {backgroundGreen | backgroundBlue, background}, + ansiBackgroundWhite: {backgroundRed | backgroundGreen | backgroundBlue, background}, + ansiBackgroundDefault: {0, background}, + + ansiLightForegroundGray: {foregroundIntensity, foreground}, + ansiLightForegroundRed: {foregroundIntensity | foregroundRed, foreground}, + ansiLightForegroundGreen: {foregroundIntensity | foregroundGreen, foreground}, + ansiLightForegroundYellow: {foregroundIntensity | foregroundRed | foregroundGreen, foreground}, + ansiLightForegroundBlue: {foregroundIntensity | foregroundBlue, foreground}, + ansiLightForegroundMagenta: {foregroundIntensity | foregroundRed | foregroundBlue, foreground}, + ansiLightForegroundCyan: {foregroundIntensity | foregroundGreen | foregroundBlue, foreground}, + ansiLightForegroundWhite: {foregroundIntensity | foregroundRed | foregroundGreen | foregroundBlue, foreground}, + + ansiLightBackgroundGray: {backgroundIntensity, background}, + ansiLightBackgroundRed: {backgroundIntensity | backgroundRed, background}, + ansiLightBackgroundGreen: {backgroundIntensity | backgroundGreen, background}, + ansiLightBackgroundYellow: {backgroundIntensity | backgroundRed | backgroundGreen, background}, + ansiLightBackgroundBlue: {backgroundIntensity | backgroundBlue, background}, + ansiLightBackgroundMagenta: {backgroundIntensity | backgroundRed | backgroundBlue, background}, + ansiLightBackgroundCyan: {backgroundIntensity | backgroundGreen | backgroundBlue, background}, + ansiLightBackgroundWhite: {backgroundIntensity | backgroundRed | backgroundGreen | backgroundBlue, background}, +} + +var ( + kernel32 = syscall.NewLazyDLL("kernel32.dll") + procSetConsoleTextAttribute = kernel32.NewProc("SetConsoleTextAttribute") + procGetConsoleScreenBufferInfo = kernel32.NewProc("GetConsoleScreenBufferInfo") + defaultAttr *textAttributes +) + +func init() { + screenInfo := getConsoleScreenBufferInfo(uintptr(syscall.Stdout)) + if screenInfo != nil { + colorMap[ansiForegroundDefault] = winColor{ + screenInfo.WAttributes & (foregroundRed | foregroundGreen | foregroundBlue), + foreground, + } + colorMap[ansiBackgroundDefault] = winColor{ + screenInfo.WAttributes & (backgroundRed | backgroundGreen | backgroundBlue), + background, + } + defaultAttr = convertTextAttr(screenInfo.WAttributes) + } +} + +type coord struct { + X, Y int16 +} + +type smallRect struct { + Left, Top, Right, Bottom int16 +} + +type consoleScreenBufferInfo struct { + DwSize coord + DwCursorPosition coord + WAttributes uint16 + SrWindow smallRect + DwMaximumWindowSize coord +} + +func getConsoleScreenBufferInfo(hConsoleOutput uintptr) *consoleScreenBufferInfo { + var csbi consoleScreenBufferInfo + ret, _, _ := procGetConsoleScreenBufferInfo.Call( + hConsoleOutput, + uintptr(unsafe.Pointer(&csbi))) + if ret == 0 { + return nil + } + return &csbi +} + +func setConsoleTextAttribute(hConsoleOutput uintptr, wAttributes uint16) bool { + ret, _, _ := procSetConsoleTextAttribute.Call( + hConsoleOutput, + uintptr(wAttributes)) + return ret != 0 +} + +type textAttributes struct { + foregroundColor uint16 + backgroundColor uint16 + foregroundIntensity uint16 + backgroundIntensity uint16 + underscore uint16 + otherAttributes uint16 +} + +func convertTextAttr(winAttr uint16) *textAttributes { + fgColor := winAttr & (foregroundRed | foregroundGreen | foregroundBlue) + bgColor := winAttr & (backgroundRed | backgroundGreen | backgroundBlue) + fgIntensity := winAttr & foregroundIntensity + bgIntensity := winAttr & backgroundIntensity + underline := winAttr & underscore + otherAttributes := winAttr &^ (foregroundMask | backgroundMask | underscore) + return &textAttributes{fgColor, bgColor, fgIntensity, bgIntensity, underline, otherAttributes} +} + +func convertWinAttr(textAttr *textAttributes) uint16 { + var winAttr uint16 + winAttr |= textAttr.foregroundColor + winAttr |= textAttr.backgroundColor + winAttr |= textAttr.foregroundIntensity + winAttr |= textAttr.backgroundIntensity + winAttr |= textAttr.underscore + winAttr |= textAttr.otherAttributes + return winAttr +} + +func changeColor(param []byte) parseResult { + screenInfo := getConsoleScreenBufferInfo(uintptr(syscall.Stdout)) + if screenInfo == nil { + return noConsole + } + + winAttr := convertTextAttr(screenInfo.WAttributes) + strParam := string(param) + if len(strParam) <= 0 { + strParam = "0" + } + csiParam := strings.Split(strParam, string(separatorChar)) + for _, p := range csiParam { + c, ok := colorMap[p] + switch { + case !ok: + switch p { + case ansiReset: + winAttr.foregroundColor = defaultAttr.foregroundColor + winAttr.backgroundColor = defaultAttr.backgroundColor + winAttr.foregroundIntensity = defaultAttr.foregroundIntensity + winAttr.backgroundIntensity = defaultAttr.backgroundIntensity + winAttr.underscore = 0 + winAttr.otherAttributes = 0 + case ansiIntensityOn: + winAttr.foregroundIntensity = foregroundIntensity + case ansiIntensityOff: + winAttr.foregroundIntensity = 0 + case ansiUnderlineOn: + winAttr.underscore = underscore + case ansiUnderlineOff: + winAttr.underscore = 0 + case ansiBlinkOn: + winAttr.backgroundIntensity = backgroundIntensity + case ansiBlinkOff: + winAttr.backgroundIntensity = 0 + default: + // unknown code + } + case c.drawType == foreground: + winAttr.foregroundColor = c.code + case c.drawType == background: + winAttr.backgroundColor = c.code + } + } + winTextAttribute := convertWinAttr(winAttr) + setConsoleTextAttribute(uintptr(syscall.Stdout), winTextAttribute) + + return changedColor +} + +func parseEscapeSequence(command byte, param []byte) parseResult { + if defaultAttr == nil { + return noConsole + } + + switch command { + case sgrCode: + return changeColor(param) + default: + return unknown + } +} + +func (cw *ansiColorWriter) flushBuffer() (int, error) { + return cw.flushTo(cw.w) +} + +func (cw *ansiColorWriter) resetBuffer() (int, error) { + return cw.flushTo(nil) +} + +func (cw *ansiColorWriter) flushTo(w io.Writer) (int, error) { + var n1, n2 int + var err error + + startBytes := cw.paramStartBuf.Bytes() + cw.paramStartBuf.Reset() + if w != nil { + n1, err = cw.w.Write(startBytes) + if err != nil { + return n1, err + } + } else { + n1 = len(startBytes) + } + paramBytes := cw.paramBuf.Bytes() + cw.paramBuf.Reset() + if w != nil { + n2, err = cw.w.Write(paramBytes) + if err != nil { + return n1 + n2, err + } + } else { + n2 = len(paramBytes) + } + return n1 + n2, nil +} + +func isParameterChar(b byte) bool { + return ('0' <= b && b <= '9') || b == separatorChar +} + +func (cw *ansiColorWriter) Write(p []byte) (int, error) { + var r, nw, first, last int + if cw.mode != DiscardNonColorEscSeq { + cw.state = outsideCsiCode + cw.resetBuffer() + } + + var err error + for i, ch := range p { + switch cw.state { + case outsideCsiCode: + if ch == firstCsiChar { + cw.paramStartBuf.WriteByte(ch) + cw.state = firstCsiCode + } + case firstCsiCode: + switch ch { + case firstCsiChar: + cw.paramStartBuf.WriteByte(ch) + break + case secondeCsiChar: + cw.paramStartBuf.WriteByte(ch) + cw.state = secondCsiCode + last = i - 1 + default: + cw.resetBuffer() + cw.state = outsideCsiCode + } + case secondCsiCode: + if isParameterChar(ch) { + cw.paramBuf.WriteByte(ch) + } else { + nw, err = cw.w.Write(p[first:last]) + r += nw + if err != nil { + return r, err + } + first = i + 1 + result := parseEscapeSequence(ch, cw.paramBuf.Bytes()) + if result == noConsole || (cw.mode == OutputNonColorEscSeq && result == unknown) { + cw.paramBuf.WriteByte(ch) + nw, err := cw.flushBuffer() + if err != nil { + return r, err + } + r += nw + } else { + n, _ := cw.resetBuffer() + // Add one more to the size of the buffer for the last ch + r += n + 1 + } + + cw.state = outsideCsiCode + } + default: + cw.state = outsideCsiCode + } + } + + if cw.mode != DiscardNonColorEscSeq || cw.state == outsideCsiCode { + nw, err = cw.w.Write(p[first:]) + r += nw + } + + return r, err +} diff --git a/vendor/github.com/astaxie/beego/logs/conn.go b/vendor/github.com/astaxie/beego/logs/conn.go new file mode 100644 index 00000000..6d5bf6bf --- /dev/null +++ b/vendor/github.com/astaxie/beego/logs/conn.go @@ -0,0 +1,117 @@ +// Copyright 2014 beego Author. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package logs + +import ( + "encoding/json" + "io" + "net" + "time" +) + +// connWriter implements LoggerInterface. +// it writes messages in keep-live tcp connection. +type connWriter struct { + lg *logWriter + innerWriter io.WriteCloser + ReconnectOnMsg bool `json:"reconnectOnMsg"` + Reconnect bool `json:"reconnect"` + Net string `json:"net"` + Addr string `json:"addr"` + Level int `json:"level"` +} + +// NewConn create new ConnWrite returning as LoggerInterface. +func NewConn() Logger { + conn := new(connWriter) + conn.Level = LevelTrace + return conn +} + +// Init init connection writer with json config. +// json config only need key "level". +func (c *connWriter) Init(jsonConfig string) error { + return json.Unmarshal([]byte(jsonConfig), c) +} + +// WriteMsg write message in connection. +// if connection is down, try to re-connect. +func (c *connWriter) WriteMsg(when time.Time, msg string, level int) error { + if level > c.Level { + return nil + } + if c.needToConnectOnMsg() { + err := c.connect() + if err != nil { + return err + } + } + + if c.ReconnectOnMsg { + defer c.innerWriter.Close() + } + + c.lg.println(when, msg) + return nil +} + +// Flush implementing method. empty. +func (c *connWriter) Flush() { + +} + +// Destroy destroy connection writer and close tcp listener. +func (c *connWriter) Destroy() { + if c.innerWriter != nil { + c.innerWriter.Close() + } +} + +func (c *connWriter) connect() error { + if c.innerWriter != nil { + c.innerWriter.Close() + c.innerWriter = nil + } + + conn, err := net.Dial(c.Net, c.Addr) + if err != nil { + return err + } + + if tcpConn, ok := conn.(*net.TCPConn); ok { + tcpConn.SetKeepAlive(true) + } + + c.innerWriter = conn + c.lg = newLogWriter(conn) + return nil +} + +func (c *connWriter) needToConnectOnMsg() bool { + if c.Reconnect { + c.Reconnect = false + return true + } + + if c.innerWriter == nil { + return true + } + + return c.ReconnectOnMsg +} + +func init() { + Register(AdapterConn, NewConn) +} diff --git a/vendor/github.com/astaxie/beego/logs/console.go b/vendor/github.com/astaxie/beego/logs/console.go new file mode 100644 index 00000000..e75f2a1b --- /dev/null +++ b/vendor/github.com/astaxie/beego/logs/console.go @@ -0,0 +1,101 @@ +// Copyright 2014 beego Author. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package logs + +import ( + "encoding/json" + "os" + "runtime" + "time" +) + +// brush is a color join function +type brush func(string) string + +// newBrush return a fix color Brush +func newBrush(color string) brush { + pre := "\033[" + reset := "\033[0m" + return func(text string) string { + return pre + color + "m" + text + reset + } +} + +var colors = []brush{ + newBrush("1;37"), // Emergency white + newBrush("1;36"), // Alert cyan + newBrush("1;35"), // Critical magenta + newBrush("1;31"), // Error red + newBrush("1;33"), // Warning yellow + newBrush("1;32"), // Notice green + newBrush("1;34"), // Informational blue + newBrush("1;44"), // Debug Background blue +} + +// consoleWriter implements LoggerInterface and writes messages to terminal. +type consoleWriter struct { + lg *logWriter + Level int `json:"level"` + Colorful bool `json:"color"` //this filed is useful only when system's terminal supports color +} + +// NewConsole create ConsoleWriter returning as LoggerInterface. +func NewConsole() Logger { + cw := &consoleWriter{ + lg: newLogWriter(os.Stdout), + Level: LevelDebug, + Colorful: runtime.GOOS != "windows", + } + return cw +} + +// Init init console logger. +// jsonConfig like '{"level":LevelTrace}'. +func (c *consoleWriter) Init(jsonConfig string) error { + if len(jsonConfig) == 0 { + return nil + } + err := json.Unmarshal([]byte(jsonConfig), c) + if runtime.GOOS == "windows" { + c.Colorful = false + } + return err +} + +// WriteMsg write message in console. +func (c *consoleWriter) WriteMsg(when time.Time, msg string, level int) error { + if level > c.Level { + return nil + } + if c.Colorful { + msg = colors[level](msg) + } + c.lg.println(when, msg) + return nil +} + +// Destroy implementing method. empty. +func (c *consoleWriter) Destroy() { + +} + +// Flush implementing method. empty. +func (c *consoleWriter) Flush() { + +} + +func init() { + Register(AdapterConsole, NewConsole) +} diff --git a/vendor/github.com/astaxie/beego/logs/file.go b/vendor/github.com/astaxie/beego/logs/file.go new file mode 100644 index 00000000..8e5117d2 --- /dev/null +++ b/vendor/github.com/astaxie/beego/logs/file.go @@ -0,0 +1,335 @@ +// Copyright 2014 beego Author. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package logs + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "os" + "path/filepath" + "strconv" + "strings" + "sync" + "time" +) + +// fileLogWriter implements LoggerInterface. +// It writes messages by lines limit, file size limit, or time frequency. +type fileLogWriter struct { + sync.RWMutex // write log order by order and atomic incr maxLinesCurLines and maxSizeCurSize + // The opened file + Filename string `json:"filename"` + fileWriter *os.File + + // Rotate at line + MaxLines int `json:"maxlines"` + maxLinesCurLines int + + // Rotate at size + MaxSize int `json:"maxsize"` + maxSizeCurSize int + + // Rotate daily + Daily bool `json:"daily"` + MaxDays int64 `json:"maxdays"` + dailyOpenDate int + dailyOpenTime time.Time + + Rotate bool `json:"rotate"` + + Level int `json:"level"` + + Perm string `json:"perm"` + + RotatePerm string `json:"rotateperm"` + + fileNameOnly, suffix string // like "project.log", project is fileNameOnly and .log is suffix +} + +// newFileWriter create a FileLogWriter returning as LoggerInterface. +func newFileWriter() Logger { + w := &fileLogWriter{ + Daily: true, + MaxDays: 7, + Rotate: true, + RotatePerm: "0440", + Level: LevelTrace, + Perm: "0660", + } + return w +} + +// Init file logger with json config. +// jsonConfig like: +// { +// "filename":"logs/beego.log", +// "maxLines":10000, +// "maxsize":1024, +// "daily":true, +// "maxDays":15, +// "rotate":true, +// "perm":"0600" +// } +func (w *fileLogWriter) Init(jsonConfig string) error { + err := json.Unmarshal([]byte(jsonConfig), w) + if err != nil { + return err + } + if len(w.Filename) == 0 { + return errors.New("jsonconfig must have filename") + } + w.suffix = filepath.Ext(w.Filename) + w.fileNameOnly = strings.TrimSuffix(w.Filename, w.suffix) + if w.suffix == "" { + w.suffix = ".log" + } + err = w.startLogger() + return err +} + +// start file logger. create log file and set to locker-inside file writer. +func (w *fileLogWriter) startLogger() error { + file, err := w.createLogFile() + if err != nil { + return err + } + if w.fileWriter != nil { + w.fileWriter.Close() + } + w.fileWriter = file + return w.initFd() +} + +func (w *fileLogWriter) needRotate(size int, day int) bool { + return (w.MaxLines > 0 && w.maxLinesCurLines >= w.MaxLines) || + (w.MaxSize > 0 && w.maxSizeCurSize >= w.MaxSize) || + (w.Daily && day != w.dailyOpenDate) + +} + +// WriteMsg write logger message into file. +func (w *fileLogWriter) WriteMsg(when time.Time, msg string, level int) error { + if level > w.Level { + return nil + } + h, d := formatTimeHeader(when) + msg = string(h) + msg + "\n" + if w.Rotate { + w.RLock() + if w.needRotate(len(msg), d) { + w.RUnlock() + w.Lock() + if w.needRotate(len(msg), d) { + if err := w.doRotate(when); err != nil { + fmt.Fprintf(os.Stderr, "FileLogWriter(%q): %s\n", w.Filename, err) + } + } + w.Unlock() + } else { + w.RUnlock() + } + } + + w.Lock() + _, err := w.fileWriter.Write([]byte(msg)) + if err == nil { + w.maxLinesCurLines++ + w.maxSizeCurSize += len(msg) + } + w.Unlock() + return err +} + +func (w *fileLogWriter) createLogFile() (*os.File, error) { + // Open the log file + perm, err := strconv.ParseInt(w.Perm, 8, 64) + if err != nil { + return nil, err + } + fd, err := os.OpenFile(w.Filename, os.O_WRONLY|os.O_APPEND|os.O_CREATE, os.FileMode(perm)) + if err == nil { + // Make sure file perm is user set perm cause of `os.OpenFile` will obey umask + os.Chmod(w.Filename, os.FileMode(perm)) + } + return fd, err +} + +func (w *fileLogWriter) initFd() error { + fd := w.fileWriter + fInfo, err := fd.Stat() + if err != nil { + return fmt.Errorf("get stat err: %s", err) + } + w.maxSizeCurSize = int(fInfo.Size()) + w.dailyOpenTime = time.Now() + w.dailyOpenDate = w.dailyOpenTime.Day() + w.maxLinesCurLines = 0 + if w.Daily { + go w.dailyRotate(w.dailyOpenTime) + } + if fInfo.Size() > 0 && w.MaxLines > 0 { + count, err := w.lines() + if err != nil { + return err + } + w.maxLinesCurLines = count + } + return nil +} + +func (w *fileLogWriter) dailyRotate(openTime time.Time) { + y, m, d := openTime.Add(24 * time.Hour).Date() + nextDay := time.Date(y, m, d, 0, 0, 0, 0, openTime.Location()) + tm := time.NewTimer(time.Duration(nextDay.UnixNano() - openTime.UnixNano() + 100)) + <-tm.C + w.Lock() + if w.needRotate(0, time.Now().Day()) { + if err := w.doRotate(time.Now()); err != nil { + fmt.Fprintf(os.Stderr, "FileLogWriter(%q): %s\n", w.Filename, err) + } + } + w.Unlock() +} + +func (w *fileLogWriter) lines() (int, error) { + fd, err := os.Open(w.Filename) + if err != nil { + return 0, err + } + defer fd.Close() + + buf := make([]byte, 32768) // 32k + count := 0 + lineSep := []byte{'\n'} + + for { + c, err := fd.Read(buf) + if err != nil && err != io.EOF { + return count, err + } + + count += bytes.Count(buf[:c], lineSep) + + if err == io.EOF { + break + } + } + + return count, nil +} + +// DoRotate means it need to write file in new file. +// new file name like xx.2013-01-01.log (daily) or xx.001.log (by line or size) +func (w *fileLogWriter) doRotate(logTime time.Time) error { + // file exists + // Find the next available number + num := 1 + fName := "" + rotatePerm, err := strconv.ParseInt(w.RotatePerm, 8, 64) + if err != nil { + return err + } + + _, err = os.Lstat(w.Filename) + if err != nil { + //even if the file is not exist or other ,we should RESTART the logger + goto RESTART_LOGGER + } + + if w.MaxLines > 0 || w.MaxSize > 0 { + for ; err == nil && num <= 999; num++ { + fName = w.fileNameOnly + fmt.Sprintf(".%s.%03d%s", logTime.Format("2006-01-02"), num, w.suffix) + _, err = os.Lstat(fName) + } + } else { + fName = fmt.Sprintf("%s.%s%s", w.fileNameOnly, w.dailyOpenTime.Format("2006-01-02"), w.suffix) + _, err = os.Lstat(fName) + for ; err == nil && num <= 999; num++ { + fName = w.fileNameOnly + fmt.Sprintf(".%s.%03d%s", w.dailyOpenTime.Format("2006-01-02"), num, w.suffix) + _, err = os.Lstat(fName) + } + } + // return error if the last file checked still existed + if err == nil { + return fmt.Errorf("Rotate: Cannot find free log number to rename %s", w.Filename) + } + + // close fileWriter before rename + w.fileWriter.Close() + + // Rename the file to its new found name + // even if occurs error,we MUST guarantee to restart new logger + err = os.Rename(w.Filename, fName) + if err != nil { + goto RESTART_LOGGER + } + + err = os.Chmod(fName, os.FileMode(rotatePerm)) + +RESTART_LOGGER: + + startLoggerErr := w.startLogger() + go w.deleteOldLog() + + if startLoggerErr != nil { + return fmt.Errorf("Rotate StartLogger: %s", startLoggerErr) + } + if err != nil { + return fmt.Errorf("Rotate: %s", err) + } + return nil +} + +func (w *fileLogWriter) deleteOldLog() { + dir := filepath.Dir(w.Filename) + filepath.Walk(dir, func(path string, info os.FileInfo, err error) (returnErr error) { + defer func() { + if r := recover(); r != nil { + fmt.Fprintf(os.Stderr, "Unable to delete old log '%s', error: %v\n", path, r) + } + }() + + if info == nil { + return + } + + if !info.IsDir() && info.ModTime().Add(24*time.Hour*time.Duration(w.MaxDays)).Before(time.Now()) { + if strings.HasPrefix(filepath.Base(path), filepath.Base(w.fileNameOnly)) && + strings.HasSuffix(filepath.Base(path), w.suffix) { + os.Remove(path) + } + } + return + }) +} + +// Destroy close the file description, close file writer. +func (w *fileLogWriter) Destroy() { + w.fileWriter.Close() +} + +// Flush flush file logger. +// there are no buffering messages in file logger in memory. +// flush file means sync file from disk. +func (w *fileLogWriter) Flush() { + w.fileWriter.Sync() +} + +func init() { + Register(AdapterFile, newFileWriter) +} diff --git a/vendor/github.com/astaxie/beego/logs/jianliao.go b/vendor/github.com/astaxie/beego/logs/jianliao.go new file mode 100644 index 00000000..88ba0f9a --- /dev/null +++ b/vendor/github.com/astaxie/beego/logs/jianliao.go @@ -0,0 +1,72 @@ +package logs + +import ( + "encoding/json" + "fmt" + "net/http" + "net/url" + "time" +) + +// JLWriter implements beego LoggerInterface and is used to send jiaoliao webhook +type JLWriter struct { + AuthorName string `json:"authorname"` + Title string `json:"title"` + WebhookURL string `json:"webhookurl"` + RedirectURL string `json:"redirecturl,omitempty"` + ImageURL string `json:"imageurl,omitempty"` + Level int `json:"level"` +} + +// newJLWriter create jiaoliao writer. +func newJLWriter() Logger { + return &JLWriter{Level: LevelTrace} +} + +// Init JLWriter with json config string +func (s *JLWriter) Init(jsonconfig string) error { + return json.Unmarshal([]byte(jsonconfig), s) +} + +// WriteMsg write message in smtp writer. +// it will send an email with subject and only this message. +func (s *JLWriter) WriteMsg(when time.Time, msg string, level int) error { + if level > s.Level { + return nil + } + + text := fmt.Sprintf("%s %s", when.Format("2006-01-02 15:04:05"), msg) + + form := url.Values{} + form.Add("authorName", s.AuthorName) + form.Add("title", s.Title) + form.Add("text", text) + if s.RedirectURL != "" { + form.Add("redirectUrl", s.RedirectURL) + } + if s.ImageURL != "" { + form.Add("imageUrl", s.ImageURL) + } + + resp, err := http.PostForm(s.WebhookURL, form) + if err != nil { + return err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("Post webhook failed %s %d", resp.Status, resp.StatusCode) + } + return nil +} + +// Flush implementing method. empty. +func (s *JLWriter) Flush() { +} + +// Destroy implementing method. empty. +func (s *JLWriter) Destroy() { +} + +func init() { + Register(AdapterJianLiao, newJLWriter) +} diff --git a/vendor/github.com/astaxie/beego/logs/log.go b/vendor/github.com/astaxie/beego/logs/log.go new file mode 100644 index 00000000..0e97a70e --- /dev/null +++ b/vendor/github.com/astaxie/beego/logs/log.go @@ -0,0 +1,646 @@ +// Copyright 2014 beego Author. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package logs provide a general log interface +// Usage: +// +// import "github.com/astaxie/beego/logs" +// +// log := NewLogger(10000) +// log.SetLogger("console", "") +// +// > the first params stand for how many channel +// +// Use it like this: +// +// log.Trace("trace") +// log.Info("info") +// log.Warn("warning") +// log.Debug("debug") +// log.Critical("critical") +// +// more docs http://beego.me/docs/module/logs.md +package logs + +import ( + "fmt" + "log" + "os" + "path" + "runtime" + "strconv" + "strings" + "sync" + "time" +) + +// RFC5424 log message levels. +const ( + LevelEmergency = iota + LevelAlert + LevelCritical + LevelError + LevelWarning + LevelNotice + LevelInformational + LevelDebug +) + +// levelLogLogger is defined to implement log.Logger +// the real log level will be LevelEmergency +const levelLoggerImpl = -1 + +// Name for adapter with beego official support +const ( + AdapterConsole = "console" + AdapterFile = "file" + AdapterMultiFile = "multifile" + AdapterMail = "smtp" + AdapterConn = "conn" + AdapterEs = "es" + AdapterJianLiao = "jianliao" + AdapterSlack = "slack" + AdapterAliLS = "alils" +) + +// Legacy log level constants to ensure backwards compatibility. +const ( + LevelInfo = LevelInformational + LevelTrace = LevelDebug + LevelWarn = LevelWarning +) + +type newLoggerFunc func() Logger + +// Logger defines the behavior of a log provider. +type Logger interface { + Init(config string) error + WriteMsg(when time.Time, msg string, level int) error + Destroy() + Flush() +} + +var adapters = make(map[string]newLoggerFunc) +var levelPrefix = [LevelDebug + 1]string{"[M] ", "[A] ", "[C] ", "[E] ", "[W] ", "[N] ", "[I] ", "[D] "} + +// Register makes a log provide available by the provided name. +// If Register is called twice with the same name or if driver is nil, +// it panics. +func Register(name string, log newLoggerFunc) { + if log == nil { + panic("logs: Register provide is nil") + } + if _, dup := adapters[name]; dup { + panic("logs: Register called twice for provider " + name) + } + adapters[name] = log +} + +// BeeLogger is default logger in beego application. +// it can contain several providers and log message into all providers. +type BeeLogger struct { + lock sync.Mutex + level int + init bool + enableFuncCallDepth bool + loggerFuncCallDepth int + asynchronous bool + msgChanLen int64 + msgChan chan *logMsg + signalChan chan string + wg sync.WaitGroup + outputs []*nameLogger +} + +const defaultAsyncMsgLen = 1e3 + +type nameLogger struct { + Logger + name string +} + +type logMsg struct { + level int + msg string + when time.Time +} + +var logMsgPool *sync.Pool + +// NewLogger returns a new BeeLogger. +// channelLen means the number of messages in chan(used where asynchronous is true). +// if the buffering chan is full, logger adapters write to file or other way. +func NewLogger(channelLens ...int64) *BeeLogger { + bl := new(BeeLogger) + bl.level = LevelDebug + bl.loggerFuncCallDepth = 2 + bl.msgChanLen = append(channelLens, 0)[0] + if bl.msgChanLen <= 0 { + bl.msgChanLen = defaultAsyncMsgLen + } + bl.signalChan = make(chan string, 1) + bl.setLogger(AdapterConsole) + return bl +} + +// Async set the log to asynchronous and start the goroutine +func (bl *BeeLogger) Async(msgLen ...int64) *BeeLogger { + bl.lock.Lock() + defer bl.lock.Unlock() + if bl.asynchronous { + return bl + } + bl.asynchronous = true + if len(msgLen) > 0 && msgLen[0] > 0 { + bl.msgChanLen = msgLen[0] + } + bl.msgChan = make(chan *logMsg, bl.msgChanLen) + logMsgPool = &sync.Pool{ + New: func() interface{} { + return &logMsg{} + }, + } + bl.wg.Add(1) + go bl.startLogger() + return bl +} + +// SetLogger provides a given logger adapter into BeeLogger with config string. +// config need to be correct JSON as string: {"interval":360}. +func (bl *BeeLogger) setLogger(adapterName string, configs ...string) error { + config := append(configs, "{}")[0] + for _, l := range bl.outputs { + if l.name == adapterName { + return fmt.Errorf("logs: duplicate adaptername %q (you have set this logger before)", adapterName) + } + } + + log, ok := adapters[adapterName] + if !ok { + return fmt.Errorf("logs: unknown adaptername %q (forgotten Register?)", adapterName) + } + + lg := log() + err := lg.Init(config) + if err != nil { + fmt.Fprintln(os.Stderr, "logs.BeeLogger.SetLogger: "+err.Error()) + return err + } + bl.outputs = append(bl.outputs, &nameLogger{name: adapterName, Logger: lg}) + return nil +} + +// SetLogger provides a given logger adapter into BeeLogger with config string. +// config need to be correct JSON as string: {"interval":360}. +func (bl *BeeLogger) SetLogger(adapterName string, configs ...string) error { + bl.lock.Lock() + defer bl.lock.Unlock() + if !bl.init { + bl.outputs = []*nameLogger{} + bl.init = true + } + return bl.setLogger(adapterName, configs...) +} + +// DelLogger remove a logger adapter in BeeLogger. +func (bl *BeeLogger) DelLogger(adapterName string) error { + bl.lock.Lock() + defer bl.lock.Unlock() + outputs := []*nameLogger{} + for _, lg := range bl.outputs { + if lg.name == adapterName { + lg.Destroy() + } else { + outputs = append(outputs, lg) + } + } + if len(outputs) == len(bl.outputs) { + return fmt.Errorf("logs: unknown adaptername %q (forgotten Register?)", adapterName) + } + bl.outputs = outputs + return nil +} + +func (bl *BeeLogger) writeToLoggers(when time.Time, msg string, level int) { + for _, l := range bl.outputs { + err := l.WriteMsg(when, msg, level) + if err != nil { + fmt.Fprintf(os.Stderr, "unable to WriteMsg to adapter:%v,error:%v\n", l.name, err) + } + } +} + +func (bl *BeeLogger) Write(p []byte) (n int, err error) { + if len(p) == 0 { + return 0, nil + } + // writeMsg will always add a '\n' character + if p[len(p)-1] == '\n' { + p = p[0 : len(p)-1] + } + // set levelLoggerImpl to ensure all log message will be write out + err = bl.writeMsg(levelLoggerImpl, string(p)) + if err == nil { + return len(p), err + } + return 0, err +} + +func (bl *BeeLogger) writeMsg(logLevel int, msg string, v ...interface{}) error { + if !bl.init { + bl.lock.Lock() + bl.setLogger(AdapterConsole) + bl.lock.Unlock() + } + + if len(v) > 0 { + msg = fmt.Sprintf(msg, v...) + } + when := time.Now() + if bl.enableFuncCallDepth { + _, file, line, ok := runtime.Caller(bl.loggerFuncCallDepth) + if !ok { + file = "???" + line = 0 + } + _, filename := path.Split(file) + msg = "[" + filename + ":" + strconv.Itoa(line) + "] " + msg + } + + //set level info in front of filename info + if logLevel == levelLoggerImpl { + // set to emergency to ensure all log will be print out correctly + logLevel = LevelEmergency + } else { + msg = levelPrefix[logLevel] + msg + } + + if bl.asynchronous { + lm := logMsgPool.Get().(*logMsg) + lm.level = logLevel + lm.msg = msg + lm.when = when + bl.msgChan <- lm + } else { + bl.writeToLoggers(when, msg, logLevel) + } + return nil +} + +// SetLevel Set log message level. +// If message level (such as LevelDebug) is higher than logger level (such as LevelWarning), +// log providers will not even be sent the message. +func (bl *BeeLogger) SetLevel(l int) { + bl.level = l +} + +// SetLogFuncCallDepth set log funcCallDepth +func (bl *BeeLogger) SetLogFuncCallDepth(d int) { + bl.loggerFuncCallDepth = d +} + +// GetLogFuncCallDepth return log funcCallDepth for wrapper +func (bl *BeeLogger) GetLogFuncCallDepth() int { + return bl.loggerFuncCallDepth +} + +// EnableFuncCallDepth enable log funcCallDepth +func (bl *BeeLogger) EnableFuncCallDepth(b bool) { + bl.enableFuncCallDepth = b +} + +// start logger chan reading. +// when chan is not empty, write logs. +func (bl *BeeLogger) startLogger() { + gameOver := false + for { + select { + case bm := <-bl.msgChan: + bl.writeToLoggers(bm.when, bm.msg, bm.level) + logMsgPool.Put(bm) + case sg := <-bl.signalChan: + // Now should only send "flush" or "close" to bl.signalChan + bl.flush() + if sg == "close" { + for _, l := range bl.outputs { + l.Destroy() + } + bl.outputs = nil + gameOver = true + } + bl.wg.Done() + } + if gameOver { + break + } + } +} + +// Emergency Log EMERGENCY level message. +func (bl *BeeLogger) Emergency(format string, v ...interface{}) { + if LevelEmergency > bl.level { + return + } + bl.writeMsg(LevelEmergency, format, v...) +} + +// Alert Log ALERT level message. +func (bl *BeeLogger) Alert(format string, v ...interface{}) { + if LevelAlert > bl.level { + return + } + bl.writeMsg(LevelAlert, format, v...) +} + +// Critical Log CRITICAL level message. +func (bl *BeeLogger) Critical(format string, v ...interface{}) { + if LevelCritical > bl.level { + return + } + bl.writeMsg(LevelCritical, format, v...) +} + +// Error Log ERROR level message. +func (bl *BeeLogger) Error(format string, v ...interface{}) { + if LevelError > bl.level { + return + } + bl.writeMsg(LevelError, format, v...) +} + +// Warning Log WARNING level message. +func (bl *BeeLogger) Warning(format string, v ...interface{}) { + if LevelWarn > bl.level { + return + } + bl.writeMsg(LevelWarn, format, v...) +} + +// Notice Log NOTICE level message. +func (bl *BeeLogger) Notice(format string, v ...interface{}) { + if LevelNotice > bl.level { + return + } + bl.writeMsg(LevelNotice, format, v...) +} + +// Informational Log INFORMATIONAL level message. +func (bl *BeeLogger) Informational(format string, v ...interface{}) { + if LevelInfo > bl.level { + return + } + bl.writeMsg(LevelInfo, format, v...) +} + +// Debug Log DEBUG level message. +func (bl *BeeLogger) Debug(format string, v ...interface{}) { + if LevelDebug > bl.level { + return + } + bl.writeMsg(LevelDebug, format, v...) +} + +// Warn Log WARN level message. +// compatibility alias for Warning() +func (bl *BeeLogger) Warn(format string, v ...interface{}) { + if LevelWarn > bl.level { + return + } + bl.writeMsg(LevelWarn, format, v...) +} + +// Info Log INFO level message. +// compatibility alias for Informational() +func (bl *BeeLogger) Info(format string, v ...interface{}) { + if LevelInfo > bl.level { + return + } + bl.writeMsg(LevelInfo, format, v...) +} + +// Trace Log TRACE level message. +// compatibility alias for Debug() +func (bl *BeeLogger) Trace(format string, v ...interface{}) { + if LevelDebug > bl.level { + return + } + bl.writeMsg(LevelDebug, format, v...) +} + +// Flush flush all chan data. +func (bl *BeeLogger) Flush() { + if bl.asynchronous { + bl.signalChan <- "flush" + bl.wg.Wait() + bl.wg.Add(1) + return + } + bl.flush() +} + +// Close close logger, flush all chan data and destroy all adapters in BeeLogger. +func (bl *BeeLogger) Close() { + if bl.asynchronous { + bl.signalChan <- "close" + bl.wg.Wait() + close(bl.msgChan) + } else { + bl.flush() + for _, l := range bl.outputs { + l.Destroy() + } + bl.outputs = nil + } + close(bl.signalChan) +} + +// Reset close all outputs, and set bl.outputs to nil +func (bl *BeeLogger) Reset() { + bl.Flush() + for _, l := range bl.outputs { + l.Destroy() + } + bl.outputs = nil +} + +func (bl *BeeLogger) flush() { + if bl.asynchronous { + for { + if len(bl.msgChan) > 0 { + bm := <-bl.msgChan + bl.writeToLoggers(bm.when, bm.msg, bm.level) + logMsgPool.Put(bm) + continue + } + break + } + } + for _, l := range bl.outputs { + l.Flush() + } +} + +// beeLogger references the used application logger. +var beeLogger = NewLogger() + +// GetBeeLogger returns the default BeeLogger +func GetBeeLogger() *BeeLogger { + return beeLogger +} + +var beeLoggerMap = struct { + sync.RWMutex + logs map[string]*log.Logger +}{ + logs: map[string]*log.Logger{}, +} + +// GetLogger returns the default BeeLogger +func GetLogger(prefixes ...string) *log.Logger { + prefix := append(prefixes, "")[0] + if prefix != "" { + prefix = fmt.Sprintf(`[%s] `, strings.ToUpper(prefix)) + } + beeLoggerMap.RLock() + l, ok := beeLoggerMap.logs[prefix] + if ok { + beeLoggerMap.RUnlock() + return l + } + beeLoggerMap.RUnlock() + beeLoggerMap.Lock() + defer beeLoggerMap.Unlock() + l, ok = beeLoggerMap.logs[prefix] + if !ok { + l = log.New(beeLogger, prefix, 0) + beeLoggerMap.logs[prefix] = l + } + return l +} + +// Reset will remove all the adapter +func Reset() { + beeLogger.Reset() +} + +// Async set the beelogger with Async mode and hold msglen messages +func Async(msgLen ...int64) *BeeLogger { + return beeLogger.Async(msgLen...) +} + +// SetLevel sets the global log level used by the simple logger. +func SetLevel(l int) { + beeLogger.SetLevel(l) +} + +// EnableFuncCallDepth enable log funcCallDepth +func EnableFuncCallDepth(b bool) { + beeLogger.enableFuncCallDepth = b +} + +// SetLogFuncCall set the CallDepth, default is 4 +func SetLogFuncCall(b bool) { + beeLogger.EnableFuncCallDepth(b) + beeLogger.SetLogFuncCallDepth(4) +} + +// SetLogFuncCallDepth set log funcCallDepth +func SetLogFuncCallDepth(d int) { + beeLogger.loggerFuncCallDepth = d +} + +// SetLogger sets a new logger. +func SetLogger(adapter string, config ...string) error { + return beeLogger.SetLogger(adapter, config...) +} + +// Emergency logs a message at emergency level. +func Emergency(f interface{}, v ...interface{}) { + beeLogger.Emergency(formatLog(f, v...)) +} + +// Alert logs a message at alert level. +func Alert(f interface{}, v ...interface{}) { + beeLogger.Alert(formatLog(f, v...)) +} + +// Critical logs a message at critical level. +func Critical(f interface{}, v ...interface{}) { + beeLogger.Critical(formatLog(f, v...)) +} + +// Error logs a message at error level. +func Error(f interface{}, v ...interface{}) { + beeLogger.Error(formatLog(f, v...)) +} + +// Warning logs a message at warning level. +func Warning(f interface{}, v ...interface{}) { + beeLogger.Warn(formatLog(f, v...)) +} + +// Warn compatibility alias for Warning() +func Warn(f interface{}, v ...interface{}) { + beeLogger.Warn(formatLog(f, v...)) +} + +// Notice logs a message at notice level. +func Notice(f interface{}, v ...interface{}) { + beeLogger.Notice(formatLog(f, v...)) +} + +// Informational logs a message at info level. +func Informational(f interface{}, v ...interface{}) { + beeLogger.Info(formatLog(f, v...)) +} + +// Info compatibility alias for Warning() +func Info(f interface{}, v ...interface{}) { + beeLogger.Info(formatLog(f, v...)) +} + +// Debug logs a message at debug level. +func Debug(f interface{}, v ...interface{}) { + beeLogger.Debug(formatLog(f, v...)) +} + +// Trace logs a message at trace level. +// compatibility alias for Warning() +func Trace(f interface{}, v ...interface{}) { + beeLogger.Trace(formatLog(f, v...)) +} + +func formatLog(f interface{}, v ...interface{}) string { + var msg string + switch f.(type) { + case string: + msg = f.(string) + if len(v) == 0 { + return msg + } + if strings.Contains(msg, "%") && !strings.Contains(msg, "%%") { + //format string + } else { + //do not contain format char + msg += strings.Repeat(" %v", len(v)) + } + default: + msg = fmt.Sprint(f) + if len(v) == 0 { + return msg + } + msg += strings.Repeat(" %v", len(v)) + } + return fmt.Sprintf(msg, v...) +} diff --git a/vendor/github.com/astaxie/beego/logs/logger.go b/vendor/github.com/astaxie/beego/logs/logger.go new file mode 100644 index 00000000..1700901f --- /dev/null +++ b/vendor/github.com/astaxie/beego/logs/logger.go @@ -0,0 +1,208 @@ +// Copyright 2014 beego Author. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package logs + +import ( + "fmt" + "io" + "os" + "sync" + "time" +) + +type logWriter struct { + sync.Mutex + writer io.Writer +} + +func newLogWriter(wr io.Writer) *logWriter { + return &logWriter{writer: wr} +} + +func (lg *logWriter) println(when time.Time, msg string) { + lg.Lock() + h, _ := formatTimeHeader(when) + lg.writer.Write(append(append(h, msg...), '\n')) + lg.Unlock() +} + +type outputMode int + +// DiscardNonColorEscSeq supports the divided color escape sequence. +// But non-color escape sequence is not output. +// Please use the OutputNonColorEscSeq If you want to output a non-color +// escape sequences such as ncurses. However, it does not support the divided +// color escape sequence. +const ( + _ outputMode = iota + DiscardNonColorEscSeq + OutputNonColorEscSeq +) + +// NewAnsiColorWriter creates and initializes a new ansiColorWriter +// using io.Writer w as its initial contents. +// In the console of Windows, which change the foreground and background +// colors of the text by the escape sequence. +// In the console of other systems, which writes to w all text. +func NewAnsiColorWriter(w io.Writer) io.Writer { + return NewModeAnsiColorWriter(w, DiscardNonColorEscSeq) +} + +// NewModeAnsiColorWriter create and initializes a new ansiColorWriter +// by specifying the outputMode. +func NewModeAnsiColorWriter(w io.Writer, mode outputMode) io.Writer { + if _, ok := w.(*ansiColorWriter); !ok { + return &ansiColorWriter{ + w: w, + mode: mode, + } + } + return w +} + +const ( + y1 = `0123456789` + y2 = `0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789` + y3 = `0000000000111111111122222222223333333333444444444455555555556666666666777777777788888888889999999999` + y4 = `0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789` + mo1 = `000000000111` + mo2 = `123456789012` + d1 = `0000000001111111111222222222233` + d2 = `1234567890123456789012345678901` + h1 = `000000000011111111112222` + h2 = `012345678901234567890123` + mi1 = `000000000011111111112222222222333333333344444444445555555555` + mi2 = `012345678901234567890123456789012345678901234567890123456789` + s1 = `000000000011111111112222222222333333333344444444445555555555` + s2 = `012345678901234567890123456789012345678901234567890123456789` + ns1 = `0123456789` +) + +func formatTimeHeader(when time.Time) ([]byte, int) { + y, mo, d := when.Date() + h, mi, s := when.Clock() + ns := when.Nanosecond()/1000000 + //len("2006/01/02 15:04:05.123 ")==24 + var buf [24]byte + + buf[0] = y1[y/1000%10] + buf[1] = y2[y/100] + buf[2] = y3[y-y/100*100] + buf[3] = y4[y-y/100*100] + buf[4] = '/' + buf[5] = mo1[mo-1] + buf[6] = mo2[mo-1] + buf[7] = '/' + buf[8] = d1[d-1] + buf[9] = d2[d-1] + buf[10] = ' ' + buf[11] = h1[h] + buf[12] = h2[h] + buf[13] = ':' + buf[14] = mi1[mi] + buf[15] = mi2[mi] + buf[16] = ':' + buf[17] = s1[s] + buf[18] = s2[s] + buf[19] = '.' + buf[20] = ns1[ns/100] + buf[21] = ns1[ns%100/10] + buf[22] = ns1[ns%10] + + buf[23] = ' ' + + return buf[0:], d +} + +var ( + green = string([]byte{27, 91, 57, 55, 59, 52, 50, 109}) + white = string([]byte{27, 91, 57, 48, 59, 52, 55, 109}) + yellow = string([]byte{27, 91, 57, 55, 59, 52, 51, 109}) + red = string([]byte{27, 91, 57, 55, 59, 52, 49, 109}) + blue = string([]byte{27, 91, 57, 55, 59, 52, 52, 109}) + magenta = string([]byte{27, 91, 57, 55, 59, 52, 53, 109}) + cyan = string([]byte{27, 91, 57, 55, 59, 52, 54, 109}) + + w32Green = string([]byte{27, 91, 52, 50, 109}) + w32White = string([]byte{27, 91, 52, 55, 109}) + w32Yellow = string([]byte{27, 91, 52, 51, 109}) + w32Red = string([]byte{27, 91, 52, 49, 109}) + w32Blue = string([]byte{27, 91, 52, 52, 109}) + w32Magenta = string([]byte{27, 91, 52, 53, 109}) + w32Cyan = string([]byte{27, 91, 52, 54, 109}) + + reset = string([]byte{27, 91, 48, 109}) +) + +// ColorByStatus return color by http code +// 2xx return Green +// 3xx return White +// 4xx return Yellow +// 5xx return Red +func ColorByStatus(cond bool, code int) string { + switch { + case code >= 200 && code < 300: + return map[bool]string{true: green, false: w32Green}[cond] + case code >= 300 && code < 400: + return map[bool]string{true: white, false: w32White}[cond] + case code >= 400 && code < 500: + return map[bool]string{true: yellow, false: w32Yellow}[cond] + default: + return map[bool]string{true: red, false: w32Red}[cond] + } +} + +// ColorByMethod return color by http code +// GET return Blue +// POST return Cyan +// PUT return Yellow +// DELETE return Red +// PATCH return Green +// HEAD return Magenta +// OPTIONS return WHITE +func ColorByMethod(cond bool, method string) string { + switch method { + case "GET": + return map[bool]string{true: blue, false: w32Blue}[cond] + case "POST": + return map[bool]string{true: cyan, false: w32Cyan}[cond] + case "PUT": + return map[bool]string{true: yellow, false: w32Yellow}[cond] + case "DELETE": + return map[bool]string{true: red, false: w32Red}[cond] + case "PATCH": + return map[bool]string{true: green, false: w32Green}[cond] + case "HEAD": + return map[bool]string{true: magenta, false: w32Magenta}[cond] + case "OPTIONS": + return map[bool]string{true: white, false: w32White}[cond] + default: + return reset + } +} + +// Guard Mutex to guarantee atomic of W32Debug(string) function +var mu sync.Mutex + +// W32Debug Helper method to output colored logs in Windows terminals +func W32Debug(msg string) { + mu.Lock() + defer mu.Unlock() + + current := time.Now() + w := NewAnsiColorWriter(os.Stdout) + + fmt.Fprintf(w, "[beego] %v %s\n", current.Format("2006/01/02 - 15:04:05"), msg) +} diff --git a/vendor/github.com/astaxie/beego/logs/multifile.go b/vendor/github.com/astaxie/beego/logs/multifile.go new file mode 100644 index 00000000..63204e17 --- /dev/null +++ b/vendor/github.com/astaxie/beego/logs/multifile.go @@ -0,0 +1,116 @@ +// Copyright 2014 beego Author. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package logs + +import ( + "encoding/json" + "time" +) + +// A filesLogWriter manages several fileLogWriter +// filesLogWriter will write logs to the file in json configuration and write the same level log to correspond file +// means if the file name in configuration is project.log filesLogWriter will create project.error.log/project.debug.log +// and write the error-level logs to project.error.log and write the debug-level logs to project.debug.log +// the rotate attribute also acts like fileLogWriter +type multiFileLogWriter struct { + writers [LevelDebug + 1 + 1]*fileLogWriter // the last one for fullLogWriter + fullLogWriter *fileLogWriter + Separate []string `json:"separate"` +} + +var levelNames = [...]string{"emergency", "alert", "critical", "error", "warning", "notice", "info", "debug"} + +// Init file logger with json config. +// jsonConfig like: +// { +// "filename":"logs/beego.log", +// "maxLines":0, +// "maxsize":0, +// "daily":true, +// "maxDays":15, +// "rotate":true, +// "perm":0600, +// "separate":["emergency", "alert", "critical", "error", "warning", "notice", "info", "debug"], +// } + +func (f *multiFileLogWriter) Init(config string) error { + writer := newFileWriter().(*fileLogWriter) + err := writer.Init(config) + if err != nil { + return err + } + f.fullLogWriter = writer + f.writers[LevelDebug+1] = writer + + //unmarshal "separate" field to f.Separate + json.Unmarshal([]byte(config), f) + + jsonMap := map[string]interface{}{} + json.Unmarshal([]byte(config), &jsonMap) + + for i := LevelEmergency; i < LevelDebug+1; i++ { + for _, v := range f.Separate { + if v == levelNames[i] { + jsonMap["filename"] = f.fullLogWriter.fileNameOnly + "." + levelNames[i] + f.fullLogWriter.suffix + jsonMap["level"] = i + bs, _ := json.Marshal(jsonMap) + writer = newFileWriter().(*fileLogWriter) + writer.Init(string(bs)) + f.writers[i] = writer + } + } + } + + return nil +} + +func (f *multiFileLogWriter) Destroy() { + for i := 0; i < len(f.writers); i++ { + if f.writers[i] != nil { + f.writers[i].Destroy() + } + } +} + +func (f *multiFileLogWriter) WriteMsg(when time.Time, msg string, level int) error { + if f.fullLogWriter != nil { + f.fullLogWriter.WriteMsg(when, msg, level) + } + for i := 0; i < len(f.writers)-1; i++ { + if f.writers[i] != nil { + if level == f.writers[i].Level { + f.writers[i].WriteMsg(when, msg, level) + } + } + } + return nil +} + +func (f *multiFileLogWriter) Flush() { + for i := 0; i < len(f.writers); i++ { + if f.writers[i] != nil { + f.writers[i].Flush() + } + } +} + +// newFilesWriter create a FileLogWriter returning as LoggerInterface. +func newFilesWriter() Logger { + return &multiFileLogWriter{} +} + +func init() { + Register(AdapterMultiFile, newFilesWriter) +} diff --git a/vendor/github.com/astaxie/beego/logs/slack.go b/vendor/github.com/astaxie/beego/logs/slack.go new file mode 100644 index 00000000..1cd2e5ae --- /dev/null +++ b/vendor/github.com/astaxie/beego/logs/slack.go @@ -0,0 +1,60 @@ +package logs + +import ( + "encoding/json" + "fmt" + "net/http" + "net/url" + "time" +) + +// SLACKWriter implements beego LoggerInterface and is used to send jiaoliao webhook +type SLACKWriter struct { + WebhookURL string `json:"webhookurl"` + Level int `json:"level"` +} + +// newSLACKWriter create jiaoliao writer. +func newSLACKWriter() Logger { + return &SLACKWriter{Level: LevelTrace} +} + +// Init SLACKWriter with json config string +func (s *SLACKWriter) Init(jsonconfig string) error { + return json.Unmarshal([]byte(jsonconfig), s) +} + +// WriteMsg write message in smtp writer. +// it will send an email with subject and only this message. +func (s *SLACKWriter) WriteMsg(when time.Time, msg string, level int) error { + if level > s.Level { + return nil + } + + text := fmt.Sprintf("{\"text\": \"%s %s\"}", when.Format("2006-01-02 15:04:05"), msg) + + form := url.Values{} + form.Add("payload", text) + + resp, err := http.PostForm(s.WebhookURL, form) + if err != nil { + return err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("Post webhook failed %s %d", resp.Status, resp.StatusCode) + } + return nil +} + +// Flush implementing method. empty. +func (s *SLACKWriter) Flush() { +} + +// Destroy implementing method. empty. +func (s *SLACKWriter) Destroy() { +} + +func init() { + Register(AdapterSlack, newSLACKWriter) +} diff --git a/vendor/github.com/astaxie/beego/logs/smtp.go b/vendor/github.com/astaxie/beego/logs/smtp.go new file mode 100644 index 00000000..6208d7b8 --- /dev/null +++ b/vendor/github.com/astaxie/beego/logs/smtp.go @@ -0,0 +1,149 @@ +// Copyright 2014 beego Author. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package logs + +import ( + "crypto/tls" + "encoding/json" + "fmt" + "net" + "net/smtp" + "strings" + "time" +) + +// SMTPWriter implements LoggerInterface and is used to send emails via given SMTP-server. +type SMTPWriter struct { + Username string `json:"username"` + Password string `json:"password"` + Host string `json:"host"` + Subject string `json:"subject"` + FromAddress string `json:"fromAddress"` + RecipientAddresses []string `json:"sendTos"` + Level int `json:"level"` +} + +// NewSMTPWriter create smtp writer. +func newSMTPWriter() Logger { + return &SMTPWriter{Level: LevelTrace} +} + +// Init smtp writer with json config. +// config like: +// { +// "username":"example@gmail.com", +// "password:"password", +// "host":"smtp.gmail.com:465", +// "subject":"email title", +// "fromAddress":"from@example.com", +// "sendTos":["email1","email2"], +// "level":LevelError +// } +func (s *SMTPWriter) Init(jsonconfig string) error { + return json.Unmarshal([]byte(jsonconfig), s) +} + +func (s *SMTPWriter) getSMTPAuth(host string) smtp.Auth { + if len(strings.Trim(s.Username, " ")) == 0 && len(strings.Trim(s.Password, " ")) == 0 { + return nil + } + return smtp.PlainAuth( + "", + s.Username, + s.Password, + host, + ) +} + +func (s *SMTPWriter) sendMail(hostAddressWithPort string, auth smtp.Auth, fromAddress string, recipients []string, msgContent []byte) error { + client, err := smtp.Dial(hostAddressWithPort) + if err != nil { + return err + } + + host, _, _ := net.SplitHostPort(hostAddressWithPort) + tlsConn := &tls.Config{ + InsecureSkipVerify: true, + ServerName: host, + } + if err = client.StartTLS(tlsConn); err != nil { + return err + } + + if auth != nil { + if err = client.Auth(auth); err != nil { + return err + } + } + + if err = client.Mail(fromAddress); err != nil { + return err + } + + for _, rec := range recipients { + if err = client.Rcpt(rec); err != nil { + return err + } + } + + w, err := client.Data() + if err != nil { + return err + } + _, err = w.Write(msgContent) + if err != nil { + return err + } + + err = w.Close() + if err != nil { + return err + } + + return client.Quit() +} + +// WriteMsg write message in smtp writer. +// it will send an email with subject and only this message. +func (s *SMTPWriter) WriteMsg(when time.Time, msg string, level int) error { + if level > s.Level { + return nil + } + + hp := strings.Split(s.Host, ":") + + // Set up authentication information. + auth := s.getSMTPAuth(hp[0]) + + // Connect to the server, authenticate, set the sender and recipient, + // and send the email all in one step. + contentType := "Content-Type: text/plain" + "; charset=UTF-8" + mailmsg := []byte("To: " + strings.Join(s.RecipientAddresses, ";") + "\r\nFrom: " + s.FromAddress + "<" + s.FromAddress + + ">\r\nSubject: " + s.Subject + "\r\n" + contentType + "\r\n\r\n" + fmt.Sprintf(".%s", when.Format("2006-01-02 15:04:05")) + msg) + + return s.sendMail(s.Host, auth, s.FromAddress, s.RecipientAddresses, mailmsg) +} + +// Flush implementing method. empty. +func (s *SMTPWriter) Flush() { +} + +// Destroy implementing method. empty. +func (s *SMTPWriter) Destroy() { +} + +func init() { + Register(AdapterMail, newSMTPWriter) +} diff --git a/vendor/github.com/dchest/uniuri/README.md b/vendor/github.com/dchest/uniuri/README.md new file mode 100644 index 00000000..b321a5fa --- /dev/null +++ b/vendor/github.com/dchest/uniuri/README.md @@ -0,0 +1,97 @@ +Package uniuri +===================== + +[![Build Status](https://travis-ci.org/dchest/uniuri.svg)](https://travis-ci.org/dchest/uniuri) + +```go +import "github.com/dchest/uniuri" +``` + +Package uniuri generates random strings good for use in URIs to identify +unique objects. + +Example usage: + +```go +s := uniuri.New() // s is now "apHCJBl7L1OmC57n" +``` + +A standard string created by New() is 16 bytes in length and consists of +Latin upper and lowercase letters, and numbers (from the set of 62 allowed +characters), which means that it has ~95 bits of entropy. To get more +entropy, you can use NewLen(UUIDLen), which returns 20-byte string, giving +~119 bits of entropy, or any other desired length. + +Functions read from crypto/rand random source, and panic if they fail to +read from it. + + +Constants +--------- + +```go +const ( + // StdLen is a standard length of uniuri string to achive ~95 bits of entropy. + StdLen = 16 + // UUIDLen is a length of uniuri string to achive ~119 bits of entropy, closest + // to what can be losslessly converted to UUIDv4 (122 bits). + UUIDLen = 20 +) + +``` + + + +Variables +--------- + +```go +var StdChars = []byte("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789") +``` + + +StdChars is a set of standard characters allowed in uniuri string. + + +Functions +--------- + +### func New + +```go +func New() string +``` + +New returns a new random string of the standard length, consisting of +standard characters. + +### func NewLen + +```go +func NewLen(length int) string +``` + +NewLen returns a new random string of the provided length, consisting of +standard characters. + +### func NewLenChars + +```go +func NewLenChars(length int, chars []byte) string +``` + +NewLenChars returns a new random string of the provided length, consisting +of the provided byte slice of allowed characters (maximum 256). + + + +Public domain dedication +------------------------ + +Written in 2011-2014 by Dmitry Chestnykh + +The author(s) have dedicated all copyright and related and +neighboring rights to this software to the public domain +worldwide. Distributed without any warranty. +http://creativecommons.org/publicdomain/zero/1.0/ + diff --git a/vendor/github.com/dchest/uniuri/uniuri.go b/vendor/github.com/dchest/uniuri/uniuri.go new file mode 100644 index 00000000..6393446c --- /dev/null +++ b/vendor/github.com/dchest/uniuri/uniuri.go @@ -0,0 +1,81 @@ +// Written in 2011-2014 by Dmitry Chestnykh +// +// The author(s) have dedicated all copyright and related and +// neighboring rights to this software to the public domain +// worldwide. Distributed without any warranty. +// http://creativecommons.org/publicdomain/zero/1.0/ + +// Package uniuri generates random strings good for use in URIs to identify +// unique objects. +// +// Example usage: +// +// s := uniuri.New() // s is now "apHCJBl7L1OmC57n" +// +// A standard string created by New() is 16 bytes in length and consists of +// Latin upper and lowercase letters, and numbers (from the set of 62 allowed +// characters), which means that it has ~95 bits of entropy. To get more +// entropy, you can use NewLen(UUIDLen), which returns 20-byte string, giving +// ~119 bits of entropy, or any other desired length. +// +// Functions read from crypto/rand random source, and panic if they fail to +// read from it. +package uniuri + +import "crypto/rand" + +const ( + // StdLen is a standard length of uniuri string to achive ~95 bits of entropy. + StdLen = 16 + // UUIDLen is a length of uniuri string to achive ~119 bits of entropy, closest + // to what can be losslessly converted to UUIDv4 (122 bits). + UUIDLen = 20 +) + +// StdChars is a set of standard characters allowed in uniuri string. +var StdChars = []byte("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789") + +// New returns a new random string of the standard length, consisting of +// standard characters. +func New() string { + return NewLenChars(StdLen, StdChars) +} + +// NewLen returns a new random string of the provided length, consisting of +// standard characters. +func NewLen(length int) string { + return NewLenChars(length, StdChars) +} + +// NewLenChars returns a new random string of the provided length, consisting +// of the provided byte slice of allowed characters (maximum 256). +func NewLenChars(length int, chars []byte) string { + if length == 0 { + return "" + } + clen := len(chars) + if clen < 2 || clen > 256 { + panic("uniuri: wrong charset length for NewLenChars") + } + maxrb := 255 - (256 % clen) + b := make([]byte, length) + r := make([]byte, length+(length/4)) // storage for random bytes. + i := 0 + for { + if _, err := rand.Read(r); err != nil { + panic("uniuri: error reading random bytes: " + err.Error()) + } + for _, rb := range r { + c := int(rb) + if c > maxrb { + // Skip this number to avoid modulo bias. + continue + } + b[i] = chars[c%clen] + i++ + if i == length { + return string(b) + } + } + } +} diff --git a/vendor/github.com/gedex/inflector/CakePHP_LICENSE.txt b/vendor/github.com/gedex/inflector/CakePHP_LICENSE.txt new file mode 100644 index 00000000..414ab1e7 --- /dev/null +++ b/vendor/github.com/gedex/inflector/CakePHP_LICENSE.txt @@ -0,0 +1,28 @@ +The MIT License + +CakePHP(tm) : The Rapid Development PHP Framework (http://cakephp.org) +Copyright (c) 2005-2013, Cake Software Foundation, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. + +Cake Software Foundation, Inc. +1785 E. Sahara Avenue, +Suite 490-204 +Las Vegas, Nevada 89104, +United States of America. \ No newline at end of file diff --git a/vendor/github.com/gedex/inflector/LICENSE.md b/vendor/github.com/gedex/inflector/LICENSE.md new file mode 100644 index 00000000..cdbc2176 --- /dev/null +++ b/vendor/github.com/gedex/inflector/LICENSE.md @@ -0,0 +1,29 @@ +Copyright (c) 2013 Akeda Bagus . All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +---------- + +Much of this library was inspired from CakePHP's inflector, a PHP +framework licensed under MIT license (see CakePHP_LICENSE.txt). diff --git a/vendor/github.com/gedex/inflector/README.md b/vendor/github.com/gedex/inflector/README.md new file mode 100644 index 00000000..45c7b266 --- /dev/null +++ b/vendor/github.com/gedex/inflector/README.md @@ -0,0 +1,25 @@ +Inflector +========= + +Inflector pluralizes and singularizes English nouns. + +[![Build Status](https://travis-ci.org/gedex/inflector.png?branch=master)](https://travis-ci.org/gedex/inflector) +[![Coverage Status](https://coveralls.io/repos/gedex/inflector/badge.png?branch=master)](https://coveralls.io/r/gedex/inflector?branch=master) +[![GoDoc](https://godoc.org/github.com/gedex/inflector?status.svg)](https://godoc.org/github.com/gedex/inflector) + +## Basic Usage + +There are only two exported functions: `Pluralize` and `Singularize`. + +~~~go +fmt.Println(inflector.Singularize("People")) // will print "Person" +fmt.Println(inflector.Pluralize("octopus")) // will print "octopuses" +~~~ + +## Credits + +* [CakePHP's Inflector](https://github.com/cakephp/cakephp/blob/master/lib/Cake/Utility/Inflector.php) + +## License + +This library is distributed under the BSD-style license found in the LICENSE.md file. diff --git a/vendor/github.com/gedex/inflector/inflector.go b/vendor/github.com/gedex/inflector/inflector.go new file mode 100644 index 00000000..319f936c --- /dev/null +++ b/vendor/github.com/gedex/inflector/inflector.go @@ -0,0 +1,355 @@ +// Copyright 2013 Akeda Bagus . All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +/* +Package inflector pluralizes and singularizes English nouns. + +There are only two exported functions: `Pluralize` and `Singularize`. + + s := "People" + fmt.Println(inflector.Singularize(s)) // will print "Person" + + s2 := "octopus" + fmt.Println(inflector.Pluralize(s2)) // will print "octopuses" + +*/ +package inflector + +import ( + "bytes" + "fmt" + "regexp" + "strings" + "sync" +) + +// Rule represents name of the inflector rule, can be +// Plural or Singular +type Rule int + +const ( + Plural = iota + Singular +) + +// InflectorRule represents inflector rule +type InflectorRule struct { + Rules []*ruleItem + Irregular []*irregularItem + Uninflected []string + compiledIrregular *regexp.Regexp + compiledUninflected *regexp.Regexp + compiledRules []*compiledRule +} + +type ruleItem struct { + pattern string + replacement string +} + +type irregularItem struct { + word string + replacement string +} + +// compiledRule represents compiled version of Inflector.Rules. +type compiledRule struct { + replacement string + *regexp.Regexp +} + +// threadsafe access to rules and caches +var mutex sync.Mutex +var rules = make(map[Rule]*InflectorRule) + +// Words that should not be inflected +var uninflected = []string{ + `Amoyese`, `bison`, `Borghese`, `bream`, `breeches`, `britches`, `buffalo`, + `cantus`, `carp`, `chassis`, `clippers`, `cod`, `coitus`, `Congoese`, + `contretemps`, `corps`, `debris`, `diabetes`, `djinn`, `eland`, `elk`, + `equipment`, `Faroese`, `flounder`, `Foochowese`, `gallows`, `Genevese`, + `Genoese`, `Gilbertese`, `graffiti`, `headquarters`, `herpes`, `hijinks`, + `Hottentotese`, `information`, `innings`, `jackanapes`, `Kiplingese`, + `Kongoese`, `Lucchese`, `mackerel`, `Maltese`, `.*?media`, `mews`, `moose`, + `mumps`, `Nankingese`, `news`, `nexus`, `Niasese`, `Pekingese`, + `Piedmontese`, `pincers`, `Pistoiese`, `pliers`, `Portuguese`, `proceedings`, + `rabies`, `rice`, `rhinoceros`, `salmon`, `Sarawakese`, `scissors`, + `sea[- ]bass`, `series`, `Shavese`, `shears`, `siemens`, `species`, `swine`, + `testes`, `trousers`, `trout`, `tuna`, `Vermontese`, `Wenchowese`, `whiting`, + `wildebeest`, `Yengeese`, +} + +// Plural words that should not be inflected +var uninflectedPlurals = []string{ + `.*[nrlm]ese`, `.*deer`, `.*fish`, `.*measles`, `.*ois`, `.*pox`, `.*sheep`, + `people`, +} + +// Singular words that should not be inflected +var uninflectedSingulars = []string{ + `.*[nrlm]ese`, `.*deer`, `.*fish`, `.*measles`, `.*ois`, `.*pox`, `.*sheep`, + `.*ss`, +} + +type cache map[string]string + +// Inflected words that already cached for immediate retrieval from a given Rule +var caches = make(map[Rule]cache) + +// map of irregular words where its key is a word and its value is the replacement +var irregularMaps = make(map[Rule]cache) + +func init() { + + rules[Plural] = &InflectorRule{ + Rules: []*ruleItem{ + {`(?i)(s)tatus$`, `${1}${2}tatuses`}, + {`(?i)(quiz)$`, `${1}zes`}, + {`(?i)^(ox)$`, `${1}${2}en`}, + {`(?i)([m|l])ouse$`, `${1}ice`}, + {`(?i)(matr|vert|ind)(ix|ex)$`, `${1}ices`}, + {`(?i)(x|ch|ss|sh)$`, `${1}es`}, + {`(?i)([^aeiouy]|qu)y$`, `${1}ies`}, + {`(?i)(hive)$`, `$1s`}, + {`(?i)(?:([^f])fe|([lre])f)$`, `${1}${2}ves`}, + {`(?i)sis$`, `ses`}, + {`(?i)([ti])um$`, `${1}a`}, + {`(?i)(p)erson$`, `${1}eople`}, + {`(?i)(m)an$`, `${1}en`}, + {`(?i)(c)hild$`, `${1}hildren`}, + {`(?i)(buffal|tomat)o$`, `${1}${2}oes`}, + {`(?i)(alumn|bacill|cact|foc|fung|nucle|radi|stimul|syllab|termin|vir)us$`, `${1}i`}, + {`(?i)us$`, `uses`}, + {`(?i)(alias)$`, `${1}es`}, + {`(?i)(ax|cris|test)is$`, `${1}es`}, + {`s$`, `s`}, + {`^$`, ``}, + {`$`, `s`}, + }, + Irregular: []*irregularItem{ + {`atlas`, `atlases`}, + {`beef`, `beefs`}, + {`brother`, `brothers`}, + {`cafe`, `cafes`}, + {`child`, `children`}, + {`cookie`, `cookies`}, + {`corpus`, `corpuses`}, + {`cow`, `cows`}, + {`ganglion`, `ganglions`}, + {`genie`, `genies`}, + {`genus`, `genera`}, + {`graffito`, `graffiti`}, + {`hoof`, `hoofs`}, + {`loaf`, `loaves`}, + {`man`, `men`}, + {`money`, `monies`}, + {`mongoose`, `mongooses`}, + {`move`, `moves`}, + {`mythos`, `mythoi`}, + {`niche`, `niches`}, + {`numen`, `numina`}, + {`occiput`, `occiputs`}, + {`octopus`, `octopuses`}, + {`opus`, `opuses`}, + {`ox`, `oxen`}, + {`penis`, `penises`}, + {`person`, `people`}, + {`sex`, `sexes`}, + {`soliloquy`, `soliloquies`}, + {`testis`, `testes`}, + {`trilby`, `trilbys`}, + {`turf`, `turfs`}, + {`potato`, `potatoes`}, + {`hero`, `heroes`}, + {`tooth`, `teeth`}, + {`goose`, `geese`}, + {`foot`, `feet`}, + }, + } + prepare(Plural) + + rules[Singular] = &InflectorRule{ + Rules: []*ruleItem{ + {`(?i)(s)tatuses$`, `${1}${2}tatus`}, + {`(?i)^(.*)(menu)s$`, `${1}${2}`}, + {`(?i)(quiz)zes$`, `$1`}, + {`(?i)(matr)ices$`, `${1}ix`}, + {`(?i)(vert|ind)ices$`, `${1}ex`}, + {`(?i)^(ox)en`, `$1`}, + {`(?i)(alias)(es)*$`, `$1`}, + {`(?i)(alumn|bacill|cact|foc|fung|nucle|radi|stimul|syllab|termin|viri?)i$`, `${1}us`}, + {`(?i)([ftw]ax)es`, `$1`}, + {`(?i)(cris|ax|test)es$`, `${1}is`}, + {`(?i)(shoe|slave)s$`, `$1`}, + {`(?i)(o)es$`, `$1`}, + {`ouses$`, `ouse`}, + {`([^a])uses$`, `${1}us`}, + {`(?i)([m|l])ice$`, `${1}ouse`}, + {`(?i)(x|ch|ss|sh)es$`, `$1`}, + {`(?i)(m)ovies$`, `${1}${2}ovie`}, + {`(?i)(s)eries$`, `${1}${2}eries`}, + {`(?i)([^aeiouy]|qu)ies$`, `${1}y`}, + {`(?i)(tive)s$`, `$1`}, + {`(?i)([lre])ves$`, `${1}f`}, + {`(?i)([^fo])ves$`, `${1}fe`}, + {`(?i)(hive)s$`, `$1`}, + {`(?i)(drive)s$`, `$1`}, + {`(?i)(^analy)ses$`, `${1}sis`}, + {`(?i)(analy|diagno|^ba|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$`, `${1}${2}sis`}, + {`(?i)([ti])a$`, `${1}um`}, + {`(?i)(p)eople$`, `${1}${2}erson`}, + {`(?i)(m)en$`, `${1}an`}, + {`(?i)(c)hildren$`, `${1}${2}hild`}, + {`(?i)(n)ews$`, `${1}${2}ews`}, + {`eaus$`, `eau`}, + {`^(.*us)$`, `$1`}, + {`(?i)s$`, ``}, + }, + Irregular: []*irregularItem{ + {`foes`, `foe`}, + {`waves`, `wave`}, + {`curves`, `curve`}, + {`atlases`, `atlas`}, + {`beefs`, `beef`}, + {`brothers`, `brother`}, + {`cafes`, `cafe`}, + {`children`, `child`}, + {`cookies`, `cookie`}, + {`corpuses`, `corpus`}, + {`cows`, `cow`}, + {`ganglions`, `ganglion`}, + {`genies`, `genie`}, + {`genera`, `genus`}, + {`graffiti`, `graffito`}, + {`hoofs`, `hoof`}, + {`loaves`, `loaf`}, + {`men`, `man`}, + {`monies`, `money`}, + {`mongooses`, `mongoose`}, + {`moves`, `move`}, + {`mythoi`, `mythos`}, + {`niches`, `niche`}, + {`numina`, `numen`}, + {`occiputs`, `occiput`}, + {`octopuses`, `octopus`}, + {`opuses`, `opus`}, + {`oxen`, `ox`}, + {`penises`, `penis`}, + {`people`, `person`}, + {`sexes`, `sex`}, + {`soliloquies`, `soliloquy`}, + {`testes`, `testis`}, + {`trilbys`, `trilby`}, + {`turfs`, `turf`}, + {`potatoes`, `potato`}, + {`heroes`, `hero`}, + {`teeth`, `tooth`}, + {`geese`, `goose`}, + {`feet`, `foot`}, + }, + } + prepare(Singular) +} + +// prepare rule, e.g., compile the pattern. +func prepare(r Rule) error { + var reString string + + switch r { + case Plural: + // Merge global uninflected with singularsUninflected + rules[r].Uninflected = merge(uninflected, uninflectedPlurals) + case Singular: + // Merge global uninflected with singularsUninflected + rules[r].Uninflected = merge(uninflected, uninflectedSingulars) + } + + // Set InflectorRule.compiledUninflected by joining InflectorRule.Uninflected into + // a single string then compile it. + reString = fmt.Sprintf(`(?i)(^(?:%s))$`, strings.Join(rules[r].Uninflected, `|`)) + rules[r].compiledUninflected = regexp.MustCompile(reString) + + // Prepare irregularMaps + irregularMaps[r] = make(cache, len(rules[r].Irregular)) + + // Set InflectorRule.compiledIrregular by joining the irregularItem.word of Inflector.Irregular + // into a single string then compile it. + vIrregulars := make([]string, len(rules[r].Irregular)) + for i, item := range rules[r].Irregular { + vIrregulars[i] = item.word + irregularMaps[r][item.word] = item.replacement + } + reString = fmt.Sprintf(`(?i)(.*)\b((?:%s))$`, strings.Join(vIrregulars, `|`)) + rules[r].compiledIrregular = regexp.MustCompile(reString) + + // Compile all patterns in InflectorRule.Rules + rules[r].compiledRules = make([]*compiledRule, len(rules[r].Rules)) + for i, item := range rules[r].Rules { + rules[r].compiledRules[i] = &compiledRule{item.replacement, regexp.MustCompile(item.pattern)} + } + + // Prepare caches + caches[r] = make(cache) + + return nil +} + +// merge slice a and slice b +func merge(a []string, b []string) []string { + result := make([]string, len(a)+len(b)) + copy(result, a) + copy(result[len(a):], b) + + return result +} + +// Pluralize returns string s in plural form. +func Pluralize(s string) string { + return getInflected(Plural, s) +} + +// Singularize returns string s in singular form. +func Singularize(s string) string { + return getInflected(Singular, s) +} + +func getInflected(r Rule, s string) string { + mutex.Lock() + defer mutex.Unlock() + if v, ok := caches[r][s]; ok { + return v + } + + // Check for irregular words + if res := rules[r].compiledIrregular.FindStringSubmatch(s); len(res) >= 3 { + var buf bytes.Buffer + + buf.WriteString(res[1]) + buf.WriteString(s[0:1]) + buf.WriteString(irregularMaps[r][strings.ToLower(res[2])][1:]) + + // Cache it then returns + caches[r][s] = buf.String() + return caches[r][s] + } + + // Check for uninflected words + if rules[r].compiledUninflected.MatchString(s) { + caches[r][s] = s + return caches[r][s] + } + + // Check each rule + for _, re := range rules[r].compiledRules { + if re.MatchString(s) { + caches[r][s] = re.ReplaceAllString(s, re.replacement) + return caches[r][s] + } + } + + // Returns unaltered + caches[r][s] = s + return caches[r][s] +} diff --git a/vendor/github.com/golang/glog/LICENSE b/vendor/github.com/golang/glog/LICENSE new file mode 100644 index 00000000..37ec93a1 --- /dev/null +++ b/vendor/github.com/golang/glog/LICENSE @@ -0,0 +1,191 @@ +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and +distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright +owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities +that control, are controlled by, or are under common control with that entity. +For the purposes of this definition, "control" means (i) the power, direct or +indirect, to cause the direction or management of such entity, whether by +contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the +outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising +permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including +but not limited to software source code, documentation source, and configuration +files. + +"Object" form shall mean any form resulting from mechanical transformation or +translation of a Source form, including but not limited to compiled object code, +generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made +available under the License, as indicated by a copyright notice that is included +in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that +is based on (or derived from) the Work and for which the editorial revisions, +annotations, elaborations, or other modifications represent, as a whole, an +original work of authorship. For the purposes of this License, Derivative Works +shall not include works that remain separable from, or merely link (or bind by +name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version +of the Work and any modifications or additions to that Work or Derivative Works +thereof, that is intentionally submitted to Licensor for inclusion in the Work +by the copyright owner or by an individual or Legal Entity authorized to submit +on behalf of the copyright owner. For the purposes of this definition, +"submitted" means any form of electronic, verbal, or written communication sent +to the Licensor or its representatives, including but not limited to +communication on electronic mailing lists, source code control systems, and +issue tracking systems that are managed by, or on behalf of, the Licensor for +the purpose of discussing and improving the Work, but excluding communication +that is conspicuously marked or otherwise designated in writing by the copyright +owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf +of whom a Contribution has been received by Licensor and subsequently +incorporated within the Work. + +2. Grant of Copyright License. + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable copyright license to reproduce, prepare Derivative Works of, +publicly display, publicly perform, sublicense, and distribute the Work and such +Derivative Works in Source or Object form. + +3. Grant of Patent License. + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable (except as stated in this section) patent license to make, have +made, use, offer to sell, sell, import, and otherwise transfer the Work, where +such license applies only to those patent claims licensable by such Contributor +that are necessarily infringed by their Contribution(s) alone or by combination +of their Contribution(s) with the Work to which such Contribution(s) was +submitted. If You institute patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the Work or a +Contribution incorporated within the Work constitutes direct or contributory +patent infringement, then any patent licenses granted to You under this License +for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. + +You may reproduce and distribute copies of the Work or Derivative Works thereof +in any medium, with or without modifications, and in Source or Object form, +provided that You meet the following conditions: + +You must give any other recipients of the Work or Derivative Works a copy of +this License; and +You must cause any modified files to carry prominent notices stating that You +changed the files; and +You must retain, in the Source form of any Derivative Works that You distribute, +all copyright, patent, trademark, and attribution notices from the Source form +of the Work, excluding those notices that do not pertain to any part of the +Derivative Works; and +If the Work includes a "NOTICE" text file as part of its distribution, then any +Derivative Works that You distribute must include a readable copy of the +attribution notices contained within such NOTICE file, excluding those notices +that do not pertain to any part of the Derivative Works, in at least one of the +following places: within a NOTICE text file distributed as part of the +Derivative Works; within the Source form or documentation, if provided along +with the Derivative Works; or, within a display generated by the Derivative +Works, if and wherever such third-party notices normally appear. The contents of +the NOTICE file are for informational purposes only and do not modify the +License. You may add Your own attribution notices within Derivative Works that +You distribute, alongside or as an addendum to the NOTICE text from the Work, +provided that such additional attribution notices cannot be construed as +modifying the License. +You may add Your own copyright statement to Your modifications and may provide +additional or different license terms and conditions for use, reproduction, or +distribution of Your modifications, or for any such Derivative Works as a whole, +provided Your use, reproduction, and distribution of the Work otherwise complies +with the conditions stated in this License. + +5. Submission of Contributions. + +Unless You explicitly state otherwise, any Contribution intentionally submitted +for inclusion in the Work by You to the Licensor shall be under the terms and +conditions of this License, without any additional terms or conditions. +Notwithstanding the above, nothing herein shall supersede or modify the terms of +any separate license agreement you may have executed with Licensor regarding +such Contributions. + +6. Trademarks. + +This License does not grant permission to use the trade names, trademarks, +service marks, or product names of the Licensor, except as required for +reasonable and customary use in describing the origin of the Work and +reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. + +Unless required by applicable law or agreed to in writing, Licensor provides the +Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, +including, without limitation, any warranties or conditions of TITLE, +NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are +solely responsible for determining the appropriateness of using or +redistributing the Work and assume any risks associated with Your exercise of +permissions under this License. + +8. Limitation of Liability. + +In no event and under no legal theory, whether in tort (including negligence), +contract, or otherwise, unless required by applicable law (such as deliberate +and grossly negligent acts) or agreed to in writing, shall any Contributor be +liable to You for damages, including any direct, indirect, special, incidental, +or consequential damages of any character arising as a result of this License or +out of the use or inability to use the Work (including but not limited to +damages for loss of goodwill, work stoppage, computer failure or malfunction, or +any and all other commercial damages or losses), even if such Contributor has +been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. + +While redistributing the Work or Derivative Works thereof, You may choose to +offer, and charge a fee for, acceptance of support, warranty, indemnity, or +other liability obligations and/or rights consistent with this License. However, +in accepting such obligations, You may act only on Your own behalf and on Your +sole responsibility, not on behalf of any other Contributor, and only if You +agree to indemnify, defend, and hold each Contributor harmless for any liability +incurred by, or claims asserted against, such Contributor by reason of your +accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work + +To apply the Apache License to your work, attach the following boilerplate +notice, with the fields enclosed by brackets "[]" replaced with your own +identifying information. (Don't include the brackets!) The text should be +enclosed in the appropriate comment syntax for the file format. We also +recommend that a file or class name and description of purpose be included on +the same "printed page" as the copyright notice for easier identification within +third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/vendor/github.com/golang/glog/README b/vendor/github.com/golang/glog/README new file mode 100644 index 00000000..387b4eb6 --- /dev/null +++ b/vendor/github.com/golang/glog/README @@ -0,0 +1,44 @@ +glog +==== + +Leveled execution logs for Go. + +This is an efficient pure Go implementation of leveled logs in the +manner of the open source C++ package + https://github.com/google/glog + +By binding methods to booleans it is possible to use the log package +without paying the expense of evaluating the arguments to the log. +Through the -vmodule flag, the package also provides fine-grained +control over logging at the file level. + +The comment from glog.go introduces the ideas: + + Package glog implements logging analogous to the Google-internal + C++ INFO/ERROR/V setup. It provides functions Info, Warning, + Error, Fatal, plus formatting variants such as Infof. It + also provides V-style logging controlled by the -v and + -vmodule=file=2 flags. + + Basic examples: + + glog.Info("Prepare to repel boarders") + + glog.Fatalf("Initialization failed: %s", err) + + See the documentation for the V function for an explanation + of these examples: + + if glog.V(2) { + glog.Info("Starting transaction...") + } + + glog.V(2).Infoln("Processed", nItems, "elements") + + +The repository contains an open source version of the log package +used inside Google. The master copy of the source lives inside +Google, not here. The code in this repo is for export only and is not itself +under development. Feature requests will be ignored. + +Send bug reports to golang-nuts@googlegroups.com. diff --git a/vendor/github.com/golang/glog/glog.go b/vendor/github.com/golang/glog/glog.go new file mode 100644 index 00000000..54bd7afd --- /dev/null +++ b/vendor/github.com/golang/glog/glog.go @@ -0,0 +1,1180 @@ +// Go support for leveled logs, analogous to https://code.google.com/p/google-glog/ +// +// Copyright 2013 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package glog implements logging analogous to the Google-internal C++ INFO/ERROR/V setup. +// It provides functions Info, Warning, Error, Fatal, plus formatting variants such as +// Infof. It also provides V-style logging controlled by the -v and -vmodule=file=2 flags. +// +// Basic examples: +// +// glog.Info("Prepare to repel boarders") +// +// glog.Fatalf("Initialization failed: %s", err) +// +// See the documentation for the V function for an explanation of these examples: +// +// if glog.V(2) { +// glog.Info("Starting transaction...") +// } +// +// glog.V(2).Infoln("Processed", nItems, "elements") +// +// Log output is buffered and written periodically using Flush. Programs +// should call Flush before exiting to guarantee all log output is written. +// +// By default, all log statements write to files in a temporary directory. +// This package provides several flags that modify this behavior. +// As a result, flag.Parse must be called before any logging is done. +// +// -logtostderr=false +// Logs are written to standard error instead of to files. +// -alsologtostderr=false +// Logs are written to standard error as well as to files. +// -stderrthreshold=ERROR +// Log events at or above this severity are logged to standard +// error as well as to files. +// -log_dir="" +// Log files will be written to this directory instead of the +// default temporary directory. +// +// Other flags provide aids to debugging. +// +// -log_backtrace_at="" +// When set to a file and line number holding a logging statement, +// such as +// -log_backtrace_at=gopherflakes.go:234 +// a stack trace will be written to the Info log whenever execution +// hits that statement. (Unlike with -vmodule, the ".go" must be +// present.) +// -v=0 +// Enable V-leveled logging at the specified level. +// -vmodule="" +// The syntax of the argument is a comma-separated list of pattern=N, +// where pattern is a literal file name (minus the ".go" suffix) or +// "glob" pattern and N is a V level. For instance, +// -vmodule=gopher*=3 +// sets the V level to 3 in all Go files whose names begin "gopher". +// +package glog + +import ( + "bufio" + "bytes" + "errors" + "flag" + "fmt" + "io" + stdLog "log" + "os" + "path/filepath" + "runtime" + "strconv" + "strings" + "sync" + "sync/atomic" + "time" +) + +// severity identifies the sort of log: info, warning etc. It also implements +// the flag.Value interface. The -stderrthreshold flag is of type severity and +// should be modified only through the flag.Value interface. The values match +// the corresponding constants in C++. +type severity int32 // sync/atomic int32 + +// These constants identify the log levels in order of increasing severity. +// A message written to a high-severity log file is also written to each +// lower-severity log file. +const ( + infoLog severity = iota + warningLog + errorLog + fatalLog + numSeverity = 4 +) + +const severityChar = "IWEF" + +var severityName = []string{ + infoLog: "INFO", + warningLog: "WARNING", + errorLog: "ERROR", + fatalLog: "FATAL", +} + +// get returns the value of the severity. +func (s *severity) get() severity { + return severity(atomic.LoadInt32((*int32)(s))) +} + +// set sets the value of the severity. +func (s *severity) set(val severity) { + atomic.StoreInt32((*int32)(s), int32(val)) +} + +// String is part of the flag.Value interface. +func (s *severity) String() string { + return strconv.FormatInt(int64(*s), 10) +} + +// Get is part of the flag.Value interface. +func (s *severity) Get() interface{} { + return *s +} + +// Set is part of the flag.Value interface. +func (s *severity) Set(value string) error { + var threshold severity + // Is it a known name? + if v, ok := severityByName(value); ok { + threshold = v + } else { + v, err := strconv.Atoi(value) + if err != nil { + return err + } + threshold = severity(v) + } + logging.stderrThreshold.set(threshold) + return nil +} + +func severityByName(s string) (severity, bool) { + s = strings.ToUpper(s) + for i, name := range severityName { + if name == s { + return severity(i), true + } + } + return 0, false +} + +// OutputStats tracks the number of output lines and bytes written. +type OutputStats struct { + lines int64 + bytes int64 +} + +// Lines returns the number of lines written. +func (s *OutputStats) Lines() int64 { + return atomic.LoadInt64(&s.lines) +} + +// Bytes returns the number of bytes written. +func (s *OutputStats) Bytes() int64 { + return atomic.LoadInt64(&s.bytes) +} + +// Stats tracks the number of lines of output and number of bytes +// per severity level. Values must be read with atomic.LoadInt64. +var Stats struct { + Info, Warning, Error OutputStats +} + +var severityStats = [numSeverity]*OutputStats{ + infoLog: &Stats.Info, + warningLog: &Stats.Warning, + errorLog: &Stats.Error, +} + +// Level is exported because it appears in the arguments to V and is +// the type of the v flag, which can be set programmatically. +// It's a distinct type because we want to discriminate it from logType. +// Variables of type level are only changed under logging.mu. +// The -v flag is read only with atomic ops, so the state of the logging +// module is consistent. + +// Level is treated as a sync/atomic int32. + +// Level specifies a level of verbosity for V logs. *Level implements +// flag.Value; the -v flag is of type Level and should be modified +// only through the flag.Value interface. +type Level int32 + +// get returns the value of the Level. +func (l *Level) get() Level { + return Level(atomic.LoadInt32((*int32)(l))) +} + +// set sets the value of the Level. +func (l *Level) set(val Level) { + atomic.StoreInt32((*int32)(l), int32(val)) +} + +// String is part of the flag.Value interface. +func (l *Level) String() string { + return strconv.FormatInt(int64(*l), 10) +} + +// Get is part of the flag.Value interface. +func (l *Level) Get() interface{} { + return *l +} + +// Set is part of the flag.Value interface. +func (l *Level) Set(value string) error { + v, err := strconv.Atoi(value) + if err != nil { + return err + } + logging.mu.Lock() + defer logging.mu.Unlock() + logging.setVState(Level(v), logging.vmodule.filter, false) + return nil +} + +// moduleSpec represents the setting of the -vmodule flag. +type moduleSpec struct { + filter []modulePat +} + +// modulePat contains a filter for the -vmodule flag. +// It holds a verbosity level and a file pattern to match. +type modulePat struct { + pattern string + literal bool // The pattern is a literal string + level Level +} + +// match reports whether the file matches the pattern. It uses a string +// comparison if the pattern contains no metacharacters. +func (m *modulePat) match(file string) bool { + if m.literal { + return file == m.pattern + } + match, _ := filepath.Match(m.pattern, file) + return match +} + +func (m *moduleSpec) String() string { + // Lock because the type is not atomic. TODO: clean this up. + logging.mu.Lock() + defer logging.mu.Unlock() + var b bytes.Buffer + for i, f := range m.filter { + if i > 0 { + b.WriteRune(',') + } + fmt.Fprintf(&b, "%s=%d", f.pattern, f.level) + } + return b.String() +} + +// Get is part of the (Go 1.2) flag.Getter interface. It always returns nil for this flag type since the +// struct is not exported. +func (m *moduleSpec) Get() interface{} { + return nil +} + +var errVmoduleSyntax = errors.New("syntax error: expect comma-separated list of filename=N") + +// Syntax: -vmodule=recordio=2,file=1,gfs*=3 +func (m *moduleSpec) Set(value string) error { + var filter []modulePat + for _, pat := range strings.Split(value, ",") { + if len(pat) == 0 { + // Empty strings such as from a trailing comma can be ignored. + continue + } + patLev := strings.Split(pat, "=") + if len(patLev) != 2 || len(patLev[0]) == 0 || len(patLev[1]) == 0 { + return errVmoduleSyntax + } + pattern := patLev[0] + v, err := strconv.Atoi(patLev[1]) + if err != nil { + return errors.New("syntax error: expect comma-separated list of filename=N") + } + if v < 0 { + return errors.New("negative value for vmodule level") + } + if v == 0 { + continue // Ignore. It's harmless but no point in paying the overhead. + } + // TODO: check syntax of filter? + filter = append(filter, modulePat{pattern, isLiteral(pattern), Level(v)}) + } + logging.mu.Lock() + defer logging.mu.Unlock() + logging.setVState(logging.verbosity, filter, true) + return nil +} + +// isLiteral reports whether the pattern is a literal string, that is, has no metacharacters +// that require filepath.Match to be called to match the pattern. +func isLiteral(pattern string) bool { + return !strings.ContainsAny(pattern, `\*?[]`) +} + +// traceLocation represents the setting of the -log_backtrace_at flag. +type traceLocation struct { + file string + line int +} + +// isSet reports whether the trace location has been specified. +// logging.mu is held. +func (t *traceLocation) isSet() bool { + return t.line > 0 +} + +// match reports whether the specified file and line matches the trace location. +// The argument file name is the full path, not the basename specified in the flag. +// logging.mu is held. +func (t *traceLocation) match(file string, line int) bool { + if t.line != line { + return false + } + if i := strings.LastIndex(file, "/"); i >= 0 { + file = file[i+1:] + } + return t.file == file +} + +func (t *traceLocation) String() string { + // Lock because the type is not atomic. TODO: clean this up. + logging.mu.Lock() + defer logging.mu.Unlock() + return fmt.Sprintf("%s:%d", t.file, t.line) +} + +// Get is part of the (Go 1.2) flag.Getter interface. It always returns nil for this flag type since the +// struct is not exported +func (t *traceLocation) Get() interface{} { + return nil +} + +var errTraceSyntax = errors.New("syntax error: expect file.go:234") + +// Syntax: -log_backtrace_at=gopherflakes.go:234 +// Note that unlike vmodule the file extension is included here. +func (t *traceLocation) Set(value string) error { + if value == "" { + // Unset. + t.line = 0 + t.file = "" + } + fields := strings.Split(value, ":") + if len(fields) != 2 { + return errTraceSyntax + } + file, line := fields[0], fields[1] + if !strings.Contains(file, ".") { + return errTraceSyntax + } + v, err := strconv.Atoi(line) + if err != nil { + return errTraceSyntax + } + if v <= 0 { + return errors.New("negative or zero value for level") + } + logging.mu.Lock() + defer logging.mu.Unlock() + t.line = v + t.file = file + return nil +} + +// flushSyncWriter is the interface satisfied by logging destinations. +type flushSyncWriter interface { + Flush() error + Sync() error + io.Writer +} + +func init() { + flag.BoolVar(&logging.toStderr, "logtostderr", false, "log to standard error instead of files") + flag.BoolVar(&logging.alsoToStderr, "alsologtostderr", false, "log to standard error as well as files") + flag.Var(&logging.verbosity, "v", "log level for V logs") + flag.Var(&logging.stderrThreshold, "stderrthreshold", "logs at or above this threshold go to stderr") + flag.Var(&logging.vmodule, "vmodule", "comma-separated list of pattern=N settings for file-filtered logging") + flag.Var(&logging.traceLocation, "log_backtrace_at", "when logging hits line file:N, emit a stack trace") + + // Default stderrThreshold is ERROR. + logging.stderrThreshold = errorLog + + logging.setVState(0, nil, false) + go logging.flushDaemon() +} + +// Flush flushes all pending log I/O. +func Flush() { + logging.lockAndFlushAll() +} + +// loggingT collects all the global state of the logging setup. +type loggingT struct { + // Boolean flags. Not handled atomically because the flag.Value interface + // does not let us avoid the =true, and that shorthand is necessary for + // compatibility. TODO: does this matter enough to fix? Seems unlikely. + toStderr bool // The -logtostderr flag. + alsoToStderr bool // The -alsologtostderr flag. + + // Level flag. Handled atomically. + stderrThreshold severity // The -stderrthreshold flag. + + // freeList is a list of byte buffers, maintained under freeListMu. + freeList *buffer + // freeListMu maintains the free list. It is separate from the main mutex + // so buffers can be grabbed and printed to without holding the main lock, + // for better parallelization. + freeListMu sync.Mutex + + // mu protects the remaining elements of this structure and is + // used to synchronize logging. + mu sync.Mutex + // file holds writer for each of the log types. + file [numSeverity]flushSyncWriter + // pcs is used in V to avoid an allocation when computing the caller's PC. + pcs [1]uintptr + // vmap is a cache of the V Level for each V() call site, identified by PC. + // It is wiped whenever the vmodule flag changes state. + vmap map[uintptr]Level + // filterLength stores the length of the vmodule filter chain. If greater + // than zero, it means vmodule is enabled. It may be read safely + // using sync.LoadInt32, but is only modified under mu. + filterLength int32 + // traceLocation is the state of the -log_backtrace_at flag. + traceLocation traceLocation + // These flags are modified only under lock, although verbosity may be fetched + // safely using atomic.LoadInt32. + vmodule moduleSpec // The state of the -vmodule flag. + verbosity Level // V logging level, the value of the -v flag/ +} + +// buffer holds a byte Buffer for reuse. The zero value is ready for use. +type buffer struct { + bytes.Buffer + tmp [64]byte // temporary byte array for creating headers. + next *buffer +} + +var logging loggingT + +// setVState sets a consistent state for V logging. +// l.mu is held. +func (l *loggingT) setVState(verbosity Level, filter []modulePat, setFilter bool) { + // Turn verbosity off so V will not fire while we are in transition. + logging.verbosity.set(0) + // Ditto for filter length. + atomic.StoreInt32(&logging.filterLength, 0) + + // Set the new filters and wipe the pc->Level map if the filter has changed. + if setFilter { + logging.vmodule.filter = filter + logging.vmap = make(map[uintptr]Level) + } + + // Things are consistent now, so enable filtering and verbosity. + // They are enabled in order opposite to that in V. + atomic.StoreInt32(&logging.filterLength, int32(len(filter))) + logging.verbosity.set(verbosity) +} + +// getBuffer returns a new, ready-to-use buffer. +func (l *loggingT) getBuffer() *buffer { + l.freeListMu.Lock() + b := l.freeList + if b != nil { + l.freeList = b.next + } + l.freeListMu.Unlock() + if b == nil { + b = new(buffer) + } else { + b.next = nil + b.Reset() + } + return b +} + +// putBuffer returns a buffer to the free list. +func (l *loggingT) putBuffer(b *buffer) { + if b.Len() >= 256 { + // Let big buffers die a natural death. + return + } + l.freeListMu.Lock() + b.next = l.freeList + l.freeList = b + l.freeListMu.Unlock() +} + +var timeNow = time.Now // Stubbed out for testing. + +/* +header formats a log header as defined by the C++ implementation. +It returns a buffer containing the formatted header and the user's file and line number. +The depth specifies how many stack frames above lives the source line to be identified in the log message. + +Log lines have this form: + Lmmdd hh:mm:ss.uuuuuu threadid file:line] msg... +where the fields are defined as follows: + L A single character, representing the log level (eg 'I' for INFO) + mm The month (zero padded; ie May is '05') + dd The day (zero padded) + hh:mm:ss.uuuuuu Time in hours, minutes and fractional seconds + threadid The space-padded thread ID as returned by GetTID() + file The file name + line The line number + msg The user-supplied message +*/ +func (l *loggingT) header(s severity, depth int) (*buffer, string, int) { + _, file, line, ok := runtime.Caller(3 + depth) + if !ok { + file = "???" + line = 1 + } else { + slash := strings.LastIndex(file, "/") + if slash >= 0 { + file = file[slash+1:] + } + } + return l.formatHeader(s, file, line), file, line +} + +// formatHeader formats a log header using the provided file name and line number. +func (l *loggingT) formatHeader(s severity, file string, line int) *buffer { + now := timeNow() + if line < 0 { + line = 0 // not a real line number, but acceptable to someDigits + } + if s > fatalLog { + s = infoLog // for safety. + } + buf := l.getBuffer() + + // Avoid Fprintf, for speed. The format is so simple that we can do it quickly by hand. + // It's worth about 3X. Fprintf is hard. + _, month, day := now.Date() + hour, minute, second := now.Clock() + // Lmmdd hh:mm:ss.uuuuuu threadid file:line] + buf.tmp[0] = severityChar[s] + buf.twoDigits(1, int(month)) + buf.twoDigits(3, day) + buf.tmp[5] = ' ' + buf.twoDigits(6, hour) + buf.tmp[8] = ':' + buf.twoDigits(9, minute) + buf.tmp[11] = ':' + buf.twoDigits(12, second) + buf.tmp[14] = '.' + buf.nDigits(6, 15, now.Nanosecond()/1000, '0') + buf.tmp[21] = ' ' + buf.nDigits(7, 22, pid, ' ') // TODO: should be TID + buf.tmp[29] = ' ' + buf.Write(buf.tmp[:30]) + buf.WriteString(file) + buf.tmp[0] = ':' + n := buf.someDigits(1, line) + buf.tmp[n+1] = ']' + buf.tmp[n+2] = ' ' + buf.Write(buf.tmp[:n+3]) + return buf +} + +// Some custom tiny helper functions to print the log header efficiently. + +const digits = "0123456789" + +// twoDigits formats a zero-prefixed two-digit integer at buf.tmp[i]. +func (buf *buffer) twoDigits(i, d int) { + buf.tmp[i+1] = digits[d%10] + d /= 10 + buf.tmp[i] = digits[d%10] +} + +// nDigits formats an n-digit integer at buf.tmp[i], +// padding with pad on the left. +// It assumes d >= 0. +func (buf *buffer) nDigits(n, i, d int, pad byte) { + j := n - 1 + for ; j >= 0 && d > 0; j-- { + buf.tmp[i+j] = digits[d%10] + d /= 10 + } + for ; j >= 0; j-- { + buf.tmp[i+j] = pad + } +} + +// someDigits formats a zero-prefixed variable-width integer at buf.tmp[i]. +func (buf *buffer) someDigits(i, d int) int { + // Print into the top, then copy down. We know there's space for at least + // a 10-digit number. + j := len(buf.tmp) + for { + j-- + buf.tmp[j] = digits[d%10] + d /= 10 + if d == 0 { + break + } + } + return copy(buf.tmp[i:], buf.tmp[j:]) +} + +func (l *loggingT) println(s severity, args ...interface{}) { + buf, file, line := l.header(s, 0) + fmt.Fprintln(buf, args...) + l.output(s, buf, file, line, false) +} + +func (l *loggingT) print(s severity, args ...interface{}) { + l.printDepth(s, 1, args...) +} + +func (l *loggingT) printDepth(s severity, depth int, args ...interface{}) { + buf, file, line := l.header(s, depth) + fmt.Fprint(buf, args...) + if buf.Bytes()[buf.Len()-1] != '\n' { + buf.WriteByte('\n') + } + l.output(s, buf, file, line, false) +} + +func (l *loggingT) printf(s severity, format string, args ...interface{}) { + buf, file, line := l.header(s, 0) + fmt.Fprintf(buf, format, args...) + if buf.Bytes()[buf.Len()-1] != '\n' { + buf.WriteByte('\n') + } + l.output(s, buf, file, line, false) +} + +// printWithFileLine behaves like print but uses the provided file and line number. If +// alsoLogToStderr is true, the log message always appears on standard error; it +// will also appear in the log file unless --logtostderr is set. +func (l *loggingT) printWithFileLine(s severity, file string, line int, alsoToStderr bool, args ...interface{}) { + buf := l.formatHeader(s, file, line) + fmt.Fprint(buf, args...) + if buf.Bytes()[buf.Len()-1] != '\n' { + buf.WriteByte('\n') + } + l.output(s, buf, file, line, alsoToStderr) +} + +// output writes the data to the log files and releases the buffer. +func (l *loggingT) output(s severity, buf *buffer, file string, line int, alsoToStderr bool) { + l.mu.Lock() + if l.traceLocation.isSet() { + if l.traceLocation.match(file, line) { + buf.Write(stacks(false)) + } + } + data := buf.Bytes() + if !flag.Parsed() { + os.Stderr.Write([]byte("ERROR: logging before flag.Parse: ")) + os.Stderr.Write(data) + } else if l.toStderr { + os.Stderr.Write(data) + } else { + if alsoToStderr || l.alsoToStderr || s >= l.stderrThreshold.get() { + os.Stderr.Write(data) + } + if l.file[s] == nil { + if err := l.createFiles(s); err != nil { + os.Stderr.Write(data) // Make sure the message appears somewhere. + l.exit(err) + } + } + switch s { + case fatalLog: + l.file[fatalLog].Write(data) + fallthrough + case errorLog: + l.file[errorLog].Write(data) + fallthrough + case warningLog: + l.file[warningLog].Write(data) + fallthrough + case infoLog: + l.file[infoLog].Write(data) + } + } + if s == fatalLog { + // If we got here via Exit rather than Fatal, print no stacks. + if atomic.LoadUint32(&fatalNoStacks) > 0 { + l.mu.Unlock() + timeoutFlush(10 * time.Second) + os.Exit(1) + } + // Dump all goroutine stacks before exiting. + // First, make sure we see the trace for the current goroutine on standard error. + // If -logtostderr has been specified, the loop below will do that anyway + // as the first stack in the full dump. + if !l.toStderr { + os.Stderr.Write(stacks(false)) + } + // Write the stack trace for all goroutines to the files. + trace := stacks(true) + logExitFunc = func(error) {} // If we get a write error, we'll still exit below. + for log := fatalLog; log >= infoLog; log-- { + if f := l.file[log]; f != nil { // Can be nil if -logtostderr is set. + f.Write(trace) + } + } + l.mu.Unlock() + timeoutFlush(10 * time.Second) + os.Exit(255) // C++ uses -1, which is silly because it's anded with 255 anyway. + } + l.putBuffer(buf) + l.mu.Unlock() + if stats := severityStats[s]; stats != nil { + atomic.AddInt64(&stats.lines, 1) + atomic.AddInt64(&stats.bytes, int64(len(data))) + } +} + +// timeoutFlush calls Flush and returns when it completes or after timeout +// elapses, whichever happens first. This is needed because the hooks invoked +// by Flush may deadlock when glog.Fatal is called from a hook that holds +// a lock. +func timeoutFlush(timeout time.Duration) { + done := make(chan bool, 1) + go func() { + Flush() // calls logging.lockAndFlushAll() + done <- true + }() + select { + case <-done: + case <-time.After(timeout): + fmt.Fprintln(os.Stderr, "glog: Flush took longer than", timeout) + } +} + +// stacks is a wrapper for runtime.Stack that attempts to recover the data for all goroutines. +func stacks(all bool) []byte { + // We don't know how big the traces are, so grow a few times if they don't fit. Start large, though. + n := 10000 + if all { + n = 100000 + } + var trace []byte + for i := 0; i < 5; i++ { + trace = make([]byte, n) + nbytes := runtime.Stack(trace, all) + if nbytes < len(trace) { + return trace[:nbytes] + } + n *= 2 + } + return trace +} + +// logExitFunc provides a simple mechanism to override the default behavior +// of exiting on error. Used in testing and to guarantee we reach a required exit +// for fatal logs. Instead, exit could be a function rather than a method but that +// would make its use clumsier. +var logExitFunc func(error) + +// exit is called if there is trouble creating or writing log files. +// It flushes the logs and exits the program; there's no point in hanging around. +// l.mu is held. +func (l *loggingT) exit(err error) { + fmt.Fprintf(os.Stderr, "log: exiting because of error: %s\n", err) + // If logExitFunc is set, we do that instead of exiting. + if logExitFunc != nil { + logExitFunc(err) + return + } + l.flushAll() + os.Exit(2) +} + +// syncBuffer joins a bufio.Writer to its underlying file, providing access to the +// file's Sync method and providing a wrapper for the Write method that provides log +// file rotation. There are conflicting methods, so the file cannot be embedded. +// l.mu is held for all its methods. +type syncBuffer struct { + logger *loggingT + *bufio.Writer + file *os.File + sev severity + nbytes uint64 // The number of bytes written to this file +} + +func (sb *syncBuffer) Sync() error { + return sb.file.Sync() +} + +func (sb *syncBuffer) Write(p []byte) (n int, err error) { + if sb.nbytes+uint64(len(p)) >= MaxSize { + if err := sb.rotateFile(time.Now()); err != nil { + sb.logger.exit(err) + } + } + n, err = sb.Writer.Write(p) + sb.nbytes += uint64(n) + if err != nil { + sb.logger.exit(err) + } + return +} + +// rotateFile closes the syncBuffer's file and starts a new one. +func (sb *syncBuffer) rotateFile(now time.Time) error { + if sb.file != nil { + sb.Flush() + sb.file.Close() + } + var err error + sb.file, _, err = create(severityName[sb.sev], now) + sb.nbytes = 0 + if err != nil { + return err + } + + sb.Writer = bufio.NewWriterSize(sb.file, bufferSize) + + // Write header. + var buf bytes.Buffer + fmt.Fprintf(&buf, "Log file created at: %s\n", now.Format("2006/01/02 15:04:05")) + fmt.Fprintf(&buf, "Running on machine: %s\n", host) + fmt.Fprintf(&buf, "Binary: Built with %s %s for %s/%s\n", runtime.Compiler, runtime.Version(), runtime.GOOS, runtime.GOARCH) + fmt.Fprintf(&buf, "Log line format: [IWEF]mmdd hh:mm:ss.uuuuuu threadid file:line] msg\n") + n, err := sb.file.Write(buf.Bytes()) + sb.nbytes += uint64(n) + return err +} + +// bufferSize sizes the buffer associated with each log file. It's large +// so that log records can accumulate without the logging thread blocking +// on disk I/O. The flushDaemon will block instead. +const bufferSize = 256 * 1024 + +// createFiles creates all the log files for severity from sev down to infoLog. +// l.mu is held. +func (l *loggingT) createFiles(sev severity) error { + now := time.Now() + // Files are created in decreasing severity order, so as soon as we find one + // has already been created, we can stop. + for s := sev; s >= infoLog && l.file[s] == nil; s-- { + sb := &syncBuffer{ + logger: l, + sev: s, + } + if err := sb.rotateFile(now); err != nil { + return err + } + l.file[s] = sb + } + return nil +} + +const flushInterval = 30 * time.Second + +// flushDaemon periodically flushes the log file buffers. +func (l *loggingT) flushDaemon() { + for _ = range time.NewTicker(flushInterval).C { + l.lockAndFlushAll() + } +} + +// lockAndFlushAll is like flushAll but locks l.mu first. +func (l *loggingT) lockAndFlushAll() { + l.mu.Lock() + l.flushAll() + l.mu.Unlock() +} + +// flushAll flushes all the logs and attempts to "sync" their data to disk. +// l.mu is held. +func (l *loggingT) flushAll() { + // Flush from fatal down, in case there's trouble flushing. + for s := fatalLog; s >= infoLog; s-- { + file := l.file[s] + if file != nil { + file.Flush() // ignore error + file.Sync() // ignore error + } + } +} + +// CopyStandardLogTo arranges for messages written to the Go "log" package's +// default logs to also appear in the Google logs for the named and lower +// severities. Subsequent changes to the standard log's default output location +// or format may break this behavior. +// +// Valid names are "INFO", "WARNING", "ERROR", and "FATAL". If the name is not +// recognized, CopyStandardLogTo panics. +func CopyStandardLogTo(name string) { + sev, ok := severityByName(name) + if !ok { + panic(fmt.Sprintf("log.CopyStandardLogTo(%q): unrecognized severity name", name)) + } + // Set a log format that captures the user's file and line: + // d.go:23: message + stdLog.SetFlags(stdLog.Lshortfile) + stdLog.SetOutput(logBridge(sev)) +} + +// logBridge provides the Write method that enables CopyStandardLogTo to connect +// Go's standard logs to the logs provided by this package. +type logBridge severity + +// Write parses the standard logging line and passes its components to the +// logger for severity(lb). +func (lb logBridge) Write(b []byte) (n int, err error) { + var ( + file = "???" + line = 1 + text string + ) + // Split "d.go:23: message" into "d.go", "23", and "message". + if parts := bytes.SplitN(b, []byte{':'}, 3); len(parts) != 3 || len(parts[0]) < 1 || len(parts[2]) < 1 { + text = fmt.Sprintf("bad log format: %s", b) + } else { + file = string(parts[0]) + text = string(parts[2][1:]) // skip leading space + line, err = strconv.Atoi(string(parts[1])) + if err != nil { + text = fmt.Sprintf("bad line number: %s", b) + line = 1 + } + } + // printWithFileLine with alsoToStderr=true, so standard log messages + // always appear on standard error. + logging.printWithFileLine(severity(lb), file, line, true, text) + return len(b), nil +} + +// setV computes and remembers the V level for a given PC +// when vmodule is enabled. +// File pattern matching takes the basename of the file, stripped +// of its .go suffix, and uses filepath.Match, which is a little more +// general than the *? matching used in C++. +// l.mu is held. +func (l *loggingT) setV(pc uintptr) Level { + fn := runtime.FuncForPC(pc) + file, _ := fn.FileLine(pc) + // The file is something like /a/b/c/d.go. We want just the d. + if strings.HasSuffix(file, ".go") { + file = file[:len(file)-3] + } + if slash := strings.LastIndex(file, "/"); slash >= 0 { + file = file[slash+1:] + } + for _, filter := range l.vmodule.filter { + if filter.match(file) { + l.vmap[pc] = filter.level + return filter.level + } + } + l.vmap[pc] = 0 + return 0 +} + +// Verbose is a boolean type that implements Infof (like Printf) etc. +// See the documentation of V for more information. +type Verbose bool + +// V reports whether verbosity at the call site is at least the requested level. +// The returned value is a boolean of type Verbose, which implements Info, Infoln +// and Infof. These methods will write to the Info log if called. +// Thus, one may write either +// if glog.V(2) { glog.Info("log this") } +// or +// glog.V(2).Info("log this") +// The second form is shorter but the first is cheaper if logging is off because it does +// not evaluate its arguments. +// +// Whether an individual call to V generates a log record depends on the setting of +// the -v and --vmodule flags; both are off by default. If the level in the call to +// V is at least the value of -v, or of -vmodule for the source file containing the +// call, the V call will log. +func V(level Level) Verbose { + // This function tries hard to be cheap unless there's work to do. + // The fast path is two atomic loads and compares. + + // Here is a cheap but safe test to see if V logging is enabled globally. + if logging.verbosity.get() >= level { + return Verbose(true) + } + + // It's off globally but it vmodule may still be set. + // Here is another cheap but safe test to see if vmodule is enabled. + if atomic.LoadInt32(&logging.filterLength) > 0 { + // Now we need a proper lock to use the logging structure. The pcs field + // is shared so we must lock before accessing it. This is fairly expensive, + // but if V logging is enabled we're slow anyway. + logging.mu.Lock() + defer logging.mu.Unlock() + if runtime.Callers(2, logging.pcs[:]) == 0 { + return Verbose(false) + } + v, ok := logging.vmap[logging.pcs[0]] + if !ok { + v = logging.setV(logging.pcs[0]) + } + return Verbose(v >= level) + } + return Verbose(false) +} + +// Info is equivalent to the global Info function, guarded by the value of v. +// See the documentation of V for usage. +func (v Verbose) Info(args ...interface{}) { + if v { + logging.print(infoLog, args...) + } +} + +// Infoln is equivalent to the global Infoln function, guarded by the value of v. +// See the documentation of V for usage. +func (v Verbose) Infoln(args ...interface{}) { + if v { + logging.println(infoLog, args...) + } +} + +// Infof is equivalent to the global Infof function, guarded by the value of v. +// See the documentation of V for usage. +func (v Verbose) Infof(format string, args ...interface{}) { + if v { + logging.printf(infoLog, format, args...) + } +} + +// Info logs to the INFO log. +// Arguments are handled in the manner of fmt.Print; a newline is appended if missing. +func Info(args ...interface{}) { + logging.print(infoLog, args...) +} + +// InfoDepth acts as Info but uses depth to determine which call frame to log. +// InfoDepth(0, "msg") is the same as Info("msg"). +func InfoDepth(depth int, args ...interface{}) { + logging.printDepth(infoLog, depth, args...) +} + +// Infoln logs to the INFO log. +// Arguments are handled in the manner of fmt.Println; a newline is appended if missing. +func Infoln(args ...interface{}) { + logging.println(infoLog, args...) +} + +// Infof logs to the INFO log. +// Arguments are handled in the manner of fmt.Printf; a newline is appended if missing. +func Infof(format string, args ...interface{}) { + logging.printf(infoLog, format, args...) +} + +// Warning logs to the WARNING and INFO logs. +// Arguments are handled in the manner of fmt.Print; a newline is appended if missing. +func Warning(args ...interface{}) { + logging.print(warningLog, args...) +} + +// WarningDepth acts as Warning but uses depth to determine which call frame to log. +// WarningDepth(0, "msg") is the same as Warning("msg"). +func WarningDepth(depth int, args ...interface{}) { + logging.printDepth(warningLog, depth, args...) +} + +// Warningln logs to the WARNING and INFO logs. +// Arguments are handled in the manner of fmt.Println; a newline is appended if missing. +func Warningln(args ...interface{}) { + logging.println(warningLog, args...) +} + +// Warningf logs to the WARNING and INFO logs. +// Arguments are handled in the manner of fmt.Printf; a newline is appended if missing. +func Warningf(format string, args ...interface{}) { + logging.printf(warningLog, format, args...) +} + +// Error logs to the ERROR, WARNING, and INFO logs. +// Arguments are handled in the manner of fmt.Print; a newline is appended if missing. +func Error(args ...interface{}) { + logging.print(errorLog, args...) +} + +// ErrorDepth acts as Error but uses depth to determine which call frame to log. +// ErrorDepth(0, "msg") is the same as Error("msg"). +func ErrorDepth(depth int, args ...interface{}) { + logging.printDepth(errorLog, depth, args...) +} + +// Errorln logs to the ERROR, WARNING, and INFO logs. +// Arguments are handled in the manner of fmt.Println; a newline is appended if missing. +func Errorln(args ...interface{}) { + logging.println(errorLog, args...) +} + +// Errorf logs to the ERROR, WARNING, and INFO logs. +// Arguments are handled in the manner of fmt.Printf; a newline is appended if missing. +func Errorf(format string, args ...interface{}) { + logging.printf(errorLog, format, args...) +} + +// Fatal logs to the FATAL, ERROR, WARNING, and INFO logs, +// including a stack trace of all running goroutines, then calls os.Exit(255). +// Arguments are handled in the manner of fmt.Print; a newline is appended if missing. +func Fatal(args ...interface{}) { + logging.print(fatalLog, args...) +} + +// FatalDepth acts as Fatal but uses depth to determine which call frame to log. +// FatalDepth(0, "msg") is the same as Fatal("msg"). +func FatalDepth(depth int, args ...interface{}) { + logging.printDepth(fatalLog, depth, args...) +} + +// Fatalln logs to the FATAL, ERROR, WARNING, and INFO logs, +// including a stack trace of all running goroutines, then calls os.Exit(255). +// Arguments are handled in the manner of fmt.Println; a newline is appended if missing. +func Fatalln(args ...interface{}) { + logging.println(fatalLog, args...) +} + +// Fatalf logs to the FATAL, ERROR, WARNING, and INFO logs, +// including a stack trace of all running goroutines, then calls os.Exit(255). +// Arguments are handled in the manner of fmt.Printf; a newline is appended if missing. +func Fatalf(format string, args ...interface{}) { + logging.printf(fatalLog, format, args...) +} + +// fatalNoStacks is non-zero if we are to exit without dumping goroutine stacks. +// It allows Exit and relatives to use the Fatal logs. +var fatalNoStacks uint32 + +// Exit logs to the FATAL, ERROR, WARNING, and INFO logs, then calls os.Exit(1). +// Arguments are handled in the manner of fmt.Print; a newline is appended if missing. +func Exit(args ...interface{}) { + atomic.StoreUint32(&fatalNoStacks, 1) + logging.print(fatalLog, args...) +} + +// ExitDepth acts as Exit but uses depth to determine which call frame to log. +// ExitDepth(0, "msg") is the same as Exit("msg"). +func ExitDepth(depth int, args ...interface{}) { + atomic.StoreUint32(&fatalNoStacks, 1) + logging.printDepth(fatalLog, depth, args...) +} + +// Exitln logs to the FATAL, ERROR, WARNING, and INFO logs, then calls os.Exit(1). +func Exitln(args ...interface{}) { + atomic.StoreUint32(&fatalNoStacks, 1) + logging.println(fatalLog, args...) +} + +// Exitf logs to the FATAL, ERROR, WARNING, and INFO logs, then calls os.Exit(1). +// Arguments are handled in the manner of fmt.Printf; a newline is appended if missing. +func Exitf(format string, args ...interface{}) { + atomic.StoreUint32(&fatalNoStacks, 1) + logging.printf(fatalLog, format, args...) +} diff --git a/vendor/github.com/golang/glog/glog_file.go b/vendor/github.com/golang/glog/glog_file.go new file mode 100644 index 00000000..65075d28 --- /dev/null +++ b/vendor/github.com/golang/glog/glog_file.go @@ -0,0 +1,124 @@ +// Go support for leveled logs, analogous to https://code.google.com/p/google-glog/ +// +// Copyright 2013 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// File I/O for logs. + +package glog + +import ( + "errors" + "flag" + "fmt" + "os" + "os/user" + "path/filepath" + "strings" + "sync" + "time" +) + +// MaxSize is the maximum size of a log file in bytes. +var MaxSize uint64 = 1024 * 1024 * 1800 + +// logDirs lists the candidate directories for new log files. +var logDirs []string + +// If non-empty, overrides the choice of directory in which to write logs. +// See createLogDirs for the full list of possible destinations. +var logDir = flag.String("log_dir", "", "If non-empty, write log files in this directory") + +func createLogDirs() { + if *logDir != "" { + logDirs = append(logDirs, *logDir) + } + logDirs = append(logDirs, os.TempDir()) +} + +var ( + pid = os.Getpid() + program = filepath.Base(os.Args[0]) + host = "unknownhost" + userName = "unknownuser" +) + +func init() { + h, err := os.Hostname() + if err == nil { + host = shortHostname(h) + } + + current, err := user.Current() + if err == nil { + userName = current.Username + } + + // Sanitize userName since it may contain filepath separators on Windows. + userName = strings.Replace(userName, `\`, "_", -1) +} + +// shortHostname returns its argument, truncating at the first period. +// For instance, given "www.google.com" it returns "www". +func shortHostname(hostname string) string { + if i := strings.Index(hostname, "."); i >= 0 { + return hostname[:i] + } + return hostname +} + +// logName returns a new log file name containing tag, with start time t, and +// the name for the symlink for tag. +func logName(tag string, t time.Time) (name, link string) { + name = fmt.Sprintf("%s.%s.%s.log.%s.%04d%02d%02d-%02d%02d%02d.%d", + program, + host, + userName, + tag, + t.Year(), + t.Month(), + t.Day(), + t.Hour(), + t.Minute(), + t.Second(), + pid) + return name, program + "." + tag +} + +var onceLogDirs sync.Once + +// create creates a new log file and returns the file and its filename, which +// contains tag ("INFO", "FATAL", etc.) and t. If the file is created +// successfully, create also attempts to update the symlink for that tag, ignoring +// errors. +func create(tag string, t time.Time) (f *os.File, filename string, err error) { + onceLogDirs.Do(createLogDirs) + if len(logDirs) == 0 { + return nil, "", errors.New("log: no log dirs") + } + name, link := logName(tag, t) + var lastErr error + for _, dir := range logDirs { + fname := filepath.Join(dir, name) + f, err := os.Create(fname) + if err == nil { + symlink := filepath.Join(dir, link) + os.Remove(symlink) // ignore err + os.Symlink(name, symlink) // ignore err + return f, fname, nil + } + lastErr = err + } + return nil, "", fmt.Errorf("log: cannot create log: %v", lastErr) +} diff --git a/vendor/github.com/golang/protobuf/LICENSE b/vendor/github.com/golang/protobuf/LICENSE new file mode 100644 index 00000000..0f646931 --- /dev/null +++ b/vendor/github.com/golang/protobuf/LICENSE @@ -0,0 +1,28 @@ +Copyright 2010 The Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + diff --git a/vendor/github.com/golang/protobuf/proto/clone.go b/vendor/github.com/golang/protobuf/proto/clone.go new file mode 100644 index 00000000..3cd3249f --- /dev/null +++ b/vendor/github.com/golang/protobuf/proto/clone.go @@ -0,0 +1,253 @@ +// Go support for Protocol Buffers - Google's data interchange format +// +// Copyright 2011 The Go Authors. All rights reserved. +// https://github.com/golang/protobuf +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +// Protocol buffer deep copy and merge. +// TODO: RawMessage. + +package proto + +import ( + "fmt" + "log" + "reflect" + "strings" +) + +// Clone returns a deep copy of a protocol buffer. +func Clone(src Message) Message { + in := reflect.ValueOf(src) + if in.IsNil() { + return src + } + out := reflect.New(in.Type().Elem()) + dst := out.Interface().(Message) + Merge(dst, src) + return dst +} + +// Merger is the interface representing objects that can merge messages of the same type. +type Merger interface { + // Merge merges src into this message. + // Required and optional fields that are set in src will be set to that value in dst. + // Elements of repeated fields will be appended. + // + // Merge may panic if called with a different argument type than the receiver. + Merge(src Message) +} + +// generatedMerger is the custom merge method that generated protos will have. +// We must add this method since a generate Merge method will conflict with +// many existing protos that have a Merge data field already defined. +type generatedMerger interface { + XXX_Merge(src Message) +} + +// Merge merges src into dst. +// Required and optional fields that are set in src will be set to that value in dst. +// Elements of repeated fields will be appended. +// Merge panics if src and dst are not the same type, or if dst is nil. +func Merge(dst, src Message) { + if m, ok := dst.(Merger); ok { + m.Merge(src) + return + } + + in := reflect.ValueOf(src) + out := reflect.ValueOf(dst) + if out.IsNil() { + panic("proto: nil destination") + } + if in.Type() != out.Type() { + panic(fmt.Sprintf("proto.Merge(%T, %T) type mismatch", dst, src)) + } + if in.IsNil() { + return // Merge from nil src is a noop + } + if m, ok := dst.(generatedMerger); ok { + m.XXX_Merge(src) + return + } + mergeStruct(out.Elem(), in.Elem()) +} + +func mergeStruct(out, in reflect.Value) { + sprop := GetProperties(in.Type()) + for i := 0; i < in.NumField(); i++ { + f := in.Type().Field(i) + if strings.HasPrefix(f.Name, "XXX_") { + continue + } + mergeAny(out.Field(i), in.Field(i), false, sprop.Prop[i]) + } + + if emIn, err := extendable(in.Addr().Interface()); err == nil { + emOut, _ := extendable(out.Addr().Interface()) + mIn, muIn := emIn.extensionsRead() + if mIn != nil { + mOut := emOut.extensionsWrite() + muIn.Lock() + mergeExtension(mOut, mIn) + muIn.Unlock() + } + } + + uf := in.FieldByName("XXX_unrecognized") + if !uf.IsValid() { + return + } + uin := uf.Bytes() + if len(uin) > 0 { + out.FieldByName("XXX_unrecognized").SetBytes(append([]byte(nil), uin...)) + } +} + +// mergeAny performs a merge between two values of the same type. +// viaPtr indicates whether the values were indirected through a pointer (implying proto2). +// prop is set if this is a struct field (it may be nil). +func mergeAny(out, in reflect.Value, viaPtr bool, prop *Properties) { + if in.Type() == protoMessageType { + if !in.IsNil() { + if out.IsNil() { + out.Set(reflect.ValueOf(Clone(in.Interface().(Message)))) + } else { + Merge(out.Interface().(Message), in.Interface().(Message)) + } + } + return + } + switch in.Kind() { + case reflect.Bool, reflect.Float32, reflect.Float64, reflect.Int32, reflect.Int64, + reflect.String, reflect.Uint32, reflect.Uint64: + if !viaPtr && isProto3Zero(in) { + return + } + out.Set(in) + case reflect.Interface: + // Probably a oneof field; copy non-nil values. + if in.IsNil() { + return + } + // Allocate destination if it is not set, or set to a different type. + // Otherwise we will merge as normal. + if out.IsNil() || out.Elem().Type() != in.Elem().Type() { + out.Set(reflect.New(in.Elem().Elem().Type())) // interface -> *T -> T -> new(T) + } + mergeAny(out.Elem(), in.Elem(), false, nil) + case reflect.Map: + if in.Len() == 0 { + return + } + if out.IsNil() { + out.Set(reflect.MakeMap(in.Type())) + } + // For maps with value types of *T or []byte we need to deep copy each value. + elemKind := in.Type().Elem().Kind() + for _, key := range in.MapKeys() { + var val reflect.Value + switch elemKind { + case reflect.Ptr: + val = reflect.New(in.Type().Elem().Elem()) + mergeAny(val, in.MapIndex(key), false, nil) + case reflect.Slice: + val = in.MapIndex(key) + val = reflect.ValueOf(append([]byte{}, val.Bytes()...)) + default: + val = in.MapIndex(key) + } + out.SetMapIndex(key, val) + } + case reflect.Ptr: + if in.IsNil() { + return + } + if out.IsNil() { + out.Set(reflect.New(in.Elem().Type())) + } + mergeAny(out.Elem(), in.Elem(), true, nil) + case reflect.Slice: + if in.IsNil() { + return + } + if in.Type().Elem().Kind() == reflect.Uint8 { + // []byte is a scalar bytes field, not a repeated field. + + // Edge case: if this is in a proto3 message, a zero length + // bytes field is considered the zero value, and should not + // be merged. + if prop != nil && prop.proto3 && in.Len() == 0 { + return + } + + // Make a deep copy. + // Append to []byte{} instead of []byte(nil) so that we never end up + // with a nil result. + out.SetBytes(append([]byte{}, in.Bytes()...)) + return + } + n := in.Len() + if out.IsNil() { + out.Set(reflect.MakeSlice(in.Type(), 0, n)) + } + switch in.Type().Elem().Kind() { + case reflect.Bool, reflect.Float32, reflect.Float64, reflect.Int32, reflect.Int64, + reflect.String, reflect.Uint32, reflect.Uint64: + out.Set(reflect.AppendSlice(out, in)) + default: + for i := 0; i < n; i++ { + x := reflect.Indirect(reflect.New(in.Type().Elem())) + mergeAny(x, in.Index(i), false, nil) + out.Set(reflect.Append(out, x)) + } + } + case reflect.Struct: + mergeStruct(out, in) + default: + // unknown type, so not a protocol buffer + log.Printf("proto: don't know how to copy %v", in) + } +} + +func mergeExtension(out, in map[int32]Extension) { + for extNum, eIn := range in { + eOut := Extension{desc: eIn.desc} + if eIn.value != nil { + v := reflect.New(reflect.TypeOf(eIn.value)).Elem() + mergeAny(v, reflect.ValueOf(eIn.value), false, nil) + eOut.value = v.Interface() + } + if eIn.enc != nil { + eOut.enc = make([]byte, len(eIn.enc)) + copy(eOut.enc, eIn.enc) + } + + out[extNum] = eOut + } +} diff --git a/vendor/github.com/golang/protobuf/proto/decode.go b/vendor/github.com/golang/protobuf/proto/decode.go new file mode 100644 index 00000000..d9aa3c42 --- /dev/null +++ b/vendor/github.com/golang/protobuf/proto/decode.go @@ -0,0 +1,428 @@ +// Go support for Protocol Buffers - Google's data interchange format +// +// Copyright 2010 The Go Authors. All rights reserved. +// https://github.com/golang/protobuf +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package proto + +/* + * Routines for decoding protocol buffer data to construct in-memory representations. + */ + +import ( + "errors" + "fmt" + "io" +) + +// errOverflow is returned when an integer is too large to be represented. +var errOverflow = errors.New("proto: integer overflow") + +// ErrInternalBadWireType is returned by generated code when an incorrect +// wire type is encountered. It does not get returned to user code. +var ErrInternalBadWireType = errors.New("proto: internal error: bad wiretype for oneof") + +// DecodeVarint reads a varint-encoded integer from the slice. +// It returns the integer and the number of bytes consumed, or +// zero if there is not enough. +// This is the format for the +// int32, int64, uint32, uint64, bool, and enum +// protocol buffer types. +func DecodeVarint(buf []byte) (x uint64, n int) { + for shift := uint(0); shift < 64; shift += 7 { + if n >= len(buf) { + return 0, 0 + } + b := uint64(buf[n]) + n++ + x |= (b & 0x7F) << shift + if (b & 0x80) == 0 { + return x, n + } + } + + // The number is too large to represent in a 64-bit value. + return 0, 0 +} + +func (p *Buffer) decodeVarintSlow() (x uint64, err error) { + i := p.index + l := len(p.buf) + + for shift := uint(0); shift < 64; shift += 7 { + if i >= l { + err = io.ErrUnexpectedEOF + return + } + b := p.buf[i] + i++ + x |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + p.index = i + return + } + } + + // The number is too large to represent in a 64-bit value. + err = errOverflow + return +} + +// DecodeVarint reads a varint-encoded integer from the Buffer. +// This is the format for the +// int32, int64, uint32, uint64, bool, and enum +// protocol buffer types. +func (p *Buffer) DecodeVarint() (x uint64, err error) { + i := p.index + buf := p.buf + + if i >= len(buf) { + return 0, io.ErrUnexpectedEOF + } else if buf[i] < 0x80 { + p.index++ + return uint64(buf[i]), nil + } else if len(buf)-i < 10 { + return p.decodeVarintSlow() + } + + var b uint64 + // we already checked the first byte + x = uint64(buf[i]) - 0x80 + i++ + + b = uint64(buf[i]) + i++ + x += b << 7 + if b&0x80 == 0 { + goto done + } + x -= 0x80 << 7 + + b = uint64(buf[i]) + i++ + x += b << 14 + if b&0x80 == 0 { + goto done + } + x -= 0x80 << 14 + + b = uint64(buf[i]) + i++ + x += b << 21 + if b&0x80 == 0 { + goto done + } + x -= 0x80 << 21 + + b = uint64(buf[i]) + i++ + x += b << 28 + if b&0x80 == 0 { + goto done + } + x -= 0x80 << 28 + + b = uint64(buf[i]) + i++ + x += b << 35 + if b&0x80 == 0 { + goto done + } + x -= 0x80 << 35 + + b = uint64(buf[i]) + i++ + x += b << 42 + if b&0x80 == 0 { + goto done + } + x -= 0x80 << 42 + + b = uint64(buf[i]) + i++ + x += b << 49 + if b&0x80 == 0 { + goto done + } + x -= 0x80 << 49 + + b = uint64(buf[i]) + i++ + x += b << 56 + if b&0x80 == 0 { + goto done + } + x -= 0x80 << 56 + + b = uint64(buf[i]) + i++ + x += b << 63 + if b&0x80 == 0 { + goto done + } + // x -= 0x80 << 63 // Always zero. + + return 0, errOverflow + +done: + p.index = i + return x, nil +} + +// DecodeFixed64 reads a 64-bit integer from the Buffer. +// This is the format for the +// fixed64, sfixed64, and double protocol buffer types. +func (p *Buffer) DecodeFixed64() (x uint64, err error) { + // x, err already 0 + i := p.index + 8 + if i < 0 || i > len(p.buf) { + err = io.ErrUnexpectedEOF + return + } + p.index = i + + x = uint64(p.buf[i-8]) + x |= uint64(p.buf[i-7]) << 8 + x |= uint64(p.buf[i-6]) << 16 + x |= uint64(p.buf[i-5]) << 24 + x |= uint64(p.buf[i-4]) << 32 + x |= uint64(p.buf[i-3]) << 40 + x |= uint64(p.buf[i-2]) << 48 + x |= uint64(p.buf[i-1]) << 56 + return +} + +// DecodeFixed32 reads a 32-bit integer from the Buffer. +// This is the format for the +// fixed32, sfixed32, and float protocol buffer types. +func (p *Buffer) DecodeFixed32() (x uint64, err error) { + // x, err already 0 + i := p.index + 4 + if i < 0 || i > len(p.buf) { + err = io.ErrUnexpectedEOF + return + } + p.index = i + + x = uint64(p.buf[i-4]) + x |= uint64(p.buf[i-3]) << 8 + x |= uint64(p.buf[i-2]) << 16 + x |= uint64(p.buf[i-1]) << 24 + return +} + +// DecodeZigzag64 reads a zigzag-encoded 64-bit integer +// from the Buffer. +// This is the format used for the sint64 protocol buffer type. +func (p *Buffer) DecodeZigzag64() (x uint64, err error) { + x, err = p.DecodeVarint() + if err != nil { + return + } + x = (x >> 1) ^ uint64((int64(x&1)<<63)>>63) + return +} + +// DecodeZigzag32 reads a zigzag-encoded 32-bit integer +// from the Buffer. +// This is the format used for the sint32 protocol buffer type. +func (p *Buffer) DecodeZigzag32() (x uint64, err error) { + x, err = p.DecodeVarint() + if err != nil { + return + } + x = uint64((uint32(x) >> 1) ^ uint32((int32(x&1)<<31)>>31)) + return +} + +// DecodeRawBytes reads a count-delimited byte buffer from the Buffer. +// This is the format used for the bytes protocol buffer +// type and for embedded messages. +func (p *Buffer) DecodeRawBytes(alloc bool) (buf []byte, err error) { + n, err := p.DecodeVarint() + if err != nil { + return nil, err + } + + nb := int(n) + if nb < 0 { + return nil, fmt.Errorf("proto: bad byte length %d", nb) + } + end := p.index + nb + if end < p.index || end > len(p.buf) { + return nil, io.ErrUnexpectedEOF + } + + if !alloc { + // todo: check if can get more uses of alloc=false + buf = p.buf[p.index:end] + p.index += nb + return + } + + buf = make([]byte, nb) + copy(buf, p.buf[p.index:]) + p.index += nb + return +} + +// DecodeStringBytes reads an encoded string from the Buffer. +// This is the format used for the proto2 string type. +func (p *Buffer) DecodeStringBytes() (s string, err error) { + buf, err := p.DecodeRawBytes(false) + if err != nil { + return + } + return string(buf), nil +} + +// Unmarshaler is the interface representing objects that can +// unmarshal themselves. The argument points to data that may be +// overwritten, so implementations should not keep references to the +// buffer. +// Unmarshal implementations should not clear the receiver. +// Any unmarshaled data should be merged into the receiver. +// Callers of Unmarshal that do not want to retain existing data +// should Reset the receiver before calling Unmarshal. +type Unmarshaler interface { + Unmarshal([]byte) error +} + +// newUnmarshaler is the interface representing objects that can +// unmarshal themselves. The semantics are identical to Unmarshaler. +// +// This exists to support protoc-gen-go generated messages. +// The proto package will stop type-asserting to this interface in the future. +// +// DO NOT DEPEND ON THIS. +type newUnmarshaler interface { + XXX_Unmarshal([]byte) error +} + +// Unmarshal parses the protocol buffer representation in buf and places the +// decoded result in pb. If the struct underlying pb does not match +// the data in buf, the results can be unpredictable. +// +// Unmarshal resets pb before starting to unmarshal, so any +// existing data in pb is always removed. Use UnmarshalMerge +// to preserve and append to existing data. +func Unmarshal(buf []byte, pb Message) error { + pb.Reset() + if u, ok := pb.(newUnmarshaler); ok { + return u.XXX_Unmarshal(buf) + } + if u, ok := pb.(Unmarshaler); ok { + return u.Unmarshal(buf) + } + return NewBuffer(buf).Unmarshal(pb) +} + +// UnmarshalMerge parses the protocol buffer representation in buf and +// writes the decoded result to pb. If the struct underlying pb does not match +// the data in buf, the results can be unpredictable. +// +// UnmarshalMerge merges into existing data in pb. +// Most code should use Unmarshal instead. +func UnmarshalMerge(buf []byte, pb Message) error { + if u, ok := pb.(newUnmarshaler); ok { + return u.XXX_Unmarshal(buf) + } + if u, ok := pb.(Unmarshaler); ok { + // NOTE: The history of proto have unfortunately been inconsistent + // whether Unmarshaler should or should not implicitly clear itself. + // Some implementations do, most do not. + // Thus, calling this here may or may not do what people want. + // + // See https://github.com/golang/protobuf/issues/424 + return u.Unmarshal(buf) + } + return NewBuffer(buf).Unmarshal(pb) +} + +// DecodeMessage reads a count-delimited message from the Buffer. +func (p *Buffer) DecodeMessage(pb Message) error { + enc, err := p.DecodeRawBytes(false) + if err != nil { + return err + } + return NewBuffer(enc).Unmarshal(pb) +} + +// DecodeGroup reads a tag-delimited group from the Buffer. +// StartGroup tag is already consumed. This function consumes +// EndGroup tag. +func (p *Buffer) DecodeGroup(pb Message) error { + b := p.buf[p.index:] + x, y := findEndGroup(b) + if x < 0 { + return io.ErrUnexpectedEOF + } + err := Unmarshal(b[:x], pb) + p.index += y + return err +} + +// Unmarshal parses the protocol buffer representation in the +// Buffer and places the decoded result in pb. If the struct +// underlying pb does not match the data in the buffer, the results can be +// unpredictable. +// +// Unlike proto.Unmarshal, this does not reset pb before starting to unmarshal. +func (p *Buffer) Unmarshal(pb Message) error { + // If the object can unmarshal itself, let it. + if u, ok := pb.(newUnmarshaler); ok { + err := u.XXX_Unmarshal(p.buf[p.index:]) + p.index = len(p.buf) + return err + } + if u, ok := pb.(Unmarshaler); ok { + // NOTE: The history of proto have unfortunately been inconsistent + // whether Unmarshaler should or should not implicitly clear itself. + // Some implementations do, most do not. + // Thus, calling this here may or may not do what people want. + // + // See https://github.com/golang/protobuf/issues/424 + err := u.Unmarshal(p.buf[p.index:]) + p.index = len(p.buf) + return err + } + + // Slow workaround for messages that aren't Unmarshalers. + // This includes some hand-coded .pb.go files and + // bootstrap protos. + // TODO: fix all of those and then add Unmarshal to + // the Message interface. Then: + // The cast above and code below can be deleted. + // The old unmarshaler can be deleted. + // Clients can call Unmarshal directly (can already do that, actually). + var info InternalMessageInfo + err := info.Unmarshal(pb, p.buf[p.index:]) + p.index = len(p.buf) + return err +} diff --git a/vendor/github.com/golang/protobuf/proto/discard.go b/vendor/github.com/golang/protobuf/proto/discard.go new file mode 100644 index 00000000..dea2617c --- /dev/null +++ b/vendor/github.com/golang/protobuf/proto/discard.go @@ -0,0 +1,350 @@ +// Go support for Protocol Buffers - Google's data interchange format +// +// Copyright 2017 The Go Authors. All rights reserved. +// https://github.com/golang/protobuf +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package proto + +import ( + "fmt" + "reflect" + "strings" + "sync" + "sync/atomic" +) + +type generatedDiscarder interface { + XXX_DiscardUnknown() +} + +// DiscardUnknown recursively discards all unknown fields from this message +// and all embedded messages. +// +// When unmarshaling a message with unrecognized fields, the tags and values +// of such fields are preserved in the Message. This allows a later call to +// marshal to be able to produce a message that continues to have those +// unrecognized fields. To avoid this, DiscardUnknown is used to +// explicitly clear the unknown fields after unmarshaling. +// +// For proto2 messages, the unknown fields of message extensions are only +// discarded from messages that have been accessed via GetExtension. +func DiscardUnknown(m Message) { + if m, ok := m.(generatedDiscarder); ok { + m.XXX_DiscardUnknown() + return + } + // TODO: Dynamically populate a InternalMessageInfo for legacy messages, + // but the master branch has no implementation for InternalMessageInfo, + // so it would be more work to replicate that approach. + discardLegacy(m) +} + +// DiscardUnknown recursively discards all unknown fields. +func (a *InternalMessageInfo) DiscardUnknown(m Message) { + di := atomicLoadDiscardInfo(&a.discard) + if di == nil { + di = getDiscardInfo(reflect.TypeOf(m).Elem()) + atomicStoreDiscardInfo(&a.discard, di) + } + di.discard(toPointer(&m)) +} + +type discardInfo struct { + typ reflect.Type + + initialized int32 // 0: only typ is valid, 1: everything is valid + lock sync.Mutex + + fields []discardFieldInfo + unrecognized field +} + +type discardFieldInfo struct { + field field // Offset of field, guaranteed to be valid + discard func(src pointer) +} + +var ( + discardInfoMap = map[reflect.Type]*discardInfo{} + discardInfoLock sync.Mutex +) + +func getDiscardInfo(t reflect.Type) *discardInfo { + discardInfoLock.Lock() + defer discardInfoLock.Unlock() + di := discardInfoMap[t] + if di == nil { + di = &discardInfo{typ: t} + discardInfoMap[t] = di + } + return di +} + +func (di *discardInfo) discard(src pointer) { + if src.isNil() { + return // Nothing to do. + } + + if atomic.LoadInt32(&di.initialized) == 0 { + di.computeDiscardInfo() + } + + for _, fi := range di.fields { + sfp := src.offset(fi.field) + fi.discard(sfp) + } + + // For proto2 messages, only discard unknown fields in message extensions + // that have been accessed via GetExtension. + if em, err := extendable(src.asPointerTo(di.typ).Interface()); err == nil { + // Ignore lock since DiscardUnknown is not concurrency safe. + emm, _ := em.extensionsRead() + for _, mx := range emm { + if m, ok := mx.value.(Message); ok { + DiscardUnknown(m) + } + } + } + + if di.unrecognized.IsValid() { + *src.offset(di.unrecognized).toBytes() = nil + } +} + +func (di *discardInfo) computeDiscardInfo() { + di.lock.Lock() + defer di.lock.Unlock() + if di.initialized != 0 { + return + } + t := di.typ + n := t.NumField() + + for i := 0; i < n; i++ { + f := t.Field(i) + if strings.HasPrefix(f.Name, "XXX_") { + continue + } + + dfi := discardFieldInfo{field: toField(&f)} + tf := f.Type + + // Unwrap tf to get its most basic type. + var isPointer, isSlice bool + if tf.Kind() == reflect.Slice && tf.Elem().Kind() != reflect.Uint8 { + isSlice = true + tf = tf.Elem() + } + if tf.Kind() == reflect.Ptr { + isPointer = true + tf = tf.Elem() + } + if isPointer && isSlice && tf.Kind() != reflect.Struct { + panic(fmt.Sprintf("%v.%s cannot be a slice of pointers to primitive types", t, f.Name)) + } + + switch tf.Kind() { + case reflect.Struct: + switch { + case !isPointer: + panic(fmt.Sprintf("%v.%s cannot be a direct struct value", t, f.Name)) + case isSlice: // E.g., []*pb.T + di := getDiscardInfo(tf) + dfi.discard = func(src pointer) { + sps := src.getPointerSlice() + for _, sp := range sps { + if !sp.isNil() { + di.discard(sp) + } + } + } + default: // E.g., *pb.T + di := getDiscardInfo(tf) + dfi.discard = func(src pointer) { + sp := src.getPointer() + if !sp.isNil() { + di.discard(sp) + } + } + } + case reflect.Map: + switch { + case isPointer || isSlice: + panic(fmt.Sprintf("%v.%s cannot be a pointer to a map or a slice of map values", t, f.Name)) + default: // E.g., map[K]V + if tf.Elem().Kind() == reflect.Ptr { // Proto struct (e.g., *T) + dfi.discard = func(src pointer) { + sm := src.asPointerTo(tf).Elem() + if sm.Len() == 0 { + return + } + for _, key := range sm.MapKeys() { + val := sm.MapIndex(key) + DiscardUnknown(val.Interface().(Message)) + } + } + } else { + dfi.discard = func(pointer) {} // Noop + } + } + case reflect.Interface: + // Must be oneof field. + switch { + case isPointer || isSlice: + panic(fmt.Sprintf("%v.%s cannot be a pointer to a interface or a slice of interface values", t, f.Name)) + default: // E.g., interface{} + // TODO: Make this faster? + dfi.discard = func(src pointer) { + su := src.asPointerTo(tf).Elem() + if !su.IsNil() { + sv := su.Elem().Elem().Field(0) + if sv.Kind() == reflect.Ptr && sv.IsNil() { + return + } + switch sv.Type().Kind() { + case reflect.Ptr: // Proto struct (e.g., *T) + DiscardUnknown(sv.Interface().(Message)) + } + } + } + } + default: + continue + } + di.fields = append(di.fields, dfi) + } + + di.unrecognized = invalidField + if f, ok := t.FieldByName("XXX_unrecognized"); ok { + if f.Type != reflect.TypeOf([]byte{}) { + panic("expected XXX_unrecognized to be of type []byte") + } + di.unrecognized = toField(&f) + } + + atomic.StoreInt32(&di.initialized, 1) +} + +func discardLegacy(m Message) { + v := reflect.ValueOf(m) + if v.Kind() != reflect.Ptr || v.IsNil() { + return + } + v = v.Elem() + if v.Kind() != reflect.Struct { + return + } + t := v.Type() + + for i := 0; i < v.NumField(); i++ { + f := t.Field(i) + if strings.HasPrefix(f.Name, "XXX_") { + continue + } + vf := v.Field(i) + tf := f.Type + + // Unwrap tf to get its most basic type. + var isPointer, isSlice bool + if tf.Kind() == reflect.Slice && tf.Elem().Kind() != reflect.Uint8 { + isSlice = true + tf = tf.Elem() + } + if tf.Kind() == reflect.Ptr { + isPointer = true + tf = tf.Elem() + } + if isPointer && isSlice && tf.Kind() != reflect.Struct { + panic(fmt.Sprintf("%T.%s cannot be a slice of pointers to primitive types", m, f.Name)) + } + + switch tf.Kind() { + case reflect.Struct: + switch { + case !isPointer: + panic(fmt.Sprintf("%T.%s cannot be a direct struct value", m, f.Name)) + case isSlice: // E.g., []*pb.T + for j := 0; j < vf.Len(); j++ { + discardLegacy(vf.Index(j).Interface().(Message)) + } + default: // E.g., *pb.T + discardLegacy(vf.Interface().(Message)) + } + case reflect.Map: + switch { + case isPointer || isSlice: + panic(fmt.Sprintf("%T.%s cannot be a pointer to a map or a slice of map values", m, f.Name)) + default: // E.g., map[K]V + tv := vf.Type().Elem() + if tv.Kind() == reflect.Ptr && tv.Implements(protoMessageType) { // Proto struct (e.g., *T) + for _, key := range vf.MapKeys() { + val := vf.MapIndex(key) + discardLegacy(val.Interface().(Message)) + } + } + } + case reflect.Interface: + // Must be oneof field. + switch { + case isPointer || isSlice: + panic(fmt.Sprintf("%T.%s cannot be a pointer to a interface or a slice of interface values", m, f.Name)) + default: // E.g., test_proto.isCommunique_Union interface + if !vf.IsNil() && f.Tag.Get("protobuf_oneof") != "" { + vf = vf.Elem() // E.g., *test_proto.Communique_Msg + if !vf.IsNil() { + vf = vf.Elem() // E.g., test_proto.Communique_Msg + vf = vf.Field(0) // E.g., Proto struct (e.g., *T) or primitive value + if vf.Kind() == reflect.Ptr { + discardLegacy(vf.Interface().(Message)) + } + } + } + } + } + } + + if vf := v.FieldByName("XXX_unrecognized"); vf.IsValid() { + if vf.Type() != reflect.TypeOf([]byte{}) { + panic("expected XXX_unrecognized to be of type []byte") + } + vf.Set(reflect.ValueOf([]byte(nil))) + } + + // For proto2 messages, only discard unknown fields in message extensions + // that have been accessed via GetExtension. + if em, err := extendable(m); err == nil { + // Ignore lock since discardLegacy is not concurrency safe. + emm, _ := em.extensionsRead() + for _, mx := range emm { + if m, ok := mx.value.(Message); ok { + discardLegacy(m) + } + } + } +} diff --git a/vendor/github.com/golang/protobuf/proto/encode.go b/vendor/github.com/golang/protobuf/proto/encode.go new file mode 100644 index 00000000..4c35d337 --- /dev/null +++ b/vendor/github.com/golang/protobuf/proto/encode.go @@ -0,0 +1,218 @@ +// Go support for Protocol Buffers - Google's data interchange format +// +// Copyright 2010 The Go Authors. All rights reserved. +// https://github.com/golang/protobuf +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package proto + +/* + * Routines for encoding data into the wire format for protocol buffers. + */ + +import ( + "errors" + "fmt" + "reflect" +) + +// RequiredNotSetError is an error type returned by either Marshal or Unmarshal. +// Marshal reports this when a required field is not initialized. +// Unmarshal reports this when a required field is missing from the wire data. +type RequiredNotSetError struct { + field string +} + +func (e *RequiredNotSetError) Error() string { + if e.field == "" { + return fmt.Sprintf("proto: required field not set") + } + return fmt.Sprintf("proto: required field %q not set", e.field) +} + +var ( + // errRepeatedHasNil is the error returned if Marshal is called with + // a struct with a repeated field containing a nil element. + errRepeatedHasNil = errors.New("proto: repeated field has nil element") + + // errOneofHasNil is the error returned if Marshal is called with + // a struct with a oneof field containing a nil element. + errOneofHasNil = errors.New("proto: oneof field has nil value") + + // ErrNil is the error returned if Marshal is called with nil. + ErrNil = errors.New("proto: Marshal called with nil") + + // ErrTooLarge is the error returned if Marshal is called with a + // message that encodes to >2GB. + ErrTooLarge = errors.New("proto: message encodes to over 2 GB") +) + +// The fundamental encoders that put bytes on the wire. +// Those that take integer types all accept uint64 and are +// therefore of type valueEncoder. + +const maxVarintBytes = 10 // maximum length of a varint + +// EncodeVarint returns the varint encoding of x. +// This is the format for the +// int32, int64, uint32, uint64, bool, and enum +// protocol buffer types. +// Not used by the package itself, but helpful to clients +// wishing to use the same encoding. +func EncodeVarint(x uint64) []byte { + var buf [maxVarintBytes]byte + var n int + for n = 0; x > 127; n++ { + buf[n] = 0x80 | uint8(x&0x7F) + x >>= 7 + } + buf[n] = uint8(x) + n++ + return buf[0:n] +} + +// EncodeVarint writes a varint-encoded integer to the Buffer. +// This is the format for the +// int32, int64, uint32, uint64, bool, and enum +// protocol buffer types. +func (p *Buffer) EncodeVarint(x uint64) error { + for x >= 1<<7 { + p.buf = append(p.buf, uint8(x&0x7f|0x80)) + x >>= 7 + } + p.buf = append(p.buf, uint8(x)) + return nil +} + +// SizeVarint returns the varint encoding size of an integer. +func SizeVarint(x uint64) int { + switch { + case x < 1<<7: + return 1 + case x < 1<<14: + return 2 + case x < 1<<21: + return 3 + case x < 1<<28: + return 4 + case x < 1<<35: + return 5 + case x < 1<<42: + return 6 + case x < 1<<49: + return 7 + case x < 1<<56: + return 8 + case x < 1<<63: + return 9 + } + return 10 +} + +// EncodeFixed64 writes a 64-bit integer to the Buffer. +// This is the format for the +// fixed64, sfixed64, and double protocol buffer types. +func (p *Buffer) EncodeFixed64(x uint64) error { + p.buf = append(p.buf, + uint8(x), + uint8(x>>8), + uint8(x>>16), + uint8(x>>24), + uint8(x>>32), + uint8(x>>40), + uint8(x>>48), + uint8(x>>56)) + return nil +} + +// EncodeFixed32 writes a 32-bit integer to the Buffer. +// This is the format for the +// fixed32, sfixed32, and float protocol buffer types. +func (p *Buffer) EncodeFixed32(x uint64) error { + p.buf = append(p.buf, + uint8(x), + uint8(x>>8), + uint8(x>>16), + uint8(x>>24)) + return nil +} + +// EncodeZigzag64 writes a zigzag-encoded 64-bit integer +// to the Buffer. +// This is the format used for the sint64 protocol buffer type. +func (p *Buffer) EncodeZigzag64(x uint64) error { + // use signed number to get arithmetic right shift. + return p.EncodeVarint(uint64((x << 1) ^ uint64((int64(x) >> 63)))) +} + +// EncodeZigzag32 writes a zigzag-encoded 32-bit integer +// to the Buffer. +// This is the format used for the sint32 protocol buffer type. +func (p *Buffer) EncodeZigzag32(x uint64) error { + // use signed number to get arithmetic right shift. + return p.EncodeVarint(uint64((uint32(x) << 1) ^ uint32((int32(x) >> 31)))) +} + +// EncodeRawBytes writes a count-delimited byte buffer to the Buffer. +// This is the format used for the bytes protocol buffer +// type and for embedded messages. +func (p *Buffer) EncodeRawBytes(b []byte) error { + p.EncodeVarint(uint64(len(b))) + p.buf = append(p.buf, b...) + return nil +} + +// EncodeStringBytes writes an encoded string to the Buffer. +// This is the format used for the proto2 string type. +func (p *Buffer) EncodeStringBytes(s string) error { + p.EncodeVarint(uint64(len(s))) + p.buf = append(p.buf, s...) + return nil +} + +// Marshaler is the interface representing objects that can marshal themselves. +type Marshaler interface { + Marshal() ([]byte, error) +} + +// EncodeMessage writes the protocol buffer to the Buffer, +// prefixed by a varint-encoded length. +func (p *Buffer) EncodeMessage(pb Message) error { + siz := Size(pb) + p.EncodeVarint(uint64(siz)) + return p.Marshal(pb) +} + +// All protocol buffer fields are nillable, but be careful. +func isNil(v reflect.Value) bool { + switch v.Kind() { + case reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice: + return v.IsNil() + } + return false +} diff --git a/vendor/github.com/golang/protobuf/proto/equal.go b/vendor/github.com/golang/protobuf/proto/equal.go new file mode 100644 index 00000000..d4db5a1c --- /dev/null +++ b/vendor/github.com/golang/protobuf/proto/equal.go @@ -0,0 +1,300 @@ +// Go support for Protocol Buffers - Google's data interchange format +// +// Copyright 2011 The Go Authors. All rights reserved. +// https://github.com/golang/protobuf +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +// Protocol buffer comparison. + +package proto + +import ( + "bytes" + "log" + "reflect" + "strings" +) + +/* +Equal returns true iff protocol buffers a and b are equal. +The arguments must both be pointers to protocol buffer structs. + +Equality is defined in this way: + - Two messages are equal iff they are the same type, + corresponding fields are equal, unknown field sets + are equal, and extensions sets are equal. + - Two set scalar fields are equal iff their values are equal. + If the fields are of a floating-point type, remember that + NaN != x for all x, including NaN. If the message is defined + in a proto3 .proto file, fields are not "set"; specifically, + zero length proto3 "bytes" fields are equal (nil == {}). + - Two repeated fields are equal iff their lengths are the same, + and their corresponding elements are equal. Note a "bytes" field, + although represented by []byte, is not a repeated field and the + rule for the scalar fields described above applies. + - Two unset fields are equal. + - Two unknown field sets are equal if their current + encoded state is equal. + - Two extension sets are equal iff they have corresponding + elements that are pairwise equal. + - Two map fields are equal iff their lengths are the same, + and they contain the same set of elements. Zero-length map + fields are equal. + - Every other combination of things are not equal. + +The return value is undefined if a and b are not protocol buffers. +*/ +func Equal(a, b Message) bool { + if a == nil || b == nil { + return a == b + } + v1, v2 := reflect.ValueOf(a), reflect.ValueOf(b) + if v1.Type() != v2.Type() { + return false + } + if v1.Kind() == reflect.Ptr { + if v1.IsNil() { + return v2.IsNil() + } + if v2.IsNil() { + return false + } + v1, v2 = v1.Elem(), v2.Elem() + } + if v1.Kind() != reflect.Struct { + return false + } + return equalStruct(v1, v2) +} + +// v1 and v2 are known to have the same type. +func equalStruct(v1, v2 reflect.Value) bool { + sprop := GetProperties(v1.Type()) + for i := 0; i < v1.NumField(); i++ { + f := v1.Type().Field(i) + if strings.HasPrefix(f.Name, "XXX_") { + continue + } + f1, f2 := v1.Field(i), v2.Field(i) + if f.Type.Kind() == reflect.Ptr { + if n1, n2 := f1.IsNil(), f2.IsNil(); n1 && n2 { + // both unset + continue + } else if n1 != n2 { + // set/unset mismatch + return false + } + f1, f2 = f1.Elem(), f2.Elem() + } + if !equalAny(f1, f2, sprop.Prop[i]) { + return false + } + } + + if em1 := v1.FieldByName("XXX_InternalExtensions"); em1.IsValid() { + em2 := v2.FieldByName("XXX_InternalExtensions") + if !equalExtensions(v1.Type(), em1.Interface().(XXX_InternalExtensions), em2.Interface().(XXX_InternalExtensions)) { + return false + } + } + + if em1 := v1.FieldByName("XXX_extensions"); em1.IsValid() { + em2 := v2.FieldByName("XXX_extensions") + if !equalExtMap(v1.Type(), em1.Interface().(map[int32]Extension), em2.Interface().(map[int32]Extension)) { + return false + } + } + + uf := v1.FieldByName("XXX_unrecognized") + if !uf.IsValid() { + return true + } + + u1 := uf.Bytes() + u2 := v2.FieldByName("XXX_unrecognized").Bytes() + return bytes.Equal(u1, u2) +} + +// v1 and v2 are known to have the same type. +// prop may be nil. +func equalAny(v1, v2 reflect.Value, prop *Properties) bool { + if v1.Type() == protoMessageType { + m1, _ := v1.Interface().(Message) + m2, _ := v2.Interface().(Message) + return Equal(m1, m2) + } + switch v1.Kind() { + case reflect.Bool: + return v1.Bool() == v2.Bool() + case reflect.Float32, reflect.Float64: + return v1.Float() == v2.Float() + case reflect.Int32, reflect.Int64: + return v1.Int() == v2.Int() + case reflect.Interface: + // Probably a oneof field; compare the inner values. + n1, n2 := v1.IsNil(), v2.IsNil() + if n1 || n2 { + return n1 == n2 + } + e1, e2 := v1.Elem(), v2.Elem() + if e1.Type() != e2.Type() { + return false + } + return equalAny(e1, e2, nil) + case reflect.Map: + if v1.Len() != v2.Len() { + return false + } + for _, key := range v1.MapKeys() { + val2 := v2.MapIndex(key) + if !val2.IsValid() { + // This key was not found in the second map. + return false + } + if !equalAny(v1.MapIndex(key), val2, nil) { + return false + } + } + return true + case reflect.Ptr: + // Maps may have nil values in them, so check for nil. + if v1.IsNil() && v2.IsNil() { + return true + } + if v1.IsNil() != v2.IsNil() { + return false + } + return equalAny(v1.Elem(), v2.Elem(), prop) + case reflect.Slice: + if v1.Type().Elem().Kind() == reflect.Uint8 { + // short circuit: []byte + + // Edge case: if this is in a proto3 message, a zero length + // bytes field is considered the zero value. + if prop != nil && prop.proto3 && v1.Len() == 0 && v2.Len() == 0 { + return true + } + if v1.IsNil() != v2.IsNil() { + return false + } + return bytes.Equal(v1.Interface().([]byte), v2.Interface().([]byte)) + } + + if v1.Len() != v2.Len() { + return false + } + for i := 0; i < v1.Len(); i++ { + if !equalAny(v1.Index(i), v2.Index(i), prop) { + return false + } + } + return true + case reflect.String: + return v1.Interface().(string) == v2.Interface().(string) + case reflect.Struct: + return equalStruct(v1, v2) + case reflect.Uint32, reflect.Uint64: + return v1.Uint() == v2.Uint() + } + + // unknown type, so not a protocol buffer + log.Printf("proto: don't know how to compare %v", v1) + return false +} + +// base is the struct type that the extensions are based on. +// x1 and x2 are InternalExtensions. +func equalExtensions(base reflect.Type, x1, x2 XXX_InternalExtensions) bool { + em1, _ := x1.extensionsRead() + em2, _ := x2.extensionsRead() + return equalExtMap(base, em1, em2) +} + +func equalExtMap(base reflect.Type, em1, em2 map[int32]Extension) bool { + if len(em1) != len(em2) { + return false + } + + for extNum, e1 := range em1 { + e2, ok := em2[extNum] + if !ok { + return false + } + + m1, m2 := e1.value, e2.value + + if m1 == nil && m2 == nil { + // Both have only encoded form. + if bytes.Equal(e1.enc, e2.enc) { + continue + } + // The bytes are different, but the extensions might still be + // equal. We need to decode them to compare. + } + + if m1 != nil && m2 != nil { + // Both are unencoded. + if !equalAny(reflect.ValueOf(m1), reflect.ValueOf(m2), nil) { + return false + } + continue + } + + // At least one is encoded. To do a semantically correct comparison + // we need to unmarshal them first. + var desc *ExtensionDesc + if m := extensionMaps[base]; m != nil { + desc = m[extNum] + } + if desc == nil { + // If both have only encoded form and the bytes are the same, + // it is handled above. We get here when the bytes are different. + // We don't know how to decode it, so just compare them as byte + // slices. + log.Printf("proto: don't know how to compare extension %d of %v", extNum, base) + return false + } + var err error + if m1 == nil { + m1, err = decodeExtension(e1.enc, desc) + } + if m2 == nil && err == nil { + m2, err = decodeExtension(e2.enc, desc) + } + if err != nil { + // The encoded form is invalid. + log.Printf("proto: badly encoded extension %d of %v: %v", extNum, base, err) + return false + } + if !equalAny(reflect.ValueOf(m1), reflect.ValueOf(m2), nil) { + return false + } + } + + return true +} diff --git a/vendor/github.com/golang/protobuf/proto/extensions.go b/vendor/github.com/golang/protobuf/proto/extensions.go new file mode 100644 index 00000000..816a3b9d --- /dev/null +++ b/vendor/github.com/golang/protobuf/proto/extensions.go @@ -0,0 +1,543 @@ +// Go support for Protocol Buffers - Google's data interchange format +// +// Copyright 2010 The Go Authors. All rights reserved. +// https://github.com/golang/protobuf +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package proto + +/* + * Types and routines for supporting protocol buffer extensions. + */ + +import ( + "errors" + "fmt" + "io" + "reflect" + "strconv" + "sync" +) + +// ErrMissingExtension is the error returned by GetExtension if the named extension is not in the message. +var ErrMissingExtension = errors.New("proto: missing extension") + +// ExtensionRange represents a range of message extensions for a protocol buffer. +// Used in code generated by the protocol compiler. +type ExtensionRange struct { + Start, End int32 // both inclusive +} + +// extendableProto is an interface implemented by any protocol buffer generated by the current +// proto compiler that may be extended. +type extendableProto interface { + Message + ExtensionRangeArray() []ExtensionRange + extensionsWrite() map[int32]Extension + extensionsRead() (map[int32]Extension, sync.Locker) +} + +// extendableProtoV1 is an interface implemented by a protocol buffer generated by the previous +// version of the proto compiler that may be extended. +type extendableProtoV1 interface { + Message + ExtensionRangeArray() []ExtensionRange + ExtensionMap() map[int32]Extension +} + +// extensionAdapter is a wrapper around extendableProtoV1 that implements extendableProto. +type extensionAdapter struct { + extendableProtoV1 +} + +func (e extensionAdapter) extensionsWrite() map[int32]Extension { + return e.ExtensionMap() +} + +func (e extensionAdapter) extensionsRead() (map[int32]Extension, sync.Locker) { + return e.ExtensionMap(), notLocker{} +} + +// notLocker is a sync.Locker whose Lock and Unlock methods are nops. +type notLocker struct{} + +func (n notLocker) Lock() {} +func (n notLocker) Unlock() {} + +// extendable returns the extendableProto interface for the given generated proto message. +// If the proto message has the old extension format, it returns a wrapper that implements +// the extendableProto interface. +func extendable(p interface{}) (extendableProto, error) { + switch p := p.(type) { + case extendableProto: + if isNilPtr(p) { + return nil, fmt.Errorf("proto: nil %T is not extendable", p) + } + return p, nil + case extendableProtoV1: + if isNilPtr(p) { + return nil, fmt.Errorf("proto: nil %T is not extendable", p) + } + return extensionAdapter{p}, nil + } + // Don't allocate a specific error containing %T: + // this is the hot path for Clone and MarshalText. + return nil, errNotExtendable +} + +var errNotExtendable = errors.New("proto: not an extendable proto.Message") + +func isNilPtr(x interface{}) bool { + v := reflect.ValueOf(x) + return v.Kind() == reflect.Ptr && v.IsNil() +} + +// XXX_InternalExtensions is an internal representation of proto extensions. +// +// Each generated message struct type embeds an anonymous XXX_InternalExtensions field, +// thus gaining the unexported 'extensions' method, which can be called only from the proto package. +// +// The methods of XXX_InternalExtensions are not concurrency safe in general, +// but calls to logically read-only methods such as has and get may be executed concurrently. +type XXX_InternalExtensions struct { + // The struct must be indirect so that if a user inadvertently copies a + // generated message and its embedded XXX_InternalExtensions, they + // avoid the mayhem of a copied mutex. + // + // The mutex serializes all logically read-only operations to p.extensionMap. + // It is up to the client to ensure that write operations to p.extensionMap are + // mutually exclusive with other accesses. + p *struct { + mu sync.Mutex + extensionMap map[int32]Extension + } +} + +// extensionsWrite returns the extension map, creating it on first use. +func (e *XXX_InternalExtensions) extensionsWrite() map[int32]Extension { + if e.p == nil { + e.p = new(struct { + mu sync.Mutex + extensionMap map[int32]Extension + }) + e.p.extensionMap = make(map[int32]Extension) + } + return e.p.extensionMap +} + +// extensionsRead returns the extensions map for read-only use. It may be nil. +// The caller must hold the returned mutex's lock when accessing Elements within the map. +func (e *XXX_InternalExtensions) extensionsRead() (map[int32]Extension, sync.Locker) { + if e.p == nil { + return nil, nil + } + return e.p.extensionMap, &e.p.mu +} + +// ExtensionDesc represents an extension specification. +// Used in generated code from the protocol compiler. +type ExtensionDesc struct { + ExtendedType Message // nil pointer to the type that is being extended + ExtensionType interface{} // nil pointer to the extension type + Field int32 // field number + Name string // fully-qualified name of extension, for text formatting + Tag string // protobuf tag style + Filename string // name of the file in which the extension is defined +} + +func (ed *ExtensionDesc) repeated() bool { + t := reflect.TypeOf(ed.ExtensionType) + return t.Kind() == reflect.Slice && t.Elem().Kind() != reflect.Uint8 +} + +// Extension represents an extension in a message. +type Extension struct { + // When an extension is stored in a message using SetExtension + // only desc and value are set. When the message is marshaled + // enc will be set to the encoded form of the message. + // + // When a message is unmarshaled and contains extensions, each + // extension will have only enc set. When such an extension is + // accessed using GetExtension (or GetExtensions) desc and value + // will be set. + desc *ExtensionDesc + value interface{} + enc []byte +} + +// SetRawExtension is for testing only. +func SetRawExtension(base Message, id int32, b []byte) { + epb, err := extendable(base) + if err != nil { + return + } + extmap := epb.extensionsWrite() + extmap[id] = Extension{enc: b} +} + +// isExtensionField returns true iff the given field number is in an extension range. +func isExtensionField(pb extendableProto, field int32) bool { + for _, er := range pb.ExtensionRangeArray() { + if er.Start <= field && field <= er.End { + return true + } + } + return false +} + +// checkExtensionTypes checks that the given extension is valid for pb. +func checkExtensionTypes(pb extendableProto, extension *ExtensionDesc) error { + var pbi interface{} = pb + // Check the extended type. + if ea, ok := pbi.(extensionAdapter); ok { + pbi = ea.extendableProtoV1 + } + if a, b := reflect.TypeOf(pbi), reflect.TypeOf(extension.ExtendedType); a != b { + return fmt.Errorf("proto: bad extended type; %v does not extend %v", b, a) + } + // Check the range. + if !isExtensionField(pb, extension.Field) { + return errors.New("proto: bad extension number; not in declared ranges") + } + return nil +} + +// extPropKey is sufficient to uniquely identify an extension. +type extPropKey struct { + base reflect.Type + field int32 +} + +var extProp = struct { + sync.RWMutex + m map[extPropKey]*Properties +}{ + m: make(map[extPropKey]*Properties), +} + +func extensionProperties(ed *ExtensionDesc) *Properties { + key := extPropKey{base: reflect.TypeOf(ed.ExtendedType), field: ed.Field} + + extProp.RLock() + if prop, ok := extProp.m[key]; ok { + extProp.RUnlock() + return prop + } + extProp.RUnlock() + + extProp.Lock() + defer extProp.Unlock() + // Check again. + if prop, ok := extProp.m[key]; ok { + return prop + } + + prop := new(Properties) + prop.Init(reflect.TypeOf(ed.ExtensionType), "unknown_name", ed.Tag, nil) + extProp.m[key] = prop + return prop +} + +// HasExtension returns whether the given extension is present in pb. +func HasExtension(pb Message, extension *ExtensionDesc) bool { + // TODO: Check types, field numbers, etc.? + epb, err := extendable(pb) + if err != nil { + return false + } + extmap, mu := epb.extensionsRead() + if extmap == nil { + return false + } + mu.Lock() + _, ok := extmap[extension.Field] + mu.Unlock() + return ok +} + +// ClearExtension removes the given extension from pb. +func ClearExtension(pb Message, extension *ExtensionDesc) { + epb, err := extendable(pb) + if err != nil { + return + } + // TODO: Check types, field numbers, etc.? + extmap := epb.extensionsWrite() + delete(extmap, extension.Field) +} + +// GetExtension retrieves a proto2 extended field from pb. +// +// If the descriptor is type complete (i.e., ExtensionDesc.ExtensionType is non-nil), +// then GetExtension parses the encoded field and returns a Go value of the specified type. +// If the field is not present, then the default value is returned (if one is specified), +// otherwise ErrMissingExtension is reported. +// +// If the descriptor is not type complete (i.e., ExtensionDesc.ExtensionType is nil), +// then GetExtension returns the raw encoded bytes of the field extension. +func GetExtension(pb Message, extension *ExtensionDesc) (interface{}, error) { + epb, err := extendable(pb) + if err != nil { + return nil, err + } + + if extension.ExtendedType != nil { + // can only check type if this is a complete descriptor + if err := checkExtensionTypes(epb, extension); err != nil { + return nil, err + } + } + + emap, mu := epb.extensionsRead() + if emap == nil { + return defaultExtensionValue(extension) + } + mu.Lock() + defer mu.Unlock() + e, ok := emap[extension.Field] + if !ok { + // defaultExtensionValue returns the default value or + // ErrMissingExtension if there is no default. + return defaultExtensionValue(extension) + } + + if e.value != nil { + // Already decoded. Check the descriptor, though. + if e.desc != extension { + // This shouldn't happen. If it does, it means that + // GetExtension was called twice with two different + // descriptors with the same field number. + return nil, errors.New("proto: descriptor conflict") + } + return e.value, nil + } + + if extension.ExtensionType == nil { + // incomplete descriptor + return e.enc, nil + } + + v, err := decodeExtension(e.enc, extension) + if err != nil { + return nil, err + } + + // Remember the decoded version and drop the encoded version. + // That way it is safe to mutate what we return. + e.value = v + e.desc = extension + e.enc = nil + emap[extension.Field] = e + return e.value, nil +} + +// defaultExtensionValue returns the default value for extension. +// If no default for an extension is defined ErrMissingExtension is returned. +func defaultExtensionValue(extension *ExtensionDesc) (interface{}, error) { + if extension.ExtensionType == nil { + // incomplete descriptor, so no default + return nil, ErrMissingExtension + } + + t := reflect.TypeOf(extension.ExtensionType) + props := extensionProperties(extension) + + sf, _, err := fieldDefault(t, props) + if err != nil { + return nil, err + } + + if sf == nil || sf.value == nil { + // There is no default value. + return nil, ErrMissingExtension + } + + if t.Kind() != reflect.Ptr { + // We do not need to return a Ptr, we can directly return sf.value. + return sf.value, nil + } + + // We need to return an interface{} that is a pointer to sf.value. + value := reflect.New(t).Elem() + value.Set(reflect.New(value.Type().Elem())) + if sf.kind == reflect.Int32 { + // We may have an int32 or an enum, but the underlying data is int32. + // Since we can't set an int32 into a non int32 reflect.value directly + // set it as a int32. + value.Elem().SetInt(int64(sf.value.(int32))) + } else { + value.Elem().Set(reflect.ValueOf(sf.value)) + } + return value.Interface(), nil +} + +// decodeExtension decodes an extension encoded in b. +func decodeExtension(b []byte, extension *ExtensionDesc) (interface{}, error) { + t := reflect.TypeOf(extension.ExtensionType) + unmarshal := typeUnmarshaler(t, extension.Tag) + + // t is a pointer to a struct, pointer to basic type or a slice. + // Allocate space to store the pointer/slice. + value := reflect.New(t).Elem() + + var err error + for { + x, n := decodeVarint(b) + if n == 0 { + return nil, io.ErrUnexpectedEOF + } + b = b[n:] + wire := int(x) & 7 + + b, err = unmarshal(b, valToPointer(value.Addr()), wire) + if err != nil { + return nil, err + } + + if len(b) == 0 { + break + } + } + return value.Interface(), nil +} + +// GetExtensions returns a slice of the extensions present in pb that are also listed in es. +// The returned slice has the same length as es; missing extensions will appear as nil elements. +func GetExtensions(pb Message, es []*ExtensionDesc) (extensions []interface{}, err error) { + epb, err := extendable(pb) + if err != nil { + return nil, err + } + extensions = make([]interface{}, len(es)) + for i, e := range es { + extensions[i], err = GetExtension(epb, e) + if err == ErrMissingExtension { + err = nil + } + if err != nil { + return + } + } + return +} + +// ExtensionDescs returns a new slice containing pb's extension descriptors, in undefined order. +// For non-registered extensions, ExtensionDescs returns an incomplete descriptor containing +// just the Field field, which defines the extension's field number. +func ExtensionDescs(pb Message) ([]*ExtensionDesc, error) { + epb, err := extendable(pb) + if err != nil { + return nil, err + } + registeredExtensions := RegisteredExtensions(pb) + + emap, mu := epb.extensionsRead() + if emap == nil { + return nil, nil + } + mu.Lock() + defer mu.Unlock() + extensions := make([]*ExtensionDesc, 0, len(emap)) + for extid, e := range emap { + desc := e.desc + if desc == nil { + desc = registeredExtensions[extid] + if desc == nil { + desc = &ExtensionDesc{Field: extid} + } + } + + extensions = append(extensions, desc) + } + return extensions, nil +} + +// SetExtension sets the specified extension of pb to the specified value. +func SetExtension(pb Message, extension *ExtensionDesc, value interface{}) error { + epb, err := extendable(pb) + if err != nil { + return err + } + if err := checkExtensionTypes(epb, extension); err != nil { + return err + } + typ := reflect.TypeOf(extension.ExtensionType) + if typ != reflect.TypeOf(value) { + return errors.New("proto: bad extension value type") + } + // nil extension values need to be caught early, because the + // encoder can't distinguish an ErrNil due to a nil extension + // from an ErrNil due to a missing field. Extensions are + // always optional, so the encoder would just swallow the error + // and drop all the extensions from the encoded message. + if reflect.ValueOf(value).IsNil() { + return fmt.Errorf("proto: SetExtension called with nil value of type %T", value) + } + + extmap := epb.extensionsWrite() + extmap[extension.Field] = Extension{desc: extension, value: value} + return nil +} + +// ClearAllExtensions clears all extensions from pb. +func ClearAllExtensions(pb Message) { + epb, err := extendable(pb) + if err != nil { + return + } + m := epb.extensionsWrite() + for k := range m { + delete(m, k) + } +} + +// A global registry of extensions. +// The generated code will register the generated descriptors by calling RegisterExtension. + +var extensionMaps = make(map[reflect.Type]map[int32]*ExtensionDesc) + +// RegisterExtension is called from the generated code. +func RegisterExtension(desc *ExtensionDesc) { + st := reflect.TypeOf(desc.ExtendedType).Elem() + m := extensionMaps[st] + if m == nil { + m = make(map[int32]*ExtensionDesc) + extensionMaps[st] = m + } + if _, ok := m[desc.Field]; ok { + panic("proto: duplicate extension registered: " + st.String() + " " + strconv.Itoa(int(desc.Field))) + } + m[desc.Field] = desc +} + +// RegisteredExtensions returns a map of the registered extensions of a +// protocol buffer struct, indexed by the extension number. +// The argument pb should be a nil pointer to the struct type. +func RegisteredExtensions(pb Message) map[int32]*ExtensionDesc { + return extensionMaps[reflect.TypeOf(pb).Elem()] +} diff --git a/vendor/github.com/golang/protobuf/proto/lib.go b/vendor/github.com/golang/protobuf/proto/lib.go new file mode 100644 index 00000000..0e2191b8 --- /dev/null +++ b/vendor/github.com/golang/protobuf/proto/lib.go @@ -0,0 +1,921 @@ +// Go support for Protocol Buffers - Google's data interchange format +// +// Copyright 2010 The Go Authors. All rights reserved. +// https://github.com/golang/protobuf +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/* +Package proto converts data structures to and from the wire format of +protocol buffers. It works in concert with the Go source code generated +for .proto files by the protocol compiler. + +A summary of the properties of the protocol buffer interface +for a protocol buffer variable v: + + - Names are turned from camel_case to CamelCase for export. + - There are no methods on v to set fields; just treat + them as structure fields. + - There are getters that return a field's value if set, + and return the field's default value if unset. + The getters work even if the receiver is a nil message. + - The zero value for a struct is its correct initialization state. + All desired fields must be set before marshaling. + - A Reset() method will restore a protobuf struct to its zero state. + - Non-repeated fields are pointers to the values; nil means unset. + That is, optional or required field int32 f becomes F *int32. + - Repeated fields are slices. + - Helper functions are available to aid the setting of fields. + msg.Foo = proto.String("hello") // set field + - Constants are defined to hold the default values of all fields that + have them. They have the form Default_StructName_FieldName. + Because the getter methods handle defaulted values, + direct use of these constants should be rare. + - Enums are given type names and maps from names to values. + Enum values are prefixed by the enclosing message's name, or by the + enum's type name if it is a top-level enum. Enum types have a String + method, and a Enum method to assist in message construction. + - Nested messages, groups and enums have type names prefixed with the name of + the surrounding message type. + - Extensions are given descriptor names that start with E_, + followed by an underscore-delimited list of the nested messages + that contain it (if any) followed by the CamelCased name of the + extension field itself. HasExtension, ClearExtension, GetExtension + and SetExtension are functions for manipulating extensions. + - Oneof field sets are given a single field in their message, + with distinguished wrapper types for each possible field value. + - Marshal and Unmarshal are functions to encode and decode the wire format. + +When the .proto file specifies `syntax="proto3"`, there are some differences: + + - Non-repeated fields of non-message type are values instead of pointers. + - Enum types do not get an Enum method. + +The simplest way to describe this is to see an example. +Given file test.proto, containing + + package example; + + enum FOO { X = 17; } + + message Test { + required string label = 1; + optional int32 type = 2 [default=77]; + repeated int64 reps = 3; + optional group OptionalGroup = 4 { + required string RequiredField = 5; + } + oneof union { + int32 number = 6; + string name = 7; + } + } + +The resulting file, test.pb.go, is: + + package example + + import proto "github.com/golang/protobuf/proto" + import math "math" + + type FOO int32 + const ( + FOO_X FOO = 17 + ) + var FOO_name = map[int32]string{ + 17: "X", + } + var FOO_value = map[string]int32{ + "X": 17, + } + + func (x FOO) Enum() *FOO { + p := new(FOO) + *p = x + return p + } + func (x FOO) String() string { + return proto.EnumName(FOO_name, int32(x)) + } + func (x *FOO) UnmarshalJSON(data []byte) error { + value, err := proto.UnmarshalJSONEnum(FOO_value, data) + if err != nil { + return err + } + *x = FOO(value) + return nil + } + + type Test struct { + Label *string `protobuf:"bytes,1,req,name=label" json:"label,omitempty"` + Type *int32 `protobuf:"varint,2,opt,name=type,def=77" json:"type,omitempty"` + Reps []int64 `protobuf:"varint,3,rep,name=reps" json:"reps,omitempty"` + Optionalgroup *Test_OptionalGroup `protobuf:"group,4,opt,name=OptionalGroup" json:"optionalgroup,omitempty"` + // Types that are valid to be assigned to Union: + // *Test_Number + // *Test_Name + Union isTest_Union `protobuf_oneof:"union"` + XXX_unrecognized []byte `json:"-"` + } + func (m *Test) Reset() { *m = Test{} } + func (m *Test) String() string { return proto.CompactTextString(m) } + func (*Test) ProtoMessage() {} + + type isTest_Union interface { + isTest_Union() + } + + type Test_Number struct { + Number int32 `protobuf:"varint,6,opt,name=number"` + } + type Test_Name struct { + Name string `protobuf:"bytes,7,opt,name=name"` + } + + func (*Test_Number) isTest_Union() {} + func (*Test_Name) isTest_Union() {} + + func (m *Test) GetUnion() isTest_Union { + if m != nil { + return m.Union + } + return nil + } + const Default_Test_Type int32 = 77 + + func (m *Test) GetLabel() string { + if m != nil && m.Label != nil { + return *m.Label + } + return "" + } + + func (m *Test) GetType() int32 { + if m != nil && m.Type != nil { + return *m.Type + } + return Default_Test_Type + } + + func (m *Test) GetOptionalgroup() *Test_OptionalGroup { + if m != nil { + return m.Optionalgroup + } + return nil + } + + type Test_OptionalGroup struct { + RequiredField *string `protobuf:"bytes,5,req" json:"RequiredField,omitempty"` + } + func (m *Test_OptionalGroup) Reset() { *m = Test_OptionalGroup{} } + func (m *Test_OptionalGroup) String() string { return proto.CompactTextString(m) } + + func (m *Test_OptionalGroup) GetRequiredField() string { + if m != nil && m.RequiredField != nil { + return *m.RequiredField + } + return "" + } + + func (m *Test) GetNumber() int32 { + if x, ok := m.GetUnion().(*Test_Number); ok { + return x.Number + } + return 0 + } + + func (m *Test) GetName() string { + if x, ok := m.GetUnion().(*Test_Name); ok { + return x.Name + } + return "" + } + + func init() { + proto.RegisterEnum("example.FOO", FOO_name, FOO_value) + } + +To create and play with a Test object: + + package main + + import ( + "log" + + "github.com/golang/protobuf/proto" + pb "./example.pb" + ) + + func main() { + test := &pb.Test{ + Label: proto.String("hello"), + Type: proto.Int32(17), + Reps: []int64{1, 2, 3}, + Optionalgroup: &pb.Test_OptionalGroup{ + RequiredField: proto.String("good bye"), + }, + Union: &pb.Test_Name{"fred"}, + } + data, err := proto.Marshal(test) + if err != nil { + log.Fatal("marshaling error: ", err) + } + newTest := &pb.Test{} + err = proto.Unmarshal(data, newTest) + if err != nil { + log.Fatal("unmarshaling error: ", err) + } + // Now test and newTest contain the same data. + if test.GetLabel() != newTest.GetLabel() { + log.Fatalf("data mismatch %q != %q", test.GetLabel(), newTest.GetLabel()) + } + // Use a type switch to determine which oneof was set. + switch u := test.Union.(type) { + case *pb.Test_Number: // u.Number contains the number. + case *pb.Test_Name: // u.Name contains the string. + } + // etc. + } +*/ +package proto + +import ( + "encoding/json" + "errors" + "fmt" + "log" + "reflect" + "sort" + "strconv" + "sync" +) + +var errInvalidUTF8 = errors.New("proto: invalid UTF-8 string") + +// Message is implemented by generated protocol buffer messages. +type Message interface { + Reset() + String() string + ProtoMessage() +} + +// Stats records allocation details about the protocol buffer encoders +// and decoders. Useful for tuning the library itself. +type Stats struct { + Emalloc uint64 // mallocs in encode + Dmalloc uint64 // mallocs in decode + Encode uint64 // number of encodes + Decode uint64 // number of decodes + Chit uint64 // number of cache hits + Cmiss uint64 // number of cache misses + Size uint64 // number of sizes +} + +// Set to true to enable stats collection. +const collectStats = false + +var stats Stats + +// GetStats returns a copy of the global Stats structure. +func GetStats() Stats { return stats } + +// A Buffer is a buffer manager for marshaling and unmarshaling +// protocol buffers. It may be reused between invocations to +// reduce memory usage. It is not necessary to use a Buffer; +// the global functions Marshal and Unmarshal create a +// temporary Buffer and are fine for most applications. +type Buffer struct { + buf []byte // encode/decode byte stream + index int // read point + + deterministic bool +} + +// NewBuffer allocates a new Buffer and initializes its internal data to +// the contents of the argument slice. +func NewBuffer(e []byte) *Buffer { + return &Buffer{buf: e} +} + +// Reset resets the Buffer, ready for marshaling a new protocol buffer. +func (p *Buffer) Reset() { + p.buf = p.buf[0:0] // for reading/writing + p.index = 0 // for reading +} + +// SetBuf replaces the internal buffer with the slice, +// ready for unmarshaling the contents of the slice. +func (p *Buffer) SetBuf(s []byte) { + p.buf = s + p.index = 0 +} + +// Bytes returns the contents of the Buffer. +func (p *Buffer) Bytes() []byte { return p.buf } + +// SetDeterministic sets whether to use deterministic serialization. +// +// Deterministic serialization guarantees that for a given binary, equal +// messages will always be serialized to the same bytes. This implies: +// +// - Repeated serialization of a message will return the same bytes. +// - Different processes of the same binary (which may be executing on +// different machines) will serialize equal messages to the same bytes. +// +// Note that the deterministic serialization is NOT canonical across +// languages. It is not guaranteed to remain stable over time. It is unstable +// across different builds with schema changes due to unknown fields. +// Users who need canonical serialization (e.g., persistent storage in a +// canonical form, fingerprinting, etc.) should define their own +// canonicalization specification and implement their own serializer rather +// than relying on this API. +// +// If deterministic serialization is requested, map entries will be sorted +// by keys in lexographical order. This is an implementation detail and +// subject to change. +func (p *Buffer) SetDeterministic(deterministic bool) { + p.deterministic = deterministic +} + +/* + * Helper routines for simplifying the creation of optional fields of basic type. + */ + +// Bool is a helper routine that allocates a new bool value +// to store v and returns a pointer to it. +func Bool(v bool) *bool { + return &v +} + +// Int32 is a helper routine that allocates a new int32 value +// to store v and returns a pointer to it. +func Int32(v int32) *int32 { + return &v +} + +// Int is a helper routine that allocates a new int32 value +// to store v and returns a pointer to it, but unlike Int32 +// its argument value is an int. +func Int(v int) *int32 { + p := new(int32) + *p = int32(v) + return p +} + +// Int64 is a helper routine that allocates a new int64 value +// to store v and returns a pointer to it. +func Int64(v int64) *int64 { + return &v +} + +// Float32 is a helper routine that allocates a new float32 value +// to store v and returns a pointer to it. +func Float32(v float32) *float32 { + return &v +} + +// Float64 is a helper routine that allocates a new float64 value +// to store v and returns a pointer to it. +func Float64(v float64) *float64 { + return &v +} + +// Uint32 is a helper routine that allocates a new uint32 value +// to store v and returns a pointer to it. +func Uint32(v uint32) *uint32 { + return &v +} + +// Uint64 is a helper routine that allocates a new uint64 value +// to store v and returns a pointer to it. +func Uint64(v uint64) *uint64 { + return &v +} + +// String is a helper routine that allocates a new string value +// to store v and returns a pointer to it. +func String(v string) *string { + return &v +} + +// EnumName is a helper function to simplify printing protocol buffer enums +// by name. Given an enum map and a value, it returns a useful string. +func EnumName(m map[int32]string, v int32) string { + s, ok := m[v] + if ok { + return s + } + return strconv.Itoa(int(v)) +} + +// UnmarshalJSONEnum is a helper function to simplify recovering enum int values +// from their JSON-encoded representation. Given a map from the enum's symbolic +// names to its int values, and a byte buffer containing the JSON-encoded +// value, it returns an int32 that can be cast to the enum type by the caller. +// +// The function can deal with both JSON representations, numeric and symbolic. +func UnmarshalJSONEnum(m map[string]int32, data []byte, enumName string) (int32, error) { + if data[0] == '"' { + // New style: enums are strings. + var repr string + if err := json.Unmarshal(data, &repr); err != nil { + return -1, err + } + val, ok := m[repr] + if !ok { + return 0, fmt.Errorf("unrecognized enum %s value %q", enumName, repr) + } + return val, nil + } + // Old style: enums are ints. + var val int32 + if err := json.Unmarshal(data, &val); err != nil { + return 0, fmt.Errorf("cannot unmarshal %#q into enum %s", data, enumName) + } + return val, nil +} + +// DebugPrint dumps the encoded data in b in a debugging format with a header +// including the string s. Used in testing but made available for general debugging. +func (p *Buffer) DebugPrint(s string, b []byte) { + var u uint64 + + obuf := p.buf + index := p.index + p.buf = b + p.index = 0 + depth := 0 + + fmt.Printf("\n--- %s ---\n", s) + +out: + for { + for i := 0; i < depth; i++ { + fmt.Print(" ") + } + + index := p.index + if index == len(p.buf) { + break + } + + op, err := p.DecodeVarint() + if err != nil { + fmt.Printf("%3d: fetching op err %v\n", index, err) + break out + } + tag := op >> 3 + wire := op & 7 + + switch wire { + default: + fmt.Printf("%3d: t=%3d unknown wire=%d\n", + index, tag, wire) + break out + + case WireBytes: + var r []byte + + r, err = p.DecodeRawBytes(false) + if err != nil { + break out + } + fmt.Printf("%3d: t=%3d bytes [%d]", index, tag, len(r)) + if len(r) <= 6 { + for i := 0; i < len(r); i++ { + fmt.Printf(" %.2x", r[i]) + } + } else { + for i := 0; i < 3; i++ { + fmt.Printf(" %.2x", r[i]) + } + fmt.Printf(" ..") + for i := len(r) - 3; i < len(r); i++ { + fmt.Printf(" %.2x", r[i]) + } + } + fmt.Printf("\n") + + case WireFixed32: + u, err = p.DecodeFixed32() + if err != nil { + fmt.Printf("%3d: t=%3d fix32 err %v\n", index, tag, err) + break out + } + fmt.Printf("%3d: t=%3d fix32 %d\n", index, tag, u) + + case WireFixed64: + u, err = p.DecodeFixed64() + if err != nil { + fmt.Printf("%3d: t=%3d fix64 err %v\n", index, tag, err) + break out + } + fmt.Printf("%3d: t=%3d fix64 %d\n", index, tag, u) + + case WireVarint: + u, err = p.DecodeVarint() + if err != nil { + fmt.Printf("%3d: t=%3d varint err %v\n", index, tag, err) + break out + } + fmt.Printf("%3d: t=%3d varint %d\n", index, tag, u) + + case WireStartGroup: + fmt.Printf("%3d: t=%3d start\n", index, tag) + depth++ + + case WireEndGroup: + depth-- + fmt.Printf("%3d: t=%3d end\n", index, tag) + } + } + + if depth != 0 { + fmt.Printf("%3d: start-end not balanced %d\n", p.index, depth) + } + fmt.Printf("\n") + + p.buf = obuf + p.index = index +} + +// SetDefaults sets unset protocol buffer fields to their default values. +// It only modifies fields that are both unset and have defined defaults. +// It recursively sets default values in any non-nil sub-messages. +func SetDefaults(pb Message) { + setDefaults(reflect.ValueOf(pb), true, false) +} + +// v is a pointer to a struct. +func setDefaults(v reflect.Value, recur, zeros bool) { + v = v.Elem() + + defaultMu.RLock() + dm, ok := defaults[v.Type()] + defaultMu.RUnlock() + if !ok { + dm = buildDefaultMessage(v.Type()) + defaultMu.Lock() + defaults[v.Type()] = dm + defaultMu.Unlock() + } + + for _, sf := range dm.scalars { + f := v.Field(sf.index) + if !f.IsNil() { + // field already set + continue + } + dv := sf.value + if dv == nil && !zeros { + // no explicit default, and don't want to set zeros + continue + } + fptr := f.Addr().Interface() // **T + // TODO: Consider batching the allocations we do here. + switch sf.kind { + case reflect.Bool: + b := new(bool) + if dv != nil { + *b = dv.(bool) + } + *(fptr.(**bool)) = b + case reflect.Float32: + f := new(float32) + if dv != nil { + *f = dv.(float32) + } + *(fptr.(**float32)) = f + case reflect.Float64: + f := new(float64) + if dv != nil { + *f = dv.(float64) + } + *(fptr.(**float64)) = f + case reflect.Int32: + // might be an enum + if ft := f.Type(); ft != int32PtrType { + // enum + f.Set(reflect.New(ft.Elem())) + if dv != nil { + f.Elem().SetInt(int64(dv.(int32))) + } + } else { + // int32 field + i := new(int32) + if dv != nil { + *i = dv.(int32) + } + *(fptr.(**int32)) = i + } + case reflect.Int64: + i := new(int64) + if dv != nil { + *i = dv.(int64) + } + *(fptr.(**int64)) = i + case reflect.String: + s := new(string) + if dv != nil { + *s = dv.(string) + } + *(fptr.(**string)) = s + case reflect.Uint8: + // exceptional case: []byte + var b []byte + if dv != nil { + db := dv.([]byte) + b = make([]byte, len(db)) + copy(b, db) + } else { + b = []byte{} + } + *(fptr.(*[]byte)) = b + case reflect.Uint32: + u := new(uint32) + if dv != nil { + *u = dv.(uint32) + } + *(fptr.(**uint32)) = u + case reflect.Uint64: + u := new(uint64) + if dv != nil { + *u = dv.(uint64) + } + *(fptr.(**uint64)) = u + default: + log.Printf("proto: can't set default for field %v (sf.kind=%v)", f, sf.kind) + } + } + + for _, ni := range dm.nested { + f := v.Field(ni) + // f is *T or []*T or map[T]*T + switch f.Kind() { + case reflect.Ptr: + if f.IsNil() { + continue + } + setDefaults(f, recur, zeros) + + case reflect.Slice: + for i := 0; i < f.Len(); i++ { + e := f.Index(i) + if e.IsNil() { + continue + } + setDefaults(e, recur, zeros) + } + + case reflect.Map: + for _, k := range f.MapKeys() { + e := f.MapIndex(k) + if e.IsNil() { + continue + } + setDefaults(e, recur, zeros) + } + } + } +} + +var ( + // defaults maps a protocol buffer struct type to a slice of the fields, + // with its scalar fields set to their proto-declared non-zero default values. + defaultMu sync.RWMutex + defaults = make(map[reflect.Type]defaultMessage) + + int32PtrType = reflect.TypeOf((*int32)(nil)) +) + +// defaultMessage represents information about the default values of a message. +type defaultMessage struct { + scalars []scalarField + nested []int // struct field index of nested messages +} + +type scalarField struct { + index int // struct field index + kind reflect.Kind // element type (the T in *T or []T) + value interface{} // the proto-declared default value, or nil +} + +// t is a struct type. +func buildDefaultMessage(t reflect.Type) (dm defaultMessage) { + sprop := GetProperties(t) + for _, prop := range sprop.Prop { + fi, ok := sprop.decoderTags.get(prop.Tag) + if !ok { + // XXX_unrecognized + continue + } + ft := t.Field(fi).Type + + sf, nested, err := fieldDefault(ft, prop) + switch { + case err != nil: + log.Print(err) + case nested: + dm.nested = append(dm.nested, fi) + case sf != nil: + sf.index = fi + dm.scalars = append(dm.scalars, *sf) + } + } + + return dm +} + +// fieldDefault returns the scalarField for field type ft. +// sf will be nil if the field can not have a default. +// nestedMessage will be true if this is a nested message. +// Note that sf.index is not set on return. +func fieldDefault(ft reflect.Type, prop *Properties) (sf *scalarField, nestedMessage bool, err error) { + var canHaveDefault bool + switch ft.Kind() { + case reflect.Ptr: + if ft.Elem().Kind() == reflect.Struct { + nestedMessage = true + } else { + canHaveDefault = true // proto2 scalar field + } + + case reflect.Slice: + switch ft.Elem().Kind() { + case reflect.Ptr: + nestedMessage = true // repeated message + case reflect.Uint8: + canHaveDefault = true // bytes field + } + + case reflect.Map: + if ft.Elem().Kind() == reflect.Ptr { + nestedMessage = true // map with message values + } + } + + if !canHaveDefault { + if nestedMessage { + return nil, true, nil + } + return nil, false, nil + } + + // We now know that ft is a pointer or slice. + sf = &scalarField{kind: ft.Elem().Kind()} + + // scalar fields without defaults + if !prop.HasDefault { + return sf, false, nil + } + + // a scalar field: either *T or []byte + switch ft.Elem().Kind() { + case reflect.Bool: + x, err := strconv.ParseBool(prop.Default) + if err != nil { + return nil, false, fmt.Errorf("proto: bad default bool %q: %v", prop.Default, err) + } + sf.value = x + case reflect.Float32: + x, err := strconv.ParseFloat(prop.Default, 32) + if err != nil { + return nil, false, fmt.Errorf("proto: bad default float32 %q: %v", prop.Default, err) + } + sf.value = float32(x) + case reflect.Float64: + x, err := strconv.ParseFloat(prop.Default, 64) + if err != nil { + return nil, false, fmt.Errorf("proto: bad default float64 %q: %v", prop.Default, err) + } + sf.value = x + case reflect.Int32: + x, err := strconv.ParseInt(prop.Default, 10, 32) + if err != nil { + return nil, false, fmt.Errorf("proto: bad default int32 %q: %v", prop.Default, err) + } + sf.value = int32(x) + case reflect.Int64: + x, err := strconv.ParseInt(prop.Default, 10, 64) + if err != nil { + return nil, false, fmt.Errorf("proto: bad default int64 %q: %v", prop.Default, err) + } + sf.value = x + case reflect.String: + sf.value = prop.Default + case reflect.Uint8: + // []byte (not *uint8) + sf.value = []byte(prop.Default) + case reflect.Uint32: + x, err := strconv.ParseUint(prop.Default, 10, 32) + if err != nil { + return nil, false, fmt.Errorf("proto: bad default uint32 %q: %v", prop.Default, err) + } + sf.value = uint32(x) + case reflect.Uint64: + x, err := strconv.ParseUint(prop.Default, 10, 64) + if err != nil { + return nil, false, fmt.Errorf("proto: bad default uint64 %q: %v", prop.Default, err) + } + sf.value = x + default: + return nil, false, fmt.Errorf("proto: unhandled def kind %v", ft.Elem().Kind()) + } + + return sf, false, nil +} + +// mapKeys returns a sort.Interface to be used for sorting the map keys. +// Map fields may have key types of non-float scalars, strings and enums. +func mapKeys(vs []reflect.Value) sort.Interface { + s := mapKeySorter{vs: vs} + + // Type specialization per https://developers.google.com/protocol-buffers/docs/proto#maps. + if len(vs) == 0 { + return s + } + switch vs[0].Kind() { + case reflect.Int32, reflect.Int64: + s.less = func(a, b reflect.Value) bool { return a.Int() < b.Int() } + case reflect.Uint32, reflect.Uint64: + s.less = func(a, b reflect.Value) bool { return a.Uint() < b.Uint() } + case reflect.Bool: + s.less = func(a, b reflect.Value) bool { return !a.Bool() && b.Bool() } // false < true + case reflect.String: + s.less = func(a, b reflect.Value) bool { return a.String() < b.String() } + default: + panic(fmt.Sprintf("unsupported map key type: %v", vs[0].Kind())) + } + + return s +} + +type mapKeySorter struct { + vs []reflect.Value + less func(a, b reflect.Value) bool +} + +func (s mapKeySorter) Len() int { return len(s.vs) } +func (s mapKeySorter) Swap(i, j int) { s.vs[i], s.vs[j] = s.vs[j], s.vs[i] } +func (s mapKeySorter) Less(i, j int) bool { + return s.less(s.vs[i], s.vs[j]) +} + +// isProto3Zero reports whether v is a zero proto3 value. +func isProto3Zero(v reflect.Value) bool { + switch v.Kind() { + case reflect.Bool: + return !v.Bool() + case reflect.Int32, reflect.Int64: + return v.Int() == 0 + case reflect.Uint32, reflect.Uint64: + return v.Uint() == 0 + case reflect.Float32, reflect.Float64: + return v.Float() == 0 + case reflect.String: + return v.String() == "" + } + return false +} + +// ProtoPackageIsVersion2 is referenced from generated protocol buffer files +// to assert that that code is compatible with this version of the proto package. +const ProtoPackageIsVersion2 = true + +// ProtoPackageIsVersion1 is referenced from generated protocol buffer files +// to assert that that code is compatible with this version of the proto package. +const ProtoPackageIsVersion1 = true + +// InternalMessageInfo is a type used internally by generated .pb.go files. +// This type is not intended to be used by non-generated code. +// This type is not subject to any compatibility guarantee. +type InternalMessageInfo struct { + marshal *marshalInfo + unmarshal *unmarshalInfo + merge *mergeInfo + discard *discardInfo +} diff --git a/vendor/github.com/golang/protobuf/proto/message_set.go b/vendor/github.com/golang/protobuf/proto/message_set.go new file mode 100644 index 00000000..3b6ca41d --- /dev/null +++ b/vendor/github.com/golang/protobuf/proto/message_set.go @@ -0,0 +1,314 @@ +// Go support for Protocol Buffers - Google's data interchange format +// +// Copyright 2010 The Go Authors. All rights reserved. +// https://github.com/golang/protobuf +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package proto + +/* + * Support for message sets. + */ + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "reflect" + "sort" + "sync" +) + +// errNoMessageTypeID occurs when a protocol buffer does not have a message type ID. +// A message type ID is required for storing a protocol buffer in a message set. +var errNoMessageTypeID = errors.New("proto does not have a message type ID") + +// The first two types (_MessageSet_Item and messageSet) +// model what the protocol compiler produces for the following protocol message: +// message MessageSet { +// repeated group Item = 1 { +// required int32 type_id = 2; +// required string message = 3; +// }; +// } +// That is the MessageSet wire format. We can't use a proto to generate these +// because that would introduce a circular dependency between it and this package. + +type _MessageSet_Item struct { + TypeId *int32 `protobuf:"varint,2,req,name=type_id"` + Message []byte `protobuf:"bytes,3,req,name=message"` +} + +type messageSet struct { + Item []*_MessageSet_Item `protobuf:"group,1,rep"` + XXX_unrecognized []byte + // TODO: caching? +} + +// Make sure messageSet is a Message. +var _ Message = (*messageSet)(nil) + +// messageTypeIder is an interface satisfied by a protocol buffer type +// that may be stored in a MessageSet. +type messageTypeIder interface { + MessageTypeId() int32 +} + +func (ms *messageSet) find(pb Message) *_MessageSet_Item { + mti, ok := pb.(messageTypeIder) + if !ok { + return nil + } + id := mti.MessageTypeId() + for _, item := range ms.Item { + if *item.TypeId == id { + return item + } + } + return nil +} + +func (ms *messageSet) Has(pb Message) bool { + return ms.find(pb) != nil +} + +func (ms *messageSet) Unmarshal(pb Message) error { + if item := ms.find(pb); item != nil { + return Unmarshal(item.Message, pb) + } + if _, ok := pb.(messageTypeIder); !ok { + return errNoMessageTypeID + } + return nil // TODO: return error instead? +} + +func (ms *messageSet) Marshal(pb Message) error { + msg, err := Marshal(pb) + if err != nil { + return err + } + if item := ms.find(pb); item != nil { + // reuse existing item + item.Message = msg + return nil + } + + mti, ok := pb.(messageTypeIder) + if !ok { + return errNoMessageTypeID + } + + mtid := mti.MessageTypeId() + ms.Item = append(ms.Item, &_MessageSet_Item{ + TypeId: &mtid, + Message: msg, + }) + return nil +} + +func (ms *messageSet) Reset() { *ms = messageSet{} } +func (ms *messageSet) String() string { return CompactTextString(ms) } +func (*messageSet) ProtoMessage() {} + +// Support for the message_set_wire_format message option. + +func skipVarint(buf []byte) []byte { + i := 0 + for ; buf[i]&0x80 != 0; i++ { + } + return buf[i+1:] +} + +// MarshalMessageSet encodes the extension map represented by m in the message set wire format. +// It is called by generated Marshal methods on protocol buffer messages with the message_set_wire_format option. +func MarshalMessageSet(exts interface{}) ([]byte, error) { + return marshalMessageSet(exts, false) +} + +// marshaMessageSet implements above function, with the opt to turn on / off deterministic during Marshal. +func marshalMessageSet(exts interface{}, deterministic bool) ([]byte, error) { + switch exts := exts.(type) { + case *XXX_InternalExtensions: + var u marshalInfo + siz := u.sizeMessageSet(exts) + b := make([]byte, 0, siz) + return u.appendMessageSet(b, exts, deterministic) + + case map[int32]Extension: + // This is an old-style extension map. + // Wrap it in a new-style XXX_InternalExtensions. + ie := XXX_InternalExtensions{ + p: &struct { + mu sync.Mutex + extensionMap map[int32]Extension + }{ + extensionMap: exts, + }, + } + + var u marshalInfo + siz := u.sizeMessageSet(&ie) + b := make([]byte, 0, siz) + return u.appendMessageSet(b, &ie, deterministic) + + default: + return nil, errors.New("proto: not an extension map") + } +} + +// UnmarshalMessageSet decodes the extension map encoded in buf in the message set wire format. +// It is called by Unmarshal methods on protocol buffer messages with the message_set_wire_format option. +func UnmarshalMessageSet(buf []byte, exts interface{}) error { + var m map[int32]Extension + switch exts := exts.(type) { + case *XXX_InternalExtensions: + m = exts.extensionsWrite() + case map[int32]Extension: + m = exts + default: + return errors.New("proto: not an extension map") + } + + ms := new(messageSet) + if err := Unmarshal(buf, ms); err != nil { + return err + } + for _, item := range ms.Item { + id := *item.TypeId + msg := item.Message + + // Restore wire type and field number varint, plus length varint. + // Be careful to preserve duplicate items. + b := EncodeVarint(uint64(id)<<3 | WireBytes) + if ext, ok := m[id]; ok { + // Existing data; rip off the tag and length varint + // so we join the new data correctly. + // We can assume that ext.enc is set because we are unmarshaling. + o := ext.enc[len(b):] // skip wire type and field number + _, n := DecodeVarint(o) // calculate length of length varint + o = o[n:] // skip length varint + msg = append(o, msg...) // join old data and new data + } + b = append(b, EncodeVarint(uint64(len(msg)))...) + b = append(b, msg...) + + m[id] = Extension{enc: b} + } + return nil +} + +// MarshalMessageSetJSON encodes the extension map represented by m in JSON format. +// It is called by generated MarshalJSON methods on protocol buffer messages with the message_set_wire_format option. +func MarshalMessageSetJSON(exts interface{}) ([]byte, error) { + var m map[int32]Extension + switch exts := exts.(type) { + case *XXX_InternalExtensions: + var mu sync.Locker + m, mu = exts.extensionsRead() + if m != nil { + // Keep the extensions map locked until we're done marshaling to prevent + // races between marshaling and unmarshaling the lazily-{en,de}coded + // values. + mu.Lock() + defer mu.Unlock() + } + case map[int32]Extension: + m = exts + default: + return nil, errors.New("proto: not an extension map") + } + var b bytes.Buffer + b.WriteByte('{') + + // Process the map in key order for deterministic output. + ids := make([]int32, 0, len(m)) + for id := range m { + ids = append(ids, id) + } + sort.Sort(int32Slice(ids)) // int32Slice defined in text.go + + for i, id := range ids { + ext := m[id] + msd, ok := messageSetMap[id] + if !ok { + // Unknown type; we can't render it, so skip it. + continue + } + + if i > 0 && b.Len() > 1 { + b.WriteByte(',') + } + + fmt.Fprintf(&b, `"[%s]":`, msd.name) + + x := ext.value + if x == nil { + x = reflect.New(msd.t.Elem()).Interface() + if err := Unmarshal(ext.enc, x.(Message)); err != nil { + return nil, err + } + } + d, err := json.Marshal(x) + if err != nil { + return nil, err + } + b.Write(d) + } + b.WriteByte('}') + return b.Bytes(), nil +} + +// UnmarshalMessageSetJSON decodes the extension map encoded in buf in JSON format. +// It is called by generated UnmarshalJSON methods on protocol buffer messages with the message_set_wire_format option. +func UnmarshalMessageSetJSON(buf []byte, exts interface{}) error { + // Common-case fast path. + if len(buf) == 0 || bytes.Equal(buf, []byte("{}")) { + return nil + } + + // This is fairly tricky, and it's not clear that it is needed. + return errors.New("TODO: UnmarshalMessageSetJSON not yet implemented") +} + +// A global registry of types that can be used in a MessageSet. + +var messageSetMap = make(map[int32]messageSetDesc) + +type messageSetDesc struct { + t reflect.Type // pointer to struct + name string +} + +// RegisterMessageSetType is called from the generated code. +func RegisterMessageSetType(m Message, fieldNum int32, name string) { + messageSetMap[fieldNum] = messageSetDesc{ + t: reflect.TypeOf(m), + name: name, + } +} diff --git a/vendor/github.com/golang/protobuf/proto/pointer_reflect.go b/vendor/github.com/golang/protobuf/proto/pointer_reflect.go new file mode 100644 index 00000000..b6cad908 --- /dev/null +++ b/vendor/github.com/golang/protobuf/proto/pointer_reflect.go @@ -0,0 +1,357 @@ +// Go support for Protocol Buffers - Google's data interchange format +// +// Copyright 2012 The Go Authors. All rights reserved. +// https://github.com/golang/protobuf +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +// +build purego appengine js + +// This file contains an implementation of proto field accesses using package reflect. +// It is slower than the code in pointer_unsafe.go but it avoids package unsafe and can +// be used on App Engine. + +package proto + +import ( + "reflect" + "sync" +) + +const unsafeAllowed = false + +// A field identifies a field in a struct, accessible from a pointer. +// In this implementation, a field is identified by the sequence of field indices +// passed to reflect's FieldByIndex. +type field []int + +// toField returns a field equivalent to the given reflect field. +func toField(f *reflect.StructField) field { + return f.Index +} + +// invalidField is an invalid field identifier. +var invalidField = field(nil) + +// zeroField is a noop when calling pointer.offset. +var zeroField = field([]int{}) + +// IsValid reports whether the field identifier is valid. +func (f field) IsValid() bool { return f != nil } + +// The pointer type is for the table-driven decoder. +// The implementation here uses a reflect.Value of pointer type to +// create a generic pointer. In pointer_unsafe.go we use unsafe +// instead of reflect to implement the same (but faster) interface. +type pointer struct { + v reflect.Value +} + +// toPointer converts an interface of pointer type to a pointer +// that points to the same target. +func toPointer(i *Message) pointer { + return pointer{v: reflect.ValueOf(*i)} +} + +// toAddrPointer converts an interface to a pointer that points to +// the interface data. +func toAddrPointer(i *interface{}, isptr bool) pointer { + v := reflect.ValueOf(*i) + u := reflect.New(v.Type()) + u.Elem().Set(v) + return pointer{v: u} +} + +// valToPointer converts v to a pointer. v must be of pointer type. +func valToPointer(v reflect.Value) pointer { + return pointer{v: v} +} + +// offset converts from a pointer to a structure to a pointer to +// one of its fields. +func (p pointer) offset(f field) pointer { + return pointer{v: p.v.Elem().FieldByIndex(f).Addr()} +} + +func (p pointer) isNil() bool { + return p.v.IsNil() +} + +// grow updates the slice s in place to make it one element longer. +// s must be addressable. +// Returns the (addressable) new element. +func grow(s reflect.Value) reflect.Value { + n, m := s.Len(), s.Cap() + if n < m { + s.SetLen(n + 1) + } else { + s.Set(reflect.Append(s, reflect.Zero(s.Type().Elem()))) + } + return s.Index(n) +} + +func (p pointer) toInt64() *int64 { + return p.v.Interface().(*int64) +} +func (p pointer) toInt64Ptr() **int64 { + return p.v.Interface().(**int64) +} +func (p pointer) toInt64Slice() *[]int64 { + return p.v.Interface().(*[]int64) +} + +var int32ptr = reflect.TypeOf((*int32)(nil)) + +func (p pointer) toInt32() *int32 { + return p.v.Convert(int32ptr).Interface().(*int32) +} + +// The toInt32Ptr/Slice methods don't work because of enums. +// Instead, we must use set/get methods for the int32ptr/slice case. +/* + func (p pointer) toInt32Ptr() **int32 { + return p.v.Interface().(**int32) +} + func (p pointer) toInt32Slice() *[]int32 { + return p.v.Interface().(*[]int32) +} +*/ +func (p pointer) getInt32Ptr() *int32 { + if p.v.Type().Elem().Elem() == reflect.TypeOf(int32(0)) { + // raw int32 type + return p.v.Elem().Interface().(*int32) + } + // an enum + return p.v.Elem().Convert(int32PtrType).Interface().(*int32) +} +func (p pointer) setInt32Ptr(v int32) { + // Allocate value in a *int32. Possibly convert that to a *enum. + // Then assign it to a **int32 or **enum. + // Note: we can convert *int32 to *enum, but we can't convert + // **int32 to **enum! + p.v.Elem().Set(reflect.ValueOf(&v).Convert(p.v.Type().Elem())) +} + +// getInt32Slice copies []int32 from p as a new slice. +// This behavior differs from the implementation in pointer_unsafe.go. +func (p pointer) getInt32Slice() []int32 { + if p.v.Type().Elem().Elem() == reflect.TypeOf(int32(0)) { + // raw int32 type + return p.v.Elem().Interface().([]int32) + } + // an enum + // Allocate a []int32, then assign []enum's values into it. + // Note: we can't convert []enum to []int32. + slice := p.v.Elem() + s := make([]int32, slice.Len()) + for i := 0; i < slice.Len(); i++ { + s[i] = int32(slice.Index(i).Int()) + } + return s +} + +// setInt32Slice copies []int32 into p as a new slice. +// This behavior differs from the implementation in pointer_unsafe.go. +func (p pointer) setInt32Slice(v []int32) { + if p.v.Type().Elem().Elem() == reflect.TypeOf(int32(0)) { + // raw int32 type + p.v.Elem().Set(reflect.ValueOf(v)) + return + } + // an enum + // Allocate a []enum, then assign []int32's values into it. + // Note: we can't convert []enum to []int32. + slice := reflect.MakeSlice(p.v.Type().Elem(), len(v), cap(v)) + for i, x := range v { + slice.Index(i).SetInt(int64(x)) + } + p.v.Elem().Set(slice) +} +func (p pointer) appendInt32Slice(v int32) { + grow(p.v.Elem()).SetInt(int64(v)) +} + +func (p pointer) toUint64() *uint64 { + return p.v.Interface().(*uint64) +} +func (p pointer) toUint64Ptr() **uint64 { + return p.v.Interface().(**uint64) +} +func (p pointer) toUint64Slice() *[]uint64 { + return p.v.Interface().(*[]uint64) +} +func (p pointer) toUint32() *uint32 { + return p.v.Interface().(*uint32) +} +func (p pointer) toUint32Ptr() **uint32 { + return p.v.Interface().(**uint32) +} +func (p pointer) toUint32Slice() *[]uint32 { + return p.v.Interface().(*[]uint32) +} +func (p pointer) toBool() *bool { + return p.v.Interface().(*bool) +} +func (p pointer) toBoolPtr() **bool { + return p.v.Interface().(**bool) +} +func (p pointer) toBoolSlice() *[]bool { + return p.v.Interface().(*[]bool) +} +func (p pointer) toFloat64() *float64 { + return p.v.Interface().(*float64) +} +func (p pointer) toFloat64Ptr() **float64 { + return p.v.Interface().(**float64) +} +func (p pointer) toFloat64Slice() *[]float64 { + return p.v.Interface().(*[]float64) +} +func (p pointer) toFloat32() *float32 { + return p.v.Interface().(*float32) +} +func (p pointer) toFloat32Ptr() **float32 { + return p.v.Interface().(**float32) +} +func (p pointer) toFloat32Slice() *[]float32 { + return p.v.Interface().(*[]float32) +} +func (p pointer) toString() *string { + return p.v.Interface().(*string) +} +func (p pointer) toStringPtr() **string { + return p.v.Interface().(**string) +} +func (p pointer) toStringSlice() *[]string { + return p.v.Interface().(*[]string) +} +func (p pointer) toBytes() *[]byte { + return p.v.Interface().(*[]byte) +} +func (p pointer) toBytesSlice() *[][]byte { + return p.v.Interface().(*[][]byte) +} +func (p pointer) toExtensions() *XXX_InternalExtensions { + return p.v.Interface().(*XXX_InternalExtensions) +} +func (p pointer) toOldExtensions() *map[int32]Extension { + return p.v.Interface().(*map[int32]Extension) +} +func (p pointer) getPointer() pointer { + return pointer{v: p.v.Elem()} +} +func (p pointer) setPointer(q pointer) { + p.v.Elem().Set(q.v) +} +func (p pointer) appendPointer(q pointer) { + grow(p.v.Elem()).Set(q.v) +} + +// getPointerSlice copies []*T from p as a new []pointer. +// This behavior differs from the implementation in pointer_unsafe.go. +func (p pointer) getPointerSlice() []pointer { + if p.v.IsNil() { + return nil + } + n := p.v.Elem().Len() + s := make([]pointer, n) + for i := 0; i < n; i++ { + s[i] = pointer{v: p.v.Elem().Index(i)} + } + return s +} + +// setPointerSlice copies []pointer into p as a new []*T. +// This behavior differs from the implementation in pointer_unsafe.go. +func (p pointer) setPointerSlice(v []pointer) { + if v == nil { + p.v.Elem().Set(reflect.New(p.v.Elem().Type()).Elem()) + return + } + s := reflect.MakeSlice(p.v.Elem().Type(), 0, len(v)) + for _, p := range v { + s = reflect.Append(s, p.v) + } + p.v.Elem().Set(s) +} + +// getInterfacePointer returns a pointer that points to the +// interface data of the interface pointed by p. +func (p pointer) getInterfacePointer() pointer { + if p.v.Elem().IsNil() { + return pointer{v: p.v.Elem()} + } + return pointer{v: p.v.Elem().Elem().Elem().Field(0).Addr()} // *interface -> interface -> *struct -> struct +} + +func (p pointer) asPointerTo(t reflect.Type) reflect.Value { + // TODO: check that p.v.Type().Elem() == t? + return p.v +} + +func atomicLoadUnmarshalInfo(p **unmarshalInfo) *unmarshalInfo { + atomicLock.Lock() + defer atomicLock.Unlock() + return *p +} +func atomicStoreUnmarshalInfo(p **unmarshalInfo, v *unmarshalInfo) { + atomicLock.Lock() + defer atomicLock.Unlock() + *p = v +} +func atomicLoadMarshalInfo(p **marshalInfo) *marshalInfo { + atomicLock.Lock() + defer atomicLock.Unlock() + return *p +} +func atomicStoreMarshalInfo(p **marshalInfo, v *marshalInfo) { + atomicLock.Lock() + defer atomicLock.Unlock() + *p = v +} +func atomicLoadMergeInfo(p **mergeInfo) *mergeInfo { + atomicLock.Lock() + defer atomicLock.Unlock() + return *p +} +func atomicStoreMergeInfo(p **mergeInfo, v *mergeInfo) { + atomicLock.Lock() + defer atomicLock.Unlock() + *p = v +} +func atomicLoadDiscardInfo(p **discardInfo) *discardInfo { + atomicLock.Lock() + defer atomicLock.Unlock() + return *p +} +func atomicStoreDiscardInfo(p **discardInfo, v *discardInfo) { + atomicLock.Lock() + defer atomicLock.Unlock() + *p = v +} + +var atomicLock sync.Mutex diff --git a/vendor/github.com/golang/protobuf/proto/pointer_unsafe.go b/vendor/github.com/golang/protobuf/proto/pointer_unsafe.go new file mode 100644 index 00000000..d55a335d --- /dev/null +++ b/vendor/github.com/golang/protobuf/proto/pointer_unsafe.go @@ -0,0 +1,308 @@ +// Go support for Protocol Buffers - Google's data interchange format +// +// Copyright 2012 The Go Authors. All rights reserved. +// https://github.com/golang/protobuf +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +// +build !purego,!appengine,!js + +// This file contains the implementation of the proto field accesses using package unsafe. + +package proto + +import ( + "reflect" + "sync/atomic" + "unsafe" +) + +const unsafeAllowed = true + +// A field identifies a field in a struct, accessible from a pointer. +// In this implementation, a field is identified by its byte offset from the start of the struct. +type field uintptr + +// toField returns a field equivalent to the given reflect field. +func toField(f *reflect.StructField) field { + return field(f.Offset) +} + +// invalidField is an invalid field identifier. +const invalidField = ^field(0) + +// zeroField is a noop when calling pointer.offset. +const zeroField = field(0) + +// IsValid reports whether the field identifier is valid. +func (f field) IsValid() bool { + return f != invalidField +} + +// The pointer type below is for the new table-driven encoder/decoder. +// The implementation here uses unsafe.Pointer to create a generic pointer. +// In pointer_reflect.go we use reflect instead of unsafe to implement +// the same (but slower) interface. +type pointer struct { + p unsafe.Pointer +} + +// size of pointer +var ptrSize = unsafe.Sizeof(uintptr(0)) + +// toPointer converts an interface of pointer type to a pointer +// that points to the same target. +func toPointer(i *Message) pointer { + // Super-tricky - read pointer out of data word of interface value. + // Saves ~25ns over the equivalent: + // return valToPointer(reflect.ValueOf(*i)) + return pointer{p: (*[2]unsafe.Pointer)(unsafe.Pointer(i))[1]} +} + +// toAddrPointer converts an interface to a pointer that points to +// the interface data. +func toAddrPointer(i *interface{}, isptr bool) pointer { + // Super-tricky - read or get the address of data word of interface value. + if isptr { + // The interface is of pointer type, thus it is a direct interface. + // The data word is the pointer data itself. We take its address. + return pointer{p: unsafe.Pointer(uintptr(unsafe.Pointer(i)) + ptrSize)} + } + // The interface is not of pointer type. The data word is the pointer + // to the data. + return pointer{p: (*[2]unsafe.Pointer)(unsafe.Pointer(i))[1]} +} + +// valToPointer converts v to a pointer. v must be of pointer type. +func valToPointer(v reflect.Value) pointer { + return pointer{p: unsafe.Pointer(v.Pointer())} +} + +// offset converts from a pointer to a structure to a pointer to +// one of its fields. +func (p pointer) offset(f field) pointer { + // For safety, we should panic if !f.IsValid, however calling panic causes + // this to no longer be inlineable, which is a serious performance cost. + /* + if !f.IsValid() { + panic("invalid field") + } + */ + return pointer{p: unsafe.Pointer(uintptr(p.p) + uintptr(f))} +} + +func (p pointer) isNil() bool { + return p.p == nil +} + +func (p pointer) toInt64() *int64 { + return (*int64)(p.p) +} +func (p pointer) toInt64Ptr() **int64 { + return (**int64)(p.p) +} +func (p pointer) toInt64Slice() *[]int64 { + return (*[]int64)(p.p) +} +func (p pointer) toInt32() *int32 { + return (*int32)(p.p) +} + +// See pointer_reflect.go for why toInt32Ptr/Slice doesn't exist. +/* + func (p pointer) toInt32Ptr() **int32 { + return (**int32)(p.p) + } + func (p pointer) toInt32Slice() *[]int32 { + return (*[]int32)(p.p) + } +*/ +func (p pointer) getInt32Ptr() *int32 { + return *(**int32)(p.p) +} +func (p pointer) setInt32Ptr(v int32) { + *(**int32)(p.p) = &v +} + +// getInt32Slice loads a []int32 from p. +// The value returned is aliased with the original slice. +// This behavior differs from the implementation in pointer_reflect.go. +func (p pointer) getInt32Slice() []int32 { + return *(*[]int32)(p.p) +} + +// setInt32Slice stores a []int32 to p. +// The value set is aliased with the input slice. +// This behavior differs from the implementation in pointer_reflect.go. +func (p pointer) setInt32Slice(v []int32) { + *(*[]int32)(p.p) = v +} + +// TODO: Can we get rid of appendInt32Slice and use setInt32Slice instead? +func (p pointer) appendInt32Slice(v int32) { + s := (*[]int32)(p.p) + *s = append(*s, v) +} + +func (p pointer) toUint64() *uint64 { + return (*uint64)(p.p) +} +func (p pointer) toUint64Ptr() **uint64 { + return (**uint64)(p.p) +} +func (p pointer) toUint64Slice() *[]uint64 { + return (*[]uint64)(p.p) +} +func (p pointer) toUint32() *uint32 { + return (*uint32)(p.p) +} +func (p pointer) toUint32Ptr() **uint32 { + return (**uint32)(p.p) +} +func (p pointer) toUint32Slice() *[]uint32 { + return (*[]uint32)(p.p) +} +func (p pointer) toBool() *bool { + return (*bool)(p.p) +} +func (p pointer) toBoolPtr() **bool { + return (**bool)(p.p) +} +func (p pointer) toBoolSlice() *[]bool { + return (*[]bool)(p.p) +} +func (p pointer) toFloat64() *float64 { + return (*float64)(p.p) +} +func (p pointer) toFloat64Ptr() **float64 { + return (**float64)(p.p) +} +func (p pointer) toFloat64Slice() *[]float64 { + return (*[]float64)(p.p) +} +func (p pointer) toFloat32() *float32 { + return (*float32)(p.p) +} +func (p pointer) toFloat32Ptr() **float32 { + return (**float32)(p.p) +} +func (p pointer) toFloat32Slice() *[]float32 { + return (*[]float32)(p.p) +} +func (p pointer) toString() *string { + return (*string)(p.p) +} +func (p pointer) toStringPtr() **string { + return (**string)(p.p) +} +func (p pointer) toStringSlice() *[]string { + return (*[]string)(p.p) +} +func (p pointer) toBytes() *[]byte { + return (*[]byte)(p.p) +} +func (p pointer) toBytesSlice() *[][]byte { + return (*[][]byte)(p.p) +} +func (p pointer) toExtensions() *XXX_InternalExtensions { + return (*XXX_InternalExtensions)(p.p) +} +func (p pointer) toOldExtensions() *map[int32]Extension { + return (*map[int32]Extension)(p.p) +} + +// getPointerSlice loads []*T from p as a []pointer. +// The value returned is aliased with the original slice. +// This behavior differs from the implementation in pointer_reflect.go. +func (p pointer) getPointerSlice() []pointer { + // Super-tricky - p should point to a []*T where T is a + // message type. We load it as []pointer. + return *(*[]pointer)(p.p) +} + +// setPointerSlice stores []pointer into p as a []*T. +// The value set is aliased with the input slice. +// This behavior differs from the implementation in pointer_reflect.go. +func (p pointer) setPointerSlice(v []pointer) { + // Super-tricky - p should point to a []*T where T is a + // message type. We store it as []pointer. + *(*[]pointer)(p.p) = v +} + +// getPointer loads the pointer at p and returns it. +func (p pointer) getPointer() pointer { + return pointer{p: *(*unsafe.Pointer)(p.p)} +} + +// setPointer stores the pointer q at p. +func (p pointer) setPointer(q pointer) { + *(*unsafe.Pointer)(p.p) = q.p +} + +// append q to the slice pointed to by p. +func (p pointer) appendPointer(q pointer) { + s := (*[]unsafe.Pointer)(p.p) + *s = append(*s, q.p) +} + +// getInterfacePointer returns a pointer that points to the +// interface data of the interface pointed by p. +func (p pointer) getInterfacePointer() pointer { + // Super-tricky - read pointer out of data word of interface value. + return pointer{p: (*(*[2]unsafe.Pointer)(p.p))[1]} +} + +// asPointerTo returns a reflect.Value that is a pointer to an +// object of type t stored at p. +func (p pointer) asPointerTo(t reflect.Type) reflect.Value { + return reflect.NewAt(t, p.p) +} + +func atomicLoadUnmarshalInfo(p **unmarshalInfo) *unmarshalInfo { + return (*unmarshalInfo)(atomic.LoadPointer((*unsafe.Pointer)(unsafe.Pointer(p)))) +} +func atomicStoreUnmarshalInfo(p **unmarshalInfo, v *unmarshalInfo) { + atomic.StorePointer((*unsafe.Pointer)(unsafe.Pointer(p)), unsafe.Pointer(v)) +} +func atomicLoadMarshalInfo(p **marshalInfo) *marshalInfo { + return (*marshalInfo)(atomic.LoadPointer((*unsafe.Pointer)(unsafe.Pointer(p)))) +} +func atomicStoreMarshalInfo(p **marshalInfo, v *marshalInfo) { + atomic.StorePointer((*unsafe.Pointer)(unsafe.Pointer(p)), unsafe.Pointer(v)) +} +func atomicLoadMergeInfo(p **mergeInfo) *mergeInfo { + return (*mergeInfo)(atomic.LoadPointer((*unsafe.Pointer)(unsafe.Pointer(p)))) +} +func atomicStoreMergeInfo(p **mergeInfo, v *mergeInfo) { + atomic.StorePointer((*unsafe.Pointer)(unsafe.Pointer(p)), unsafe.Pointer(v)) +} +func atomicLoadDiscardInfo(p **discardInfo) *discardInfo { + return (*discardInfo)(atomic.LoadPointer((*unsafe.Pointer)(unsafe.Pointer(p)))) +} +func atomicStoreDiscardInfo(p **discardInfo, v *discardInfo) { + atomic.StorePointer((*unsafe.Pointer)(unsafe.Pointer(p)), unsafe.Pointer(v)) +} diff --git a/vendor/github.com/golang/protobuf/proto/properties.go b/vendor/github.com/golang/protobuf/proto/properties.go new file mode 100644 index 00000000..f710adab --- /dev/null +++ b/vendor/github.com/golang/protobuf/proto/properties.go @@ -0,0 +1,544 @@ +// Go support for Protocol Buffers - Google's data interchange format +// +// Copyright 2010 The Go Authors. All rights reserved. +// https://github.com/golang/protobuf +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package proto + +/* + * Routines for encoding data into the wire format for protocol buffers. + */ + +import ( + "fmt" + "log" + "os" + "reflect" + "sort" + "strconv" + "strings" + "sync" +) + +const debug bool = false + +// Constants that identify the encoding of a value on the wire. +const ( + WireVarint = 0 + WireFixed64 = 1 + WireBytes = 2 + WireStartGroup = 3 + WireEndGroup = 4 + WireFixed32 = 5 +) + +// tagMap is an optimization over map[int]int for typical protocol buffer +// use-cases. Encoded protocol buffers are often in tag order with small tag +// numbers. +type tagMap struct { + fastTags []int + slowTags map[int]int +} + +// tagMapFastLimit is the upper bound on the tag number that will be stored in +// the tagMap slice rather than its map. +const tagMapFastLimit = 1024 + +func (p *tagMap) get(t int) (int, bool) { + if t > 0 && t < tagMapFastLimit { + if t >= len(p.fastTags) { + return 0, false + } + fi := p.fastTags[t] + return fi, fi >= 0 + } + fi, ok := p.slowTags[t] + return fi, ok +} + +func (p *tagMap) put(t int, fi int) { + if t > 0 && t < tagMapFastLimit { + for len(p.fastTags) < t+1 { + p.fastTags = append(p.fastTags, -1) + } + p.fastTags[t] = fi + return + } + if p.slowTags == nil { + p.slowTags = make(map[int]int) + } + p.slowTags[t] = fi +} + +// StructProperties represents properties for all the fields of a struct. +// decoderTags and decoderOrigNames should only be used by the decoder. +type StructProperties struct { + Prop []*Properties // properties for each field + reqCount int // required count + decoderTags tagMap // map from proto tag to struct field number + decoderOrigNames map[string]int // map from original name to struct field number + order []int // list of struct field numbers in tag order + + // OneofTypes contains information about the oneof fields in this message. + // It is keyed by the original name of a field. + OneofTypes map[string]*OneofProperties +} + +// OneofProperties represents information about a specific field in a oneof. +type OneofProperties struct { + Type reflect.Type // pointer to generated struct type for this oneof field + Field int // struct field number of the containing oneof in the message + Prop *Properties +} + +// Implement the sorting interface so we can sort the fields in tag order, as recommended by the spec. +// See encode.go, (*Buffer).enc_struct. + +func (sp *StructProperties) Len() int { return len(sp.order) } +func (sp *StructProperties) Less(i, j int) bool { + return sp.Prop[sp.order[i]].Tag < sp.Prop[sp.order[j]].Tag +} +func (sp *StructProperties) Swap(i, j int) { sp.order[i], sp.order[j] = sp.order[j], sp.order[i] } + +// Properties represents the protocol-specific behavior of a single struct field. +type Properties struct { + Name string // name of the field, for error messages + OrigName string // original name before protocol compiler (always set) + JSONName string // name to use for JSON; determined by protoc + Wire string + WireType int + Tag int + Required bool + Optional bool + Repeated bool + Packed bool // relevant for repeated primitives only + Enum string // set for enum types only + proto3 bool // whether this is known to be a proto3 field; set for []byte only + oneof bool // whether this is a oneof field + + Default string // default value + HasDefault bool // whether an explicit default was provided + + stype reflect.Type // set for struct types only + sprop *StructProperties // set for struct types only + + mtype reflect.Type // set for map types only + mkeyprop *Properties // set for map types only + mvalprop *Properties // set for map types only +} + +// String formats the properties in the protobuf struct field tag style. +func (p *Properties) String() string { + s := p.Wire + s += "," + s += strconv.Itoa(p.Tag) + if p.Required { + s += ",req" + } + if p.Optional { + s += ",opt" + } + if p.Repeated { + s += ",rep" + } + if p.Packed { + s += ",packed" + } + s += ",name=" + p.OrigName + if p.JSONName != p.OrigName { + s += ",json=" + p.JSONName + } + if p.proto3 { + s += ",proto3" + } + if p.oneof { + s += ",oneof" + } + if len(p.Enum) > 0 { + s += ",enum=" + p.Enum + } + if p.HasDefault { + s += ",def=" + p.Default + } + return s +} + +// Parse populates p by parsing a string in the protobuf struct field tag style. +func (p *Properties) Parse(s string) { + // "bytes,49,opt,name=foo,def=hello!" + fields := strings.Split(s, ",") // breaks def=, but handled below. + if len(fields) < 2 { + fmt.Fprintf(os.Stderr, "proto: tag has too few fields: %q\n", s) + return + } + + p.Wire = fields[0] + switch p.Wire { + case "varint": + p.WireType = WireVarint + case "fixed32": + p.WireType = WireFixed32 + case "fixed64": + p.WireType = WireFixed64 + case "zigzag32": + p.WireType = WireVarint + case "zigzag64": + p.WireType = WireVarint + case "bytes", "group": + p.WireType = WireBytes + // no numeric converter for non-numeric types + default: + fmt.Fprintf(os.Stderr, "proto: tag has unknown wire type: %q\n", s) + return + } + + var err error + p.Tag, err = strconv.Atoi(fields[1]) + if err != nil { + return + } + +outer: + for i := 2; i < len(fields); i++ { + f := fields[i] + switch { + case f == "req": + p.Required = true + case f == "opt": + p.Optional = true + case f == "rep": + p.Repeated = true + case f == "packed": + p.Packed = true + case strings.HasPrefix(f, "name="): + p.OrigName = f[5:] + case strings.HasPrefix(f, "json="): + p.JSONName = f[5:] + case strings.HasPrefix(f, "enum="): + p.Enum = f[5:] + case f == "proto3": + p.proto3 = true + case f == "oneof": + p.oneof = true + case strings.HasPrefix(f, "def="): + p.HasDefault = true + p.Default = f[4:] // rest of string + if i+1 < len(fields) { + // Commas aren't escaped, and def is always last. + p.Default += "," + strings.Join(fields[i+1:], ",") + break outer + } + } + } +} + +var protoMessageType = reflect.TypeOf((*Message)(nil)).Elem() + +// setFieldProps initializes the field properties for submessages and maps. +func (p *Properties) setFieldProps(typ reflect.Type, f *reflect.StructField, lockGetProp bool) { + switch t1 := typ; t1.Kind() { + case reflect.Ptr: + if t1.Elem().Kind() == reflect.Struct { + p.stype = t1.Elem() + } + + case reflect.Slice: + if t2 := t1.Elem(); t2.Kind() == reflect.Ptr && t2.Elem().Kind() == reflect.Struct { + p.stype = t2.Elem() + } + + case reflect.Map: + p.mtype = t1 + p.mkeyprop = &Properties{} + p.mkeyprop.init(reflect.PtrTo(p.mtype.Key()), "Key", f.Tag.Get("protobuf_key"), nil, lockGetProp) + p.mvalprop = &Properties{} + vtype := p.mtype.Elem() + if vtype.Kind() != reflect.Ptr && vtype.Kind() != reflect.Slice { + // The value type is not a message (*T) or bytes ([]byte), + // so we need encoders for the pointer to this type. + vtype = reflect.PtrTo(vtype) + } + p.mvalprop.init(vtype, "Value", f.Tag.Get("protobuf_val"), nil, lockGetProp) + } + + if p.stype != nil { + if lockGetProp { + p.sprop = GetProperties(p.stype) + } else { + p.sprop = getPropertiesLocked(p.stype) + } + } +} + +var ( + marshalerType = reflect.TypeOf((*Marshaler)(nil)).Elem() +) + +// Init populates the properties from a protocol buffer struct tag. +func (p *Properties) Init(typ reflect.Type, name, tag string, f *reflect.StructField) { + p.init(typ, name, tag, f, true) +} + +func (p *Properties) init(typ reflect.Type, name, tag string, f *reflect.StructField, lockGetProp bool) { + // "bytes,49,opt,def=hello!" + p.Name = name + p.OrigName = name + if tag == "" { + return + } + p.Parse(tag) + p.setFieldProps(typ, f, lockGetProp) +} + +var ( + propertiesMu sync.RWMutex + propertiesMap = make(map[reflect.Type]*StructProperties) +) + +// GetProperties returns the list of properties for the type represented by t. +// t must represent a generated struct type of a protocol message. +func GetProperties(t reflect.Type) *StructProperties { + if t.Kind() != reflect.Struct { + panic("proto: type must have kind struct") + } + + // Most calls to GetProperties in a long-running program will be + // retrieving details for types we have seen before. + propertiesMu.RLock() + sprop, ok := propertiesMap[t] + propertiesMu.RUnlock() + if ok { + if collectStats { + stats.Chit++ + } + return sprop + } + + propertiesMu.Lock() + sprop = getPropertiesLocked(t) + propertiesMu.Unlock() + return sprop +} + +// getPropertiesLocked requires that propertiesMu is held. +func getPropertiesLocked(t reflect.Type) *StructProperties { + if prop, ok := propertiesMap[t]; ok { + if collectStats { + stats.Chit++ + } + return prop + } + if collectStats { + stats.Cmiss++ + } + + prop := new(StructProperties) + // in case of recursive protos, fill this in now. + propertiesMap[t] = prop + + // build properties + prop.Prop = make([]*Properties, t.NumField()) + prop.order = make([]int, t.NumField()) + + for i := 0; i < t.NumField(); i++ { + f := t.Field(i) + p := new(Properties) + name := f.Name + p.init(f.Type, name, f.Tag.Get("protobuf"), &f, false) + + oneof := f.Tag.Get("protobuf_oneof") // special case + if oneof != "" { + // Oneof fields don't use the traditional protobuf tag. + p.OrigName = oneof + } + prop.Prop[i] = p + prop.order[i] = i + if debug { + print(i, " ", f.Name, " ", t.String(), " ") + if p.Tag > 0 { + print(p.String()) + } + print("\n") + } + } + + // Re-order prop.order. + sort.Sort(prop) + + type oneofMessage interface { + XXX_OneofFuncs() (func(Message, *Buffer) error, func(Message, int, int, *Buffer) (bool, error), func(Message) int, []interface{}) + } + if om, ok := reflect.Zero(reflect.PtrTo(t)).Interface().(oneofMessage); ok { + var oots []interface{} + _, _, _, oots = om.XXX_OneofFuncs() + + // Interpret oneof metadata. + prop.OneofTypes = make(map[string]*OneofProperties) + for _, oot := range oots { + oop := &OneofProperties{ + Type: reflect.ValueOf(oot).Type(), // *T + Prop: new(Properties), + } + sft := oop.Type.Elem().Field(0) + oop.Prop.Name = sft.Name + oop.Prop.Parse(sft.Tag.Get("protobuf")) + // There will be exactly one interface field that + // this new value is assignable to. + for i := 0; i < t.NumField(); i++ { + f := t.Field(i) + if f.Type.Kind() != reflect.Interface { + continue + } + if !oop.Type.AssignableTo(f.Type) { + continue + } + oop.Field = i + break + } + prop.OneofTypes[oop.Prop.OrigName] = oop + } + } + + // build required counts + // build tags + reqCount := 0 + prop.decoderOrigNames = make(map[string]int) + for i, p := range prop.Prop { + if strings.HasPrefix(p.Name, "XXX_") { + // Internal fields should not appear in tags/origNames maps. + // They are handled specially when encoding and decoding. + continue + } + if p.Required { + reqCount++ + } + prop.decoderTags.put(p.Tag, i) + prop.decoderOrigNames[p.OrigName] = i + } + prop.reqCount = reqCount + + return prop +} + +// A global registry of enum types. +// The generated code will register the generated maps by calling RegisterEnum. + +var enumValueMaps = make(map[string]map[string]int32) + +// RegisterEnum is called from the generated code to install the enum descriptor +// maps into the global table to aid parsing text format protocol buffers. +func RegisterEnum(typeName string, unusedNameMap map[int32]string, valueMap map[string]int32) { + if _, ok := enumValueMaps[typeName]; ok { + panic("proto: duplicate enum registered: " + typeName) + } + enumValueMaps[typeName] = valueMap +} + +// EnumValueMap returns the mapping from names to integers of the +// enum type enumType, or a nil if not found. +func EnumValueMap(enumType string) map[string]int32 { + return enumValueMaps[enumType] +} + +// A registry of all linked message types. +// The string is a fully-qualified proto name ("pkg.Message"). +var ( + protoTypedNils = make(map[string]Message) // a map from proto names to typed nil pointers + protoMapTypes = make(map[string]reflect.Type) // a map from proto names to map types + revProtoTypes = make(map[reflect.Type]string) +) + +// RegisterType is called from generated code and maps from the fully qualified +// proto name to the type (pointer to struct) of the protocol buffer. +func RegisterType(x Message, name string) { + if _, ok := protoTypedNils[name]; ok { + // TODO: Some day, make this a panic. + log.Printf("proto: duplicate proto type registered: %s", name) + return + } + t := reflect.TypeOf(x) + if v := reflect.ValueOf(x); v.Kind() == reflect.Ptr && v.Pointer() == 0 { + // Generated code always calls RegisterType with nil x. + // This check is just for extra safety. + protoTypedNils[name] = x + } else { + protoTypedNils[name] = reflect.Zero(t).Interface().(Message) + } + revProtoTypes[t] = name +} + +// RegisterMapType is called from generated code and maps from the fully qualified +// proto name to the native map type of the proto map definition. +func RegisterMapType(x interface{}, name string) { + if reflect.TypeOf(x).Kind() != reflect.Map { + panic(fmt.Sprintf("RegisterMapType(%T, %q); want map", x, name)) + } + if _, ok := protoMapTypes[name]; ok { + log.Printf("proto: duplicate proto type registered: %s", name) + return + } + t := reflect.TypeOf(x) + protoMapTypes[name] = t + revProtoTypes[t] = name +} + +// MessageName returns the fully-qualified proto name for the given message type. +func MessageName(x Message) string { + type xname interface { + XXX_MessageName() string + } + if m, ok := x.(xname); ok { + return m.XXX_MessageName() + } + return revProtoTypes[reflect.TypeOf(x)] +} + +// MessageType returns the message type (pointer to struct) for a named message. +// The type is not guaranteed to implement proto.Message if the name refers to a +// map entry. +func MessageType(name string) reflect.Type { + if t, ok := protoTypedNils[name]; ok { + return reflect.TypeOf(t) + } + return protoMapTypes[name] +} + +// A registry of all linked proto files. +var ( + protoFiles = make(map[string][]byte) // file name => fileDescriptor +) + +// RegisterFile is called from generated code and maps from the +// full file name of a .proto file to its compressed FileDescriptorProto. +func RegisterFile(filename string, fileDescriptor []byte) { + protoFiles[filename] = fileDescriptor +} + +// FileDescriptor returns the compressed FileDescriptorProto for a .proto file. +func FileDescriptor(filename string) []byte { return protoFiles[filename] } diff --git a/vendor/github.com/golang/protobuf/proto/table_marshal.go b/vendor/github.com/golang/protobuf/proto/table_marshal.go new file mode 100644 index 00000000..be7b2428 --- /dev/null +++ b/vendor/github.com/golang/protobuf/proto/table_marshal.go @@ -0,0 +1,2685 @@ +// Go support for Protocol Buffers - Google's data interchange format +// +// Copyright 2016 The Go Authors. All rights reserved. +// https://github.com/golang/protobuf +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package proto + +import ( + "errors" + "fmt" + "math" + "reflect" + "sort" + "strconv" + "strings" + "sync" + "sync/atomic" + "unicode/utf8" +) + +// a sizer takes a pointer to a field and the size of its tag, computes the size of +// the encoded data. +type sizer func(pointer, int) int + +// a marshaler takes a byte slice, a pointer to a field, and its tag (in wire format), +// marshals the field to the end of the slice, returns the slice and error (if any). +type marshaler func(b []byte, ptr pointer, wiretag uint64, deterministic bool) ([]byte, error) + +// marshalInfo is the information used for marshaling a message. +type marshalInfo struct { + typ reflect.Type + fields []*marshalFieldInfo + unrecognized field // offset of XXX_unrecognized + extensions field // offset of XXX_InternalExtensions + v1extensions field // offset of XXX_extensions + sizecache field // offset of XXX_sizecache + initialized int32 // 0 -- only typ is set, 1 -- fully initialized + messageset bool // uses message set wire format + hasmarshaler bool // has custom marshaler + sync.RWMutex // protect extElems map, also for initialization + extElems map[int32]*marshalElemInfo // info of extension elements +} + +// marshalFieldInfo is the information used for marshaling a field of a message. +type marshalFieldInfo struct { + field field + wiretag uint64 // tag in wire format + tagsize int // size of tag in wire format + sizer sizer + marshaler marshaler + isPointer bool + required bool // field is required + name string // name of the field, for error reporting + oneofElems map[reflect.Type]*marshalElemInfo // info of oneof elements +} + +// marshalElemInfo is the information used for marshaling an extension or oneof element. +type marshalElemInfo struct { + wiretag uint64 // tag in wire format + tagsize int // size of tag in wire format + sizer sizer + marshaler marshaler + isptr bool // elem is pointer typed, thus interface of this type is a direct interface (extension only) +} + +var ( + marshalInfoMap = map[reflect.Type]*marshalInfo{} + marshalInfoLock sync.Mutex +) + +// getMarshalInfo returns the information to marshal a given type of message. +// The info it returns may not necessarily initialized. +// t is the type of the message (NOT the pointer to it). +func getMarshalInfo(t reflect.Type) *marshalInfo { + marshalInfoLock.Lock() + u, ok := marshalInfoMap[t] + if !ok { + u = &marshalInfo{typ: t} + marshalInfoMap[t] = u + } + marshalInfoLock.Unlock() + return u +} + +// Size is the entry point from generated code, +// and should be ONLY called by generated code. +// It computes the size of encoded data of msg. +// a is a pointer to a place to store cached marshal info. +func (a *InternalMessageInfo) Size(msg Message) int { + u := getMessageMarshalInfo(msg, a) + ptr := toPointer(&msg) + if ptr.isNil() { + // We get here if msg is a typed nil ((*SomeMessage)(nil)), + // so it satisfies the interface, and msg == nil wouldn't + // catch it. We don't want crash in this case. + return 0 + } + return u.size(ptr) +} + +// Marshal is the entry point from generated code, +// and should be ONLY called by generated code. +// It marshals msg to the end of b. +// a is a pointer to a place to store cached marshal info. +func (a *InternalMessageInfo) Marshal(b []byte, msg Message, deterministic bool) ([]byte, error) { + u := getMessageMarshalInfo(msg, a) + ptr := toPointer(&msg) + if ptr.isNil() { + // We get here if msg is a typed nil ((*SomeMessage)(nil)), + // so it satisfies the interface, and msg == nil wouldn't + // catch it. We don't want crash in this case. + return b, ErrNil + } + return u.marshal(b, ptr, deterministic) +} + +func getMessageMarshalInfo(msg interface{}, a *InternalMessageInfo) *marshalInfo { + // u := a.marshal, but atomically. + // We use an atomic here to ensure memory consistency. + u := atomicLoadMarshalInfo(&a.marshal) + if u == nil { + // Get marshal information from type of message. + t := reflect.ValueOf(msg).Type() + if t.Kind() != reflect.Ptr { + panic(fmt.Sprintf("cannot handle non-pointer message type %v", t)) + } + u = getMarshalInfo(t.Elem()) + // Store it in the cache for later users. + // a.marshal = u, but atomically. + atomicStoreMarshalInfo(&a.marshal, u) + } + return u +} + +// size is the main function to compute the size of the encoded data of a message. +// ptr is the pointer to the message. +func (u *marshalInfo) size(ptr pointer) int { + if atomic.LoadInt32(&u.initialized) == 0 { + u.computeMarshalInfo() + } + + // If the message can marshal itself, let it do it, for compatibility. + // NOTE: This is not efficient. + if u.hasmarshaler { + m := ptr.asPointerTo(u.typ).Interface().(Marshaler) + b, _ := m.Marshal() + return len(b) + } + + n := 0 + for _, f := range u.fields { + if f.isPointer && ptr.offset(f.field).getPointer().isNil() { + // nil pointer always marshals to nothing + continue + } + n += f.sizer(ptr.offset(f.field), f.tagsize) + } + if u.extensions.IsValid() { + e := ptr.offset(u.extensions).toExtensions() + if u.messageset { + n += u.sizeMessageSet(e) + } else { + n += u.sizeExtensions(e) + } + } + if u.v1extensions.IsValid() { + m := *ptr.offset(u.v1extensions).toOldExtensions() + n += u.sizeV1Extensions(m) + } + if u.unrecognized.IsValid() { + s := *ptr.offset(u.unrecognized).toBytes() + n += len(s) + } + // cache the result for use in marshal + if u.sizecache.IsValid() { + atomic.StoreInt32(ptr.offset(u.sizecache).toInt32(), int32(n)) + } + return n +} + +// cachedsize gets the size from cache. If there is no cache (i.e. message is not generated), +// fall back to compute the size. +func (u *marshalInfo) cachedsize(ptr pointer) int { + if u.sizecache.IsValid() { + return int(atomic.LoadInt32(ptr.offset(u.sizecache).toInt32())) + } + return u.size(ptr) +} + +// marshal is the main function to marshal a message. It takes a byte slice and appends +// the encoded data to the end of the slice, returns the slice and error (if any). +// ptr is the pointer to the message. +// If deterministic is true, map is marshaled in deterministic order. +func (u *marshalInfo) marshal(b []byte, ptr pointer, deterministic bool) ([]byte, error) { + if atomic.LoadInt32(&u.initialized) == 0 { + u.computeMarshalInfo() + } + + // If the message can marshal itself, let it do it, for compatibility. + // NOTE: This is not efficient. + if u.hasmarshaler { + m := ptr.asPointerTo(u.typ).Interface().(Marshaler) + b1, err := m.Marshal() + b = append(b, b1...) + return b, err + } + + var err, errreq error + // The old marshaler encodes extensions at beginning. + if u.extensions.IsValid() { + e := ptr.offset(u.extensions).toExtensions() + if u.messageset { + b, err = u.appendMessageSet(b, e, deterministic) + } else { + b, err = u.appendExtensions(b, e, deterministic) + } + if err != nil { + return b, err + } + } + if u.v1extensions.IsValid() { + m := *ptr.offset(u.v1extensions).toOldExtensions() + b, err = u.appendV1Extensions(b, m, deterministic) + if err != nil { + return b, err + } + } + for _, f := range u.fields { + if f.required && errreq == nil { + if ptr.offset(f.field).getPointer().isNil() { + // Required field is not set. + // We record the error but keep going, to give a complete marshaling. + errreq = &RequiredNotSetError{f.name} + continue + } + } + if f.isPointer && ptr.offset(f.field).getPointer().isNil() { + // nil pointer always marshals to nothing + continue + } + b, err = f.marshaler(b, ptr.offset(f.field), f.wiretag, deterministic) + if err != nil { + if err1, ok := err.(*RequiredNotSetError); ok { + // Required field in submessage is not set. + // We record the error but keep going, to give a complete marshaling. + if errreq == nil { + errreq = &RequiredNotSetError{f.name + "." + err1.field} + } + continue + } + if err == errRepeatedHasNil { + err = errors.New("proto: repeated field " + f.name + " has nil element") + } + if err == errInvalidUTF8 { + fullName := revProtoTypes[reflect.PtrTo(u.typ)] + "." + f.name + err = fmt.Errorf("proto: string field %q contains invalid UTF-8", fullName) + } + return b, err + } + } + if u.unrecognized.IsValid() { + s := *ptr.offset(u.unrecognized).toBytes() + b = append(b, s...) + } + return b, errreq +} + +// computeMarshalInfo initializes the marshal info. +func (u *marshalInfo) computeMarshalInfo() { + u.Lock() + defer u.Unlock() + if u.initialized != 0 { // non-atomic read is ok as it is protected by the lock + return + } + + t := u.typ + u.unrecognized = invalidField + u.extensions = invalidField + u.v1extensions = invalidField + u.sizecache = invalidField + + // If the message can marshal itself, let it do it, for compatibility. + // NOTE: This is not efficient. + if reflect.PtrTo(t).Implements(marshalerType) { + u.hasmarshaler = true + atomic.StoreInt32(&u.initialized, 1) + return + } + + // get oneof implementers + var oneofImplementers []interface{} + if m, ok := reflect.Zero(reflect.PtrTo(t)).Interface().(oneofMessage); ok { + _, _, _, oneofImplementers = m.XXX_OneofFuncs() + } + + n := t.NumField() + + // deal with XXX fields first + for i := 0; i < t.NumField(); i++ { + f := t.Field(i) + if !strings.HasPrefix(f.Name, "XXX_") { + continue + } + switch f.Name { + case "XXX_sizecache": + u.sizecache = toField(&f) + case "XXX_unrecognized": + u.unrecognized = toField(&f) + case "XXX_InternalExtensions": + u.extensions = toField(&f) + u.messageset = f.Tag.Get("protobuf_messageset") == "1" + case "XXX_extensions": + u.v1extensions = toField(&f) + case "XXX_NoUnkeyedLiteral": + // nothing to do + default: + panic("unknown XXX field: " + f.Name) + } + n-- + } + + // normal fields + fields := make([]marshalFieldInfo, n) // batch allocation + u.fields = make([]*marshalFieldInfo, 0, n) + for i, j := 0, 0; i < t.NumField(); i++ { + f := t.Field(i) + + if strings.HasPrefix(f.Name, "XXX_") { + continue + } + field := &fields[j] + j++ + field.name = f.Name + u.fields = append(u.fields, field) + if f.Tag.Get("protobuf_oneof") != "" { + field.computeOneofFieldInfo(&f, oneofImplementers) + continue + } + if f.Tag.Get("protobuf") == "" { + // field has no tag (not in generated message), ignore it + u.fields = u.fields[:len(u.fields)-1] + j-- + continue + } + field.computeMarshalFieldInfo(&f) + } + + // fields are marshaled in tag order on the wire. + sort.Sort(byTag(u.fields)) + + atomic.StoreInt32(&u.initialized, 1) +} + +// helper for sorting fields by tag +type byTag []*marshalFieldInfo + +func (a byTag) Len() int { return len(a) } +func (a byTag) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a byTag) Less(i, j int) bool { return a[i].wiretag < a[j].wiretag } + +// getExtElemInfo returns the information to marshal an extension element. +// The info it returns is initialized. +func (u *marshalInfo) getExtElemInfo(desc *ExtensionDesc) *marshalElemInfo { + // get from cache first + u.RLock() + e, ok := u.extElems[desc.Field] + u.RUnlock() + if ok { + return e + } + + t := reflect.TypeOf(desc.ExtensionType) // pointer or slice to basic type or struct + tags := strings.Split(desc.Tag, ",") + tag, err := strconv.Atoi(tags[1]) + if err != nil { + panic("tag is not an integer") + } + wt := wiretype(tags[0]) + sizer, marshaler := typeMarshaler(t, tags, false, false) + e = &marshalElemInfo{ + wiretag: uint64(tag)<<3 | wt, + tagsize: SizeVarint(uint64(tag) << 3), + sizer: sizer, + marshaler: marshaler, + isptr: t.Kind() == reflect.Ptr, + } + + // update cache + u.Lock() + if u.extElems == nil { + u.extElems = make(map[int32]*marshalElemInfo) + } + u.extElems[desc.Field] = e + u.Unlock() + return e +} + +// computeMarshalFieldInfo fills up the information to marshal a field. +func (fi *marshalFieldInfo) computeMarshalFieldInfo(f *reflect.StructField) { + // parse protobuf tag of the field. + // tag has format of "bytes,49,opt,name=foo,def=hello!" + tags := strings.Split(f.Tag.Get("protobuf"), ",") + if tags[0] == "" { + return + } + tag, err := strconv.Atoi(tags[1]) + if err != nil { + panic("tag is not an integer") + } + wt := wiretype(tags[0]) + if tags[2] == "req" { + fi.required = true + } + fi.setTag(f, tag, wt) + fi.setMarshaler(f, tags) +} + +func (fi *marshalFieldInfo) computeOneofFieldInfo(f *reflect.StructField, oneofImplementers []interface{}) { + fi.field = toField(f) + fi.wiretag = 1<<31 - 1 // Use a large tag number, make oneofs sorted at the end. This tag will not appear on the wire. + fi.isPointer = true + fi.sizer, fi.marshaler = makeOneOfMarshaler(fi, f) + fi.oneofElems = make(map[reflect.Type]*marshalElemInfo) + + ityp := f.Type // interface type + for _, o := range oneofImplementers { + t := reflect.TypeOf(o) + if !t.Implements(ityp) { + continue + } + sf := t.Elem().Field(0) // oneof implementer is a struct with a single field + tags := strings.Split(sf.Tag.Get("protobuf"), ",") + tag, err := strconv.Atoi(tags[1]) + if err != nil { + panic("tag is not an integer") + } + wt := wiretype(tags[0]) + sizer, marshaler := typeMarshaler(sf.Type, tags, false, true) // oneof should not omit any zero value + fi.oneofElems[t.Elem()] = &marshalElemInfo{ + wiretag: uint64(tag)<<3 | wt, + tagsize: SizeVarint(uint64(tag) << 3), + sizer: sizer, + marshaler: marshaler, + } + } +} + +type oneofMessage interface { + XXX_OneofFuncs() (func(Message, *Buffer) error, func(Message, int, int, *Buffer) (bool, error), func(Message) int, []interface{}) +} + +// wiretype returns the wire encoding of the type. +func wiretype(encoding string) uint64 { + switch encoding { + case "fixed32": + return WireFixed32 + case "fixed64": + return WireFixed64 + case "varint", "zigzag32", "zigzag64": + return WireVarint + case "bytes": + return WireBytes + case "group": + return WireStartGroup + } + panic("unknown wire type " + encoding) +} + +// setTag fills up the tag (in wire format) and its size in the info of a field. +func (fi *marshalFieldInfo) setTag(f *reflect.StructField, tag int, wt uint64) { + fi.field = toField(f) + fi.wiretag = uint64(tag)<<3 | wt + fi.tagsize = SizeVarint(uint64(tag) << 3) +} + +// setMarshaler fills up the sizer and marshaler in the info of a field. +func (fi *marshalFieldInfo) setMarshaler(f *reflect.StructField, tags []string) { + switch f.Type.Kind() { + case reflect.Map: + // map field + fi.isPointer = true + fi.sizer, fi.marshaler = makeMapMarshaler(f) + return + case reflect.Ptr, reflect.Slice: + fi.isPointer = true + } + fi.sizer, fi.marshaler = typeMarshaler(f.Type, tags, true, false) +} + +// typeMarshaler returns the sizer and marshaler of a given field. +// t is the type of the field. +// tags is the generated "protobuf" tag of the field. +// If nozero is true, zero value is not marshaled to the wire. +// If oneof is true, it is a oneof field. +func typeMarshaler(t reflect.Type, tags []string, nozero, oneof bool) (sizer, marshaler) { + encoding := tags[0] + + pointer := false + slice := false + if t.Kind() == reflect.Slice && t.Elem().Kind() != reflect.Uint8 { + slice = true + t = t.Elem() + } + if t.Kind() == reflect.Ptr { + pointer = true + t = t.Elem() + } + + packed := false + proto3 := false + for i := 2; i < len(tags); i++ { + if tags[i] == "packed" { + packed = true + } + if tags[i] == "proto3" { + proto3 = true + } + } + + switch t.Kind() { + case reflect.Bool: + if pointer { + return sizeBoolPtr, appendBoolPtr + } + if slice { + if packed { + return sizeBoolPackedSlice, appendBoolPackedSlice + } + return sizeBoolSlice, appendBoolSlice + } + if nozero { + return sizeBoolValueNoZero, appendBoolValueNoZero + } + return sizeBoolValue, appendBoolValue + case reflect.Uint32: + switch encoding { + case "fixed32": + if pointer { + return sizeFixed32Ptr, appendFixed32Ptr + } + if slice { + if packed { + return sizeFixed32PackedSlice, appendFixed32PackedSlice + } + return sizeFixed32Slice, appendFixed32Slice + } + if nozero { + return sizeFixed32ValueNoZero, appendFixed32ValueNoZero + } + return sizeFixed32Value, appendFixed32Value + case "varint": + if pointer { + return sizeVarint32Ptr, appendVarint32Ptr + } + if slice { + if packed { + return sizeVarint32PackedSlice, appendVarint32PackedSlice + } + return sizeVarint32Slice, appendVarint32Slice + } + if nozero { + return sizeVarint32ValueNoZero, appendVarint32ValueNoZero + } + return sizeVarint32Value, appendVarint32Value + } + case reflect.Int32: + switch encoding { + case "fixed32": + if pointer { + return sizeFixedS32Ptr, appendFixedS32Ptr + } + if slice { + if packed { + return sizeFixedS32PackedSlice, appendFixedS32PackedSlice + } + return sizeFixedS32Slice, appendFixedS32Slice + } + if nozero { + return sizeFixedS32ValueNoZero, appendFixedS32ValueNoZero + } + return sizeFixedS32Value, appendFixedS32Value + case "varint": + if pointer { + return sizeVarintS32Ptr, appendVarintS32Ptr + } + if slice { + if packed { + return sizeVarintS32PackedSlice, appendVarintS32PackedSlice + } + return sizeVarintS32Slice, appendVarintS32Slice + } + if nozero { + return sizeVarintS32ValueNoZero, appendVarintS32ValueNoZero + } + return sizeVarintS32Value, appendVarintS32Value + case "zigzag32": + if pointer { + return sizeZigzag32Ptr, appendZigzag32Ptr + } + if slice { + if packed { + return sizeZigzag32PackedSlice, appendZigzag32PackedSlice + } + return sizeZigzag32Slice, appendZigzag32Slice + } + if nozero { + return sizeZigzag32ValueNoZero, appendZigzag32ValueNoZero + } + return sizeZigzag32Value, appendZigzag32Value + } + case reflect.Uint64: + switch encoding { + case "fixed64": + if pointer { + return sizeFixed64Ptr, appendFixed64Ptr + } + if slice { + if packed { + return sizeFixed64PackedSlice, appendFixed64PackedSlice + } + return sizeFixed64Slice, appendFixed64Slice + } + if nozero { + return sizeFixed64ValueNoZero, appendFixed64ValueNoZero + } + return sizeFixed64Value, appendFixed64Value + case "varint": + if pointer { + return sizeVarint64Ptr, appendVarint64Ptr + } + if slice { + if packed { + return sizeVarint64PackedSlice, appendVarint64PackedSlice + } + return sizeVarint64Slice, appendVarint64Slice + } + if nozero { + return sizeVarint64ValueNoZero, appendVarint64ValueNoZero + } + return sizeVarint64Value, appendVarint64Value + } + case reflect.Int64: + switch encoding { + case "fixed64": + if pointer { + return sizeFixedS64Ptr, appendFixedS64Ptr + } + if slice { + if packed { + return sizeFixedS64PackedSlice, appendFixedS64PackedSlice + } + return sizeFixedS64Slice, appendFixedS64Slice + } + if nozero { + return sizeFixedS64ValueNoZero, appendFixedS64ValueNoZero + } + return sizeFixedS64Value, appendFixedS64Value + case "varint": + if pointer { + return sizeVarintS64Ptr, appendVarintS64Ptr + } + if slice { + if packed { + return sizeVarintS64PackedSlice, appendVarintS64PackedSlice + } + return sizeVarintS64Slice, appendVarintS64Slice + } + if nozero { + return sizeVarintS64ValueNoZero, appendVarintS64ValueNoZero + } + return sizeVarintS64Value, appendVarintS64Value + case "zigzag64": + if pointer { + return sizeZigzag64Ptr, appendZigzag64Ptr + } + if slice { + if packed { + return sizeZigzag64PackedSlice, appendZigzag64PackedSlice + } + return sizeZigzag64Slice, appendZigzag64Slice + } + if nozero { + return sizeZigzag64ValueNoZero, appendZigzag64ValueNoZero + } + return sizeZigzag64Value, appendZigzag64Value + } + case reflect.Float32: + if pointer { + return sizeFloat32Ptr, appendFloat32Ptr + } + if slice { + if packed { + return sizeFloat32PackedSlice, appendFloat32PackedSlice + } + return sizeFloat32Slice, appendFloat32Slice + } + if nozero { + return sizeFloat32ValueNoZero, appendFloat32ValueNoZero + } + return sizeFloat32Value, appendFloat32Value + case reflect.Float64: + if pointer { + return sizeFloat64Ptr, appendFloat64Ptr + } + if slice { + if packed { + return sizeFloat64PackedSlice, appendFloat64PackedSlice + } + return sizeFloat64Slice, appendFloat64Slice + } + if nozero { + return sizeFloat64ValueNoZero, appendFloat64ValueNoZero + } + return sizeFloat64Value, appendFloat64Value + case reflect.String: + if pointer { + return sizeStringPtr, appendStringPtr + } + if slice { + return sizeStringSlice, appendStringSlice + } + if nozero { + return sizeStringValueNoZero, appendStringValueNoZero + } + return sizeStringValue, appendStringValue + case reflect.Slice: + if slice { + return sizeBytesSlice, appendBytesSlice + } + if oneof { + // Oneof bytes field may also have "proto3" tag. + // We want to marshal it as a oneof field. Do this + // check before the proto3 check. + return sizeBytesOneof, appendBytesOneof + } + if proto3 { + return sizeBytes3, appendBytes3 + } + return sizeBytes, appendBytes + case reflect.Struct: + switch encoding { + case "group": + if slice { + return makeGroupSliceMarshaler(getMarshalInfo(t)) + } + return makeGroupMarshaler(getMarshalInfo(t)) + case "bytes": + if slice { + return makeMessageSliceMarshaler(getMarshalInfo(t)) + } + return makeMessageMarshaler(getMarshalInfo(t)) + } + } + panic(fmt.Sprintf("unknown or mismatched type: type: %v, wire type: %v", t, encoding)) +} + +// Below are functions to size/marshal a specific type of a field. +// They are stored in the field's info, and called by function pointers. +// They have type sizer or marshaler. + +func sizeFixed32Value(_ pointer, tagsize int) int { + return 4 + tagsize +} +func sizeFixed32ValueNoZero(ptr pointer, tagsize int) int { + v := *ptr.toUint32() + if v == 0 { + return 0 + } + return 4 + tagsize +} +func sizeFixed32Ptr(ptr pointer, tagsize int) int { + p := *ptr.toUint32Ptr() + if p == nil { + return 0 + } + return 4 + tagsize +} +func sizeFixed32Slice(ptr pointer, tagsize int) int { + s := *ptr.toUint32Slice() + return (4 + tagsize) * len(s) +} +func sizeFixed32PackedSlice(ptr pointer, tagsize int) int { + s := *ptr.toUint32Slice() + if len(s) == 0 { + return 0 + } + return 4*len(s) + SizeVarint(uint64(4*len(s))) + tagsize +} +func sizeFixedS32Value(_ pointer, tagsize int) int { + return 4 + tagsize +} +func sizeFixedS32ValueNoZero(ptr pointer, tagsize int) int { + v := *ptr.toInt32() + if v == 0 { + return 0 + } + return 4 + tagsize +} +func sizeFixedS32Ptr(ptr pointer, tagsize int) int { + p := ptr.getInt32Ptr() + if p == nil { + return 0 + } + return 4 + tagsize +} +func sizeFixedS32Slice(ptr pointer, tagsize int) int { + s := ptr.getInt32Slice() + return (4 + tagsize) * len(s) +} +func sizeFixedS32PackedSlice(ptr pointer, tagsize int) int { + s := ptr.getInt32Slice() + if len(s) == 0 { + return 0 + } + return 4*len(s) + SizeVarint(uint64(4*len(s))) + tagsize +} +func sizeFloat32Value(_ pointer, tagsize int) int { + return 4 + tagsize +} +func sizeFloat32ValueNoZero(ptr pointer, tagsize int) int { + v := math.Float32bits(*ptr.toFloat32()) + if v == 0 { + return 0 + } + return 4 + tagsize +} +func sizeFloat32Ptr(ptr pointer, tagsize int) int { + p := *ptr.toFloat32Ptr() + if p == nil { + return 0 + } + return 4 + tagsize +} +func sizeFloat32Slice(ptr pointer, tagsize int) int { + s := *ptr.toFloat32Slice() + return (4 + tagsize) * len(s) +} +func sizeFloat32PackedSlice(ptr pointer, tagsize int) int { + s := *ptr.toFloat32Slice() + if len(s) == 0 { + return 0 + } + return 4*len(s) + SizeVarint(uint64(4*len(s))) + tagsize +} +func sizeFixed64Value(_ pointer, tagsize int) int { + return 8 + tagsize +} +func sizeFixed64ValueNoZero(ptr pointer, tagsize int) int { + v := *ptr.toUint64() + if v == 0 { + return 0 + } + return 8 + tagsize +} +func sizeFixed64Ptr(ptr pointer, tagsize int) int { + p := *ptr.toUint64Ptr() + if p == nil { + return 0 + } + return 8 + tagsize +} +func sizeFixed64Slice(ptr pointer, tagsize int) int { + s := *ptr.toUint64Slice() + return (8 + tagsize) * len(s) +} +func sizeFixed64PackedSlice(ptr pointer, tagsize int) int { + s := *ptr.toUint64Slice() + if len(s) == 0 { + return 0 + } + return 8*len(s) + SizeVarint(uint64(8*len(s))) + tagsize +} +func sizeFixedS64Value(_ pointer, tagsize int) int { + return 8 + tagsize +} +func sizeFixedS64ValueNoZero(ptr pointer, tagsize int) int { + v := *ptr.toInt64() + if v == 0 { + return 0 + } + return 8 + tagsize +} +func sizeFixedS64Ptr(ptr pointer, tagsize int) int { + p := *ptr.toInt64Ptr() + if p == nil { + return 0 + } + return 8 + tagsize +} +func sizeFixedS64Slice(ptr pointer, tagsize int) int { + s := *ptr.toInt64Slice() + return (8 + tagsize) * len(s) +} +func sizeFixedS64PackedSlice(ptr pointer, tagsize int) int { + s := *ptr.toInt64Slice() + if len(s) == 0 { + return 0 + } + return 8*len(s) + SizeVarint(uint64(8*len(s))) + tagsize +} +func sizeFloat64Value(_ pointer, tagsize int) int { + return 8 + tagsize +} +func sizeFloat64ValueNoZero(ptr pointer, tagsize int) int { + v := math.Float64bits(*ptr.toFloat64()) + if v == 0 { + return 0 + } + return 8 + tagsize +} +func sizeFloat64Ptr(ptr pointer, tagsize int) int { + p := *ptr.toFloat64Ptr() + if p == nil { + return 0 + } + return 8 + tagsize +} +func sizeFloat64Slice(ptr pointer, tagsize int) int { + s := *ptr.toFloat64Slice() + return (8 + tagsize) * len(s) +} +func sizeFloat64PackedSlice(ptr pointer, tagsize int) int { + s := *ptr.toFloat64Slice() + if len(s) == 0 { + return 0 + } + return 8*len(s) + SizeVarint(uint64(8*len(s))) + tagsize +} +func sizeVarint32Value(ptr pointer, tagsize int) int { + v := *ptr.toUint32() + return SizeVarint(uint64(v)) + tagsize +} +func sizeVarint32ValueNoZero(ptr pointer, tagsize int) int { + v := *ptr.toUint32() + if v == 0 { + return 0 + } + return SizeVarint(uint64(v)) + tagsize +} +func sizeVarint32Ptr(ptr pointer, tagsize int) int { + p := *ptr.toUint32Ptr() + if p == nil { + return 0 + } + return SizeVarint(uint64(*p)) + tagsize +} +func sizeVarint32Slice(ptr pointer, tagsize int) int { + s := *ptr.toUint32Slice() + n := 0 + for _, v := range s { + n += SizeVarint(uint64(v)) + tagsize + } + return n +} +func sizeVarint32PackedSlice(ptr pointer, tagsize int) int { + s := *ptr.toUint32Slice() + if len(s) == 0 { + return 0 + } + n := 0 + for _, v := range s { + n += SizeVarint(uint64(v)) + } + return n + SizeVarint(uint64(n)) + tagsize +} +func sizeVarintS32Value(ptr pointer, tagsize int) int { + v := *ptr.toInt32() + return SizeVarint(uint64(v)) + tagsize +} +func sizeVarintS32ValueNoZero(ptr pointer, tagsize int) int { + v := *ptr.toInt32() + if v == 0 { + return 0 + } + return SizeVarint(uint64(v)) + tagsize +} +func sizeVarintS32Ptr(ptr pointer, tagsize int) int { + p := ptr.getInt32Ptr() + if p == nil { + return 0 + } + return SizeVarint(uint64(*p)) + tagsize +} +func sizeVarintS32Slice(ptr pointer, tagsize int) int { + s := ptr.getInt32Slice() + n := 0 + for _, v := range s { + n += SizeVarint(uint64(v)) + tagsize + } + return n +} +func sizeVarintS32PackedSlice(ptr pointer, tagsize int) int { + s := ptr.getInt32Slice() + if len(s) == 0 { + return 0 + } + n := 0 + for _, v := range s { + n += SizeVarint(uint64(v)) + } + return n + SizeVarint(uint64(n)) + tagsize +} +func sizeVarint64Value(ptr pointer, tagsize int) int { + v := *ptr.toUint64() + return SizeVarint(v) + tagsize +} +func sizeVarint64ValueNoZero(ptr pointer, tagsize int) int { + v := *ptr.toUint64() + if v == 0 { + return 0 + } + return SizeVarint(v) + tagsize +} +func sizeVarint64Ptr(ptr pointer, tagsize int) int { + p := *ptr.toUint64Ptr() + if p == nil { + return 0 + } + return SizeVarint(*p) + tagsize +} +func sizeVarint64Slice(ptr pointer, tagsize int) int { + s := *ptr.toUint64Slice() + n := 0 + for _, v := range s { + n += SizeVarint(v) + tagsize + } + return n +} +func sizeVarint64PackedSlice(ptr pointer, tagsize int) int { + s := *ptr.toUint64Slice() + if len(s) == 0 { + return 0 + } + n := 0 + for _, v := range s { + n += SizeVarint(v) + } + return n + SizeVarint(uint64(n)) + tagsize +} +func sizeVarintS64Value(ptr pointer, tagsize int) int { + v := *ptr.toInt64() + return SizeVarint(uint64(v)) + tagsize +} +func sizeVarintS64ValueNoZero(ptr pointer, tagsize int) int { + v := *ptr.toInt64() + if v == 0 { + return 0 + } + return SizeVarint(uint64(v)) + tagsize +} +func sizeVarintS64Ptr(ptr pointer, tagsize int) int { + p := *ptr.toInt64Ptr() + if p == nil { + return 0 + } + return SizeVarint(uint64(*p)) + tagsize +} +func sizeVarintS64Slice(ptr pointer, tagsize int) int { + s := *ptr.toInt64Slice() + n := 0 + for _, v := range s { + n += SizeVarint(uint64(v)) + tagsize + } + return n +} +func sizeVarintS64PackedSlice(ptr pointer, tagsize int) int { + s := *ptr.toInt64Slice() + if len(s) == 0 { + return 0 + } + n := 0 + for _, v := range s { + n += SizeVarint(uint64(v)) + } + return n + SizeVarint(uint64(n)) + tagsize +} +func sizeZigzag32Value(ptr pointer, tagsize int) int { + v := *ptr.toInt32() + return SizeVarint(uint64((uint32(v)<<1)^uint32((int32(v)>>31)))) + tagsize +} +func sizeZigzag32ValueNoZero(ptr pointer, tagsize int) int { + v := *ptr.toInt32() + if v == 0 { + return 0 + } + return SizeVarint(uint64((uint32(v)<<1)^uint32((int32(v)>>31)))) + tagsize +} +func sizeZigzag32Ptr(ptr pointer, tagsize int) int { + p := ptr.getInt32Ptr() + if p == nil { + return 0 + } + v := *p + return SizeVarint(uint64((uint32(v)<<1)^uint32((int32(v)>>31)))) + tagsize +} +func sizeZigzag32Slice(ptr pointer, tagsize int) int { + s := ptr.getInt32Slice() + n := 0 + for _, v := range s { + n += SizeVarint(uint64((uint32(v)<<1)^uint32((int32(v)>>31)))) + tagsize + } + return n +} +func sizeZigzag32PackedSlice(ptr pointer, tagsize int) int { + s := ptr.getInt32Slice() + if len(s) == 0 { + return 0 + } + n := 0 + for _, v := range s { + n += SizeVarint(uint64((uint32(v) << 1) ^ uint32((int32(v) >> 31)))) + } + return n + SizeVarint(uint64(n)) + tagsize +} +func sizeZigzag64Value(ptr pointer, tagsize int) int { + v := *ptr.toInt64() + return SizeVarint(uint64(v<<1)^uint64((int64(v)>>63))) + tagsize +} +func sizeZigzag64ValueNoZero(ptr pointer, tagsize int) int { + v := *ptr.toInt64() + if v == 0 { + return 0 + } + return SizeVarint(uint64(v<<1)^uint64((int64(v)>>63))) + tagsize +} +func sizeZigzag64Ptr(ptr pointer, tagsize int) int { + p := *ptr.toInt64Ptr() + if p == nil { + return 0 + } + v := *p + return SizeVarint(uint64(v<<1)^uint64((int64(v)>>63))) + tagsize +} +func sizeZigzag64Slice(ptr pointer, tagsize int) int { + s := *ptr.toInt64Slice() + n := 0 + for _, v := range s { + n += SizeVarint(uint64(v<<1)^uint64((int64(v)>>63))) + tagsize + } + return n +} +func sizeZigzag64PackedSlice(ptr pointer, tagsize int) int { + s := *ptr.toInt64Slice() + if len(s) == 0 { + return 0 + } + n := 0 + for _, v := range s { + n += SizeVarint(uint64(v<<1) ^ uint64((int64(v) >> 63))) + } + return n + SizeVarint(uint64(n)) + tagsize +} +func sizeBoolValue(_ pointer, tagsize int) int { + return 1 + tagsize +} +func sizeBoolValueNoZero(ptr pointer, tagsize int) int { + v := *ptr.toBool() + if !v { + return 0 + } + return 1 + tagsize +} +func sizeBoolPtr(ptr pointer, tagsize int) int { + p := *ptr.toBoolPtr() + if p == nil { + return 0 + } + return 1 + tagsize +} +func sizeBoolSlice(ptr pointer, tagsize int) int { + s := *ptr.toBoolSlice() + return (1 + tagsize) * len(s) +} +func sizeBoolPackedSlice(ptr pointer, tagsize int) int { + s := *ptr.toBoolSlice() + if len(s) == 0 { + return 0 + } + return len(s) + SizeVarint(uint64(len(s))) + tagsize +} +func sizeStringValue(ptr pointer, tagsize int) int { + v := *ptr.toString() + return len(v) + SizeVarint(uint64(len(v))) + tagsize +} +func sizeStringValueNoZero(ptr pointer, tagsize int) int { + v := *ptr.toString() + if v == "" { + return 0 + } + return len(v) + SizeVarint(uint64(len(v))) + tagsize +} +func sizeStringPtr(ptr pointer, tagsize int) int { + p := *ptr.toStringPtr() + if p == nil { + return 0 + } + v := *p + return len(v) + SizeVarint(uint64(len(v))) + tagsize +} +func sizeStringSlice(ptr pointer, tagsize int) int { + s := *ptr.toStringSlice() + n := 0 + for _, v := range s { + n += len(v) + SizeVarint(uint64(len(v))) + tagsize + } + return n +} +func sizeBytes(ptr pointer, tagsize int) int { + v := *ptr.toBytes() + if v == nil { + return 0 + } + return len(v) + SizeVarint(uint64(len(v))) + tagsize +} +func sizeBytes3(ptr pointer, tagsize int) int { + v := *ptr.toBytes() + if len(v) == 0 { + return 0 + } + return len(v) + SizeVarint(uint64(len(v))) + tagsize +} +func sizeBytesOneof(ptr pointer, tagsize int) int { + v := *ptr.toBytes() + return len(v) + SizeVarint(uint64(len(v))) + tagsize +} +func sizeBytesSlice(ptr pointer, tagsize int) int { + s := *ptr.toBytesSlice() + n := 0 + for _, v := range s { + n += len(v) + SizeVarint(uint64(len(v))) + tagsize + } + return n +} + +// appendFixed32 appends an encoded fixed32 to b. +func appendFixed32(b []byte, v uint32) []byte { + b = append(b, + byte(v), + byte(v>>8), + byte(v>>16), + byte(v>>24)) + return b +} + +// appendFixed64 appends an encoded fixed64 to b. +func appendFixed64(b []byte, v uint64) []byte { + b = append(b, + byte(v), + byte(v>>8), + byte(v>>16), + byte(v>>24), + byte(v>>32), + byte(v>>40), + byte(v>>48), + byte(v>>56)) + return b +} + +// appendVarint appends an encoded varint to b. +func appendVarint(b []byte, v uint64) []byte { + // TODO: make 1-byte (maybe 2-byte) case inline-able, once we + // have non-leaf inliner. + switch { + case v < 1<<7: + b = append(b, byte(v)) + case v < 1<<14: + b = append(b, + byte(v&0x7f|0x80), + byte(v>>7)) + case v < 1<<21: + b = append(b, + byte(v&0x7f|0x80), + byte((v>>7)&0x7f|0x80), + byte(v>>14)) + case v < 1<<28: + b = append(b, + byte(v&0x7f|0x80), + byte((v>>7)&0x7f|0x80), + byte((v>>14)&0x7f|0x80), + byte(v>>21)) + case v < 1<<35: + b = append(b, + byte(v&0x7f|0x80), + byte((v>>7)&0x7f|0x80), + byte((v>>14)&0x7f|0x80), + byte((v>>21)&0x7f|0x80), + byte(v>>28)) + case v < 1<<42: + b = append(b, + byte(v&0x7f|0x80), + byte((v>>7)&0x7f|0x80), + byte((v>>14)&0x7f|0x80), + byte((v>>21)&0x7f|0x80), + byte((v>>28)&0x7f|0x80), + byte(v>>35)) + case v < 1<<49: + b = append(b, + byte(v&0x7f|0x80), + byte((v>>7)&0x7f|0x80), + byte((v>>14)&0x7f|0x80), + byte((v>>21)&0x7f|0x80), + byte((v>>28)&0x7f|0x80), + byte((v>>35)&0x7f|0x80), + byte(v>>42)) + case v < 1<<56: + b = append(b, + byte(v&0x7f|0x80), + byte((v>>7)&0x7f|0x80), + byte((v>>14)&0x7f|0x80), + byte((v>>21)&0x7f|0x80), + byte((v>>28)&0x7f|0x80), + byte((v>>35)&0x7f|0x80), + byte((v>>42)&0x7f|0x80), + byte(v>>49)) + case v < 1<<63: + b = append(b, + byte(v&0x7f|0x80), + byte((v>>7)&0x7f|0x80), + byte((v>>14)&0x7f|0x80), + byte((v>>21)&0x7f|0x80), + byte((v>>28)&0x7f|0x80), + byte((v>>35)&0x7f|0x80), + byte((v>>42)&0x7f|0x80), + byte((v>>49)&0x7f|0x80), + byte(v>>56)) + default: + b = append(b, + byte(v&0x7f|0x80), + byte((v>>7)&0x7f|0x80), + byte((v>>14)&0x7f|0x80), + byte((v>>21)&0x7f|0x80), + byte((v>>28)&0x7f|0x80), + byte((v>>35)&0x7f|0x80), + byte((v>>42)&0x7f|0x80), + byte((v>>49)&0x7f|0x80), + byte((v>>56)&0x7f|0x80), + 1) + } + return b +} + +func appendFixed32Value(b []byte, ptr pointer, wiretag uint64, _ bool) ([]byte, error) { + v := *ptr.toUint32() + b = appendVarint(b, wiretag) + b = appendFixed32(b, v) + return b, nil +} +func appendFixed32ValueNoZero(b []byte, ptr pointer, wiretag uint64, _ bool) ([]byte, error) { + v := *ptr.toUint32() + if v == 0 { + return b, nil + } + b = appendVarint(b, wiretag) + b = appendFixed32(b, v) + return b, nil +} +func appendFixed32Ptr(b []byte, ptr pointer, wiretag uint64, _ bool) ([]byte, error) { + p := *ptr.toUint32Ptr() + if p == nil { + return b, nil + } + b = appendVarint(b, wiretag) + b = appendFixed32(b, *p) + return b, nil +} +func appendFixed32Slice(b []byte, ptr pointer, wiretag uint64, _ bool) ([]byte, error) { + s := *ptr.toUint32Slice() + for _, v := range s { + b = appendVarint(b, wiretag) + b = appendFixed32(b, v) + } + return b, nil +} +func appendFixed32PackedSlice(b []byte, ptr pointer, wiretag uint64, _ bool) ([]byte, error) { + s := *ptr.toUint32Slice() + if len(s) == 0 { + return b, nil + } + b = appendVarint(b, wiretag&^7|WireBytes) + b = appendVarint(b, uint64(4*len(s))) + for _, v := range s { + b = appendFixed32(b, v) + } + return b, nil +} +func appendFixedS32Value(b []byte, ptr pointer, wiretag uint64, _ bool) ([]byte, error) { + v := *ptr.toInt32() + b = appendVarint(b, wiretag) + b = appendFixed32(b, uint32(v)) + return b, nil +} +func appendFixedS32ValueNoZero(b []byte, ptr pointer, wiretag uint64, _ bool) ([]byte, error) { + v := *ptr.toInt32() + if v == 0 { + return b, nil + } + b = appendVarint(b, wiretag) + b = appendFixed32(b, uint32(v)) + return b, nil +} +func appendFixedS32Ptr(b []byte, ptr pointer, wiretag uint64, _ bool) ([]byte, error) { + p := ptr.getInt32Ptr() + if p == nil { + return b, nil + } + b = appendVarint(b, wiretag) + b = appendFixed32(b, uint32(*p)) + return b, nil +} +func appendFixedS32Slice(b []byte, ptr pointer, wiretag uint64, _ bool) ([]byte, error) { + s := ptr.getInt32Slice() + for _, v := range s { + b = appendVarint(b, wiretag) + b = appendFixed32(b, uint32(v)) + } + return b, nil +} +func appendFixedS32PackedSlice(b []byte, ptr pointer, wiretag uint64, _ bool) ([]byte, error) { + s := ptr.getInt32Slice() + if len(s) == 0 { + return b, nil + } + b = appendVarint(b, wiretag&^7|WireBytes) + b = appendVarint(b, uint64(4*len(s))) + for _, v := range s { + b = appendFixed32(b, uint32(v)) + } + return b, nil +} +func appendFloat32Value(b []byte, ptr pointer, wiretag uint64, _ bool) ([]byte, error) { + v := math.Float32bits(*ptr.toFloat32()) + b = appendVarint(b, wiretag) + b = appendFixed32(b, v) + return b, nil +} +func appendFloat32ValueNoZero(b []byte, ptr pointer, wiretag uint64, _ bool) ([]byte, error) { + v := math.Float32bits(*ptr.toFloat32()) + if v == 0 { + return b, nil + } + b = appendVarint(b, wiretag) + b = appendFixed32(b, v) + return b, nil +} +func appendFloat32Ptr(b []byte, ptr pointer, wiretag uint64, _ bool) ([]byte, error) { + p := *ptr.toFloat32Ptr() + if p == nil { + return b, nil + } + b = appendVarint(b, wiretag) + b = appendFixed32(b, math.Float32bits(*p)) + return b, nil +} +func appendFloat32Slice(b []byte, ptr pointer, wiretag uint64, _ bool) ([]byte, error) { + s := *ptr.toFloat32Slice() + for _, v := range s { + b = appendVarint(b, wiretag) + b = appendFixed32(b, math.Float32bits(v)) + } + return b, nil +} +func appendFloat32PackedSlice(b []byte, ptr pointer, wiretag uint64, _ bool) ([]byte, error) { + s := *ptr.toFloat32Slice() + if len(s) == 0 { + return b, nil + } + b = appendVarint(b, wiretag&^7|WireBytes) + b = appendVarint(b, uint64(4*len(s))) + for _, v := range s { + b = appendFixed32(b, math.Float32bits(v)) + } + return b, nil +} +func appendFixed64Value(b []byte, ptr pointer, wiretag uint64, _ bool) ([]byte, error) { + v := *ptr.toUint64() + b = appendVarint(b, wiretag) + b = appendFixed64(b, v) + return b, nil +} +func appendFixed64ValueNoZero(b []byte, ptr pointer, wiretag uint64, _ bool) ([]byte, error) { + v := *ptr.toUint64() + if v == 0 { + return b, nil + } + b = appendVarint(b, wiretag) + b = appendFixed64(b, v) + return b, nil +} +func appendFixed64Ptr(b []byte, ptr pointer, wiretag uint64, _ bool) ([]byte, error) { + p := *ptr.toUint64Ptr() + if p == nil { + return b, nil + } + b = appendVarint(b, wiretag) + b = appendFixed64(b, *p) + return b, nil +} +func appendFixed64Slice(b []byte, ptr pointer, wiretag uint64, _ bool) ([]byte, error) { + s := *ptr.toUint64Slice() + for _, v := range s { + b = appendVarint(b, wiretag) + b = appendFixed64(b, v) + } + return b, nil +} +func appendFixed64PackedSlice(b []byte, ptr pointer, wiretag uint64, _ bool) ([]byte, error) { + s := *ptr.toUint64Slice() + if len(s) == 0 { + return b, nil + } + b = appendVarint(b, wiretag&^7|WireBytes) + b = appendVarint(b, uint64(8*len(s))) + for _, v := range s { + b = appendFixed64(b, v) + } + return b, nil +} +func appendFixedS64Value(b []byte, ptr pointer, wiretag uint64, _ bool) ([]byte, error) { + v := *ptr.toInt64() + b = appendVarint(b, wiretag) + b = appendFixed64(b, uint64(v)) + return b, nil +} +func appendFixedS64ValueNoZero(b []byte, ptr pointer, wiretag uint64, _ bool) ([]byte, error) { + v := *ptr.toInt64() + if v == 0 { + return b, nil + } + b = appendVarint(b, wiretag) + b = appendFixed64(b, uint64(v)) + return b, nil +} +func appendFixedS64Ptr(b []byte, ptr pointer, wiretag uint64, _ bool) ([]byte, error) { + p := *ptr.toInt64Ptr() + if p == nil { + return b, nil + } + b = appendVarint(b, wiretag) + b = appendFixed64(b, uint64(*p)) + return b, nil +} +func appendFixedS64Slice(b []byte, ptr pointer, wiretag uint64, _ bool) ([]byte, error) { + s := *ptr.toInt64Slice() + for _, v := range s { + b = appendVarint(b, wiretag) + b = appendFixed64(b, uint64(v)) + } + return b, nil +} +func appendFixedS64PackedSlice(b []byte, ptr pointer, wiretag uint64, _ bool) ([]byte, error) { + s := *ptr.toInt64Slice() + if len(s) == 0 { + return b, nil + } + b = appendVarint(b, wiretag&^7|WireBytes) + b = appendVarint(b, uint64(8*len(s))) + for _, v := range s { + b = appendFixed64(b, uint64(v)) + } + return b, nil +} +func appendFloat64Value(b []byte, ptr pointer, wiretag uint64, _ bool) ([]byte, error) { + v := math.Float64bits(*ptr.toFloat64()) + b = appendVarint(b, wiretag) + b = appendFixed64(b, v) + return b, nil +} +func appendFloat64ValueNoZero(b []byte, ptr pointer, wiretag uint64, _ bool) ([]byte, error) { + v := math.Float64bits(*ptr.toFloat64()) + if v == 0 { + return b, nil + } + b = appendVarint(b, wiretag) + b = appendFixed64(b, v) + return b, nil +} +func appendFloat64Ptr(b []byte, ptr pointer, wiretag uint64, _ bool) ([]byte, error) { + p := *ptr.toFloat64Ptr() + if p == nil { + return b, nil + } + b = appendVarint(b, wiretag) + b = appendFixed64(b, math.Float64bits(*p)) + return b, nil +} +func appendFloat64Slice(b []byte, ptr pointer, wiretag uint64, _ bool) ([]byte, error) { + s := *ptr.toFloat64Slice() + for _, v := range s { + b = appendVarint(b, wiretag) + b = appendFixed64(b, math.Float64bits(v)) + } + return b, nil +} +func appendFloat64PackedSlice(b []byte, ptr pointer, wiretag uint64, _ bool) ([]byte, error) { + s := *ptr.toFloat64Slice() + if len(s) == 0 { + return b, nil + } + b = appendVarint(b, wiretag&^7|WireBytes) + b = appendVarint(b, uint64(8*len(s))) + for _, v := range s { + b = appendFixed64(b, math.Float64bits(v)) + } + return b, nil +} +func appendVarint32Value(b []byte, ptr pointer, wiretag uint64, _ bool) ([]byte, error) { + v := *ptr.toUint32() + b = appendVarint(b, wiretag) + b = appendVarint(b, uint64(v)) + return b, nil +} +func appendVarint32ValueNoZero(b []byte, ptr pointer, wiretag uint64, _ bool) ([]byte, error) { + v := *ptr.toUint32() + if v == 0 { + return b, nil + } + b = appendVarint(b, wiretag) + b = appendVarint(b, uint64(v)) + return b, nil +} +func appendVarint32Ptr(b []byte, ptr pointer, wiretag uint64, _ bool) ([]byte, error) { + p := *ptr.toUint32Ptr() + if p == nil { + return b, nil + } + b = appendVarint(b, wiretag) + b = appendVarint(b, uint64(*p)) + return b, nil +} +func appendVarint32Slice(b []byte, ptr pointer, wiretag uint64, _ bool) ([]byte, error) { + s := *ptr.toUint32Slice() + for _, v := range s { + b = appendVarint(b, wiretag) + b = appendVarint(b, uint64(v)) + } + return b, nil +} +func appendVarint32PackedSlice(b []byte, ptr pointer, wiretag uint64, _ bool) ([]byte, error) { + s := *ptr.toUint32Slice() + if len(s) == 0 { + return b, nil + } + b = appendVarint(b, wiretag&^7|WireBytes) + // compute size + n := 0 + for _, v := range s { + n += SizeVarint(uint64(v)) + } + b = appendVarint(b, uint64(n)) + for _, v := range s { + b = appendVarint(b, uint64(v)) + } + return b, nil +} +func appendVarintS32Value(b []byte, ptr pointer, wiretag uint64, _ bool) ([]byte, error) { + v := *ptr.toInt32() + b = appendVarint(b, wiretag) + b = appendVarint(b, uint64(v)) + return b, nil +} +func appendVarintS32ValueNoZero(b []byte, ptr pointer, wiretag uint64, _ bool) ([]byte, error) { + v := *ptr.toInt32() + if v == 0 { + return b, nil + } + b = appendVarint(b, wiretag) + b = appendVarint(b, uint64(v)) + return b, nil +} +func appendVarintS32Ptr(b []byte, ptr pointer, wiretag uint64, _ bool) ([]byte, error) { + p := ptr.getInt32Ptr() + if p == nil { + return b, nil + } + b = appendVarint(b, wiretag) + b = appendVarint(b, uint64(*p)) + return b, nil +} +func appendVarintS32Slice(b []byte, ptr pointer, wiretag uint64, _ bool) ([]byte, error) { + s := ptr.getInt32Slice() + for _, v := range s { + b = appendVarint(b, wiretag) + b = appendVarint(b, uint64(v)) + } + return b, nil +} +func appendVarintS32PackedSlice(b []byte, ptr pointer, wiretag uint64, _ bool) ([]byte, error) { + s := ptr.getInt32Slice() + if len(s) == 0 { + return b, nil + } + b = appendVarint(b, wiretag&^7|WireBytes) + // compute size + n := 0 + for _, v := range s { + n += SizeVarint(uint64(v)) + } + b = appendVarint(b, uint64(n)) + for _, v := range s { + b = appendVarint(b, uint64(v)) + } + return b, nil +} +func appendVarint64Value(b []byte, ptr pointer, wiretag uint64, _ bool) ([]byte, error) { + v := *ptr.toUint64() + b = appendVarint(b, wiretag) + b = appendVarint(b, v) + return b, nil +} +func appendVarint64ValueNoZero(b []byte, ptr pointer, wiretag uint64, _ bool) ([]byte, error) { + v := *ptr.toUint64() + if v == 0 { + return b, nil + } + b = appendVarint(b, wiretag) + b = appendVarint(b, v) + return b, nil +} +func appendVarint64Ptr(b []byte, ptr pointer, wiretag uint64, _ bool) ([]byte, error) { + p := *ptr.toUint64Ptr() + if p == nil { + return b, nil + } + b = appendVarint(b, wiretag) + b = appendVarint(b, *p) + return b, nil +} +func appendVarint64Slice(b []byte, ptr pointer, wiretag uint64, _ bool) ([]byte, error) { + s := *ptr.toUint64Slice() + for _, v := range s { + b = appendVarint(b, wiretag) + b = appendVarint(b, v) + } + return b, nil +} +func appendVarint64PackedSlice(b []byte, ptr pointer, wiretag uint64, _ bool) ([]byte, error) { + s := *ptr.toUint64Slice() + if len(s) == 0 { + return b, nil + } + b = appendVarint(b, wiretag&^7|WireBytes) + // compute size + n := 0 + for _, v := range s { + n += SizeVarint(v) + } + b = appendVarint(b, uint64(n)) + for _, v := range s { + b = appendVarint(b, v) + } + return b, nil +} +func appendVarintS64Value(b []byte, ptr pointer, wiretag uint64, _ bool) ([]byte, error) { + v := *ptr.toInt64() + b = appendVarint(b, wiretag) + b = appendVarint(b, uint64(v)) + return b, nil +} +func appendVarintS64ValueNoZero(b []byte, ptr pointer, wiretag uint64, _ bool) ([]byte, error) { + v := *ptr.toInt64() + if v == 0 { + return b, nil + } + b = appendVarint(b, wiretag) + b = appendVarint(b, uint64(v)) + return b, nil +} +func appendVarintS64Ptr(b []byte, ptr pointer, wiretag uint64, _ bool) ([]byte, error) { + p := *ptr.toInt64Ptr() + if p == nil { + return b, nil + } + b = appendVarint(b, wiretag) + b = appendVarint(b, uint64(*p)) + return b, nil +} +func appendVarintS64Slice(b []byte, ptr pointer, wiretag uint64, _ bool) ([]byte, error) { + s := *ptr.toInt64Slice() + for _, v := range s { + b = appendVarint(b, wiretag) + b = appendVarint(b, uint64(v)) + } + return b, nil +} +func appendVarintS64PackedSlice(b []byte, ptr pointer, wiretag uint64, _ bool) ([]byte, error) { + s := *ptr.toInt64Slice() + if len(s) == 0 { + return b, nil + } + b = appendVarint(b, wiretag&^7|WireBytes) + // compute size + n := 0 + for _, v := range s { + n += SizeVarint(uint64(v)) + } + b = appendVarint(b, uint64(n)) + for _, v := range s { + b = appendVarint(b, uint64(v)) + } + return b, nil +} +func appendZigzag32Value(b []byte, ptr pointer, wiretag uint64, _ bool) ([]byte, error) { + v := *ptr.toInt32() + b = appendVarint(b, wiretag) + b = appendVarint(b, uint64((uint32(v)<<1)^uint32((int32(v)>>31)))) + return b, nil +} +func appendZigzag32ValueNoZero(b []byte, ptr pointer, wiretag uint64, _ bool) ([]byte, error) { + v := *ptr.toInt32() + if v == 0 { + return b, nil + } + b = appendVarint(b, wiretag) + b = appendVarint(b, uint64((uint32(v)<<1)^uint32((int32(v)>>31)))) + return b, nil +} +func appendZigzag32Ptr(b []byte, ptr pointer, wiretag uint64, _ bool) ([]byte, error) { + p := ptr.getInt32Ptr() + if p == nil { + return b, nil + } + b = appendVarint(b, wiretag) + v := *p + b = appendVarint(b, uint64((uint32(v)<<1)^uint32((int32(v)>>31)))) + return b, nil +} +func appendZigzag32Slice(b []byte, ptr pointer, wiretag uint64, _ bool) ([]byte, error) { + s := ptr.getInt32Slice() + for _, v := range s { + b = appendVarint(b, wiretag) + b = appendVarint(b, uint64((uint32(v)<<1)^uint32((int32(v)>>31)))) + } + return b, nil +} +func appendZigzag32PackedSlice(b []byte, ptr pointer, wiretag uint64, _ bool) ([]byte, error) { + s := ptr.getInt32Slice() + if len(s) == 0 { + return b, nil + } + b = appendVarint(b, wiretag&^7|WireBytes) + // compute size + n := 0 + for _, v := range s { + n += SizeVarint(uint64((uint32(v) << 1) ^ uint32((int32(v) >> 31)))) + } + b = appendVarint(b, uint64(n)) + for _, v := range s { + b = appendVarint(b, uint64((uint32(v)<<1)^uint32((int32(v)>>31)))) + } + return b, nil +} +func appendZigzag64Value(b []byte, ptr pointer, wiretag uint64, _ bool) ([]byte, error) { + v := *ptr.toInt64() + b = appendVarint(b, wiretag) + b = appendVarint(b, uint64(v<<1)^uint64((int64(v)>>63))) + return b, nil +} +func appendZigzag64ValueNoZero(b []byte, ptr pointer, wiretag uint64, _ bool) ([]byte, error) { + v := *ptr.toInt64() + if v == 0 { + return b, nil + } + b = appendVarint(b, wiretag) + b = appendVarint(b, uint64(v<<1)^uint64((int64(v)>>63))) + return b, nil +} +func appendZigzag64Ptr(b []byte, ptr pointer, wiretag uint64, _ bool) ([]byte, error) { + p := *ptr.toInt64Ptr() + if p == nil { + return b, nil + } + b = appendVarint(b, wiretag) + v := *p + b = appendVarint(b, uint64(v<<1)^uint64((int64(v)>>63))) + return b, nil +} +func appendZigzag64Slice(b []byte, ptr pointer, wiretag uint64, _ bool) ([]byte, error) { + s := *ptr.toInt64Slice() + for _, v := range s { + b = appendVarint(b, wiretag) + b = appendVarint(b, uint64(v<<1)^uint64((int64(v)>>63))) + } + return b, nil +} +func appendZigzag64PackedSlice(b []byte, ptr pointer, wiretag uint64, _ bool) ([]byte, error) { + s := *ptr.toInt64Slice() + if len(s) == 0 { + return b, nil + } + b = appendVarint(b, wiretag&^7|WireBytes) + // compute size + n := 0 + for _, v := range s { + n += SizeVarint(uint64(v<<1) ^ uint64((int64(v) >> 63))) + } + b = appendVarint(b, uint64(n)) + for _, v := range s { + b = appendVarint(b, uint64(v<<1)^uint64((int64(v)>>63))) + } + return b, nil +} +func appendBoolValue(b []byte, ptr pointer, wiretag uint64, _ bool) ([]byte, error) { + v := *ptr.toBool() + b = appendVarint(b, wiretag) + if v { + b = append(b, 1) + } else { + b = append(b, 0) + } + return b, nil +} +func appendBoolValueNoZero(b []byte, ptr pointer, wiretag uint64, _ bool) ([]byte, error) { + v := *ptr.toBool() + if !v { + return b, nil + } + b = appendVarint(b, wiretag) + b = append(b, 1) + return b, nil +} + +func appendBoolPtr(b []byte, ptr pointer, wiretag uint64, _ bool) ([]byte, error) { + p := *ptr.toBoolPtr() + if p == nil { + return b, nil + } + b = appendVarint(b, wiretag) + if *p { + b = append(b, 1) + } else { + b = append(b, 0) + } + return b, nil +} +func appendBoolSlice(b []byte, ptr pointer, wiretag uint64, _ bool) ([]byte, error) { + s := *ptr.toBoolSlice() + for _, v := range s { + b = appendVarint(b, wiretag) + if v { + b = append(b, 1) + } else { + b = append(b, 0) + } + } + return b, nil +} +func appendBoolPackedSlice(b []byte, ptr pointer, wiretag uint64, _ bool) ([]byte, error) { + s := *ptr.toBoolSlice() + if len(s) == 0 { + return b, nil + } + b = appendVarint(b, wiretag&^7|WireBytes) + b = appendVarint(b, uint64(len(s))) + for _, v := range s { + if v { + b = append(b, 1) + } else { + b = append(b, 0) + } + } + return b, nil +} +func appendStringValue(b []byte, ptr pointer, wiretag uint64, _ bool) ([]byte, error) { + v := *ptr.toString() + if !utf8.ValidString(v) { + return nil, errInvalidUTF8 + } + b = appendVarint(b, wiretag) + b = appendVarint(b, uint64(len(v))) + b = append(b, v...) + return b, nil +} +func appendStringValueNoZero(b []byte, ptr pointer, wiretag uint64, _ bool) ([]byte, error) { + v := *ptr.toString() + if v == "" { + return b, nil + } + if !utf8.ValidString(v) { + return nil, errInvalidUTF8 + } + b = appendVarint(b, wiretag) + b = appendVarint(b, uint64(len(v))) + b = append(b, v...) + return b, nil +} +func appendStringPtr(b []byte, ptr pointer, wiretag uint64, _ bool) ([]byte, error) { + p := *ptr.toStringPtr() + if p == nil { + return b, nil + } + v := *p + if !utf8.ValidString(v) { + return nil, errInvalidUTF8 + } + b = appendVarint(b, wiretag) + b = appendVarint(b, uint64(len(v))) + b = append(b, v...) + return b, nil +} +func appendStringSlice(b []byte, ptr pointer, wiretag uint64, _ bool) ([]byte, error) { + s := *ptr.toStringSlice() + for _, v := range s { + if !utf8.ValidString(v) { + return nil, errInvalidUTF8 + } + b = appendVarint(b, wiretag) + b = appendVarint(b, uint64(len(v))) + b = append(b, v...) + } + return b, nil +} +func appendBytes(b []byte, ptr pointer, wiretag uint64, _ bool) ([]byte, error) { + v := *ptr.toBytes() + if v == nil { + return b, nil + } + b = appendVarint(b, wiretag) + b = appendVarint(b, uint64(len(v))) + b = append(b, v...) + return b, nil +} +func appendBytes3(b []byte, ptr pointer, wiretag uint64, _ bool) ([]byte, error) { + v := *ptr.toBytes() + if len(v) == 0 { + return b, nil + } + b = appendVarint(b, wiretag) + b = appendVarint(b, uint64(len(v))) + b = append(b, v...) + return b, nil +} +func appendBytesOneof(b []byte, ptr pointer, wiretag uint64, _ bool) ([]byte, error) { + v := *ptr.toBytes() + b = appendVarint(b, wiretag) + b = appendVarint(b, uint64(len(v))) + b = append(b, v...) + return b, nil +} +func appendBytesSlice(b []byte, ptr pointer, wiretag uint64, _ bool) ([]byte, error) { + s := *ptr.toBytesSlice() + for _, v := range s { + b = appendVarint(b, wiretag) + b = appendVarint(b, uint64(len(v))) + b = append(b, v...) + } + return b, nil +} + +// makeGroupMarshaler returns the sizer and marshaler for a group. +// u is the marshal info of the underlying message. +func makeGroupMarshaler(u *marshalInfo) (sizer, marshaler) { + return func(ptr pointer, tagsize int) int { + p := ptr.getPointer() + if p.isNil() { + return 0 + } + return u.size(p) + 2*tagsize + }, + func(b []byte, ptr pointer, wiretag uint64, deterministic bool) ([]byte, error) { + p := ptr.getPointer() + if p.isNil() { + return b, nil + } + var err error + b = appendVarint(b, wiretag) // start group + b, err = u.marshal(b, p, deterministic) + b = appendVarint(b, wiretag+(WireEndGroup-WireStartGroup)) // end group + return b, err + } +} + +// makeGroupSliceMarshaler returns the sizer and marshaler for a group slice. +// u is the marshal info of the underlying message. +func makeGroupSliceMarshaler(u *marshalInfo) (sizer, marshaler) { + return func(ptr pointer, tagsize int) int { + s := ptr.getPointerSlice() + n := 0 + for _, v := range s { + if v.isNil() { + continue + } + n += u.size(v) + 2*tagsize + } + return n + }, + func(b []byte, ptr pointer, wiretag uint64, deterministic bool) ([]byte, error) { + s := ptr.getPointerSlice() + var err, errreq error + for _, v := range s { + if v.isNil() { + return b, errRepeatedHasNil + } + b = appendVarint(b, wiretag) // start group + b, err = u.marshal(b, v, deterministic) + b = appendVarint(b, wiretag+(WireEndGroup-WireStartGroup)) // end group + if err != nil { + if _, ok := err.(*RequiredNotSetError); ok { + // Required field in submessage is not set. + // We record the error but keep going, to give a complete marshaling. + if errreq == nil { + errreq = err + } + continue + } + if err == ErrNil { + err = errRepeatedHasNil + } + return b, err + } + } + return b, errreq + } +} + +// makeMessageMarshaler returns the sizer and marshaler for a message field. +// u is the marshal info of the message. +func makeMessageMarshaler(u *marshalInfo) (sizer, marshaler) { + return func(ptr pointer, tagsize int) int { + p := ptr.getPointer() + if p.isNil() { + return 0 + } + siz := u.size(p) + return siz + SizeVarint(uint64(siz)) + tagsize + }, + func(b []byte, ptr pointer, wiretag uint64, deterministic bool) ([]byte, error) { + p := ptr.getPointer() + if p.isNil() { + return b, nil + } + b = appendVarint(b, wiretag) + siz := u.cachedsize(p) + b = appendVarint(b, uint64(siz)) + return u.marshal(b, p, deterministic) + } +} + +// makeMessageSliceMarshaler returns the sizer and marshaler for a message slice. +// u is the marshal info of the message. +func makeMessageSliceMarshaler(u *marshalInfo) (sizer, marshaler) { + return func(ptr pointer, tagsize int) int { + s := ptr.getPointerSlice() + n := 0 + for _, v := range s { + if v.isNil() { + continue + } + siz := u.size(v) + n += siz + SizeVarint(uint64(siz)) + tagsize + } + return n + }, + func(b []byte, ptr pointer, wiretag uint64, deterministic bool) ([]byte, error) { + s := ptr.getPointerSlice() + var err, errreq error + for _, v := range s { + if v.isNil() { + return b, errRepeatedHasNil + } + b = appendVarint(b, wiretag) + siz := u.cachedsize(v) + b = appendVarint(b, uint64(siz)) + b, err = u.marshal(b, v, deterministic) + + if err != nil { + if _, ok := err.(*RequiredNotSetError); ok { + // Required field in submessage is not set. + // We record the error but keep going, to give a complete marshaling. + if errreq == nil { + errreq = err + } + continue + } + if err == ErrNil { + err = errRepeatedHasNil + } + return b, err + } + } + return b, errreq + } +} + +// makeMapMarshaler returns the sizer and marshaler for a map field. +// f is the pointer to the reflect data structure of the field. +func makeMapMarshaler(f *reflect.StructField) (sizer, marshaler) { + // figure out key and value type + t := f.Type + keyType := t.Key() + valType := t.Elem() + keyTags := strings.Split(f.Tag.Get("protobuf_key"), ",") + valTags := strings.Split(f.Tag.Get("protobuf_val"), ",") + keySizer, keyMarshaler := typeMarshaler(keyType, keyTags, false, false) // don't omit zero value in map + valSizer, valMarshaler := typeMarshaler(valType, valTags, false, false) // don't omit zero value in map + keyWireTag := 1<<3 | wiretype(keyTags[0]) + valWireTag := 2<<3 | wiretype(valTags[0]) + + // We create an interface to get the addresses of the map key and value. + // If value is pointer-typed, the interface is a direct interface, the + // idata itself is the value. Otherwise, the idata is the pointer to the + // value. + // Key cannot be pointer-typed. + valIsPtr := valType.Kind() == reflect.Ptr + return func(ptr pointer, tagsize int) int { + m := ptr.asPointerTo(t).Elem() // the map + n := 0 + for _, k := range m.MapKeys() { + ki := k.Interface() + vi := m.MapIndex(k).Interface() + kaddr := toAddrPointer(&ki, false) // pointer to key + vaddr := toAddrPointer(&vi, valIsPtr) // pointer to value + siz := keySizer(kaddr, 1) + valSizer(vaddr, 1) // tag of key = 1 (size=1), tag of val = 2 (size=1) + n += siz + SizeVarint(uint64(siz)) + tagsize + } + return n + }, + func(b []byte, ptr pointer, tag uint64, deterministic bool) ([]byte, error) { + m := ptr.asPointerTo(t).Elem() // the map + var err error + keys := m.MapKeys() + if len(keys) > 1 && deterministic { + sort.Sort(mapKeys(keys)) + } + for _, k := range keys { + ki := k.Interface() + vi := m.MapIndex(k).Interface() + kaddr := toAddrPointer(&ki, false) // pointer to key + vaddr := toAddrPointer(&vi, valIsPtr) // pointer to value + b = appendVarint(b, tag) + siz := keySizer(kaddr, 1) + valSizer(vaddr, 1) // tag of key = 1 (size=1), tag of val = 2 (size=1) + b = appendVarint(b, uint64(siz)) + b, err = keyMarshaler(b, kaddr, keyWireTag, deterministic) + if err != nil { + return b, err + } + b, err = valMarshaler(b, vaddr, valWireTag, deterministic) + if err != nil && err != ErrNil { // allow nil value in map + return b, err + } + } + return b, nil + } +} + +// makeOneOfMarshaler returns the sizer and marshaler for a oneof field. +// fi is the marshal info of the field. +// f is the pointer to the reflect data structure of the field. +func makeOneOfMarshaler(fi *marshalFieldInfo, f *reflect.StructField) (sizer, marshaler) { + // Oneof field is an interface. We need to get the actual data type on the fly. + t := f.Type + return func(ptr pointer, _ int) int { + p := ptr.getInterfacePointer() + if p.isNil() { + return 0 + } + v := ptr.asPointerTo(t).Elem().Elem().Elem() // *interface -> interface -> *struct -> struct + telem := v.Type() + e := fi.oneofElems[telem] + return e.sizer(p, e.tagsize) + }, + func(b []byte, ptr pointer, _ uint64, deterministic bool) ([]byte, error) { + p := ptr.getInterfacePointer() + if p.isNil() { + return b, nil + } + v := ptr.asPointerTo(t).Elem().Elem().Elem() // *interface -> interface -> *struct -> struct + telem := v.Type() + if telem.Field(0).Type.Kind() == reflect.Ptr && p.getPointer().isNil() { + return b, errOneofHasNil + } + e := fi.oneofElems[telem] + return e.marshaler(b, p, e.wiretag, deterministic) + } +} + +// sizeExtensions computes the size of encoded data for a XXX_InternalExtensions field. +func (u *marshalInfo) sizeExtensions(ext *XXX_InternalExtensions) int { + m, mu := ext.extensionsRead() + if m == nil { + return 0 + } + mu.Lock() + + n := 0 + for _, e := range m { + if e.value == nil || e.desc == nil { + // Extension is only in its encoded form. + n += len(e.enc) + continue + } + + // We don't skip extensions that have an encoded form set, + // because the extension value may have been mutated after + // the last time this function was called. + ei := u.getExtElemInfo(e.desc) + v := e.value + p := toAddrPointer(&v, ei.isptr) + n += ei.sizer(p, ei.tagsize) + } + mu.Unlock() + return n +} + +// appendExtensions marshals a XXX_InternalExtensions field to the end of byte slice b. +func (u *marshalInfo) appendExtensions(b []byte, ext *XXX_InternalExtensions, deterministic bool) ([]byte, error) { + m, mu := ext.extensionsRead() + if m == nil { + return b, nil + } + mu.Lock() + defer mu.Unlock() + + var err error + + // Fast-path for common cases: zero or one extensions. + // Don't bother sorting the keys. + if len(m) <= 1 { + for _, e := range m { + if e.value == nil || e.desc == nil { + // Extension is only in its encoded form. + b = append(b, e.enc...) + continue + } + + // We don't skip extensions that have an encoded form set, + // because the extension value may have been mutated after + // the last time this function was called. + + ei := u.getExtElemInfo(e.desc) + v := e.value + p := toAddrPointer(&v, ei.isptr) + b, err = ei.marshaler(b, p, ei.wiretag, deterministic) + if err != nil { + return b, err + } + } + return b, nil + } + + // Sort the keys to provide a deterministic encoding. + // Not sure this is required, but the old code does it. + keys := make([]int, 0, len(m)) + for k := range m { + keys = append(keys, int(k)) + } + sort.Ints(keys) + + for _, k := range keys { + e := m[int32(k)] + if e.value == nil || e.desc == nil { + // Extension is only in its encoded form. + b = append(b, e.enc...) + continue + } + + // We don't skip extensions that have an encoded form set, + // because the extension value may have been mutated after + // the last time this function was called. + + ei := u.getExtElemInfo(e.desc) + v := e.value + p := toAddrPointer(&v, ei.isptr) + b, err = ei.marshaler(b, p, ei.wiretag, deterministic) + if err != nil { + return b, err + } + } + return b, nil +} + +// message set format is: +// message MessageSet { +// repeated group Item = 1 { +// required int32 type_id = 2; +// required string message = 3; +// }; +// } + +// sizeMessageSet computes the size of encoded data for a XXX_InternalExtensions field +// in message set format (above). +func (u *marshalInfo) sizeMessageSet(ext *XXX_InternalExtensions) int { + m, mu := ext.extensionsRead() + if m == nil { + return 0 + } + mu.Lock() + + n := 0 + for id, e := range m { + n += 2 // start group, end group. tag = 1 (size=1) + n += SizeVarint(uint64(id)) + 1 // type_id, tag = 2 (size=1) + + if e.value == nil || e.desc == nil { + // Extension is only in its encoded form. + msgWithLen := skipVarint(e.enc) // skip old tag, but leave the length varint + siz := len(msgWithLen) + n += siz + 1 // message, tag = 3 (size=1) + continue + } + + // We don't skip extensions that have an encoded form set, + // because the extension value may have been mutated after + // the last time this function was called. + + ei := u.getExtElemInfo(e.desc) + v := e.value + p := toAddrPointer(&v, ei.isptr) + n += ei.sizer(p, 1) // message, tag = 3 (size=1) + } + mu.Unlock() + return n +} + +// appendMessageSet marshals a XXX_InternalExtensions field in message set format (above) +// to the end of byte slice b. +func (u *marshalInfo) appendMessageSet(b []byte, ext *XXX_InternalExtensions, deterministic bool) ([]byte, error) { + m, mu := ext.extensionsRead() + if m == nil { + return b, nil + } + mu.Lock() + defer mu.Unlock() + + var err error + + // Fast-path for common cases: zero or one extensions. + // Don't bother sorting the keys. + if len(m) <= 1 { + for id, e := range m { + b = append(b, 1<<3|WireStartGroup) + b = append(b, 2<<3|WireVarint) + b = appendVarint(b, uint64(id)) + + if e.value == nil || e.desc == nil { + // Extension is only in its encoded form. + msgWithLen := skipVarint(e.enc) // skip old tag, but leave the length varint + b = append(b, 3<<3|WireBytes) + b = append(b, msgWithLen...) + b = append(b, 1<<3|WireEndGroup) + continue + } + + // We don't skip extensions that have an encoded form set, + // because the extension value may have been mutated after + // the last time this function was called. + + ei := u.getExtElemInfo(e.desc) + v := e.value + p := toAddrPointer(&v, ei.isptr) + b, err = ei.marshaler(b, p, 3<<3|WireBytes, deterministic) + if err != nil { + return b, err + } + b = append(b, 1<<3|WireEndGroup) + } + return b, nil + } + + // Sort the keys to provide a deterministic encoding. + keys := make([]int, 0, len(m)) + for k := range m { + keys = append(keys, int(k)) + } + sort.Ints(keys) + + for _, id := range keys { + e := m[int32(id)] + b = append(b, 1<<3|WireStartGroup) + b = append(b, 2<<3|WireVarint) + b = appendVarint(b, uint64(id)) + + if e.value == nil || e.desc == nil { + // Extension is only in its encoded form. + msgWithLen := skipVarint(e.enc) // skip old tag, but leave the length varint + b = append(b, 3<<3|WireBytes) + b = append(b, msgWithLen...) + b = append(b, 1<<3|WireEndGroup) + continue + } + + // We don't skip extensions that have an encoded form set, + // because the extension value may have been mutated after + // the last time this function was called. + + ei := u.getExtElemInfo(e.desc) + v := e.value + p := toAddrPointer(&v, ei.isptr) + b, err = ei.marshaler(b, p, 3<<3|WireBytes, deterministic) + b = append(b, 1<<3|WireEndGroup) + if err != nil { + return b, err + } + } + return b, nil +} + +// sizeV1Extensions computes the size of encoded data for a V1-API extension field. +func (u *marshalInfo) sizeV1Extensions(m map[int32]Extension) int { + if m == nil { + return 0 + } + + n := 0 + for _, e := range m { + if e.value == nil || e.desc == nil { + // Extension is only in its encoded form. + n += len(e.enc) + continue + } + + // We don't skip extensions that have an encoded form set, + // because the extension value may have been mutated after + // the last time this function was called. + + ei := u.getExtElemInfo(e.desc) + v := e.value + p := toAddrPointer(&v, ei.isptr) + n += ei.sizer(p, ei.tagsize) + } + return n +} + +// appendV1Extensions marshals a V1-API extension field to the end of byte slice b. +func (u *marshalInfo) appendV1Extensions(b []byte, m map[int32]Extension, deterministic bool) ([]byte, error) { + if m == nil { + return b, nil + } + + // Sort the keys to provide a deterministic encoding. + keys := make([]int, 0, len(m)) + for k := range m { + keys = append(keys, int(k)) + } + sort.Ints(keys) + + var err error + for _, k := range keys { + e := m[int32(k)] + if e.value == nil || e.desc == nil { + // Extension is only in its encoded form. + b = append(b, e.enc...) + continue + } + + // We don't skip extensions that have an encoded form set, + // because the extension value may have been mutated after + // the last time this function was called. + + ei := u.getExtElemInfo(e.desc) + v := e.value + p := toAddrPointer(&v, ei.isptr) + b, err = ei.marshaler(b, p, ei.wiretag, deterministic) + if err != nil { + return b, err + } + } + return b, nil +} + +// newMarshaler is the interface representing objects that can marshal themselves. +// +// This exists to support protoc-gen-go generated messages. +// The proto package will stop type-asserting to this interface in the future. +// +// DO NOT DEPEND ON THIS. +type newMarshaler interface { + XXX_Size() int + XXX_Marshal(b []byte, deterministic bool) ([]byte, error) +} + +// Size returns the encoded size of a protocol buffer message. +// This is the main entry point. +func Size(pb Message) int { + if m, ok := pb.(newMarshaler); ok { + return m.XXX_Size() + } + if m, ok := pb.(Marshaler); ok { + // If the message can marshal itself, let it do it, for compatibility. + // NOTE: This is not efficient. + b, _ := m.Marshal() + return len(b) + } + // in case somehow we didn't generate the wrapper + if pb == nil { + return 0 + } + var info InternalMessageInfo + return info.Size(pb) +} + +// Marshal takes a protocol buffer message +// and encodes it into the wire format, returning the data. +// This is the main entry point. +func Marshal(pb Message) ([]byte, error) { + if m, ok := pb.(newMarshaler); ok { + siz := m.XXX_Size() + b := make([]byte, 0, siz) + return m.XXX_Marshal(b, false) + } + if m, ok := pb.(Marshaler); ok { + // If the message can marshal itself, let it do it, for compatibility. + // NOTE: This is not efficient. + return m.Marshal() + } + // in case somehow we didn't generate the wrapper + if pb == nil { + return nil, ErrNil + } + var info InternalMessageInfo + siz := info.Size(pb) + b := make([]byte, 0, siz) + return info.Marshal(b, pb, false) +} + +// Marshal takes a protocol buffer message +// and encodes it into the wire format, writing the result to the +// Buffer. +// This is an alternative entry point. It is not necessary to use +// a Buffer for most applications. +func (p *Buffer) Marshal(pb Message) error { + var err error + if m, ok := pb.(newMarshaler); ok { + siz := m.XXX_Size() + p.grow(siz) // make sure buf has enough capacity + p.buf, err = m.XXX_Marshal(p.buf, p.deterministic) + return err + } + if m, ok := pb.(Marshaler); ok { + // If the message can marshal itself, let it do it, for compatibility. + // NOTE: This is not efficient. + b, err := m.Marshal() + p.buf = append(p.buf, b...) + return err + } + // in case somehow we didn't generate the wrapper + if pb == nil { + return ErrNil + } + var info InternalMessageInfo + siz := info.Size(pb) + p.grow(siz) // make sure buf has enough capacity + p.buf, err = info.Marshal(p.buf, pb, p.deterministic) + return err +} + +// grow grows the buffer's capacity, if necessary, to guarantee space for +// another n bytes. After grow(n), at least n bytes can be written to the +// buffer without another allocation. +func (p *Buffer) grow(n int) { + need := len(p.buf) + n + if need <= cap(p.buf) { + return + } + newCap := len(p.buf) * 2 + if newCap < need { + newCap = need + } + p.buf = append(make([]byte, 0, newCap), p.buf...) +} diff --git a/vendor/github.com/golang/protobuf/proto/table_merge.go b/vendor/github.com/golang/protobuf/proto/table_merge.go new file mode 100644 index 00000000..5525def6 --- /dev/null +++ b/vendor/github.com/golang/protobuf/proto/table_merge.go @@ -0,0 +1,654 @@ +// Go support for Protocol Buffers - Google's data interchange format +// +// Copyright 2016 The Go Authors. All rights reserved. +// https://github.com/golang/protobuf +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package proto + +import ( + "fmt" + "reflect" + "strings" + "sync" + "sync/atomic" +) + +// Merge merges the src message into dst. +// This assumes that dst and src of the same type and are non-nil. +func (a *InternalMessageInfo) Merge(dst, src Message) { + mi := atomicLoadMergeInfo(&a.merge) + if mi == nil { + mi = getMergeInfo(reflect.TypeOf(dst).Elem()) + atomicStoreMergeInfo(&a.merge, mi) + } + mi.merge(toPointer(&dst), toPointer(&src)) +} + +type mergeInfo struct { + typ reflect.Type + + initialized int32 // 0: only typ is valid, 1: everything is valid + lock sync.Mutex + + fields []mergeFieldInfo + unrecognized field // Offset of XXX_unrecognized +} + +type mergeFieldInfo struct { + field field // Offset of field, guaranteed to be valid + + // isPointer reports whether the value in the field is a pointer. + // This is true for the following situations: + // * Pointer to struct + // * Pointer to basic type (proto2 only) + // * Slice (first value in slice header is a pointer) + // * String (first value in string header is a pointer) + isPointer bool + + // basicWidth reports the width of the field assuming that it is directly + // embedded in the struct (as is the case for basic types in proto3). + // The possible values are: + // 0: invalid + // 1: bool + // 4: int32, uint32, float32 + // 8: int64, uint64, float64 + basicWidth int + + // Where dst and src are pointers to the types being merged. + merge func(dst, src pointer) +} + +var ( + mergeInfoMap = map[reflect.Type]*mergeInfo{} + mergeInfoLock sync.Mutex +) + +func getMergeInfo(t reflect.Type) *mergeInfo { + mergeInfoLock.Lock() + defer mergeInfoLock.Unlock() + mi := mergeInfoMap[t] + if mi == nil { + mi = &mergeInfo{typ: t} + mergeInfoMap[t] = mi + } + return mi +} + +// merge merges src into dst assuming they are both of type *mi.typ. +func (mi *mergeInfo) merge(dst, src pointer) { + if dst.isNil() { + panic("proto: nil destination") + } + if src.isNil() { + return // Nothing to do. + } + + if atomic.LoadInt32(&mi.initialized) == 0 { + mi.computeMergeInfo() + } + + for _, fi := range mi.fields { + sfp := src.offset(fi.field) + + // As an optimization, we can avoid the merge function call cost + // if we know for sure that the source will have no effect + // by checking if it is the zero value. + if unsafeAllowed { + if fi.isPointer && sfp.getPointer().isNil() { // Could be slice or string + continue + } + if fi.basicWidth > 0 { + switch { + case fi.basicWidth == 1 && !*sfp.toBool(): + continue + case fi.basicWidth == 4 && *sfp.toUint32() == 0: + continue + case fi.basicWidth == 8 && *sfp.toUint64() == 0: + continue + } + } + } + + dfp := dst.offset(fi.field) + fi.merge(dfp, sfp) + } + + // TODO: Make this faster? + out := dst.asPointerTo(mi.typ).Elem() + in := src.asPointerTo(mi.typ).Elem() + if emIn, err := extendable(in.Addr().Interface()); err == nil { + emOut, _ := extendable(out.Addr().Interface()) + mIn, muIn := emIn.extensionsRead() + if mIn != nil { + mOut := emOut.extensionsWrite() + muIn.Lock() + mergeExtension(mOut, mIn) + muIn.Unlock() + } + } + + if mi.unrecognized.IsValid() { + if b := *src.offset(mi.unrecognized).toBytes(); len(b) > 0 { + *dst.offset(mi.unrecognized).toBytes() = append([]byte(nil), b...) + } + } +} + +func (mi *mergeInfo) computeMergeInfo() { + mi.lock.Lock() + defer mi.lock.Unlock() + if mi.initialized != 0 { + return + } + t := mi.typ + n := t.NumField() + + props := GetProperties(t) + for i := 0; i < n; i++ { + f := t.Field(i) + if strings.HasPrefix(f.Name, "XXX_") { + continue + } + + mfi := mergeFieldInfo{field: toField(&f)} + tf := f.Type + + // As an optimization, we can avoid the merge function call cost + // if we know for sure that the source will have no effect + // by checking if it is the zero value. + if unsafeAllowed { + switch tf.Kind() { + case reflect.Ptr, reflect.Slice, reflect.String: + // As a special case, we assume slices and strings are pointers + // since we know that the first field in the SliceSlice or + // StringHeader is a data pointer. + mfi.isPointer = true + case reflect.Bool: + mfi.basicWidth = 1 + case reflect.Int32, reflect.Uint32, reflect.Float32: + mfi.basicWidth = 4 + case reflect.Int64, reflect.Uint64, reflect.Float64: + mfi.basicWidth = 8 + } + } + + // Unwrap tf to get at its most basic type. + var isPointer, isSlice bool + if tf.Kind() == reflect.Slice && tf.Elem().Kind() != reflect.Uint8 { + isSlice = true + tf = tf.Elem() + } + if tf.Kind() == reflect.Ptr { + isPointer = true + tf = tf.Elem() + } + if isPointer && isSlice && tf.Kind() != reflect.Struct { + panic("both pointer and slice for basic type in " + tf.Name()) + } + + switch tf.Kind() { + case reflect.Int32: + switch { + case isSlice: // E.g., []int32 + mfi.merge = func(dst, src pointer) { + // NOTE: toInt32Slice is not defined (see pointer_reflect.go). + /* + sfsp := src.toInt32Slice() + if *sfsp != nil { + dfsp := dst.toInt32Slice() + *dfsp = append(*dfsp, *sfsp...) + if *dfsp == nil { + *dfsp = []int64{} + } + } + */ + sfs := src.getInt32Slice() + if sfs != nil { + dfs := dst.getInt32Slice() + dfs = append(dfs, sfs...) + if dfs == nil { + dfs = []int32{} + } + dst.setInt32Slice(dfs) + } + } + case isPointer: // E.g., *int32 + mfi.merge = func(dst, src pointer) { + // NOTE: toInt32Ptr is not defined (see pointer_reflect.go). + /* + sfpp := src.toInt32Ptr() + if *sfpp != nil { + dfpp := dst.toInt32Ptr() + if *dfpp == nil { + *dfpp = Int32(**sfpp) + } else { + **dfpp = **sfpp + } + } + */ + sfp := src.getInt32Ptr() + if sfp != nil { + dfp := dst.getInt32Ptr() + if dfp == nil { + dst.setInt32Ptr(*sfp) + } else { + *dfp = *sfp + } + } + } + default: // E.g., int32 + mfi.merge = func(dst, src pointer) { + if v := *src.toInt32(); v != 0 { + *dst.toInt32() = v + } + } + } + case reflect.Int64: + switch { + case isSlice: // E.g., []int64 + mfi.merge = func(dst, src pointer) { + sfsp := src.toInt64Slice() + if *sfsp != nil { + dfsp := dst.toInt64Slice() + *dfsp = append(*dfsp, *sfsp...) + if *dfsp == nil { + *dfsp = []int64{} + } + } + } + case isPointer: // E.g., *int64 + mfi.merge = func(dst, src pointer) { + sfpp := src.toInt64Ptr() + if *sfpp != nil { + dfpp := dst.toInt64Ptr() + if *dfpp == nil { + *dfpp = Int64(**sfpp) + } else { + **dfpp = **sfpp + } + } + } + default: // E.g., int64 + mfi.merge = func(dst, src pointer) { + if v := *src.toInt64(); v != 0 { + *dst.toInt64() = v + } + } + } + case reflect.Uint32: + switch { + case isSlice: // E.g., []uint32 + mfi.merge = func(dst, src pointer) { + sfsp := src.toUint32Slice() + if *sfsp != nil { + dfsp := dst.toUint32Slice() + *dfsp = append(*dfsp, *sfsp...) + if *dfsp == nil { + *dfsp = []uint32{} + } + } + } + case isPointer: // E.g., *uint32 + mfi.merge = func(dst, src pointer) { + sfpp := src.toUint32Ptr() + if *sfpp != nil { + dfpp := dst.toUint32Ptr() + if *dfpp == nil { + *dfpp = Uint32(**sfpp) + } else { + **dfpp = **sfpp + } + } + } + default: // E.g., uint32 + mfi.merge = func(dst, src pointer) { + if v := *src.toUint32(); v != 0 { + *dst.toUint32() = v + } + } + } + case reflect.Uint64: + switch { + case isSlice: // E.g., []uint64 + mfi.merge = func(dst, src pointer) { + sfsp := src.toUint64Slice() + if *sfsp != nil { + dfsp := dst.toUint64Slice() + *dfsp = append(*dfsp, *sfsp...) + if *dfsp == nil { + *dfsp = []uint64{} + } + } + } + case isPointer: // E.g., *uint64 + mfi.merge = func(dst, src pointer) { + sfpp := src.toUint64Ptr() + if *sfpp != nil { + dfpp := dst.toUint64Ptr() + if *dfpp == nil { + *dfpp = Uint64(**sfpp) + } else { + **dfpp = **sfpp + } + } + } + default: // E.g., uint64 + mfi.merge = func(dst, src pointer) { + if v := *src.toUint64(); v != 0 { + *dst.toUint64() = v + } + } + } + case reflect.Float32: + switch { + case isSlice: // E.g., []float32 + mfi.merge = func(dst, src pointer) { + sfsp := src.toFloat32Slice() + if *sfsp != nil { + dfsp := dst.toFloat32Slice() + *dfsp = append(*dfsp, *sfsp...) + if *dfsp == nil { + *dfsp = []float32{} + } + } + } + case isPointer: // E.g., *float32 + mfi.merge = func(dst, src pointer) { + sfpp := src.toFloat32Ptr() + if *sfpp != nil { + dfpp := dst.toFloat32Ptr() + if *dfpp == nil { + *dfpp = Float32(**sfpp) + } else { + **dfpp = **sfpp + } + } + } + default: // E.g., float32 + mfi.merge = func(dst, src pointer) { + if v := *src.toFloat32(); v != 0 { + *dst.toFloat32() = v + } + } + } + case reflect.Float64: + switch { + case isSlice: // E.g., []float64 + mfi.merge = func(dst, src pointer) { + sfsp := src.toFloat64Slice() + if *sfsp != nil { + dfsp := dst.toFloat64Slice() + *dfsp = append(*dfsp, *sfsp...) + if *dfsp == nil { + *dfsp = []float64{} + } + } + } + case isPointer: // E.g., *float64 + mfi.merge = func(dst, src pointer) { + sfpp := src.toFloat64Ptr() + if *sfpp != nil { + dfpp := dst.toFloat64Ptr() + if *dfpp == nil { + *dfpp = Float64(**sfpp) + } else { + **dfpp = **sfpp + } + } + } + default: // E.g., float64 + mfi.merge = func(dst, src pointer) { + if v := *src.toFloat64(); v != 0 { + *dst.toFloat64() = v + } + } + } + case reflect.Bool: + switch { + case isSlice: // E.g., []bool + mfi.merge = func(dst, src pointer) { + sfsp := src.toBoolSlice() + if *sfsp != nil { + dfsp := dst.toBoolSlice() + *dfsp = append(*dfsp, *sfsp...) + if *dfsp == nil { + *dfsp = []bool{} + } + } + } + case isPointer: // E.g., *bool + mfi.merge = func(dst, src pointer) { + sfpp := src.toBoolPtr() + if *sfpp != nil { + dfpp := dst.toBoolPtr() + if *dfpp == nil { + *dfpp = Bool(**sfpp) + } else { + **dfpp = **sfpp + } + } + } + default: // E.g., bool + mfi.merge = func(dst, src pointer) { + if v := *src.toBool(); v { + *dst.toBool() = v + } + } + } + case reflect.String: + switch { + case isSlice: // E.g., []string + mfi.merge = func(dst, src pointer) { + sfsp := src.toStringSlice() + if *sfsp != nil { + dfsp := dst.toStringSlice() + *dfsp = append(*dfsp, *sfsp...) + if *dfsp == nil { + *dfsp = []string{} + } + } + } + case isPointer: // E.g., *string + mfi.merge = func(dst, src pointer) { + sfpp := src.toStringPtr() + if *sfpp != nil { + dfpp := dst.toStringPtr() + if *dfpp == nil { + *dfpp = String(**sfpp) + } else { + **dfpp = **sfpp + } + } + } + default: // E.g., string + mfi.merge = func(dst, src pointer) { + if v := *src.toString(); v != "" { + *dst.toString() = v + } + } + } + case reflect.Slice: + isProto3 := props.Prop[i].proto3 + switch { + case isPointer: + panic("bad pointer in byte slice case in " + tf.Name()) + case tf.Elem().Kind() != reflect.Uint8: + panic("bad element kind in byte slice case in " + tf.Name()) + case isSlice: // E.g., [][]byte + mfi.merge = func(dst, src pointer) { + sbsp := src.toBytesSlice() + if *sbsp != nil { + dbsp := dst.toBytesSlice() + for _, sb := range *sbsp { + if sb == nil { + *dbsp = append(*dbsp, nil) + } else { + *dbsp = append(*dbsp, append([]byte{}, sb...)) + } + } + if *dbsp == nil { + *dbsp = [][]byte{} + } + } + } + default: // E.g., []byte + mfi.merge = func(dst, src pointer) { + sbp := src.toBytes() + if *sbp != nil { + dbp := dst.toBytes() + if !isProto3 || len(*sbp) > 0 { + *dbp = append([]byte{}, *sbp...) + } + } + } + } + case reflect.Struct: + switch { + case !isPointer: + panic(fmt.Sprintf("message field %s without pointer", tf)) + case isSlice: // E.g., []*pb.T + mi := getMergeInfo(tf) + mfi.merge = func(dst, src pointer) { + sps := src.getPointerSlice() + if sps != nil { + dps := dst.getPointerSlice() + for _, sp := range sps { + var dp pointer + if !sp.isNil() { + dp = valToPointer(reflect.New(tf)) + mi.merge(dp, sp) + } + dps = append(dps, dp) + } + if dps == nil { + dps = []pointer{} + } + dst.setPointerSlice(dps) + } + } + default: // E.g., *pb.T + mi := getMergeInfo(tf) + mfi.merge = func(dst, src pointer) { + sp := src.getPointer() + if !sp.isNil() { + dp := dst.getPointer() + if dp.isNil() { + dp = valToPointer(reflect.New(tf)) + dst.setPointer(dp) + } + mi.merge(dp, sp) + } + } + } + case reflect.Map: + switch { + case isPointer || isSlice: + panic("bad pointer or slice in map case in " + tf.Name()) + default: // E.g., map[K]V + mfi.merge = func(dst, src pointer) { + sm := src.asPointerTo(tf).Elem() + if sm.Len() == 0 { + return + } + dm := dst.asPointerTo(tf).Elem() + if dm.IsNil() { + dm.Set(reflect.MakeMap(tf)) + } + + switch tf.Elem().Kind() { + case reflect.Ptr: // Proto struct (e.g., *T) + for _, key := range sm.MapKeys() { + val := sm.MapIndex(key) + val = reflect.ValueOf(Clone(val.Interface().(Message))) + dm.SetMapIndex(key, val) + } + case reflect.Slice: // E.g. Bytes type (e.g., []byte) + for _, key := range sm.MapKeys() { + val := sm.MapIndex(key) + val = reflect.ValueOf(append([]byte{}, val.Bytes()...)) + dm.SetMapIndex(key, val) + } + default: // Basic type (e.g., string) + for _, key := range sm.MapKeys() { + val := sm.MapIndex(key) + dm.SetMapIndex(key, val) + } + } + } + } + case reflect.Interface: + // Must be oneof field. + switch { + case isPointer || isSlice: + panic("bad pointer or slice in interface case in " + tf.Name()) + default: // E.g., interface{} + // TODO: Make this faster? + mfi.merge = func(dst, src pointer) { + su := src.asPointerTo(tf).Elem() + if !su.IsNil() { + du := dst.asPointerTo(tf).Elem() + typ := su.Elem().Type() + if du.IsNil() || du.Elem().Type() != typ { + du.Set(reflect.New(typ.Elem())) // Initialize interface if empty + } + sv := su.Elem().Elem().Field(0) + if sv.Kind() == reflect.Ptr && sv.IsNil() { + return + } + dv := du.Elem().Elem().Field(0) + if dv.Kind() == reflect.Ptr && dv.IsNil() { + dv.Set(reflect.New(sv.Type().Elem())) // Initialize proto message if empty + } + switch sv.Type().Kind() { + case reflect.Ptr: // Proto struct (e.g., *T) + Merge(dv.Interface().(Message), sv.Interface().(Message)) + case reflect.Slice: // E.g. Bytes type (e.g., []byte) + dv.Set(reflect.ValueOf(append([]byte{}, sv.Bytes()...))) + default: // Basic type (e.g., string) + dv.Set(sv) + } + } + } + } + default: + panic(fmt.Sprintf("merger not found for type:%s", tf)) + } + mi.fields = append(mi.fields, mfi) + } + + mi.unrecognized = invalidField + if f, ok := t.FieldByName("XXX_unrecognized"); ok { + if f.Type != reflect.TypeOf([]byte{}) { + panic("expected XXX_unrecognized to be of type []byte") + } + mi.unrecognized = toField(&f) + } + + atomic.StoreInt32(&mi.initialized, 1) +} diff --git a/vendor/github.com/golang/protobuf/proto/table_unmarshal.go b/vendor/github.com/golang/protobuf/proto/table_unmarshal.go new file mode 100644 index 00000000..96764347 --- /dev/null +++ b/vendor/github.com/golang/protobuf/proto/table_unmarshal.go @@ -0,0 +1,1981 @@ +// Go support for Protocol Buffers - Google's data interchange format +// +// Copyright 2016 The Go Authors. All rights reserved. +// https://github.com/golang/protobuf +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package proto + +import ( + "errors" + "fmt" + "io" + "math" + "reflect" + "strconv" + "strings" + "sync" + "sync/atomic" + "unicode/utf8" +) + +// Unmarshal is the entry point from the generated .pb.go files. +// This function is not intended to be used by non-generated code. +// This function is not subject to any compatibility guarantee. +// msg contains a pointer to a protocol buffer struct. +// b is the data to be unmarshaled into the protocol buffer. +// a is a pointer to a place to store cached unmarshal information. +func (a *InternalMessageInfo) Unmarshal(msg Message, b []byte) error { + // Load the unmarshal information for this message type. + // The atomic load ensures memory consistency. + u := atomicLoadUnmarshalInfo(&a.unmarshal) + if u == nil { + // Slow path: find unmarshal info for msg, update a with it. + u = getUnmarshalInfo(reflect.TypeOf(msg).Elem()) + atomicStoreUnmarshalInfo(&a.unmarshal, u) + } + // Then do the unmarshaling. + err := u.unmarshal(toPointer(&msg), b) + return err +} + +type unmarshalInfo struct { + typ reflect.Type // type of the protobuf struct + + // 0 = only typ field is initialized + // 1 = completely initialized + initialized int32 + lock sync.Mutex // prevents double initialization + dense []unmarshalFieldInfo // fields indexed by tag # + sparse map[uint64]unmarshalFieldInfo // fields indexed by tag # + reqFields []string // names of required fields + reqMask uint64 // 1< 0 { + // Read tag and wire type. + // Special case 1 and 2 byte varints. + var x uint64 + if b[0] < 128 { + x = uint64(b[0]) + b = b[1:] + } else if len(b) >= 2 && b[1] < 128 { + x = uint64(b[0]&0x7f) + uint64(b[1])<<7 + b = b[2:] + } else { + var n int + x, n = decodeVarint(b) + if n == 0 { + return io.ErrUnexpectedEOF + } + b = b[n:] + } + tag := x >> 3 + wire := int(x) & 7 + + // Dispatch on the tag to one of the unmarshal* functions below. + var f unmarshalFieldInfo + if tag < uint64(len(u.dense)) { + f = u.dense[tag] + } else { + f = u.sparse[tag] + } + if fn := f.unmarshal; fn != nil { + var err error + b, err = fn(b, m.offset(f.field), wire) + if err == nil { + reqMask |= f.reqMask + continue + } + if r, ok := err.(*RequiredNotSetError); ok { + // Remember this error, but keep parsing. We need to produce + // a full parse even if a required field is missing. + rnse = r + reqMask |= f.reqMask + continue + } + if err != errInternalBadWireType { + if err == errInvalidUTF8 { + fullName := revProtoTypes[reflect.PtrTo(u.typ)] + "." + f.name + err = fmt.Errorf("proto: string field %q contains invalid UTF-8", fullName) + } + return err + } + // Fragments with bad wire type are treated as unknown fields. + } + + // Unknown tag. + if !u.unrecognized.IsValid() { + // Don't keep unrecognized data; just skip it. + var err error + b, err = skipField(b, wire) + if err != nil { + return err + } + continue + } + // Keep unrecognized data around. + // maybe in extensions, maybe in the unrecognized field. + z := m.offset(u.unrecognized).toBytes() + var emap map[int32]Extension + var e Extension + for _, r := range u.extensionRanges { + if uint64(r.Start) <= tag && tag <= uint64(r.End) { + if u.extensions.IsValid() { + mp := m.offset(u.extensions).toExtensions() + emap = mp.extensionsWrite() + e = emap[int32(tag)] + z = &e.enc + break + } + if u.oldExtensions.IsValid() { + p := m.offset(u.oldExtensions).toOldExtensions() + emap = *p + if emap == nil { + emap = map[int32]Extension{} + *p = emap + } + e = emap[int32(tag)] + z = &e.enc + break + } + panic("no extensions field available") + } + } + + // Use wire type to skip data. + var err error + b0 := b + b, err = skipField(b, wire) + if err != nil { + return err + } + *z = encodeVarint(*z, tag<<3|uint64(wire)) + *z = append(*z, b0[:len(b0)-len(b)]...) + + if emap != nil { + emap[int32(tag)] = e + } + } + if rnse != nil { + // A required field of a submessage/group is missing. Return that error. + return rnse + } + if reqMask != u.reqMask { + // A required field of this message is missing. + for _, n := range u.reqFields { + if reqMask&1 == 0 { + return &RequiredNotSetError{n} + } + reqMask >>= 1 + } + } + return nil +} + +// computeUnmarshalInfo fills in u with information for use +// in unmarshaling protocol buffers of type u.typ. +func (u *unmarshalInfo) computeUnmarshalInfo() { + u.lock.Lock() + defer u.lock.Unlock() + if u.initialized != 0 { + return + } + t := u.typ + n := t.NumField() + + // Set up the "not found" value for the unrecognized byte buffer. + // This is the default for proto3. + u.unrecognized = invalidField + u.extensions = invalidField + u.oldExtensions = invalidField + + // List of the generated type and offset for each oneof field. + type oneofField struct { + ityp reflect.Type // interface type of oneof field + field field // offset in containing message + } + var oneofFields []oneofField + + for i := 0; i < n; i++ { + f := t.Field(i) + if f.Name == "XXX_unrecognized" { + // The byte slice used to hold unrecognized input is special. + if f.Type != reflect.TypeOf(([]byte)(nil)) { + panic("bad type for XXX_unrecognized field: " + f.Type.Name()) + } + u.unrecognized = toField(&f) + continue + } + if f.Name == "XXX_InternalExtensions" { + // Ditto here. + if f.Type != reflect.TypeOf(XXX_InternalExtensions{}) { + panic("bad type for XXX_InternalExtensions field: " + f.Type.Name()) + } + u.extensions = toField(&f) + if f.Tag.Get("protobuf_messageset") == "1" { + u.isMessageSet = true + } + continue + } + if f.Name == "XXX_extensions" { + // An older form of the extensions field. + if f.Type != reflect.TypeOf((map[int32]Extension)(nil)) { + panic("bad type for XXX_extensions field: " + f.Type.Name()) + } + u.oldExtensions = toField(&f) + continue + } + if f.Name == "XXX_NoUnkeyedLiteral" || f.Name == "XXX_sizecache" { + continue + } + + oneof := f.Tag.Get("protobuf_oneof") + if oneof != "" { + oneofFields = append(oneofFields, oneofField{f.Type, toField(&f)}) + // The rest of oneof processing happens below. + continue + } + + tags := f.Tag.Get("protobuf") + tagArray := strings.Split(tags, ",") + if len(tagArray) < 2 { + panic("protobuf tag not enough fields in " + t.Name() + "." + f.Name + ": " + tags) + } + tag, err := strconv.Atoi(tagArray[1]) + if err != nil { + panic("protobuf tag field not an integer: " + tagArray[1]) + } + + name := "" + for _, tag := range tagArray[3:] { + if strings.HasPrefix(tag, "name=") { + name = tag[5:] + } + } + + // Extract unmarshaling function from the field (its type and tags). + unmarshal := fieldUnmarshaler(&f) + + // Required field? + var reqMask uint64 + if tagArray[2] == "req" { + bit := len(u.reqFields) + u.reqFields = append(u.reqFields, name) + reqMask = uint64(1) << uint(bit) + // TODO: if we have more than 64 required fields, we end up + // not verifying that all required fields are present. + // Fix this, perhaps using a count of required fields? + } + + // Store the info in the correct slot in the message. + u.setTag(tag, toField(&f), unmarshal, reqMask, name) + } + + // Find any types associated with oneof fields. + // TODO: XXX_OneofFuncs returns more info than we need. Get rid of some of it? + fn := reflect.Zero(reflect.PtrTo(t)).MethodByName("XXX_OneofFuncs") + if fn.IsValid() { + res := fn.Call(nil)[3] // last return value from XXX_OneofFuncs: []interface{} + for i := res.Len() - 1; i >= 0; i-- { + v := res.Index(i) // interface{} + tptr := reflect.ValueOf(v.Interface()).Type() // *Msg_X + typ := tptr.Elem() // Msg_X + + f := typ.Field(0) // oneof implementers have one field + baseUnmarshal := fieldUnmarshaler(&f) + tags := strings.Split(f.Tag.Get("protobuf"), ",") + fieldNum, err := strconv.Atoi(tags[1]) + if err != nil { + panic("protobuf tag field not an integer: " + tags[1]) + } + var name string + for _, tag := range tags { + if strings.HasPrefix(tag, "name=") { + name = strings.TrimPrefix(tag, "name=") + break + } + } + + // Find the oneof field that this struct implements. + // Might take O(n^2) to process all of the oneofs, but who cares. + for _, of := range oneofFields { + if tptr.Implements(of.ityp) { + // We have found the corresponding interface for this struct. + // That lets us know where this struct should be stored + // when we encounter it during unmarshaling. + unmarshal := makeUnmarshalOneof(typ, of.ityp, baseUnmarshal) + u.setTag(fieldNum, of.field, unmarshal, 0, name) + } + } + } + } + + // Get extension ranges, if any. + fn = reflect.Zero(reflect.PtrTo(t)).MethodByName("ExtensionRangeArray") + if fn.IsValid() { + if !u.extensions.IsValid() && !u.oldExtensions.IsValid() { + panic("a message with extensions, but no extensions field in " + t.Name()) + } + u.extensionRanges = fn.Call(nil)[0].Interface().([]ExtensionRange) + } + + // Explicitly disallow tag 0. This will ensure we flag an error + // when decoding a buffer of all zeros. Without this code, we + // would decode and skip an all-zero buffer of even length. + // [0 0] is [tag=0/wiretype=varint varint-encoded-0]. + u.setTag(0, zeroField, func(b []byte, f pointer, w int) ([]byte, error) { + return nil, fmt.Errorf("proto: %s: illegal tag 0 (wire type %d)", t, w) + }, 0, "") + + // Set mask for required field check. + u.reqMask = uint64(1)<= 0 && (tag < 16 || tag < 2*n) { // TODO: what are the right numbers here? + for len(u.dense) <= tag { + u.dense = append(u.dense, unmarshalFieldInfo{}) + } + u.dense[tag] = i + return + } + if u.sparse == nil { + u.sparse = map[uint64]unmarshalFieldInfo{} + } + u.sparse[uint64(tag)] = i +} + +// fieldUnmarshaler returns an unmarshaler for the given field. +func fieldUnmarshaler(f *reflect.StructField) unmarshaler { + if f.Type.Kind() == reflect.Map { + return makeUnmarshalMap(f) + } + return typeUnmarshaler(f.Type, f.Tag.Get("protobuf")) +} + +// typeUnmarshaler returns an unmarshaler for the given field type / field tag pair. +func typeUnmarshaler(t reflect.Type, tags string) unmarshaler { + tagArray := strings.Split(tags, ",") + encoding := tagArray[0] + name := "unknown" + for _, tag := range tagArray[3:] { + if strings.HasPrefix(tag, "name=") { + name = tag[5:] + } + } + + // Figure out packaging (pointer, slice, or both) + slice := false + pointer := false + if t.Kind() == reflect.Slice && t.Elem().Kind() != reflect.Uint8 { + slice = true + t = t.Elem() + } + if t.Kind() == reflect.Ptr { + pointer = true + t = t.Elem() + } + + // We'll never have both pointer and slice for basic types. + if pointer && slice && t.Kind() != reflect.Struct { + panic("both pointer and slice for basic type in " + t.Name()) + } + + switch t.Kind() { + case reflect.Bool: + if pointer { + return unmarshalBoolPtr + } + if slice { + return unmarshalBoolSlice + } + return unmarshalBoolValue + case reflect.Int32: + switch encoding { + case "fixed32": + if pointer { + return unmarshalFixedS32Ptr + } + if slice { + return unmarshalFixedS32Slice + } + return unmarshalFixedS32Value + case "varint": + // this could be int32 or enum + if pointer { + return unmarshalInt32Ptr + } + if slice { + return unmarshalInt32Slice + } + return unmarshalInt32Value + case "zigzag32": + if pointer { + return unmarshalSint32Ptr + } + if slice { + return unmarshalSint32Slice + } + return unmarshalSint32Value + } + case reflect.Int64: + switch encoding { + case "fixed64": + if pointer { + return unmarshalFixedS64Ptr + } + if slice { + return unmarshalFixedS64Slice + } + return unmarshalFixedS64Value + case "varint": + if pointer { + return unmarshalInt64Ptr + } + if slice { + return unmarshalInt64Slice + } + return unmarshalInt64Value + case "zigzag64": + if pointer { + return unmarshalSint64Ptr + } + if slice { + return unmarshalSint64Slice + } + return unmarshalSint64Value + } + case reflect.Uint32: + switch encoding { + case "fixed32": + if pointer { + return unmarshalFixed32Ptr + } + if slice { + return unmarshalFixed32Slice + } + return unmarshalFixed32Value + case "varint": + if pointer { + return unmarshalUint32Ptr + } + if slice { + return unmarshalUint32Slice + } + return unmarshalUint32Value + } + case reflect.Uint64: + switch encoding { + case "fixed64": + if pointer { + return unmarshalFixed64Ptr + } + if slice { + return unmarshalFixed64Slice + } + return unmarshalFixed64Value + case "varint": + if pointer { + return unmarshalUint64Ptr + } + if slice { + return unmarshalUint64Slice + } + return unmarshalUint64Value + } + case reflect.Float32: + if pointer { + return unmarshalFloat32Ptr + } + if slice { + return unmarshalFloat32Slice + } + return unmarshalFloat32Value + case reflect.Float64: + if pointer { + return unmarshalFloat64Ptr + } + if slice { + return unmarshalFloat64Slice + } + return unmarshalFloat64Value + case reflect.Map: + panic("map type in typeUnmarshaler in " + t.Name()) + case reflect.Slice: + if pointer { + panic("bad pointer in slice case in " + t.Name()) + } + if slice { + return unmarshalBytesSlice + } + return unmarshalBytesValue + case reflect.String: + if pointer { + return unmarshalStringPtr + } + if slice { + return unmarshalStringSlice + } + return unmarshalStringValue + case reflect.Struct: + // message or group field + if !pointer { + panic(fmt.Sprintf("message/group field %s:%s without pointer", t, encoding)) + } + switch encoding { + case "bytes": + if slice { + return makeUnmarshalMessageSlicePtr(getUnmarshalInfo(t), name) + } + return makeUnmarshalMessagePtr(getUnmarshalInfo(t), name) + case "group": + if slice { + return makeUnmarshalGroupSlicePtr(getUnmarshalInfo(t), name) + } + return makeUnmarshalGroupPtr(getUnmarshalInfo(t), name) + } + } + panic(fmt.Sprintf("unmarshaler not found type:%s encoding:%s", t, encoding)) +} + +// Below are all the unmarshalers for individual fields of various types. + +func unmarshalInt64Value(b []byte, f pointer, w int) ([]byte, error) { + if w != WireVarint { + return b, errInternalBadWireType + } + x, n := decodeVarint(b) + if n == 0 { + return nil, io.ErrUnexpectedEOF + } + b = b[n:] + v := int64(x) + *f.toInt64() = v + return b, nil +} + +func unmarshalInt64Ptr(b []byte, f pointer, w int) ([]byte, error) { + if w != WireVarint { + return b, errInternalBadWireType + } + x, n := decodeVarint(b) + if n == 0 { + return nil, io.ErrUnexpectedEOF + } + b = b[n:] + v := int64(x) + *f.toInt64Ptr() = &v + return b, nil +} + +func unmarshalInt64Slice(b []byte, f pointer, w int) ([]byte, error) { + if w == WireBytes { // packed + x, n := decodeVarint(b) + if n == 0 { + return nil, io.ErrUnexpectedEOF + } + b = b[n:] + if x > uint64(len(b)) { + return nil, io.ErrUnexpectedEOF + } + res := b[x:] + b = b[:x] + for len(b) > 0 { + x, n = decodeVarint(b) + if n == 0 { + return nil, io.ErrUnexpectedEOF + } + b = b[n:] + v := int64(x) + s := f.toInt64Slice() + *s = append(*s, v) + } + return res, nil + } + if w != WireVarint { + return b, errInternalBadWireType + } + x, n := decodeVarint(b) + if n == 0 { + return nil, io.ErrUnexpectedEOF + } + b = b[n:] + v := int64(x) + s := f.toInt64Slice() + *s = append(*s, v) + return b, nil +} + +func unmarshalSint64Value(b []byte, f pointer, w int) ([]byte, error) { + if w != WireVarint { + return b, errInternalBadWireType + } + x, n := decodeVarint(b) + if n == 0 { + return nil, io.ErrUnexpectedEOF + } + b = b[n:] + v := int64(x>>1) ^ int64(x)<<63>>63 + *f.toInt64() = v + return b, nil +} + +func unmarshalSint64Ptr(b []byte, f pointer, w int) ([]byte, error) { + if w != WireVarint { + return b, errInternalBadWireType + } + x, n := decodeVarint(b) + if n == 0 { + return nil, io.ErrUnexpectedEOF + } + b = b[n:] + v := int64(x>>1) ^ int64(x)<<63>>63 + *f.toInt64Ptr() = &v + return b, nil +} + +func unmarshalSint64Slice(b []byte, f pointer, w int) ([]byte, error) { + if w == WireBytes { // packed + x, n := decodeVarint(b) + if n == 0 { + return nil, io.ErrUnexpectedEOF + } + b = b[n:] + if x > uint64(len(b)) { + return nil, io.ErrUnexpectedEOF + } + res := b[x:] + b = b[:x] + for len(b) > 0 { + x, n = decodeVarint(b) + if n == 0 { + return nil, io.ErrUnexpectedEOF + } + b = b[n:] + v := int64(x>>1) ^ int64(x)<<63>>63 + s := f.toInt64Slice() + *s = append(*s, v) + } + return res, nil + } + if w != WireVarint { + return b, errInternalBadWireType + } + x, n := decodeVarint(b) + if n == 0 { + return nil, io.ErrUnexpectedEOF + } + b = b[n:] + v := int64(x>>1) ^ int64(x)<<63>>63 + s := f.toInt64Slice() + *s = append(*s, v) + return b, nil +} + +func unmarshalUint64Value(b []byte, f pointer, w int) ([]byte, error) { + if w != WireVarint { + return b, errInternalBadWireType + } + x, n := decodeVarint(b) + if n == 0 { + return nil, io.ErrUnexpectedEOF + } + b = b[n:] + v := uint64(x) + *f.toUint64() = v + return b, nil +} + +func unmarshalUint64Ptr(b []byte, f pointer, w int) ([]byte, error) { + if w != WireVarint { + return b, errInternalBadWireType + } + x, n := decodeVarint(b) + if n == 0 { + return nil, io.ErrUnexpectedEOF + } + b = b[n:] + v := uint64(x) + *f.toUint64Ptr() = &v + return b, nil +} + +func unmarshalUint64Slice(b []byte, f pointer, w int) ([]byte, error) { + if w == WireBytes { // packed + x, n := decodeVarint(b) + if n == 0 { + return nil, io.ErrUnexpectedEOF + } + b = b[n:] + if x > uint64(len(b)) { + return nil, io.ErrUnexpectedEOF + } + res := b[x:] + b = b[:x] + for len(b) > 0 { + x, n = decodeVarint(b) + if n == 0 { + return nil, io.ErrUnexpectedEOF + } + b = b[n:] + v := uint64(x) + s := f.toUint64Slice() + *s = append(*s, v) + } + return res, nil + } + if w != WireVarint { + return b, errInternalBadWireType + } + x, n := decodeVarint(b) + if n == 0 { + return nil, io.ErrUnexpectedEOF + } + b = b[n:] + v := uint64(x) + s := f.toUint64Slice() + *s = append(*s, v) + return b, nil +} + +func unmarshalInt32Value(b []byte, f pointer, w int) ([]byte, error) { + if w != WireVarint { + return b, errInternalBadWireType + } + x, n := decodeVarint(b) + if n == 0 { + return nil, io.ErrUnexpectedEOF + } + b = b[n:] + v := int32(x) + *f.toInt32() = v + return b, nil +} + +func unmarshalInt32Ptr(b []byte, f pointer, w int) ([]byte, error) { + if w != WireVarint { + return b, errInternalBadWireType + } + x, n := decodeVarint(b) + if n == 0 { + return nil, io.ErrUnexpectedEOF + } + b = b[n:] + v := int32(x) + f.setInt32Ptr(v) + return b, nil +} + +func unmarshalInt32Slice(b []byte, f pointer, w int) ([]byte, error) { + if w == WireBytes { // packed + x, n := decodeVarint(b) + if n == 0 { + return nil, io.ErrUnexpectedEOF + } + b = b[n:] + if x > uint64(len(b)) { + return nil, io.ErrUnexpectedEOF + } + res := b[x:] + b = b[:x] + for len(b) > 0 { + x, n = decodeVarint(b) + if n == 0 { + return nil, io.ErrUnexpectedEOF + } + b = b[n:] + v := int32(x) + f.appendInt32Slice(v) + } + return res, nil + } + if w != WireVarint { + return b, errInternalBadWireType + } + x, n := decodeVarint(b) + if n == 0 { + return nil, io.ErrUnexpectedEOF + } + b = b[n:] + v := int32(x) + f.appendInt32Slice(v) + return b, nil +} + +func unmarshalSint32Value(b []byte, f pointer, w int) ([]byte, error) { + if w != WireVarint { + return b, errInternalBadWireType + } + x, n := decodeVarint(b) + if n == 0 { + return nil, io.ErrUnexpectedEOF + } + b = b[n:] + v := int32(x>>1) ^ int32(x)<<31>>31 + *f.toInt32() = v + return b, nil +} + +func unmarshalSint32Ptr(b []byte, f pointer, w int) ([]byte, error) { + if w != WireVarint { + return b, errInternalBadWireType + } + x, n := decodeVarint(b) + if n == 0 { + return nil, io.ErrUnexpectedEOF + } + b = b[n:] + v := int32(x>>1) ^ int32(x)<<31>>31 + f.setInt32Ptr(v) + return b, nil +} + +func unmarshalSint32Slice(b []byte, f pointer, w int) ([]byte, error) { + if w == WireBytes { // packed + x, n := decodeVarint(b) + if n == 0 { + return nil, io.ErrUnexpectedEOF + } + b = b[n:] + if x > uint64(len(b)) { + return nil, io.ErrUnexpectedEOF + } + res := b[x:] + b = b[:x] + for len(b) > 0 { + x, n = decodeVarint(b) + if n == 0 { + return nil, io.ErrUnexpectedEOF + } + b = b[n:] + v := int32(x>>1) ^ int32(x)<<31>>31 + f.appendInt32Slice(v) + } + return res, nil + } + if w != WireVarint { + return b, errInternalBadWireType + } + x, n := decodeVarint(b) + if n == 0 { + return nil, io.ErrUnexpectedEOF + } + b = b[n:] + v := int32(x>>1) ^ int32(x)<<31>>31 + f.appendInt32Slice(v) + return b, nil +} + +func unmarshalUint32Value(b []byte, f pointer, w int) ([]byte, error) { + if w != WireVarint { + return b, errInternalBadWireType + } + x, n := decodeVarint(b) + if n == 0 { + return nil, io.ErrUnexpectedEOF + } + b = b[n:] + v := uint32(x) + *f.toUint32() = v + return b, nil +} + +func unmarshalUint32Ptr(b []byte, f pointer, w int) ([]byte, error) { + if w != WireVarint { + return b, errInternalBadWireType + } + x, n := decodeVarint(b) + if n == 0 { + return nil, io.ErrUnexpectedEOF + } + b = b[n:] + v := uint32(x) + *f.toUint32Ptr() = &v + return b, nil +} + +func unmarshalUint32Slice(b []byte, f pointer, w int) ([]byte, error) { + if w == WireBytes { // packed + x, n := decodeVarint(b) + if n == 0 { + return nil, io.ErrUnexpectedEOF + } + b = b[n:] + if x > uint64(len(b)) { + return nil, io.ErrUnexpectedEOF + } + res := b[x:] + b = b[:x] + for len(b) > 0 { + x, n = decodeVarint(b) + if n == 0 { + return nil, io.ErrUnexpectedEOF + } + b = b[n:] + v := uint32(x) + s := f.toUint32Slice() + *s = append(*s, v) + } + return res, nil + } + if w != WireVarint { + return b, errInternalBadWireType + } + x, n := decodeVarint(b) + if n == 0 { + return nil, io.ErrUnexpectedEOF + } + b = b[n:] + v := uint32(x) + s := f.toUint32Slice() + *s = append(*s, v) + return b, nil +} + +func unmarshalFixed64Value(b []byte, f pointer, w int) ([]byte, error) { + if w != WireFixed64 { + return b, errInternalBadWireType + } + if len(b) < 8 { + return nil, io.ErrUnexpectedEOF + } + v := uint64(b[0]) | uint64(b[1])<<8 | uint64(b[2])<<16 | uint64(b[3])<<24 | uint64(b[4])<<32 | uint64(b[5])<<40 | uint64(b[6])<<48 | uint64(b[7])<<56 + *f.toUint64() = v + return b[8:], nil +} + +func unmarshalFixed64Ptr(b []byte, f pointer, w int) ([]byte, error) { + if w != WireFixed64 { + return b, errInternalBadWireType + } + if len(b) < 8 { + return nil, io.ErrUnexpectedEOF + } + v := uint64(b[0]) | uint64(b[1])<<8 | uint64(b[2])<<16 | uint64(b[3])<<24 | uint64(b[4])<<32 | uint64(b[5])<<40 | uint64(b[6])<<48 | uint64(b[7])<<56 + *f.toUint64Ptr() = &v + return b[8:], nil +} + +func unmarshalFixed64Slice(b []byte, f pointer, w int) ([]byte, error) { + if w == WireBytes { // packed + x, n := decodeVarint(b) + if n == 0 { + return nil, io.ErrUnexpectedEOF + } + b = b[n:] + if x > uint64(len(b)) { + return nil, io.ErrUnexpectedEOF + } + res := b[x:] + b = b[:x] + for len(b) > 0 { + if len(b) < 8 { + return nil, io.ErrUnexpectedEOF + } + v := uint64(b[0]) | uint64(b[1])<<8 | uint64(b[2])<<16 | uint64(b[3])<<24 | uint64(b[4])<<32 | uint64(b[5])<<40 | uint64(b[6])<<48 | uint64(b[7])<<56 + s := f.toUint64Slice() + *s = append(*s, v) + b = b[8:] + } + return res, nil + } + if w != WireFixed64 { + return b, errInternalBadWireType + } + if len(b) < 8 { + return nil, io.ErrUnexpectedEOF + } + v := uint64(b[0]) | uint64(b[1])<<8 | uint64(b[2])<<16 | uint64(b[3])<<24 | uint64(b[4])<<32 | uint64(b[5])<<40 | uint64(b[6])<<48 | uint64(b[7])<<56 + s := f.toUint64Slice() + *s = append(*s, v) + return b[8:], nil +} + +func unmarshalFixedS64Value(b []byte, f pointer, w int) ([]byte, error) { + if w != WireFixed64 { + return b, errInternalBadWireType + } + if len(b) < 8 { + return nil, io.ErrUnexpectedEOF + } + v := int64(b[0]) | int64(b[1])<<8 | int64(b[2])<<16 | int64(b[3])<<24 | int64(b[4])<<32 | int64(b[5])<<40 | int64(b[6])<<48 | int64(b[7])<<56 + *f.toInt64() = v + return b[8:], nil +} + +func unmarshalFixedS64Ptr(b []byte, f pointer, w int) ([]byte, error) { + if w != WireFixed64 { + return b, errInternalBadWireType + } + if len(b) < 8 { + return nil, io.ErrUnexpectedEOF + } + v := int64(b[0]) | int64(b[1])<<8 | int64(b[2])<<16 | int64(b[3])<<24 | int64(b[4])<<32 | int64(b[5])<<40 | int64(b[6])<<48 | int64(b[7])<<56 + *f.toInt64Ptr() = &v + return b[8:], nil +} + +func unmarshalFixedS64Slice(b []byte, f pointer, w int) ([]byte, error) { + if w == WireBytes { // packed + x, n := decodeVarint(b) + if n == 0 { + return nil, io.ErrUnexpectedEOF + } + b = b[n:] + if x > uint64(len(b)) { + return nil, io.ErrUnexpectedEOF + } + res := b[x:] + b = b[:x] + for len(b) > 0 { + if len(b) < 8 { + return nil, io.ErrUnexpectedEOF + } + v := int64(b[0]) | int64(b[1])<<8 | int64(b[2])<<16 | int64(b[3])<<24 | int64(b[4])<<32 | int64(b[5])<<40 | int64(b[6])<<48 | int64(b[7])<<56 + s := f.toInt64Slice() + *s = append(*s, v) + b = b[8:] + } + return res, nil + } + if w != WireFixed64 { + return b, errInternalBadWireType + } + if len(b) < 8 { + return nil, io.ErrUnexpectedEOF + } + v := int64(b[0]) | int64(b[1])<<8 | int64(b[2])<<16 | int64(b[3])<<24 | int64(b[4])<<32 | int64(b[5])<<40 | int64(b[6])<<48 | int64(b[7])<<56 + s := f.toInt64Slice() + *s = append(*s, v) + return b[8:], nil +} + +func unmarshalFixed32Value(b []byte, f pointer, w int) ([]byte, error) { + if w != WireFixed32 { + return b, errInternalBadWireType + } + if len(b) < 4 { + return nil, io.ErrUnexpectedEOF + } + v := uint32(b[0]) | uint32(b[1])<<8 | uint32(b[2])<<16 | uint32(b[3])<<24 + *f.toUint32() = v + return b[4:], nil +} + +func unmarshalFixed32Ptr(b []byte, f pointer, w int) ([]byte, error) { + if w != WireFixed32 { + return b, errInternalBadWireType + } + if len(b) < 4 { + return nil, io.ErrUnexpectedEOF + } + v := uint32(b[0]) | uint32(b[1])<<8 | uint32(b[2])<<16 | uint32(b[3])<<24 + *f.toUint32Ptr() = &v + return b[4:], nil +} + +func unmarshalFixed32Slice(b []byte, f pointer, w int) ([]byte, error) { + if w == WireBytes { // packed + x, n := decodeVarint(b) + if n == 0 { + return nil, io.ErrUnexpectedEOF + } + b = b[n:] + if x > uint64(len(b)) { + return nil, io.ErrUnexpectedEOF + } + res := b[x:] + b = b[:x] + for len(b) > 0 { + if len(b) < 4 { + return nil, io.ErrUnexpectedEOF + } + v := uint32(b[0]) | uint32(b[1])<<8 | uint32(b[2])<<16 | uint32(b[3])<<24 + s := f.toUint32Slice() + *s = append(*s, v) + b = b[4:] + } + return res, nil + } + if w != WireFixed32 { + return b, errInternalBadWireType + } + if len(b) < 4 { + return nil, io.ErrUnexpectedEOF + } + v := uint32(b[0]) | uint32(b[1])<<8 | uint32(b[2])<<16 | uint32(b[3])<<24 + s := f.toUint32Slice() + *s = append(*s, v) + return b[4:], nil +} + +func unmarshalFixedS32Value(b []byte, f pointer, w int) ([]byte, error) { + if w != WireFixed32 { + return b, errInternalBadWireType + } + if len(b) < 4 { + return nil, io.ErrUnexpectedEOF + } + v := int32(b[0]) | int32(b[1])<<8 | int32(b[2])<<16 | int32(b[3])<<24 + *f.toInt32() = v + return b[4:], nil +} + +func unmarshalFixedS32Ptr(b []byte, f pointer, w int) ([]byte, error) { + if w != WireFixed32 { + return b, errInternalBadWireType + } + if len(b) < 4 { + return nil, io.ErrUnexpectedEOF + } + v := int32(b[0]) | int32(b[1])<<8 | int32(b[2])<<16 | int32(b[3])<<24 + f.setInt32Ptr(v) + return b[4:], nil +} + +func unmarshalFixedS32Slice(b []byte, f pointer, w int) ([]byte, error) { + if w == WireBytes { // packed + x, n := decodeVarint(b) + if n == 0 { + return nil, io.ErrUnexpectedEOF + } + b = b[n:] + if x > uint64(len(b)) { + return nil, io.ErrUnexpectedEOF + } + res := b[x:] + b = b[:x] + for len(b) > 0 { + if len(b) < 4 { + return nil, io.ErrUnexpectedEOF + } + v := int32(b[0]) | int32(b[1])<<8 | int32(b[2])<<16 | int32(b[3])<<24 + f.appendInt32Slice(v) + b = b[4:] + } + return res, nil + } + if w != WireFixed32 { + return b, errInternalBadWireType + } + if len(b) < 4 { + return nil, io.ErrUnexpectedEOF + } + v := int32(b[0]) | int32(b[1])<<8 | int32(b[2])<<16 | int32(b[3])<<24 + f.appendInt32Slice(v) + return b[4:], nil +} + +func unmarshalBoolValue(b []byte, f pointer, w int) ([]byte, error) { + if w != WireVarint { + return b, errInternalBadWireType + } + // Note: any length varint is allowed, even though any sane + // encoder will use one byte. + // See https://github.com/golang/protobuf/issues/76 + x, n := decodeVarint(b) + if n == 0 { + return nil, io.ErrUnexpectedEOF + } + // TODO: check if x>1? Tests seem to indicate no. + v := x != 0 + *f.toBool() = v + return b[n:], nil +} + +func unmarshalBoolPtr(b []byte, f pointer, w int) ([]byte, error) { + if w != WireVarint { + return b, errInternalBadWireType + } + x, n := decodeVarint(b) + if n == 0 { + return nil, io.ErrUnexpectedEOF + } + v := x != 0 + *f.toBoolPtr() = &v + return b[n:], nil +} + +func unmarshalBoolSlice(b []byte, f pointer, w int) ([]byte, error) { + if w == WireBytes { // packed + x, n := decodeVarint(b) + if n == 0 { + return nil, io.ErrUnexpectedEOF + } + b = b[n:] + if x > uint64(len(b)) { + return nil, io.ErrUnexpectedEOF + } + res := b[x:] + b = b[:x] + for len(b) > 0 { + x, n = decodeVarint(b) + if n == 0 { + return nil, io.ErrUnexpectedEOF + } + v := x != 0 + s := f.toBoolSlice() + *s = append(*s, v) + b = b[n:] + } + return res, nil + } + if w != WireVarint { + return b, errInternalBadWireType + } + x, n := decodeVarint(b) + if n == 0 { + return nil, io.ErrUnexpectedEOF + } + v := x != 0 + s := f.toBoolSlice() + *s = append(*s, v) + return b[n:], nil +} + +func unmarshalFloat64Value(b []byte, f pointer, w int) ([]byte, error) { + if w != WireFixed64 { + return b, errInternalBadWireType + } + if len(b) < 8 { + return nil, io.ErrUnexpectedEOF + } + v := math.Float64frombits(uint64(b[0]) | uint64(b[1])<<8 | uint64(b[2])<<16 | uint64(b[3])<<24 | uint64(b[4])<<32 | uint64(b[5])<<40 | uint64(b[6])<<48 | uint64(b[7])<<56) + *f.toFloat64() = v + return b[8:], nil +} + +func unmarshalFloat64Ptr(b []byte, f pointer, w int) ([]byte, error) { + if w != WireFixed64 { + return b, errInternalBadWireType + } + if len(b) < 8 { + return nil, io.ErrUnexpectedEOF + } + v := math.Float64frombits(uint64(b[0]) | uint64(b[1])<<8 | uint64(b[2])<<16 | uint64(b[3])<<24 | uint64(b[4])<<32 | uint64(b[5])<<40 | uint64(b[6])<<48 | uint64(b[7])<<56) + *f.toFloat64Ptr() = &v + return b[8:], nil +} + +func unmarshalFloat64Slice(b []byte, f pointer, w int) ([]byte, error) { + if w == WireBytes { // packed + x, n := decodeVarint(b) + if n == 0 { + return nil, io.ErrUnexpectedEOF + } + b = b[n:] + if x > uint64(len(b)) { + return nil, io.ErrUnexpectedEOF + } + res := b[x:] + b = b[:x] + for len(b) > 0 { + if len(b) < 8 { + return nil, io.ErrUnexpectedEOF + } + v := math.Float64frombits(uint64(b[0]) | uint64(b[1])<<8 | uint64(b[2])<<16 | uint64(b[3])<<24 | uint64(b[4])<<32 | uint64(b[5])<<40 | uint64(b[6])<<48 | uint64(b[7])<<56) + s := f.toFloat64Slice() + *s = append(*s, v) + b = b[8:] + } + return res, nil + } + if w != WireFixed64 { + return b, errInternalBadWireType + } + if len(b) < 8 { + return nil, io.ErrUnexpectedEOF + } + v := math.Float64frombits(uint64(b[0]) | uint64(b[1])<<8 | uint64(b[2])<<16 | uint64(b[3])<<24 | uint64(b[4])<<32 | uint64(b[5])<<40 | uint64(b[6])<<48 | uint64(b[7])<<56) + s := f.toFloat64Slice() + *s = append(*s, v) + return b[8:], nil +} + +func unmarshalFloat32Value(b []byte, f pointer, w int) ([]byte, error) { + if w != WireFixed32 { + return b, errInternalBadWireType + } + if len(b) < 4 { + return nil, io.ErrUnexpectedEOF + } + v := math.Float32frombits(uint32(b[0]) | uint32(b[1])<<8 | uint32(b[2])<<16 | uint32(b[3])<<24) + *f.toFloat32() = v + return b[4:], nil +} + +func unmarshalFloat32Ptr(b []byte, f pointer, w int) ([]byte, error) { + if w != WireFixed32 { + return b, errInternalBadWireType + } + if len(b) < 4 { + return nil, io.ErrUnexpectedEOF + } + v := math.Float32frombits(uint32(b[0]) | uint32(b[1])<<8 | uint32(b[2])<<16 | uint32(b[3])<<24) + *f.toFloat32Ptr() = &v + return b[4:], nil +} + +func unmarshalFloat32Slice(b []byte, f pointer, w int) ([]byte, error) { + if w == WireBytes { // packed + x, n := decodeVarint(b) + if n == 0 { + return nil, io.ErrUnexpectedEOF + } + b = b[n:] + if x > uint64(len(b)) { + return nil, io.ErrUnexpectedEOF + } + res := b[x:] + b = b[:x] + for len(b) > 0 { + if len(b) < 4 { + return nil, io.ErrUnexpectedEOF + } + v := math.Float32frombits(uint32(b[0]) | uint32(b[1])<<8 | uint32(b[2])<<16 | uint32(b[3])<<24) + s := f.toFloat32Slice() + *s = append(*s, v) + b = b[4:] + } + return res, nil + } + if w != WireFixed32 { + return b, errInternalBadWireType + } + if len(b) < 4 { + return nil, io.ErrUnexpectedEOF + } + v := math.Float32frombits(uint32(b[0]) | uint32(b[1])<<8 | uint32(b[2])<<16 | uint32(b[3])<<24) + s := f.toFloat32Slice() + *s = append(*s, v) + return b[4:], nil +} + +func unmarshalStringValue(b []byte, f pointer, w int) ([]byte, error) { + if w != WireBytes { + return b, errInternalBadWireType + } + x, n := decodeVarint(b) + if n == 0 { + return nil, io.ErrUnexpectedEOF + } + b = b[n:] + if x > uint64(len(b)) { + return nil, io.ErrUnexpectedEOF + } + v := string(b[:x]) + if !utf8.ValidString(v) { + return nil, errInvalidUTF8 + } + *f.toString() = v + return b[x:], nil +} + +func unmarshalStringPtr(b []byte, f pointer, w int) ([]byte, error) { + if w != WireBytes { + return b, errInternalBadWireType + } + x, n := decodeVarint(b) + if n == 0 { + return nil, io.ErrUnexpectedEOF + } + b = b[n:] + if x > uint64(len(b)) { + return nil, io.ErrUnexpectedEOF + } + v := string(b[:x]) + if !utf8.ValidString(v) { + return nil, errInvalidUTF8 + } + *f.toStringPtr() = &v + return b[x:], nil +} + +func unmarshalStringSlice(b []byte, f pointer, w int) ([]byte, error) { + if w != WireBytes { + return b, errInternalBadWireType + } + x, n := decodeVarint(b) + if n == 0 { + return nil, io.ErrUnexpectedEOF + } + b = b[n:] + if x > uint64(len(b)) { + return nil, io.ErrUnexpectedEOF + } + v := string(b[:x]) + if !utf8.ValidString(v) { + return nil, errInvalidUTF8 + } + s := f.toStringSlice() + *s = append(*s, v) + return b[x:], nil +} + +var emptyBuf [0]byte + +func unmarshalBytesValue(b []byte, f pointer, w int) ([]byte, error) { + if w != WireBytes { + return b, errInternalBadWireType + } + x, n := decodeVarint(b) + if n == 0 { + return nil, io.ErrUnexpectedEOF + } + b = b[n:] + if x > uint64(len(b)) { + return nil, io.ErrUnexpectedEOF + } + // The use of append here is a trick which avoids the zeroing + // that would be required if we used a make/copy pair. + // We append to emptyBuf instead of nil because we want + // a non-nil result even when the length is 0. + v := append(emptyBuf[:], b[:x]...) + *f.toBytes() = v + return b[x:], nil +} + +func unmarshalBytesSlice(b []byte, f pointer, w int) ([]byte, error) { + if w != WireBytes { + return b, errInternalBadWireType + } + x, n := decodeVarint(b) + if n == 0 { + return nil, io.ErrUnexpectedEOF + } + b = b[n:] + if x > uint64(len(b)) { + return nil, io.ErrUnexpectedEOF + } + v := append(emptyBuf[:], b[:x]...) + s := f.toBytesSlice() + *s = append(*s, v) + return b[x:], nil +} + +func makeUnmarshalMessagePtr(sub *unmarshalInfo, name string) unmarshaler { + return func(b []byte, f pointer, w int) ([]byte, error) { + if w != WireBytes { + return b, errInternalBadWireType + } + x, n := decodeVarint(b) + if n == 0 { + return nil, io.ErrUnexpectedEOF + } + b = b[n:] + if x > uint64(len(b)) { + return nil, io.ErrUnexpectedEOF + } + // First read the message field to see if something is there. + // The semantics of multiple submessages are weird. Instead of + // the last one winning (as it is for all other fields), multiple + // submessages are merged. + v := f.getPointer() + if v.isNil() { + v = valToPointer(reflect.New(sub.typ)) + f.setPointer(v) + } + err := sub.unmarshal(v, b[:x]) + if err != nil { + if r, ok := err.(*RequiredNotSetError); ok { + r.field = name + "." + r.field + } else { + return nil, err + } + } + return b[x:], err + } +} + +func makeUnmarshalMessageSlicePtr(sub *unmarshalInfo, name string) unmarshaler { + return func(b []byte, f pointer, w int) ([]byte, error) { + if w != WireBytes { + return b, errInternalBadWireType + } + x, n := decodeVarint(b) + if n == 0 { + return nil, io.ErrUnexpectedEOF + } + b = b[n:] + if x > uint64(len(b)) { + return nil, io.ErrUnexpectedEOF + } + v := valToPointer(reflect.New(sub.typ)) + err := sub.unmarshal(v, b[:x]) + if err != nil { + if r, ok := err.(*RequiredNotSetError); ok { + r.field = name + "." + r.field + } else { + return nil, err + } + } + f.appendPointer(v) + return b[x:], err + } +} + +func makeUnmarshalGroupPtr(sub *unmarshalInfo, name string) unmarshaler { + return func(b []byte, f pointer, w int) ([]byte, error) { + if w != WireStartGroup { + return b, errInternalBadWireType + } + x, y := findEndGroup(b) + if x < 0 { + return nil, io.ErrUnexpectedEOF + } + v := f.getPointer() + if v.isNil() { + v = valToPointer(reflect.New(sub.typ)) + f.setPointer(v) + } + err := sub.unmarshal(v, b[:x]) + if err != nil { + if r, ok := err.(*RequiredNotSetError); ok { + r.field = name + "." + r.field + } else { + return nil, err + } + } + return b[y:], err + } +} + +func makeUnmarshalGroupSlicePtr(sub *unmarshalInfo, name string) unmarshaler { + return func(b []byte, f pointer, w int) ([]byte, error) { + if w != WireStartGroup { + return b, errInternalBadWireType + } + x, y := findEndGroup(b) + if x < 0 { + return nil, io.ErrUnexpectedEOF + } + v := valToPointer(reflect.New(sub.typ)) + err := sub.unmarshal(v, b[:x]) + if err != nil { + if r, ok := err.(*RequiredNotSetError); ok { + r.field = name + "." + r.field + } else { + return nil, err + } + } + f.appendPointer(v) + return b[y:], err + } +} + +func makeUnmarshalMap(f *reflect.StructField) unmarshaler { + t := f.Type + kt := t.Key() + vt := t.Elem() + unmarshalKey := typeUnmarshaler(kt, f.Tag.Get("protobuf_key")) + unmarshalVal := typeUnmarshaler(vt, f.Tag.Get("protobuf_val")) + return func(b []byte, f pointer, w int) ([]byte, error) { + // The map entry is a submessage. Figure out how big it is. + if w != WireBytes { + return nil, fmt.Errorf("proto: bad wiretype for map field: got %d want %d", w, WireBytes) + } + x, n := decodeVarint(b) + if n == 0 { + return nil, io.ErrUnexpectedEOF + } + b = b[n:] + if x > uint64(len(b)) { + return nil, io.ErrUnexpectedEOF + } + r := b[x:] // unused data to return + b = b[:x] // data for map entry + + // Note: we could use #keys * #values ~= 200 functions + // to do map decoding without reflection. Probably not worth it. + // Maps will be somewhat slow. Oh well. + + // Read key and value from data. + k := reflect.New(kt) + v := reflect.New(vt) + for len(b) > 0 { + x, n := decodeVarint(b) + if n == 0 { + return nil, io.ErrUnexpectedEOF + } + wire := int(x) & 7 + b = b[n:] + + var err error + switch x >> 3 { + case 1: + b, err = unmarshalKey(b, valToPointer(k), wire) + case 2: + b, err = unmarshalVal(b, valToPointer(v), wire) + default: + err = errInternalBadWireType // skip unknown tag + } + + if err == nil { + continue + } + if err != errInternalBadWireType { + return nil, err + } + + // Skip past unknown fields. + b, err = skipField(b, wire) + if err != nil { + return nil, err + } + } + + // Get map, allocate if needed. + m := f.asPointerTo(t).Elem() // an addressable map[K]T + if m.IsNil() { + m.Set(reflect.MakeMap(t)) + } + + // Insert into map. + m.SetMapIndex(k.Elem(), v.Elem()) + + return r, nil + } +} + +// makeUnmarshalOneof makes an unmarshaler for oneof fields. +// for: +// message Msg { +// oneof F { +// int64 X = 1; +// float64 Y = 2; +// } +// } +// typ is the type of the concrete entry for a oneof case (e.g. Msg_X). +// ityp is the interface type of the oneof field (e.g. isMsg_F). +// unmarshal is the unmarshaler for the base type of the oneof case (e.g. int64). +// Note that this function will be called once for each case in the oneof. +func makeUnmarshalOneof(typ, ityp reflect.Type, unmarshal unmarshaler) unmarshaler { + sf := typ.Field(0) + field0 := toField(&sf) + return func(b []byte, f pointer, w int) ([]byte, error) { + // Allocate holder for value. + v := reflect.New(typ) + + // Unmarshal data into holder. + // We unmarshal into the first field of the holder object. + var err error + b, err = unmarshal(b, valToPointer(v).offset(field0), w) + if err != nil { + return nil, err + } + + // Write pointer to holder into target field. + f.asPointerTo(ityp).Elem().Set(v) + + return b, nil + } +} + +// Error used by decode internally. +var errInternalBadWireType = errors.New("proto: internal error: bad wiretype") + +// skipField skips past a field of type wire and returns the remaining bytes. +func skipField(b []byte, wire int) ([]byte, error) { + switch wire { + case WireVarint: + _, k := decodeVarint(b) + if k == 0 { + return b, io.ErrUnexpectedEOF + } + b = b[k:] + case WireFixed32: + if len(b) < 4 { + return b, io.ErrUnexpectedEOF + } + b = b[4:] + case WireFixed64: + if len(b) < 8 { + return b, io.ErrUnexpectedEOF + } + b = b[8:] + case WireBytes: + m, k := decodeVarint(b) + if k == 0 || uint64(len(b)-k) < m { + return b, io.ErrUnexpectedEOF + } + b = b[uint64(k)+m:] + case WireStartGroup: + _, i := findEndGroup(b) + if i == -1 { + return b, io.ErrUnexpectedEOF + } + b = b[i:] + default: + return b, fmt.Errorf("proto: can't skip unknown wire type %d", wire) + } + return b, nil +} + +// findEndGroup finds the index of the next EndGroup tag. +// Groups may be nested, so the "next" EndGroup tag is the first +// unpaired EndGroup. +// findEndGroup returns the indexes of the start and end of the EndGroup tag. +// Returns (-1,-1) if it can't find one. +func findEndGroup(b []byte) (int, int) { + depth := 1 + i := 0 + for { + x, n := decodeVarint(b[i:]) + if n == 0 { + return -1, -1 + } + j := i + i += n + switch x & 7 { + case WireVarint: + _, k := decodeVarint(b[i:]) + if k == 0 { + return -1, -1 + } + i += k + case WireFixed32: + if len(b)-4 < i { + return -1, -1 + } + i += 4 + case WireFixed64: + if len(b)-8 < i { + return -1, -1 + } + i += 8 + case WireBytes: + m, k := decodeVarint(b[i:]) + if k == 0 { + return -1, -1 + } + i += k + if uint64(len(b)-i) < m { + return -1, -1 + } + i += int(m) + case WireStartGroup: + depth++ + case WireEndGroup: + depth-- + if depth == 0 { + return j, i + } + default: + return -1, -1 + } + } +} + +// encodeVarint appends a varint-encoded integer to b and returns the result. +func encodeVarint(b []byte, x uint64) []byte { + for x >= 1<<7 { + b = append(b, byte(x&0x7f|0x80)) + x >>= 7 + } + return append(b, byte(x)) +} + +// decodeVarint reads a varint-encoded integer from b. +// Returns the decoded integer and the number of bytes read. +// If there is an error, it returns 0,0. +func decodeVarint(b []byte) (uint64, int) { + var x, y uint64 + if len(b) <= 0 { + goto bad + } + x = uint64(b[0]) + if x < 0x80 { + return x, 1 + } + x -= 0x80 + + if len(b) <= 1 { + goto bad + } + y = uint64(b[1]) + x += y << 7 + if y < 0x80 { + return x, 2 + } + x -= 0x80 << 7 + + if len(b) <= 2 { + goto bad + } + y = uint64(b[2]) + x += y << 14 + if y < 0x80 { + return x, 3 + } + x -= 0x80 << 14 + + if len(b) <= 3 { + goto bad + } + y = uint64(b[3]) + x += y << 21 + if y < 0x80 { + return x, 4 + } + x -= 0x80 << 21 + + if len(b) <= 4 { + goto bad + } + y = uint64(b[4]) + x += y << 28 + if y < 0x80 { + return x, 5 + } + x -= 0x80 << 28 + + if len(b) <= 5 { + goto bad + } + y = uint64(b[5]) + x += y << 35 + if y < 0x80 { + return x, 6 + } + x -= 0x80 << 35 + + if len(b) <= 6 { + goto bad + } + y = uint64(b[6]) + x += y << 42 + if y < 0x80 { + return x, 7 + } + x -= 0x80 << 42 + + if len(b) <= 7 { + goto bad + } + y = uint64(b[7]) + x += y << 49 + if y < 0x80 { + return x, 8 + } + x -= 0x80 << 49 + + if len(b) <= 8 { + goto bad + } + y = uint64(b[8]) + x += y << 56 + if y < 0x80 { + return x, 9 + } + x -= 0x80 << 56 + + if len(b) <= 9 { + goto bad + } + y = uint64(b[9]) + x += y << 63 + if y < 2 { + return x, 10 + } + +bad: + return 0, 0 +} diff --git a/vendor/github.com/golang/protobuf/proto/text.go b/vendor/github.com/golang/protobuf/proto/text.go new file mode 100644 index 00000000..2205fdaa --- /dev/null +++ b/vendor/github.com/golang/protobuf/proto/text.go @@ -0,0 +1,843 @@ +// Go support for Protocol Buffers - Google's data interchange format +// +// Copyright 2010 The Go Authors. All rights reserved. +// https://github.com/golang/protobuf +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package proto + +// Functions for writing the text protocol buffer format. + +import ( + "bufio" + "bytes" + "encoding" + "errors" + "fmt" + "io" + "log" + "math" + "reflect" + "sort" + "strings" +) + +var ( + newline = []byte("\n") + spaces = []byte(" ") + endBraceNewline = []byte("}\n") + backslashN = []byte{'\\', 'n'} + backslashR = []byte{'\\', 'r'} + backslashT = []byte{'\\', 't'} + backslashDQ = []byte{'\\', '"'} + backslashBS = []byte{'\\', '\\'} + posInf = []byte("inf") + negInf = []byte("-inf") + nan = []byte("nan") +) + +type writer interface { + io.Writer + WriteByte(byte) error +} + +// textWriter is an io.Writer that tracks its indentation level. +type textWriter struct { + ind int + complete bool // if the current position is a complete line + compact bool // whether to write out as a one-liner + w writer +} + +func (w *textWriter) WriteString(s string) (n int, err error) { + if !strings.Contains(s, "\n") { + if !w.compact && w.complete { + w.writeIndent() + } + w.complete = false + return io.WriteString(w.w, s) + } + // WriteString is typically called without newlines, so this + // codepath and its copy are rare. We copy to avoid + // duplicating all of Write's logic here. + return w.Write([]byte(s)) +} + +func (w *textWriter) Write(p []byte) (n int, err error) { + newlines := bytes.Count(p, newline) + if newlines == 0 { + if !w.compact && w.complete { + w.writeIndent() + } + n, err = w.w.Write(p) + w.complete = false + return n, err + } + + frags := bytes.SplitN(p, newline, newlines+1) + if w.compact { + for i, frag := range frags { + if i > 0 { + if err := w.w.WriteByte(' '); err != nil { + return n, err + } + n++ + } + nn, err := w.w.Write(frag) + n += nn + if err != nil { + return n, err + } + } + return n, nil + } + + for i, frag := range frags { + if w.complete { + w.writeIndent() + } + nn, err := w.w.Write(frag) + n += nn + if err != nil { + return n, err + } + if i+1 < len(frags) { + if err := w.w.WriteByte('\n'); err != nil { + return n, err + } + n++ + } + } + w.complete = len(frags[len(frags)-1]) == 0 + return n, nil +} + +func (w *textWriter) WriteByte(c byte) error { + if w.compact && c == '\n' { + c = ' ' + } + if !w.compact && w.complete { + w.writeIndent() + } + err := w.w.WriteByte(c) + w.complete = c == '\n' + return err +} + +func (w *textWriter) indent() { w.ind++ } + +func (w *textWriter) unindent() { + if w.ind == 0 { + log.Print("proto: textWriter unindented too far") + return + } + w.ind-- +} + +func writeName(w *textWriter, props *Properties) error { + if _, err := w.WriteString(props.OrigName); err != nil { + return err + } + if props.Wire != "group" { + return w.WriteByte(':') + } + return nil +} + +func requiresQuotes(u string) bool { + // When type URL contains any characters except [0-9A-Za-z./\-]*, it must be quoted. + for _, ch := range u { + switch { + case ch == '.' || ch == '/' || ch == '_': + continue + case '0' <= ch && ch <= '9': + continue + case 'A' <= ch && ch <= 'Z': + continue + case 'a' <= ch && ch <= 'z': + continue + default: + return true + } + } + return false +} + +// isAny reports whether sv is a google.protobuf.Any message +func isAny(sv reflect.Value) bool { + type wkt interface { + XXX_WellKnownType() string + } + t, ok := sv.Addr().Interface().(wkt) + return ok && t.XXX_WellKnownType() == "Any" +} + +// writeProto3Any writes an expanded google.protobuf.Any message. +// +// It returns (false, nil) if sv value can't be unmarshaled (e.g. because +// required messages are not linked in). +// +// It returns (true, error) when sv was written in expanded format or an error +// was encountered. +func (tm *TextMarshaler) writeProto3Any(w *textWriter, sv reflect.Value) (bool, error) { + turl := sv.FieldByName("TypeUrl") + val := sv.FieldByName("Value") + if !turl.IsValid() || !val.IsValid() { + return true, errors.New("proto: invalid google.protobuf.Any message") + } + + b, ok := val.Interface().([]byte) + if !ok { + return true, errors.New("proto: invalid google.protobuf.Any message") + } + + parts := strings.Split(turl.String(), "/") + mt := MessageType(parts[len(parts)-1]) + if mt == nil { + return false, nil + } + m := reflect.New(mt.Elem()) + if err := Unmarshal(b, m.Interface().(Message)); err != nil { + return false, nil + } + w.Write([]byte("[")) + u := turl.String() + if requiresQuotes(u) { + writeString(w, u) + } else { + w.Write([]byte(u)) + } + if w.compact { + w.Write([]byte("]:<")) + } else { + w.Write([]byte("]: <\n")) + w.ind++ + } + if err := tm.writeStruct(w, m.Elem()); err != nil { + return true, err + } + if w.compact { + w.Write([]byte("> ")) + } else { + w.ind-- + w.Write([]byte(">\n")) + } + return true, nil +} + +func (tm *TextMarshaler) writeStruct(w *textWriter, sv reflect.Value) error { + if tm.ExpandAny && isAny(sv) { + if canExpand, err := tm.writeProto3Any(w, sv); canExpand { + return err + } + } + st := sv.Type() + sprops := GetProperties(st) + for i := 0; i < sv.NumField(); i++ { + fv := sv.Field(i) + props := sprops.Prop[i] + name := st.Field(i).Name + + if name == "XXX_NoUnkeyedLiteral" { + continue + } + + if strings.HasPrefix(name, "XXX_") { + // There are two XXX_ fields: + // XXX_unrecognized []byte + // XXX_extensions map[int32]proto.Extension + // The first is handled here; + // the second is handled at the bottom of this function. + if name == "XXX_unrecognized" && !fv.IsNil() { + if err := writeUnknownStruct(w, fv.Interface().([]byte)); err != nil { + return err + } + } + continue + } + if fv.Kind() == reflect.Ptr && fv.IsNil() { + // Field not filled in. This could be an optional field or + // a required field that wasn't filled in. Either way, there + // isn't anything we can show for it. + continue + } + if fv.Kind() == reflect.Slice && fv.IsNil() { + // Repeated field that is empty, or a bytes field that is unused. + continue + } + + if props.Repeated && fv.Kind() == reflect.Slice { + // Repeated field. + for j := 0; j < fv.Len(); j++ { + if err := writeName(w, props); err != nil { + return err + } + if !w.compact { + if err := w.WriteByte(' '); err != nil { + return err + } + } + v := fv.Index(j) + if v.Kind() == reflect.Ptr && v.IsNil() { + // A nil message in a repeated field is not valid, + // but we can handle that more gracefully than panicking. + if _, err := w.Write([]byte("\n")); err != nil { + return err + } + continue + } + if err := tm.writeAny(w, v, props); err != nil { + return err + } + if err := w.WriteByte('\n'); err != nil { + return err + } + } + continue + } + if fv.Kind() == reflect.Map { + // Map fields are rendered as a repeated struct with key/value fields. + keys := fv.MapKeys() + sort.Sort(mapKeys(keys)) + for _, key := range keys { + val := fv.MapIndex(key) + if err := writeName(w, props); err != nil { + return err + } + if !w.compact { + if err := w.WriteByte(' '); err != nil { + return err + } + } + // open struct + if err := w.WriteByte('<'); err != nil { + return err + } + if !w.compact { + if err := w.WriteByte('\n'); err != nil { + return err + } + } + w.indent() + // key + if _, err := w.WriteString("key:"); err != nil { + return err + } + if !w.compact { + if err := w.WriteByte(' '); err != nil { + return err + } + } + if err := tm.writeAny(w, key, props.mkeyprop); err != nil { + return err + } + if err := w.WriteByte('\n'); err != nil { + return err + } + // nil values aren't legal, but we can avoid panicking because of them. + if val.Kind() != reflect.Ptr || !val.IsNil() { + // value + if _, err := w.WriteString("value:"); err != nil { + return err + } + if !w.compact { + if err := w.WriteByte(' '); err != nil { + return err + } + } + if err := tm.writeAny(w, val, props.mvalprop); err != nil { + return err + } + if err := w.WriteByte('\n'); err != nil { + return err + } + } + // close struct + w.unindent() + if err := w.WriteByte('>'); err != nil { + return err + } + if err := w.WriteByte('\n'); err != nil { + return err + } + } + continue + } + if props.proto3 && fv.Kind() == reflect.Slice && fv.Len() == 0 { + // empty bytes field + continue + } + if fv.Kind() != reflect.Ptr && fv.Kind() != reflect.Slice { + // proto3 non-repeated scalar field; skip if zero value + if isProto3Zero(fv) { + continue + } + } + + if fv.Kind() == reflect.Interface { + // Check if it is a oneof. + if st.Field(i).Tag.Get("protobuf_oneof") != "" { + // fv is nil, or holds a pointer to generated struct. + // That generated struct has exactly one field, + // which has a protobuf struct tag. + if fv.IsNil() { + continue + } + inner := fv.Elem().Elem() // interface -> *T -> T + tag := inner.Type().Field(0).Tag.Get("protobuf") + props = new(Properties) // Overwrite the outer props var, but not its pointee. + props.Parse(tag) + // Write the value in the oneof, not the oneof itself. + fv = inner.Field(0) + + // Special case to cope with malformed messages gracefully: + // If the value in the oneof is a nil pointer, don't panic + // in writeAny. + if fv.Kind() == reflect.Ptr && fv.IsNil() { + // Use errors.New so writeAny won't render quotes. + msg := errors.New("/* nil */") + fv = reflect.ValueOf(&msg).Elem() + } + } + } + + if err := writeName(w, props); err != nil { + return err + } + if !w.compact { + if err := w.WriteByte(' '); err != nil { + return err + } + } + + // Enums have a String method, so writeAny will work fine. + if err := tm.writeAny(w, fv, props); err != nil { + return err + } + + if err := w.WriteByte('\n'); err != nil { + return err + } + } + + // Extensions (the XXX_extensions field). + pv := sv.Addr() + if _, err := extendable(pv.Interface()); err == nil { + if err := tm.writeExtensions(w, pv); err != nil { + return err + } + } + + return nil +} + +// writeAny writes an arbitrary field. +func (tm *TextMarshaler) writeAny(w *textWriter, v reflect.Value, props *Properties) error { + v = reflect.Indirect(v) + + // Floats have special cases. + if v.Kind() == reflect.Float32 || v.Kind() == reflect.Float64 { + x := v.Float() + var b []byte + switch { + case math.IsInf(x, 1): + b = posInf + case math.IsInf(x, -1): + b = negInf + case math.IsNaN(x): + b = nan + } + if b != nil { + _, err := w.Write(b) + return err + } + // Other values are handled below. + } + + // We don't attempt to serialise every possible value type; only those + // that can occur in protocol buffers. + switch v.Kind() { + case reflect.Slice: + // Should only be a []byte; repeated fields are handled in writeStruct. + if err := writeString(w, string(v.Bytes())); err != nil { + return err + } + case reflect.String: + if err := writeString(w, v.String()); err != nil { + return err + } + case reflect.Struct: + // Required/optional group/message. + var bra, ket byte = '<', '>' + if props != nil && props.Wire == "group" { + bra, ket = '{', '}' + } + if err := w.WriteByte(bra); err != nil { + return err + } + if !w.compact { + if err := w.WriteByte('\n'); err != nil { + return err + } + } + w.indent() + if v.CanAddr() { + // Calling v.Interface on a struct causes the reflect package to + // copy the entire struct. This is racy with the new Marshaler + // since we atomically update the XXX_sizecache. + // + // Thus, we retrieve a pointer to the struct if possible to avoid + // a race since v.Interface on the pointer doesn't copy the struct. + // + // If v is not addressable, then we are not worried about a race + // since it implies that the binary Marshaler cannot possibly be + // mutating this value. + v = v.Addr() + } + if etm, ok := v.Interface().(encoding.TextMarshaler); ok { + text, err := etm.MarshalText() + if err != nil { + return err + } + if _, err = w.Write(text); err != nil { + return err + } + } else { + if v.Kind() == reflect.Ptr { + v = v.Elem() + } + if err := tm.writeStruct(w, v); err != nil { + return err + } + } + w.unindent() + if err := w.WriteByte(ket); err != nil { + return err + } + default: + _, err := fmt.Fprint(w, v.Interface()) + return err + } + return nil +} + +// equivalent to C's isprint. +func isprint(c byte) bool { + return c >= 0x20 && c < 0x7f +} + +// writeString writes a string in the protocol buffer text format. +// It is similar to strconv.Quote except we don't use Go escape sequences, +// we treat the string as a byte sequence, and we use octal escapes. +// These differences are to maintain interoperability with the other +// languages' implementations of the text format. +func writeString(w *textWriter, s string) error { + // use WriteByte here to get any needed indent + if err := w.WriteByte('"'); err != nil { + return err + } + // Loop over the bytes, not the runes. + for i := 0; i < len(s); i++ { + var err error + // Divergence from C++: we don't escape apostrophes. + // There's no need to escape them, and the C++ parser + // copes with a naked apostrophe. + switch c := s[i]; c { + case '\n': + _, err = w.w.Write(backslashN) + case '\r': + _, err = w.w.Write(backslashR) + case '\t': + _, err = w.w.Write(backslashT) + case '"': + _, err = w.w.Write(backslashDQ) + case '\\': + _, err = w.w.Write(backslashBS) + default: + if isprint(c) { + err = w.w.WriteByte(c) + } else { + _, err = fmt.Fprintf(w.w, "\\%03o", c) + } + } + if err != nil { + return err + } + } + return w.WriteByte('"') +} + +func writeUnknownStruct(w *textWriter, data []byte) (err error) { + if !w.compact { + if _, err := fmt.Fprintf(w, "/* %d unknown bytes */\n", len(data)); err != nil { + return err + } + } + b := NewBuffer(data) + for b.index < len(b.buf) { + x, err := b.DecodeVarint() + if err != nil { + _, err := fmt.Fprintf(w, "/* %v */\n", err) + return err + } + wire, tag := x&7, x>>3 + if wire == WireEndGroup { + w.unindent() + if _, err := w.Write(endBraceNewline); err != nil { + return err + } + continue + } + if _, err := fmt.Fprint(w, tag); err != nil { + return err + } + if wire != WireStartGroup { + if err := w.WriteByte(':'); err != nil { + return err + } + } + if !w.compact || wire == WireStartGroup { + if err := w.WriteByte(' '); err != nil { + return err + } + } + switch wire { + case WireBytes: + buf, e := b.DecodeRawBytes(false) + if e == nil { + _, err = fmt.Fprintf(w, "%q", buf) + } else { + _, err = fmt.Fprintf(w, "/* %v */", e) + } + case WireFixed32: + x, err = b.DecodeFixed32() + err = writeUnknownInt(w, x, err) + case WireFixed64: + x, err = b.DecodeFixed64() + err = writeUnknownInt(w, x, err) + case WireStartGroup: + err = w.WriteByte('{') + w.indent() + case WireVarint: + x, err = b.DecodeVarint() + err = writeUnknownInt(w, x, err) + default: + _, err = fmt.Fprintf(w, "/* unknown wire type %d */", wire) + } + if err != nil { + return err + } + if err = w.WriteByte('\n'); err != nil { + return err + } + } + return nil +} + +func writeUnknownInt(w *textWriter, x uint64, err error) error { + if err == nil { + _, err = fmt.Fprint(w, x) + } else { + _, err = fmt.Fprintf(w, "/* %v */", err) + } + return err +} + +type int32Slice []int32 + +func (s int32Slice) Len() int { return len(s) } +func (s int32Slice) Less(i, j int) bool { return s[i] < s[j] } +func (s int32Slice) Swap(i, j int) { s[i], s[j] = s[j], s[i] } + +// writeExtensions writes all the extensions in pv. +// pv is assumed to be a pointer to a protocol message struct that is extendable. +func (tm *TextMarshaler) writeExtensions(w *textWriter, pv reflect.Value) error { + emap := extensionMaps[pv.Type().Elem()] + ep, _ := extendable(pv.Interface()) + + // Order the extensions by ID. + // This isn't strictly necessary, but it will give us + // canonical output, which will also make testing easier. + m, mu := ep.extensionsRead() + if m == nil { + return nil + } + mu.Lock() + ids := make([]int32, 0, len(m)) + for id := range m { + ids = append(ids, id) + } + sort.Sort(int32Slice(ids)) + mu.Unlock() + + for _, extNum := range ids { + ext := m[extNum] + var desc *ExtensionDesc + if emap != nil { + desc = emap[extNum] + } + if desc == nil { + // Unknown extension. + if err := writeUnknownStruct(w, ext.enc); err != nil { + return err + } + continue + } + + pb, err := GetExtension(ep, desc) + if err != nil { + return fmt.Errorf("failed getting extension: %v", err) + } + + // Repeated extensions will appear as a slice. + if !desc.repeated() { + if err := tm.writeExtension(w, desc.Name, pb); err != nil { + return err + } + } else { + v := reflect.ValueOf(pb) + for i := 0; i < v.Len(); i++ { + if err := tm.writeExtension(w, desc.Name, v.Index(i).Interface()); err != nil { + return err + } + } + } + } + return nil +} + +func (tm *TextMarshaler) writeExtension(w *textWriter, name string, pb interface{}) error { + if _, err := fmt.Fprintf(w, "[%s]:", name); err != nil { + return err + } + if !w.compact { + if err := w.WriteByte(' '); err != nil { + return err + } + } + if err := tm.writeAny(w, reflect.ValueOf(pb), nil); err != nil { + return err + } + if err := w.WriteByte('\n'); err != nil { + return err + } + return nil +} + +func (w *textWriter) writeIndent() { + if !w.complete { + return + } + remain := w.ind * 2 + for remain > 0 { + n := remain + if n > len(spaces) { + n = len(spaces) + } + w.w.Write(spaces[:n]) + remain -= n + } + w.complete = false +} + +// TextMarshaler is a configurable text format marshaler. +type TextMarshaler struct { + Compact bool // use compact text format (one line). + ExpandAny bool // expand google.protobuf.Any messages of known types +} + +// Marshal writes a given protocol buffer in text format. +// The only errors returned are from w. +func (tm *TextMarshaler) Marshal(w io.Writer, pb Message) error { + val := reflect.ValueOf(pb) + if pb == nil || val.IsNil() { + w.Write([]byte("")) + return nil + } + var bw *bufio.Writer + ww, ok := w.(writer) + if !ok { + bw = bufio.NewWriter(w) + ww = bw + } + aw := &textWriter{ + w: ww, + complete: true, + compact: tm.Compact, + } + + if etm, ok := pb.(encoding.TextMarshaler); ok { + text, err := etm.MarshalText() + if err != nil { + return err + } + if _, err = aw.Write(text); err != nil { + return err + } + if bw != nil { + return bw.Flush() + } + return nil + } + // Dereference the received pointer so we don't have outer < and >. + v := reflect.Indirect(val) + if err := tm.writeStruct(aw, v); err != nil { + return err + } + if bw != nil { + return bw.Flush() + } + return nil +} + +// Text is the same as Marshal, but returns the string directly. +func (tm *TextMarshaler) Text(pb Message) string { + var buf bytes.Buffer + tm.Marshal(&buf, pb) + return buf.String() +} + +var ( + defaultTextMarshaler = TextMarshaler{} + compactTextMarshaler = TextMarshaler{Compact: true} +) + +// TODO: consider removing some of the Marshal functions below. + +// MarshalText writes a given protocol buffer in text format. +// The only errors returned are from w. +func MarshalText(w io.Writer, pb Message) error { return defaultTextMarshaler.Marshal(w, pb) } + +// MarshalTextString is the same as MarshalText, but returns the string directly. +func MarshalTextString(pb Message) string { return defaultTextMarshaler.Text(pb) } + +// CompactText writes a given protocol buffer in compact text format (one line). +func CompactText(w io.Writer, pb Message) error { return compactTextMarshaler.Marshal(w, pb) } + +// CompactTextString is the same as CompactText, but returns the string directly. +func CompactTextString(pb Message) string { return compactTextMarshaler.Text(pb) } diff --git a/vendor/github.com/golang/protobuf/proto/text_parser.go b/vendor/github.com/golang/protobuf/proto/text_parser.go new file mode 100644 index 00000000..0685bae3 --- /dev/null +++ b/vendor/github.com/golang/protobuf/proto/text_parser.go @@ -0,0 +1,880 @@ +// Go support for Protocol Buffers - Google's data interchange format +// +// Copyright 2010 The Go Authors. All rights reserved. +// https://github.com/golang/protobuf +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package proto + +// Functions for parsing the Text protocol buffer format. +// TODO: message sets. + +import ( + "encoding" + "errors" + "fmt" + "reflect" + "strconv" + "strings" + "unicode/utf8" +) + +// Error string emitted when deserializing Any and fields are already set +const anyRepeatedlyUnpacked = "Any message unpacked multiple times, or %q already set" + +type ParseError struct { + Message string + Line int // 1-based line number + Offset int // 0-based byte offset from start of input +} + +func (p *ParseError) Error() string { + if p.Line == 1 { + // show offset only for first line + return fmt.Sprintf("line 1.%d: %v", p.Offset, p.Message) + } + return fmt.Sprintf("line %d: %v", p.Line, p.Message) +} + +type token struct { + value string + err *ParseError + line int // line number + offset int // byte number from start of input, not start of line + unquoted string // the unquoted version of value, if it was a quoted string +} + +func (t *token) String() string { + if t.err == nil { + return fmt.Sprintf("%q (line=%d, offset=%d)", t.value, t.line, t.offset) + } + return fmt.Sprintf("parse error: %v", t.err) +} + +type textParser struct { + s string // remaining input + done bool // whether the parsing is finished (success or error) + backed bool // whether back() was called + offset, line int + cur token +} + +func newTextParser(s string) *textParser { + p := new(textParser) + p.s = s + p.line = 1 + p.cur.line = 1 + return p +} + +func (p *textParser) errorf(format string, a ...interface{}) *ParseError { + pe := &ParseError{fmt.Sprintf(format, a...), p.cur.line, p.cur.offset} + p.cur.err = pe + p.done = true + return pe +} + +// Numbers and identifiers are matched by [-+._A-Za-z0-9] +func isIdentOrNumberChar(c byte) bool { + switch { + case 'A' <= c && c <= 'Z', 'a' <= c && c <= 'z': + return true + case '0' <= c && c <= '9': + return true + } + switch c { + case '-', '+', '.', '_': + return true + } + return false +} + +func isWhitespace(c byte) bool { + switch c { + case ' ', '\t', '\n', '\r': + return true + } + return false +} + +func isQuote(c byte) bool { + switch c { + case '"', '\'': + return true + } + return false +} + +func (p *textParser) skipWhitespace() { + i := 0 + for i < len(p.s) && (isWhitespace(p.s[i]) || p.s[i] == '#') { + if p.s[i] == '#' { + // comment; skip to end of line or input + for i < len(p.s) && p.s[i] != '\n' { + i++ + } + if i == len(p.s) { + break + } + } + if p.s[i] == '\n' { + p.line++ + } + i++ + } + p.offset += i + p.s = p.s[i:len(p.s)] + if len(p.s) == 0 { + p.done = true + } +} + +func (p *textParser) advance() { + // Skip whitespace + p.skipWhitespace() + if p.done { + return + } + + // Start of non-whitespace + p.cur.err = nil + p.cur.offset, p.cur.line = p.offset, p.line + p.cur.unquoted = "" + switch p.s[0] { + case '<', '>', '{', '}', ':', '[', ']', ';', ',', '/': + // Single symbol + p.cur.value, p.s = p.s[0:1], p.s[1:len(p.s)] + case '"', '\'': + // Quoted string + i := 1 + for i < len(p.s) && p.s[i] != p.s[0] && p.s[i] != '\n' { + if p.s[i] == '\\' && i+1 < len(p.s) { + // skip escaped char + i++ + } + i++ + } + if i >= len(p.s) || p.s[i] != p.s[0] { + p.errorf("unmatched quote") + return + } + unq, err := unquoteC(p.s[1:i], rune(p.s[0])) + if err != nil { + p.errorf("invalid quoted string %s: %v", p.s[0:i+1], err) + return + } + p.cur.value, p.s = p.s[0:i+1], p.s[i+1:len(p.s)] + p.cur.unquoted = unq + default: + i := 0 + for i < len(p.s) && isIdentOrNumberChar(p.s[i]) { + i++ + } + if i == 0 { + p.errorf("unexpected byte %#x", p.s[0]) + return + } + p.cur.value, p.s = p.s[0:i], p.s[i:len(p.s)] + } + p.offset += len(p.cur.value) +} + +var ( + errBadUTF8 = errors.New("proto: bad UTF-8") +) + +func unquoteC(s string, quote rune) (string, error) { + // This is based on C++'s tokenizer.cc. + // Despite its name, this is *not* parsing C syntax. + // For instance, "\0" is an invalid quoted string. + + // Avoid allocation in trivial cases. + simple := true + for _, r := range s { + if r == '\\' || r == quote { + simple = false + break + } + } + if simple { + return s, nil + } + + buf := make([]byte, 0, 3*len(s)/2) + for len(s) > 0 { + r, n := utf8.DecodeRuneInString(s) + if r == utf8.RuneError && n == 1 { + return "", errBadUTF8 + } + s = s[n:] + if r != '\\' { + if r < utf8.RuneSelf { + buf = append(buf, byte(r)) + } else { + buf = append(buf, string(r)...) + } + continue + } + + ch, tail, err := unescape(s) + if err != nil { + return "", err + } + buf = append(buf, ch...) + s = tail + } + return string(buf), nil +} + +func unescape(s string) (ch string, tail string, err error) { + r, n := utf8.DecodeRuneInString(s) + if r == utf8.RuneError && n == 1 { + return "", "", errBadUTF8 + } + s = s[n:] + switch r { + case 'a': + return "\a", s, nil + case 'b': + return "\b", s, nil + case 'f': + return "\f", s, nil + case 'n': + return "\n", s, nil + case 'r': + return "\r", s, nil + case 't': + return "\t", s, nil + case 'v': + return "\v", s, nil + case '?': + return "?", s, nil // trigraph workaround + case '\'', '"', '\\': + return string(r), s, nil + case '0', '1', '2', '3', '4', '5', '6', '7': + if len(s) < 2 { + return "", "", fmt.Errorf(`\%c requires 2 following digits`, r) + } + ss := string(r) + s[:2] + s = s[2:] + i, err := strconv.ParseUint(ss, 8, 8) + if err != nil { + return "", "", fmt.Errorf(`\%s contains non-octal digits`, ss) + } + return string([]byte{byte(i)}), s, nil + case 'x', 'X', 'u', 'U': + var n int + switch r { + case 'x', 'X': + n = 2 + case 'u': + n = 4 + case 'U': + n = 8 + } + if len(s) < n { + return "", "", fmt.Errorf(`\%c requires %d following digits`, r, n) + } + ss := s[:n] + s = s[n:] + i, err := strconv.ParseUint(ss, 16, 64) + if err != nil { + return "", "", fmt.Errorf(`\%c%s contains non-hexadecimal digits`, r, ss) + } + if r == 'x' || r == 'X' { + return string([]byte{byte(i)}), s, nil + } + if i > utf8.MaxRune { + return "", "", fmt.Errorf(`\%c%s is not a valid Unicode code point`, r, ss) + } + return string(i), s, nil + } + return "", "", fmt.Errorf(`unknown escape \%c`, r) +} + +// Back off the parser by one token. Can only be done between calls to next(). +// It makes the next advance() a no-op. +func (p *textParser) back() { p.backed = true } + +// Advances the parser and returns the new current token. +func (p *textParser) next() *token { + if p.backed || p.done { + p.backed = false + return &p.cur + } + p.advance() + if p.done { + p.cur.value = "" + } else if len(p.cur.value) > 0 && isQuote(p.cur.value[0]) { + // Look for multiple quoted strings separated by whitespace, + // and concatenate them. + cat := p.cur + for { + p.skipWhitespace() + if p.done || !isQuote(p.s[0]) { + break + } + p.advance() + if p.cur.err != nil { + return &p.cur + } + cat.value += " " + p.cur.value + cat.unquoted += p.cur.unquoted + } + p.done = false // parser may have seen EOF, but we want to return cat + p.cur = cat + } + return &p.cur +} + +func (p *textParser) consumeToken(s string) error { + tok := p.next() + if tok.err != nil { + return tok.err + } + if tok.value != s { + p.back() + return p.errorf("expected %q, found %q", s, tok.value) + } + return nil +} + +// Return a RequiredNotSetError indicating which required field was not set. +func (p *textParser) missingRequiredFieldError(sv reflect.Value) *RequiredNotSetError { + st := sv.Type() + sprops := GetProperties(st) + for i := 0; i < st.NumField(); i++ { + if !isNil(sv.Field(i)) { + continue + } + + props := sprops.Prop[i] + if props.Required { + return &RequiredNotSetError{fmt.Sprintf("%v.%v", st, props.OrigName)} + } + } + return &RequiredNotSetError{fmt.Sprintf("%v.", st)} // should not happen +} + +// Returns the index in the struct for the named field, as well as the parsed tag properties. +func structFieldByName(sprops *StructProperties, name string) (int, *Properties, bool) { + i, ok := sprops.decoderOrigNames[name] + if ok { + return i, sprops.Prop[i], true + } + return -1, nil, false +} + +// Consume a ':' from the input stream (if the next token is a colon), +// returning an error if a colon is needed but not present. +func (p *textParser) checkForColon(props *Properties, typ reflect.Type) *ParseError { + tok := p.next() + if tok.err != nil { + return tok.err + } + if tok.value != ":" { + // Colon is optional when the field is a group or message. + needColon := true + switch props.Wire { + case "group": + needColon = false + case "bytes": + // A "bytes" field is either a message, a string, or a repeated field; + // those three become *T, *string and []T respectively, so we can check for + // this field being a pointer to a non-string. + if typ.Kind() == reflect.Ptr { + // *T or *string + if typ.Elem().Kind() == reflect.String { + break + } + } else if typ.Kind() == reflect.Slice { + // []T or []*T + if typ.Elem().Kind() != reflect.Ptr { + break + } + } else if typ.Kind() == reflect.String { + // The proto3 exception is for a string field, + // which requires a colon. + break + } + needColon = false + } + if needColon { + return p.errorf("expected ':', found %q", tok.value) + } + p.back() + } + return nil +} + +func (p *textParser) readStruct(sv reflect.Value, terminator string) error { + st := sv.Type() + sprops := GetProperties(st) + reqCount := sprops.reqCount + var reqFieldErr error + fieldSet := make(map[string]bool) + // A struct is a sequence of "name: value", terminated by one of + // '>' or '}', or the end of the input. A name may also be + // "[extension]" or "[type/url]". + // + // The whole struct can also be an expanded Any message, like: + // [type/url] < ... struct contents ... > + for { + tok := p.next() + if tok.err != nil { + return tok.err + } + if tok.value == terminator { + break + } + if tok.value == "[" { + // Looks like an extension or an Any. + // + // TODO: Check whether we need to handle + // namespace rooted names (e.g. ".something.Foo"). + extName, err := p.consumeExtName() + if err != nil { + return err + } + + if s := strings.LastIndex(extName, "/"); s >= 0 { + // If it contains a slash, it's an Any type URL. + messageName := extName[s+1:] + mt := MessageType(messageName) + if mt == nil { + return p.errorf("unrecognized message %q in google.protobuf.Any", messageName) + } + tok = p.next() + if tok.err != nil { + return tok.err + } + // consume an optional colon + if tok.value == ":" { + tok = p.next() + if tok.err != nil { + return tok.err + } + } + var terminator string + switch tok.value { + case "<": + terminator = ">" + case "{": + terminator = "}" + default: + return p.errorf("expected '{' or '<', found %q", tok.value) + } + v := reflect.New(mt.Elem()) + if pe := p.readStruct(v.Elem(), terminator); pe != nil { + return pe + } + b, err := Marshal(v.Interface().(Message)) + if err != nil { + return p.errorf("failed to marshal message of type %q: %v", messageName, err) + } + if fieldSet["type_url"] { + return p.errorf(anyRepeatedlyUnpacked, "type_url") + } + if fieldSet["value"] { + return p.errorf(anyRepeatedlyUnpacked, "value") + } + sv.FieldByName("TypeUrl").SetString(extName) + sv.FieldByName("Value").SetBytes(b) + fieldSet["type_url"] = true + fieldSet["value"] = true + continue + } + + var desc *ExtensionDesc + // This could be faster, but it's functional. + // TODO: Do something smarter than a linear scan. + for _, d := range RegisteredExtensions(reflect.New(st).Interface().(Message)) { + if d.Name == extName { + desc = d + break + } + } + if desc == nil { + return p.errorf("unrecognized extension %q", extName) + } + + props := &Properties{} + props.Parse(desc.Tag) + + typ := reflect.TypeOf(desc.ExtensionType) + if err := p.checkForColon(props, typ); err != nil { + return err + } + + rep := desc.repeated() + + // Read the extension structure, and set it in + // the value we're constructing. + var ext reflect.Value + if !rep { + ext = reflect.New(typ).Elem() + } else { + ext = reflect.New(typ.Elem()).Elem() + } + if err := p.readAny(ext, props); err != nil { + if _, ok := err.(*RequiredNotSetError); !ok { + return err + } + reqFieldErr = err + } + ep := sv.Addr().Interface().(Message) + if !rep { + SetExtension(ep, desc, ext.Interface()) + } else { + old, err := GetExtension(ep, desc) + var sl reflect.Value + if err == nil { + sl = reflect.ValueOf(old) // existing slice + } else { + sl = reflect.MakeSlice(typ, 0, 1) + } + sl = reflect.Append(sl, ext) + SetExtension(ep, desc, sl.Interface()) + } + if err := p.consumeOptionalSeparator(); err != nil { + return err + } + continue + } + + // This is a normal, non-extension field. + name := tok.value + var dst reflect.Value + fi, props, ok := structFieldByName(sprops, name) + if ok { + dst = sv.Field(fi) + } else if oop, ok := sprops.OneofTypes[name]; ok { + // It is a oneof. + props = oop.Prop + nv := reflect.New(oop.Type.Elem()) + dst = nv.Elem().Field(0) + field := sv.Field(oop.Field) + if !field.IsNil() { + return p.errorf("field '%s' would overwrite already parsed oneof '%s'", name, sv.Type().Field(oop.Field).Name) + } + field.Set(nv) + } + if !dst.IsValid() { + return p.errorf("unknown field name %q in %v", name, st) + } + + if dst.Kind() == reflect.Map { + // Consume any colon. + if err := p.checkForColon(props, dst.Type()); err != nil { + return err + } + + // Construct the map if it doesn't already exist. + if dst.IsNil() { + dst.Set(reflect.MakeMap(dst.Type())) + } + key := reflect.New(dst.Type().Key()).Elem() + val := reflect.New(dst.Type().Elem()).Elem() + + // The map entry should be this sequence of tokens: + // < key : KEY value : VALUE > + // However, implementations may omit key or value, and technically + // we should support them in any order. See b/28924776 for a time + // this went wrong. + + tok := p.next() + var terminator string + switch tok.value { + case "<": + terminator = ">" + case "{": + terminator = "}" + default: + return p.errorf("expected '{' or '<', found %q", tok.value) + } + for { + tok := p.next() + if tok.err != nil { + return tok.err + } + if tok.value == terminator { + break + } + switch tok.value { + case "key": + if err := p.consumeToken(":"); err != nil { + return err + } + if err := p.readAny(key, props.mkeyprop); err != nil { + return err + } + if err := p.consumeOptionalSeparator(); err != nil { + return err + } + case "value": + if err := p.checkForColon(props.mvalprop, dst.Type().Elem()); err != nil { + return err + } + if err := p.readAny(val, props.mvalprop); err != nil { + return err + } + if err := p.consumeOptionalSeparator(); err != nil { + return err + } + default: + p.back() + return p.errorf(`expected "key", "value", or %q, found %q`, terminator, tok.value) + } + } + + dst.SetMapIndex(key, val) + continue + } + + // Check that it's not already set if it's not a repeated field. + if !props.Repeated && fieldSet[name] { + return p.errorf("non-repeated field %q was repeated", name) + } + + if err := p.checkForColon(props, dst.Type()); err != nil { + return err + } + + // Parse into the field. + fieldSet[name] = true + if err := p.readAny(dst, props); err != nil { + if _, ok := err.(*RequiredNotSetError); !ok { + return err + } + reqFieldErr = err + } + if props.Required { + reqCount-- + } + + if err := p.consumeOptionalSeparator(); err != nil { + return err + } + + } + + if reqCount > 0 { + return p.missingRequiredFieldError(sv) + } + return reqFieldErr +} + +// consumeExtName consumes extension name or expanded Any type URL and the +// following ']'. It returns the name or URL consumed. +func (p *textParser) consumeExtName() (string, error) { + tok := p.next() + if tok.err != nil { + return "", tok.err + } + + // If extension name or type url is quoted, it's a single token. + if len(tok.value) > 2 && isQuote(tok.value[0]) && tok.value[len(tok.value)-1] == tok.value[0] { + name, err := unquoteC(tok.value[1:len(tok.value)-1], rune(tok.value[0])) + if err != nil { + return "", err + } + return name, p.consumeToken("]") + } + + // Consume everything up to "]" + var parts []string + for tok.value != "]" { + parts = append(parts, tok.value) + tok = p.next() + if tok.err != nil { + return "", p.errorf("unrecognized type_url or extension name: %s", tok.err) + } + if p.done && tok.value != "]" { + return "", p.errorf("unclosed type_url or extension name") + } + } + return strings.Join(parts, ""), nil +} + +// consumeOptionalSeparator consumes an optional semicolon or comma. +// It is used in readStruct to provide backward compatibility. +func (p *textParser) consumeOptionalSeparator() error { + tok := p.next() + if tok.err != nil { + return tok.err + } + if tok.value != ";" && tok.value != "," { + p.back() + } + return nil +} + +func (p *textParser) readAny(v reflect.Value, props *Properties) error { + tok := p.next() + if tok.err != nil { + return tok.err + } + if tok.value == "" { + return p.errorf("unexpected EOF") + } + + switch fv := v; fv.Kind() { + case reflect.Slice: + at := v.Type() + if at.Elem().Kind() == reflect.Uint8 { + // Special case for []byte + if tok.value[0] != '"' && tok.value[0] != '\'' { + // Deliberately written out here, as the error after + // this switch statement would write "invalid []byte: ...", + // which is not as user-friendly. + return p.errorf("invalid string: %v", tok.value) + } + bytes := []byte(tok.unquoted) + fv.Set(reflect.ValueOf(bytes)) + return nil + } + // Repeated field. + if tok.value == "[" { + // Repeated field with list notation, like [1,2,3]. + for { + fv.Set(reflect.Append(fv, reflect.New(at.Elem()).Elem())) + err := p.readAny(fv.Index(fv.Len()-1), props) + if err != nil { + return err + } + tok := p.next() + if tok.err != nil { + return tok.err + } + if tok.value == "]" { + break + } + if tok.value != "," { + return p.errorf("Expected ']' or ',' found %q", tok.value) + } + } + return nil + } + // One value of the repeated field. + p.back() + fv.Set(reflect.Append(fv, reflect.New(at.Elem()).Elem())) + return p.readAny(fv.Index(fv.Len()-1), props) + case reflect.Bool: + // true/1/t/True or false/f/0/False. + switch tok.value { + case "true", "1", "t", "True": + fv.SetBool(true) + return nil + case "false", "0", "f", "False": + fv.SetBool(false) + return nil + } + case reflect.Float32, reflect.Float64: + v := tok.value + // Ignore 'f' for compatibility with output generated by C++, but don't + // remove 'f' when the value is "-inf" or "inf". + if strings.HasSuffix(v, "f") && tok.value != "-inf" && tok.value != "inf" { + v = v[:len(v)-1] + } + if f, err := strconv.ParseFloat(v, fv.Type().Bits()); err == nil { + fv.SetFloat(f) + return nil + } + case reflect.Int32: + if x, err := strconv.ParseInt(tok.value, 0, 32); err == nil { + fv.SetInt(x) + return nil + } + + if len(props.Enum) == 0 { + break + } + m, ok := enumValueMaps[props.Enum] + if !ok { + break + } + x, ok := m[tok.value] + if !ok { + break + } + fv.SetInt(int64(x)) + return nil + case reflect.Int64: + if x, err := strconv.ParseInt(tok.value, 0, 64); err == nil { + fv.SetInt(x) + return nil + } + + case reflect.Ptr: + // A basic field (indirected through pointer), or a repeated message/group + p.back() + fv.Set(reflect.New(fv.Type().Elem())) + return p.readAny(fv.Elem(), props) + case reflect.String: + if tok.value[0] == '"' || tok.value[0] == '\'' { + fv.SetString(tok.unquoted) + return nil + } + case reflect.Struct: + var terminator string + switch tok.value { + case "{": + terminator = "}" + case "<": + terminator = ">" + default: + return p.errorf("expected '{' or '<', found %q", tok.value) + } + // TODO: Handle nested messages which implement encoding.TextUnmarshaler. + return p.readStruct(fv, terminator) + case reflect.Uint32: + if x, err := strconv.ParseUint(tok.value, 0, 32); err == nil { + fv.SetUint(uint64(x)) + return nil + } + case reflect.Uint64: + if x, err := strconv.ParseUint(tok.value, 0, 64); err == nil { + fv.SetUint(x) + return nil + } + } + return p.errorf("invalid %v: %v", v.Type(), tok.value) +} + +// UnmarshalText reads a protocol buffer in Text format. UnmarshalText resets pb +// before starting to unmarshal, so any existing data in pb is always removed. +// If a required field is not set and no other error occurs, +// UnmarshalText returns *RequiredNotSetError. +func UnmarshalText(s string, pb Message) error { + if um, ok := pb.(encoding.TextUnmarshaler); ok { + return um.UnmarshalText([]byte(s)) + } + pb.Reset() + v := reflect.ValueOf(pb) + return newTextParser(s).readStruct(v.Elem(), "") +} diff --git a/vendor/github.com/golang/protobuf/ptypes/any.go b/vendor/github.com/golang/protobuf/ptypes/any.go new file mode 100644 index 00000000..70276e8f --- /dev/null +++ b/vendor/github.com/golang/protobuf/ptypes/any.go @@ -0,0 +1,141 @@ +// Go support for Protocol Buffers - Google's data interchange format +// +// Copyright 2016 The Go Authors. All rights reserved. +// https://github.com/golang/protobuf +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package ptypes + +// This file implements functions to marshal proto.Message to/from +// google.protobuf.Any message. + +import ( + "fmt" + "reflect" + "strings" + + "github.com/golang/protobuf/proto" + "github.com/golang/protobuf/ptypes/any" +) + +const googleApis = "type.googleapis.com/" + +// AnyMessageName returns the name of the message contained in a google.protobuf.Any message. +// +// Note that regular type assertions should be done using the Is +// function. AnyMessageName is provided for less common use cases like filtering a +// sequence of Any messages based on a set of allowed message type names. +func AnyMessageName(any *any.Any) (string, error) { + if any == nil { + return "", fmt.Errorf("message is nil") + } + slash := strings.LastIndex(any.TypeUrl, "/") + if slash < 0 { + return "", fmt.Errorf("message type url %q is invalid", any.TypeUrl) + } + return any.TypeUrl[slash+1:], nil +} + +// MarshalAny takes the protocol buffer and encodes it into google.protobuf.Any. +func MarshalAny(pb proto.Message) (*any.Any, error) { + value, err := proto.Marshal(pb) + if err != nil { + return nil, err + } + return &any.Any{TypeUrl: googleApis + proto.MessageName(pb), Value: value}, nil +} + +// DynamicAny is a value that can be passed to UnmarshalAny to automatically +// allocate a proto.Message for the type specified in a google.protobuf.Any +// message. The allocated message is stored in the embedded proto.Message. +// +// Example: +// +// var x ptypes.DynamicAny +// if err := ptypes.UnmarshalAny(a, &x); err != nil { ... } +// fmt.Printf("unmarshaled message: %v", x.Message) +type DynamicAny struct { + proto.Message +} + +// Empty returns a new proto.Message of the type specified in a +// google.protobuf.Any message. It returns an error if corresponding message +// type isn't linked in. +func Empty(any *any.Any) (proto.Message, error) { + aname, err := AnyMessageName(any) + if err != nil { + return nil, err + } + + t := proto.MessageType(aname) + if t == nil { + return nil, fmt.Errorf("any: message type %q isn't linked in", aname) + } + return reflect.New(t.Elem()).Interface().(proto.Message), nil +} + +// UnmarshalAny parses the protocol buffer representation in a google.protobuf.Any +// message and places the decoded result in pb. It returns an error if type of +// contents of Any message does not match type of pb message. +// +// pb can be a proto.Message, or a *DynamicAny. +func UnmarshalAny(any *any.Any, pb proto.Message) error { + if d, ok := pb.(*DynamicAny); ok { + if d.Message == nil { + var err error + d.Message, err = Empty(any) + if err != nil { + return err + } + } + return UnmarshalAny(any, d.Message) + } + + aname, err := AnyMessageName(any) + if err != nil { + return err + } + + mname := proto.MessageName(pb) + if aname != mname { + return fmt.Errorf("mismatched message type: got %q want %q", aname, mname) + } + return proto.Unmarshal(any.Value, pb) +} + +// Is returns true if any value contains a given message type. +func Is(any *any.Any, pb proto.Message) bool { + // The following is equivalent to AnyMessageName(any) == proto.MessageName(pb), + // but it avoids scanning TypeUrl for the slash. + if any == nil { + return false + } + name := proto.MessageName(pb) + prefix := len(any.TypeUrl) - len(name) + return prefix >= 1 && any.TypeUrl[prefix-1] == '/' && any.TypeUrl[prefix:] == name +} diff --git a/vendor/github.com/golang/protobuf/ptypes/any/any.pb.go b/vendor/github.com/golang/protobuf/ptypes/any/any.pb.go new file mode 100644 index 00000000..f67edc7d --- /dev/null +++ b/vendor/github.com/golang/protobuf/ptypes/any/any.pb.go @@ -0,0 +1,191 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// source: google/protobuf/any.proto + +package any // import "github.com/golang/protobuf/ptypes/any" + +import proto "github.com/golang/protobuf/proto" +import fmt "fmt" +import math "math" + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package + +// `Any` contains an arbitrary serialized protocol buffer message along with a +// URL that describes the type of the serialized message. +// +// Protobuf library provides support to pack/unpack Any values in the form +// of utility functions or additional generated methods of the Any type. +// +// Example 1: Pack and unpack a message in C++. +// +// Foo foo = ...; +// Any any; +// any.PackFrom(foo); +// ... +// if (any.UnpackTo(&foo)) { +// ... +// } +// +// Example 2: Pack and unpack a message in Java. +// +// Foo foo = ...; +// Any any = Any.pack(foo); +// ... +// if (any.is(Foo.class)) { +// foo = any.unpack(Foo.class); +// } +// +// Example 3: Pack and unpack a message in Python. +// +// foo = Foo(...) +// any = Any() +// any.Pack(foo) +// ... +// if any.Is(Foo.DESCRIPTOR): +// any.Unpack(foo) +// ... +// +// Example 4: Pack and unpack a message in Go +// +// foo := &pb.Foo{...} +// any, err := ptypes.MarshalAny(foo) +// ... +// foo := &pb.Foo{} +// if err := ptypes.UnmarshalAny(any, foo); err != nil { +// ... +// } +// +// The pack methods provided by protobuf library will by default use +// 'type.googleapis.com/full.type.name' as the type URL and the unpack +// methods only use the fully qualified type name after the last '/' +// in the type URL, for example "foo.bar.com/x/y.z" will yield type +// name "y.z". +// +// +// JSON +// ==== +// The JSON representation of an `Any` value uses the regular +// representation of the deserialized, embedded message, with an +// additional field `@type` which contains the type URL. Example: +// +// package google.profile; +// message Person { +// string first_name = 1; +// string last_name = 2; +// } +// +// { +// "@type": "type.googleapis.com/google.profile.Person", +// "firstName": , +// "lastName": +// } +// +// If the embedded message type is well-known and has a custom JSON +// representation, that representation will be embedded adding a field +// `value` which holds the custom JSON in addition to the `@type` +// field. Example (for message [google.protobuf.Duration][]): +// +// { +// "@type": "type.googleapis.com/google.protobuf.Duration", +// "value": "1.212s" +// } +// +type Any struct { + // A URL/resource name whose content describes the type of the + // serialized protocol buffer message. + // + // For URLs which use the scheme `http`, `https`, or no scheme, the + // following restrictions and interpretations apply: + // + // * If no scheme is provided, `https` is assumed. + // * The last segment of the URL's path must represent the fully + // qualified name of the type (as in `path/google.protobuf.Duration`). + // The name should be in a canonical form (e.g., leading "." is + // not accepted). + // * An HTTP GET on the URL must yield a [google.protobuf.Type][] + // value in binary format, or produce an error. + // * Applications are allowed to cache lookup results based on the + // URL, or have them precompiled into a binary to avoid any + // lookup. Therefore, binary compatibility needs to be preserved + // on changes to types. (Use versioned type names to manage + // breaking changes.) + // + // Schemes other than `http`, `https` (or the empty scheme) might be + // used with implementation specific semantics. + // + TypeUrl string `protobuf:"bytes,1,opt,name=type_url,json=typeUrl" json:"type_url,omitempty"` + // Must be a valid serialized protocol buffer of the above specified type. + Value []byte `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *Any) Reset() { *m = Any{} } +func (m *Any) String() string { return proto.CompactTextString(m) } +func (*Any) ProtoMessage() {} +func (*Any) Descriptor() ([]byte, []int) { + return fileDescriptor_any_744b9ca530f228db, []int{0} +} +func (*Any) XXX_WellKnownType() string { return "Any" } +func (m *Any) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_Any.Unmarshal(m, b) +} +func (m *Any) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_Any.Marshal(b, m, deterministic) +} +func (dst *Any) XXX_Merge(src proto.Message) { + xxx_messageInfo_Any.Merge(dst, src) +} +func (m *Any) XXX_Size() int { + return xxx_messageInfo_Any.Size(m) +} +func (m *Any) XXX_DiscardUnknown() { + xxx_messageInfo_Any.DiscardUnknown(m) +} + +var xxx_messageInfo_Any proto.InternalMessageInfo + +func (m *Any) GetTypeUrl() string { + if m != nil { + return m.TypeUrl + } + return "" +} + +func (m *Any) GetValue() []byte { + if m != nil { + return m.Value + } + return nil +} + +func init() { + proto.RegisterType((*Any)(nil), "google.protobuf.Any") +} + +func init() { proto.RegisterFile("google/protobuf/any.proto", fileDescriptor_any_744b9ca530f228db) } + +var fileDescriptor_any_744b9ca530f228db = []byte{ + // 185 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x92, 0x4c, 0xcf, 0xcf, 0x4f, + 0xcf, 0x49, 0xd5, 0x2f, 0x28, 0xca, 0x2f, 0xc9, 0x4f, 0x2a, 0x4d, 0xd3, 0x4f, 0xcc, 0xab, 0xd4, + 0x03, 0x73, 0x84, 0xf8, 0x21, 0x52, 0x7a, 0x30, 0x29, 0x25, 0x33, 0x2e, 0x66, 0xc7, 0xbc, 0x4a, + 0x21, 0x49, 0x2e, 0x8e, 0x92, 0xca, 0x82, 0xd4, 0xf8, 0xd2, 0xa2, 0x1c, 0x09, 0x46, 0x05, 0x46, + 0x0d, 0xce, 0x20, 0x76, 0x10, 0x3f, 0xb4, 0x28, 0x47, 0x48, 0x84, 0x8b, 0xb5, 0x2c, 0x31, 0xa7, + 0x34, 0x55, 0x82, 0x49, 0x81, 0x51, 0x83, 0x27, 0x08, 0xc2, 0x71, 0xca, 0xe7, 0x12, 0x4e, 0xce, + 0xcf, 0xd5, 0x43, 0x33, 0xce, 0x89, 0xc3, 0x31, 0xaf, 0x32, 0x00, 0xc4, 0x09, 0x60, 0x8c, 0x52, + 0x4d, 0xcf, 0x2c, 0xc9, 0x28, 0x4d, 0xd2, 0x4b, 0xce, 0xcf, 0xd5, 0x4f, 0xcf, 0xcf, 0x49, 0xcc, + 0x4b, 0x47, 0xb8, 0xa8, 0x00, 0x64, 0x7a, 0x31, 0xc8, 0x61, 0x8b, 0x98, 0x98, 0xdd, 0x03, 0x9c, + 0x56, 0x31, 0xc9, 0xb9, 0x43, 0x8c, 0x0a, 0x80, 0x2a, 0xd1, 0x0b, 0x4f, 0xcd, 0xc9, 0xf1, 0xce, + 0xcb, 0x2f, 0xcf, 0x0b, 0x01, 0x29, 0x4d, 0x62, 0x03, 0xeb, 0x35, 0x06, 0x04, 0x00, 0x00, 0xff, + 0xff, 0x13, 0xf8, 0xe8, 0x42, 0xdd, 0x00, 0x00, 0x00, +} diff --git a/vendor/github.com/golang/protobuf/ptypes/any/any.proto b/vendor/github.com/golang/protobuf/ptypes/any/any.proto new file mode 100644 index 00000000..c7486676 --- /dev/null +++ b/vendor/github.com/golang/protobuf/ptypes/any/any.proto @@ -0,0 +1,149 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2008 Google Inc. All rights reserved. +// https://developers.google.com/protocol-buffers/ +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +syntax = "proto3"; + +package google.protobuf; + +option csharp_namespace = "Google.Protobuf.WellKnownTypes"; +option go_package = "github.com/golang/protobuf/ptypes/any"; +option java_package = "com.google.protobuf"; +option java_outer_classname = "AnyProto"; +option java_multiple_files = true; +option objc_class_prefix = "GPB"; + +// `Any` contains an arbitrary serialized protocol buffer message along with a +// URL that describes the type of the serialized message. +// +// Protobuf library provides support to pack/unpack Any values in the form +// of utility functions or additional generated methods of the Any type. +// +// Example 1: Pack and unpack a message in C++. +// +// Foo foo = ...; +// Any any; +// any.PackFrom(foo); +// ... +// if (any.UnpackTo(&foo)) { +// ... +// } +// +// Example 2: Pack and unpack a message in Java. +// +// Foo foo = ...; +// Any any = Any.pack(foo); +// ... +// if (any.is(Foo.class)) { +// foo = any.unpack(Foo.class); +// } +// +// Example 3: Pack and unpack a message in Python. +// +// foo = Foo(...) +// any = Any() +// any.Pack(foo) +// ... +// if any.Is(Foo.DESCRIPTOR): +// any.Unpack(foo) +// ... +// +// Example 4: Pack and unpack a message in Go +// +// foo := &pb.Foo{...} +// any, err := ptypes.MarshalAny(foo) +// ... +// foo := &pb.Foo{} +// if err := ptypes.UnmarshalAny(any, foo); err != nil { +// ... +// } +// +// The pack methods provided by protobuf library will by default use +// 'type.googleapis.com/full.type.name' as the type URL and the unpack +// methods only use the fully qualified type name after the last '/' +// in the type URL, for example "foo.bar.com/x/y.z" will yield type +// name "y.z". +// +// +// JSON +// ==== +// The JSON representation of an `Any` value uses the regular +// representation of the deserialized, embedded message, with an +// additional field `@type` which contains the type URL. Example: +// +// package google.profile; +// message Person { +// string first_name = 1; +// string last_name = 2; +// } +// +// { +// "@type": "type.googleapis.com/google.profile.Person", +// "firstName": , +// "lastName": +// } +// +// If the embedded message type is well-known and has a custom JSON +// representation, that representation will be embedded adding a field +// `value` which holds the custom JSON in addition to the `@type` +// field. Example (for message [google.protobuf.Duration][]): +// +// { +// "@type": "type.googleapis.com/google.protobuf.Duration", +// "value": "1.212s" +// } +// +message Any { + // A URL/resource name whose content describes the type of the + // serialized protocol buffer message. + // + // For URLs which use the scheme `http`, `https`, or no scheme, the + // following restrictions and interpretations apply: + // + // * If no scheme is provided, `https` is assumed. + // * The last segment of the URL's path must represent the fully + // qualified name of the type (as in `path/google.protobuf.Duration`). + // The name should be in a canonical form (e.g., leading "." is + // not accepted). + // * An HTTP GET on the URL must yield a [google.protobuf.Type][] + // value in binary format, or produce an error. + // * Applications are allowed to cache lookup results based on the + // URL, or have them precompiled into a binary to avoid any + // lookup. Therefore, binary compatibility needs to be preserved + // on changes to types. (Use versioned type names to manage + // breaking changes.) + // + // Schemes other than `http`, `https` (or the empty scheme) might be + // used with implementation specific semantics. + // + string type_url = 1; + + // Must be a valid serialized protocol buffer of the above specified type. + bytes value = 2; +} diff --git a/vendor/github.com/golang/protobuf/ptypes/doc.go b/vendor/github.com/golang/protobuf/ptypes/doc.go new file mode 100644 index 00000000..c0d595da --- /dev/null +++ b/vendor/github.com/golang/protobuf/ptypes/doc.go @@ -0,0 +1,35 @@ +// Go support for Protocol Buffers - Google's data interchange format +// +// Copyright 2016 The Go Authors. All rights reserved. +// https://github.com/golang/protobuf +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/* +Package ptypes contains code for interacting with well-known types. +*/ +package ptypes diff --git a/vendor/github.com/golang/protobuf/ptypes/duration.go b/vendor/github.com/golang/protobuf/ptypes/duration.go new file mode 100644 index 00000000..65cb0f8e --- /dev/null +++ b/vendor/github.com/golang/protobuf/ptypes/duration.go @@ -0,0 +1,102 @@ +// Go support for Protocol Buffers - Google's data interchange format +// +// Copyright 2016 The Go Authors. All rights reserved. +// https://github.com/golang/protobuf +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package ptypes + +// This file implements conversions between google.protobuf.Duration +// and time.Duration. + +import ( + "errors" + "fmt" + "time" + + durpb "github.com/golang/protobuf/ptypes/duration" +) + +const ( + // Range of a durpb.Duration in seconds, as specified in + // google/protobuf/duration.proto. This is about 10,000 years in seconds. + maxSeconds = int64(10000 * 365.25 * 24 * 60 * 60) + minSeconds = -maxSeconds +) + +// validateDuration determines whether the durpb.Duration is valid according to the +// definition in google/protobuf/duration.proto. A valid durpb.Duration +// may still be too large to fit into a time.Duration (the range of durpb.Duration +// is about 10,000 years, and the range of time.Duration is about 290). +func validateDuration(d *durpb.Duration) error { + if d == nil { + return errors.New("duration: nil Duration") + } + if d.Seconds < minSeconds || d.Seconds > maxSeconds { + return fmt.Errorf("duration: %v: seconds out of range", d) + } + if d.Nanos <= -1e9 || d.Nanos >= 1e9 { + return fmt.Errorf("duration: %v: nanos out of range", d) + } + // Seconds and Nanos must have the same sign, unless d.Nanos is zero. + if (d.Seconds < 0 && d.Nanos > 0) || (d.Seconds > 0 && d.Nanos < 0) { + return fmt.Errorf("duration: %v: seconds and nanos have different signs", d) + } + return nil +} + +// Duration converts a durpb.Duration to a time.Duration. Duration +// returns an error if the durpb.Duration is invalid or is too large to be +// represented in a time.Duration. +func Duration(p *durpb.Duration) (time.Duration, error) { + if err := validateDuration(p); err != nil { + return 0, err + } + d := time.Duration(p.Seconds) * time.Second + if int64(d/time.Second) != p.Seconds { + return 0, fmt.Errorf("duration: %v is out of range for time.Duration", p) + } + if p.Nanos != 0 { + d += time.Duration(p.Nanos) + if (d < 0) != (p.Nanos < 0) { + return 0, fmt.Errorf("duration: %v is out of range for time.Duration", p) + } + } + return d, nil +} + +// DurationProto converts a time.Duration to a durpb.Duration. +func DurationProto(d time.Duration) *durpb.Duration { + nanos := d.Nanoseconds() + secs := nanos / 1e9 + nanos -= secs * 1e9 + return &durpb.Duration{ + Seconds: secs, + Nanos: int32(nanos), + } +} diff --git a/vendor/github.com/golang/protobuf/ptypes/duration/duration.pb.go b/vendor/github.com/golang/protobuf/ptypes/duration/duration.pb.go new file mode 100644 index 00000000..4d75473b --- /dev/null +++ b/vendor/github.com/golang/protobuf/ptypes/duration/duration.pb.go @@ -0,0 +1,159 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// source: google/protobuf/duration.proto + +package duration // import "github.com/golang/protobuf/ptypes/duration" + +import proto "github.com/golang/protobuf/proto" +import fmt "fmt" +import math "math" + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package + +// A Duration represents a signed, fixed-length span of time represented +// as a count of seconds and fractions of seconds at nanosecond +// resolution. It is independent of any calendar and concepts like "day" +// or "month". It is related to Timestamp in that the difference between +// two Timestamp values is a Duration and it can be added or subtracted +// from a Timestamp. Range is approximately +-10,000 years. +// +// # Examples +// +// Example 1: Compute Duration from two Timestamps in pseudo code. +// +// Timestamp start = ...; +// Timestamp end = ...; +// Duration duration = ...; +// +// duration.seconds = end.seconds - start.seconds; +// duration.nanos = end.nanos - start.nanos; +// +// if (duration.seconds < 0 && duration.nanos > 0) { +// duration.seconds += 1; +// duration.nanos -= 1000000000; +// } else if (durations.seconds > 0 && duration.nanos < 0) { +// duration.seconds -= 1; +// duration.nanos += 1000000000; +// } +// +// Example 2: Compute Timestamp from Timestamp + Duration in pseudo code. +// +// Timestamp start = ...; +// Duration duration = ...; +// Timestamp end = ...; +// +// end.seconds = start.seconds + duration.seconds; +// end.nanos = start.nanos + duration.nanos; +// +// if (end.nanos < 0) { +// end.seconds -= 1; +// end.nanos += 1000000000; +// } else if (end.nanos >= 1000000000) { +// end.seconds += 1; +// end.nanos -= 1000000000; +// } +// +// Example 3: Compute Duration from datetime.timedelta in Python. +// +// td = datetime.timedelta(days=3, minutes=10) +// duration = Duration() +// duration.FromTimedelta(td) +// +// # JSON Mapping +// +// In JSON format, the Duration type is encoded as a string rather than an +// object, where the string ends in the suffix "s" (indicating seconds) and +// is preceded by the number of seconds, with nanoseconds expressed as +// fractional seconds. For example, 3 seconds with 0 nanoseconds should be +// encoded in JSON format as "3s", while 3 seconds and 1 nanosecond should +// be expressed in JSON format as "3.000000001s", and 3 seconds and 1 +// microsecond should be expressed in JSON format as "3.000001s". +// +// +type Duration struct { + // Signed seconds of the span of time. Must be from -315,576,000,000 + // to +315,576,000,000 inclusive. Note: these bounds are computed from: + // 60 sec/min * 60 min/hr * 24 hr/day * 365.25 days/year * 10000 years + Seconds int64 `protobuf:"varint,1,opt,name=seconds" json:"seconds,omitempty"` + // Signed fractions of a second at nanosecond resolution of the span + // of time. Durations less than one second are represented with a 0 + // `seconds` field and a positive or negative `nanos` field. For durations + // of one second or more, a non-zero value for the `nanos` field must be + // of the same sign as the `seconds` field. Must be from -999,999,999 + // to +999,999,999 inclusive. + Nanos int32 `protobuf:"varint,2,opt,name=nanos" json:"nanos,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *Duration) Reset() { *m = Duration{} } +func (m *Duration) String() string { return proto.CompactTextString(m) } +func (*Duration) ProtoMessage() {} +func (*Duration) Descriptor() ([]byte, []int) { + return fileDescriptor_duration_e7d612259e3f0613, []int{0} +} +func (*Duration) XXX_WellKnownType() string { return "Duration" } +func (m *Duration) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_Duration.Unmarshal(m, b) +} +func (m *Duration) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_Duration.Marshal(b, m, deterministic) +} +func (dst *Duration) XXX_Merge(src proto.Message) { + xxx_messageInfo_Duration.Merge(dst, src) +} +func (m *Duration) XXX_Size() int { + return xxx_messageInfo_Duration.Size(m) +} +func (m *Duration) XXX_DiscardUnknown() { + xxx_messageInfo_Duration.DiscardUnknown(m) +} + +var xxx_messageInfo_Duration proto.InternalMessageInfo + +func (m *Duration) GetSeconds() int64 { + if m != nil { + return m.Seconds + } + return 0 +} + +func (m *Duration) GetNanos() int32 { + if m != nil { + return m.Nanos + } + return 0 +} + +func init() { + proto.RegisterType((*Duration)(nil), "google.protobuf.Duration") +} + +func init() { + proto.RegisterFile("google/protobuf/duration.proto", fileDescriptor_duration_e7d612259e3f0613) +} + +var fileDescriptor_duration_e7d612259e3f0613 = []byte{ + // 190 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x92, 0x4b, 0xcf, 0xcf, 0x4f, + 0xcf, 0x49, 0xd5, 0x2f, 0x28, 0xca, 0x2f, 0xc9, 0x4f, 0x2a, 0x4d, 0xd3, 0x4f, 0x29, 0x2d, 0x4a, + 0x2c, 0xc9, 0xcc, 0xcf, 0xd3, 0x03, 0x8b, 0x08, 0xf1, 0x43, 0xe4, 0xf5, 0x60, 0xf2, 0x4a, 0x56, + 0x5c, 0x1c, 0x2e, 0x50, 0x25, 0x42, 0x12, 0x5c, 0xec, 0xc5, 0xa9, 0xc9, 0xf9, 0x79, 0x29, 0xc5, + 0x12, 0x8c, 0x0a, 0x8c, 0x1a, 0xcc, 0x41, 0x30, 0xae, 0x90, 0x08, 0x17, 0x6b, 0x5e, 0x62, 0x5e, + 0x7e, 0xb1, 0x04, 0x93, 0x02, 0xa3, 0x06, 0x6b, 0x10, 0x84, 0xe3, 0x54, 0xc3, 0x25, 0x9c, 0x9c, + 0x9f, 0xab, 0x87, 0x66, 0xa4, 0x13, 0x2f, 0xcc, 0xc0, 0x00, 0x90, 0x48, 0x00, 0x63, 0x94, 0x56, + 0x7a, 0x66, 0x49, 0x46, 0x69, 0x92, 0x5e, 0x72, 0x7e, 0xae, 0x7e, 0x7a, 0x7e, 0x4e, 0x62, 0x5e, + 0x3a, 0xc2, 0x7d, 0x05, 0x25, 0x95, 0x05, 0xa9, 0xc5, 0x70, 0x67, 0xfe, 0x60, 0x64, 0x5c, 0xc4, + 0xc4, 0xec, 0x1e, 0xe0, 0xb4, 0x8a, 0x49, 0xce, 0x1d, 0x62, 0x6e, 0x00, 0x54, 0xa9, 0x5e, 0x78, + 0x6a, 0x4e, 0x8e, 0x77, 0x5e, 0x7e, 0x79, 0x5e, 0x08, 0x48, 0x4b, 0x12, 0x1b, 0xd8, 0x0c, 0x63, + 0x40, 0x00, 0x00, 0x00, 0xff, 0xff, 0xdc, 0x84, 0x30, 0xff, 0xf3, 0x00, 0x00, 0x00, +} diff --git a/vendor/github.com/golang/protobuf/ptypes/duration/duration.proto b/vendor/github.com/golang/protobuf/ptypes/duration/duration.proto new file mode 100644 index 00000000..975fce41 --- /dev/null +++ b/vendor/github.com/golang/protobuf/ptypes/duration/duration.proto @@ -0,0 +1,117 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2008 Google Inc. All rights reserved. +// https://developers.google.com/protocol-buffers/ +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +syntax = "proto3"; + +package google.protobuf; + +option csharp_namespace = "Google.Protobuf.WellKnownTypes"; +option cc_enable_arenas = true; +option go_package = "github.com/golang/protobuf/ptypes/duration"; +option java_package = "com.google.protobuf"; +option java_outer_classname = "DurationProto"; +option java_multiple_files = true; +option objc_class_prefix = "GPB"; + +// A Duration represents a signed, fixed-length span of time represented +// as a count of seconds and fractions of seconds at nanosecond +// resolution. It is independent of any calendar and concepts like "day" +// or "month". It is related to Timestamp in that the difference between +// two Timestamp values is a Duration and it can be added or subtracted +// from a Timestamp. Range is approximately +-10,000 years. +// +// # Examples +// +// Example 1: Compute Duration from two Timestamps in pseudo code. +// +// Timestamp start = ...; +// Timestamp end = ...; +// Duration duration = ...; +// +// duration.seconds = end.seconds - start.seconds; +// duration.nanos = end.nanos - start.nanos; +// +// if (duration.seconds < 0 && duration.nanos > 0) { +// duration.seconds += 1; +// duration.nanos -= 1000000000; +// } else if (durations.seconds > 0 && duration.nanos < 0) { +// duration.seconds -= 1; +// duration.nanos += 1000000000; +// } +// +// Example 2: Compute Timestamp from Timestamp + Duration in pseudo code. +// +// Timestamp start = ...; +// Duration duration = ...; +// Timestamp end = ...; +// +// end.seconds = start.seconds + duration.seconds; +// end.nanos = start.nanos + duration.nanos; +// +// if (end.nanos < 0) { +// end.seconds -= 1; +// end.nanos += 1000000000; +// } else if (end.nanos >= 1000000000) { +// end.seconds += 1; +// end.nanos -= 1000000000; +// } +// +// Example 3: Compute Duration from datetime.timedelta in Python. +// +// td = datetime.timedelta(days=3, minutes=10) +// duration = Duration() +// duration.FromTimedelta(td) +// +// # JSON Mapping +// +// In JSON format, the Duration type is encoded as a string rather than an +// object, where the string ends in the suffix "s" (indicating seconds) and +// is preceded by the number of seconds, with nanoseconds expressed as +// fractional seconds. For example, 3 seconds with 0 nanoseconds should be +// encoded in JSON format as "3s", while 3 seconds and 1 nanosecond should +// be expressed in JSON format as "3.000000001s", and 3 seconds and 1 +// microsecond should be expressed in JSON format as "3.000001s". +// +// +message Duration { + + // Signed seconds of the span of time. Must be from -315,576,000,000 + // to +315,576,000,000 inclusive. Note: these bounds are computed from: + // 60 sec/min * 60 min/hr * 24 hr/day * 365.25 days/year * 10000 years + int64 seconds = 1; + + // Signed fractions of a second at nanosecond resolution of the span + // of time. Durations less than one second are represented with a 0 + // `seconds` field and a positive or negative `nanos` field. For durations + // of one second or more, a non-zero value for the `nanos` field must be + // of the same sign as the `seconds` field. Must be from -999,999,999 + // to +999,999,999 inclusive. + int32 nanos = 2; +} diff --git a/vendor/github.com/golang/protobuf/ptypes/timestamp.go b/vendor/github.com/golang/protobuf/ptypes/timestamp.go new file mode 100644 index 00000000..47f10dbc --- /dev/null +++ b/vendor/github.com/golang/protobuf/ptypes/timestamp.go @@ -0,0 +1,134 @@ +// Go support for Protocol Buffers - Google's data interchange format +// +// Copyright 2016 The Go Authors. All rights reserved. +// https://github.com/golang/protobuf +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package ptypes + +// This file implements operations on google.protobuf.Timestamp. + +import ( + "errors" + "fmt" + "time" + + tspb "github.com/golang/protobuf/ptypes/timestamp" +) + +const ( + // Seconds field of the earliest valid Timestamp. + // This is time.Date(1, 1, 1, 0, 0, 0, 0, time.UTC).Unix(). + minValidSeconds = -62135596800 + // Seconds field just after the latest valid Timestamp. + // This is time.Date(10000, 1, 1, 0, 0, 0, 0, time.UTC).Unix(). + maxValidSeconds = 253402300800 +) + +// validateTimestamp determines whether a Timestamp is valid. +// A valid timestamp represents a time in the range +// [0001-01-01, 10000-01-01) and has a Nanos field +// in the range [0, 1e9). +// +// If the Timestamp is valid, validateTimestamp returns nil. +// Otherwise, it returns an error that describes +// the problem. +// +// Every valid Timestamp can be represented by a time.Time, but the converse is not true. +func validateTimestamp(ts *tspb.Timestamp) error { + if ts == nil { + return errors.New("timestamp: nil Timestamp") + } + if ts.Seconds < minValidSeconds { + return fmt.Errorf("timestamp: %v before 0001-01-01", ts) + } + if ts.Seconds >= maxValidSeconds { + return fmt.Errorf("timestamp: %v after 10000-01-01", ts) + } + if ts.Nanos < 0 || ts.Nanos >= 1e9 { + return fmt.Errorf("timestamp: %v: nanos not in range [0, 1e9)", ts) + } + return nil +} + +// Timestamp converts a google.protobuf.Timestamp proto to a time.Time. +// It returns an error if the argument is invalid. +// +// Unlike most Go functions, if Timestamp returns an error, the first return value +// is not the zero time.Time. Instead, it is the value obtained from the +// time.Unix function when passed the contents of the Timestamp, in the UTC +// locale. This may or may not be a meaningful time; many invalid Timestamps +// do map to valid time.Times. +// +// A nil Timestamp returns an error. The first return value in that case is +// undefined. +func Timestamp(ts *tspb.Timestamp) (time.Time, error) { + // Don't return the zero value on error, because corresponds to a valid + // timestamp. Instead return whatever time.Unix gives us. + var t time.Time + if ts == nil { + t = time.Unix(0, 0).UTC() // treat nil like the empty Timestamp + } else { + t = time.Unix(ts.Seconds, int64(ts.Nanos)).UTC() + } + return t, validateTimestamp(ts) +} + +// TimestampNow returns a google.protobuf.Timestamp for the current time. +func TimestampNow() *tspb.Timestamp { + ts, err := TimestampProto(time.Now()) + if err != nil { + panic("ptypes: time.Now() out of Timestamp range") + } + return ts +} + +// TimestampProto converts the time.Time to a google.protobuf.Timestamp proto. +// It returns an error if the resulting Timestamp is invalid. +func TimestampProto(t time.Time) (*tspb.Timestamp, error) { + seconds := t.Unix() + nanos := int32(t.Sub(time.Unix(seconds, 0))) + ts := &tspb.Timestamp{ + Seconds: seconds, + Nanos: nanos, + } + if err := validateTimestamp(ts); err != nil { + return nil, err + } + return ts, nil +} + +// TimestampString returns the RFC 3339 string for valid Timestamps. For invalid +// Timestamps, it returns an error message in parentheses. +func TimestampString(ts *tspb.Timestamp) string { + t, err := Timestamp(ts) + if err != nil { + return fmt.Sprintf("(%v)", err) + } + return t.Format(time.RFC3339Nano) +} diff --git a/vendor/github.com/golang/protobuf/ptypes/timestamp/timestamp.pb.go b/vendor/github.com/golang/protobuf/ptypes/timestamp/timestamp.pb.go new file mode 100644 index 00000000..e9c22228 --- /dev/null +++ b/vendor/github.com/golang/protobuf/ptypes/timestamp/timestamp.pb.go @@ -0,0 +1,175 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// source: google/protobuf/timestamp.proto + +package timestamp // import "github.com/golang/protobuf/ptypes/timestamp" + +import proto "github.com/golang/protobuf/proto" +import fmt "fmt" +import math "math" + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package + +// A Timestamp represents a point in time independent of any time zone +// or calendar, represented as seconds and fractions of seconds at +// nanosecond resolution in UTC Epoch time. It is encoded using the +// Proleptic Gregorian Calendar which extends the Gregorian calendar +// backwards to year one. It is encoded assuming all minutes are 60 +// seconds long, i.e. leap seconds are "smeared" so that no leap second +// table is needed for interpretation. Range is from +// 0001-01-01T00:00:00Z to 9999-12-31T23:59:59.999999999Z. +// By restricting to that range, we ensure that we can convert to +// and from RFC 3339 date strings. +// See [https://www.ietf.org/rfc/rfc3339.txt](https://www.ietf.org/rfc/rfc3339.txt). +// +// # Examples +// +// Example 1: Compute Timestamp from POSIX `time()`. +// +// Timestamp timestamp; +// timestamp.set_seconds(time(NULL)); +// timestamp.set_nanos(0); +// +// Example 2: Compute Timestamp from POSIX `gettimeofday()`. +// +// struct timeval tv; +// gettimeofday(&tv, NULL); +// +// Timestamp timestamp; +// timestamp.set_seconds(tv.tv_sec); +// timestamp.set_nanos(tv.tv_usec * 1000); +// +// Example 3: Compute Timestamp from Win32 `GetSystemTimeAsFileTime()`. +// +// FILETIME ft; +// GetSystemTimeAsFileTime(&ft); +// UINT64 ticks = (((UINT64)ft.dwHighDateTime) << 32) | ft.dwLowDateTime; +// +// // A Windows tick is 100 nanoseconds. Windows epoch 1601-01-01T00:00:00Z +// // is 11644473600 seconds before Unix epoch 1970-01-01T00:00:00Z. +// Timestamp timestamp; +// timestamp.set_seconds((INT64) ((ticks / 10000000) - 11644473600LL)); +// timestamp.set_nanos((INT32) ((ticks % 10000000) * 100)); +// +// Example 4: Compute Timestamp from Java `System.currentTimeMillis()`. +// +// long millis = System.currentTimeMillis(); +// +// Timestamp timestamp = Timestamp.newBuilder().setSeconds(millis / 1000) +// .setNanos((int) ((millis % 1000) * 1000000)).build(); +// +// +// Example 5: Compute Timestamp from current time in Python. +// +// timestamp = Timestamp() +// timestamp.GetCurrentTime() +// +// # JSON Mapping +// +// In JSON format, the Timestamp type is encoded as a string in the +// [RFC 3339](https://www.ietf.org/rfc/rfc3339.txt) format. That is, the +// format is "{year}-{month}-{day}T{hour}:{min}:{sec}[.{frac_sec}]Z" +// where {year} is always expressed using four digits while {month}, {day}, +// {hour}, {min}, and {sec} are zero-padded to two digits each. The fractional +// seconds, which can go up to 9 digits (i.e. up to 1 nanosecond resolution), +// are optional. The "Z" suffix indicates the timezone ("UTC"); the timezone +// is required, though only UTC (as indicated by "Z") is presently supported. +// +// For example, "2017-01-15T01:30:15.01Z" encodes 15.01 seconds past +// 01:30 UTC on January 15, 2017. +// +// In JavaScript, one can convert a Date object to this format using the +// standard [toISOString()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString] +// method. In Python, a standard `datetime.datetime` object can be converted +// to this format using [`strftime`](https://docs.python.org/2/library/time.html#time.strftime) +// with the time format spec '%Y-%m-%dT%H:%M:%S.%fZ'. Likewise, in Java, one +// can use the Joda Time's [`ISODateTimeFormat.dateTime()`]( +// http://www.joda.org/joda-time/apidocs/org/joda/time/format/ISODateTimeFormat.html#dateTime--) +// to obtain a formatter capable of generating timestamps in this format. +// +// +type Timestamp struct { + // Represents seconds of UTC time since Unix epoch + // 1970-01-01T00:00:00Z. Must be from 0001-01-01T00:00:00Z to + // 9999-12-31T23:59:59Z inclusive. + Seconds int64 `protobuf:"varint,1,opt,name=seconds" json:"seconds,omitempty"` + // Non-negative fractions of a second at nanosecond resolution. Negative + // second values with fractions must still have non-negative nanos values + // that count forward in time. Must be from 0 to 999,999,999 + // inclusive. + Nanos int32 `protobuf:"varint,2,opt,name=nanos" json:"nanos,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *Timestamp) Reset() { *m = Timestamp{} } +func (m *Timestamp) String() string { return proto.CompactTextString(m) } +func (*Timestamp) ProtoMessage() {} +func (*Timestamp) Descriptor() ([]byte, []int) { + return fileDescriptor_timestamp_b826e8e5fba671a8, []int{0} +} +func (*Timestamp) XXX_WellKnownType() string { return "Timestamp" } +func (m *Timestamp) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_Timestamp.Unmarshal(m, b) +} +func (m *Timestamp) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_Timestamp.Marshal(b, m, deterministic) +} +func (dst *Timestamp) XXX_Merge(src proto.Message) { + xxx_messageInfo_Timestamp.Merge(dst, src) +} +func (m *Timestamp) XXX_Size() int { + return xxx_messageInfo_Timestamp.Size(m) +} +func (m *Timestamp) XXX_DiscardUnknown() { + xxx_messageInfo_Timestamp.DiscardUnknown(m) +} + +var xxx_messageInfo_Timestamp proto.InternalMessageInfo + +func (m *Timestamp) GetSeconds() int64 { + if m != nil { + return m.Seconds + } + return 0 +} + +func (m *Timestamp) GetNanos() int32 { + if m != nil { + return m.Nanos + } + return 0 +} + +func init() { + proto.RegisterType((*Timestamp)(nil), "google.protobuf.Timestamp") +} + +func init() { + proto.RegisterFile("google/protobuf/timestamp.proto", fileDescriptor_timestamp_b826e8e5fba671a8) +} + +var fileDescriptor_timestamp_b826e8e5fba671a8 = []byte{ + // 191 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x92, 0x4f, 0xcf, 0xcf, 0x4f, + 0xcf, 0x49, 0xd5, 0x2f, 0x28, 0xca, 0x2f, 0xc9, 0x4f, 0x2a, 0x4d, 0xd3, 0x2f, 0xc9, 0xcc, 0x4d, + 0x2d, 0x2e, 0x49, 0xcc, 0x2d, 0xd0, 0x03, 0x0b, 0x09, 0xf1, 0x43, 0x14, 0xe8, 0xc1, 0x14, 0x28, + 0x59, 0x73, 0x71, 0x86, 0xc0, 0xd4, 0x08, 0x49, 0x70, 0xb1, 0x17, 0xa7, 0x26, 0xe7, 0xe7, 0xa5, + 0x14, 0x4b, 0x30, 0x2a, 0x30, 0x6a, 0x30, 0x07, 0xc1, 0xb8, 0x42, 0x22, 0x5c, 0xac, 0x79, 0x89, + 0x79, 0xf9, 0xc5, 0x12, 0x4c, 0x0a, 0x8c, 0x1a, 0xac, 0x41, 0x10, 0x8e, 0x53, 0x1d, 0x97, 0x70, + 0x72, 0x7e, 0xae, 0x1e, 0x9a, 0x99, 0x4e, 0x7c, 0x70, 0x13, 0x03, 0x40, 0x42, 0x01, 0x8c, 0x51, + 0xda, 0xe9, 0x99, 0x25, 0x19, 0xa5, 0x49, 0x7a, 0xc9, 0xf9, 0xb9, 0xfa, 0xe9, 0xf9, 0x39, 0x89, + 0x79, 0xe9, 0x08, 0x27, 0x16, 0x94, 0x54, 0x16, 0xa4, 0x16, 0x23, 0x5c, 0xfa, 0x83, 0x91, 0x71, + 0x11, 0x13, 0xb3, 0x7b, 0x80, 0xd3, 0x2a, 0x26, 0x39, 0x77, 0x88, 0xc9, 0x01, 0x50, 0xb5, 0x7a, + 0xe1, 0xa9, 0x39, 0x39, 0xde, 0x79, 0xf9, 0xe5, 0x79, 0x21, 0x20, 0x3d, 0x49, 0x6c, 0x60, 0x43, + 0x8c, 0x01, 0x01, 0x00, 0x00, 0xff, 0xff, 0xbc, 0x77, 0x4a, 0x07, 0xf7, 0x00, 0x00, 0x00, +} diff --git a/vendor/github.com/golang/protobuf/ptypes/timestamp/timestamp.proto b/vendor/github.com/golang/protobuf/ptypes/timestamp/timestamp.proto new file mode 100644 index 00000000..06750ab1 --- /dev/null +++ b/vendor/github.com/golang/protobuf/ptypes/timestamp/timestamp.proto @@ -0,0 +1,133 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2008 Google Inc. All rights reserved. +// https://developers.google.com/protocol-buffers/ +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +syntax = "proto3"; + +package google.protobuf; + +option csharp_namespace = "Google.Protobuf.WellKnownTypes"; +option cc_enable_arenas = true; +option go_package = "github.com/golang/protobuf/ptypes/timestamp"; +option java_package = "com.google.protobuf"; +option java_outer_classname = "TimestampProto"; +option java_multiple_files = true; +option objc_class_prefix = "GPB"; + +// A Timestamp represents a point in time independent of any time zone +// or calendar, represented as seconds and fractions of seconds at +// nanosecond resolution in UTC Epoch time. It is encoded using the +// Proleptic Gregorian Calendar which extends the Gregorian calendar +// backwards to year one. It is encoded assuming all minutes are 60 +// seconds long, i.e. leap seconds are "smeared" so that no leap second +// table is needed for interpretation. Range is from +// 0001-01-01T00:00:00Z to 9999-12-31T23:59:59.999999999Z. +// By restricting to that range, we ensure that we can convert to +// and from RFC 3339 date strings. +// See [https://www.ietf.org/rfc/rfc3339.txt](https://www.ietf.org/rfc/rfc3339.txt). +// +// # Examples +// +// Example 1: Compute Timestamp from POSIX `time()`. +// +// Timestamp timestamp; +// timestamp.set_seconds(time(NULL)); +// timestamp.set_nanos(0); +// +// Example 2: Compute Timestamp from POSIX `gettimeofday()`. +// +// struct timeval tv; +// gettimeofday(&tv, NULL); +// +// Timestamp timestamp; +// timestamp.set_seconds(tv.tv_sec); +// timestamp.set_nanos(tv.tv_usec * 1000); +// +// Example 3: Compute Timestamp from Win32 `GetSystemTimeAsFileTime()`. +// +// FILETIME ft; +// GetSystemTimeAsFileTime(&ft); +// UINT64 ticks = (((UINT64)ft.dwHighDateTime) << 32) | ft.dwLowDateTime; +// +// // A Windows tick is 100 nanoseconds. Windows epoch 1601-01-01T00:00:00Z +// // is 11644473600 seconds before Unix epoch 1970-01-01T00:00:00Z. +// Timestamp timestamp; +// timestamp.set_seconds((INT64) ((ticks / 10000000) - 11644473600LL)); +// timestamp.set_nanos((INT32) ((ticks % 10000000) * 100)); +// +// Example 4: Compute Timestamp from Java `System.currentTimeMillis()`. +// +// long millis = System.currentTimeMillis(); +// +// Timestamp timestamp = Timestamp.newBuilder().setSeconds(millis / 1000) +// .setNanos((int) ((millis % 1000) * 1000000)).build(); +// +// +// Example 5: Compute Timestamp from current time in Python. +// +// timestamp = Timestamp() +// timestamp.GetCurrentTime() +// +// # JSON Mapping +// +// In JSON format, the Timestamp type is encoded as a string in the +// [RFC 3339](https://www.ietf.org/rfc/rfc3339.txt) format. That is, the +// format is "{year}-{month}-{day}T{hour}:{min}:{sec}[.{frac_sec}]Z" +// where {year} is always expressed using four digits while {month}, {day}, +// {hour}, {min}, and {sec} are zero-padded to two digits each. The fractional +// seconds, which can go up to 9 digits (i.e. up to 1 nanosecond resolution), +// are optional. The "Z" suffix indicates the timezone ("UTC"); the timezone +// is required, though only UTC (as indicated by "Z") is presently supported. +// +// For example, "2017-01-15T01:30:15.01Z" encodes 15.01 seconds past +// 01:30 UTC on January 15, 2017. +// +// In JavaScript, one can convert a Date object to this format using the +// standard [toISOString()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString] +// method. In Python, a standard `datetime.datetime` object can be converted +// to this format using [`strftime`](https://docs.python.org/2/library/time.html#time.strftime) +// with the time format spec '%Y-%m-%dT%H:%M:%S.%fZ'. Likewise, in Java, one +// can use the Joda Time's [`ISODateTimeFormat.dateTime()`]( +// http://www.joda.org/joda-time/apidocs/org/joda/time/format/ISODateTimeFormat.html#dateTime--) +// to obtain a formatter capable of generating timestamps in this format. +// +// +message Timestamp { + + // Represents seconds of UTC time since Unix epoch + // 1970-01-01T00:00:00Z. Must be from 0001-01-01T00:00:00Z to + // 9999-12-31T23:59:59Z inclusive. + int64 seconds = 1; + + // Non-negative fractions of a second at nanosecond resolution. Negative + // second values with fractions must still have non-negative nanos values + // that count forward in time. Must be from 0 to 999,999,999 + // inclusive. + int32 nanos = 2; +} diff --git a/vendor/github.com/kr/pretty/License b/vendor/github.com/kr/pretty/License new file mode 100644 index 00000000..05c783cc --- /dev/null +++ b/vendor/github.com/kr/pretty/License @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright 2012 Keith Rarick + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/github.com/kr/pretty/Readme b/vendor/github.com/kr/pretty/Readme new file mode 100644 index 00000000..c589fc62 --- /dev/null +++ b/vendor/github.com/kr/pretty/Readme @@ -0,0 +1,9 @@ +package pretty + + import "github.com/kr/pretty" + + Package pretty provides pretty-printing for Go values. + +Documentation + + http://godoc.org/github.com/kr/pretty diff --git a/vendor/github.com/kr/pretty/diff.go b/vendor/github.com/kr/pretty/diff.go new file mode 100644 index 00000000..6aa7f743 --- /dev/null +++ b/vendor/github.com/kr/pretty/diff.go @@ -0,0 +1,265 @@ +package pretty + +import ( + "fmt" + "io" + "reflect" +) + +type sbuf []string + +func (p *sbuf) Printf(format string, a ...interface{}) { + s := fmt.Sprintf(format, a...) + *p = append(*p, s) +} + +// Diff returns a slice where each element describes +// a difference between a and b. +func Diff(a, b interface{}) (desc []string) { + Pdiff((*sbuf)(&desc), a, b) + return desc +} + +// wprintfer calls Fprintf on w for each Printf call +// with a trailing newline. +type wprintfer struct{ w io.Writer } + +func (p *wprintfer) Printf(format string, a ...interface{}) { + fmt.Fprintf(p.w, format+"\n", a...) +} + +// Fdiff writes to w a description of the differences between a and b. +func Fdiff(w io.Writer, a, b interface{}) { + Pdiff(&wprintfer{w}, a, b) +} + +type Printfer interface { + Printf(format string, a ...interface{}) +} + +// Pdiff prints to p a description of the differences between a and b. +// It calls Printf once for each difference, with no trailing newline. +// The standard library log.Logger is a Printfer. +func Pdiff(p Printfer, a, b interface{}) { + diffPrinter{w: p}.diff(reflect.ValueOf(a), reflect.ValueOf(b)) +} + +type Logfer interface { + Logf(format string, a ...interface{}) +} + +// logprintfer calls Fprintf on w for each Printf call +// with a trailing newline. +type logprintfer struct{ l Logfer } + +func (p *logprintfer) Printf(format string, a ...interface{}) { + p.l.Logf(format, a...) +} + +// Ldiff prints to l a description of the differences between a and b. +// It calls Logf once for each difference, with no trailing newline. +// The standard library testing.T and testing.B are Logfers. +func Ldiff(l Logfer, a, b interface{}) { + Pdiff(&logprintfer{l}, a, b) +} + +type diffPrinter struct { + w Printfer + l string // label +} + +func (w diffPrinter) printf(f string, a ...interface{}) { + var l string + if w.l != "" { + l = w.l + ": " + } + w.w.Printf(l+f, a...) +} + +func (w diffPrinter) diff(av, bv reflect.Value) { + if !av.IsValid() && bv.IsValid() { + w.printf("nil != %# v", formatter{v: bv, quote: true}) + return + } + if av.IsValid() && !bv.IsValid() { + w.printf("%# v != nil", formatter{v: av, quote: true}) + return + } + if !av.IsValid() && !bv.IsValid() { + return + } + + at := av.Type() + bt := bv.Type() + if at != bt { + w.printf("%v != %v", at, bt) + return + } + + switch kind := at.Kind(); kind { + case reflect.Bool: + if a, b := av.Bool(), bv.Bool(); a != b { + w.printf("%v != %v", a, b) + } + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + if a, b := av.Int(), bv.Int(); a != b { + w.printf("%d != %d", a, b) + } + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + if a, b := av.Uint(), bv.Uint(); a != b { + w.printf("%d != %d", a, b) + } + case reflect.Float32, reflect.Float64: + if a, b := av.Float(), bv.Float(); a != b { + w.printf("%v != %v", a, b) + } + case reflect.Complex64, reflect.Complex128: + if a, b := av.Complex(), bv.Complex(); a != b { + w.printf("%v != %v", a, b) + } + case reflect.Array: + n := av.Len() + for i := 0; i < n; i++ { + w.relabel(fmt.Sprintf("[%d]", i)).diff(av.Index(i), bv.Index(i)) + } + case reflect.Chan, reflect.Func, reflect.UnsafePointer: + if a, b := av.Pointer(), bv.Pointer(); a != b { + w.printf("%#x != %#x", a, b) + } + case reflect.Interface: + w.diff(av.Elem(), bv.Elem()) + case reflect.Map: + ak, both, bk := keyDiff(av.MapKeys(), bv.MapKeys()) + for _, k := range ak { + w := w.relabel(fmt.Sprintf("[%#v]", k)) + w.printf("%q != (missing)", av.MapIndex(k)) + } + for _, k := range both { + w := w.relabel(fmt.Sprintf("[%#v]", k)) + w.diff(av.MapIndex(k), bv.MapIndex(k)) + } + for _, k := range bk { + w := w.relabel(fmt.Sprintf("[%#v]", k)) + w.printf("(missing) != %q", bv.MapIndex(k)) + } + case reflect.Ptr: + switch { + case av.IsNil() && !bv.IsNil(): + w.printf("nil != %# v", formatter{v: bv, quote: true}) + case !av.IsNil() && bv.IsNil(): + w.printf("%# v != nil", formatter{v: av, quote: true}) + case !av.IsNil() && !bv.IsNil(): + w.diff(av.Elem(), bv.Elem()) + } + case reflect.Slice: + lenA := av.Len() + lenB := bv.Len() + if lenA != lenB { + w.printf("%s[%d] != %s[%d]", av.Type(), lenA, bv.Type(), lenB) + break + } + for i := 0; i < lenA; i++ { + w.relabel(fmt.Sprintf("[%d]", i)).diff(av.Index(i), bv.Index(i)) + } + case reflect.String: + if a, b := av.String(), bv.String(); a != b { + w.printf("%q != %q", a, b) + } + case reflect.Struct: + for i := 0; i < av.NumField(); i++ { + w.relabel(at.Field(i).Name).diff(av.Field(i), bv.Field(i)) + } + default: + panic("unknown reflect Kind: " + kind.String()) + } +} + +func (d diffPrinter) relabel(name string) (d1 diffPrinter) { + d1 = d + if d.l != "" && name[0] != '[' { + d1.l += "." + } + d1.l += name + return d1 +} + +// keyEqual compares a and b for equality. +// Both a and b must be valid map keys. +func keyEqual(av, bv reflect.Value) bool { + if !av.IsValid() && !bv.IsValid() { + return true + } + if !av.IsValid() || !bv.IsValid() || av.Type() != bv.Type() { + return false + } + switch kind := av.Kind(); kind { + case reflect.Bool: + a, b := av.Bool(), bv.Bool() + return a == b + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + a, b := av.Int(), bv.Int() + return a == b + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + a, b := av.Uint(), bv.Uint() + return a == b + case reflect.Float32, reflect.Float64: + a, b := av.Float(), bv.Float() + return a == b + case reflect.Complex64, reflect.Complex128: + a, b := av.Complex(), bv.Complex() + return a == b + case reflect.Array: + for i := 0; i < av.Len(); i++ { + if !keyEqual(av.Index(i), bv.Index(i)) { + return false + } + } + return true + case reflect.Chan, reflect.UnsafePointer, reflect.Ptr: + a, b := av.Pointer(), bv.Pointer() + return a == b + case reflect.Interface: + return keyEqual(av.Elem(), bv.Elem()) + case reflect.String: + a, b := av.String(), bv.String() + return a == b + case reflect.Struct: + for i := 0; i < av.NumField(); i++ { + if !keyEqual(av.Field(i), bv.Field(i)) { + return false + } + } + return true + default: + panic("invalid map key type " + av.Type().String()) + } +} + +func keyDiff(a, b []reflect.Value) (ak, both, bk []reflect.Value) { + for _, av := range a { + inBoth := false + for _, bv := range b { + if keyEqual(av, bv) { + inBoth = true + both = append(both, av) + break + } + } + if !inBoth { + ak = append(ak, av) + } + } + for _, bv := range b { + inBoth := false + for _, av := range a { + if keyEqual(av, bv) { + inBoth = true + break + } + } + if !inBoth { + bk = append(bk, bv) + } + } + return +} diff --git a/vendor/github.com/kr/pretty/formatter.go b/vendor/github.com/kr/pretty/formatter.go new file mode 100644 index 00000000..a317d7b8 --- /dev/null +++ b/vendor/github.com/kr/pretty/formatter.go @@ -0,0 +1,328 @@ +package pretty + +import ( + "fmt" + "io" + "reflect" + "strconv" + "text/tabwriter" + + "github.com/kr/text" +) + +type formatter struct { + v reflect.Value + force bool + quote bool +} + +// Formatter makes a wrapper, f, that will format x as go source with line +// breaks and tabs. Object f responds to the "%v" formatting verb when both the +// "#" and " " (space) flags are set, for example: +// +// fmt.Sprintf("%# v", Formatter(x)) +// +// If one of these two flags is not set, or any other verb is used, f will +// format x according to the usual rules of package fmt. +// In particular, if x satisfies fmt.Formatter, then x.Format will be called. +func Formatter(x interface{}) (f fmt.Formatter) { + return formatter{v: reflect.ValueOf(x), quote: true} +} + +func (fo formatter) String() string { + return fmt.Sprint(fo.v.Interface()) // unwrap it +} + +func (fo formatter) passThrough(f fmt.State, c rune) { + s := "%" + for i := 0; i < 128; i++ { + if f.Flag(i) { + s += string(i) + } + } + if w, ok := f.Width(); ok { + s += fmt.Sprintf("%d", w) + } + if p, ok := f.Precision(); ok { + s += fmt.Sprintf(".%d", p) + } + s += string(c) + fmt.Fprintf(f, s, fo.v.Interface()) +} + +func (fo formatter) Format(f fmt.State, c rune) { + if fo.force || c == 'v' && f.Flag('#') && f.Flag(' ') { + w := tabwriter.NewWriter(f, 4, 4, 1, ' ', 0) + p := &printer{tw: w, Writer: w, visited: make(map[visit]int)} + p.printValue(fo.v, true, fo.quote) + w.Flush() + return + } + fo.passThrough(f, c) +} + +type printer struct { + io.Writer + tw *tabwriter.Writer + visited map[visit]int + depth int +} + +func (p *printer) indent() *printer { + q := *p + q.tw = tabwriter.NewWriter(p.Writer, 4, 4, 1, ' ', 0) + q.Writer = text.NewIndentWriter(q.tw, []byte{'\t'}) + return &q +} + +func (p *printer) printInline(v reflect.Value, x interface{}, showType bool) { + if showType { + io.WriteString(p, v.Type().String()) + fmt.Fprintf(p, "(%#v)", x) + } else { + fmt.Fprintf(p, "%#v", x) + } +} + +// printValue must keep track of already-printed pointer values to avoid +// infinite recursion. +type visit struct { + v uintptr + typ reflect.Type +} + +func (p *printer) printValue(v reflect.Value, showType, quote bool) { + if p.depth > 10 { + io.WriteString(p, "!%v(DEPTH EXCEEDED)") + return + } + + switch v.Kind() { + case reflect.Bool: + p.printInline(v, v.Bool(), showType) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + p.printInline(v, v.Int(), showType) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + p.printInline(v, v.Uint(), showType) + case reflect.Float32, reflect.Float64: + p.printInline(v, v.Float(), showType) + case reflect.Complex64, reflect.Complex128: + fmt.Fprintf(p, "%#v", v.Complex()) + case reflect.String: + p.fmtString(v.String(), quote) + case reflect.Map: + t := v.Type() + if showType { + io.WriteString(p, t.String()) + } + writeByte(p, '{') + if nonzero(v) { + expand := !canInline(v.Type()) + pp := p + if expand { + writeByte(p, '\n') + pp = p.indent() + } + keys := v.MapKeys() + for i := 0; i < v.Len(); i++ { + showTypeInStruct := true + k := keys[i] + mv := v.MapIndex(k) + pp.printValue(k, false, true) + writeByte(pp, ':') + if expand { + writeByte(pp, '\t') + } + showTypeInStruct = t.Elem().Kind() == reflect.Interface + pp.printValue(mv, showTypeInStruct, true) + if expand { + io.WriteString(pp, ",\n") + } else if i < v.Len()-1 { + io.WriteString(pp, ", ") + } + } + if expand { + pp.tw.Flush() + } + } + writeByte(p, '}') + case reflect.Struct: + t := v.Type() + if v.CanAddr() { + addr := v.UnsafeAddr() + vis := visit{addr, t} + if vd, ok := p.visited[vis]; ok && vd < p.depth { + p.fmtString(t.String()+"{(CYCLIC REFERENCE)}", false) + break // don't print v again + } + p.visited[vis] = p.depth + } + + if showType { + io.WriteString(p, t.String()) + } + writeByte(p, '{') + if nonzero(v) { + expand := !canInline(v.Type()) + pp := p + if expand { + writeByte(p, '\n') + pp = p.indent() + } + for i := 0; i < v.NumField(); i++ { + showTypeInStruct := true + if f := t.Field(i); f.Name != "" { + io.WriteString(pp, f.Name) + writeByte(pp, ':') + if expand { + writeByte(pp, '\t') + } + showTypeInStruct = labelType(f.Type) + } + pp.printValue(getField(v, i), showTypeInStruct, true) + if expand { + io.WriteString(pp, ",\n") + } else if i < v.NumField()-1 { + io.WriteString(pp, ", ") + } + } + if expand { + pp.tw.Flush() + } + } + writeByte(p, '}') + case reflect.Interface: + switch e := v.Elem(); { + case e.Kind() == reflect.Invalid: + io.WriteString(p, "nil") + case e.IsValid(): + pp := *p + pp.depth++ + pp.printValue(e, showType, true) + default: + io.WriteString(p, v.Type().String()) + io.WriteString(p, "(nil)") + } + case reflect.Array, reflect.Slice: + t := v.Type() + if showType { + io.WriteString(p, t.String()) + } + if v.Kind() == reflect.Slice && v.IsNil() && showType { + io.WriteString(p, "(nil)") + break + } + if v.Kind() == reflect.Slice && v.IsNil() { + io.WriteString(p, "nil") + break + } + writeByte(p, '{') + expand := !canInline(v.Type()) + pp := p + if expand { + writeByte(p, '\n') + pp = p.indent() + } + for i := 0; i < v.Len(); i++ { + showTypeInSlice := t.Elem().Kind() == reflect.Interface + pp.printValue(v.Index(i), showTypeInSlice, true) + if expand { + io.WriteString(pp, ",\n") + } else if i < v.Len()-1 { + io.WriteString(pp, ", ") + } + } + if expand { + pp.tw.Flush() + } + writeByte(p, '}') + case reflect.Ptr: + e := v.Elem() + if !e.IsValid() { + writeByte(p, '(') + io.WriteString(p, v.Type().String()) + io.WriteString(p, ")(nil)") + } else { + pp := *p + pp.depth++ + writeByte(pp, '&') + pp.printValue(e, true, true) + } + case reflect.Chan: + x := v.Pointer() + if showType { + writeByte(p, '(') + io.WriteString(p, v.Type().String()) + fmt.Fprintf(p, ")(%#v)", x) + } else { + fmt.Fprintf(p, "%#v", x) + } + case reflect.Func: + io.WriteString(p, v.Type().String()) + io.WriteString(p, " {...}") + case reflect.UnsafePointer: + p.printInline(v, v.Pointer(), showType) + case reflect.Invalid: + io.WriteString(p, "nil") + } +} + +func canInline(t reflect.Type) bool { + switch t.Kind() { + case reflect.Map: + return !canExpand(t.Elem()) + case reflect.Struct: + for i := 0; i < t.NumField(); i++ { + if canExpand(t.Field(i).Type) { + return false + } + } + return true + case reflect.Interface: + return false + case reflect.Array, reflect.Slice: + return !canExpand(t.Elem()) + case reflect.Ptr: + return false + case reflect.Chan, reflect.Func, reflect.UnsafePointer: + return false + } + return true +} + +func canExpand(t reflect.Type) bool { + switch t.Kind() { + case reflect.Map, reflect.Struct, + reflect.Interface, reflect.Array, reflect.Slice, + reflect.Ptr: + return true + } + return false +} + +func labelType(t reflect.Type) bool { + switch t.Kind() { + case reflect.Interface, reflect.Struct: + return true + } + return false +} + +func (p *printer) fmtString(s string, quote bool) { + if quote { + s = strconv.Quote(s) + } + io.WriteString(p, s) +} + +func writeByte(w io.Writer, b byte) { + w.Write([]byte{b}) +} + +func getField(v reflect.Value, i int) reflect.Value { + val := v.Field(i) + if val.Kind() == reflect.Interface && !val.IsNil() { + val = val.Elem() + } + return val +} diff --git a/vendor/github.com/kr/pretty/go.mod b/vendor/github.com/kr/pretty/go.mod new file mode 100644 index 00000000..1e295331 --- /dev/null +++ b/vendor/github.com/kr/pretty/go.mod @@ -0,0 +1,3 @@ +module "github.com/kr/pretty" + +require "github.com/kr/text" v0.1.0 diff --git a/vendor/github.com/kr/pretty/pretty.go b/vendor/github.com/kr/pretty/pretty.go new file mode 100644 index 00000000..49423ec7 --- /dev/null +++ b/vendor/github.com/kr/pretty/pretty.go @@ -0,0 +1,108 @@ +// Package pretty provides pretty-printing for Go values. This is +// useful during debugging, to avoid wrapping long output lines in +// the terminal. +// +// It provides a function, Formatter, that can be used with any +// function that accepts a format string. It also provides +// convenience wrappers for functions in packages fmt and log. +package pretty + +import ( + "fmt" + "io" + "log" + "reflect" +) + +// Errorf is a convenience wrapper for fmt.Errorf. +// +// Calling Errorf(f, x, y) is equivalent to +// fmt.Errorf(f, Formatter(x), Formatter(y)). +func Errorf(format string, a ...interface{}) error { + return fmt.Errorf(format, wrap(a, false)...) +} + +// Fprintf is a convenience wrapper for fmt.Fprintf. +// +// Calling Fprintf(w, f, x, y) is equivalent to +// fmt.Fprintf(w, f, Formatter(x), Formatter(y)). +func Fprintf(w io.Writer, format string, a ...interface{}) (n int, error error) { + return fmt.Fprintf(w, format, wrap(a, false)...) +} + +// Log is a convenience wrapper for log.Printf. +// +// Calling Log(x, y) is equivalent to +// log.Print(Formatter(x), Formatter(y)), but each operand is +// formatted with "%# v". +func Log(a ...interface{}) { + log.Print(wrap(a, true)...) +} + +// Logf is a convenience wrapper for log.Printf. +// +// Calling Logf(f, x, y) is equivalent to +// log.Printf(f, Formatter(x), Formatter(y)). +func Logf(format string, a ...interface{}) { + log.Printf(format, wrap(a, false)...) +} + +// Logln is a convenience wrapper for log.Printf. +// +// Calling Logln(x, y) is equivalent to +// log.Println(Formatter(x), Formatter(y)), but each operand is +// formatted with "%# v". +func Logln(a ...interface{}) { + log.Println(wrap(a, true)...) +} + +// Print pretty-prints its operands and writes to standard output. +// +// Calling Print(x, y) is equivalent to +// fmt.Print(Formatter(x), Formatter(y)), but each operand is +// formatted with "%# v". +func Print(a ...interface{}) (n int, errno error) { + return fmt.Print(wrap(a, true)...) +} + +// Printf is a convenience wrapper for fmt.Printf. +// +// Calling Printf(f, x, y) is equivalent to +// fmt.Printf(f, Formatter(x), Formatter(y)). +func Printf(format string, a ...interface{}) (n int, errno error) { + return fmt.Printf(format, wrap(a, false)...) +} + +// Println pretty-prints its operands and writes to standard output. +// +// Calling Print(x, y) is equivalent to +// fmt.Println(Formatter(x), Formatter(y)), but each operand is +// formatted with "%# v". +func Println(a ...interface{}) (n int, errno error) { + return fmt.Println(wrap(a, true)...) +} + +// Sprint is a convenience wrapper for fmt.Sprintf. +// +// Calling Sprint(x, y) is equivalent to +// fmt.Sprint(Formatter(x), Formatter(y)), but each operand is +// formatted with "%# v". +func Sprint(a ...interface{}) string { + return fmt.Sprint(wrap(a, true)...) +} + +// Sprintf is a convenience wrapper for fmt.Sprintf. +// +// Calling Sprintf(f, x, y) is equivalent to +// fmt.Sprintf(f, Formatter(x), Formatter(y)). +func Sprintf(format string, a ...interface{}) string { + return fmt.Sprintf(format, wrap(a, false)...) +} + +func wrap(a []interface{}, force bool) []interface{} { + w := make([]interface{}, len(a)) + for i, x := range a { + w[i] = formatter{v: reflect.ValueOf(x), force: force} + } + return w +} diff --git a/vendor/github.com/kr/pretty/zero.go b/vendor/github.com/kr/pretty/zero.go new file mode 100644 index 00000000..abb5b6fc --- /dev/null +++ b/vendor/github.com/kr/pretty/zero.go @@ -0,0 +1,41 @@ +package pretty + +import ( + "reflect" +) + +func nonzero(v reflect.Value) bool { + switch v.Kind() { + case reflect.Bool: + return v.Bool() + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return v.Int() != 0 + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return v.Uint() != 0 + case reflect.Float32, reflect.Float64: + return v.Float() != 0 + case reflect.Complex64, reflect.Complex128: + return v.Complex() != complex(0, 0) + case reflect.String: + return v.String() != "" + case reflect.Struct: + for i := 0; i < v.NumField(); i++ { + if nonzero(getField(v, i)) { + return true + } + } + return false + case reflect.Array: + for i := 0; i < v.Len(); i++ { + if nonzero(v.Index(i)) { + return true + } + } + return false + case reflect.Map, reflect.Interface, reflect.Slice, reflect.Ptr, reflect.Chan, reflect.Func: + return !v.IsNil() + case reflect.UnsafePointer: + return v.Pointer() != 0 + } + return true +} diff --git a/vendor/github.com/kr/text/License b/vendor/github.com/kr/text/License new file mode 100644 index 00000000..480a3280 --- /dev/null +++ b/vendor/github.com/kr/text/License @@ -0,0 +1,19 @@ +Copyright 2012 Keith Rarick + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/github.com/kr/text/Readme b/vendor/github.com/kr/text/Readme new file mode 100644 index 00000000..7e6e7c06 --- /dev/null +++ b/vendor/github.com/kr/text/Readme @@ -0,0 +1,3 @@ +This is a Go package for manipulating paragraphs of text. + +See http://go.pkgdoc.org/github.com/kr/text for full documentation. diff --git a/vendor/github.com/kr/text/doc.go b/vendor/github.com/kr/text/doc.go new file mode 100644 index 00000000..cf4c198f --- /dev/null +++ b/vendor/github.com/kr/text/doc.go @@ -0,0 +1,3 @@ +// Package text provides rudimentary functions for manipulating text in +// paragraphs. +package text diff --git a/vendor/github.com/kr/text/go.mod b/vendor/github.com/kr/text/go.mod new file mode 100644 index 00000000..fa0528b9 --- /dev/null +++ b/vendor/github.com/kr/text/go.mod @@ -0,0 +1,3 @@ +module "github.com/kr/text" + +require "github.com/kr/pty" v1.1.1 diff --git a/vendor/github.com/kr/text/indent.go b/vendor/github.com/kr/text/indent.go new file mode 100644 index 00000000..4ebac45c --- /dev/null +++ b/vendor/github.com/kr/text/indent.go @@ -0,0 +1,74 @@ +package text + +import ( + "io" +) + +// Indent inserts prefix at the beginning of each non-empty line of s. The +// end-of-line marker is NL. +func Indent(s, prefix string) string { + return string(IndentBytes([]byte(s), []byte(prefix))) +} + +// IndentBytes inserts prefix at the beginning of each non-empty line of b. +// The end-of-line marker is NL. +func IndentBytes(b, prefix []byte) []byte { + var res []byte + bol := true + for _, c := range b { + if bol && c != '\n' { + res = append(res, prefix...) + } + res = append(res, c) + bol = c == '\n' + } + return res +} + +// Writer indents each line of its input. +type indentWriter struct { + w io.Writer + bol bool + pre [][]byte + sel int + off int +} + +// NewIndentWriter makes a new write filter that indents the input +// lines. Each line is prefixed in order with the corresponding +// element of pre. If there are more lines than elements, the last +// element of pre is repeated for each subsequent line. +func NewIndentWriter(w io.Writer, pre ...[]byte) io.Writer { + return &indentWriter{ + w: w, + pre: pre, + bol: true, + } +} + +// The only errors returned are from the underlying indentWriter. +func (w *indentWriter) Write(p []byte) (n int, err error) { + for _, c := range p { + if w.bol { + var i int + i, err = w.w.Write(w.pre[w.sel][w.off:]) + w.off += i + if err != nil { + return n, err + } + } + _, err = w.w.Write([]byte{c}) + if err != nil { + return n, err + } + n++ + w.bol = c == '\n' + if w.bol { + w.off = 0 + if w.sel < len(w.pre)-1 { + w.sel++ + } + } + } + return n, nil +} diff --git a/vendor/github.com/kr/text/wrap.go b/vendor/github.com/kr/text/wrap.go new file mode 100644 index 00000000..b09bb037 --- /dev/null +++ b/vendor/github.com/kr/text/wrap.go @@ -0,0 +1,86 @@ +package text + +import ( + "bytes" + "math" +) + +var ( + nl = []byte{'\n'} + sp = []byte{' '} +) + +const defaultPenalty = 1e5 + +// Wrap wraps s into a paragraph of lines of length lim, with minimal +// raggedness. +func Wrap(s string, lim int) string { + return string(WrapBytes([]byte(s), lim)) +} + +// WrapBytes wraps b into a paragraph of lines of length lim, with minimal +// raggedness. +func WrapBytes(b []byte, lim int) []byte { + words := bytes.Split(bytes.Replace(bytes.TrimSpace(b), nl, sp, -1), sp) + var lines [][]byte + for _, line := range WrapWords(words, 1, lim, defaultPenalty) { + lines = append(lines, bytes.Join(line, sp)) + } + return bytes.Join(lines, nl) +} + +// WrapWords is the low-level line-breaking algorithm, useful if you need more +// control over the details of the text wrapping process. For most uses, either +// Wrap or WrapBytes will be sufficient and more convenient. +// +// WrapWords splits a list of words into lines with minimal "raggedness", +// treating each byte as one unit, accounting for spc units between adjacent +// words on each line, and attempting to limit lines to lim units. Raggedness +// is the total error over all lines, where error is the square of the +// difference of the length of the line and lim. Too-long lines (which only +// happen when a single word is longer than lim units) have pen penalty units +// added to the error. +func WrapWords(words [][]byte, spc, lim, pen int) [][][]byte { + n := len(words) + + length := make([][]int, n) + for i := 0; i < n; i++ { + length[i] = make([]int, n) + length[i][i] = len(words[i]) + for j := i + 1; j < n; j++ { + length[i][j] = length[i][j-1] + spc + len(words[j]) + } + } + + nbrk := make([]int, n) + cost := make([]int, n) + for i := range cost { + cost[i] = math.MaxInt32 + } + for i := n - 1; i >= 0; i-- { + if length[i][n-1] <= lim || i == n-1 { + cost[i] = 0 + nbrk[i] = n + } else { + for j := i + 1; j < n; j++ { + d := lim - length[i][j-1] + c := d*d + cost[j] + if length[i][j-1] > lim { + c += pen // too-long lines get a worse penalty + } + if c < cost[i] { + cost[i] = c + nbrk[i] = j + } + } + } + } + + var lines [][][]byte + i := 0 + for i < n { + lines = append(lines, words[i:nbrk[i]]) + i = nbrk[i] + } + return lines +} diff --git a/vendor/github.com/percona/go-mysql/LICENSE b/vendor/github.com/percona/go-mysql/LICENSE new file mode 100644 index 00000000..dbbe3558 --- /dev/null +++ b/vendor/github.com/percona/go-mysql/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/vendor/github.com/percona/go-mysql/query/query.go b/vendor/github.com/percona/go-mysql/query/query.go new file mode 100644 index 00000000..4f8bfcd0 --- /dev/null +++ b/vendor/github.com/percona/go-mysql/query/query.go @@ -0,0 +1,804 @@ +/* + Copyright (c) 2014-2015, Percona LLC and/or its affiliates. All rights reserved. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see +*/ + +// Package query provides functions to transform queries. +package query + +/* + Fingerprint is highly-specialized and operates on a single principle: + + 1. We only replace numbers, quoted strings, and value lists. + + Although we must handle a lot of details, exceptions, and special cases, + that principle distills the problem to its essence which makes it + possible to solve without a true SQL syntax parser (or regex). It means + that we can simply copy and ignore the vast majority of the query because + if it's not a number, quoted string, or value list then there's nothing + to do. With a regex solution we can stop there and simply do transformations + like s/\d+/?/g and s/'[^']*'/?/g but the details, exceptions, and specials + cases make that only a partial, crude solution because, for example, with + col = 'It\s' an escaped quote char.' now the regex needs to handle inner, + escaped quotes so it becomes more complicated and slower. There are many + more difficult problems like this. Consequently, neither regex nor this + solution can be simple, but at least this solution is very fast compared + to regex because it makes a single pass through the query whereas regex + makes as many passes as there are regexes and, worst case, a single regex + can make several passes due to backtracking. To handle problems like these, + this solution is a simple state machine. In the previous example problem, + once the first quote char (') is seen we enter the inQuote state. In this + state, if an escape char (\) is seen, this is remembered and the next char + is ignored. This allows us to correctly detect the real ending quote char: + it's the first unescaped quote char. Consequently, we can simply ignore + everything else in the quoted value. The same basic logic applies to numbers + and value lists. + + With that principle and basic logic in mind, you'll notice three major code + blocks: + 1. Skip parts of the query for certain states. + 2. Change state based on rune and current state. + 3. Copy a slice of the query into the fingerprint. + The order is important because it enforces the basic logic: once we enter + certain states we must process and finish them first because in these states + we ignore everything but whatever ends the state. As mentioned above, the + inQuote state is handled in the first block. When in this state, only the + real ending quote char ends it. Consequently nothing will trick the parser; + for example the quoted value in col="INSERT INTO t VALUES ('no problem')" + will not be mistaken for another query and another quoted value. -- + The second block is primarily where cpFromOffset and cpToOffset are set + which are used by the third block to copy that range of the query into the + fingerprint. The second block stops, switches state, copies, and lets + the code enter the first block which skips through number, quoted value, or + value list that the second block found. +*/ + +import ( + "crypto/md5" + "fmt" + "io" + "strings" +) + +const ( + unknown byte = iota + inWord // \S+ + inNumber // [0-9a-fA-Fx.-] + inSpace // space, tab, \r, \n + inOp // [=<>!] (usually precedes a number) + opOrNumber // + in 2 + 2 or +3e-9 + inQuote // '...' or "..." + subOrOLC // - or start of -- comment + inDash // -- begins a one-line comment if followed by space + inOLC // -- comment (at least one space after dash is required) + divOrMLC // / operator or start of /* comment */ + mlcOrMySQLCode // /* comment */ or /*! MySQL-specific code */ + inMLC // /* comment */ + inValues // VALUES (1), ..., (N) + moreValuesOrUnknown // , (2nd+) or ON DUPLICATE KEY or end of query + orderBy // ORDER BY + onDupeKeyUpdate // ON DUPLICATE KEY UPDATE + inNumberInWord // e.g. db23 + inBackticks // `table-1` + inMySQLCode // /*! MySQL-specific code */ +) + +var stateName map[byte]string = map[byte]string{ + 0: "unknown", + 1: "inWord", + 2: "inNumber", + 3: "inSpace", + 4: "inOp", + 5: "opOrNumber", + 6: "inQuote", + 7: "subOrOLC", + 8: "inDash", + 9: "inOLC", + 10: "divOrMLC", + 11: "mlcOrMySQLCode", + 12: "inMLC", + 13: "inValues", + 14: "moreValuesOrUnknown", + 15: "orderBy", + 16: "onDupeKeyUpdate", + 17: "inNumberInWord", + 18: "inBackTicks", + 19: "inMySQLCode", +} + +// Debug prints very verbose tracing information to STDOUT. +var Debug bool = false + +// ReplaceNumbersInWords enables replacing numbers in words. For example: +// `SELECT c FROM org235.t` -> `SELECT c FROM org?.t`. For more examples +// look at test query_test.go/TestFingerprintWithNumberInDbName. +var ReplaceNumbersInWords = false + +// Fingerprint returns the canonical form of q. The primary transformations are: +// - Replace values with ? +// - Collapse whitespace +// - Remove comments +// - Lowercase everything +// Additional trasnformations are performed which change the syntax of the +// original query without affecting its performance characteristics. For +// example, "ORDER BY col ASC" is the same as "ORDER BY col", so "ASC" in the +// fingerprint is removed. +func Fingerprint(q string) string { + q += " " // need range to run off end of original query + prevWord := "" + f := make([]byte, len(q)) + fi := 0 + pr := rune(0) // previous rune + s := unknown // current state + sqlState := unknown + quoteChar := rune(0) + cpFromOffset := 0 + cpToOffset := 0 + addSpace := false + escape := false + parOpen := 0 + parOpenTotal := 0 + valueNo := 0 + firstPar := 0 + + for qi, r := range q { + if Debug { + fmt.Printf("\n%d:%d %s/%s [%d:%d] %x %q\n", qi, fi, stateName[s], stateName[sqlState], cpFromOffset, cpToOffset, r, r) + } + + /** + * 1. Skip parts of the query for certain states. + */ + + if s == inQuote { + // We're in a 'quoted value' or "quoted value". The quoted value + // ends at the first non-escaped matching quote character (' or "). + if r != quoteChar { + // The only char inside a quoted value we need to track is \, + // the escape char. This allows us to tell that the 2nd ' in + // '\'' is escaped, not the ending quote char. + if escape { + if Debug { + fmt.Println("Ignore quoted literal") + } + escape = false + } else if r == '\\' { + if Debug { + fmt.Println("Escape") + } + escape = true + } else { + if Debug { + fmt.Println("Ignore quoted value") + } + } + } else if escape { + // \' or \" + if Debug { + fmt.Println("Quote literal") + } + escape = false + } else { + // 'foo' -> ? + // "foo" -> ? + if Debug { + fmt.Println("Quote end") + } + escape = false + + // qi = the closing quote char, so +1 to ensure we don't copy + // anything before this, i.e. quoted value is done, move on. + cpFromOffset = qi + 1 + + if sqlState == inValues { + // ('Hello world!', ...) -> VALUES (, ...) + // The inValues state uses this state to skip quoted values, + // so we don't replace them with ?; the inValues blocks will + // replace the entire value list with ?+. + s = inValues + } else { + f[fi] = '?' + fi++ + s = unknown + } + } + continue + } else if s == inBackticks { + if r != '`' { + // The only char inside a quoted value we need to track is \, + // the escape char. This allows us to tell that the 2nd ' in + // '\`' is escaped, not the ending quote char. + if escape { + if Debug { + fmt.Println("Ignore backtick literal") + } + escape = false + } else if r == '\\' { + if Debug { + fmt.Println("Escape") + } + escape = true + } else { + if Debug { + fmt.Println("Ignore quoted value") + } + } + } else if escape { + // \` + if Debug { + fmt.Println("Quote literal") + } + escape = false + } else { + if Debug { + fmt.Println("Quote end") + } + escape = false + + // qi = the closing backtick, so +1 to ensure we don't copy + // anything before this, i.e. quoted value is done, move on. + //cpFromOffset = qi + 1 + cpToOffset = qi + 1 + + s = inWord + } + continue + + } else if s == inNumberInWord { + // Replaces number in words with ? + // e.g. `db37` to `db?` + // Parser can fall into inNumberInWord only if + // option ReplaceNumbersInWords is turned on + if r >= '0' && r <= '9' { + if Debug { + fmt.Println("Ignore digit in word") + } + continue + } + // 123 -> ?, 0xff -> ?, 1e-9 -> ?, etc. + if Debug { + fmt.Println("Number in word end") + } + f[fi] = '?' + fi++ + cpFromOffset = qi + if isSpace(r) { + s = unknown + } else { + s = inWord + } + } else if s == inNumber { + // We're in a number which can be something simple like 123 or + // something trickier like 1e-9 or 0xFF. The pathological case is + // like 12ff: this is valid hex number and a valid ident (e.g. table + // name). We can't detect this; the best we can do is realize that + // 12ffz is not a number because of the z. + if (r >= '0' && r <= '9') || (r >= 'a' && r <= 'f') || (r >= 'A' && r <= 'F') || r == '.' || r == 'x' || r == '-' { + if Debug { + fmt.Println("Ignore digit") + } + continue + } + if (r >= 'g' && r <= 'z') || (r >= 'G' && r <= 'Z') || r == '_' { + if Debug { + fmt.Println("Not a number") + } + cpToOffset = qi + s = inWord + } else if sqlState == inMySQLCode { + // If we are in /*![version] ... */, keep the version number + cpToOffset = qi + s = inWord + sqlState = unknown + } else { + // 123 -> ?, 0xff -> ?, 1e-9 -> ?, etc. + if Debug { + fmt.Println("Number end") + } + f[fi] = '?' + fi++ + cpFromOffset = qi + cpToOffset = qi + s = unknown + } + } else if s == inValues { + // We're in the (val1),...,(valN) after IN or VALUE[S]. A single + // () value ends when the parenthesis are balanced, but... + if r == ')' { + parOpen-- + parOpenTotal++ + if Debug { + fmt.Println("Close parenthesis", parOpen) + } + } else if r == '(' { + parOpen++ + if Debug { + fmt.Println("Open parenthesis", parOpen) + } + if parOpen == 1 { + firstPar = qi + } + } else if r == '\'' || r == '"' { + // VALUES ('Hello world!') -> enter inQuote state to skip + // the quoted value so ')' in 'This ) is a trick' doesn't + // balance an outer parenthesis. + if Debug { + fmt.Println("Quote begin") + } + s = inQuote + quoteChar = r + continue + } else if isSpace(r) { + if Debug { + fmt.Println("Space") + } + continue + } + if parOpen > 0 { + // Parenthesis are not balanced yet; i.e. haven't reached + // closing ) for this value. + continue + } + if parOpenTotal == 0 { + // SELECT value FROM t + if Debug { + fmt.Println("Literal values not VALUES()") + } + s = inWord + continue + } + // () -> (?+) only for first value + if Debug { + fmt.Println("Values end") + } + valueNo++ + if valueNo == 1 { + if qi-firstPar > 1 { + copy(f[fi:fi+4], "(?+)") + fi += 4 + } else { + // INSERT INTO t VALUES () + copy(f[fi:fi+2], "()") + fi += 2 + } + firstPar = 0 + } + // ... the difficult part is that there may be other values, e.g. + // (1), (2), (3). So we enter the following state. The values list + // ends when the next char is not a comma. + s = moreValuesOrUnknown + pr = r + cpFromOffset = qi + 1 + parOpenTotal = 0 + continue + } else if s == inMLC { + // We're in a /* mutli-line comments */. Skip and ignore it all. + if pr == '*' && r == '/' { + // /* foo */ -> (nothing) + if Debug { + fmt.Printf("Multi-line comment end. pr: %s\n", string(pr)) + } + s = unknown + } else { + pr = r + if Debug { + fmt.Println("Ignore multi-line comment content") + } + } + continue + } else if s == mlcOrMySQLCode { + // We're at the start of either a /* multi-line comment */ or some + // /*![version] some MySQL-specific code */. The ! after the /* + // determines which one. + if r != '!' { + if Debug { + fmt.Println("Multi-line comment") + } + s = inMLC + continue + } else { + // /*![version] SQL_NO_CACHE */ -> /*![version] SQL_NO_CACHE */ (no change) + if Debug { + fmt.Println("MySQL-specific code") + } + s = inWord + sqlState = inMySQLCode + } + } else if s == inOLC { + // We're in a -- one line comment. A space after -- is required. + // It ends at the end of the line, but there can be more query after + // it like: + // SELECT * -- comment + // FROM t + // is really "SELECT * FROM t". + if r == 0x0A { // newline + if Debug { + fmt.Println("One-line comment end") + } + s = unknown + } + continue + } else if isSpace(r) && isSpace(pr) { + // All space is collapsed into a single space, so if this char is + // a space and the previous was too, then skip the extra space. + if Debug { + fmt.Println("Skip space") + } + // +1 here ensures we actually skip the extra space in certain + // cases like "select \n-- bar\n foo". When a part of the query + // triggers a copy of preceding chars, if the only preceding char + // is a space then it's incorrectly copied, but +1 sets cpFromOffset + // to the same offset as the trigger char, thus avoiding the copy. + // For example in that ^ query, the offsets are: + // 0 's' + // 1 'e' + // 2 'l' + // 3 'e' + // 4 'c' + // 5 't' + // 6 ' ' + // 7 '\n' + // 8 '-' + // After copying 'select ', we are here @ 7 and intend to skip the + // newline. Next, the '-' @ 8 triggers a copy of any preceding + // chars. So here if we set cpFromOffset = 7 then 7:8 is copied, + // the newline, but setting cpFromOffset = 7 + 1 is 8:8 and so + // nothing is copied as we want. Actually, cpToOffset is still 6 + // in this case, but 8:6 avoids the copy too. + cpFromOffset = qi + 1 + pr = r + continue + } + + /** + * 2. Change state based on rune and current state. + */ + + switch { + case r >= 0x30 && r <= 0x39: // 0-9 + switch s { + case opOrNumber: + if Debug { + fmt.Println("+/-First digit") + } + cpToOffset = qi - 1 + s = inNumber + case inOp: + if Debug { + fmt.Println("First digit after operator") + } + cpToOffset = qi + s = inNumber + case inWord: + if pr == '(' { + if Debug { + fmt.Println("Number in function") + } + cpToOffset = qi + s = inNumber + } else if pr == ',' { + // foo,4 -- 4 may be a number literal or a word/ident + if Debug { + fmt.Println("Number or word") + } + s = inNumber + cpToOffset = qi + } else { + if Debug { + fmt.Println("Number in word") + } + if ReplaceNumbersInWords { + s = inNumberInWord + cpToOffset = qi + } + } + default: + if Debug { + fmt.Println("Number literal") + } + s = inNumber + cpToOffset = qi + } + case isSpace(r): + if s == unknown { + if Debug { + fmt.Println("Lost in space") + } + if fi > 0 && !isSpace(rune(f[fi-1])) { + if Debug { + fmt.Println("Add space") + } + f[fi] = ' ' + fi++ + // This is a common case: a space after skipping something, + // e.g. col = 'foo'. We want only the first space, + // so advance cpFromOffset to whatever is after the space + // and if it's more space then space skipping block will + // handle it. + cpFromOffset = qi + 1 + } + } else if s == inDash { + if Debug { + fmt.Println("One-line comment begin") + } + s = inOLC + if cpToOffset > 2 { + cpToOffset = qi - 2 + } + } else if s == moreValuesOrUnknown { + if Debug { + fmt.Println("Space after values") + } + if valueNo == 1 { + f[fi] = ' ' + fi++ + } + } else { + if Debug { + fmt.Println("Word end") + } + word := strings.ToLower(q[cpFromOffset:qi]) + // Only match USE if it is the first word in the query, otherwise, + // it could be a USE INDEX + if word == "use" && prevWord == "" { + return "use ?" + } else if (word == "null" && (prevWord != "is" && prevWord != "not")) || word == "null," { + if Debug { + fmt.Println("NULL as value") + } + f[fi] = '?' + fi++ + if word[len(word)-1] == ',' { + f[fi] = ',' + fi++ + } + f[fi] = ' ' + fi++ + cpFromOffset = qi + 1 + } else if prevWord == "order" && word == "by" { + if Debug { + fmt.Println("ORDER BY begin") + } + sqlState = orderBy + } else if sqlState == orderBy && wordIn(word, "asc", "asc,", "asc ") { + if Debug { + fmt.Println("ORDER BY ASC") + } + cpFromOffset = qi + if word[len(word)-1] == ',' { + fi-- + f[fi] = ',' + f[fi+1] = ' ' + fi += 2 + } + } else if prevWord == "key" && word == "update" { + if Debug { + fmt.Println("ON DUPLICATE KEY UPDATE begin") + } + sqlState = onDupeKeyUpdate + } + s = inSpace + cpToOffset = qi + addSpace = true + } + case r == '\'' || r == '"': + if pr != '\\' { + if s != inQuote { + if Debug { + fmt.Println("Quote begin") + } + s = inQuote + quoteChar = r + cpToOffset = qi + if pr == 'x' || pr == 'b' { + if Debug { + fmt.Println("Hex/binary value") + } + // We're at the first quote char of x'0F' + // (or b'0101', etc.), so -2 for the quote char and + // the x or b char to copy anything before and up to + // this value. + cpToOffset = -2 + } + } + } + case r == '`': + if pr != '\\' { + if s != inBackticks { + if Debug { + fmt.Println("Beckticks begin") + } + s = inBackticks + } + + } + case r == '=' || r == '<' || r == '>' || r == '!': + if Debug { + fmt.Println("Operator") + } + if s != inWord && s != inOp { + cpFromOffset = qi + } + s = inOp + case r == '/': + if Debug { + fmt.Println("Op or multi-line comment") + } + s = divOrMLC + case r == '*' && s == divOrMLC: + if Debug { + fmt.Println("Multi-line comment or MySQL-specific code") + } + s = mlcOrMySQLCode + case r == '+': + if Debug { + fmt.Println("Operator or number") + } + s = opOrNumber + case r == '-': + if pr == '-' { + if Debug { + fmt.Println("Dash") + } + s = inDash + } else { + if Debug { + fmt.Println("Operator or number") + } + s = opOrNumber + } + case r == '.': + if s == inNumber || s == inOp { + if Debug { + fmt.Println("Floating point number") + } + s = inNumber + cpToOffset = qi + } + case r == '(': + if prevWord == "call" { + // 'CALL foo(...)' -> 'call foo' + if Debug { + fmt.Println("CALL sp_name") + } + return "call " + q[cpFromOffset:qi] + } else if sqlState != onDupeKeyUpdate && (((s == inSpace || s == moreValuesOrUnknown) && (prevWord == "value" || prevWord == "values" || prevWord == "in")) || wordIn(q[cpFromOffset:qi], "value", "values", "in")) { + // VALUE(, VALUE (, VALUES(, VALUES (, IN(, or IN( + // but not after ON DUPLICATE KEY UPDATE + if Debug { + fmt.Println("Values begin") + } + s = inValues + sqlState = inValues + parOpen = 1 + firstPar = qi + if valueNo == 0 { + cpToOffset = qi + } + } else if s != inWord { + if Debug { + fmt.Println("Random (") + } + valueNo = 0 + cpFromOffset = qi + s = inWord + } + case r == ',' && s == moreValuesOrUnknown: + if Debug { + fmt.Println("More values") + } + case r == ':' && prevWord == "administrator": + // 'administrator command: Init DB' -> 'administrator command: Init DB' (no change) + if Debug { + fmt.Println("Admin cmd") + } + return q[0 : len(q)-1] // original query minus the trailing space we added + case r == '#': + if Debug { + fmt.Println("One-line comment begin") + } + addSpace = false + s = inOLC + default: + if s != inWord && s != inOp { + // If in a word or operator then keep copying the query, else + // previous chars were being ignored for some reasons but now + // we should start copying again, so set cpFromOffset. Example: + // col=NOW(). 'col' will be set to copy, but then '=' will put + // us in inOp state which, if a value follows, will trigger a + // copy of "col=", but "NOW()" is not a value so "N" is caught + // here and since s=inOp still we do not copy yet (this block is + // is not entered). + if Debug { + fmt.Println("Random character") + } + valueNo = 0 + cpFromOffset = qi + + if sqlState == inValues { + // Values are comma-separated, so the first random char + // marks the end of the VALUE() or IN() list. + if Debug { + fmt.Println("No more values") + } + sqlState = unknown + } + } + s = inWord + } + + /** + * 3. Copy a slice of the query into the fingerprint. + */ + + if cpToOffset > cpFromOffset { + l := cpToOffset - cpFromOffset + prevWord = strings.ToLower(q[cpFromOffset:cpToOffset]) + if Debug { + fmt.Printf("copy '%s' (%d:%d, %d:%d) %d\n", prevWord, fi, fi+l, cpFromOffset, cpToOffset, l) + } + copy(f[fi:fi+l], prevWord) + fi += l + cpFromOffset = cpToOffset + if wordIn(prevWord, "in", "value", "values") && sqlState != onDupeKeyUpdate { + // IN () -> in(?+) + // VALUES () -> values(?+) + addSpace = false + s = inValues + sqlState = inValues + } else if addSpace { + if Debug { + fmt.Println("Add space") + } + f[fi] = ' ' + fi++ + cpFromOffset++ + addSpace = false + } + } + pr = r + } + + // Remove trailing spaces. + for fi > 0 && isSpace(rune(f[fi-1])) { + fi-- + } + + // Clean up control characters, and return the fingerprint + return strings.Replace(string(f[0:fi]), "\x00", "", -1) +} + +func isSpace(r rune) bool { + return r == 0x20 || r == 0x09 || r == 0x0D || r == 0x0A +} + +func wordIn(q string, words ...string) bool { + q = strings.ToLower(q) + for _, word := range words { + if q == word { + return true + } + } + return false +} + +// Id returns the right-most 16 characters of the MD5 checksum of fingerprint. +// Query IDs are the shortest way to uniquely identify queries. +func Id(fingerprint string) string { + id := md5.New() + io.WriteString(id, fingerprint) + h := fmt.Sprintf("%x", id.Sum(nil)) + return strings.ToUpper(h[16:32]) +} diff --git a/vendor/github.com/russross/blackfriday/LICENSE.txt b/vendor/github.com/russross/blackfriday/LICENSE.txt new file mode 100644 index 00000000..2885af36 --- /dev/null +++ b/vendor/github.com/russross/blackfriday/LICENSE.txt @@ -0,0 +1,29 @@ +Blackfriday is distributed under the Simplified BSD License: + +> Copyright © 2011 Russ Ross +> All rights reserved. +> +> Redistribution and use in source and binary forms, with or without +> modification, are permitted provided that the following conditions +> are met: +> +> 1. Redistributions of source code must retain the above copyright +> notice, this list of conditions and the following disclaimer. +> +> 2. Redistributions in binary form must reproduce the above +> copyright notice, this list of conditions and the following +> disclaimer in the documentation and/or other materials provided with +> the distribution. +> +> THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +> "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +> LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +> FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +> COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +> INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +> BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +> LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +> CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +> LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +> ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +> POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/russross/blackfriday/README.md b/vendor/github.com/russross/blackfriday/README.md new file mode 100644 index 00000000..e0066b0f --- /dev/null +++ b/vendor/github.com/russross/blackfriday/README.md @@ -0,0 +1,363 @@ +Blackfriday +[![Build Status][BuildSVG]][BuildURL] +[![Godoc][GodocV2SVG]][GodocV2URL] +=========== + +Blackfriday is a [Markdown][1] processor implemented in [Go][2]. It +is paranoid about its input (so you can safely feed it user-supplied +data), it is fast, it supports common extensions (tables, smart +punctuation substitutions, etc.), and it is safe for all utf-8 +(unicode) input. + +HTML output is currently supported, along with Smartypants +extensions. + +It started as a translation from C of [Sundown][3]. + + +Installation +------------ + +Blackfriday is compatible with any modern Go release. With Go and git installed: + + go get -u gopkg.in/russross/blackfriday.v2 + +will download, compile, and install the package into your `$GOPATH` directory +hierarchy. + + +Versions +-------- + +Currently maintained and recommended version of Blackfriday is `v2`. It's being +developed on its own branch: https://github.com/russross/blackfriday/tree/v2 and the +documentation is available at +https://godoc.org/gopkg.in/russross/blackfriday.v2. + +It is `go get`-able via [gopkg.in][6] at `gopkg.in/russross/blackfriday.v2`, +but we highly recommend using package management tool like [dep][7] or +[Glide][8] and make use of semantic versioning. With package management you +should import `github.com/russross/blackfriday` and specify that you're using +version 2.0.0. + +Version 2 offers a number of improvements over v1: + +* Cleaned up API +* A separate call to [`Parse`][4], which produces an abstract syntax tree for + the document +* Latest bug fixes +* Flexibility to easily add your own rendering extensions + +Potential drawbacks: + +* Our benchmarks show v2 to be slightly slower than v1. Currently in the + ballpark of around 15%. +* API breakage. If you can't afford modifying your code to adhere to the new API + and don't care too much about the new features, v2 is probably not for you. +* Several bug fixes are trailing behind and still need to be forward-ported to + v2. See issue [#348](https://github.com/russross/blackfriday/issues/348) for + tracking. + +If you are still interested in the legacy `v1`, you can import it from +`github.com/russross/blackfriday`. Documentation for the legacy v1 can be found +here: https://godoc.org/github.com/russross/blackfriday + +### Known issue with `dep` + +There is a known problem with using Blackfriday v1 _transitively_ and `dep`. +Currently `dep` prioritizes semver versions over anything else, and picks the +latest one, plus it does not apply a `[[constraint]]` specifier to transitively +pulled in packages. So if you're using something that uses Blackfriday v1, but +that something does not use `dep` yet, you will get Blackfriday v2 pulled in and +your first dependency will fail to build. + +There are couple of fixes for it, documented here: +https://github.com/golang/dep/blob/master/docs/FAQ.md#how-do-i-constrain-a-transitive-dependencys-version + +Meanwhile, `dep` team is working on a more general solution to the constraints +on transitive dependencies problem: https://github.com/golang/dep/issues/1124. + + +Usage +----- + +### v1 + +For basic usage, it is as simple as getting your input into a byte +slice and calling: + + output := blackfriday.MarkdownBasic(input) + +This renders it with no extensions enabled. To get a more useful +feature set, use this instead: + + output := blackfriday.MarkdownCommon(input) + +### v2 + +For the most sensible markdown processing, it is as simple as getting your input +into a byte slice and calling: + +```go +output := blackfriday.Run(input) +``` + +Your input will be parsed and the output rendered with a set of most popular +extensions enabled. If you want the most basic feature set, corresponding with +the bare Markdown specification, use: + +```go +output := blackfriday.Run(input, blackfriday.WithNoExtensions()) +``` + +### Sanitize untrusted content + +Blackfriday itself does nothing to protect against malicious content. If you are +dealing with user-supplied markdown, we recommend running Blackfriday's output +through HTML sanitizer such as [Bluemonday][5]. + +Here's an example of simple usage of Blackfriday together with Bluemonday: + +```go +import ( + "github.com/microcosm-cc/bluemonday" + "gopkg.in/russross/blackfriday.v2" +) + +// ... +unsafe := blackfriday.Run(input) +html := bluemonday.UGCPolicy().SanitizeBytes(unsafe) +``` + +### Custom options, v1 + +If you want to customize the set of options, first get a renderer +(currently only the HTML output engine), then use it to +call the more general `Markdown` function. For examples, see the +implementations of `MarkdownBasic` and `MarkdownCommon` in +`markdown.go`. + +### Custom options, v2 + +If you want to customize the set of options, use `blackfriday.WithExtensions`, +`blackfriday.WithRenderer` and `blackfriday.WithRefOverride`. + +### `blackfriday-tool` + +You can also check out `blackfriday-tool` for a more complete example +of how to use it. Download and install it using: + + go get github.com/russross/blackfriday-tool + +This is a simple command-line tool that allows you to process a +markdown file using a standalone program. You can also browse the +source directly on github if you are just looking for some example +code: + +* + +Note that if you have not already done so, installing +`blackfriday-tool` will be sufficient to download and install +blackfriday in addition to the tool itself. The tool binary will be +installed in `$GOPATH/bin`. This is a statically-linked binary that +can be copied to wherever you need it without worrying about +dependencies and library versions. + +### Sanitized anchor names + +Blackfriday includes an algorithm for creating sanitized anchor names +corresponding to a given input text. This algorithm is used to create +anchors for headings when `EXTENSION_AUTO_HEADER_IDS` is enabled. The +algorithm has a specification, so that other packages can create +compatible anchor names and links to those anchors. + +The specification is located at https://godoc.org/github.com/russross/blackfriday#hdr-Sanitized_Anchor_Names. + +[`SanitizedAnchorName`](https://godoc.org/github.com/russross/blackfriday#SanitizedAnchorName) exposes this functionality, and can be used to +create compatible links to the anchor names generated by blackfriday. +This algorithm is also implemented in a small standalone package at +[`github.com/shurcooL/sanitized_anchor_name`](https://godoc.org/github.com/shurcooL/sanitized_anchor_name). It can be useful for clients +that want a small package and don't need full functionality of blackfriday. + + +Features +-------- + +All features of Sundown are supported, including: + +* **Compatibility**. The Markdown v1.0.3 test suite passes with + the `--tidy` option. Without `--tidy`, the differences are + mostly in whitespace and entity escaping, where blackfriday is + more consistent and cleaner. + +* **Common extensions**, including table support, fenced code + blocks, autolinks, strikethroughs, non-strict emphasis, etc. + +* **Safety**. Blackfriday is paranoid when parsing, making it safe + to feed untrusted user input without fear of bad things + happening. The test suite stress tests this and there are no + known inputs that make it crash. If you find one, please let me + know and send me the input that does it. + + NOTE: "safety" in this context means *runtime safety only*. In order to + protect yourself against JavaScript injection in untrusted content, see + [this example](https://github.com/russross/blackfriday#sanitize-untrusted-content). + +* **Fast processing**. It is fast enough to render on-demand in + most web applications without having to cache the output. + +* **Thread safety**. You can run multiple parsers in different + goroutines without ill effect. There is no dependence on global + shared state. + +* **Minimal dependencies**. Blackfriday only depends on standard + library packages in Go. The source code is pretty + self-contained, so it is easy to add to any project, including + Google App Engine projects. + +* **Standards compliant**. Output successfully validates using the + W3C validation tool for HTML 4.01 and XHTML 1.0 Transitional. + + +Extensions +---------- + +In addition to the standard markdown syntax, this package +implements the following extensions: + +* **Intra-word emphasis supression**. The `_` character is + commonly used inside words when discussing code, so having + markdown interpret it as an emphasis command is usually the + wrong thing. Blackfriday lets you treat all emphasis markers as + normal characters when they occur inside a word. + +* **Tables**. Tables can be created by drawing them in the input + using a simple syntax: + + ``` + Name | Age + --------|------ + Bob | 27 + Alice | 23 + ``` + +* **Fenced code blocks**. In addition to the normal 4-space + indentation to mark code blocks, you can explicitly mark them + and supply a language (to make syntax highlighting simple). Just + mark it like this: + + ``` go + func getTrue() bool { + return true + } + ``` + + You can use 3 or more backticks to mark the beginning of the + block, and the same number to mark the end of the block. + + To preserve classes of fenced code blocks while using the bluemonday + HTML sanitizer, use the following policy: + + ``` go + p := bluemonday.UGCPolicy() + p.AllowAttrs("class").Matching(regexp.MustCompile("^language-[a-zA-Z0-9]+$")).OnElements("code") + html := p.SanitizeBytes(unsafe) + ``` + +* **Definition lists**. A simple definition list is made of a single-line + term followed by a colon and the definition for that term. + + Cat + : Fluffy animal everyone likes + + Internet + : Vector of transmission for pictures of cats + + Terms must be separated from the previous definition by a blank line. + +* **Footnotes**. A marker in the text that will become a superscript number; + a footnote definition that will be placed in a list of footnotes at the + end of the document. A footnote looks like this: + + This is a footnote.[^1] + + [^1]: the footnote text. + +* **Autolinking**. Blackfriday can find URLs that have not been + explicitly marked as links and turn them into links. + +* **Strikethrough**. Use two tildes (`~~`) to mark text that + should be crossed out. + +* **Hard line breaks**. With this extension enabled (it is off by + default in the `MarkdownBasic` and `MarkdownCommon` convenience + functions), newlines in the input translate into line breaks in + the output. + +* **Smart quotes**. Smartypants-style punctuation substitution is + supported, turning normal double- and single-quote marks into + curly quotes, etc. + +* **LaTeX-style dash parsing** is an additional option, where `--` + is translated into `–`, and `---` is translated into + `—`. This differs from most smartypants processors, which + turn a single hyphen into an ndash and a double hyphen into an + mdash. + +* **Smart fractions**, where anything that looks like a fraction + is translated into suitable HTML (instead of just a few special + cases like most smartypant processors). For example, `4/5` + becomes `45`, which renders as + 45. + + +Other renderers +--------------- + +Blackfriday is structured to allow alternative rendering engines. Here +are a few of note: + +* [github_flavored_markdown](https://godoc.org/github.com/shurcooL/github_flavored_markdown): + provides a GitHub Flavored Markdown renderer with fenced code block + highlighting, clickable heading anchor links. + + It's not customizable, and its goal is to produce HTML output + equivalent to the [GitHub Markdown API endpoint](https://developer.github.com/v3/markdown/#render-a-markdown-document-in-raw-mode), + except the rendering is performed locally. + +* [markdownfmt](https://github.com/shurcooL/markdownfmt): like gofmt, + but for markdown. + +* [LaTeX output](https://bitbucket.org/ambrevar/blackfriday-latex): + renders output as LaTeX. + + +TODO +---- + +* More unit testing +* Improve Unicode support. It does not understand all Unicode + rules (about what constitutes a letter, a punctuation symbol, + etc.), so it may fail to detect word boundaries correctly in + some instances. It is safe on all UTF-8 input. + + +License +------- + +[Blackfriday is distributed under the Simplified BSD License](LICENSE.txt) + + + [1]: https://daringfireball.net/projects/markdown/ "Markdown" + [2]: https://golang.org/ "Go Language" + [3]: https://github.com/vmg/sundown "Sundown" + [4]: https://godoc.org/gopkg.in/russross/blackfriday.v2#Parse "Parse func" + [5]: https://github.com/microcosm-cc/bluemonday "Bluemonday" + [6]: https://labix.org/gopkg.in "gopkg.in" + [7]: https://github.com/golang/dep/ "dep" + [8]: https://github.com/Masterminds/glide "Glide" + + [BuildSVG]: https://travis-ci.org/russross/blackfriday.svg?branch=master + [BuildURL]: https://travis-ci.org/russross/blackfriday + [GodocV2SVG]: https://godoc.org/gopkg.in/russross/blackfriday.v2?status.svg + [GodocV2URL]: https://godoc.org/gopkg.in/russross/blackfriday.v2 diff --git a/vendor/github.com/russross/blackfriday/block.go b/vendor/github.com/russross/blackfriday/block.go new file mode 100644 index 00000000..929638aa --- /dev/null +++ b/vendor/github.com/russross/blackfriday/block.go @@ -0,0 +1,1451 @@ +// +// Blackfriday Markdown Processor +// Available at http://github.com/russross/blackfriday +// +// Copyright © 2011 Russ Ross . +// Distributed under the Simplified BSD License. +// See README.md for details. +// + +// +// Functions to parse block-level elements. +// + +package blackfriday + +import ( + "bytes" + "strings" + "unicode" +) + +// Parse block-level data. +// Note: this function and many that it calls assume that +// the input buffer ends with a newline. +func (p *parser) block(out *bytes.Buffer, data []byte) { + if len(data) == 0 || data[len(data)-1] != '\n' { + panic("block input is missing terminating newline") + } + + // this is called recursively: enforce a maximum depth + if p.nesting >= p.maxNesting { + return + } + p.nesting++ + + // parse out one block-level construct at a time + for len(data) > 0 { + // prefixed header: + // + // # Header 1 + // ## Header 2 + // ... + // ###### Header 6 + if p.isPrefixHeader(data) { + data = data[p.prefixHeader(out, data):] + continue + } + + // block of preformatted HTML: + // + //
    + // ... + //
    + if data[0] == '<' { + if i := p.html(out, data, true); i > 0 { + data = data[i:] + continue + } + } + + // title block + // + // % stuff + // % more stuff + // % even more stuff + if p.flags&EXTENSION_TITLEBLOCK != 0 { + if data[0] == '%' { + if i := p.titleBlock(out, data, true); i > 0 { + data = data[i:] + continue + } + } + } + + // blank lines. note: returns the # of bytes to skip + if i := p.isEmpty(data); i > 0 { + data = data[i:] + continue + } + + // indented code block: + // + // func max(a, b int) int { + // if a > b { + // return a + // } + // return b + // } + if p.codePrefix(data) > 0 { + data = data[p.code(out, data):] + continue + } + + // fenced code block: + // + // ``` go info string here + // func fact(n int) int { + // if n <= 1 { + // return n + // } + // return n * fact(n-1) + // } + // ``` + if p.flags&EXTENSION_FENCED_CODE != 0 { + if i := p.fencedCodeBlock(out, data, true); i > 0 { + data = data[i:] + continue + } + } + + // horizontal rule: + // + // ------ + // or + // ****** + // or + // ______ + if p.isHRule(data) { + p.r.HRule(out) + var i int + for i = 0; data[i] != '\n'; i++ { + } + data = data[i:] + continue + } + + // block quote: + // + // > A big quote I found somewhere + // > on the web + if p.quotePrefix(data) > 0 { + data = data[p.quote(out, data):] + continue + } + + // table: + // + // Name | Age | Phone + // ------|-----|--------- + // Bob | 31 | 555-1234 + // Alice | 27 | 555-4321 + if p.flags&EXTENSION_TABLES != 0 { + if i := p.table(out, data); i > 0 { + data = data[i:] + continue + } + } + + // an itemized/unordered list: + // + // * Item 1 + // * Item 2 + // + // also works with + or - + if p.uliPrefix(data) > 0 { + data = data[p.list(out, data, 0):] + continue + } + + // a numbered/ordered list: + // + // 1. Item 1 + // 2. Item 2 + if p.oliPrefix(data) > 0 { + data = data[p.list(out, data, LIST_TYPE_ORDERED):] + continue + } + + // definition lists: + // + // Term 1 + // : Definition a + // : Definition b + // + // Term 2 + // : Definition c + if p.flags&EXTENSION_DEFINITION_LISTS != 0 { + if p.dliPrefix(data) > 0 { + data = data[p.list(out, data, LIST_TYPE_DEFINITION):] + continue + } + } + + // anything else must look like a normal paragraph + // note: this finds underlined headers, too + data = data[p.paragraph(out, data):] + } + + p.nesting-- +} + +func (p *parser) isPrefixHeader(data []byte) bool { + if data[0] != '#' { + return false + } + + if p.flags&EXTENSION_SPACE_HEADERS != 0 { + level := 0 + for level < 6 && data[level] == '#' { + level++ + } + if data[level] != ' ' { + return false + } + } + return true +} + +func (p *parser) prefixHeader(out *bytes.Buffer, data []byte) int { + level := 0 + for level < 6 && data[level] == '#' { + level++ + } + i := skipChar(data, level, ' ') + end := skipUntilChar(data, i, '\n') + skip := end + id := "" + if p.flags&EXTENSION_HEADER_IDS != 0 { + j, k := 0, 0 + // find start/end of header id + for j = i; j < end-1 && (data[j] != '{' || data[j+1] != '#'); j++ { + } + for k = j + 1; k < end && data[k] != '}'; k++ { + } + // extract header id iff found + if j < end && k < end { + id = string(data[j+2 : k]) + end = j + skip = k + 1 + for end > 0 && data[end-1] == ' ' { + end-- + } + } + } + for end > 0 && data[end-1] == '#' { + if isBackslashEscaped(data, end-1) { + break + } + end-- + } + for end > 0 && data[end-1] == ' ' { + end-- + } + if end > i { + if id == "" && p.flags&EXTENSION_AUTO_HEADER_IDS != 0 { + id = SanitizedAnchorName(string(data[i:end])) + } + work := func() bool { + p.inline(out, data[i:end]) + return true + } + p.r.Header(out, work, level, id) + } + return skip +} + +func (p *parser) isUnderlinedHeader(data []byte) int { + // test of level 1 header + if data[0] == '=' { + i := skipChar(data, 1, '=') + i = skipChar(data, i, ' ') + if data[i] == '\n' { + return 1 + } else { + return 0 + } + } + + // test of level 2 header + if data[0] == '-' { + i := skipChar(data, 1, '-') + i = skipChar(data, i, ' ') + if data[i] == '\n' { + return 2 + } else { + return 0 + } + } + + return 0 +} + +func (p *parser) titleBlock(out *bytes.Buffer, data []byte, doRender bool) int { + if data[0] != '%' { + return 0 + } + splitData := bytes.Split(data, []byte("\n")) + var i int + for idx, b := range splitData { + if !bytes.HasPrefix(b, []byte("%")) { + i = idx // - 1 + break + } + } + + data = bytes.Join(splitData[0:i], []byte("\n")) + p.r.TitleBlock(out, data) + + return len(data) +} + +func (p *parser) html(out *bytes.Buffer, data []byte, doRender bool) int { + var i, j int + + // identify the opening tag + if data[0] != '<' { + return 0 + } + curtag, tagfound := p.htmlFindTag(data[1:]) + + // handle special cases + if !tagfound { + // check for an HTML comment + if size := p.htmlComment(out, data, doRender); size > 0 { + return size + } + + // check for an
    tag + if size := p.htmlHr(out, data, doRender); size > 0 { + return size + } + + // check for HTML CDATA + if size := p.htmlCDATA(out, data, doRender); size > 0 { + return size + } + + // no special case recognized + return 0 + } + + // look for an unindented matching closing tag + // followed by a blank line + found := false + /* + closetag := []byte("\n") + j = len(curtag) + 1 + for !found { + // scan for a closing tag at the beginning of a line + if skip := bytes.Index(data[j:], closetag); skip >= 0 { + j += skip + len(closetag) + } else { + break + } + + // see if it is the only thing on the line + if skip := p.isEmpty(data[j:]); skip > 0 { + // see if it is followed by a blank line/eof + j += skip + if j >= len(data) { + found = true + i = j + } else { + if skip := p.isEmpty(data[j:]); skip > 0 { + j += skip + found = true + i = j + } + } + } + } + */ + + // if not found, try a second pass looking for indented match + // but not if tag is "ins" or "del" (following original Markdown.pl) + if !found && curtag != "ins" && curtag != "del" { + i = 1 + for i < len(data) { + i++ + for i < len(data) && !(data[i-1] == '<' && data[i] == '/') { + i++ + } + + if i+2+len(curtag) >= len(data) { + break + } + + j = p.htmlFindEnd(curtag, data[i-1:]) + + if j > 0 { + i += j - 1 + found = true + break + } + } + } + + if !found { + return 0 + } + + // the end of the block has been found + if doRender { + // trim newlines + end := i + for end > 0 && data[end-1] == '\n' { + end-- + } + p.r.BlockHtml(out, data[:end]) + } + + return i +} + +func (p *parser) renderHTMLBlock(out *bytes.Buffer, data []byte, start int, doRender bool) int { + // html block needs to end with a blank line + if i := p.isEmpty(data[start:]); i > 0 { + size := start + i + if doRender { + // trim trailing newlines + end := size + for end > 0 && data[end-1] == '\n' { + end-- + } + p.r.BlockHtml(out, data[:end]) + } + return size + } + return 0 +} + +// HTML comment, lax form +func (p *parser) htmlComment(out *bytes.Buffer, data []byte, doRender bool) int { + i := p.inlineHTMLComment(out, data) + return p.renderHTMLBlock(out, data, i, doRender) +} + +// HTML CDATA section +func (p *parser) htmlCDATA(out *bytes.Buffer, data []byte, doRender bool) int { + const cdataTag = "') { + i++ + } + i++ + // no end-of-comment marker + if i >= len(data) { + return 0 + } + return p.renderHTMLBlock(out, data, i, doRender) +} + +// HR, which is the only self-closing block tag considered +func (p *parser) htmlHr(out *bytes.Buffer, data []byte, doRender bool) int { + if data[0] != '<' || (data[1] != 'h' && data[1] != 'H') || (data[2] != 'r' && data[2] != 'R') { + return 0 + } + if data[3] != ' ' && data[3] != '/' && data[3] != '>' { + // not an
    tag after all; at least not a valid one + return 0 + } + + i := 3 + for data[i] != '>' && data[i] != '\n' { + i++ + } + + if data[i] == '>' { + return p.renderHTMLBlock(out, data, i+1, doRender) + } + + return 0 +} + +func (p *parser) htmlFindTag(data []byte) (string, bool) { + i := 0 + for isalnum(data[i]) { + i++ + } + key := string(data[:i]) + if _, ok := blockTags[key]; ok { + return key, true + } + return "", false +} + +func (p *parser) htmlFindEnd(tag string, data []byte) int { + // assume data[0] == '<' && data[1] == '/' already tested + + // check if tag is a match + closetag := []byte("") + if !bytes.HasPrefix(data, closetag) { + return 0 + } + i := len(closetag) + + // check that the rest of the line is blank + skip := 0 + if skip = p.isEmpty(data[i:]); skip == 0 { + return 0 + } + i += skip + skip = 0 + + if i >= len(data) { + return i + } + + if p.flags&EXTENSION_LAX_HTML_BLOCKS != 0 { + return i + } + if skip = p.isEmpty(data[i:]); skip == 0 { + // following line must be blank + return 0 + } + + return i + skip +} + +func (*parser) isEmpty(data []byte) int { + // it is okay to call isEmpty on an empty buffer + if len(data) == 0 { + return 0 + } + + var i int + for i = 0; i < len(data) && data[i] != '\n'; i++ { + if data[i] != ' ' && data[i] != '\t' { + return 0 + } + } + return i + 1 +} + +func (*parser) isHRule(data []byte) bool { + i := 0 + + // skip up to three spaces + for i < 3 && data[i] == ' ' { + i++ + } + + // look at the hrule char + if data[i] != '*' && data[i] != '-' && data[i] != '_' { + return false + } + c := data[i] + + // the whole line must be the char or whitespace + n := 0 + for data[i] != '\n' { + switch { + case data[i] == c: + n++ + case data[i] != ' ': + return false + } + i++ + } + + return n >= 3 +} + +// isFenceLine checks if there's a fence line (e.g., ``` or ``` go) at the beginning of data, +// and returns the end index if so, or 0 otherwise. It also returns the marker found. +// If syntax is not nil, it gets set to the syntax specified in the fence line. +// A final newline is mandatory to recognize the fence line, unless newlineOptional is true. +func isFenceLine(data []byte, info *string, oldmarker string, newlineOptional bool) (end int, marker string) { + i, size := 0, 0 + + // skip up to three spaces + for i < len(data) && i < 3 && data[i] == ' ' { + i++ + } + + // check for the marker characters: ~ or ` + if i >= len(data) { + return 0, "" + } + if data[i] != '~' && data[i] != '`' { + return 0, "" + } + + c := data[i] + + // the whole line must be the same char or whitespace + for i < len(data) && data[i] == c { + size++ + i++ + } + + // the marker char must occur at least 3 times + if size < 3 { + return 0, "" + } + marker = string(data[i-size : i]) + + // if this is the end marker, it must match the beginning marker + if oldmarker != "" && marker != oldmarker { + return 0, "" + } + + // TODO(shurcooL): It's probably a good idea to simplify the 2 code paths here + // into one, always get the info string, and discard it if the caller doesn't care. + if info != nil { + infoLength := 0 + i = skipChar(data, i, ' ') + + if i >= len(data) { + if newlineOptional && i == len(data) { + return i, marker + } + return 0, "" + } + + infoStart := i + + if data[i] == '{' { + i++ + infoStart++ + + for i < len(data) && data[i] != '}' && data[i] != '\n' { + infoLength++ + i++ + } + + if i >= len(data) || data[i] != '}' { + return 0, "" + } + + // strip all whitespace at the beginning and the end + // of the {} block + for infoLength > 0 && isspace(data[infoStart]) { + infoStart++ + infoLength-- + } + + for infoLength > 0 && isspace(data[infoStart+infoLength-1]) { + infoLength-- + } + + i++ + } else { + for i < len(data) && !isverticalspace(data[i]) { + infoLength++ + i++ + } + } + + *info = strings.TrimSpace(string(data[infoStart : infoStart+infoLength])) + } + + i = skipChar(data, i, ' ') + if i >= len(data) || data[i] != '\n' { + if newlineOptional && i == len(data) { + return i, marker + } + return 0, "" + } + + return i + 1, marker // Take newline into account. +} + +// fencedCodeBlock returns the end index if data contains a fenced code block at the beginning, +// or 0 otherwise. It writes to out if doRender is true, otherwise it has no side effects. +// If doRender is true, a final newline is mandatory to recognize the fenced code block. +func (p *parser) fencedCodeBlock(out *bytes.Buffer, data []byte, doRender bool) int { + var infoString string + beg, marker := isFenceLine(data, &infoString, "", false) + if beg == 0 || beg >= len(data) { + return 0 + } + + var work bytes.Buffer + + for { + // safe to assume beg < len(data) + + // check for the end of the code block + newlineOptional := !doRender + fenceEnd, _ := isFenceLine(data[beg:], nil, marker, newlineOptional) + if fenceEnd != 0 { + beg += fenceEnd + break + } + + // copy the current line + end := skipUntilChar(data, beg, '\n') + 1 + + // did we reach the end of the buffer without a closing marker? + if end >= len(data) { + return 0 + } + + // verbatim copy to the working buffer + if doRender { + work.Write(data[beg:end]) + } + beg = end + } + + if doRender { + p.r.BlockCode(out, work.Bytes(), infoString) + } + + return beg +} + +func (p *parser) table(out *bytes.Buffer, data []byte) int { + var header bytes.Buffer + i, columns := p.tableHeader(&header, data) + if i == 0 { + return 0 + } + + var body bytes.Buffer + + for i < len(data) { + pipes, rowStart := 0, i + for ; data[i] != '\n'; i++ { + if data[i] == '|' { + pipes++ + } + } + + if pipes == 0 { + i = rowStart + break + } + + // include the newline in data sent to tableRow + i++ + p.tableRow(&body, data[rowStart:i], columns, false) + } + + p.r.Table(out, header.Bytes(), body.Bytes(), columns) + + return i +} + +// check if the specified position is preceded by an odd number of backslashes +func isBackslashEscaped(data []byte, i int) bool { + backslashes := 0 + for i-backslashes-1 >= 0 && data[i-backslashes-1] == '\\' { + backslashes++ + } + return backslashes&1 == 1 +} + +func (p *parser) tableHeader(out *bytes.Buffer, data []byte) (size int, columns []int) { + i := 0 + colCount := 1 + for i = 0; data[i] != '\n'; i++ { + if data[i] == '|' && !isBackslashEscaped(data, i) { + colCount++ + } + } + + // doesn't look like a table header + if colCount == 1 { + return + } + + // include the newline in the data sent to tableRow + header := data[:i+1] + + // column count ignores pipes at beginning or end of line + if data[0] == '|' { + colCount-- + } + if i > 2 && data[i-1] == '|' && !isBackslashEscaped(data, i-1) { + colCount-- + } + + columns = make([]int, colCount) + + // move on to the header underline + i++ + if i >= len(data) { + return + } + + if data[i] == '|' && !isBackslashEscaped(data, i) { + i++ + } + i = skipChar(data, i, ' ') + + // each column header is of form: / *:?-+:? *|/ with # dashes + # colons >= 3 + // and trailing | optional on last column + col := 0 + for data[i] != '\n' { + dashes := 0 + + if data[i] == ':' { + i++ + columns[col] |= TABLE_ALIGNMENT_LEFT + dashes++ + } + for data[i] == '-' { + i++ + dashes++ + } + if data[i] == ':' { + i++ + columns[col] |= TABLE_ALIGNMENT_RIGHT + dashes++ + } + for data[i] == ' ' { + i++ + } + + // end of column test is messy + switch { + case dashes < 3: + // not a valid column + return + + case data[i] == '|' && !isBackslashEscaped(data, i): + // marker found, now skip past trailing whitespace + col++ + i++ + for data[i] == ' ' { + i++ + } + + // trailing junk found after last column + if col >= colCount && data[i] != '\n' { + return + } + + case (data[i] != '|' || isBackslashEscaped(data, i)) && col+1 < colCount: + // something else found where marker was required + return + + case data[i] == '\n': + // marker is optional for the last column + col++ + + default: + // trailing junk found after last column + return + } + } + if col != colCount { + return + } + + p.tableRow(out, header, columns, true) + size = i + 1 + return +} + +func (p *parser) tableRow(out *bytes.Buffer, data []byte, columns []int, header bool) { + i, col := 0, 0 + var rowWork bytes.Buffer + + if data[i] == '|' && !isBackslashEscaped(data, i) { + i++ + } + + for col = 0; col < len(columns) && i < len(data); col++ { + for data[i] == ' ' { + i++ + } + + cellStart := i + + for (data[i] != '|' || isBackslashEscaped(data, i)) && data[i] != '\n' { + i++ + } + + cellEnd := i + + // skip the end-of-cell marker, possibly taking us past end of buffer + i++ + + for cellEnd > cellStart && data[cellEnd-1] == ' ' { + cellEnd-- + } + + var cellWork bytes.Buffer + p.inline(&cellWork, data[cellStart:cellEnd]) + + if header { + p.r.TableHeaderCell(&rowWork, cellWork.Bytes(), columns[col]) + } else { + p.r.TableCell(&rowWork, cellWork.Bytes(), columns[col]) + } + } + + // pad it out with empty columns to get the right number + for ; col < len(columns); col++ { + if header { + p.r.TableHeaderCell(&rowWork, nil, columns[col]) + } else { + p.r.TableCell(&rowWork, nil, columns[col]) + } + } + + // silently ignore rows with too many cells + + p.r.TableRow(out, rowWork.Bytes()) +} + +// returns blockquote prefix length +func (p *parser) quotePrefix(data []byte) int { + i := 0 + for i < 3 && data[i] == ' ' { + i++ + } + if data[i] == '>' { + if data[i+1] == ' ' { + return i + 2 + } + return i + 1 + } + return 0 +} + +// blockquote ends with at least one blank line +// followed by something without a blockquote prefix +func (p *parser) terminateBlockquote(data []byte, beg, end int) bool { + if p.isEmpty(data[beg:]) <= 0 { + return false + } + if end >= len(data) { + return true + } + return p.quotePrefix(data[end:]) == 0 && p.isEmpty(data[end:]) == 0 +} + +// parse a blockquote fragment +func (p *parser) quote(out *bytes.Buffer, data []byte) int { + var raw bytes.Buffer + beg, end := 0, 0 + for beg < len(data) { + end = beg + // Step over whole lines, collecting them. While doing that, check for + // fenced code and if one's found, incorporate it altogether, + // irregardless of any contents inside it + for data[end] != '\n' { + if p.flags&EXTENSION_FENCED_CODE != 0 { + if i := p.fencedCodeBlock(out, data[end:], false); i > 0 { + // -1 to compensate for the extra end++ after the loop: + end += i - 1 + break + } + } + end++ + } + end++ + + if pre := p.quotePrefix(data[beg:]); pre > 0 { + // skip the prefix + beg += pre + } else if p.terminateBlockquote(data, beg, end) { + break + } + + // this line is part of the blockquote + raw.Write(data[beg:end]) + beg = end + } + + var cooked bytes.Buffer + p.block(&cooked, raw.Bytes()) + p.r.BlockQuote(out, cooked.Bytes()) + return end +} + +// returns prefix length for block code +func (p *parser) codePrefix(data []byte) int { + if data[0] == ' ' && data[1] == ' ' && data[2] == ' ' && data[3] == ' ' { + return 4 + } + return 0 +} + +func (p *parser) code(out *bytes.Buffer, data []byte) int { + var work bytes.Buffer + + i := 0 + for i < len(data) { + beg := i + for data[i] != '\n' { + i++ + } + i++ + + blankline := p.isEmpty(data[beg:i]) > 0 + if pre := p.codePrefix(data[beg:i]); pre > 0 { + beg += pre + } else if !blankline { + // non-empty, non-prefixed line breaks the pre + i = beg + break + } + + // verbatim copy to the working buffeu + if blankline { + work.WriteByte('\n') + } else { + work.Write(data[beg:i]) + } + } + + // trim all the \n off the end of work + workbytes := work.Bytes() + eol := len(workbytes) + for eol > 0 && workbytes[eol-1] == '\n' { + eol-- + } + if eol != len(workbytes) { + work.Truncate(eol) + } + + work.WriteByte('\n') + + p.r.BlockCode(out, work.Bytes(), "") + + return i +} + +// returns unordered list item prefix +func (p *parser) uliPrefix(data []byte) int { + i := 0 + + // start with up to 3 spaces + for i < 3 && data[i] == ' ' { + i++ + } + + // need a *, +, or - followed by a space + if (data[i] != '*' && data[i] != '+' && data[i] != '-') || + data[i+1] != ' ' { + return 0 + } + return i + 2 +} + +// returns ordered list item prefix +func (p *parser) oliPrefix(data []byte) int { + i := 0 + + // start with up to 3 spaces + for i < 3 && data[i] == ' ' { + i++ + } + + // count the digits + start := i + for data[i] >= '0' && data[i] <= '9' { + i++ + } + + // we need >= 1 digits followed by a dot and a space + if start == i || data[i] != '.' || data[i+1] != ' ' { + return 0 + } + return i + 2 +} + +// returns definition list item prefix +func (p *parser) dliPrefix(data []byte) int { + i := 0 + + // need a : followed by a spaces + if data[i] != ':' || data[i+1] != ' ' { + return 0 + } + for data[i] == ' ' { + i++ + } + return i + 2 +} + +// parse ordered or unordered list block +func (p *parser) list(out *bytes.Buffer, data []byte, flags int) int { + i := 0 + flags |= LIST_ITEM_BEGINNING_OF_LIST + work := func() bool { + for i < len(data) { + skip := p.listItem(out, data[i:], &flags) + i += skip + + if skip == 0 || flags&LIST_ITEM_END_OF_LIST != 0 { + break + } + flags &= ^LIST_ITEM_BEGINNING_OF_LIST + } + return true + } + + p.r.List(out, work, flags) + return i +} + +// Parse a single list item. +// Assumes initial prefix is already removed if this is a sublist. +func (p *parser) listItem(out *bytes.Buffer, data []byte, flags *int) int { + // keep track of the indentation of the first line + itemIndent := 0 + for itemIndent < 3 && data[itemIndent] == ' ' { + itemIndent++ + } + + i := p.uliPrefix(data) + if i == 0 { + i = p.oliPrefix(data) + } + if i == 0 { + i = p.dliPrefix(data) + // reset definition term flag + if i > 0 { + *flags &= ^LIST_TYPE_TERM + } + } + if i == 0 { + // if in defnition list, set term flag and continue + if *flags&LIST_TYPE_DEFINITION != 0 { + *flags |= LIST_TYPE_TERM + } else { + return 0 + } + } + + // skip leading whitespace on first line + for data[i] == ' ' { + i++ + } + + // find the end of the line + line := i + for i > 0 && data[i-1] != '\n' { + i++ + } + + // get working buffer + var raw bytes.Buffer + + // put the first line into the working buffer + raw.Write(data[line:i]) + line = i + + // process the following lines + containsBlankLine := false + sublist := 0 + +gatherlines: + for line < len(data) { + i++ + + // find the end of this line + for data[i-1] != '\n' { + i++ + } + + // if it is an empty line, guess that it is part of this item + // and move on to the next line + if p.isEmpty(data[line:i]) > 0 { + containsBlankLine = true + raw.Write(data[line:i]) + line = i + continue + } + + // calculate the indentation + indent := 0 + for indent < 4 && line+indent < i && data[line+indent] == ' ' { + indent++ + } + + chunk := data[line+indent : i] + + // evaluate how this line fits in + switch { + // is this a nested list item? + case (p.uliPrefix(chunk) > 0 && !p.isHRule(chunk)) || + p.oliPrefix(chunk) > 0 || + p.dliPrefix(chunk) > 0: + + if containsBlankLine { + // end the list if the type changed after a blank line + if indent <= itemIndent && + ((*flags&LIST_TYPE_ORDERED != 0 && p.uliPrefix(chunk) > 0) || + (*flags&LIST_TYPE_ORDERED == 0 && p.oliPrefix(chunk) > 0)) { + + *flags |= LIST_ITEM_END_OF_LIST + break gatherlines + } + *flags |= LIST_ITEM_CONTAINS_BLOCK + } + + // to be a nested list, it must be indented more + // if not, it is the next item in the same list + if indent <= itemIndent { + break gatherlines + } + + // is this the first item in the nested list? + if sublist == 0 { + sublist = raw.Len() + } + + // is this a nested prefix header? + case p.isPrefixHeader(chunk): + // if the header is not indented, it is not nested in the list + // and thus ends the list + if containsBlankLine && indent < 4 { + *flags |= LIST_ITEM_END_OF_LIST + break gatherlines + } + *flags |= LIST_ITEM_CONTAINS_BLOCK + + // anything following an empty line is only part + // of this item if it is indented 4 spaces + // (regardless of the indentation of the beginning of the item) + case containsBlankLine && indent < 4: + if *flags&LIST_TYPE_DEFINITION != 0 && i < len(data)-1 { + // is the next item still a part of this list? + next := i + for data[next] != '\n' { + next++ + } + for next < len(data)-1 && data[next] == '\n' { + next++ + } + if i < len(data)-1 && data[i] != ':' && data[next] != ':' { + *flags |= LIST_ITEM_END_OF_LIST + } + } else { + *flags |= LIST_ITEM_END_OF_LIST + } + break gatherlines + + // a blank line means this should be parsed as a block + case containsBlankLine: + *flags |= LIST_ITEM_CONTAINS_BLOCK + } + + containsBlankLine = false + + // add the line into the working buffer without prefix + raw.Write(data[line+indent : i]) + + line = i + } + + // If reached end of data, the Renderer.ListItem call we're going to make below + // is definitely the last in the list. + if line >= len(data) { + *flags |= LIST_ITEM_END_OF_LIST + } + + rawBytes := raw.Bytes() + + // render the contents of the list item + var cooked bytes.Buffer + if *flags&LIST_ITEM_CONTAINS_BLOCK != 0 && *flags&LIST_TYPE_TERM == 0 { + // intermediate render of block item, except for definition term + if sublist > 0 { + p.block(&cooked, rawBytes[:sublist]) + p.block(&cooked, rawBytes[sublist:]) + } else { + p.block(&cooked, rawBytes) + } + } else { + // intermediate render of inline item + if sublist > 0 { + p.inline(&cooked, rawBytes[:sublist]) + p.block(&cooked, rawBytes[sublist:]) + } else { + p.inline(&cooked, rawBytes) + } + } + + // render the actual list item + cookedBytes := cooked.Bytes() + parsedEnd := len(cookedBytes) + + // strip trailing newlines + for parsedEnd > 0 && cookedBytes[parsedEnd-1] == '\n' { + parsedEnd-- + } + p.r.ListItem(out, cookedBytes[:parsedEnd], *flags) + + return line +} + +// render a single paragraph that has already been parsed out +func (p *parser) renderParagraph(out *bytes.Buffer, data []byte) { + if len(data) == 0 { + return + } + + // trim leading spaces + beg := 0 + for data[beg] == ' ' { + beg++ + } + + // trim trailing newline + end := len(data) - 1 + + // trim trailing spaces + for end > beg && data[end-1] == ' ' { + end-- + } + + work := func() bool { + p.inline(out, data[beg:end]) + return true + } + p.r.Paragraph(out, work) +} + +func (p *parser) paragraph(out *bytes.Buffer, data []byte) int { + // prev: index of 1st char of previous line + // line: index of 1st char of current line + // i: index of cursor/end of current line + var prev, line, i int + + // keep going until we find something to mark the end of the paragraph + for i < len(data) { + // mark the beginning of the current line + prev = line + current := data[i:] + line = i + + // did we find a blank line marking the end of the paragraph? + if n := p.isEmpty(current); n > 0 { + // did this blank line followed by a definition list item? + if p.flags&EXTENSION_DEFINITION_LISTS != 0 { + if i < len(data)-1 && data[i+1] == ':' { + return p.list(out, data[prev:], LIST_TYPE_DEFINITION) + } + } + + p.renderParagraph(out, data[:i]) + return i + n + } + + // an underline under some text marks a header, so our paragraph ended on prev line + if i > 0 { + if level := p.isUnderlinedHeader(current); level > 0 { + // render the paragraph + p.renderParagraph(out, data[:prev]) + + // ignore leading and trailing whitespace + eol := i - 1 + for prev < eol && data[prev] == ' ' { + prev++ + } + for eol > prev && data[eol-1] == ' ' { + eol-- + } + + // render the header + // this ugly double closure avoids forcing variables onto the heap + work := func(o *bytes.Buffer, pp *parser, d []byte) func() bool { + return func() bool { + pp.inline(o, d) + return true + } + }(out, p, data[prev:eol]) + + id := "" + if p.flags&EXTENSION_AUTO_HEADER_IDS != 0 { + id = SanitizedAnchorName(string(data[prev:eol])) + } + + p.r.Header(out, work, level, id) + + // find the end of the underline + for data[i] != '\n' { + i++ + } + return i + } + } + + // if the next line starts a block of HTML, then the paragraph ends here + if p.flags&EXTENSION_LAX_HTML_BLOCKS != 0 { + if data[i] == '<' && p.html(out, current, false) > 0 { + // rewind to before the HTML block + p.renderParagraph(out, data[:i]) + return i + } + } + + // if there's a prefixed header or a horizontal rule after this, paragraph is over + if p.isPrefixHeader(current) || p.isHRule(current) { + p.renderParagraph(out, data[:i]) + return i + } + + // if there's a fenced code block, paragraph is over + if p.flags&EXTENSION_FENCED_CODE != 0 { + if p.fencedCodeBlock(out, current, false) > 0 { + p.renderParagraph(out, data[:i]) + return i + } + } + + // if there's a definition list item, prev line is a definition term + if p.flags&EXTENSION_DEFINITION_LISTS != 0 { + if p.dliPrefix(current) != 0 { + return p.list(out, data[prev:], LIST_TYPE_DEFINITION) + } + } + + // if there's a list after this, paragraph is over + if p.flags&EXTENSION_NO_EMPTY_LINE_BEFORE_BLOCK != 0 { + if p.uliPrefix(current) != 0 || + p.oliPrefix(current) != 0 || + p.quotePrefix(current) != 0 || + p.codePrefix(current) != 0 { + p.renderParagraph(out, data[:i]) + return i + } + } + + // otherwise, scan to the beginning of the next line + for data[i] != '\n' { + i++ + } + i++ + } + + p.renderParagraph(out, data[:i]) + return i +} + +// SanitizedAnchorName returns a sanitized anchor name for the given text. +// +// It implements the algorithm specified in the package comment. +func SanitizedAnchorName(text string) string { + var anchorName []rune + futureDash := false + for _, r := range text { + switch { + case unicode.IsLetter(r) || unicode.IsNumber(r): + if futureDash && len(anchorName) > 0 { + anchorName = append(anchorName, '-') + } + futureDash = false + anchorName = append(anchorName, unicode.ToLower(r)) + default: + futureDash = true + } + } + return string(anchorName) +} diff --git a/vendor/github.com/russross/blackfriday/doc.go b/vendor/github.com/russross/blackfriday/doc.go new file mode 100644 index 00000000..9656c42a --- /dev/null +++ b/vendor/github.com/russross/blackfriday/doc.go @@ -0,0 +1,32 @@ +// Package blackfriday is a Markdown processor. +// +// It translates plain text with simple formatting rules into HTML or LaTeX. +// +// Sanitized Anchor Names +// +// Blackfriday includes an algorithm for creating sanitized anchor names +// corresponding to a given input text. This algorithm is used to create +// anchors for headings when EXTENSION_AUTO_HEADER_IDS is enabled. The +// algorithm is specified below, so that other packages can create +// compatible anchor names and links to those anchors. +// +// The algorithm iterates over the input text, interpreted as UTF-8, +// one Unicode code point (rune) at a time. All runes that are letters (category L) +// or numbers (category N) are considered valid characters. They are mapped to +// lower case, and included in the output. All other runes are considered +// invalid characters. Invalid characters that preceed the first valid character, +// as well as invalid character that follow the last valid character +// are dropped completely. All other sequences of invalid characters +// between two valid characters are replaced with a single dash character '-'. +// +// SanitizedAnchorName exposes this functionality, and can be used to +// create compatible links to the anchor names generated by blackfriday. +// This algorithm is also implemented in a small standalone package at +// github.com/shurcooL/sanitized_anchor_name. It can be useful for clients +// that want a small package and don't need full functionality of blackfriday. +package blackfriday + +// NOTE: Keep Sanitized Anchor Name algorithm in sync with package +// github.com/shurcooL/sanitized_anchor_name. +// Otherwise, users of sanitized_anchor_name will get anchor names +// that are incompatible with those generated by blackfriday. diff --git a/vendor/github.com/russross/blackfriday/html.go b/vendor/github.com/russross/blackfriday/html.go new file mode 100644 index 00000000..e0a6c69c --- /dev/null +++ b/vendor/github.com/russross/blackfriday/html.go @@ -0,0 +1,938 @@ +// +// Blackfriday Markdown Processor +// Available at http://github.com/russross/blackfriday +// +// Copyright © 2011 Russ Ross . +// Distributed under the Simplified BSD License. +// See README.md for details. +// + +// +// +// HTML rendering backend +// +// + +package blackfriday + +import ( + "bytes" + "fmt" + "regexp" + "strconv" + "strings" +) + +// Html renderer configuration options. +const ( + HTML_SKIP_HTML = 1 << iota // skip preformatted HTML blocks + HTML_SKIP_STYLE // skip embedded