From 6fbf875645d71075d95cf959267270118a79f3f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=90=E4=BA=91=E9=87=91YunjinXu?= Date: Mon, 16 Oct 2023 10:48:28 +0800 Subject: [PATCH 1/6] import gorm.io/driver/sqlserver@v1.1.2 --- go.mod | 4 ++++ go.sum | 15 +++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/go.mod b/go.mod index e1753cee..fcb8f7c7 100644 --- a/go.mod +++ b/go.mod @@ -30,6 +30,7 @@ require ( gopkg.in/yaml.v3 v3.0.1 gorm.io/driver/mysql v1.0.3 gorm.io/driver/postgres v1.2.1 + gorm.io/driver/sqlserver v1.1.2 gorm.io/gorm v1.22.2 ) @@ -46,6 +47,7 @@ require ( github.com/coreos/go-systemd/v22 v22.3.2 // indirect github.com/dapr/dapr v1.10.9 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/denisenkom/go-mssqldb v0.12.3 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/emicklei/go-restful/v3 v3.10.2 // indirect github.com/fatih/color v1.14.1 // indirect @@ -66,6 +68,8 @@ require ( github.com/go-playground/validator/v10 v10.14.0 // indirect github.com/goccy/go-json v0.10.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe // indirect + github.com/golang-sql/sqlexp v0.1.0 // indirect github.com/golang/mock v1.6.0 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/golang/snappy v0.0.4 // indirect diff --git a/go.sum b/go.sum index 0db03dcc..fc6da8bb 100644 --- a/go.sum +++ b/go.sum @@ -415,11 +415,14 @@ github.com/Azure/azure-sdk-for-go v37.1.0+incompatible/go.mod h1:9XXNKU+eRnpl9mo github.com/Azure/azure-sdk-for-go v51.1.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/azure-sdk-for-go v56.3.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/azure-sdk-for-go v65.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= +github.com/Azure/azure-sdk-for-go/sdk/azcore v0.19.0/go.mod h1:h6H6c8enJmmocHUbLiiGY6sx7f9i+X3m1CHdd5c6Rdw= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.0.0/go.mod h1:uGG2W01BaETf0Ozp+QxxKJdMBNRWPdstHG0Fmdwn1/U= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.1.0/go.mod h1:uGG2W01BaETf0Ozp+QxxKJdMBNRWPdstHG0Fmdwn1/U= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.11.0/go.mod h1:HcM1YX14R7CJcghJGOYCgdezslRSVzqwLf/q+4Y2r/0= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.0.0/go.mod h1:+6sju8gk8FRmSajX3Oz4G5Gm7P+mbqE9FVaXXFYTkCM= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.1.0/go.mod h1:bhXu1AjYL+wutSL/kpSq6s7733q2Rb0yuot9Zgfqa/0= github.com/Azure/azure-sdk-for-go/sdk/data/aztables v1.0.1/go.mod h1:l3wvZkG9oW07GLBW5Cd0WwG5asOfJ8aqE8raUvNzLpk= +github.com/Azure/azure-sdk-for-go/sdk/internal v0.7.0/go.mod h1:yqy467j36fJxcRV2TzfVZ1pCb5vxm4BtZPUdYWe/Xo8= github.com/Azure/azure-sdk-for-go/sdk/internal v1.0.0/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w= github.com/Azure/azure-sdk-for-go/sdk/keyvault/azsecrets v0.7.1/go.mod h1:WcC2Tk6JyRlqjn2byvinNnZzgdXmZ1tOiIOWNh1u0uA= github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal v0.5.0/go.mod h1:9V2j0jn9jDEkCkv8w/bKTNppX/d0FVA1ud77xCIP4KA= @@ -902,6 +905,9 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/deepmap/oapi-codegen v1.3.6/go.mod h1:aBozjEveG+33xPiP55Iw/XbVkhtZHEGLq3nxlX0+hfU= github.com/deepmap/oapi-codegen v1.8.1/go.mod h1:YLgSKSDv/bZQB7N4ws6luhozi3cEdRktEqrX88CvjIw= github.com/denisenkom/go-mssqldb v0.0.0-20210411162248-d9abbec934ba/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= +github.com/denisenkom/go-mssqldb v0.11.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= +github.com/denisenkom/go-mssqldb v0.12.3 h1:pBSGx9Tq67pBOTLmxNuirNTeB8Vjmf886Kx+8Y+8shw= +github.com/denisenkom/go-mssqldb v0.12.3/go.mod h1:k0mtMFOnU+AihqFxPMiF05rtiDrorD1Vrm1KEz5hxDo= github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba/go.mod h1:dV8lFg6daOBZbT6/BDGIz6Y3WFGn8juu6G+CQ6LHtl0= github.com/devigned/tab v0.1.1/go.mod h1:XG9mPq0dFghrYvoBF3xdRrJzSTX1b7IQrvaL9mzjeJY= github.com/dghubble/go-twitter v0.0.0-20190719072343-39e5462e111f/go.mod h1:xfg4uS5LEzOj8PgZV7SQYRHbG7jPUnelEiaAVJxmhJE= @@ -923,6 +929,7 @@ github.com/dimfeld/httptreemux v5.0.1+incompatible/go.mod h1:rbUlSV+CCpv/SuqUTP/ github.com/distribution/distribution/v3 v3.0.0-20211118083504-a29a3c99a684/go.mod h1:UfCu3YXJJCI+IdnqGgYP82dk2+Joxmv+mUTVBES6wac= github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko= +github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= github.com/docker/cli v0.0.0-20191017083524-a8ff7f821017/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/cli v20.10.11+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/distribution v0.0.0-20190905152932-14b96e55d84c/go.mod h1:0+TTO4EOBfRPhZXAeF1Vu+W3hHZ8eLp8PgKVZlcvtFY= @@ -1227,7 +1234,10 @@ github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzw github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-jwt/jwt/v4 v4.4.3/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +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-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= +github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= @@ -1999,6 +2009,7 @@ github.com/pierrec/lz4 v2.5.2+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pierrec/lz4/v4 v4.1.14/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA= github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4/go.mod h1:N6UoU20jOqggOuDwUaBQpluzLNDqif3kq9z2wpdYEfQ= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -2562,6 +2573,7 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20201216223049-8b5274cf687f/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= @@ -2698,6 +2710,7 @@ golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= @@ -3454,6 +3467,8 @@ gorm.io/driver/mysql v1.0.3 h1:+JKBYPfn1tygR1/of/Fh2T8iwuVwzt+PEJmKaXzMQXg= gorm.io/driver/mysql v1.0.3/go.mod h1:twGxftLBlFgNVNakL7F+P/x9oYqoymG3YYT8cAfI9oI= gorm.io/driver/postgres v1.2.1 h1:JDQKnF7MC51dgL09Vbydc5kl83KkVDlcXfSPJ+xhh68= gorm.io/driver/postgres v1.2.1/go.mod h1:SHRZhu+D0tLOHV5qbxZRUM6kBcf3jp/kxPz2mYMTsNY= +gorm.io/driver/sqlserver v1.1.2 h1:MmOAvxnfqGMYS/I9jMwrMlc1+62S0clG3RUEvfEkTVo= +gorm.io/driver/sqlserver v1.1.2/go.mod h1:mEmVwRbwsgY+EmU7MWypjefdbX5uFgbE07kklGvLmcg= gorm.io/gorm v1.20.4/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= gorm.io/gorm v1.22.0/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0= gorm.io/gorm v1.22.2 h1:1iKcvyJnR5bHydBhDqTwasOkoo6+o4Ms5cknSt6qP7I= From 442b16acd942e2f5698dd213743bd39bfd7fa7d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=90=E4=BA=91=E9=87=91YunjinXu?= Date: Mon, 16 Oct 2023 10:50:30 +0800 Subject: [PATCH 2/6] add sqlserver init script file --- sqls/dtmsvr.storage.sqlserver.sql | 60 +++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 sqls/dtmsvr.storage.sqlserver.sql diff --git a/sqls/dtmsvr.storage.sqlserver.sql b/sqls/dtmsvr.storage.sqlserver.sql new file mode 100644 index 00000000..5a4678f1 --- /dev/null +++ b/sqls/dtmsvr.storage.sqlserver.sql @@ -0,0 +1,60 @@ +if db_id('dtm') is null +begin + CREATE DATABASE dtm +end; + +drop table IF EXISTS dtm.dbo.trans_global; +CREATE TABLE dtm.dbo.trans_global ( + id bigint NOT NULL IDENTITY, + gid varchar(128) NOT NULL , -- COMMENT 'global transaction id', + trans_type varchar(45) not null , -- COMMENT 'transaction type: saga | xa | tcc | msg', + status varchar(12) NOT NULL , -- COMMENT 'transaction status: prepared | submitted | aborting | succeed | failed', + query_prepared varchar(1024) NOT NULL , -- COMMENT 'url to check for msg|workflow', + protocol varchar(45) not null , -- COMMENT 'protocol: http | grpc | json-rpc', + create_time datetimeoffset DEFAULT NULL, + update_time datetimeoffset DEFAULT NULL, + finish_time datetimeoffset DEFAULT NULL, + rollback_time datetimeoffset DEFAULT NULL, + options varchar(1024) DEFAULT '' , -- COMMENT 'options for transaction like: TimeoutToFail, RequestTimeout', + custom_data varchar(1024) DEFAULT '' , -- COMMENT 'custom data for transaction', + next_cron_interval int default null , -- COMMENT 'next cron interval. for use of cron job', + next_cron_time datetimeoffset default null , -- COMMENT 'next time to process this trans. for use of cron job', + owner varchar(128) not null default '' , -- COMMENT 'who is locking this trans', + ext_data VARCHAR(max) , -- COMMENT 'extra data for this trans. currently used in workflow pattern', + result varchar(1024) DEFAULT '' , -- COMMENT 'result for transaction', + rollback_reason varchar(1024) DEFAULT '' , -- COMMENT 'rollback reason for transaction', + PRIMARY KEY (id), + CONSTRAINT gid UNIQUE (gid) WITH(IGNORE_DUP_KEY = ON) +); +CREATE INDEX[owner] ON [dtm].[dbo].[trans_global]([owner] ASC) +CREATE INDEX[status_next_cron_time] ON [dtm].[dbo].[trans_global]([status] ASC, next_cron_time ASC) ---- COMMENT 'cron job will use this index to query trans' + +drop table IF EXISTS dtm.dbo.trans_branch_op; +CREATE TABLE dtm.dbo.trans_branch_op ( + id bigint NOT NULL IDENTITY, + gid varchar(128) NOT NULL , -- COMMENT 'global transaction id', + url varchar(1024) NOT NULL , -- COMMENT 'the url of this op', + data VARCHAR(max) , -- COMMENT 'request body, depreceated', + bin_data VARBINARY(max) , -- COMMENT 'request body', + branch_id VARCHAR(128) NOT NULL , -- COMMENT 'transaction branch ID', + op varchar(45) NOT NULL , -- COMMENT 'transaction operation type like: action | compensate | try | confirm | cancel', + status varchar(45) NOT NULL , -- COMMENT 'transaction op status: prepared | succeed | failed', + finish_time datetimeoffset DEFAULT NULL, + rollback_time datetimeoffset DEFAULT NULL, + create_time datetimeoffset DEFAULT NULL, + update_time datetimeoffset DEFAULT NULL, + PRIMARY KEY (id), + CONSTRAINT gid_uniq UNIQUE (gid, branch_id, op) +); +drop table IF EXISTS dtm.dbo.kv; +CREATE TABLE dtm.dbo.kv ( + id bigint NOT NULL IDENTITY, + cat varchar(45) NOT NULL , -- COMMENT 'the category of this data', + k varchar(128) NOT NULL, + v VARCHAR(max), + version bigint default 1 , -- COMMENT 'version of the value', + create_time datetimeoffset default NULL, + update_time datetimeoffset DEFAULT NULL, + PRIMARY KEY (id), + CONSTRAINT uniq_k UNIQUE (cat, k) +); From 07b3d70d4f192c18c3191f9d7281a49306bf9ba4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=90=E4=BA=91=E9=87=91YunjinXu?= Date: Mon, 16 Oct 2023 10:53:06 +0800 Subject: [PATCH 3/6] adapter helper and github action --- .github/workflows/tests.yml | 10 ++++++++++ helper/compose.store.yml | 9 +++++++++ helper/test-cover.sh | 2 +- 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e25ebc81..5b181e84 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -45,6 +45,16 @@ jobs: - /etc/timezone:/etc/timezone:ro ports: - 27017:27017 + sqlserver: + image: mcr.microsoft.com/mssql/server:2019-latest + volumes: + - /etc/localtime:/etc/localtime:ro + - /etc/timezone:/etc/timezone:ro + env: + ACCEPT_EULA: Y + MSSQL_SA_PASSWORD: p@ssw0rd + ports: + - '1433:1433' steps: - name: Set up Go 1.19 uses: actions/setup-go@v2 diff --git a/helper/compose.store.yml b/helper/compose.store.yml index 83e929fe..206ef09b 100644 --- a/helper/compose.store.yml +++ b/helper/compose.store.yml @@ -36,3 +36,12 @@ services: - /etc/localtime:/etc/localtime:ro ports: - '27017:27017' + sqlserver2019: + image: mcr.microsoft.com/mssql/server:2019-latest + volumes: + - /etc/localtime:/etc/localtime:ro + ports: + - 1433:1433 + environment: + - ACCEPT_EULA=Y + - MSSQL_SA_PASSWORD=p@ssw0rd diff --git a/helper/test-cover.sh b/helper/test-cover.sh index c5860cad..786a1fed 100755 --- a/helper/test-cover.sh +++ b/helper/test-cover.sh @@ -1,7 +1,7 @@ set -x export DTM_DEBUG=1 echo "mode: count" > coverage.txt -for store in redis boltdb mysql postgres; do +for store in redis boltdb mysql postgres sqlserver; do TEST_STORE=$store go test -failfast -covermode count -coverprofile=profile.out -coverpkg=github.com/dtm-labs/dtm/client/dtmcli,github.com/dtm-labs/dtm/client/dtmcli/dtmimp,github.com/dtm-labs/logger,github.com/dtm-labs/dtm/client/dtmgrpc,github.com/dtm-labs/dtm/client/workflow,github.com/dtm-labs/dtm/client/dtmgrpc/dtmgimp,github.com/dtm-labs/dtm/dtmsvr,dtmsvr/config,github.com/dtm-labs/dtm/dtmsvr/storage,github.com/dtm-labs/dtm/dtmsvr/storage/boltdb,github.com/dtm-labs/dtm/dtmsvr/storage/redis,github.com/dtm-labs/dtm/dtmsvr/storage/registry,github.com/dtm-labs/dtm/dtmsvr/storage/sql,github.com/dtm-labs/dtm/dtmutil -gcflags=-l ./... || exit 1 echo "TEST_STORE=$store finished" if [ -f profile.out ]; then From b210ad7b8ce052a03aeadb77c4b3df0f92729bc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=90=E4=BA=91=E9=87=91YunjinXu?= Date: Mon, 16 Oct 2023 10:51:54 +0800 Subject: [PATCH 4/6] implement sqlserver storage using gorm.io/driver/sqlserver --- client/dtmcli/consts.go | 2 ++ client/dtmcli/dtmimp/consts.go | 2 ++ client/dtmcli/dtmimp/db_special.go | 21 +++++++++++++++++++ client/dtmcli/dtmimp/utils.go | 15 ++++++++++++++ dtmsvr/config/config.go | 4 +++- dtmsvr/storage/registry/registry.go | 5 +++-- dtmsvr/storage/sql/sql.go | 31 +++++++++++++++++++++-------- dtmutil/db.go | 6 ++++++ test/main_test.go | 4 ++++ 9 files changed, 79 insertions(+), 11 deletions(-) diff --git a/client/dtmcli/consts.go b/client/dtmcli/consts.go index 7ae4fc63..bb87a15e 100644 --- a/client/dtmcli/consts.go +++ b/client/dtmcli/consts.go @@ -35,6 +35,8 @@ const ( DBTypeMysql = dtmimp.DBTypeMysql // DBTypePostgres const for driver postgres DBTypePostgres = dtmimp.DBTypePostgres + // DBTypeSqlServer const for driver SqlServer + DBTypeSqlServer = dtmimp.DBTypeSqlServer ) // MapSuccess HTTP result of SUCCESS diff --git a/client/dtmcli/dtmimp/consts.go b/client/dtmcli/dtmimp/consts.go index 6f4e6cd3..036e6dc3 100644 --- a/client/dtmcli/dtmimp/consts.go +++ b/client/dtmcli/dtmimp/consts.go @@ -36,6 +36,8 @@ const ( DBTypeMysql = "mysql" // DBTypePostgres const for driver postgres DBTypePostgres = "postgres" + // DBTypeSqlServer const for driver SqlServer + DBTypeSqlServer = "sqlserver" // DBTypeRedis const for driver redis DBTypeRedis = "redis" // Jrpc const for json-rpc diff --git a/client/dtmcli/dtmimp/db_special.go b/client/dtmcli/dtmimp/db_special.go index d9128b15..2f0c6d41 100644 --- a/client/dtmcli/dtmimp/db_special.go +++ b/client/dtmcli/dtmimp/db_special.go @@ -78,6 +78,27 @@ func init() { dbSpecials[DBTypePostgres] = &postgresDBSpecial{} } +// TODO sqlserver implement (for go client only, not for dtm server) +type sqlserverDBSpecial struct{} + +func (*sqlserverDBSpecial) GetPlaceHoldSQL(sql string) string { + // TODO sqlserver implement + return sql +} + +func (*sqlserverDBSpecial) GetInsertIgnoreTemplate(tableAndValues string, pgConstraint string) string { + // TODO sqlserver implement + return "" +} + +func (*sqlserverDBSpecial) GetXaSQL(command string, xid string) string { + // TODO sqlserver implement + return "" +} +func init() { + dbSpecials[DBTypeSqlServer] = &sqlserverDBSpecial{} +} + // GetDBSpecial get DBSpecial for currentDBType func GetDBSpecial(dbType string) DBSpecial { if dbType == "" { diff --git a/client/dtmcli/dtmimp/utils.go b/client/dtmcli/dtmimp/utils.go index b0a6a8a5..223ed39d 100644 --- a/client/dtmcli/dtmimp/utils.go +++ b/client/dtmcli/dtmimp/utils.go @@ -228,11 +228,26 @@ func GetDsn(conf DBConf) string { conf.User, conf.Password, host, conf.Port, conf.Db), "postgres": fmt.Sprintf("host=%s user=%s password=%s dbname='%s' search_path=%s port=%d sslmode=disable", host, conf.User, conf.Password, conf.Db, conf.Schema, conf.Port), + // sqlserver://sa:mypass@localhost:1234?database=master&connection+timeout=30 + "sqlserver": getSqlServerConnectionString(&conf, &host), }[driver] PanicIf(dsn == "", fmt.Errorf("unknow driver: %s", driver)) return dsn } +func getSqlServerConnectionString(conf *DBConf, host *string) string { + query := url.Values{} + query.Add("database", conf.Db) + u := &url.URL{ + Scheme: "sqlserver", + User: url.UserPassword(conf.User, conf.Password), + Host: fmt.Sprintf("%s:%d", *host, conf.Port), + // Path: instance, // if connecting to an instance instead of a port + RawQuery: query.Encode(), + } + return u.String() +} + // RespAsErrorByJSONRPC translate json rpc resty response to error func RespAsErrorByJSONRPC(resp *resty.Response) error { str := resp.String() diff --git a/dtmsvr/config/config.go b/dtmsvr/config/config.go index 2021db4f..e9ba1e6f 100644 --- a/dtmsvr/config/config.go +++ b/dtmsvr/config/config.go @@ -20,6 +20,8 @@ const ( BoltDb = "boltdb" // Postgres is postgres driver Postgres = "postgres" + // SqlServer is SQL Server driver + SqlServer = "sqlserver" ) // MicroService config type for microservice based grpc @@ -65,7 +67,7 @@ type Store struct { // IsDB checks config driver is mysql or postgres func (s *Store) IsDB() bool { - return s.Driver == dtmcli.DBTypeMysql || s.Driver == dtmcli.DBTypePostgres + return s.Driver == dtmcli.DBTypeMysql || s.Driver == dtmcli.DBTypePostgres || s.Driver == dtmcli.DBTypeSqlServer } // GetDBConf returns db conf info diff --git a/dtmsvr/storage/registry/registry.go b/dtmsvr/storage/registry/registry.go index 297c8751..469d20d1 100644 --- a/dtmsvr/storage/registry/registry.go +++ b/dtmsvr/storage/registry/registry.go @@ -37,8 +37,9 @@ var storeFactorys = map[string]StorageFactory{ return &redis.Store{} }, }, - "mysql": sqlFac, - "postgres": sqlFac, + "mysql": sqlFac, + "postgres": sqlFac, + "sqlserver": sqlFac, } // GetStore returns storage.Store diff --git a/dtmsvr/storage/sql/sql.go b/dtmsvr/storage/sql/sql.go index 8e444bc5..8ae53a48 100644 --- a/dtmsvr/storage/sql/sql.go +++ b/dtmsvr/storage/sql/sql.go @@ -69,10 +69,10 @@ func (s *Store) ScanTransGlobalStores(position *string, limit int64, condition s query = query.Where("trans_type = ?", condition.TransType) } if !condition.CreateTimeStart.IsZero() { - query = query.Where("create_time >= ?", condition.CreateTimeStart.Format("2006-01-02 15:04:05")) + query = query.Where("create_time >= ?", condition.CreateTimeStart) } if !condition.CreateTimeEnd.IsZero() { - query = query.Where("create_time <= ?", condition.CreateTimeEnd.Format("2006-01-02 15:04:05")) + query = query.Where("create_time <= ?", condition.CreateTimeEnd) } dbr := query.Order("id desc").Limit(int(limit)).Find(&globals) @@ -105,7 +105,13 @@ func (s *Store) UpdateBranches(branches []storage.TransBranchStore, updates []st func (s *Store) LockGlobalSaveBranches(gid string, status string, branches []storage.TransBranchStore, branchStart int) { err := dbGet().Transaction(func(tx *gorm.DB) error { g := &storage.TransGlobalStore{} - dbr := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Model(g).Where("gid=? and status=?", gid, status).First(g) + var dbr *gorm.DB + // sqlserver sql should be: SELECT * FROM "trans_global" with(RowLock,UpdLock) ,but gorm generates "FOR UPDATE" at the back, raw sql instead. + if conf.Store.Driver == config.SqlServer { + dbr = tx.Raw("SELECT * FROM trans_global with(RowLock,UpdLock) WHERE gid=? and status=? ORDER BY id OFFSET 0 ROW FETCH NEXT 1 ROWS ONLY ", gid, status).First(g) + } else { + dbr = tx.Clauses(clause.Locking{Strength: "UPDATE"}).Model(g).Where("gid=? and status=?", gid, status).First(g) + } if dbr.Error == nil { if branchStart == -1 { dbr = tx.Create(branches) @@ -164,11 +170,16 @@ func (s *Store) LockOneGlobalTrans(expireIn time.Duration) *storage.TransGlobalS where := fmt.Sprintf(`next_cron_time < '%s' and status in ('prepared', 'aborting', 'submitted')`, nextCronTime) order := map[string]string{ - dtmimp.DBTypeMysql: `order by rand()`, - dtmimp.DBTypePostgres: `order by random()`, + dtmimp.DBTypeMysql: `order by rand()`, + dtmimp.DBTypePostgres: `order by random()`, + dtmimp.DBTypeSqlServer: `order by rand()`, }[conf.Store.Driver] - ssql := fmt.Sprintf(`select id from trans_global where %s %s limit 1`, where, order) + ssql := map[string]string{ + dtmimp.DBTypeMysql: fmt.Sprintf(`select id from trans_global where %s %s limit 1`, where, order), + dtmimp.DBTypePostgres: fmt.Sprintf(`select id from trans_global where %s %s limit 1`, where, order), + dtmimp.DBTypeSqlServer: fmt.Sprintf(`select top 1 id from trans_global where %s %s`, where, order), + }[conf.Store.Driver] var id int64 err := db.ToSQLDB().QueryRow(ssql).Scan(&id) if errors.Is(err, sql.ErrNoRows) { @@ -198,8 +209,9 @@ func (s *Store) LockOneGlobalTrans(expireIn time.Duration) *storage.TransGlobalS func (s *Store) ResetCronTime(after time.Duration, limit int64) (succeedCount int64, hasRemaining bool, err error) { nextCronTime := getTimeStr(int64(after / time.Second)) where := map[string]string{ - dtmimp.DBTypeMysql: fmt.Sprintf(`next_cron_time > '%s' and status in ('prepared', 'aborting', 'submitted') limit %d`, nextCronTime, limit), - dtmimp.DBTypePostgres: fmt.Sprintf(`id in (select id from trans_global where next_cron_time > '%s' and status in ('prepared', 'aborting', 'submitted') limit %d )`, nextCronTime, limit), + dtmimp.DBTypeMysql: fmt.Sprintf(`next_cron_time > '%s' and status in ('prepared', 'aborting', 'submitted') limit %d`, nextCronTime, limit), + dtmimp.DBTypePostgres: fmt.Sprintf(`id in (select id from trans_global where next_cron_time > '%s' and status in ('prepared', 'aborting', 'submitted') limit %d )`, nextCronTime, limit), + dtmimp.DBTypeSqlServer: fmt.Sprintf(`id in (select top %d id from trans_global where next_cron_time > '%s' and status in ('prepared', 'aborting', 'submitted') )`, limit, nextCronTime), }[conf.Store.Driver] sql := fmt.Sprintf(`UPDATE trans_global SET update_time='%s',next_cron_time='%s' WHERE %s`, @@ -317,5 +329,8 @@ func wrapError(err error) error { } func getTimeStr(afterSecond int64) string { + if conf.Store.Driver == config.SqlServer { + return dtmutil.GetNextTime(afterSecond).Format(time.RFC3339) + } return dtmutil.GetNextTime(afterSecond).Format("2006-01-02 15:04:05") } diff --git a/dtmutil/db.go b/dtmutil/db.go index 7e8423e4..4237ccff 100644 --- a/dtmutil/db.go +++ b/dtmutil/db.go @@ -11,8 +11,11 @@ import ( "github.com/dtm-labs/logger" _ "github.com/go-sql-driver/mysql" // register mysql driver _ "github.com/lib/pq" // register postgres driver + + // _ "github.com/microsoft/go-mssqldb" // Microsoft's package conflicts with gorm's package: panic: sql: Register called twice for driver mssql "gorm.io/driver/mysql" "gorm.io/driver/postgres" + "gorm.io/driver/sqlserver" // register sqlserver driver, "gorm.io/gorm" ) @@ -27,6 +30,9 @@ func getGormDialetor(driver string, dsn string) gorm.Dialector { if driver == dtmcli.DBTypePostgres { return postgres.Open(dsn) } + if driver == dtmcli.DBTypeSqlServer { + return sqlserver.Open(dsn) + } dtmimp.PanicIf(driver != dtmcli.DBTypeMysql, fmt.Errorf("unknown driver: %s", driver)) return mysql.Open(dsn) } diff --git a/test/main_test.go b/test/main_test.go index b91e9dd8..5b2c1cec 100644 --- a/test/main_test.go +++ b/test/main_test.go @@ -53,6 +53,10 @@ func TestMain(m *testing.M) { conf.Store.User = "" conf.Store.Password = "" conf.Store.Port = 6379 + } else if tenv == config.SqlServer { + conf.Store.User = "sa" + conf.Store.Password = "p@ssw0rd" + conf.Store.Port = 1433 } conf.Store.Db = "" registry.WaitStoreUp() From 17b3b1256b6535ce62aace256983eece9c134723 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=90=E4=BA=91=E9=87=91YunjinXu?= Date: Sun, 29 Oct 2023 20:59:42 +0800 Subject: [PATCH 5/6] SqlServer->SQLServer, combine select+order by --- client/dtmcli/consts.go | 4 ++-- client/dtmcli/dtmimp/consts.go | 4 ++-- client/dtmcli/dtmimp/db_special.go | 2 +- client/dtmcli/dtmimp/utils.go | 4 ++-- dtmsvr/config/config.go | 6 +++--- dtmsvr/storage/sql/sql.go | 18 ++++++------------ dtmutil/db.go | 2 +- test/main_test.go | 2 +- 8 files changed, 18 insertions(+), 24 deletions(-) diff --git a/client/dtmcli/consts.go b/client/dtmcli/consts.go index bb87a15e..a0aab2c1 100644 --- a/client/dtmcli/consts.go +++ b/client/dtmcli/consts.go @@ -35,8 +35,8 @@ const ( DBTypeMysql = dtmimp.DBTypeMysql // DBTypePostgres const for driver postgres DBTypePostgres = dtmimp.DBTypePostgres - // DBTypeSqlServer const for driver SqlServer - DBTypeSqlServer = dtmimp.DBTypeSqlServer + // DBTypeSQLServer const for driver SQLServer + DBTypeSQLServer = dtmimp.DBTypeSQLServer ) // MapSuccess HTTP result of SUCCESS diff --git a/client/dtmcli/dtmimp/consts.go b/client/dtmcli/dtmimp/consts.go index 036e6dc3..362e8a94 100644 --- a/client/dtmcli/dtmimp/consts.go +++ b/client/dtmcli/dtmimp/consts.go @@ -36,8 +36,8 @@ const ( DBTypeMysql = "mysql" // DBTypePostgres const for driver postgres DBTypePostgres = "postgres" - // DBTypeSqlServer const for driver SqlServer - DBTypeSqlServer = "sqlserver" + // DBTypeSQLServer const for driver SQLServer + DBTypeSQLServer = "sqlserver" // DBTypeRedis const for driver redis DBTypeRedis = "redis" // Jrpc const for json-rpc diff --git a/client/dtmcli/dtmimp/db_special.go b/client/dtmcli/dtmimp/db_special.go index 2f0c6d41..5a2bb837 100644 --- a/client/dtmcli/dtmimp/db_special.go +++ b/client/dtmcli/dtmimp/db_special.go @@ -96,7 +96,7 @@ func (*sqlserverDBSpecial) GetXaSQL(command string, xid string) string { return "" } func init() { - dbSpecials[DBTypeSqlServer] = &sqlserverDBSpecial{} + dbSpecials[DBTypeSQLServer] = &sqlserverDBSpecial{} } // GetDBSpecial get DBSpecial for currentDBType diff --git a/client/dtmcli/dtmimp/utils.go b/client/dtmcli/dtmimp/utils.go index 223ed39d..e62234fe 100644 --- a/client/dtmcli/dtmimp/utils.go +++ b/client/dtmcli/dtmimp/utils.go @@ -229,13 +229,13 @@ func GetDsn(conf DBConf) string { "postgres": fmt.Sprintf("host=%s user=%s password=%s dbname='%s' search_path=%s port=%d sslmode=disable", host, conf.User, conf.Password, conf.Db, conf.Schema, conf.Port), // sqlserver://sa:mypass@localhost:1234?database=master&connection+timeout=30 - "sqlserver": getSqlServerConnectionString(&conf, &host), + "sqlserver": getSQLServerConnectionString(&conf, &host), }[driver] PanicIf(dsn == "", fmt.Errorf("unknow driver: %s", driver)) return dsn } -func getSqlServerConnectionString(conf *DBConf, host *string) string { +func getSQLServerConnectionString(conf *DBConf, host *string) string { query := url.Values{} query.Add("database", conf.Db) u := &url.URL{ diff --git a/dtmsvr/config/config.go b/dtmsvr/config/config.go index e9ba1e6f..30f01150 100644 --- a/dtmsvr/config/config.go +++ b/dtmsvr/config/config.go @@ -20,8 +20,8 @@ const ( BoltDb = "boltdb" // Postgres is postgres driver Postgres = "postgres" - // SqlServer is SQL Server driver - SqlServer = "sqlserver" + // SQLServer is SQL Server driver + SQLServer = "sqlserver" ) // MicroService config type for microservice based grpc @@ -67,7 +67,7 @@ type Store struct { // IsDB checks config driver is mysql or postgres func (s *Store) IsDB() bool { - return s.Driver == dtmcli.DBTypeMysql || s.Driver == dtmcli.DBTypePostgres || s.Driver == dtmcli.DBTypeSqlServer + return s.Driver == dtmcli.DBTypeMysql || s.Driver == dtmcli.DBTypePostgres || s.Driver == dtmcli.DBTypeSQLServer } // GetDBConf returns db conf info diff --git a/dtmsvr/storage/sql/sql.go b/dtmsvr/storage/sql/sql.go index 8ae53a48..569c966e 100644 --- a/dtmsvr/storage/sql/sql.go +++ b/dtmsvr/storage/sql/sql.go @@ -107,7 +107,7 @@ func (s *Store) LockGlobalSaveBranches(gid string, status string, branches []sto g := &storage.TransGlobalStore{} var dbr *gorm.DB // sqlserver sql should be: SELECT * FROM "trans_global" with(RowLock,UpdLock) ,but gorm generates "FOR UPDATE" at the back, raw sql instead. - if conf.Store.Driver == config.SqlServer { + if conf.Store.Driver == config.SQLServer { dbr = tx.Raw("SELECT * FROM trans_global with(RowLock,UpdLock) WHERE gid=? and status=? ORDER BY id OFFSET 0 ROW FETCH NEXT 1 ROWS ONLY ", gid, status).First(g) } else { dbr = tx.Clauses(clause.Locking{Strength: "UPDATE"}).Model(g).Where("gid=? and status=?", gid, status).First(g) @@ -169,16 +169,10 @@ func (s *Store) LockOneGlobalTrans(expireIn time.Duration) *storage.TransGlobalS nextCronTime := getTimeStr(int64(expireIn / time.Second)) where := fmt.Sprintf(`next_cron_time < '%s' and status in ('prepared', 'aborting', 'submitted')`, nextCronTime) - order := map[string]string{ - dtmimp.DBTypeMysql: `order by rand()`, - dtmimp.DBTypePostgres: `order by random()`, - dtmimp.DBTypeSqlServer: `order by rand()`, - }[conf.Store.Driver] - ssql := map[string]string{ - dtmimp.DBTypeMysql: fmt.Sprintf(`select id from trans_global where %s %s limit 1`, where, order), - dtmimp.DBTypePostgres: fmt.Sprintf(`select id from trans_global where %s %s limit 1`, where, order), - dtmimp.DBTypeSqlServer: fmt.Sprintf(`select top 1 id from trans_global where %s %s`, where, order), + dtmimp.DBTypeMysql: fmt.Sprintf(`select id from trans_global where %s order by rand() limit 1`, where), + dtmimp.DBTypePostgres: fmt.Sprintf(`select id from trans_global where %s order by random() limit 1`, where), + dtmimp.DBTypeSQLServer: fmt.Sprintf(`select top 1 id from trans_global where %s order by rand()`, where), }[conf.Store.Driver] var id int64 err := db.ToSQLDB().QueryRow(ssql).Scan(&id) @@ -211,7 +205,7 @@ func (s *Store) ResetCronTime(after time.Duration, limit int64) (succeedCount in where := map[string]string{ dtmimp.DBTypeMysql: fmt.Sprintf(`next_cron_time > '%s' and status in ('prepared', 'aborting', 'submitted') limit %d`, nextCronTime, limit), dtmimp.DBTypePostgres: fmt.Sprintf(`id in (select id from trans_global where next_cron_time > '%s' and status in ('prepared', 'aborting', 'submitted') limit %d )`, nextCronTime, limit), - dtmimp.DBTypeSqlServer: fmt.Sprintf(`id in (select top %d id from trans_global where next_cron_time > '%s' and status in ('prepared', 'aborting', 'submitted') )`, limit, nextCronTime), + dtmimp.DBTypeSQLServer: fmt.Sprintf(`id in (select top %d id from trans_global where next_cron_time > '%s' and status in ('prepared', 'aborting', 'submitted') )`, limit, nextCronTime), }[conf.Store.Driver] sql := fmt.Sprintf(`UPDATE trans_global SET update_time='%s',next_cron_time='%s' WHERE %s`, @@ -329,7 +323,7 @@ func wrapError(err error) error { } func getTimeStr(afterSecond int64) string { - if conf.Store.Driver == config.SqlServer { + if conf.Store.Driver == config.SQLServer { return dtmutil.GetNextTime(afterSecond).Format(time.RFC3339) } return dtmutil.GetNextTime(afterSecond).Format("2006-01-02 15:04:05") diff --git a/dtmutil/db.go b/dtmutil/db.go index 4237ccff..db09ab98 100644 --- a/dtmutil/db.go +++ b/dtmutil/db.go @@ -30,7 +30,7 @@ func getGormDialetor(driver string, dsn string) gorm.Dialector { if driver == dtmcli.DBTypePostgres { return postgres.Open(dsn) } - if driver == dtmcli.DBTypeSqlServer { + if driver == dtmcli.DBTypeSQLServer { return sqlserver.Open(dsn) } dtmimp.PanicIf(driver != dtmcli.DBTypeMysql, fmt.Errorf("unknown driver: %s", driver)) diff --git a/test/main_test.go b/test/main_test.go index 5b2c1cec..9568992c 100644 --- a/test/main_test.go +++ b/test/main_test.go @@ -53,7 +53,7 @@ func TestMain(m *testing.M) { conf.Store.User = "" conf.Store.Password = "" conf.Store.Port = 6379 - } else if tenv == config.SqlServer { + } else if tenv == config.SQLServer { conf.Store.User = "sa" conf.Store.Password = "p@ssw0rd" conf.Store.Port = 1433 From e233895e1c86909b9f368dd0342d260ae67e398a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=90=E4=BA=91=E9=87=91YunjinXu?= Date: Sun, 29 Oct 2023 21:14:42 +0800 Subject: [PATCH 6/6] fix sql server error: main_test.go dtmsvr.PopulateDB(false) invokes GetPlaceHoldSQL(sql) --- client/dtmcli/dtmimp/db_special.go | 21 --------------------- client/dtmcli/dtmimp/utils.go | 4 +++- 2 files changed, 3 insertions(+), 22 deletions(-) diff --git a/client/dtmcli/dtmimp/db_special.go b/client/dtmcli/dtmimp/db_special.go index 5a2bb837..d9128b15 100644 --- a/client/dtmcli/dtmimp/db_special.go +++ b/client/dtmcli/dtmimp/db_special.go @@ -78,27 +78,6 @@ func init() { dbSpecials[DBTypePostgres] = &postgresDBSpecial{} } -// TODO sqlserver implement (for go client only, not for dtm server) -type sqlserverDBSpecial struct{} - -func (*sqlserverDBSpecial) GetPlaceHoldSQL(sql string) string { - // TODO sqlserver implement - return sql -} - -func (*sqlserverDBSpecial) GetInsertIgnoreTemplate(tableAndValues string, pgConstraint string) string { - // TODO sqlserver implement - return "" -} - -func (*sqlserverDBSpecial) GetXaSQL(command string, xid string) string { - // TODO sqlserver implement - return "" -} -func init() { - dbSpecials[DBTypeSQLServer] = &sqlserverDBSpecial{} -} - // GetDBSpecial get DBSpecial for currentDBType func GetDBSpecial(dbType string) DBSpecial { if dbType == "" { diff --git a/client/dtmcli/dtmimp/utils.go b/client/dtmcli/dtmimp/utils.go index e62234fe..cc7b912f 100644 --- a/client/dtmcli/dtmimp/utils.go +++ b/client/dtmcli/dtmimp/utils.go @@ -207,7 +207,9 @@ func DBExec(dbType string, db DB, sql string, values ...interface{}) (affected i return 0, nil } began := time.Now() - sql = GetDBSpecial(dbType).GetPlaceHoldSQL(sql) + if len(values) > 0 { + sql = GetDBSpecial(dbType).GetPlaceHoldSQL(sql) + } r, rerr := db.Exec(sql, values...) used := time.Since(began) / time.Millisecond if rerr == nil {