From 968104c4692c72fc8220c53702a561b376bfcc65 Mon Sep 17 00:00:00 2001 From: Simone Gotti Date: Thu, 21 Feb 2019 16:05:54 +0100 Subject: [PATCH] Add database helper --- go.mod | 10 ++ go.sum | 25 +++++ internal/db/create.go | 72 ++++++++++++ internal/db/db.go | 253 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 360 insertions(+) create mode 100644 internal/db/create.go create mode 100644 internal/db/db.go diff --git a/go.mod b/go.mod index e4b29f3..0377a0a 100644 --- a/go.mod +++ b/go.mod @@ -1,14 +1,24 @@ module github.com/sorintlab/agola require ( + github.com/Masterminds/squirrel v1.1.0 github.com/go-ini/ini v1.42.0 // indirect + github.com/go-sql-driver/mysql v1.4.1 // indirect + github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e // indirect + github.com/jtolds/gls v4.2.1+incompatible // indirect + github.com/lib/pq v1.0.0 // indirect + github.com/mattn/go-sqlite3 v1.10.0 github.com/minio/minio-go v6.0.14+incompatible github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/pkg/errors v0.8.0 + github.com/smartystreets/assertions v0.0.0-20190215210624-980c5ac6f3ac // indirect + github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c // indirect go.etcd.io/etcd v0.0.0-20181128220305-dedae6eb7c25 go.uber.org/zap v1.9.1 golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 // indirect golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e // indirect golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 // indirect + google.golang.org/appengine v1.4.0 // indirect + gopkg.in/ini.v1 v1.42.0 // indirect gopkg.in/yaml.v2 v2.2.2 // indirect ) diff --git a/go.sum b/go.sum index 6aac979..d67a1e7 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/Masterminds/squirrel v1.1.0 h1:baP1qLdoQCeTw3ifCdOq2dkYc6vGcmRdaociKLbEJXs= +github.com/Masterminds/squirrel v1.1.0/go.mod h1:yaPeOnPG5ZRwL9oKdTsO/prlkPbXWZlRVMQ/gGlzIuA= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY= @@ -22,6 +24,8 @@ github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-ini/ini v1.42.0 h1:TWr1wGj35+UiWHlBA8er89seFXxzwFn11spilrrj+38= github.com/go-ini/ini v1.42.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= +github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA= +github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/gogo/protobuf v1.0.0 h1:2jyBKDKU/8v3v2xVR2PtiWQviFUyiaGk2rpfyFT8rTM= github.com/gogo/protobuf v1.0.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= @@ -34,6 +38,8 @@ github.com/google/btree v0.0.0-20180124185431-e89373fe6b4a h1:ZJu5NB1Bk5ms4vw0Xu github.com/google/btree v0.0.0-20180124185431-e89373fe6b4a/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/uuid v1.0.0 h1:b4Gk+7WdP/d3HZH8EJsZpvV7EtDOgaZLtnaNGIu1adA= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e h1:JKmoR8x90Iww1ks85zJ1lfDGgIiMDuIptTOhJq+zKyg= +github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c h1:Lh2aW+HnU2Nbe1gqD9SOJLJxW1jBMmQOktN2acDyJk8= github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0 h1:Iju5GlWwrvL6UBg4zJJt3btmonfrMlCDdsejg4CZE7c= @@ -48,13 +54,23 @@ github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NH github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 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/jtolds/gls v4.2.1+incompatible h1:fSuqC+Gmlu6l/ZYAoZzx2pyucC8Xza35fpRVWLVmUEE= +github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/kr/pty v1.0.0 h1:jR04h3bskdxb8xt+5B6MoxPwDhMCe0oEgxug4Ca1YSA= github.com/kr/pty v1.0.0/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw= +github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= +github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk= +github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= +github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A= +github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-sqlite3 v1.10.0 h1:jbhqpg7tQe4SupckyijYiy0mJJ/pRyHvXf7JdWK860o= +github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/matttproud/golang_protobuf_extensions v1.0.0 h1:YNOwxxSJzSUARoD9KRZLzM9Y858MNGCOACTvCW9TSAc= github.com/matttproud/golang_protobuf_extensions v1.0.0/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/minio/minio-go v6.0.14+incompatible h1:fnV+GD28LeqdN6vT2XdGKW8Qe/IfjJDswNVuni6km9o= @@ -80,6 +96,10 @@ github.com/prometheus/procfs v0.0.0-20180612222113-7d6f385de8be h1:MoyXp/VjXUwM0 github.com/prometheus/procfs v0.0.0-20180612222113-7d6f385de8be/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/sirupsen/logrus v1.0.5 h1:8c8b5uO0zS4X6RPl/sd1ENwSkIc0/H2PaHxE3udaE8I= github.com/sirupsen/logrus v1.0.5/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= +github.com/smartystreets/assertions v0.0.0-20190215210624-980c5ac6f3ac h1:wbW+Bybf9pXxnCFAOWZTqkRjAc7rAIwo2e1ArUhiHxg= +github.com/smartystreets/assertions v0.0.0-20190215210624-980c5ac6f3ac/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c h1:Ho+uVpkel/udgjbwB5Lktg9BtvJSh2DT0Hi6LPSyI2w= +github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s= github.com/soheilhy/cmux v0.1.4 h1:0HKaf1o97UwFjHH9o5XsHUOF+tqmdA7KEzXLpiyaw0E= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8= @@ -108,6 +128,7 @@ go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= golang.org/x/crypto v0.0.0-20180608092829-8ac0e0d97ce4/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 h1:mKdxBk7AujPs8kU4m80U72y/zjbZ3UcXC7dClwKbUI0= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd h1:nTDtHvHSdCn1m6ITfMRqtOd/9+7a3s8RBNOZ3eYZzJA= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e h1:bRhVy7zSSasaqNksaRZiA5EEI+Ei4I1nO5Jh72wfHlg= @@ -122,6 +143,8 @@ golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2 h1:+DCIGbF/swA92ohVg0//6X2IVY3KZs6p9mix0ziNYJM= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/genproto v0.0.0-20180608181217-32ee49c4dd80 h1:GL7nK1hkDKrkor0eVOYcMdIsUGErFnaC2gpBOVC+vbI= google.golang.org/genproto v0.0.0-20180608181217-32ee49c4dd80/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/grpc v1.14.0 h1:ArxJuB1NWfPY6r9Gp9gqwplT0Ge7nqv9msgu03lHLmo= @@ -135,6 +158,8 @@ gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2 h1:OAj3g0cR6Dx/R07QgQe8wkA9RNjB2u4i700xBkIT4e0= gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= +gopkg.in/ini.v1 v1.42.0 h1:7N3gPTt50s8GuLortA00n8AqRTk75qOP98+mTPpgzRk= +gopkg.in/ini.v1 v1.42.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/internal/db/create.go b/internal/db/create.go new file mode 100644 index 0000000..4f862ba --- /dev/null +++ b/internal/db/create.go @@ -0,0 +1,72 @@ +// Copyright 2019 Sorint.lab +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied +// See the License for the specific language governing permissions and +// limitations under the License. + +package db + +import ( + "database/sql" + + sq "github.com/Masterminds/squirrel" + "github.com/pkg/errors" +) + +const dbVersionTableDDLTmpl = ` + create table if not exists dbversion (version int not null, time timestamptz not null) +` + +const dbVersion = 1 + +func (db *DB) Create(stmts []string) error { + sb := sq.StatementBuilder.PlaceholderFormat(sq.Dollar) + + err := db.Do(func(tx *Tx) error { + if _, err := tx.Exec(dbVersionTableDDLTmpl); err != nil { + return errors.Wrap(err, "failed to create dbversion table") + } + return nil + }) + if err != nil { + return err + } + + err = db.Do(func(tx *Tx) error { + var version sql.NullInt64 + q, args, err := sb.Select("max(version)").From("dbversion").ToSql() + if err != nil { + return err + } + if err := tx.QueryRow(q, args...).Scan(&version); err != nil { + return errors.Wrap(err, "cannot get current db version") + } + if version.Valid { + return nil + } + + for _, stmt := range stmts { + if _, err := tx.Exec(stmt); err != nil { + return errors.Wrapf(err, "creation failed") + } + } + + q, args, err = sb.Insert("dbversion").Columns("version", "time").Values(dbVersion, "now()").ToSql() + if err != nil { + return err + } + if _, err := tx.Exec(q, args...); err != nil { + return errors.Wrap(err, "failed to update dbversion table") + } + return nil + }) + return err +} diff --git a/internal/db/db.go b/internal/db/db.go new file mode 100644 index 0000000..974afa7 --- /dev/null +++ b/internal/db/db.go @@ -0,0 +1,253 @@ +// Copyright 2019 Sorint.lab +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied +// See the License for the specific language governing permissions and +// limitations under the License. + +package db + +import ( + "context" + "database/sql" + "regexp" + "time" + + _ "github.com/mattn/go-sqlite3" + "github.com/pkg/errors" +) + +type Type string + +const ( + Sqlite3 Type = "sqlite3" + Postgres Type = "postgres" +) + +type dbData struct { + t Type + queryReplacers []replacer + supportsTimezones bool +} + +type replacer struct { + re *regexp.Regexp + with string +} + +// match a postgres query bind variable. E.g. "$1", "$12", etc. +var bindRegexp = regexp.MustCompile(`\$\d+`) + +func matchLiteral(s string) *regexp.Regexp { + return regexp.MustCompile(`\b` + regexp.QuoteMeta(s) + `\b`) +} + +var ( + dbDataPostgres = dbData{ + t: Postgres, + supportsTimezones: true, + queryReplacers: []replacer{ + // Remove sqlite3 only statements + {regexp.MustCompile(`--SQLITE3\n.*`), ""}, + }, + } + + dbDataSQLite3 = dbData{ + t: Sqlite3, + supportsTimezones: false, + queryReplacers: []replacer{ + {bindRegexp, "?"}, + {matchLiteral("true"), "1"}, + {matchLiteral("false"), "0"}, + {matchLiteral("boolean"), "integer"}, + {matchLiteral("bytea"), "blob"}, + // timestamp is a declared type suported by the go-sqlite3 driver + {matchLiteral("timestamptz"), "timestamp"}, + // convert now to the max precision time available with sqlite3 + {regexp.MustCompile(`\bnow\(\)`), "strftime('%Y-%m-%d %H:%M:%f', 'now')"}, + {regexp.MustCompile(`select pg_advisory_xact_lock\(.*`), "select 1"}, + {regexp.MustCompile(`notify\s+.*`), "select 1"}, + // Remove postgres only statements + {regexp.MustCompile(`--POSTGRES\n.*`), ""}, + }, + } +) + +func (t dbData) translate(query string) string { + for _, r := range t.queryReplacers { + query = r.re.ReplaceAllString(query, r.with) + } + return query +} + +// translateArgs translates query parameters that may be unique to +// a specific SQL flavor. For example, standardizing "time.Time" +// types to UTC for clients that don't provide timezone support. +func (t dbData) translateArgs(args []interface{}) []interface{} { + if t.supportsTimezones { + return args + } + + for i, arg := range args { + if t, ok := arg.(time.Time); ok { + args[i] = t.UTC() + } + } + return args +} + +// DB wraps a sql.DB to add special behaviors based on the db type +type DB struct { + db *sql.DB + data dbData +} + +func NewDB(dbType Type, dbConnString string) (*DB, error) { + var data dbData + var driverName string + switch dbType { + case Postgres: + data = dbDataPostgres + driverName = "postgres" + case Sqlite3: + data = dbDataSQLite3 + driverName = "sqlite3" + dbConnString = "file:" + dbConnString + "?cache=shared&_journal=wal&_foreign_keys=true&_case_sensitive_like=false" + default: + return nil, errors.New("unknown db type") + } + + sqldb, err := sql.Open(driverName, dbConnString) + if err != nil { + return nil, errors.WithStack(err) + } + + db := &DB{ + db: sqldb, + data: data, + } + + return db, nil +} + +// Tx wraps a sql.Tx to offer: +// * apply some statement mutations before executing it +// * locking around concurrent executions of statements (since the underlying +// sql driver doesn't support concurrent statements on the same +// connection/transaction) +type Tx struct { + db *DB + tx *sql.Tx +} + +func (db *DB) Close() error { + return db.db.Close() +} + +func (db *DB) Conn() (*sql.Conn, error) { + return db.db.Conn(context.TODO()) +} + +func (db *DB) NewUnstartedTx() *Tx { + return &Tx{ + db: db, + } +} + +func (db *DB) NewTx() (*Tx, error) { + tx := db.NewUnstartedTx() + if err := tx.Start(); err != nil { + return nil, err + } + + return tx, nil +} + +func (db *DB) Do(f func(tx *Tx) error) error { + tx, err := db.NewTx() + if err != nil { + return err + } + defer func() { + if p := recover(); p != nil { + tx.Rollback() + panic(p) + } + }() + if err = f(tx); err != nil { + tx.Rollback() + return err + } + return tx.Commit() +} + +func (tx *Tx) Start() error { + wtx, err := tx.db.db.Begin() + if err != nil { + return errors.WithStack(err) + } + switch tx.db.data.t { + case Postgres: + if _, err := wtx.Exec("SET TRANSACTION ISOLATION LEVEL REPEATABLE READ"); err != nil { + return errors.WithStack(err) + } + } + tx.tx = wtx + return nil +} + +func (tx *Tx) Commit() error { + if tx.tx == nil { + return nil + } + return tx.tx.Commit() +} + +func (tx *Tx) Rollback() error { + if tx.tx == nil { + return nil + } + return tx.tx.Rollback() +} + +func (tx *Tx) Exec(query string, args ...interface{}) (sql.Result, error) { + query = tx.db.data.translate(query) + r, err := tx.tx.Exec(query, tx.db.data.translateArgs(args)...) + return r, errors.WithStack(err) +} + +func (tx *Tx) Query(query string, args ...interface{}) (*sql.Rows, error) { + query = tx.db.data.translate(query) + r, err := tx.tx.Query(query, tx.db.data.translateArgs(args)...) + return r, errors.WithStack(err) +} + +func (tx *Tx) QueryRow(query string, args ...interface{}) *sql.Row { + query = tx.db.data.translate(query) + return tx.tx.QueryRow(query, tx.db.data.translateArgs(args)...) +} + +func (tx *Tx) CurTime() (time.Time, error) { + switch tx.db.data.t { + case Sqlite3: + var timestring string + if err := tx.QueryRow("select now()").Scan(×tring); err != nil { + return time.Time{}, errors.WithStack(err) + } + return time.ParseInLocation("2006-01-02 15:04:05.999999999", timestring, time.UTC) + case Postgres: + var now time.Time + if err := tx.QueryRow("select now()").Scan(&now); err != nil { + return time.Time{}, errors.WithStack(err) + } + return now, nil + } + return time.Time{}, errors.New("unknown db type") +}