[
  {
    "path": ".editorconfig",
    "content": "[*]\nend_of_line = lf\ninsert_final_newline = true\ncharset = utf-8\n\n# tab_size = 4 spaces\n[*.go]\nindent_style = tab\nindent_size = 4\ntrim_trailing_whitespace = true\n"
  },
  {
    "path": ".gitignore",
    "content": "files\nbin\ncoverage.out\n.idea/\n*.iml\n*.swp\n*.log\n*.fail.go\n.DS_Store\n.vscode/\n*.sql\n"
  },
  {
    "path": "Makefile",
    "content": "PROJECT=bingo2sql\nGOPATH ?= $(shell go env GOPATH)\n\n# Ensure GOPATH is set before running build process.\nifeq \"$(GOPATH)\" \"\"\n  $(error Please set the environment variable GOPATH before running `make`)\nendif\nFAIL_ON_STDOUT := awk '{ print } END { if (NR > 0) { exit 1 } }'\n\nCURDIR := $(shell pwd)\npath_to_add := $(addsuffix /bin,$(subst :,/bin:,$(GOPATH)))\nexport PATH := $(path_to_add):$(PATH)\n\nGO        := GO111MODULE=on go\nGOBUILD   := CGO_ENABLED=0 $(GO) build $(BUILD_FLAG)\n\nVERSION := $(shell git describe --tags --dirty)\n\n# 指定部分单元测试跳过\nifeq (\"$(SHORT)\", \"1\")\n\tGOTEST    := CGO_ENABLED=1 $(GO) test -p 3 -short\nelse\n\tGOTEST    := CGO_ENABLED=1 $(GO) test -p 3\nendif\n\nOVERALLS  := CGO_ENABLED=1 GO111MODULE=on overalls\nGOVERALLS := goveralls\n\nARCH      := \"`uname -s`\"\nLINUX     := \"Linux\"\nMAC       := \"Darwin\"\nPACKAGE_LIST  := go list ./...| grep -vE \"vendor\"\nPACKAGES  := $$($(PACKAGE_LIST))\nPACKAGE_DIRECTORIES := $(PACKAGE_LIST) | sed 's|github.com/hanchuanchuan/$(PROJECT)/||'\nFILES     := $$(find $$($(PACKAGE_DIRECTORIES)) -name \"*.go\" | grep -vE \"vendor\")\n\nGOFAIL_ENABLE  := $$(find $$PWD/ -type d | grep -vE \"(\\.git|vendor)\" | xargs gofail enable)\nGOFAIL_DISABLE := $$(find $$PWD/ -type d | grep -vE \"(\\.git|vendor)\" | xargs gofail disable)\n\n# LDFLAGS += -X \"github.com/hanchuanchuan/$(PROJECT)//mysql.TiDBReleaseVersion=$(shell git describe --tags --dirty)\"\n# LDFLAGS += -X \"github.com/hanchuanchuan/$(PROJECT)//util/printer.TiDBBuildTS=$(shell date '+%Y-%m-%d %H:%M:%S')\"\n# LDFLAGS += -X \"github.com/hanchuanchuan/$(PROJECT)//util/printer.TiDBGitHash=$(shell git rev-parse HEAD)\"\n# LDFLAGS += -X \"github.com/hanchuanchuan/$(PROJECT)//util/printer.TiDBGitBranch=$(shell git rev-parse --abbrev-ref HEAD)\"\n# LDFLAGS += -X \"github.com/hanchuanchuan/$(PROJECT)//util/printer.GoVersion=$(shell go version)\"\n\nCHECK_LDFLAGS += $(LDFLAGS)\n\n.PHONY: all build update clean test gotest server check\n\ndefault: server buildsucc\n\nbuild:\n\tGOOS=linux GOARCH=amd64 $(GOBUILD) -ldflags '-s -w $(LDFLAGS)' -o bin/$(PROJECT) main.go\n\n\nserver-admin-check: server_check buildsucc\n\nbuildsucc:\n\t@echo Build bingo2sql successfully!\n\n# The retool tools.json is setup from hack/retool-install.sh\ncheck-setup:\n\t@which retool >/dev/null 2>&1 || go get github.com/twitchtv/retool\n\t@retool sync\n\ncheck: check-setup fmt lint vet\n\n# These need to be fixed before they can be ran regularly\ncheck-fail: goword check-static check-slow\n\nfmt:\n\t@echo \"gofmt (simplify)\"\n\t@gofmt -s -l -w $(FILES) 2>&1 | $(FAIL_ON_STDOUT)\n\ngoword:\n\tretool do goword $(FILES) 2>&1 | $(FAIL_ON_STDOUT)\n\ncheck-static:\n\t@ # vet and fmt have problems with vendor when ran through metalinter\n\tCGO_ENABLED=0 retool do gometalinter.v2 --disable-all --deadline 120s \\\n\t  --enable misspell \\\n\t  --enable megacheck \\\n\t  --enable ineffassign \\\n\t  $$($(PACKAGE_DIRECTORIES))\n\ncheck-slow:\n\tCGO_ENABLED=0 retool do gometalinter.v2 --disable-all \\\n\t  --enable errcheck \\\n\t  $$($(PACKAGE_DIRECTORIES))\n\tCGO_ENABLED=0 retool do gosec $$($(PACKAGE_DIRECTORIES))\n\nlint:\n\t@echo \"linting\"\n\t@CGO_ENABLED=0 retool do revive -formatter friendly -config revive.toml $(PACKAGES)\n\nvet:\n\t@echo \"vet\"\n\t@go vet -all -shadow $(PACKAGES) 2>&1 | $(FAIL_ON_STDOUT)\n\nclean:\n\t$(GO) clean -i ./...\n\trm -rf *.out\n\ntest: gotest\n\ngotest:\n\t$(GO) get github.com/etcd-io/gofail@v0.0.0-20180808172546-51ce9a71510a\n\t@$(GOFAIL_ENABLE)\nifeq (\"$(TRAVIS_COVERAGE)\", \"1\")\n\t@echo \"Running in TRAVIS_COVERAGE mode.\"\n\t@export log_level=error; \\\n\tgo get github.com/go-playground/overalls\n\t# go get github.com/mattn/goveralls\n\t# $(OVERALLS) -project=github.com/hanchuanchuan/$(PROJECT)/ -covermode=count -ignore='.git,vendor,cmd,docs,LICENSES' || { $(GOFAIL_DISABLE); exit 1; }\n\t# $(GOVERALLS) -service=travis-ci -coverprofile=overalls.coverprofile || { $(GOFAIL_DISABLE); exit 1; }\n\n\t$(OVERALLS) -project=github.com/hanchuanchuan/$(PROJECT)/ -covermode=count -ignore='.git,vendor,cmd,docs,LICENSES' -concurrency=1 -- -short || { $(GOFAIL_DISABLE); exit 1; }\nelse\n\t@echo \"Running in native mode.\"\n\t@export log_level=error; \\\n\t$(GOTEST) -timeout 30m -cover $(PACKAGES) || { $(GOFAIL_DISABLE); exit 1; }\nendif\n\t@$(GOFAIL_DISABLE)\n\nrace:\n\t$(GO) get github.com/etcd-io/gofail@v0.0.0-20180808172546-51ce9a71510a\n\t@$(GOFAIL_ENABLE)\n\t@export log_level=debug; \\\n\t$(GOTEST) -timeout 30m -race $(PACKAGES) || { $(GOFAIL_DISABLE); exit 1; }\n\t@$(GOFAIL_DISABLE)\n\nleak:\n\t$(GO) get github.com/etcd-io/gofail@v0.0.0-20180808172546-51ce9a71510a\n\t@$(GOFAIL_ENABLE)\n\t@export log_level=debug; \\\n\t$(GOTEST) -tags leak $(PACKAGES) || { $(GOFAIL_DISABLE); exit 1; }\n\t@$(GOFAIL_DISABLE)\n\ntikv_integration_test:\n\t$(GO) get github.com/etcd-io/gofail@v0.0.0-20180808172546-51ce9a71510a\n\t@$(GOFAIL_ENABLE)\n\t$(GOTEST) ./store/tikv/. -with-tikv=true || { $(GOFAIL_DISABLE); exit 1; }\n\t@$(GOFAIL_DISABLE)\n\nRACE_FLAG =\nifeq (\"$(WITH_RACE)\", \"1\")\n\tRACE_FLAG = -race\n\tGOBUILD   = GOPATH=$(GOPATH) CGO_ENABLED=1 $(GO) build\nendif\n\n\nserver:\n\t$(GOBUILD) $(RACE_FLAG) -ldflags '$(LDFLAGS)' -o bin/$(PROJECT) main.go\n\nserver_old:\n\t$(GOBUILD) $(RACE_FLAG) -ldflags '$(LDFLAGS)' -o bin/$(PROJECT) cmd/bingo2sql/main.go\n\nserver_check:\n\t$(GOBUILD) $(RACE_FLAG) -ldflags '$(CHECK_LDFLAGS)' -o bin/$(PROJECT) main.go\n\n\nupdate:\n\twhich dep 2>/dev/null || go get -u github.com/golang/dep/cmd/dep\nifdef PKG\n\tdep ensure -add ${PKG}\nelse\n\tdep ensure -update\nendif\n\t@echo \"removing test files\"\n\tdep prune\n\tbash ./hack/clean_vendor.sh\n\n\ngofail-enable:\n# Converting gofail failpoints...\n\t@$(GOFAIL_ENABLE)\n\ngofail-disable:\n# Restoring gofail failpoints...\n\t@$(GOFAIL_DISABLE)\n\nupload-coverage: SHELL:=/bin/bash\nupload-coverage:\nifeq (\"$(TRAVIS_COVERAGE)\", \"1\")\n\tmv overalls.coverprofile coverage.txt\n\tbash <(curl -s https://codecov.io/bash)\nendif\n\n\n# \twindows无法build,github.com/outbrain/golib有引用syslog.Writer,其在windows未实现.\n.PHONY: release\nrelease:\n\t@echo \"$(CGREEN)Cross platform building for release ...$(CEND)\"\n\t# @for GOOS in linux; do\n\t@for GOOS in windows darwin linux; do \\\n\t\techo \"Building $${GOOS}-$${GOARCH} ...\"; \\\n\t\tGOOS=$${GOOS} GOARCH=amd64 $(GOBUILD) -ldflags '-s -w $(LDFLAGS)'  -o bin/$(PROJECT) cmd/bingo2sql.go; \\\n\t\tcd bin; \\\n\t\ttar -czf $(PROJECT)-$${GOOS}-${VERSION}.tar.gz $(PROJECT); \\\n\t\trm -f $(PROJECT); \\\n\t\tcd ..; \\\n\tdone\n\ndocker:\n\tGOOS=linux GOARCH=amd64 $(GOBUILD) -ldflags '-s -w $(LDFLAGS)' -o bin/$(PROJECT) cmd/bingo2sql.go\n\tv1=$(shell git tag | awk -F'-' '{print $1}' |tail -1) && docker build -t hanchuanchuan/$(PROJECT)/:$${v1} . \\\n\t&& docker tag hanchuanchuan/$(PROJECT)/:$${v1} hanchuanchuan/$(PROJECT)/:latest\n\ndocker-push:\n\tv1=$(shell git tag|tail -1) && docker push hanchuanchuan/$(PROJECT)/:$${v1} \\\n\t&& docker push hanchuanchuan/$(PROJECT)/:latest\n"
  },
  {
    "path": "README.md",
    "content": "# bingo2sql\nMySQL Binlog 解析工具\n\n从MySQL binlog解析出原始SQL，对应的回滚SQL等。\n\n#### 功能说明\n\n- 本地离线解析：指定本地binlog文件和要解析的表结构即可\n- 远程在线解析：指定远程数据库地址，起止时间范围或binlog范围，可指定库/表和操作类型，GTID/线程号等\n- 解析服务API：提供HTTP协议方式的解析接口，支持解析和打包下载\n\n#### 限制和要求\n\n- MySQL必须开启binlog\n- binlog_format = row\n- binlog_row_image = full\n\n#### 测试对比\n\n测试步骤及结束详见 [效率测试](docs/test.md)\n\n\n### 支持模式\n\n#### 1. 本地解析\n```sh\nbingo2sql --start-file=~/db_cmdb/blog/mysql-bin.000001 -t table.sql\n```\n其中`-t`参数指定的是建表语句文件,内容类似:\n```sql\n-- 需要解析哪个表,提供哪个表的建表语句\nCREATE TABLE `tt` (\n  id int auto_increment primary key,\n  `TABLE_NAME` varchar(64) NOT NULL DEFAULT ''\n) ;\n```\n\n\n#### 2. 远程解析\n\n远程解析的参数及使用均与binlog2sql类似\n\n```\nbingo2sql -h=127.0.0.1 -P 3306 -u test -p test -d db1_3306_test_inc \\\n  --start-time=\"2006-01-02 15:04:05\" -t t1 -B\n```\n\n#### 3. 解析服务\n\nbingo2sql 支持以服务方式运行,提供解析的HTTP接口支持\n\n```sh\nbingo2sql --server --config=config.ini\n```\n\n\n### 支持选项\n\n**解析模式**\n\n- --stop-never 持续解析binlog。可选。默认false，同步至执行命令时最新的binlog位置。\n\n- -K, --no-primary-key 对INSERT语句去除主键。可选。默认false\n\n- -B, --flashback 生成回滚SQL，可解析大文件，不受内存限制。可选。默认false。与stop-never或no-primary-key不能同时添加。\n\n- -M, --minimal-update 最小化update语句. 可选. (default true)\n\n- -I, --minimal-insert 使用包含多个VALUES列表的多行语法编写INSERT语句. (default true)\n\n**解析范围控制**\n\n- --start-file 起始解析文件，只需文件名，无需全路径 。可选，如果指定了起止时间，可以忽略该参数。\n\n- --start-pos 起始解析位置。可选。默认为start-file的起始位置。\n\n- --stop-file 终止解析文件。可选。若解析模式为stop-never，此选项失效。\n\n- --stop-pos 终止解析位置。可选。默认为stop-file的最末位置；若解析模式为stop-never，此选项失效。\n\n- --start-time 起始解析时间，格式'%Y-%m-%d %H:%M:%S'等。可选。默认不过滤。\n\n- --stop-time 终止解析时间，格式'%Y-%m-%d %H:%M:%S'等。可选。默认不过滤。\n\n- -C, --connection-id 线程号，可解析指定线程的SQL。\n\n- -g, --gtid GTID范围.格式为uuid:编号[-编号],多个时以逗号分隔，例如：6573bb29-9d94-11e9-9e0c-0242ac130002:1-100\n\n- --max 解析的最大行数,设置为0则不限制，以避免解析范围过大 (default 100000)\n\n**对象过滤**\n\n-d, --databases 只解析目标db的sql，多个库用逗号隔开，如-d db1,db2。可选。默认为空。\n\n-t, --tables 只解析目标table的sql，多张表用逗号隔开，如-t tbl1,tbl2。可选。默认为空。\n\n--ddl 解析ddl，仅支持正向解析。可选。默认false。\n\n--sql-type 只解析指定类型，支持 insert,update,delete。多个类型用逗号隔开，如--sql-type=insert,delete。可选。默认为增删改都解析。\n\n\n**附加信息**\n\n- --show-gtid            显示gtid (default true)\n\n- --show-time            显示执行时间,同一时间仅显示首次 (default true)\n\n- --show-all-time        显示每条SQL的执行时间 (default false)\n\n- --show-thread          显示线程号,便于区别同一进程操作 (default false)\n\n- -o, --output          本地或远程解析时，可输出到指定文件(置空则输出到控制台，可通过 > file重定向)\n\n**mysql连接配置** (仅远程解析需要)\n\n```\n -h host\n -P port\n -u user\n -p password\n```\n\n#### 致谢\n    bingo2sql借鉴和学习了很多业界知名的开源项目，在此表示感谢！\n- [go-mysql](https://github.com/siddontang/go-mysql) handle MySQL network protocol and replication\n- [binlog2sql](https://github.com/danfengcao/binlog2sql) Parse MySQL binlog to SQL you want\n- [binlog_rollback](https://github.com/GoDannyLai/binlog_rollback) mysql binlog rollback | flashback | redo | dml report | ddl info\n"
  },
  {
    "path": "circle.yml",
    "content": "version: 2\n\ngeneral:\n  branches:\n    ignore:\n      - gh-pages\n\njobs:\n  build:\n    branches:\n      ignore: gh-pages\n\n    environment:\n      TZ: \"Asia/Shanghai\"\n\n    docker:\n      - image: circleci/golang:1.14\n      - image: circleci/mysql:5.7.31\n      # - image: mysql:5.7\n        command: mysqld --lower_case_table_names=1 --character-set-server=utf8mb4  --collation-server=utf8mb4_bin --innodb-large-prefix=true --log-bin=on --server_id=111 --explicit_defaults_for_timestamp=true --gtid-mode=1 --enforce_gtid_consistency=1 --log-slave-updates=1 --innodb_buffer_pool_size=512M --max_allowed_packet=64M\n        environment:\n          MYSQL_ALLOW_EMPTY_PASSWORD: true\n          TZ: \"Asia/Shanghai\"\n        # volumes:\n        #   - /var/lib/mysql:/var/lib/mysql\n\n    working_directory: /go/src/github.com/hanchuanchuan/bingo2sql\n\n    steps:\n      - run:\n          name: Install mysql-client\n          command: sudo apt-get update -y --allow-releaseinfo-change && sudo apt upgrade -y && sudo apt-get install default-mysql-client && sudo apt-get install --reinstall ca-certificates libgnutls30 -y\n      - add_ssh_keys:\n          fingerprints:\n            - \"42:94:88:f4:be:ff:0b:b9:e8:ae:05:26:f1:e7:fd:57\"\n      - checkout\n      - run:\n          name: Waiting for MySQL to be ready\n          command: |\n            for i in `seq 1 10`;\n            do\n              nc -z localhost 3306 && echo Success && exit 0\n              echo -n .\n              sleep 1\n            done\n            echo Failed waiting for MySQL && exit 1\n      - run:\n          name: mysql init\n          command: mysql -h 127.0.0.1 -u root -e \"select version();create database if not exists test DEFAULT CHARACTER SET utf8;create database if not exists test_inc DEFAULT CHARACTER SET utf8;grant all on *.* to test@'127.0.0.1' identified by 'test';FLUSH PRIVILEGES;show databases;show variables like 'explicit_defaults_for_timestamp';\"\n      - run:\n          name: \"Show Variables\"\n          command: mysql -h 127.0.0.1 -u root -e \"show variables\"\n      - run: rm -f go.sum\n      - run: mysql -h 127.0.0.1 -u root -e \"set global GTID_MODE = ON_PERMISSIVE;\"\n      - run: mysql -h 127.0.0.1 -u root -e \"set global GTID_MODE = ON;\"\n      - run:\n          name: \"Build & Test\"\n          command: make test\n          no_output_timeout: 1200\n\n      # - setup_remote_docker:\n      #     docker_layer_caching: false\n      # - run:\n      #     name: Publish Docker Image to Docker Hub\n      #     command: |\n      #       if [ \"${CIRCLE_BRANCH}\" == \"master\" ]; then\n      #         echo \"$DOCKERHUB_USERNAME\"\n      #         echo \"$DOCKERHUB_PASS\" | docker login -u \"$DOCKERHUB_USERNAME\" --password-stdin\n      #         make docker\n      #         make docker-push\n      #       fi\n"
  },
  {
    "path": "cmd/bingo2sql/main.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"math\"\n\t\"os\"\n\t\"sync\"\n\n\t\"github.com/hanchuanchuan/bingo2sql/core\"\n\t_ \"github.com/jinzhu/gorm/dialects/mysql\"\n\t\"github.com/pkg/profile\"\n\tlog \"github.com/sirupsen/logrus\"\n\t\"github.com/spf13/pflag\"\n)\n\n// var parserProcess map[string]*parser.MyBinlogParser\nvar parserProcess sync.Map\n\ntype ParseInfo struct {\n\tID        string `json:\"id\"`\n\tParseRows int    `json:\"parse_rows\"`\n\tPercent   int    `json:\"percent\"`\n}\n\nvar (\n\tlogHeader     = `${time_rfc3339} ${prefix} ${level} ${short_file} ${line} `\n\trequestHeader = `${time_rfc3339} ${remote_ip} ${method} ${uri} ${status} ${error} ${latency_human}` + \"\\n\"\n\tflag          = pflag.NewFlagSet(\"bingo2sql\", pflag.ExitOnError)\n)\n\nvar (\n\thost     = flag.StringP(\"host\", \"h\", \"\", \"数据库地址\")\n\tport     = flag.IntP(\"port\", \"P\", 3306, \"端口号\")\n\tuser     = flag.StringP(\"user\", \"u\", \"\", \"用户名\")\n\tpassword = flag.StringP(\"password\", \"p\", \"\", \"密码\")\n\n\tstartFile = flag.String(\"start-file\", \"\", \"起始binlog文件\")\n\tstopFile  = flag.String(\"stop-file\", \"\", \"结束binlog文件\")\n\n\tstartPosition = flag.Int(\"start-pos\", 0, \"起始位置\")\n\tstopPosition  = flag.Int(\"stop-pos\", 0, \"结束位置\")\n\n\tstartTime = flag.String(\"start-time\", \"\", \"开始时间\")\n\tstopTime  = flag.String(\"stop-time\", \"\", \"结束时间\")\n\n\tdatabases = flag.StringP(\"databases\", \"d\", \"\", \"数据库列表,多个时以逗号分隔\")\n\ttables    = flag.StringP(\"tables\", \"t\", \"\", \"表名或表结构文件.远程解析时指定表名(可多个,以逗号分隔),本地解析时指定表结构文件\")\n\n\tthreadID = flag.Uint64P(\"connection-id\", \"C\", 0, \"指定线程ID\")\n\n\tflashback = flagBoolean(\"flashback\", \"B\", false, \"逆向语句\")\n\n\tparseDDL = flagBoolean(\"ddl\", \"\", false, \"解析DDL语句(仅正向SQL)\")\n\n\tsqlType = flag.String(\"sql-type\", \"insert,delete,update\", \"解析的语句类型\")\n\n\tmaxRows = flag.Int(\"max\", 100000, \"解析的最大行数,设置为0则不限制\")\n\tthreads = flag.Int(\"threads\", 64, \"解析线程数,影响文件解析速度\")\n\n\toutput = flag.StringP(\"output\", \"o\", \"\", \"输出到指定文件\")\n\n\tgtid = flag.StringP(\"gtid\", \"g\", \"\", \"GTID范围.格式为uuid:编号[-编号],多个时以逗号分隔\")\n\n\t// removePrimary = flagBoolean(\"no-primary-key\", \"K\", false, \"对INSERT语句去除主键. 可选.\")\n\n\tminimalUpdate = flagBoolean(\"minimal-update\", \"M\", true, \"最小化update语句. 可选.\")\n\n\tminimalInsert = flagBoolean(\"minimal-insert\", \"I\", true, \"使用包含多个VALUES列表的多行语法编写INSERT语句.\")\n\n\tstopNever = flagBoolean(\"stop-never\", \"N\", false, \"持续解析binlog\")\n\n\tshowGTID    = flagBoolean(\"show-gtid\", \"\", true, \"显示gtid\")\n\tshowTime    = flagBoolean(\"show-time\", \"\", true, \"显示执行时间,同一时间仅显示首次\")\n\tshowAllTime = flagBoolean(\"show-all-time\", \"\", false, \"显示每条SQL的执行时间\")\n\tshowThread  = flagBoolean(\"show-thread\", \"\", false, \"显示线程号,便于区别同一进程操作\")\n\n\trunServer  = flagBoolean(\"server\", \"s\", false, \"启动API服务\")\n\tsummary    = flagBoolean(\"summary\", \"S\", false, \"统计binlog文件中的DML次数等信息\")\n\tconfigFile = flag.StringP(\"config\", \"c\", \"config.ini\", \"以服务方式启动时需指定配置文件\")\n\n\tdebug = flagBoolean(\"debug\", \"\", false, \"调试模式,输出详细日志\")\n\tmode  = flag.String(\"profile-mode\", \"\", \"enable profiling mode, one of [cpu, mem, mutex, block]\")\n\t// cpuProfile = flagBoolean(\"cpu-profile\", \"\", false, \"调试模式,开启CPU性能跟踪\")\n)\n\nfunc main() {\n\tflag.SortFlags = false\n\t// 隐藏CPU性能跟踪调试参数\n\t// flag.MarkHidden(\"cpu-profile\")\n\n\tif err := flag.Parse(os.Args[1:]); err != nil {\n\t\tlog.Error(err)\n\t\treturn\n\t}\n\n\tif len(os.Args) < 2 {\n\t\tfmt.Fprint(os.Stderr, \"Usage of bingo2sql:\\n\")\n\t\tflag.PrintDefaults()\n\t\treturn\n\t}\n\n\tswitch *mode {\n\tcase \"cpu\":\n\t\tdefer profile.Start(profile.CPUProfile, profile.ProfilePath(\".\")).Stop()\n\tcase \"mem\":\n\t\tdefer profile.Start(profile.MemProfile, profile.ProfilePath(\".\")).Stop()\n\tcase \"mutex\":\n\t\tdefer profile.Start(profile.MutexProfile, profile.ProfilePath(\".\")).Stop()\n\tcase \"block\":\n\t\tdefer profile.Start(profile.BlockProfile, profile.ProfilePath(\".\")).Stop()\n\tdefault:\n\t\t// do nothing\n\t}\n\n\tif *debug {\n\t\tlog.SetLevel(log.DebugLevel)\n\t} else {\n\t\tlog.SetLevel(log.ErrorLevel)\n\t}\n\t// 以独立工具运行\n\trunParse()\n}\n\n// runParse 执行binlog解析\nfunc runParse() {\n\n\tcfg := &core.BinlogParserConfig{\n\t\tHost:     *host,\n\t\tPort:     uint16(*port),\n\t\tUser:     *user,\n\t\tPassword: *password,\n\n\t\tStartFile:     *startFile,\n\t\tStopFile:      *stopFile,\n\t\tStartPosition: *startPosition,\n\t\tStopPosition:  *stopPosition,\n\n\t\tStartTime: *startTime,\n\t\tStopTime:  *stopTime,\n\n\t\tFlashback: *flashback,\n\n\t\tParseDDL:     *parseDDL,\n\t\tIncludeGtids: *gtid,\n\n\t\tDatabases: *databases,\n\t\tTables:    *tables,\n\t\tSqlType:   *sqlType,\n\t\tMaxRows:   *maxRows,\n\t\tThreads:   *threads,\n\n\t\tOutputFileStr: *output,\n\n\t\t// RemovePrimary: *removePrimary,\n\t\tMinimalUpdate: *minimalUpdate,\n\t\tMinimalInsert: *minimalInsert,\n\n\t\tShowGTID:    *showGTID,\n\t\tShowTime:    *showTime,\n\t\tShowAllTime: *showAllTime,\n\t\tShowThread:  *showThread,\n\n\t\tStopNever: *stopNever,\n\t}\n\n\t// thread_id溢出处理\n\tif *threadID > math.MaxUint32 {\n\t\tcfg.ThreadID = uint32(*threadID % (1 << 32))\n\t} else {\n\t\tcfg.ThreadID = uint32(*threadID)\n\t}\n\n\tif p, err := core.NewBinlogParser(context.Background(), cfg); err != nil {\n\t\tlog.Error(\"binlog解析操作失败\")\n\t\tlog.Error(err)\n\t\treturn\n\t} else {\n\t\terr = p.Parser()\n\t\tif err != nil {\n\t\t\tlog.Error(\"binlog解析操作失败\")\n\t\t\tlog.Error(err)\n\t\t\treturn\n\t\t}\n\t}\n}\n\nfunc flagBoolean(name string, shorthand string, defaultVal bool, usage string) *bool {\n\tif !defaultVal {\n\t\t// Fix #4125, golang do not print default false value in usage, so we append it.\n\t\tusage = fmt.Sprintf(\"%s (default false)\", usage)\n\t}\n\treturn flag.BoolP(name, shorthand, defaultVal, usage)\n}\n"
  },
  {
    "path": "cmd/local.go",
    "content": "/*\nCopyright © 2020 NAME HERE <EMAIL ADDRESS>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n\thttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\npackage cmd\n\nimport (\n\t\"context\"\n\t\"math\"\n\n\t\"github.com/hanchuanchuan/bingo2sql/core\"\n\tlog \"github.com/sirupsen/logrus\"\n\t\"github.com/spf13/cobra\"\n)\n\nvar threadID uint64\n\n// rootCmd represents the base command when called without any subcommands\nvar localCmd = &cobra.Command{\n\tUse:   \"local\",\n\tShort: \"本地解析\",\n\tLong:  `指定binlog文件和表结构文件`,\n\t// Uncomment the following line if your bare application\n\t// has an action associated with it:\n\tRun: func(cmd *cobra.Command, args []string) {\n\t\tif cfg.Debug {\n\t\t\tlog.SetLevel(log.DebugLevel)\n\t\t} else {\n\t\t\tlog.SetLevel(log.ErrorLevel)\n\t\t}\n\t\t// thread_id溢出处理\n\t\tif threadID > math.MaxUint32 {\n\t\t\tcfg.ThreadID = uint32(threadID % (1 << 32))\n\t\t} else {\n\t\t\tcfg.ThreadID = uint32(threadID)\n\t\t}\n\t\tif p, err := core.NewBinlogParser(context.Background(), &cfg); err != nil {\n\t\t\tlog.Error(\"binlog解析操作失败\")\n\t\t\tlog.Error(err)\n\t\t\treturn\n\t\t} else {\n\t\t\terr = p.Parser()\n\t\t\tif err != nil {\n\t\t\t\tlog.Error(\"binlog解析操作失败\")\n\t\t\t\tlog.Error(err)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\n\t},\n}\n\nfunc init() {\n\trootCmd.AddCommand(localCmd)\n\n\tflag := localCmd.Flags()\n\tflag.SortFlags = false\n\n\tinitCommonFalg(flag)\n}\n"
  },
  {
    "path": "cmd/remote.go",
    "content": "/*\nCopyright © 2020 NAME HERE <EMAIL ADDRESS>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n\thttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\npackage cmd\n\nimport (\n\t\"context\"\n\t\"math\"\n\n\t\"github.com/hanchuanchuan/bingo2sql/core\"\n\tlog \"github.com/sirupsen/logrus\"\n\t\"github.com/spf13/cobra\"\n)\n\n// rootCmd represents the base command when called without any subcommands\nvar remoteCmd = &cobra.Command{\n\tUse:   \"remote\",\n\tShort: \"远程解析\",\n\tLong:  `远程解析需要指定数据库地址/端口等信息`,\n\t// Uncomment the following line if your bare application\n\t// has an action associated with it:\n\tRun: func(cmd *cobra.Command, args []string) {\n\t\t// thread_id溢出处理\n\t\tif threadID > math.MaxUint32 {\n\t\t\tcfg.ThreadID = uint32(threadID % (1 << 32))\n\t\t} else {\n\t\t\tcfg.ThreadID = uint32(threadID)\n\t\t}\n\t\tif p, err := core.NewBinlogParser(context.Background(), &cfg); err != nil {\n\t\t\tlog.Error(\"binlog解析操作失败\")\n\t\t\tlog.Error(err)\n\t\t\treturn\n\t\t} else {\n\t\t\terr = p.Parser()\n\t\t\tif err != nil {\n\t\t\t\tlog.Error(\"binlog解析操作失败\")\n\t\t\t\tlog.Error(err)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t},\n}\n\nfunc init() {\n\trootCmd.AddCommand(remoteCmd)\n\n\tflag := remoteCmd.Flags()\n\tflag.SortFlags = false\n\n\tflag.StringVarP(&cfg.Host, \"host\", \"h\", \"\", \"host\")\n\tflag.Uint16VarP(&cfg.Port, \"port\", \"P\", 3306, \"port\")\n\tflag.StringVarP(&cfg.User, \"user\", \"u\", \"\", \"user\")\n\tflag.StringVarP(&cfg.Password, \"password\", \"p\", \"\", \"password\")\n\tflag.Bool(\"help\", false, \"help for remote\")\n\n\tflag.BoolVarP(&cfg.StopNever, \"stop-never\", \"N\", false, \"持续解析binlog\")\n\n\tinitCommonFalg(flag)\n}\n"
  },
  {
    "path": "cmd/root.go",
    "content": "/*\nCopyright © 2020 NAME HERE <EMAIL ADDRESS>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n\thttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\npackage cmd\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"math\"\n\t\"os\"\n\n\t\"github.com/hanchuanchuan/bingo2sql/core\"\n\t\"github.com/pkg/profile\"\n\tlog \"github.com/sirupsen/logrus\"\n\t\"github.com/spf13/cobra\"\n\t\"github.com/spf13/pflag\"\n\n\thomedir \"github.com/mitchellh/go-homedir\"\n\t\"github.com/spf13/viper\"\n)\n\nvar mode string\n\n// rootCmd represents the base command when called without any subcommands\nvar rootCmd = &cobra.Command{\n\tUse:   \"bingo2sql\",\n\tShort: \"mysql binlog解析工具\",\n\tLong:  `用于解析mysql binlog,支持正向,逆向. 可本地或远程解析.`,\n\t// Uncomment the following line if your bare application\n\t// has an action associated with it:\n\tRun: func(cmd *cobra.Command, args []string) {\n\t\t// thread_id溢出处理\n\t\tif threadID > math.MaxUint32 {\n\t\t\tcfg.ThreadID = uint32(threadID % (1 << 32))\n\t\t} else {\n\t\t\tcfg.ThreadID = uint32(threadID)\n\t\t}\n\t\tif p, err := core.NewBinlogParser(context.Background(), &cfg); err != nil {\n\t\t\tlog.Error(\"binlog解析操作失败\")\n\t\t\tlog.Error(err)\n\t\t\treturn\n\t\t} else {\n\t\t\terr = p.Parser()\n\t\t\tif err != nil {\n\t\t\t\tlog.Error(\"binlog解析操作失败\")\n\t\t\t\tlog.Error(err)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t},\n}\n\n// Execute adds all child commands to the root command and sets flags appropriately.\n// This is called by main.main(). It only needs to happen once to the rootCmd.\nfunc Execute() {\n\tif err := rootCmd.Execute(); err != nil {\n\t\tfmt.Println(err)\n\t\tos.Exit(1)\n\t}\n}\n\nvar cfg core.BinlogParserConfig\n\nfunc init() {\n\tcobra.OnInitialize(initConfig)\n\n\t// Here you will define your flags and configuration settings.\n\t// Cobra supports persistent flags, which, if defined here,\n\t// will be global for your application.\n\n\t// rootCmd.PersistentFlags().StringVar(&cfgFile, \"config\", \"\", \"config file (default is $HOME/.bingo2sql.yaml)\")\n\n\t// Cobra also supports local flags, which will only run\n\t// when this action is called directly.\n\t// rootCmd.Flags().BoolP(\"toggle\", \"t\", false, \"Help message for toggle\")\n\n\tcfg = core.BinlogParserConfig{\n\t\tPort: 3306,\n\t}\n\n\trootCmd.PersistentFlags().BoolVarP(&cfg.Debug, \"debug\", \"\", false, \"调试模式,输出详细日志.sets log level to debug\")\n\n\tflag := rootCmd.Flags()\n\tflag.SortFlags = false\n\n\tflag.StringVarP(&cfg.Host, \"host\", \"h\", \"\", \"host\")\n\tflag.Uint16VarP(&cfg.Port, \"port\", \"P\", 3306, \"port\")\n\tflag.StringVarP(&cfg.User, \"user\", \"u\", \"\", \"user\")\n\tflag.StringVarP(&cfg.Password, \"password\", \"p\", \"\", \"password\")\n\tflag.Bool(\"help\", false, \"help for remote\")\n\tinitCommonFalg(flag)\n\tflag.BoolVarP(&cfg.StopNever, \"stop-never\", \"N\", false, \"持续解析binlog\")\n}\n\n// initConfig reads in config file and ENV variables if set.\nfunc initConfig() {\n\tif cfgFile != \"\" {\n\t\t// Use config file from the flag.\n\t\tviper.SetConfigFile(cfgFile)\n\t} else {\n\t\t// Find home directory.\n\t\thome, err := homedir.Dir()\n\t\tif err != nil {\n\t\t\tfmt.Println(err)\n\t\t\tos.Exit(1)\n\t\t}\n\n\t\t// Search config in home directory with name \".bingo2sql\" (without extension).\n\t\tviper.AddConfigPath(home)\n\t\tviper.SetConfigName(\".bingo2sql\")\n\t}\n\n\tviper.AutomaticEnv() // read in environment variables that match\n\n\t// If a config file is found, read it in.\n\tif err := viper.ReadInConfig(); err == nil {\n\t\tfmt.Println(\"Using config file:\", viper.ConfigFileUsed())\n\t}\n}\n\nfunc flagBoolean(flag *pflag.FlagSet, p *bool, name string, shorthand string, defaultVal bool, usage string) {\n\tif !defaultVal {\n\t\t// Fix #4125, golang do not print default false value in usage, so we append it.\n\t\tusage = fmt.Sprintf(\"%s (default false)\", usage)\n\t}\n\tflag.BoolVarP(p, name, shorthand, defaultVal, usage)\n}\n\nfunc initCommonFalg(flag *pflag.FlagSet) {\n\n\tflag.StringVar(&cfg.StartFile, \"start-file\", \"\", \"起始binlog文件\")\n\tflag.StringVar(&cfg.StopFile, \"stop-file\", \"\", \"结束binlog文件\")\n\tflag.IntVar(&cfg.StartPosition, \"start-pos\", 0, \"起始位置\")\n\tflag.IntVar(&cfg.StopPosition, \"stop-pos\", 0, \"结束位置\")\n\n\tflag.StringVar(&cfg.StartTime, \"start-time\", \"\", \"开始时间\")\n\tflag.StringVar(&cfg.StopTime, \"stop-time\", \"\", \"结束时间\")\n\n\tflag.StringVarP(&cfg.Databases, \"databases\", \"d\", \"\", \"数据库列表,多个时以逗号分隔\")\n\tflag.StringVarP(&cfg.Tables, \"tables\", \"t\", \"\", \"表名,如果数据库为多个,则需指名表前缀,多个时以逗号分隔\")\n\n\tflag.Uint64VarP(&threadID, \"connection-id\", \"C\", 0, \"指定线程ID\")\n\tflag.BoolVarP(&cfg.Flashback, \"flashback\", \"B\", false, \"逆向语句\")\n\tflag.BoolVarP(&cfg.ParseDDL, \"ddl\", \"\", false, \"解析DDL语句(仅正向SQL)\")\n\n\tflag.StringVar(&cfg.SqlType, \"sql-type\", \"insert,delete,update\", \"解析的语句类型\")\n\n\tflag.IntVar(&cfg.MaxRows, \"max\", 100000, \"解析的最大行数,设置为0则不限制\")\n\tflag.IntVar(&cfg.Threads, \"threads\", 64, \"解析线程数,影响文件解析速度\")\n\n\tflag.StringVarP(&cfg.OutputFileStr, \"output\", \"o\", \"\", \"输出到指定文件\")\n\n\tflag.StringVarP(&cfg.IncludeGtids, \"gtid\", \"g\", \"\", \"GTID范围.格式为uuid:编号[-编号],多个时以逗号分隔\")\n\n\tflagBoolean(flag, &cfg.RemovePrimary, \"no-primary-key\", \"K\", false, \"对INSERT语句去除主键. 可选.\")\n\n\tflagBoolean(flag, &cfg.MinimalUpdate, \"minimal-update\", \"M\", true, \"最小化update语句. 可选.\")\n\n\tflagBoolean(flag, &cfg.MinimalInsert, \"minimal-insert\", \"I\", true, \"使用包含多个VALUES列表的多行语法编写INSERT语句.\")\n\n\tflagBoolean(flag, &cfg.ShowGTID, \"show-gtid\", \"\", true, \"显示gtid\")\n\tflagBoolean(flag, &cfg.ShowTime, \"show-time\", \"\", true, \"显示执行时间,同一时间仅显示首次\")\n\tflagBoolean(flag, &cfg.ShowAllTime, \"show-all-time\", \"\", false, \"显示每条SQL的执行时间\")\n\tflagBoolean(flag, &cfg.ShowThread, \"show-thread\", \"\", false, \"显示线程号,便于区别同一进程操作\")\n\n\tflag.StringVar(&mode, \"profile-mode\", \"\", \"enable profiling mode, one of [cpu, mem, mutex, block]\")\n\n\tswitch mode {\n\tcase \"cpu\":\n\t\tdefer profile.Start(profile.CPUProfile, profile.ProfilePath(\".\")).Stop()\n\tcase \"mem\":\n\t\tdefer profile.Start(profile.MemProfile, profile.ProfilePath(\".\")).Stop()\n\tcase \"mutex\":\n\t\tdefer profile.Start(profile.MutexProfile, profile.ProfilePath(\".\")).Stop()\n\tcase \"block\":\n\t\tdefer profile.Start(profile.BlockProfile, profile.ProfilePath(\".\")).Stop()\n\t}\n}\n"
  },
  {
    "path": "cmd/server.go",
    "content": "/*\nCopyright © 2020 NAME HERE <EMAIL ADDRESS>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\npackage cmd\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"os\"\n\t\"time\"\n\n\t\"github.com/hanchuanchuan/bingo2sql/parse\"\n\t\"github.com/labstack/echo/v4\"\n\t\"github.com/labstack/echo/v4/middleware\"\n\t\"github.com/siddontang/go/log\"\n\t\"github.com/spf13/cobra\"\n\t\"github.com/spf13/viper\"\n)\n\nvar cfgFile string\n\n// rootCmd represents the base command when called without any subcommands\nvar serverCmd = &cobra.Command{\n\tUse:   \"server\",\n\tShort: \"以服务方式运行\",\n\tLong:  `用于解析mysql binlog,支持正向,逆向. 可本地或远程解析.`,\n\t// Uncomment the following line if your bare application\n\t// has an action associated with it:\n\tRun: func(cmd *cobra.Command, args []string) {\n\t\tstartServer()\n\n\t},\n}\n\nfunc init() {\n\trootCmd.AddCommand(serverCmd)\n\n\tflag := serverCmd.Flags()\n\n\tflag.StringVarP(&cfgFile, \"config\", \"c\", \"config.ini\", \"以服务方式启动时需指定配置文件\")\n}\n\nfunc startServer() {\n\n\tviper := viper.New()\n\tviper.SetConfigFile(cfgFile)\n\tviper.SetConfigType(\"ini\")\n\n\tif err := viper.ReadInConfig(); err != nil {\n\t\tlog.Fatalf(\"Error: %s\", err.Error())\n\t\treturn\n\t}\n\n\t// logDir := cnf.Section(\"Bingo\").Key(\"log\").String()\n\t// httpLogDir := cnf.Section(\"Bingo\").Key(\"httplog\").String()\n\t// level := cnf.Section(\"Bingo\").Key(\"logLevel\").String()\n\n\tlogDir := viper.GetString(\"Bingo.log\")\n\thttpLogDir := viper.GetString(\"Bingo.httplog\")\n\n\t//echo's output log file\n\telog, err := os.OpenFile(logDir, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)\n\tif err != nil {\n\t\tfmt.Println(fmt.Sprintf(`open echo log file %s error: %s`, logDir, err.Error()))\n\t\treturn\n\t}\n\tdefer elog.Close()\n\n\thttplog, err := os.OpenFile(httpLogDir, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)\n\tif err != nil {\n\t\tfmt.Println(fmt.Sprintf(`open echo log file %s error: %s`, httpLogDir, err.Error()))\n\t\treturn\n\t}\n\tdefer httplog.Close()\n\n\t// // 初始化Router\n\t// r := mux.NewRouter()\n\t// // // 静态文件路由\n\t// // r.PathPrefix(\"/static/\").Handler(http.StripPrefix(\"/static/\", http.FileServer(http.Dir(dir))))\n\t// // 普通路由\n\t// r.HandleFunc(\"/\", HomeHandler)\n\n\t// r.Use(TestMiddleware)\n\t// http.ListenAndServe(\":3000\", r)\n\n\t// lvl, _ := zerolog.ParseLevel(level)\n\t// zerolog.SetGlobalLevel(lvl)\n\n\t// log.Logger = log.With().Caller().Logger().Output(\n\t// \tzerolog.ConsoleWriter{Out: elog, NoColor: true})\n\n\t// log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})\n\n\t//new echo\n\trouter := echo.New()\n\trouter.Server.WriteTimeout = time.Duration(30) * time.Second\n\n\t// Middleware\n\trouter.Use(middleware.Logger())\n\trouter.Use(middleware.Recover())\n\n\t// Middleware\n\t// router.Use(middleware.LoggerWithConfig(\n\t// \tmiddleware.LoggerConfig{Output: httplog, Format: requestHeader}))\n\t// router.Use(middleware.Recover())\n\n\tlog.Info(`parse binlog tool is started`)\n\tgroup := router.Group(\"/binlog\")\n\n\tgroup.GET(\"/\", func(c echo.Context) error {\n\t\treturn c.String(http.StatusOK, \"Hello, World!\\n\")\n\t})\n\n\t// 发起binlog解析请求\n\tgroup.POST(\"/parse\", parse.ParseBinlog)\n\n\t// 解析进程的进度\n\tgroup.GET(\"/parse/:id\", parse.GetParseInfo)\n\n\t// 获取所有解析\n\tgroup.GET(\"/parse\", parse.GetAllParse)\n\n\t// 停止解析操作\n\tgroup.DELETE(\"/parse/:id\", parse.ParseBinlogStop)\n\n\t// 下载解析生成的文件\n\tgroup.GET(\"/parse/download/:id\", parse.Download)\n\n\t// router.Logger.Fatal(router.Start(addr))\n\trouter.Logger.Fatal(router.Start(\":8077\"))\n}\n"
  },
  {
    "path": "cmd/stats.go",
    "content": "/*\nCopyright © 2020 NAME HERE <EMAIL ADDRESS>\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\npackage cmd\n\nimport (\n\t\"github.com/hanchuanchuan/bingo2sql/core\"\n\t\"github.com/pkg/profile\"\n\tlog \"github.com/sirupsen/logrus\"\n\t\"github.com/spf13/cobra\"\n)\n\n// runServer = flagBoolean(flag,, \"server\", \"s\", false, \"启动API服务\")\n// summary = flagBoolean(flag, \"summary\", \"S\", false, \"统计binlog文件中的DML次数等信息\")\n// configFile = flag.StringP(\"config\", \"c\", \"config.ini\", \"以服务方式启动时需指定配置文件\")\n\n// rootCmd represents the base command when called without any subcommands\nvar summaryCmd = &cobra.Command{\n\tUse:   \"stats\",\n\tShort: \"stats binlog events\",\n\tLong:  `统计binlog文件中的DML次数等信息`,\n\tRun: func(cmd *cobra.Command, args []string) {\n\t\tif cfg.Debug {\n\t\t\tlog.SetLevel(log.DebugLevel)\n\t\t} else {\n\t\t\tlog.SetLevel(log.ErrorLevel)\n\t\t}\n\t\tif p, err := core.NewBinlogParserStats(&cfg); err != nil {\n\t\t\tlog.Error(\"binlog解析操作失败\")\n\t\t\tlog.Error(err)\n\t\t\treturn\n\t\t} else {\n\t\t\terr = p.ParserStats()\n\t\t\tif err != nil {\n\t\t\t\tlog.Error(\"binlog解析操作失败\")\n\t\t\t\tlog.Error(err)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\n\t},\n}\n\nfunc init() {\n\trootCmd.AddCommand(summaryCmd)\n\n\tflag := summaryCmd.Flags()\n\tflag.SortFlags = false\n\n\tflag.StringVarP(&cfg.Host, \"host\", \"h\", \"\", \"host\")\n\tflag.Uint16VarP(&cfg.Port, \"port\", \"P\", 3306, \"port\")\n\tflag.StringVarP(&cfg.User, \"user\", \"u\", \"\", \"user\")\n\tflag.StringVarP(&cfg.Password, \"password\", \"p\", \"\", \"password\")\n\tflag.Bool(\"help\", false, \"help for remote\")\n\n\tflag.StringVar(&cfg.StartFile, \"start-file\", \"\", \"起始binlog文件\")\n\tflag.StringVar(&cfg.StopFile, \"stop-file\", \"\", \"结束binlog文件\")\n\tflag.IntVar(&cfg.StartPosition, \"start-pos\", 0, \"起始位置\")\n\tflag.IntVar(&cfg.StopPosition, \"stop-pos\", 0, \"结束位置\")\n\n\tflag.StringVar(&cfg.StartTime, \"start-time\", \"\", \"开始时间\")\n\tflag.StringVar(&cfg.StopTime, \"stop-time\", \"\", \"结束时间\")\n\n\tflag.StringVarP(&cfg.Databases, \"databases\", \"d\", \"\", \"数据库列表,多个时以逗号分隔\")\n\tflag.StringVarP(&cfg.Tables, \"tables\", \"t\", \"\", \"表名,如果数据库为多个,则需指名表前缀,多个时以逗号分隔\")\n\n\tflag.BoolVarP(&cfg.ParseDDL, \"ddl\", \"\", false, \"解析DDL语句(仅正向SQL)\")\n\n\tflag.IntVar(&cfg.Threads, \"threads\", 64, \"解析线程数,影响文件解析速度\")\n\n\tflag.StringVarP(&cfg.OutputFileStr, \"output\", \"o\", \"\", \"输出到指定文件\")\n\n\tflag.StringVar(&mode, \"profile-mode\", \"\", \"enable profiling mode, one of [cpu, mem, mutex, block]\")\n\n\tswitch mode {\n\tcase \"cpu\":\n\t\tdefer profile.Start(profile.CPUProfile, profile.ProfilePath(\".\")).Stop()\n\tcase \"mem\":\n\t\tdefer profile.Start(profile.MemProfile, profile.ProfilePath(\".\")).Stop()\n\tcase \"mutex\":\n\t\tdefer profile.Start(profile.MutexProfile, profile.ProfilePath(\".\")).Stop()\n\tcase \"block\":\n\t\tdefer profile.Start(profile.BlockProfile, profile.ProfilePath(\".\")).Stop()\n\t}\n\n}\n"
  },
  {
    "path": "cnf/config.ini",
    "content": "[Bingo]\naddr = :8077\nlog = logs/go.log\nhttplog = logs/http.log\nwriteTimeout = 30\nsocketAddr = 127.0.0.1:8090\n# 日志级别:  debug info warn error fatal\nlogLevel = debug\n\n# 数据库配置\n[DBConfig]\nHost = 127.0.0.1\nPort = 3306\nUser = test\nPassword = test\nDB = dbmonitor\n\n# 数据库配置\n[BackupDBConfig]\nHost = 127.0.0.1\nPort = 20001\nUser = test\nPassword = test"
  },
  {
    "path": "core/parseFile.go",
    "content": "package core\n\nimport (\n\t\"bufio\"\n\t\"os\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/go-mysql-org/go-mysql/mysql\"\n\t\"github.com/go-mysql-org/go-mysql/replication\"\n\t\"github.com/mholt/archiver/v3\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\nfunc (p *MyBinlogParser) parserFile() error {\n\n\tvar wg sync.WaitGroup\n\n\tdefer timeTrack(time.Now(), \"parserFile\")\n\n\tdefer func() {\n\t\tif p.outputFile != nil {\n\t\t\tp.outputFile.Close()\n\t\t}\n\t}()\n\n\tvar err error\n\n\tp.running = true\n\n\tif len(p.outputFileName) > 0 {\n\n\t\tif Exists(p.outputFileName) {\n\t\t\t// p.checkError(errors.New(\"指定的文件已存在!\"))\n\t\t\t// 不作追加,改为文件存在时清除内容,O_APPEND\n\t\t\tp.outputFile, err = os.OpenFile(p.outputFileName,\n\t\t\t\tos.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0666)\n\t\t} else {\n\t\t\tp.outputFile, err = os.Create(p.outputFileName)\n\t\t}\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tp.bufferWriter = bufio.NewWriter(p.outputFile)\n\t}\n\n\tb := replication.NewBinlogParser()\n\tb.SetUseDecimal(true)\n\n\tp.currentPosition = mysql.Position{\n\t\tName: p.startFile,\n\t\tPos:  uint32(p.cfg.StartPosition),\n\t}\n\n\tsendTime := time.Now().Add(time.Second * 5)\n\tsendCount := 0\n\n\twg.Add(1)\n\tgo p.ProcessChan(&wg)\n\n\tf := func(e *replication.BinlogEvent) error {\n\n\t\tok, err := p.parseSingleEvent(e)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif !ok {\n\t\t\tb.Stop()\n\t\t}\n\n\t\tif e.Header.EventType == replication.QUERY_EVENT ||\n\t\t\te.Header.EventType == replication.UPDATE_ROWS_EVENTv2 ||\n\t\t\te.Header.EventType == replication.WRITE_ROWS_EVENTv2 ||\n\t\t\te.Header.EventType == replication.DELETE_ROWS_EVENTv2 {\n\n\t\t\tif len(p.cfg.SocketUser) > 0 {\n\t\t\t\t// 如果指定了websocket用户,就每5s发送一次进度通知\n\t\t\t\tif p.changeRows > sendCount && time.Now().After(sendTime) {\n\t\t\t\t\tsendCount = p.changeRows\n\t\t\t\t\tsendTime = time.Now().Add(time.Second * 5)\n\n\t\t\t\t\tkwargs := map[string]interface{}{\"rows\": p.changeRows}\n\t\t\t\t\tif p.stopTimestamp > 0 && p.startTimestamp > 0 && p.stopTimestamp > p.startTimestamp {\n\t\t\t\t\t\tkwargs[\"pct\"] = (e.Header.Timestamp - p.startTimestamp) * 100 / (p.stopTimestamp - p.startTimestamp)\n\t\t\t\t\t}\n\t\t\t\t\tgo sendMsg(p.cfg.SocketUser, \"binlog_parse_progress\", \"binlog解析进度\", \"\", kwargs)\n\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif p.cfg.MaxRows > 0 && p.changeRows >= p.cfg.MaxRows {\n\t\t\t\tlog.Info(\"已超出最大行数\")\n\n\t\t\t\tif !p.cfg.StopNever {\n\t\t\t\t\tb.Stop()\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// 再次判断是否到结束位置,以免处在临界值,且无新日志时程序卡住\n\t\tif e.Header.Timestamp > 0 {\n\t\t\tif p.stopTimestamp > 0 && e.Header.Timestamp >= p.stopTimestamp {\n\t\t\t\tlog.Warn(\"已超出结束时间\")\n\n\t\t\t\tb.Stop()\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\n\t\treturn nil\n\t}\n\n\terr = b.ParseFile(p.startFile, int64(p.cfg.StartPosition), f)\n\n\tif err != nil {\n\t\tprintln(err.Error())\n\t}\n\n\tlog.Info(\"操作完成\")\n\n\tclose(p.ch)\n\n\twg.Wait()\n\n\tif len(p.cfg.SocketUser) > 0 {\n\t\tif p.changeRows > 0 {\n\t\t\tkwargs := map[string]interface{}{\"rows\": p.changeRows}\n\t\t\tkwargs[\"pct\"] = 99\n\n\t\t\tgo sendMsg(p.cfg.SocketUser, \"binlog_parse_progress\", \"binlog解析进度\", \"\", kwargs)\n\n\t\t\tvar url string\n\t\t\tif strings.Count(p.outputFileName, \".\") > 0 {\n\t\t\t\ta := strings.Split(p.outputFileName, \".\")\n\t\t\t\turl = strings.Join(a[0:len(a)-1], \".\")\n\t\t\t} else {\n\t\t\t\turl = p.outputFileName\n\t\t\t}\n\n\t\t\turl = url + \".tar.gz\"\n\n\t\t\t// err := archiver.TarGz.Make(url, []string{p.outputFileName})\n\t\t\t// err := archiver.TarGz.Archiver(\n\t\t\t//     []string{p.outputFileName},\n\t\t\t//     url)\n\t\t\terr := archiver.Archive([]string{p.outputFileName}, url)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tfileInfo, _ := os.Stat(url)\n\t\t\t//文件大小\n\t\t\tfilesize := fileInfo.Size()\n\n\t\t\t// 压缩完成后删除文件\n\t\t\tp.clear()\n\n\t\t\tlog.Info(\"压缩完成\")\n\t\t\tkwargs = map[string]interface{}{\"ok\": \"1\", \"pct\": 100, \"rows\": p.changeRows,\n\t\t\t\t\"url\": \"/go/download/\" + strings.Replace(url, \"../\", \"\", -1), \"size\": filesize}\n\t\t\tgo sendMsg(p.cfg.SocketUser, \"binlog_parse_progress\", \"binlog解析进度\",\n\t\t\t\t\"\", kwargs)\n\t\t} else {\n\n\t\t\t// 没有数据时清除文件\n\t\t\tp.clear()\n\n\t\t\tkwargs := map[string]interface{}{\"ok\": \"1\", \"size\": 0, \"pct\": 100, \"rows\": 0}\n\n\t\t\tgo sendMsg(p.cfg.SocketUser, \"binlog_parse_progress\", \"binlog解析进度\", \"\", kwargs)\n\t\t}\n\t}\n\n\tlog.Info(\"操作结束\")\n\treturn nil\n}\n"
  },
  {
    "path": "core/parser.go",
    "content": "package core\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"context\"\n\t\"database/sql/driver\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"math\"\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/go-mysql-org/go-mysql/mysql\"\n\t\"github.com/go-mysql-org/go-mysql/replication\"\n\tuuid \"github.com/hanchuanchuan/bingo2sql/utils/uuid\"\n\t\"github.com/jinzhu/gorm\"\n\t\"github.com/jinzhu/now\"\n\t\"github.com/mholt/archiver/v3\"\n\t\"github.com/shopspring/decimal\"\n\tlog \"github.com/sirupsen/logrus\"\n\n\t\"github.com/hanchuanchuan/goInception/ast\"\n\ttidb \"github.com/hanchuanchuan/goInception/mysql\"\n\ttidbParser \"github.com/hanchuanchuan/goInception/parser\"\n)\n\nconst digits01 = \"0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789\"\nconst digits10 = \"0000000000111111111122222222223333333333444444444455555555556666666666777777777788888888889999999999\"\n\n// Column 列结构\ntype Column struct {\n\tgorm.Model\n\tColumnName       string `gorm:\"Column:COLUMN_NAME\"`\n\tCollationName    string `gorm:\"Column:COLLATION_NAME\"`\n\tCharacterSetName string `gorm:\"Column:CHARACTER_SET_NAME\"`\n\tColumnComment    string `gorm:\"Column:COLUMN_COMMENT\"`\n\tColumnType       string `gorm:\"Column:COLUMN_TYPE\"`\n\tColumnKey        string `gorm:\"Column:COLUMN_KEY\"`\n\tExtra            string `gorm:\"Column:EXTRA\"`\n\tisGenerated      *bool  `gorm:\"-\"`\n}\n\n// IsGenerated 是否为计算列\nfunc (f *Column) IsGenerated() bool {\n\tif f.isGenerated == nil {\n\t\tv := strings.Contains(f.Extra, \"VIRTUAL GENERATED\") ||\n\t\t\tstrings.Contains(f.Extra, \"STORED GENERATED\")\n\t\tf.isGenerated = &v\n\t}\n\treturn *f.isGenerated\n}\n\n// IsUnsigned 是否无符号列\nfunc (f *Column) IsUnsigned() bool {\n\tcolumnType := f.ColumnType\n\tif strings.Contains(columnType, \"unsigned\") || strings.Contains(columnType, \"zerofill\") {\n\t\treturn true\n\t}\n\treturn false\n}\n\n// Table 表结构\ntype Table struct {\n\ttableID uint64\n\n\t// 仅本地解析使用\n\tTableName string\n\tSchema    string\n\n\tColumns    []Column\n\thasPrimary bool\n\tprimarys   map[int]bool\n}\n\n// ValidColumns 可用列\nfunc (t *Table) ValidColumns() (result []Column) {\n\tif t == nil {\n\t\treturn\n\t}\n\tfor _, f := range t.Columns {\n\t\tif !f.IsGenerated() {\n\t\t\tresult = append(result, f)\n\t\t}\n\t}\n\treturn result\n}\n\n// MasterStatus 主库binlog信息（相对bingo2sql的主库，可以是只读库）\ntype MasterStatus struct {\n\tgorm.Model\n\tFile              string `gorm:\"Column:File\"`\n\tPosition          int    `gorm:\"Column:Position\"`\n\tBinlog_Do_DB      string `gorm:\"Column:Binlog_Do_DB\"`\n\tBinlog_Ignore_DB  string `gorm:\"Column:Binlog_Ignore_DB\"`\n\tExecuted_Gtid_Set string `gorm:\"Column:Executed_Gtid_Set\"`\n}\n\n// MasterLog 主库binlog日志\ntype MasterLog struct {\n\tgorm.Model\n\tName string `gorm:\"Column:Log_name\"`\n\tSize int    `gorm:\"Column:File_size\"`\n}\n\n// GtidSetInfo GTID集合,可指定或排除GTID范围\ntype GtidSetInfo struct {\n\tuuid       []byte\n\tstartSeqNo int64\n\tstopSeqNo  int64\n}\n\n// BinlogParserConfig 解析选项\ntype BinlogParserConfig struct {\n\tFlavor string\n\n\t// InsID    int    `json:\"ins_id\" form:\"ins_id\"`\n\tHost     string `json:\"host\" form:\"host\"`\n\tPort     uint16 `json:\"port\" form:\"port\"`\n\tUser     string `json:\"user\" form:\"user\"`\n\tPassword string `json:\"password\" form:\"password\"`\n\n\t// binlog文件,位置\n\tStartFile     string `json:\"start_file\" form:\"start_file\"`\n\tStopFile      string `json:\"stop_file\" form:\"stop_file\"`\n\tStartPosition int    `json:\"start_position\" form:\"start_position\"`\n\tStopPosition  int    `json:\"stop_position\" form:\"stop_position\"`\n\n\t// 起止时间\n\tStartTime string `json:\"start_time\" form:\"start_time\"`\n\tStopTime  string `json:\"stop_time\" form:\"stop_time\"`\n\n\t// 回滚\n\tFlashback bool `json:\"flashback\" form:\"flashback\"`\n\n\t// 解析DDL语句\n\tParseDDL bool `json:\"parse_ddl\" form:\"parse_ddl\"`\n\n\t// 限制的数据库\n\tDatabases string `json:\"databases\" form:\"databases\"`\n\t// 限制的表\n\tTables string `json:\"tables\" form:\"tables\"`\n\t// sql类型\n\tSqlType string `json:\"sql_type\" form:\"sql_type\"`\n\t// 最大解析行数\n\tMaxRows int `json:\"max_rows\" form:\"max_rows\"`\n\t// 解析线程数,影响文件解析速度\n\tThreads int `json:\"threads\" form:\"threads\"`\n\n\t// 输出到指定文件.为空时输出到控制台\n\tOutputFileStr string\n\n\t// 输出所有列(已弃用,有主键时均会使用主键)\n\t// AllColumns bool `json:\"all_columns\" form:\"all_columns\"`\n\n\t// debug模式,打印详细日志\n\tDebug bool `json:\"debug\" form:\"debug\"`\n\n\t// websocket的用户名,用以发送进度提醒\n\tSocketUser string `json:\"socket_user\" form:\"socket_user\"`\n\n\t// 解析任务开始时间\n\tbeginTime int64\n\n\tThreadID uint32 `json:\"thread_id\" form:\"thread_id\"`\n\n\tIncludeGtids string `json:\"include_gtids\" form:\"include_gtids\"`\n\n\t// 对INSERT语句去除主键,以便于使用. 默认为false\n\tRemovePrimary bool\n\t// 持续解析binlog\n\tStopNever bool\n\t// 最小化Update语句, 当开启时,update语句中未变更的值不再记录到解析结果中\n\tMinimalUpdate bool\n\t// 使用包含多个VALUES列表的多行语法编写INSERT语句\n\tMinimalInsert bool\n\n\t// 输出设置\n\tShowGTID    bool // 输出GTID\n\tShowTime    bool // 输出执行时间(相同时间时仅返回首次)\n\tShowAllTime bool // 输出所有执行时间\n\tShowThread  bool // 输出线程号,方便判断同一执行人的操作\n\n\tuniqueKey string\n}\n\ntype Parser interface {\n\twrite([]byte, *replication.BinlogEvent)\n}\n\n// row 协程解析binlog事件的通道\ntype row struct {\n\tsql  []byte\n\te    *replication.BinlogEvent\n\tgtid []byte\n\topid string\n\n\t// 用以打印线程号,不打印时置空\n\tthreadID uint32\n}\n\ntype baseParser struct {\n\tctx      context.Context\n\tcancelFn context.CancelFunc\n\n\t// 记录表结构以重用\n\tallTables map[uint64]*Table\n\n\tcfg *BinlogParserConfig\n\n\tstartFile string\n\tstopFile  string\n\n\t// binlog数据库\n\tdb *gorm.DB\n\n\trunning bool\n\n\tlastTimestamp uint32\n\n\twrite1 Parser\n\n\tch chan *row\n\n\t// currentBackupInfo BackupInfo\n\n\t// gtid []byte\n\t// write interface{}\n\n\tOnlyDatabases map[string]bool\n\tOnlyTables    map[string]string\n\tSqlTypes      map[string]bool\n\n\t// 本地解析模式.指定binlog文件和表结构本地解析\n\tlocalMode bool\n}\n\n// MyBinlogParser 解析类\ntype MyBinlogParser struct {\n\tbaseParser\n\n\tmasterStatus *MasterStatus\n\n\tstartTimestamp uint32\n\tstopTimestamp  uint32\n\t// 输出到指定文件\n\toutputFile     *os.File\n\toutputFileName string\n\n\t// db *gorm.DB\n\n\t// 使用buffer写的测试\n\t// 解析600M日志,约500w行,直接使用文件时,用时5min\n\t// 使用buffer用时1min\n\tbufferWriter *bufio.Writer\n\n\t// lastTimestamp uint32\n\n\tgtid      []byte\n\tlastGtid  []byte\n\tgtidEvent *replication.GTIDEvent\n\n\tincludeGtids []*GtidSetInfo\n\n\tjumpGtids map[*GtidSetInfo]bool\n\n\t// 表结构缓存(仅用于本地解析)\n\ttableCacheList map[string]*Table\n\n\t// 解析用临时变量\n\tcurrentPosition  mysql.Position // 当前binlog位置\n\tcurrentThreadID  uint32         // 当前event线程号\n\tchangeRows       int            // 解析SQL行数\n\tcurrentTimtstamp uint32         // 当前解析到的时间戳\n\n\t// 保存binlogs文件和position,用以计算percent\n\tbinlogs []MasterLog\n\n\t// 表结构发生变化后导致无法正确解析的列表\n\tignoreTables map[string]interface{}\n}\n\nvar (\n\tTimeFormat   string = \"2006-01-02 15:04:05\"\n\tTimeLocation *time.Location\n)\n\nfunc init() {\n\tTimeLocation, _ = time.LoadLocation(\"Asia/Shanghai\")\n}\n\n// SetUniqueKey 自定义唯一标识, 指定后通过ID()将返回该值\nfunc (cfg *BinlogParserConfig) SetUniqueKey(key string) {\n\tcfg.uniqueKey = key\n}\n\n// ID 解析任务的唯一标识\nfunc (cfg *BinlogParserConfig) ID() string {\n\tif cfg.uniqueKey != \"\" {\n\t\treturn cfg.uniqueKey\n\t}\n\n\ts1 := strings.Replace(cfg.Host, \".\", \"_\", -1)\n\tif len(s1) > 20 {\n\t\ts1 = s1[:20]\n\t}\n\tvar s2 string\n\t// if len(cfg.StartTime) > 0 {\n\t// \ts2 = strings.Replace(cfg.StartTime, \".\", \"_\", -1)\n\t// \ts2 = strings.Replace(s2, \" \", \"_\", -1)\n\t// \ts2 = strings.Replace(s2, \":\", \"\", -1)\n\t// } else {\n\t// \ts2 = strconv.FormatInt(cfg.beginTime, 10)\n\t// }\n\ts2 = strconv.FormatInt(cfg.beginTime, 10)\n\tif cfg.Flashback {\n\t\ts2 += \"_rollback\"\n\t}\n\treturn fmt.Sprintf(\"%s_%d_%s\",\n\t\ts1,\n\t\tcfg.Port,\n\t\ts2)\n}\n\n// Parser 远程解析\nfunc (p *MyBinlogParser) Parser() error {\n\t// runtime.GOMAXPROCS(runtime.NumCPU())\n\n\tif p.cfg.Host == \"\" && p.cfg.StartFile != \"\" {\n\t\t_, err := os.Stat(p.cfg.StartFile)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"open start_file: %w\", err)\n\t\t}\n\t\treturn p.parserFile()\n\t}\n\n\tvar wg sync.WaitGroup\n\n\tdefer timeTrack(time.Now(), \"Parser\")\n\n\tdefer func() {\n\t\tif p.outputFile != nil {\n\t\t\tp.outputFile.Close()\n\t\t}\n\t}()\n\n\tvar err error\n\n\tp.running = true\n\n\tp.db, err = p.getDB()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tdefer p.db.Close()\n\n\tif len(p.outputFileName) > 0 {\n\n\t\tif Exists(p.outputFileName) {\n\t\t\t// p.checkError(errors.New(\"指定的文件已存在!\"))\n\t\t\t// 不作追加,改为文件存在时清除内容,O_APPEND\n\t\t\tp.outputFile, err = os.OpenFile(p.outputFileName,\n\t\t\t\tos.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0666)\n\t\t} else {\n\t\t\tp.outputFile, err = os.Create(p.outputFileName)\n\t\t}\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"create output file: %w\", err)\n\t\t}\n\n\t\tp.bufferWriter = bufio.NewWriter(p.outputFile)\n\t}\n\n\tcfg := replication.BinlogSyncerConfig{\n\t\tServerID: 2000000111,\n\t\tFlavor:   p.cfg.Flavor,\n\n\t\tHost:       p.cfg.Host,\n\t\tPort:       p.cfg.Port,\n\t\tUser:       p.cfg.User,\n\t\tPassword:   p.cfg.Password,\n\t\tUseDecimal: true,\n\t\t// RawModeEnabled:  p.cfg.RawMode,\n\t\t// SemiSyncEnabled: p.cfg.SemiSync,\n\t}\n\n\tb := replication.NewBinlogSyncer(cfg)\n\tdefer b.Close()\n\n\tp.currentPosition = mysql.Position{\n\t\tName: p.startFile,\n\t\tPos:  uint32(p.cfg.StartPosition),\n\t}\n\n\ts, err := b.StartSync(p.currentPosition)\n\tif err != nil {\n\t\tlog.Infof(\"Start sync error: %v\\n\", err)\n\t\treturn fmt.Errorf(\"Start sync: %w\", err)\n\t}\n\n\tsendTime := time.Now().Add(time.Second * 5)\n\tsendCount := 0\n\n\twg.Add(1)\n\tgo p.ProcessChan(&wg)\n\n\t// var ctx context.Context\n\n\tvar finishError error\nFOR:\n\tfor {\n\t\t// if p.cfg.StopNever {\n\t\t// \tctx = p.ctx\n\t\t// } else {\n\t\t// \tvar cancel context.CancelFunc\n\t\t// \tctx, cancel = context.WithTimeout(p.ctx, 10*time.Second)\n\t\t// \tdefer cancel()\n\t\t// }\n\t\t// e, err := s.GetEvent(context.Background())\n\t\te, err := s.GetEvent(p.ctx)\n\t\tif err != nil {\n\t\t\t// Try to output all left events\n\t\t\t// events := s.DumpEvents()\n\t\t\t// for _, e := range events {\n\t\t\t//  // e.Dump(os.Stdout)\n\t\t\t//  log.Info(\"===============\")\n\t\t\t//  log.Info(e.Header.EventType)\n\t\t\t// }\n\t\t\tif err == context.DeadlineExceeded {\n\t\t\t\tlog.Warnf(\"Waiting for timeout(10s), no new event is generated, automatically stop: %v\\n\", err)\n\t\t\t\t// err = fmt.Errorf(\"Waiting for timeout(10s), no new event is generated, automatically stop: %v\\n\", err)\n\t\t\t\terr = nil\n\t\t\t} else {\n\t\t\t\tlog.Errorf(\"Get event error: %v\\n\", err)\n\t\t\t}\n\t\t\tfinishError = fmt.Errorf(\"get binlog event: %w\", err)\n\t\t\tbreak FOR\n\t\t}\n\n\t\tselect {\n\t\tcase <-p.ctx.Done():\n\t\t\tfinishError = context.Canceled\n\t\t\tbreak FOR\n\t\tdefault:\n\t\t\tok, err := p.parseSingleEvent(e)\n\t\t\tif err != nil {\n\t\t\t\tfinishError = err\n\t\t\t\tbreak FOR\n\t\t\t}\n\t\t\tif !ok {\n\t\t\t\tbreak FOR\n\t\t\t}\n\n\t\t\t// if time.Now().After(sendTime) {\n\t\t\t// \tvar m runtime.MemStats\n\t\t\t// \truntime.ReadMemStats(&m)\n\t\t\t// \tlog.Debugf(\"%d MB\\n\", m.Alloc/1024/1024)\n\t\t\t// \tsendTime = time.Now().Add(time.Second * 1)\n\t\t\t// }\n\n\t\t\tif len(p.cfg.SocketUser) > 0 {\n\t\t\t\t// 如果指定了websocket用户,就每5s发送一次进度通知\n\t\t\t\tif p.changeRows > sendCount && time.Now().After(sendTime) {\n\t\t\t\t\tsendCount = p.changeRows\n\t\t\t\t\tsendTime = time.Now().Add(time.Second * 5)\n\n\t\t\t\t\tkwargs := map[string]interface{}{\"rows\": p.changeRows}\n\t\t\t\t\tif p.stopTimestamp > 0 && p.startTimestamp > 0 && p.stopTimestamp > p.startTimestamp {\n\t\t\t\t\t\tkwargs[\"pct\"] = (e.Header.Timestamp - p.startTimestamp) * 100 / (p.stopTimestamp - p.startTimestamp)\n\t\t\t\t\t}\n\t\t\t\t\tgo sendMsg(p.cfg.SocketUser, \"binlog_parse_progress\", \"binlog解析进度\", \"\", kwargs)\n\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tclose(p.ch)\n\twg.Wait()\n\n\tif len(p.cfg.SocketUser) > 0 {\n\t\tif p.changeRows > 0 {\n\t\t\tkwargs := map[string]interface{}{\"rows\": p.changeRows}\n\t\t\tkwargs[\"pct\"] = 99\n\n\t\t\tgo sendMsg(p.cfg.SocketUser, \"binlog_parse_progress\", \"binlog解析进度\", \"\", kwargs)\n\n\t\t\t// 打包,以便下载\n\t\t\tfileSize, err := p.Archive()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tkwargs = map[string]interface{}{\n\t\t\t\t\"ok\":   \"1\",\n\t\t\t\t\"pct\":  100,\n\t\t\t\t\"rows\": p.changeRows,\n\t\t\t\t\"size\": fileSize}\n\t\t\tgo sendMsg(p.cfg.SocketUser, \"binlog_parse_progress\", \"binlog解析进度\",\n\t\t\t\t\"\", kwargs)\n\t\t} else {\n\n\t\t\t// 没有数据时清除文件\n\t\t\tp.clear()\n\n\t\t\tkwargs := map[string]interface{}{\"ok\": \"1\", \"size\": 0, \"pct\": 100, \"rows\": 0}\n\n\t\t\tgo sendMsg(p.cfg.SocketUser, \"binlog_parse_progress\", \"binlog解析进度\", \"\", kwargs)\n\t\t}\n\t}\n\n\tp.running = false\n\tlog.WithField(\"parsed_rows\", p.changeRows).Info(\"解析完成\")\n\treturn finishError\n}\n\n// checkFinish 检查是否需要结束循环\nfunc (p *MyBinlogParser) checkFinish(currentPosition *mysql.Position) int {\n\treturnValue := -1\n\t// 满足以下任一条件时结束循环\n\t// * binlog文件超出指定结束文件\n\t// * 超出指定结束文件的指定位置\n\t// * 超出当前最新binlog位置\n\n\t// 当前binlog解析位置 相对于配置的结束位置/最新位置\n\t// 超出时返回 1\n\t// 等于时返回 0\n\t// 小于时返回 -1\n\n\t// 根据是否为事件开始做不同判断\n\t// 事件开始时做大于判断\n\t// 事件结束时做等于判断\n\tvar stopMsg string\n\tif p.stopFile != \"\" && currentPosition.Name > p.stopFile {\n\t\tstopMsg = \"超出指定结束文件\"\n\t\treturnValue = 1\n\t} else if p.cfg.StopPosition > 0 && currentPosition.Name == p.stopFile {\n\t\tif currentPosition.Pos > uint32(p.cfg.StopPosition) {\n\t\t\tstopMsg = \"超出指定位置\"\n\t\t\treturnValue = 1\n\t\t} else if currentPosition.Pos == uint32(p.cfg.StopPosition) {\n\t\t\tstopMsg = \"超出指定位置\"\n\t\t\treturnValue = 0\n\t\t}\n\t}\n\n\tif p.masterStatus != nil {\n\t\tif currentPosition.Name > p.masterStatus.File ||\n\t\t\tcurrentPosition.Name == p.masterStatus.File &&\n\t\t\t\tcurrentPosition.Pos > uint32(p.masterStatus.Position) {\n\t\t\tstopMsg = \"超出最新binlog位置\"\n\t\t\treturnValue = 1\n\t\t} else if currentPosition.Name == p.masterStatus.File &&\n\t\t\tcurrentPosition.Pos == uint32(p.masterStatus.Position) {\n\t\t\tstopMsg = \"超出最新binlog位置\"\n\t\t\treturnValue = 0\n\t\t}\n\t}\n\n\tif stopMsg != \"\" {\n\t\tlog.WithFields(log.Fields{\n\t\t\t\"当前文件\": currentPosition.Name,\n\t\t\t\"结束文件\": p.stopFile,\n\t\t\t\"当前位置\": currentPosition.Pos,\n\t\t\t\"结束位置\": p.cfg.StopPosition,\n\t\t}).Info(stopMsg)\n\t}\n\treturn returnValue\n}\n\n// clear 压缩完成或者没有数据时删除文件\nfunc (p *MyBinlogParser) clear() {\n\terr := os.Remove(p.outputFileName)\n\tif err != nil {\n\t\tlog.Error(err)\n\t\tlog.Errorf(\"删除文件失败! %s\", p.outputFileName)\n\t}\n}\n\n// isGtidEventInGtidSet 检查gtid事件\n// 0 正常,包含\n// 1 跳转,不包含\n// 2 超出结束位置,跳过\nfunc (p *MyBinlogParser) isGtidEventInGtidSet() (status uint8) {\n\n\te := p.gtidEvent\n\n\t// 0 正常,包含\n\t// 1 跳转,不包含\n\t// 2 超出结束位置,跳过\n\tif len(p.includeGtids) == 0 {\n\t\tstatus = 0\n\t\treturn\n\t}\n\n\tif e == nil {\n\t\tstatus = 0\n\t\treturn\n\t}\n\n\tfor _, info := range p.includeGtids {\n\t\tif ok, _ := p.jumpGtids[info]; !ok && bytes.Equal(e.SID, info.uuid) {\n\t\t\tif e.GNO < info.startSeqNo {\n\t\t\t\tstatus = 1\n\t\t\t\treturn\n\t\t\t} else if e.GNO > info.stopSeqNo {\n\t\t\t\tp.jumpGtids[info] = true\n\t\t\t} else {\n\t\t\t\tstatus = 0\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}\n\n\tall_ok := true\n\tfor _, value := range p.jumpGtids {\n\t\tif !value {\n\t\t\tall_ok = false\n\t\t}\n\t}\n\n\tif all_ok {\n\t\tstatus = 2\n\t\treturn\n\t}\n\n\tstatus = 1\n\treturn\n}\n\nfunc (p *MyBinlogParser) Stop() {\n\tlog.Warn(\"Process killed\")\n\tif p.cancelFn != nil {\n\t\tp.cancelFn()\n\t}\n\tp.running = false\n}\n\nfunc (p *MyBinlogParser) write(b []byte, binEvent *replication.BinlogEvent) {\n\tdata := &row{\n\t\tsql: b,\n\t\te:   binEvent,\n\t}\n\n\tif p.Config().ShowThread {\n\t\tdata.threadID = p.currentThreadID\n\t}\n\tif p.Config().ShowGTID {\n\t\tdata.gtid = p.gtid\n\t}\n\tp.ch <- data\n}\n\n// byteEquals 判断字节切片是否相等\nfunc byteEquals(v1, v2 []byte) bool {\n\tif len(v1) != len(v2) {\n\t\treturn false\n\t}\n\tfor i, v := range v1 {\n\t\tif v != v2[i] {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\nfunc (p *MyBinlogParser) myWrite(data *row) {\n\tvar buf bytes.Buffer\n\t// 输出GTID\n\tif p.Config().ShowGTID && len(data.gtid) > 0 {\n\t\tif len(p.lastGtid) == 0 {\n\t\t\tp.lastGtid = data.gtid\n\t\t\tbuf.WriteString(\"# \")\n\t\t\tbuf.Write(data.gtid)\n\t\t\tbuf.WriteString(\"\\n\")\n\n\t\t} else if !byteEquals(data.gtid, p.lastGtid) {\n\t\t\tp.lastGtid = data.gtid\n\t\t\tbuf.WriteString(\"\\n# \")\n\t\t\tbuf.Write(data.gtid)\n\t\t\tbuf.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\tbuf.Write(data.sql)\n\tif p.Config().ShowAllTime {\n\t\ttimeinfo := fmt.Sprintf(\"; # %s\",\n\t\t\ttime.Unix(int64(data.e.Header.Timestamp), 0).Format(TimeFormat))\n\t\tbuf.WriteString(timeinfo)\n\t\tif p.Config().ShowThread {\n\t\t\tbuf.WriteString(\" # thread_id=\")\n\t\t\tbuf.WriteString(strconv.Itoa(int(data.threadID)))\n\t\t}\n\t\tbuf.WriteString(\"\\n\")\n\t} else if p.Config().ShowTime {\n\t\tif p.lastTimestamp != data.e.Header.Timestamp {\n\t\t\ttimeinfo := fmt.Sprintf(\"; # %s\",\n\t\t\t\ttime.Unix(int64(data.e.Header.Timestamp), 0).Format(TimeFormat))\n\t\t\tbuf.WriteString(timeinfo)\n\t\t\tp.lastTimestamp = data.e.Header.Timestamp\n\t\t} else {\n\t\t\tbuf.WriteString(\";\")\n\t\t}\n\n\t\tif p.Config().ShowThread {\n\t\t\tbuf.WriteString(\" # thread_id=\")\n\t\t\tbuf.WriteString(strconv.Itoa(int(data.threadID)))\n\t\t}\n\t\tbuf.WriteString(\"\\n\")\n\n\t} else {\n\t\tif p.Config().ShowThread {\n\t\t\tbuf.WriteString(\" # thread_id=\")\n\t\t\tbuf.WriteString(strconv.Itoa(int(data.threadID)))\n\t\t} else {\n\t\t\tbuf.WriteString(\";\")\n\t\t}\n\t\tbuf.WriteString(\"\\n\")\n\t}\n\n\tp.write2(buf.Bytes())\n}\n\nfunc (p *MyBinlogParser) write2(b []byte) {\n\tif len(p.outputFileName) > 0 {\n\t\tp.bufferWriter.Write(b)\n\t} else {\n\t\tfmt.Print(string(b))\n\t}\n}\n\n// NewBinlogParser binlog解析器\nfunc NewBinlogParser(ctx context.Context, cfg *BinlogParserConfig) (*MyBinlogParser, error) {\n\n\tp := new(MyBinlogParser)\n\n\tp.allTables = make(map[uint64]*Table)\n\tp.jumpGtids = make(map[*GtidSetInfo]bool)\n\tp.ch = make(chan *row, cfg.Threads)\n\n\tp.write1 = p\n\n\tcfg.beginTime = time.Now().Unix()\n\tp.cfg = cfg\n\n\tp.OnlyDatabases = make(map[string]bool)\n\t// [table_name] = db_name\n\tp.OnlyTables = make(map[string]string)\n\tp.SqlTypes = make(map[string]bool)\n\tp.ignoreTables = make(map[string]interface{})\n\n\tp.startFile = cfg.StartFile\n\tp.stopFile = cfg.StopFile\n\n\tif err := p.parseGtidSets(); err != nil {\n\t\treturn nil, err\n\t}\n\n\tif len(cfg.SocketUser) > 0 {\n\t\tp.outputFileName = fmt.Sprintf(\"files/%s.sql\", p.cfg.ID())\n\n\t\t// p.outputFileName = fmt.Sprintf(\"files/%s_%s.sql\", time.Now().Format(\"20060102_1504\"), p.cfg.Id())\n\n\t\t// var fileName []string\n\t\t// fileName = append(fileName, strings.Replace(cfg.Host, \".\", \"_\", -1))\n\t\t// fileName = append(fileName, strconv.Itoa(int(cfg.Port)))\n\t\t// if cfg.Databases != \"\" {\n\t\t// \tfileName = append(fileName, cfg.Databases)\n\t\t// }\n\t\t// if cfg.Tables != \"\" {\n\t\t// \tfileName = append(fileName, cfg.Tables)\n\t\t// }\n\n\t\t// fileName = append(fileName, time.Now().Format(\"20060102_1504\"))\n\n\t\t// if cfg.Flashback {\n\t\t// \tfileName = append(fileName, \"rollback\")\n\t\t// }\n\n\t\t// p.outputFileName = fmt.Sprintf(\"files/%s.sql\", strings.Join(fileName, \"_\"))\n\t} else {\n\t\tif cfg.OutputFileStr != \"\" {\n\t\t\tp.outputFileName = cfg.OutputFileStr\n\t\t} else {\n\t\t\tp.outputFile = os.Stdout\n\t\t}\n\t}\n\n\t// 本地解析模式,host为空,表名为SQL文件\n\tif p.cfg.Host == \"\" {\n\t\tif p.cfg.Tables == \"\" {\n\t\t\treturn nil, fmt.Errorf(\"本地解析模式请指定表结构文件--tables\")\n\t\t}\n\t\t_, err := os.Stat(p.cfg.Tables)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"读取表结构文件失败(%s): %v\", p.cfg.Tables, err)\n\t\t}\n\n\t\tif err := p.readTableSchema(p.cfg.Tables); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"读取表结构文件失败(%s): %v\", p.cfg.Tables, err)\n\t\t}\n\n\t\tif len(p.tableCacheList) == 0 {\n\t\t\treturn nil, fmt.Errorf(\"未找到建表语句! 请提供需要解析的表对应的建表语句,并以分号分隔.\")\n\t\t} else {\n\t\t\tlog.Infof(\"共读取到表结构 %d 个\", len(p.tableCacheList))\n\t\t}\n\n\t\tp.localMode = true\n\t}\n\n\tif !p.localMode {\n\t\tvar err error\n\t\tp.db, err = p.getDB()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tdefer p.db.Close()\n\n\t\tp.masterStatus, err = p.mysqlMasterStatus()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tif p.cfg.Debug {\n\t\t\tp.db.LogMode(true)\n\t\t}\n\t}\n\n\tif err := p.parserInit(); err != nil {\n\t\treturn nil, err\n\t}\n\n\tp.ctx, p.cancelFn = context.WithCancel(ctx)\n\n\treturn p, nil\n}\n\nfunc (p *MyBinlogParser) ProcessChan(wg *sync.WaitGroup) {\n\tfor {\n\t\tr := <-p.ch\n\t\tif r == nil {\n\t\t\tif len(p.outputFileName) > 0 {\n\t\t\t\tp.bufferWriter.Flush()\n\t\t\t}\n\t\t\twg.Done()\n\t\t\tbreak\n\t\t}\n\t\t// p.myWrite(r.sql, r.e, r.gtid)\n\t\tp.myWrite(r)\n\t}\n}\n\n// parseGtidSets 解析gtid集合\nfunc (p *MyBinlogParser) parseGtidSets() error {\n\tif len(p.cfg.IncludeGtids) == 0 {\n\t\treturn nil\n\t}\n\n\tsets := strings.Split(p.cfg.IncludeGtids, \",\")\n\n\tfor _, s := range sets {\n\t\tg := &GtidSetInfo{}\n\t\tstr := strings.Split(s, \":\")\n\t\tif len(str) != 2 {\n\t\t\treturn errors.New(\"错误GTID格式!正确格式为uuid:编号[-编号],多个时以逗号分隔\")\n\t\t}\n\n\t\t// 把uuid逆向为16位[]byte\n\t\tu2, err := uuid.FromString(str[0])\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"GTID解析失败!(%s)\", err.Error())\n\t\t}\n\n\t\tg.uuid = u2.Bytes()\n\n\t\tnos := strings.Split(str[1], \"-\")\n\t\tif len(nos) == 1 {\n\t\t\tn, err := strconv.ParseInt(nos[0], 10, 64)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"GTID解析失败!(%s)\", err.Error())\n\t\t\t}\n\t\t\tg.startSeqNo = n\n\t\t\tg.stopSeqNo = n\n\t\t} else {\n\t\t\tn, err := strconv.ParseInt(nos[0], 10, 64)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"GTID解析失败!(%s)\", err.Error())\n\t\t\t}\n\t\t\tn2, err := strconv.ParseInt(nos[1], 10, 64)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"GTID解析失败!(%s)\", err.Error())\n\t\t\t}\n\t\t\tg.startSeqNo = n\n\t\t\tg.stopSeqNo = n2\n\t\t}\n\n\t\tp.jumpGtids[g] = false\n\t\tp.includeGtids = append(p.includeGtids, g)\n\t\t// fmt.Println(*g)\n\t}\n\n\tlog.Debugf(\"gtid集合数量: %d\", len(p.includeGtids))\n\n\treturn nil\n}\n\nfunc (p *baseParser) getDB() (*gorm.DB, error) {\n\tif p.cfg.Host == \"\" {\n\t\treturn nil, fmt.Errorf(\"请指定数据库地址\")\n\t}\n\tif p.cfg.Port == 0 {\n\t\treturn nil, fmt.Errorf(\"请指定数据库端口\")\n\t}\n\tif p.cfg.User == \"\" {\n\t\treturn nil, fmt.Errorf(\"请指定数据库用户名\")\n\t}\n\taddr := fmt.Sprintf(\"%s:%s@tcp(%s:%d)/mysql?charset=utf8mb4&parseTime=True&loc=Local\",\n\t\tp.cfg.User, p.cfg.Password, p.cfg.Host, p.cfg.Port)\n\n\treturn gorm.Open(\"mysql\", addr)\n}\n\n// parserInit 解析服务初始化\nfunc (p *MyBinlogParser) parserInit() error {\n\n\tdefer timeTrack(time.Now(), \"parserInit\")\n\n\tif len(p.outputFileName) > 0 {\n\t\tvar err error\n\t\tif Exists(p.outputFileName) {\n\t\t\t// p.checkError(errors.New(\"指定的文件已存在!\"))\n\t\t\t// 不作追加,改为文件存在时清除内容,O_APPEND\n\t\t\tp.outputFile, err = os.OpenFile(p.outputFileName,\n\t\t\t\tos.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0666)\n\t\t} else {\n\t\t\tp.outputFile, err = os.Create(p.outputFileName)\n\t\t}\n\t\tp.checkError(err)\n\n\t\t// outputFile, err := os.OpenFile(outputFileStr, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0666)\n\t\t// if err != nil {\n\t\t//  log.Info(index, \"open file failed.\", err.Error())\n\t\t//  break\n\t\t// }\n\t\tdefer p.outputFile.Close()\n\n\t\tp.bufferWriter = bufio.NewWriter(p.outputFile)\n\t}\n\n\tif p.cfg.StartTime != \"\" {\n\t\tt, err := now.Parse(p.cfg.StartTime)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tp.startTimestamp = uint32(t.Unix())\n\t}\n\tif p.cfg.StopTime != \"\" {\n\t\tt, err := now.Parse(p.cfg.StopTime)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tp.stopTimestamp = uint32(t.Unix())\n\t}\n\n\t// 如果未指定开始文件,就自动解析\n\tif len(p.startFile) == 0 {\n\n\t\tbinlogs := p.autoParseBinlogPosition()\n\t\tif len(binlogs) == 0 {\n\t\t\tp.checkError(errors.New(\"无法获取master binlog\"))\n\t\t}\n\n\t\tp.startFile = binlogs[0].Name\n\n\t\tif p.startTimestamp > 0 || p.stopTimestamp > 0 {\n\t\t\tfor _, masterLog := range binlogs {\n\t\t\t\ttimestamp, err := p.getBinlogFirstTimestamp(masterLog.Name)\n\t\t\t\tp.checkError(err)\n\n\t\t\t\tlog.WithFields(log.Fields{\n\t\t\t\t\t\"起始时间\":       time.Unix(int64(timestamp), 0).Format(TimeFormat),\n\t\t\t\t\t\"binlogFile\": masterLog.Name,\n\t\t\t\t}).Info(\"binlog信息\")\n\n\t\t\t\tif timestamp <= p.startTimestamp {\n\t\t\t\t\tp.startFile = masterLog.Name\n\t\t\t\t}\n\n\t\t\t\tif p.stopFile == \"\" && p.stopTimestamp > 0 && timestamp > p.stopTimestamp {\n\t\t\t\t\tp.stopFile = masterLog.Name\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif len(p.startFile) == 0 {\n\t\t\tp.checkError(errors.New(\"未能解析指定时间段的binlog文件,请检查后重试!\"))\n\t\t}\n\n\t\tlog.Infof(\"根据指定的时间段,解析出的开始binlog文件是:%s,结束文件是:%s\\n\",\n\t\t\tp.startFile, p.stopFile)\n\t}\n\n\t// // 未指定结束文件时,仅解析该文件\n\t// if len(p.stopFile) == 0 {\n\t// \tp.stopFile = p.startFile\n\t// }\n\n\tif len(p.cfg.SqlType) > 0 {\n\t\tfor _, s := range strings.Split(p.cfg.SqlType, \",\") {\n\t\t\tp.SqlTypes[s] = true\n\t\t}\n\t} else {\n\t\tp.SqlTypes[\"insert\"] = true\n\t\tp.SqlTypes[\"update\"] = true\n\t\tp.SqlTypes[\"delete\"] = true\n\t}\n\n\tif len(p.cfg.Databases) > 0 {\n\t\tfor _, db := range strings.Split(p.cfg.Databases, \",\") {\n\t\t\tdb = strings.ToLower(strings.Trim(db, \" `\"))\n\t\t\tp.OnlyDatabases[db] = true\n\t\t}\n\t}\n\n\tif len(p.cfg.Tables) > 0 {\n\t\tfor _, s := range strings.Split(p.cfg.Tables, \",\") {\n\t\t\tif strings.Contains(s, \".\") {\n\t\t\t\tnames := strings.SplitN(s, \".\", 2)\n\t\t\t\tdb, table := names[0], names[1]\n\t\t\t\tdb = strings.ToLower(strings.Trim(db, \" `\"))\n\t\t\t\ttable = strings.ToLower(strings.Trim(table, \" `\"))\n\t\t\t\tp.OnlyTables[table] = db\n\t\t\t} else {\n\t\t\t\tkey := strings.ToLower(strings.Trim(s, \" `\"))\n\t\t\t\tp.OnlyTables[key] = \"\"\n\t\t\t}\n\n\t\t}\n\t}\n\n\t// log.Infof(\"dbs: %#v\", p.OnlyDatabases)\n\t// log.Infof(\"tbls: %#v\", p.OnlyTables)\n\n\treturn nil\n}\n\nfunc Exists(filename string) bool {\n\t_, err := os.Stat(filename)\n\treturn err == nil || os.IsExist(err)\n}\n\n// getBinlogFirstTimestamp 获取binlog文件第一个时间戳\nfunc (p *MyBinlogParser) getBinlogFirstTimestamp(file string) (uint32, error) {\n\n\tlogLevel := log.GetLevel()\n\tdefer func() {\n\t\tlog.SetLevel(logLevel)\n\t}()\n\t// 临时调整日志级别,忽略close sync异常\n\tlog.SetLevel(log.FatalLevel)\n\n\tcfg := replication.BinlogSyncerConfig{\n\t\tServerID: 2000000110,\n\t\tFlavor:   p.cfg.Flavor,\n\n\t\tHost:     p.cfg.Host,\n\t\tPort:     p.cfg.Port,\n\t\tUser:     p.cfg.User,\n\t\tPassword: p.cfg.Password,\n\t\t// RawModeEnabled:  p.cfg.RawMode,\n\t\t// SemiSyncEnabled: p.cfg.SemiSync,\n\t}\n\n\tb := replication.NewBinlogSyncer(cfg)\n\n\tpos := mysql.Position{Name: file,\n\t\tPos: uint32(4)}\n\n\ts, err := b.StartSync(pos)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\tdefer func() {\n\t\tb.Close()\n\t}()\n\tfor {\n\t\t// return\n\t\te, err := s.GetEvent(context.Background())\n\t\tif err != nil {\n\t\t\t// b.Close()\n\t\t\treturn 0, err\n\t\t}\n\n\t\t// log.Infof(\"事件类型:%s\", e.Header.EventType)\n\t\tif e.Header.Timestamp > 0 {\n\t\t\t// b.Close()\n\t\t\treturn e.Header.Timestamp, nil\n\t\t}\n\t}\n}\n\n// check 读取文件的函数调用大多数都需要检查错误，\n// 使用下面这个错误检查方法可以方便一点\nfunc check(e error) {\n\tif e != nil {\n\t\tpanic(e)\n\t}\n}\n\nfunc timeParseToUnix(timeStr string) (uint32, error) {\n\t// t, err := time.Parse(TimeFormat, *timeStr)\n\tt, err := time.ParseInLocation(TimeFormat, timeStr, TimeLocation)\n\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\tstamp := t.Unix()\n\treturn uint32(stamp), nil\n}\n\nfunc (p *MyBinlogParser) checkError(e error) {\n\tif e != nil {\n\t\tlog.Error(e)\n\n\t\tif len(p.cfg.SocketUser) > 0 {\n\t\t\tkwargs := map[string]interface{}{\"error\": e.Error()}\n\t\t\tsendMsg(p.cfg.SocketUser, \"binlog_parse_progress\", \"binlog解析进度\",\n\t\t\t\t\"\", kwargs)\n\t\t}\n\t\tpanic(e)\n\t}\n}\n\nfunc (p *MyBinlogParser) schemaFilter(table *replication.TableMapEvent) bool {\n\tif len(p.OnlyDatabases) == 0 && len(p.OnlyTables) == 0 {\n\t\treturn true\n\t}\n\tdbName := strings.ToLower(string(table.Schema))\n\tif len(p.OnlyDatabases) > 0 {\n\t\tif _, ok := p.OnlyDatabases[dbName]; !ok {\n\t\t\treturn false\n\t\t}\n\t}\n\ttableName := strings.ToLower(string(table.Table))\n\tif len(p.OnlyTables) > 0 {\n\n\t\tif db, ok := p.OnlyTables[tableName]; !ok {\n\t\t\treturn false\n\t\t} else if db != \"\" && db != dbName {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\n// generateInsertSQL 生成insert语句\nfunc (p *MyBinlogParser) generateInsertSQL(t *Table, e *replication.RowsEvent,\n\tbinEvent *replication.BinlogEvent) error {\n\ttableName := getTableName(e)\n\t// if len(t.Columns) < int(e.ColumnCount) {\n\t// \treturn fmt.Errorf(\"表%s缺少列!当前列数:%d,binlog的列数%d\",\n\t// \t\ttableName, len(t.Columns), e.ColumnCount)\n\t// }\n\n\tvar columnNames []string\n\tc := \"`%s`\"\n\ttemplate := \"INSERT INTO %s(%s) VALUES(%s)\"\n\tif p.cfg.MinimalInsert && len(e.Rows) > 1 {\n\t\ttemplate = \"INSERT INTO %s(%s) VALUES%s\"\n\t}\n\n\tfor i, col := range t.Columns {\n\t\tif i < int(e.ColumnCount) && !col.IsGenerated() {\n\t\t\t//  有主键且设置去除主键时 作特殊处理\n\t\t\tif t.hasPrimary && p.cfg.RemovePrimary {\n\t\t\t\tif _, ok := t.primarys[i]; !ok {\n\t\t\t\t\tcolumnNames = append(columnNames, fmt.Sprintf(c, col.ColumnName))\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tcolumnNames = append(columnNames, fmt.Sprintf(c, col.ColumnName))\n\t\t\t}\n\t\t}\n\t}\n\n\tparamValues := strings.Repeat(\"?,\", len(columnNames))\n\tparamValues = strings.TrimRight(paramValues, \",\")\n\n\tif p.cfg.MinimalInsert && len(e.Rows) > 1 {\n\t\tparamValues = strings.Repeat(\"(\"+paramValues+\"),\", len(e.Rows))\n\t\tparamValues = strings.TrimRight(paramValues, \",\")\n\t}\n\n\tsql := fmt.Sprintf(template, tableName, strings.Join(columnNames, \",\"), paramValues)\n\n\tvar vv []driver.Value\n\tfor _, rows := range e.Rows {\n\n\t\tfor i, d := range rows {\n\t\t\tif t.Columns[i].IsGenerated() {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif t.hasPrimary && p.cfg.RemovePrimary {\n\t\t\t\tif _, ok := t.primarys[i]; ok {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif t.Columns[i].IsUnsigned() {\n\t\t\t\td = processValue(d, GetDataTypeBase(t.Columns[i].ColumnType))\n\t\t\t}\n\t\t\tvv = append(vv, d)\n\t\t}\n\n\t\tif !p.cfg.MinimalInsert || len(e.Rows) == 1 {\n\t\t\tr, err := InterpolateParams(sql, vv)\n\t\t\tif err != nil {\n\t\t\t\tlog.Error(err)\n\t\t\t}\n\t\t\tp.write(r, binEvent)\n\t\t\tvv = nil\n\t\t}\n\t}\n\n\tif p.cfg.MinimalInsert && len(e.Rows) > 1 {\n\t\tr, err := InterpolateParams(sql, vv)\n\t\tif err != nil {\n\t\t\tlog.Error(err)\n\t\t}\n\t\tp.write(r, binEvent)\n\t}\n\n\treturn nil\n}\n\nfunc (p *MyBinlogParser) checkCanParse(t *Table, e *replication.RowsEvent) bool {\n\tif len(t.Columns) < int(e.ColumnCount) {\n\t\ttableName := getTableName(e)\n\t\tif _, ok := p.ignoreTables[tableName]; !ok {\n\t\t\tp.ignoreTables[tableName] = nil\n\t\t\tlog.Warn(fmt.Sprintf(\"表%s缺少列,已忽略.(当前列数:%d,binlog的列数%d)\",\n\t\t\t\ttableName, len(t.Columns), e.ColumnCount))\n\t\t}\n\t\treturn false\n\t}\n\treturn true\n}\n\n// generateDeleteSQL 生成delete语句\nfunc (p *MyBinlogParser) generateDeleteSQL(t *Table, e *replication.RowsEvent,\n\tbinEvent *replication.BinlogEvent) error {\n\ttableName := getTableName(e)\n\t// if len(t.Columns) < int(e.ColumnCount) {\n\t// \treturn fmt.Errorf(\"表%s缺少列!当前列数:%d,binlog的列数%d\",\n\t// \t\ttableName, len(t.Columns), e.ColumnCount)\n\t// }\n\n\ttemplate := \"DELETE FROM %s WHERE\"\n\n\tsql := fmt.Sprintf(template, tableName)\n\n\tc_null := \" `%s` IS ?\"\n\tc := \" `%s`=?\"\n\tvar columnNames []string\n\tfor _, rows := range e.Rows {\n\n\t\tcolumnNames = nil\n\n\t\tvar vv []driver.Value\n\t\tfor i, d := range rows {\n\t\t\tif t.hasPrimary {\n\t\t\t\t_, ok := t.primarys[i]\n\t\t\t\tif ok {\n\t\t\t\t\tif t.Columns[i].IsUnsigned() {\n\t\t\t\t\t\td = processValue(d, GetDataTypeBase(t.Columns[i].ColumnType))\n\t\t\t\t\t}\n\t\t\t\t\tvv = append(vv, d)\n\t\t\t\t\tif d == nil {\n\t\t\t\t\t\tcolumnNames = append(columnNames,\n\t\t\t\t\t\t\tfmt.Sprintf(c_null, t.Columns[i].ColumnName))\n\t\t\t\t\t} else {\n\t\t\t\t\t\tcolumnNames = append(columnNames,\n\t\t\t\t\t\t\tfmt.Sprintf(c, t.Columns[i].ColumnName))\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif t.Columns[i].IsUnsigned() {\n\t\t\t\t\td = processValue(d, GetDataTypeBase(t.Columns[i].ColumnType))\n\t\t\t\t}\n\t\t\t\tvv = append(vv, d)\n\n\t\t\t\tif d == nil {\n\t\t\t\t\tcolumnNames = append(columnNames,\n\t\t\t\t\t\tfmt.Sprintf(c_null, t.Columns[i].ColumnName))\n\t\t\t\t} else {\n\t\t\t\t\tcolumnNames = append(columnNames,\n\t\t\t\t\t\tfmt.Sprintf(c, t.Columns[i].ColumnName))\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tnewSql := strings.Join([]string{sql, strings.Join(columnNames, \" AND\")}, \"\")\n\n\t\tr, err := InterpolateParams(newSql, vv)\n\t\tif err != nil {\n\t\t\tlog.Error(err)\n\t\t}\n\n\t\tp.write(r, binEvent)\n\n\t}\n\n\treturn nil\n}\n\n// processValue 处理无符号值(unsigned)\nfunc processValue(value driver.Value, dataType string) driver.Value {\n\tif value == nil {\n\t\treturn value\n\t}\n\n\tswitch v := value.(type) {\n\tcase int8:\n\t\tif v >= 0 {\n\t\t\treturn value\n\t\t}\n\t\treturn int64(1<<8 + int64(v))\n\tcase int16:\n\t\tif v >= 0 {\n\t\t\treturn value\n\t\t}\n\t\treturn int64(1<<16 + int64(v))\n\tcase int32:\n\t\tif v >= 0 {\n\t\t\treturn value\n\t\t}\n\t\tif dataType == \"mediumint\" {\n\t\t\treturn int64(1<<24 + int64(v))\n\t\t}\n\t\treturn int64(1<<32 + int64(v))\n\tcase int64:\n\t\tif v >= 0 {\n\t\t\treturn value\n\t\t}\n\t\treturn math.MaxUint64 - uint64(abs(v)) + 1\n\t// case int:\n\t// case float32:\n\t// case float64:\n\n\tdefault:\n\t\t// log.Error(\"解析错误\")\n\t\t// log.Errorf(\"%T\", v)\n\t\treturn value\n\t}\n}\n\nfunc abs(n int64) int64 {\n\ty := n >> 63\n\treturn (n ^ y) - y\n}\n\n// generateUpdateSQL 生成update语句\nfunc (p *MyBinlogParser) generateUpdateSQL(t *Table, e *replication.RowsEvent,\n\tbinEvent *replication.BinlogEvent) error {\n\ttableName := getTableName(e)\n\t// if len(t.Columns) < int(e.ColumnCount) {\n\t// \treturn fmt.Errorf(\"表%s缺少列!当前列数:%d,binlog的列数%d\",\n\t// \t\ttableName, len(t.Columns), e.ColumnCount)\n\t// }\n\n\ttemplate := \"UPDATE %s SET%s WHERE\"\n\n\tsetValue := \" `%s`=?\"\n\n\tvar columnNames []string\n\tc_null := \" `%s` IS ?\"\n\tc := \" `%s`=?\"\n\n\tvar sql string\n\tvar sets []string\n\n\t// for i, col := range t.Columns {\n\t// \tif i < int(e.ColumnCount) {\n\t// \t\tsets = append(sets, fmt.Sprintf(setValue, col.ColumnName))\n\t// \t}\n\t// }\n\n\t// 最小化回滚语句, 当开启时,update语句中未变更的值不再记录到回滚语句中\n\tminimalMode := p.cfg.MinimalUpdate\n\n\tif !minimalMode {\n\t\tfor i, col := range t.Columns {\n\t\t\tif i < int(e.ColumnCount) && !col.IsGenerated() {\n\t\t\t\tsets = append(sets, fmt.Sprintf(setValue, col.ColumnName))\n\t\t\t}\n\t\t}\n\t\tsql = fmt.Sprintf(template, tableName, strings.Join(sets, \",\"))\n\t}\n\n\t// sql := fmt.Sprintf(template,tableName,\n\t// \tstrings.Join(sets, \",\"))\n\n\tvar (\n\t\toldValues []driver.Value\n\t\tnewValues []driver.Value\n\t\tnewSql    string\n\t)\n\n\t// update时, Rows为2的倍数, 双数index为旧值,单数index为新值\n\tfor i, rows := range e.Rows {\n\n\t\tif i%2 == 0 {\n\t\t\t// 旧值\n\t\t\tcolumnNames = nil\n\n\t\t\tfor j, d := range rows {\n\t\t\t\tif t.Columns[j].IsGenerated() {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tif t.hasPrimary {\n\t\t\t\t\tif _, ok := t.primarys[j]; ok {\n\t\t\t\t\t\toldValues = append(oldValues, d)\n\n\t\t\t\t\t\tif d == nil {\n\t\t\t\t\t\t\tcolumnNames = append(columnNames,\n\t\t\t\t\t\t\t\tfmt.Sprintf(c_null, t.Columns[j].ColumnName))\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tcolumnNames = append(columnNames,\n\t\t\t\t\t\t\t\tfmt.Sprintf(c, t.Columns[j].ColumnName))\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tif t.Columns[j].IsUnsigned() {\n\t\t\t\t\t\td = processValue(d, GetDataTypeBase(t.Columns[j].ColumnType))\n\t\t\t\t\t}\n\t\t\t\t\toldValues = append(oldValues, d)\n\n\t\t\t\t\tif d == nil {\n\t\t\t\t\t\tcolumnNames = append(columnNames,\n\t\t\t\t\t\t\tfmt.Sprintf(c_null, t.Columns[j].ColumnName))\n\t\t\t\t\t} else {\n\t\t\t\t\t\tcolumnNames = append(columnNames,\n\t\t\t\t\t\t\tfmt.Sprintf(c, t.Columns[j].ColumnName))\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\t// 新值\n\t\t\tfor j, d := range rows {\n\t\t\t\tif t.Columns[j].IsGenerated() {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tif minimalMode {\n\t\t\t\t\t// 最小化模式下,列如果相等则省略\n\t\t\t\t\tif !compareValue(d, e.Rows[i-1][j]) {\n\t\t\t\t\t\tif t.Columns[j].IsUnsigned() {\n\t\t\t\t\t\t\td = processValue(d, GetDataTypeBase(t.Columns[j].ColumnType))\n\t\t\t\t\t\t}\n\t\t\t\t\t\tnewValues = append(newValues, d)\n\t\t\t\t\t\tif j < len(t.Columns) {\n\t\t\t\t\t\t\tsets = append(sets, fmt.Sprintf(setValue, t.Columns[j].ColumnName))\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tif t.Columns[j].IsUnsigned() {\n\t\t\t\t\t\td = processValue(d, GetDataTypeBase(t.Columns[j].ColumnType))\n\t\t\t\t\t}\n\t\t\t\t\tnewValues = append(newValues, d)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tnewValues = append(newValues, oldValues...)\n\t\t\tif minimalMode {\n\t\t\t\tsql = fmt.Sprintf(template, tableName,\n\t\t\t\t\tstrings.Join(sets, \",\"))\n\t\t\t\tsets = nil\n\t\t\t}\n\t\t\tnewSql = strings.Join([]string{sql, strings.Join(columnNames, \" AND\")}, \"\")\n\t\t\t// log.Info(newSql, len(newValues))\n\t\t\tr, err := InterpolateParams(newSql, newValues)\n\t\t\tp.checkError(err)\n\n\t\t\tp.write(r, binEvent)\n\n\t\t\toldValues = nil\n\t\t\tnewValues = nil\n\t\t}\n\n\t}\n\n\treturn nil\n}\n\n// generateUpdateRollbackSQL 生成update语句\nfunc (p *MyBinlogParser) generateUpdateRollbackSQL(t *Table, e *replication.RowsEvent,\n\tbinEvent *replication.BinlogEvent) error {\n\ttableName := getTableName(e)\n\t// if len(t.Columns) < int(e.ColumnCount) {\n\t// \treturn fmt.Errorf(\"表%s缺少列!当前列数:%d,binlog的列数%d\",\n\t// \t\ttableName, len(t.Columns), e.ColumnCount)\n\t// }\n\n\ttemplate := \"UPDATE %s SET%s WHERE\"\n\n\tsetValue := \" `%s`=?\"\n\n\tvar columnNames []string\n\tc_null := \" `%s` IS ?\"\n\tc := \" `%s`=?\"\n\n\tvar sql string\n\tvar sets []string\n\n\t// 最小化回滚语句, 当开启时,update语句中未变更的值不再记录到回滚语句中\n\tminimalMode := p.cfg.MinimalUpdate\n\n\tif !minimalMode {\n\t\tfor i, col := range t.Columns {\n\t\t\tif i < int(e.ColumnCount) && !col.IsGenerated() {\n\t\t\t\tsets = append(sets, fmt.Sprintf(setValue, col.ColumnName))\n\t\t\t}\n\t\t}\n\t\tsql = fmt.Sprintf(template, tableName, strings.Join(sets, \",\"))\n\t}\n\n\tvar (\n\t\toldValues []driver.Value\n\t\tnewValues []driver.Value\n\t\tnewSql    string\n\t)\n\t// update时, Rows为2的倍数, 双数index为旧值,单数index为新值\n\tfor i, rows := range e.Rows {\n\n\t\tif i%2 == 0 {\n\t\t\t// 旧值\n\t\t\tfor j, d := range rows {\n\t\t\t\tif t.Columns[j].IsGenerated() {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tif minimalMode {\n\t\t\t\t\t// 最小化模式下,列如果相等则省略\n\t\t\t\t\tif !compareValue(d, e.Rows[i+1][j]) {\n\t\t\t\t\t\tif t.Columns[j].IsUnsigned() {\n\t\t\t\t\t\t\td = processValue(d, GetDataTypeBase(t.Columns[j].ColumnType))\n\t\t\t\t\t\t}\n\t\t\t\t\t\tnewValues = append(newValues, d)\n\t\t\t\t\t\tif j < len(t.Columns) {\n\t\t\t\t\t\t\tsets = append(sets, fmt.Sprintf(setValue, t.Columns[j].ColumnName))\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tif t.Columns[j].IsUnsigned() {\n\t\t\t\t\t\td = processValue(d, GetDataTypeBase(t.Columns[j].ColumnType))\n\t\t\t\t\t}\n\t\t\t\t\tnewValues = append(newValues, d)\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\t// 新值\n\t\t\tif p.cfg.Flashback {\n\t\t\t\tcolumnNames = nil\n\t\t\t}\n\t\t\tfor j, d := range rows {\n\t\t\t\tif t.hasPrimary {\n\t\t\t\t\tif _, ok := t.primarys[j]; ok {\n\t\t\t\t\t\tif t.Columns[j].IsGenerated() {\n\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif t.Columns[j].IsUnsigned() {\n\t\t\t\t\t\t\td = processValue(d, GetDataTypeBase(t.Columns[j].ColumnType))\n\t\t\t\t\t\t}\n\t\t\t\t\t\toldValues = append(oldValues, d)\n\n\t\t\t\t\t\tif d == nil {\n\t\t\t\t\t\t\tcolumnNames = append(columnNames,\n\t\t\t\t\t\t\t\tfmt.Sprintf(c_null, t.Columns[j].ColumnName))\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tcolumnNames = append(columnNames,\n\t\t\t\t\t\t\t\tfmt.Sprintf(c, t.Columns[j].ColumnName))\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tif t.Columns[j].IsGenerated() {\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t\tif t.Columns[j].IsUnsigned() {\n\t\t\t\t\t\td = processValue(d, GetDataTypeBase(t.Columns[j].ColumnType))\n\t\t\t\t\t}\n\t\t\t\t\toldValues = append(oldValues, d)\n\n\t\t\t\t\tif d == nil {\n\t\t\t\t\t\tcolumnNames = append(columnNames,\n\t\t\t\t\t\t\tfmt.Sprintf(c_null, t.Columns[j].ColumnName))\n\t\t\t\t\t} else {\n\t\t\t\t\t\tcolumnNames = append(columnNames,\n\t\t\t\t\t\t\tfmt.Sprintf(c, t.Columns[j].ColumnName))\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tnewValues = append(newValues, oldValues...)\n\t\t\tif minimalMode {\n\t\t\t\tsql = fmt.Sprintf(template, tableName, strings.Join(sets, \",\"))\n\t\t\t\tsets = nil\n\t\t\t}\n\t\t\tnewSql = strings.Join([]string{sql, strings.Join(columnNames, \" AND\")}, \"\")\n\t\t\tr, err := InterpolateParams(newSql, newValues)\n\t\t\tp.checkError(err)\n\n\t\t\tp.write(r, binEvent)\n\n\t\t\toldValues = nil\n\t\t\tnewValues = nil\n\t\t}\n\t}\n\n\treturn nil\n}\nfunc (p *MyBinlogParser) tableInformation(tableId uint64, schema []byte, tableName []byte) (*Table, error) {\n\n\ttable, ok := p.allTables[tableId]\n\tif ok {\n\t\treturn table, nil\n\t}\n\n\tif p.localMode && len(p.tableCacheList) > 0 {\n\t\tkey := strings.ToLower(fmt.Sprintf(\"%s.%s\", string(schema), string(tableName)))\n\t\tif table, ok := p.tableCacheList[key]; ok {\n\t\t\tp.allTables[tableId] = table\n\t\t\treturn table, nil\n\t\t} else {\n\t\t\tkey = strings.ToLower(string(tableName))\n\t\t\tif table, ok := p.tableCacheList[key]; ok {\n\t\t\t\tp.allTables[tableId] = table\n\t\t\t\treturn table, nil\n\t\t\t}\n\t\t}\n\t\treturn nil, nil\n\t}\n\n\tsql := `SELECT COLUMN_NAME, ifnull(COLLATION_NAME,'') as COLLATION_NAME,\n            ifnull(CHARACTER_SET_NAME,'') as CHARACTER_SET_NAME,\n            ifnull(COLUMN_COMMENT,'') as COLUMN_COMMENT, COLUMN_TYPE,\n            ifnull(COLUMN_KEY,'') as COLUMN_KEY,\n\t\t\tifnull(EXTRA,'') as EXTRA\n            FROM information_schema.columns\n            WHERE table_schema = ? and table_name = ?\n            ORDER BY ORDINAL_POSITION`\n\n\tvar columns []Column\n\tp.db.Raw(sql, string(schema), string(tableName)).Scan(&columns)\n\n\tprimarys := make(map[int]bool)\n\tuniques := make(map[int]bool)\n\n\tfor i, r := range columns {\n\t\tif r.ColumnKey == \"PRI\" {\n\t\t\tprimarys[i] = true\n\t\t}\n\t\tif r.ColumnKey == \"UNI\" {\n\t\t\tuniques[i] = true\n\t\t}\n\t}\n\n\tnewTable := new(Table)\n\tnewTable.tableID = tableId\n\tnewTable.Columns = columns\n\n\t// if !p.cfg.AllColumns {\n\tif len(primarys) > 0 {\n\t\tnewTable.primarys = primarys\n\t\tnewTable.hasPrimary = true\n\t} else if len(uniques) > 0 {\n\t\tnewTable.primarys = uniques\n\t\tnewTable.hasPrimary = true\n\t} else {\n\t\tnewTable.hasPrimary = false\n\t}\n\t// }\n\n\tp.allTables[tableId] = newTable\n\n\treturn newTable, nil\n}\n\nfunc (p *MyBinlogParser) mysqlMasterStatus() (*MasterStatus, error) {\n\n\tdefer timeTrack(time.Now(), \"mysqlMasterStatus\")\n\n\t// rows, err := db.Query(\"SHOW MASTER STATUS\")\n\t// p.checkError(err)\n\n\t// defer rows.Close()\n\n\tr := MasterStatus{}\n\n\tp.db.Raw(\"SHOW MASTER STATUS\").Scan(&r)\n\n\t// columns, _ := rows.Columns()\n\n\t// r := MasterStatus{}\n\t// for rows.Next() {\n\t//  if len(columns) == 4 {\n\t//      err = rows.Scan(&(r.File), &(r.Position), &(r.Binlog_Do_DB),\n\t//          &(r.Binlog_Ignore_DB))\n\t//  } else if len(columns) == 5 {\n\t//      err = rows.Scan(&(r.File), &(r.Position), &(r.Binlog_Do_DB),\n\t//          &(r.Binlog_Ignore_DB), &(r.Executed_Gtid_Set))\n\t//  } else {\n\t//      return nil, errors.New(\"show master status返回的列数无法解析!!!\")\n\t//  }\n\t//  p.checkError(err)\n\t// }\n\n\treturn &r, nil\n}\n\nfunc (p *MyBinlogParser) autoParseBinlogPosition() []MasterLog {\n\n\tif p.binlogs != nil {\n\t\treturn p.binlogs\n\t}\n\tvar binlogIndex []MasterLog\n\tp.db.Raw(\"SHOW MASTER LOGS\").Scan(&binlogIndex)\n\n\tp.binlogs = binlogIndex\n\treturn binlogIndex\n}\n\n// InterpolateParams 填充占位符参数\nfunc InterpolateParams(query string, args []driver.Value) ([]byte, error) {\n\t// Number of ? should be same to len(args)\n\tif strings.Count(query, \"?\") != len(args) {\n\t\tlog.WithFields(log.Fields{\n\t\t\t\"需要参数\": strings.Count(query, \"?\"),\n\t\t\t\"提供参数\": len(args),\n\t\t}).Error(\"sql的参数个数不匹配\")\n\n\t\treturn nil, errors.New(\"driver: skip fast-path; continue as if unimplemented\")\n\t}\n\n\tvar buf []byte\n\n\targPos := 0\n\n\tfor i := 0; i < len(query); i++ {\n\t\tq := strings.IndexByte(query[i:], '?')\n\t\tif q == -1 {\n\t\t\tbuf = append(buf, query[i:]...)\n\t\t\tbreak\n\t\t}\n\t\tbuf = append(buf, query[i:i+q]...)\n\t\ti += q\n\n\t\targ := args[argPos]\n\t\targPos++\n\n\t\tif arg == nil {\n\t\t\tbuf = append(buf, \"NULL\"...)\n\t\t\tcontinue\n\t\t}\n\n\t\tswitch v := arg.(type) {\n\t\tcase int8:\n\t\t\tbuf = strconv.AppendInt(buf, int64(v), 10)\n\t\tcase int16:\n\t\t\tbuf = strconv.AppendInt(buf, int64(v), 10)\n\t\tcase int32:\n\t\t\tbuf = strconv.AppendInt(buf, int64(v), 10)\n\t\tcase int64:\n\t\t\tbuf = strconv.AppendInt(buf, v, 10)\n\t\tcase uint64:\n\t\t\tbuf = strconv.AppendUint(buf, uint64(v), 10)\n\t\tcase int:\n\t\t\tbuf = strconv.AppendInt(buf, int64(v), 10)\n\t\tcase decimal.Decimal:\n\t\t\tbuf = append(buf, v.String()...)\n\t\tcase float32:\n\t\t\tbuf = strconv.AppendFloat(buf, float64(v), 'g', -1, 32)\n\t\tcase float64:\n\t\t\tbuf = strconv.AppendFloat(buf, v, 'g', -1, 64)\n\t\tcase bool:\n\t\t\tif v {\n\t\t\t\tbuf = append(buf, '1')\n\t\t\t} else {\n\t\t\t\tbuf = append(buf, '0')\n\t\t\t}\n\t\tcase time.Time:\n\t\t\tif v.IsZero() {\n\t\t\t\tbuf = append(buf, \"'0000-00-00'\"...)\n\t\t\t} else {\n\t\t\t\tv := v.In(time.UTC)\n\t\t\t\tv = v.Add(time.Nanosecond * 500) // To round under microsecond\n\t\t\t\tyear := v.Year()\n\t\t\t\tyear100 := year / 100\n\t\t\t\tyear1 := year % 100\n\t\t\t\tmonth := v.Month()\n\t\t\t\tday := v.Day()\n\t\t\t\thour := v.Hour()\n\t\t\t\tminute := v.Minute()\n\t\t\t\tsecond := v.Second()\n\t\t\t\tmicro := v.Nanosecond() / 1000\n\n\t\t\t\tbuf = append(buf, []byte{\n\t\t\t\t\t'\\'',\n\t\t\t\t\tdigits10[year100], digits01[year100],\n\t\t\t\t\tdigits10[year1], digits01[year1],\n\t\t\t\t\t'-',\n\t\t\t\t\tdigits10[month], digits01[month],\n\t\t\t\t\t'-',\n\t\t\t\t\tdigits10[day], digits01[day],\n\t\t\t\t\t' ',\n\t\t\t\t\tdigits10[hour], digits01[hour],\n\t\t\t\t\t':',\n\t\t\t\t\tdigits10[minute], digits01[minute],\n\t\t\t\t\t':',\n\t\t\t\t\tdigits10[second], digits01[second],\n\t\t\t\t}...)\n\n\t\t\t\tif micro != 0 {\n\t\t\t\t\tmicro10000 := micro / 10000\n\t\t\t\t\tmicro100 := micro / 100 % 100\n\t\t\t\t\tmicro1 := micro % 100\n\t\t\t\t\tbuf = append(buf, []byte{\n\t\t\t\t\t\t'.',\n\t\t\t\t\t\tdigits10[micro10000], digits01[micro10000],\n\t\t\t\t\t\tdigits10[micro100], digits01[micro100],\n\t\t\t\t\t\tdigits10[micro1], digits01[micro1],\n\t\t\t\t\t}...)\n\t\t\t\t}\n\t\t\t\tbuf = append(buf, '\\'')\n\t\t\t}\n\t\tcase string:\n\t\t\tbuf = append(buf, '\\'')\n\t\t\tbuf = escapeBytesBackslash(buf, []byte(v))\n\t\t\tbuf = append(buf, '\\'')\n\t\tcase []byte:\n\t\t\tif v == nil {\n\t\t\t\tbuf = append(buf, \"NULL\"...)\n\t\t\t} else {\n\t\t\t\t// buf = append(buf, \"_binary'\"...)\n\t\t\t\tbuf = append(buf, '\\'')\n\n\t\t\t\tbuf = escapeBytesBackslash(buf, v)\n\n\t\t\t\tbuf = append(buf, '\\'')\n\t\t\t}\n\t\tdefault:\n\t\t\t// fmt.Println(v)\n\t\t\tlog.Printf(\"%T\", v)\n\t\t\tlog.Info(\"解析错误\")\n\t\t\treturn nil, errors.New(\"driver: skip fast-path; continue as if unimplemented\")\n\t\t}\n\n\t\t// 4 << 20 , 4MB\n\t\t// 移除对单行大小的判断,不入库不需要该限制\n\t\t// if len(buf)+4 > 4<<20 {\n\t\t// \t// log.Print(\"%T\", v)\n\t\t// \tlog.Info(\"解析错误\")\n\t\t// \treturn nil, errors.New(\"driver: skip fast-path; continue as if unimplemented\")\n\t\t// }\n\t}\n\tif argPos != len(args) {\n\t\t// log.Print(\"%T\", v)\n\t\tlog.Info(\"解析错误\")\n\t\treturn nil, errors.New(\"driver: skip fast-path; continue as if unimplemented\")\n\t}\n\treturn buf, nil\n}\n\n// reserveBuffer checks cap(buf) and expand buffer to len(buf) + appendSize.\n// If cap(buf) is not enough, reallocate new buffer.\nfunc reserveBuffer(buf []byte, appendSize int) []byte {\n\tnewSize := len(buf) + appendSize\n\tif cap(buf) < newSize {\n\t\t// Grow buffer exponentially\n\t\tnewBuf := make([]byte, len(buf)*2+appendSize)\n\t\tcopy(newBuf, buf)\n\t\tbuf = newBuf\n\t}\n\treturn buf[:newSize]\n}\n\n// escapeBytesBackslash escapes []byte with backslashes (\\)\n// This escapes the contents of a string (provided as []byte) by adding backslashes before special\n// characters, and turning others into specific escape sequences, such as\n// turning newlines into \\n and null bytes into \\0.\n// https://github.com/mysql/mysql-server/blob/mysql-5.7.5/mysys/charset.c#L823-L932\nfunc escapeBytesBackslash(buf, v []byte) []byte {\n\tpos := len(buf)\n\tbuf = reserveBuffer(buf, len(v)*2)\n\n\tfor _, c := range v {\n\t\tswitch c {\n\t\tcase '\\x00':\n\t\t\tbuf[pos] = '\\\\'\n\t\t\tbuf[pos+1] = '0'\n\t\t\tpos += 2\n\t\tcase '\\n':\n\t\t\tbuf[pos] = '\\\\'\n\t\t\tbuf[pos+1] = 'n'\n\t\t\tpos += 2\n\t\tcase '\\r':\n\t\t\tbuf[pos] = '\\\\'\n\t\t\tbuf[pos+1] = 'r'\n\t\t\tpos += 2\n\t\tcase '\\x1a':\n\t\t\tbuf[pos] = '\\\\'\n\t\t\tbuf[pos+1] = 'Z'\n\t\t\tpos += 2\n\t\tcase '\\'':\n\t\t\tbuf[pos] = '\\\\'\n\t\t\tbuf[pos+1] = '\\''\n\t\t\tpos += 2\n\t\tcase '\"':\n\t\t\tbuf[pos] = '\\\\'\n\t\t\tbuf[pos+1] = '\"'\n\t\t\tpos += 2\n\t\tcase '\\\\':\n\t\t\tbuf[pos] = '\\\\'\n\t\t\tbuf[pos+1] = '\\\\'\n\t\t\tpos += 2\n\t\tdefault:\n\t\t\tbuf[pos] = c\n\t\t\tpos++\n\t\t}\n\t}\n\n\treturn buf[:pos]\n}\n\n// escapeStringBackslash is similar to escapeBytesBackslash but for string.\nfunc escapeStringBackslash(buf []byte, v string) []byte {\n\tpos := len(buf)\n\tbuf = reserveBuffer(buf, len(v)*2)\n\n\tfor i := 0; i < len(v); i++ {\n\t\tc := v[i]\n\t\tswitch c {\n\t\tcase '\\x00':\n\t\t\tbuf[pos] = '\\\\'\n\t\t\tbuf[pos+1] = '0'\n\t\t\tpos += 2\n\t\tcase '\\n':\n\t\t\tbuf[pos] = '\\\\'\n\t\t\tbuf[pos+1] = 'n'\n\t\t\tpos += 2\n\t\tcase '\\r':\n\t\t\tbuf[pos] = '\\\\'\n\t\t\tbuf[pos+1] = 'r'\n\t\t\tpos += 2\n\t\tcase '\\x1a':\n\t\t\tbuf[pos] = '\\\\'\n\t\t\tbuf[pos+1] = 'Z'\n\t\t\tpos += 2\n\t\tcase '\\'':\n\t\t\tbuf[pos] = '\\\\'\n\t\t\tbuf[pos+1] = '\\''\n\t\t\tpos += 2\n\t\tcase '\"':\n\t\t\tbuf[pos] = '\\\\'\n\t\t\tbuf[pos+1] = '\"'\n\t\t\tpos += 2\n\t\tcase '\\\\':\n\t\t\tbuf[pos] = '\\\\'\n\t\t\tbuf[pos+1] = '\\\\'\n\t\t\tpos += 2\n\t\tdefault:\n\t\t\tbuf[pos] = c\n\t\t\tpos++\n\t\t}\n\t}\n\n\treturn buf[:pos]\n}\n\n// escapeBytesQuotes escapes apostrophes in []byte by doubling them up.\n// This escapes the contents of a string by doubling up any apostrophes that\n// it contains. This is used when the NO_BACKSLASH_ESCAPES SQL_MODE is in\n// effect on the server.\n// https://github.com/mysql/mysql-server/blob/mysql-5.7.5/mysys/charset.c#L963-L1038\nfunc escapeBytesQuotes(buf, v []byte) []byte {\n\tpos := len(buf)\n\tbuf = reserveBuffer(buf, len(v)*2)\n\n\tfor _, c := range v {\n\t\tif c == '\\'' {\n\t\t\tbuf[pos] = '\\''\n\t\t\tbuf[pos+1] = '\\''\n\t\t\tpos += 2\n\t\t} else {\n\t\t\tbuf[pos] = c\n\t\t\tpos++\n\t\t}\n\t}\n\n\treturn buf[:pos]\n}\n\n// escapeStringQuotes is similar to escapeBytesQuotes but for string.\nfunc escapeStringQuotes(buf []byte, v string) []byte {\n\tpos := len(buf)\n\tbuf = reserveBuffer(buf, len(v)*2)\n\n\tfor i := 0; i < len(v); i++ {\n\t\tc := v[i]\n\t\tif c == '\\'' {\n\t\t\tbuf[pos] = '\\''\n\t\t\tbuf[pos+1] = '\\''\n\t\t\tpos += 2\n\t\t} else {\n\t\t\tbuf[pos] = c\n\t\t\tpos++\n\t\t}\n\t}\n\n\treturn buf[:pos]\n}\n\n// GetDataTypeBase 获取dataType中的数据类型，忽略长度\nfunc GetDataTypeBase(dataType string) string {\n\tif i := strings.Index(dataType, \"(\"); i > 0 {\n\t\treturn dataType[0:i]\n\t}\n\n\treturn dataType\n}\n\n// readTableSchema 读取表结构\nfunc (p *MyBinlogParser) readTableSchema(path string) error {\n\tfileObj, err := os.Open(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer fileObj.Close()\n\n\treader := bufio.NewReader(fileObj)\n\tp.tableCacheList = make(map[string]*Table)\n\n\tvar buf []string\n\tquotaIsDouble := true\n\tparser := tidbParser.New()\n\tfor {\n\t\tline, err := reader.ReadString('\\n')\n\t\tif err != nil {\n\t\t\tif err != io.EOF {\n\t\t\t\tlog.Println(err)\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\tif strings.Count(line, \"'\")%2 == 1 {\n\t\t\tquotaIsDouble = !quotaIsDouble\n\t\t}\n\n\t\tif ((strings.HasSuffix(line, \";\") || strings.HasSuffix(line, \";\\r\")) &&\n\t\t\tquotaIsDouble) || err == io.EOF {\n\t\t\tbuf = append(buf, line)\n\t\t\ts1 := strings.Join(buf, \"\\n\")\n\t\t\ts1 = strings.TrimRight(s1, \";\")\n\t\t\tbuf = nil\n\t\t\tstmtNodes, _, err := parser.Parse(s1, \"utf8mb4\", \"utf8mb4_bin\")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"解析失败: %v\", err)\n\t\t\t}\n\n\t\t\tfor _, stmtNode := range stmtNodes {\n\t\t\t\t//  是ASCII码160的特殊空格\n\t\t\t\t// currentSql := strings.Trim(stmtNode.Text(), \" ;\\t\\n\\v\\f\\r \")\n\t\t\t\tswitch node := stmtNode.(type) {\n\t\t\t\tcase *ast.CreateTableStmt:\n\t\t\t\t\tp.cacheNewTable(buildTableInfo(node))\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tbuf = append(buf, line)\n\t\t}\n\n\t\tif err == io.EOF {\n\t\t\tbreak\n\t\t}\n\t}\n\treturn nil\n}\n\n// cacheNewTable 缓存表结构\nfunc (p *MyBinlogParser) cacheNewTable(t *Table) {\n\n\tvar key string\n\tif t.Schema == \"\" {\n\t\tkey = t.TableName\n\t} else {\n\t\tkey = fmt.Sprintf(\"%s.%s\", t.Schema, t.TableName)\n\t}\n\n\tkey = strings.ToLower(key)\n\n\tp.tableCacheList[key] = t\n\tp.OnlyTables[strings.ToLower(t.TableName)] = t.Schema\n}\n\n// buildTableInfo 构建表结构\nfunc buildTableInfo(node *ast.CreateTableStmt) *Table {\n\ttable := &Table{\n\t\tSchema:    node.Table.Schema.String(),\n\t\tTableName: node.Table.Name.String(),\n\t}\n\n\tfor _, ct := range node.Constraints {\n\t\tswitch ct.Tp {\n\t\t// 设置主键标志\n\t\tcase ast.ConstraintPrimaryKey:\n\t\t\tfor _, col := range ct.Keys {\n\t\t\t\tfor _, field := range node.Cols {\n\t\t\t\t\tif field.Name.Name.L == col.Column.Name.L {\n\t\t\t\t\t\tfield.Tp.Flag |= tidb.PriKeyFlag\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\tcase ast.ConstraintUniq, ast.ConstraintUniqIndex, ast.ConstraintUniqKey:\n\t\t\tfor _, col := range ct.Keys {\n\t\t\t\tfor _, field := range node.Cols {\n\t\t\t\t\tif field.Name.Name.L == col.Column.Name.L {\n\t\t\t\t\t\tfield.Tp.Flag |= tidb.UniqueKeyFlag\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\ttable.Columns = make([]Column, 0, len(node.Cols))\n\tfor _, field := range node.Cols {\n\t\ttable.Columns = append(table.Columns, *buildNewColumnToCache(table, field))\n\t}\n\n\ttable.configPrimaryKey()\n\n\treturn table\n}\n\n// buildNewColumnToCache 构建列\nfunc buildNewColumnToCache(t *Table, field *ast.ColumnDef) *Column {\n\tc := &Column{}\n\n\tc.ColumnName = field.Name.Name.String()\n\tc.ColumnType = field.Tp.InfoSchemaStr()\n\n\tfor _, op := range field.Options {\n\t\tswitch op.Tp {\n\t\tcase ast.ColumnOptionComment:\n\t\t\tc.ColumnComment = op.Expr.GetDatum().GetString()\n\n\t\tcase ast.ColumnOptionPrimaryKey:\n\t\t\tc.ColumnKey = \"PRI\"\n\t\tcase ast.ColumnOptionUniqKey:\n\t\t\tc.ColumnKey = \"UNI\"\n\t\tcase ast.ColumnOptionAutoIncrement:\n\t\t\t// c.Extra += \"auto_increment\"\n\t\tcase ast.ColumnOptionGenerated:\n\t\t\tif op.Stored {\n\t\t\t\tc.Extra += \" STORED GENERATED\"\n\t\t\t} else {\n\t\t\t\tc.Extra += \" VIRTUAL GENERATED\"\n\t\t\t}\n\t\t}\n\t}\n\n\tif c.ColumnKey != \"PRI\" && tidb.HasPriKeyFlag(field.Tp.Flag) {\n\t\tc.ColumnKey = \"PRI\"\n\t} else if tidb.HasUniKeyFlag(field.Tp.Flag) {\n\t\tc.ColumnKey = \"UNI\"\n\t}\n\treturn c\n}\n\n// compareValue 比较两值是否相等\nfunc compareValue(v1 interface{}, v2 interface{}) bool {\n\tequal := false\n\n\t// 处理特殊情况\n\tif v1 == nil && v2 == nil {\n\t\treturn true\n\t}\n\tif v1 == nil || v2 == nil {\n\t\treturn false\n\t}\n\n\tswitch v := v1.(type) {\n\tcase []byte:\n\t\tequal = byteEquals(v, v2.([]byte))\n\tcase decimal.Decimal:\n\t\tif newDec, ok := v2.(decimal.Decimal); ok {\n\t\t\tequal = v.Equal(newDec)\n\t\t} else {\n\t\t\tequal = false\n\t\t}\n\tdefault:\n\t\tequal = v1 == v2\n\t}\n\n\treturn equal\n}\n\n// configPrimaryKey 配置主键设置\nfunc (t *Table) configPrimaryKey() {\n\tvar primarys map[int]bool\n\tprimarys = make(map[int]bool)\n\n\tvar uniques map[int]bool\n\tuniques = make(map[int]bool)\n\tfor i, r := range t.Columns {\n\t\tif r.ColumnKey == \"PRI\" {\n\t\t\tprimarys[i] = true\n\t\t}\n\t\tif r.ColumnKey == \"UNI\" {\n\t\t\tuniques[i] = true\n\t\t}\n\t}\n\n\t// newTable.tableId = tableId\n\tif len(primarys) > 0 {\n\t\tt.primarys = primarys\n\t\tt.hasPrimary = true\n\t} else if len(uniques) > 0 {\n\t\tt.primarys = uniques\n\t\tt.hasPrimary = true\n\t} else {\n\t\tt.hasPrimary = false\n\t}\n}\n\nfunc (p *MyBinlogParser) parseSingleEvent(e *replication.BinlogEvent) (ok bool, err error) {\n\t// 是否继续,默认为true\n\tok = true\n\tfinishFlag := -1\n\n\tif e.Header.LogPos > 0 {\n\t\tp.currentPosition.Pos = e.Header.LogPos\n\t}\n\n\tif e.Header.EventType == replication.ROTATE_EVENT {\n\t\tif event, ok := e.Event.(*replication.RotateEvent); ok {\n\t\t\tp.currentPosition = mysql.Position{\n\t\t\t\tName: string(event.NextLogName),\n\t\t\t\tPos:  uint32(event.Position)}\n\t\t}\n\t}\n\n\tif !p.cfg.StopNever {\n\n\t\tif e.Header.Timestamp > 0 {\n\t\t\tif p.startTimestamp > 0 && e.Header.Timestamp < p.startTimestamp {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif p.stopTimestamp > 0 && e.Header.Timestamp > p.stopTimestamp {\n\t\t\t\tlog.Warn(\"已超出结束时间\")\n\t\t\t\treturn false, nil\n\t\t\t}\n\t\t}\n\n\t\tp.currentTimtstamp = e.Header.Timestamp\n\n\t\tfinishFlag = p.checkFinish(&p.currentPosition)\n\t\tif finishFlag == 1 {\n\t\t\tlog.Warn(\"is finish\")\n\t\t\treturn false, nil\n\t\t}\n\t}\n\n\t// 只需解析GTID返回内的event\n\tif len(p.includeGtids) > 0 {\n\t\tswitch e.Header.EventType {\n\t\tcase replication.TABLE_MAP_EVENT, replication.QUERY_EVENT,\n\t\t\treplication.WRITE_ROWS_EVENTv1, replication.WRITE_ROWS_EVENTv2,\n\t\t\treplication.DELETE_ROWS_EVENTv1, replication.DELETE_ROWS_EVENTv2,\n\t\t\treplication.UPDATE_ROWS_EVENTv1, replication.UPDATE_ROWS_EVENTv2:\n\t\t\tstatus := p.isGtidEventInGtidSet()\n\t\t\tif status == 1 {\n\t\t\t\treturn\n\t\t\t} else if status == 2 {\n\t\t\t\tlog.Info(\"已超出GTID范围,自动结束\")\n\n\t\t\t\tif !p.cfg.StopNever {\n\t\t\t\t\treturn false, nil\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tswitch event := e.Event.(type) {\n\tcase *replication.GTIDEvent:\n\t\tif len(p.includeGtids) > 0 {\n\t\t\tp.gtidEvent = event\n\t\t}\n\t\t// e.Dump(os.Stdout)\n\t\tu, _ := uuid.FromBytes(event.SID)\n\t\tp.gtid = append([]byte(u.String()), []byte(fmt.Sprintf(\":%d\", event.GNO))...)\n\t\t// fmt.Println(p.gtid)\n\tcase *replication.TableMapEvent:\n\t\tif !p.schemaFilter(event) {\n\t\t\treturn\n\t\t}\n\t\t_, err = p.tableInformation(event.TableID, event.Schema, event.Table)\n\t\tif err != nil {\n\t\t\treturn false, err\n\t\t}\n\tcase *replication.QueryEvent:\n\t\tif p.cfg.ThreadID > 0 || p.cfg.ShowThread {\n\t\t\tp.currentThreadID = event.SlaveProxyID\n\t\t\tif p.cfg.ThreadID != p.currentThreadID {\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\n\t\t// 回滚或者仅dml语句时 跳过\n\t\tif p.cfg.Flashback || !p.cfg.ParseDDL {\n\t\t\treturn\n\t\t}\n\n\t\tif string(event.Query) != \"BEGIN\" && string(event.Query) != \"COMMIT\" {\n\t\t\tif len(event.Schema) > 0 {\n\t\t\t\tp.write(append([]byte(fmt.Sprintf(\"USE `%s`;\\n\", event.Schema)), event.Query...), e)\n\t\t\t}\n\t\t\t// changeRows++\n\t\t} else {\n\t\t\t// fmt.Println(string(event.Query))\n\t\t\t// log.Info(\"#start %d %d %d\", e.Header.LogPos,\n\t\t\t//  e.Header.LogPos+e.Header.EventSize,\n\t\t\t//  e.Header.LogPos-e.Header.EventSize)\n\t\t\t// if binlog_event.query == 'BEGIN':\n\t\t\t//                    e_start_pos = last_pos\n\t\t}\n\tcase *replication.RowsEvent:\n\t\tif !p.schemaFilter(event.Table) {\n\t\t\treturn\n\t\t}\n\t\tif p.cfg.ThreadID > 0 && p.cfg.ThreadID != p.currentThreadID {\n\t\t\treturn\n\t\t}\n\t\ttable, err := p.tableInformation(event.TableID, event.Table.Schema, event.Table.Table)\n\t\tif err != nil {\n\t\t\treturn false, err\n\t\t}\n\n\t\tswitch e.Header.EventType {\n\t\tcase replication.WRITE_ROWS_EVENTv1, replication.WRITE_ROWS_EVENTv2:\n\t\t\tif _, ok := p.SqlTypes[\"insert\"]; ok && p.checkCanParse(table, event) {\n\t\t\t\tif p.cfg.Flashback {\n\t\t\t\t\terr = p.generateDeleteSQL(table, event, e)\n\t\t\t\t} else {\n\t\t\t\t\terr = p.generateInsertSQL(table, event, e)\n\t\t\t\t}\n\t\t\t\tp.changeRows = p.changeRows + len(event.Rows)\n\t\t\t}\n\t\tcase replication.DELETE_ROWS_EVENTv1, replication.DELETE_ROWS_EVENTv2:\n\t\t\tif _, ok := p.SqlTypes[\"delete\"]; ok && p.checkCanParse(table, event) {\n\t\t\t\tif p.cfg.Flashback {\n\t\t\t\t\terr = p.generateInsertSQL(table, event, e)\n\t\t\t\t} else {\n\t\t\t\t\terr = p.generateDeleteSQL(table, event, e)\n\t\t\t\t}\n\t\t\t\tp.changeRows = p.changeRows + len(event.Rows)\n\t\t\t}\n\t\tcase replication.UPDATE_ROWS_EVENTv1, replication.UPDATE_ROWS_EVENTv2:\n\t\t\tif _, ok := p.SqlTypes[\"update\"]; ok && p.checkCanParse(table, event) {\n\t\t\t\tif p.cfg.Flashback {\n\t\t\t\t\terr = p.generateUpdateRollbackSQL(table, event, e)\n\t\t\t\t} else {\n\t\t\t\t\terr = p.generateUpdateSQL(table, event, e)\n\t\t\t\t}\n\t\t\t\tp.changeRows = p.changeRows + len(event.Rows)/2\n\t\t\t}\n\t\t}\n\n\t\tif err != nil {\n\t\t\treturn false, err\n\t\t}\n\n\t}\n\n\tif p.cfg.MaxRows > 0 && p.changeRows >= p.cfg.MaxRows {\n\t\tlog.Info(\"已超出最大行数\")\n\t\treturn false, nil\n\t}\n\n\t// 再次判断是否到结束位置,以免处在临界值,且无新日志时程序卡住\n\tif e.Header.Timestamp > 0 {\n\t\tif p.stopTimestamp > 0 && e.Header.Timestamp > p.stopTimestamp {\n\t\t\tlog.Warn(\"已超出结束时间\")\n\t\t\treturn false, nil\n\t\t}\n\t}\n\n\tif !p.running {\n\t\tlog.Warn(\"进程手动中止\")\n\t\treturn false, nil\n\t}\n\n\tif finishFlag > -1 {\n\t\treturn false, nil\n\t}\n\n\treturn\n}\n\n// ParseRows 获取已解析行数\nfunc (p *MyBinlogParser) Config() *BinlogParserConfig {\n\treturn p.cfg\n}\n\n// ParseRows 获取已解析行数\nfunc (p *MyBinlogParser) ParseRows() int {\n\treturn p.changeRows\n}\n\n// Percent 获取解析百分比\nfunc (p *MyBinlogParser) Percent() int {\n\tif !p.running {\n\t\treturn 100\n\t}\n\tif p.cfg.StartFile != \"\" {\n\t\tstart := mysql.Position{\n\t\t\tName: p.cfg.StartFile,\n\t\t\tPos:  uint32(p.cfg.StartPosition),\n\t\t}\n\t\tstop := mysql.Position{\n\t\t\tName: p.cfg.StopFile,\n\t\t\tPos:  uint32(p.cfg.StopPosition),\n\t\t}\n\t\treturn ComputePercent(start, stop, p.currentPosition,\n\t\t\t*p.masterStatus, p.autoParseBinlogPosition())\n\t}\n\n\tif p.stopTimestamp > 0 {\n\t\tif p.currentTimtstamp < p.startTimestamp || p.stopTimestamp == p.startTimestamp {\n\t\t\treturn 0\n\t\t} else {\n\t\t\treturn int((p.currentTimtstamp - p.startTimestamp) * 100 / (p.stopTimestamp - p.startTimestamp))\n\t\t}\n\t}\n\n\tif p.cfg.MaxRows > 0 {\n\t\tif p.changeRows < p.cfg.MaxRows {\n\t\t\treturn p.changeRows / p.cfg.MaxRows * 100\n\t\t}\n\t\treturn 100\n\t}\n\n\treturn 0\n}\n\n// Archive文件压缩打包\nfunc (p *MyBinlogParser) Archive() (fileSize int64, err error) {\n\tvar url string\n\tif strings.Count(p.outputFileName, \".\") > 0 {\n\t\ta := strings.Split(p.outputFileName, \".\")\n\t\turl = strings.Join(a[0:len(a)-1], \".\")\n\t} else {\n\t\turl = p.outputFileName\n\t}\n\n\turl = url + \".tar.gz\"\n\n\terr = archiver.Archive([]string{p.outputFileName}, url)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\tfileInfo, _ := os.Stat(url)\n\t//文件大小\n\tfileSize = fileInfo.Size()\n\n\t// 压缩完成后删除文件\n\tp.clear()\n\n\tlog.Info(\"打包完成\")\n\treturn fileSize, nil\n}\n\n// getTableName 获取RowsEvent的表名\nfunc getTableName(e *replication.RowsEvent) string {\n\tvar build strings.Builder\n\tbuild.WriteString(\"`\")\n\tbuild.Write(e.Table.Schema)\n\tbuild.WriteString(\"`.`\")\n\tbuild.Write(e.Table.Table)\n\tbuild.WriteString(\"`\")\n\treturn build.String()\n}\n\nfunc ComputePercent(start, stop, currnet mysql.Position,\n\tmasterStatus MasterStatus, binlogs []MasterLog) int {\n\tif start.Name == stop.Name && stop.Pos > 0 {\n\t\treturn int((currnet.Pos - start.Pos) * 100 / (stop.Pos - start.Pos))\n\t} else {\n\t\tstopFile := stop.Name\n\t\tstopPosition := int(stop.Pos)\n\t\tif stopFile == \"\" {\n\t\t\tstopFile = masterStatus.File\n\t\t\tstopPosition = masterStatus.Position\n\t\t}\n\t\ttotal := 0\n\t\tparsed := 0\n\t\tfor _, binlog := range binlogs {\n\t\t\tif binlog.Name < start.Name {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif binlog.Name > stopFile {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\t// log.Info(\"---\")\n\t\t\t// log.Infof(\"total: %d, parsed: %d, binlog: %s  %v\",\n\t\t\t// \ttotal, parsed, binlog.Name, binlog.Size)\n\t\t\t// 第一个日志文件指定了文件\n\t\t\tif binlog.Name == start.Name && binlog.Size > int(start.Pos) {\n\t\t\t\ttotal += binlog.Size - int(start.Pos)\n\t\t\t} else if binlog.Name == stopFile {\n\t\t\t\t// 最后一个日志文件\n\t\t\t\tif stopPosition > 0 {\n\t\t\t\t\ttotal += stopPosition\n\t\t\t\t} else {\n\t\t\t\t\ttotal += binlog.Size\n\t\t\t\t}\n\t\t\t} else if binlog.Name < stopFile {\n\t\t\t\ttotal += binlog.Size\n\t\t\t}\n\n\t\t\tif currnet.Name == binlog.Name {\n\t\t\t\tif binlog.Name == start.Name {\n\t\t\t\t\tif currnet.Pos > start.Pos {\n\t\t\t\t\t\tparsed += int(currnet.Pos - start.Pos)\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tparsed += int(currnet.Pos)\n\t\t\t\t}\n\t\t\t} else if binlog.Name < currnet.Name {\n\t\t\t\tif binlog.Name == start.Name {\n\t\t\t\t\tif binlog.Size > int(start.Pos) {\n\t\t\t\t\t\tparsed += binlog.Size - int(start.Pos)\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tparsed += binlog.Size\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// log.Infof(\"total: %d, parsed: %d, binlog: %s  %v\",\n\t\t\t// \ttotal, parsed, binlog.Name, binlog.Size)\n\t\t}\n\t\tpecent := 0\n\t\tif total > 0 {\n\t\t\tpecent = parsed * 100 / total\n\t\t}\n\t\tif parsed >= total {\n\t\t\tpecent = 100\n\t\t}\n\t\t// log.Infof(\"parsed: %d, total: %d,  percent: %v\",\n\t\t// \tparsed, total, pecent)\n\t\treturn pecent\n\t}\n}\n"
  },
  {
    "path": "core/parserV2.go",
    "content": "package core\n\nimport (\n\t\"bytes\"\n\t\"database/sql/driver\"\n\t\"errors\"\n\t\"fmt\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-mysql-org/go-mysql/replication\"\n\t\"github.com/shopspring/decimal\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\n// generateInsertSQL 生成insert语句\nfunc (p *MyBinlogParser) generateInsertSQL_2(t *Table, e *replication.RowsEvent,\n\tbinEvent *replication.BinlogEvent) error {\n\n\tif len(t.Columns) < int(e.ColumnCount) {\n\t\treturn fmt.Errorf(\"表%s.%s缺少列!当前列数:%d,binlog的列数%d\",\n\t\t\te.Table.Schema, e.Table.Table, len(t.Columns), e.ColumnCount)\n\t}\n\n\tvar columnNames []string\n\tc := \"`%s`\"\n\n\tbuf := &bytes.Buffer{}\n\tbuf.WriteString(\"INSERT \")\n\t// if s.ignore {\n\t// \t_, _ = buf.WriteString(\"IGNORE \")\n\t// }\n\tbuf.WriteString(\"INTO \")\n\tbuf.WriteString(\"`\")\n\tbuf.Write(e.Table.Schema)\n\tbuf.WriteString(\"`.`\")\n\tbuf.Write(e.Table.Table)\n\tbuf.WriteString(\"`\")\n\tbuf.WriteString(\"(\")\n\tstrings.Join(columnNames, \",\")\n\n\tfor i, col := range t.Columns {\n\t\tif i < int(e.ColumnCount) {\n\t\t\t//  有主键且设置去除主键时 作特殊处理\n\t\t\tif t.hasPrimary && p.cfg.RemovePrimary {\n\t\t\t\tif _, ok := t.primarys[i]; !ok {\n\t\t\t\t\tcolumnNames = append(columnNames, fmt.Sprintf(c, col.ColumnName))\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tcolumnNames = append(columnNames, fmt.Sprintf(c, col.ColumnName))\n\t\t\t}\n\t\t}\n\t}\n\n\tbuf.WriteString(strings.Join(columnNames, \",\"))\n\tbuf.WriteString(\") VALUES\")\n\n\tvar insertSQL string\n\tif !p.Config().MinimalInsert {\n\t\tinsertSQL = buf.String()\n\t}\n\tfor rowIndex, rows := range e.Rows {\n\t\tif rowIndex > 0 {\n\t\t\tif p.Config().MinimalInsert {\n\t\t\t\tbuf.WriteString(\",\")\n\t\t\t} else {\n\t\t\t\tbuf.WriteString(insertSQL)\n\t\t\t}\n\t\t}\n\n\t\tbuf.WriteString(\"(\")\n\n\t\tfor colIndex, d := range rows {\n\t\t\tif t.hasPrimary && p.cfg.RemovePrimary {\n\t\t\t\tif _, ok := t.primarys[colIndex]; ok {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif t.Columns[colIndex].IsUnsigned() {\n\t\t\t\td = processValue(d, GetDataTypeBase(t.Columns[colIndex].ColumnType))\n\t\t\t}\n\t\t\tv, err := valueSerialize(d)\n\t\t\tif err != nil {\n\t\t\t\tlog.Error(err)\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif colIndex > 0 {\n\t\t\t\tbuf.WriteByte(',')\n\t\t\t}\n\t\t\tbuf.Write(v)\n\t\t}\n\n\t\tbuf.WriteString(\")\")\n\n\t\tif !p.cfg.MinimalInsert {\n\t\t\tp.write(buf.Bytes(), binEvent)\n\t\t\tbuf = &bytes.Buffer{}\n\t\t}\n\t}\n\n\tif p.cfg.MinimalInsert {\n\t\tp.write(buf.Bytes(), binEvent)\n\t\tbuf.Reset()\n\t}\n\n\treturn nil\n}\n\n// generateDeleteSQL 生成delete语句\nfunc (p *MyBinlogParser) generateDeleteSQL_2(t *Table, e *replication.RowsEvent,\n\tbinEvent *replication.BinlogEvent) error {\n\n\tif len(t.Columns) < int(e.ColumnCount) {\n\t\treturn fmt.Errorf(\"表%s.%s缺少列!当前列数:%d,binlog的列数%d\",\n\t\t\te.Table.Schema, e.Table.Table, len(t.Columns), e.ColumnCount)\n\t}\n\n\tfor _, rows := range e.Rows {\n\n\t\tbuf := &bytes.Buffer{}\n\t\tbuf.WriteString(\"DELETE FROM \")\n\t\tbuf.WriteString(\"`\")\n\t\tbuf.Write(e.Table.Schema)\n\t\tbuf.WriteString(\"`.`\")\n\t\tbuf.Write(e.Table.Table)\n\t\tbuf.WriteString(\"` WHERE \")\n\n\t\tinitFirst := false\n\t\tfor colIndex, d := range rows {\n\t\t\tif t.hasPrimary {\n\t\t\t\tif _, ok := t.primarys[colIndex]; ok {\n\t\t\t\t\tif initFirst {\n\t\t\t\t\t\tbuf.WriteString(\" AND \")\n\t\t\t\t\t}\n\t\t\t\t\tinitFirst = true\n\t\t\t\t\tif t.Columns[colIndex].IsUnsigned() {\n\t\t\t\t\t\td = processValue(d, GetDataTypeBase(t.Columns[colIndex].ColumnType))\n\t\t\t\t\t}\n\t\t\t\t\tif d == nil {\n\t\t\t\t\t\tbuf.WriteString(\"`\")\n\t\t\t\t\t\tbuf.WriteString(t.Columns[colIndex].ColumnName)\n\t\t\t\t\t\tbuf.WriteString(\"` IS NULL\")\n\t\t\t\t\t} else {\n\t\t\t\t\t\tv, err := valueSerialize(d)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\tlog.Error(err)\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbuf.WriteString(\"`\")\n\t\t\t\t\t\tbuf.WriteString(t.Columns[colIndex].ColumnName)\n\t\t\t\t\t\tbuf.WriteString(\"`=\")\n\t\t\t\t\t\tbuf.Write(v)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif initFirst {\n\t\t\t\t\tbuf.WriteString(\" AND \")\n\t\t\t\t}\n\t\t\t\tinitFirst = true\n\t\t\t\tif t.Columns[colIndex].IsUnsigned() {\n\t\t\t\t\td = processValue(d, GetDataTypeBase(t.Columns[colIndex].ColumnType))\n\t\t\t\t}\n\t\t\t\tif d == nil {\n\t\t\t\t\tbuf.WriteString(\"`\")\n\t\t\t\t\tbuf.WriteString(t.Columns[colIndex].ColumnName)\n\t\t\t\t\tbuf.WriteString(\"` IS NULL\")\n\t\t\t\t} else {\n\t\t\t\t\tv, err := valueSerialize(d)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tlog.Error(err)\n\t\t\t\t\t}\n\t\t\t\t\tbuf.WriteString(\"`\")\n\t\t\t\t\tbuf.WriteString(t.Columns[colIndex].ColumnName)\n\t\t\t\t\tbuf.WriteString(\"`=\")\n\t\t\t\t\tbuf.Write(v)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tp.write(buf.Bytes(), binEvent)\n\t\t// buf = &bytes.Buffer{}\n\t}\n\n\treturn nil\n}\n\n// generateUpdateSQL 生成update语句\nfunc (p *MyBinlogParser) generateUpdateSQL_2(t *Table, e *replication.RowsEvent,\n\tbinEvent *replication.BinlogEvent) error {\n\n\tif len(t.Columns) < int(e.ColumnCount) {\n\t\treturn fmt.Errorf(\"表%s.%s缺少列!当前列数:%d,binlog的列数%d\",\n\t\t\te.Table.Schema, e.Table.Table, len(t.Columns), e.ColumnCount)\n\t}\n\n\t// 最小化回滚语句, 当开启时,update语句中未变更的值不再记录到回滚语句中\n\tminimalMode := p.cfg.MinimalUpdate\n\n\t// update时, Rows为2的倍数, 双数index为旧值,单数index为新值\n\tbuf := &bytes.Buffer{}\n\tbufWhere := &bytes.Buffer{}\n\n\tfor rowIndex, row := range e.Rows {\n\n\t\tif rowIndex%2 == 0 {\n\t\t\t// 旧值\n\t\t\tfor colIndex, d := range row {\n\t\t\t\tif t.hasPrimary {\n\t\t\t\t\tif _, ok := t.primarys[colIndex]; ok {\n\t\t\t\t\t\tif bufWhere.Len() > 0 {\n\t\t\t\t\t\t\tbufWhere.WriteString(\" AND \")\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif d == nil {\n\t\t\t\t\t\t\tbufWhere.WriteString(\"`\")\n\t\t\t\t\t\t\tbufWhere.WriteString(t.Columns[colIndex].ColumnName)\n\t\t\t\t\t\t\tbufWhere.WriteString(\"` IS NULL\")\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tbufWhere.WriteString(\"`\")\n\t\t\t\t\t\t\tbufWhere.WriteString(t.Columns[colIndex].ColumnName)\n\t\t\t\t\t\t\tbufWhere.WriteString(\"`=\")\n\n\t\t\t\t\t\t\tv, err := valueSerialize(d)\n\t\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\t\tlog.Error(err)\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tbufWhere.Write(v)\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tif colIndex > 0 {\n\t\t\t\t\t\tbufWhere.WriteString(\" AND \")\n\t\t\t\t\t}\n\n\t\t\t\t\tif t.Columns[colIndex].IsUnsigned() {\n\t\t\t\t\t\td = processValue(d, GetDataTypeBase(t.Columns[colIndex].ColumnType))\n\t\t\t\t\t}\n\n\t\t\t\t\tif d == nil {\n\t\t\t\t\t\tbufWhere.WriteString(\"`\")\n\t\t\t\t\t\tbufWhere.WriteString(t.Columns[colIndex].ColumnName)\n\t\t\t\t\t\tbufWhere.WriteString(\"` IS NULL\")\n\t\t\t\t\t} else {\n\t\t\t\t\t\tv, err := valueSerialize(d)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\tlog.Error(err)\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbufWhere.WriteString(\"`\")\n\t\t\t\t\t\tbufWhere.WriteString(t.Columns[colIndex].ColumnName)\n\t\t\t\t\t\tbufWhere.WriteString(\"`=\")\n\t\t\t\t\t\tbufWhere.Write(v)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tinitFirst := false\n\n\t\t\tbuf.WriteString(\"UPDATE \")\n\t\t\tbuf.WriteString(\"`\")\n\t\t\tbuf.Write(e.Table.Schema)\n\t\t\tbuf.WriteString(\"`.`\")\n\t\t\tbuf.Write(e.Table.Table)\n\t\t\tbuf.WriteString(\"` SET \")\n\n\t\t\tfor colIndex, d := range row {\n\t\t\t\tif minimalMode {\n\t\t\t\t\t// 最小化模式下,列如果相等则省略\n\t\t\t\t\tif !compareValue(d, e.Rows[rowIndex-1][colIndex]) {\n\t\t\t\t\t\tif initFirst {\n\t\t\t\t\t\t\tbuf.WriteByte(',')\n\t\t\t\t\t\t}\n\t\t\t\t\t\tinitFirst = true\n\t\t\t\t\t\tif t.Columns[colIndex].IsUnsigned() {\n\t\t\t\t\t\t\td = processValue(d, GetDataTypeBase(t.Columns[colIndex].ColumnType))\n\t\t\t\t\t\t}\n\t\t\t\t\t\tv, err := valueSerialize(d)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\tlog.Error(err)\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tbuf.WriteString(\"`\")\n\t\t\t\t\t\tbuf.WriteString(t.Columns[colIndex].ColumnName)\n\t\t\t\t\t\tbuf.WriteString(\"`=\")\n\t\t\t\t\t\tbuf.Write(v)\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tif colIndex > 0 {\n\t\t\t\t\t\tbuf.WriteByte(',')\n\t\t\t\t\t}\n\t\t\t\t\tif t.Columns[colIndex].IsUnsigned() {\n\t\t\t\t\t\td = processValue(d, GetDataTypeBase(t.Columns[colIndex].ColumnType))\n\t\t\t\t\t}\n\t\t\t\t\tv, err := valueSerialize(d)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tlog.Error(err)\n\t\t\t\t\t}\n\t\t\t\t\tbuf.WriteString(\"`\")\n\t\t\t\t\tbuf.WriteString(t.Columns[colIndex].ColumnName)\n\t\t\t\t\tbuf.WriteString(\"`=\")\n\t\t\t\t\tbuf.Write(v)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tbuf.WriteString(\" WHERE \")\n\t\t\tbuf.Write(bufWhere.Bytes())\n\n\t\t\tp.write(buf.Bytes(), binEvent)\n\n\t\t\tbuf = &bytes.Buffer{}\n\t\t\tbufWhere = &bytes.Buffer{}\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// generateUpdateRollbackSQL 生成update语句\nfunc (p *MyBinlogParser) generateUpdateRollbackSQL_2(t *Table, e *replication.RowsEvent,\n\tbinEvent *replication.BinlogEvent) error {\n\n\tif len(t.Columns) < int(e.ColumnCount) {\n\t\treturn fmt.Errorf(\"表%s.%s缺少列!当前列数:%d,binlog的列数%d\",\n\t\t\te.Table.Schema, e.Table.Table, len(t.Columns), e.ColumnCount)\n\t}\n\n\t// 最小化回滚语句, 当开启时,update语句中未变更的值不再记录到回滚语句中\n\tminimalMode := p.cfg.MinimalUpdate\n\n\t// update时, Rows为2的倍数, 双数index为旧值,单数index为新值\n\tbuf := &bytes.Buffer{}\n\tbufWhere := &bytes.Buffer{}\n\n\tfor rowIndex, rows := range e.Rows {\n\t\t// 旧值\n\t\tif rowIndex%2 == 0 {\n\n\t\t\tbuf.WriteString(\"UPDATE \")\n\t\t\tbuf.WriteString(\"`\")\n\t\t\tbuf.Write(e.Table.Schema)\n\t\t\tbuf.WriteString(\"`.`\")\n\t\t\tbuf.Write(e.Table.Table)\n\t\t\tbuf.WriteString(\"` SET \")\n\n\t\t\tinitFirst := false\n\t\t\tfor colIndex, d := range rows {\n\t\t\t\tif minimalMode {\n\t\t\t\t\t// 最小化模式下,列如果相等则省略\n\t\t\t\t\tif !compareValue(d, e.Rows[rowIndex+1][colIndex]) {\n\t\t\t\t\t\tif initFirst {\n\t\t\t\t\t\t\tbuf.WriteString(\",\")\n\t\t\t\t\t\t}\n\t\t\t\t\t\tinitFirst = true\n\t\t\t\t\t\tif t.Columns[colIndex].IsUnsigned() {\n\t\t\t\t\t\t\td = processValue(d, GetDataTypeBase(t.Columns[colIndex].ColumnType))\n\t\t\t\t\t\t}\n\t\t\t\t\t\tv, err := valueSerialize(d)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\tlog.Error(err)\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tbuf.WriteString(\"`\")\n\t\t\t\t\t\tbuf.WriteString(t.Columns[colIndex].ColumnName)\n\t\t\t\t\t\tbuf.WriteString(\"`=\")\n\t\t\t\t\t\tbuf.Write(v)\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tif colIndex > 0 {\n\t\t\t\t\t\tbuf.WriteString(\",\")\n\t\t\t\t\t}\n\t\t\t\t\tif t.Columns[colIndex].IsUnsigned() {\n\t\t\t\t\t\td = processValue(d, GetDataTypeBase(t.Columns[colIndex].ColumnType))\n\t\t\t\t\t}\n\t\t\t\t\tv, err := valueSerialize(d)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tlog.Error(err)\n\t\t\t\t\t}\n\n\t\t\t\t\tbuf.WriteString(\"`\")\n\t\t\t\t\tbuf.WriteString(t.Columns[colIndex].ColumnName)\n\t\t\t\t\tbuf.WriteString(\"`=\")\n\t\t\t\t\tbuf.Write(v)\n\t\t\t\t}\n\t\t\t}\n\t\t} else { // 新值\n\t\t\tfor colIndex, d := range rows {\n\t\t\t\tif t.hasPrimary {\n\t\t\t\t\t_, ok := t.primarys[colIndex]\n\t\t\t\t\tif ok {\n\t\t\t\t\t\tif t.Columns[colIndex].IsUnsigned() {\n\t\t\t\t\t\t\td = processValue(d, GetDataTypeBase(t.Columns[colIndex].ColumnType))\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif bufWhere.Len() > 0 {\n\t\t\t\t\t\t\tbufWhere.WriteString(\" AND \")\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif d == nil {\n\t\t\t\t\t\t\tbufWhere.WriteString(\"`\")\n\t\t\t\t\t\t\tbufWhere.WriteString(t.Columns[colIndex].ColumnName)\n\t\t\t\t\t\t\tbufWhere.WriteString(\"` IS NULL\")\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tbufWhere.WriteString(\"`\")\n\t\t\t\t\t\t\tbufWhere.WriteString(t.Columns[colIndex].ColumnName)\n\t\t\t\t\t\t\tbufWhere.WriteString(\"`=\")\n\n\t\t\t\t\t\t\tv, err := valueSerialize(d)\n\t\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\t\tlog.Error(err)\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tbufWhere.Write(v)\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tif t.Columns[colIndex].IsUnsigned() {\n\t\t\t\t\t\td = processValue(d, GetDataTypeBase(t.Columns[colIndex].ColumnType))\n\t\t\t\t\t}\n\t\t\t\t\tif bufWhere.Len() > 0 {\n\t\t\t\t\t\tbufWhere.WriteString(\" AND \")\n\t\t\t\t\t}\n\t\t\t\t\tif d == nil {\n\t\t\t\t\t\tbufWhere.WriteString(\"`\")\n\t\t\t\t\t\tbufWhere.WriteString(t.Columns[colIndex].ColumnName)\n\t\t\t\t\t\tbufWhere.WriteString(\"` IS NULL\")\n\t\t\t\t\t} else {\n\t\t\t\t\t\tbufWhere.WriteString(\"`\")\n\t\t\t\t\t\tbufWhere.WriteString(t.Columns[colIndex].ColumnName)\n\t\t\t\t\t\tbufWhere.WriteString(\"`=\")\n\n\t\t\t\t\t\tv, err := valueSerialize(d)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\tlog.Error(err)\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbufWhere.Write(v)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tbuf.WriteString(\" WHERE \")\n\t\t\tbuf.Write(bufWhere.Bytes())\n\n\t\t\tp.write(buf.Bytes(), binEvent)\n\n\t\t\tbuf = &bytes.Buffer{}\n\t\t\tbufWhere = &bytes.Buffer{}\n\t\t}\n\n\t}\n\n\treturn nil\n}\n\n// valueSerialize 参数转换成数据库类型\nfunc valueSerialize(arg driver.Value) (buf []byte, err error) {\n\n\tif arg == nil {\n\t\tbuf = append(buf, \"NULL\"...)\n\t\treturn buf, nil\n\t}\n\n\tswitch v := arg.(type) {\n\tcase int8:\n\t\tbuf = strconv.AppendInt(buf, int64(v), 10)\n\tcase int16:\n\t\tbuf = strconv.AppendInt(buf, int64(v), 10)\n\tcase int32:\n\t\tbuf = strconv.AppendInt(buf, int64(v), 10)\n\tcase int64:\n\t\tbuf = strconv.AppendInt(buf, v, 10)\n\tcase uint64:\n\t\tbuf = strconv.AppendUint(buf, uint64(v), 10)\n\tcase int:\n\t\tbuf = strconv.AppendInt(buf, int64(v), 10)\n\tcase decimal.Decimal:\n\t\tbuf = append(buf, v.String()...)\n\tcase float32:\n\t\tbuf = strconv.AppendFloat(buf, float64(v), 'g', -1, 32)\n\tcase float64:\n\t\tbuf = strconv.AppendFloat(buf, v, 'g', -1, 64)\n\tcase bool:\n\t\tif v {\n\t\t\tbuf = append(buf, '1')\n\t\t} else {\n\t\t\tbuf = append(buf, '0')\n\t\t}\n\tcase time.Time:\n\t\tif v.IsZero() {\n\t\t\tbuf = append(buf, \"'0000-00-00'\"...)\n\t\t} else {\n\t\t\tv := v.In(time.UTC)\n\t\t\tv = v.Add(time.Nanosecond * 500) // To round under microsecond\n\t\t\tyear := v.Year()\n\t\t\tyear100 := year / 100\n\t\t\tyear1 := year % 100\n\t\t\tmonth := v.Month()\n\t\t\tday := v.Day()\n\t\t\thour := v.Hour()\n\t\t\tminute := v.Minute()\n\t\t\tsecond := v.Second()\n\t\t\tmicro := v.Nanosecond() / 1000\n\n\t\t\tbuf = append(buf, []byte{\n\t\t\t\t'\\'',\n\t\t\t\tdigits10[year100], digits01[year100],\n\t\t\t\tdigits10[year1], digits01[year1],\n\t\t\t\t'-',\n\t\t\t\tdigits10[month], digits01[month],\n\t\t\t\t'-',\n\t\t\t\tdigits10[day], digits01[day],\n\t\t\t\t' ',\n\t\t\t\tdigits10[hour], digits01[hour],\n\t\t\t\t':',\n\t\t\t\tdigits10[minute], digits01[minute],\n\t\t\t\t':',\n\t\t\t\tdigits10[second], digits01[second],\n\t\t\t}...)\n\n\t\t\tif micro != 0 {\n\t\t\t\tmicro10000 := micro / 10000\n\t\t\t\tmicro100 := micro / 100 % 100\n\t\t\t\tmicro1 := micro % 100\n\t\t\t\tbuf = append(buf, []byte{\n\t\t\t\t\t'.',\n\t\t\t\t\tdigits10[micro10000], digits01[micro10000],\n\t\t\t\t\tdigits10[micro100], digits01[micro100],\n\t\t\t\t\tdigits10[micro1], digits01[micro1],\n\t\t\t\t}...)\n\t\t\t}\n\t\t\tbuf = append(buf, '\\'')\n\t\t}\n\tcase string:\n\t\tbuf = append(buf, '\\'')\n\t\tbuf = escapeBytesBackslash(buf, []byte(v))\n\t\tbuf = append(buf, '\\'')\n\tcase []byte:\n\t\tif v == nil {\n\t\t\tbuf = append(buf, \"NULL\"...)\n\t\t} else {\n\t\t\t// buf = append(buf, \"_binary'\"...)\n\t\t\tbuf = append(buf, '\\'')\n\n\t\t\tbuf = escapeBytesBackslash(buf, v)\n\n\t\t\tbuf = append(buf, '\\'')\n\t\t}\n\tdefault:\n\t\t// fmt.Println(v)\n\t\tlog.Printf(\"%T\", v)\n\t\tlog.Info(\"解析错误\")\n\t\treturn nil, errors.New(\"driver: skip fast-path; continue as if unimplemented\")\n\t}\n\n\t// 4 << 20 , 4MB\n\tif len(buf)+4 > 4<<20 {\n\t\t// log.Print(\"%T\", v)\n\t\tlog.Info(\"解析错误\")\n\t\treturn nil, errors.New(\"driver: skip fast-path; continue as if unimplemented\")\n\t}\n\n\treturn buf, nil\n}\n"
  },
  {
    "path": "core/parser_stats.go",
    "content": "package core\n\nimport (\n\t\"bufio\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"sort\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-mysql-org/go-mysql/mysql\"\n\t\"github.com/go-mysql-org/go-mysql/replication\"\n\t\"github.com/jinzhu/now\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\ntype SummaryStats struct {\n\tInserts   int          `json:\"insert\"`\n\tUpdates   int          `json:\"update\"`\n\tDeletes   int          `json:\"delete\"`\n\tTotal     int          `json:\"total\"`\n\tStartTime time.Time    `json:\"start_time\"`\n\tEndTime   time.Time    `json:\"end_time\"`\n\tTables    []TableStats `json:\"tables\"`\n}\n\ntype TableStats struct {\n\tTable   string\n\tDB      string\n\tInserts int `json:\"insert\"`\n\tUpdates int `json:\"update\"`\n\tDeletes int `json:\"delete\"`\n\tTotal   int `json:\"total\"`\n}\n\nfunc (s *SummaryStats) String() string {\n\tif len(s.Tables) > 1 {\n\t\tsort.SliceStable(s.Tables, func(cur, next int) bool {\n\t\t\treturn s.Tables[cur].Total < s.Tables[next].Total\n\t\t})\n\t}\n\tvar tableStr string\n\tfor _, t := range s.Tables {\n\t\ttableStr += t.String()\n\t}\n\n\t// fmt.Sprintf(\"start: %v\\n\"+\" stop: %v\\n\", s.StartTime.Format(time.RFC3339),\n\t// s.EndTime.Format(time.RFC3339)) + \"\\n\" +\n\treturn tableStr +\n\t\tfmt.Sprintf(\"\\n\"+`Summary: %d\n\tinsert: %d\n\tupdate: %d\n\tdelete: %d`+\"\\n\", s.Total, s.Inserts, s.Updates, s.Deletes)\n}\n\nfunc (t *TableStats) String() string {\n\treturn fmt.Sprintf(`%s.%s: %d [insert:%d, update:%d, delete:%d]`+\"\\n\", t.DB, t.Table, t.Total, t.Inserts, t.Updates, t.Deletes)\n}\n\ntype BinlogParserStats struct {\n\tbaseParser\n\n\tmasterStatus *MasterStatus\n\n\tstartTimestamp uint32\n\tstopTimestamp  uint32\n\t// 输出到指定文件\n\toutputFile     *os.File\n\toutputFileName string\n\n\t// db *gorm.DB\n\n\t// 使用buffer写的测试\n\t// 解析600M日志,约500w行,直接使用文件时,用时5min\n\t// 使用buffer用时1min\n\tbufferWriter *bufio.Writer\n\n\t// 表结构缓存(仅用于本地解析)\n\ttableCacheList map[string]*Table\n\n\t// 解析用临时变量\n\tcurrentPosition  mysql.Position // 当前binlog位置\n\tcurrentTimtstamp uint32         // 当前解析到的时间戳\n\n\t// 保存binlogs文件和position,用以计算percent\n\tbinlogs []MasterLog\n\n\tstats       SummaryStats\n\tstatsTables map[string]*TableStats\n}\n\n// Parser 远程解析\nfunc (p *BinlogParserStats) ParserStats() error {\n\t// runtime.GOMAXPROCS(runtime.NumCPU())\n\n\tif p.cfg.Host == \"\" && p.cfg.StartFile != \"\" {\n\t\t_, err := os.Stat(p.cfg.StartFile)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"open start_file: %w\", err)\n\t\t}\n\t\treturn p.parserFile()\n\t}\n\n\tdefer timeTrack(time.Now(), \"Parser\")\n\n\tdefer func() {\n\t\tif p.outputFile != nil {\n\t\t\tp.outputFile.Close()\n\t\t}\n\t}()\n\n\tvar err error\n\n\tp.running = true\n\n\tp.db, err = p.getDB()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tdefer p.db.Close()\n\n\tif len(p.outputFileName) > 0 {\n\n\t\tif Exists(p.outputFileName) {\n\t\t\t// p.checkError(errors.New(\"指定的文件已存在!\"))\n\t\t\t// 不作追加,改为文件存在时清除内容,O_APPEND\n\t\t\tp.outputFile, err = os.OpenFile(p.outputFileName,\n\t\t\t\tos.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0666)\n\t\t} else {\n\t\t\tp.outputFile, err = os.Create(p.outputFileName)\n\t\t}\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"create output file: %w\", err)\n\t\t}\n\n\t\tp.bufferWriter = bufio.NewWriter(p.outputFile)\n\t}\n\n\tcfg := replication.BinlogSyncerConfig{\n\t\tServerID: 2000000211,\n\t\tFlavor:   p.cfg.Flavor,\n\n\t\tHost:       p.cfg.Host,\n\t\tPort:       p.cfg.Port,\n\t\tUser:       p.cfg.User,\n\t\tPassword:   p.cfg.Password,\n\t\tUseDecimal: true,\n\t}\n\n\tb := replication.NewBinlogSyncer(cfg)\n\tdefer b.Close()\n\n\tp.currentPosition = mysql.Position{\n\t\tName: p.startFile,\n\t\tPos:  uint32(p.cfg.StartPosition),\n\t}\n\n\ts, err := b.StartSync(p.currentPosition)\n\tif err != nil {\n\t\tlog.Infof(\"Start sync error: %v\\n\", err)\n\t\treturn fmt.Errorf(\"Start sync: %w\", err)\n\t}\n\n\tvar finishError error\nFOR:\n\tfor {\n\t\te, err := s.GetEvent(p.ctx)\n\t\tif err != nil {\n\t\t\tif err == context.DeadlineExceeded {\n\t\t\t\tlog.Warnf(\"Waiting for timeout(10s), no new event is generated, automatically stop: %v\\n\", err)\n\t\t\t\t// err = fmt.Errorf(\"Waiting for timeout(10s), no new event is generated, automatically stop: %v\\n\", err)\n\t\t\t\terr = nil\n\t\t\t} else {\n\t\t\t\tlog.Errorf(\"Get event error: %v\\n\", err)\n\t\t\t}\n\t\t\tfinishError = fmt.Errorf(\"get binlog event: %w\", err)\n\t\t\tbreak FOR\n\t\t}\n\n\t\tselect {\n\t\tcase <-p.ctx.Done():\n\t\t\tfinishError = context.Canceled\n\t\t\tbreak FOR\n\t\tdefault:\n\t\t\tok, err := p.parseSingleEvent(e)\n\t\t\tif err != nil {\n\t\t\t\tfinishError = err\n\t\t\t\tbreak FOR\n\t\t\t}\n\t\t\tif !ok {\n\t\t\t\tbreak FOR\n\t\t\t}\n\t\t}\n\t}\n\n\tp.running = false\n\treturn finishError\n}\n\n// checkFinish 检查是否需要结束循环\nfunc (p *BinlogParserStats) checkFinish(currentPosition *mysql.Position) int {\n\treturnValue := -1\n\t// 满足以下任一条件时结束循环\n\t// * binlog文件超出指定结束文件\n\t// * 超出指定结束文件的指定位置\n\t// * 超出当前最新binlog位置\n\n\t// 当前binlog解析位置 相对于配置的结束位置/最新位置\n\t// 超出时返回 1\n\t// 等于时返回 0\n\t// 小于时返回 -1\n\n\t// 根据是否为事件开始做不同判断\n\t// 事件开始时做大于判断\n\t// 事件结束时做等于判断\n\tvar stopMsg string\n\tif p.stopFile != \"\" && currentPosition.Name > p.stopFile {\n\t\tstopMsg = \"超出指定结束文件\"\n\t\treturnValue = 1\n\t} else if p.cfg.StopPosition > 0 && currentPosition.Name == p.stopFile {\n\t\tif currentPosition.Pos > uint32(p.cfg.StopPosition) {\n\t\t\tstopMsg = \"超出指定位置\"\n\t\t\treturnValue = 1\n\t\t} else if currentPosition.Pos == uint32(p.cfg.StopPosition) {\n\t\t\tstopMsg = \"超出指定位置\"\n\t\t\treturnValue = 0\n\t\t}\n\t}\n\n\tif p.masterStatus != nil {\n\t\tif currentPosition.Name > p.masterStatus.File ||\n\t\t\tcurrentPosition.Name == p.masterStatus.File &&\n\t\t\t\tcurrentPosition.Pos > uint32(p.masterStatus.Position) {\n\t\t\tstopMsg = \"超出最新binlog位置\"\n\t\t\treturnValue = 1\n\t\t} else if currentPosition.Name == p.masterStatus.File &&\n\t\t\tcurrentPosition.Pos == uint32(p.masterStatus.Position) {\n\t\t\tstopMsg = \"超出最新binlog位置\"\n\t\t\treturnValue = 0\n\t\t}\n\t}\n\n\tif stopMsg != \"\" {\n\t\tlog.WithFields(log.Fields{\n\t\t\t\"当前文件\": currentPosition.Name,\n\t\t\t\"结束文件\": p.stopFile,\n\t\t\t\"当前位置\": currentPosition.Pos,\n\t\t\t\"结束位置\": p.cfg.StopPosition,\n\t\t}).Info(stopMsg)\n\t}\n\treturn returnValue\n}\n\n// clear 压缩完成或者没有数据时删除文件\nfunc (p *BinlogParserStats) clear() {\n\terr := os.Remove(p.outputFileName)\n\tif err != nil {\n\t\tlog.Error(err)\n\t\tlog.Errorf(\"删除文件失败! %s\", p.outputFileName)\n\t}\n}\n\nfunc (p *BinlogParserStats) Stop() {\n\tlog.Warn(\"Process killed\")\n\tif p.cancelFn != nil {\n\t\tp.cancelFn()\n\t}\n\tp.running = false\n}\n\nfunc (p *BinlogParserStats) writeString(str string) {\n\tif len(p.outputFileName) > 0 {\n\t\tp.bufferWriter.WriteString(str)\n\t} else {\n\t\tfmt.Print(str)\n\t}\n}\n\n// NewBinlogParserStats binlog解析器\nfunc NewBinlogParserStats(cfg *BinlogParserConfig) (*BinlogParserStats, error) {\n\n\tp := new(BinlogParserStats)\n\n\tp.allTables = make(map[uint64]*Table)\n\n\tcfg.beginTime = time.Now().Unix()\n\tp.cfg = cfg\n\n\tp.OnlyDatabases = make(map[string]bool)\n\t// [table_name] = db_name\n\tp.OnlyTables = make(map[string]string)\n\tp.SqlTypes = make(map[string]bool)\n\n\tp.startFile = cfg.StartFile\n\tp.stopFile = cfg.StopFile\n\n\tif cfg.OutputFileStr != \"\" {\n\t\tp.outputFileName = cfg.OutputFileStr\n\t} else {\n\t\tp.outputFile = os.Stdout\n\t}\n\n\t// 本地解析模式,host为空,表名为SQL文件\n\tif p.cfg.Host == \"\" {\n\t\tp.localMode = true\n\t}\n\n\tif !p.localMode {\n\t\tvar err error\n\t\tp.db, err = p.getDB()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tdefer p.db.Close()\n\n\t\tp.masterStatus, err = p.mysqlMasterStatus()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tif p.cfg.Debug {\n\t\t\tp.db.LogMode(true)\n\t\t}\n\t}\n\n\tif err := p.parserInit(); err != nil {\n\t\treturn nil, err\n\t}\n\n\tp.ctx, p.cancelFn = context.WithCancel(context.Background())\n\n\tp.stats = SummaryStats{}\n\tp.statsTables = make(map[string]*TableStats)\n\treturn p, nil\n}\n\n// parserInit 解析服务初始化\nfunc (p *BinlogParserStats) parserInit() error {\n\n\tdefer timeTrack(time.Now(), \"parserInit\")\n\n\tif len(p.outputFileName) > 0 {\n\t\tvar err error\n\t\tif Exists(p.outputFileName) {\n\t\t\t// p.checkError(errors.New(\"指定的文件已存在!\"))\n\t\t\t// 不作追加,改为文件存在时清除内容,O_APPEND\n\t\t\tp.outputFile, err = os.OpenFile(p.outputFileName,\n\t\t\t\tos.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0666)\n\t\t} else {\n\t\t\tp.outputFile, err = os.Create(p.outputFileName)\n\t\t}\n\t\tp.checkError(err)\n\n\t\t// outputFile, err := os.OpenFile(outputFileStr, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0666)\n\t\t// if err != nil {\n\t\t//  log.Info(index, \"open file failed.\", err.Error())\n\t\t//  break\n\t\t// }\n\t\tdefer p.outputFile.Close()\n\n\t\tp.bufferWriter = bufio.NewWriter(p.outputFile)\n\t}\n\n\tif p.cfg.StartTime != \"\" {\n\t\tt, err := now.Parse(p.cfg.StartTime)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tp.startTimestamp = uint32(t.Unix())\n\t}\n\tif p.cfg.StopTime != \"\" {\n\t\tt, err := now.Parse(p.cfg.StopTime)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tp.stopTimestamp = uint32(t.Unix())\n\t}\n\n\t// 如果未指定开始文件,就自动解析\n\tif len(p.startFile) == 0 {\n\n\t\tbinlogs := p.autoParseBinlogPosition()\n\t\tif len(binlogs) == 0 {\n\t\t\tp.checkError(errors.New(\"无法获取master binlog\"))\n\t\t}\n\n\t\tp.startFile = binlogs[0].Name\n\n\t\tif p.startTimestamp > 0 || p.stopTimestamp > 0 {\n\t\t\tfor _, masterLog := range binlogs {\n\t\t\t\ttimestamp, err := p.getBinlogFirstTimestamp(masterLog.Name)\n\t\t\t\tp.checkError(err)\n\n\t\t\t\tlog.WithFields(log.Fields{\n\t\t\t\t\t\"起始时间\":       time.Unix(int64(timestamp), 0).Format(TimeFormat),\n\t\t\t\t\t\"binlogFile\": masterLog.Name,\n\t\t\t\t}).Info(\"binlog信息\")\n\n\t\t\t\tif timestamp <= p.startTimestamp {\n\t\t\t\t\tp.startFile = masterLog.Name\n\t\t\t\t}\n\n\t\t\t\tif p.stopFile == \"\" && p.stopTimestamp > 0 && timestamp > p.stopTimestamp {\n\t\t\t\t\tp.stopFile = masterLog.Name\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif len(p.startFile) == 0 {\n\t\t\tp.checkError(errors.New(\"未能解析指定时间段的binlog文件,请检查后重试!\"))\n\t\t}\n\n\t\tlog.Infof(\"根据指定的时间段,解析出的开始binlog文件是:%s,结束文件是:%s\\n\",\n\t\t\tp.startFile, p.stopFile)\n\t}\n\n\t// // 未指定结束文件时,仅解析该文件\n\t// if len(p.stopFile) == 0 {\n\t// \tp.stopFile = p.startFile\n\t// }\n\n\tif len(p.cfg.SqlType) > 0 {\n\t\tfor _, s := range strings.Split(p.cfg.SqlType, \",\") {\n\t\t\tp.SqlTypes[s] = true\n\t\t}\n\t} else {\n\t\tp.SqlTypes[\"insert\"] = true\n\t\tp.SqlTypes[\"update\"] = true\n\t\tp.SqlTypes[\"delete\"] = true\n\t}\n\n\tif len(p.cfg.Databases) > 0 {\n\t\tfor _, db := range strings.Split(p.cfg.Databases, \",\") {\n\t\t\tdb = strings.ToLower(strings.Trim(db, \" `\"))\n\t\t\tp.OnlyDatabases[db] = true\n\t\t}\n\t}\n\n\tif len(p.cfg.Tables) > 0 {\n\t\tfor _, s := range strings.Split(p.cfg.Tables, \",\") {\n\t\t\tif strings.Contains(s, \".\") {\n\t\t\t\tnames := strings.SplitN(s, \".\", 2)\n\t\t\t\tdb, table := names[0], names[1]\n\t\t\t\tdb = strings.ToLower(strings.Trim(db, \" `\"))\n\t\t\t\ttable = strings.ToLower(strings.Trim(table, \" `\"))\n\t\t\t\tp.OnlyTables[table] = db\n\t\t\t} else {\n\t\t\t\tkey := strings.ToLower(strings.Trim(s, \" `\"))\n\t\t\t\tp.OnlyTables[key] = \"\"\n\t\t\t}\n\n\t\t}\n\t}\n\n\t// log.Infof(\"dbs: %#v\", p.OnlyDatabases)\n\t// log.Infof(\"tbls: %#v\", p.OnlyTables)\n\n\treturn nil\n}\n\n// getBinlogFirstTimestamp 获取binlog文件第一个时间戳\nfunc (p *BinlogParserStats) getBinlogFirstTimestamp(file string) (uint32, error) {\n\n\tlogLevel := log.GetLevel()\n\tdefer func() {\n\t\tlog.SetLevel(logLevel)\n\t}()\n\t// 临时调整日志级别,忽略close sync异常\n\tlog.SetLevel(log.FatalLevel)\n\n\tcfg := replication.BinlogSyncerConfig{\n\t\tServerID: 2000000110,\n\t\tFlavor:   p.cfg.Flavor,\n\n\t\tHost:     p.cfg.Host,\n\t\tPort:     p.cfg.Port,\n\t\tUser:     p.cfg.User,\n\t\tPassword: p.cfg.Password,\n\t\t// RawModeEnabled:  p.cfg.RawMode,\n\t\t// SemiSyncEnabled: p.cfg.SemiSync,\n\t}\n\n\tb := replication.NewBinlogSyncer(cfg)\n\n\tpos := mysql.Position{Name: file,\n\t\tPos: uint32(4)}\n\n\ts, err := b.StartSync(pos)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\tdefer func() {\n\t\tb.Close()\n\t}()\n\tfor {\n\t\t// return\n\t\te, err := s.GetEvent(context.Background())\n\t\tif err != nil {\n\t\t\t// b.Close()\n\t\t\treturn 0, err\n\t\t}\n\n\t\t// log.Infof(\"事件类型:%s\", e.Header.EventType)\n\t\tif e.Header.Timestamp > 0 {\n\t\t\t// b.Close()\n\t\t\treturn e.Header.Timestamp, nil\n\t\t}\n\t}\n}\n\nfunc (p *BinlogParserStats) checkError(e error) {\n\tif e != nil {\n\t\tlog.Error(e)\n\t\tpanic(e)\n\t}\n}\n\nfunc (p *BinlogParserStats) mysqlMasterStatus() (*MasterStatus, error) {\n\n\tdefer timeTrack(time.Now(), \"mysqlMasterStatus\")\n\n\tr := MasterStatus{}\n\n\tp.db.Raw(\"SHOW MASTER STATUS\").Scan(&r)\n\n\treturn &r, nil\n}\n\nfunc (p *BinlogParserStats) autoParseBinlogPosition() []MasterLog {\n\n\tif p.binlogs != nil {\n\t\treturn p.binlogs\n\t}\n\tvar binlogIndex []MasterLog\n\tp.db.Raw(\"SHOW MASTER LOGS\").Scan(&binlogIndex)\n\n\tp.binlogs = binlogIndex\n\treturn binlogIndex\n}\n\nfunc getDBTableKey(db, table string) string {\n\treturn fmt.Sprintf(\"`%s`.`%s`\", strings.ToLower(db), strings.ToLower(table))\n}\n\nfunc (p *BinlogParserStats) parseSingleEvent(e *replication.BinlogEvent) (ok bool, err error) {\n\t// 是否继续,默认为true\n\tok = true\n\tfinishFlag := -1\n\n\tif e.Header.LogPos > 0 {\n\t\tp.currentPosition.Pos = e.Header.LogPos\n\t}\n\n\tif e.Header.EventType == replication.ROTATE_EVENT {\n\t\tif event, ok := e.Event.(*replication.RotateEvent); ok {\n\t\t\tp.currentPosition = mysql.Position{\n\t\t\t\tName: string(event.NextLogName),\n\t\t\t\tPos:  uint32(event.Position)}\n\t\t}\n\t}\n\n\tif e.Header.Timestamp > 0 {\n\t\tif p.startTimestamp > 0 && e.Header.Timestamp < p.startTimestamp {\n\t\t\treturn\n\t\t}\n\n\t\tif p.stats.StartTime.IsZero() {\n\t\t\tp.stats.StartTime = time.Unix(int64(e.Header.Timestamp), 0)\n\t\t}\n\t\tp.stats.EndTime = time.Unix(int64(e.Header.Timestamp), 0)\n\n\t\tif p.stopTimestamp > 0 && e.Header.Timestamp > p.stopTimestamp {\n\t\t\tlog.Warn(\"已超出结束时间\")\n\t\t\treturn false, nil\n\t\t}\n\t}\n\n\tp.currentTimtstamp = e.Header.Timestamp\n\n\tfinishFlag = p.checkFinish(&p.currentPosition)\n\tif finishFlag == 1 {\n\t\tlog.Warn(\"is finish\")\n\t\treturn false, nil\n\t}\n\n\tswitch event := e.Event.(type) {\n\tcase *replication.RowsEvent:\n\t\tkey := getDBTableKey(string(event.Table.Schema), string(event.Table.Table))\n\t\tif _, ok := p.statsTables[key]; !ok {\n\t\t\tp.statsTables[key] = &TableStats{\n\t\t\t\tDB:    string(event.Table.Schema),\n\t\t\t\tTable: string(event.Table.Table),\n\t\t\t}\n\t\t}\n\t\tcurrentTable := p.statsTables[key]\n\n\t\tswitch e.Header.EventType {\n\t\tcase replication.WRITE_ROWS_EVENTv1, replication.WRITE_ROWS_EVENTv2:\n\t\t\tcurrentTable.Inserts += len(event.Rows)\n\t\t\tcurrentTable.Total += len(event.Rows)\n\t\tcase replication.DELETE_ROWS_EVENTv1, replication.DELETE_ROWS_EVENTv2:\n\t\t\tcurrentTable.Deletes += len(event.Rows)\n\t\t\tcurrentTable.Total += len(event.Rows)\n\t\tcase replication.UPDATE_ROWS_EVENTv1, replication.UPDATE_ROWS_EVENTv2:\n\t\t\tcurrentTable.Updates += len(event.Rows) / 2\n\t\t\tcurrentTable.Total += len(event.Rows)\n\t\t}\n\t}\n\n\t// 再次判断是否到结束位置,以免处在临界值,且无新日志时程序卡住\n\tif e.Header.Timestamp > 0 {\n\t\tif p.stopTimestamp > 0 && e.Header.Timestamp > p.stopTimestamp {\n\t\t\tlog.Warn(\"已超出结束时间\")\n\t\t\treturn false, nil\n\t\t}\n\t}\n\n\tif !p.running {\n\t\tlog.Warn(\"进程手动中止\")\n\t\treturn false, nil\n\t}\n\n\tif finishFlag > -1 {\n\t\treturn false, nil\n\t}\n\n\treturn\n}\n\nfunc (p *BinlogParserStats) parserFile() error {\n\tdefer timeTrack(time.Now(), \"parserFile\")\n\n\tdefer func() {\n\t\tif p.outputFile != nil {\n\t\t\tp.outputFile.Close()\n\t\t}\n\t}()\n\n\tvar err error\n\n\tp.running = true\n\n\tif len(p.outputFileName) > 0 {\n\t\tif Exists(p.outputFileName) {\n\t\t\t// p.checkError(errors.New(\"指定的文件已存在!\"))\n\t\t\t// 不作追加,改为文件存在时清除内容,O_APPEND\n\t\t\tp.outputFile, err = os.OpenFile(p.outputFileName,\n\t\t\t\tos.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0666)\n\t\t} else {\n\t\t\tp.outputFile, err = os.Create(p.outputFileName)\n\t\t}\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tp.bufferWriter = bufio.NewWriter(p.outputFile)\n\t}\n\n\tb := replication.NewBinlogParser()\n\tb.SetUseDecimal(true)\n\n\tp.currentPosition = mysql.Position{\n\t\tName: p.startFile,\n\t\tPos:  uint32(p.cfg.StartPosition),\n\t}\n\n\tf := func(e *replication.BinlogEvent) error {\n\n\t\tok, err := p.parseSingleEvent(e)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif !ok {\n\t\t\tb.Stop()\n\t\t}\n\n\t\t// 再次判断是否到结束位置,以免处在临界值,且无新日志时程序卡住\n\t\tif e.Header.Timestamp > 0 {\n\t\t\tif p.stopTimestamp > 0 && e.Header.Timestamp >= p.stopTimestamp {\n\t\t\t\tlog.Warn(\"已超出结束时间\")\n\n\t\t\t\tb.Stop()\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\n\t\treturn nil\n\t}\n\n\terr = b.ParseFile(p.startFile, int64(p.cfg.StartPosition), f)\n\n\tif err != nil {\n\t\tprintln(err.Error())\n\t}\n\n\tp.stats.Tables = make([]TableStats, 0, len(p.statsTables))\n\tfor _, t := range p.statsTables {\n\t\tp.stats.Inserts += t.Inserts\n\t\tp.stats.Updates += t.Updates\n\t\tp.stats.Deletes += t.Deletes\n\t\tp.stats.Total += t.Total\n\t\tp.stats.Tables = append(p.stats.Tables, *t)\n\t}\n\n\tp.writeString(p.stats.String())\n\n\treturn nil\n}\n"
  },
  {
    "path": "core/parser_test.go",
    "content": "package core_test\n\nimport (\n\t\"bufio\"\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"io\"\n\t\"io/ioutil\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-mysql-org/go-mysql/client\"\n\t\"github.com/go-mysql-org/go-mysql/mysql\"\n\t_ \"github.com/go-sql-driver/mysql\"\n\t\"github.com/hanchuanchuan/bingo2sql/core\"\n\t. \"github.com/pingcap/check\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\n// Use docker mysql to test, mysql is 3306, mariadb is 3316\nvar testHost = flag.String(\"host\", \"127.0.0.1\", \"MySQL master host\")\n\nvar testOutputLogs = flag.Bool(\"out\", false, \"output binlog event\")\n\nvar allTables = map[string]string{\n\t\"test_json_v2\": `CREATE TABLE IF NOT EXISTS test_json_v2 (\n\t\t\tid INT,\n\t\t\tc JSON,\n\t\t\tPRIMARY KEY (id)\n\t\t\t) ENGINE=InnoDB`,\n\t\"test_replication\": `CREATE TABLE IF NOT EXISTS test_replication (\n\t\t\t\tid BIGINT(64) UNSIGNED  NOT NULL AUTO_INCREMENT,\n\t\t\t\tstr VARCHAR(256),\n\t\t\t\tf FLOAT,\n\t\t\t\td DOUBLE,\n\t\t\t\tde DECIMAL(10,2),\n\t\t\t\ti INT,\n\t\t\t\tbi BIGINT,\n\t\t\t\te enum (\"e1\", \"e2\"),\n\t\t\t\tb BIT(8),\n\t\t\t\ty YEAR,\n\t\t\t\tda DATE,\n\t\t\t\tts TIMESTAMP,\n\t\t\t\tdt DATETIME,\n\t\t\t\ttm TIME,\n\t\t\t\tt TEXT,\n\t\t\t\tbb BLOB,\n\t\t\t\tse SET('a', 'b', 'c'),\n\t\t\t\tPRIMARY KEY (id)\n\t\t\t) ENGINE=InnoDB DEFAULT CHARSET=utf8`,\n\t\"test_json\": `CREATE TABLE IF NOT EXISTS test_json (\n\t\t\tid BIGINT(64) UNSIGNED  NOT NULL AUTO_INCREMENT,\n\t\t\tc1 JSON,\n\t\t\tc2 DECIMAL(10, 0),\n\t\t\tPRIMARY KEY (id)\n\t\t\t) ENGINE=InnoDB`,\n\t\"test_geo\": `CREATE TABLE IF NOT EXISTS test_geo (id int auto_increment primary key, g GEOMETRY)`,\n\t\"test_parse_time\": `CREATE TABLE IF NOT EXISTS test_parse_time (\n\t\tid int auto_increment primary key,\n\t\ta1 DATETIME,\n\t\ta2 DATETIME(3),\n\t\ta3 DATETIME(6),\n\t\tb1 TIMESTAMP,\n\t\tb2 TIMESTAMP(3) ,\n\t\tb3 TIMESTAMP(6))`,\n\t\"test_simple\": `CREATE TABLE IF NOT EXISTS test_simple (\n\t\t\tid BIGINT(64) UNSIGNED  NOT NULL AUTO_INCREMENT,\n\t\t\tc1 varchar(100),\n\t\t\tc2 int,\n\t\t\tPRIMARY KEY (id)\n\t\t\t) ENGINE=InnoDB`,\n\t\"test_long_text\": `CREATE TABLE IF NOT EXISTS test_long_text (\n\t\t\tid BIGINT(64) UNSIGNED  NOT NULL AUTO_INCREMENT,\n\t\t\tc1 longtext,\n\t\t\tPRIMARY KEY (id)\n\t\t\t) ENGINE=InnoDB`,\n\t\"test_generated\": `CREATE TABLE IF NOT EXISTS test_generated (\n\t\t\tid int primary key,\n\t\t\tprice int,\n\t\t\tnumber int,\n\t\t\ttotal int generated always as (price*number))`,\n}\n\nvar (\n\t// 解析binlog生成的SQL文件\n\tbinlogOutputFile string = \"binlog_output.sql\"\n\t// 本地方式解析binlog生成的SQL文件\n\tlocalOutputFile string = \"binlog_output_local.sql\"\n\n\t// 表结构文件. 用于本地解析\n\ttableSchemaFile string = \"table_schema.sql\"\n)\nvar (\n\tdefaultConfig core.BinlogParserConfig\n\tlocalConfig   core.BinlogParserConfig\n)\nvar _ = Suite(&testParserSuite{})\n\nfunc TestBinLogSyncer(t *testing.T) {\n\tTestingT(t)\n}\n\ntype testParserSuite struct {\n\tc *client.Conn\n\n\tflavor string\n\n\tconfig      core.BinlogParserConfig // 远程解析\n\tlocalConfig core.BinlogParserConfig // 本地解析\n}\n\nfunc (t *testParserSuite) SetUpSuite(c *C) {\n\tdefaultConfig = core.BinlogParserConfig{\n\t\tHost:     *testHost,\n\t\tPort:     3306,\n\t\tUser:     \"test\",\n\t\tPassword: \"test\",\n\n\t\tStartFile: \"mysql-bin.000001\",\n\n\t\tDatabases:     \"test\",\n\t\tOutputFileStr: binlogOutputFile,\n\t}\n\n\tlocalConfig = core.BinlogParserConfig{\n\t\t// 通过setBinlogDir自动获取\n\t\t// StartFile: \"mysql-bin.000001\",\n\n\t\tDatabases:     \"test\",\n\t\tTables:        tableSchemaFile,\n\t\tOutputFileStr: localOutputFile,\n\t}\n\n\tt.config = defaultConfig\n\tt.localConfig = localConfig\n\n\tt.initTableSchema()\n\tt.setBinlogDir(c)\n\n\tt.createTables(c)\n\n\tlog.SetLevel(log.InfoLevel)\n}\n\nfunc (t *testParserSuite) TearDownSuite(c *C) {\n\tos.Remove(binlogOutputFile)\n\tos.Remove(localOutputFile)\n\tos.Remove(tableSchemaFile)\n}\n\nfunc (t *testParserSuite) SetUpTest(c *C) {\n}\n\nfunc (t *testParserSuite) TearDownTest(c *C) {\n\tt.reset()\n\n\t// if t.b != nil {\n\t// \tt.b.Close()\n\t// \tt.b = nil\n\t// }\n\n\tif t.c != nil {\n\t\tt.c.Close()\n\t\tt.c = nil\n\t}\n}\n\nfunc (t *testParserSuite) testExecute(c *C, query ...string) {\n\tfor _, q := range query {\n\t\t_, err := t.c.Execute(q)\n\t\tc.Assert(err, IsNil)\n\t}\n}\n\nfunc (t *testParserSuite) setBinlogDir(c *C) {\n\tif t.c == nil {\n\t\tt.setupTest(c, mysql.MySQLFlavor)\n\t}\n\n\tresult, err := t.c.Execute(\"show variables like 'log_bin_basename'\")\n\tc.Assert(err, IsNil)\n\n\tbasename, err := result.GetString(0, 1)\n\tc.Assert(err, IsNil)\n\n\tbasename = strings.Replace(basename, \"/data/mysql\", \"/Users/hanchuanchuan\", 1)\n\n\tt.localConfig.StartFile = fmt.Sprintf(\"%s.000001\", basename)\n\tlocalConfig.StartFile = fmt.Sprintf(\"%s.000001\", basename)\n\tlog.Infof(\"开始本地日志文件:%s\", localConfig.StartFile)\n\n\tresult, err = t.c.Execute(\"show master logs\")\n\tc.Assert(err, IsNil)\n\n\tbasename, err = result.GetString(0, 0)\n\tc.Assert(err, IsNil)\n\tt.config.StartFile = basename\n\tdefaultConfig.StartFile = basename\n\tlog.Infof(\"开始日志文件:%s\", basename)\n\n}\n\nfunc (t *testParserSuite) SetFlashback(v bool) {\n\tt.config.Flashback = v\n\tt.localConfig.Flashback = v\n}\n\nfunc (t *testParserSuite) SetMinimalUpdate(v bool) {\n\tt.config.MinimalUpdate = v\n\tt.localConfig.MinimalUpdate = v\n}\n\nfunc (t *testParserSuite) SetRemovePrimary(v bool) {\n\tt.config.RemovePrimary = v\n\tt.localConfig.RemovePrimary = v\n}\n\nfunc (t *testParserSuite) SetSQLType(v string) {\n\tt.config.SqlType = v\n\tt.localConfig.SqlType = v\n}\n\nfunc (t *testParserSuite) SetIncludeGtids(v string) {\n\tt.config.IncludeGtids = v\n\tt.localConfig.IncludeGtids = v\n}\n\nfunc (t *testParserSuite) setupTest(c *C, flavor string) {\n\tvar port uint16 = 3306\n\tswitch flavor {\n\tcase mysql.MariaDBFlavor:\n\t\tport = 3316\n\t}\n\n\tt.flavor = flavor\n\n\tvar err error\n\tif t.c != nil {\n\t\tt.c.Close()\n\t}\n\n\t// db, err := sql.Open(sqlType, \"user:password@tcp(127.0.0.1:3306)/database?multiStatements=true\")\n\t// if err != nil {\n\t// \tlog.Fatalf(\"open mysql err: %s\", err)\n\t// }\n\n\tt.c, err = client.Connect(fmt.Sprintf(\"%s:%d\", *testHost, port), \"test\", \"test\", \"\")\n\tc.Assert(err, IsNil)\n\n\t// _, err = t.c.Execute(\"CREATE DATABASE IF NOT EXISTS test\")\n\t// c.Assert(err, IsNil)\n\n\t_, err = t.c.Execute(\"USE test\")\n\tc.Assert(err, IsNil)\n\n\t_, err = t.c.Execute(\"set binlog_format = 'row'\")\n\tc.Assert(err, IsNil)\n}\n\nfunc (t *testParserSuite) getThreadID(c *C) uint32 {\n\tresult, err := t.c.Execute(\"select connection_id()\")\n\tc.Assert(err, IsNil)\n\n\tthreadID, err := result.GetInt(0, 0)\n\tc.Assert(err, IsNil)\n\t// log.Errorf(\"%#v\", threadID)\n\treturn uint32(threadID)\n}\n\nfunc (t *testParserSuite) getServerUUID(c *C) string {\n\tresult, err := t.c.Execute(\"show variables like 'server_uuid'\")\n\tc.Assert(err, IsNil)\n\n\tuuid, err := result.GetString(0, 1)\n\tc.Assert(err, IsNil)\n\tlog.Infof(\"server uuid: %#v\", uuid)\n\n\tresult, err = t.c.Execute(\"show master status;\")\n\tc.Assert(err, IsNil)\n\tfile, _ := result.GetString(0, 0)\n\tgtidSet, _ := result.GetString(0, 4)\n\tlog.Infof(\"binlog: %s, gtid set: %s\", file, gtidSet)\n\n\treturn uuid\n}\n\n// func (t *testParserSuite) testPositionSync(c *C) {\n// \t//get current master binlog file and position\n// \tr, err := t.c.Execute(\"SHOW MASTER STATUS\")\n// \tc.Assert(err, IsNil)\n// \tbinFile, _ := r.GetString(0, 0)\n// \tbinPos, _ := r.GetInt(0, 1)\n\n// \ts, err := t.b.StartSync(mysql.Position{Name: binFile, Pos: uint32(binPos)})\n// \tc.Assert(err, IsNil)\n\n// \t// Test re-sync.\n// \ttime.Sleep(100 * time.Millisecond)\n// \tt.b.c.SetReadDeadline(time.Now().Add(time.Millisecond))\n// \ttime.Sleep(100 * time.Millisecond)\n\n// \tt.testSync(c, s)\n// }\n\n// func (t *testParserSuite) TestMysqlPositionSync(c *C) {\n// \tt.setupTest(c, mysql.MySQLFlavor)\n// \tt.testPositionSync(c)\n// }\n\n// func (t *testParserSuite) TestMysqlGTIDSync(c *C) {\n// \tt.setupTest(c, mysql.MySQLFlavor)\n\n// \tr, err := t.c.Execute(\"SELECT @@gtid_mode\")\n// \tc.Assert(err, IsNil)\n// \tmodeOn, _ := r.GetString(0, 0)\n// \tif modeOn != \"ON\" {\n// \t\tc.Skip(\"GTID mode is not ON\")\n// \t}\n\n// \tr, err = t.c.Execute(\"SHOW GLOBAL VARIABLES LIKE 'SERVER_UUID'\")\n// \tc.Assert(err, IsNil)\n\n// \tvar masterUuid uuid.UUID\n// \tif s, _ := r.GetString(0, 1); len(s) > 0 && s != \"NONE\" {\n// \t\tmasterUuid, err = uuid.FromString(s)\n// \t\tc.Assert(err, IsNil)\n// \t}\n\n// \tset, _ := mysql.ParseMysqlGTIDSet(fmt.Sprintf(\"%s:%d-%d\", masterUuid.String(), 1, 2))\n\n// \ts, err := t.b.StartSyncGTID(set)\n// \tc.Assert(err, IsNil)\n\n// \tt.testSync(c, s)\n// }\n\n// func (t *testParserSuite) TestMariadbPositionSync(c *C) {\n// \tt.setupTest(c, mysql.MariaDBFlavor)\n\n// \tt.testPositionSync(c)\n// }\n\n// func (t *testParserSuite) TestMariadbGTIDSync(c *C) {\n// \tt.setupTest(c, mysql.MariaDBFlavor)\n\n// \t// get current master gtid binlog pos\n// \tr, err := t.c.Execute(\"SELECT @@gtid_binlog_pos\")\n// \tc.Assert(err, IsNil)\n\n// \tstr, _ := r.GetString(0, 0)\n// \tset, _ := mysql.ParseMariadbGTIDSet(str)\n\n// \ts, err := t.b.StartSyncGTID(set)\n// \tc.Assert(err, IsNil)\n\n// \tt.testSync(c, s)\n// }\n\n// func (t *testParserSuite) TestMariadbAnnotateRows(c *C) {\n// \tt.setupTest(c, mysql.MariaDBFlavor)\n// \tt.b.cfg.DumpCommandFlag = BINLOG_SEND_ANNOTATE_ROWS_EVENT\n// \tt.testPositionSync(c)\n// }\n\n// func (t *testParserSuite) TestMysqlSemiPositionSync(c *C) {\n// \tt.setupTest(c, mysql.MySQLFlavor)\n\n// \tt.b.cfg.SemiSyncEnabled = true\n\n// \tt.testPositionSync(c)\n// }\n\n// func (t *testParserSuite) TestMysqlBinlogCodec(c *C) {\n// \tt.setupTest(c, mysql.MySQLFlavor)\n\n// \tt.testExecute(c, \"RESET MASTER\")\n\n// \tvar wg sync.WaitGroup\n// \twg.Add(1)\n// \tdefer wg.Wait()\n\n// \tgo func() {\n// \t\tdefer wg.Done()\n\n// \t\tt.testSync(c, nil)\n\n// \t\tt.testExecute(c, \"FLUSH LOGS\")\n\n// \t\tt.testSync(c, nil)\n// \t}()\n\n// \tbinlogDir := \"./var\"\n\n// \tos.RemoveAll(binlogDir)\n\n// \terr := t.b.StartBackup(binlogDir, mysql.Position{Name: \"\", Pos: uint32(0)}, 2*time.Second)\n// \tc.Assert(err, IsNil)\n\n// \tp := NewBinlogParser()\n// \tp.SetVerifyChecksum(true)\n\n// \tf := func(e *BinlogEvent) error {\n// \t\tif *testOutputLogs {\n// \t\t\te.Dump(os.Stdout)\n// \t\t\tos.Stdout.Sync()\n// \t\t}\n// \t\treturn nil\n// \t}\n\n// \tdir, err := os.Open(binlogDir)\n// \tc.Assert(err, IsNil)\n// \tdefer dir.Close()\n\n// \tfiles, err := dir.Readdirnames(-1)\n// \tc.Assert(err, IsNil)\n\n// \tfor _, file := range files {\n// \t\terr = p.ParseFile(path.Join(binlogDir, file), 0, f)\n// \t\tc.Assert(err, IsNil)\n// \t}\n// }\n\nfunc (t *testParserSuite) checkBinlog(c *C, sqls ...string) {\n\tbinlogs := t.getBinlog(c)\n\tc.Assert(len(binlogs), Equals, len(sqls), Commentf(\"%#v\", binlogs))\n\tfor i, line := range binlogs {\n\t\tc.Assert(line, Equals, sqls[i], Commentf(\"%#v\", binlogs))\n\t}\n}\n\nfunc (t *testParserSuite) getBinlog(c *C) []string {\n\t// 在线方式解析\n\tresultOnline := t.getBinlogWithConfig(c, &t.config)\n\n\t// 判断本地文件是否存在\n\tif _, err := os.Stat(t.localConfig.StartFile); err == nil {\n\t\t// if runtime.GOOS == \"linux\" {\n\t\t// 本地解析\n\t\tresultLocal := t.getBinlogWithConfig(c, &t.localConfig)\n\n\t\tc.Assert(len(resultOnline), Equals, len(resultLocal), Commentf(\"local: %#v, online: %#v\", resultLocal, resultOnline))\n\t\tfor i, line := range resultOnline {\n\t\t\tc.Assert(line, Equals, resultLocal[i], Commentf(\"local: %#v, online: %#v\", resultLocal[i], resultOnline))\n\t\t}\n\t} else {\n\t\tlog.Warnf(\"跳过本地文件解析! 本地文件不存在:%s\", t.localConfig.StartFile)\n\t}\n\n\treturn resultOnline\n}\n\n// getBinlogWithConfig 根据配置文件\nfunc (t *testParserSuite) getBinlogWithConfig(c *C, config *core.BinlogParserConfig) []string {\n\n\tp, err := core.NewBinlogParser(context.Background(), config)\n\tc.Assert(err, IsNil)\n\n\terr = p.Parser()\n\tc.Assert(err, IsNil)\n\n\tfileObj, err := os.Open(config.OutputFileStr)\n\tc.Assert(err, IsNil)\n\n\tdefer fileObj.Close()\n\t//一个文件对象本身是实现了io.Reader的 使用bufio.NewReader去初始化一个Reader对象，存在buffer中的，读取一次就会被清空\n\treader := bufio.NewReader(fileObj)\n\n\tvar buf []string\n\n\tfor {\n\t\tline, err := reader.ReadString('\\n')\n\t\tif err != nil {\n\t\t\tif err != io.EOF {\n\t\t\t\tc.Assert(err, IsNil)\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\tbreak\n\t\t}\n\n\t\tline = strings.TrimSpace(line)\n\n\t\tif strings.HasPrefix(line, \"# \") || line == \"\" {\n\t\t\tcontinue\n\t\t}\n\n\t\tif strings.Contains(line, \"# \") {\n\t\t\tline = line[0:strings.Index(line, \"# \")]\n\t\t}\n\n\t\tbuf = append(buf, strings.TrimSpace(line))\n\t}\n\treturn buf\n}\n\nfunc (t *testParserSuite) reset() {\n\tt.config = defaultConfig\n\tt.localConfig = localConfig\n\t// log.SetLevel(log.ErrorLevel)\n}\n\nfunc (t *testParserSuite) TestSync(c *C) {\n\tt.setupTest(c, mysql.MySQLFlavor)\n\n\tt.testExecute(c, `RESET MASTER;`,\n\t\t// \"SET SESSION binlog_format = 'MIXED'\",\n\t\t`DROP TABLE IF EXISTS test_replication`,\n\t\t`CREATE TABLE test_replication (\n\t\t\tid BIGINT(64) UNSIGNED  NOT NULL AUTO_INCREMENT,\n\t\t\tstr VARCHAR(256),\n\t\t\tf FLOAT,\n\t\t\td DOUBLE,\n\t\t\tde DECIMAL(10,2),\n\t\t\ti INT,\n\t\t\tbi BIGINT,\n\t\t\te enum (\"e1\", \"e2\"),\n\t\t\tb BIT(8),\n\t\t\ty YEAR,\n\t\t\tda DATE,\n\t\t\tts TIMESTAMP,\n\t\t\tdt DATETIME,\n\t\t\ttm TIME,\n\t\t\tt TEXT,\n\t\t\tbb BLOB,\n\t\t\tse SET('a', 'b', 'c'),\n\t\t\tPRIMARY KEY (id)\n\t\t) ENGINE=InnoDB DEFAULT CHARSET=utf8`)\n\n\t//use row format\n\tt.testExecute(c,\n\t\t`INSERT INTO test_replication (str, f, i, e, b, y, da, ts, dt, tm, de, t, bb, se)\n\t\tVALUES (\"3\", -3.14, 10, \"e1\", 0b0011, 1985,\n\t\t\"2012-05-07\", \"2012-05-07 14:01:01\", \"2012-05-07 14:01:01\",\n\t\t\"14:01:01\", -45363.64, \"abc\", \"12345\", \"a,b\")`)\n\n\tt.checkBinlog(c, \"INSERT INTO `test`.`test_replication`(`id`,`str`,`f`,`d`,`de`,`i`,`bi`,`e`,`b`,`y`,`da`,`ts`,`dt`,`tm`,`t`,`bb`,`se`) VALUES(1,'3',-3.14,NULL,-45363.64,10,NULL,1,3,1985,'2012-05-07','2012-05-07 14:01:01','2012-05-07 14:01:01','14:01:01','abc','12345',3);\")\n\n\tt.checkBinlog(c, \"INSERT INTO `test`.`test_replication`(`id`,`str`,`f`,`d`,`de`,`i`,`bi`,`e`,`b`,`y`,`da`,`ts`,`dt`,`tm`,`t`,`bb`,`se`) VALUES(1,'3',-3.14,NULL,-45363.64,10,NULL,1,3,1985,'2012-05-07','2012-05-07 14:01:01','2012-05-07 14:01:01','14:01:01','abc','12345',3);\")\n\n\tt.SetFlashback(true)\n\tt.checkBinlog(c, \"DELETE FROM `test`.`test_replication` WHERE `id`=1;\")\n\n\tt.SetFlashback(false)\n\tt.SetRemovePrimary(true)\n\tt.checkBinlog(c, \"INSERT INTO `test`.`test_replication`(`str`,`f`,`d`,`de`,`i`,`bi`,`e`,`b`,`y`,`da`,`ts`,`dt`,`tm`,`t`,`bb`,`se`) VALUES('3',-3.14,NULL,-45363.64,10,NULL,1,3,1985,'2012-05-07','2012-05-07 14:01:01','2012-05-07 14:01:01','14:01:01','abc','12345',3);\")\n\n}\n\nfunc (t *testParserSuite) TestParseDDL(c *C) {\n\tt.setupTest(c, mysql.MySQLFlavor)\n\n\tt.testExecute(c, `RESET MASTER;`,\n\t\t`DROP TABLE IF EXISTS test_replication`,\n\t\t`CREATE TABLE test_replication (\n\t\t\tid BIGINT(64) UNSIGNED  NOT NULL AUTO_INCREMENT,\n\t\t\tstr VARCHAR(256),\n\t\t\tf FLOAT,\n\t\t\td DOUBLE,\n\t\t\tde DECIMAL(10,2),\n\t\t\ti INT,\n\t\t\tbi BIGINT,\n\t\t\te enum (\"e1\", \"e2\"),\n\t\t\tb BIT(8),\n\t\t\ty YEAR,\n\t\t\tda DATE,\n\t\t\tts TIMESTAMP,\n\t\t\tdt DATETIME,\n\t\t\ttm TIME,\n\t\t\tt TEXT,\n\t\t\tbb BLOB,\n\t\t\tse SET('a', 'b', 'c'),\n\t\t\tPRIMARY KEY (id)\n\t\t) ENGINE=InnoDB DEFAULT CHARSET=utf8`)\n\n\t//use row format\n\tt.testExecute(c,\n\t\t`INSERT INTO test_replication (str, f, i, e, b, y, da, ts, dt, tm, de, t, bb, se)\n\t\tVALUES (\"3\", -3.14, 10, \"e1\", 0b0011, 1985,\n\t\t\"2012-05-07\", \"2012-05-07 14:01:01\", \"2012-05-07 14:01:01\",\n\t\t\"14:01:01\", -45363.64, \"abc\", \"12345\", \"a,b\")`)\n\n\tt.config.ParseDDL = true\n\tt.localConfig.ParseDDL = true\n\n\tt.checkBinlog(c,\n\t\t\"USE `test`;\",\n\t\t\"DROP TABLE IF EXISTS `test_replication` /* generated by server */;\",\n\t\t\"USE `test`;\",\n\t\t\"CREATE TABLE test_replication (\",\n\t\t\"id BIGINT(64) UNSIGNED  NOT NULL AUTO_INCREMENT,\",\n\t\t\"str VARCHAR(256),\",\n\t\t\"f FLOAT,\", \"d DOUBLE,\",\n\t\t\"de DECIMAL(10,2),\", \"i INT,\",\n\t\t\"bi BIGINT,\", \"e enum (\\\"e1\\\", \\\"e2\\\"),\",\n\t\t\"b BIT(8),\", \"y YEAR,\", \"da DATE,\",\n\t\t\"ts TIMESTAMP,\", \"dt DATETIME,\",\n\t\t\"tm TIME,\", \"t TEXT,\", \"bb BLOB,\",\n\t\t\"se SET('a', 'b', 'c'),\", \"PRIMARY KEY (id)\",\n\t\t\") ENGINE=InnoDB DEFAULT CHARSET=utf8;\",\n\t\t\"INSERT INTO `test`.`test_replication`(`id`,`str`,`f`,`d`,`de`,`i`,`bi`,`e`,`b`,`y`,`da`,`ts`,`dt`,`tm`,`t`,`bb`,`se`) VALUES(1,'3',-3.14,NULL,-45363.64,10,NULL,1,3,1985,'2012-05-07','2012-05-07 14:01:01','2012-05-07 14:01:01','14:01:01','abc','12345',3);\",\n\t)\n\n\tt.SetFlashback(true)\n\tt.checkBinlog(c,\n\t\t\"DELETE FROM `test`.`test_replication` WHERE `id`=1;\",\n\t)\n\n}\n\nfunc (t *testParserSuite) TestStopTime(c *C) {\n\tt.setupTest(c, mysql.MySQLFlavor)\n\n\tt.testExecute(c, `RESET MASTER;`,\n\t\t`DROP TABLE IF EXISTS test_replication`,\n\t\t`CREATE TABLE test_replication (\n\t\t\tid BIGINT(64) UNSIGNED  NOT NULL AUTO_INCREMENT,\n\t\t\tstr VARCHAR(256),\n\t\t\tf FLOAT,\n\t\t\td DOUBLE,\n\t\t\tde DECIMAL(10,2),\n\t\t\ti INT,\n\t\t\tbi BIGINT,\n\t\t\te enum (\"e1\", \"e2\"),\n\t\t\tb BIT(8),\n\t\t\ty YEAR,\n\t\t\tda DATE,\n\t\t\tts TIMESTAMP,\n\t\t\tdt DATETIME,\n\t\t\ttm TIME,\n\t\t\tt TEXT,\n\t\t\tbb BLOB,\n\t\t\tse SET('a', 'b', 'c'),\n\t\t\tPRIMARY KEY (id)\n\t\t) ENGINE=InnoDB DEFAULT CHARSET=utf8`)\n\n\t//use row format\n\tt.testExecute(c,\n\t\t`INSERT INTO test_replication (str, f, i, e, b, y, da, ts, dt, tm, de, t, bb, se)\n\t\tVALUES (\"3\", -3.14, 10, \"e1\", 0b0011, 1985,\n\t\t\"2012-05-07\", \"2012-05-07 14:01:01\", \"2012-05-07 14:01:01\",\n\t\t\"14:01:01\", -45363.64, \"abc\", \"12345\", \"a,b\")`)\n\n\tt.config.StartTime = time.Now().Add(-10 * time.Minute).Format(\"2006-01-02 15:04\")\n\tt.localConfig.StartTime = t.config.StartTime\n\tt.checkBinlog(c, \"INSERT INTO `test`.`test_replication`(`id`,`str`,`f`,`d`,`de`,`i`,`bi`,`e`,`b`,`y`,`da`,`ts`,`dt`,`tm`,`t`,`bb`,`se`) VALUES(1,'3',-3.14,NULL,-45363.64,10,NULL,1,3,1985,'2012-05-07','2012-05-07 14:01:01','2012-05-07 14:01:01','14:01:01','abc','12345',3);\")\n\n\tt.config.StopTime = time.Now().Add(-5 * time.Minute).Format(\"2006-01-02 15:04\")\n\tt.localConfig.StopTime = t.config.StopTime\n\tt.SetFlashback(true)\n\t// 时间限制范围内无数据\n\tt.checkBinlog(c)\n\n\tt.config.StopTime = time.Now().Add(time.Minute).Format(\"2006-01-02 15:04:05\")\n\tt.localConfig.StopTime = t.config.StopTime\n\tt.checkBinlog(c, \"DELETE FROM `test`.`test_replication` WHERE `id`=1;\")\n\n\tt.SetFlashback(false)\n\tt.SetRemovePrimary(true)\n\tt.checkBinlog(c, \"INSERT INTO `test`.`test_replication`(`str`,`f`,`d`,`de`,`i`,`bi`,`e`,`b`,`y`,`da`,`ts`,`dt`,`tm`,`t`,`bb`,`se`) VALUES('3',-3.14,NULL,-45363.64,10,NULL,1,3,1985,'2012-05-07','2012-05-07 14:01:01','2012-05-07 14:01:01','14:01:01','abc','12345',3);\")\n\n}\nfunc (t *testParserSuite) TestGeometry(c *C) {\n\tt.setupTest(c, mysql.MySQLFlavor)\n\n\tt.testExecute(c,\n\t\t\"RESET MASTER\",\n\t\t\"DROP TABLE IF EXISTS test_geo\",\n\t\t`CREATE TABLE test_geo (id int auto_increment primary key, g GEOMETRY)`,\n\t)\n\n\ttbls := []string{\n\t\t`INSERT INTO test_geo(g) VALUES (POINT(1, 1))`,\n\t\t`INSERT INTO test_geo(g) VALUES (LINESTRING(POINT(0,0), POINT(1,1), POINT(2,2)))`,\n\t\t`DELETE from test_geo where id>0`,\n\t}\n\n\tt.testExecute(c, tbls...)\n\n\tt.SetFlashback(true)\n\tt.checkBinlog(c,\n\t\t\"DELETE FROM `test`.`test_geo` WHERE `id`=1;\",\n\t\t\"DELETE FROM `test`.`test_geo` WHERE `id`=2;\",\n\t\t\"INSERT INTO `test`.`test_geo`(`id`,`g`) VALUES(1,'\\\\0\\\\0\\\\0\\\\0\\x01\\x01\\\\0\\\\0\\\\0\\\\0\\\\0\\\\0\\\\0\\\\0\\\\0\\xf0?\\\\0\\\\0\\\\0\\\\0\\\\0\\\\0\\xf0?');\",\n\t\t\"INSERT INTO `test`.`test_geo`(`id`,`g`) VALUES(2,'\\\\0\\\\0\\\\0\\\\0\\x01\\x02\\\\0\\\\0\\\\0\\x03\\\\0\\\\0\\\\0\\\\0\\\\0\\\\0\\\\0\\\\0\\\\0\\\\0\\\\0\\\\0\\\\0\\\\0\\\\0\\\\0\\\\0\\\\0\\\\0\\\\0\\\\0\\\\0\\\\0\\\\0\\\\0\\xf0?\\\\0\\\\0\\\\0\\\\0\\\\0\\\\0\\xf0?\\\\0\\\\0\\\\0\\\\0\\\\0\\\\0\\\\0@\\\\0\\\\0\\\\0\\\\0\\\\0\\\\0\\\\0@');\",\n\t)\n}\n\nfunc (t *testParserSuite) TestDatetime(c *C) {\n\tt.setupTest(c, mysql.MySQLFlavor)\n\n\tt.testExecute(c,\n\t\t\"RESET MASTER\",\n\t\t`SET sql_mode=''`,\n\t\t`DROP TABLE IF EXISTS test_parse_time`,\n\t\t`CREATE TABLE test_parse_time (\n\t\t\tid int auto_increment primary key,\n\t\t\ta1 DATETIME,\n\t\t\ta2 DATETIME(3),\n\t\t\ta3 DATETIME(6),\n\t\t\tb1 TIMESTAMP,\n\t\t\tb2 TIMESTAMP(3) ,\n\t\t\tb3 TIMESTAMP(6))`,\n\t)\n\n\tt.testExecute(c, `INSERT INTO test_parse_time(a1,a2,a3,b1,b2,b3) VALUES\n\t\t(\"2014-09-08 17:51:04.123456\", \"2014-09-08 17:51:04.123456\", \"2014-09-08 17:51:04.123456\",\n\t\t\"2014-09-08 17:51:04.123456\",\"2014-09-08 17:51:04.123456\",\"2014-09-08 17:51:04.123456\"),\n\t\t(\"0000-00-00 00:00:00.000000\", \"0000-00-00 00:00:00.000000\", \"0000-00-00 00:00:00.000000\",\n\t\t\"0000-00-00 00:00:00.000000\", \"0000-00-00 00:00:00.000000\", \"0000-00-00 00:00:00.000000\"),\n\t\t(\"2014-09-08 17:51:04.000456\", \"2014-09-08 17:51:04.000456\", \"2014-09-08 17:51:04.000456\",\n\t\t\"2014-09-08 17:51:04.000456\",\"2014-09-08 17:51:04.000456\",\"2014-09-08 17:51:04.000456\")`,\n\t\t`delete from test_parse_time where id > 0`)\n\n\tt.SetFlashback(true)\n\tt.checkBinlog(c,\n\t\t\"DELETE FROM `test`.`test_parse_time` WHERE `id`=1;\",\n\t\t\"DELETE FROM `test`.`test_parse_time` WHERE `id`=2;\",\n\t\t\"DELETE FROM `test`.`test_parse_time` WHERE `id`=3;\",\n\t\t\"INSERT INTO `test`.`test_parse_time`(`id`,`a1`,`a2`,`a3`,`b1`,`b2`,`b3`) VALUES(1,'2014-09-08 17:51:04','2014-09-08 17:51:04.123','2014-09-08 17:51:04.123456','2014-09-08 17:51:04','2014-09-08 17:51:04.123','2014-09-08 17:51:04.123456');\",\n\t\t\"INSERT INTO `test`.`test_parse_time`(`id`,`a1`,`a2`,`a3`,`b1`,`b2`,`b3`) VALUES(2,'0000-00-00 00:00:00','0000-00-00 00:00:00.000','0000-00-00 00:00:00.000000','0000-00-00 00:00:00','0000-00-00 00:00:00.000','0000-00-00 00:00:00.000000');\",\n\t\t\"INSERT INTO `test`.`test_parse_time`(`id`,`a1`,`a2`,`a3`,`b1`,`b2`,`b3`) VALUES(3,'2014-09-08 17:51:04','2014-09-08 17:51:04.000','2014-09-08 17:51:04.000456','2014-09-08 17:51:04','2014-09-08 17:51:04.000','2014-09-08 17:51:04.000456');\",\n\t)\n}\n\nfunc (t *testParserSuite) TestBinlogRowImageMinimal(c *C) {\n\tt.setupTest(c, mysql.MySQLFlavor)\n\n\tid := 100\n\tt.testExecute(c, `RESET MASTER;`,\n\t\t\"SET SESSION binlog_row_image = 'MINIMAL'\",\n\t\tfmt.Sprintf(`INSERT INTO test_replication (id, str, f, i, bb, de) VALUES (%d, \"4\", -3.14, 100, \"abc\", -45635.64)`, id),\n\t\tfmt.Sprintf(`UPDATE test_replication SET f = -12.14, de = 555.34 WHERE id = %d`, id),\n\t\tfmt.Sprintf(`DELETE FROM test_replication WHERE id = %d`, id))\n\n\tt.SetSQLType(\"update\")\n\n\tt.checkBinlog(c,\n\t\t\"UPDATE `test`.`test_replication` SET `id`=NULL, `str`=NULL, `f`=-12.14, `d`=NULL, `de`=555.34, `i`=NULL, `bi`=NULL, `e`=NULL, `b`=NULL, `y`=NULL, `da`=NULL, `ts`=NULL, `dt`=NULL, `tm`=NULL, `t`=NULL, `bb`=NULL, `se`=NULL WHERE `id`=100;\")\n\n\tt.SetFlashback(true)\n\tt.SetMinimalUpdate(true)\n\tt.checkBinlog(c,\n\t\t\"UPDATE `test`.`test_replication` SET `id`=100, `f`=NULL, `de`=NULL WHERE `id` IS NULL;\",\n\t)\n\n\tt.SetFlashback(false)\n\tt.checkBinlog(c,\n\t\t\"UPDATE `test`.`test_replication` SET `id`=NULL, `f`=-12.14, `de`=555.34 WHERE `id`=100;\",\n\t)\n\n\tt.testExecute(c, \"SET SESSION binlog_row_image = 'FULL'\")\n\n}\n\nfunc (t *testParserSuite) TestMinimalUpdate(c *C) {\n\tt.setupTest(c, mysql.MySQLFlavor)\n\n\tid := 100\n\tt.testExecute(c, `RESET MASTER;`,\n\t\tfmt.Sprintf(`INSERT INTO test_replication (id, str, f, i, bb, de) VALUES (%d, \"4\", -3.14, 100, \"abc\", -45635.64)`, id),\n\t\tfmt.Sprintf(`UPDATE test_replication SET f = -12.14, de = 555.34 WHERE id = %d`, id),\n\t\tfmt.Sprintf(`UPDATE test_replication SET str=null WHERE id = %d`, id),\n\t\tfmt.Sprintf(`DELETE FROM test_replication WHERE id = %d`, id))\n\n\tt.SetSQLType(\"update\")\n\n\tt.checkBinlog(c,\n\t\t\"UPDATE `test`.`test_replication` SET `id`=100, `str`='4', `f`=-12.14, `d`=NULL, `de`=555.34, `i`=100, `bi`=NULL, `e`=NULL, `b`=NULL, `y`=NULL, `da`=NULL, `ts`=NULL, `dt`=NULL, `tm`=NULL, `t`=NULL, `bb`='abc', `se`=NULL WHERE `id`=100;\",\n\t\t\"UPDATE `test`.`test_replication` SET `id`=100, `str`=NULL, `f`=-12.14, `d`=NULL, `de`=555.34, `i`=100, `bi`=NULL, `e`=NULL, `b`=NULL, `y`=NULL, `da`=NULL, `ts`=NULL, `dt`=NULL, `tm`=NULL, `t`=NULL, `bb`='abc', `se`=NULL WHERE `id`=100;\")\n\n\tt.SetFlashback(true)\n\tt.SetMinimalUpdate(true)\n\tt.checkBinlog(c,\n\t\t\"UPDATE `test`.`test_replication` SET `f`=-3.14, `de`=-45635.64 WHERE `id`=100;\",\n\t\t\"UPDATE `test`.`test_replication` SET `str`='4' WHERE `id`=100;\",\n\t)\n\n\tt.SetFlashback(false)\n\tt.checkBinlog(c,\n\t\t\"UPDATE `test`.`test_replication` SET `f`=-12.14, `de`=555.34 WHERE `id`=100;\",\n\t\t\"UPDATE `test`.`test_replication` SET `str`=NULL WHERE `id`=100;\",\n\t)\n}\n\nfunc (t *testParserSuite) TestFieldGenerated(c *C) {\n\tt.setupTest(c, mysql.MySQLFlavor)\n\n\tid := 1\n\tt.testExecute(c, `RESET MASTER;`,\n\t\t`INSERT INTO test_generated(id,price,number) VALUES (1,1,1),(2,3,4)`,\n\t\tfmt.Sprintf(`UPDATE test_generated SET price = 10, number = 20 WHERE id = %d`, id),\n\t\tfmt.Sprintf(`delete from test_generated WHERE id = %d`, id),\n\t)\n\n\t// t.SetSQLType(\"insert\")\n\tt.SetSQLType(\"insert,update,delete\")\n\n\tt.checkBinlog(c,\n\t\t\"INSERT INTO `test`.`test_generated`(`id`,`price`,`number`) VALUES(1,1,1);\", \"INSERT INTO `test`.`test_generated`(`id`,`price`,`number`) VALUES(2,3,4);\", \"UPDATE `test`.`test_generated` SET `id`=1, `price`=10, `number`=20 WHERE `id`=1;\",\n\t\t\"DELETE FROM `test`.`test_generated` WHERE `id`=1;\",\n\t)\n\n\tt.SetFlashback(true)\n\tt.checkBinlog(c,\n\t\t\"DELETE FROM `test`.`test_generated` WHERE `id`=1;\",\n\t\t\"DELETE FROM `test`.`test_generated` WHERE `id`=2;\",\n\t\t\"UPDATE `test`.`test_generated` SET `id`=1, `price`=1, `number`=1 WHERE `id`=1;\",\n\t\t\"INSERT INTO `test`.`test_generated`(`id`,`price`,`number`) VALUES(1,10,20);\",\n\t)\n}\n\nfunc (t *testParserSuite) TestTextMax(c *C) {\n\tt.setupTest(c, mysql.MySQLFlavor)\n\n\tid := 100\n\n\t// 65536 = 64k\n\tvalue := strings.Repeat(\"a\", 1024*1024*10)\n\n\tt.testExecute(c, `RESET MASTER;`,\n\t\t`DROP TABLE IF EXISTS test_long_text`,\n\t\t`CREATE TABLE test_long_text (\n\t\t\t\tid BIGINT(64) UNSIGNED  NOT NULL AUTO_INCREMENT,\n\t\t\t\tc1 longtext,\n\t\t\t\tPRIMARY KEY (id)\n\t\t\t\t) ENGINE=InnoDB`,\n\t\tfmt.Sprintf(`INSERT INTO test_long_text (id, c1) VALUES (%d, \"\")`, id),\n\t\tfmt.Sprintf(`UPDATE test_long_text SET c1 = '%s' WHERE id = %d`, value, id),\n\t\tfmt.Sprintf(`DELETE FROM test_long_text WHERE id = %d`, id))\n\n\tt.checkBinlog(c,\n\t\t\"INSERT INTO `test`.`test_long_text`(`id`,`c1`) VALUES(100,'');\",\n\t\tfmt.Sprintf(\"UPDATE `test`.`test_long_text` SET `id`=100, `c1`='%s' WHERE `id`=100;\", value),\n\t\t\"DELETE FROM `test`.`test_long_text` WHERE `id`=100;\",\n\t)\n\n\tt.SetFlashback(true)\n\tt.SetMinimalUpdate(true)\n\tt.checkBinlog(c,\n\t\t\"DELETE FROM `test`.`test_long_text` WHERE `id`=100;\",\n\t\t\"UPDATE `test`.`test_long_text` SET `c1`='' WHERE `id`=100;\",\n\t\tfmt.Sprintf(\"INSERT INTO `test`.`test_long_text`(`id`,`c1`) VALUES(100,'%s');\", value),\n\t)\n\n}\n\nfunc (t *testParserSuite) TestUpdate2Null(c *C) {\n\tt.setupTest(c, mysql.MySQLFlavor)\n\n\tid := 100\n\tt.testExecute(c, `RESET MASTER;`,\n\t\tfmt.Sprintf(`INSERT INTO test_replication (id,str, f, i, e, b, y, da, ts, dt, tm, de, t, bb, se)\n\t\tVALUES (%d,\"3\", -3.14, 10, \"e1\", 0b0011, 1985,\n\t\t\"2012-05-07\", \"2012-05-07 14:01:01\", \"2012-05-07 14:01:01\",\n\t\t\"14:01:01\", -45363.64, \"abc\", \"12345\", \"a,b\")`, id),\n\t\tfmt.Sprintf(`UPDATE test_replication SET  str = null,f = null,d = null,de = null,i = null,bi = null,e = null,b = null,y = null,da = null,ts = null,dt = null,tm = null,t = null,bb = null WHERE id = %d`, id),\n\t\tfmt.Sprintf(`DELETE FROM test_replication WHERE id = %d`, id))\n\n\tt.SetSQLType(\"update\")\n\n\tt.checkBinlog(c,\n\t\t\"UPDATE `test`.`test_replication` SET `id`=100, `str`=NULL, `f`=NULL, `d`=NULL, `de`=NULL, `i`=NULL, `bi`=NULL, `e`=NULL, `b`=NULL, `y`=NULL, `da`=NULL, `ts`=NULL, `dt`=NULL, `tm`=NULL, `t`=NULL, `bb`=NULL, `se`=3 WHERE `id`=100;\")\n\n\tt.SetFlashback(true)\n\tt.SetMinimalUpdate(true)\n\tt.checkBinlog(c,\n\t\t\"UPDATE `test`.`test_replication` SET `str`='3', `f`=-3.14, `de`=-45363.64, `i`=10, `e`=1, `b`=3, `y`=1985, `da`='2012-05-07', `ts`='2012-05-07 14:01:01', `dt`='2012-05-07 14:01:01', `tm`='14:01:01', `t`='abc', `bb`='12345' WHERE `id`=100;\",\n\t)\n\n\tt.SetFlashback(false)\n\tt.checkBinlog(c,\n\t\t\"UPDATE `test`.`test_replication` SET `str`=NULL, `f`=NULL, `de`=NULL, `i`=NULL, `e`=NULL, `b`=NULL, `y`=NULL, `da`=NULL, `ts`=NULL, `dt`=NULL, `tm`=NULL, `t`=NULL, `bb`=NULL WHERE `id`=100;\",\n\t)\n\n}\n\nfunc (t *testParserSuite) TestRemovePrimary(c *C) {\n\tt.setupTest(c, mysql.MySQLFlavor)\n\n\tid := 100\n\tt.testExecute(c, `RESET MASTER;`,\n\t\tfmt.Sprintf(`INSERT INTO test_replication (id, str, f, i, bb, de) VALUES (%d, \"4\", -3.14, 100, \"abc\", -45635.64)`, id),\n\t\tfmt.Sprintf(`DELETE FROM test_replication WHERE id = %d`, id))\n\n\tt.checkBinlog(c, \"INSERT INTO `test`.`test_replication`(`id`,`str`,`f`,`d`,`de`,`i`,`bi`,`e`,`b`,`y`,`da`,`ts`,`dt`,`tm`,`t`,`bb`,`se`) VALUES(100,'4',-3.14,NULL,-45635.64,100,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,'abc',NULL);\",\n\t\t\"DELETE FROM `test`.`test_replication` WHERE `id`=100;\")\n\n\tt.SetFlashback(true)\n\tt.checkBinlog(c,\n\t\t\"DELETE FROM `test`.`test_replication` WHERE `id`=100;\",\n\t\t\"INSERT INTO `test`.`test_replication`(`id`,`str`,`f`,`d`,`de`,`i`,`bi`,`e`,`b`,`y`,`da`,`ts`,`dt`,`tm`,`t`,`bb`,`se`) VALUES(100,'4',-3.14,NULL,-45635.64,100,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,'abc',NULL);\",\n\t)\n\n\tt.SetFlashback(false)\n\tt.SetRemovePrimary(true)\n\tt.checkBinlog(c,\n\t\t\"INSERT INTO `test`.`test_replication`(`str`,`f`,`d`,`de`,`i`,`bi`,`e`,`b`,`y`,`da`,`ts`,`dt`,`tm`,`t`,`bb`,`se`) VALUES('4',-3.14,NULL,-45635.64,100,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,'abc',NULL);\",\n\t\t\"DELETE FROM `test`.`test_replication` WHERE `id`=100;\")\n\n}\n\n// TestThreadID 测试指定线程号,操作后断开重连\nfunc (t *testParserSuite) TestThreadID(c *C) {\n\tt.setupTest(c, mysql.MySQLFlavor)\n\n\tt.testExecute(c, `RESET MASTER;`,\n\t\t`DROP TABLE IF EXISTS test_simple`,\n\t\t`DROP TABLE IF EXISTS test_simple`,\n\t\t`CREATE TABLE test_simple (\n\t\t\t\tid BIGINT(64) UNSIGNED  NOT NULL AUTO_INCREMENT,\n\t\t\t\tc1 varchar(100),\n\t\t\t\tc2 int,\n\t\t\t\tPRIMARY KEY (id)\n\t\t\t\t) ENGINE=InnoDB`)\n\n\tt.testExecute(c,\n\t\t`INSERT INTO test_simple (c1, c2) VALUES ('test',1)`,\n\t\t`DELETE FROM test_simple WHERE id > 0`)\n\n\tthreadID := t.getThreadID(c)\n\n\tif t.c != nil {\n\t\tt.c.Close()\n\t\tt.c = nil\n\t}\n\n\tt.setupTest(c, mysql.MySQLFlavor)\n\n\tt.testExecute(c,\n\t\t`INSERT INTO test_simple (c1, c2) VALUES ('test2',2)`,\n\t\t`DELETE FROM test_simple WHERE id > 0`)\n\n\t// 未限制线程号时返回所有数据\n\tt.checkBinlog(c,\n\t\t\"INSERT INTO `test`.`test_simple`(`id`,`c1`,`c2`) VALUES(1,'test',1);\",\n\t\t\"DELETE FROM `test`.`test_simple` WHERE `id`=1;\",\n\t\t\"INSERT INTO `test`.`test_simple`(`id`,`c1`,`c2`) VALUES(2,'test2',2);\",\n\t\t\"DELETE FROM `test`.`test_simple` WHERE `id`=2;\")\n\n\tt.config.ThreadID = threadID\n\tt.localConfig.ThreadID = t.config.ThreadID\n\tt.checkBinlog(c,\n\t\t\"INSERT INTO `test`.`test_simple`(`id`,`c1`,`c2`) VALUES(1,'test',1);\",\n\t\t\"DELETE FROM `test`.`test_simple` WHERE `id`=1;\")\n\n\tt.SetFlashback(true)\n\tt.checkBinlog(c,\n\t\t\"DELETE FROM `test`.`test_simple` WHERE `id`=1;\",\n\t\t\"INSERT INTO `test`.`test_simple`(`id`,`c1`,`c2`) VALUES(1,'test',1);\",\n\t)\n}\n\n// TestInsert 测试insert\nfunc (t *testParserSuite) TestInsert(c *C) {\n\tt.setupTest(c, mysql.MySQLFlavor)\n\n\tt.testExecute(c, `RESET MASTER;`,\n\t\t`DROP TABLE IF EXISTS test_simple`,\n\t\t`CREATE TABLE test_simple (\n\t\t\t\tid BIGINT(64) UNSIGNED  NOT NULL AUTO_INCREMENT,\n\t\t\t\tc1 varchar(100),\n\t\t\t\tc2 int,\n\t\t\t\tPRIMARY KEY (id)\n\t\t\t\t) ENGINE=InnoDB`)\n\n\tt.testExecute(c,\n\t\t`INSERT INTO test_simple (c1, c2) VALUES ('a1',1),('a2',2),('a3',3),('a4',4),('a5',5)`,\n\t\t`DELETE FROM test_simple WHERE id > 0`)\n\n\tt.config.MinimalInsert = false\n\tt.localConfig.MinimalInsert = false\n\n\tt.checkBinlog(c,\n\t\t\"INSERT INTO `test`.`test_simple`(`id`,`c1`,`c2`) VALUES(1,'a1',1);\",\n\t\t\"INSERT INTO `test`.`test_simple`(`id`,`c1`,`c2`) VALUES(2,'a2',2);\",\n\t\t\"INSERT INTO `test`.`test_simple`(`id`,`c1`,`c2`) VALUES(3,'a3',3);\",\n\t\t\"INSERT INTO `test`.`test_simple`(`id`,`c1`,`c2`) VALUES(4,'a4',4);\",\n\t\t\"INSERT INTO `test`.`test_simple`(`id`,`c1`,`c2`) VALUES(5,'a5',5);\",\n\t\t\"DELETE FROM `test`.`test_simple` WHERE `id`=1;\",\n\t\t\"DELETE FROM `test`.`test_simple` WHERE `id`=2;\",\n\t\t\"DELETE FROM `test`.`test_simple` WHERE `id`=3;\",\n\t\t\"DELETE FROM `test`.`test_simple` WHERE `id`=4;\",\n\t\t\"DELETE FROM `test`.`test_simple` WHERE `id`=5;\")\n\n\tt.config.MinimalInsert = true\n\tt.localConfig.MinimalInsert = true\n\tt.checkBinlog(c,\n\t\t\"INSERT INTO `test`.`test_simple`(`id`,`c1`,`c2`) VALUES(1,'a1',1),(2,'a2',2),(3,'a3',3),(4,'a4',4),(5,'a5',5);\",\n\t\t\"DELETE FROM `test`.`test_simple` WHERE `id`=1;\",\n\t\t\"DELETE FROM `test`.`test_simple` WHERE `id`=2;\",\n\t\t\"DELETE FROM `test`.`test_simple` WHERE `id`=3;\",\n\t\t\"DELETE FROM `test`.`test_simple` WHERE `id`=4;\",\n\t\t\"DELETE FROM `test`.`test_simple` WHERE `id`=5;\")\n\n\tt.SetFlashback(true)\n\tt.checkBinlog(c,\n\t\t\"DELETE FROM `test`.`test_simple` WHERE `id`=1;\",\n\t\t\"DELETE FROM `test`.`test_simple` WHERE `id`=2;\",\n\t\t\"DELETE FROM `test`.`test_simple` WHERE `id`=3;\",\n\t\t\"DELETE FROM `test`.`test_simple` WHERE `id`=4;\",\n\t\t\"DELETE FROM `test`.`test_simple` WHERE `id`=5;\",\n\t\t\"INSERT INTO `test`.`test_simple`(`id`,`c1`,`c2`) VALUES(1,'a1',1),(2,'a2',2),(3,'a3',3),(4,'a4',4),(5,'a5',5);\",\n\t)\n}\n\n// TestThreadID 测试指定线程号,操作后断开重连\nfunc (t *testParserSuite) TestGTID(c *C) {\n\tt.setupTest(c, mysql.MySQLFlavor)\n\n\tt.testExecute(c, `RESET MASTER;`,\n\t\t`DROP TABLE IF EXISTS test_simple`,\n\t\t`CREATE TABLE test_simple (\n\t\t\t\tid BIGINT(64) UNSIGNED  NOT NULL AUTO_INCREMENT,\n\t\t\t\tc1 varchar(100),\n\t\t\t\tc2 int,\n\t\t\t\tPRIMARY KEY (id)\n\t\t\t\t) ENGINE=InnoDB`)\n\n\tt.testExecute(c,\n\t\t`INSERT INTO test_simple (c1, c2) VALUES ('test',1)`,\n\t\t`DELETE FROM test_simple WHERE id > 0`)\n\n\tt.testExecute(c,\n\t\t`INSERT INTO test_simple (c1, c2) VALUES ('test2',2)`,\n\t\t`DELETE FROM test_simple WHERE id > 0`)\n\n\tuuid := t.getServerUUID(c)\n\n\t// ---- 错误GTID ----\n\n\tt.SetIncludeGtids(\"123\")\n\t_, err := core.NewBinlogParser(context.Background(), &t.config)\n\tc.Assert(err, NotNil)\n\tc.Assert(err.Error(), Equals, \"错误GTID格式!正确格式为uuid:编号[-编号],多个时以逗号分隔\")\n\n\tt.SetIncludeGtids(uuid)\n\t_, err = core.NewBinlogParser(context.Background(), &t.localConfig)\n\tc.Assert(err, NotNil)\n\tc.Assert(err.Error(), Equals, \"错误GTID格式!正确格式为uuid:编号[-编号],多个时以逗号分隔\")\n\n\tt.SetIncludeGtids(uuid + \":abc\")\n\t_, err = core.NewBinlogParser(context.Background(), &t.localConfig)\n\tc.Assert(err, NotNil)\n\tc.Assert(err.Error(), Equals, \"GTID解析失败!(strconv.ParseInt: parsing \\\"abc\\\": invalid syntax)\")\n\n\t// ------ end ------\n\n\tt.SetIncludeGtids(uuid + \":3\")\n\n\tt.checkBinlog(c,\n\t\t\"INSERT INTO `test`.`test_simple`(`id`,`c1`,`c2`) VALUES(1,'test',1);\")\n\n\tt.SetIncludeGtids(uuid + \":3-4\")\n\n\tt.checkBinlog(c,\n\t\t\"INSERT INTO `test`.`test_simple`(`id`,`c1`,`c2`) VALUES(1,'test',1);\",\n\t\t\"DELETE FROM `test`.`test_simple` WHERE `id`=1;\")\n\n\tt.SetIncludeGtids(uuid + \":5\")\n\tt.checkBinlog(c,\n\t\t\"INSERT INTO `test`.`test_simple`(`id`,`c1`,`c2`) VALUES(2,'test2',2);\")\n\n\tt.SetIncludeGtids(uuid + \":3-4,\" + uuid + \":6-7\")\n\n\tt.checkBinlog(c,\n\t\t\"INSERT INTO `test`.`test_simple`(`id`,`c1`,`c2`) VALUES(1,'test',1);\",\n\t\t\"DELETE FROM `test`.`test_simple` WHERE `id`=1;\",\n\t\t\"DELETE FROM `test`.`test_simple` WHERE `id`=2;\",\n\t)\n\n}\n\nfunc (t *testParserSuite) TestJson(c *C) {\n\tt.setupTest(c, mysql.MySQLFlavor)\n\n\tt.testExecute(c, `RESET MASTER;`,\n\t\t`DROP TABLE IF EXISTS test_json`,\n\t\t`CREATE TABLE test_json (\n\t\t\t\tid BIGINT(64) UNSIGNED  NOT NULL AUTO_INCREMENT,\n\t\t\t\tc1 JSON,\n\t\t\t\tc2 DECIMAL(10, 0),\n\t\t\t\tPRIMARY KEY (id)\n\t\t\t\t) ENGINE=InnoDB`)\n\n\tt.testExecute(c,\n\t\t`INSERT INTO test_json (c2) VALUES (1)`,\n\t\t`INSERT INTO test_json (c1, c2) VALUES ('{\"key1\": \"value1\", \"key2\": \"value2\"}', 1)`,\n\t\t`update test_json set c1 = '{\"key1\": \"value123\"}',c2=2000 where id =2`,\n\t\t`delete from test_json where id > 0`)\n\n\tt.SetFlashback(true)\n\tt.checkBinlog(c,\n\t\t\"DELETE FROM `test`.`test_json` WHERE `id`=1;\",\n\t\t\"DELETE FROM `test`.`test_json` WHERE `id`=2;\",\n\t\t\"UPDATE `test`.`test_json` SET `id`=2, `c1`='{\\\\\\\"key1\\\\\\\":\\\\\\\"value1\\\\\\\",\\\\\\\"key2\\\\\\\":\\\\\\\"value2\\\\\\\"}', `c2`=1 WHERE `id`=2;\",\n\t\t\"INSERT INTO `test`.`test_json`(`id`,`c1`,`c2`) VALUES(1,NULL,1);\",\n\t\t\"INSERT INTO `test`.`test_json`(`id`,`c1`,`c2`) VALUES(2,'{\\\\\\\"key1\\\\\\\":\\\\\\\"value123\\\\\\\"}',2000);\")\n}\n\nfunc (t *testParserSuite) TestJsonV2(c *C) {\n\tt.setupTest(c, mysql.MySQLFlavor)\n\n\tt.testExecute(c, `RESET MASTER;`,\n\t\t\"DROP TABLE IF EXISTS test_json_v2\",\n\t\t`CREATE TABLE test_json_v2 (\n\t\t\t\tid INT,\n\t\t\t\tc JSON,\n\t\t\t\tPRIMARY KEY (id)\n\t\t\t\t) ENGINE=InnoDB`)\n\n\ttbls := []string{\n\t\t`INSERT INTO test_json_v2 VALUES (0, NULL)`,\n\t\t`INSERT INTO test_json_v2 VALUES (1, '{\\\"a\\\": 2}')`,\n\t\t`INSERT INTO test_json_v2 VALUES (2, '[1,2]')`,\n\t\t`INSERT INTO test_json_v2 VALUES (3, '{\\\"a\\\":\\\"b\\\", \\\"c\\\":\\\"d\\\",\\\"ab\\\":\\\"abc\\\", \\\"bc\\\": [\\\"x\\\", \\\"y\\\"]}')`,\n\t\t`INSERT INTO test_json_v2 VALUES (4, '[\\\"here\\\", [\\\"I\\\", \\\"am\\\"], \\\"!!!\\\"]')`,\n\t\t`INSERT INTO test_json_v2 VALUES (5, '\\\"scalar string\\\"')`,\n\t\t`INSERT INTO test_json_v2 VALUES (6, 'true')`,\n\t\t`INSERT INTO test_json_v2 VALUES (7, 'false')`,\n\t\t`INSERT INTO test_json_v2 VALUES (8, 'null')`,\n\t\t`INSERT INTO test_json_v2 VALUES (9, '-1')`,\n\t\t`INSERT INTO test_json_v2 VALUES (10, CAST(CAST(1 AS UNSIGNED) AS JSON))`,\n\t\t`INSERT INTO test_json_v2 VALUES (11, '32767')`,\n\t\t`INSERT INTO test_json_v2 VALUES (12, '32768')`,\n\t\t`INSERT INTO test_json_v2 VALUES (13, '-32768')`,\n\t\t`INSERT INTO test_json_v2 VALUES (14, '-32769')`,\n\t\t`INSERT INTO test_json_v2 VALUES (15, '2147483647')`,\n\t\t`INSERT INTO test_json_v2 VALUES (16, '2147483648')`,\n\t\t`INSERT INTO test_json_v2 VALUES (17, '-2147483648')`,\n\t\t`INSERT INTO test_json_v2 VALUES (18, '-2147483649')`,\n\t\t`INSERT INTO test_json_v2 VALUES (19, '18446744073709551615')`,\n\t\t`INSERT INTO test_json_v2 VALUES (20, '18446744073709551616')`,\n\t\t`INSERT INTO test_json_v2 VALUES (21, '3.14')`,\n\t\t`INSERT INTO test_json_v2 VALUES (22, '{}')`,\n\t\t`INSERT INTO test_json_v2 VALUES (23, '[]')`,\n\t\t`INSERT INTO test_json_v2 VALUES (24, CAST(CAST('2015-01-15 23:24:25' AS DATETIME) AS JSON))`,\n\t\t`INSERT INTO test_json_v2 VALUES (25, CAST(CAST('23:24:25' AS TIME) AS JSON))`,\n\t\t`INSERT INTO test_json_v2 VALUES (125, CAST(CAST('23:24:25.12' AS TIME(3)) AS JSON))`,\n\t\t`INSERT INTO test_json_v2 VALUES (225, CAST(CAST('23:24:25.0237' AS TIME(3)) AS JSON))`,\n\t\t`INSERT INTO test_json_v2 VALUES (26, CAST(CAST('2015-01-15' AS DATE) AS JSON))`,\n\t\t`INSERT INTO test_json_v2 VALUES (27, CAST(TIMESTAMP'2015-01-15 23:24:25' AS JSON))`,\n\t\t`INSERT INTO test_json_v2 VALUES (127, CAST(TIMESTAMP'2015-01-15 23:24:25.12' AS JSON))`,\n\t\t`INSERT INTO test_json_v2 VALUES (227, CAST(TIMESTAMP'2015-01-15 23:24:25.0237' AS JSON))`,\n\t\t`INSERT INTO test_json_v2 VALUES (327, CAST(UNIX_TIMESTAMP('2015-01-15 23:24:25') AS JSON))`,\n\t\t`INSERT INTO test_json_v2 VALUES (28, CAST(ST_GeomFromText('POINT(1 1)') AS JSON))`,\n\t\t`INSERT INTO test_json_v2 VALUES (29, CAST('[]' AS CHAR CHARACTER SET 'ascii'))`,\n\t\t// TODO: 30 and 31 are BIT type from JSON_TYPE, may support later.\n\t\t`INSERT INTO test_json_v2 VALUES (30, CAST(x'cafe' AS JSON))`,\n\t\t`INSERT INTO test_json_v2 VALUES (31, CAST(x'cafebabe' AS JSON))`,\n\t\t`INSERT INTO test_json_v2 VALUES (100, CONCAT('{\\\"', REPEAT('a', 2 * 100 - 1), '\\\":123}'))`,\n\t}\n\n\tt.testExecute(c, tbls...)\n\n\tt.checkBinlog(c,\n\t\t\"INSERT INTO `test`.`test_json_v2`(`id`,`c`) VALUES(0,NULL);\",\n\t\t\"INSERT INTO `test`.`test_json_v2`(`id`,`c`) VALUES(1,'{\\\\\\\"a\\\\\\\":2}');\",\n\t\t\"INSERT INTO `test`.`test_json_v2`(`id`,`c`) VALUES(2,'[1,2]');\",\n\t\t\"INSERT INTO `test`.`test_json_v2`(`id`,`c`) VALUES(3,'{\\\\\\\"a\\\\\\\":\\\\\\\"b\\\\\\\",\\\\\\\"ab\\\\\\\":\\\\\\\"abc\\\\\\\",\\\\\\\"bc\\\\\\\":[\\\\\\\"x\\\\\\\",\\\\\\\"y\\\\\\\"],\\\\\\\"c\\\\\\\":\\\\\\\"d\\\\\\\"}');\",\n\t\t\"INSERT INTO `test`.`test_json_v2`(`id`,`c`) VALUES(4,'[\\\\\\\"here\\\\\\\",[\\\\\\\"I\\\\\\\",\\\\\\\"am\\\\\\\"],\\\\\\\"!!!\\\\\\\"]');\",\n\t\t\"INSERT INTO `test`.`test_json_v2`(`id`,`c`) VALUES(5,'\\\\\\\"scalar string\\\\\\\"');\",\n\t\t\"INSERT INTO `test`.`test_json_v2`(`id`,`c`) VALUES(6,'true');\",\n\t\t\"INSERT INTO `test`.`test_json_v2`(`id`,`c`) VALUES(7,'false');\",\n\t\t\"INSERT INTO `test`.`test_json_v2`(`id`,`c`) VALUES(8,'null');\",\n\t\t\"INSERT INTO `test`.`test_json_v2`(`id`,`c`) VALUES(9,'-1');\",\n\t\t\"INSERT INTO `test`.`test_json_v2`(`id`,`c`) VALUES(10,'1');\",\n\t\t\"INSERT INTO `test`.`test_json_v2`(`id`,`c`) VALUES(11,'32767');\",\n\t\t\"INSERT INTO `test`.`test_json_v2`(`id`,`c`) VALUES(12,'32768');\",\n\t\t\"INSERT INTO `test`.`test_json_v2`(`id`,`c`) VALUES(13,'-32768');\",\n\t\t\"INSERT INTO `test`.`test_json_v2`(`id`,`c`) VALUES(14,'-32769');\",\n\t\t\"INSERT INTO `test`.`test_json_v2`(`id`,`c`) VALUES(15,'2147483647');\",\n\t\t\"INSERT INTO `test`.`test_json_v2`(`id`,`c`) VALUES(16,'2147483648');\",\n\t\t\"INSERT INTO `test`.`test_json_v2`(`id`,`c`) VALUES(17,'-2147483648');\",\n\t\t\"INSERT INTO `test`.`test_json_v2`(`id`,`c`) VALUES(18,'-2147483649');\",\n\t\t\"INSERT INTO `test`.`test_json_v2`(`id`,`c`) VALUES(19,'18446744073709551615');\",\n\t\t\"INSERT INTO `test`.`test_json_v2`(`id`,`c`) VALUES(20,'18446744073709552000');\",\n\t\t\"INSERT INTO `test`.`test_json_v2`(`id`,`c`) VALUES(21,'3.14');\",\n\t\t\"INSERT INTO `test`.`test_json_v2`(`id`,`c`) VALUES(22,'{}');\",\n\t\t\"INSERT INTO `test`.`test_json_v2`(`id`,`c`) VALUES(23,'[]');\",\n\t\t\"INSERT INTO `test`.`test_json_v2`(`id`,`c`) VALUES(24,'\\\\\\\"2015-01-15 23:24:25.000000\\\\\\\"');\",\n\t\t\"INSERT INTO `test`.`test_json_v2`(`id`,`c`) VALUES(25,'\\\\\\\"23:24:25.000000\\\\\\\"');\",\n\t\t\"INSERT INTO `test`.`test_json_v2`(`id`,`c`) VALUES(125,'\\\\\\\"23:24:25.120000\\\\\\\"');\",\n\t\t\"INSERT INTO `test`.`test_json_v2`(`id`,`c`) VALUES(225,'\\\\\\\"23:24:25.024000\\\\\\\"');\",\n\t\t\"INSERT INTO `test`.`test_json_v2`(`id`,`c`) VALUES(26,'\\\\\\\"2015-01-15 00:00:00.000000\\\\\\\"');\",\n\t\t\"INSERT INTO `test`.`test_json_v2`(`id`,`c`) VALUES(27,'\\\\\\\"2015-01-15 23:24:25.000000\\\\\\\"');\",\n\t\t\"INSERT INTO `test`.`test_json_v2`(`id`,`c`) VALUES(127,'\\\\\\\"2015-01-15 23:24:25.120000\\\\\\\"');\",\n\t\t\"INSERT INTO `test`.`test_json_v2`(`id`,`c`) VALUES(227,'\\\\\\\"2015-01-15 23:24:25.023700\\\\\\\"');\",\n\t\t\"INSERT INTO `test`.`test_json_v2`(`id`,`c`) VALUES(327,'1421335465');\",\n\t\t\"INSERT INTO `test`.`test_json_v2`(`id`,`c`) VALUES(28,'{\\\\\\\"coordinates\\\\\\\":[1,1],\\\\\\\"type\\\\\\\":\\\\\\\"Point\\\\\\\"}');\",\n\t\t\"INSERT INTO `test`.`test_json_v2`(`id`,`c`) VALUES(29,'[]');\",\n\t\t\"INSERT INTO `test`.`test_json_v2`(`id`,`c`) VALUES(30,'\\\\\\\"\\\\\\\\ufffd\\\\\\\\ufffd\\\\\\\"');\",\n\t\t\"INSERT INTO `test`.`test_json_v2`(`id`,`c`) VALUES(31,'\\\\\\\"\\\\\\\\ufffd\\\\\\\\ufffd\\\\\\\\ufffd\\\\\\\\ufffd\\\\\\\"');\",\n\t\t\"INSERT INTO `test`.`test_json_v2`(`id`,`c`) VALUES(100,'{\\\\\\\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\\\\\\\":123}');\")\n\n\tt.testExecute(c, \"delete from test_json_v2 where id >0\")\n}\n\nfunc (t *testParserSuite) initTableSchema(tableName ...string) {\n\n\tvar tables []string\n\n\tif len(tableName) > 0 {\n\t\tfor _, name := range tableName {\n\t\t\tif v, ok := allTables[name]; ok {\n\t\t\t\ttables = append(tables, v)\n\t\t\t}\n\t\t}\n\t} else {\n\t\tfor _, value := range allTables {\n\t\t\ttables = append(tables, value)\n\t\t}\n\t}\n\n\terr := ioutil.WriteFile(tableSchemaFile, []byte(strings.Join(tables, \";\\n\")), 0666)\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n}\n\n// createTable 初始化时创建所有表\nfunc (t *testParserSuite) createTables(c *C) {\n\tvar tables []string\n\tfor tableName, value := range allTables {\n\t\ttables = append(tables, value)\n\t\ttables = append(tables, fmt.Sprintf(\"truncate table `%s`;\", tableName))\n\t}\n\tt.testExecute(c, tables...)\n}\n\nfunc TestComputePercent(t *testing.T) {\n\tstart := mysql.Position{\n\t\tName: \"001\",\n\t\tPos:  0,\n\t}\n\tstop := mysql.Position{\n\t\tName: \"001\",\n\t\tPos:  0,\n\t}\n\tcurrent := mysql.Position{\n\t\tName: \"001\",\n\t\tPos:  100,\n\t}\n\tmaster := core.MasterStatus{\n\t\tFile:     \"002\",\n\t\tPosition: 600,\n\t}\n\tbinlogs := []core.MasterLog{\n\t\t{\n\t\t\tName: \"001\",\n\t\t\tSize: 1000,\n\t\t},\n\t\t{\n\t\t\tName: \"002\",\n\t\t\tSize: 1000,\n\t\t},\n\t}\n\n\tpct := core.ComputePercent(start, stop, current,\n\t\tmaster, binlogs)\n\texpected := 10\n\tif pct != expected {\n\t\tt.Errorf(\"got %v expected %v\", pct, expected)\n\t}\n\n\tstop = mysql.Position{\n\t\tName: \"002\",\n\t\tPos:  0,\n\t}\n\tpct = core.ComputePercent(start, stop, current,\n\t\tmaster, binlogs)\n\texpected = 5\n\tif pct != expected {\n\t\tt.Errorf(\"got %v expected %v\", pct, expected)\n\t}\n\n\tstop = mysql.Position{\n\t\tName: \"003\",\n\t\tPos:  0,\n\t}\n\tpct = core.ComputePercent(start, stop, current,\n\t\tmaster, binlogs)\n\texpected = 5\n\tif pct != expected {\n\t\tt.Errorf(\"got %v expected %v\", pct, expected)\n\t}\n\n\tstart = mysql.Position{\n\t\tName: \"001\",\n\t\tPos:  200,\n\t}\n\tstop = mysql.Position{\n\t\tName: \"\",\n\t\tPos:  0,\n\t}\n\tcurrent = mysql.Position{\n\t\tName: \"002\",\n\t\tPos:  400,\n\t}\n\tpct = core.ComputePercent(start, stop, current,\n\t\tmaster, binlogs)\n\texpected = 85\n\tif pct != expected {\n\t\tt.Errorf(\"got %v expected %v\", pct, expected)\n\t}\n\n}\n"
  },
  {
    "path": "core/socket.go",
    "content": "// go-mysqlbinlog: a simple binlog tool to sync remote MySQL binlog.\n// go-mysqlbinlog supports semi-sync mode like facebook mysqlbinlog.\n// see http://yoshinorimatsunobu.blogspot.com/2014/04/semi-synchronous-replication-at-facebook.html\npackage core\n\n// time go run mainRemote.go -start-time=\"2018-09-17 00:00:00\" -stop-time=\"2018-09-25 00:00:00\" -o=1.sql\nimport (\n\t\"fmt\"\n\n\t\"github.com/imroc/req\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\nvar URL string\n\nvar header = req.Header{\"Accept\": \"application/json\"}\n\n// func init() {\n\n//     cnf, err := ini.Load(\"../cnf/config.ini\")\n//     if err != nil {\n//         log.Fatal().Err(err).Msg(\"加载配置文件失败!\")\n//         return\n//     }\n\n//     addr := cnf.Section(\"Bingo\").Key(\"socketAddr\").String()\n\n//     URL = fmt.Sprintf(\"http://%s/socket\", addr)\n\n//     req.SetTimeout(5 * time.Second)\n// }\n\n// 向客户端推送消息\nfunc sendMsg(user string, event string, title string, text string, kwargs map[string]interface{}) bool {\n\n\tif user == \"\" {\n\t\treturn true\n\t}\n\n\tif URL == \"\" {\n\t\treturn true\n\t}\n\n\turl := fmt.Sprintf(\"%s/room/%s\", URL, user)\n\n\tparam := req.Param{\n\t\t\"event\":   event,\n\t\t\"title\":   title,\n\t\t\"content\": text,\n\t}\n\n\tif kwargs != nil && len(kwargs) > 0 {\n\t\tfor k, v := range kwargs {\n\t\t\tparam[k] = v\n\t\t}\n\t}\n\n\tr, err := req.Post(url, header, param)\n\tif err != nil {\n\t\tlog.Error(\"请求websocket失败!\")\n\t\tlog.Print(err)\n\t\treturn false\n\t}\n\t// r.ToJSON(&foo)       // 响应体转成对象\n\t// log.Printf(\"%+v\", r) // 打印详细信息\n\n\tresp := r.Response()\n\n\tif resp.StatusCode == 200 {\n\t\treturn true\n\t} else {\n\t\tlog.Error(\"请求websocket失败!\")\n\t\tlog.Printf(\"%+v\", r) // 打印详细信息\n\t\treturn false\n\t}\n}\n"
  },
  {
    "path": "core/time.go",
    "content": "package core\n\nimport (\n\t\"time\"\n)\n\nfunc timeTrack(start time.Time, name string) {\n\t// elapsed := time.Since(start)\n\t// log.Printf(\"%s took %s\", name, time.Since(start))\n}\n"
  },
  {
    "path": "docs/test.md",
    "content": "# 效率测试\n\n测试结果\n\nmysqlbinlog > bingo2sql > binlog_rollback > binlog2sql\n\n**说明：**\n\n该测试结果可能存在的偏差如下：\n\n- 输出格式略有差异\n- 测试所在环境的状态变化\n- binlog_rollback 输出了binlog分析等信息等\n\n\n### 测试数据准备\n\n- 准备100w行基础数据\n- 更新\n- 删除\n- 获取binlog位置\n\n\n#### 创建测试表\n\n```sql\n\nuse test;\n\nreset master;\n\ndrop table if exists tt;\n\nCREATE TABLE `tt` (\n  ID bigint unsigned auto_increment primary key,\n  `TABLE_CATALOG` varchar(512) NOT NULL DEFAULT '',\n  `TABLE_SCHEMA` varchar(64) NOT NULL DEFAULT '',\n  `TABLE_NAME` varchar(64) NOT NULL DEFAULT '',\n  `COLUMN_NAME` varchar(64) NOT NULL DEFAULT '',\n  `ORDINAL_POSITION` bigint(21) unsigned NOT NULL DEFAULT '0',\n  `COLUMN_DEFAULT` longtext,\n  `IS_NULLABLE` varchar(3) NOT NULL DEFAULT '',\n  `DATA_TYPE` varchar(64) NOT NULL DEFAULT '',\n  `CHARACTER_MAXIMUM_LENGTH` bigint(21) unsigned DEFAULT NULL,\n  `CHARACTER_OCTET_LENGTH` bigint(21) unsigned DEFAULT NULL,\n  `NUMERIC_PRECISION` bigint(21) unsigned DEFAULT NULL,\n  `NUMERIC_SCALE` bigint(21) unsigned DEFAULT NULL,\n  `DATETIME_PRECISION` bigint(21) unsigned DEFAULT NULL,\n  `CHARACTER_SET_NAME` varchar(32) DEFAULT NULL,\n  `COLLATION_NAME` varchar(32) DEFAULT NULL,\n  `COLUMN_TYPE` longtext NOT NULL,\n  `COLUMN_KEY` varchar(3) NOT NULL DEFAULT '',\n  `EXTRA` varchar(30) NOT NULL DEFAULT '',\n  `PRIVILEGES` varchar(80) NOT NULL DEFAULT '',\n  `COLUMN_COMMENT` varchar(1024) NOT NULL DEFAULT '',\n  `GENERATION_EXPRESSION` longtext NOT NULL\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\n```\n\n\n#### 搭建测试数据\n\n\n\n```sql\n\ninsert into tt(TABLE_CATALOG,TABLE_SCHEMA,TABLE_NAME,COLUMN_NAME,ORDINAL_POSITION,COLUMN_DEFAULT,IS_NULLABLE,DATA_TYPE,CHARACTER_MAXIMUM_LENGTH,CHARACTER_OCTET_LENGTH,NUMERIC_PRECISION,NUMERIC_SCALE,DATETIME_PRECISION,CHARACTER_SET_NAME,COLLATION_NAME,COLUMN_TYPE,COLUMN_KEY,EXTRA,PRIVILEGES,COLUMN_COMMENT,GENERATION_EXPRESSION)\nselect * from information_schema.columns limit 1000;\n\n# 准备100w行基础数据\ninsert into tt(TABLE_CATALOG,TABLE_SCHEMA,TABLE_NAME,COLUMN_NAME,ORDINAL_POSITION,COLUMN_DEFAULT,IS_NULLABLE,DATA_TYPE,CHARACTER_MAXIMUM_LENGTH,CHARACTER_OCTET_LENGTH,NUMERIC_PRECISION,NUMERIC_SCALE,DATETIME_PRECISION,CHARACTER_SET_NAME,COLLATION_NAME,COLUMN_TYPE,COLUMN_KEY,EXTRA,PRIVILEGES,COLUMN_COMMENT,GENERATION_EXPRESSION)\nselect tt.TABLE_CATALOG,tt.TABLE_SCHEMA,tt.TABLE_NAME,tt.COLUMN_NAME,tt.ORDINAL_POSITION,tt.COLUMN_DEFAULT,tt.IS_NULLABLE,tt.DATA_TYPE,tt.CHARACTER_MAXIMUM_LENGTH,tt.CHARACTER_OCTET_LENGTH,tt.NUMERIC_PRECISION,tt.NUMERIC_SCALE,tt.DATETIME_PRECISION,tt.CHARACTER_SET_NAME,tt.COLLATION_NAME,tt.COLUMN_TYPE,tt.COLUMN_KEY,tt.EXTRA,tt.PRIVILEGES,tt.COLUMN_COMMENT,tt.GENERATION_EXPRESSION from tt,tt t1;\n\n\nupdate tt set ORDINAL_POSITION=ORDINAL_POSITION+10 where id%5=0;\n\nupdate tt set TABLE_NAME=concat(TABLE_NAME,\"1\"),COLLATION_NAME=concat(COLLATION_NAME,\"2\"),\nCOLUMN_COMMENT = TABLE_NAME where id%4=0;\n\ndelete from tt where id%2=0;\n\n# 获取binlog位置以便设置解析参数\nshow BINARY LOGS;\n```\n\nLog_name         | File_size\n-----------| -----------\n mysql-bin.000001 | 357491554\n\n\n#### bin2sql 测试脚本\n\n```sh\n\nfor i in {1..10}; do\necho $i;\n/usr/bin/time -f \"real %E  |  user %U  |  sys %S  |  cpu %P\" -a -o out.txt bin/bingo2sql -h 127.0.0.1 -P 3306 -u test -p test --start-file=mysql-bin.000001 --start-pos=4 --stop-file=mysql-bin.000001 --stop-pos=357490000 -o /tmp/binlog2/out.sql --max=0 --show-gtid=false --show-time=false;\ndone\ncat out.txt;\n\n```\n\n执行结果\n\nreal  |  user  |  sys | cpu\n---- | ---- | ---- | ----\nreal 0:13.49  |  user 33.65  |  sys 2.68  |  cpu 269%\nreal 0:13.61  |  user 34.21  |  sys 2.75  |  cpu 271%\nreal 0:13.42  |  user 33.26  |  sys 2.61  |  cpu 267%\nreal 0:13.61  |  user 33.99  |  sys 2.64  |  cpu 269%\nreal 0:13.85  |  user 34.77  |  sys 2.65  |  cpu 270%\nreal 0:13.58  |  user 33.58  |  sys 2.62  |  cpu 266%\nreal 0:13.61  |  user 34.06  |  sys 2.59  |  cpu 269%\nreal 0:13.47  |  user 33.39  |  sys 2.65  |  cpu 267%\nreal 0:11.68  |  user 28.75  |  sys 2.29  |  cpu 265%\nreal 0:11.45  |  user 28.68  |  sys 2.11  |  cpu 268%\n\n\n#### binlog_rollback 测试脚本\n```sh\n\nrm -f out.txt;\nfor i in {1..10}; do\necho $i;\n/usr/bin/time -f \"real %E  |  user %U  |  sys %S  |  cpu %P\" -a -o out.txt ./binlog_rollback -m repl -w 2sql -M mysql -t 4 -H 127.0.0.1 -P 3306 -u test -p test -sbin mysql-bin.000001 -spos 4 -ebin mysql-bin.000001 -epos 357490000 -o /tmp/binlog -dj=\"\"\ndone\ncat out.txt;\n\n```\n\n执行结果\n\nreal  |  user  |  sys | cpu\n---- | ---- | ---- | ----\nreal 0:28.79  |  user 68.74  |  sys 8.46  |  cpu 268%\nreal 0:27.35  |  user 66.22  |  sys 7.86  |  cpu 270%\nreal 0:28.58  |  user 69.29  |  sys 8.31  |  cpu 271%\nreal 0:29.19  |  user 70.78  |  sys 8.47  |  cpu 271%\nreal 0:28.31  |  user 68.91  |  sys 8.06  |  cpu 271%\nreal 0:28.67  |  user 68.67  |  sys 8.33  |  cpu 268%\nreal 0:28.40  |  user 68.92  |  sys 7.73  |  cpu 269%\nreal 0:28.44  |  user 69.04  |  sys 8.05  |  cpu 271%\nreal 0:28.43  |  user 68.97  |  sys 7.94  |  cpu 270%\nreal 0:28.87  |  user 69.27  |  sys 7.99  |  cpu 267%\n\n\n\n#### bin2sql测试脚本\n```sh\n\nrm -f out.txt;\nfor i in {1..10}; do\necho $i;\n\n/usr/bin/time -f \"real %E  |  user %U  |  sys %S  |  cpu %P\" -a -o out.txt python binlog2sql.py -h127.0.0.1 -P3306 -utest -p'test' --start-file='mysql-bin.000001' --start-position=4 --stop-file='mysql-bin.000001' --stop-position=357490000 --only-dml > /tmp/binlog3/out.sql\n\ndone\ncat out.txt;\n\n```\n\n执行结果\n\n**无**\n感兴趣的同学可以自行测试。\n\n\n#### mysqlbinlog 测试脚本\n```sh\n\nrm -f out.txt;\nfor i in {1..10}; do\necho $i;\n/usr/bin/time -f \"real %E  |  user %U  |  sys %S  |  cpu %P\" -a -o out.txt mysqlbinlog -h127.0.0.1 -P3306 -utest -p'test' --start-position=4 --stop-position=357490000 --base64-output=DECODE-ROWS -v  -r /tmp/binlog4/out.sql /data/mysql/db_cmdb/blog/mysql-bin.000001\ndone\ncat out.txt;\n\n```\n\n执行结果\n\nreal  |  user  |  sys | cpu\n---- | ---- | ---- | ----\nreal 0:13.42  |  user 11.43  |  sys 1.96  |  cpu 99%\nreal 0:13.29  |  user 11.37  |  sys 1.88  |  cpu 99%\nreal 0:13.59  |  user 11.54  |  sys 1.94  |  cpu 99%\nreal 0:13.17  |  user 11.20  |  sys 1.94  |  cpu 99%\nreal 0:13.27  |  user 11.37  |  sys 1.83  |  cpu 99%\nreal 0:13.76  |  user 11.62  |  sys 2.04  |  cpu 99%\nreal 0:13.98  |  user 12.01  |  sys 1.93  |  cpu 99%\nreal 0:14.05  |  user 11.86  |  sys 1.95  |  cpu 98%\nreal 0:13.76  |  user 11.67  |  sys 1.96  |  cpu 99%\nreal 0:13.79  |  user 11.72  |  sys 2.01  |  cpu 99%\n\n\n"
  },
  {
    "path": "go.mod",
    "content": "module github.com/hanchuanchuan/bingo2sql\n\nreplace github.com/sirupsen/logrus => github.com/sirupsen/logrus v1.2.0\n\ngo 1.18\n\n// replace golang.org/x/sys => github.com/golang/sys v0.0.0-20200201011859-915c9c3d4ccf\n\nrequire (\n\tgithub.com/go-mysql-org/go-mysql v1.6.0\n\tgithub.com/go-sql-driver/mysql v1.5.0\n\tgithub.com/hanchuanchuan/goInception v1.2.0\n\tgithub.com/imroc/req v0.3.0\n\tgithub.com/jinzhu/gorm v1.9.12\n\tgithub.com/jinzhu/now v1.1.1\n\tgithub.com/labstack/echo/v4 v4.1.14\n\tgithub.com/mholt/archiver/v3 v3.3.0\n\tgithub.com/mitchellh/go-homedir v1.1.0\n\tgithub.com/pingcap/check v0.0.0-20190102082844-67f458068fc8\n\tgithub.com/pkg/profile v1.4.0\n\tgithub.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24\n\tgithub.com/sirupsen/logrus v1.4.2\n\tgithub.com/spf13/cobra v0.0.6\n\tgithub.com/spf13/pflag v1.0.5\n\tgithub.com/spf13/viper v1.6.2\n)\n\nrequire github.com/etcd-io/gofail v0.0.0-20180808172546-51ce9a71510a // indirect\n\nrequire (\n\tgithub.com/andybalholm/brotli v0.0.0-20190621154722-5f990b63d2d6 // indirect\n\tgithub.com/cznic/mathutil v0.0.0-20181122101859-297441e03548 // indirect\n\tgithub.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect\n\tgithub.com/dsnet/compress v0.0.1 // indirect\n\tgithub.com/frankban/quicktest v1.7.2 // indirect\n\tgithub.com/fsnotify/fsnotify v1.4.7 // indirect\n\tgithub.com/golang/gddo v0.0.0-20190419222130-af0f2af80721 // indirect\n\tgithub.com/golang/protobuf v1.3.2 // indirect\n\tgithub.com/golang/snappy v0.0.1 // indirect\n\tgithub.com/google/go-cmp v0.3.1 // indirect\n\tgithub.com/google/uuid v1.3.0 // indirect\n\tgithub.com/hashicorp/hcl v1.0.0 // indirect\n\tgithub.com/inconshreveable/mousetrap v1.0.0 // indirect\n\tgithub.com/jinzhu/inflection v1.0.0 // indirect\n\tgithub.com/juju/errors v0.0.0-20190930114154-d42613fe1ab9 // indirect\n\tgithub.com/klauspost/compress v1.9.7 // indirect\n\tgithub.com/klauspost/pgzip v1.2.1 // indirect\n\tgithub.com/konsorten/go-windows-terminal-sequences v1.0.1 // indirect\n\tgithub.com/labstack/gommon v0.3.0 // indirect\n\tgithub.com/magiconair/properties v1.8.1 // indirect\n\tgithub.com/mattn/go-colorable v0.1.4 // indirect\n\tgithub.com/mattn/go-isatty v0.0.11 // indirect\n\tgithub.com/mitchellh/mapstructure v1.1.2 // indirect\n\tgithub.com/nwaples/rardecode v1.0.0 // indirect\n\tgithub.com/pelletier/go-toml v1.6.0 // indirect\n\tgithub.com/pierrec/lz4 v2.4.1+incompatible // indirect\n\tgithub.com/pingcap/errors v0.11.5-0.20201126102027-b0a155152ca3 // indirect\n\tgithub.com/pingcap/tipb v0.0.0-20190428032612-535e1abaa330 // indirect\n\tgithub.com/remyoudompheng/bigfft v0.0.0-20190728182440-6a916e37a237 // indirect\n\tgithub.com/siddontang/go v0.0.0-20180604090527-bdc77568d726\n\tgithub.com/siddontang/go-log v0.0.0-20180807004314-8d05993dda07 // indirect\n\tgithub.com/spf13/afero v1.2.2 // indirect\n\tgithub.com/spf13/cast v1.3.1 // indirect\n\t// github.com/spf13/cobra v0.0.6\n\tgithub.com/spf13/jwalterweatherman v1.1.0 // indirect\n\tgithub.com/subosito/gotenv v1.2.0 // indirect\n\tgithub.com/ulikunitz/xz v0.5.6 // indirect\n\tgithub.com/valyala/bytebufferpool v1.0.0 // indirect\n\tgithub.com/valyala/fasttemplate v1.1.0 // indirect\n\tgithub.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect\n\tgithub.com/youtube/vitess v2.1.1+incompatible // indirect\n\tgo.uber.org/atomic v1.7.0 // indirect\n\tgolang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 // indirect\n\tgolang.org/x/net v0.0.0-20201021035429-f5854403a974 // indirect\n\tgolang.org/x/sys v0.0.0-20220624220833-87e55d714810 // indirect\n\tgolang.org/x/text v0.3.6 // indirect\n\tgopkg.in/ini.v1 v1.52.0 // indirect\n\tgopkg.in/warnings.v0 v0.1.2 // indirect\n\tgopkg.in/yaml.v2 v2.2.8 // indirect\n)\n"
  },
  {
    "path": "go.sum",
    "content": "cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ngithub.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=\ngithub.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=\ngithub.com/CorgiMan/json2 v0.0.0-20150213135156-e72957aba209 h1:rRZPHlNlREFwuEtpYikMNZPs4l5g6zic54l2XDAK4ws=\ngithub.com/CorgiMan/json2 v0.0.0-20150213135156-e72957aba209/go.mod h1:VwmVPvMIZlx23Q7F1umyYmkhNDqf6WQKfMUhGEdVcLA=\ngithub.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=\ngithub.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=\ngithub.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=\ngithub.com/andybalholm/brotli v0.0.0-20190621154722-5f990b63d2d6 h1:bZ28Hqta7TFAK3Q08CMvv8y3/8ATaEqv2nGoc6yff6c=\ngithub.com/andybalholm/brotli v0.0.0-20190621154722-5f990b63d2d6/go.mod h1:+lx6/Aqd1kLJ1GQfkvOnaZ1WGmLpMpbprPuIOOZX30U=\ngithub.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=\ngithub.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=\ngithub.com/beorn7/perks v1.0.0 h1:HWo1m869IqiPhD389kmkxeTalrjNbbJTC8LXupb+sl0=\ngithub.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=\ngithub.com/blacktear23/go-proxyprotocol v0.0.0-20171102103907-62e368e1c470/go.mod h1:VKt7CNAQxpFpSDz3sXyj9hY/GbVsQCr0sB3w59nE7lU=\ngithub.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps=\ngithub.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=\ngithub.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=\ngithub.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd h1:qMd81Ts1T2OTKmB4acZcyKaMtRnY5Y44NuXGX2GFJ1w=\ngithub.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI=\ngithub.com/coreos/bbolt v1.3.0/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=\ngithub.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=\ngithub.com/coreos/etcd v3.3.10+incompatible h1:jFneRYjIvLMLhDLCzuTuU4rSJUjRplcJQ7pD7MnhC04=\ngithub.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=\ngithub.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=\ngithub.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=\ngithub.com/coreos/go-systemd v0.0.0-20181031085051-9002847aa142/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=\ngithub.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=\ngithub.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=\ngithub.com/cznic/golex v0.0.0-20181122101858-9c343928389c/go.mod h1:+bmmJDNmKlhWNG+gwWCkaBoTy39Fs+bzRxVBzoTQbIc=\ngithub.com/cznic/mathutil v0.0.0-20181021201202-eba54fb065b7/go.mod h1:e6NPNENfs9mPDVNRekM7lKScauxd5kXTr1Mfyig6TDM=\ngithub.com/cznic/mathutil v0.0.0-20181122101859-297441e03548 h1:iwZdTE0PVqJCos1vaoKsclOGD3ADKpshg3SRtYBbwso=\ngithub.com/cznic/mathutil v0.0.0-20181122101859-297441e03548/go.mod h1:e6NPNENfs9mPDVNRekM7lKScauxd5kXTr1Mfyig6TDM=\ngithub.com/cznic/parser v0.0.0-20160622100904-31edd927e5b1/go.mod h1:2B43mz36vGZNZEwkWi8ayRSSUXLfjL8OkbzwW4NcPMM=\ngithub.com/cznic/parser v0.0.0-20181122101858-d773202d5b1f/go.mod h1:2B43mz36vGZNZEwkWi8ayRSSUXLfjL8OkbzwW4NcPMM=\ngithub.com/cznic/sortutil v0.0.0-20150617083342-4c7342852e65/go.mod h1:q2w6Bg5jeox1B+QkJ6Wp/+Vn0G/bo3f1uY7Fn3vivIQ=\ngithub.com/cznic/sortutil v0.0.0-20181122101858-f5f958428db8 h1:LpMLYGyy67BoAFGda1NeOBQwqlv7nUXpm+rIVHGxZZ4=\ngithub.com/cznic/sortutil v0.0.0-20181122101858-f5f958428db8/go.mod h1:q2w6Bg5jeox1B+QkJ6Wp/+Vn0G/bo3f1uY7Fn3vivIQ=\ngithub.com/cznic/strutil v0.0.0-20171016134553-529a34b1c186/go.mod h1:AHHPPPXTw0h6pVabbcbyGRK1DckRn7r/STdZEeIDzZc=\ngithub.com/cznic/strutil v0.0.0-20181122101858-275e90344537/go.mod h1:AHHPPPXTw0h6pVabbcbyGRK1DckRn7r/STdZEeIDzZc=\ngithub.com/cznic/y v0.0.0-20170802143616-045f81c6662a/go.mod h1:1rk5VM7oSnA4vjp+hrLQ3HWHa+Y4yPCa3/CsJrcNnvs=\ngithub.com/cznic/y v0.0.0-20181122101901-b05e8c2e8d7b/go.mod h1:1rk5VM7oSnA4vjp+hrLQ3HWHa+Y4yPCa3/CsJrcNnvs=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/denisenkom/go-mssqldb v0.0.0-20190121005146-b04fd42d9952/go.mod h1:xN/JuLBIz4bjkxNmByTiV1IbhfnYb6oo99phBn4Eqhc=\ngithub.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd h1:83Wprp6ROGeiHFAP8WJdI2RoxALQYgdllERc3N5N2DM=\ngithub.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=\ngithub.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=\ngithub.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=\ngithub.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=\ngithub.com/dsnet/compress v0.0.1 h1:PlZu0n3Tuv04TzpfPbrnI0HW/YwodEXDS+oPKahKF0Q=\ngithub.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5JflhBbQEHo=\ngithub.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=\ngithub.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=\ngithub.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM=\ngithub.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y=\ngithub.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=\ngithub.com/etcd-io/gofail v0.0.0-20180808172546-51ce9a71510a h1:QNEenQIsGDEEfFNSnN+h6hE1OwnHqTg7Dl9gEk1Cko4=\ngithub.com/etcd-io/gofail v0.0.0-20180808172546-51ce9a71510a/go.mod h1:49H/RkXP8pKaZy4h0d+NW16rSLhyVBt4o6VLJbmOqDE=\ngithub.com/frankban/quicktest v1.7.2 h1:2QxQoC1TS09S7fhCPsrvqYdvP1H5M1P1ih5ABm3BTYk=\ngithub.com/frankban/quicktest v1.7.2/go.mod h1:jaStnuzAqU1AJdCO0l53JDCJrVDKcS03DbaAcR7Ks/o=\ngithub.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=\ngithub.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=\ngithub.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=\ngithub.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=\ngithub.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=\ngithub.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=\ngithub.com/go-mysql-org/go-mysql v1.6.0 h1:19B5fojzZcri/1wj9G/1+ws8RJ3N6rJs2X5c/+kBLuQ=\ngithub.com/go-mysql-org/go-mysql v1.6.0/go.mod h1:GX0clmylJLdZEYAojPCDTCvwZxbTBrke93dV55715u0=\ngithub.com/go-sql-driver/mysql v1.3.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=\ngithub.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=\ngithub.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=\ngithub.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=\ngithub.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=\ngithub.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=\ngithub.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=\ngithub.com/gogo/protobuf v0.0.0-20180717141946-636bf0302bc9/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=\ngithub.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=\ngithub.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE=\ngithub.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=\ngithub.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY=\ngithub.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=\ngithub.com/golang/gddo v0.0.0-20190419222130-af0f2af80721 h1:KRMr9A3qfbVM7iV/WcLY/rL5LICqwMHLhwRXKu99fXw=\ngithub.com/golang/gddo v0.0.0-20190419222130-af0f2af80721/go.mod h1:xEhNfoBDX1hzLm2Nf80qUvZ2sVwoMZ8d6IE2SrsQfh4=\ngithub.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=\ngithub.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=\ngithub.com/golang/groupcache v0.0.0-20181024230925-c65c006176ff/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=\ngithub.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=\ngithub.com/golang/protobuf v0.0.0-20180814211427-aa810b61a9c7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=\ngithub.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=\ngithub.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=\ngithub.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=\ngithub.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=\ngithub.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo=\ngithub.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=\ngithub.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=\ngithub.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg=\ngithub.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=\ngithub.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=\ngithub.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=\ngithub.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=\ngithub.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=\ngithub.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=\ngithub.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=\ngithub.com/grpc-ecosystem/go-grpc-middleware v1.0.0 h1:Iju5GlWwrvL6UBg4zJJt3btmonfrMlCDdsejg4CZE7c=\ngithub.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=\ngithub.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho=\ngithub.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=\ngithub.com/grpc-ecosystem/grpc-gateway v1.5.1/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=\ngithub.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=\ngithub.com/hanchuanchuan/gh-ost v1.0.49-0.20200114083508-62a578b91654 h1:PRc0l9OaUyPNPoMo0nruucADDwpFZkVvxMbGCZXpjFg=\ngithub.com/hanchuanchuan/gh-ost v1.0.49-0.20200114083508-62a578b91654/go.mod h1:9NockcUOKVL+JLrnCsXtpRAuPseKoMVqfDmn251PbG0=\ngithub.com/hanchuanchuan/go-mysql v0.0.0-20200114082439-6d0d8d3a982e h1:IjLlaPxsWKkYuePu8Ed2YbHLuBAuKLW6lclhmeq2A6I=\ngithub.com/hanchuanchuan/go-mysql v0.0.0-20200114082439-6d0d8d3a982e/go.mod h1:HJV3Ej7p/Ck/htb5F5VwYAHDL02jA4J6uAufArtmsos=\ngithub.com/hanchuanchuan/goInception v1.2.0 h1:uAai3jAT8D7zwE6YhmMDaJg+3eTPfQauYiNMq9xtztY=\ngithub.com/hanchuanchuan/goInception v1.2.0/go.mod h1:idDo5pLO0j10hNjc2xf1fht01QEbUNTTyPVHGdDp9Z4=\ngithub.com/hanchuanchuan/golib v0.0.0-20200113085747-47643bc243f1 h1:h4yTn7Lu9roQdSZezhf8BwMi3R/KWikL6xV02rmZj6Q=\ngithub.com/hanchuanchuan/golib v0.0.0-20200113085747-47643bc243f1/go.mod h1:6V4x6V71pCU4o9UfPmemdrU9sEhSF6AmXt+PiFr8x4Q=\ngithub.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=\ngithub.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=\ngithub.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=\ngithub.com/imroc/req v0.2.3/go.mod h1:J9FsaNHDTIVyW/b5r6/Df5qKEEEq2WzZKIgKSajd1AE=\ngithub.com/imroc/req v0.3.0 h1:3EioagmlSG+z+KySToa+Ylo3pTFZs+jh3Brl7ngU12U=\ngithub.com/imroc/req v0.3.0/go.mod h1:F+NZ+2EFSo6EFXdeIbpfE9hcC233id70kf0byW97Caw=\ngithub.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=\ngithub.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=\ngithub.com/jinzhu/gorm v1.9.2/go.mod h1:Vla75njaFJ8clLU1W44h34PjIkijhjHIYnZxMqCdxqo=\ngithub.com/jinzhu/gorm v1.9.12 h1:Drgk1clyWT9t9ERbzHza6Mj/8FY/CqMyVzOiHviMo6Q=\ngithub.com/jinzhu/gorm v1.9.12/go.mod h1:vhTjlKSJUTWNtcbQtrMBFCxy7eXTzeCAzfL5fBZT/Qs=\ngithub.com/jinzhu/inflection v0.0.0-20180308033659-04140366298a/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=\ngithub.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=\ngithub.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=\ngithub.com/jinzhu/now v0.0.0-20181116074157-8ec929ed50c3/go.mod h1:oHTiXerJ20+SfYcrdlBO7rzZRJWGwSTQ0iUY2jI6Gfc=\ngithub.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=\ngithub.com/jinzhu/now v1.1.1 h1:g39TucaRWyV3dwDO++eEc6qf8TVIQ/Da48WmqjZ3i7E=\ngithub.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=\ngithub.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks=\ngithub.com/jmoiron/sqlx v1.3.3/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ=\ngithub.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=\ngithub.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=\ngithub.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=\ngithub.com/juju/errors v0.0.0-20181118221551-089d3ea4e4d5/go.mod h1:W54LbzXuIE0boCoNJfwqpmkKJ1O4TCTZMetAt6jGk7Q=\ngithub.com/juju/errors v0.0.0-20190930114154-d42613fe1ab9 h1:hJix6idebFclqlfZCHE7EUX7uqLCyb70nHNHH1XKGBg=\ngithub.com/juju/errors v0.0.0-20190930114154-d42613fe1ab9/go.mod h1:W54LbzXuIE0boCoNJfwqpmkKJ1O4TCTZMetAt6jGk7Q=\ngithub.com/juju/loggo v0.0.0-20180524022052-584905176618/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U=\ngithub.com/juju/testing v0.0.0-20180920084828-472a3e8b2073/go.mod h1:63prj8cnj0tU0S9OHjGJn+b1h0ZghCndfnbQolrYTwA=\ngithub.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=\ngithub.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=\ngithub.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=\ngithub.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=\ngithub.com/klauspost/compress v1.9.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=\ngithub.com/klauspost/compress v1.9.7 h1:hYW1gP94JUmAhBtJ+LNz5My+gBobDxPR1iVuKug26aA=\ngithub.com/klauspost/compress v1.9.7/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=\ngithub.com/klauspost/cpuid v0.0.0-20170728055534-ae7887de9fa5/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=\ngithub.com/klauspost/cpuid v1.2.0 h1:NMpwD2G9JSFOE1/TJjGSo5zG7Yb2bTe7eq1jH+irmeE=\ngithub.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=\ngithub.com/klauspost/pgzip v1.2.1 h1:oIPZROsWuPHpOdMVWLuJZXwgjhrW8r1yEX8UqMyeNHM=\ngithub.com/klauspost/pgzip v1.2.1/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=\ngithub.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=\ngithub.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=\ngithub.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=\ngithub.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=\ngithub.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs=\ngithub.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=\ngithub.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=\ngithub.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=\ngithub.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=\ngithub.com/labstack/echo/v4 v4.1.14 h1:h8XP66UfB3tUm+L3QPw7tmwAu3pJaA/nyfHPCcz46ic=\ngithub.com/labstack/echo/v4 v4.1.14/go.mod h1:Q5KZ1vD3V5FEzjM79hjwVrC3ABr7F5IdM23bXQMRDGg=\ngithub.com/labstack/gommon v0.3.0 h1:JEeO0bvc78PKdyHxloTKiF8BD5iGrH8T6MSeGvSgob0=\ngithub.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=\ngithub.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=\ngithub.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=\ngithub.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0=\ngithub.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=\ngithub.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=\ngithub.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=\ngithub.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=\ngithub.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=\ngithub.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=\ngithub.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=\ngithub.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=\ngithub.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=\ngithub.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM=\ngithub.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=\ngithub.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=\ngithub.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=\ngithub.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=\ngithub.com/mattn/go-sqlite3 v2.0.1+incompatible h1:xQ15muvnzGBHpIpdrNi1DA5x0+TcBZzsIDwmw9uTHzw=\ngithub.com/mattn/go-sqlite3 v2.0.1+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=\ngithub.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=\ngithub.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=\ngithub.com/mholt/archiver/v3 v3.3.0 h1:vWjhY8SQp5yzM9P6OJ/eZEkmi3UAbRrxCq48MxjAzig=\ngithub.com/mholt/archiver/v3 v3.3.0/go.mod h1:YnQtqsp+94Rwd0D/rk5cnLrxusUBUXg+08Ebtr1Mqao=\ngithub.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=\ngithub.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=\ngithub.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=\ngithub.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=\ngithub.com/montanaflynn/stats v0.0.0-20180911141734-db72e6cae808/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=\ngithub.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=\ngithub.com/myesui/uuid v1.0.0/go.mod h1:2CDfNgU0LR8mIdO8vdWd8i9gWWxLlcoIGGpSNgafq84=\ngithub.com/ngaut/pools v0.0.0-20180318154953-b7bc8c42aac7 h1:7KAv7KMGTTqSmYZtNdcNTgsos+vFzULLwyElndwn+5c=\ngithub.com/ngaut/pools v0.0.0-20180318154953-b7bc8c42aac7/go.mod h1:iWMfgwqYW+e8n5lC/jjNEhwcjbRDpl5NT7n2h+4UNcI=\ngithub.com/ngaut/sync2 v0.0.0-20141008032647-7a24ed77b2ef h1:K0Fn+DoFqNqktdZtdV3bPQ/0cuYh2H4rkg0tytX/07k=\ngithub.com/ngaut/sync2 v0.0.0-20141008032647-7a24ed77b2ef/go.mod h1:7WjlapSfwQyo6LNmIvEWzsW1hbBQfpUO4JWnuQRmva8=\ngithub.com/nwaples/rardecode v1.0.0 h1:r7vGuS5akxOnR4JQSkko62RJ1ReCMXxQRPtxsiFMBOs=\ngithub.com/nwaples/rardecode v1.0.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=\ngithub.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=\ngithub.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=\ngithub.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=\ngithub.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=\ngithub.com/opentracing/basictracer-go v1.0.0 h1:YyUAhaEfjoWXclZVJ9sGoNct7j4TVk7lZWlQw5UXuoo=\ngithub.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74=\ngithub.com/opentracing/opentracing-go v1.0.2 h1:3jA2P6O1F9UOrWVpwrIo17pu01KWvNWg4X946/Y5Zwg=\ngithub.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=\ngithub.com/outbrain/golib v0.0.0-20180830062331-ab954725f502 h1:oS2s2j8GP70jE0IDMyA4BO4HgvkNcjHfR40h2fyhc7s=\ngithub.com/outbrain/golib v0.0.0-20180830062331-ab954725f502/go.mod h1:JDhu//MMvcPVPH889Xr7DyamEbTLumgDBALGUyXrz1g=\ngithub.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=\ngithub.com/pelletier/go-toml v1.6.0 h1:aetoXYr0Tv7xRU/V4B4IZJ2QcbtMUFoNb3ORp7TzIK4=\ngithub.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys=\ngithub.com/percona/go-mysql v0.0.0-20190307200310-f5cfaf6a5e55 h1:kx48fD4K+GXb7YqwykiWirM8GRtoMcpWbGlaAb3IsqE=\ngithub.com/percona/go-mysql v0.0.0-20190307200310-f5cfaf6a5e55/go.mod h1:/SGLf9OMxlnK6jq4mkFiImBcJXXk5jwD+lDrwDaGXcw=\ngithub.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=\ngithub.com/pierrec/lz4 v2.4.1+incompatible h1:mFe7ttWaflA46Mhqh+jUfjp2qTbPYxLB2/OyBppH9dg=\ngithub.com/pierrec/lz4 v2.4.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=\ngithub.com/pingcap/check v0.0.0-20190102082844-67f458068fc8 h1:USx2/E1bX46VG32FIw034Au6seQ2fY9NEILmNh/UlQg=\ngithub.com/pingcap/check v0.0.0-20190102082844-67f458068fc8/go.mod h1:B1+S9LNcuMyLH/4HMTViQOJevkGiik3wW2AN9zb2fNQ=\ngithub.com/pingcap/errors v0.11.0/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=\ngithub.com/pingcap/errors v0.11.5-0.20201029093017-5a7df2af2ac7/go.mod h1:G7x87le1poQzLB/TqvTJI2ILrSgobnq4Ut7luOwvfvI=\ngithub.com/pingcap/errors v0.11.5-0.20201126102027-b0a155152ca3 h1:LllgC9eGfqzkfubMgjKIDyZYaa609nNWAyNZtpy2B3M=\ngithub.com/pingcap/errors v0.11.5-0.20201126102027-b0a155152ca3/go.mod h1:G7x87le1poQzLB/TqvTJI2ILrSgobnq4Ut7luOwvfvI=\ngithub.com/pingcap/goleveldb v0.0.0-20171020122428-b9ff6c35079e h1:P73/4dPCL96rGrobssy1nVy2VaVpNCuLpCbr+FEaTA8=\ngithub.com/pingcap/goleveldb v0.0.0-20171020122428-b9ff6c35079e/go.mod h1:O17XtbryoCJhkKGbT62+L2OlrniwqiGLSqrmdHCMzZw=\ngithub.com/pingcap/kvproto v0.0.0-20181206061346-54cf0a0dfe55 h1:0vbRC39OU0Ep2GFgV5BwZ1u3viC5uk8yb3c07rLRMvY=\ngithub.com/pingcap/kvproto v0.0.0-20181206061346-54cf0a0dfe55/go.mod h1:Ja9XPjot9q4/3JyCZodnWDGNXt4pKemhIYCvVJM7P24=\ngithub.com/pingcap/log v0.0.0-20200511115504-543df19646ad/go.mod h1:4rbK1p9ILyIfb6hU7OG2CiWSqMXnp3JMbiaVJ6mvoY8=\ngithub.com/pingcap/log v0.0.0-20210317133921-96f4fcab92a4/go.mod h1:4rbK1p9ILyIfb6hU7OG2CiWSqMXnp3JMbiaVJ6mvoY8=\ngithub.com/pingcap/parser v0.0.0-20190506092653-e336082eb825/go.mod h1:1FNvfp9+J0wvc4kl8eGNh7Rqrxveg15jJoWo/a0uHwA=\ngithub.com/pingcap/parser v0.0.0-20210415081931-48e7f467fd74/go.mod h1:xZC8I7bug4GJ5KtHhgAikjTfU4kBv1Sbo3Pf1MZ6lVw=\ngithub.com/pingcap/pd v2.1.0+incompatible h1:X0o443C/jXF6yJiiP1xTRxjKPHRe4gwQqjGcFTz1MuU=\ngithub.com/pingcap/pd v2.1.0+incompatible/go.mod h1:nD3+EoYes4+aNNODO99ES59V83MZSI+dFbhyr667a0E=\ngithub.com/pingcap/tipb v0.0.0-20170310053819-1043caee48da/go.mod h1:RtkHW8WbcNxj8lsbzjaILci01CtYnYbIkQhjyZWrWVI=\ngithub.com/pingcap/tipb v0.0.0-20190428032612-535e1abaa330 h1:rRMLMjIMFulCX9sGKZ1hoov/iROMsKyC8Snc02nSukw=\ngithub.com/pingcap/tipb v0.0.0-20190428032612-535e1abaa330/go.mod h1:RtkHW8WbcNxj8lsbzjaILci01CtYnYbIkQhjyZWrWVI=\ngithub.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/errors v0.9.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/profile v1.4.0 h1:uCmaf4vVbWAOZz36k1hrQD7ijGRzLwaME8Am/7a4jZI=\ngithub.com/pkg/profile v1.4.0/go.mod h1:NWz/XGvpEW1FyYQ7fCx4dqYBLlfTcE+A9FLAkNKqjFE=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/prometheus/client_golang v0.9.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=\ngithub.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=\ngithub.com/prometheus/client_golang v0.9.3 h1:9iH4JKXLzFbOAdtqv/a+j8aewx2Y8lAjAydhbaScPF8=\ngithub.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=\ngithub.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=\ngithub.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx/RA8rT8tKFRuGUZhuA90OyIBpPCXkcbwU8DE=\ngithub.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/prometheus/common v0.0.0-20181020173914-7e9e6cabbd39/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=\ngithub.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=\ngithub.com/prometheus/common v0.4.0 h1:7etb9YClo3a6HjLzfl6rIQaU+FDfi0VSX39io3aQ+DM=\ngithub.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=\ngithub.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=\ngithub.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084 h1:sofwID9zm4tzrgykg80hfFph1mryUeLRsUfoocVVmRY=\ngithub.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=\ngithub.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=\ngithub.com/remyoudompheng/bigfft v0.0.0-20170806203942-52369c62f446/go.mod h1:uYEyJGbgTkfkS4+E/PavXkNJcbFIpEtjt2B0KDQ5+9M=\ngithub.com/remyoudompheng/bigfft v0.0.0-20190728182440-6a916e37a237 h1:HQagqIiBmr8YXawX/le3+O26N+vPPC1PtjaF3mwnook=\ngithub.com/remyoudompheng/bigfft v0.0.0-20190728182440-6a916e37a237/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=\ngithub.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=\ngithub.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=\ngithub.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=\ngithub.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=\ngithub.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=\ngithub.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24 h1:pntxY8Ary0t43dCZ5dqY4YTJCObLY1kIXl0uzMv+7DE=\ngithub.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=\ngithub.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=\ngithub.com/siddontang/go v0.0.0-20180604090527-bdc77568d726 h1:xT+JlYxNGqyT+XcU8iUrN18JYed2TvG9yN5ULG2jATM=\ngithub.com/siddontang/go v0.0.0-20180604090527-bdc77568d726/go.mod h1:3yhqj7WBBfRhbBlzyOC3gUxftwsU0u8gqevxwIHQpMw=\ngithub.com/siddontang/go-log v0.0.0-20180807004314-8d05993dda07 h1:oI+RNwuC9jF2g2lP0u0cVEEZrc/AYBCuFdvwrLWM/6Q=\ngithub.com/siddontang/go-log v0.0.0-20180807004314-8d05993dda07/go.mod h1:yFdBgwXP24JziuRl2NMUahT7nGLNOKi1SIiFxMttVD4=\ngithub.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo=\ngithub.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=\ngithub.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=\ngithub.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=\ngithub.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=\ngithub.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=\ngithub.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=\ngithub.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72 h1:qLC7fQah7D6K1B0ujays3HV9gkFtllcxhzImRR7ArPQ=\ngithub.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=\ngithub.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=\ngithub.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc=\ngithub.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=\ngithub.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=\ngithub.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng=\ngithub.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=\ngithub.com/spf13/cobra v0.0.6 h1:breEStsVwemnKh2/s6gMvSdMEkwW0sK8vGStnlVBMCs=\ngithub.com/spf13/cobra v0.0.6/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=\ngithub.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=\ngithub.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=\ngithub.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=\ngithub.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=\ngithub.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=\ngithub.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\ngithub.com/spf13/viper v1.3.1/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=\ngithub.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=\ngithub.com/spf13/viper v1.6.2 h1:7aKfF+e8/k68gda3LOjo5RxiUqddoFxVq4BKBPrxk5E=\ngithub.com/spf13/viper v1.6.2/go.mod h1:t3iDnF5Jlj76alVNuyFBk5oUMCvsrkbvZK0WQdfDi5k=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=\ngithub.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=\ngithub.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=\ngithub.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=\ngithub.com/tmc/grpc-websocket-proxy v0.0.0-20171017195756-830351dc03c6/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=\ngithub.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=\ngithub.com/twinj/uuid v1.0.0 h1:fzz7COZnDrXGTAOHGuUGYd6sG+JMq+AoE7+Jlu0przk=\ngithub.com/twinj/uuid v1.0.0/go.mod h1:mMgcE1RHFUFqe5AfiwlINXisXfDGro23fWdPUfOMjRY=\ngithub.com/uber-go/atomic v1.3.2/go.mod h1:/Ct5t2lcmbJ4OSe/waGBoaVvVqtO0bmtfVNex1PFV8g=\ngithub.com/uber/jaeger-client-go v2.15.0+incompatible h1:NP3qsSqNxh8VYr956ur1N/1C1PjvOJnJykCzcD5QHbk=\ngithub.com/uber/jaeger-client-go v2.15.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk=\ngithub.com/uber/jaeger-lib v1.5.0 h1:OHbgr8l656Ub3Fw5k9SWnBfIEwvoHQ+W2y+Aa9D1Uyo=\ngithub.com/uber/jaeger-lib v1.5.0/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U=\ngithub.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=\ngithub.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=\ngithub.com/ulikunitz/xz v0.5.6 h1:jGHAfXawEGZQ3blwU5wnWKQJvAraT7Ftq9EXjnXYgt8=\ngithub.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8=\ngithub.com/unrolled/render v0.0.0-20180914162206-b9786414de4d/go.mod h1:tu82oB5W2ykJRVioYsB+IQKcft7ryBr7w12qMBUPyXg=\ngithub.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=\ngithub.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=\ngithub.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=\ngithub.com/valyala/fasttemplate v1.1.0 h1:RZqt0yGBsps8NGvLSGW804QQqCUYYLsaOjTVHy1Ocw4=\ngithub.com/valyala/fasttemplate v1.1.0/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=\ngithub.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=\ngithub.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=\ngithub.com/xiang90/probing v0.0.0-20160813154853-07dd2e8dfe18/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=\ngithub.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=\ngithub.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=\ngithub.com/youtube/vitess v2.1.1+incompatible h1:SE+P7DNX/jw5RHFs5CHRhZQjq402EJFCD33JhzQMdDw=\ngithub.com/youtube/vitess v2.1.1+incompatible/go.mod h1:hpMim5/30F1r+0P8GGtB29d0gWHr0IZ5unS+CG0zMx8=\ngithub.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngo.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=\ngo.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=\ngo.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=\ngo.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=\ngo.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=\ngo.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=\ngo.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=\ngo.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=\ngo.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=\ngo.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=\ngo.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=\ngo.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=\ngo.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=\ngo.uber.org/zap v1.15.0/go.mod h1:Mb2vm2krFEG5DV0W9qcHBYFtp/Wku1cvYaqPsS/WYfc=\ngo.uber.org/zap v1.16.0 h1:uFRZXykJGK9lLY4HtgSw44DnIcAM+kRBP7x5m+NpAOM=\ngo.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ=\ngolang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=\ngolang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20191227163750-53104e6ec876/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20200109152110-61a87790db17/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=\ngolang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=\ngolang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=\ngolang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=\ngolang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=\ngolang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20201021035429-f5854403a974 h1:IX6qOQeG5uLjB/hjjwjedwfjND0hgjPMMyO1RoIXQNI=\ngolang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=\ngolang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20220624220833-87e55d714810 h1:rHZQSjJdAI4Xf5Qzeh2bBc5YJIkPFVM6oDtMFYmgws0=\ngolang.org/x/sys v0.0.0-20220624220833-87e55d714810/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/text v0.0.0-20171214130843-f21a4dfb5e38/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=\ngolang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20181008205924-a2b3f7f249e9/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20201125231158-b5590deeca9b/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngoogle.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=\ngoogle.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=\ngoogle.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=\ngoogle.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=\ngoogle.golang.org/genproto v0.0.0-20181004005441-af9cb2a35e7f h1:FU37niK8AQ59mHcskRyQL7H0ErSeNh650vdcj8HqdSI=\ngoogle.golang.org/genproto v0.0.0-20181004005441-af9cb2a35e7f/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=\ngoogle.golang.org/grpc v0.0.0-20180607172857-7a6a684ca69e/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=\ngoogle.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=\ngoogle.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=\ngoogle.golang.org/grpc v1.21.0 h1:G+97AoqBnmZIT91cLG/EkCoK9NSelj64P8bOHHNmGn0=\ngoogle.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=\ngopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=\ngopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=\ngopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=\ngopkg.in/gcfg.v1 v1.2.3 h1:m8OOJ4ccYHnx2f4gQwpno8nAX5OGOh7RLaaz0pj3Ogs=\ngopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=\ngopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=\ngopkg.in/ini.v1 v1.52.0 h1:j+Lt/M1oPPejkniCg1TkWE2J3Eh1oZTsHSXzMTzUXn4=\ngopkg.in/ini.v1 v1.52.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=\ngopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=\ngopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=\ngopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=\ngopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=\ngopkg.in/stretchr/testify.v1 v1.2.2/go.mod h1:QI5V/q6UbPmuhtm10CaFZxED9NreB8PnFYN9JcR6TxU=\ngopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=\ngopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=\ngopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=\ngopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=\ngopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=\ngopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\nhonnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=\nvitess.io/vitess v2.1.1+incompatible h1:nuuGHiWYWpudD3gOCLeGzol2EJ25e/u5Wer2wV1O130=\nvitess.io/vitess v2.1.1+incompatible/go.mod h1:h4qvkyNYTOC0xI+vcidSWoka0gQAZc9ZPHbkHo48gP0=\n"
  },
  {
    "path": "main.go",
    "content": "package main\n\nimport \"github.com/hanchuanchuan/bingo2sql/cmd\"\n\nfunc main() {\n\tcmd.Execute()\n}\n"
  },
  {
    "path": "parse/bingo2sql.go",
    "content": "package parse\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"os\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/hanchuanchuan/bingo2sql/core\"\n\t_ \"github.com/jinzhu/gorm/dialects/mysql\"\n\t\"github.com/labstack/echo/v4\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\n// var parserProcess map[string]*parser.MyBinlogParser\nvar parserProcess sync.Map\n\ntype ParseInfo struct {\n\tID        string `json:\"id\"`\n\tParseRows int    `json:\"parse_rows\"`\n\tPercent   int    `json:\"percent\"`\n}\n\nfunc TestMiddleware(next http.Handler) http.Handler {\n\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t// Do stuff here\n\t\tfmt.Println(\"middleware print: \", r.RequestURI)\n\t\t// Call the next handler, which can be another middleware in the chain, or the final handler.\n\t\tnext.ServeHTTP(w, r)\n\t})\n}\n\nfunc HomeHandler(w http.ResponseWriter, r *http.Request) {\n\tw.WriteHeader(http.StatusOK)\n\tfmt.Fprintf(w, \"this is home\")\n}\n\n// GetParseInfo 获取解析进程信息\nfunc GetParseInfo(c echo.Context) error {\n\tid := c.Param(\"id\")\n\n\tif len(id) == 0 {\n\t\treturn c.JSON(http.StatusOK, map[string]string{\n\t\t\t\"error\": \"无效参数!\",\n\t\t})\n\t}\n\n\tif v, ok := parserProcess.Load(id); ok {\n\t\tif p, ok := v.(*core.MyBinlogParser); ok {\n\t\t\tdata := &ParseInfo{\n\t\t\t\tID:        id,\n\t\t\t\tParseRows: p.ParseRows(),\n\t\t\t\tPercent:   p.Percent(),\n\t\t\t}\n\t\t\treturn c.JSON(http.StatusOK, data)\n\t\t}\n\t}\n\n\treturn c.JSON(http.StatusNotFound, \"Not Found!\")\n}\n\n// GetAllParse 获取所有进程信息\nfunc GetAllParse(c echo.Context) error {\n\n\t// log.Print(\"当前解析进程数量: \", len(parserProcess.Range()))\n\n\t//\n\t// for _, p := range parserProcess {\n\t// \tdata := &ParseInfo{\n\t// \t\tID:        p.Config().Id(),\n\t// \t\tParseRows: p.ParseRows(),\n\t// \t\tPercent:   p.Percent(),\n\t// \t}\n\t// \tresponse = append(response, data)\n\t// }\n\t// return c.JSON(http.StatusOK, response)\n\n\tcount := 0\n\t// response := make([]*ParseInfo, 0, len(parserProcess))\n\tresponse := make([]*ParseInfo, 0)\n\t// 遍历所有sync.Map中的键值对\n\tparserProcess.Range(func(k, v interface{}) bool {\n\t\tcount++\n\t\tif p, ok := v.(*core.MyBinlogParser); ok {\n\t\t\tdata := &ParseInfo{\n\t\t\t\tID:        p.Config().ID(),\n\t\t\t\tParseRows: p.ParseRows(),\n\t\t\t\tPercent:   p.Percent(),\n\t\t\t}\n\t\t\tresponse = append(response, data)\n\t\t}\n\t\tfmt.Println(\"iterate:\", k, v)\n\t\treturn true\n\t})\n\tlog.Print(\"当前解析进程数量: \", count)\n\treturn c.JSON(http.StatusOK, response)\n\n}\n\nfunc ParseBinlog(c echo.Context) error {\n\n\tcfg := &core.BinlogParserConfig{\n\t\tShowGTID: true,\n\t\tShowTime: true,\n\n\t\tMinimalInsert: true,\n\t\tMinimalUpdate: true,\n\t}\n\n\tif err := c.Bind(cfg); err != nil {\n\t\treturn err\n\t}\n\n\tlog.Infof(\"config: %#v\", cfg)\n\n\t// 指定默认的socket用户,用来生成SQL文件\n\tif cfg.SocketUser == \"\" {\n\t\tcfg.SocketUser = \"test\"\n\t}\n\n\t// if cfg.InsID == 0 {\n\t// \tr := map[string]string{\"error\": \"请指定数据库地址\"}\n\t// \treturn c.JSON(http.StatusOK, r)\n\t// }\n\n\tp, err := core.NewBinlogParser(context.Background(), cfg)\n\tif err != nil {\n\t\tlog.Error(\"binlog解析操作失败\")\n\t\tlog.Error(err)\n\t\tout := map[string]string{\"error\": err.Error()}\n\t\treturn c.JSON(http.StatusOK, out)\n\t}\n\n\tif err := recover(); err != nil {\n\n\t\tif e, ok := err.(error); ok {\n\t\t\tlog.Error(\"binlog解析操作失败\")\n\t\t\tlog.Error(err)\n\t\t\tout := map[string]string{\"error\": e.Error()}\n\t\t\treturn c.JSON(http.StatusOK, out)\n\t\t} else {\n\t\t\tout := map[string]string{\"error\": \"未知的错误\"}\n\t\t\treturn c.JSON(http.StatusOK, out)\n\t\t}\n\t} else {\n\t\tid := cfg.ID()\n\t\t// parserProcess[id] = p\n\t\tparserProcess.Store(id, p)\n\t\tgo func() {\n\t\t\tdefer func() {\n\t\t\t\ttime.AfterFunc(time.Minute, func() {\n\t\t\t\t\tparserProcess.Delete(id)\n\t\t\t\t})\n\t\t\t}()\n\t\t\terr := p.Parser()\n\t\t\tif err != nil {\n\t\t\t\tlog.Error(err)\n\t\t\t}\n\t\t}()\n\n\t\tr := map[string]string{\"id\": id}\n\t\treturn c.JSON(http.StatusOK, r)\n\t}\n}\n\nfunc ParseBinlogStop(c echo.Context) error {\n\tid := c.Param(\"id\")\n\n\tif len(id) == 0 {\n\t\treturn c.JSON(http.StatusOK, map[string]string{\n\t\t\t\"error\": \"无效参数!\",\n\t\t})\n\t}\n\n\tresponse := make(map[string]string)\n\n\t// log.Print(\"当前解析进程数量: \", len(parserProcess))\n\n\t// defer delete(parserProcess, id)\n\tdefer parserProcess.Delete(id)\n\n\tif v, ok := parserProcess.Load(id); ok {\n\t\tif p, ok := v.(*core.MyBinlogParser); ok {\n\t\t\tp.Stop()\n\n\t\t\tresponse[\"ok\"] = \"1\"\n\t\t\treturn c.JSON(http.StatusOK, response)\n\t\t}\n\t}\n\n\tresponse[\"ok\"] = \"2\"\n\treturn c.JSON(http.StatusOK, response)\n\n\t// if p, ok := parserProcess[id]; ok {\n\t// \tp.Stop()\n\n\t// \tresponse[\"ok\"] = \"1\"\n\t// \treturn c.JSON(http.StatusOK, response)\n\t// } else {\n\t// \tresponse[\"ok\"] = \"2\"\n\t// \treturn c.JSON(http.StatusOK, response)\n\t// }\n}\n\nfunc Download(c echo.Context) error {\n\tid := c.Param(\"id\")\n\tpath := \"files/\" + id + \".tar.gz\"\n\n\t_, err := os.Stat(path)\n\tif err != nil {\n\t\tlog.Error(err)\n\t\treturn c.JSON(http.StatusNotFound,\n\t\t\tfmt.Sprintf(\"%s no such file or directory\", id))\n\t\t// return err\n\t} else {\n\t\tlog.Infof(\"下载路径: %s\", path)\n\t\treturn c.Attachment(path, c.Param(\"name\"))\n\t}\n\n\t// return c.Inline(path, c.Param(\"name\"))\n\n\t// return c.File(path)\n}\n"
  },
  {
    "path": "parse/log.go",
    "content": "package parse\n\nimport (\n\t\"fmt\"\n\t\"path\"\n\t\"runtime\"\n\t\"strings\"\n\n\tlog \"github.com/sirupsen/logrus\"\n)\n\nvar (\n\t// BuildTS 打包时间\n\tBuildTS = \"\"\n\t// GitHash GIT提交ID\n\tGitHash = \"\"\n\t// GitBranch GIT分支\n\tGitBranch = \"\"\n\t// GoVersion GOLANG版本\n\tGoVersion = \"\"\n\t// GitVersion GIT版本\n\tGitVersion = \"\"\n)\n\nfunc init() {\n\n\tlog.SetFormatter(&log.TextFormatter{\n\t\tDisableColors: false,\n\t\tFullTimestamp: true,\n\t})\n\n\t// log.SetFormatter(&log.TextFormatter{\n\t// \tDisableColors: true,\n\t// \tFullTimestamp: true,\n\t// })\n\n\t// log.SetReportCaller(true)\n\n\t// 输出文件名和行号\n\tlog.AddHook(&contextHook{})\n}\n\n// modifyHook injects file name and line pos into log entry.\ntype contextHook struct{}\n\n// Fire implements logrus.Hook interface\n// https://github.com/sirupsen/logrus/issues/63\nfunc (hook *contextHook) Fire(entry *log.Entry) error {\n\tpc := make([]uintptr, 4)\n\tcnt := runtime.Callers(6, pc)\n\tfor i := 0; i < cnt; i++ {\n\t\tfu := runtime.FuncForPC(pc[i] - 1)\n\t\tname := fu.Name()\n\t\tif !isSkippedPackageName(name) {\n\t\t\tfile, line := fu.FileLine(pc[i] - 1)\n\t\t\tentry.Data[\"file\"] = path.Base(file)\n\t\t\tentry.Data[\"line\"] = line\n\t\t\tbreak\n\t\t}\n\t}\n\treturn nil\n}\n\n// Levels implements logrus.Hook interface.\nfunc (hook *contextHook) Levels() []log.Level {\n\treturn log.AllLevels\n}\n\n// isSKippedPackageName tests wether path name is on log library calling stack.\nfunc isSkippedPackageName(name string) bool {\n\treturn strings.Contains(name, \"github.com/sirupsen/logrus\") ||\n\t\tstrings.Contains(name, \"github.com/coreos/pkg/capnslog\")\n}\n\n// PrintVersion prints the  version information.\nfunc PrintVersion() {\n\tlog.Infof(\"Version: %s\", GitVersion)\n\tlog.Infof(\"Git Commit Hash: %s\", GitHash)\n\tlog.Infof(\"Git Branch: %s\", GitBranch)\n\tlog.Infof(\"UTC Build Time:  %s\", BuildTS)\n\tlog.Infof(\"Go Version:  %s\", GoVersion)\n}\n\n// GetInfo returns the git hash and build time of this -server binary.\nfunc GetInfo() string {\n\treturn fmt.Sprintf(\n\t\t\"Version: %v\\n\"+\n\t\t\t\"Git Commit Hash: %s\\n\"+\n\t\t\t\"Git Branch: %s\\n\"+\n\t\t\t\"UTC Build Time: %s\\n\"+\n\t\t\t\"Go Version: %s\",\n\t\tGitVersion,\n\t\tGitHash,\n\t\tGitBranch,\n\t\tBuildTS,\n\t\tGoVersion,\n\t)\n}\n"
  },
  {
    "path": "utils/uuid/codec.go",
    "content": "// Copyright (C) 2013-2018 by Maxim Bublis <b@codemonkey.ru>\n//\n// Permission is hereby granted, free of charge, to any person obtaining\n// a copy of this software and associated documentation files (the\n// \"Software\"), to deal in the Software without restriction, including\n// without limitation the rights to use, copy, modify, merge, publish,\n// distribute, sublicense, and/or sell copies of the Software, and to\n// permit persons to whom the Software is furnished to do so, subject to\n// the following conditions:\n//\n// The above copyright notice and this permission notice shall be\n// included in all copies or substantial portions of the Software.\n//\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\n// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\n// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\n// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\n// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\n// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\n// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n\npackage uuid\n\nimport (\n\t\"bytes\"\n\t\"encoding/hex\"\n\t\"fmt\"\n)\n\n// FromBytes returns UUID converted from raw byte slice input.\n// It will return error if the slice isn't 16 bytes long.\nfunc FromBytes(input []byte) (u UUID, err error) {\n\terr = u.UnmarshalBinary(input)\n\treturn\n}\n\n// FromBytesOrNil returns UUID converted from raw byte slice input.\n// Same behavior as FromBytes, but returns a Nil UUID on error.\nfunc FromBytesOrNil(input []byte) UUID {\n\tuuid, err := FromBytes(input)\n\tif err != nil {\n\t\treturn Nil\n\t}\n\treturn uuid\n}\n\n// FromString returns UUID parsed from string input.\n// Input is expected in a form accepted by UnmarshalText.\nfunc FromString(input string) (u UUID, err error) {\n\terr = u.UnmarshalText([]byte(input))\n\treturn\n}\n\n// FromStringOrNil returns UUID parsed from string input.\n// Same behavior as FromString, but returns a Nil UUID on error.\nfunc FromStringOrNil(input string) UUID {\n\tuuid, err := FromString(input)\n\tif err != nil {\n\t\treturn Nil\n\t}\n\treturn uuid\n}\n\n// MarshalText implements the encoding.TextMarshaler interface.\n// The encoding is the same as returned by String.\nfunc (u UUID) MarshalText() (text []byte, err error) {\n\ttext = []byte(u.String())\n\treturn\n}\n\n// UnmarshalText implements the encoding.TextUnmarshaler interface.\n// Following formats are supported:\n//   \"6ba7b810-9dad-11d1-80b4-00c04fd430c8\",\n//   \"{6ba7b810-9dad-11d1-80b4-00c04fd430c8}\",\n//   \"urn:uuid:6ba7b810-9dad-11d1-80b4-00c04fd430c8\"\n//   \"6ba7b8109dad11d180b400c04fd430c8\"\n// ABNF for supported UUID text representation follows:\n//   uuid := canonical | hashlike | braced | urn\n//   plain := canonical | hashlike\n//   canonical := 4hexoct '-' 2hexoct '-' 2hexoct '-' 6hexoct\n//   hashlike := 12hexoct\n//   braced := '{' plain '}'\n//   urn := URN ':' UUID-NID ':' plain\n//   URN := 'urn'\n//   UUID-NID := 'uuid'\n//   12hexoct := 6hexoct 6hexoct\n//   6hexoct := 4hexoct 2hexoct\n//   4hexoct := 2hexoct 2hexoct\n//   2hexoct := hexoct hexoct\n//   hexoct := hexdig hexdig\n//   hexdig := '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' |\n//             'a' | 'b' | 'c' | 'd' | 'e' | 'f' |\n//             'A' | 'B' | 'C' | 'D' | 'E' | 'F'\nfunc (u *UUID) UnmarshalText(text []byte) (err error) {\n\tswitch len(text) {\n\tcase 32:\n\t\treturn u.decodeHashLike(text)\n\tcase 36:\n\t\treturn u.decodeCanonical(text)\n\tcase 38:\n\t\treturn u.decodeBraced(text)\n\tcase 41:\n\t\tfallthrough\n\tcase 45:\n\t\treturn u.decodeURN(text)\n\tdefault:\n\t\treturn fmt.Errorf(\"uuid: incorrect UUID length: %s\", text)\n\t}\n}\n\n// decodeCanonical decodes UUID string in format\n// \"6ba7b810-9dad-11d1-80b4-00c04fd430c8\".\nfunc (u *UUID) decodeCanonical(t []byte) (err error) {\n\tif t[8] != '-' || t[13] != '-' || t[18] != '-' || t[23] != '-' {\n\t\treturn fmt.Errorf(\"uuid: incorrect UUID format %s\", t)\n\t}\n\n\tsrc := t[:]\n\tdst := u[:]\n\n\tfor i, byteGroup := range byteGroups {\n\t\tif i > 0 {\n\t\t\tsrc = src[1:] // skip dash\n\t\t}\n\t\t_, err = hex.Decode(dst[:byteGroup/2], src[:byteGroup])\n\t\tif err != nil {\n\t\t\treturn\n\t\t}\n\t\tsrc = src[byteGroup:]\n\t\tdst = dst[byteGroup/2:]\n\t}\n\n\treturn\n}\n\n// decodeHashLike decodes UUID string in format\n// \"6ba7b8109dad11d180b400c04fd430c8\".\nfunc (u *UUID) decodeHashLike(t []byte) (err error) {\n\tsrc := t[:]\n\tdst := u[:]\n\n\tif _, err = hex.Decode(dst, src); err != nil {\n\t\treturn err\n\t}\n\treturn\n}\n\n// decodeBraced decodes UUID string in format\n// \"{6ba7b810-9dad-11d1-80b4-00c04fd430c8}\" or in format\n// \"{6ba7b8109dad11d180b400c04fd430c8}\".\nfunc (u *UUID) decodeBraced(t []byte) (err error) {\n\tl := len(t)\n\n\tif t[0] != '{' || t[l-1] != '}' {\n\t\treturn fmt.Errorf(\"uuid: incorrect UUID format %s\", t)\n\t}\n\n\treturn u.decodePlain(t[1 : l-1])\n}\n\n// decodeURN decodes UUID string in format\n// \"urn:uuid:6ba7b810-9dad-11d1-80b4-00c04fd430c8\" or in format\n// \"urn:uuid:6ba7b8109dad11d180b400c04fd430c8\".\nfunc (u *UUID) decodeURN(t []byte) (err error) {\n\ttotal := len(t)\n\n\turn_uuid_prefix := t[:9]\n\n\tif !bytes.Equal(urn_uuid_prefix, urnPrefix) {\n\t\treturn fmt.Errorf(\"uuid: incorrect UUID format: %s\", t)\n\t}\n\n\treturn u.decodePlain(t[9:total])\n}\n\n// decodePlain decodes UUID string in canonical format\n// \"6ba7b810-9dad-11d1-80b4-00c04fd430c8\" or in hash-like format\n// \"6ba7b8109dad11d180b400c04fd430c8\".\nfunc (u *UUID) decodePlain(t []byte) (err error) {\n\tswitch len(t) {\n\tcase 32:\n\t\treturn u.decodeHashLike(t)\n\tcase 36:\n\t\treturn u.decodeCanonical(t)\n\tdefault:\n\t\treturn fmt.Errorf(\"uuid: incorrrect UUID length: %s\", t)\n\t}\n}\n\n// MarshalBinary implements the encoding.BinaryMarshaler interface.\nfunc (u UUID) MarshalBinary() (data []byte, err error) {\n\tdata = u.Bytes()\n\treturn\n}\n\n// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface.\n// It will return error if the slice isn't 16 bytes long.\nfunc (u *UUID) UnmarshalBinary(data []byte) (err error) {\n\tif len(data) != Size {\n\t\terr = fmt.Errorf(\"uuid: UUID must be exactly 16 bytes long, got %d bytes\", len(data))\n\t\treturn\n\t}\n\tcopy(u[:], data)\n\n\treturn\n}\n"
  },
  {
    "path": "utils/uuid/generator.go",
    "content": "// Copyright (C) 2013-2018 by Maxim Bublis <b@codemonkey.ru>\n//\n// Permission is hereby granted, free of charge, to any person obtaining\n// a copy of this software and associated documentation files (the\n// \"Software\"), to deal in the Software without restriction, including\n// without limitation the rights to use, copy, modify, merge, publish,\n// distribute, sublicense, and/or sell copies of the Software, and to\n// permit persons to whom the Software is furnished to do so, subject to\n// the following conditions:\n//\n// The above copyright notice and this permission notice shall be\n// included in all copies or substantial portions of the Software.\n//\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\n// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\n// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\n// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\n// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\n// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\n// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n\npackage uuid\n\nimport (\n\t\"crypto/md5\"\n\t\"crypto/rand\"\n\t\"crypto/sha1\"\n\t\"encoding/binary\"\n\t\"fmt\"\n\t\"hash\"\n\t\"io\"\n\t\"net\"\n\t\"os\"\n\t\"sync\"\n\t\"time\"\n)\n\n// Difference in 100-nanosecond intervals between\n// UUID epoch (October 15, 1582) and Unix epoch (January 1, 1970).\nconst epochStart = 122192928000000000\n\ntype epochFunc func() time.Time\ntype hwAddrFunc func() (net.HardwareAddr, error)\n\nvar (\n\tglobal = newRFC4122Generator()\n\n\tposixUID = uint32(os.Getuid())\n\tposixGID = uint32(os.Getgid())\n)\n\n// NewV1 returns UUID based on current timestamp and MAC address.\nfunc NewV1() (UUID, error) {\n\treturn global.NewV1()\n}\n\n// NewV2 returns DCE Security UUID based on POSIX UID/GID.\nfunc NewV2(domain byte) (UUID, error) {\n\treturn global.NewV2(domain)\n}\n\n// NewV3 returns UUID based on MD5 hash of namespace UUID and name.\nfunc NewV3(ns UUID, name string) UUID {\n\treturn global.NewV3(ns, name)\n}\n\n// NewV4 returns random generated UUID.\nfunc NewV4() (UUID, error) {\n\treturn global.NewV4()\n}\n\n// NewV5 returns UUID based on SHA-1 hash of namespace UUID and name.\nfunc NewV5(ns UUID, name string) UUID {\n\treturn global.NewV5(ns, name)\n}\n\n// Generator provides interface for generating UUIDs.\ntype Generator interface {\n\tNewV1() (UUID, error)\n\tNewV2(domain byte) (UUID, error)\n\tNewV3(ns UUID, name string) UUID\n\tNewV4() (UUID, error)\n\tNewV5(ns UUID, name string) UUID\n}\n\n// Default generator implementation.\ntype rfc4122Generator struct {\n\tclockSequenceOnce sync.Once\n\thardwareAddrOnce  sync.Once\n\tstorageMutex      sync.Mutex\n\n\trand io.Reader\n\n\tepochFunc     epochFunc\n\thwAddrFunc    hwAddrFunc\n\tlastTime      uint64\n\tclockSequence uint16\n\thardwareAddr  [6]byte\n}\n\nfunc newRFC4122Generator() Generator {\n\treturn &rfc4122Generator{\n\t\tepochFunc:  time.Now,\n\t\thwAddrFunc: defaultHWAddrFunc,\n\t\trand:       rand.Reader,\n\t}\n}\n\n// NewV1 returns UUID based on current timestamp and MAC address.\nfunc (g *rfc4122Generator) NewV1() (UUID, error) {\n\tu := UUID{}\n\n\ttimeNow, clockSeq, err := g.getClockSequence()\n\tif err != nil {\n\t\treturn Nil, err\n\t}\n\tbinary.BigEndian.PutUint32(u[0:], uint32(timeNow))\n\tbinary.BigEndian.PutUint16(u[4:], uint16(timeNow>>32))\n\tbinary.BigEndian.PutUint16(u[6:], uint16(timeNow>>48))\n\tbinary.BigEndian.PutUint16(u[8:], clockSeq)\n\n\thardwareAddr, err := g.getHardwareAddr()\n\tif err != nil {\n\t\treturn Nil, err\n\t}\n\tcopy(u[10:], hardwareAddr)\n\n\tu.SetVersion(V1)\n\tu.SetVariant(VariantRFC4122)\n\n\treturn u, nil\n}\n\n// NewV2 returns DCE Security UUID based on POSIX UID/GID.\nfunc (g *rfc4122Generator) NewV2(domain byte) (UUID, error) {\n\tu, err := g.NewV1()\n\tif err != nil {\n\t\treturn Nil, err\n\t}\n\n\tswitch domain {\n\tcase DomainPerson:\n\t\tbinary.BigEndian.PutUint32(u[:], posixUID)\n\tcase DomainGroup:\n\t\tbinary.BigEndian.PutUint32(u[:], posixGID)\n\t}\n\n\tu[9] = domain\n\n\tu.SetVersion(V2)\n\tu.SetVariant(VariantRFC4122)\n\n\treturn u, nil\n}\n\n// NewV3 returns UUID based on MD5 hash of namespace UUID and name.\nfunc (g *rfc4122Generator) NewV3(ns UUID, name string) UUID {\n\tu := newFromHash(md5.New(), ns, name)\n\tu.SetVersion(V3)\n\tu.SetVariant(VariantRFC4122)\n\n\treturn u\n}\n\n// NewV4 returns random generated UUID.\nfunc (g *rfc4122Generator) NewV4() (UUID, error) {\n\tu := UUID{}\n\tif _, err := io.ReadFull(g.rand, u[:]); err != nil {\n\t\treturn Nil, err\n\t}\n\tu.SetVersion(V4)\n\tu.SetVariant(VariantRFC4122)\n\n\treturn u, nil\n}\n\n// NewV5 returns UUID based on SHA-1 hash of namespace UUID and name.\nfunc (g *rfc4122Generator) NewV5(ns UUID, name string) UUID {\n\tu := newFromHash(sha1.New(), ns, name)\n\tu.SetVersion(V5)\n\tu.SetVariant(VariantRFC4122)\n\n\treturn u\n}\n\n// Returns epoch and clock sequence.\nfunc (g *rfc4122Generator) getClockSequence() (uint64, uint16, error) {\n\tvar err error\n\tg.clockSequenceOnce.Do(func() {\n\t\tbuf := make([]byte, 2)\n\t\tif _, err = io.ReadFull(g.rand, buf); err != nil {\n\t\t\treturn\n\t\t}\n\t\tg.clockSequence = binary.BigEndian.Uint16(buf)\n\t})\n\tif err != nil {\n\t\treturn 0, 0, err\n\t}\n\n\tg.storageMutex.Lock()\n\tdefer g.storageMutex.Unlock()\n\n\ttimeNow := g.getEpoch()\n\t// Clock didn't change since last UUID generation.\n\t// Should increase clock sequence.\n\tif timeNow <= g.lastTime {\n\t\tg.clockSequence++\n\t}\n\tg.lastTime = timeNow\n\n\treturn timeNow, g.clockSequence, nil\n}\n\n// Returns hardware address.\nfunc (g *rfc4122Generator) getHardwareAddr() ([]byte, error) {\n\tvar err error\n\tg.hardwareAddrOnce.Do(func() {\n\t\tif hwAddr, err := g.hwAddrFunc(); err == nil {\n\t\t\tcopy(g.hardwareAddr[:], hwAddr)\n\t\t\treturn\n\t\t}\n\n\t\t// Initialize hardwareAddr randomly in case\n\t\t// of real network interfaces absence.\n\t\tif _, err = io.ReadFull(g.rand, g.hardwareAddr[:]); err != nil {\n\t\t\treturn\n\t\t}\n\t\t// Set multicast bit as recommended by RFC 4122\n\t\tg.hardwareAddr[0] |= 0x01\n\t})\n\tif err != nil {\n\t\treturn []byte{}, err\n\t}\n\treturn g.hardwareAddr[:], nil\n}\n\n// Returns difference in 100-nanosecond intervals between\n// UUID epoch (October 15, 1582) and current time.\nfunc (g *rfc4122Generator) getEpoch() uint64 {\n\treturn epochStart + uint64(g.epochFunc().UnixNano()/100)\n}\n\n// Returns UUID based on hashing of namespace UUID and name.\nfunc newFromHash(h hash.Hash, ns UUID, name string) UUID {\n\tu := UUID{}\n\th.Write(ns[:])\n\th.Write([]byte(name))\n\tcopy(u[:], h.Sum(nil))\n\n\treturn u\n}\n\n// Returns hardware address.\nfunc defaultHWAddrFunc() (net.HardwareAddr, error) {\n\tifaces, err := net.Interfaces()\n\tif err != nil {\n\t\treturn []byte{}, err\n\t}\n\tfor _, iface := range ifaces {\n\t\tif len(iface.HardwareAddr) >= 6 {\n\t\t\treturn iface.HardwareAddr, nil\n\t\t}\n\t}\n\treturn []byte{}, fmt.Errorf(\"uuid: no HW address found\")\n}\n"
  },
  {
    "path": "utils/uuid/sql.go",
    "content": "// Copyright (C) 2013-2018 by Maxim Bublis <b@codemonkey.ru>\n//\n// Permission is hereby granted, free of charge, to any person obtaining\n// a copy of this software and associated documentation files (the\n// \"Software\"), to deal in the Software without restriction, including\n// without limitation the rights to use, copy, modify, merge, publish,\n// distribute, sublicense, and/or sell copies of the Software, and to\n// permit persons to whom the Software is furnished to do so, subject to\n// the following conditions:\n//\n// The above copyright notice and this permission notice shall be\n// included in all copies or substantial portions of the Software.\n//\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\n// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\n// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\n// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\n// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\n// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\n// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n\npackage uuid\n\nimport (\n\t\"database/sql/driver\"\n\t\"fmt\"\n)\n\n// Value implements the driver.Valuer interface.\nfunc (u UUID) Value() (driver.Value, error) {\n\treturn u.String(), nil\n}\n\n// Scan implements the sql.Scanner interface.\n// A 16-byte slice is handled by UnmarshalBinary, while\n// a longer byte slice or a string is handled by UnmarshalText.\nfunc (u *UUID) Scan(src interface{}) error {\n\tswitch src := src.(type) {\n\tcase []byte:\n\t\tif len(src) == Size {\n\t\t\treturn u.UnmarshalBinary(src)\n\t\t}\n\t\treturn u.UnmarshalText(src)\n\n\tcase string:\n\t\treturn u.UnmarshalText([]byte(src))\n\t}\n\n\treturn fmt.Errorf(\"uuid: cannot convert %T to UUID\", src)\n}\n\n// NullUUID can be used with the standard sql package to represent a\n// UUID value that can be NULL in the database\ntype NullUUID struct {\n\tUUID  UUID\n\tValid bool\n}\n\n// Value implements the driver.Valuer interface.\nfunc (u NullUUID) Value() (driver.Value, error) {\n\tif !u.Valid {\n\t\treturn nil, nil\n\t}\n\t// Delegate to UUID Value function\n\treturn u.UUID.Value()\n}\n\n// Scan implements the sql.Scanner interface.\nfunc (u *NullUUID) Scan(src interface{}) error {\n\tif src == nil {\n\t\tu.UUID, u.Valid = Nil, false\n\t\treturn nil\n\t}\n\n\t// Delegate to UUID Scan function\n\tu.Valid = true\n\treturn u.UUID.Scan(src)\n}\n"
  },
  {
    "path": "utils/uuid/uuid.go",
    "content": "// Copyright (C) 2013-2018 by Maxim Bublis <b@codemonkey.ru>\n//\n// Permission is hereby granted, free of charge, to any person obtaining\n// a copy of this software and associated documentation files (the\n// \"Software\"), to deal in the Software without restriction, including\n// without limitation the rights to use, copy, modify, merge, publish,\n// distribute, sublicense, and/or sell copies of the Software, and to\n// permit persons to whom the Software is furnished to do so, subject to\n// the following conditions:\n//\n// The above copyright notice and this permission notice shall be\n// included in all copies or substantial portions of the Software.\n//\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\n// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\n// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\n// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\n// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\n// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\n// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n\n// Package uuid provides implementation of Universally Unique Identifier (UUID).\n// Supported versions are 1, 3, 4 and 5 (as specified in RFC 4122) and\n// version 2 (as specified in DCE 1.1).\npackage uuid\n\nimport (\n\t\"bytes\"\n\t\"encoding/hex\"\n)\n\n// Size of a UUID in bytes.\nconst Size = 16\n\n// UUID representation compliant with specification\n// described in RFC 4122.\ntype UUID [Size]byte\n\n// UUID versions\nconst (\n\t_ byte = iota\n\tV1\n\tV2\n\tV3\n\tV4\n\tV5\n)\n\n// UUID layout variants.\nconst (\n\tVariantNCS byte = iota\n\tVariantRFC4122\n\tVariantMicrosoft\n\tVariantFuture\n)\n\n// UUID DCE domains.\nconst (\n\tDomainPerson = iota\n\tDomainGroup\n\tDomainOrg\n)\n\n// String parse helpers.\nvar (\n\turnPrefix  = []byte(\"urn:uuid:\")\n\tbyteGroups = []int{8, 4, 4, 4, 12}\n)\n\n// Nil is special form of UUID that is specified to have all\n// 128 bits set to zero.\nvar Nil = UUID{}\n\n// Predefined namespace UUIDs.\nvar (\n\tNamespaceDNS  = Must(FromString(\"6ba7b810-9dad-11d1-80b4-00c04fd430c8\"))\n\tNamespaceURL  = Must(FromString(\"6ba7b811-9dad-11d1-80b4-00c04fd430c8\"))\n\tNamespaceOID  = Must(FromString(\"6ba7b812-9dad-11d1-80b4-00c04fd430c8\"))\n\tNamespaceX500 = Must(FromString(\"6ba7b814-9dad-11d1-80b4-00c04fd430c8\"))\n)\n\n// Equal returns true if u1 and u2 equals, otherwise returns false.\nfunc Equal(u1 UUID, u2 UUID) bool {\n\treturn bytes.Equal(u1[:], u2[:])\n}\n\n// Version returns algorithm version used to generate UUID.\nfunc (u UUID) Version() byte {\n\treturn u[6] >> 4\n}\n\n// Variant returns UUID layout variant.\nfunc (u UUID) Variant() byte {\n\tswitch {\n\tcase (u[8] >> 7) == 0x00:\n\t\treturn VariantNCS\n\tcase (u[8] >> 6) == 0x02:\n\t\treturn VariantRFC4122\n\tcase (u[8] >> 5) == 0x06:\n\t\treturn VariantMicrosoft\n\tcase (u[8] >> 5) == 0x07:\n\t\tfallthrough\n\tdefault:\n\t\treturn VariantFuture\n\t}\n}\n\n// Bytes returns bytes slice representation of UUID.\nfunc (u UUID) Bytes() []byte {\n\treturn u[:]\n}\n\n// Returns canonical string representation of UUID:\n// xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx.\nfunc (u UUID) String() string {\n\tbuf := make([]byte, 36)\n\n\thex.Encode(buf[0:8], u[0:4])\n\tbuf[8] = '-'\n\thex.Encode(buf[9:13], u[4:6])\n\tbuf[13] = '-'\n\thex.Encode(buf[14:18], u[6:8])\n\tbuf[18] = '-'\n\thex.Encode(buf[19:23], u[8:10])\n\tbuf[23] = '-'\n\thex.Encode(buf[24:], u[10:])\n\n\treturn string(buf)\n}\n\n// SetVersion sets version bits.\nfunc (u *UUID) SetVersion(v byte) {\n\tu[6] = (u[6] & 0x0f) | (v << 4)\n}\n\n// SetVariant sets variant bits.\nfunc (u *UUID) SetVariant(v byte) {\n\tswitch v {\n\tcase VariantNCS:\n\t\tu[8] = (u[8]&(0xff>>1) | (0x00 << 7))\n\tcase VariantRFC4122:\n\t\tu[8] = (u[8]&(0xff>>2) | (0x02 << 6))\n\tcase VariantMicrosoft:\n\t\tu[8] = (u[8]&(0xff>>3) | (0x06 << 5))\n\tcase VariantFuture:\n\t\tfallthrough\n\tdefault:\n\t\tu[8] = (u[8]&(0xff>>3) | (0x07 << 5))\n\t}\n}\n\n// Must is a helper that wraps a call to a function returning (UUID, error)\n// and panics if the error is non-nil. It is intended for use in variable\n// initializations such as\n//\tvar packageUUID = uuid.Must(uuid.FromString(\"123e4567-e89b-12d3-a456-426655440000\"));\nfunc Must(u UUID, err error) UUID {\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn u\n}\n"
  }
]