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 pressreturn
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 pressreturn
will draw a horizontal line.
+ +YAML Front Matter
+ +Typora support YAML Front Matter now. Input
+ +---
at the top of the article and then pressEnter
will introduce one. Or insert one metadata block from the menu.Table of Contents (TOC)
+ +Input
+ +[toc]
then pressReturn
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.~~
becomesMistaken 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 fromEdit
->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:
+ +
<u>underline</u>
<img src="http://www.w3.org/html/logo/img/mark-word-icon.png" width="200px" />
(And width
, height
attribute in HTML tag, and width
, height
, zoom
style in style
attribute will be applied.)<!-- This is some comments -->
<a href="http://typora.io" target="_blank">link</a>
.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.
+ +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:
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~
/
To use this feature, first, please enable it in Preference
Panel -> Markdown
Tab. Then use ^
to wrap superscript content, for example: X^2^
.
To use this feature, first, please enable it in Preference
Panel -> Markdown
Tab. Then use ==
to wrap superscript content, for example: ==highlight==
.
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
")), "|",
+ 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": "(
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 | *
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 | *
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 | *
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 00000000..5cdf8c77
Binary files /dev/null and b/doc/example/sakila.sql.gz differ
diff --git a/doc/example/slow.log.digest b/doc/example/slow.log.digest
new file mode 100644
index 00000000..db4305e4
--- /dev/null
+++ b/doc/example/slow.log.digest
@@ -0,0 +1,83 @@
+
+# 13.7s user time, 20ms system time, 27.95M rss, 181.32M vsz
+# Current date: Thu May 17 15:24:49 2018
+# Hostname: 127.0.0.1
+# Files: slow.log
+# Overall: 75.28k total, 21 unique, 1.36 QPS, 0.22x concurrency __________
+# Time range: 2018-05-17 00:01:47 to 15:24:47
+# Attribute total min max avg 95% stddev median
+# ============ ======= ======= ======= ======= ======= ======= =======
+# Exec time 12368s 20ms 6s 164ms 501ms 208ms 95ms
+# Lock time 2s 0 311us 30us 38us 5us 27us
+# Rows sent 21.79M 0 28.49k 303.58 49.17 2.44k 0.99
+# Rows examine 103.77M 0 31.41k 1.41k 4.49k 2.96k 621.67
+# Query size 8.58M 17 7.78k 119.54 143.84 32.94 112.70
+
+# Profile
+# Rank Query ID Response time Calls R/Call V/M Item
+# ==== ================== ================ ===== ====== ===== ============
+# 1 0x6F837C9DA962A07D 11374.6099 92.0% 67535 0.1684 0.27 SELECT test.table_?
+# 2 0x0B991403AD4E8932 803.2640 6.5% 5993 0.1340 0.24 SELECT test.table_?
+# MISC 0xMISC 190.1791 1.5% 1751 0.1086 0.0 <19 ITEMS>
+
+# 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
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" + curtag + ">")
+ 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("" + tag + ">")
+ 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