diff --git a/README.md b/README.md index 9182dff..54e4959 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,27 @@ PluginName 字面值必须与 sqle 主仓 `sqle/sqle/driver/v2/util.go` 中的 ` make docker_install ``` +> 注:当前 `go.mod` 内 `replace ../sqle` / `replace ../sqle-pg-plugin` 是为了让本地开发 +> 沙箱(无内网 GOPROXY、无法拉 actiontech 私有仓库 pseudo-version)也能直接 +> `go build`。在 CI 与正式发布环境(具备内网 GOPROXY 可拉到锁定 tag)下,请在 +> code_review 阶段或 vendor 落盘前**移除这两条 replace**——require 块的版本号已是 +> 生产形态。 + +### 开发期沙箱本地构建(无内网 GOPROXY 时) + +```bash +docker run --rm --network host \ + -v /path/to/workspace:/workspace \ + -e GOPROXY="https://goproxy.cn,direct" \ + -e GOSUMDB=off \ + -w /workspace/sqle-gaussdb-plugin \ + golang:1.19.6 sh -c " + git config --global --add safe.directory /workspace/sqle-gaussdb-plugin && \ + GOOS=linux GOARCH=amd64 go build -o bin/sqle-gaussdb-plugin ./cmd/sqle-gaussdb-plugin && \ + GOOS=linux GOARCH=amd64 go build -o bin/sqle-opengauss-plugin ./cmd/sqle-opengauss-plugin + " +``` + 构建产物(位于 `./bin/`): ``` diff --git a/go.mod b/go.mod index bff7bcd..8505c6f 100644 --- a/go.mod +++ b/go.mod @@ -33,7 +33,7 @@ require ( github.com/mattn/go-isatty v0.0.20 // indirect github.com/mitchellh/go-testing-interface v1.14.0 // indirect github.com/oklog/run v1.0.0 // indirect - github.com/pkg/errors v0.9.1 // indirect + github.com/pkg/errors v0.9.1 golang.org/x/crypto v0.42.0 // indirect golang.org/x/net v0.44.0 // indirect golang.org/x/sys v0.44.0 // indirect @@ -43,9 +43,10 @@ require ( google.golang.org/protobuf v1.34.2 // indirect ) +require github.com/DATA-DOG/go-sqlmock v1.5.0 + require ( github.com/360EntSecGroup-Skylar/excelize v1.4.1 // indirect - github.com/DATA-DOG/go-sqlmock v1.5.0 // indirect github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d // indirect github.com/actiontech/dms v0.0.0-20260520024857-5bc318ea23da // indirect github.com/bwmarrin/snowflake v0.3.0 // indirect @@ -103,10 +104,15 @@ require ( // slices 标准包的更新版本(典型崩溃版本:crypto v0.42.0 / sys v0.44.0)。版本号来自 // sqle-pg-plugin/go.sum 实测。 replace ( - github.com/actiontech/dms => github.com/actiontech/dms v0.0.0-20251027081421-309bc24335ca github.com/actiontech/sqle => ../sqle github.com/actiontech/sqle-pg-plugin => ../sqle-pg-plugin + + // sqle 主仓 vendor 内已锁定 sjjian/parser fork(含 parser.Token 类型扩展), + // 本仓库 module 必须同步该 replace 才能让 ../sqle 内部的 mysql/splitter 包编译过。 + // 版本号取自 sqle-pg-plugin/go.mod line 79。 + github.com/pingcap/parser => github.com/sjjian/parser v0.0.0-20240704052347-b6199b7bccae + golang.org/x/crypto => golang.org/x/crypto v0.14.0 golang.org/x/net => golang.org/x/net v0.17.0 golang.org/x/sync => golang.org/x/sync v0.1.0 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..2554413 --- /dev/null +++ b/go.sum @@ -0,0 +1,929 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.81.0 h1:at8Tk2zUz63cLPR0JPWm5vp77pEZmzxEQBEfRKn1VV8= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.4.0/go.mod h1:ZusYJWlOshgSBGbt6K3GnB3MT3H1xs2id9+TCl4fDBA= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.10.0 h1:STgFzyU5/8miMl0//zKh2aQeTyeaUH3WN9bSUiJ09bA= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +gitee.com/opengauss/openGauss-connector-go-pq v1.0.7 h1:plLidoldV5RfMU6i/I+tvRKtP3sfDyUzQ//HGXLLsZo= +gitee.com/opengauss/openGauss-connector-go-pq v1.0.7/go.mod h1:2UEp+ug6ls6C0pLfZgBn7VBzBntFUzxJuy+6FlQ7qyI= +github.com/360EntSecGroup-Skylar/excelize v1.4.1 h1:l55mJb6rkkaUzOpSsgEeKYtS6/0gHwBYyfo5Jcjv/Ks= +github.com/360EntSecGroup-Skylar/excelize v1.4.1/go.mod h1:vnax29X2usfl7HHkBrX5EvSCJcmH3dT9luvxzu8iGAE= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= +github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60= +github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= +github.com/Jeffail/gabs/v2 v2.5.1/go.mod h1:xCn81vdHKxFUuWWAaD5jCTQDNPBMh5pPs9IJ+NcziBI= +github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= +github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc= +github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d h1:G0m3OIz70MZUWq3EgK3CesDbo8upS2Vm9/P3FtgI+Jk= +github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= +github.com/VividCortex/ewma v1.1.1 h1:MnEK4VOv6n0RSY4vtRe3h11qjxL3+t0B8yOL8iMXdcM= +github.com/VividCortex/ewma v1.1.1/go.mod h1:2Tkkvm3sRDVXaiyucHiACn4cqf7DpdyLvmxzcbUokwA= +github.com/actiontech/dms v0.0.0-20251027081421-309bc24335ca h1:GE+leuN+4bIG0sOFtEv6kHIo02huMebch5j17fe/0Pc= +github.com/actiontech/dms v0.0.0-20251027081421-309bc24335ca/go.mod h1:Jm+tDoBYmX8R8zqgPJRq1NwZXdxO/F+HxVRJVF65WXA= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/antihax/optional v0.0.0-20180407024304-ca021399b1a6/go.mod h1:V8iCPQYkqmusNa815XgQio277wI47sdRh1dUOLdyC6Q= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/appleboy/gin-jwt/v2 v2.6.3/go.mod h1:MfPYA4ogzvOcVkRwAxT7quHOtQmVKDpTwxyUrC2DNw0= +github.com/appleboy/gofight/v2 v2.1.2/go.mod h1:frW+U1QZEdDgixycTj4CygQ48yLTUhplt43+Wczp3rw= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/aws/aws-sdk-go v1.26.1/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/aws/aws-sdk-go v1.30.24/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= +github.com/aws/aws-sdk-go v1.34.2 h1:9vCknCdTAmmV4ht7lPuda7aJXzllXwEQyCMZKJHjBrM= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/blacktear23/go-proxyprotocol v0.0.0-20180807104634-af7a81e8dd0d/go.mod h1:VKt7CNAQxpFpSDz3sXyj9hY/GbVsQCr0sB3w59nE7lU= +github.com/bwmarrin/snowflake v0.3.0 h1:xm67bEhkKh6ij1790JB83OujPR5CzNe8QuQqAgISZN0= +github.com/bwmarrin/snowflake v0.3.0/go.mod h1:NdZxfVWX+oR6y2K0o6qAYv6gIOP9rjG0/E9WsDpxqwE= +github.com/cenkalti/backoff/v4 v4.0.2/go.mod h1:eEew/i+1Q6OrCDZh3WiXYv3+nJwBASZ8Bog/87DQnVg= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cheggaaa/pb/v3 v3.0.1/go.mod h1:SqqeMF/pMOIu3xgGoxtPYhMNQP258xE4x/XRTYua+KU= +github.com/cheggaaa/pb/v3 v3.0.4 h1:QZEPYOj2ix6d5oEg63fbHmpolrnNiwjUsk+h74Yt4bM= +github.com/cheggaaa/pb/v3 v3.0.4/go.mod h1:7rgWxLrAUcFMkvJuv09+DYi7mMUYi8nO9iOWcvGJPfw= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20171208011716-f6d7a1f6fbf3/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= +github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= +github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= +github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= +github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= +github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20181031085051-9002847aa142/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f h1:JOrtw2xFKzlg+cbHpyrpLDmnN1HqhBfnX7WDiW7eG2c= +github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f h1:lBNOc5arjvs8E5mO2tbpBpLoyyu8B6e44T7hJy6potg= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/corona10/goimagehash v1.0.2/go.mod h1:/l9umBhvcHQXVtQO1V6Gp1yD20STawkhRnnX0D1bvVI= +github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/cznic/golex v0.0.0-20181122101858-9c343928389c/go.mod h1:+bmmJDNmKlhWNG+gwWCkaBoTy39Fs+bzRxVBzoTQbIc= +github.com/cznic/mathutil v0.0.0-20181122101859-297441e03548 h1:iwZdTE0PVqJCos1vaoKsclOGD3ADKpshg3SRtYBbwso= +github.com/cznic/mathutil v0.0.0-20181122101859-297441e03548/go.mod h1:e6NPNENfs9mPDVNRekM7lKScauxd5kXTr1Mfyig6TDM= +github.com/cznic/parser v0.0.0-20160622100904-31edd927e5b1/go.mod h1:2B43mz36vGZNZEwkWi8ayRSSUXLfjL8OkbzwW4NcPMM= +github.com/cznic/sortutil v0.0.0-20181122101858-f5f958428db8 h1:LpMLYGyy67BoAFGda1NeOBQwqlv7nUXpm+rIVHGxZZ4= +github.com/cznic/sortutil v0.0.0-20181122101858-f5f958428db8/go.mod h1:q2w6Bg5jeox1B+QkJ6Wp/+Vn0G/bo3f1uY7Fn3vivIQ= +github.com/cznic/strutil v0.0.0-20171016134553-529a34b1c186/go.mod h1:AHHPPPXTw0h6pVabbcbyGRK1DckRn7r/STdZEeIDzZc= +github.com/cznic/y v0.0.0-20170802143616-045f81c6662a/go.mod h1:1rk5VM7oSnA4vjp+hrLQ3HWHa+Y4yPCa3/CsJrcNnvs= +github.com/danjacques/gofslock v0.0.0-20191023191349-0a45f885bc37/go.mod h1:DC3JtzuG7kxMvJ6dZmf2ymjNyoXwgtklr7FN+Um2B0U= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= +github.com/denisenkom/go-mssqldb v0.9.0 h1:RSohk2RsiZqLZ0zCjtfn3S4Gp4exhpBWHyQ7D0yGjAk= +github.com/denisenkom/go-mssqldb v0.9.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= +github.com/dgraph-io/ristretto v0.0.1 h1:cJwdnj42uV8Jg4+KLrYovLiCgIfz9wtWm6E6KA+1tLs= +github.com/dgraph-io/ristretto v0.0.1/go.mod h1:T40EBc7CJke8TkpiYfGGKAeFjSaxuFXhuXRyumBd6RE= +github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA= +github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dustin/go-humanize v0.0.0-20180421182945-02af3965c54e/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM= +github.com/elazarl/go-bindata-assetfs v1.0.0/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= +github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94= +github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsouza/fake-gcs-server v1.15.0/go.mod h1:HNxAJ/+FY/XSsxuwz8iIYdp2GtMmPbJ8WQjjGMxd6Qk= +github.com/fsouza/fake-gcs-server v1.17.0/go.mod h1:D1rTE4YCyHFNa99oyJJ5HyclvN/0uQR+pM/VdlL83bw= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32/go.mod h1:GIjDIg/heH5DOkXY3YJ/wNhfHsQHoXGjl8G8amsYQ1I= +github.com/gin-contrib/gzip v0.0.1/go.mod h1:fGBJBCdt6qCZuCAOwWuFhBB4OOq9EFqlo5dEaFhhu5w= +github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= +github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.3.0/go.mod h1:7cKuhb5qV2ggCFctp2fJQ+ErvciLZrIeoOSOm6mUr7Y= +github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM= +github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do= +github.com/go-bindata/go-bindata/v3 v3.1.3/go.mod h1:1/zrpXsLD8YDIbhZRqXzm1Ghc7NhEvIN9+Z6R5/xH4I= +github.com/go-chi/chi v4.0.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= +github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI= +github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM= +github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= +github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= +github.com/go-openapi/jsonreference v0.19.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= +github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= +github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= +github.com/go-openapi/spec v0.19.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= +github.com/go-openapi/spec v0.19.4/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= +github.com/go-openapi/spec v0.19.7/go.mod h1:Hm2Jr4jv8G1ciIAo+frC/Ft+rR2kQDh8JHKHb3gWUSk= +github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= +github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.8/go.mod h1:ao+8BpOPyKdpQz3AOJfbeEVpLmWAvlT1IfTe5McPyhY= +github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/overalls v0.0.0-20180201144345-22ec1a223b7c/go.mod h1:UqxAgEOt89sCiXlrc/ycnx00LVvUO/eS8tMUkWX4R7w= +github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc= +github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/goccy/go-graphviz v0.0.5/go.mod h1:wXVsXxmyMQU6TN3zGRttjNn3h+iCAS7xQFC6TlNvLhk= +github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw= +github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gogo/protobuf v0.0.0-20180717141946-636bf0302bc9/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= +github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY= +github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20181024230925-c65c006176ff/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/protobuf v0.0.0-20180814211427-aa810b61a9c7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190930153522-6ce02741cba3/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20200407044318-7d83b28da2e9 h1:K+lX49/3eURCE1IjlaZN//u6c+9nfDAMnyQ9E2dsJbY= +github.com/google/pprof v0.0.0-20200407044318-7d83b28da2e9/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/shlex v0.0.0-20181106134648-c34317bd91bf/go.mod h1:RpwtwJQFrIEPstU94h88MWPXP2ektJZ8cZ0YntAmXiE= +github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/gorilla/handlers v1.4.2/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= +github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/websocket v1.2.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-middleware v1.1.0 h1:THDBEeQ9xZ8JEaCLyLQqXMMdRqNr0QAUJTIkQAUtFjg= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/grpc-ecosystem/grpc-gateway v1.12.1/go.mod h1:8XEsbTttt/W+VvjtQhLACqCisSPWTxCZ7sBRjU6iH9c= +github.com/grpc-ecosystem/grpc-gateway v1.14.3/go.mod h1:6CwZWGDSPRJidgKAtJVvND6soZe6fT7iteq8wDPdhb0= +github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/gtank/cryptopasta v0.0.0-20170601214702-1f550f6f2f69/go.mod h1:YLEMZOtU+AZ7dhN9T/IpGhXVGly2bvkJQ+zxj3WeVQo= +github.com/hashicorp/go-hclog v0.14.1 h1:nQcJDQwIAGnmoUWp8ubocEX40cCml/17YkF6csQLReU= +github.com/hashicorp/go-hclog v0.14.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= +github.com/hashicorp/go-plugin v1.4.2 h1:yFvG3ufXXpqiMiZx9HLcaK3XbIqQ1WJFR/F1a2CuVw0= +github.com/hashicorp/go-plugin v1.4.2/go.mod h1:5fGEH17QVwTTcR0zV7yhDPLLmFX9YSZ38b18Udy6vYQ= +github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= +github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb h1:b5rjCoWHc7eqmAS4/qyk21ZsHyb6Mxv/jykxvNTkU4M= +github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/hypnoglow/gormzap v0.3.0/go.mod h1:5Wom8B7Jl2oK0Im9hs6KQ+Kl92w4Y7gKCrj66rhyvw0= +github.com/iancoleman/strcase v0.0.0-20191112232945-16388991a334/go.mod h1:SK73tn/9oHe+/Y0h39VT4UCxmurVJkR5NA7kMEAOgSE= +github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= +github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= +github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= +github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= +github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= +github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= +github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= +github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= +github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY= +github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= +github.com/jackc/pgconn v1.10.0 h1:4EYhlDVEMsJ30nNj0mmgwIUXoq7e9sMJrVC2ED6QlCU= +github.com/jackc/pgconn v1.10.0/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= +github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= +github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= +github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= +github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c= +github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc= +github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= +github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= +github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= +github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= +github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= +github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgproto3/v2 v2.1.1 h1:7PQ/4gLoqnl87ZxL7xjO0DR5gYuviDCZxQJsUlFW1eI= +github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg= +github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= +github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= +github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= +github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= +github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM= +github.com/jackc/pgtype v1.8.1 h1:9k0IXtdJXHJbyAWQgbWr1lU+MEhPXZz6RIXxfR5oxXs= +github.com/jackc/pgtype v1.8.1/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= +github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= +github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= +github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= +github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= +github.com/jackc/pgx/v4 v4.13.0 h1:JCjhT5vmhMAf/YwBHLvrBn4OGdIQBiFG6ym8Zmdx570= +github.com/jackc/pgx/v4 v4.13.0/go.mod h1:9P4X524sErlaxj0XSGZk7s+LD0eOyu1ZDUrrpznYDF0= +github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jhump/protoreflect v1.6.0 h1:h5jfMVslIg6l29nsMs0D8Wj17RDVdNYti0vDN/PZZoE= +github.com/jhump/protoreflect v1.6.0/go.mod h1:eaTn3RZAmMBcV0fifFvlm6VHNz3wSkYyXYWUh7ymB74= +github.com/jinzhu/gorm v1.9.12/go.mod h1:vhTjlKSJUTWNtcbQtrMBFCxy7eXTzeCAzfL5fBZT/Qs= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= +github.com/jonboulle/clockwork v0.1.0 h1:VKV+ZcuP6l3yW9doeqz6ziZGgcynBVQO+obU0+0hcPo= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/joomcode/errorx v1.0.1/go.mod h1:kgco15ekB6cs+4Xjzo7SPeXzx38PbJzBwbnu9qfVNHQ= +github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1 h1:6QPYqodiu3GuPL+7mfx+NwDdp2eTkp9IfEUpgAwUN0o= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/juju/ratelimit v1.0.1 h1:+7AIFJVQ0EQgq/K9+0Krm7m530Du7tIz0METWzN0RgY= +github.com/juju/ratelimit v1.0.1/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kevinburke/go-bindata v3.18.0+incompatible/go.mod h1:/pEEZ72flUW2p0yi30bslSp9YqD9pysLxunQDdb2CPM= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/cpuid v0.0.0-20170728055534-ae7887de9fa5/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +github.com/klauspost/cpuid v1.2.0 h1:NMpwD2G9JSFOE1/TJjGSo5zG7Yb2bTe7eq1jH+irmeE= +github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= +github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/labstack/echo/v4 v4.10.2 h1:n1jAhnq/elIFTHr1EYpiYtyKgx4RW9ccVgkqByZaN2M= +github.com/labstack/echo/v4 v4.10.2/go.mod h1:OEyqf2//K1DFdE57vw2DRgWY0M7s65IVQO2FzvI4J5k= +github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8= +github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM= +github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8= +github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.1/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= +github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= +github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= +github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= +github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= +github.com/mattn/go-sqlite3 v2.0.1+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/mgechev/dots v0.0.0-20190921121421-c36f7dcfbb81/go.mod h1:KQ7+USdGKfpPjXk4Ga+5XxQM4Lm4e3gAogrreFAYpOg= +github.com/mgechev/revive v1.0.2/go.mod h1:rb0dQy1LVAxW9SWy5R3LPUjevzUbUS316U5MFySA2lo= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/go-testing-interface v1.14.0 h1:/x0XQ6h+3U3nAyk1yx+bHPURrKa9sVVvYbuqZ7pIAtI= +github.com/mitchellh/go-testing-interface v1.14.0/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= +github.com/montanaflynn/stats v0.0.0-20151014174947-eeaced052adb/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= +github.com/montanaflynn/stats v0.0.0-20180911141734-db72e6cae808/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= +github.com/montanaflynn/stats v0.5.0/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/nfnt/resize v0.0.0-20160724205520-891127d8d1b5/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= +github.com/ngaut/pools v0.0.0-20180318154953-b7bc8c42aac7 h1:7KAv7KMGTTqSmYZtNdcNTgsos+vFzULLwyElndwn+5c= +github.com/ngaut/pools v0.0.0-20180318154953-b7bc8c42aac7/go.mod h1:iWMfgwqYW+e8n5lC/jjNEhwcjbRDpl5NT7n2h+4UNcI= +github.com/ngaut/sync2 v0.0.0-20141008032647-7a24ed77b2ef h1:K0Fn+DoFqNqktdZtdV3bPQ/0cuYh2H4rkg0tytX/07k= +github.com/ngaut/sync2 v0.0.0-20141008032647-7a24ed77b2ef/go.mod h1:7WjlapSfwQyo6LNmIvEWzsW1hbBQfpUO4JWnuQRmva8= +github.com/nicksnyder/go-i18n v1.10.0/go.mod h1:HrK7VCrbOvQoUAQ7Vpy7i87N7JZZZ7R2xBGjv0j365Q= +github.com/nicksnyder/go-i18n/v2 v2.4.0 h1:3IcvPOAvnCKwNm0TB0dLDTuawWEj+ax/RERNC+diLMM= +github.com/nicksnyder/go-i18n/v2 v2.4.0/go.mod h1:nxYSZE9M0bf3Y70gPQjN9ha7XNHX7gMc814+6wVyEI4= +github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= +github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= +github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.4.2/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.8.1/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA= +github.com/opentracing/basictracer-go v1.0.0 h1:YyUAhaEfjoWXclZVJ9sGoNct7j4TVk7lZWlQw5UXuoo= +github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74= +github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU= +github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pelletier/go-toml v1.3.0/go.mod h1:PN7xzY2wHTK0K9p34ErDQMlFxa51Fk0OUruD3k1mMwo= +github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5/go.mod h1:jvVRKCrJTQWu0XVbaOlby/2lO20uSCHEMzzplHXte1o= +github.com/phf/go-queue v0.0.0-20170504031614-9abe38d0371d/go.mod h1:lXfE4PvvTW5xOjO6Mba8zDPyw8M93B6AQ7frTGnMlA8= +github.com/pingcap-incubator/tidb-dashboard v0.0.0-20200407064406-b2b8ad403d01/go.mod h1:77fCh8d3oKzC5ceOJWeZXAS/mLzVgdZ7rKniwmOyFuo= +github.com/pingcap-incubator/tidb-dashboard v0.0.0-20200514075710-eecc9a4525b5/go.mod h1:8q+yDx0STBPri8xS4A2duS1dAf+xO0cMtjwe0t6MWJk= +github.com/pingcap/br v0.0.0-20200426093517-dd11ae28b885/go.mod h1:4w3meMnk7HDNpNgjuRAxavruTeKJvUiXxoEWTjzXPnA= +github.com/pingcap/br v0.0.0-20200521085655-53201addd4ad/go.mod h1:SlSUHWY7QUoooiYxOKuJ8kUh2KjI29ogBh89YXz2dLA= +github.com/pingcap/br v0.0.0-20200623060633-439a1c2b2bfd h1:vEoTsslkTbSiMMAY8XHsI/D0gih8y/kOPQytXYgc7t0= +github.com/pingcap/br v0.0.0-20200623060633-439a1c2b2bfd/go.mod h1:NGee2H9vXLunFIBXGb3uFsWRpw3BBo822sY4dyXepqo= +github.com/pingcap/check v0.0.0-20190102082844-67f458068fc8/go.mod h1:B1+S9LNcuMyLH/4HMTViQOJevkGiik3wW2AN9zb2fNQ= +github.com/pingcap/check v0.0.0-20191107115940-caf2b9e6ccf4/go.mod h1:PYMCGwN0JHjoqGr3HrZoD+b8Tgx8bKnArhSq8YVzUMc= +github.com/pingcap/check v0.0.0-20191216031241-8a5a85928f12/go.mod h1:PYMCGwN0JHjoqGr3HrZoD+b8Tgx8bKnArhSq8YVzUMc= +github.com/pingcap/check v0.0.0-20200212061837-5e12011dc712 h1:R8gStypOBmpnHEx1qi//SaqxJVI4inOqljg/Aj5/390= +github.com/pingcap/check v0.0.0-20200212061837-5e12011dc712/go.mod h1:PYMCGwN0JHjoqGr3HrZoD+b8Tgx8bKnArhSq8YVzUMc= +github.com/pingcap/errcode v0.0.0-20180921232412-a1a7271709d9 h1:KH4f4Si9XK6/IW50HtoaiLIFHGkapOM6w83za47UYik= +github.com/pingcap/errcode v0.0.0-20180921232412-a1a7271709d9/go.mod h1:4b2X8xSqxIroj/IZ9MX/VGZhAwc11wB9wRIzHvz6SeM= +github.com/pingcap/errors v0.11.0/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= +github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= +github.com/pingcap/errors v0.11.5-0.20190809092503-95897b64e011/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= +github.com/pingcap/errors v0.11.5-0.20201126102027-b0a155152ca3 h1:LllgC9eGfqzkfubMgjKIDyZYaa609nNWAyNZtpy2B3M= +github.com/pingcap/errors v0.11.5-0.20201126102027-b0a155152ca3/go.mod h1:G7x87le1poQzLB/TqvTJI2ILrSgobnq4Ut7luOwvfvI= +github.com/pingcap/failpoint v0.0.0-20191029060244-12f4ac2fd11d/go.mod h1:DNS3Qg7bEDhU6EXNHF+XSv/PGznQaMJ5FWvctpm6pQI= +github.com/pingcap/failpoint v0.0.0-20200210140405-f8f9fb234798/go.mod h1:DNS3Qg7bEDhU6EXNHF+XSv/PGznQaMJ5FWvctpm6pQI= +github.com/pingcap/failpoint v0.0.0-20200506114213-c17f16071c53/go.mod h1:w4PEZ5y16LeofeeGwdgZB4ddv9bLyDuIX+ljstgKZyk= +github.com/pingcap/failpoint v0.0.0-20200603062251-b230c36c413c h1:cm0zAj+Tab94mp4OH+VoLJiSNQvZO4pWDGJ8KEk2a0c= +github.com/pingcap/failpoint v0.0.0-20200603062251-b230c36c413c/go.mod h1:w4PEZ5y16LeofeeGwdgZB4ddv9bLyDuIX+ljstgKZyk= +github.com/pingcap/fn v0.0.0-20191016082858-07623b84a47d/go.mod h1:fMRU1BA1y+r89AxUoaAar4JjrhUkVDt0o0Np6V8XbDQ= +github.com/pingcap/goleveldb v0.0.0-20191226122134-f82aafb29989 h1:surzm05a8C9dN8dIUmo4Be2+pMRb6f55i+UIYrluu2E= +github.com/pingcap/goleveldb v0.0.0-20191226122134-f82aafb29989/go.mod h1:O17XtbryoCJhkKGbT62+L2OlrniwqiGLSqrmdHCMzZw= +github.com/pingcap/kvproto v0.0.0-20191211054548-3c6b38ea5107/go.mod h1:WWLmULLO7l8IOcQG+t+ItJ3fEcrL5FxF0Wu+HrMy26w= +github.com/pingcap/kvproto v0.0.0-20200214064158-62d31900d88e/go.mod h1:IOdRDPLyda8GX2hE/jO7gqaCV/PNFh8BZQCQZXfIOqI= +github.com/pingcap/kvproto v0.0.0-20200411081810-b85805c9476c/go.mod h1:IOdRDPLyda8GX2hE/jO7gqaCV/PNFh8BZQCQZXfIOqI= +github.com/pingcap/kvproto v0.0.0-20200417092353-efbe03bcffbd/go.mod h1:IOdRDPLyda8GX2hE/jO7gqaCV/PNFh8BZQCQZXfIOqI= +github.com/pingcap/kvproto v0.0.0-20200420075417-e0c6e8842f22/go.mod h1:IOdRDPLyda8GX2hE/jO7gqaCV/PNFh8BZQCQZXfIOqI= +github.com/pingcap/kvproto v0.0.0-20200423020121-038e31959c2a/go.mod h1:IOdRDPLyda8GX2hE/jO7gqaCV/PNFh8BZQCQZXfIOqI= +github.com/pingcap/kvproto v0.0.0-20200424032552-6650270c39c3/go.mod h1:IOdRDPLyda8GX2hE/jO7gqaCV/PNFh8BZQCQZXfIOqI= +github.com/pingcap/kvproto v0.0.0-20200518112156-d4aeb467de29 h1:NpW1OuYrIl+IQrSsVbtyHpHpazmSCHy+ysrOixY0xY4= +github.com/pingcap/kvproto v0.0.0-20200518112156-d4aeb467de29/go.mod h1:IOdRDPLyda8GX2hE/jO7gqaCV/PNFh8BZQCQZXfIOqI= +github.com/pingcap/log v0.0.0-20191012051959-b742a5d432e9/go.mod h1:4rbK1p9ILyIfb6hU7OG2CiWSqMXnp3JMbiaVJ6mvoY8= +github.com/pingcap/log v0.0.0-20200117041106-d28c14d3b1cd/go.mod h1:4rbK1p9ILyIfb6hU7OG2CiWSqMXnp3JMbiaVJ6mvoY8= +github.com/pingcap/log v0.0.0-20200511115504-543df19646ad/go.mod h1:4rbK1p9ILyIfb6hU7OG2CiWSqMXnp3JMbiaVJ6mvoY8= +github.com/pingcap/log v0.0.0-20210317133921-96f4fcab92a4 h1:ERrF0fTuIOnwfGbt71Ji3DKbOEaP189tjym50u8gpC8= +github.com/pingcap/log v0.0.0-20210317133921-96f4fcab92a4/go.mod h1:4rbK1p9ILyIfb6hU7OG2CiWSqMXnp3JMbiaVJ6mvoY8= +github.com/pingcap/pd/v4 v4.0.0-rc.1.0.20200422143320-428acd53eba2/go.mod h1:s+utZtXDznOiL24VK0qGmtoHjjXNsscJx3m1n8cC56s= +github.com/pingcap/pd/v4 v4.0.0-rc.2.0.20200520083007-2c251bd8f181 h1:FM+PzdoR3fmWAJx3ug+p5aOgs5aZYwFkoDL7Potdsz0= +github.com/pingcap/pd/v4 v4.0.0-rc.2.0.20200520083007-2c251bd8f181/go.mod h1:q4HTx/bA8aKBa4S7L+SQKHvjRPXCRV0tA0yRw0qkZSA= +github.com/pingcap/sysutil v0.0.0-20200206130906-2bfa6dc40bcd/go.mod h1:EB/852NMQ+aRKioCpToQ94Wl7fktV+FNnxf3CX/TTXI= +github.com/pingcap/sysutil v0.0.0-20200408114249-ed3bd6f7fdb1 h1:PI8YpTl45F8ilNkrPtT4IdbcZB1SCEa+gK/U5GJYl3E= +github.com/pingcap/sysutil v0.0.0-20200408114249-ed3bd6f7fdb1/go.mod h1:EB/852NMQ+aRKioCpToQ94Wl7fktV+FNnxf3CX/TTXI= +github.com/pingcap/tidb v1.1.0-beta.0.20200424154252-5ede18f10eed/go.mod h1:m2VDlJDbUeHPCXAfKPajqLmB1uLvWpkKk3zALNqDYdw= +github.com/pingcap/tidb v1.1.0-beta.0.20200509133407-a9dc72cf2558/go.mod h1:cXNbVSQAkwwmjFQmEnEPI00Z2/Y/KOhouttUPERiInE= +github.com/pingcap/tidb v1.1.0-beta.0.20200606093724-b5b4da0e6a90/go.mod h1:aaBBi3OJmYjENWY31YYOY8K6UoZZYgjZVZH56D0QIdE= +github.com/pingcap/tidb v1.1.0-beta.0.20200630082100-328b6d0a955c h1:l30VSRmrTC7+mTBFeexTKtrACZn5x0uMA18aXIhcAqI= +github.com/pingcap/tidb v1.1.0-beta.0.20200630082100-328b6d0a955c/go.mod h1:E2QWmROq7fUwvyxKoy0Bu8rfqV8ZlZDlZwBwOuesyKw= +github.com/pingcap/tidb-tools v4.0.0-beta.1.0.20200306084441-875bd09aa3d5+incompatible/go.mod h1:XGdcy9+yqlDSEMTpOXnwf3hiTeqrV6MN/u1se9N8yIM= +github.com/pingcap/tidb-tools v4.0.0-rc.1.0.20200421113014-507d2bb3a15e+incompatible/go.mod h1:XGdcy9+yqlDSEMTpOXnwf3hiTeqrV6MN/u1se9N8yIM= +github.com/pingcap/tidb-tools v4.0.0-rc.1.0.20200514040632-f76b3e428e19+incompatible h1:/JKsYjsa5Ug8v5CN4zIbJGIqsvgBUkGwaP/rEScVvWM= +github.com/pingcap/tidb-tools v4.0.0-rc.1.0.20200514040632-f76b3e428e19+incompatible/go.mod h1:XGdcy9+yqlDSEMTpOXnwf3hiTeqrV6MN/u1se9N8yIM= +github.com/pingcap/tipb v0.0.0-20200417094153-7316d94df1ee/go.mod h1:RtkHW8WbcNxj8lsbzjaILci01CtYnYbIkQhjyZWrWVI= +github.com/pingcap/tipb v0.0.0-20200522051215-f31a15d98fce h1:LDyY6Xh/Z/SHVQ10erWtoOwIxHSTtlpPQ9cvS+BfRMY= +github.com/pingcap/tipb v0.0.0-20200522051215-f31a15d98fce/go.mod h1:RtkHW8WbcNxj8lsbzjaILci01CtYnYbIkQhjyZWrWVI= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.5.1/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= +github.com/prometheus/client_golang v1.11.0 h1:HNkLOAEQMIDv/K+04rukrLx6ch7msSRwf3/SASFAGtQ= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= +github.com/prometheus/common v0.29.0 h1:3jqPBvKT4OHAbje2Ql7KeaaSicDBCxMYwEJU1zRJceE= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= +github.com/prometheus/procfs v0.6.0 h1:mxy4L2jP6qMonqmq+aTtOx1ifVWUgG/TAmntgbh3xv4= +github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/remyoudompheng/bigfft v0.0.0-20190728182440-6a916e37a237 h1:HQagqIiBmr8YXawX/le3+O26N+vPPC1PtjaF3mwnook= +github.com/remyoudompheng/bigfft v0.0.0-20190728182440-6a916e37a237/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= +github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= +github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= +github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sasha-s/go-deadlock v0.2.0/go.mod h1:StQn567HiB1fF2yJ44N9au7wOhrPS3iZqiDbRupzT10= +github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= +github.com/sergi/go-diff v1.0.1-0.20180205163309-da645544ed44/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/shirou/gopsutil v2.19.10+incompatible h1:lA4Pi29JEVIQIgATSeftHSY0rMGI9CLrl2ZvDLiahto= +github.com/shirou/gopsutil v2.19.10+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= +github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4 h1:udFKJ0aHUL60LboW/A+DfgoHVedieIzIXE8uylPue0U= +github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4/go.mod h1:qsXQc7+bwAM3Q1u/4XEfrquwF8Lw7D7y5cD8CuHnfIc= +github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= +github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= +github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/shurcooL/vfsgen v0.0.0-20181020040650-a97a25d856ca/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw= +github.com/sijms/go-ora/v2 v2.2.15 h1:GJfudyOHT+DUgBPvS2wP/8lgmuer3OKfha8C0xnThW8= +github.com/sijms/go-ora/v2 v2.2.15/go.mod h1:jzfAFD+4CXHE+LjGWFl6cPrtiIpQVxakI2gvrMF2w6Y= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= +github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sjjian/parser v0.0.0-20240704052347-b6199b7bccae h1:rTwogc7Uq0/7zMHNhpQTqjtGKdccVh8Z8vHReIlsXG0= +github.com/sjjian/parser v0.0.0-20240704052347-b6199b7bccae/go.mod h1:Qq2tnreUXwVo7NAKAHmbWFsgqpDUkxwhJCClY+ZCudA= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72 h1:qLC7fQah7D6K1B0ujays3HV9gkFtllcxhzImRR7ArPQ= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= +github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= +github.com/spf13/cobra v1.1.1 h1:KfztREH0tPxJJ+geloSLaAkaPkr4ki2Er5quFV1TDo4= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.2.3-0.20181224173747-660f15d67dbb/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/swaggo/files v0.0.0-20190704085106-630677cd5c14/go.mod h1:gxQT6pBGRuIGunNf/+tSOB5OHvguWi8Tbt82WOkf35E= +github.com/swaggo/gin-swagger v1.2.0/go.mod h1:qlH2+W7zXGZkczuL+r2nEBR2JTT+/lX05Nn6vPhc7OI= +github.com/swaggo/http-swagger v0.0.0-20200103000832-0e9263c4b516/go.mod h1:O1lAbCgAAX/KZ80LM/OXwtWFI/5TvZlwxSg8Cq08PV0= +github.com/swaggo/http-swagger v0.0.0-20200308142732-58ac5e232fba/go.mod h1:O1lAbCgAAX/KZ80LM/OXwtWFI/5TvZlwxSg8Cq08PV0= +github.com/swaggo/swag v1.5.1/go.mod h1:1Bl9F/ZBpVWh22nY0zmYyASPO1lI/zIwRDrpZU+tv8Y= +github.com/swaggo/swag v1.6.3/go.mod h1:wcc83tB4Mb2aNiL/HP4MFeQdpHUrca+Rp/DRNgWAUio= +github.com/swaggo/swag v1.6.5/go.mod h1:Y7ZLSS0d0DdxhWGVhQdu+Bu1QhaF5k0RD7FKdiAykeY= +github.com/swaggo/swag v1.6.6-0.20200323071853-8e21f4cefeea/go.mod h1:xDhTyuFIujYiN3DKWC/H/83xcfHp+UE/IzWWampG7Zc= +github.com/syndtr/goleveldb v0.0.0-20180815032940-ae2bd5eed72d/go.mod h1:Z4AUp2Km+PwemOoO/VB5AOx9XSsIItzFjoJlOSiYmn0= +github.com/syndtr/goleveldb v1.0.1-0.20190625010220-02440ea7a285 h1:uSDYjYejelKyceA6DiCsngFof9jAyeaSyX9XC5a1a7Q= +github.com/syndtr/goleveldb v1.0.1-0.20190625010220-02440ea7a285/go.mod h1:9OrXJhf154huy1nPWmuSrkgjPUtUNhA+Zmy+6AESzuA= +github.com/tiancaiamao/appdash v0.0.0-20181126055449-889f96f722a2/go.mod h1:2PfKggNGDuadAa0LElHrByyrz4JPZ9fFx6Gs7nx7ZZU= +github.com/tidwall/gjson v1.3.5/go.mod h1:P256ACg0Mn+j1RXIDXoss50DeIABTYK1PULOJHhxOls= +github.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E= +github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho= +github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE= +github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/tmc/grpc-websocket-proxy v0.0.0-20171017195756-830351dc03c6/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/uber-go/atomic v1.3.2/go.mod h1:/Ct5t2lcmbJ4OSe/waGBoaVvVqtO0bmtfVNex1PFV8g= +github.com/uber-go/atomic v1.4.0 h1:yOuPqEq4ovnhEjpHmfFwsqBXDYbQeT6Nb0bwD6XnD5o= +github.com/uber/jaeger-client-go v2.15.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= +github.com/uber/jaeger-client-go v2.22.1+incompatible h1:NHcubEkVbahf9t3p75TOCR83gdUHXjRJvjoBh1yACsM= +github.com/uber/jaeger-client-go v2.22.1+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= +github.com/uber/jaeger-lib v1.5.0/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= +github.com/uber/jaeger-lib v2.2.0+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= +github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVKhn2Um6rjCsSsg= +github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= +github.com/ugorji/go v1.1.5-pre/go.mod h1:FwP/aQVg39TXzItUBMwnWp9T9gPQnXw4Poh4/oBQZ/0= +github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/ugorji/go/codec v0.0.0-20181022190402-e5e69e061d4f/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/ugorji/go/codec v1.1.5-pre/go.mod h1:tULtS6Gy1AE1yCENaw4Vb//HLH5njI2tfCQDUqRd8fI= +github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +github.com/unrolled/render v0.0.0-20171102162132-65450fb6b2d3/go.mod h1:tu82oB5W2ykJRVioYsB+IQKcft7ryBr7w12qMBUPyXg= +github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= +github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/urfave/cli/v2 v2.1.1/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= +github.com/urfave/negroni v0.3.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= +github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5Qo6v2eYzo7kUS51QINcR5jNpbZS8= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/yookoala/realpath v1.0.0/go.mod h1:gJJMA9wuX7AcqLy1+ffPatSCySA1FQ2S8Ya9AIoYBpE= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= +go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/bbolt v1.3.3 h1:MUGmc65QhB3pIlaQ5bB4LwqSj6GIonVJXpZiaKNyaKk= +go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= +go.etcd.io/etcd v0.5.0-alpha.5.0.20191023171146-3cf2f69b5738 h1:lWF4f9Nypl1ZqSb4gLeh/DGvBYVaUYHuiB93teOmwgc= +go.etcd.io/etcd v0.5.0-alpha.5.0.20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/automaxprocs v1.2.0/go.mod h1:YfO3fm683kQpzETxlTGZhGIVmXAhaw3gxeBADbpZtnU= +go.uber.org/dig v1.8.0/go.mod h1:X34SnWGr8Fyla9zQNO2GSO2D+TIuqB14OS8JhYocIyw= +go.uber.org/fx v1.10.0/go.mod h1:vLRicqpG/qQEzno4SYU86iCwfT95EZza+Eba0ItuxqY= +go.uber.org/goleak v0.10.0/go.mod h1:VCZuO8V8mFPlL0F5J5GK1rtHV3DrFcQ1R8ryq7FK0aI= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= +go.uber.org/multierr v1.4.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= +go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= +go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= +go.uber.org/zap v1.8.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.12.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= +go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= +go.uber.org/zap v1.14.1/go.mod h1:Mb2vm2krFEG5DV0W9qcHBYFtp/Wku1cvYaqPsS/WYfc= +go.uber.org/zap v1.15.0/go.mod h1:Mb2vm2krFEG5DV0W9qcHBYFtp/Wku1cvYaqPsS/WYfc= +go.uber.org/zap v1.17.0 h1:MTjgFu6ZLKvY6Pvaqk97GlxNBuMpV4Hy/3P6tRGlI2U= +go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= +golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20200119044424-58c23975cae1/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 h1:VLliZ0d+/avPrXXH+OakdXhpJuEoBZuwh1m2j7U6Iug= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4= +golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f h1:Qmd2pbz05z7z6lm0DrgQVVPuBm92jqujBKMHMOlOQEw= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/telemetry v0.0.0-20260519152614-eab6ae52b5e2 h1:2EucmYlcIsc8Y6aLj+kX90Y00hmjqLNlw935kc13R2k= +golang.org/x/telemetry v0.0.0-20260519152614-eab6ae52b5e2/go.mod h1:Eqhaxk/wZsWEH8CRxLwj6xzEJbz7k1EFGqx7nyCoabE= +golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= +golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606050223-4d9ae51c2468/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190611222205-d73e1c7e250b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191030062658-86caa796c7ab/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191107010934-f79515f33823/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191114200427-caa0b0f7d508/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200225230052-807dcd883420/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200325010219-a49f79bcc224/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200325203130-f53864d0dba1/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= +golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.1/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.45.0 h1:pqMffJFLBVUDIoYsHcqtxgQVTsmxMDpYLOc5MT4Jrww= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/genproto v0.0.0-20170818010345-ee236bd376b0/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20181004005441-af9cb2a35e7f/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20190927181202-20e1ac93f88c/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4 h1:myaecH64R0bIEDjNORIel4iXubqzaHU1K2z8ajBwWcM= +google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/grpc v0.0.0-20180607172857-7a6a684ca69e/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= +google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.24.0/go.mod h1:XDChyiUovWa60DnaeDeZmSW86xtLtjtZbwvSiRnRtcA= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= +google.golang.org/grpc v1.50.1 h1:DS/BukOZWp8s6p4Dt/tOaJaTQyPyOoCcrjroHuCeLzY= +google.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +gopkg.in/alecthomas/gometalinter.v2 v2.0.12/go.mod h1:NDRytsqEZyolNuAgTzJkZMkSQM7FIKyzVzGhjB/qfYo= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/alecthomas/kingpin.v3-unstable v3.0.0-20180810215634-df19058c872c/go.mod h1:3HH7i1SgMqlzxCcBmUHW657sD4Kvv9sC3HpL3YukzwA= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= +gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y= +gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= +gopkg.in/go-playground/validator.v9 v9.31.0/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= +gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= +gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= +gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gorm.io/gorm v1.24.3 h1:WL2ifUmzR/SLp85CSURAfybcHnGZ+yLSGSxgYXlFBHg= +gorm.io/gorm v1.24.3/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= +sourcegraph.com/sourcegraph/appdash v0.0.0-20180531100431-4c381bd170b4 h1:VO9oZbbkvTwqLimlQt15QNdOOBArT2dw/bvzsMZBiqQ= +sourcegraph.com/sourcegraph/appdash v0.0.0-20180531100431-4c381bd170b4/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= +sourcegraph.com/sourcegraph/appdash-data v0.0.0-20151005221446-73f23eafcf67/go.mod h1:L5q+DGLGOQFpo1snNEkLOJT2d1YTW66rWNzatr3He1k= +vitess.io/vitess v0.12.0 h1:pzQpNHLqBG/3JZnZ5A0U25017b81CuxeR+s6ry9/wl4= +vitess.io/vitess v0.12.0/go.mod h1:QKt1VKLbuFqSi39giZa3z2i+ZG7/uSasnahoYjSB/go= diff --git a/internal/dialector/dialector.go b/internal/dialector/dialector.go index 88a66cf..e4b0013 100644 --- a/internal/dialector/dialector.go +++ b/internal/dialector/dialector.go @@ -1,28 +1,65 @@ -// Package dialector — minimal placeholder for Task-Dev-002. +// Package dialector implements the Dialector for GaussDB / openGauss data +// sources used by sqle-gaussdb-plugin. // -// The real Dialector for GaussDB / openGauss must: -// - return DriverName "opengauss" (registered by gitee.com/opengauss/openGauss-connector-go-pq) -// - build libpq key=value DSN with branch-specific defaults (port 8000 / 5432, sslmode, etc.) -// - expose connect-timeout fallback and additional-param hooks +// Two responsibilities: // -// All of the above belongs to Task-Dev-003 (plan §3.1). This file only ensures -// driverPkg.NewDriverBuilder(&Dialector{...}) compiles — every method is the -// smallest zero-value form that satisfies the driverPkg.Dialector interface. +// 1. Satisfy the driverPkg.Dialector interface (String / DatabaseAdditionalParam / +// Open / ShowDatabaseSQL) so that sqle's pkg/driver.DriverBuilder can use this +// dialect end-to-end (see sqle/sqle/pkg/driver/dialector.go). +// 2. Expose three plugin-internal helpers — DriverName / DefaultPort / BuildDSN — +// that encapsulate the GaussDB vs openGauss branch logic (default port, +// default sslmode, libpq key=value DSN layout). These helpers are pulled out +// of Open so that unit tests can assert the DSN purely as a string, without +// touching a real database (design §4.3 + §17.1.1). +// +// design §4.3 伪代码 uses `*driverV2.Config` as BuildDSN's parameter but accesses +// fields such as `cfg.Host` that live on `*driverV2.DSN` (the inner struct of +// Config). Real sqle code paths always pass `*driverV2.DSN` to Dialector.Open, +// so we keep BuildDSN's signature as `*driverV2.DSN`. This is semantically +// equivalent to the design pseudocode and avoids a redundant `cfg.DSN.Host` +// access in callers. package dialector import ( "context" "database/sql" - "errors" + "fmt" + "strings" driverV2 "github.com/actiontech/sqle/sqle/driver/v2" driverPkg "github.com/actiontech/sqle/sqle/pkg/driver" "github.com/actiontech/sqle/sqle/pkg/params" + "github.com/pkg/errors" + + "github.com/actiontech/sqle-gaussdb-plugin/pkg/consts" + + // Register the database/sql driver name "opengauss" via the openGauss + // official Go driver (gitee.com/opengauss/openGauss-connector-go-pq). + // design §3.4 / plan §3.1 mandate this registration. + _ "gitee.com/opengauss/openGauss-connector-go-pq" ) -// Dialector identifies which variant (GaussDB or openGauss) the current plugin -// process represents. Variant must be one of consts.PluginNameGaussDB / -// consts.PluginNameOpenGauss (set by the cmd/* main.go entrypoint). +// driverNameOpenGauss is the database/sql driver name registered by the blank +// import above. Both GaussDB (port 8000) and openGauss (port 5432) connect +// through the same libpq-compatible driver — only the DSN differs. +const driverNameOpenGauss = "opengauss" + +// defaultDatabaseName is the libpq fallback database when DSN.DatabaseName is +// empty. design §4.3 伪代码 fixes this to "postgres" for both variants. +const defaultDatabaseName = "postgres" + +// connectTimeoutSeconds is the libpq connect_timeout DSN parameter value used +// as a sane fallback. exploration_dev.md §1.4 R-Q1-1 mandated this so that +// hung TCP handshakes fail fast within plugin Ping budget. +const connectTimeoutSeconds = 5 + +// Dialector identifies which variant (GaussDB or openGauss) the current +// plugin process represents. Variant must be one of consts.PluginNameGaussDB +// or consts.PluginNameOpenGauss (set by the cmd/* main.go entrypoint). +// +// Embedding driverPkg.BaseDialector inherits the default ShowDatabaseSQL / +// DatabaseAdditionalParam / GetConn implementations from sqle; we override +// the ones that need GaussDB / openGauss specific behaviour below. type Dialector struct { driverPkg.BaseDialector Variant string @@ -32,32 +69,130 @@ type Dialector struct { var _ driverPkg.Dialector = (*Dialector)(nil) // String returns the dialect name. driverPkg.NewDriverBuilder copies this into -// DriverMetas.PluginName at construction time; cmd/* main.go overrides -// builder.Meta.PluginName afterwards to the exact "GaussDB" / "openGauss" -// literal (design §4.4 strict-match contract). +// DriverMetas.PluginName at construction time; cmd/* main.go then overrides +// builder.Meta.PluginName to the strict "GaussDB" / "openGauss" literal +// required by design §4.4 (PluginName strict-match contract). func (d *Dialector) String() string { return d.Variant } -// DatabaseAdditionalParam returns plugin-specific extra connection parameters. -// Task-Dev-002 returns an empty slice; real SSL / authtype params land in -// Task-Dev-003 per design §4.3. +// DatabaseAdditionalParam returns plugin-level extra connection parameters +// surfaced to the DMS data-source UI. Currently empty: SSL / authtype are +// supplied via DSN.AdditionalParams at runtime (design §4.3 表格 + §4.3 DSN +// 拼装规则). Future per-variant additions land here. func (d *Dialector) DatabaseAdditionalParam() params.Params { return params.Params{} } -// Open is the minimum placeholder that satisfies the driverPkg.Dialector -// interface. It deliberately returns an error so that any code path that -// reaches it during the skeleton stage fails fast with a clear message. The -// real implementation (DSN construction + libpq driver name="opengauss") lands -// in Task-Dev-003. +// ShowDatabaseSQL returns the SQL used by sqle to enumerate databases on the +// data source. openGauss / GaussDB share the same pg_database catalog as +// upstream PostgreSQL, so the statement matches sqle-pg-plugin's variant. +func (d *Dialector) ShowDatabaseSQL() string { + return "SELECT datname FROM pg_database WHERE datname NOT IN ('template0', 'template1');" +} + +// DriverName returns the database/sql driver name registered by the openGauss +// official Go driver (see the blank import at the top of this file). Both +// variants share the same driver — the variant only changes DSN content. +func (d *Dialector) DriverName() string { + return driverNameOpenGauss +} + +// DefaultPort returns the default TCP port for the current variant. design +// §4.3 表格: +// +// GaussDB → 8000 (commercial GaussDB default) +// openGauss → 5432 (openGauss 6.0 default) +// +// Any unrecognised Variant returns 0. The empty Variant case is exercised by +// TestDialector_DefaultPort to lock the fallback contract. +func (d *Dialector) DefaultPort() int { + switch d.Variant { + case consts.PluginNameGaussDB: + return consts.DefaultPortGaussDB + case consts.PluginNameOpenGauss: + return consts.DefaultPortOpenGauss + default: + return 0 + } +} + +// BuildDSN constructs a libpq key=value DSN string for the openGauss driver. +// design §4.3 关键片段 specifies the layout: +// +// - 5 baseline fields: host / port / user / password / dbname +// - connect_timeout fallback (5 s) +// - GaussDB defaults sslmode=disable; openGauss does not declare sslmode +// (leaves the driver default "prefer" untouched) +// - User-supplied AdditionalParams override defaults — they are appended last, +// and libpq's parser keeps the last occurrence of a duplicate key +// +// Values are escaped per libpq single-quoted form: backslashes are doubled and +// single quotes are escaped as `\'` so that passwords like `it's` survive +// transit to the driver. This matches the escape strategy used by +// github.com/lib/pq's url.go (the openGauss Go driver inherits the same logic). +func (d *Dialector) BuildDSN(dsn *driverV2.DSN) string { + if dsn == nil { + return "" + } + + dbName := dsn.DatabaseName + if dbName == "" { + dbName = defaultDatabaseName + } + + parts := []string{ + fmt.Sprintf("host=%s", escape(dsn.Host)), + fmt.Sprintf("port=%s", escape(dsn.Port)), + fmt.Sprintf("user=%s", escape(dsn.User)), + fmt.Sprintf("password=%s", escape(dsn.Password)), + fmt.Sprintf("dbname=%s", escape(dbName)), + fmt.Sprintf("connect_timeout=%d", connectTimeoutSeconds), + } + + // SSL: GaussDB commercial 默认 sslmode=disable 即可握手 (exploration §1.2 + // 实测 OK)。openGauss 默认走驱动行为 (sslmode=prefer),无需显式声明。 + if d.Variant == consts.PluginNameGaussDB { + parts = append(parts, "sslmode=disable") + } + + // AdditionalParams 由 DBA 在 DMS 数据源 UI 显式填写。它们追加在末尾, + // 使得 libpq 解析时同 key 会以**最后一个**为准,从而覆盖前面的默认值 + // (e.g. 用户填 sslmode=verify-full 会覆盖 GaussDB 的 sslmode=disable)。 + for _, p := range dsn.AdditionalParams { + if p == nil || p.Key == "" { + continue + } + parts = append(parts, fmt.Sprintf("%s=%s", p.Key, escape(p.Value))) + } + + return strings.Join(parts, " ") +} + +// Open implements the driverPkg.Dialector interface. It composes BuildDSN with +// BaseDialector.GetConn so that the same DSN logic flows through both unit +// tests (which call BuildDSN directly) and runtime connections. func (d *Dialector) Open(dsn *driverV2.DSN) (*sql.DB, *sql.Conn, error) { - _ = context.TODO - return nil, nil, errors.New("sqle-gaussdb-plugin: Dialector.Open is not implemented yet (Task-Dev-003)") + _ = context.TODO // retained for parity with design §5.1 reference + if dsn == nil { + return nil, nil, errors.New("sqle-gaussdb-plugin: Dialector.Open requires non-nil DSN") + } + dataSourceName := d.BuildDSN(dsn) + db, conn, err := d.BaseDialector.GetConn(d.DriverName(), dataSourceName) + if err != nil { + return nil, nil, errors.Wrap(err, "sqle-gaussdb-plugin: open connection") + } + return db, conn, nil } -// ShowDatabaseSQL returns the SQL used by sqle to enumerate databases on the -// data source. Placeholder for Task-Dev-002. -func (d *Dialector) ShowDatabaseSQL() string { - return "SELECT datname FROM pg_database WHERE datname NOT IN ('template0', 'template1')" +// escape returns v wrapped in single quotes with embedded backslashes and +// single quotes escaped per libpq's connection-string syntax. The wrapping +// is unconditional so that empty values still surface as `''`, which libpq +// accepts as "use default / unset". +func escape(v string) string { + // Order matters: replace backslashes first so the escapes we introduce + // for single quotes are not double-escaped. + v = strings.ReplaceAll(v, `\`, `\\`) + v = strings.ReplaceAll(v, `'`, `\'`) + return "'" + v + "'" } diff --git a/internal/dialector/dialector_test.go b/internal/dialector/dialector_test.go new file mode 100644 index 0000000..163d825 --- /dev/null +++ b/internal/dialector/dialector_test.go @@ -0,0 +1,311 @@ +// Package dialector unit tests — design §17.1.1 coverage matrix. +// +// Three map-case test functions, each pinned to one design §17.1.1 bucket: +// +// - TestDialector_DefaultPort default port branching +// - TestDialector_BuildDSN_BaselineFields baseline DSN field layout + +// sslmode default per variant +// - TestDialector_BuildDSN_AdditionalParamsAndEscape +// user override priority + libpq +// single-quote / backslash escape +// +// Hard constraints (todo.md Task-Dev-003 §C / Task-level §约束): +// - All cases use a Go map keyed by human-readable scenario name (no +// for-range slice + index assertion). +// - No sql.Open, no time.Sleep, no network — pure string assertions. +// - sslmode presence / absence is asserted strictly; other field order is +// not, to match libpq's order-independent parser. +package dialector + +import ( + "strings" + "testing" + + driverV2 "github.com/actiontech/sqle/sqle/driver/v2" + "github.com/actiontech/sqle/sqle/pkg/params" + + "github.com/actiontech/sqle-gaussdb-plugin/pkg/consts" +) + +// ----------------------------------------------------------------------------- +// TestDialector_DefaultPort +// ----------------------------------------------------------------------------- + +func TestDialector_DefaultPort(t *testing.T) { + type tc struct { + variant string + wantPort int + } + cases := map[string]tc{ + "gaussdb variant returns commercial default 8000": { + variant: consts.PluginNameGaussDB, + wantPort: consts.DefaultPortGaussDB, // 8000 + }, + "opengauss variant returns opengauss-6.0 default 5432": { + variant: consts.PluginNameOpenGauss, + wantPort: consts.DefaultPortOpenGauss, // 5432 + }, + "empty variant returns 0 (sentinel for unknown variant)": { + variant: "", + wantPort: 0, + }, + "unknown variant returns 0 (defensive default)": { + variant: "Postgres", + wantPort: 0, + }, + } + for name, c := range cases { + t.Run(name, func(t *testing.T) { + d := &Dialector{Variant: c.variant} + got := d.DefaultPort() + if got != c.wantPort { + t.Fatalf("variant=%q DefaultPort: got %d, want %d", c.variant, got, c.wantPort) + } + }) + } +} + +// ----------------------------------------------------------------------------- +// TestDialector_BuildDSN_BaselineFields +// ----------------------------------------------------------------------------- + +func TestDialector_BuildDSN_BaselineFields(t *testing.T) { + type tc struct { + variant string + dsn *driverV2.DSN + mustContain []string // every substring must appear in the rendered DSN + mustReject []string // every substring must NOT appear + } + cases := map[string]tc{ + "gaussdb: 5 baseline fields + connect_timeout + sslmode=disable": { + variant: consts.PluginNameGaussDB, + dsn: &driverV2.DSN{ + Host: "10.0.0.1", + Port: "8000", + User: "root", + Password: "secret", + DatabaseName: "gaussdb", + }, + mustContain: []string{ + "host='10.0.0.1'", + "port='8000'", + "user='root'", + "password='secret'", + "dbname='gaussdb'", + "connect_timeout=5", + "sslmode=disable", + }, + }, + "opengauss: 5 baseline fields + connect_timeout, sslmode NOT declared": { + variant: consts.PluginNameOpenGauss, + dsn: &driverV2.DSN{ + Host: "127.0.0.1", + Port: "5432", + User: "gaussdb", + Password: "openGauss@123", + DatabaseName: "postgres", + }, + mustContain: []string{ + "host='127.0.0.1'", + "port='5432'", + "user='gaussdb'", + "password='openGauss@123'", + "dbname='postgres'", + "connect_timeout=5", + }, + mustReject: []string{ + "sslmode", // openGauss leaves sslmode untouched per design §4.3 + }, + }, + "gaussdb: empty DatabaseName falls back to libpq 'postgres'": { + variant: consts.PluginNameGaussDB, + dsn: &driverV2.DSN{ + Host: "db.internal", + Port: "8000", + User: "gaussdba", + Password: "pwd", + DatabaseName: "", // <- triggers fallback + }, + mustContain: []string{ + "dbname='postgres'", + "sslmode=disable", + }, + }, + } + for name, c := range cases { + t.Run(name, func(t *testing.T) { + d := &Dialector{Variant: c.variant} + got := d.BuildDSN(c.dsn) + + for _, want := range c.mustContain { + if !strings.Contains(got, want) { + t.Errorf("variant=%q DSN must contain %q\n full DSN: %s", c.variant, want, got) + } + } + for _, reject := range c.mustReject { + if strings.Contains(got, reject) { + t.Errorf("variant=%q DSN must NOT contain %q\n full DSN: %s", c.variant, reject, got) + } + } + }) + } +} + +// ----------------------------------------------------------------------------- +// TestDialector_BuildDSN_AdditionalParamsAndEscape +// ----------------------------------------------------------------------------- + +func TestDialector_BuildDSN_AdditionalParamsAndEscape(t *testing.T) { + type tc struct { + variant string + dsn *driverV2.DSN + mustContain []string + // dupeKeyLast asserts that within the rendered DSN string the *last* + // occurrence of a libpq key is the one expected — i.e. user-supplied + // AdditionalParams override the default value. The map key is the + // libpq key, the value is the expected key=value substring that must + // be the last hit for that key. + dupeKeyLast map[string]string + } + cases := map[string]tc{ + "gaussdb: user AdditionalParam sslmode=verify-full overrides default disable": { + variant: consts.PluginNameGaussDB, + dsn: &driverV2.DSN{ + Host: "10.0.0.2", + Port: "8000", + User: "root", + Password: "p", + DatabaseName: "gaussdb", + AdditionalParams: params.Params{ + ¶ms.Param{Key: "sslmode", Value: "verify-full"}, + }, + }, + mustContain: []string{ + "sslmode=disable", // default emitted first + "sslmode='verify-full'", // user override emitted after + }, + dupeKeyLast: map[string]string{ + "sslmode": "sslmode='verify-full'", + }, + }, + "opengauss: user AdditionalParam sslmode=require lands in DSN (no default to override)": { + variant: consts.PluginNameOpenGauss, + dsn: &driverV2.DSN{ + Host: "127.0.0.1", + Port: "5432", + User: "gaussdb", + Password: "p", + DatabaseName: "postgres", + AdditionalParams: params.Params{ + ¶ms.Param{Key: "sslmode", Value: "require"}, + ¶ms.Param{Key: "application_name", Value: "sqle-plugin"}, + }, + }, + mustContain: []string{ + "sslmode='require'", + "application_name='sqle-plugin'", + }, + }, + "password with single quote must be escaped via backslash": { + variant: consts.PluginNameGaussDB, + dsn: &driverV2.DSN{ + Host: "h", + Port: "8000", + User: "u", + Password: "it's", + DatabaseName: "d", + }, + mustContain: []string{ + // libpq escape: ' → \', wrapped in single quotes → + // password='it\'s' + `password='it\'s'`, + }, + }, + "password with backslash must be escaped to double backslash": { + variant: consts.PluginNameOpenGauss, + dsn: &driverV2.DSN{ + Host: "h", + Port: "5432", + User: "u", + Password: `back\slash`, + DatabaseName: "d", + }, + mustContain: []string{ + `password='back\\slash'`, + }, + }, + "empty AdditionalParam key is ignored (defensive)": { + variant: consts.PluginNameGaussDB, + dsn: &driverV2.DSN{ + Host: "h", + Port: "8000", + User: "u", + Password: "p", + DatabaseName: "d", + AdditionalParams: params.Params{ + ¶ms.Param{Key: "", Value: "noise"}, + ¶ms.Param{Key: "application_name", Value: "ok"}, + }, + }, + mustContain: []string{ + "application_name='ok'", + }, + }, + } + + for name, c := range cases { + t.Run(name, func(t *testing.T) { + d := &Dialector{Variant: c.variant} + got := d.BuildDSN(c.dsn) + + for _, want := range c.mustContain { + if !strings.Contains(got, want) { + t.Errorf("variant=%q DSN must contain %q\n full DSN: %s", c.variant, want, got) + } + } + // Ensure user-supplied key occurs after the default so libpq's + // last-wins parsing resolves to the user value. We check this + // purely by index in the rendered string. + for key, lastSub := range c.dupeKeyLast { + lastIdx := strings.LastIndex(got, lastSub) + if lastIdx < 0 { + t.Fatalf("variant=%q expected last %s key form %q missing\n full DSN: %s", c.variant, key, lastSub, got) + } + // Anything matching key= after lastIdx + len(lastSub) would + // mean the override is not last; ensure there is no + // subsequent "=" occurrence. + tail := got[lastIdx+len(lastSub):] + if strings.Contains(tail, key+"=") || strings.Contains(tail, " "+key+"=") { + t.Errorf("variant=%q key %q is not the last occurrence in DSN\n full DSN: %s", c.variant, key, got) + } + } + }) + } +} + +// ----------------------------------------------------------------------------- +// Compile-time sanity: the three required helper methods exist and have the +// expected signatures. This is cheaper than a typed assertion in production +// code and surfaces accidental signature drift during refactors. +// ----------------------------------------------------------------------------- + +var ( + _ func() string = (&Dialector{}).DriverName + _ func() int = (&Dialector{}).DefaultPort + _ func(dsn *driverV2.DSN) string = (&Dialector{}).BuildDSN +) + +// TestDialector_DriverName locks the database/sql driver name registered by +// the openGauss official Go driver. The blank import at the top of +// dialector.go is the only place that calls sql.Register("opengauss", ...), +// so any drift here is an immediate red flag. +func TestDialector_DriverName(t *testing.T) { + d := &Dialector{Variant: consts.PluginNameGaussDB} + if got := d.DriverName(); got != "opengauss" { + t.Fatalf(`DriverName: got %q, want "opengauss"`, got) + } + d2 := &Dialector{Variant: consts.PluginNameOpenGauss} + if got := d2.DriverName(); got != "opengauss" { + t.Fatalf(`DriverName (openGauss variant): got %q, want "opengauss"`, got) + } +} diff --git a/internal/differ/differ.go b/internal/differ/differ.go new file mode 100644 index 0000000..2f37f59 --- /dev/null +++ b/internal/differ/differ.go @@ -0,0 +1,388 @@ +// differ.go — GenerateModifySQLs 入口 + 4 类对象 × 3 种差异分类的 ModifySQL +// 字符串拼接实现(Task-Dev-007)。 +// +// 注:package differ 的 godoc 包注释统一写在 doc.go;本文件顶部使用普通注释 +// 块以避免 `go vet` 报 duplicate package documentation(参考 episodic +// go_package_doc_dual_file_collision_20260521)。 +// +// 类型签名漂移说明(semantic/design_pseudocode_param_type_drift_20260521): +// design §7 伪代码把 differ 入口写成 +// +// Generate(base, compared []*driverV2.DatabaseSchemaObjectResult) []string +// +// 但 real 链路(sqle/sqle/driver/v2/driver_interface.go:431-468)中 +// DatabaseSchemaObjectResult 是 **per-schema** 聚合容器,包含 SchemaName 与 +// DatabaseObjectDDLs,差异比对真正消费的是 schema-pair 内的对象 DDL 列表。 +// 按"业务字段访问语义优先 + 文件头注释 + commit 标注偏离"准则,本实现将 +// differ 入口入参定为 schema-pair 内的对象 DDL 列表: +// +// GenerateModifySQLs( +// baseSchemaName string, +// baseDDLs []*driverV2.DatabaseObjectDDL, +// comparedSchemaName string, +// comparedDDLs []*driverV2.DatabaseObjectDDL, +// ) []string +// +// driver 接入(Task-Dev-008)时一次调用 extractor 取得 base / compared 两个 +// *DatabaseSchemaObjectResult 后,把 .SchemaName 和 .DatabaseObjectDDLs 解构传 +// 入,无需在 differ 层做二次类型转换;语义与 design §7 完全等价。 +// +// 实现路径选择(design §7.1):PG 系幂等 DDL(CREATE OR REPLACE VIEW / +// FUNCTION / PROCEDURE)+ TABLE 走 DROP/CREATE 兜底;本期不实现语义级 ALTER。 +// +// SQL 注入兜底(design §6.3 + critical_boundaries[3]):differ 不直接拼接用户 +// 输入,schema/object 名字面值来自 extractor 上游产出(已通过 $1 / $2 参数化 +// 绑定 + pg_get_* 服务端拼装),differ 仅做字符串拼接;**禁止** 在本层做任何 +// 额外 escape / 引号包裹。 + +package differ + +import ( + "fmt" + "regexp" + "strings" + + driverV2 "github.com/actiontech/sqle/sqle/driver/v2" +) + +// rewriteSchemaQualifier 把 baseDDL 中所有 `baseSchema.` 前缀替换为 `comparedSchema.`, +// 用于 VIEW / FUNCTION / PROCEDURE 在 base 侧 ObjectDDL 含 CREATE OR REPLACE +// 形如 `CREATE OR REPLACE VIEW base.v AS ...` 时,把目标 schema 改写为 compared, +// 让变更 SQL 实际作用在 compared 侧(design §7.2 收敛方向;docs/test/case-2-2.md +// 与 case-2-3.md 缺陷溯源)。 +// +// 替换策略:使用 strings.ReplaceAll 全字符串替换。该策略在结构化 DDL(pg_get_* +// 输出)中是安全的: +// +// - CREATE OR REPLACE VIEW / FUNCTION / PROCEDURE 行中的 schema 限定一定形如 +// `schemaName.objectName`,替换后语义等价; +// - 视图 / 函数体内引用了 base schema 同名对象(如 view 体 `FROM base.t_user`) +// 时一并被改写,与 base 侧 DDL 在 compared schema 上重建为"自包含定义"的 +// 语义一致; +// - 若 baseSchema 与 comparedSchema 相同(自对比兜底),ReplaceAll 是 no-op。 +// +// 不做替换的两种边界: +// +// - baseSchema 为空字符串:ReplaceAll 短路返回原 DDL; +// - DDL 用双引号包裹了 schema(如 `"BaseSchema".obj`):本期 schema 名全小写 +// 不带引号,不进入此分支;未来若需支持引号场景可同步处理 `"`+baseSchema+`"` +// 形态。 +func rewriteSchemaQualifier(ddl, baseSchema, comparedSchema string) string { + if baseSchema == "" || baseSchema == comparedSchema { + return ddl + } + // 用 `.` 作为最小可识别前缀,避免误命中字面值包含 schemaName 但 + // 不是限定符(虽然 pg_get_* 输出里通常不会出现这种边界,仍 defensive)。 + oldPrefix := baseSchema + "." + newPrefix := comparedSchema + "." + return strings.ReplaceAll(ddl, oldPrefix, newPrefix) +} + +// setSearchPathPattern 匹配 pg_get_tabledef 输出的首行 +// `SET search_path = ;`。 +// +// 设计要点: +// +// - 行首 `^` + 多行模式 `(?m)`:仅匹配行首的 SET 语句,避免误伤 DDL 体 +// 内(如表注释)的字面 "SET search_path"; +// - `\s+` / `\s*` 容忍 GaussDB / openGauss 间空白宽容差异; +// - schema 字面值用 capture group `([A-Za-z0-9_]+)`,限定标识符字符, +// 避免越界;本仓 schema 名全小写下划线(test_2905_base / +// test_2905_compared),完全落在该字符集内; +// - 末尾分号可选(pg_get_tabledef 实测带分号,但稳健性需要兼容缺分号)。 +// +// 见 Task-Test-Fix-002 P2-A:pg_get_tabledef 在 GaussDB / openGauss 上返回 +// `SET search_path = ;\nCREATE TABLE (...) WITH (...);\n +// ALTER TABLE ADD CONSTRAINT ...;`;TABLE 不带 schema 限定的 CREATE +// 依赖此 SET 行决定建在哪个 schema。Task-Test-Fix-001 P1.2 修复了 VIEW / +// FUNCTION / PROCEDURE 三类的 schema 限定改写(CREATE OR REPLACE schema.name +// 形态),但 TABLE 形态不同,需要单独改写 SET search_path 行(case-3-1.md +// 缺陷溯源)。 +var setSearchPathPattern = regexp.MustCompile(`(?m)^SET\s+search_path\s*=\s*([A-Za-z0-9_]+)(\s*;?)`) + +// rewriteSetSearchPath 把 TABLE DDL 文本(pg_get_tabledef 输出)首行 +// `SET search_path = baseSchema;` 改写为 `SET search_path = comparedSchema;`, +// 让后续裸表名 CREATE TABLE 落在 compared schema 而非 base schema。 +// +// 仅替换 `SET search_path = ` 这一精确匹配,不影响: +// +// - DDL 体内(如 view 体 / function 体)任何 SET search_path 字面值(实际 +// pg_get_tabledef 输出不含 view/function 体,但稳健性需要锁定 base 字面值 +// 匹配,避免 grep-style 误伤); +// - 替换后的 SET 语句末尾分号 / 空白原样保留(capture group $2 回写)。 +// +// design §7.2 红线:schema 改写只能 base→compared 单向,不可反向;当 base 与 +// compared schema 相同(自对比兜底)或 base 为空时短路返回原 DDL。 +func rewriteSetSearchPath(ddl, baseSchema, comparedSchema string) string { + if baseSchema == "" || baseSchema == comparedSchema { + return ddl + } + // 用 ReplaceAllStringFunc 精确捕获 schema 字面值,仅当与 baseSchema 相 + // 等时才做替换(避免误命中其他 schema 的 SET 行 —— 实际 pg_get_tabledef + // 输出只有一行 SET 且必然指向 baseSchema,仍保留这层 defensive 校验)。 + return setSearchPathPattern.ReplaceAllStringFunc(ddl, func(m string) string { + sub := setSearchPathPattern.FindStringSubmatch(m) + if len(sub) < 3 || sub[1] != baseSchema { + return m + } + return "SET search_path = " + comparedSchema + sub[2] + }) +} + +// dropTableWarningComment 是 DROP TABLE 语句前置的固定注释行,byte-for-byte +// 锁定(design §7.3 + impact_analysis.yml R-Q4-1 / risk_points 第 4 条)。 +// +// 任何编辑(含空格 / 标点 / 大小写偏移)都会破坏工单审批人侧的格式契约—— +// 该注释是 DROP TABLE 误删生产数据的唯一兜底(无第二道防御),单测 +// TestDiffer_WarningCommentInjection 专门用字面值副本断言锁定。 +const dropTableWarningComment = "-- WARNING: DROP TABLE will drop data. Review before executing." + +// ustoreNormalizePattern 把 `storage_type=` 归一为 +// `storage_type=ustore`,用于 DDL 字符串比较时的兜底(Task-Test-Fix-002 P4 +// 兼容性合并修复)。 +// +// 背景:GaussDB pg_get_tabledef 输出的 `WITH (orientation=row, storage_type=...)` +// 子句中 storage_type 的大小写在 base / compared 两侧偶发不一致——首次 +// CREATE TABLE 服务端缓存写 `USTORE`,重建后变 `ustore`;同表结构因纯大小写 +// 字面差异被识别为 inconsistent,误报变更。pg 系数据源的 WITH 子句字面值 +// 大小写不影响存储引擎语义。case-2-1.md 遗留次要 P4 缺陷溯源。 +var ustoreNormalizePattern = regexp.MustCompile(`(?i)storage_type\s*=\s*ustore`) + +// normalizeDDLForCompare 把 DDL 文本做字符串比较前的归一化: +// +// 1. P4: `storage_type=USTORE` 大小写归一到 `storage_type=ustore`; +// +// 仅用于"两侧 ObjectDDL 是否相等"短路判断的副本比较,**不**改原 DDL(落盘到 +// modify SQL 时仍是 base 侧原文,避免影响下游执行)。 +// +// 后续 P4 之外的归一化项可以在该 helper 内部继续累加(如未来发现的其他 +// 大小写规范化点),保持调用点单一。 +func normalizeDDLForCompare(s string) string { + return ustoreNormalizePattern.ReplaceAllString(s, "storage_type=ustore") +} + +// objectKey 是 (ObjectName, ObjectType) 二元组私有类型,用于在 base / compared +// 两侧 DDL 列表间建立索引。 +// +// FUNCTION / PROCEDURE 同名重载在 Task-Dev-006 已让 ObjectName 携带参数签名 +// (形如 `fn(integer, text)`),故 (ObjectName, ObjectType) 元组天然唯一, +// 不会把 N 个重载坍缩成一条(参考 semantic +// sqle_plugin_overload_objectname_signature_20260521)。 +type objectKey struct { + Name string + Type string +} + +// GenerateModifySQLs 对单个 schema-pair 内的 base/compared DDL 列表做差异 +// 对比,按 design §7.2 矩阵生成 ModifySQLs 字符串切片。 +// +// 入参顺序:base 三元组先于 compared 三元组;driver 接入时 base/compared 一 +// 一对应(base 为目标形态、compared 为源端形态——让 compared 收敛到 base 形态)。 +// +// 对比策略(design §6.4 + §7.2 / §7.4): +// +// 1. 按 (ObjectName, ObjectType) 元组建 map(baseMap / comparedMap); +// 2. 遍历 compared 一次,处理"仅 compared 侧有"与"两侧都有但 ObjectDDL 不同" +// 两类;过程中将命中的 base key 标记,剩余 base map 未访问的 key 即"仅 +// base 侧有"; +// 3. 两侧都有且 ObjectDDL 完全相同 → 不输出。 +// +// 本函数无 db 依赖、无 ctx 依赖、无副作用,纯字符串拼接。 +// +// 不实现的场景(design §7.5):分区表 SPLIT/MERGE、列存变更、分布键变更等 +// 随 `pg_get_tabledef` 原文携带,按整表 DROP+CREATE 兜底;INDEX / TRIGGER 本 +// 期不在抽取范围 → DDL 不出现 → diff 不出现。 +// +// 防御性处理:对意外的 ObjectType(如 INDEX / TRIGGER)跳过不生成 ModifySQL +// 且不 panic(extractor 上游已拒绝 unsupported 类型)。 +func GenerateModifySQLs( + baseSchemaName string, + baseDDLs []*driverV2.DatabaseObjectDDL, + comparedSchemaName string, + comparedDDLs []*driverV2.DatabaseObjectDDL, +) []string { + baseMap := buildObjectMap(baseDDLs) + comparedMap := buildObjectMap(comparedDDLs) + + modifySQLs := make([]string, 0) + consumedBaseKeys := make(map[objectKey]bool, len(baseMap)) + + // 第一遍:遍历 compared 处理"仅 compared 侧有"与"两侧都有但不同"。 + for key, comparedDDL := range comparedMap { + baseDDL, existsInBase := baseMap[key] + if !existsInBase { + // 仅 compared 侧有 → 生成 DROP(让 compared 收敛到 base 形态)。 + if sql := genOnlyComparedSide(comparedSchemaName, key.Type, comparedDDL); sql != "" { + modifySQLs = append(modifySQLs, sql) + } + continue + } + + consumedBaseKeys[key] = true + + // 两侧都有:比较 ObjectDDL 文本(含 Task-Test-Fix-002 P5 + P4 + // 双层归一化): + // + // P5(schema 替换前置):把 base 侧 ObjectDDL 内的 baseSchema. 限定 + // 全部替换为 comparedSchema.,让"仅 schema 限定不同 + body 相同"的 + // 场景(典型:FUNCTION/PROCEDURE 重载的 same body)也能被识别为 + // "无实质差异",不输出冗余 CREATE OR REPLACE(case-2-3.md 遗留 + // 次要缺陷); + // + // P4(USTORE 大小写归一):把两侧 DDL 中 `storage_type=USTORE` / + // `storage_type=ustore` 归一到统一形态再比较,避免 GaussDB + // pg_get_tabledef 输出的大小写漂移误判(case-2-1.md 遗留次要缺陷); + // + // 比较仅作用于 normalize 后的副本,不修改原 ObjectDDL;命中相等时 + // 短路跳过,否则继续走 genBothDiff 输出真实变更。 + // 双层 schema 改写:rewriteSchemaQualifier 处理 `schema.name` 限定形态 + // (VIEW/FUNCTION/PROCEDURE 路径),rewriteSetSearchPath 处理 + // `SET search_path = schema` 形态(TABLE 路径)。两者互不干扰: + // rewriteSchemaQualifier 走的是 `.` 前缀模式,不会误伤 + // SET 行;rewriteSetSearchPath 走的是行首正则,不会误伤 schema.name。 + baseAfterSchemaRewrite := rewriteSetSearchPath( + rewriteSchemaQualifier(baseDDL.ObjectDDL, baseSchemaName, comparedSchemaName), + baseSchemaName, comparedSchemaName, + ) + baseForCompare := normalizeDDLForCompare(baseAfterSchemaRewrite) + comparedForCompare := normalizeDDLForCompare(comparedDDL.ObjectDDL) + if baseForCompare == comparedForCompare { + // 完全相同(含 schema 改写后的等价 + USTORE 归一) → 无变更,跳过 + // (Task-Test-Fix-001 P1.3 重载 same DDL 短路 + Task-Test-Fix-002 + // P4/P5 兼容性归一短路)。 + continue + } + + // 两侧都有但不同 → 按对象类型生成"双向收敛"语句。 + if sql := genBothDiff(baseSchemaName, comparedSchemaName, key.Type, baseDDL); sql != "" { + modifySQLs = append(modifySQLs, sql) + } + } + + // 第二遍:扫描 base map 中未消费的 key 即"仅 base 侧有"。 + for key, baseDDL := range baseMap { + if consumedBaseKeys[key] { + continue + } + if sql := genOnlyBaseSide(baseSchemaName, comparedSchemaName, key.Type, baseDDL); sql != "" { + modifySQLs = append(modifySQLs, sql) + } + } + + return modifySQLs +} + +// buildObjectMap 按 (ObjectName, ObjectType) 元组把 DDL 列表索引化。 +// +// 防御性跳过 nil 条目 / nil DatabaseObject,避免 driver / extractor 上游偶发 +// 空指针时 panic。 +// +// 注:differ 入参语义约定为"已过滤好的真实存在对象列表",extractor 上游兜 +// 底「对象不存在 → 占位 ObjectDDL==""」(design §6.5)由 sqle-ee 业务层 +// compareSchema 在调用 differ 之前过滤掉占位条目;differ 层不再二次识别 +// ObjectDDL=="",避免双重语义入口。 +func buildObjectMap(ddls []*driverV2.DatabaseObjectDDL) map[objectKey]*driverV2.DatabaseObjectDDL { + m := make(map[objectKey]*driverV2.DatabaseObjectDDL, len(ddls)) + for _, ddl := range ddls { + if ddl == nil || ddl.DatabaseObject == nil { + continue + } + key := objectKey{ + Name: ddl.DatabaseObject.ObjectName, + Type: ddl.DatabaseObject.ObjectType, + } + m[key] = ddl + } + return m +} + +// genOnlyBaseSide 处理"仅 base 侧有"分支(design §7.2 表第 1 行)。 +// +// 该分支的语义是"compared 侧不存在该对象 → 在 compared schema 上重建 base 形态"。 +// 对 4 类对象的处理: +// +// - TABLE: pg_get_tabledef 原文(含 CREATE TABLE)。extractor 上游产出形态为 +// `SET search_path = baseSchema;\nCREATE TABLE (...);\n...`,CREATE +// 是裸表名,依赖 SET 行决定建在哪个 schema。Task-Test-Fix-002 P2-A: +// 必须把首行 SET search_path 改写为 comparedSchema,让 CREATE 落在 +// compared 侧(case-3-1.md 缺陷溯源); +// - VIEW / FUNCTION / PROCEDURE: ObjectDDL 含 `CREATE OR REPLACE X base.name`, +// 必须把 base schema 前缀替换为 compared schema,让变更 SQL 在 compared +// 侧创建对象(Task-Test-Fix-001 P1.2;design §7.2 收敛方向)。 +// +// 不支持 ObjectType 返回空字符串,调用方跳过。 +func genOnlyBaseSide(baseSchema, comparedSchema, objectType string, baseDDL *driverV2.DatabaseObjectDDL) string { + switch objectType { + case driverV2.ObjectType_TABLE: + return rewriteSetSearchPath(baseDDL.ObjectDDL, baseSchema, comparedSchema) + case driverV2.ObjectType_VIEW, + driverV2.ObjectType_FUNCTION, + driverV2.ObjectType_PROCEDURE: + return rewriteSchemaQualifier(baseDDL.ObjectDDL, baseSchema, comparedSchema) + default: + return "" + } +} + +// genOnlyComparedSide 处理"仅 compared 侧有"分支(design §7.2 表第 2 行)。 +// +// schema 参数取 comparedSchemaName:DROP 发生在 compared schema 上,让目标库 +// 收敛到 base schema 形态。 +// +// 输出格式(无引号包裹,PG 系大小写敏感由 ObjectName 字面值表达;schema 与 +// 对象名之间用 `.` 分隔;无尾随分号): +// +// - TABLE -> dropTableWarningComment + "\n" + DROP TABLE IF EXISTS schema.name +// - VIEW -> DROP VIEW IF EXISTS schema.name +// - FUNCTION -> DROP FUNCTION IF EXISTS schema.name(arg_signature) // R-Q4-2 +// - PROCEDURE -> DROP PROCEDURE IF EXISTS schema.name(arg_signature) // R-Q4-2 +// +// FUNCTION / PROCEDURE 的参数签名已在 Task-Dev-006 落地到 ObjectName 字段 +// (形如 `fn(integer, text)`),直接拼字面值即满足 R-Q4-2(同名重载 DROP 必 +// 须带参数列表才能被 PG 唯一定位)。 +func genOnlyComparedSide(schema, objectType string, comparedDDL *driverV2.DatabaseObjectDDL) string { + objectName := comparedDDL.DatabaseObject.ObjectName + switch objectType { + case driverV2.ObjectType_TABLE: + return dropTableWarningComment + "\n" + + fmt.Sprintf("DROP TABLE IF EXISTS %s.%s", schema, objectName) + case driverV2.ObjectType_VIEW: + return fmt.Sprintf("DROP VIEW IF EXISTS %s.%s", schema, objectName) + case driverV2.ObjectType_FUNCTION: + return fmt.Sprintf("DROP FUNCTION IF EXISTS %s.%s", schema, objectName) + case driverV2.ObjectType_PROCEDURE: + return fmt.Sprintf("DROP PROCEDURE IF EXISTS %s.%s", schema, objectName) + default: + return "" + } +} + +// genBothDiff 处理"两侧都有但 ObjectDDL 不同"分支(design §7.2 表第 3 行)。 +// +// TABLE 走 DROP+CREATE 兜底(PG 系无 ALTER TABLE 幂等路径,整表替换是 +// design §7.1 选定方案);VIEW / FUNCTION / PROCEDURE 用 base 侧 ObjectDDL +// (已含 CREATE OR REPLACE,PG 系幂等),但 schema 限定需改写为 compared +// (Task-Test-Fix-001 P1.2;同 genOnlyBaseSide 处理)。 +// +// schema 参数取 comparedSchema:DROP 发生在 compared schema 上;后续 +// CREATE TABLE 也作用于 compared 端(base 侧 ObjectDDL 含完整 schema 限定, +// 由 extractor 上游 pg_get_tabledef 拼装)。 +func genBothDiff(baseSchema, comparedSchema, objectType string, baseDDL *driverV2.DatabaseObjectDDL) string { + switch objectType { + case driverV2.ObjectType_TABLE: + // 形态:WARNING 注释 + "\n" + DROP TABLE IF EXISTS schema.name; + + // "\n" + CREATE TABLE 原文(已改写 SET search_path 到 compared 侧)。 + // Task-Test-Fix-002 P2-A:与 genOnlyBaseSide 同步改写 SET search_path, + // 否则两侧都有但 DDL 不同的 TABLE 仍会撞 base schema 同名表 + // (case-3-1.md / case-2-1.md 系列缺陷的同源根因)。 + return dropTableWarningComment + "\n" + + fmt.Sprintf("DROP TABLE IF EXISTS %s.%s;", comparedSchema, baseDDL.DatabaseObject.ObjectName) + + "\n" + rewriteSetSearchPath(baseDDL.ObjectDDL, baseSchema, comparedSchema) + case driverV2.ObjectType_VIEW, + driverV2.ObjectType_FUNCTION, + driverV2.ObjectType_PROCEDURE: + return rewriteSchemaQualifier(baseDDL.ObjectDDL, baseSchema, comparedSchema) + default: + return "" + } +} diff --git a/internal/differ/differ_test.go b/internal/differ/differ_test.go new file mode 100644 index 0000000..9efe909 --- /dev/null +++ b/internal/differ/differ_test.go @@ -0,0 +1,879 @@ +// differ_test.go covers design §17.1.4 — Differ 单测矩阵: +// +// - 4 类对象(TABLE / VIEW / FUNCTION / PROCEDURE)× 3 种差异分类 +// (仅 base / 仅 compared / 两侧 ObjectDDL 不同)= 12 个分支 +// - DROP TABLE WARNING 注释字面值(compat-RISK-5 / impact_analysis.yml R-Q4-1) +// byte-for-byte 锁定 +// - DROP FUNCTION/PROCEDURE 带参数签名(R-Q4-2)+ 重载下两条独立 DROP +// - 两侧 ObjectDDL 完全相同 → ModifySQLs 长度 0(无变更跳过) +// - 不支持 ObjectType(INDEX / TRIGGER 等)→ 静默跳过,不 panic +// - 混合 4 类对象的端到端集合断言(顺序无关,sort.Strings) +// +// 测试纯字符串断言:无 sqlmock / 无 sql.Open / 无网络 / 无文件 IO(differ 是纯 +// 函数,符合 plan §6.2 / todo Task-Dev-007 子任务 D 的"纯字符串拼接断言"约束)。 +// +// 顺序无关断言:参考 expertise_docs/semantic/sqle_plugin_overload_objectname_signature_20260521.md +// 与 sqlmock_rows_iteration_empty_and_order_20260521.md —— map 迭代顺序 Go runtime +// 不保证,所有多元素断言用 sort.Strings(got) == sort.Strings(want) 模式。 + +package differ + +import ( + "sort" + "strings" + "testing" + + driverV2 "github.com/actiontech/sqle/sqle/driver/v2" +) + +// expectedWarning 是 DROP TABLE 前置注释字面值的测试副本;与 differ.go 包级 +// 常量 dropTableWarningComment 字面值一致。 +// +// 故意维护独立副本(而非 import 包级常量)—— 当 dropTableWarningComment 被 +// 无意改写时,测试同步失效,让 code_review 能一眼看到字面值偏离 +// (compat-RISK-5 兜底)。 +const expectedWarning = "-- WARNING: DROP TABLE will drop data. Review before executing." + +// ---- 工具构造器 ---- + +// makeDDL 构造单个 *driverV2.DatabaseObjectDDL 测试 fixture。 +func makeDDL(name, objType, ddl string) *driverV2.DatabaseObjectDDL { + return &driverV2.DatabaseObjectDDL{ + DatabaseObject: &driverV2.DatabaseObject{ + ObjectName: name, + ObjectType: objType, + }, + ObjectDDL: ddl, + } +} + +// ---- a) TestDiffer_TableOnlyBase ---- + +// TestDiffer_TableOnlyBase :base 有 + compared 无 → 直接输出 base 侧 CREATE TABLE +// 原文(不含 WARNING / 不含 DROP)。 +// design §7.2 表第 1 行(TABLE 列)。 +func TestDiffer_TableOnlyBase(t *testing.T) { + const tableDDL = "CREATE TABLE public.t1 (id int) WITH (orientation=row);" + + cases := map[string]struct { + base []*driverV2.DatabaseObjectDDL + want string + }{ + "base_only_table": { + base: []*driverV2.DatabaseObjectDDL{makeDDL("t1", driverV2.ObjectType_TABLE, tableDDL)}, + want: tableDDL, + }, + } + + for name, c := range cases { + t.Run(name, func(t *testing.T) { + got := GenerateModifySQLs("public", c.base, "public", nil) + if len(got) != 1 { + t.Fatalf("len(got) = %d, want 1; got=%v", len(got), got) + } + if got[0] != c.want { + t.Errorf("got[0] = %q, want %q", got[0], c.want) + } + if strings.Contains(got[0], expectedWarning) { + t.Errorf("only-base TABLE must not contain WARNING comment; got=%q", got[0]) + } + if strings.Contains(got[0], "DROP TABLE") { + t.Errorf("only-base TABLE must not contain DROP TABLE; got=%q", got[0]) + } + }) + } +} + +// ---- b) TestDiffer_TableOnlyCompared ---- + +// TestDiffer_TableOnlyCompared :仅 compared 有 → WARNING 前置 + DROP TABLE IF +// EXISTS schema.name(无引号 / 无分号)。 +// design §7.2 表第 2 行(TABLE 列)+ §7.3 WARNING 注释格式。 +func TestDiffer_TableOnlyCompared(t *testing.T) { + cases := map[string]struct { + comparedSchema string + compared []*driverV2.DatabaseObjectDDL + wantSuffix string // WARNING + "\n" 之后的 DROP 行字面值 + }{ + "compared_only_table": { + comparedSchema: "public", + compared: []*driverV2.DatabaseObjectDDL{ + makeDDL("t_dropme", driverV2.ObjectType_TABLE, "CREATE TABLE public.t_dropme (id int);"), + }, + wantSuffix: "DROP TABLE IF EXISTS public.t_dropme", + }, + "compared_only_table_chinese_schema": { + comparedSchema: "中文模式", + compared: []*driverV2.DatabaseObjectDDL{ + makeDDL("t1", driverV2.ObjectType_TABLE, "CREATE TABLE 中文模式.t1 (id int);"), + }, + wantSuffix: "DROP TABLE IF EXISTS 中文模式.t1", + }, + } + + for name, c := range cases { + t.Run(name, func(t *testing.T) { + got := GenerateModifySQLs("base", nil, c.comparedSchema, c.compared) + if len(got) != 1 { + t.Fatalf("len(got) = %d, want 1; got=%v", len(got), got) + } + want := expectedWarning + "\n" + c.wantSuffix + if got[0] != want { + t.Errorf("got = %q\nwant = %q", got[0], want) + } + // 二次校验首行严格等于 WARNING(compat-RISK-5 兜底) + firstLine := strings.SplitN(got[0], "\n", 2)[0] + if firstLine != expectedWarning { + t.Errorf("first line = %q, want %q", firstLine, expectedWarning) + } + }) + } +} + +// ---- c) TestDiffer_TableBothDiff ---- + +// TestDiffer_TableBothDiff :两侧都有但 ObjectDDL 不同 → WARNING + DROP(带分号 +// + 换行)+ base 侧 CREATE TABLE 原文。 +// design §7.2 表第 3 行(TABLE 列)。 +func TestDiffer_TableBothDiff(t *testing.T) { + const ( + baseDDL = "CREATE TABLE public.t1 (id int, name text);" + comparedDDL = "CREATE TABLE public.t1 (id int);" + ) + + cases := map[string]struct { + base, compared []*driverV2.DatabaseObjectDDL + }{ + "table_both_diff": { + base: []*driverV2.DatabaseObjectDDL{makeDDL("t1", driverV2.ObjectType_TABLE, baseDDL)}, + compared: []*driverV2.DatabaseObjectDDL{makeDDL("t1", driverV2.ObjectType_TABLE, comparedDDL)}, + }, + } + + for name, c := range cases { + t.Run(name, func(t *testing.T) { + got := GenerateModifySQLs("base_schema", c.base, "compared_schema", c.compared) + if len(got) != 1 { + t.Fatalf("len(got) = %d, want 1; got=%v", len(got), got) + } + text := got[0] + // 同时含 WARNING + DROP(带分号)+ base CREATE 原文,且顺序 DROP 在前 / CREATE 在后 + if !strings.Contains(text, expectedWarning) { + t.Errorf("text missing WARNING; got=%q", text) + } + dropLine := "DROP TABLE IF EXISTS compared_schema.t1;" + if !strings.Contains(text, dropLine) { + t.Errorf("text missing %q; got=%q", dropLine, text) + } + if !strings.Contains(text, baseDDL) { + t.Errorf("text missing base DDL %q; got=%q", baseDDL, text) + } + dropIdx := strings.Index(text, "DROP TABLE") + createIdx := strings.Index(text, "CREATE TABLE") + if dropIdx < 0 || createIdx < 0 || dropIdx > createIdx { + t.Errorf("DROP must precede CREATE in both-diff TABLE; dropIdx=%d createIdx=%d text=%q", dropIdx, createIdx, text) + } + }) + } +} + +// ---- d) TestDiffer_ViewBothDiff(含 view 3 种差异) ---- + +// TestDiffer_ViewBothDiff 覆盖 VIEW 三种差异分类(design §7.2 表 VIEW 列): +// - view_only_base: base 侧 ObjectDDL 原文(CREATE OR REPLACE VIEW,extractor +// 上游保证) +// - view_only_compared: DROP VIEW IF EXISTS schema.viewName(无 WARNING) +// - view_both_diff: base 侧 ObjectDDL(PG 系幂等,无 DROP) +func TestDiffer_ViewBothDiff(t *testing.T) { + const ( + viewBaseDDL = "CREATE OR REPLACE VIEW public.v1 AS SELECT 1 AS id;" + viewComparedDDL = "CREATE OR REPLACE VIEW public.v1 AS SELECT 2 AS id;" + ) + + cases := map[string]struct { + base, compared []*driverV2.DatabaseObjectDDL + comparedSchema string + want string + }{ + "view_only_base": { + base: []*driverV2.DatabaseObjectDDL{makeDDL("v1", driverV2.ObjectType_VIEW, viewBaseDDL)}, + compared: nil, + comparedSchema: "public", + want: viewBaseDDL, + }, + "view_only_compared": { + base: nil, + compared: []*driverV2.DatabaseObjectDDL{makeDDL("v_drop", driverV2.ObjectType_VIEW, viewComparedDDL)}, + comparedSchema: "public", + want: "DROP VIEW IF EXISTS public.v_drop", + }, + "view_both_diff": { + base: []*driverV2.DatabaseObjectDDL{makeDDL("v1", driverV2.ObjectType_VIEW, viewBaseDDL)}, + compared: []*driverV2.DatabaseObjectDDL{makeDDL("v1", driverV2.ObjectType_VIEW, viewComparedDDL)}, + comparedSchema: "public", + want: viewBaseDDL, // PG 系幂等,无 DROP,直接 base 侧 CREATE OR REPLACE + }, + } + + for name, c := range cases { + t.Run(name, func(t *testing.T) { + got := GenerateModifySQLs("base_schema", c.base, c.comparedSchema, c.compared) + if len(got) != 1 { + t.Fatalf("len(got) = %d, want 1; got=%v", len(got), got) + } + if got[0] != c.want { + t.Errorf("got = %q\nwant = %q", got[0], c.want) + } + // VIEW 的任一差异分类都**不应**包含 DROP TABLE WARNING + if strings.Contains(got[0], expectedWarning) { + t.Errorf("VIEW must not contain DROP TABLE WARNING; got=%q", got[0]) + } + // VIEW only_base / both_diff 不应含 DROP;only_compared 则只含 DROP VIEW + if name == "view_only_base" || name == "view_both_diff" { + if strings.Contains(got[0], "DROP ") { + t.Errorf("%s must not contain DROP; got=%q", name, got[0]) + } + } + }) + } +} + +// ---- e) TestDiffer_FunctionBothDiff(含 function 3 种差异 + 参数签名) ---- + +// TestDiffer_FunctionBothDiff 覆盖 FUNCTION 三种差异分类(design §7.2 表 FUNCTION +// 列 + §7.4 参数签名 R-Q4-2): +// - function_only_base: base 侧 ObjectDDL 原文(CREATE OR REPLACE FUNCTION) +// - function_only_compared_with_args: DROP FUNCTION IF EXISTS schema.fn(integer, text) +// - function_both_diff: base 侧 ObjectDDL(无 DROP,PG 系幂等) +func TestDiffer_FunctionBothDiff(t *testing.T) { + const ( + fnBaseDDL = "CREATE OR REPLACE FUNCTION public.fn(integer, text) RETURNS int AS $$ SELECT 1 $$ LANGUAGE SQL;" + fnComparedDDL = "CREATE OR REPLACE FUNCTION public.fn(integer, text) RETURNS int AS $$ SELECT 2 $$ LANGUAGE SQL;" + ) + + cases := map[string]struct { + base, compared []*driverV2.DatabaseObjectDDL + comparedSchema string + want string + }{ + "function_only_base": { + base: []*driverV2.DatabaseObjectDDL{makeDDL("fn(integer, text)", driverV2.ObjectType_FUNCTION, fnBaseDDL)}, + compared: nil, + comparedSchema: "public", + want: fnBaseDDL, + }, + "function_only_compared_with_args": { + base: nil, + compared: []*driverV2.DatabaseObjectDDL{makeDDL("fn(integer, text)", driverV2.ObjectType_FUNCTION, fnComparedDDL)}, + comparedSchema: "public", + want: "DROP FUNCTION IF EXISTS public.fn(integer, text)", + }, + "function_both_diff": { + base: []*driverV2.DatabaseObjectDDL{makeDDL("fn(integer, text)", driverV2.ObjectType_FUNCTION, fnBaseDDL)}, + compared: []*driverV2.DatabaseObjectDDL{makeDDL("fn(integer, text)", driverV2.ObjectType_FUNCTION, fnComparedDDL)}, + comparedSchema: "public", + want: fnBaseDDL, // PG 系幂等 + }, + } + + for name, c := range cases { + t.Run(name, func(t *testing.T) { + got := GenerateModifySQLs("base_schema", c.base, c.comparedSchema, c.compared) + if len(got) != 1 { + t.Fatalf("len(got) = %d, want 1; got=%v", len(got), got) + } + if got[0] != c.want { + t.Errorf("got = %q\nwant = %q", got[0], c.want) + } + }) + } +} + +// ---- f) TestDiffer_ProcedureBothDiff(含 procedure 3 种差异 + 参数签名) ---- + +// TestDiffer_ProcedureBothDiff 覆盖 PROCEDURE 三种差异分类(design §7.2 表 +// PROCEDURE 列 + §7.4 参数签名 R-Q4-2)。 +func TestDiffer_ProcedureBothDiff(t *testing.T) { + const ( + procBaseDDL = "CREATE OR REPLACE PROCEDURE public.proc(text) LANGUAGE plpgsql AS $$ BEGIN END; $$;" + procComparedDDL = "CREATE OR REPLACE PROCEDURE public.proc(text) LANGUAGE plpgsql AS $$ BEGIN RAISE NOTICE 'x'; END; $$;" + ) + + cases := map[string]struct { + base, compared []*driverV2.DatabaseObjectDDL + comparedSchema string + want string + }{ + "procedure_only_base": { + base: []*driverV2.DatabaseObjectDDL{makeDDL("proc(text)", driverV2.ObjectType_PROCEDURE, procBaseDDL)}, + compared: nil, + comparedSchema: "public", + want: procBaseDDL, + }, + "procedure_only_compared_with_args": { + base: nil, + compared: []*driverV2.DatabaseObjectDDL{makeDDL("proc(text)", driverV2.ObjectType_PROCEDURE, procComparedDDL)}, + comparedSchema: "public", + want: "DROP PROCEDURE IF EXISTS public.proc(text)", + }, + "procedure_both_diff": { + base: []*driverV2.DatabaseObjectDDL{makeDDL("proc(text)", driverV2.ObjectType_PROCEDURE, procBaseDDL)}, + compared: []*driverV2.DatabaseObjectDDL{makeDDL("proc(text)", driverV2.ObjectType_PROCEDURE, procComparedDDL)}, + comparedSchema: "public", + want: procBaseDDL, + }, + } + + for name, c := range cases { + t.Run(name, func(t *testing.T) { + got := GenerateModifySQLs("base_schema", c.base, c.comparedSchema, c.compared) + if len(got) != 1 { + t.Fatalf("len(got) = %d, want 1; got=%v", len(got), got) + } + if got[0] != c.want { + t.Errorf("got = %q\nwant = %q", got[0], c.want) + } + }) + } +} + +// ---- g) TestDiffer_FunctionDropWithArgs(design §17.1.4 line 880) ---- + +// TestDiffer_FunctionDropWithArgs 锁定 R-Q4-2:当 compared 侧出现两条同名不同 +// 参数签名的 FUNCTION 重载时,differ 必须产出两条独立 DROP,每条 DROP 都带 +// 完整参数签名(避免 PG 报 "function xxx is not unique")。 +// +// map 迭代顺序 Go runtime 不保证 → 用 sort.Strings 顺序无关断言。 +func TestDiffer_FunctionDropWithArgs(t *testing.T) { + t.Run("function_drop_with_args_two_overloads", func(t *testing.T) { + compared := []*driverV2.DatabaseObjectDDL{ + makeDDL("fn(integer)", driverV2.ObjectType_FUNCTION, + "CREATE OR REPLACE FUNCTION public.fn(integer) RETURNS int AS $$ SELECT 1 $$ LANGUAGE SQL;"), + makeDDL("fn(integer, text)", driverV2.ObjectType_FUNCTION, + "CREATE OR REPLACE FUNCTION public.fn(integer, text) RETURNS int AS $$ SELECT 2 $$ LANGUAGE SQL;"), + } + + got := GenerateModifySQLs("base_schema", nil, "public", compared) + if len(got) != 2 { + t.Fatalf("len(got) = %d, want 2; got=%v", len(got), got) + } + + // 集合包含 + 顺序无关断言 + want := []string{ + "DROP FUNCTION IF EXISTS public.fn(integer)", + "DROP FUNCTION IF EXISTS public.fn(integer, text)", + } + sortedGot := append([]string(nil), got...) + sort.Strings(sortedGot) + sortedWant := append([]string(nil), want...) + sort.Strings(sortedWant) + for i := range sortedWant { + if sortedGot[i] != sortedWant[i] { + t.Errorf("sortedGot[%d] = %q, want %q", i, sortedGot[i], sortedWant[i]) + } + } + + // map key 唯一性兜底断言(避免重载被坍缩) + uniq := make(map[string]bool) + for _, s := range got { + if uniq[s] { + t.Errorf("duplicate ModifySQL %q; map key uniqueness broken", s) + } + uniq[s] = true + } + }) +} + +// ---- h) TestDiffer_WarningCommentInjection(design §17.1.4 line 881 / compat-RISK-5) ---- + +// TestDiffer_WarningCommentInjection 锁定 DROP TABLE WARNING 注释行字面值 +// (byte-for-byte),任何标点 / 大小写 / 空格偏移都让测试失败。 +// +// 兜底 impact_analysis.yml risk_points 第 4 条「DROP TABLE 兜底可能误删生产数据」。 +func TestDiffer_WarningCommentInjection(t *testing.T) { + t.Run("warning_comment_byte_for_byte", func(t *testing.T) { + compared := []*driverV2.DatabaseObjectDDL{ + makeDDL("t1", driverV2.ObjectType_TABLE, "CREATE TABLE public.t1 (id int);"), + } + got := GenerateModifySQLs("base", nil, "public", compared) + if len(got) != 1 { + t.Fatalf("len(got) = %d, want 1; got=%v", len(got), got) + } + // 严格断言第一行字面值 + firstLine := strings.SplitN(got[0], "\n", 2)[0] + if firstLine != expectedWarning { + t.Errorf("first line = %q, want %q (byte-for-byte)", firstLine, expectedWarning) + } + // 拒绝尾随空白 + if strings.TrimRight(firstLine, " \t") != firstLine { + t.Errorf("trailing whitespace in first line: %q", firstLine) + } + // 禁止 // 注释风格 + if strings.HasPrefix(firstLine, "//") { + t.Errorf("first line must not use Go-style // comment; got %q", firstLine) + } + // 禁止 --! / -- ! 等带感叹号 + if strings.HasPrefix(firstLine, "--!") || strings.HasPrefix(firstLine, "-- !") { + t.Errorf("first line must not contain extra punctuation; got %q", firstLine) + } + }) +} + +// ---- 增强覆盖(与 base 任务卡"额外建议"对齐) ---- + +// TestDiffer_TableBothSame_NoOutput :两侧 ObjectDDL 完全相同 → 跳过,ModifySQLs +// 长度 0(design §7.2 矩阵「无变更不输出」语义)。 +func TestDiffer_TableBothSame_NoOutput(t *testing.T) { + const sameDDL = "CREATE TABLE public.t1 (id int);" + cases := map[string]struct { + base, compared []*driverV2.DatabaseObjectDDL + }{ + "table_both_same": { + base: []*driverV2.DatabaseObjectDDL{makeDDL("t1", driverV2.ObjectType_TABLE, sameDDL)}, + compared: []*driverV2.DatabaseObjectDDL{makeDDL("t1", driverV2.ObjectType_TABLE, sameDDL)}, + }, + "view_both_same": { + base: []*driverV2.DatabaseObjectDDL{makeDDL("v1", driverV2.ObjectType_VIEW, "CREATE OR REPLACE VIEW public.v1 AS SELECT 1;")}, + compared: []*driverV2.DatabaseObjectDDL{makeDDL("v1", driverV2.ObjectType_VIEW, "CREATE OR REPLACE VIEW public.v1 AS SELECT 1;")}, + }, + "function_both_same": { + base: []*driverV2.DatabaseObjectDDL{makeDDL("fn()", driverV2.ObjectType_FUNCTION, "CREATE OR REPLACE FUNCTION public.fn() RETURNS int AS $$ SELECT 1 $$ LANGUAGE SQL;")}, + compared: []*driverV2.DatabaseObjectDDL{makeDDL("fn()", driverV2.ObjectType_FUNCTION, "CREATE OR REPLACE FUNCTION public.fn() RETURNS int AS $$ SELECT 1 $$ LANGUAGE SQL;")}, + }, + } + + for name, c := range cases { + t.Run(name, func(t *testing.T) { + got := GenerateModifySQLs("public", c.base, "public", c.compared) + if len(got) != 0 { + t.Errorf("len(got) = %d, want 0; got=%v", len(got), got) + } + }) + } +} + +// TestDiffer_MixedObjectTypes :混合 4 类对象一次性输入,验证总数 + 集合包含。 +// 用 sort.Strings 顺序无关断言。 +func TestDiffer_MixedObjectTypes(t *testing.T) { + t.Run("mixed_4_types", func(t *testing.T) { + const ( + tableBase = "CREATE TABLE public.t1 (id int);" + viewBase = "CREATE OR REPLACE VIEW public.v1 AS SELECT 1 AS id;" + fnCompared = "CREATE OR REPLACE FUNCTION public.fn(integer) RETURNS int AS $$ SELECT 1 $$ LANGUAGE SQL;" + procBase = "CREATE OR REPLACE PROCEDURE public.proc(text) LANGUAGE plpgsql AS $$ BEGIN END; $$;" + procCompared = "CREATE OR REPLACE PROCEDURE public.proc(text) LANGUAGE plpgsql AS $$ BEGIN RAISE NOTICE 'x'; END; $$;" + ) + + base := []*driverV2.DatabaseObjectDDL{ + makeDDL("t1", driverV2.ObjectType_TABLE, tableBase), + makeDDL("v1", driverV2.ObjectType_VIEW, viewBase), + makeDDL("proc(text)", driverV2.ObjectType_PROCEDURE, procBase), + } + compared := []*driverV2.DatabaseObjectDDL{ + makeDDL("fn(integer)", driverV2.ObjectType_FUNCTION, fnCompared), + makeDDL("proc(text)", driverV2.ObjectType_PROCEDURE, procCompared), + } + + got := GenerateModifySQLs("base_schema", base, "compared_schema", compared) + // 期望 4 条 ModifySQL: + // - t1 仅 base → tableBase 原文 + // - v1 仅 base → viewBase 原文 + // - fn(integer) 仅 compared → DROP FUNCTION ... + // - proc(text) 两侧不同 → procBase(PG 幂等) + if len(got) != 4 { + t.Fatalf("len(got) = %d, want 4; got=%v", len(got), got) + } + + wantContains := map[string]bool{ + tableBase: false, + viewBase: false, + "DROP FUNCTION IF EXISTS compared_schema.fn(integer)": false, + procBase: false, + } + for _, s := range got { + if _, ok := wantContains[s]; ok { + wantContains[s] = true + } + } + for k, hit := range wantContains { + if !hit { + t.Errorf("expected ModifySQL %q not found in got=%v", k, got) + } + } + }) +} + +// TestDiffer_UnsupportedObjectTypeIgnored :differ 收到 INDEX / TRIGGER 等 +// 本期不支持的 ObjectType 时静默跳过(design §7.5 兜底 + 防御性),不 panic、 +// 不进入输出。 +func TestDiffer_UnsupportedObjectTypeIgnored(t *testing.T) { + cases := map[string]struct { + base, compared []*driverV2.DatabaseObjectDDL + }{ + "index_only_base": { + base: []*driverV2.DatabaseObjectDDL{makeDDL("idx1", "INDEX", "CREATE INDEX idx1 ON public.t1(id);")}, + }, + "trigger_only_compared": { + compared: []*driverV2.DatabaseObjectDDL{makeDDL("trg1", "TRIGGER", "CREATE TRIGGER trg1 ...")}, + }, + "index_both_diff": { + base: []*driverV2.DatabaseObjectDDL{makeDDL("idx1", "INDEX", "CREATE INDEX idx1 ON public.t1(id);")}, + compared: []*driverV2.DatabaseObjectDDL{makeDDL("idx1", "INDEX", "CREATE INDEX idx1 ON public.t1(id, name);")}, + }, + } + + for name, c := range cases { + t.Run(name, func(t *testing.T) { + got := GenerateModifySQLs("public", c.base, "public", c.compared) + if len(got) != 0 { + t.Errorf("unsupported ObjectType must be ignored; got=%v", got) + } + }) + } +} + +// TestDiffer_NilAndEmptyDefensive :防御性测试 nil 边界。 +// - 双侧 nil / 空 slice → 空输出 +// - base/compared 含 nil 元素 → 跳过 nil,不 panic +// - 含 nil DatabaseObject 字段 → 跳过,不 panic +// +// 注:differ 入参语义约定为"已过滤好的真实存在对象列表",extractor 上游 +// 兜底(design §6.5 「对象不存在 → 占位 ObjectDDL=""」)由 sqle-ee 业务层 +// compareSchema 在调用 differ 之前过滤掉占位条目;differ 层不再二次识别 +// ObjectDDL=="",避免双重语义入口(如果 compared 侧 ObjectDDL=="" 进入 +// differ,会被当作真实对象生成 DROP,这是上游应该兜底的状态而非 differ +// 的失误)。 +func TestDiffer_NilAndEmptyDefensive(t *testing.T) { + cases := map[string]struct { + base, compared []*driverV2.DatabaseObjectDDL + wantLen int + }{ + "both_nil": {wantLen: 0}, + "both_empty_slice": { + base: []*driverV2.DatabaseObjectDDL{}, + compared: []*driverV2.DatabaseObjectDDL{}, + wantLen: 0, + }, + "base_has_nil_element_skip": { + base: []*driverV2.DatabaseObjectDDL{nil}, + wantLen: 0, + }, + "compared_has_nil_databaseobject_skip": { + compared: []*driverV2.DatabaseObjectDDL{ + {DatabaseObject: nil, ObjectDDL: "junk"}, + }, + wantLen: 0, + }, + } + + for name, c := range cases { + t.Run(name, func(t *testing.T) { + got := GenerateModifySQLs("public", c.base, "public", c.compared) + if len(got) != c.wantLen { + t.Errorf("len(got) = %d, want %d; got=%v", len(got), c.wantLen, got) + } + }) + } +} + +// ---- i) TestDiffer_RewriteSchemaQualifier (Task-Test-Fix-001 P1.2) ---- + +// TestRewriteSchemaQualifier 直接断言 helper:baseSchema. 前缀被替换为 +// comparedSchema.;自对比与空 baseSchema 走 no-op。 +func TestRewriteSchemaQualifier(t *testing.T) { + cases := map[string]struct { + ddl, base, compared, want string + }{ + "view_create_or_replace_qualified": { + ddl: "CREATE OR REPLACE VIEW test_2905_base.v_user_summary AS SELECT id FROM test_2905_base.t_user;", + base: "test_2905_base", + compared: "test_2905_compared", + want: "CREATE OR REPLACE VIEW test_2905_compared.v_user_summary AS SELECT id FROM test_2905_compared.t_user;", + }, + "function_qualified": { + ddl: "CREATE OR REPLACE FUNCTION test_2905_base.fn(integer) RETURNS int AS $$ SELECT 1 $$ LANGUAGE SQL;", + base: "test_2905_base", + compared: "test_2905_compared", + want: "CREATE OR REPLACE FUNCTION test_2905_compared.fn(integer) RETURNS int AS $$ SELECT 1 $$ LANGUAGE SQL;", + }, + "self_compare_noop": { + ddl: "CREATE OR REPLACE VIEW public.v AS SELECT 1;", + base: "public", + compared: "public", + want: "CREATE OR REPLACE VIEW public.v AS SELECT 1;", + }, + "empty_base_schema_noop": { + ddl: "CREATE TABLE x;", + base: "", + compared: "compared", + want: "CREATE TABLE x;", + }, + "no_match_noop": { + ddl: "CREATE OR REPLACE VIEW public.v AS SELECT 1;", + base: "other_schema", + compared: "compared_schema", + want: "CREATE OR REPLACE VIEW public.v AS SELECT 1;", + }, + } + + for name, c := range cases { + c := c + t.Run(name, func(t *testing.T) { + got := rewriteSchemaQualifier(c.ddl, c.base, c.compared) + if got != c.want { + t.Errorf("got = %q\nwant = %q", got, c.want) + } + }) + } +} + +// TestGenOnlyBaseSide_RewriteViewSchema 端到端验证 P1.2 修复:VIEW 仅 base 侧 +// 有时,输出的 CREATE OR REPLACE VIEW schema 限定改写为 compared 侧。 +func TestGenOnlyBaseSide_RewriteViewSchema(t *testing.T) { + baseSchema := "test_2905_base" + comparedSchema := "test_2905_compared" + const viewDDL = "CREATE OR REPLACE VIEW test_2905_base.v_user_summary AS SELECT t_user.id, t_user.name FROM test_2905_base.t_user;" + + base := []*driverV2.DatabaseObjectDDL{ + makeDDL("v_user_summary", driverV2.ObjectType_VIEW, viewDDL), + } + got := GenerateModifySQLs(baseSchema, base, comparedSchema, nil) + if len(got) != 1 { + t.Fatalf("len(got) = %d, want 1; got=%v", len(got), got) + } + if strings.Contains(got[0], "test_2905_base.") { + t.Errorf("base schema qualifier must be rewritten; got=%q", got[0]) + } + if !strings.Contains(got[0], "test_2905_compared.v_user_summary") { + t.Errorf("compared schema must appear in rewritten DDL; got=%q", got[0]) + } + if !strings.Contains(got[0], "CREATE OR REPLACE VIEW") { + t.Errorf("expected CREATE OR REPLACE VIEW; got=%q", got[0]) + } +} + +// TestGenBothDiff_RewriteFunctionSchema 验证 P1.2 修复对 FUNCTION 两侧都有但 +// DDL 不同分支的 schema 改写。 +func TestGenBothDiff_RewriteFunctionSchema(t *testing.T) { + baseSchema := "base_s" + comparedSchema := "compared_s" + const fnBase = "CREATE OR REPLACE FUNCTION base_s.fn(integer) RETURNS int AS $$ SELECT 1 $$ LANGUAGE SQL;" + const fnCompared = "CREATE OR REPLACE FUNCTION compared_s.fn(integer) RETURNS int AS $$ SELECT 2 $$ LANGUAGE SQL;" + + base := []*driverV2.DatabaseObjectDDL{ + makeDDL("fn(integer)", driverV2.ObjectType_FUNCTION, fnBase), + } + compared := []*driverV2.DatabaseObjectDDL{ + makeDDL("fn(integer)", driverV2.ObjectType_FUNCTION, fnCompared), + } + got := GenerateModifySQLs(baseSchema, base, comparedSchema, compared) + if len(got) != 1 { + t.Fatalf("len(got) = %d, want 1; got=%v", len(got), got) + } + if strings.Contains(got[0], "base_s.fn") { + t.Errorf("base schema qualifier must be rewritten; got=%q", got[0]) + } + if !strings.Contains(got[0], "compared_s.fn(integer)") { + t.Errorf("compared schema must appear in rewritten DDL; got=%q", got[0]) + } +} + +// TestGenBothDiff_SameDDLOverload_NoOutput (P1.3 验证) 双侧同 ObjectName 且 +// DDL 完全相同 → ModifySQLs 长度 0(短路过滤)。 +func TestGenBothDiff_SameDDLOverload_NoOutput(t *testing.T) { + const fnDDL = "CREATE OR REPLACE FUNCTION s.fn(integer) RETURNS int AS $$ SELECT 1 $$ LANGUAGE SQL;" + + base := []*driverV2.DatabaseObjectDDL{ + makeDDL("fn(integer)", driverV2.ObjectType_FUNCTION, fnDDL), + } + compared := []*driverV2.DatabaseObjectDDL{ + makeDDL("fn(integer)", driverV2.ObjectType_FUNCTION, fnDDL), + } + got := GenerateModifySQLs("s", base, "s", compared) + if len(got) != 0 { + t.Errorf("same DDL overload must be filtered; got=%v", got) + } +} + +// TestGenOnlyBaseSide_RewriteTableSearchPath 覆盖 Task-Test-Fix-002 P2-A: +// "仅 base 侧有"的 TABLE 输出必须把 pg_get_tabledef 首行 +// `SET search_path = baseSchema` 改写为 `SET search_path = comparedSchema`, +// 否则后续裸表名 CREATE TABLE 会落到 base schema 撞已存在同名表 +// (case-3-1.md 缺陷溯源)。 +func TestGenOnlyBaseSide_RewriteTableSearchPath(t *testing.T) { + const baseDDL = "SET search_path = test_2905_base;\n" + + "CREATE TABLE t_order (\n" + + " id integer NOT NULL,\n" + + " amount numeric(10,2)\n" + + ")\n" + + "WITH (orientation=row, compression=no);\n" + + "ALTER TABLE t_order ADD CONSTRAINT t_order_pkey PRIMARY KEY (id);" + + base := []*driverV2.DatabaseObjectDDL{ + makeDDL("t_order", driverV2.ObjectType_TABLE, baseDDL), + } + got := GenerateModifySQLs("test_2905_base", base, "test_2905_compared", nil) + if len(got) != 1 { + t.Fatalf("expect 1 modify SQL, got %d: %v", len(got), got) + } + if !strings.Contains(got[0], "SET search_path = test_2905_compared") { + t.Errorf("expect SET search_path rewritten to compared schema; got=%q", got[0]) + } + // search_path 必须只有 compared,base 字面值不应出现在 SET 行(容忍其他 + // 位置如表名 / 字段不带 base schema 前缀,因为 pg_get_tabledef 输出 TABLE + // 名是裸表名,不会含 base schema 字面)。 + for _, line := range strings.Split(got[0], "\n") { + trim := strings.TrimSpace(line) + if strings.HasPrefix(trim, "SET ") && strings.Contains(trim, "test_2905_base") { + t.Errorf("SET line still references base schema: %q", trim) + } + } +} + +// TestGenBothDiff_RewriteTableSearchPath 覆盖 Task-Test-Fix-002 P2-A: +// "两侧都有但 DDL 不同"的 TABLE 输出必须同样改写 SET search_path 到 compared, +// 并保留 WARNING + DROP IF EXISTS comparedSchema.name 前缀(case-2-1.md 双侧 +// 都有 t_user 但 base 多列的场景)。 +func TestGenBothDiff_RewriteTableSearchPath(t *testing.T) { + const baseDDL = "SET search_path = test_2905_base;\n" + + "CREATE TABLE t_user (\n" + + " id integer NOT NULL,\n" + + " name text,\n" + + " email text\n" + + ")\n" + + "WITH (orientation=row, compression=no);" + const comparedDDL = "SET search_path = test_2905_compared;\n" + + "CREATE TABLE t_user (\n" + + " id integer NOT NULL,\n" + + " name text\n" + + ")\n" + + "WITH (orientation=row, compression=no);" + + base := []*driverV2.DatabaseObjectDDL{ + makeDDL("t_user", driverV2.ObjectType_TABLE, baseDDL), + } + compared := []*driverV2.DatabaseObjectDDL{ + makeDDL("t_user", driverV2.ObjectType_TABLE, comparedDDL), + } + + got := GenerateModifySQLs("test_2905_base", base, "test_2905_compared", compared) + if len(got) != 1 { + t.Fatalf("expect 1 modify SQL, got %d: %v", len(got), got) + } + out := got[0] + if !strings.HasPrefix(out, expectedWarning) { + t.Errorf("expect WARNING comment as first line; got=%q", out) + } + if !strings.Contains(out, "DROP TABLE IF EXISTS test_2905_compared.t_user;") { + t.Errorf("expect DROP TABLE IF EXISTS compared schema; got=%q", out) + } + if !strings.Contains(out, "SET search_path = test_2905_compared") { + t.Errorf("expect SET search_path rewritten to compared schema; got=%q", out) + } + // SET 行不能再含 base schema 字面(容忍 DROP TABLE 之前的"comparedSchema" + // 出现,那是合法的限定)。 + for _, line := range strings.Split(out, "\n") { + trim := strings.TrimSpace(line) + if strings.HasPrefix(trim, "SET ") && strings.Contains(trim, "test_2905_base") { + t.Errorf("SET line still references base schema: %q", trim) + } + } +} + +// TestGenBothDiff_USTORENormalize_NoOutput 覆盖 Task-Test-Fix-002 P4: +// 两侧 DDL 仅 `storage_type=USTORE` / `storage_type=ustore` 大小写差异时, +// 短路跳过,不输出冗余 modify SQL(case-2-1.md 遗留次要缺陷溯源)。 +func TestGenBothDiff_USTORENormalize_NoOutput(t *testing.T) { + const baseDDL = "SET search_path = test_2905_base;\n" + + "CREATE TABLE t (id integer)\n" + + "WITH (orientation=row, storage_type=USTORE, compression=no);" + const comparedDDL = "SET search_path = test_2905_compared;\n" + + "CREATE TABLE t (id integer)\n" + + "WITH (orientation=row, storage_type=ustore, compression=no);" + + base := []*driverV2.DatabaseObjectDDL{ + makeDDL("t", driverV2.ObjectType_TABLE, baseDDL), + } + compared := []*driverV2.DatabaseObjectDDL{ + makeDDL("t", driverV2.ObjectType_TABLE, comparedDDL), + } + got := GenerateModifySQLs("test_2905_base", base, "test_2905_compared", compared) + if len(got) != 0 { + t.Errorf("USTORE/ustore case-only differences must be filtered; got=%v", got) + } +} + +// TestGenBothDiff_SameDDLDifferentSchema_NoOutput 覆盖 Task-Test-Fix-002 P5: +// 两侧 DDL 仅 schema 限定不同(base 写 base.fn vs compared 写 compared.fn)、 +// body 完全一致时,differ 在字符串短路前先做 schema 替换再比较 → 等价 → +// 短路跳过,不输出冗余 CREATE OR REPLACE(case-2-3.md 遗留次要缺陷溯源)。 +func TestGenBothDiff_SameDDLDifferentSchema_NoOutput(t *testing.T) { + const baseDDL = "CREATE OR REPLACE FUNCTION test_2905_base.fn_calc(p integer)\n" + + "RETURNS integer LANGUAGE plpgsql AS $function$ BEGIN RETURN p * 2; END; $function$;" + const comparedDDL = "CREATE OR REPLACE FUNCTION test_2905_compared.fn_calc(p integer)\n" + + "RETURNS integer LANGUAGE plpgsql AS $function$ BEGIN RETURN p * 2; END; $function$;" + + base := []*driverV2.DatabaseObjectDDL{ + makeDDL("fn_calc(integer)", driverV2.ObjectType_FUNCTION, baseDDL), + } + compared := []*driverV2.DatabaseObjectDDL{ + makeDDL("fn_calc(integer)", driverV2.ObjectType_FUNCTION, comparedDDL), + } + got := GenerateModifySQLs("test_2905_base", base, "test_2905_compared", compared) + if len(got) != 0 { + t.Errorf("same DDL with only schema-qualifier difference must be filtered; got=%v", got) + } +} + +// TestRewriteSetSearchPath_GuardRails 覆盖 P2-A helper 的边界: +// +// - baseSchema 为空 / 与 compared 相同 → no-op +// - SET 行不匹配 baseSchema 字面值 → 保留原文(不应误改) +// - 缺末尾分号 / 容忍空白 +func TestRewriteSetSearchPath_GuardRails(t *testing.T) { + cases := map[string]struct { + in string + base string + compared string + want string + }{ + "empty_base_short_circuit": { + in: "SET search_path = anyschema;\nCREATE TABLE x (id int);", + base: "", + compared: "compared", + want: "SET search_path = anyschema;\nCREATE TABLE x (id int);", + }, + "same_schema_short_circuit": { + in: "SET search_path = s;\nCREATE TABLE x (id int);", + base: "s", + compared: "s", + want: "SET search_path = s;\nCREATE TABLE x (id int);", + }, + "normal_rewrite_with_semicolon": { + in: "SET search_path = base;\nCREATE TABLE x (id int);", + base: "base", + compared: "compared", + want: "SET search_path = compared;\nCREATE TABLE x (id int);", + }, + "normal_rewrite_without_semicolon": { + in: "SET search_path = base\nCREATE TABLE x (id int);", + base: "base", + compared: "compared", + want: "SET search_path = compared\nCREATE TABLE x (id int);", + }, + "set_line_not_match_base_literal": { + in: "SET search_path = other;\nCREATE TABLE x (id int);", + base: "base", + compared: "compared", + want: "SET search_path = other;\nCREATE TABLE x (id int);", + }, + } + for name, c := range cases { + t.Run(name, func(t *testing.T) { + got := rewriteSetSearchPath(c.in, c.base, c.compared) + if got != c.want { + t.Errorf("rewriteSetSearchPath(%q,%q,%q):\n got: %q\nwant: %q", c.in, c.base, c.compared, got, c.want) + } + }) + } +} diff --git a/internal/driver/driver_common.go b/internal/driver/driver_common.go new file mode 100644 index 0000000..d879e89 --- /dev/null +++ b/internal/driver/driver_common.go @@ -0,0 +1,392 @@ +// Package driver provides the GaussDB and openGauss driverV2.Driver +// implementations for sqle-gaussdb-plugin. +// +// File layout (Task-Dev-004, plan §4.1): +// +// - driver_common.go shared Driver struct, connection-pool policy and the +// wrapPGError SQLSTATE passthrough helper used by both +// variants. +// - driver_gaussdb.go GaussDBDriver aggregation + NewGaussDBDriverImpl +// factory (PluginName="GaussDB"). +// - driver_opengauss.go OpenGaussDriver aggregation + NewOpenGaussDriverImpl +// factory (PluginName="openGauss"). +// +// Both factories keep the same signature as the previous stub.go so that +// cmd/sqle-gaussdb-plugin/main.go and cmd/sqle-opengauss-plugin/main.go +// continue to compile without any change. +package driver + +import ( + "context" + "database/sql" + "errors" + "reflect" + "strings" + "time" + + driverV2 "github.com/actiontech/sqle/sqle/driver/v2" + driverPkg "github.com/actiontech/sqle/sqle/pkg/driver" + hclog "github.com/hashicorp/go-hclog" + + "github.com/actiontech/sqle-gaussdb-plugin/internal/differ" + "github.com/actiontech/sqle-gaussdb-plugin/internal/extractor" +) + +// stripObjectNameSignature 把 plugin gRPC 入参中的 ObjectName 字符串剥除参数 +// 签名部分,返回 bare 名(pg_proc.proname 字面值,无括号 / 参数列表)。 +// +// 背景(Task-Test-Fix-002 P2-B; +// expertise_docs/episodic/sqle_gaussdb_modify_sql_object_name_signature_mismatch_20260521.md): +// +// - sqle-ee 业务层 / dms-ui-ee UI 主路径在调 plugin +// GetDatabaseDiffModifySQL / GetDatabaseObjectDDL 时,会把树中显示文本 +// (含参数签名,如 `fn_calc(p integer)` / `proc_alert(p TEXT)`)作为 +// ObjectName 透传给 plugin; +// - plugin extractor 内部 extractFunction / extractProcedure 用 ObjectName +// 作为 `pg_proc.proname = $2` 的绑定参数查询;pg_proc.proname 字面值 +// 永远是 bare name(PG 系数据源系统目录约定),含括号 / 参数签名的 +// 字符串永远查不到任何行,被走"占位 ObjectDDL=空"分支; +// - 后果是 base / compared 两侧都返回空 DDL 占位,differ 比对得到 +// modify_sqls=[],UI 抽屉显示"暂无数据"(case-3-3.md / case-3-4.md)。 +// +// 修复策略:plugin 入口前置剥签名 → 用 bare name 查 pg_proc → 命中 N 个重载 +// → 每条 ObjectName 由 plugin extractor 重新拼为 `fn_calc()` 规范 +// 形态(pg_get_function_arguments 输出)。这样: +// +// - base / compared 两侧 normalize 后 ObjectName 一致(规范签名 key), +// differ map 比对正确; +// - 重载场景天然分裂为多条 entry,不存在 ambiguous 问题(与 R-Q4-2 重载 +// 语义对齐); +// - 兼容用户传 bare 名(不带括号)的 API 旁路调用(IndexByte 返回 -1 → 原 +// 样返回)。 +// +// 切分策略:以 `(` 为分界点,取前缀部分;不剥除 schema 前缀(plugin 入口 +// 的 ObjectName 不带 schema,schema 由 SchemaName 字段单独承载)。 +func stripObjectNameSignature(s string) string { + idx := strings.IndexByte(s, '(') + if idx < 0 { + return s + } + // 用 TrimRight 容忍 `fn_calc (p int)` 这样在括号前带空格的形态(虽然 + // dms-ui-ee 实际不会这么传,但稳健性需要 defensive)。 + return strings.TrimRight(s[:idx], " \t") +} + +// normalizeObjectsForExtractor 把入参对象列表中的 ObjectName 剥签名后返回 +// 新的列表,原列表 / 原 *DatabaseObject 指针不修改(避免影响调用方持有的 +// proto 消息状态)。 +// +// 仅 FUNCTION / PROCEDURE 才需要剥签名;TABLE / VIEW 的 ObjectName 不含 +// 括号,IndexByte 返回 -1,stripObjectNameSignature 短路返回原值,所以全量 +// normalize 不影响 TABLE / VIEW 路径。 +func normalizeObjectsForExtractor(objs []*driverV2.DatabaseObject) []*driverV2.DatabaseObject { + if len(objs) == 0 { + return objs + } + out := make([]*driverV2.DatabaseObject, 0, len(objs)) + for _, obj := range objs { + if obj == nil { + out = append(out, obj) + continue + } + bare := stripObjectNameSignature(obj.ObjectName) + if bare == obj.ObjectName { + out = append(out, obj) + continue + } + // 拷贝一份新值,不修改原 proto 消息。 + cp := *obj + cp.ObjectName = bare + out = append(out, &cp) + } + return out +} + +// Connection-pool policy shared by the GaussDB and openGauss variants. The +// values match sqle-pg-plugin's runtime profile so the new plugin does not +// introduce a different pool-behaviour footprint (compat-RISK-2; design §5.1 +// line 328). +const ( + maxOpenConns = 10 + maxIdleConns = 5 + connMaxLifetime = 30 * time.Minute +) + +// Driver is the shared backing struct embedded by both GaussDBDriver and +// OpenGaussDriver. By embedding *driverPkg.DriverImpl we inherit the default +// implementation of every driverV2.Driver method (Close / Ping / Exec / Audit / +// Parse / Query / ... — see sqle/sqle/pkg/driver/impl.go). The variants only +// override the methods that need custom behaviour, which keeps the surface +// area minimal and unblocks the capability matrix declared by cmd/* main.go. +// +// Variant is the human-readable PluginName ("GaussDB" / "openGauss") used by +// unit tests to assert that the two factory functions produce two structurally +// distinct Driver instances (design §17.1.5 — PluginNameRegistration). +type Driver struct { + *driverPkg.DriverImpl + Variant string +} + +// Compile-time assertion: *Driver must continue to satisfy driverV2.Driver. +// If the upstream interface grows a new method, the embedded DriverImpl is +// expected to provide a default and this assertion catches the gap early. +var _ driverV2.Driver = (*Driver)(nil) + +// newCommon constructs the shared Driver value used by both variants. +// +// It delegates to driverPkg.NewDriverImpl so that the sqle main repo's +// connection bootstrap path (Dialector.Open → *sql.DB + *sql.Conn) is reused +// verbatim. Once DriverImpl has the *sql.DB in hand we apply the plugin's +// connection-pool policy on top (design §5.1 line 328). +// +// variant is the consts.PluginName* string used for diagnostics and unit-test +// assertions; it does not influence connection behaviour because the libpq +// DSN built by Dialector.BuildDSN already encodes the variant-specific defaults. +func newCommon( + l hclog.Logger, + dt driverPkg.Dialector, + ah *driverPkg.AuditHandler, + cfg *driverV2.Config, + variant string, +) (*Driver, error) { + rawDriver, err := driverPkg.NewDriverImpl(l, dt, ah, cfg) + if err != nil { + return nil, err + } + impl, ok := rawDriver.(*driverPkg.DriverImpl) + if !ok { + // Defensive guard: sqle main repo always returns *DriverImpl here, but + // asserting it explicitly keeps the type contract local to this file. + return nil, errors.New( + "sqle-gaussdb-plugin: NewDriverImpl did not return *driverPkg.DriverImpl") + } + applyPoolPolicy(impl.DB) + return &Driver{DriverImpl: impl, Variant: variant}, nil +} + +// applyPoolPolicy sets the shared connection-pool limits on db. It is a no-op +// when db is nil so that the cfg.DSN==nil path (driverPkg.NewDriverImpl +// returns a Driver with no underlying *sql.DB — see impl.go:37-39) does not +// panic. +func applyPoolPolicy(db *sql.DB) { + if db == nil { + return + } + db.SetMaxOpenConns(maxOpenConns) + db.SetMaxIdleConns(maxIdleConns) + db.SetConnMaxLifetime(connMaxLifetime) +} + +// Ping overrides the embedded DriverImpl.Ping so that the plugin always pings +// through *sql.DB rather than *sql.Conn. design §5.1 关键片段 mandates this +// shape; the DB path also lets the connect_timeout DSN parameter (set by +// dialector.BuildDSN, exploration §1.4 R-Q1-1) actually bound the wait. +// +// When the underlying *sql.DB has not been initialised (cfg.DSN == nil at +// driver construction time, e.g. during unit tests that exercise only the +// capability matrix), we surface a clear error instead of dereferencing a nil +// pointer. +func (d *Driver) Ping(ctx context.Context) error { + if d == nil || d.DriverImpl == nil || d.DriverImpl.DB == nil { + return errors.New("sqle-gaussdb-plugin: Ping called before database connection initialised") + } + return wrapPGError(d.DriverImpl.DB.PingContext(ctx)) +} + +// wrapPGError surfaces openGauss / GaussDB SQLSTATE codes verbatim to upper +// layers (design §5.3). The strategy is: +// +// - If err is nil, return nil. +// - If err is a *pq.Error from gitee.com/opengauss/openGauss-connector-go-pq +// (the only sql driver this plugin ever registers), return it unchanged. +// The lib/pq-compatible Error type already implements `error` with a +// "pq: " string and the .Code field exposes the raw 5-character +// SQLSTATE for callers that want to branch on it. +// - For any other error type (network failure, context cancellation, etc.) +// return the error unchanged. We deliberately do not classify or +// translate non-pq errors so that the original cause survives the trip +// through plugin gRPC. +// +// Implementation note: we type-detect the pq.Error via reflect on the type +// name so this file does not need a direct import of +// gitee.com/opengauss/openGauss-connector-go-pq (that import lives in cmd/* +// for the side-effect sql.Register and in internal/dialector for the same +// reason). Keeping the driver/ package free of the connector dependency keeps +// the unit-test surface small — sqlmock-backed tests do not need to vendor +// the openGauss connector to satisfy the wrapPGError code path. +func wrapPGError(err error) error { + if err == nil { + return nil + } + // Walk the wrap chain looking for an *openGauss pq.Error (or the + // lib/pq.Error fork that shares the same exported type name and field + // shape — design §5.3 explicitly relies on this structural compatibility). + cur := err + for cur != nil { + t := reflect.TypeOf(cur) + if t != nil && t.Kind() == reflect.Ptr { + elem := t.Elem() + if elem.Kind() == reflect.Struct && elem.Name() == "Error" { + // Best-effort check: the openGauss / lib-pq Error type is + // identified by package path + type name. We only need it to + // pass through unchanged, so once we recognise it we return + // the original error verbatim. + pkg := elem.PkgPath() + if pkg == "gitee.com/opengauss/openGauss-connector-go-pq" || + pkg == "github.com/lib/pq" { + return err + } + } + } + next := errors.Unwrap(cur) + if next == nil { + break + } + cur = next + } + return err +} + +// GetDatabaseObjectDDL delegates to internal/extractor (Task-Dev-005 / 006). +// See plan §4.2 / §6.1: the per-schema iteration constructs a single shared +// *extractor.Extractor (one *sql.DB scope per Driver instance) and calls +// Extract once per *driverV2.DatabaseSchemaInfo, appending each result into +// the returned slice. +// +// Defensive guard: when the embedded *sql.DB has not been initialised (e.g. +// cfg.DSN==nil capability-probe path), surface a clear error rather than +// dereferencing nil — same form as Ping above. +// +// Error propagation: a single extract failure returns (nil, err) immediately; +// callers receive the first failure as the deterministic outcome and the +// plugin gRPC layer surfaces it to dms-ui-ee. +// +// Defining this method on *Driver (rather than overriding on +// GaussDBDriver / OpenGaussDriver) means both variants share one body via +// embedding. The previous per-variant overrides that returned the +// errNotImplemented sentinel have been removed by Task-Dev-008. +func (d *Driver) GetDatabaseObjectDDL( + ctx context.Context, + objInfos []*driverV2.DatabaseSchemaInfo, +) ([]*driverV2.DatabaseSchemaObjectResult, error) { + if d == nil || d.DriverImpl == nil || d.DriverImpl.DB == nil { + return nil, errors.New( + "sqle-gaussdb-plugin: GetDatabaseObjectDDL called before database connection initialised") + } + + // One Extractor per call site is fine — construction is cheap and the + // receiver only retains *sql.DB; reusing it across the loop keeps the + // single-instance semantic explicit. + ex := extractor.NewExtractor(d.DriverImpl.DB) + + results := make([]*driverV2.DatabaseSchemaObjectResult, 0, len(objInfos)) + for _, info := range objInfos { + if info == nil { + continue + } + // Task-Test-Fix-002 P2-B: 入口剥签名,让带签名的 FUNCTION/PROCEDURE + // ObjectName(如 `fn_calc(p integer)`)也能命中 pg_proc.proname。 + // 该 normalize 仅作用于 extractor 入参,不影响 result.SchemaName / + // 返回的 ObjectName(由 extractor 内部按 pg_get_function_arguments + // 输出重新拼接)。 + normInfo := &driverV2.DatabaseSchemaInfo{ + SchemaName: info.SchemaName, + DatabaseObjects: normalizeObjectsForExtractor(info.DatabaseObjects), + } + result, err := ex.Extract(ctx, normInfo) + if err != nil { + return nil, err + } + results = append(results, result) + } + return results, nil +} + +// GetDatabaseDiffModifySQL delegates to internal/extractor + internal/differ +// (Task-Dev-005 / 006 / 007). See plan §4.3 / §6.1. +// +// For each *driverV2.DatabasCompareSchemaInfo we run extractor.Extract twice +// (base side then compared side) over the SAME DatabaseObjects list — the +// real chain ships base/compared schema names alongside one shared object +// list, so the driver layer destructures that into two extractor calls that +// only differ in SchemaName. The two results feed differ.GenerateModifySQLs +// (Task-Dev-007 4-arg signature) to produce the ModifySQLs string slice. +// +// SchemaName on the returned DatabaseDiffModifySQLResult is set to +// info.BaseSchemaName per design §7 / impact_analysis.yml line 226 — business +// layer organises the comparison tree by base schema. The compared schema +// name is already encoded into DROP statements' schema qualifier inside +// differ output, so we deliberately do not surface it on the outer result. +// +// calibratedDSN: not used this period. design §5 / plan §4 does not require +// rebuilding the database connection at differ time — d.DriverImpl.DB already +// holds the target-instance connection established at construction. The +// parameter is retained on the signature (interface contract) and explicitly +// discarded with `_ = calibratedDSN` so go vet's unused-parameter check stays +// silent; future cross-instance compare scenarios may consume it. +// +// Defensive guard mirrors GetDatabaseObjectDDL: nil DB → explicit error. +// +// Error propagation: any extract failure (base or compared side) returns +// (nil, err) immediately. We do not partially fail / partially succeed — +// callers see a deterministic single-error outcome. +func (d *Driver) GetDatabaseDiffModifySQL( + ctx context.Context, + calibratedDSN *driverV2.DSN, + objInfos []*driverV2.DatabasCompareSchemaInfo, +) ([]*driverV2.DatabaseDiffModifySQLResult, error) { + if d == nil || d.DriverImpl == nil || d.DriverImpl.DB == nil { + return nil, errors.New( + "sqle-gaussdb-plugin: GetDatabaseDiffModifySQL called before database connection initialised") + } + // reserved for future cross-instance compare; this period uses the + // embedded *sql.DB exclusively (single-instance both-side compare). + _ = calibratedDSN + + ex := extractor.NewExtractor(d.DriverImpl.DB) + + results := make([]*driverV2.DatabaseDiffModifySQLResult, 0, len(objInfos)) + for _, info := range objInfos { + if info == nil { + continue + } + + // Task-Test-Fix-002 P2-B: 入口剥签名(同 GetDatabaseObjectDDL)。 + // UI 主路径会把树中显示文本 `fn_calc(p integer)` 作为 ObjectName + // 透传给 plugin,必须在调 extractor 之前剥签名,否则 + // pg_proc.proname 查询命中 0 行,走"占位空 DDL"分支,differ 比对 + // 输出 modify_sqls=[](case-3-3.md / case-3-4.md 缺陷溯源)。 + normObjs := normalizeObjectsForExtractor(info.DatabaseObjects) + + baseInfo := &driverV2.DatabaseSchemaInfo{ + SchemaName: info.BaseSchemaName, + DatabaseObjects: normObjs, + } + baseResult, err := ex.Extract(ctx, baseInfo) + if err != nil { + return nil, err + } + + comparedInfo := &driverV2.DatabaseSchemaInfo{ + SchemaName: info.ComparedSchemaName, + DatabaseObjects: normObjs, + } + comparedResult, err := ex.Extract(ctx, comparedInfo) + if err != nil { + return nil, err + } + + sqls := differ.GenerateModifySQLs( + baseResult.SchemaName, + baseResult.DatabaseObjectDDLs, + comparedResult.SchemaName, + comparedResult.DatabaseObjectDDLs, + ) + + results = append(results, &driverV2.DatabaseDiffModifySQLResult{ + SchemaName: info.BaseSchemaName, + ModifySQLs: sqls, + }) + } + return results, nil +} diff --git a/internal/driver/driver_gaussdb.go b/internal/driver/driver_gaussdb.go new file mode 100644 index 0000000..24977e6 --- /dev/null +++ b/internal/driver/driver_gaussdb.go @@ -0,0 +1,56 @@ +// driver_gaussdb.go implements the PluginName="GaussDB" variant of the +// sqle-gaussdb-plugin Driver. +// +// Structurally it is a thin aggregation over the shared Driver type defined +// in driver_common.go. The only GaussDB-specific behaviour at the Driver +// layer (vs the Dialector layer) is the variant tag carried on the embedded +// Driver, which lets sqlmock-backed unit tests assert that the two factory +// functions produce two distinct instances (design §17.1.5 +// PluginNameRegistration). +// +// GetDatabaseObjectDDL / GetDatabaseDiffModifySQL are NOT overridden here — +// since Task-Dev-008 the real implementations live on *Driver (driver_common.go) +// and both GaussDBDriver and OpenGaussDriver inherit them via embedding. The +// per-variant override that previously returned an errNotImplemented sentinel +// has been removed because internal/extractor (Task-Dev-005 / 006) and +// internal/differ (Task-Dev-007) have landed. +package driver + +import ( + driverV2 "github.com/actiontech/sqle/sqle/driver/v2" + driverPkg "github.com/actiontech/sqle/sqle/pkg/driver" + hclog "github.com/hashicorp/go-hclog" + + "github.com/actiontech/sqle-gaussdb-plugin/pkg/consts" +) + +// GaussDBDriver is the driverV2.Driver implementation that +// cmd/sqle-gaussdb-plugin/main.go serves under PluginName="GaussDB". +type GaussDBDriver struct { + *Driver +} + +// Compile-time assertion that *GaussDBDriver satisfies driverV2.Driver. This +// is structurally guaranteed by the embedded *Driver (which in turn embeds +// *driverPkg.DriverImpl), but locking it here surfaces refactor breakage +// immediately. +var _ driverV2.Driver = (*GaussDBDriver)(nil) + +// NewGaussDBDriverImpl is the Driver factory consumed by +// cmd/sqle-gaussdb-plugin/main.go via builder.Serve. The function signature +// matches driverPkg.DriverBuilder.Serve's fn parameter shape and is identical +// to the previous stub.go signature on purpose — preserving it means the cmd +// entrypoint never has to change between Task-Dev-002 (stub) and Task-Dev-004 +// (real Driver). +func NewGaussDBDriverImpl( + l hclog.Logger, + dt driverPkg.Dialector, + ah *driverPkg.AuditHandler, + cfg *driverV2.Config, +) (driverV2.Driver, error) { + base, err := newCommon(l, dt, ah, cfg, consts.PluginNameGaussDB) + if err != nil { + return nil, err + } + return &GaussDBDriver{Driver: base}, nil +} diff --git a/internal/driver/driver_opengauss.go b/internal/driver/driver_opengauss.go new file mode 100644 index 0000000..1721810 --- /dev/null +++ b/internal/driver/driver_opengauss.go @@ -0,0 +1,45 @@ +// driver_opengauss.go implements the PluginName="openGauss" variant of the +// sqle-gaussdb-plugin Driver. See driver_gaussdb.go for the structural +// rationale — this file mirrors that shape with only the variant tag / +// PluginName differing. Variant-specific connection behaviour (default port, +// default sslmode, libpq DSN layout) lives in internal/dialector and never +// in this layer; design §3.2 / §4.3 keeps the split deliberate. +// +// GetDatabaseObjectDDL / GetDatabaseDiffModifySQL are NOT overridden here — +// since Task-Dev-008 the real implementations live on *Driver (driver_common.go), +// inherited via embedding. design §6.1 / §7.1 explicitly declares the extractor +// and differ as variant-agnostic, so the two variants share a single body. +package driver + +import ( + driverV2 "github.com/actiontech/sqle/sqle/driver/v2" + driverPkg "github.com/actiontech/sqle/sqle/pkg/driver" + hclog "github.com/hashicorp/go-hclog" + + "github.com/actiontech/sqle-gaussdb-plugin/pkg/consts" +) + +// OpenGaussDriver is the driverV2.Driver implementation that +// cmd/sqle-opengauss-plugin/main.go serves under PluginName="openGauss". +type OpenGaussDriver struct { + *Driver +} + +// Compile-time assertion: *OpenGaussDriver satisfies driverV2.Driver. +var _ driverV2.Driver = (*OpenGaussDriver)(nil) + +// NewOpenGaussDriverImpl is the Driver factory consumed by +// cmd/sqle-opengauss-plugin/main.go via builder.Serve. Signature matches the +// previous stub.go exactly so the cmd entrypoint never has to change. +func NewOpenGaussDriverImpl( + l hclog.Logger, + dt driverPkg.Dialector, + ah *driverPkg.AuditHandler, + cfg *driverV2.Config, +) (driverV2.Driver, error) { + base, err := newCommon(l, dt, ah, cfg, consts.PluginNameOpenGauss) + if err != nil { + return nil, err + } + return &OpenGaussDriver{Driver: base}, nil +} diff --git a/internal/driver/driver_test.go b/internal/driver/driver_test.go new file mode 100644 index 0000000..5c6e2d9 --- /dev/null +++ b/internal/driver/driver_test.go @@ -0,0 +1,1002 @@ +// driver_test.go covers design §17.1.5 — Driver capability matrix + Ping +// behaviour — using map-case style and DATA-DOG/go-sqlmock to keep all +// database interactions hermetic. The tests deliberately avoid the +// newCommon → driverPkg.NewDriverImpl factory path because that path requires +// a real Dialector.Open and would either touch the network or duplicate the +// dialector tests. Instead each case constructs *Driver directly, injecting +// the sqlmock-backed *sql.DB into the embedded *driverPkg.DriverImpl. This +// keeps the test focused on Driver-layer behaviour: pool policy, Ping +// delegation, variant tagging, and the not-implemented sentinel for +// extractor / differ entry-points. +package driver + +import ( + "context" + "database/sql" + "errors" + "strings" + "testing" + "time" + + "github.com/DATA-DOG/go-sqlmock" + driverV2 "github.com/actiontech/sqle/sqle/driver/v2" + driverPkg "github.com/actiontech/sqle/sqle/pkg/driver" + + "github.com/actiontech/sqle-gaussdb-plugin/pkg/consts" +) + +// newTestDriver wires a sqlmock-backed *sql.DB into a *Driver with the given +// variant. The returned cleanup function closes the underlying mock so the +// test does not leak file descriptors. We bypass newCommon because that path +// triggers Dialector.Open which would need a real network connection. +// +// MonitorPingsOption(true) is required for ExpectPing().WillDelayFor to take +// effect — sqlmock defaults to ignoring db.Ping() so the Ping-timeout case +// would otherwise return immediately instead of honouring the configured +// delay (see go-sqlmock README "MonitorPings" section). +func newTestDriver(t *testing.T, variant string) (*Driver, sqlmock.Sqlmock, func()) { + t.Helper() + db, mock, err := sqlmock.New(sqlmock.MonitorPingsOption(true)) + if err != nil { + t.Fatalf("sqlmock.New: %v", err) + } + impl := &driverPkg.DriverImpl{ + Config: &driverV2.Config{}, + DB: db, + } + d := &Driver{DriverImpl: impl, Variant: variant} + cleanup := func() { _ = db.Close() } + return d, mock, cleanup +} + +// TestDriver_PluginNameRegistration covers design §17.1.5 +// PluginNameRegistration. The two PluginName variants must produce +// structurally distinct Driver instances with the correct Variant tag, and +// must continue to satisfy driverV2.Driver at runtime (the compile-time +// assertions in driver_gaussdb.go / driver_opengauss.go cover the static +// side; this test exercises the dynamic side via a runtime type assertion on +// the factory return value). +func TestDriver_PluginNameRegistration(t *testing.T) { + cases := map[string]struct { + variant string + factory func(*Driver) driverV2.Driver + wantConcrete string + wantPluginName string + }{ + "GaussDB variant wraps shared Driver": { + variant: consts.PluginNameGaussDB, + factory: func(d *Driver) driverV2.Driver { return &GaussDBDriver{Driver: d} }, + wantConcrete: "*driver.GaussDBDriver", + wantPluginName: "GaussDB", + }, + "openGauss variant wraps shared Driver": { + variant: consts.PluginNameOpenGauss, + factory: func(d *Driver) driverV2.Driver { return &OpenGaussDriver{Driver: d} }, + wantConcrete: "*driver.OpenGaussDriver", + wantPluginName: "openGauss", + }, + "variant tag persists through embed chain": { + variant: "GaussDB", + factory: func(d *Driver) driverV2.Driver { return &GaussDBDriver{Driver: d} }, + wantConcrete: "*driver.GaussDBDriver", + wantPluginName: "GaussDB", + }, + } + + for name, c := range cases { + t.Run(name, func(t *testing.T) { + base, _, cleanup := newTestDriver(t, c.variant) + defer cleanup() + + drv := c.factory(base) + + // Concrete type name asserts that the two factory functions land + // on two structurally distinct types. + gotConcrete := concreteTypeName(drv) + if gotConcrete != c.wantConcrete { + t.Errorf("concrete type = %s, want %s", gotConcrete, c.wantConcrete) + } + + // Variant tag survives the embed chain — both + // (*GaussDBDriver).Driver and (*OpenGaussDriver).Driver point at + // the shared *Driver value whose Variant was set by the factory. + gotVariant := "" + switch x := drv.(type) { + case *GaussDBDriver: + gotVariant = x.Variant + case *OpenGaussDriver: + gotVariant = x.Variant + default: + t.Fatalf("unexpected concrete type %T", drv) + } + if gotVariant != c.wantPluginName { + t.Errorf("Variant = %q, want %q", gotVariant, c.wantPluginName) + } + }) + } +} + +// TestDriver_CapabilityMatrix locks the design §5.2 / §17.1.5 capability +// matrix. Per Task-Dev-004 派单 the test enumerates each of the **ten** +// OptionalModule values from design §5.2 that has a corresponding +// driverV2.Driver method, asserting the surface behaviour the plugin commits +// to today: +// +// - OptionalGetDatabaseObjectDDL (enabled this period — placeholder) +// - OptionalGetDatabaseDiffModifySQL (enabled this period — placeholder) +// - OptionalModuleGenRollbackSQL (disabled — DriverImpl default empty) +// - OptionalModuleExplain (disabled — DriverImpl default empty) +// - OptionalModuleGetTableMeta (disabled — DriverImpl default empty) +// - OptionalModuleExtractTableFromSQL (disabled — DriverImpl default empty) +// - OptionalModuleEstimateSQLAffectRows (disabled — DriverImpl default empty) +// - OptionalModuleKillProcess (disabled — DriverImpl default empty) +// - OptionalBackup (disabled — DriverImpl default empty) +// - OptionalModuleQuery (disabled — requires *sql.Conn, surfaces +// the "database conn not initialized" +// sentinel since cfg.DSN==nil) +// +// The eleventh entry on the design table, OptionalModuleI18n, has no +// dedicated method on driverV2.Driver — it is signalled via DriverMetas only, +// so it is exercised by the dedicated TestCapabilityMatrix_I18nMetadataOnly +// below rather than through this main matrix. +// +// The two GaussDB and openGauss variants exercise the same matrix; running +// both keeps the variant decoupling honest while still tracking design §5.2. +func TestDriver_CapabilityMatrix(t *testing.T) { + type probeResult struct { + err error + nonNil bool + details string + } + cases := map[string]struct { + module driverV2.OptionalModule + probe func(ctx context.Context, drv driverV2.Driver) probeResult + wantErrSubstr string // substring expected in err.Error(); "" means no error + wantNonNil bool // true → the probe result must be non-nil typed value + }{ + "OptionalGetDatabaseObjectDDL — enabled, returns empty result for nil objInfos": { + module: driverV2.OptionalGetDatabaseObjectDDL, + probe: func(ctx context.Context, drv driverV2.Driver) probeResult { + // Probe the real chain with nil objInfos: the for-range loop + // iterates zero times so extractor.NewExtractor is constructed + // but never invoked. Expected: ([]*DatabaseSchemaObjectResult{}, nil). + res, err := drv.GetDatabaseObjectDDL(ctx, nil) + return probeResult{err: err, nonNil: res != nil} + }, + wantNonNil: true, + }, + "OptionalGetDatabaseDiffModifySQL — enabled, returns empty result for nil objInfos": { + module: driverV2.OptionalGetDatabaseDiffModifySQL, + probe: func(ctx context.Context, drv driverV2.Driver) probeResult { + // Same probe shape: nil objInfos => empty slice, nil err. + res, err := drv.GetDatabaseDiffModifySQL(ctx, nil, nil) + return probeResult{err: err, nonNil: res != nil} + }, + wantNonNil: true, + }, + "OptionalModuleGenRollbackSQL — disabled, DriverImpl default returns empty strings": { + module: driverV2.OptionalModuleGenRollbackSQL, + probe: func(ctx context.Context, drv driverV2.Driver) probeResult { + a, b, err := drv.GenRollbackSQL(ctx, "SELECT 1") + return probeResult{err: err, details: a + "|" + b} + }, + }, + "OptionalModuleExplain — disabled, DriverImpl default returns empty *ExplainResult": { + module: driverV2.OptionalModuleExplain, + probe: func(ctx context.Context, drv driverV2.Driver) probeResult { + res, err := drv.Explain(ctx, &driverV2.ExplainConf{}) + return probeResult{err: err, nonNil: res != nil} + }, + wantNonNil: true, + }, + "OptionalModuleGetTableMeta — disabled, DriverImpl default returns empty *TableMeta": { + module: driverV2.OptionalModuleGetTableMeta, + probe: func(ctx context.Context, drv driverV2.Driver) probeResult { + res, err := drv.GetTableMeta(ctx, &driverV2.Table{}) + return probeResult{err: err, nonNil: res != nil} + }, + wantNonNil: true, + }, + "OptionalModuleExtractTableFromSQL — disabled, DriverImpl default returns empty []*Table": { + module: driverV2.OptionalModuleExtractTableFromSQL, + probe: func(ctx context.Context, drv driverV2.Driver) probeResult { + res, err := drv.ExtractTableFromSQL(ctx, "SELECT 1") + return probeResult{err: err, nonNil: res != nil} + }, + wantNonNil: true, + }, + "OptionalModuleEstimateSQLAffectRows — disabled, DriverImpl default returns empty *EstimatedAffectRows": { + module: driverV2.OptionalModuleEstimateSQLAffectRows, + probe: func(ctx context.Context, drv driverV2.Driver) probeResult { + res, err := drv.EstimateSQLAffectRows(ctx, "SELECT 1") + return probeResult{err: err, nonNil: res != nil} + }, + wantNonNil: true, + }, + "OptionalModuleKillProcess — disabled, DriverImpl default returns empty *KillProcessInfo": { + module: driverV2.OptionalModuleKillProcess, + probe: func(ctx context.Context, drv driverV2.Driver) probeResult { + res, err := drv.KillProcess(ctx) + return probeResult{err: err, nonNil: res != nil} + }, + wantNonNil: true, + }, + "OptionalBackup — disabled, DriverImpl default returns (nil, nil)": { + module: driverV2.OptionalBackup, + probe: func(ctx context.Context, drv driverV2.Driver) probeResult { + res, err := drv.Backup(ctx, &driverV2.BackupReq{}) + return probeResult{err: err, nonNil: res != nil} + }, + // DriverImpl.Backup returns (nil, nil); assert only the no-err + // contract, not the response value. + }, + "OptionalModuleQuery — disabled, no *sql.Conn → \"database conn not initialized\"": { + module: driverV2.OptionalModuleQuery, + probe: func(ctx context.Context, drv driverV2.Driver) probeResult { + _, err := drv.Query(ctx, "SELECT 1", nil) + return probeResult{err: err} + }, + wantErrSubstr: "database conn not initialized", + }, + } + + // Exercise both variants for every case so a variant-specific override + // (today: only the two enabled-this-period methods on GaussDBDriver / + // OpenGaussDriver) is caught immediately. + variants := []string{consts.PluginNameGaussDB, consts.PluginNameOpenGauss} + for _, variant := range variants { + variant := variant + for name, c := range cases { + c := c + t.Run(variant+"/"+name, func(t *testing.T) { + base, _, cleanup := newTestDriver(t, variant) + defer cleanup() + + var drv driverV2.Driver + switch variant { + case consts.PluginNameGaussDB: + drv = &GaussDBDriver{Driver: base} + case consts.PluginNameOpenGauss: + drv = &OpenGaussDriver{Driver: base} + } + + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + + got := c.probe(ctx, drv) + + if c.wantErrSubstr != "" { + if got.err == nil { + t.Fatalf("module=%s expected error containing %q, got nil", c.module, c.wantErrSubstr) + } + if !strings.Contains(got.err.Error(), c.wantErrSubstr) { + t.Errorf("module=%s error %q does not contain %q", c.module, got.err.Error(), c.wantErrSubstr) + } + return + } + if got.err != nil { + t.Fatalf("module=%s expected no error, got %v", c.module, got.err) + } + if c.wantNonNil && !got.nonNil { + t.Errorf("module=%s expected non-nil response, got nil", c.module) + } + }) + } + } +} + +// TestCapabilityMatrix_I18nMetadataOnly covers the eleventh row of design §5.2 +// (OptionalModuleI18n). The Driver interface itself has no I18n entry-point — +// the flag flows through DriverMetas.EnabledOptionalModule into the +// audit-result pipeline. This test simply locks the constant value so a +// future refactor of OptionalModule iota ordering cannot silently break the +// cmd/* main.go capability declaration. +func TestCapabilityMatrix_I18nMetadataOnly(t *testing.T) { + cases := map[string]struct { + module driverV2.OptionalModule + wantName string + }{ + "OptionalModuleI18n stringifies to \"I18n\"": { + module: driverV2.OptionalModuleI18n, + wantName: "I18n", + }, + } + for name, c := range cases { + c := c + t.Run(name, func(t *testing.T) { + if got := c.module.String(); got != c.wantName { + t.Errorf("OptionalModule.String() = %q, want %q", got, c.wantName) + } + }) + } +} + +// TestDriver_PingTimeout covers design §17.1.5 PingTimeout and exploration +// §1.4 R-Q1-1. The Driver.Ping override must (a) propagate context +// cancellation / deadline so a stuck backend does not hang the plugin process +// and (b) surface a clear error rather than panicking when the underlying +// *sql.DB is nil (the cfg.DSN==nil construction path). +func TestDriver_PingTimeout(t *testing.T) { + cases := map[string]struct { + setup func(t *testing.T) (*Driver, func()) + ctxFactory func() (context.Context, context.CancelFunc) + // wantErrSubstr is asserted when non-empty. For the deadline case we + // rely on wantCtxErrExpired instead, because sqlmock surfaces its + // internal "canceling query due to user request" string before + // database/sql wraps the ctx error; we still verify the ctx itself + // has reached the cancelled / deadline state to prove Ping honoured + // the deadline. + wantErrSubstr string + wantCtxErrExpired bool + }{ + "deadline exceeded — ctx expires, Ping returns non-nil error within budget": { + setup: func(t *testing.T) (*Driver, func()) { + d, mock, cleanup := newTestDriver(t, consts.PluginNameGaussDB) + // Force the sqlmock ping to take longer than the context + // deadline; sqlmock honours ExpectPing.WillDelayFor. + mock.ExpectPing().WillDelayFor(200 * time.Millisecond) + return d, cleanup + }, + ctxFactory: func() (context.Context, context.CancelFunc) { + return context.WithTimeout(context.Background(), 10*time.Millisecond) + }, + wantCtxErrExpired: true, + }, + "cancelled context surfaces context.Canceled": { + setup: func(t *testing.T) (*Driver, func()) { + d, mock, cleanup := newTestDriver(t, consts.PluginNameOpenGauss) + mock.ExpectPing().WillDelayFor(50 * time.Millisecond) + return d, cleanup + }, + ctxFactory: func() (context.Context, context.CancelFunc) { + ctx, cancel := context.WithCancel(context.Background()) + cancel() // cancel immediately so Ping observes the cancellation + return ctx, func() {} + }, + wantErrSubstr: "context canceled", + wantCtxErrExpired: true, + }, + "nil *sql.DB returns explicit initialisation error": { + setup: func(t *testing.T) (*Driver, func()) { + // Bypass sqlmock entirely so DriverImpl.DB stays nil. + impl := &driverPkg.DriverImpl{Config: &driverV2.Config{}} + d := &Driver{DriverImpl: impl, Variant: consts.PluginNameGaussDB} + return d, func() {} + }, + ctxFactory: func() (context.Context, context.CancelFunc) { + return context.WithTimeout(context.Background(), time.Second) + }, + wantErrSubstr: "before database connection initialised", + }, + } + + for name, c := range cases { + t.Run(name, func(t *testing.T) { + d, cleanup := c.setup(t) + defer cleanup() + + ctx, cancel := c.ctxFactory() + defer cancel() + + // Drive Ping via the GaussDBDriver wrapper to mirror the runtime + // call chain (cmd/* → driverV2.Driver → embedded *Driver.Ping). + drv := driverV2.Driver(&GaussDBDriver{Driver: d}) + + // Bound the Ping call so a regression that breaks ctx propagation + // fails the test instead of hanging the suite. + done := make(chan error, 1) + go func() { done <- drv.Ping(ctx) }() + + var err error + select { + case err = <-done: + case <-time.After(2 * time.Second): + t.Fatal("Ping did not return within 2s; ctx propagation broken") + } + + if err == nil { + t.Fatalf("expected non-nil error from Ping") + } + if c.wantErrSubstr != "" && !strings.Contains(err.Error(), c.wantErrSubstr) { + t.Errorf("error %q does not contain %q", err.Error(), c.wantErrSubstr) + } + if c.wantCtxErrExpired && ctx.Err() == nil { + t.Errorf("ctx.Err() unexpectedly nil; Ping did not honour the deadline") + } + }) + } +} + +// TestWrapPGError locks the SQLSTATE pass-through contract from design §5.3. +// Each case asserts that the helper preserves error identity (== check) so +// that callers can still type-assert the original error after wrapping. +func TestWrapPGError(t *testing.T) { + plainErr := errors.New("connection refused") + sentinel := errors.New("sqle-gaussdb-plugin: probe sentinel") + + cases := map[string]struct { + in error + wantNil bool + wantSame error + }{ + "nil propagates as nil": { + in: nil, + wantNil: true, + }, + "plain error returns same instance unchanged": { + in: plainErr, + wantSame: plainErr, + }, + "sentinel error preserves identity": { + in: sentinel, + wantSame: sentinel, + }, + } + + for name, c := range cases { + t.Run(name, func(t *testing.T) { + got := wrapPGError(c.in) + if c.wantNil { + if got != nil { + t.Errorf("expected nil, got %v", got) + } + return + } + if got != c.wantSame { //nolint:errorlint // we want pointer-equality on purpose + t.Errorf("expected same error instance %v, got %v", c.wantSame, got) + } + }) + } +} + +// TestApplyPoolPolicy locks the connection-pool constants from design §5.1 +// line 328 onto the *sql.DB after newCommon. The Stats() snapshot exposes +// MaxOpenConnections directly; MaxIdleConnections and ConnMaxLifetime are not +// readable post-set in Go 1.19, so we additionally re-call SetMaxIdleConns +// and assert the no-op (sql.DB stores the value internally; the assertion is +// implicit — applyPoolPolicy returning without panic with a nil DB exercises +// the defensive branch). +func TestApplyPoolPolicy(t *testing.T) { + cases := map[string]struct { + dbNil bool + wantMaxOpen int + }{ + "applies MaxOpenConns when db is non-nil": { + dbNil: false, + wantMaxOpen: maxOpenConns, + }, + "no-op when db is nil (cfg.DSN==nil construction path)": { + dbNil: true, + wantMaxOpen: 0, + }, + } + + for name, c := range cases { + t.Run(name, func(t *testing.T) { + var db *sql.DB + if !c.dbNil { + m, _, err := sqlmock.New() + if err != nil { + t.Fatalf("sqlmock.New: %v", err) + } + defer m.Close() + db = m + } + applyPoolPolicy(db) + if c.dbNil { + // The defensive nil check is the contract; reaching this point + // without panicking proves it. + return + } + if got := db.Stats().MaxOpenConnections; got != c.wantMaxOpen { + t.Errorf("MaxOpenConnections = %d, want %d", got, c.wantMaxOpen) + } + }) + } +} + +// extractTableSQLLiteral mirrors internal/extractor/table.go:extractTableSQL +// byte-for-byte. We deliberately duplicate the literal here (rather than +// importing the unexported const) so the real-chain driver tests below do not +// take a code dependency on the extractor package internals — driver layer's +// contract is `extractor.NewExtractor(db).Extract(ctx, info)` and nothing else. +// Any drift between this literal and extractor/table.go would be caught by the +// existing TestExtractor_Extract_TableAndView (which exercises the canonical +// const directly); the redundancy here is intentional documentation of the +// SQL the driver layer ultimately issues. +const extractTableSQLLiteral = `SELECT pg_get_tabledef(c.oid) +FROM pg_class c +JOIN pg_namespace n ON n.oid = c.relnamespace +WHERE n.nspname = $1 + AND c.relkind IN ('r', 'p') + AND c.relname = $2` + +// newRealChainTestDriver builds a *Driver whose embedded *sql.DB is a +// sqlmock-backed connection with QueryMatcherEqual mode (so SQL-literal +// assertions in real-chain tests below lock the byte-for-byte design §6.2.1 +// template; see semantic sqlmock_query_matcher_equal_injection_defense_20260521). +// Unlike newTestDriver above, MonitorPingsOption is left at its default — the +// real-chain tests never call Ping. +func newRealChainTestDriver(t *testing.T) (*Driver, sqlmock.Sqlmock, func()) { + t.Helper() + db, mock, err := sqlmock.New( + sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual), + ) + if err != nil { + t.Fatalf("sqlmock.New: %v", err) + } + impl := &driverPkg.DriverImpl{ + Config: &driverV2.Config{}, + DB: db, + } + d := &Driver{DriverImpl: impl, Variant: consts.PluginNameGaussDB} + cleanup := func() { _ = db.Close() } + return d, mock, cleanup +} + +// TestDriver_GetDatabaseObjectDDL_RealChain exercises the live wiring from +// driver.GetDatabaseObjectDDL through extractor.NewExtractor(db).Extract to +// the *sql.DB catalog query. Three sub-cases cover the contractual surface: +// +// - empty objInfos slice → empty results + nil error +// - one DatabaseSchemaInfo with one TABLE object → results length 1 with +// the round-tripped pg_get_tabledef DDL +// - catalog query error (sql.ErrConnDone) → error propagates, results nil +// +// All cases use sqlmock.QueryMatcherEqual via newRealChainTestDriver so the +// design §6.2.1 SQL template is locked byte-for-byte. +func TestDriver_GetDatabaseObjectDDL_RealChain(t *testing.T) { + const tableDDL = "SET search_path = 'public'; CREATE TABLE public.t1 (id int) WITH (orientation=row);" + + cases := map[string]struct { + objInfos []*driverV2.DatabaseSchemaInfo + mockSetup func(mock sqlmock.Sqlmock) + wantLen int + wantErr bool + wantErrSubstr string + wantFirstDDL string + wantFirstObjNm string + }{ + "empty objInfos returns empty slice": { + objInfos: []*driverV2.DatabaseSchemaInfo{}, + mockSetup: func(mock sqlmock.Sqlmock) {}, // no calls expected + wantLen: 0, + }, + "single schema with one table extracted": { + objInfos: []*driverV2.DatabaseSchemaInfo{ + { + SchemaName: "public", + DatabaseObjects: []*driverV2.DatabaseObject{ + {ObjectName: "t1", ObjectType: driverV2.ObjectType_TABLE}, + }, + }, + }, + mockSetup: func(mock sqlmock.Sqlmock) { + mock.ExpectQuery(extractTableSQLLiteral). + WithArgs("public", "t1"). + WillReturnRows(sqlmock.NewRows([]string{"pg_get_tabledef"}).AddRow(tableDDL)) + }, + wantLen: 1, + wantFirstDDL: tableDDL, + wantFirstObjNm: "t1", + }, + "extract error propagates": { + objInfos: []*driverV2.DatabaseSchemaInfo{ + { + SchemaName: "public", + DatabaseObjects: []*driverV2.DatabaseObject{ + {ObjectName: "t1", ObjectType: driverV2.ObjectType_TABLE}, + }, + }, + }, + mockSetup: func(mock sqlmock.Sqlmock) { + mock.ExpectQuery(extractTableSQLLiteral). + WithArgs("public", "t1"). + WillReturnError(sql.ErrConnDone) + }, + wantErr: true, + wantErrSubstr: sql.ErrConnDone.Error(), + }, + } + + for name, c := range cases { + c := c + t.Run(name, func(t *testing.T) { + d, mock, cleanup := newRealChainTestDriver(t) + defer cleanup() + c.mockSetup(mock) + + drv := driverV2.Driver(&GaussDBDriver{Driver: d}) + + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + + res, err := drv.GetDatabaseObjectDDL(ctx, c.objInfos) + if c.wantErr { + if err == nil { + t.Fatalf("expected error, got nil; res=%v", res) + } + if c.wantErrSubstr != "" && !strings.Contains(err.Error(), c.wantErrSubstr) { + t.Errorf("err %q does not contain %q", err.Error(), c.wantErrSubstr) + } + if res != nil { + t.Errorf("expected nil res on error, got %v", res) + } + } else { + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got := len(res); got != c.wantLen { + t.Fatalf("len(res) = %d, want %d", got, c.wantLen) + } + if c.wantLen > 0 { + first := res[0] + if first == nil { + t.Fatal("res[0] is nil") + } + if got := len(first.DatabaseObjectDDLs); got != 1 { + t.Fatalf("len(res[0].DatabaseObjectDDLs) = %d, want 1", got) + } + ddl := first.DatabaseObjectDDLs[0] + if ddl.ObjectDDL != c.wantFirstDDL { + t.Errorf("ObjectDDL = %q, want %q", ddl.ObjectDDL, c.wantFirstDDL) + } + if ddl.DatabaseObject == nil || ddl.DatabaseObject.ObjectName != c.wantFirstObjNm { + t.Errorf("DatabaseObject.ObjectName = %v, want %q", ddl.DatabaseObject, c.wantFirstObjNm) + } + } + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("sqlmock expectations: %v", err) + } + }) + } +} + +// TestDriver_GetDatabaseDiffModifySQL_RealChain exercises the live wiring from +// driver.GetDatabaseDiffModifySQL through two extractor.Extract invocations +// (base side then compared side) into differ.GenerateModifySQLs. +// +// Sub-cases: +// +// 1. empty objInfos slice → empty results + nil error +// 2. single DatabasCompareSchemaInfo with one TABLE that diffs between +// base and compared → one DatabaseDiffModifySQLResult whose ModifySQLs +// contains the WARNING comment + DROP TABLE IF EXISTS + CREATE TABLE +// (end-to-end DROP+CREATE wiring, design §7.2 / R-Q4-1) +// 3. base-side extract error → error propagates, compared-side ExpectQuery +// is intentionally NOT registered to prove the second call is short- +// circuited (sqlmock.ExpectationsWereMet would still pass because we +// never queued the second expectation) +// +// All cases lock the design §6.2.1 SQL template byte-for-byte via +// QueryMatcherEqual; the base and compared sides issue the SAME SQL with the +// SAME args (only the SchemaName parameter differs) and sqlmock consumes them +// in registration order. +func TestDriver_GetDatabaseDiffModifySQL_RealChain(t *testing.T) { + const ( + baseTableDDL = "CREATE TABLE base.t (id int);" + comparedTableDDL = "CREATE TABLE compared.t (id int, name text);" + ) + + cases := map[string]struct { + objInfos []*driverV2.DatabasCompareSchemaInfo + mockSetup func(mock sqlmock.Sqlmock) + wantLen int + wantErr bool + wantErrSubstr string + wantSchemaName string + wantModifyContain []string // every substring must appear inside the joined ModifySQLs + }{ + "empty objInfos returns empty slice": { + objInfos: []*driverV2.DatabasCompareSchemaInfo{}, + mockSetup: func(mock sqlmock.Sqlmock) {}, // no calls expected + wantLen: 0, + }, + "single compare info with table both diff produces DROP+CREATE": { + objInfos: []*driverV2.DatabasCompareSchemaInfo{ + { + BaseSchemaName: "base", + ComparedSchemaName: "compared", + DatabaseObjects: []*driverV2.DatabaseObject{ + {ObjectName: "t", ObjectType: driverV2.ObjectType_TABLE}, + }, + }, + }, + mockSetup: func(mock sqlmock.Sqlmock) { + // First extract: base side (SchemaName="base", ObjectName="t"). + mock.ExpectQuery(extractTableSQLLiteral). + WithArgs("base", "t"). + WillReturnRows(sqlmock.NewRows([]string{"pg_get_tabledef"}).AddRow(baseTableDDL)) + // Second extract: compared side (SchemaName="compared", ObjectName="t"). + mock.ExpectQuery(extractTableSQLLiteral). + WithArgs("compared", "t"). + WillReturnRows(sqlmock.NewRows([]string{"pg_get_tabledef"}).AddRow(comparedTableDDL)) + }, + wantLen: 1, + wantSchemaName: "base", + wantModifyContain: []string{ + "WARNING", + "DROP TABLE IF EXISTS compared.t", + baseTableDDL, // CREATE TABLE base.t (id int); + }, + }, + "base side extract error propagates": { + objInfos: []*driverV2.DatabasCompareSchemaInfo{ + { + BaseSchemaName: "base", + ComparedSchemaName: "compared", + DatabaseObjects: []*driverV2.DatabaseObject{ + {ObjectName: "t", ObjectType: driverV2.ObjectType_TABLE}, + }, + }, + }, + mockSetup: func(mock sqlmock.Sqlmock) { + // Only the base-side query is expected; compared-side must NOT + // fire (verified implicitly: sqlmock would report unexpected + // query if it did, and ExpectationsWereMet would still pass + // because we never queued the second expectation). + mock.ExpectQuery(extractTableSQLLiteral). + WithArgs("base", "t"). + WillReturnError(sql.ErrConnDone) + }, + wantErr: true, + wantErrSubstr: sql.ErrConnDone.Error(), + }, + } + + for name, c := range cases { + c := c + t.Run(name, func(t *testing.T) { + d, mock, cleanup := newRealChainTestDriver(t) + defer cleanup() + c.mockSetup(mock) + + drv := driverV2.Driver(&GaussDBDriver{Driver: d}) + + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + + res, err := drv.GetDatabaseDiffModifySQL(ctx, nil, c.objInfos) + if c.wantErr { + if err == nil { + t.Fatalf("expected error, got nil; res=%v", res) + } + if c.wantErrSubstr != "" && !strings.Contains(err.Error(), c.wantErrSubstr) { + t.Errorf("err %q does not contain %q", err.Error(), c.wantErrSubstr) + } + if res != nil { + t.Errorf("expected nil res on error, got %v", res) + } + } else { + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got := len(res); got != c.wantLen { + t.Fatalf("len(res) = %d, want %d", got, c.wantLen) + } + if c.wantLen > 0 { + first := res[0] + if first == nil { + t.Fatal("res[0] is nil") + } + if first.SchemaName != c.wantSchemaName { + t.Errorf("SchemaName = %q, want %q", first.SchemaName, c.wantSchemaName) + } + if len(first.ModifySQLs) != 1 { + t.Fatalf("len(ModifySQLs) = %d, want 1; got=%v", len(first.ModifySQLs), first.ModifySQLs) + } + joined := first.ModifySQLs[0] + for _, sub := range c.wantModifyContain { + if !strings.Contains(joined, sub) { + t.Errorf("ModifySQLs[0] missing %q; full=%q", sub, joined) + } + } + } + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("sqlmock expectations: %v", err) + } + }) + } +} + +// extractFunctionDefSQLLiteral / extractFunctionArgsSQLLiteral mirror +// internal/extractor/function.go byte-for-byte (same duplication rationale as +// extractTableSQLLiteral above). +const extractFunctionDefSQLLiteral = `SELECT (pg_get_functiondef(p.oid)).definition +FROM pg_proc p +JOIN pg_namespace n ON n.oid = p.pronamespace +WHERE n.nspname = $1 + AND p.prokind = 'f' + AND p.proname = $2 +ORDER BY p.oid` + +const extractFunctionArgsSQLLiteral = `SELECT pg_get_function_arguments(p.oid) +FROM pg_proc p +JOIN pg_namespace n ON n.oid = p.pronamespace +WHERE n.nspname = $1 + AND p.prokind = 'f' + AND p.proname = $2 +ORDER BY p.oid` + +// TestStripObjectNameSignature 覆盖 Task-Test-Fix-002 P2-B 入口前置 +// normalize helper: +// +// - 带签名 → 剥到 bare name +// - 无签名 → 原样返回 +// - 括号前带空格 → TrimRight 容忍 +// - 空串 → 空串 +// - 仅括号 → 空串 +func TestStripObjectNameSignature(t *testing.T) { + cases := map[string]struct { + in string + want string + }{ + "function_with_param_name_and_type": {in: "fn_calc(p integer)", want: "fn_calc"}, + "function_with_type_only": {in: "fn_calc(integer)", want: "fn_calc"}, + "function_with_multi_args": {in: "fn(integer, text)", want: "fn"}, + "function_with_no_args": {in: "fn()", want: "fn"}, + "bare_name_unchanged": {in: "fn_calc", want: "fn_calc"}, + "empty_string_unchanged": {in: "", want: ""}, + "procedure_with_TEXT_typed_param": {in: "proc_alert(p TEXT)", want: "proc_alert"}, + "space_before_paren_trimmed": {in: "fn_calc (integer)", want: "fn_calc"}, + } + for name, c := range cases { + t.Run(name, func(t *testing.T) { + got := stripObjectNameSignature(c.in) + if got != c.want { + t.Errorf("stripObjectNameSignature(%q) = %q, want %q", c.in, got, c.want) + } + }) + } +} + +// TestNormalizeObjectsForExtractor 覆盖批量 normalize helper: +// +// - 不修改原 *DatabaseObject 指针(拷贝新值) +// - TABLE/VIEW 路径(无括号)原样透传 +// - FUNCTION/PROCEDURE 路径剥签名 +// - nil 切片 / 空切片 / 含 nil 元素 安全处理 +func TestNormalizeObjectsForExtractor(t *testing.T) { + t.Run("empty_input_short_circuits", func(t *testing.T) { + if got := normalizeObjectsForExtractor(nil); got != nil { + t.Errorf("nil input should pass through, got %v", got) + } + if got := normalizeObjectsForExtractor([]*driverV2.DatabaseObject{}); len(got) != 0 { + t.Errorf("empty input should pass through, got %v", got) + } + }) + + t.Run("function_with_signature_stripped_without_mutating_original", func(t *testing.T) { + orig := []*driverV2.DatabaseObject{ + {ObjectName: "fn_calc(p integer)", ObjectType: driverV2.ObjectType_FUNCTION}, + {ObjectName: "t_user", ObjectType: driverV2.ObjectType_TABLE}, + } + got := normalizeObjectsForExtractor(orig) + if len(got) != 2 { + t.Fatalf("got len = %d, want 2", len(got)) + } + if got[0].ObjectName != "fn_calc" { + t.Errorf("got[0].ObjectName = %q, want %q", got[0].ObjectName, "fn_calc") + } + if got[1].ObjectName != "t_user" { + t.Errorf("got[1].ObjectName = %q, want %q", got[1].ObjectName, "t_user") + } + // 原列表不可被改写。 + if orig[0].ObjectName != "fn_calc(p integer)" { + t.Errorf("original ObjectName mutated to %q", orig[0].ObjectName) + } + // TABLE/VIEW 路径下,未修改的元素可以直接复用原指针(节省拷贝)。 + if got[1] != orig[1] { + t.Errorf("TABLE path should reuse the original pointer to avoid copy") + } + }) + + t.Run("nil_element_is_preserved", func(t *testing.T) { + orig := []*driverV2.DatabaseObject{ + nil, + {ObjectName: "fn(integer)", ObjectType: driverV2.ObjectType_FUNCTION}, + } + got := normalizeObjectsForExtractor(orig) + if len(got) != 2 || got[0] != nil { + t.Fatalf("nil element not preserved: %v", got) + } + if got[1].ObjectName != "fn" { + t.Errorf("got[1].ObjectName = %q, want %q", got[1].ObjectName, "fn") + } + }) +} + +// TestDriver_GetDatabaseDiffModifySQL_StripsObjectNameSignature 覆盖 +// Task-Test-Fix-002 P2-B 端到端:UI 主路径传入带签名 ObjectName +// `fn_calc(p integer)` 时,plugin GetDatabaseDiffModifySQL 入口剥签名得到 +// bare `fn_calc` 再传给 extractor —— extractor 用 `proname = 'fn_calc'` +// 命中 pg_proc,差异 SQL 正常输出,UI 抽屉不再"暂无数据" +// (case-3-3.md / case-3-4.md 缺陷复测断言)。 +func TestDriver_GetDatabaseDiffModifySQL_StripsObjectNameSignature(t *testing.T) { + const ( + baseFnDDL = "CREATE OR REPLACE FUNCTION base.fn_calc(p integer) RETURNS integer LANGUAGE plpgsql AS $function$ BEGIN RETURN p * 2; END $function$;" + comparedFnDDL = "CREATE OR REPLACE FUNCTION compared.fn_calc(p integer) RETURNS integer LANGUAGE plpgsql AS $function$ BEGIN RETURN p * 3; END $function$;" + ) + + d, mock, cleanup := newRealChainTestDriver(t) + defer cleanup() + + // 关键断言:driver 入口收到带签名 ObjectName,但 extractor 实际收到 bare + // 名 → sqlmock ExpectQuery 用 bare "fn_calc" 作为参数匹配;如果 P2-B 未 + // 修复,sqlmock 会因"unexpected argument fn_calc(p integer)"而报错。 + mock.ExpectQuery(extractFunctionDefSQLLiteral). + WithArgs("base", "fn_calc"). + WillReturnRows(sqlmock.NewRows([]string{"definition"}).AddRow(baseFnDDL)) + mock.ExpectQuery(extractFunctionArgsSQLLiteral). + WithArgs("base", "fn_calc"). + WillReturnRows(sqlmock.NewRows([]string{"arguments"}).AddRow("p integer")) + mock.ExpectQuery(extractFunctionDefSQLLiteral). + WithArgs("compared", "fn_calc"). + WillReturnRows(sqlmock.NewRows([]string{"definition"}).AddRow(comparedFnDDL)) + mock.ExpectQuery(extractFunctionArgsSQLLiteral). + WithArgs("compared", "fn_calc"). + WillReturnRows(sqlmock.NewRows([]string{"arguments"}).AddRow("p integer")) + + drv := driverV2.Driver(&GaussDBDriver{Driver: d}) + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + + objInfos := []*driverV2.DatabasCompareSchemaInfo{ + { + BaseSchemaName: "base", + ComparedSchemaName: "compared", + DatabaseObjects: []*driverV2.DatabaseObject{ + {ObjectName: "fn_calc(p integer)", ObjectType: driverV2.ObjectType_FUNCTION}, + }, + }, + } + + res, err := drv.GetDatabaseDiffModifySQL(ctx, nil, objInfos) + if err != nil { + t.Fatalf("GetDatabaseDiffModifySQL: %v", err) + } + if len(res) != 1 { + t.Fatalf("len(res) = %d, want 1", len(res)) + } + if len(res[0].ModifySQLs) != 1 { + t.Fatalf("len(ModifySQLs) = %d, want 1; got=%v", len(res[0].ModifySQLs), res[0].ModifySQLs) + } + sql := res[0].ModifySQLs[0] + // modify SQL 必须落到 compared schema 上(base→compared schema 改写已生效)。 + if !strings.Contains(sql, "CREATE OR REPLACE FUNCTION compared.fn_calc") { + t.Errorf("expect rewritten CREATE OR REPLACE FUNCTION compared.fn_calc; got=%q", sql) + } + // 仍带参数签名 (p integer)(R-Q4-2 兼容)。 + if !strings.Contains(sql, "(p integer)") { + t.Errorf("expect param signature (p integer) preserved; got=%q", sql) + } + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("sqlmock expectations: %v", err) + } +} + +// concreteTypeName returns the package-qualified type name of v. We avoid the +// fmt %T format directive because it includes a leading package selector that +// differs between go test invocations (e.g. "*driver.GaussDBDriver" vs +// "*github.com/.../driver.GaussDBDriver" in some build modes). +func concreteTypeName(v interface{}) string { + if v == nil { + return "" + } + // Use a switch over the known concrete types so the assertion is explicit + // and survives a future refactor that adds more variants. + switch v.(type) { + case *GaussDBDriver: + return "*driver.GaussDBDriver" + case *OpenGaussDriver: + return "*driver.OpenGaussDriver" + case *Driver: + return "*driver.Driver" + default: + return "unknown" + } +} diff --git a/internal/driver/stub.go b/internal/driver/stub.go deleted file mode 100644 index 8661d45..0000000 --- a/internal/driver/stub.go +++ /dev/null @@ -1,48 +0,0 @@ -// Package driver 提供 sqle-gaussdb-plugin 的 driverV2.Driver 实现。 -// -// 本文件 stub.go 为 Task-Dev-002 阶段的最小可编译占位: -// - 通过嵌入 driverPkg.DriverImpl 自动继承 driverV2.Driver 接口的全部方法实现 -// (Close / Ping / Exec / Audit / Parse / GetDatabaseObjectDDL / GetDatabaseDiffModifySQL 等); -// - 暴露 NewGaussDBDriverImpl / NewOpenGaussDriverImpl 两个工厂函数, -// 供 cmd/sqle-gaussdb-plugin/main.go 与 cmd/sqle-opengauss-plugin/main.go 各自 -// 调用 builder.Serve(...) 时使用; -// - 真正连接 GaussDB / openGauss 的逻辑(基于 dialector.BuildDSN + sql.Open 的 -// "opengauss" driver name)见 Task-Dev-003(plan §4.1 driver_common.go)。 -// -// 提示:本 stub 不再使用 `//go:build !real_driver` 等 build tag —— Task-Dev-003 -// 起会**原地替换**本文件为真实实现(driver_common.go / driver_gaussdb.go / -// driver_opengauss.go 三文件),见 plan §4.1 / §4.2 / §4.3。 -package driver - -import ( - driverV2 "github.com/actiontech/sqle/sqle/driver/v2" - driverPkg "github.com/actiontech/sqle/sqle/pkg/driver" - hclog "github.com/hashicorp/go-hclog" -) - -// NewGaussDBDriverImpl 是 PluginName=GaussDB 二进制(cmd/sqle-gaussdb-plugin/main.go) -// 调用 builder.Serve 时使用的 Driver 工厂函数。 -// -// 与 PluginName=openGauss 共用同一段 stub 实现 —— 真正的差异化逻辑(DSN 默认端口、 -// sslmode 等)在 Dialector 层做分支,Driver 层保持统一(design §3.2 / §4.3)。 -// -// TODO(Task-Dev-003): 替换为 driver_gaussdb.go 中的真实实现。 -func NewGaussDBDriverImpl(l hclog.Logger, dt driverPkg.Dialector, ah *driverPkg.AuditHandler, cfg *driverV2.Config) (driverV2.Driver, error) { - return newStub(l, dt, ah, cfg) -} - -// NewOpenGaussDriverImpl 是 PluginName=openGauss 二进制(cmd/sqle-opengauss-plugin/main.go) -// 调用 builder.Serve 时使用的 Driver 工厂函数。 -// -// TODO(Task-Dev-003): 替换为 driver_opengauss.go 中的真实实现。 -func NewOpenGaussDriverImpl(l hclog.Logger, dt driverPkg.Dialector, ah *driverPkg.AuditHandler, cfg *driverV2.Config) (driverV2.Driver, error) { - return newStub(l, dt, ah, cfg) -} - -// newStub 是 GaussDB / openGauss 两个工厂函数共用的内部构造函数。 -// 委托给 driverPkg.NewDriverImpl —— 该工厂会按 cfg.DSN 是否为 nil 决定是否调用 -// Dialector.Open;本 stub 阶段 Open 返回 "not implemented yet" 错误, -// 真正连库的代码在 Task-Dev-003 落地。 -func newStub(l hclog.Logger, dt driverPkg.Dialector, ah *driverPkg.AuditHandler, cfg *driverV2.Config) (driverV2.Driver, error) { - return driverPkg.NewDriverImpl(l, dt, ah, cfg) -} diff --git a/internal/extractor/enumerate.go b/internal/extractor/enumerate.go new file mode 100644 index 0000000..b8d5330 --- /dev/null +++ b/internal/extractor/enumerate.go @@ -0,0 +1,146 @@ +// enumerate.go — Extract 入口的"空 DatabaseObjects → 按 schema 全枚举"兜底 +// (Task-Test-Fix-001 P0)。 +// +// 背景:sqle-ee 主路径 execute_comparison 调用 plugin.GetDatabaseObjectDDL +// 时传入的 *DatabaseSchemaInfo 仅设置 SchemaName,DatabaseObjects 为 nil/空, +// 期望 plugin 按 SchemaName 自行枚举该 schema 下全部对象。 +// +// 该约定在 design 文档与 sqle-ee/sqle/server/compare/database_compare_ee.go +// line 88-95 中存在,但 plugin Task-Dev-005 初版仅做"显式列表迭代",遗漏了 +// 该兜底——导致主路径 `inconsistent_num` 恒为 0(详见 +// docs/test/case-2-1.md ~ case-2-4.md 与 +// expertise_docs/episodic/sqle_gaussdb_compare_main_path_empty_objects_bug_20260521.md)。 +// +// 实现策略:在 Extract 顶部调用 enumerateSchemaObjects 把 4 类对象的 name +// 列表先查出来,转成等价的 []*DatabaseObject 写回 info.DatabaseObjects,再走 +// 既有 switch 分发逻辑,避免重复实现 4 类对象的抽取代码。 +// +// SQL 模板说明: +// +// - TABLE: pg_class.relkind = 'r'(普通表;分区表 'p' 不在本期 design §6.2.1 +// 的对象类型枚举内,主路径暂不枚举分区表); +// - VIEW: pg_class.relkind = 'v'; +// - FUNCTION: pg_proc.prokind = 'f',GROUP BY p.proname 去重——重载分裂在 +// extractFunction helper 内部处理; +// - PROCEDURE: pg_proc.prokind = 'p',同 GROUP BY 去重。 +// +// 所有 SQL 用 $1 参数化绑定 schema 名(design §6.3 红线),禁止 Go 侧拼接。 +// +// 注:对象不存在 / schema 不存在 / 权限不足等错误由调用方透传,与 +// table.go / view.go 既有的 wrapPGError 一致;本 helper 不做额外吞错。 + +package extractor + +import ( + "context" + + driverV2 "github.com/actiontech/sqle/sqle/driver/v2" +) + +// listTablesSQL 枚举指定 schema 下所有普通表(pg_class.relkind = 'r')。 +// +// 严格 byte-for-byte 锁定,避免单测/集成测试偏离;ORDER BY relname 让结果 +// 具备确定性(多 instance 测试间可直接做断言)。 +const listTablesSQL = `SELECT c.relname +FROM pg_class c +JOIN pg_namespace n ON n.oid = c.relnamespace +WHERE n.nspname = $1 + AND c.relkind = 'r' +ORDER BY c.relname` + +// listViewsSQL 枚举指定 schema 下所有视图(pg_class.relkind = 'v')。 +const listViewsSQL = `SELECT c.relname +FROM pg_class c +JOIN pg_namespace n ON n.oid = c.relnamespace +WHERE n.nspname = $1 + AND c.relkind = 'v' +ORDER BY c.relname` + +// listFunctionsSQL 枚举指定 schema 下所有 FUNCTION 名(pg_proc.prokind = 'f')。 +// +// GROUP BY proname 去重:同名重载在本枚举阶段只产出**一个** ObjectName,重载 +// 分裂在 extractFunction helper 内部按 (proname, pg_get_function_arguments) +// 全部展开(design §6.4),避免 enumerate → switch → extractFunction 链路里 +// 出现重复查询。 +const listFunctionsSQL = `SELECT p.proname +FROM pg_proc p +JOIN pg_namespace n ON n.oid = p.pronamespace +WHERE n.nspname = $1 + AND p.prokind = 'f' +GROUP BY p.proname +ORDER BY p.proname` + +// listProceduresSQL 枚举指定 schema 下所有 PROCEDURE 名(pg_proc.prokind = 'p')。 +// 同 listFunctionsSQL 的 GROUP BY 去重策略。 +const listProceduresSQL = `SELECT p.proname +FROM pg_proc p +JOIN pg_namespace n ON n.oid = p.pronamespace +WHERE n.nspname = $1 + AND p.prokind = 'p' +GROUP BY p.proname +ORDER BY p.proname` + +// enumerateSchemaObjects 把 4 类对象的 name 列表汇集成等价的 +// []*driverV2.DatabaseObject 切片,按 (TABLE → VIEW → FUNCTION → PROCEDURE) +// 顺序追加,单类内按 name 字典序(来自 SQL ORDER BY),整体顺序确定。 +// +// 任一查询失败立即透传 error,不做 partial 返回——避免 plugin 把"半枚举" +// 当成"全枚举"造成主路径 inconsistent_num 误判。 +func (e *Extractor) enumerateSchemaObjects( + ctx context.Context, + schemaName string, +) ([]*driverV2.DatabaseObject, error) { + var objects []*driverV2.DatabaseObject + + type listSpec struct { + sql string + objectType string + } + specs := []listSpec{ + {listTablesSQL, driverV2.ObjectType_TABLE}, + {listViewsSQL, driverV2.ObjectType_VIEW}, + {listFunctionsSQL, driverV2.ObjectType_FUNCTION}, + {listProceduresSQL, driverV2.ObjectType_PROCEDURE}, + } + + for _, spec := range specs { + names, err := e.queryObjectNames(ctx, spec.sql, schemaName) + if err != nil { + return nil, err + } + for _, n := range names { + objects = append(objects, &driverV2.DatabaseObject{ + ObjectName: n, + ObjectType: spec.objectType, + }) + } + } + + return objects, nil +} + +// queryObjectNames 是 4 类枚举 SQL 共用的"单列 name 列表"查询 helper。 +// 入参 sqlText 已含 $1 占位,arg 仅 schema 名。 +func (e *Extractor) queryObjectNames( + ctx context.Context, + sqlText, schemaName string, +) ([]string, error) { + rows, err := e.db.QueryContext(ctx, sqlText, schemaName) + if err != nil { + return nil, err + } + defer rows.Close() + + var names []string + for rows.Next() { + var n string + if scanErr := rows.Scan(&n); scanErr != nil { + return nil, scanErr + } + names = append(names, n) + } + if iterErr := rows.Err(); iterErr != nil { + return nil, iterErr + } + return names, nil +} diff --git a/internal/extractor/enumerate_test.go b/internal/extractor/enumerate_test.go new file mode 100644 index 0000000..9494510 --- /dev/null +++ b/internal/extractor/enumerate_test.go @@ -0,0 +1,191 @@ +// enumerate_test.go — 覆盖 Task-Test-Fix-001 P0 修复: +// +// - listTablesSQL / listViewsSQL / listFunctionsSQL / listProceduresSQL 四条 +// 枚举 SQL 字面值锁定(sqlmock.QueryMatcherEqual); +// - Extract 顶部"空 DatabaseObjects → 全枚举 + switch 分发"端到端路径; +// - 多类型混合 + 顺序无关断言(sort.Strings;参考 expertise_docs/semantic/ +// sqlmock_rows_iteration_empty_and_order_20260521.md); +// - 任一枚举 SQL 失败 → 不 partial 返回,错误透传。 +// +// 关注矩阵: +// +// - TABLE 全枚举 → 走 extractTable 拿 DDL; +// - VIEW 全枚举 → 走 extractView 拿 DDL; +// - FUNCTION / PROCEDURE 全枚举 GROUP BY proname 去重,重载分裂留在 helper。 + +package extractor + +import ( + "context" + "errors" + "sort" + "strings" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + driverV2 "github.com/actiontech/sqle/sqle/driver/v2" +) + +// TestExtract_EmptyObjects_FullEnumeration 端到端覆盖空 DatabaseObjects → +// 全枚举 → 4 类对象 DDL 抽取的完整链路。 +func TestExtract_EmptyObjects_FullEnumeration(t *testing.T) { + db, mock, cleanup := newMock(t) + defer cleanup() + + // 期望 enumerateSchemaObjects 内 4 条枚举 SQL 顺序触发。 + mock.ExpectQuery(listTablesSQL). + WithArgs("public"). + WillReturnRows(sqlmock.NewRows([]string{"relname"}). + AddRow("t_a"). + AddRow("t_b")) + + mock.ExpectQuery(listViewsSQL). + WithArgs("public"). + WillReturnRows(sqlmock.NewRows([]string{"relname"}). + AddRow("v_a")) + + mock.ExpectQuery(listFunctionsSQL). + WithArgs("public"). + WillReturnRows(sqlmock.NewRows([]string{"proname"}). + AddRow("fn_a")) + + mock.ExpectQuery(listProceduresSQL). + WithArgs("public"). + WillReturnRows(sqlmock.NewRows([]string{"proname"}). + AddRow("proc_a")) + + // 期望随后按 4 类对象顺序触发 extractTable / extractView / extractFunction / + // extractProcedure 的 SQL。 + mock.ExpectQuery(extractTableSQL). + WithArgs("public", "t_a"). + WillReturnRows(sqlmock.NewRows([]string{"pg_get_tabledef"}). + AddRow("CREATE TABLE public.t_a (id int);")) + mock.ExpectQuery(extractTableSQL). + WithArgs("public", "t_b"). + WillReturnRows(sqlmock.NewRows([]string{"pg_get_tabledef"}). + AddRow("CREATE TABLE public.t_b (id int);")) + mock.ExpectQuery(extractViewSQL). + WithArgs("public", "v_a"). + WillReturnRows(sqlmock.NewRows([]string{"ddl"}). + AddRow("CREATE OR REPLACE VIEW public.v_a AS SELECT 1;")) + // extractFunction / extractProcedure 在阶段 2 改成两次单列查询,此处 mock + // 两组单列 SQL:先 defs,再 args。 + mock.ExpectQuery(extractFunctionDefSQL). + WithArgs("public", "fn_a"). + WillReturnRows(sqlmock.NewRows([]string{"functiondef"}). + AddRow("CREATE OR REPLACE FUNCTION public.fn_a() RETURNS int AS $$ BEGIN RETURN 1; END; $$ LANGUAGE plpgsql")) + mock.ExpectQuery(extractFunctionArgsSQL). + WithArgs("public", "fn_a"). + WillReturnRows(sqlmock.NewRows([]string{"arguments"}). + AddRow("")) + mock.ExpectQuery(extractProcedureDefSQL). + WithArgs("public", "proc_a"). + WillReturnRows(sqlmock.NewRows([]string{"procdef"}). + AddRow("CREATE OR REPLACE PROCEDURE public.proc_a() AS BEGIN NULL; END;")) + mock.ExpectQuery(extractProcedureArgsSQL). + WithArgs("public", "proc_a"). + WillReturnRows(sqlmock.NewRows([]string{"arguments"}). + AddRow("")) + + ex := NewExtractor(db) + result, err := ex.Extract(context.Background(), &driverV2.DatabaseSchemaInfo{ + SchemaName: "public", + DatabaseObjects: nil, // 空:触发全枚举兜底 + }) + if err != nil { + t.Fatalf("Extract: %v", err) + } + if result == nil { + t.Fatal("Extract returned nil result") + } + + if result.SchemaName != "public" { + t.Errorf("SchemaName = %q, want %q", result.SchemaName, "public") + } + + // 顺序无关断言(map 迭代顺序 Go runtime 不保证;本测试是按 ORDER BY 顺序 + // + helper 串行迭代,理论上稳定但仍 defensive sort)。 + var gotNames []string + for _, ddl := range result.DatabaseObjectDDLs { + gotNames = append(gotNames, ddl.DatabaseObject.ObjectType+":"+ddl.DatabaseObject.ObjectName) + } + sort.Strings(gotNames) + wantNames := []string{ + driverV2.ObjectType_FUNCTION + ":fn_a()", + driverV2.ObjectType_PROCEDURE + ":proc_a()", + driverV2.ObjectType_TABLE + ":t_a", + driverV2.ObjectType_TABLE + ":t_b", + driverV2.ObjectType_VIEW + ":v_a", + } + sort.Strings(wantNames) + if len(gotNames) != len(wantNames) { + t.Fatalf("got names = %v, want %v", gotNames, wantNames) + } + for i := range gotNames { + if gotNames[i] != wantNames[i] { + t.Errorf("got[%d] = %q, want %q", i, gotNames[i], wantNames[i]) + } + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("unmet expectations: %v", err) + } +} + +// TestExtract_EmptyObjects_EmptySchema 覆盖 schema 内 0 对象的边界(4 条枚举 +// SQL 都返回 0 行 → DatabaseObjectDDLs 为空 slice,不报错)。 +func TestExtract_EmptyObjects_EmptySchema(t *testing.T) { + db, mock, cleanup := newMock(t) + defer cleanup() + + emptyRows := func(col string) *sqlmock.Rows { return sqlmock.NewRows([]string{col}) } + mock.ExpectQuery(listTablesSQL).WithArgs("empty").WillReturnRows(emptyRows("relname")) + mock.ExpectQuery(listViewsSQL).WithArgs("empty").WillReturnRows(emptyRows("relname")) + mock.ExpectQuery(listFunctionsSQL).WithArgs("empty").WillReturnRows(emptyRows("proname")) + mock.ExpectQuery(listProceduresSQL).WithArgs("empty").WillReturnRows(emptyRows("proname")) + + ex := NewExtractor(db) + result, err := ex.Extract(context.Background(), &driverV2.DatabaseSchemaInfo{ + SchemaName: "empty", + }) + if err != nil { + t.Fatalf("Extract: %v", err) + } + if result == nil { + t.Fatal("Extract returned nil result") + } + if len(result.DatabaseObjectDDLs) != 0 { + t.Errorf("DatabaseObjectDDLs len = %d, want 0", len(result.DatabaseObjectDDLs)) + } + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("unmet expectations: %v", err) + } +} + +// TestExtract_EmptyObjects_EnumerateError 断言任一枚举 SQL 失败时错误透传, +// 不做 partial 返回。 +func TestExtract_EmptyObjects_EnumerateError(t *testing.T) { + db, mock, cleanup := newMock(t) + defer cleanup() + + mock.ExpectQuery(listTablesSQL). + WithArgs("restricted"). + WillReturnError(errors.New("permission denied for schema pg_catalog")) + + ex := NewExtractor(db) + result, err := ex.Extract(context.Background(), &driverV2.DatabaseSchemaInfo{ + SchemaName: "restricted", + }) + if err == nil { + t.Fatal("expected error, got nil") + } + if !strings.Contains(err.Error(), "permission denied") { + t.Errorf("error = %q, expected substring 'permission denied'", err.Error()) + } + if result != nil { + t.Errorf("expected nil result on error, got %+v", result) + } + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("unmet expectations: %v", err) + } +} diff --git a/internal/extractor/extractor.go b/internal/extractor/extractor.go new file mode 100644 index 0000000..1d4d039 --- /dev/null +++ b/internal/extractor/extractor.go @@ -0,0 +1,151 @@ +// extractor.go — Extract 入口 + ObjectType switch 分发(Task-Dev-005)。 +// +// 注:package extractor 的 godoc 包注释统一写在 doc.go;本文件顶部使用普通 +// 注释块以避免 `go vet` 报 duplicate package documentation。 +// +// 类型签名漂移说明(semantic/design_pseudocode_param_type_drift_20260521): +// design §6.1 关键片段把 Extract 写成"对每个对象返回一个独立的 +// *driverV2.DatabaseSchemaObjectResult",但 real 链路 +// (sqle/sqle/driver/v2/driver_interface.go:431-463) 中 +// DatabaseSchemaObjectResult 是 **per-schema** 聚合的: +// +// type DatabaseSchemaObjectResult struct { +// SchemaName string +// SchemaDDL string +// DatabaseObjectDDLs []*DatabaseObjectDDL +// } +// type DatabaseObjectDDL struct { +// DatabaseObject *DatabaseObject // 含 ObjectName + ObjectType +// ObjectDDL string +// } +// +// 按"业务字段访问语义优先 + 文件头注释 + commit 标注"准则,本实现采用 real +// 链路签名: +// +// - Extract(ctx, info) → 单个 *driverV2.DatabaseSchemaObjectResult, +// SchemaName 取 info.SchemaName,DatabaseObjectDDLs 收集每个对象的 DDL; +// - extractTable / extractView 内部 helper 返回 *driverV2.DatabaseObjectDDL; +// - default 分支按 design §6.1 line 399 字面值返回错误。 +// +// 红线(design §6.3):所有 user-controlled 字符串(schema / object 名)必须 +// 通过 `$1` / `$2` 参数化绑定;禁止 Go 侧手工 escape,转义责任在 +// PostgreSQL 服务端 + libpq 驱动。 + +package extractor + +import ( + "context" + "database/sql" + "fmt" + + driverV2 "github.com/actiontech/sqle/sqle/driver/v2" +) + +// Extractor 抽取 GaussDB / openGauss 的 schema 对象 DDL。 +// +// 字段保持 unexported 与 design §6.1 伪代码一致;构造统一走 NewExtractor, +// 便于单测从外注入 sqlmock-backed *sql.DB 而无须真连 GaussDB / openGauss。 +type Extractor struct { + db *sql.DB +} + +// NewExtractor 构造 Extractor,db 由调用方负责生命周期(通常是 +// driverPkg.DriverImpl.DB,已套上连接池策略;driver.Extractor 接入留给 +// Task-Dev-007)。 +func NewExtractor(db *sql.DB) *Extractor { + return &Extractor{db: db} +} + +// Extract 按 design §6.1 入口分发 info.DatabaseObjects 中的每个对象到具体的 +// extract* helper。 +// +// 返回单个 *driverV2.DatabaseSchemaObjectResult: +// +// - SchemaName 直接取 info.SchemaName; +// - SchemaDDL 本期保持为空(design §6 未要求 schema 自身 DDL); +// - DatabaseObjectDDLs 按 info.DatabaseObjects 顺序收集,对象不存在时 +// 保留一个 ObjectDDL=="" 的占位条目(design §6.5:让业务层 +// compareSchema 把它当作"该侧不存在"处理,**不报错**)。 +// +// 分发覆盖 TABLE / VIEW / FUNCTION / PROCEDURE 四类;其他 ObjectType(INDEX / +// TRIGGER / EVENT / SEQUENCE / TYPE / PACKAGE 等)落入 default 分支返回 +// design §6.1 line 399 字面错误 "object type %s is not supported in this release"。 +// +// FUNCTION / PROCEDURE 与 TABLE / VIEW 的关键差异:helper 返回 slice 而非 +// 单个 DDL(同名重载分裂为多条独立结果,design §6.4),故需用 +// `append(..., rs...)` 展开。 +func (e *Extractor) Extract( + ctx context.Context, + info *driverV2.DatabaseSchemaInfo, +) (*driverV2.DatabaseSchemaObjectResult, error) { + if info == nil { + return nil, fmt.Errorf("extractor: DatabaseSchemaInfo is nil") + } + + result := &driverV2.DatabaseSchemaObjectResult{ + SchemaName: info.SchemaName, + } + + // Task-Test-Fix-001 P0:sqle-ee 主路径 execute_comparison 传入空 + // DatabaseObjects 期待 plugin 按 SchemaName 全枚举(参考 + // docs/test/case-2-1.md 与 expertise_docs/episodic/ + // sqle_gaussdb_compare_main_path_empty_objects_bug_20260521.md)。 + // 在显式 switch 分发前做一次"空 → 全枚举"兜底,让后续循环按既有 + // 4 类对象分发逻辑处理,避免重复实现。 + objs := info.DatabaseObjects + if len(objs) == 0 { + enumerated, err := e.enumerateSchemaObjects(ctx, info.SchemaName) + if err != nil { + return nil, err + } + objs = enumerated + } + + for _, obj := range objs { + if obj == nil { + continue + } + + switch obj.ObjectType { + case driverV2.ObjectType_TABLE: + ddl, err := e.extractTable(ctx, info.SchemaName, obj.ObjectName) + if err != nil { + return nil, err + } + result.DatabaseObjectDDLs = append(result.DatabaseObjectDDLs, ddl) + + case driverV2.ObjectType_VIEW: + ddl, err := e.extractView(ctx, info.SchemaName, obj.ObjectName) + if err != nil { + return nil, err + } + result.DatabaseObjectDDLs = append(result.DatabaseObjectDDLs, ddl) + + case driverV2.ObjectType_FUNCTION: + // 同名重载:extractFunction 返回 slice,每条 ObjectName 携带 + // 参数签名(design §6.4),用 rs... 展开聚合。 + rs, err := e.extractFunction(ctx, info.SchemaName, obj.ObjectName) + if err != nil { + return nil, err + } + result.DatabaseObjectDDLs = append(result.DatabaseObjectDDLs, rs...) + + case driverV2.ObjectType_PROCEDURE: + // 与 FUNCTION 同构;prokind 差异封装在 extractProcedure 内。 + rs, err := e.extractProcedure(ctx, info.SchemaName, obj.ObjectName) + if err != nil { + return nil, err + } + result.DatabaseObjectDDLs = append(result.DatabaseObjectDDLs, rs...) + + default: + // design §6.1 line 399 字面值,对 INDEX / TRIGGER / EVENT 等本期不支持的对象统一返回。 + return nil, fmt.Errorf( + "object type %s is not supported in this release", + obj.ObjectType, + ) + } + } + + return result, nil +} diff --git a/internal/extractor/extractor_test.go b/internal/extractor/extractor_test.go new file mode 100644 index 0000000..4aa4f17 --- /dev/null +++ b/internal/extractor/extractor_test.go @@ -0,0 +1,455 @@ +// extractor_test.go covers design §17.1.2 — DDL Extractor 单测矩阵 TABLE/VIEW +// 路径与 Extract 入口兜底: +// +// - TABLE / VIEW 正常路径(SQL 模板字面匹配 + $1 / $2 参数绑定) +// - 不支持 ObjectType 兜底(INDEX / TRIGGER / EVENT / SEQUENCE / TYPE / +// PACKAGE → default 分支,design §6.1 line 399 字面错误) +// - 对象不存在 → sql.ErrNoRows 包装为空 DDL,不报错(design §6.5) +// - catalog 权限错误 → 原样透传,不吞 SQLSTATE +// - QuoteIdent 注入防御 → 空格 / 大小写 / 中文 / SQL 注入串 / 单引号 / 双引号 +// 6 条 case,断言 sqlmock 收到的 $1 / $2 参数原值未被 Go 侧 escape +// +// FUNCTION / PROCEDURE 的重载识别(design §6.4)+ 正常分发 + 注入防御覆盖在 +// function_test.go(Task-Dev-006)。 +// +// 所有 sqlmock 用 QueryMatcherOption(QueryMatcherEqual) 模式锁定 SQL 文本, +// 避免正则模糊匹配掩盖 design 模板偏离。 + +package extractor + +import ( + "context" + "database/sql" + "errors" + "strings" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + driverV2 "github.com/actiontech/sqle/sqle/driver/v2" +) + +// newMock 构造一个 sqlmock.QueryMatcherEqual 模式的 *sql.DB + sqlmock,并返回 +// 清理函数;所有测试统一走该 helper 确保匹配策略一致。 +func newMock(t *testing.T) (*sql.DB, sqlmock.Sqlmock, func()) { + t.Helper() + db, mock, err := sqlmock.New( + sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual), + ) + if err != nil { + t.Fatalf("sqlmock.New: %v", err) + } + return db, mock, func() { _ = db.Close() } +} + +// TestExtractor_Extract_TableAndView 覆盖 Extract 入口的 TABLE / VIEW 分发与 +// 对象不存在 / catalog 权限错误兜底(design §6.2.1 / §6.2.2 / §6.5)。 +func TestExtractor_Extract_TableAndView(t *testing.T) { + const ( + tableDDLNormal = "SET search_path = 'public'; CREATE TABLE public.t1 (id int) WITH (orientation=row);" + viewDDLNormal = "CREATE OR REPLACE VIEW public.v1 AS SELECT 1 AS id;" + ) + + cases := map[string]struct { + schema string + object *driverV2.DatabaseObject + expectSQL string + expectArgs []driverV2.DatabaseObject // 仅用其字段做断言,不参与执行 + mockSetup func(mock sqlmock.Sqlmock) + wantErr bool + wantErrPart string + wantDDL string + wantObjType string + wantObjName string + wantDDLEmpty bool + }{ + "table_normal": { + schema: "public", + object: &driverV2.DatabaseObject{ObjectName: "t1", ObjectType: driverV2.ObjectType_TABLE}, + mockSetup: func(mock sqlmock.Sqlmock) { + mock.ExpectQuery(extractTableSQL). + WithArgs("public", "t1"). + WillReturnRows(sqlmock.NewRows([]string{"pg_get_tabledef"}).AddRow(tableDDLNormal)) + }, + wantDDL: tableDDLNormal, + wantObjType: driverV2.ObjectType_TABLE, + wantObjName: "t1", + }, + "view_normal": { + schema: "public", + object: &driverV2.DatabaseObject{ObjectName: "v1", ObjectType: driverV2.ObjectType_VIEW}, + mockSetup: func(mock sqlmock.Sqlmock) { + mock.ExpectQuery(extractViewSQL). + WithArgs("public", "v1"). + WillReturnRows(sqlmock.NewRows([]string{"ddl"}).AddRow(viewDDLNormal)) + }, + wantDDL: viewDDLNormal, + wantObjType: driverV2.ObjectType_VIEW, + wantObjName: "v1", + }, + "table_not_found": { + schema: "public", + object: &driverV2.DatabaseObject{ObjectName: "missing_table", ObjectType: driverV2.ObjectType_TABLE}, + mockSetup: func(mock sqlmock.Sqlmock) { + mock.ExpectQuery(extractTableSQL). + WithArgs("public", "missing_table"). + WillReturnError(sql.ErrNoRows) + }, + wantDDLEmpty: true, + wantObjType: driverV2.ObjectType_TABLE, + wantObjName: "missing_table", + }, + "view_not_found": { + schema: "public", + object: &driverV2.DatabaseObject{ObjectName: "missing_view", ObjectType: driverV2.ObjectType_VIEW}, + mockSetup: func(mock sqlmock.Sqlmock) { + mock.ExpectQuery(extractViewSQL). + WithArgs("public", "missing_view"). + WillReturnError(sql.ErrNoRows) + }, + wantDDLEmpty: true, + wantObjType: driverV2.ObjectType_VIEW, + wantObjName: "missing_view", + }, + "table_permission_denied": { + schema: "restricted", + object: &driverV2.DatabaseObject{ObjectName: "t1", ObjectType: driverV2.ObjectType_TABLE}, + mockSetup: func(mock sqlmock.Sqlmock) { + mock.ExpectQuery(extractTableSQL). + WithArgs("restricted", "t1"). + WillReturnError(errors.New("permission denied for schema pg_catalog")) + }, + wantErr: true, + wantErrPart: "permission denied", + }, + "view_permission_denied": { + schema: "restricted", + object: &driverV2.DatabaseObject{ObjectName: "v1", ObjectType: driverV2.ObjectType_VIEW}, + mockSetup: func(mock sqlmock.Sqlmock) { + mock.ExpectQuery(extractViewSQL). + WithArgs("restricted", "v1"). + WillReturnError(errors.New("permission denied for relation v1")) + }, + wantErr: true, + wantErrPart: "permission denied", + }, + } + + for name, c := range cases { + c := c + t.Run(name, func(t *testing.T) { + db, mock, cleanup := newMock(t) + defer cleanup() + c.mockSetup(mock) + + ex := NewExtractor(db) + result, err := ex.Extract(context.Background(), &driverV2.DatabaseSchemaInfo{ + SchemaName: c.schema, + DatabaseObjects: []*driverV2.DatabaseObject{c.object}, + }) + + if c.wantErr { + if err == nil { + t.Fatalf("expected error containing %q, got nil", c.wantErrPart) + } + if !strings.Contains(err.Error(), c.wantErrPart) { + t.Errorf("error %q does not contain %q", err.Error(), c.wantErrPart) + } + if result != nil { + t.Errorf("expected nil result on error, got %+v", result) + } + if mockErr := mock.ExpectationsWereMet(); mockErr != nil { + t.Errorf("unmet sqlmock expectations: %v", mockErr) + } + return + } + + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if result == nil { + t.Fatal("expected non-nil result") + } + if result.SchemaName != c.schema { + t.Errorf("SchemaName = %q, want %q", result.SchemaName, c.schema) + } + if len(result.DatabaseObjectDDLs) != 1 { + t.Fatalf("DatabaseObjectDDLs len = %d, want 1", len(result.DatabaseObjectDDLs)) + } + got := result.DatabaseObjectDDLs[0] + if got.DatabaseObject == nil { + t.Fatal("DatabaseObject metadata missing") + } + if got.DatabaseObject.ObjectName != c.wantObjName { + t.Errorf("ObjectName = %q, want %q", got.DatabaseObject.ObjectName, c.wantObjName) + } + if got.DatabaseObject.ObjectType != c.wantObjType { + t.Errorf("ObjectType = %q, want %q", got.DatabaseObject.ObjectType, c.wantObjType) + } + if c.wantDDLEmpty { + if got.ObjectDDL != "" { + t.Errorf("expected empty DDL on not-found, got %q", got.ObjectDDL) + } + } else if got.ObjectDDL != c.wantDDL { + t.Errorf("ObjectDDL mismatch:\n got %q\n want %q", got.ObjectDDL, c.wantDDL) + } + if mockErr := mock.ExpectationsWereMet(); mockErr != nil { + t.Errorf("unmet sqlmock expectations: %v", mockErr) + } + }) + } +} + +// TestExtractor_Extract_UnsupportedObjectType 覆盖 default 分支:本期不支持的 +// ObjectType 一律返回 design §6.1 line 399 字面错误。 +// +// FUNCTION / PROCEDURE 已在 Task-Dev-006 接入 extractFunction / extractProcedure +// helper(不再走 default 分支),相关分发与重载用例位于 function_test.go。 +func TestExtractor_Extract_UnsupportedObjectType(t *testing.T) { + cases := map[string]struct { + objectType string + }{ + "INDEX": {objectType: "INDEX"}, + "TRIGGER": {objectType: driverV2.ObjectType_TRIGGER}, + "EVENT": {objectType: driverV2.ObjectType_EVENT}, + "SEQUENCE": {objectType: "SEQUENCE"}, + "TYPE": {objectType: "TYPE"}, + "PACKAGE": {objectType: "PACKAGE"}, + "empty_object_type_falls_to_default": {objectType: ""}, + } + + for name, c := range cases { + c := c + t.Run(name, func(t *testing.T) { + db, mock, cleanup := newMock(t) + defer cleanup() + // 不设任何 ExpectQuery:default 分支应在 dispatch 阶段直接返回错误, + // 不会触达 DB。ExpectationsWereMet 兜底确保没有意外查询发生。 + + ex := NewExtractor(db) + result, err := ex.Extract(context.Background(), &driverV2.DatabaseSchemaInfo{ + SchemaName: "public", + DatabaseObjects: []*driverV2.DatabaseObject{ + {ObjectName: "obj", ObjectType: c.objectType}, + }, + }) + + if err == nil { + t.Fatalf("expected not-supported error, got nil; result=%+v", result) + } + wantSubstr := "is not supported in this release" + if !strings.Contains(err.Error(), wantSubstr) { + t.Errorf("error %q does not contain %q", err.Error(), wantSubstr) + } + // design §6.1 line 399 要求把 ObjectType 字面值嵌入错误信息,便于运维定位。 + if !strings.Contains(err.Error(), c.objectType) { + t.Errorf("error %q does not embed ObjectType literal %q", err.Error(), c.objectType) + } + if result != nil { + t.Errorf("expected nil result, got %+v", result) + } + if mockErr := mock.ExpectationsWereMet(); mockErr != nil { + t.Errorf("unmet sqlmock expectations: %v", mockErr) + } + }) + } +} + +// TestExtractor_QuoteIdent_TableInjectionDefense 验证 schema / table 名含 +// 特殊字符(空格 / 大小写 / 中文 / SQL 注入串 / 单双引号)时,Extractor +// 不在 Go 侧做任何 escape,原值通过 $1 / $2 透传到 sqlmock 参数断言。 +// +// 这是 compat-RISK-4(SQL 注入)的兜底测试:转义责任在 PostgreSQL 服务端 + +// libpq 驱动;Go 侧任何对 schema / object 名做手工 escape 的代码都会让本组 +// 用例的 WithArgs 断言失败。 +func TestExtractor_QuoteIdent_TableInjectionDefense(t *testing.T) { + cases := map[string]struct { + schema string + table string + }{ + "space_in_schema": {schema: "My Schema", table: "t1"}, + "uppercase_schema": {schema: "MySchema", table: "t1"}, + "chinese_schema": {schema: "中文模式", table: "t1"}, + "sql_injection_in_object": {schema: "public", table: "; DROP DATABASE postgres; --"}, + "single_quote_in_object": {schema: "public", table: "it's"}, + "double_quote_in_object": {schema: "public", table: `"quoted"`}, + "chinese_in_object_name": {schema: "public", table: "表名"}, + "mixed_special_in_schema": {schema: `s"a'b c中`, table: "t1"}, + } + + const ddlEcho = "" + + for name, c := range cases { + c := c + t.Run(name, func(t *testing.T) { + db, mock, cleanup := newMock(t) + defer cleanup() + + // WithArgs(c.schema, c.table) 强约束:sqlmock 会对每个 driver.Value + // 做 reflect.DeepEqual 比较,若 Extractor 在 Go 侧做任何 escape, + // 实参就会与 c.schema / c.table 原值不等,匹配失败导致测试 fail。 + mock.ExpectQuery(extractTableSQL). + WithArgs(c.schema, c.table). + WillReturnRows(sqlmock.NewRows([]string{"ddl"}).AddRow(ddlEcho)) + + ex := NewExtractor(db) + result, err := ex.Extract(context.Background(), &driverV2.DatabaseSchemaInfo{ + SchemaName: c.schema, + DatabaseObjects: []*driverV2.DatabaseObject{ + {ObjectName: c.table, ObjectType: driverV2.ObjectType_TABLE}, + }, + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if result == nil || len(result.DatabaseObjectDDLs) != 1 { + t.Fatalf("unexpected result shape: %+v", result) + } + got := result.DatabaseObjectDDLs[0] + if got.DatabaseObject.ObjectName != c.table { + t.Errorf("ObjectName not preserved: got %q want %q", got.DatabaseObject.ObjectName, c.table) + } + if got.ObjectDDL != ddlEcho { + t.Errorf("ObjectDDL = %q, want %q", got.ObjectDDL, ddlEcho) + } + if mockErr := mock.ExpectationsWereMet(); mockErr != nil { + t.Errorf("unmet sqlmock expectations: %v", mockErr) + } + }) + } +} + +// TestExtractor_QuoteIdent_ViewInjectionDefense 与 TestExtractor_QuoteIdent_ +// TableInjectionDefense 同结构,但走 VIEW 抽取路径(view.go 的参数化绑定 +// 同样需要防注入;design §17.1.2 要求 TABLE / VIEW 双路径都覆盖)。 +func TestExtractor_QuoteIdent_ViewInjectionDefense(t *testing.T) { + cases := map[string]struct { + schema string + view string + }{ + "space_in_schema": {schema: "My Schema", view: "v1"}, + "chinese_schema": {schema: "中文模式", view: "v1"}, + "sql_injection_in_view": {schema: "public", view: "v1'; DROP VIEW x; --"}, + "single_quote_in_view": {schema: "public", view: "it's_view"}, + "double_quote_in_view": {schema: "public", view: `"quoted_view"`}, + "chinese_in_view_name": {schema: "public", view: "视图名"}, + } + + const ddlEcho = "" + + for name, c := range cases { + c := c + t.Run(name, func(t *testing.T) { + db, mock, cleanup := newMock(t) + defer cleanup() + + mock.ExpectQuery(extractViewSQL). + WithArgs(c.schema, c.view). + WillReturnRows(sqlmock.NewRows([]string{"ddl"}).AddRow(ddlEcho)) + + ex := NewExtractor(db) + result, err := ex.Extract(context.Background(), &driverV2.DatabaseSchemaInfo{ + SchemaName: c.schema, + DatabaseObjects: []*driverV2.DatabaseObject{ + {ObjectName: c.view, ObjectType: driverV2.ObjectType_VIEW}, + }, + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if result == nil || len(result.DatabaseObjectDDLs) != 1 { + t.Fatalf("unexpected result shape: %+v", result) + } + got := result.DatabaseObjectDDLs[0] + if got.DatabaseObject.ObjectName != c.view { + t.Errorf("ObjectName not preserved: got %q want %q", got.DatabaseObject.ObjectName, c.view) + } + if got.DatabaseObject.ObjectType != driverV2.ObjectType_VIEW { + t.Errorf("ObjectType = %q, want %q", got.DatabaseObject.ObjectType, driverV2.ObjectType_VIEW) + } + if got.ObjectDDL != ddlEcho { + t.Errorf("ObjectDDL = %q, want %q", got.ObjectDDL, ddlEcho) + } + if mockErr := mock.ExpectationsWereMet(); mockErr != nil { + t.Errorf("unmet sqlmock expectations: %v", mockErr) + } + }) + } +} + +// TestExtractor_Extract_NilInfoAndMultipleObjects 锁定 Extract 入口的两个 +// 边界行为: +// +// - info == nil 直接报错(不走任何 DB 调用); +// - 多对象 info 按顺序聚合到同一个 *DatabaseSchemaObjectResult;nil 对象 +// 被跳过(不产生空 DDL 噪音)。 +// +// 这两条不在 design §17.1.2 显式列出,但属于"入口契约"的健壮性兜底,避免 +// Task-Dev-007 接入 driver.GetDatabaseObjectDDL 时撞到 nil-deref。 +func TestExtractor_Extract_NilInfoAndMultipleObjects(t *testing.T) { + t.Run("nil_info_returns_error", func(t *testing.T) { + db, _, cleanup := newMock(t) + defer cleanup() + ex := NewExtractor(db) + _, err := ex.Extract(context.Background(), nil) + if err == nil { + t.Fatal("expected error on nil info, got nil") + } + if !strings.Contains(err.Error(), "nil") { + t.Errorf("error %q does not mention nil", err.Error()) + } + }) + + t.Run("multiple_objects_aggregate_in_order", func(t *testing.T) { + db, mock, cleanup := newMock(t) + defer cleanup() + + mock.ExpectQuery(extractTableSQL). + WithArgs("public", "t1"). + WillReturnRows(sqlmock.NewRows([]string{"ddl"}).AddRow("CREATE TABLE t1")) + mock.ExpectQuery(extractViewSQL). + WithArgs("public", "v1"). + WillReturnRows(sqlmock.NewRows([]string{"ddl"}).AddRow("CREATE OR REPLACE VIEW v1")) + mock.ExpectQuery(extractTableSQL). + WithArgs("public", "t2"). + WillReturnError(sql.ErrNoRows) + + ex := NewExtractor(db) + result, err := ex.Extract(context.Background(), &driverV2.DatabaseSchemaInfo{ + SchemaName: "public", + DatabaseObjects: []*driverV2.DatabaseObject{ + {ObjectName: "t1", ObjectType: driverV2.ObjectType_TABLE}, + nil, // 应被跳过 + {ObjectName: "v1", ObjectType: driverV2.ObjectType_VIEW}, + {ObjectName: "t2", ObjectType: driverV2.ObjectType_TABLE}, // not found → 空 DDL 占位 + }, + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(result.DatabaseObjectDDLs) != 3 { + t.Fatalf("expected 3 DDL entries (nil skipped), got %d", len(result.DatabaseObjectDDLs)) + } + wantOrder := []struct { + name, typ, ddl string + }{ + {"t1", driverV2.ObjectType_TABLE, "CREATE TABLE t1"}, + {"v1", driverV2.ObjectType_VIEW, "CREATE OR REPLACE VIEW v1"}, + {"t2", driverV2.ObjectType_TABLE, ""}, + } + for i, want := range wantOrder { + got := result.DatabaseObjectDDLs[i] + if got.DatabaseObject.ObjectName != want.name { + t.Errorf("[%d] ObjectName = %q, want %q", i, got.DatabaseObject.ObjectName, want.name) + } + if got.DatabaseObject.ObjectType != want.typ { + t.Errorf("[%d] ObjectType = %q, want %q", i, got.DatabaseObject.ObjectType, want.typ) + } + if got.ObjectDDL != want.ddl { + t.Errorf("[%d] ObjectDDL = %q, want %q", i, got.ObjectDDL, want.ddl) + } + } + if mockErr := mock.ExpectationsWereMet(); mockErr != nil { + t.Errorf("unmet sqlmock expectations: %v", mockErr) + } + }) +} diff --git a/internal/extractor/function.go b/internal/extractor/function.go new file mode 100644 index 0000000..e17e79b --- /dev/null +++ b/internal/extractor/function.go @@ -0,0 +1,167 @@ +// function.go implements design §6.2.3 — FUNCTION DDL extraction via +// pg_get_functiondef + pg_get_function_arguments + prokind = 'f',并按 design +// §6.4 把 `ObjectName` 字段填为 `funcname(arg_type_list)` 形式,让上层 +// compareSchema map key 天然唯一(同名重载分裂为多条独立结果)。 +// +// 实现策略(Task-Test-Fix-001 P1.1,二次定位):用 +// `(pg_get_functiondef(oid)).definition` 字段访问把 GaussDB 的 record 返回类型 +// 拆开,并拆成 def / args 两次单列查询。 +// +// 历史背景:Task-Dev-006 初版用单 SQL 双列 SELECT +// +// SELECT pg_get_functiondef(p.oid), pg_get_function_arguments(p.oid) FROM ... +// +// 实测在 GaussDB(kernel 505.2.1)+ openGauss 官方 Go 驱动 +// `gitee.com/opengauss/openGauss-connector-go-pq` 下,输出的 sql_statement +// 文本前缀含 `(N,"..."`、尾部含 `")"`,复制到 psql 直接执行报语法错误 +// (docs/test/case-2-3.md 与 case-2-4.md)。 +// +// 第一次定位(错误):以为是"多列 SELECT 含 pg_get_functiondef 被驱动打包成 +// record 字面值"——拆成两次单列查询后**仍然**返回 tuple 字面值,证明根因 +// 不在驱动 RowDescription 层而在 SQL 返回类型本身。 +// +// 第二次定位(真因):GaussDB 的 `pg_get_functiondef(oid)` 与上游 PostgreSQL +// 的同名函数**签名不同**——GaussDB 返回的是一个 record 类型 +// `(headerlines int, definition text)`,而非 PG 的 text;当 SELECT 不带 +// `.definition` 字段访问时,驱动按 record-as-string 序列化为 +// `(headerlines,"definition")` 形态。 +// +// 修复方案:把 `pg_get_functiondef(p.oid)` 改写为 +// `(pg_get_functiondef(p.oid)).definition` 显式字段访问,让服务端在投影阶段 +// 就剥离 record 包装,驱动直接拿到 text。同时保留拆 def/args 两次单列查询 +// 的形态——既与 design §6.4 重载分裂天然对齐(ORDER BY p.oid),又规避 +// 未来 args 列也出现类似 record 返回的风险。 +// +// SQL 模板:byte-for-byte 锁定(sqlmock.QueryMatcherEqual 单测验证)。 +// +// 红线(design §6.3):schemaName / funcName 必须通过 $1 / $2 参数化绑定, +// 禁止任何 Go 侧的字符串拼接 / 手工 escape——转义责任在 PostgreSQL 服务端 + +// libpq 驱动。`pg_get_function_arguments` 返回参数签名字符串由服务端拼装, +// Go 侧仅做 `fmt.Sprintf("%s(%s)", funcName, arguments)` 拼接 ObjectName, +// 该拼接结果仅作为业务层 map key 使用,**不会**重新作为 SQL 文本下发。 + +package extractor + +import ( + "context" + "fmt" + + driverV2 "github.com/actiontech/sqle/sqle/driver/v2" +) + +// extractFunctionDefSQL 单列查询:取所有同名 FUNCTION 重载的 DDL 文本,按 oid +// 升序返回。 +// +// 注意 `(pg_get_functiondef(p.oid)).definition` 的显式字段访问——GaussDB +// 的 pg_get_functiondef 返回 record (headerlines int, definition text), +// 直接 SELECT 会拿到 `(N, "ddl")` tuple 字面值,必须取 .definition 字段才能 +// 拿到 ddl 文本本身(Task-Test-Fix-001 P1.1)。 +const extractFunctionDefSQL = `SELECT (pg_get_functiondef(p.oid)).definition +FROM pg_proc p +JOIN pg_namespace n ON n.oid = p.pronamespace +WHERE n.nspname = $1 + AND p.prokind = 'f' + AND p.proname = $2 +ORDER BY p.oid` + +// extractFunctionArgsSQL 单列查询:取所有同名 FUNCTION 重载的参数签名字符串, +// 按 oid 升序返回(与 extractFunctionDefSQL 的 oid 顺序对齐)。 +const extractFunctionArgsSQL = `SELECT pg_get_function_arguments(p.oid) +FROM pg_proc p +JOIN pg_namespace n ON n.oid = p.pronamespace +WHERE n.nspname = $1 + AND p.prokind = 'f' + AND p.proname = $2 +ORDER BY p.oid` + +// extractFunction 按 design §6.2.3 + §6.4 抽取同名 FUNCTION(含重载)的所有 +// DDL,每条独立返回。 +// +// 返回值约定(design §6.5 + R-Q4-2): +// +// - 命中 N 个重载(N ≥ 1):返回 N 个 *driverV2.DatabaseObjectDDL,ObjectName +// 拼为 `funcname(arg_type_list)`(无参数函数 arg_type_list 为空 → `fn()`); +// - 对象不存在(pg_proc 一行都没有命中):返回**单条**占位 +// *driverV2.DatabaseObjectDDL,ObjectDDL="",ObjectName 不带参数签名 +// (等价于业务层"该侧不存在"信号;与 TABLE/VIEW 兜底一致);**不报错**; +// - 其他错误(权限 / 网络 / SQLSTATE / rows.Err()):原样透传,避免吞掉 +// openGauss / GaussDB 的诊断信息。 +// +// 两次单列查询若返回行数不一致:按"min(len(defs), len(args))"对齐取较短一段, +// 不报错——一致性破坏极小概率(同事务并发增删重载),且对结果集语义无破坏。 +// schemaName / funcName 不在 Go 侧做任何 escape,全部走 database/sql 参数化 +// 绑定(design §6.3)。 +func (e *Extractor) extractFunction( + ctx context.Context, + schemaName, funcName string, +) ([]*driverV2.DatabaseObjectDDL, error) { + defs, err := e.queryFuncDefs(ctx, extractFunctionDefSQL, schemaName, funcName) + if err != nil { + return nil, err + } + args, err := e.queryFuncDefs(ctx, extractFunctionArgsSQL, schemaName, funcName) + if err != nil { + return nil, err + } + + if len(defs) == 0 { + // 对象不存在:返回单条占位 DDL,ObjectName 不带参数签名(无任何重载 + // 信息可拼接),让业务层 compareSchema 把它当作"该侧不存在"处理。 + return []*driverV2.DatabaseObjectDDL{ + { + DatabaseObject: &driverV2.DatabaseObject{ + ObjectName: funcName, + ObjectType: driverV2.ObjectType_FUNCTION, + }, + ObjectDDL: "", + }, + }, nil + } + + // 对齐两份单列结果(同 oid 顺序);防御性处理长度不一致。 + pairCount := len(defs) + if len(args) < pairCount { + pairCount = len(args) + } + + results := make([]*driverV2.DatabaseObjectDDL, 0, pairCount) + for i := 0; i < pairCount; i++ { + results = append(results, &driverV2.DatabaseObjectDDL{ + DatabaseObject: &driverV2.DatabaseObject{ + ObjectName: fmt.Sprintf("%s(%s)", funcName, args[i]), + ObjectType: driverV2.ObjectType_FUNCTION, + }, + ObjectDDL: defs[i], + }) + } + + return results, nil +} + +// queryFuncDefs 是 FUNCTION / PROCEDURE 单列查询共用的 "list 单列 string" helper。 +// +// 与 enumerate.go 的 queryObjectNames 同结构,但单独维护一个 helper 让 SQL +// 文本 + 参数语义自描述(明确是 functiondef / arguments 列表),便于阅读。 +func (e *Extractor) queryFuncDefs( + ctx context.Context, + sqlText, schemaName, objectName string, +) ([]string, error) { + rows, err := e.db.QueryContext(ctx, sqlText, schemaName, objectName) + if err != nil { + return nil, err + } + defer rows.Close() + + var out []string + for rows.Next() { + var v string + if scanErr := rows.Scan(&v); scanErr != nil { + return nil, scanErr + } + out = append(out, v) + } + if iterErr := rows.Err(); iterErr != nil { + return nil, iterErr + } + return out, nil +} diff --git a/internal/extractor/function_test.go b/internal/extractor/function_test.go new file mode 100644 index 0000000..8f0ffe6 --- /dev/null +++ b/internal/extractor/function_test.go @@ -0,0 +1,750 @@ +// function_test.go covers design §17.1.3 — FUNCTION / PROCEDURE 重载单测矩阵: +// +// - FUNCTION / PROCEDURE 正常分发(Extract 入口 → extractFunction / +// extractProcedure → 单参 / 多参 / 无参 / 对象不存在 / 权限错误透传) +// - FUNCTION 重载(无参 / 单参 / 同名两参 / 同名三重载):ObjectName 携带 +// `funcname(arg_type_list)`,map key 天然唯一(design §6.4 + R-Q4-2) +// - PROCEDURE 重载(同上结构,prokind = 'p') +// - 注入防御(FUNCTION + PROCEDURE 各 6 类输入串,与 table/view 注入测试对称) +// - Task-Test-Fix-001 P1.1:双列 SELECT 已拆成两次单列查询;单测同步 +// 更新为 mock 两条独立 query(extract*DefSQL → defs 列、extract*ArgsSQL → +// arguments 列),断言两次结果按 ORDER BY oid 顺序对齐。 +// +// 所有 sqlmock 用 QueryMatcherOption(QueryMatcherEqual) 模式锁定 SQL 文本, +// 避免正则模糊匹配掩盖 design 模板偏离;WithArgs 用原值锁定 Go 侧未 escape。 +// +// 共用 helper newMock 来自 extractor_test.go(同包)。 + +package extractor + +import ( + "context" + "errors" + "sort" + "strings" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + driverV2 "github.com/actiontech/sqle/sqle/driver/v2" +) + +// expectFuncDefsAndArgs 是 FUNCTION 路径双查询的 mock helper,rows 形如 +// [{def0, args0}, {def1, args1}, ...];defs 列 + args 列分别由两次 query 返回。 +func expectFuncDefsAndArgs(mock sqlmock.Sqlmock, schema, name string, rows [][2]string) { + defRows := sqlmock.NewRows([]string{"functiondef"}) + argRows := sqlmock.NewRows([]string{"arguments"}) + for _, r := range rows { + defRows.AddRow(r[0]) + argRows.AddRow(r[1]) + } + mock.ExpectQuery(extractFunctionDefSQL). + WithArgs(schema, name). + WillReturnRows(defRows) + mock.ExpectQuery(extractFunctionArgsSQL). + WithArgs(schema, name). + WillReturnRows(argRows) +} + +// expectProcDefsAndArgs 是 PROCEDURE 路径双查询的 mock helper(结构同上)。 +func expectProcDefsAndArgs(mock sqlmock.Sqlmock, schema, name string, rows [][2]string) { + defRows := sqlmock.NewRows([]string{"procdef"}) + argRows := sqlmock.NewRows([]string{"arguments"}) + for _, r := range rows { + defRows.AddRow(r[0]) + argRows.AddRow(r[1]) + } + mock.ExpectQuery(extractProcedureDefSQL). + WithArgs(schema, name). + WillReturnRows(defRows) + mock.ExpectQuery(extractProcedureArgsSQL). + WithArgs(schema, name). + WillReturnRows(argRows) +} + +// TestExtractor_Extract_FunctionAndProcedure 覆盖 Extract 入口的 FUNCTION / +// PROCEDURE 分发 + 对象不存在占位 + 权限错误透传(design §6.2.3 / §6.2.4 / +// §6.5)。 +func TestExtractor_Extract_FunctionAndProcedure(t *testing.T) { + const ( + funcDDLSingleArg = "CREATE OR REPLACE FUNCTION public.fn(p1 integer) RETURNS integer LANGUAGE plpgsql AS $function$ BEGIN RETURN p1; END $function$" + procDDLMultiArgs = "CREATE OR REPLACE PROCEDURE public.proc(p1 integer, p2 text) LANGUAGE plpgsql AS $procedure$ BEGIN NULL; END $procedure$" + ) + + cases := map[string]struct { + schema string + object *driverV2.DatabaseObject + mockSetup func(mock sqlmock.Sqlmock) + wantErr bool + wantErrPart string + wantLen int + wantObjName string + wantObjType string + wantDDL string + wantDDLEmpty bool + }{ + "function_normal_single_arg": { + schema: "public", + object: &driverV2.DatabaseObject{ObjectName: "fn", ObjectType: driverV2.ObjectType_FUNCTION}, + mockSetup: func(mock sqlmock.Sqlmock) { + expectFuncDefsAndArgs(mock, "public", "fn", [][2]string{ + {funcDDLSingleArg, "integer"}, + }) + }, + wantLen: 1, + wantObjName: "fn(integer)", + wantObjType: driverV2.ObjectType_FUNCTION, + wantDDL: funcDDLSingleArg, + }, + "procedure_normal_multi_args": { + schema: "public", + object: &driverV2.DatabaseObject{ObjectName: "proc", ObjectType: driverV2.ObjectType_PROCEDURE}, + mockSetup: func(mock sqlmock.Sqlmock) { + expectProcDefsAndArgs(mock, "public", "proc", [][2]string{ + {procDDLMultiArgs, "integer, text"}, + }) + }, + wantLen: 1, + wantObjName: "proc(integer, text)", + wantObjType: driverV2.ObjectType_PROCEDURE, + wantDDL: procDDLMultiArgs, + }, + "function_not_found_returns_empty_placeholder": { + schema: "public", + object: &driverV2.DatabaseObject{ObjectName: "missing_fn", ObjectType: driverV2.ObjectType_FUNCTION}, + mockSetup: func(mock sqlmock.Sqlmock) { + // 两次 query 都返回空行集 → 触发占位兜底。 + mock.ExpectQuery(extractFunctionDefSQL). + WithArgs("public", "missing_fn"). + WillReturnRows(sqlmock.NewRows([]string{"functiondef"})) + mock.ExpectQuery(extractFunctionArgsSQL). + WithArgs("public", "missing_fn"). + WillReturnRows(sqlmock.NewRows([]string{"arguments"})) + }, + wantLen: 1, + wantObjName: "missing_fn", // 不带参数签名,与"该侧不存在"语义一致 + wantObjType: driverV2.ObjectType_FUNCTION, + wantDDLEmpty: true, + }, + "procedure_not_found_returns_empty_placeholder": { + schema: "public", + object: &driverV2.DatabaseObject{ObjectName: "missing_proc", ObjectType: driverV2.ObjectType_PROCEDURE}, + mockSetup: func(mock sqlmock.Sqlmock) { + mock.ExpectQuery(extractProcedureDefSQL). + WithArgs("public", "missing_proc"). + WillReturnRows(sqlmock.NewRows([]string{"procdef"})) + mock.ExpectQuery(extractProcedureArgsSQL). + WithArgs("public", "missing_proc"). + WillReturnRows(sqlmock.NewRows([]string{"arguments"})) + }, + wantLen: 1, + wantObjName: "missing_proc", + wantObjType: driverV2.ObjectType_PROCEDURE, + wantDDLEmpty: true, + }, + "function_permission_denied_propagates": { + schema: "restricted", + object: &driverV2.DatabaseObject{ObjectName: "fn", ObjectType: driverV2.ObjectType_FUNCTION}, + mockSetup: func(mock sqlmock.Sqlmock) { + // 第一条 defs query 失败即返回,不会再发起 args query。 + mock.ExpectQuery(extractFunctionDefSQL). + WithArgs("restricted", "fn"). + WillReturnError(errors.New("permission denied for schema pg_catalog")) + }, + wantErr: true, + wantErrPart: "permission denied", + }, + } + + for name, c := range cases { + c := c + t.Run(name, func(t *testing.T) { + db, mock, cleanup := newMock(t) + defer cleanup() + c.mockSetup(mock) + + ex := NewExtractor(db) + result, err := ex.Extract(context.Background(), &driverV2.DatabaseSchemaInfo{ + SchemaName: c.schema, + DatabaseObjects: []*driverV2.DatabaseObject{c.object}, + }) + + if c.wantErr { + if err == nil { + t.Fatalf("expected error containing %q, got nil", c.wantErrPart) + } + if !strings.Contains(err.Error(), c.wantErrPart) { + t.Errorf("error %q does not contain %q", err.Error(), c.wantErrPart) + } + if result != nil { + t.Errorf("expected nil result on error, got %+v", result) + } + if mockErr := mock.ExpectationsWereMet(); mockErr != nil { + t.Errorf("unmet sqlmock expectations: %v", mockErr) + } + return + } + + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if result == nil { + t.Fatal("expected non-nil result") + } + if result.SchemaName != c.schema { + t.Errorf("SchemaName = %q, want %q", result.SchemaName, c.schema) + } + if len(result.DatabaseObjectDDLs) != c.wantLen { + t.Fatalf("DatabaseObjectDDLs len = %d, want %d", len(result.DatabaseObjectDDLs), c.wantLen) + } + got := result.DatabaseObjectDDLs[0] + if got.DatabaseObject == nil { + t.Fatal("DatabaseObject metadata missing") + } + if got.DatabaseObject.ObjectName != c.wantObjName { + t.Errorf("ObjectName = %q, want %q", got.DatabaseObject.ObjectName, c.wantObjName) + } + if got.DatabaseObject.ObjectType != c.wantObjType { + t.Errorf("ObjectType = %q, want %q", got.DatabaseObject.ObjectType, c.wantObjType) + } + if c.wantDDLEmpty { + if got.ObjectDDL != "" { + t.Errorf("expected empty DDL on not-found, got %q", got.ObjectDDL) + } + } else if got.ObjectDDL != c.wantDDL { + t.Errorf("ObjectDDL mismatch:\n got %q\n want %q", got.ObjectDDL, c.wantDDL) + } + if mockErr := mock.ExpectationsWereMet(); mockErr != nil { + t.Errorf("unmet sqlmock expectations: %v", mockErr) + } + }) + } +} + +// TestExtractor_FunctionOverload 覆盖 design §17.1.3 + §6.4 重载识别核心矩阵: +// 无参 / 单参 / 同名两重载 / 同名三重载 四类签名分支。 +// +// 每条 case 直接调用 extractFunction(绕过 Extract 入口)以便精确断言 slice +// 长度与 ObjectName 集合;这与 TestExtractor_Extract_FunctionAndProcedure 形成 +// 互补——后者覆盖入口分发,前者覆盖 helper 自身契约。 +func TestExtractor_FunctionOverload(t *testing.T) { + cases := map[string]struct { + schema string + funcName string + mockRows [][2]string // [functiondef, arguments] 序列;按 oid 顺序对齐 + wantNames []string // 期望的 ObjectName 集合(顺序无关) + wantDDLs []string // 期望的 DDL 集合(顺序无关) + }{ + "function_no_arg": { + schema: "public", + funcName: "fn", + mockRows: [][2]string{ + {"CREATE OR REPLACE FUNCTION public.fn() RETURNS void AS $$ BEGIN NULL; END $$ LANGUAGE plpgsql", ""}, + }, + wantNames: []string{"fn()"}, + wantDDLs: []string{"CREATE OR REPLACE FUNCTION public.fn() RETURNS void AS $$ BEGIN NULL; END $$ LANGUAGE plpgsql"}, + }, + "function_single_arg": { + schema: "public", + funcName: "fn", + mockRows: [][2]string{ + {"CREATE FUNCTION public.fn(p1 integer) RETURNS integer AS $$ ... $$ LANGUAGE plpgsql", "integer"}, + }, + wantNames: []string{"fn(integer)"}, + wantDDLs: []string{"CREATE FUNCTION public.fn(p1 integer) RETURNS integer AS $$ ... $$ LANGUAGE plpgsql"}, + }, + "function_two_overloads": { + schema: "public", + funcName: "fn", + mockRows: [][2]string{ + {"CREATE FUNCTION public.fn(p1 integer) RETURNS integer AS $$ ... $$ LANGUAGE plpgsql", "integer"}, + {"CREATE FUNCTION public.fn(p1 text) RETURNS text AS $$ ... $$ LANGUAGE plpgsql", "text"}, + }, + wantNames: []string{"fn(integer)", "fn(text)"}, + wantDDLs: []string{ + "CREATE FUNCTION public.fn(p1 integer) RETURNS integer AS $$ ... $$ LANGUAGE plpgsql", + "CREATE FUNCTION public.fn(p1 text) RETURNS text AS $$ ... $$ LANGUAGE plpgsql", + }, + }, + "function_three_overloads": { + schema: "public", + funcName: "fn", + mockRows: [][2]string{ + {"CREATE FUNCTION public.fn(p1 integer) RETURNS integer AS $$ ... $$ LANGUAGE plpgsql", "integer"}, + {"CREATE FUNCTION public.fn(p1 text) RETURNS text AS $$ ... $$ LANGUAGE plpgsql", "text"}, + {"CREATE FUNCTION public.fn(p1 integer, p2 text) RETURNS text AS $$ ... $$ LANGUAGE plpgsql", "integer, text"}, + }, + wantNames: []string{"fn(integer)", "fn(text)", "fn(integer, text)"}, + wantDDLs: []string{ + "CREATE FUNCTION public.fn(p1 integer) RETURNS integer AS $$ ... $$ LANGUAGE plpgsql", + "CREATE FUNCTION public.fn(p1 text) RETURNS text AS $$ ... $$ LANGUAGE plpgsql", + "CREATE FUNCTION public.fn(p1 integer, p2 text) RETURNS text AS $$ ... $$ LANGUAGE plpgsql", + }, + }, + // 「无参 + 单参」双重载场景:补充 design §6.4 中 "fn()" 与 "fn(integer)" + // 同时存在时上层 map key 不冲突的回归验证(无参函数 ObjectName 与 + // 任意带参签名永远不同,但单测必须显式断言 key 集合唯一性)。 + "function_no_arg_and_single_arg_overload": { + schema: "public", + funcName: "fn", + mockRows: [][2]string{ + {"CREATE FUNCTION public.fn() RETURNS void AS $$ BEGIN NULL; END $$ LANGUAGE plpgsql", ""}, + {"CREATE FUNCTION public.fn(p1 integer) RETURNS integer AS $$ ... $$ LANGUAGE plpgsql", "integer"}, + }, + wantNames: []string{"fn()", "fn(integer)"}, + wantDDLs: []string{ + "CREATE FUNCTION public.fn() RETURNS void AS $$ BEGIN NULL; END $$ LANGUAGE plpgsql", + "CREATE FUNCTION public.fn(p1 integer) RETURNS integer AS $$ ... $$ LANGUAGE plpgsql", + }, + }, + } + + for name, c := range cases { + c := c + t.Run(name, func(t *testing.T) { + db, mock, cleanup := newMock(t) + defer cleanup() + + expectFuncDefsAndArgs(mock, c.schema, c.funcName, c.mockRows) + + ex := NewExtractor(db) + results, err := ex.extractFunction(context.Background(), c.schema, c.funcName) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(results) != len(c.wantNames) { + t.Fatalf("results len = %d, want %d", len(results), len(c.wantNames)) + } + + gotNames := make([]string, 0, len(results)) + gotDDLs := make([]string, 0, len(results)) + gotTypes := make(map[string]bool) + for _, r := range results { + gotNames = append(gotNames, r.DatabaseObject.ObjectName) + gotDDLs = append(gotDDLs, r.ObjectDDL) + gotTypes[r.DatabaseObject.ObjectType] = true + } + sort.Strings(gotNames) + sort.Strings(gotDDLs) + wantNamesSorted := append([]string(nil), c.wantNames...) + wantDDLsSorted := append([]string(nil), c.wantDDLs...) + sort.Strings(wantNamesSorted) + sort.Strings(wantDDLsSorted) + + for i := range wantNamesSorted { + if gotNames[i] != wantNamesSorted[i] { + t.Errorf("ObjectName[%d] = %q, want %q", i, gotNames[i], wantNamesSorted[i]) + } + if gotDDLs[i] != wantDDLsSorted[i] { + t.Errorf("ObjectDDL[%d] = %q, want %q", i, gotDDLs[i], wantDDLsSorted[i]) + } + } + + if len(gotTypes) != 1 || !gotTypes[driverV2.ObjectType_FUNCTION] { + t.Errorf("ObjectType set = %v, want only {FUNCTION}", gotTypes) + } + + uniq := make(map[string]bool) + for _, n := range gotNames { + if uniq[n] { + t.Errorf("duplicate ObjectName %q in overload result; map key uniqueness broken", n) + } + uniq[n] = true + } + + if mockErr := mock.ExpectationsWereMet(); mockErr != nil { + t.Errorf("unmet sqlmock expectations: %v", mockErr) + } + }) + } +} + +// TestExtractor_ProcedureOverload 与 TestExtractor_FunctionOverload 同结构, +// 仅走 extractProcedure 路径(prokind = 'p')。 +func TestExtractor_ProcedureOverload(t *testing.T) { + cases := map[string]struct { + schema string + procName string + mockRows [][2]string + wantNames []string + wantDDLs []string + }{ + "procedure_no_arg": { + schema: "public", + procName: "proc", + mockRows: [][2]string{ + {"CREATE OR REPLACE PROCEDURE public.proc() LANGUAGE plpgsql AS $$ BEGIN NULL; END $$", ""}, + }, + wantNames: []string{"proc()"}, + wantDDLs: []string{"CREATE OR REPLACE PROCEDURE public.proc() LANGUAGE plpgsql AS $$ BEGIN NULL; END $$"}, + }, + "procedure_single_arg": { + schema: "public", + procName: "proc", + mockRows: [][2]string{ + {"CREATE PROCEDURE public.proc(p1 integer) LANGUAGE plpgsql AS $$ ... $$", "integer"}, + }, + wantNames: []string{"proc(integer)"}, + wantDDLs: []string{"CREATE PROCEDURE public.proc(p1 integer) LANGUAGE plpgsql AS $$ ... $$"}, + }, + "procedure_two_overloads": { + schema: "public", + procName: "proc", + mockRows: [][2]string{ + {"CREATE PROCEDURE public.proc(p1 integer) LANGUAGE plpgsql AS $$ ... $$", "integer"}, + {"CREATE PROCEDURE public.proc(p1 text) LANGUAGE plpgsql AS $$ ... $$", "text"}, + }, + wantNames: []string{"proc(integer)", "proc(text)"}, + wantDDLs: []string{ + "CREATE PROCEDURE public.proc(p1 integer) LANGUAGE plpgsql AS $$ ... $$", + "CREATE PROCEDURE public.proc(p1 text) LANGUAGE plpgsql AS $$ ... $$", + }, + }, + "procedure_three_overloads": { + schema: "public", + procName: "proc", + mockRows: [][2]string{ + {"CREATE PROCEDURE public.proc(p1 integer) LANGUAGE plpgsql AS $$ ... $$", "integer"}, + {"CREATE PROCEDURE public.proc(p1 text) LANGUAGE plpgsql AS $$ ... $$", "text"}, + {"CREATE PROCEDURE public.proc(p1 integer, p2 text) LANGUAGE plpgsql AS $$ ... $$", "integer, text"}, + }, + wantNames: []string{"proc(integer)", "proc(text)", "proc(integer, text)"}, + wantDDLs: []string{ + "CREATE PROCEDURE public.proc(p1 integer) LANGUAGE plpgsql AS $$ ... $$", + "CREATE PROCEDURE public.proc(p1 text) LANGUAGE plpgsql AS $$ ... $$", + "CREATE PROCEDURE public.proc(p1 integer, p2 text) LANGUAGE plpgsql AS $$ ... $$", + }, + }, + } + + for name, c := range cases { + c := c + t.Run(name, func(t *testing.T) { + db, mock, cleanup := newMock(t) + defer cleanup() + + expectProcDefsAndArgs(mock, c.schema, c.procName, c.mockRows) + + ex := NewExtractor(db) + results, err := ex.extractProcedure(context.Background(), c.schema, c.procName) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(results) != len(c.wantNames) { + t.Fatalf("results len = %d, want %d", len(results), len(c.wantNames)) + } + + gotNames := make([]string, 0, len(results)) + gotDDLs := make([]string, 0, len(results)) + gotTypes := make(map[string]bool) + for _, r := range results { + gotNames = append(gotNames, r.DatabaseObject.ObjectName) + gotDDLs = append(gotDDLs, r.ObjectDDL) + gotTypes[r.DatabaseObject.ObjectType] = true + } + sort.Strings(gotNames) + sort.Strings(gotDDLs) + wantNamesSorted := append([]string(nil), c.wantNames...) + wantDDLsSorted := append([]string(nil), c.wantDDLs...) + sort.Strings(wantNamesSorted) + sort.Strings(wantDDLsSorted) + + for i := range wantNamesSorted { + if gotNames[i] != wantNamesSorted[i] { + t.Errorf("ObjectName[%d] = %q, want %q", i, gotNames[i], wantNamesSorted[i]) + } + if gotDDLs[i] != wantDDLsSorted[i] { + t.Errorf("ObjectDDL[%d] = %q, want %q", i, gotDDLs[i], wantDDLsSorted[i]) + } + } + + if len(gotTypes) != 1 || !gotTypes[driverV2.ObjectType_PROCEDURE] { + t.Errorf("ObjectType set = %v, want only {PROCEDURE}", gotTypes) + } + + uniq := make(map[string]bool) + for _, n := range gotNames { + if uniq[n] { + t.Errorf("duplicate ObjectName %q in overload result; map key uniqueness broken", n) + } + uniq[n] = true + } + + if mockErr := mock.ExpectationsWereMet(); mockErr != nil { + t.Errorf("unmet sqlmock expectations: %v", mockErr) + } + }) + } +} + +// TestExtractor_QuoteIdent_FunctionInjectionDefense 验证 FUNCTION 路径的注入 +// 防御:schema / function 名含特殊字符(空格 / 大小写 / 中文 / SQL 注入串 / +// 单双引号)时,Extractor 不在 Go 侧做任何 escape,原值通过 $1 / $2 透传到 +// sqlmock 参数断言。 +// +// 与 TABLE / VIEW 注入测试形成全路径覆盖(compat-RISK-4 SQL 注入兜底)。 +func TestExtractor_QuoteIdent_FunctionInjectionDefense(t *testing.T) { + cases := map[string]struct { + schema string + funcName string + }{ + "space_in_schema": {schema: "My Schema", funcName: "fn"}, + "uppercase_schema": {schema: "MySchema", funcName: "fn"}, + "chinese_schema": {schema: "中文模式", funcName: "fn"}, + "sql_injection_in_func_name": {schema: "public", funcName: "fn; DROP FUNCTION x; --"}, + "single_quote_in_func_name": {schema: "public", funcName: "it's_fn"}, + "double_quote_in_func_name": {schema: "public", funcName: `"quoted_fn"`}, + } + + const ddlEcho = "" + + for name, c := range cases { + c := c + t.Run(name, func(t *testing.T) { + db, mock, cleanup := newMock(t) + defer cleanup() + + // WithArgs(c.schema, c.funcName) 强约束:sqlmock 对每个 driver.Value + // 做 reflect.DeepEqual 比较;若 Extractor 在 Go 侧做任何 escape, + // 实参就会与原值不等,匹配失败导致测试 fail。 + expectFuncDefsAndArgs(mock, c.schema, c.funcName, [][2]string{ + {ddlEcho, "integer"}, + }) + + ex := NewExtractor(db) + result, err := ex.Extract(context.Background(), &driverV2.DatabaseSchemaInfo{ + SchemaName: c.schema, + DatabaseObjects: []*driverV2.DatabaseObject{ + {ObjectName: c.funcName, ObjectType: driverV2.ObjectType_FUNCTION}, + }, + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if result == nil || len(result.DatabaseObjectDDLs) != 1 { + t.Fatalf("unexpected result shape: %+v", result) + } + got := result.DatabaseObjectDDLs[0] + // ObjectName 在重载场景拼接 "(arguments)";funcName 原值必须为前缀, + // 表明用户输入未被 Go 侧 escape。 + wantObjName := c.funcName + "(integer)" + if got.DatabaseObject.ObjectName != wantObjName { + t.Errorf("ObjectName = %q, want %q", got.DatabaseObject.ObjectName, wantObjName) + } + if got.DatabaseObject.ObjectType != driverV2.ObjectType_FUNCTION { + t.Errorf("ObjectType = %q, want %q", got.DatabaseObject.ObjectType, driverV2.ObjectType_FUNCTION) + } + if got.ObjectDDL != ddlEcho { + t.Errorf("ObjectDDL = %q, want %q", got.ObjectDDL, ddlEcho) + } + if mockErr := mock.ExpectationsWereMet(); mockErr != nil { + t.Errorf("unmet sqlmock expectations: %v", mockErr) + } + }) + } +} + +// TestExtractor_QuoteIdent_ProcedureInjectionDefense 与上一组同结构,但走 +// PROCEDURE 抽取路径,确保 procedure.go 的参数化绑定同样防注入。 +func TestExtractor_QuoteIdent_ProcedureInjectionDefense(t *testing.T) { + cases := map[string]struct { + schema string + procName string + }{ + "space_in_schema": {schema: "My Schema", procName: "proc"}, + "uppercase_schema": {schema: "MySchema", procName: "proc"}, + "chinese_schema": {schema: "中文模式", procName: "proc"}, + "sql_injection_in_proc_name": {schema: "public", procName: "proc; DROP PROCEDURE x; --"}, + "single_quote_in_proc_name": {schema: "public", procName: "it's_proc"}, + "double_quote_in_proc_name": {schema: "public", procName: `"quoted_proc"`}, + } + + const ddlEcho = "" + + for name, c := range cases { + c := c + t.Run(name, func(t *testing.T) { + db, mock, cleanup := newMock(t) + defer cleanup() + + expectProcDefsAndArgs(mock, c.schema, c.procName, [][2]string{ + {ddlEcho, "integer"}, + }) + + ex := NewExtractor(db) + result, err := ex.Extract(context.Background(), &driverV2.DatabaseSchemaInfo{ + SchemaName: c.schema, + DatabaseObjects: []*driverV2.DatabaseObject{ + {ObjectName: c.procName, ObjectType: driverV2.ObjectType_PROCEDURE}, + }, + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if result == nil || len(result.DatabaseObjectDDLs) != 1 { + t.Fatalf("unexpected result shape: %+v", result) + } + got := result.DatabaseObjectDDLs[0] + wantObjName := c.procName + "(integer)" + if got.DatabaseObject.ObjectName != wantObjName { + t.Errorf("ObjectName = %q, want %q", got.DatabaseObject.ObjectName, wantObjName) + } + if got.DatabaseObject.ObjectType != driverV2.ObjectType_PROCEDURE { + t.Errorf("ObjectType = %q, want %q", got.DatabaseObject.ObjectType, driverV2.ObjectType_PROCEDURE) + } + if got.ObjectDDL != ddlEcho { + t.Errorf("ObjectDDL = %q, want %q", got.ObjectDDL, ddlEcho) + } + if mockErr := mock.ExpectationsWereMet(); mockErr != nil { + t.Errorf("unmet sqlmock expectations: %v", mockErr) + } + }) + } +} + +// TestExtractFunction_NoTupleWrapping 是 Task-Test-Fix-001 P1.1 修复的回归 +// 验证:双列 SELECT 拆成两次单列查询后,extractor 不再在 ObjectDDL 中输出 +// `(N,"...")` 这种 Postgres tuple/record 字面值。 +// +// 验证策略:mock 单列 SQL 返回纯 DDL 文本 → 断言 ObjectDDL 等于该原值(无 +// 前导 `(N,"` 与尾部 `")"`)。同时验证 ObjectName 拼接结果不含 tuple 字面值。 +func TestExtractFunction_NoTupleWrapping(t *testing.T) { + const rawDDL = "CREATE OR REPLACE FUNCTION test_2905_base.fn(p1 integer) RETURNS integer LANGUAGE plpgsql AS $$ BEGIN RETURN p1; END $$" + + db, mock, cleanup := newMock(t) + defer cleanup() + + expectFuncDefsAndArgs(mock, "test_2905_base", "fn", [][2]string{ + {rawDDL, "integer"}, + }) + + ex := NewExtractor(db) + results, err := ex.extractFunction(context.Background(), "test_2905_base", "fn") + if err != nil { + t.Fatalf("extractFunction: %v", err) + } + if len(results) != 1 { + t.Fatalf("results len = %d, want 1", len(results)) + } + got := results[0] + if got.ObjectDDL != rawDDL { + t.Errorf("ObjectDDL = %q, want %q", got.ObjectDDL, rawDDL) + } + // 反向断言:tuple 字面值前缀 `(N,"` 不应出现。 + if strings.HasPrefix(got.ObjectDDL, "(") && strings.Contains(got.ObjectDDL, `,"`) { + t.Errorf("ObjectDDL appears to be Postgres tuple literal: %q", got.ObjectDDL) + } + if got.DatabaseObject.ObjectName != "fn(integer)" { + t.Errorf("ObjectName = %q, want %q", got.DatabaseObject.ObjectName, "fn(integer)") + } + if mockErr := mock.ExpectationsWereMet(); mockErr != nil { + t.Errorf("unmet sqlmock expectations: %v", mockErr) + } +} + +// TestExtractProcedure_NoTupleWrapping 同 TestExtractFunction_NoTupleWrapping, +// 仅走 PROCEDURE 路径(case-2-4 报告的 (1,"CREATE OR REPLACE PROCEDURE ...") +// 场景)。 +func TestExtractProcedure_NoTupleWrapping(t *testing.T) { + const rawDDL = "CREATE OR REPLACE PROCEDURE test_2905_compared.proc_log_action(p_action text) LANGUAGE plpgsql AS $$ BEGIN RAISE NOTICE 'action: %', p_action; END; $$" + + db, mock, cleanup := newMock(t) + defer cleanup() + + expectProcDefsAndArgs(mock, "test_2905_compared", "proc_log_action", [][2]string{ + {rawDDL, "p_action text"}, + }) + + ex := NewExtractor(db) + results, err := ex.extractProcedure(context.Background(), "test_2905_compared", "proc_log_action") + if err != nil { + t.Fatalf("extractProcedure: %v", err) + } + if len(results) != 1 { + t.Fatalf("results len = %d, want 1", len(results)) + } + got := results[0] + if got.ObjectDDL != rawDDL { + t.Errorf("ObjectDDL = %q, want %q", got.ObjectDDL, rawDDL) + } + if strings.HasPrefix(got.ObjectDDL, "(") && strings.Contains(got.ObjectDDL, `,"`) { + t.Errorf("ObjectDDL appears to be Postgres tuple literal: %q", got.ObjectDDL) + } + if got.DatabaseObject.ObjectName != "proc_log_action(p_action text)" { + t.Errorf("ObjectName = %q, want %q", got.DatabaseObject.ObjectName, "proc_log_action(p_action text)") + } + if mockErr := mock.ExpectationsWereMet(); mockErr != nil { + t.Errorf("unmet sqlmock expectations: %v", mockErr) + } +} + +// TestExtractProcedure_TrimTrailingSlash 覆盖 Task-Test-Fix-002 P3: +// +// openGauss pg_get_functiondef 对 PROCEDURE 返回值末尾会带一行独立的 `/` +// (PL/SQL 终止符),Go database/sql + openGauss-connector-go-pq 驱动不识别 +// `/` 作为语句终止符,会撞 `syntax error at or near "/"`。extractor 层必须 +// 剥除该末尾独立行 `/`(保留中间内嵌内容),让下游 modify_sql_statements 输出 +// 能在 Go/JDBC 驱动里直接 EXEC_OK(case-3-4.md / semantic +// opengauss_procedure_slash_terminator_driver_incompat_20260521.md 缺陷溯源)。 +func TestExtractProcedure_TrimTrailingSlash(t *testing.T) { + cases := map[string]struct { + raw string + want string + }{ + "slash_with_trailing_newline": { + raw: "CREATE OR REPLACE PROCEDURE s.p()\n" + + "AS DECLARE BEGIN NULL; END;\n" + + "/\n", + want: "CREATE OR REPLACE PROCEDURE s.p()\n" + + "AS DECLARE BEGIN NULL; END;", + }, + "slash_no_trailing_newline": { + raw: "CREATE OR REPLACE PROCEDURE s.p()\n" + + "AS DECLARE BEGIN NULL; END;\n" + + "/", + want: "CREATE OR REPLACE PROCEDURE s.p()\n" + + "AS DECLARE BEGIN NULL; END;", + }, + "slash_with_extra_blank_line_before": { + raw: "CREATE OR REPLACE PROCEDURE s.p()\n" + + "AS DECLARE BEGIN NULL; END;\n" + + "\n/\n", + want: "CREATE OR REPLACE PROCEDURE s.p()\n" + + "AS DECLARE BEGIN NULL; END;", + }, + "no_slash_unchanged": { + raw: "CREATE OR REPLACE PROCEDURE s.p()\n" + + "AS DECLARE BEGIN NULL; END;", + want: "CREATE OR REPLACE PROCEDURE s.p()\n" + + "AS DECLARE BEGIN NULL; END;", + }, + } + + for name, c := range cases { + t.Run(name, func(t *testing.T) { + db, mock, cleanup := newMock(t) + defer cleanup() + + expectProcDefsAndArgs(mock, "s", "p", [][2]string{ + {c.raw, ""}, + }) + + ex := NewExtractor(db) + results, err := ex.extractProcedure(context.Background(), "s", "p") + if err != nil { + t.Fatalf("extractProcedure: %v", err) + } + if len(results) != 1 { + t.Fatalf("results len = %d, want 1", len(results)) + } + if results[0].ObjectDDL != c.want { + t.Errorf("ObjectDDL mismatch:\n got: %q\nwant: %q", results[0].ObjectDDL, c.want) + } + if strings.HasSuffix(results[0].ObjectDDL, "/") { + t.Errorf("ObjectDDL still ends with `/`: %q", results[0].ObjectDDL) + } + if mockErr := mock.ExpectationsWereMet(); mockErr != nil { + t.Errorf("unmet sqlmock expectations: %v", mockErr) + } + }) + } +} diff --git a/internal/extractor/procedure.go b/internal/extractor/procedure.go new file mode 100644 index 0000000..2a797e6 --- /dev/null +++ b/internal/extractor/procedure.go @@ -0,0 +1,130 @@ +// procedure.go implements design §6.2.4 — PROCEDURE DDL extraction via +// pg_get_functiondef + pg_get_function_arguments + prokind = 'p',结构与 +// function.go 完全对齐,仅 prokind 字面值与 ObjectType 不同。 +// +// 实现策略:与 function.go 同因——拆成两次单列查询规避 GaussDB 驱动多列 +// 含 pg_get_functiondef 时的 record 字面值包裹(Task-Test-Fix-001 P1.1)。 +// +// 红线(design §6.3):schemaName / procName 必须通过 $1 / $2 参数化绑定, +// 禁止任何 Go 侧的字符串拼接 / 手工 escape——转义责任在 PostgreSQL 服务端 + +// libpq 驱动。 + +package extractor + +import ( + "context" + "fmt" + "regexp" + "strings" + + driverV2 "github.com/actiontech/sqle/sqle/driver/v2" +) + +// plsqlTrailingSlashPattern 匹配 PROCEDURE DDL 末尾独立行 `/`(PL/SQL 终止符)。 +// +// 设计要点: +// +// - `(?m)`:多行模式让 `^` / `$` 匹配行首行尾,方便识别"独立行"形态; +// - `\s*\n+`:允许 `/` 之前有空白行或空格; +// - `^\s*/\s*$`:`/` 单独占一行(前后允许空白); +// - `\s*\z`:行尾以及文件末尾的尾随空白都吞掉,让剥除结果干净。 +// +// 背景(semantic/opengauss_procedure_slash_terminator_driver_incompat_20260521): +// openGauss `pg_get_functiondef(oid)` 对 PROCEDURE 类型(prokind='p')返回的 +// DDL 末尾会包含一行独立的 `/`,是 PL/SQL 风格的客户端语句终止符(gsql / +// SQL*Plus 风格);Go database/sql + openGauss-connector-go-pq、JDBC、psycopg2 +// 等"驱动直连+逐语句解析"通道不识别 `/` 作为语句终止符,会撞 +// `syntax error at or near "/"`。FUNCTION(prokind='f')输出不带末尾 `/`(结尾是 +// `$function$;`),所以仅 PROCEDURE 路径需要剥除。 +// +// 中间内嵌 `/`(理论上不会出现在 PL/SQL body 内,因 `/` 是 gsql 终止符, +// PL/SQL 块体合法语法不含字面值 `/` 单独占行)不会被该正则误命中——只剥除 +// 文末尾部的 `/`。 +var plsqlTrailingSlashPattern = regexp.MustCompile(`(?m)\n\s*/\s*\z`) + +// stripPLSQLTerminator 把 PROCEDURE DDL 末尾的 PL/SQL 终止符 `/` 行剥除, +// 让 Go/JDBC 驱动可以逐语句直接执行(Task-Test-Fix-002 P3)。 +// +// 实现策略:先 TrimRight 一次尾随空白,再用正则剥末尾独立 `/` 行;剥除后 +// 再 TrimRight 一次清掉剩余空白。这样无论 pg_get_functiondef 输出末尾是 +// `...\nEND;\n/\n` 还是 `...\nEND;\n/` 都能正确处理。 +func stripPLSQLTerminator(def string) string { + def = strings.TrimRight(def, " \t\r\n") + def = plsqlTrailingSlashPattern.ReplaceAllString(def, "") + return strings.TrimRight(def, " \t\r\n") +} + +// extractProcedureDefSQL 单列查询:取所有同名 PROCEDURE 重载的 DDL 文本, +// 按 oid 升序返回。 +// +// 注意 `(pg_get_functiondef(p.oid)).definition` 的显式字段访问——同 function.go +// 注释里说明的 GaussDB record 返回类型问题(Task-Test-Fix-001 P1.1)。 +const extractProcedureDefSQL = `SELECT (pg_get_functiondef(p.oid)).definition +FROM pg_proc p +JOIN pg_namespace n ON n.oid = p.pronamespace +WHERE n.nspname = $1 + AND p.prokind = 'p' + AND p.proname = $2 +ORDER BY p.oid` + +// extractProcedureArgsSQL 单列查询:取所有同名 PROCEDURE 重载的参数签名字符串, +// 按 oid 升序返回(与 extractProcedureDefSQL 对齐)。 +const extractProcedureArgsSQL = `SELECT pg_get_function_arguments(p.oid) +FROM pg_proc p +JOIN pg_namespace n ON n.oid = p.pronamespace +WHERE n.nspname = $1 + AND p.prokind = 'p' + AND p.proname = $2 +ORDER BY p.oid` + +// extractProcedure 按 design §6.2.4 + §6.4 抽取同名 PROCEDURE(含重载)的所有 +// DDL,每条独立返回。语义与 extractFunction 完全一致,仅 prokind 字面值差异。 +// +// 复用 queryFuncDefs helper(在 function.go 定义)做单列字符串列表的两次 +// query;两次结果按 oid ORDER BY 自然对齐。 +func (e *Extractor) extractProcedure( + ctx context.Context, + schemaName, procName string, +) ([]*driverV2.DatabaseObjectDDL, error) { + defs, err := e.queryFuncDefs(ctx, extractProcedureDefSQL, schemaName, procName) + if err != nil { + return nil, err + } + args, err := e.queryFuncDefs(ctx, extractProcedureArgsSQL, schemaName, procName) + if err != nil { + return nil, err + } + + if len(defs) == 0 { + return []*driverV2.DatabaseObjectDDL{ + { + DatabaseObject: &driverV2.DatabaseObject{ + ObjectName: procName, + ObjectType: driverV2.ObjectType_PROCEDURE, + }, + ObjectDDL: "", + }, + }, nil + } + + pairCount := len(defs) + if len(args) < pairCount { + pairCount = len(args) + } + + results := make([]*driverV2.DatabaseObjectDDL, 0, pairCount) + for i := 0; i < pairCount; i++ { + // Task-Test-Fix-002 P3: 剥除 pg_get_functiondef 对 PROCEDURE 返回值 + // 末尾的 PL/SQL 终止符 `/`,让 Go/JDBC 驱动可以直接执行变更 SQL; + // FUNCTION 路径(extractFunction)不需要此处理(不带 `/`)。 + results = append(results, &driverV2.DatabaseObjectDDL{ + DatabaseObject: &driverV2.DatabaseObject{ + ObjectName: fmt.Sprintf("%s(%s)", procName, args[i]), + ObjectType: driverV2.ObjectType_PROCEDURE, + }, + ObjectDDL: stripPLSQLTerminator(defs[i]), + }) + } + + return results, nil +} diff --git a/internal/extractor/table.go b/internal/extractor/table.go new file mode 100644 index 0000000..c847bbe --- /dev/null +++ b/internal/extractor/table.go @@ -0,0 +1,75 @@ +// table.go implements design §6.2.1 — TABLE DDL extraction via +// pg_get_tabledef + relkind IN ('r', 'p'). +// +// SQL 模板严格 byte-for-byte 匹配 design §6.2.1 line 411-418;任何空白 / 换行 / +// 关键字大小写偏离都视为破坏 design 契约(单测用 +// sqlmock.QueryMatcherEqual 锁定)。 +// +// 红线(design §6.3):schemaName / tableName 必须通过 $1 / $2 参数化绑定, +// 禁止任何 Go 侧的字符串拼接 / 手工 escape——转义责任在 PostgreSQL 服务端 + +// libpq 驱动。 + +package extractor + +import ( + "context" + "database/sql" + "errors" + + driverV2 "github.com/actiontech/sqle/sqle/driver/v2" +) + +// extractTableSQL 是 TABLE DDL 抽取 SQL(design §6.2.1 line 411-418)。 +// +// 用 const 单一定义,便于单测端通过 import 同字面值断言;保持原始换行 + 缩进 +// 风格与 design.md 代码块一致。 +// +// relkind IN ('r', 'p') 同时覆盖普通表(r)与分区表(p),exploration §1.2 (4) +// 在 GaussDB 505.2.1 + openGauss 6.0 均实测可用。 +const extractTableSQL = `SELECT pg_get_tabledef(c.oid) +FROM pg_class c +JOIN pg_namespace n ON n.oid = c.relnamespace +WHERE n.nspname = $1 + AND c.relkind IN ('r', 'p') + AND c.relname = $2` + +// extractTable 按 design §6.2.1 抽取单张表的 DDL。 +// +// 返回值约定(design §6.5): +// +// - 对象不存在(pg_class 无行命中 → sql.ErrNoRows):返回一个 +// ObjectDDL=="" 的占位 *driverV2.DatabaseObjectDDL,**不报错**,让业务层 +// compareSchema 把它当作"该侧不存在"处理; +// - 其他错误(权限 / 网络 / SQLSTATE):原样透传,避免吞掉 +// openGauss / GaussDB 的诊断信息(与 driver_common.go wrapPGError 策略一致)。 +// +// schemaName / tableName 不在 Go 侧做任何 escape,全部走 database/sql 参数化 +// 绑定(design §6.3)。 +func (e *Extractor) extractTable( + ctx context.Context, + schemaName, tableName string, +) (*driverV2.DatabaseObjectDDL, error) { + objectMeta := &driverV2.DatabaseObject{ + ObjectName: tableName, + ObjectType: driverV2.ObjectType_TABLE, + } + + var ddl string + err := e.db.QueryRowContext(ctx, extractTableSQL, schemaName, tableName).Scan(&ddl) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + // 对象不存在:返回空 DDL 占位,不报错。 + return &driverV2.DatabaseObjectDDL{ + DatabaseObject: objectMeta, + ObjectDDL: "", + }, nil + } + // 透传原错误(含 SQLSTATE)。 + return nil, err + } + + return &driverV2.DatabaseObjectDDL{ + DatabaseObject: objectMeta, + ObjectDDL: ddl, + }, nil +} diff --git a/internal/extractor/view.go b/internal/extractor/view.go new file mode 100644 index 0000000..151ee44 --- /dev/null +++ b/internal/extractor/view.go @@ -0,0 +1,70 @@ +// view.go implements design §6.2.2 — VIEW DDL extraction via +// pg_get_viewdef(c.oid, true) + relkind = 'v',并在 SELECT 列表中用 +// quote_ident 拼接 schema 限定的 CREATE OR REPLACE VIEW 头部。 +// +// SQL 模板严格 byte-for-byte 匹配 design §6.2.2 line 424-432;pretty-print mode +// 必须保留 (oid, true) 第二参数(false 模式输出无换行难以 diff)。 +// +// 红线(design §6.3):schemaName / viewName 必须通过 $1 / $2 参数化绑定; +// 拼接到 SELECT 列表里的 schema 限定符通过 PostgreSQL 内建 quote_ident() +// 在数据库服务端处理,**禁止** Go 侧手工 escape。 + +package extractor + +import ( + "context" + "database/sql" + "errors" + + driverV2 "github.com/actiontech/sqle/sqle/driver/v2" +) + +// extractViewSQL 是 VIEW DDL 抽取 SQL(design §6.2.2 line 424-432)。 +// +// 头部显式拼 `CREATE OR REPLACE VIEW ...` 是因为 pg_get_viewdef 默认只返回 +// SELECT 主体;schema 与 view 名通过 quote_ident 函数在服务端转义,确保 +// PostgreSQL 标识符引用规则一致(exploration §3.2)。 +const extractViewSQL = `SELECT + 'CREATE OR REPLACE VIEW ' || quote_ident(n.nspname) || '.' || quote_ident(c.relname) || ' AS ' + || pg_get_viewdef(c.oid, true) +FROM pg_class c +JOIN pg_namespace n ON n.oid = c.relnamespace +WHERE n.nspname = $1 + AND c.relkind = 'v' + AND c.relname = $2` + +// extractView 按 design §6.2.2 抽取单个视图的 DDL。 +// +// 返回值约定与 extractTable 对齐(design §6.5): +// +// - 视图不存在 → 占位 ObjectDDL="" 不报错; +// - 其他错误 → 透传原错误,保留 SQLSTATE。 +// +// schemaName / viewName 不在 Go 侧做任何 escape——全部走 database/sql 参数化 +// 绑定 + 服务端 quote_ident(design §6.3)。 +func (e *Extractor) extractView( + ctx context.Context, + schemaName, viewName string, +) (*driverV2.DatabaseObjectDDL, error) { + objectMeta := &driverV2.DatabaseObject{ + ObjectName: viewName, + ObjectType: driverV2.ObjectType_VIEW, + } + + var ddl string + err := e.db.QueryRowContext(ctx, extractViewSQL, schemaName, viewName).Scan(&ddl) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return &driverV2.DatabaseObjectDDL{ + DatabaseObject: objectMeta, + ObjectDDL: "", + }, nil + } + return nil, err + } + + return &driverV2.DatabaseObjectDDL{ + DatabaseObject: objectMeta, + ObjectDDL: ddl, + }, nil +}