Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision
  • master
  • v1.0.0
  • v1.0.1
  • v1.1.0
  • v1.10.0
  • v1.10.1
  • v1.10.2
  • v1.11.0
  • v1.12.0
  • v1.12.1
  • v1.12.2
  • v1.12.3
  • v1.12.4
  • v1.12.5
  • v1.12.6
  • v1.12.7
  • v1.12.8
  • v1.13.0
  • v1.13.1
  • v1.13.2
  • v1.14.0
  • v1.15.0
  • v1.15.1
  • v1.15.10
  • v1.15.11
  • v1.15.12
  • v1.15.13
  • v1.15.14
  • v1.15.15
  • v1.15.16
  • v1.15.17
  • v1.15.2
  • v1.15.3
  • v1.15.4
  • v1.15.5
  • v1.15.6
  • v1.15.7
  • v1.15.8
  • v1.15.9
  • v1.16.0
  • v1.16.1
  • v1.17.0
  • v1.18.0
  • v1.18.1
  • v1.18.2
  • v1.19.0
  • v1.19.1
  • v1.19.2
  • v1.19.3
  • v1.19.4
  • v1.2.0
  • v1.20.0
  • v1.20.1
  • v1.20.2
  • v1.20.3
  • v1.21.0
  • v1.21.1
  • v1.22.0
  • v1.23.0
  • v1.23.1
  • v1.23.2
  • v1.3.0
  • v1.3.1
  • v1.3.2
  • v1.4.0
  • v1.5.0
  • v1.5.1
  • v1.6.0
  • v1.6.1
  • v1.7.0
  • v1.7.1
  • v1.7.2
  • v1.7.3
  • v1.8.0
  • v1.8.1
  • v1.9.0
76 results

Target

Select target project
  • oss/libraries/go/services/job-queues
1 result
Select Git revision
  • master
  • v1.0.0
  • v1.0.1
  • v1.1.0
  • v1.10.0
  • v1.10.1
  • v1.10.2
  • v1.11.0
  • v1.12.0
  • v1.12.1
  • v1.12.2
  • v1.12.3
  • v1.12.4
  • v1.12.5
  • v1.12.6
  • v1.12.7
  • v1.12.8
  • v1.13.0
  • v1.13.1
  • v1.13.2
  • v1.14.0
  • v1.15.0
  • v1.15.1
  • v1.15.10
  • v1.15.11
  • v1.15.12
  • v1.15.13
  • v1.15.14
  • v1.15.15
  • v1.15.16
  • v1.15.17
  • v1.15.2
  • v1.15.3
  • v1.15.4
  • v1.15.5
  • v1.15.6
  • v1.15.7
  • v1.15.8
  • v1.15.9
  • v1.16.0
  • v1.16.1
  • v1.17.0
  • v1.18.0
  • v1.18.1
  • v1.18.2
  • v1.19.0
  • v1.19.1
  • v1.19.2
  • v1.19.3
  • v1.19.4
  • v1.2.0
  • v1.20.0
  • v1.20.1
  • v1.20.2
  • v1.20.3
  • v1.21.0
  • v1.21.1
  • v1.22.0
  • v1.23.0
  • v1.23.1
  • v1.23.2
  • v1.3.0
  • v1.3.1
  • v1.3.2
  • v1.4.0
  • v1.5.0
  • v1.5.1
  • v1.6.0
  • v1.6.1
  • v1.7.0
  • v1.7.1
  • v1.7.2
  • v1.7.3
  • v1.8.0
  • v1.8.1
  • v1.9.0
76 results
Show changes
Showing
with 1238 additions and 388 deletions
...@@ -11,6 +11,14 @@ env: ...@@ -11,6 +11,14 @@ env:
MYSQL_TEST_CONCURRENT: 1 MYSQL_TEST_CONCURRENT: 1
jobs: jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dominikh/staticcheck-action@v1.3.0
with:
version: "2023.1.6"
list: list:
runs-on: ubuntu-latest runs-on: ubuntu-latest
outputs: outputs:
...@@ -23,17 +31,14 @@ jobs: ...@@ -23,17 +31,14 @@ jobs:
import os import os
go = [ go = [
# Keep the most recent production release at the top # Keep the most recent production release at the top
'1.20', '1.21',
# Older production releases # Older production releases
'1.20',
'1.19', '1.19',
'1.18', '1.18',
'1.17',
'1.16',
'1.15',
'1.14',
'1.13',
] ]
mysql = [ mysql = [
'8.1',
'8.0', '8.0',
'5.7', '5.7',
'5.6', '5.6',
...@@ -68,11 +73,11 @@ jobs: ...@@ -68,11 +73,11 @@ jobs:
fail-fast: false fail-fast: false
matrix: ${{ fromJSON(needs.list.outputs.matrix) }} matrix: ${{ fromJSON(needs.list.outputs.matrix) }}
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: actions/setup-go@v3 - uses: actions/setup-go@v5
with: with:
go-version: ${{ matrix.go }} go-version: ${{ matrix.go }}
- uses: shogo82148/actions-setup-mysql@v1.15.0 - uses: shogo82148/actions-setup-mysql@v1
with: with:
mysql-version: ${{ matrix.mysql }} mysql-version: ${{ matrix.mysql }}
user: ${{ env.MYSQL_TEST_USER }} user: ${{ env.MYSQL_TEST_USER }}
...@@ -84,13 +89,14 @@ jobs: ...@@ -84,13 +89,14 @@ jobs:
; TestConcurrent fails if max_connections is too large ; TestConcurrent fails if max_connections is too large
max_connections=50 max_connections=50
local_infile=1 local_infile=1
performance_schema=on
- name: setup database - name: setup database
run: | run: |
mysql --user 'root' --host '127.0.0.1' -e 'create database gotest;' mysql --user 'root' --host '127.0.0.1' -e 'create database gotest;'
- name: test - name: test
run: | run: |
go test -v '-covermode=count' '-coverprofile=coverage.out' go test -v '-race' '-covermode=atomic' '-coverprofile=coverage.out' -parallel 10
- name: Send coverage - name: Send coverage
uses: shogo82148/actions-goveralls@v1 uses: shogo82148/actions-goveralls@v1
......
...@@ -13,6 +13,7 @@ ...@@ -13,6 +13,7 @@
Aaron Hopkins <go-sql-driver at die.net> Aaron Hopkins <go-sql-driver at die.net>
Achille Roussel <achille.roussel at gmail.com> Achille Roussel <achille.roussel at gmail.com>
Aidan <aidan.liu at pingcap.com>
Alex Snast <alexsn at fb.com> Alex Snast <alexsn at fb.com>
Alexey Palazhchenko <alexey.palazhchenko at gmail.com> Alexey Palazhchenko <alexey.palazhchenko at gmail.com>
Andrew Reid <andrew.reid at tixtrack.com> Andrew Reid <andrew.reid at tixtrack.com>
...@@ -20,12 +21,14 @@ Animesh Ray <mail.rayanimesh at gmail.com> ...@@ -20,12 +21,14 @@ Animesh Ray <mail.rayanimesh at gmail.com>
Arne Hormann <arnehormann at gmail.com> Arne Hormann <arnehormann at gmail.com>
Ariel Mashraki <ariel at mashraki.co.il> Ariel Mashraki <ariel at mashraki.co.il>
Asta Xie <xiemengjun at gmail.com> Asta Xie <xiemengjun at gmail.com>
Brian Hendriks <brian at dolthub.com>
Bulat Gaifullin <gaifullinbf at gmail.com> Bulat Gaifullin <gaifullinbf at gmail.com>
Caine Jette <jette at alum.mit.edu> Caine Jette <jette at alum.mit.edu>
Carlos Nieto <jose.carlos at menteslibres.net> Carlos Nieto <jose.carlos at menteslibres.net>
Chris Kirkland <chriskirkland at github.com> Chris Kirkland <chriskirkland at github.com>
Chris Moos <chris at tech9computers.com> Chris Moos <chris at tech9computers.com>
Craig Wilson <craiggwilson at gmail.com> Craig Wilson <craiggwilson at gmail.com>
Daemonxiao <735462752 at qq.com>
Daniel Montoya <dsmontoyam at gmail.com> Daniel Montoya <dsmontoyam at gmail.com>
Daniel Nichter <nil at codenode.com> Daniel Nichter <nil at codenode.com>
Daniël van Eeden <git at myname.nl> Daniël van Eeden <git at myname.nl>
...@@ -33,9 +36,11 @@ Dave Protasowski <dprotaso at gmail.com> ...@@ -33,9 +36,11 @@ Dave Protasowski <dprotaso at gmail.com>
DisposaBoy <disposaboy at dby.me> DisposaBoy <disposaboy at dby.me>
Egor Smolyakov <egorsmkv at gmail.com> Egor Smolyakov <egorsmkv at gmail.com>
Erwan Martin <hello at erwan.io> Erwan Martin <hello at erwan.io>
Evan Elias <evan at skeema.net>
Evan Shaw <evan at vendhq.com> Evan Shaw <evan at vendhq.com>
Frederick Mayle <frederickmayle at gmail.com> Frederick Mayle <frederickmayle at gmail.com>
Gustavo Kristic <gkristic at gmail.com> Gustavo Kristic <gkristic at gmail.com>
Gusted <postmaster at gusted.xyz>
Hajime Nakagami <nakagami at gmail.com> Hajime Nakagami <nakagami at gmail.com>
Hanno Braun <mail at hannobraun.com> Hanno Braun <mail at hannobraun.com>
Henri Yandell <flamefew at gmail.com> Henri Yandell <flamefew at gmail.com>
...@@ -47,8 +52,11 @@ INADA Naoki <songofacandy at gmail.com> ...@@ -47,8 +52,11 @@ INADA Naoki <songofacandy at gmail.com>
Jacek Szwec <szwec.jacek at gmail.com> Jacek Szwec <szwec.jacek at gmail.com>
James Harr <james.harr at gmail.com> James Harr <james.harr at gmail.com>
Janek Vedock <janekvedock at comcast.net> Janek Vedock <janekvedock at comcast.net>
Jason Ng <oblitorum at gmail.com>
Jean-Yves Pellé <jy at pelle.link>
Jeff Hodges <jeff at somethingsimilar.com> Jeff Hodges <jeff at somethingsimilar.com>
Jeffrey Charles <jeffreycharles at gmail.com> Jeffrey Charles <jeffreycharles at gmail.com>
Jennifer Purevsuren <jennifer at dolthub.com>
Jerome Meyer <jxmeyer at gmail.com> Jerome Meyer <jxmeyer at gmail.com>
Jiajia Zhong <zhong2plus at gmail.com> Jiajia Zhong <zhong2plus at gmail.com>
Jian Zhen <zhenjl at gmail.com> Jian Zhen <zhenjl at gmail.com>
...@@ -74,9 +82,11 @@ Maciej Zimnoch <maciej.zimnoch at codilime.com> ...@@ -74,9 +82,11 @@ Maciej Zimnoch <maciej.zimnoch at codilime.com>
Michael Woolnough <michael.woolnough at gmail.com> Michael Woolnough <michael.woolnough at gmail.com>
Nathanial Murphy <nathanial.murphy at gmail.com> Nathanial Murphy <nathanial.murphy at gmail.com>
Nicola Peduzzi <thenikso at gmail.com> Nicola Peduzzi <thenikso at gmail.com>
Oliver Bone <owbone at github.com>
Olivier Mengué <dolmen at cpan.org> Olivier Mengué <dolmen at cpan.org>
oscarzhao <oscarzhaosl at gmail.com> oscarzhao <oscarzhaosl at gmail.com>
Paul Bonser <misterpib at gmail.com> Paul Bonser <misterpib at gmail.com>
Paulius Lozys <pauliuslozys at gmail.com>
Peter Schultz <peter.schultz at classmarkets.com> Peter Schultz <peter.schultz at classmarkets.com>
Phil Porada <philporada at gmail.com> Phil Porada <philporada at gmail.com>
Rebecca Chin <rchin at pivotal.io> Rebecca Chin <rchin at pivotal.io>
...@@ -95,6 +105,7 @@ Stan Putrya <root.vagner at gmail.com> ...@@ -95,6 +105,7 @@ Stan Putrya <root.vagner at gmail.com>
Stanley Gunawan <gunawan.stanley at gmail.com> Stanley Gunawan <gunawan.stanley at gmail.com>
Steven Hartland <steven.hartland at multiplay.co.uk> Steven Hartland <steven.hartland at multiplay.co.uk>
Tan Jinhua <312841925 at qq.com> Tan Jinhua <312841925 at qq.com>
Tetsuro Aoki <t.aoki1130 at gmail.com>
Thomas Wodarek <wodarekwebpage at gmail.com> Thomas Wodarek <wodarekwebpage at gmail.com>
Tim Ruffles <timruffles at gmail.com> Tim Ruffles <timruffles at gmail.com>
Tom Jenkinson <tom at tjenkinson.me> Tom Jenkinson <tom at tjenkinson.me>
...@@ -104,6 +115,7 @@ Xiangyu Hu <xiangyu.hu at outlook.com> ...@@ -104,6 +115,7 @@ Xiangyu Hu <xiangyu.hu at outlook.com>
Xiaobing Jiang <s7v7nislands at gmail.com> Xiaobing Jiang <s7v7nislands at gmail.com>
Xiuming Chen <cc at cxm.cc> Xiuming Chen <cc at cxm.cc>
Xuehong Chan <chanxuehong at gmail.com> Xuehong Chan <chanxuehong at gmail.com>
Zhang Xiang <angwerzx at 126.com>
Zhenye Xie <xiezhenye at gmail.com> Zhenye Xie <xiezhenye at gmail.com>
Zhixin Wen <john.wenzhixin at gmail.com> Zhixin Wen <john.wenzhixin at gmail.com>
Ziheng Lyu <zihenglv at gmail.com> Ziheng Lyu <zihenglv at gmail.com>
...@@ -113,14 +125,18 @@ Ziheng Lyu <zihenglv at gmail.com> ...@@ -113,14 +125,18 @@ Ziheng Lyu <zihenglv at gmail.com>
Barracuda Networks, Inc. Barracuda Networks, Inc.
Counting Ltd. Counting Ltd.
DigitalOcean Inc. DigitalOcean Inc.
Dolthub Inc.
dyves labs AG dyves labs AG
Facebook Inc. Facebook Inc.
GitHub Inc. GitHub Inc.
Google Inc. Google Inc.
InfoSum Ltd. InfoSum Ltd.
Keybase Inc. Keybase Inc.
Microsoft Corp.
Multiplay Ltd. Multiplay Ltd.
Percona LLC Percona LLC
PingCAP Inc.
Pivotal Inc. Pivotal Inc.
Shattered Silicon Ltd.
Stripe Inc. Stripe Inc.
Zendesk Inc. Zendesk Inc.
...@@ -162,7 +162,7 @@ New Features: ...@@ -162,7 +162,7 @@ New Features:
- Enable microsecond resolution on TIME, DATETIME and TIMESTAMP (#249) - Enable microsecond resolution on TIME, DATETIME and TIMESTAMP (#249)
- Support for returning table alias on Columns() (#289, #359, #382) - Support for returning table alias on Columns() (#289, #359, #382)
- Placeholder interpolation, can be actived with the DSN parameter `interpolateParams=true` (#309, #318, #490) - Placeholder interpolation, can be activated with the DSN parameter `interpolateParams=true` (#309, #318, #490)
- Support for uint64 parameters with high bit set (#332, #345) - Support for uint64 parameters with high bit set (#332, #345)
- Cleartext authentication plugin support (#327) - Cleartext authentication plugin support (#327)
- Exported ParseDSN function and the Config struct (#403, #419, #429) - Exported ParseDSN function and the Config struct (#403, #419, #429)
...@@ -206,7 +206,7 @@ Changes: ...@@ -206,7 +206,7 @@ Changes:
- Also exported the MySQLWarning type - Also exported the MySQLWarning type
- mysqlConn.Close returns the first error encountered instead of ignoring all errors - mysqlConn.Close returns the first error encountered instead of ignoring all errors
- writePacket() automatically writes the packet size to the header - writePacket() automatically writes the packet size to the header
- readPacket() uses an iterative approach instead of the recursive approach to merge splitted packets - readPacket() uses an iterative approach instead of the recursive approach to merge split packets
New Features: New Features:
...@@ -254,7 +254,7 @@ Bugfixes: ...@@ -254,7 +254,7 @@ Bugfixes:
- Fixed MySQL 4.1 support: MySQL 4.1 sends packets with lengths which differ from the specification - Fixed MySQL 4.1 support: MySQL 4.1 sends packets with lengths which differ from the specification
- Convert to DB timezone when inserting `time.Time` - Convert to DB timezone when inserting `time.Time`
- Splitted packets (more than 16MB) are now merged correctly - Split packets (more than 16MB) are now merged correctly
- Fixed false positive `io.EOF` errors when the data was fully read - Fixed false positive `io.EOF` errors when the data was fully read
- Avoid panics on reuse of closed connections - Avoid panics on reuse of closed connections
- Fixed empty string producing false nil values - Fixed empty string producing false nil values
......
...@@ -40,15 +40,23 @@ A MySQL-Driver for Go's [database/sql](https://golang.org/pkg/database/sql/) pac ...@@ -40,15 +40,23 @@ A MySQL-Driver for Go's [database/sql](https://golang.org/pkg/database/sql/) pac
* Optional placeholder interpolation * Optional placeholder interpolation
## Requirements ## Requirements
* Go 1.13 or higher. We aim to support the 3 latest versions of Go.
* MySQL (4.1+), MariaDB, Percona Server, Google CloudSQL or Sphinx (2.2.3+) * Go 1.19 or higher. We aim to support the 3 latest versions of Go.
* MySQL (5.7+) and MariaDB (10.3+) are supported.
* [TiDB](https://github.com/pingcap/tidb) is supported by PingCAP.
* Do not ask questions about TiDB in our issue tracker or forum.
* [Document](https://docs.pingcap.com/tidb/v6.1/dev-guide-sample-application-golang)
* [Forum](https://ask.pingcap.com/)
* go-mysql would work with Percona Server, Google CloudSQL or Sphinx (2.2.3+).
* Maintainers won't support them. Do not expect issues are investigated and resolved by maintainers.
* Investigate issues yourself and please send a pull request to fix it.
--------------------------------------- ---------------------------------------
## Installation ## Installation
Simple install the package to your [$GOPATH](https://github.com/golang/go/wiki/GOPATH "GOPATH") with the [go tool](https://golang.org/cmd/go/ "go command") from shell: Simple install the package to your [$GOPATH](https://github.com/golang/go/wiki/GOPATH "GOPATH") with the [go tool](https://golang.org/cmd/go/ "go command") from shell:
```bash ```bash
$ go get -u github.com/go-sql-driver/mysql go get -u github.com/go-sql-driver/mysql
``` ```
Make sure [Git is installed](https://git-scm.com/downloads) on your machine and in your system's `PATH`. Make sure [Git is installed](https://git-scm.com/downloads) on your machine and in your system's `PATH`.
...@@ -114,6 +122,12 @@ This has the same effect as an empty DSN string: ...@@ -114,6 +122,12 @@ This has the same effect as an empty DSN string:
``` ```
`dbname` is escaped by [PathEscape()](https://pkg.go.dev/net/url#PathEscape) since v1.8.0. If your database name is `dbname/withslash`, it becomes:
```
/dbname%2Fwithslash
```
Alternatively, [Config.FormatDSN](https://godoc.org/github.com/go-sql-driver/mysql#Config.FormatDSN) can be used to create a DSN string by filling a struct. Alternatively, [Config.FormatDSN](https://godoc.org/github.com/go-sql-driver/mysql#Config.FormatDSN) can be used to create a DSN string by filling a struct.
#### Password #### Password
...@@ -121,7 +135,7 @@ Passwords can consist of any character. Escaping is **not** necessary. ...@@ -121,7 +135,7 @@ Passwords can consist of any character. Escaping is **not** necessary.
#### Protocol #### Protocol
See [net.Dial](https://golang.org/pkg/net/#Dial) for more information which networks are available. See [net.Dial](https://golang.org/pkg/net/#Dial) for more information which networks are available.
In general you should use an Unix domain socket if available and TCP otherwise for best performance. In general you should use a Unix domain socket if available and TCP otherwise for best performance.
#### Address #### Address
For TCP and UDP networks, addresses have the form `host[:port]`. For TCP and UDP networks, addresses have the form `host[:port]`.
...@@ -145,7 +159,7 @@ Default: false ...@@ -145,7 +159,7 @@ Default: false
``` ```
`allowAllFiles=true` disables the file allowlist for `LOAD DATA LOCAL INFILE` and allows *all* files. `allowAllFiles=true` disables the file allowlist for `LOAD DATA LOCAL INFILE` and allows *all* files.
[*Might be insecure!*](http://dev.mysql.com/doc/refman/5.7/en/load-data-local.html) [*Might be insecure!*](https://dev.mysql.com/doc/refman/8.0/en/load-data.html#load-data-local)
##### `allowCleartextPasswords` ##### `allowCleartextPasswords`
...@@ -194,10 +208,9 @@ Valid Values: <name> ...@@ -194,10 +208,9 @@ Valid Values: <name>
Default: none Default: none
``` ```
Sets the charset used for client-server interaction (`"SET NAMES <value>"`). If multiple charsets are set (separated by a comma), the following charset is used if setting the charset failes. This enables for example support for `utf8mb4` ([introduced in MySQL 5.5.3](http://dev.mysql.com/doc/refman/5.5/en/charset-unicode-utf8mb4.html)) with fallback to `utf8` for older servers (`charset=utf8mb4,utf8`). Sets the charset used for client-server interaction (`"SET NAMES <value>"`). If multiple charsets are set (separated by a comma), the following charset is used if setting the charset fails. This enables for example support for `utf8mb4` ([introduced in MySQL 5.5.3](http://dev.mysql.com/doc/refman/5.5/en/charset-unicode-utf8mb4.html)) with fallback to `utf8` for older servers (`charset=utf8mb4,utf8`).
Usage of the `charset` parameter is discouraged because it issues additional queries to the server. See also [Unicode Support](#unicode-support).
Unless you need the fallback behavior, please use `collation` instead.
##### `checkConnLiveness` ##### `checkConnLiveness`
...@@ -226,6 +239,7 @@ The default collation (`utf8mb4_general_ci`) is supported from MySQL 5.5. You s ...@@ -226,6 +239,7 @@ The default collation (`utf8mb4_general_ci`) is supported from MySQL 5.5. You s
Collations for charset "ucs2", "utf16", "utf16le", and "utf32" can not be used ([ref](https://dev.mysql.com/doc/refman/5.7/en/charset-connection.html#charset-connection-impermissible-client-charset)). Collations for charset "ucs2", "utf16", "utf16le", and "utf32" can not be used ([ref](https://dev.mysql.com/doc/refman/5.7/en/charset-connection.html#charset-connection-impermissible-client-charset)).
See also [Unicode Support](#unicode-support).
##### `clientFoundRows` ##### `clientFoundRows`
...@@ -279,6 +293,15 @@ Note that this sets the location for time.Time values but does not change MySQL' ...@@ -279,6 +293,15 @@ Note that this sets the location for time.Time values but does not change MySQL'
Please keep in mind, that param values must be [url.QueryEscape](https://golang.org/pkg/net/url/#QueryEscape)'ed. Alternatively you can manually replace the `/` with `%2F`. For example `US/Pacific` would be `loc=US%2FPacific`. Please keep in mind, that param values must be [url.QueryEscape](https://golang.org/pkg/net/url/#QueryEscape)'ed. Alternatively you can manually replace the `/` with `%2F`. For example `US/Pacific` would be `loc=US%2FPacific`.
##### `timeTruncate`
```
Type: duration
Default: 0
```
[Truncate time values](https://pkg.go.dev/time#Duration.Truncate) to the specified duration. The value must be a decimal number with a unit suffix (*"ms"*, *"s"*, *"m"*, *"h"*), such as *"30s"*, *"0.5m"* or *"1m30s"*.
##### `maxAllowedPacket` ##### `maxAllowedPacket`
``` ```
Type: decimal number Type: decimal number
...@@ -295,9 +318,25 @@ Valid Values: true, false ...@@ -295,9 +318,25 @@ Valid Values: true, false
Default: false Default: false
``` ```
Allow multiple statements in one query. While this allows batch queries, it also greatly increases the risk of SQL injections. Only the result of the first query is returned, all other results are silently discarded. Allow multiple statements in one query. This can be used to bach multiple queries. Use [Rows.NextResultSet()](https://pkg.go.dev/database/sql#Rows.NextResultSet) to get result of the second and subsequent queries.
When `multiStatements` is used, `?` parameters must only be used in the first statement. [interpolateParams](#interpolateparams) can be used to avoid this limitation unless prepared statement is used explicitly.
It's possible to access the last inserted ID and number of affected rows for multiple statements by using `sql.Conn.Raw()` and the `mysql.Result`. For example:
When `multiStatements` is used, `?` parameters must only be used in the first statement. ```go
conn, _ := db.Conn(ctx)
conn.Raw(func(conn interface{}) error {
ex := conn.(driver.Execer)
res, err := ex.Exec(`
UPDATE point SET x = 1 WHERE y = 2;
UPDATE point SET x = 2 WHERE y = 3;
`, nil)
// Both slices have 2 elements.
log.Print(res.(mysql.Result).AllRowsAffected())
log.Print(res.(mysql.Result).AllLastInsertIds())
})
```
##### `parseTime` ##### `parseTime`
...@@ -393,6 +432,15 @@ Default: 0 ...@@ -393,6 +432,15 @@ Default: 0
I/O write timeout. The value must be a decimal number with a unit suffix (*"ms"*, *"s"*, *"m"*, *"h"*), such as *"30s"*, *"0.5m"* or *"1m30s"*. I/O write timeout. The value must be a decimal number with a unit suffix (*"ms"*, *"s"*, *"m"*, *"h"*), such as *"30s"*, *"0.5m"* or *"1m30s"*.
##### `connectionAttributes`
```
Type: comma-delimited string of user-defined "key:value" pairs
Valid Values: (<name1>:<value1>,<name2>:<value2>,...)
Default: none
```
[Connection attributes](https://dev.mysql.com/doc/refman/8.0/en/performance-schema-connection-attribute-tables.html) are key-value pairs that application programs can pass to the server at connect time.
##### System Variables ##### System Variables
...@@ -465,7 +513,7 @@ user:password@/ ...@@ -465,7 +513,7 @@ user:password@/
The connection pool is managed by Go's database/sql package. For details on how to configure the size of the pool and how long connections stay in the pool see `*DB.SetMaxOpenConns`, `*DB.SetMaxIdleConns`, and `*DB.SetConnMaxLifetime` in the [database/sql documentation](https://golang.org/pkg/database/sql/). The read, write, and dial timeouts for each individual connection are configured with the DSN parameters [`readTimeout`](#readtimeout), [`writeTimeout`](#writetimeout), and [`timeout`](#timeout), respectively. The connection pool is managed by Go's database/sql package. For details on how to configure the size of the pool and how long connections stay in the pool see `*DB.SetMaxOpenConns`, `*DB.SetMaxIdleConns`, and `*DB.SetConnMaxLifetime` in the [database/sql documentation](https://golang.org/pkg/database/sql/). The read, write, and dial timeouts for each individual connection are configured with the DSN parameters [`readTimeout`](#readtimeout), [`writeTimeout`](#writetimeout), and [`timeout`](#timeout), respectively.
## `ColumnType` Support ## `ColumnType` Support
This driver supports the [`ColumnType` interface](https://golang.org/pkg/database/sql/#ColumnType) introduced in Go 1.8, with the exception of [`ColumnType.Length()`](https://golang.org/pkg/database/sql/#ColumnType.Length), which is currently not supported. All Unsigned database type names will be returned `UNSIGNED ` with `INT`, `TINYINT`, `SMALLINT`, `BIGINT`. This driver supports the [`ColumnType` interface](https://golang.org/pkg/database/sql/#ColumnType) introduced in Go 1.8, with the exception of [`ColumnType.Length()`](https://golang.org/pkg/database/sql/#ColumnType.Length), which is currently not supported. All Unsigned database type names will be returned `UNSIGNED ` with `INT`, `TINYINT`, `SMALLINT`, `MEDIUMINT`, `BIGINT`.
## `context.Context` Support ## `context.Context` Support
Go 1.8 added `database/sql` support for `context.Context`. This driver supports query timeouts and cancellation via contexts. Go 1.8 added `database/sql` support for `context.Context`. This driver supports query timeouts and cancellation via contexts.
...@@ -478,7 +526,7 @@ For this feature you need direct access to the package. Therefore you must chang ...@@ -478,7 +526,7 @@ For this feature you need direct access to the package. Therefore you must chang
import "github.com/go-sql-driver/mysql" import "github.com/go-sql-driver/mysql"
``` ```
Files must be explicitly allowed by registering them with `mysql.RegisterLocalFile(filepath)` (recommended) or the allowlist check must be deactivated by using the DSN parameter `allowAllFiles=true` ([*Might be insecure!*](http://dev.mysql.com/doc/refman/5.7/en/load-data-local.html)). Files must be explicitly allowed by registering them with `mysql.RegisterLocalFile(filepath)` (recommended) or the allowlist check must be deactivated by using the DSN parameter `allowAllFiles=true` ([*Might be insecure!*](https://dev.mysql.com/doc/refman/8.0/en/load-data.html#load-data-local)).
To use a `io.Reader` a handler function must be registered with `mysql.RegisterReaderHandler(name, handler)` which returns a `io.Reader` or `io.ReadCloser`. The Reader is available with the filepath `Reader::<name>` then. Choose different names for different handlers and `DeregisterReaderHandler` when you don't need it anymore. To use a `io.Reader` a handler function must be registered with `mysql.RegisterReaderHandler(name, handler)` which returns a `io.Reader` or `io.ReadCloser`. The Reader is available with the filepath `Reader::<name>` then. Choose different names for different handlers and `DeregisterReaderHandler` when you don't need it anymore.
...@@ -496,9 +544,11 @@ However, many want to scan MySQL `DATE` and `DATETIME` values into `time.Time` v ...@@ -496,9 +544,11 @@ However, many want to scan MySQL `DATE` and `DATETIME` values into `time.Time` v
### Unicode support ### Unicode support
Since version 1.5 Go-MySQL-Driver automatically uses the collation ` utf8mb4_general_ci` by default. Since version 1.5 Go-MySQL-Driver automatically uses the collation ` utf8mb4_general_ci` by default.
Other collations / charsets can be set using the [`collation`](#collation) DSN parameter. Other charsets / collations can be set using the [`charset`](#charset) or [`collation`](#collation) DSN parameter.
Version 1.0 of the driver recommended adding `&charset=utf8` (alias for `SET NAMES utf8`) to the DSN to enable proper UTF-8 support. This is not necessary anymore. The [`collation`](#collation) parameter should be preferred to set another collation / charset than the default. - When only the `charset` is specified, the `SET NAMES <charset>` query is sent and the server's default collation is used.
- When both the `charset` and `collation` are specified, the `SET NAMES <charset> COLLATE <collation>` query is sent.
- When only the `collation` is specified, the collation is specified in the protocol handshake and the `SET NAMES` query is not sent. This can save one roundtrip, but note that the server may ignore the specified collation silently and use the server's default charset/collation instead.
See http://dev.mysql.com/doc/refman/8.0/en/charset-unicode.html for more details on MySQL's Unicode support. See http://dev.mysql.com/doc/refman/8.0/en/charset-unicode.html for more details on MySQL's Unicode support.
......
...@@ -13,10 +13,13 @@ import ( ...@@ -13,10 +13,13 @@ import (
"crypto/rsa" "crypto/rsa"
"crypto/sha1" "crypto/sha1"
"crypto/sha256" "crypto/sha256"
"crypto/sha512"
"crypto/x509" "crypto/x509"
"encoding/pem" "encoding/pem"
"fmt" "fmt"
"sync" "sync"
"filippo.io/edwards25519"
) )
// server pub keys registry // server pub keys registry
...@@ -33,7 +36,7 @@ var ( ...@@ -33,7 +36,7 @@ var (
// Note: The provided rsa.PublicKey instance is exclusively owned by the driver // Note: The provided rsa.PublicKey instance is exclusively owned by the driver
// after registering it and may not be modified. // after registering it and may not be modified.
// //
// data, err := ioutil.ReadFile("mykey.pem") // data, err := os.ReadFile("mykey.pem")
// if err != nil { // if err != nil {
// log.Fatal(err) // log.Fatal(err)
// } // }
...@@ -225,6 +228,44 @@ func encryptPassword(password string, seed []byte, pub *rsa.PublicKey) ([]byte, ...@@ -225,6 +228,44 @@ func encryptPassword(password string, seed []byte, pub *rsa.PublicKey) ([]byte,
return rsa.EncryptOAEP(sha1, rand.Reader, pub, plain, nil) return rsa.EncryptOAEP(sha1, rand.Reader, pub, plain, nil)
} }
// authEd25519 does ed25519 authentication used by MariaDB.
func authEd25519(scramble []byte, password string) ([]byte, error) {
// Derived from https://github.com/MariaDB/server/blob/d8e6bb00888b1f82c031938f4c8ac5d97f6874c3/plugin/auth_ed25519/ref10/sign.c
// Code style is from https://cs.opensource.google/go/go/+/refs/tags/go1.21.5:src/crypto/ed25519/ed25519.go;l=207
h := sha512.Sum512([]byte(password))
s, err := edwards25519.NewScalar().SetBytesWithClamping(h[:32])
if err != nil {
return nil, err
}
A := (&edwards25519.Point{}).ScalarBaseMult(s)
mh := sha512.New()
mh.Write(h[32:])
mh.Write(scramble)
messageDigest := mh.Sum(nil)
r, err := edwards25519.NewScalar().SetUniformBytes(messageDigest)
if err != nil {
return nil, err
}
R := (&edwards25519.Point{}).ScalarBaseMult(r)
kh := sha512.New()
kh.Write(R.Bytes())
kh.Write(A.Bytes())
kh.Write(scramble)
hramDigest := kh.Sum(nil)
k, err := edwards25519.NewScalar().SetUniformBytes(hramDigest)
if err != nil {
return nil, err
}
S := k.MultiplyAdd(k, s, r)
return append(R.Bytes(), S.Bytes()...), nil
}
func (mc *mysqlConn) sendEncryptedPassword(seed []byte, pub *rsa.PublicKey) error { func (mc *mysqlConn) sendEncryptedPassword(seed []byte, pub *rsa.PublicKey) error {
enc, err := encryptPassword(mc.cfg.Passwd, seed, pub) enc, err := encryptPassword(mc.cfg.Passwd, seed, pub)
if err != nil { if err != nil {
...@@ -290,8 +331,14 @@ func (mc *mysqlConn) auth(authData []byte, plugin string) ([]byte, error) { ...@@ -290,8 +331,14 @@ func (mc *mysqlConn) auth(authData []byte, plugin string) ([]byte, error) {
enc, err := encryptPassword(mc.cfg.Passwd, authData, pubKey) enc, err := encryptPassword(mc.cfg.Passwd, authData, pubKey)
return enc, err return enc, err
case "client_ed25519":
if len(authData) != 32 {
return nil, ErrMalformPkt
}
return authEd25519(authData, mc.cfg.Passwd)
default: default:
errLog.Print("unknown auth plugin:", plugin) mc.cfg.Logger.Print("unknown auth plugin:", plugin)
return nil, ErrUnknownPlugin return nil, ErrUnknownPlugin
} }
} }
...@@ -338,7 +385,7 @@ func (mc *mysqlConn) handleAuthResult(oldAuthData []byte, plugin string) error { ...@@ -338,7 +385,7 @@ func (mc *mysqlConn) handleAuthResult(oldAuthData []byte, plugin string) error {
switch plugin { switch plugin {
// https://insidemysql.com/preparing-your-community-connector-for-mysql-8-part-2-sha256/ // https://dev.mysql.com/blog-archive/preparing-your-community-connector-for-mysql-8-part-2-sha256/
case "caching_sha2_password": case "caching_sha2_password":
switch len(authData) { switch len(authData) {
case 0: case 0:
...@@ -346,7 +393,7 @@ func (mc *mysqlConn) handleAuthResult(oldAuthData []byte, plugin string) error { ...@@ -346,7 +393,7 @@ func (mc *mysqlConn) handleAuthResult(oldAuthData []byte, plugin string) error {
case 1: case 1:
switch authData[0] { switch authData[0] {
case cachingSha2PasswordFastAuthSuccess: case cachingSha2PasswordFastAuthSuccess:
if err = mc.readResultOK(); err == nil { if err = mc.resultUnchanged().readResultOK(); err == nil {
return nil // auth successful return nil // auth successful
} }
...@@ -376,13 +423,13 @@ func (mc *mysqlConn) handleAuthResult(oldAuthData []byte, plugin string) error { ...@@ -376,13 +423,13 @@ func (mc *mysqlConn) handleAuthResult(oldAuthData []byte, plugin string) error {
} }
if data[0] != iAuthMoreData { if data[0] != iAuthMoreData {
return fmt.Errorf("unexpect resp from server for caching_sha2_password perform full authentication") return fmt.Errorf("unexpected resp from server for caching_sha2_password, perform full authentication")
} }
// parse public key // parse public key
block, rest := pem.Decode(data[1:]) block, rest := pem.Decode(data[1:])
if block == nil { if block == nil {
return fmt.Errorf("No Pem data found, data: %s", rest) return fmt.Errorf("no pem data found, data: %s", rest)
} }
pkix, err := x509.ParsePKIXPublicKey(block.Bytes) pkix, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil { if err != nil {
...@@ -397,7 +444,7 @@ func (mc *mysqlConn) handleAuthResult(oldAuthData []byte, plugin string) error { ...@@ -397,7 +444,7 @@ func (mc *mysqlConn) handleAuthResult(oldAuthData []byte, plugin string) error {
return err return err
} }
} }
return mc.readResultOK() return mc.resultUnchanged().readResultOK()
default: default:
return ErrMalformPkt return ErrMalformPkt
...@@ -426,7 +473,7 @@ func (mc *mysqlConn) handleAuthResult(oldAuthData []byte, plugin string) error { ...@@ -426,7 +473,7 @@ func (mc *mysqlConn) handleAuthResult(oldAuthData []byte, plugin string) error {
if err != nil { if err != nil {
return err return err
} }
return mc.readResultOK() return mc.resultUnchanged().readResultOK()
} }
default: default:
......
...@@ -1328,3 +1328,54 @@ func TestAuthSwitchSHA256PasswordSecure(t *testing.T) { ...@@ -1328,3 +1328,54 @@ func TestAuthSwitchSHA256PasswordSecure(t *testing.T) {
t.Errorf("got unexpected data: %v", conn.written) t.Errorf("got unexpected data: %v", conn.written)
} }
} }
// Derived from https://github.com/MariaDB/server/blob/6b2287fff23fbdc362499501c562f01d0d2db52e/plugin/auth_ed25519/ed25519-t.c
func TestEd25519Auth(t *testing.T) {
conn, mc := newRWMockConn(1)
mc.cfg.User = "root"
mc.cfg.Passwd = "foobar"
authData := []byte("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")
plugin := "client_ed25519"
// Send Client Authentication Packet
authResp, err := mc.auth(authData, plugin)
if err != nil {
t.Fatal(err)
}
err = mc.writeHandshakeResponsePacket(authResp, plugin)
if err != nil {
t.Fatal(err)
}
// check written auth response
authRespStart := 4 + 4 + 4 + 1 + 23 + len(mc.cfg.User) + 1
authRespEnd := authRespStart + 1 + len(authResp)
writtenAuthRespLen := conn.written[authRespStart]
writtenAuthResp := conn.written[authRespStart+1 : authRespEnd]
expectedAuthResp := []byte{
232, 61, 201, 63, 67, 63, 51, 53, 86, 73, 238, 35, 170, 117, 146,
214, 26, 17, 35, 9, 8, 132, 245, 141, 48, 99, 66, 58, 36, 228, 48,
84, 115, 254, 187, 168, 88, 162, 249, 57, 35, 85, 79, 238, 167, 106,
68, 117, 56, 135, 171, 47, 20, 14, 133, 79, 15, 229, 124, 160, 176,
100, 138, 14,
}
if writtenAuthRespLen != 64 {
t.Fatalf("expected 64 bytes from client, got %d", writtenAuthRespLen)
}
if !bytes.Equal(writtenAuthResp, expectedAuthResp) {
t.Fatalf("auth response did not match expected value:\n%v\n%v", writtenAuthResp, expectedAuthResp)
}
conn.written = nil
// auth response
conn.data = []byte{
7, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, // OK
}
conn.maxReads = 1
// Handle response to auth packet
if err := mc.handleAuthResult(authData, plugin); err != nil {
t.Errorf("got error: %v", err)
}
}
...@@ -48,7 +48,7 @@ func (tb *TB) checkStmt(stmt *sql.Stmt, err error) *sql.Stmt { ...@@ -48,7 +48,7 @@ func (tb *TB) checkStmt(stmt *sql.Stmt, err error) *sql.Stmt {
func initDB(b *testing.B, queries ...string) *sql.DB { func initDB(b *testing.B, queries ...string) *sql.DB {
tb := (*TB)(b) tb := (*TB)(b)
db := tb.checkDB(sql.Open("mysql", dsn)) db := tb.checkDB(sql.Open(driverNameTest, dsn))
for _, query := range queries { for _, query := range queries {
if _, err := db.Exec(query); err != nil { if _, err := db.Exec(query); err != nil {
b.Fatalf("error on %q: %v", query, err) b.Fatalf("error on %q: %v", query, err)
...@@ -105,7 +105,7 @@ func BenchmarkExec(b *testing.B) { ...@@ -105,7 +105,7 @@ func BenchmarkExec(b *testing.B) {
tb := (*TB)(b) tb := (*TB)(b)
b.StopTimer() b.StopTimer()
b.ReportAllocs() b.ReportAllocs()
db := tb.checkDB(sql.Open("mysql", dsn)) db := tb.checkDB(sql.Open(driverNameTest, dsn))
db.SetMaxIdleConns(concurrencyLevel) db.SetMaxIdleConns(concurrencyLevel)
defer db.Close() defer db.Close()
...@@ -151,7 +151,7 @@ func BenchmarkRoundtripTxt(b *testing.B) { ...@@ -151,7 +151,7 @@ func BenchmarkRoundtripTxt(b *testing.B) {
sampleString := string(sample) sampleString := string(sample)
b.ReportAllocs() b.ReportAllocs()
tb := (*TB)(b) tb := (*TB)(b)
db := tb.checkDB(sql.Open("mysql", dsn)) db := tb.checkDB(sql.Open(driverNameTest, dsn))
defer db.Close() defer db.Close()
b.StartTimer() b.StartTimer()
var result string var result string
...@@ -184,7 +184,7 @@ func BenchmarkRoundtripBin(b *testing.B) { ...@@ -184,7 +184,7 @@ func BenchmarkRoundtripBin(b *testing.B) {
sample, min, max := initRoundtripBenchmarks() sample, min, max := initRoundtripBenchmarks()
b.ReportAllocs() b.ReportAllocs()
tb := (*TB)(b) tb := (*TB)(b)
db := tb.checkDB(sql.Open("mysql", dsn)) db := tb.checkDB(sql.Open(driverNameTest, dsn))
defer db.Close() defer db.Close()
stmt := tb.checkStmt(db.Prepare("SELECT ?")) stmt := tb.checkStmt(db.Prepare("SELECT ?"))
defer stmt.Close() defer stmt.Close()
...@@ -372,3 +372,59 @@ func BenchmarkQueryRawBytes(b *testing.B) { ...@@ -372,3 +372,59 @@ func BenchmarkQueryRawBytes(b *testing.B) {
}) })
} }
} }
// BenchmarkReceiveMassiveRows measures performance of receiving large number of rows.
func BenchmarkReceiveMassiveRows(b *testing.B) {
// Setup -- prepare 10000 rows.
db := initDB(b,
"DROP TABLE IF EXISTS foo",
"CREATE TABLE foo (id INT PRIMARY KEY, val TEXT)")
defer db.Close()
sval := strings.Repeat("x", 50)
stmt, err := db.Prepare(`INSERT INTO foo (id, val) VALUES (?, ?)` + strings.Repeat(",(?,?)", 99))
if err != nil {
b.Errorf("failed to prepare query: %v", err)
return
}
for i := 0; i < 10000; i += 100 {
args := make([]any, 200)
for j := 0; j < 100; j++ {
args[j*2] = i + j
args[j*2+1] = sval
}
_, err := stmt.Exec(args...)
if err != nil {
b.Error(err)
return
}
}
stmt.Close()
// Use b.Run() to skip expensive setup.
b.Run("query", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
rows, err := db.Query(`SELECT id, val FROM foo`)
if err != nil {
b.Errorf("failed to select: %v", err)
return
}
for rows.Next() {
var i int
var s sql.RawBytes
err = rows.Scan(&i, &s)
if err != nil {
b.Errorf("failed to scan: %v", err)
_ = rows.Close()
return
}
}
if err = rows.Err(); err != nil {
b.Errorf("failed to read rows: %v", err)
}
_ = rows.Close()
}
})
}
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
package mysql package mysql
const defaultCollation = "utf8mb4_general_ci" const defaultCollation = "utf8mb4_general_ci"
const binaryCollation = "binary" const binaryCollationID = 63
// A list of available collations mapped to the internal ID. // A list of available collations mapped to the internal ID.
// To update this map use the following MySQL query: // To update this map use the following MySQL query:
......
...@@ -17,7 +17,7 @@ import ( ...@@ -17,7 +17,7 @@ import (
) )
func TestStaleConnectionChecks(t *testing.T) { func TestStaleConnectionChecks(t *testing.T) {
runTests(t, dsn, func(dbt *DBTest) { runTestsParallel(t, dsn, func(dbt *DBTest, _ string) {
dbt.mustExec("SET @@SESSION.wait_timeout = 2") dbt.mustExec("SET @@SESSION.wait_timeout = 2")
if err := dbt.db.Ping(); err != nil { if err := dbt.db.Ping(); err != nil {
......
...@@ -24,9 +24,9 @@ type mysqlConn struct { ...@@ -24,9 +24,9 @@ type mysqlConn struct {
buf buffer buf buffer
netConn net.Conn netConn net.Conn
rawConn net.Conn // underlying connection when netConn is TLS connection. rawConn net.Conn // underlying connection when netConn is TLS connection.
affectedRows uint64 result mysqlResult // managed by clearResult() and handleOkPacket().
insertId uint64
cfg *Config cfg *Config
connector *connector
maxAllowedPacket int maxAllowedPacket int
maxWriteSize int maxWriteSize int
writeTimeout time.Duration writeTimeout time.Duration
...@@ -34,7 +34,6 @@ type mysqlConn struct { ...@@ -34,7 +34,6 @@ type mysqlConn struct {
status statusFlag status statusFlag
sequence uint8 sequence uint8
parseTime bool parseTime bool
reset bool // set when the Go SQL package calls ResetSession
// for context support (Go 1.8+) // for context support (Go 1.8+)
watching bool watching bool
...@@ -48,14 +47,19 @@ type mysqlConn struct { ...@@ -48,14 +47,19 @@ type mysqlConn struct {
// Handles parameters set in DSN after the connection is established // Handles parameters set in DSN after the connection is established
func (mc *mysqlConn) handleParams() (err error) { func (mc *mysqlConn) handleParams() (err error) {
var cmdSet strings.Builder var cmdSet strings.Builder
for param, val := range mc.cfg.Params { for param, val := range mc.cfg.Params {
switch param { switch param {
// Charset: character_set_connection, character_set_client, character_set_results // Charset: character_set_connection, character_set_client, character_set_results
case "charset": case "charset":
charsets := strings.Split(val, ",") charsets := strings.Split(val, ",")
for i := range charsets { for _, cs := range charsets {
// ignore errors here - a charset may not exist // ignore errors here - a charset may not exist
err = mc.exec("SET NAMES " + charsets[i]) if mc.cfg.Collation != "" {
err = mc.exec("SET NAMES " + cs + " COLLATE " + mc.cfg.Collation)
} else {
err = mc.exec("SET NAMES " + cs)
}
if err == nil { if err == nil {
break break
} }
...@@ -68,7 +72,7 @@ func (mc *mysqlConn) handleParams() (err error) { ...@@ -68,7 +72,7 @@ func (mc *mysqlConn) handleParams() (err error) {
default: default:
if cmdSet.Len() == 0 { if cmdSet.Len() == 0 {
// Heuristic: 29 chars for each other key=value to reduce reallocations // Heuristic: 29 chars for each other key=value to reduce reallocations
cmdSet.Grow(4 + len(param) + 1 + len(val) + 30*(len(mc.cfg.Params)-1)) cmdSet.Grow(4 + len(param) + 3 + len(val) + 30*(len(mc.cfg.Params)-1))
cmdSet.WriteString("SET ") cmdSet.WriteString("SET ")
} else { } else {
cmdSet.WriteString(", ") cmdSet.WriteString(", ")
...@@ -105,7 +109,7 @@ func (mc *mysqlConn) Begin() (driver.Tx, error) { ...@@ -105,7 +109,7 @@ func (mc *mysqlConn) Begin() (driver.Tx, error) {
func (mc *mysqlConn) begin(readOnly bool) (driver.Tx, error) { func (mc *mysqlConn) begin(readOnly bool) (driver.Tx, error) {
if mc.closed.Load() { if mc.closed.Load() {
errLog.Print(ErrInvalidConn) mc.cfg.Logger.Print(ErrInvalidConn)
return nil, driver.ErrBadConn return nil, driver.ErrBadConn
} }
var q string var q string
...@@ -147,8 +151,9 @@ func (mc *mysqlConn) cleanup() { ...@@ -147,8 +151,9 @@ func (mc *mysqlConn) cleanup() {
return return
} }
if err := mc.netConn.Close(); err != nil { if err := mc.netConn.Close(); err != nil {
errLog.Print(err) mc.cfg.Logger.Print(err)
} }
mc.clearResult()
} }
func (mc *mysqlConn) error() error { func (mc *mysqlConn) error() error {
...@@ -163,14 +168,14 @@ func (mc *mysqlConn) error() error { ...@@ -163,14 +168,14 @@ func (mc *mysqlConn) error() error {
func (mc *mysqlConn) Prepare(query string) (driver.Stmt, error) { func (mc *mysqlConn) Prepare(query string) (driver.Stmt, error) {
if mc.closed.Load() { if mc.closed.Load() {
errLog.Print(ErrInvalidConn) mc.cfg.Logger.Print(ErrInvalidConn)
return nil, driver.ErrBadConn return nil, driver.ErrBadConn
} }
// Send command // Send command
err := mc.writeCommandPacketStr(comStmtPrepare, query) err := mc.writeCommandPacketStr(comStmtPrepare, query)
if err != nil { if err != nil {
// STMT_PREPARE is safe to retry. So we can return ErrBadConn here. // STMT_PREPARE is safe to retry. So we can return ErrBadConn here.
errLog.Print(err) mc.cfg.Logger.Print(err)
return nil, driver.ErrBadConn return nil, driver.ErrBadConn
} }
...@@ -204,7 +209,7 @@ func (mc *mysqlConn) interpolateParams(query string, args []driver.Value) (strin ...@@ -204,7 +209,7 @@ func (mc *mysqlConn) interpolateParams(query string, args []driver.Value) (strin
buf, err := mc.buf.takeCompleteBuffer() buf, err := mc.buf.takeCompleteBuffer()
if err != nil { if err != nil {
// can not take the buffer. Something must be wrong with the connection // can not take the buffer. Something must be wrong with the connection
errLog.Print(err) mc.cfg.Logger.Print(err)
return "", ErrInvalidConn return "", ErrInvalidConn
} }
buf = buf[:0] buf = buf[:0]
...@@ -246,7 +251,7 @@ func (mc *mysqlConn) interpolateParams(query string, args []driver.Value) (strin ...@@ -246,7 +251,7 @@ func (mc *mysqlConn) interpolateParams(query string, args []driver.Value) (strin
buf = append(buf, "'0000-00-00'"...) buf = append(buf, "'0000-00-00'"...)
} else { } else {
buf = append(buf, '\'') buf = append(buf, '\'')
buf, err = appendDateTime(buf, v.In(mc.cfg.Loc)) buf, err = appendDateTime(buf, v.In(mc.cfg.Loc), mc.cfg.timeTruncate)
if err != nil { if err != nil {
return "", err return "", err
} }
...@@ -296,7 +301,7 @@ func (mc *mysqlConn) interpolateParams(query string, args []driver.Value) (strin ...@@ -296,7 +301,7 @@ func (mc *mysqlConn) interpolateParams(query string, args []driver.Value) (strin
func (mc *mysqlConn) Exec(query string, args []driver.Value) (driver.Result, error) { func (mc *mysqlConn) Exec(query string, args []driver.Value) (driver.Result, error) {
if mc.closed.Load() { if mc.closed.Load() {
errLog.Print(ErrInvalidConn) mc.cfg.Logger.Print(ErrInvalidConn)
return nil, driver.ErrBadConn return nil, driver.ErrBadConn
} }
if len(args) != 0 { if len(args) != 0 {
...@@ -310,28 +315,25 @@ func (mc *mysqlConn) Exec(query string, args []driver.Value) (driver.Result, err ...@@ -310,28 +315,25 @@ func (mc *mysqlConn) Exec(query string, args []driver.Value) (driver.Result, err
} }
query = prepared query = prepared
} }
mc.affectedRows = 0
mc.insertId = 0
err := mc.exec(query) err := mc.exec(query)
if err == nil { if err == nil {
return &mysqlResult{ copied := mc.result
affectedRows: int64(mc.affectedRows), return &copied, err
insertId: int64(mc.insertId),
}, err
} }
return nil, mc.markBadConn(err) return nil, mc.markBadConn(err)
} }
// Internal function to execute commands // Internal function to execute commands
func (mc *mysqlConn) exec(query string) error { func (mc *mysqlConn) exec(query string) error {
handleOk := mc.clearResult()
// Send command // Send command
if err := mc.writeCommandPacketStr(comQuery, query); err != nil { if err := mc.writeCommandPacketStr(comQuery, query); err != nil {
return mc.markBadConn(err) return mc.markBadConn(err)
} }
// Read Result // Read Result
resLen, err := mc.readResultSetHeaderPacket() resLen, err := handleOk.readResultSetHeaderPacket()
if err != nil { if err != nil {
return err return err
} }
...@@ -348,7 +350,7 @@ func (mc *mysqlConn) exec(query string) error { ...@@ -348,7 +350,7 @@ func (mc *mysqlConn) exec(query string) error {
} }
} }
return mc.discardResults() return handleOk.discardResults()
} }
func (mc *mysqlConn) Query(query string, args []driver.Value) (driver.Rows, error) { func (mc *mysqlConn) Query(query string, args []driver.Value) (driver.Rows, error) {
...@@ -356,8 +358,10 @@ func (mc *mysqlConn) Query(query string, args []driver.Value) (driver.Rows, erro ...@@ -356,8 +358,10 @@ func (mc *mysqlConn) Query(query string, args []driver.Value) (driver.Rows, erro
} }
func (mc *mysqlConn) query(query string, args []driver.Value) (*textRows, error) { func (mc *mysqlConn) query(query string, args []driver.Value) (*textRows, error) {
handleOk := mc.clearResult()
if mc.closed.Load() { if mc.closed.Load() {
errLog.Print(ErrInvalidConn) mc.cfg.Logger.Print(ErrInvalidConn)
return nil, driver.ErrBadConn return nil, driver.ErrBadConn
} }
if len(args) != 0 { if len(args) != 0 {
...@@ -376,7 +380,7 @@ func (mc *mysqlConn) query(query string, args []driver.Value) (*textRows, error) ...@@ -376,7 +380,7 @@ func (mc *mysqlConn) query(query string, args []driver.Value) (*textRows, error)
if err == nil { if err == nil {
// Read Result // Read Result
var resLen int var resLen int
resLen, err = mc.readResultSetHeaderPacket() resLen, err = handleOk.readResultSetHeaderPacket()
if err == nil { if err == nil {
rows := new(textRows) rows := new(textRows)
rows.mc = mc rows.mc = mc
...@@ -404,12 +408,13 @@ func (mc *mysqlConn) query(query string, args []driver.Value) (*textRows, error) ...@@ -404,12 +408,13 @@ func (mc *mysqlConn) query(query string, args []driver.Value) (*textRows, error)
// The returned byte slice is only valid until the next read // The returned byte slice is only valid until the next read
func (mc *mysqlConn) getSystemVar(name string) ([]byte, error) { func (mc *mysqlConn) getSystemVar(name string) ([]byte, error) {
// Send command // Send command
handleOk := mc.clearResult()
if err := mc.writeCommandPacketStr(comQuery, "SELECT @@"+name); err != nil { if err := mc.writeCommandPacketStr(comQuery, "SELECT @@"+name); err != nil {
return nil, err return nil, err
} }
// Read Result // Read Result
resLen, err := mc.readResultSetHeaderPacket() resLen, err := handleOk.readResultSetHeaderPacket()
if err == nil { if err == nil {
rows := new(textRows) rows := new(textRows)
rows.mc = mc rows.mc = mc
...@@ -451,7 +456,7 @@ func (mc *mysqlConn) finish() { ...@@ -451,7 +456,7 @@ func (mc *mysqlConn) finish() {
// Ping implements driver.Pinger interface // Ping implements driver.Pinger interface
func (mc *mysqlConn) Ping(ctx context.Context) (err error) { func (mc *mysqlConn) Ping(ctx context.Context) (err error) {
if mc.closed.Load() { if mc.closed.Load() {
errLog.Print(ErrInvalidConn) mc.cfg.Logger.Print(ErrInvalidConn)
return driver.ErrBadConn return driver.ErrBadConn
} }
...@@ -460,11 +465,12 @@ func (mc *mysqlConn) Ping(ctx context.Context) (err error) { ...@@ -460,11 +465,12 @@ func (mc *mysqlConn) Ping(ctx context.Context) (err error) {
} }
defer mc.finish() defer mc.finish()
handleOk := mc.clearResult()
if err = mc.writeCommandPacket(comPing); err != nil { if err = mc.writeCommandPacket(comPing); err != nil {
return mc.markBadConn(err) return mc.markBadConn(err)
} }
return mc.readResultOK() return handleOk.readResultOK()
} }
// BeginTx implements driver.ConnBeginTx interface // BeginTx implements driver.ConnBeginTx interface
...@@ -639,7 +645,31 @@ func (mc *mysqlConn) ResetSession(ctx context.Context) error { ...@@ -639,7 +645,31 @@ func (mc *mysqlConn) ResetSession(ctx context.Context) error {
if mc.closed.Load() { if mc.closed.Load() {
return driver.ErrBadConn return driver.ErrBadConn
} }
mc.reset = true
// Perform a stale connection check. We only perform this check for
// the first query on a connection that has been checked out of the
// connection pool: a fresh connection from the pool is more likely
// to be stale, and it has not performed any previous writes that
// could cause data corruption, so it's safe to return ErrBadConn
// if the check fails.
if mc.cfg.CheckConnLiveness {
conn := mc.netConn
if mc.rawConn != nil {
conn = mc.rawConn
}
var err error
if mc.cfg.ReadTimeout != 0 {
err = conn.SetReadDeadline(time.Now().Add(mc.cfg.ReadTimeout))
}
if err == nil {
err = connCheck(conn)
}
if err != nil {
mc.cfg.Logger.Print("closing bad idle connection: ", err)
return driver.ErrBadConn
}
}
return nil return nil
} }
......
...@@ -179,6 +179,7 @@ func TestPingErrInvalidConn(t *testing.T) { ...@@ -179,6 +179,7 @@ func TestPingErrInvalidConn(t *testing.T) {
buf: newBuffer(nc), buf: newBuffer(nc),
maxAllowedPacket: defaultMaxAllowedPacket, maxAllowedPacket: defaultMaxAllowedPacket,
closech: make(chan struct{}), closech: make(chan struct{}),
cfg: NewConfig(),
} }
err := ms.Ping(context.Background()) err := ms.Ping(context.Background())
......
...@@ -12,10 +12,53 @@ import ( ...@@ -12,10 +12,53 @@ import (
"context" "context"
"database/sql/driver" "database/sql/driver"
"net" "net"
"os"
"strconv"
"strings"
) )
type connector struct { type connector struct {
cfg *Config // immutable private copy. cfg *Config // immutable private copy.
encodedAttributes string // Encoded connection attributes.
}
func encodeConnectionAttributes(cfg *Config) string {
connAttrsBuf := make([]byte, 0)
// default connection attributes
connAttrsBuf = appendLengthEncodedString(connAttrsBuf, connAttrClientName)
connAttrsBuf = appendLengthEncodedString(connAttrsBuf, connAttrClientNameValue)
connAttrsBuf = appendLengthEncodedString(connAttrsBuf, connAttrOS)
connAttrsBuf = appendLengthEncodedString(connAttrsBuf, connAttrOSValue)
connAttrsBuf = appendLengthEncodedString(connAttrsBuf, connAttrPlatform)
connAttrsBuf = appendLengthEncodedString(connAttrsBuf, connAttrPlatformValue)
connAttrsBuf = appendLengthEncodedString(connAttrsBuf, connAttrPid)
connAttrsBuf = appendLengthEncodedString(connAttrsBuf, strconv.Itoa(os.Getpid()))
serverHost, _, _ := net.SplitHostPort(cfg.Addr)
if serverHost != "" {
connAttrsBuf = appendLengthEncodedString(connAttrsBuf, connAttrServerHost)
connAttrsBuf = appendLengthEncodedString(connAttrsBuf, serverHost)
}
// user-defined connection attributes
for _, connAttr := range strings.Split(cfg.ConnectionAttributes, ",") {
k, v, found := strings.Cut(connAttr, ":")
if !found {
continue
}
connAttrsBuf = appendLengthEncodedString(connAttrsBuf, k)
connAttrsBuf = appendLengthEncodedString(connAttrsBuf, v)
}
return string(connAttrsBuf)
}
func newConnector(cfg *Config) *connector {
encodedAttributes := encodeConnectionAttributes(cfg)
return &connector{
cfg: cfg,
encodedAttributes: encodedAttributes,
}
} }
// Connect implements driver.Connector interface. // Connect implements driver.Connector interface.
...@@ -23,12 +66,23 @@ type connector struct { ...@@ -23,12 +66,23 @@ type connector struct {
func (c *connector) Connect(ctx context.Context) (driver.Conn, error) { func (c *connector) Connect(ctx context.Context) (driver.Conn, error) {
var err error var err error
// Invoke beforeConnect if present, with a copy of the configuration
cfg := c.cfg
if c.cfg.beforeConnect != nil {
cfg = c.cfg.Clone()
err = c.cfg.beforeConnect(ctx, cfg)
if err != nil {
return nil, err
}
}
// New mysqlConn // New mysqlConn
mc := &mysqlConn{ mc := &mysqlConn{
maxAllowedPacket: maxPacketSize, maxAllowedPacket: maxPacketSize,
maxWriteSize: maxPacketSize - 1, maxWriteSize: maxPacketSize - 1,
closech: make(chan struct{}), closech: make(chan struct{}),
cfg: c.cfg, cfg: cfg,
connector: c,
} }
mc.parseTime = mc.cfg.ParseTime mc.parseTime = mc.cfg.ParseTime
...@@ -56,10 +110,7 @@ func (c *connector) Connect(ctx context.Context) (driver.Conn, error) { ...@@ -56,10 +110,7 @@ func (c *connector) Connect(ctx context.Context) (driver.Conn, error) {
// Enable TCP Keepalives on TCP connections // Enable TCP Keepalives on TCP connections
if tc, ok := mc.netConn.(*net.TCPConn); ok { if tc, ok := mc.netConn.(*net.TCPConn); ok {
if err := tc.SetKeepAlive(true); err != nil { if err := tc.SetKeepAlive(true); err != nil {
// Don't send COM_QUIT before handshake. c.cfg.Logger.Print(err)
mc.netConn.Close()
mc.netConn = nil
return nil, err
} }
} }
...@@ -92,7 +143,7 @@ func (c *connector) Connect(ctx context.Context) (driver.Conn, error) { ...@@ -92,7 +143,7 @@ func (c *connector) Connect(ctx context.Context) (driver.Conn, error) {
authResp, err := mc.auth(authData, plugin) authResp, err := mc.auth(authData, plugin)
if err != nil { if err != nil {
// try the default auth plugin, if using the requested plugin failed // try the default auth plugin, if using the requested plugin failed
errLog.Print("could not use requested auth plugin '"+plugin+"': ", err.Error()) c.cfg.Logger.Print("could not use requested auth plugin '"+plugin+"': ", err.Error())
plugin = defaultAuthPlugin plugin = defaultAuthPlugin
authResp, err = mc.auth(authData, plugin) authResp, err = mc.auth(authData, plugin)
if err != nil { if err != nil {
......
...@@ -8,11 +8,11 @@ import ( ...@@ -8,11 +8,11 @@ import (
) )
func TestConnectorReturnsTimeout(t *testing.T) { func TestConnectorReturnsTimeout(t *testing.T) {
connector := &connector{&Config{ connector := newConnector(&Config{
Net: "tcp", Net: "tcp",
Addr: "1.1.1.1:1234", Addr: "1.1.1.1:1234",
Timeout: 10 * time.Millisecond, Timeout: 10 * time.Millisecond,
}} })
_, err := connector.Connect(context.Background()) _, err := connector.Connect(context.Background())
if err == nil { if err == nil {
......
...@@ -8,12 +8,25 @@ ...@@ -8,12 +8,25 @@
package mysql package mysql
import "runtime"
const ( const (
defaultAuthPlugin = "mysql_native_password" defaultAuthPlugin = "mysql_native_password"
defaultMaxAllowedPacket = 64 << 20 // 64 MiB. See https://github.com/go-sql-driver/mysql/issues/1355 defaultMaxAllowedPacket = 64 << 20 // 64 MiB. See https://github.com/go-sql-driver/mysql/issues/1355
minProtocolVersion = 10 minProtocolVersion = 10
maxPacketSize = 1<<24 - 1 maxPacketSize = 1<<24 - 1
timeFormat = "2006-01-02 15:04:05.999999" timeFormat = "2006-01-02 15:04:05.999999"
// Connection attributes
// See https://dev.mysql.com/doc/refman/8.0/en/performance-schema-connection-attribute-tables.html#performance-schema-connection-attributes-available
connAttrClientName = "_client_name"
connAttrClientNameValue = "Go-MySQL-Driver"
connAttrOS = "_os"
connAttrOSValue = runtime.GOOS
connAttrPlatform = "_platform"
connAttrPlatformValue = runtime.GOARCH
connAttrPid = "_pid"
connAttrServerHost = "_server_host"
) )
// MySQL constants documentation: // MySQL constants documentation:
......
...@@ -55,6 +55,15 @@ func RegisterDialContext(net string, dial DialContextFunc) { ...@@ -55,6 +55,15 @@ func RegisterDialContext(net string, dial DialContextFunc) {
dials[net] = dial dials[net] = dial
} }
// DeregisterDialContext removes the custom dial function registered with the given net.
func DeregisterDialContext(net string) {
dialsLock.Lock()
defer dialsLock.Unlock()
if dials != nil {
delete(dials, net)
}
}
// RegisterDial registers a custom dial function. It can then be used by the // RegisterDial registers a custom dial function. It can then be used by the
// network address mynet(addr), where mynet is the registered new network. // network address mynet(addr), where mynet is the registered new network.
// addr is passed as a parameter to the dial function. // addr is passed as a parameter to the dial function.
...@@ -74,14 +83,18 @@ func (d MySQLDriver) Open(dsn string) (driver.Conn, error) { ...@@ -74,14 +83,18 @@ func (d MySQLDriver) Open(dsn string) (driver.Conn, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
c := &connector{ c := newConnector(cfg)
cfg: cfg,
}
return c.Connect(context.Background()) return c.Connect(context.Background())
} }
// This variable can be replaced with -ldflags like below:
// go build "-ldflags=-X github.com/go-sql-driver/mysql.driverName=custom"
var driverName = "mysql"
func init() { func init() {
sql.Register("mysql", &MySQLDriver{}) if driverName != "" {
sql.Register(driverName, &MySQLDriver{})
}
} }
// NewConnector returns new driver.Connector. // NewConnector returns new driver.Connector.
...@@ -92,7 +105,7 @@ func NewConnector(cfg *Config) (driver.Connector, error) { ...@@ -92,7 +105,7 @@ func NewConnector(cfg *Config) (driver.Connector, error) {
if err := cfg.normalize(); err != nil { if err := cfg.normalize(); err != nil {
return nil, err return nil, err
} }
return &connector{cfg: cfg}, nil return newConnector(cfg), nil
} }
// OpenConnector implements driver.DriverContext. // OpenConnector implements driver.DriverContext.
...@@ -101,7 +114,5 @@ func (d MySQLDriver) OpenConnector(dsn string) (driver.Connector, error) { ...@@ -101,7 +114,5 @@ func (d MySQLDriver) OpenConnector(dsn string) (driver.Connector, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &connector{ return newConnector(cfg), nil
cfg: cfg,
}, nil
} }
...@@ -11,13 +11,13 @@ package mysql ...@@ -11,13 +11,13 @@ package mysql
import ( import (
"bytes" "bytes"
"context" "context"
"crypto/rand"
"crypto/tls" "crypto/tls"
"database/sql" "database/sql"
"database/sql/driver" "database/sql/driver"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"log" "log"
"math" "math"
"net" "net"
...@@ -25,6 +25,7 @@ import ( ...@@ -25,6 +25,7 @@ import (
"os" "os"
"reflect" "reflect"
"runtime" "runtime"
"strconv"
"strings" "strings"
"sync" "sync"
"sync/atomic" "sync/atomic"
...@@ -32,6 +33,16 @@ import ( ...@@ -32,6 +33,16 @@ import (
"time" "time"
) )
// This variable can be replaced with -ldflags like below:
// go test "-ldflags=-X github.com/go-sql-driver/mysql.driverNameTest=custom"
var driverNameTest string
func init() {
if driverNameTest == "" {
driverNameTest = driverName
}
}
// Ensure that all the driver interfaces are implemented // Ensure that all the driver interfaces are implemented
var ( var (
_ driver.Rows = &binaryRows{} _ driver.Rows = &binaryRows{}
...@@ -83,7 +94,7 @@ func init() { ...@@ -83,7 +94,7 @@ func init() {
} }
type DBTest struct { type DBTest struct {
*testing.T testing.TB
db *sql.DB db *sql.DB
} }
...@@ -112,12 +123,14 @@ func runTestsWithMultiStatement(t *testing.T, dsn string, tests ...func(dbt *DBT ...@@ -112,12 +123,14 @@ func runTestsWithMultiStatement(t *testing.T, dsn string, tests ...func(dbt *DBT
dsn += "&multiStatements=true" dsn += "&multiStatements=true"
var db *sql.DB var db *sql.DB
if _, err := ParseDSN(dsn); err != errInvalidDSNUnsafeCollation { if _, err := ParseDSN(dsn); err != errInvalidDSNUnsafeCollation {
db, err = sql.Open("mysql", dsn) db, err = sql.Open(driverNameTest, dsn)
if err != nil { if err != nil {
t.Fatalf("error connecting: %s", err.Error()) t.Fatalf("error connecting: %s", err.Error())
} }
defer db.Close() defer db.Close()
} }
// Previous test may be skipped without dropping the test table
db.Exec("DROP TABLE IF EXISTS test")
dbt := &DBTest{t, db} dbt := &DBTest{t, db}
for _, test := range tests { for _, test := range tests {
...@@ -131,52 +144,103 @@ func runTests(t *testing.T, dsn string, tests ...func(dbt *DBTest)) { ...@@ -131,52 +144,103 @@ func runTests(t *testing.T, dsn string, tests ...func(dbt *DBTest)) {
t.Skipf("MySQL server not running on %s", netAddr) t.Skipf("MySQL server not running on %s", netAddr)
} }
db, err := sql.Open("mysql", dsn) db, err := sql.Open(driverNameTest, dsn)
if err != nil { if err != nil {
t.Fatalf("error connecting: %s", err.Error()) t.Fatalf("error connecting: %s", err.Error())
} }
defer db.Close() defer db.Close()
cleanup := func() {
db.Exec("DROP TABLE IF EXISTS test") db.Exec("DROP TABLE IF EXISTS test")
}
dsn2 := dsn + "&interpolateParams=true" dsn2 := dsn + "&interpolateParams=true"
var db2 *sql.DB var db2 *sql.DB
if _, err := ParseDSN(dsn2); err != errInvalidDSNUnsafeCollation { if _, err := ParseDSN(dsn2); err != errInvalidDSNUnsafeCollation {
db2, err = sql.Open("mysql", dsn2) db2, err = sql.Open(driverNameTest, dsn2)
if err != nil { if err != nil {
t.Fatalf("error connecting: %s", err.Error()) t.Fatalf("error connecting: %s", err.Error())
} }
defer db2.Close() defer db2.Close()
} }
dsn3 := dsn + "&multiStatements=true" for _, test := range tests {
var db3 *sql.DB test := test
if _, err := ParseDSN(dsn3); err != errInvalidDSNUnsafeCollation { t.Run("default", func(t *testing.T) {
db3, err = sql.Open("mysql", dsn3) dbt := &DBTest{t, db}
t.Cleanup(cleanup)
test(dbt)
})
if db2 != nil {
t.Run("interpolateParams", func(t *testing.T) {
dbt2 := &DBTest{t, db2}
t.Cleanup(cleanup)
test(dbt2)
})
}
}
}
// runTestsParallel runs the tests in parallel with a separate database connection for each test.
func runTestsParallel(t *testing.T, dsn string, tests ...func(dbt *DBTest, tableName string)) {
if !available {
t.Skipf("MySQL server not running on %s", netAddr)
}
newTableName := func(t *testing.T) string {
t.Helper()
var buf [8]byte
if _, err := rand.Read(buf[:]); err != nil {
t.Fatal(err)
}
return fmt.Sprintf("test_%x", buf[:])
}
t.Parallel()
for _, test := range tests {
test := test
t.Run("default", func(t *testing.T) {
t.Parallel()
tableName := newTableName(t)
db, err := sql.Open("mysql", dsn)
if err != nil { if err != nil {
t.Fatalf("error connecting: %s", err.Error()) t.Fatalf("error connecting: %s", err.Error())
} }
defer db3.Close() t.Cleanup(func() {
} db.Exec("DROP TABLE IF EXISTS " + tableName)
db.Close()
})
dbt := &DBTest{t, db} dbt := &DBTest{t, db}
dbt2 := &DBTest{t, db2} test(dbt, tableName)
dbt3 := &DBTest{t, db3} })
for _, test := range tests {
test(dbt) dsn2 := dsn + "&interpolateParams=true"
dbt.db.Exec("DROP TABLE IF EXISTS test") if _, err := ParseDSN(dsn2); err == errInvalidDSNUnsafeCollation {
if db2 != nil { t.Run("interpolateParams", func(t *testing.T) {
test(dbt2) t.Parallel()
dbt2.db.Exec("DROP TABLE IF EXISTS test")
tableName := newTableName(t)
db, err := sql.Open("mysql", dsn2)
if err != nil {
t.Fatalf("error connecting: %s", err.Error())
} }
if db3 != nil { t.Cleanup(func() {
test(dbt3) db.Exec("DROP TABLE IF EXISTS " + tableName)
dbt3.db.Exec("DROP TABLE IF EXISTS test") db.Close()
})
dbt := &DBTest{t, db}
test(dbt, tableName)
})
} }
} }
} }
func (dbt *DBTest) fail(method, query string, err error) { func (dbt *DBTest) fail(method, query string, err error) {
dbt.Helper()
if len(query) > 300 { if len(query) > 300 {
query = "[query too large to print]" query = "[query too large to print]"
} }
...@@ -184,6 +248,7 @@ func (dbt *DBTest) fail(method, query string, err error) { ...@@ -184,6 +248,7 @@ func (dbt *DBTest) fail(method, query string, err error) {
} }
func (dbt *DBTest) mustExec(query string, args ...interface{}) (res sql.Result) { func (dbt *DBTest) mustExec(query string, args ...interface{}) (res sql.Result) {
dbt.Helper()
res, err := dbt.db.Exec(query, args...) res, err := dbt.db.Exec(query, args...)
if err != nil { if err != nil {
dbt.fail("exec", query, err) dbt.fail("exec", query, err)
...@@ -192,6 +257,7 @@ func (dbt *DBTest) mustExec(query string, args ...interface{}) (res sql.Result) ...@@ -192,6 +257,7 @@ func (dbt *DBTest) mustExec(query string, args ...interface{}) (res sql.Result)
} }
func (dbt *DBTest) mustQuery(query string, args ...interface{}) (rows *sql.Rows) { func (dbt *DBTest) mustQuery(query string, args ...interface{}) (rows *sql.Rows) {
dbt.Helper()
rows, err := dbt.db.Query(query, args...) rows, err := dbt.db.Query(query, args...)
if err != nil { if err != nil {
dbt.fail("query", query, err) dbt.fail("query", query, err)
...@@ -211,7 +277,7 @@ func maybeSkip(t *testing.T, err error, skipErrno uint16) { ...@@ -211,7 +277,7 @@ func maybeSkip(t *testing.T, err error, skipErrno uint16) {
} }
func TestEmptyQuery(t *testing.T) { func TestEmptyQuery(t *testing.T) {
runTests(t, dsn, func(dbt *DBTest) { runTestsParallel(t, dsn, func(dbt *DBTest, _ string) {
// just a comment, no query // just a comment, no query
rows := dbt.mustQuery("--") rows := dbt.mustQuery("--")
defer rows.Close() defer rows.Close()
...@@ -223,20 +289,20 @@ func TestEmptyQuery(t *testing.T) { ...@@ -223,20 +289,20 @@ func TestEmptyQuery(t *testing.T) {
} }
func TestCRUD(t *testing.T) { func TestCRUD(t *testing.T) {
runTests(t, dsn, func(dbt *DBTest) { runTestsParallel(t, dsn, func(dbt *DBTest, tbl string) {
// Create Table // Create Table
dbt.mustExec("CREATE TABLE test (value BOOL)") dbt.mustExec("CREATE TABLE " + tbl + " (value BOOL)")
// Test for unexpected data // Test for unexpected data
var out bool var out bool
rows := dbt.mustQuery("SELECT * FROM test") rows := dbt.mustQuery("SELECT * FROM " + tbl)
if rows.Next() { if rows.Next() {
dbt.Error("unexpected data in empty table") dbt.Error("unexpected data in empty table")
} }
rows.Close() rows.Close()
// Create Data // Create Data
res := dbt.mustExec("INSERT INTO test VALUES (1)") res := dbt.mustExec("INSERT INTO " + tbl + " VALUES (1)")
count, err := res.RowsAffected() count, err := res.RowsAffected()
if err != nil { if err != nil {
dbt.Fatalf("res.RowsAffected() returned error: %s", err.Error()) dbt.Fatalf("res.RowsAffected() returned error: %s", err.Error())
...@@ -254,7 +320,7 @@ func TestCRUD(t *testing.T) { ...@@ -254,7 +320,7 @@ func TestCRUD(t *testing.T) {
} }
// Read // Read
rows = dbt.mustQuery("SELECT value FROM test") rows = dbt.mustQuery("SELECT value FROM " + tbl)
if rows.Next() { if rows.Next() {
rows.Scan(&out) rows.Scan(&out)
if true != out { if true != out {
...@@ -270,7 +336,7 @@ func TestCRUD(t *testing.T) { ...@@ -270,7 +336,7 @@ func TestCRUD(t *testing.T) {
rows.Close() rows.Close()
// Update // Update
res = dbt.mustExec("UPDATE test SET value = ? WHERE value = ?", false, true) res = dbt.mustExec("UPDATE "+tbl+" SET value = ? WHERE value = ?", false, true)
count, err = res.RowsAffected() count, err = res.RowsAffected()
if err != nil { if err != nil {
dbt.Fatalf("res.RowsAffected() returned error: %s", err.Error()) dbt.Fatalf("res.RowsAffected() returned error: %s", err.Error())
...@@ -280,7 +346,7 @@ func TestCRUD(t *testing.T) { ...@@ -280,7 +346,7 @@ func TestCRUD(t *testing.T) {
} }
// Check Update // Check Update
rows = dbt.mustQuery("SELECT value FROM test") rows = dbt.mustQuery("SELECT value FROM " + tbl)
if rows.Next() { if rows.Next() {
rows.Scan(&out) rows.Scan(&out)
if false != out { if false != out {
...@@ -296,7 +362,7 @@ func TestCRUD(t *testing.T) { ...@@ -296,7 +362,7 @@ func TestCRUD(t *testing.T) {
rows.Close() rows.Close()
// Delete // Delete
res = dbt.mustExec("DELETE FROM test WHERE value = ?", false) res = dbt.mustExec("DELETE FROM "+tbl+" WHERE value = ?", false)
count, err = res.RowsAffected() count, err = res.RowsAffected()
if err != nil { if err != nil {
dbt.Fatalf("res.RowsAffected() returned error: %s", err.Error()) dbt.Fatalf("res.RowsAffected() returned error: %s", err.Error())
...@@ -306,7 +372,7 @@ func TestCRUD(t *testing.T) { ...@@ -306,7 +372,7 @@ func TestCRUD(t *testing.T) {
} }
// Check for unexpected rows // Check for unexpected rows
res = dbt.mustExec("DELETE FROM test") res = dbt.mustExec("DELETE FROM " + tbl)
count, err = res.RowsAffected() count, err = res.RowsAffected()
if err != nil { if err != nil {
dbt.Fatalf("res.RowsAffected() returned error: %s", err.Error()) dbt.Fatalf("res.RowsAffected() returned error: %s", err.Error())
...@@ -317,6 +383,51 @@ func TestCRUD(t *testing.T) { ...@@ -317,6 +383,51 @@ func TestCRUD(t *testing.T) {
}) })
} }
// TestNumbers test that selecting numeric columns.
// Both of textRows and binaryRows should return same type and value.
func TestNumbersToAny(t *testing.T) {
runTestsParallel(t, dsn, func(dbt *DBTest, tbl string) {
dbt.mustExec("CREATE TABLE " + tbl + " (id INT PRIMARY KEY, b BOOL, i8 TINYINT, " +
"i16 SMALLINT, i32 INT, i64 BIGINT, f32 FLOAT, f64 DOUBLE, iu32 INT UNSIGNED)")
dbt.mustExec("INSERT INTO " + tbl + " VALUES (1, true, 127, 32767, 2147483647, 9223372036854775807, 1.25, 2.5, 4294967295)")
// Use binaryRows for interpolateParams=false and textRows for interpolateParams=true.
rows := dbt.mustQuery("SELECT b, i8, i16, i32, i64, f32, f64, iu32 FROM "+tbl+" WHERE id=?", 1)
if !rows.Next() {
dbt.Fatal("no data")
}
var b, i8, i16, i32, i64, f32, f64, iu32 any
err := rows.Scan(&b, &i8, &i16, &i32, &i64, &f32, &f64, &iu32)
if err != nil {
dbt.Fatal(err)
}
if b.(int64) != 1 {
dbt.Errorf("b != 1")
}
if i8.(int64) != 127 {
dbt.Errorf("i8 != 127")
}
if i16.(int64) != 32767 {
dbt.Errorf("i16 != 32767")
}
if i32.(int64) != 2147483647 {
dbt.Errorf("i32 != 2147483647")
}
if i64.(int64) != 9223372036854775807 {
dbt.Errorf("i64 != 9223372036854775807")
}
if f32.(float32) != 1.25 {
dbt.Errorf("f32 != 1.25")
}
if f64.(float64) != 2.5 {
dbt.Errorf("f64 != 2.5")
}
if iu32.(int64) != 4294967295 {
dbt.Errorf("iu32 != 4294967295")
}
})
}
func TestMultiQuery(t *testing.T) { func TestMultiQuery(t *testing.T) {
runTestsWithMultiStatement(t, dsn, func(dbt *DBTest) { runTestsWithMultiStatement(t, dsn, func(dbt *DBTest) {
// Create Table // Create Table
...@@ -347,8 +458,8 @@ func TestMultiQuery(t *testing.T) { ...@@ -347,8 +458,8 @@ func TestMultiQuery(t *testing.T) {
rows := dbt.mustQuery("SELECT value FROM test WHERE id=1;") rows := dbt.mustQuery("SELECT value FROM test WHERE id=1;")
if rows.Next() { if rows.Next() {
rows.Scan(&out) rows.Scan(&out)
if 5 != out { if out != 5 {
dbt.Errorf("5 != %d", out) dbt.Errorf("expected 5, got %d", out)
} }
if rows.Next() { if rows.Next() {
...@@ -363,7 +474,7 @@ func TestMultiQuery(t *testing.T) { ...@@ -363,7 +474,7 @@ func TestMultiQuery(t *testing.T) {
} }
func TestInt(t *testing.T) { func TestInt(t *testing.T) {
runTests(t, dsn, func(dbt *DBTest) { runTestsParallel(t, dsn, func(dbt *DBTest, tbl string) {
types := [5]string{"TINYINT", "SMALLINT", "MEDIUMINT", "INT", "BIGINT"} types := [5]string{"TINYINT", "SMALLINT", "MEDIUMINT", "INT", "BIGINT"}
in := int64(42) in := int64(42)
var out int64 var out int64
...@@ -371,11 +482,11 @@ func TestInt(t *testing.T) { ...@@ -371,11 +482,11 @@ func TestInt(t *testing.T) {
// SIGNED // SIGNED
for _, v := range types { for _, v := range types {
dbt.mustExec("CREATE TABLE test (value " + v + ")") dbt.mustExec("CREATE TABLE " + tbl + " (value " + v + ")")
dbt.mustExec("INSERT INTO test VALUES (?)", in) dbt.mustExec("INSERT INTO "+tbl+" VALUES (?)", in)
rows = dbt.mustQuery("SELECT value FROM test") rows = dbt.mustQuery("SELECT value FROM " + tbl)
if rows.Next() { if rows.Next() {
rows.Scan(&out) rows.Scan(&out)
if in != out { if in != out {
...@@ -386,16 +497,16 @@ func TestInt(t *testing.T) { ...@@ -386,16 +497,16 @@ func TestInt(t *testing.T) {
} }
rows.Close() rows.Close()
dbt.mustExec("DROP TABLE IF EXISTS test") dbt.mustExec("DROP TABLE IF EXISTS " + tbl)
} }
// UNSIGNED ZEROFILL // UNSIGNED ZEROFILL
for _, v := range types { for _, v := range types {
dbt.mustExec("CREATE TABLE test (value " + v + " ZEROFILL)") dbt.mustExec("CREATE TABLE " + tbl + " (value " + v + " ZEROFILL)")
dbt.mustExec("INSERT INTO test VALUES (?)", in) dbt.mustExec("INSERT INTO "+tbl+" VALUES (?)", in)
rows = dbt.mustQuery("SELECT value FROM test") rows = dbt.mustQuery("SELECT value FROM " + tbl)
if rows.Next() { if rows.Next() {
rows.Scan(&out) rows.Scan(&out)
if in != out { if in != out {
...@@ -406,21 +517,21 @@ func TestInt(t *testing.T) { ...@@ -406,21 +517,21 @@ func TestInt(t *testing.T) {
} }
rows.Close() rows.Close()
dbt.mustExec("DROP TABLE IF EXISTS test") dbt.mustExec("DROP TABLE IF EXISTS " + tbl)
} }
}) })
} }
func TestFloat32(t *testing.T) { func TestFloat32(t *testing.T) {
runTests(t, dsn, func(dbt *DBTest) { runTestsParallel(t, dsn, func(dbt *DBTest, tbl string) {
types := [2]string{"FLOAT", "DOUBLE"} types := [2]string{"FLOAT", "DOUBLE"}
in := float32(42.23) in := float32(42.23)
var out float32 var out float32
var rows *sql.Rows var rows *sql.Rows
for _, v := range types { for _, v := range types {
dbt.mustExec("CREATE TABLE test (value " + v + ")") dbt.mustExec("CREATE TABLE " + tbl + " (value " + v + ")")
dbt.mustExec("INSERT INTO test VALUES (?)", in) dbt.mustExec("INSERT INTO "+tbl+" VALUES (?)", in)
rows = dbt.mustQuery("SELECT value FROM test") rows = dbt.mustQuery("SELECT value FROM " + tbl)
if rows.Next() { if rows.Next() {
rows.Scan(&out) rows.Scan(&out)
if in != out { if in != out {
...@@ -430,21 +541,21 @@ func TestFloat32(t *testing.T) { ...@@ -430,21 +541,21 @@ func TestFloat32(t *testing.T) {
dbt.Errorf("%s: no data", v) dbt.Errorf("%s: no data", v)
} }
rows.Close() rows.Close()
dbt.mustExec("DROP TABLE IF EXISTS test") dbt.mustExec("DROP TABLE IF EXISTS " + tbl)
} }
}) })
} }
func TestFloat64(t *testing.T) { func TestFloat64(t *testing.T) {
runTests(t, dsn, func(dbt *DBTest) { runTestsParallel(t, dsn, func(dbt *DBTest, tbl string) {
types := [2]string{"FLOAT", "DOUBLE"} types := [2]string{"FLOAT", "DOUBLE"}
var expected float64 = 42.23 var expected float64 = 42.23
var out float64 var out float64
var rows *sql.Rows var rows *sql.Rows
for _, v := range types { for _, v := range types {
dbt.mustExec("CREATE TABLE test (value " + v + ")") dbt.mustExec("CREATE TABLE " + tbl + " (value " + v + ")")
dbt.mustExec("INSERT INTO test VALUES (42.23)") dbt.mustExec("INSERT INTO " + tbl + " VALUES (42.23)")
rows = dbt.mustQuery("SELECT value FROM test") rows = dbt.mustQuery("SELECT value FROM " + tbl)
if rows.Next() { if rows.Next() {
rows.Scan(&out) rows.Scan(&out)
if expected != out { if expected != out {
...@@ -454,21 +565,21 @@ func TestFloat64(t *testing.T) { ...@@ -454,21 +565,21 @@ func TestFloat64(t *testing.T) {
dbt.Errorf("%s: no data", v) dbt.Errorf("%s: no data", v)
} }
rows.Close() rows.Close()
dbt.mustExec("DROP TABLE IF EXISTS test") dbt.mustExec("DROP TABLE IF EXISTS " + tbl)
} }
}) })
} }
func TestFloat64Placeholder(t *testing.T) { func TestFloat64Placeholder(t *testing.T) {
runTests(t, dsn, func(dbt *DBTest) { runTestsParallel(t, dsn, func(dbt *DBTest, tbl string) {
types := [2]string{"FLOAT", "DOUBLE"} types := [2]string{"FLOAT", "DOUBLE"}
var expected float64 = 42.23 var expected float64 = 42.23
var out float64 var out float64
var rows *sql.Rows var rows *sql.Rows
for _, v := range types { for _, v := range types {
dbt.mustExec("CREATE TABLE test (id int, value " + v + ")") dbt.mustExec("CREATE TABLE " + tbl + " (id int, value " + v + ")")
dbt.mustExec("INSERT INTO test VALUES (1, 42.23)") dbt.mustExec("INSERT INTO " + tbl + " VALUES (1, 42.23)")
rows = dbt.mustQuery("SELECT value FROM test WHERE id = ?", 1) rows = dbt.mustQuery("SELECT value FROM "+tbl+" WHERE id = ?", 1)
if rows.Next() { if rows.Next() {
rows.Scan(&out) rows.Scan(&out)
if expected != out { if expected != out {
...@@ -478,24 +589,24 @@ func TestFloat64Placeholder(t *testing.T) { ...@@ -478,24 +589,24 @@ func TestFloat64Placeholder(t *testing.T) {
dbt.Errorf("%s: no data", v) dbt.Errorf("%s: no data", v)
} }
rows.Close() rows.Close()
dbt.mustExec("DROP TABLE IF EXISTS test") dbt.mustExec("DROP TABLE IF EXISTS " + tbl)
} }
}) })
} }
func TestString(t *testing.T) { func TestString(t *testing.T) {
runTests(t, dsn, func(dbt *DBTest) { runTestsParallel(t, dsn, func(dbt *DBTest, tbl string) {
types := [6]string{"CHAR(255)", "VARCHAR(255)", "TINYTEXT", "TEXT", "MEDIUMTEXT", "LONGTEXT"} types := [6]string{"CHAR(255)", "VARCHAR(255)", "TINYTEXT", "TEXT", "MEDIUMTEXT", "LONGTEXT"}
in := "κόσμε üöäßñóùéàâÿœ'îë Árvíztűrő いろはにほへとちりぬるを イロハニホヘト דג סקרן чащах น่าฟังเอย" in := "κόσμε üöäßñóùéàâÿœ'îë Árvíztűrő いろはにほへとちりぬるを イロハニホヘト דג סקרן чащах น่าฟังเอย"
var out string var out string
var rows *sql.Rows var rows *sql.Rows
for _, v := range types { for _, v := range types {
dbt.mustExec("CREATE TABLE test (value " + v + ") CHARACTER SET utf8") dbt.mustExec("CREATE TABLE " + tbl + " (value " + v + ") CHARACTER SET utf8")
dbt.mustExec("INSERT INTO test VALUES (?)", in) dbt.mustExec("INSERT INTO "+tbl+" VALUES (?)", in)
rows = dbt.mustQuery("SELECT value FROM test") rows = dbt.mustQuery("SELECT value FROM " + tbl)
if rows.Next() { if rows.Next() {
rows.Scan(&out) rows.Scan(&out)
if in != out { if in != out {
...@@ -506,11 +617,11 @@ func TestString(t *testing.T) { ...@@ -506,11 +617,11 @@ func TestString(t *testing.T) {
} }
rows.Close() rows.Close()
dbt.mustExec("DROP TABLE IF EXISTS test") dbt.mustExec("DROP TABLE IF EXISTS " + tbl)
} }
// BLOB // BLOB
dbt.mustExec("CREATE TABLE test (id int, value BLOB) CHARACTER SET utf8") dbt.mustExec("CREATE TABLE " + tbl + " (id int, value BLOB) CHARACTER SET utf8")
id := 2 id := 2
in = "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, " + in = "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, " +
...@@ -521,9 +632,9 @@ func TestString(t *testing.T) { ...@@ -521,9 +632,9 @@ func TestString(t *testing.T) {
"sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, " + "sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, " +
"sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. " + "sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. " +
"Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet." "Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet."
dbt.mustExec("INSERT INTO test VALUES (?, ?)", id, in) dbt.mustExec("INSERT INTO "+tbl+" VALUES (?, ?)", id, in)
err := dbt.db.QueryRow("SELECT value FROM test WHERE id = ?", id).Scan(&out) err := dbt.db.QueryRow("SELECT value FROM "+tbl+" WHERE id = ?", id).Scan(&out)
if err != nil { if err != nil {
dbt.Fatalf("Error on BLOB-Query: %s", err.Error()) dbt.Fatalf("Error on BLOB-Query: %s", err.Error())
} else if out != in { } else if out != in {
...@@ -533,7 +644,7 @@ func TestString(t *testing.T) { ...@@ -533,7 +644,7 @@ func TestString(t *testing.T) {
} }
func TestRawBytes(t *testing.T) { func TestRawBytes(t *testing.T) {
runTests(t, dsn, func(dbt *DBTest) { runTestsParallel(t, dsn, func(dbt *DBTest, _ string) {
v1 := []byte("aaa") v1 := []byte("aaa")
v2 := []byte("bbb") v2 := []byte("bbb")
rows := dbt.mustQuery("SELECT ?, ?", v1, v2) rows := dbt.mustQuery("SELECT ?, ?", v1, v2)
...@@ -562,7 +673,7 @@ func TestRawBytes(t *testing.T) { ...@@ -562,7 +673,7 @@ func TestRawBytes(t *testing.T) {
} }
func TestRawMessage(t *testing.T) { func TestRawMessage(t *testing.T) {
runTests(t, dsn, func(dbt *DBTest) { runTestsParallel(t, dsn, func(dbt *DBTest, _ string) {
v1 := json.RawMessage("{}") v1 := json.RawMessage("{}")
v2 := json.RawMessage("[]") v2 := json.RawMessage("[]")
rows := dbt.mustQuery("SELECT ?, ?", v1, v2) rows := dbt.mustQuery("SELECT ?, ?", v1, v2)
...@@ -593,14 +704,14 @@ func (tv testValuer) Value() (driver.Value, error) { ...@@ -593,14 +704,14 @@ func (tv testValuer) Value() (driver.Value, error) {
} }
func TestValuer(t *testing.T) { func TestValuer(t *testing.T) {
runTests(t, dsn, func(dbt *DBTest) { runTestsParallel(t, dsn, func(dbt *DBTest, tbl string) {
in := testValuer{"a_value"} in := testValuer{"a_value"}
var out string var out string
var rows *sql.Rows var rows *sql.Rows
dbt.mustExec("CREATE TABLE test (value VARCHAR(255)) CHARACTER SET utf8") dbt.mustExec("CREATE TABLE " + tbl + " (value VARCHAR(255)) CHARACTER SET utf8")
dbt.mustExec("INSERT INTO test VALUES (?)", in) dbt.mustExec("INSERT INTO "+tbl+" VALUES (?)", in)
rows = dbt.mustQuery("SELECT value FROM test") rows = dbt.mustQuery("SELECT value FROM " + tbl)
if rows.Next() { if rows.Next() {
rows.Scan(&out) rows.Scan(&out)
if in.value != out { if in.value != out {
...@@ -610,8 +721,6 @@ func TestValuer(t *testing.T) { ...@@ -610,8 +721,6 @@ func TestValuer(t *testing.T) {
dbt.Errorf("Valuer: no data") dbt.Errorf("Valuer: no data")
} }
rows.Close() rows.Close()
dbt.mustExec("DROP TABLE IF EXISTS test")
}) })
} }
...@@ -628,15 +737,15 @@ func (tv testValuerWithValidation) Value() (driver.Value, error) { ...@@ -628,15 +737,15 @@ func (tv testValuerWithValidation) Value() (driver.Value, error) {
} }
func TestValuerWithValidation(t *testing.T) { func TestValuerWithValidation(t *testing.T) {
runTests(t, dsn, func(dbt *DBTest) { runTestsParallel(t, dsn, func(dbt *DBTest, tbl string) {
in := testValuerWithValidation{"a_value"} in := testValuerWithValidation{"a_value"}
var out string var out string
var rows *sql.Rows var rows *sql.Rows
dbt.mustExec("CREATE TABLE testValuer (value VARCHAR(255)) CHARACTER SET utf8") dbt.mustExec("CREATE TABLE " + tbl + " (value VARCHAR(255)) CHARACTER SET utf8")
dbt.mustExec("INSERT INTO testValuer VALUES (?)", in) dbt.mustExec("INSERT INTO "+tbl+" VALUES (?)", in)
rows = dbt.mustQuery("SELECT value FROM testValuer") rows = dbt.mustQuery("SELECT value FROM " + tbl)
defer rows.Close() defer rows.Close()
if rows.Next() { if rows.Next() {
...@@ -648,19 +757,17 @@ func TestValuerWithValidation(t *testing.T) { ...@@ -648,19 +757,17 @@ func TestValuerWithValidation(t *testing.T) {
dbt.Errorf("Valuer: no data") dbt.Errorf("Valuer: no data")
} }
if _, err := dbt.db.Exec("INSERT INTO testValuer VALUES (?)", testValuerWithValidation{""}); err == nil { if _, err := dbt.db.Exec("INSERT INTO "+tbl+" VALUES (?)", testValuerWithValidation{""}); err == nil {
dbt.Errorf("Failed to check valuer error") dbt.Errorf("Failed to check valuer error")
} }
if _, err := dbt.db.Exec("INSERT INTO testValuer VALUES (?)", nil); err != nil { if _, err := dbt.db.Exec("INSERT INTO "+tbl+" VALUES (?)", nil); err != nil {
dbt.Errorf("Failed to check nil") dbt.Errorf("Failed to check nil")
} }
if _, err := dbt.db.Exec("INSERT INTO testValuer VALUES (?)", map[string]bool{}); err == nil { if _, err := dbt.db.Exec("INSERT INTO "+tbl+" VALUES (?)", map[string]bool{}); err == nil {
dbt.Errorf("Failed to check not valuer") dbt.Errorf("Failed to check not valuer")
} }
dbt.mustExec("DROP TABLE IF EXISTS testValuer")
}) })
} }
...@@ -894,7 +1001,7 @@ func TestTimestampMicros(t *testing.T) { ...@@ -894,7 +1001,7 @@ func TestTimestampMicros(t *testing.T) {
f0 := format[:19] f0 := format[:19]
f1 := format[:21] f1 := format[:21]
f6 := format[:26] f6 := format[:26]
runTests(t, dsn, func(dbt *DBTest) { runTestsParallel(t, dsn, func(dbt *DBTest, tbl string) {
// check if microseconds are supported. // check if microseconds are supported.
// Do not use timestamp(x) for that check - before 5.5.6, x would mean display width // Do not use timestamp(x) for that check - before 5.5.6, x would mean display width
// and not precision. // and not precision.
...@@ -909,7 +1016,7 @@ func TestTimestampMicros(t *testing.T) { ...@@ -909,7 +1016,7 @@ func TestTimestampMicros(t *testing.T) {
return return
} }
_, err := dbt.db.Exec(` _, err := dbt.db.Exec(`
CREATE TABLE test ( CREATE TABLE ` + tbl + ` (
value0 TIMESTAMP NOT NULL DEFAULT '` + f0 + `', value0 TIMESTAMP NOT NULL DEFAULT '` + f0 + `',
value1 TIMESTAMP(1) NOT NULL DEFAULT '` + f1 + `', value1 TIMESTAMP(1) NOT NULL DEFAULT '` + f1 + `',
value6 TIMESTAMP(6) NOT NULL DEFAULT '` + f6 + `' value6 TIMESTAMP(6) NOT NULL DEFAULT '` + f6 + `'
...@@ -918,10 +1025,10 @@ func TestTimestampMicros(t *testing.T) { ...@@ -918,10 +1025,10 @@ func TestTimestampMicros(t *testing.T) {
if err != nil { if err != nil {
dbt.Error(err) dbt.Error(err)
} }
defer dbt.mustExec("DROP TABLE IF EXISTS test") defer dbt.mustExec("DROP TABLE IF EXISTS " + tbl)
dbt.mustExec("INSERT INTO test SET value0=?, value1=?, value6=?", f0, f1, f6) dbt.mustExec("INSERT INTO "+tbl+" SET value0=?, value1=?, value6=?", f0, f1, f6)
var res0, res1, res6 string var res0, res1, res6 string
rows := dbt.mustQuery("SELECT * FROM test") rows := dbt.mustQuery("SELECT * FROM " + tbl)
defer rows.Close() defer rows.Close()
if !rows.Next() { if !rows.Next() {
dbt.Errorf("test contained no selectable values") dbt.Errorf("test contained no selectable values")
...@@ -943,7 +1050,7 @@ func TestTimestampMicros(t *testing.T) { ...@@ -943,7 +1050,7 @@ func TestTimestampMicros(t *testing.T) {
} }
func TestNULL(t *testing.T) { func TestNULL(t *testing.T) {
runTests(t, dsn, func(dbt *DBTest) { runTestsParallel(t, dsn, func(dbt *DBTest, tbl string) {
nullStmt, err := dbt.db.Prepare("SELECT NULL") nullStmt, err := dbt.db.Prepare("SELECT NULL")
if err != nil { if err != nil {
dbt.Fatal(err) dbt.Fatal(err)
...@@ -1075,12 +1182,12 @@ func TestNULL(t *testing.T) { ...@@ -1075,12 +1182,12 @@ func TestNULL(t *testing.T) {
} }
// Insert NULL // Insert NULL
dbt.mustExec("CREATE TABLE test (dummmy1 int, value int, dummy2 int)") dbt.mustExec("CREATE TABLE " + tbl + " (dummmy1 int, value int, dummy2 int)")
dbt.mustExec("INSERT INTO test VALUES (?, ?, ?)", 1, nil, 2) dbt.mustExec("INSERT INTO "+tbl+" VALUES (?, ?, ?)", 1, nil, 2)
var out interface{} var out interface{}
rows := dbt.mustQuery("SELECT * FROM test") rows := dbt.mustQuery("SELECT * FROM " + tbl)
defer rows.Close() defer rows.Close()
if rows.Next() { if rows.Next() {
rows.Scan(&out) rows.Scan(&out)
...@@ -1104,7 +1211,7 @@ func TestUint64(t *testing.T) { ...@@ -1104,7 +1211,7 @@ func TestUint64(t *testing.T) {
shigh = int64(uhigh) shigh = int64(uhigh)
stop = ^shigh stop = ^shigh
) )
runTests(t, dsn, func(dbt *DBTest) { runTestsParallel(t, dsn, func(dbt *DBTest, _ string) {
stmt, err := dbt.db.Prepare(`SELECT ?, ?, ? ,?, ?, ?, ?, ?`) stmt, err := dbt.db.Prepare(`SELECT ?, ?, ? ,?, ?, ?, ?, ?`)
if err != nil { if err != nil {
dbt.Fatal(err) dbt.Fatal(err)
...@@ -1168,7 +1275,7 @@ func TestLongData(t *testing.T) { ...@@ -1168,7 +1275,7 @@ func TestLongData(t *testing.T) {
dbt.Fatalf("LONGBLOB: length in: %d, length out: %d", len(inS), len(out)) dbt.Fatalf("LONGBLOB: length in: %d, length out: %d", len(inS), len(out))
} }
if rows.Next() { if rows.Next() {
dbt.Error("LONGBLOB: unexpexted row") dbt.Error("LONGBLOB: unexpected row")
} }
} else { } else {
dbt.Fatalf("LONGBLOB: no data") dbt.Fatalf("LONGBLOB: no data")
...@@ -1187,7 +1294,7 @@ func TestLongData(t *testing.T) { ...@@ -1187,7 +1294,7 @@ func TestLongData(t *testing.T) {
dbt.Fatalf("LONGBLOB: length in: %d, length out: %d", len(in), len(out)) dbt.Fatalf("LONGBLOB: length in: %d, length out: %d", len(in), len(out))
} }
if rows.Next() { if rows.Next() {
dbt.Error("LONGBLOB: unexpexted row") dbt.Error("LONGBLOB: unexpected row")
} }
} else { } else {
if err = rows.Err(); err != nil { if err = rows.Err(); err != nil {
...@@ -1245,7 +1352,7 @@ func TestLoadData(t *testing.T) { ...@@ -1245,7 +1352,7 @@ func TestLoadData(t *testing.T) {
dbt.mustExec("CREATE TABLE test (id INT NOT NULL PRIMARY KEY, value TEXT NOT NULL) CHARACTER SET utf8") dbt.mustExec("CREATE TABLE test (id INT NOT NULL PRIMARY KEY, value TEXT NOT NULL) CHARACTER SET utf8")
// Local File // Local File
file, err := ioutil.TempFile("", "gotest") file, err := os.CreateTemp("", "gotest")
defer os.Remove(file.Name()) defer os.Remove(file.Name())
if err != nil { if err != nil {
dbt.Fatal(err) dbt.Fatal(err)
...@@ -1263,7 +1370,7 @@ func TestLoadData(t *testing.T) { ...@@ -1263,7 +1370,7 @@ func TestLoadData(t *testing.T) {
dbt.Fatalf("unexpected row count: got %d, want 0", count) dbt.Fatalf("unexpected row count: got %d, want 0", count)
} }
// Then fille File with data and try to load it // Then fill File with data and try to load it
file.WriteString("1\ta string\n2\ta string containing a \\t\n3\ta string containing a \\n\n4\ta string containing both \\t\\n\n") file.WriteString("1\ta string\n2\ta string containing a \\t\n3\ta string containing a \\n\n4\ta string containing both \\t\\n\n")
file.Close() file.Close()
dbt.mustExec(fmt.Sprintf("LOAD DATA LOCAL INFILE %q INTO TABLE test", file.Name())) dbt.mustExec(fmt.Sprintf("LOAD DATA LOCAL INFILE %q INTO TABLE test", file.Name()))
...@@ -1294,18 +1401,18 @@ func TestLoadData(t *testing.T) { ...@@ -1294,18 +1401,18 @@ func TestLoadData(t *testing.T) {
_, err = dbt.db.Exec("LOAD DATA LOCAL INFILE 'Reader::doesnotexist' INTO TABLE test") _, err = dbt.db.Exec("LOAD DATA LOCAL INFILE 'Reader::doesnotexist' INTO TABLE test")
if err == nil { if err == nil {
dbt.Fatal("load non-existent Reader didn't fail") dbt.Fatal("load non-existent Reader didn't fail")
} else if err.Error() != "Reader 'doesnotexist' is not registered" { } else if err.Error() != "reader 'doesnotexist' is not registered" {
dbt.Fatal(err.Error()) dbt.Fatal(err.Error())
} }
}) })
} }
func TestFoundRows(t *testing.T) { func TestFoundRows1(t *testing.T) {
runTests(t, dsn, func(dbt *DBTest) { runTestsParallel(t, dsn, func(dbt *DBTest, tbl string) {
dbt.mustExec("CREATE TABLE test (id INT NOT NULL ,data INT NOT NULL)") dbt.mustExec("CREATE TABLE " + tbl + " (id INT NOT NULL ,data INT NOT NULL)")
dbt.mustExec("INSERT INTO test (id, data) VALUES (0, 0),(0, 0),(1, 0),(1, 0),(1, 1)") dbt.mustExec("INSERT INTO " + tbl + " (id, data) VALUES (0, 0),(0, 0),(1, 0),(1, 0),(1, 1)")
res := dbt.mustExec("UPDATE test SET data = 1 WHERE id = 0") res := dbt.mustExec("UPDATE " + tbl + " SET data = 1 WHERE id = 0")
count, err := res.RowsAffected() count, err := res.RowsAffected()
if err != nil { if err != nil {
dbt.Fatalf("res.RowsAffected() returned error: %s", err.Error()) dbt.Fatalf("res.RowsAffected() returned error: %s", err.Error())
...@@ -1313,7 +1420,7 @@ func TestFoundRows(t *testing.T) { ...@@ -1313,7 +1420,7 @@ func TestFoundRows(t *testing.T) {
if count != 2 { if count != 2 {
dbt.Fatalf("Expected 2 affected rows, got %d", count) dbt.Fatalf("Expected 2 affected rows, got %d", count)
} }
res = dbt.mustExec("UPDATE test SET data = 1 WHERE id = 1") res = dbt.mustExec("UPDATE " + tbl + " SET data = 1 WHERE id = 1")
count, err = res.RowsAffected() count, err = res.RowsAffected()
if err != nil { if err != nil {
dbt.Fatalf("res.RowsAffected() returned error: %s", err.Error()) dbt.Fatalf("res.RowsAffected() returned error: %s", err.Error())
...@@ -1322,11 +1429,14 @@ func TestFoundRows(t *testing.T) { ...@@ -1322,11 +1429,14 @@ func TestFoundRows(t *testing.T) {
dbt.Fatalf("Expected 2 affected rows, got %d", count) dbt.Fatalf("Expected 2 affected rows, got %d", count)
} }
}) })
runTests(t, dsn+"&clientFoundRows=true", func(dbt *DBTest) { }
dbt.mustExec("CREATE TABLE test (id INT NOT NULL ,data INT NOT NULL)")
dbt.mustExec("INSERT INTO test (id, data) VALUES (0, 0),(0, 0),(1, 0),(1, 0),(1, 1)") func TestFoundRows2(t *testing.T) {
runTestsParallel(t, dsn+"&clientFoundRows=true", func(dbt *DBTest, tbl string) {
dbt.mustExec("CREATE TABLE " + tbl + " (id INT NOT NULL ,data INT NOT NULL)")
dbt.mustExec("INSERT INTO " + tbl + " (id, data) VALUES (0, 0),(0, 0),(1, 0),(1, 0),(1, 1)")
res := dbt.mustExec("UPDATE test SET data = 1 WHERE id = 0") res := dbt.mustExec("UPDATE " + tbl + " SET data = 1 WHERE id = 0")
count, err := res.RowsAffected() count, err := res.RowsAffected()
if err != nil { if err != nil {
dbt.Fatalf("res.RowsAffected() returned error: %s", err.Error()) dbt.Fatalf("res.RowsAffected() returned error: %s", err.Error())
...@@ -1334,7 +1444,7 @@ func TestFoundRows(t *testing.T) { ...@@ -1334,7 +1444,7 @@ func TestFoundRows(t *testing.T) {
if count != 2 { if count != 2 {
dbt.Fatalf("Expected 2 matched rows, got %d", count) dbt.Fatalf("Expected 2 matched rows, got %d", count)
} }
res = dbt.mustExec("UPDATE test SET data = 1 WHERE id = 1") res = dbt.mustExec("UPDATE " + tbl + " SET data = 1 WHERE id = 1")
count, err = res.RowsAffected() count, err = res.RowsAffected()
if err != nil { if err != nil {
dbt.Fatalf("res.RowsAffected() returned error: %s", err.Error()) dbt.Fatalf("res.RowsAffected() returned error: %s", err.Error())
...@@ -1402,6 +1512,7 @@ func TestReuseClosedConnection(t *testing.T) { ...@@ -1402,6 +1512,7 @@ func TestReuseClosedConnection(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("error preparing statement: %s", err.Error()) t.Fatalf("error preparing statement: %s", err.Error())
} }
//lint:ignore SA1019 this is a test
_, err = stmt.Exec(nil) _, err = stmt.Exec(nil)
if err != nil { if err != nil {
t.Fatalf("error executing statement: %s", err.Error()) t.Fatalf("error executing statement: %s", err.Error())
...@@ -1416,6 +1527,7 @@ func TestReuseClosedConnection(t *testing.T) { ...@@ -1416,6 +1527,7 @@ func TestReuseClosedConnection(t *testing.T) {
t.Errorf("panic after reusing a closed connection: %v", err) t.Errorf("panic after reusing a closed connection: %v", err)
} }
}() }()
//lint:ignore SA1019 this is a test
_, err = stmt.Exec(nil) _, err = stmt.Exec(nil)
if err != nil && err != driver.ErrBadConn { if err != nil && err != driver.ErrBadConn {
t.Errorf("unexpected error '%s', expected '%s'", t.Errorf("unexpected error '%s', expected '%s'",
...@@ -1458,7 +1570,7 @@ func TestCharset(t *testing.T) { ...@@ -1458,7 +1570,7 @@ func TestCharset(t *testing.T) {
} }
func TestFailingCharset(t *testing.T) { func TestFailingCharset(t *testing.T) {
runTests(t, dsn+"&charset=none", func(dbt *DBTest) { runTestsParallel(t, dsn+"&charset=none", func(dbt *DBTest, _ string) {
// run query to really establish connection... // run query to really establish connection...
_, err := dbt.db.Exec("SELECT 1") _, err := dbt.db.Exec("SELECT 1")
if err == nil { if err == nil {
...@@ -1507,7 +1619,7 @@ func TestCollation(t *testing.T) { ...@@ -1507,7 +1619,7 @@ func TestCollation(t *testing.T) {
} }
func TestColumnsWithAlias(t *testing.T) { func TestColumnsWithAlias(t *testing.T) {
runTests(t, dsn+"&columnsWithAlias=true", func(dbt *DBTest) { runTestsParallel(t, dsn+"&columnsWithAlias=true", func(dbt *DBTest, _ string) {
rows := dbt.mustQuery("SELECT 1 AS A") rows := dbt.mustQuery("SELECT 1 AS A")
defer rows.Close() defer rows.Close()
cols, _ := rows.Columns() cols, _ := rows.Columns()
...@@ -1531,7 +1643,7 @@ func TestColumnsWithAlias(t *testing.T) { ...@@ -1531,7 +1643,7 @@ func TestColumnsWithAlias(t *testing.T) {
} }
func TestRawBytesResultExceedsBuffer(t *testing.T) { func TestRawBytesResultExceedsBuffer(t *testing.T) {
runTests(t, dsn, func(dbt *DBTest) { runTestsParallel(t, dsn, func(dbt *DBTest, _ string) {
// defaultBufSize from buffer.go // defaultBufSize from buffer.go
expected := strings.Repeat("abc", defaultBufSize) expected := strings.Repeat("abc", defaultBufSize)
...@@ -1590,7 +1702,7 @@ func TestTimezoneConversion(t *testing.T) { ...@@ -1590,7 +1702,7 @@ func TestTimezoneConversion(t *testing.T) {
// Special cases // Special cases
func TestRowsClose(t *testing.T) { func TestRowsClose(t *testing.T) {
runTests(t, dsn, func(dbt *DBTest) { runTestsParallel(t, dsn, func(dbt *DBTest, _ string) {
rows, err := dbt.db.Query("SELECT 1") rows, err := dbt.db.Query("SELECT 1")
if err != nil { if err != nil {
dbt.Fatal(err) dbt.Fatal(err)
...@@ -1615,7 +1727,7 @@ func TestRowsClose(t *testing.T) { ...@@ -1615,7 +1727,7 @@ func TestRowsClose(t *testing.T) {
// dangling statements // dangling statements
// http://code.google.com/p/go/issues/detail?id=3865 // http://code.google.com/p/go/issues/detail?id=3865
func TestCloseStmtBeforeRows(t *testing.T) { func TestCloseStmtBeforeRows(t *testing.T) {
runTests(t, dsn, func(dbt *DBTest) { runTestsParallel(t, dsn, func(dbt *DBTest, _ string) {
stmt, err := dbt.db.Prepare("SELECT 1") stmt, err := dbt.db.Prepare("SELECT 1")
if err != nil { if err != nil {
dbt.Fatal(err) dbt.Fatal(err)
...@@ -1656,7 +1768,7 @@ func TestCloseStmtBeforeRows(t *testing.T) { ...@@ -1656,7 +1768,7 @@ func TestCloseStmtBeforeRows(t *testing.T) {
// It is valid to have multiple Rows for the same Stmt // It is valid to have multiple Rows for the same Stmt
// http://code.google.com/p/go/issues/detail?id=3734 // http://code.google.com/p/go/issues/detail?id=3734
func TestStmtMultiRows(t *testing.T) { func TestStmtMultiRows(t *testing.T) {
runTests(t, dsn, func(dbt *DBTest) { runTestsParallel(t, dsn, func(dbt *DBTest, _ string) {
stmt, err := dbt.db.Prepare("SELECT 1 UNION SELECT 0") stmt, err := dbt.db.Prepare("SELECT 1 UNION SELECT 0")
if err != nil { if err != nil {
dbt.Fatal(err) dbt.Fatal(err)
...@@ -1807,13 +1919,13 @@ func TestConcurrent(t *testing.T) { ...@@ -1807,13 +1919,13 @@ func TestConcurrent(t *testing.T) {
} }
runTests(t, dsn, func(dbt *DBTest) { runTests(t, dsn, func(dbt *DBTest) {
var version string // var version string
if err := dbt.db.QueryRow("SELECT @@version").Scan(&version); err != nil { // if err := dbt.db.QueryRow("SELECT @@version").Scan(&version); err != nil {
dbt.Fatalf("%s", err.Error()) // dbt.Fatal(err)
} // }
if strings.Contains(strings.ToLower(version), "mariadb") { // if strings.Contains(strings.ToLower(version), "mariadb") {
t.Skip(`TODO: "fix commands out of sync. Did you run multiple statements at once?" on MariaDB`) // t.Skip(`TODO: "fix commands out of sync. Did you run multiple statements at once?" on MariaDB`)
} // }
var max int var max int
err := dbt.db.QueryRow("SELECT @@max_connections").Scan(&max) err := dbt.db.QueryRow("SELECT @@max_connections").Scan(&max)
...@@ -1840,7 +1952,6 @@ func TestConcurrent(t *testing.T) { ...@@ -1840,7 +1952,6 @@ func TestConcurrent(t *testing.T) {
defer wg.Done() defer wg.Done()
tx, err := dbt.db.Begin() tx, err := dbt.db.Begin()
atomic.AddInt32(&remaining, -1)
if err != nil { if err != nil {
if err.Error() != "Error 1040: Too many connections" { if err.Error() != "Error 1040: Too many connections" {
...@@ -1850,7 +1961,7 @@ func TestConcurrent(t *testing.T) { ...@@ -1850,7 +1961,7 @@ func TestConcurrent(t *testing.T) {
} }
// keep the connection busy until all connections are open // keep the connection busy until all connections are open
for remaining > 0 { for atomic.AddInt32(&remaining, -1) > 0 {
if _, err = tx.Exec("DO 1"); err != nil { if _, err = tx.Exec("DO 1"); err != nil {
fatalf("error on conn %d: %s", id, err.Error()) fatalf("error on conn %d: %s", id, err.Error())
return return
...@@ -1867,7 +1978,7 @@ func TestConcurrent(t *testing.T) { ...@@ -1867,7 +1978,7 @@ func TestConcurrent(t *testing.T) {
}(i) }(i)
} }
// wait until all conections are open // wait until all connections are open
wg.Wait() wg.Wait()
if fatalError != "" { if fatalError != "" {
...@@ -1883,7 +1994,7 @@ func testDialError(t *testing.T, dialErr error, expectErr error) { ...@@ -1883,7 +1994,7 @@ func testDialError(t *testing.T, dialErr error, expectErr error) {
return nil, dialErr return nil, dialErr
}) })
db, err := sql.Open("mysql", fmt.Sprintf("%s:%s@mydial(%s)/%s?timeout=30s", user, pass, addr, dbname)) db, err := sql.Open(driverNameTest, fmt.Sprintf("%s:%s@mydial(%s)/%s?timeout=30s", user, pass, addr, dbname))
if err != nil { if err != nil {
t.Fatalf("error connecting: %s", err.Error()) t.Fatalf("error connecting: %s", err.Error())
} }
...@@ -1916,13 +2027,13 @@ func TestCustomDial(t *testing.T) { ...@@ -1916,13 +2027,13 @@ func TestCustomDial(t *testing.T) {
t.Skipf("MySQL server not running on %s", netAddr) t.Skipf("MySQL server not running on %s", netAddr)
} }
// our custom dial function which justs wraps net.Dial here // our custom dial function which just wraps net.Dial here
RegisterDialContext("mydial", func(ctx context.Context, addr string) (net.Conn, error) { RegisterDialContext("mydial", func(ctx context.Context, addr string) (net.Conn, error) {
var d net.Dialer var d net.Dialer
return d.DialContext(ctx, prot, addr) return d.DialContext(ctx, prot, addr)
}) })
db, err := sql.Open("mysql", fmt.Sprintf("%s:%s@mydial(%s)/%s?timeout=30s", user, pass, addr, dbname)) db, err := sql.Open(driverNameTest, fmt.Sprintf("%s:%s@mydial(%s)/%s?timeout=30s", user, pass, addr, dbname))
if err != nil { if err != nil {
t.Fatalf("error connecting: %s", err.Error()) t.Fatalf("error connecting: %s", err.Error())
} }
...@@ -1933,6 +2044,40 @@ func TestCustomDial(t *testing.T) { ...@@ -1933,6 +2044,40 @@ func TestCustomDial(t *testing.T) {
} }
} }
func TestBeforeConnect(t *testing.T) {
if !available {
t.Skipf("MySQL server not running on %s", netAddr)
}
// dbname is set in the BeforeConnect handle
cfg, err := ParseDSN(fmt.Sprintf("%s:%s@%s/%s?timeout=30s", user, pass, netAddr, "_"))
if err != nil {
t.Fatalf("error parsing DSN: %v", err)
}
cfg.Apply(BeforeConnect(func(ctx context.Context, c *Config) error {
c.DBName = dbname
return nil
}))
connector, err := NewConnector(cfg)
if err != nil {
t.Fatalf("error creating connector: %v", err)
}
db := sql.OpenDB(connector)
defer db.Close()
var connectedDb string
err = db.QueryRow("SELECT DATABASE();").Scan(&connectedDb)
if err != nil {
t.Fatalf("error executing query: %v", err)
}
if connectedDb != dbname {
t.Fatalf("expected to connect to DB %s, but connected to %s instead", dbname, connectedDb)
}
}
func TestSQLInjection(t *testing.T) { func TestSQLInjection(t *testing.T) {
createTest := func(arg string) func(dbt *DBTest) { createTest := func(arg string) func(dbt *DBTest) {
return func(dbt *DBTest) { return func(dbt *DBTest) {
...@@ -1995,7 +2140,7 @@ func TestInsertRetrieveEscapedData(t *testing.T) { ...@@ -1995,7 +2140,7 @@ func TestInsertRetrieveEscapedData(t *testing.T) {
func TestUnixSocketAuthFail(t *testing.T) { func TestUnixSocketAuthFail(t *testing.T) {
runTests(t, dsn, func(dbt *DBTest) { runTests(t, dsn, func(dbt *DBTest) {
// Save the current logger so we can restore it. // Save the current logger so we can restore it.
oldLogger := errLog oldLogger := defaultLogger
// Set a new logger so we can capture its output. // Set a new logger so we can capture its output.
buffer := bytes.NewBuffer(make([]byte, 0, 64)) buffer := bytes.NewBuffer(make([]byte, 0, 64))
...@@ -2020,7 +2165,7 @@ func TestUnixSocketAuthFail(t *testing.T) { ...@@ -2020,7 +2165,7 @@ func TestUnixSocketAuthFail(t *testing.T) {
} }
t.Logf("socket: %s", socket) t.Logf("socket: %s", socket)
badDSN := fmt.Sprintf("%s:%s@unix(%s)/%s?timeout=30s", user, badPass, socket, dbname) badDSN := fmt.Sprintf("%s:%s@unix(%s)/%s?timeout=30s", user, badPass, socket, dbname)
db, err := sql.Open("mysql", badDSN) db, err := sql.Open(driverNameTest, badDSN)
if err != nil { if err != nil {
t.Fatalf("error connecting: %s", err.Error()) t.Fatalf("error connecting: %s", err.Error())
} }
...@@ -2155,11 +2300,51 @@ func TestRejectReadOnly(t *testing.T) { ...@@ -2155,11 +2300,51 @@ func TestRejectReadOnly(t *testing.T) {
} }
func TestPing(t *testing.T) { func TestPing(t *testing.T) {
ctx := context.Background()
runTests(t, dsn, func(dbt *DBTest) { runTests(t, dsn, func(dbt *DBTest) {
if err := dbt.db.Ping(); err != nil { if err := dbt.db.Ping(); err != nil {
dbt.fail("Ping", "Ping", err) dbt.fail("Ping", "Ping", err)
} }
}) })
runTests(t, dsn, func(dbt *DBTest) {
conn, err := dbt.db.Conn(ctx)
if err != nil {
dbt.fail("db", "Conn", err)
}
// Check that affectedRows and insertIds are cleared after each call.
conn.Raw(func(conn interface{}) error {
c := conn.(*mysqlConn)
// Issue a query that sets affectedRows and insertIds.
q, err := c.Query(`SELECT 1`, nil)
if err != nil {
dbt.fail("Conn", "Query", err)
}
if got, want := c.result.affectedRows, []int64{0}; !reflect.DeepEqual(got, want) {
dbt.Fatalf("bad affectedRows: got %v, want=%v", got, want)
}
if got, want := c.result.insertIds, []int64{0}; !reflect.DeepEqual(got, want) {
dbt.Fatalf("bad insertIds: got %v, want=%v", got, want)
}
q.Close()
// Verify that Ping() clears both fields.
for i := 0; i < 2; i++ {
if err := c.Ping(ctx); err != nil {
dbt.fail("Pinger", "Ping", err)
}
if got, want := c.result.affectedRows, []int64(nil); !reflect.DeepEqual(got, want) {
t.Errorf("bad affectedRows: got %v, want=%v", got, want)
}
if got, want := c.result.insertIds, []int64(nil); !reflect.DeepEqual(got, want) {
t.Errorf("bad affectedRows: got %v, want=%v", got, want)
}
}
return nil
})
})
} }
// See Issue #799 // See Issue #799
...@@ -2169,7 +2354,7 @@ func TestEmptyPassword(t *testing.T) { ...@@ -2169,7 +2354,7 @@ func TestEmptyPassword(t *testing.T) {
} }
dsn := fmt.Sprintf("%s:%s@%s/%s?timeout=30s", user, "", netAddr, dbname) dsn := fmt.Sprintf("%s:%s@%s/%s?timeout=30s", user, "", netAddr, dbname)
db, err := sql.Open("mysql", dsn) db, err := sql.Open(driverNameTest, dsn)
if err == nil { if err == nil {
defer db.Close() defer db.Close()
err = db.Ping() err = db.Ping()
...@@ -2379,10 +2564,47 @@ func TestMultiResultSetNoSelect(t *testing.T) { ...@@ -2379,10 +2564,47 @@ func TestMultiResultSetNoSelect(t *testing.T) {
}) })
} }
func TestExecMultipleResults(t *testing.T) {
ctx := context.Background()
runTestsWithMultiStatement(t, dsn, func(dbt *DBTest) {
dbt.mustExec(`
CREATE TABLE test (
id INT NOT NULL AUTO_INCREMENT,
value VARCHAR(255),
PRIMARY KEY (id)
)`)
conn, err := dbt.db.Conn(ctx)
if err != nil {
t.Fatalf("failed to connect: %v", err)
}
conn.Raw(func(conn interface{}) error {
//lint:ignore SA1019 this is a test
ex := conn.(driver.Execer)
res, err := ex.Exec(`
INSERT INTO test (value) VALUES ('a'), ('b');
INSERT INTO test (value) VALUES ('c'), ('d'), ('e');
`, nil)
if err != nil {
t.Fatalf("insert statements failed: %v", err)
}
mres := res.(Result)
if got, want := mres.AllRowsAffected(), []int64{2, 3}; !reflect.DeepEqual(got, want) {
t.Errorf("bad AllRowsAffected: got %v, want=%v", got, want)
}
// For INSERTs containing multiple rows, LAST_INSERT_ID() returns the
// first inserted ID, not the last.
if got, want := mres.AllLastInsertIds(), []int64{1, 3}; !reflect.DeepEqual(got, want) {
t.Errorf("bad AllLastInsertIds: got %v, want %v", got, want)
}
return nil
})
})
}
// tests if rows are set in a proper state if some results were ignored before // tests if rows are set in a proper state if some results were ignored before
// calling rows.NextResultSet. // calling rows.NextResultSet.
func TestSkipResults(t *testing.T) { func TestSkipResults(t *testing.T) {
runTests(t, dsn, func(dbt *DBTest) { runTestsParallel(t, dsn, func(dbt *DBTest, _ string) {
rows := dbt.mustQuery("SELECT 1, 2") rows := dbt.mustQuery("SELECT 1, 2")
defer rows.Close() defer rows.Close()
...@@ -2400,8 +2622,44 @@ func TestSkipResults(t *testing.T) { ...@@ -2400,8 +2622,44 @@ func TestSkipResults(t *testing.T) {
}) })
} }
func TestQueryMultipleResults(t *testing.T) {
ctx := context.Background()
runTestsWithMultiStatement(t, dsn, func(dbt *DBTest) {
dbt.mustExec(`
CREATE TABLE test (
id INT NOT NULL AUTO_INCREMENT,
value VARCHAR(255),
PRIMARY KEY (id)
)`)
conn, err := dbt.db.Conn(ctx)
if err != nil {
t.Fatalf("failed to connect: %v", err)
}
conn.Raw(func(conn interface{}) error {
//lint:ignore SA1019 this is a test
qr := conn.(driver.Queryer)
c := conn.(*mysqlConn)
// Demonstrate that repeated queries reset the affectedRows
for i := 0; i < 2; i++ {
_, err := qr.Query(`
INSERT INTO test (value) VALUES ('a'), ('b');
INSERT INTO test (value) VALUES ('c'), ('d'), ('e');
`, nil)
if err != nil {
t.Fatalf("insert statements failed: %v", err)
}
if got, want := c.result.affectedRows, []int64{2, 3}; !reflect.DeepEqual(got, want) {
t.Errorf("bad affectedRows: got %v, want=%v", got, want)
}
}
return nil
})
})
}
func TestPingContext(t *testing.T) { func TestPingContext(t *testing.T) {
runTests(t, dsn, func(dbt *DBTest) { runTestsParallel(t, dsn, func(dbt *DBTest, _ string) {
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
cancel() cancel()
if err := dbt.db.PingContext(ctx); err != context.Canceled { if err := dbt.db.PingContext(ctx); err != context.Canceled {
...@@ -2411,8 +2669,8 @@ func TestPingContext(t *testing.T) { ...@@ -2411,8 +2669,8 @@ func TestPingContext(t *testing.T) {
} }
func TestContextCancelExec(t *testing.T) { func TestContextCancelExec(t *testing.T) {
runTests(t, dsn, func(dbt *DBTest) { runTestsParallel(t, dsn, func(dbt *DBTest, tbl string) {
dbt.mustExec("CREATE TABLE test (v INTEGER)") dbt.mustExec("CREATE TABLE " + tbl + " (v INTEGER)")
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
// Delay execution for just a bit until db.ExecContext has begun. // Delay execution for just a bit until db.ExecContext has begun.
...@@ -2420,7 +2678,7 @@ func TestContextCancelExec(t *testing.T) { ...@@ -2420,7 +2678,7 @@ func TestContextCancelExec(t *testing.T) {
// This query will be canceled. // This query will be canceled.
startTime := time.Now() startTime := time.Now()
if _, err := dbt.db.ExecContext(ctx, "INSERT INTO test VALUES (SLEEP(1))"); err != context.Canceled { if _, err := dbt.db.ExecContext(ctx, "INSERT INTO "+tbl+" VALUES (SLEEP(1))"); err != context.Canceled {
dbt.Errorf("expected context.Canceled, got %v", err) dbt.Errorf("expected context.Canceled, got %v", err)
} }
if d := time.Since(startTime); d > 500*time.Millisecond { if d := time.Since(startTime); d > 500*time.Millisecond {
...@@ -2432,7 +2690,7 @@ func TestContextCancelExec(t *testing.T) { ...@@ -2432,7 +2690,7 @@ func TestContextCancelExec(t *testing.T) {
// Check how many times the query is executed. // Check how many times the query is executed.
var v int var v int
if err := dbt.db.QueryRow("SELECT COUNT(*) FROM test").Scan(&v); err != nil { if err := dbt.db.QueryRow("SELECT COUNT(*) FROM " + tbl).Scan(&v); err != nil {
dbt.Fatalf("%s", err.Error()) dbt.Fatalf("%s", err.Error())
} }
if v != 1 { // TODO: need to kill the query, and v should be 0. if v != 1 { // TODO: need to kill the query, and v should be 0.
...@@ -2440,14 +2698,14 @@ func TestContextCancelExec(t *testing.T) { ...@@ -2440,14 +2698,14 @@ func TestContextCancelExec(t *testing.T) {
} }
// Context is already canceled, so error should come before execution. // Context is already canceled, so error should come before execution.
if _, err := dbt.db.ExecContext(ctx, "INSERT INTO test VALUES (1)"); err == nil { if _, err := dbt.db.ExecContext(ctx, "INSERT INTO "+tbl+" VALUES (1)"); err == nil {
dbt.Error("expected error") dbt.Error("expected error")
} else if err.Error() != "context canceled" { } else if err.Error() != "context canceled" {
dbt.Fatalf("unexpected error: %s", err) dbt.Fatalf("unexpected error: %s", err)
} }
// The second insert query will fail, so the table has no changes. // The second insert query will fail, so the table has no changes.
if err := dbt.db.QueryRow("SELECT COUNT(*) FROM test").Scan(&v); err != nil { if err := dbt.db.QueryRow("SELECT COUNT(*) FROM " + tbl).Scan(&v); err != nil {
dbt.Fatalf("%s", err.Error()) dbt.Fatalf("%s", err.Error())
} }
if v != 1 { if v != 1 {
...@@ -2457,8 +2715,8 @@ func TestContextCancelExec(t *testing.T) { ...@@ -2457,8 +2715,8 @@ func TestContextCancelExec(t *testing.T) {
} }
func TestContextCancelQuery(t *testing.T) { func TestContextCancelQuery(t *testing.T) {
runTests(t, dsn, func(dbt *DBTest) { runTestsParallel(t, dsn, func(dbt *DBTest, tbl string) {
dbt.mustExec("CREATE TABLE test (v INTEGER)") dbt.mustExec("CREATE TABLE " + tbl + " (v INTEGER)")
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
// Delay execution for just a bit until db.ExecContext has begun. // Delay execution for just a bit until db.ExecContext has begun.
...@@ -2466,7 +2724,7 @@ func TestContextCancelQuery(t *testing.T) { ...@@ -2466,7 +2724,7 @@ func TestContextCancelQuery(t *testing.T) {
// This query will be canceled. // This query will be canceled.
startTime := time.Now() startTime := time.Now()
if _, err := dbt.db.QueryContext(ctx, "INSERT INTO test VALUES (SLEEP(1))"); err != context.Canceled { if _, err := dbt.db.QueryContext(ctx, "INSERT INTO "+tbl+" VALUES (SLEEP(1))"); err != context.Canceled {
dbt.Errorf("expected context.Canceled, got %v", err) dbt.Errorf("expected context.Canceled, got %v", err)
} }
if d := time.Since(startTime); d > 500*time.Millisecond { if d := time.Since(startTime); d > 500*time.Millisecond {
...@@ -2478,7 +2736,7 @@ func TestContextCancelQuery(t *testing.T) { ...@@ -2478,7 +2736,7 @@ func TestContextCancelQuery(t *testing.T) {
// Check how many times the query is executed. // Check how many times the query is executed.
var v int var v int
if err := dbt.db.QueryRow("SELECT COUNT(*) FROM test").Scan(&v); err != nil { if err := dbt.db.QueryRow("SELECT COUNT(*) FROM " + tbl).Scan(&v); err != nil {
dbt.Fatalf("%s", err.Error()) dbt.Fatalf("%s", err.Error())
} }
if v != 1 { // TODO: need to kill the query, and v should be 0. if v != 1 { // TODO: need to kill the query, and v should be 0.
...@@ -2486,12 +2744,12 @@ func TestContextCancelQuery(t *testing.T) { ...@@ -2486,12 +2744,12 @@ func TestContextCancelQuery(t *testing.T) {
} }
// Context is already canceled, so error should come before execution. // Context is already canceled, so error should come before execution.
if _, err := dbt.db.QueryContext(ctx, "INSERT INTO test VALUES (1)"); err != context.Canceled { if _, err := dbt.db.QueryContext(ctx, "INSERT INTO "+tbl+" VALUES (1)"); err != context.Canceled {
dbt.Errorf("expected context.Canceled, got %v", err) dbt.Errorf("expected context.Canceled, got %v", err)
} }
// The second insert query will fail, so the table has no changes. // The second insert query will fail, so the table has no changes.
if err := dbt.db.QueryRow("SELECT COUNT(*) FROM test").Scan(&v); err != nil { if err := dbt.db.QueryRow("SELECT COUNT(*) FROM " + tbl).Scan(&v); err != nil {
dbt.Fatalf("%s", err.Error()) dbt.Fatalf("%s", err.Error())
} }
if v != 1 { if v != 1 {
...@@ -2501,12 +2759,12 @@ func TestContextCancelQuery(t *testing.T) { ...@@ -2501,12 +2759,12 @@ func TestContextCancelQuery(t *testing.T) {
} }
func TestContextCancelQueryRow(t *testing.T) { func TestContextCancelQueryRow(t *testing.T) {
runTests(t, dsn, func(dbt *DBTest) { runTestsParallel(t, dsn, func(dbt *DBTest, tbl string) {
dbt.mustExec("CREATE TABLE test (v INTEGER)") dbt.mustExec("CREATE TABLE " + tbl + " (v INTEGER)")
dbt.mustExec("INSERT INTO test VALUES (1), (2), (3)") dbt.mustExec("INSERT INTO " + tbl + " VALUES (1), (2), (3)")
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
rows, err := dbt.db.QueryContext(ctx, "SELECT v FROM test") rows, err := dbt.db.QueryContext(ctx, "SELECT v FROM "+tbl)
if err != nil { if err != nil {
dbt.Fatalf("%s", err.Error()) dbt.Fatalf("%s", err.Error())
} }
...@@ -2534,7 +2792,7 @@ func TestContextCancelQueryRow(t *testing.T) { ...@@ -2534,7 +2792,7 @@ func TestContextCancelQueryRow(t *testing.T) {
} }
func TestContextCancelPrepare(t *testing.T) { func TestContextCancelPrepare(t *testing.T) {
runTests(t, dsn, func(dbt *DBTest) { runTestsParallel(t, dsn, func(dbt *DBTest, _ string) {
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
cancel() cancel()
if _, err := dbt.db.PrepareContext(ctx, "SELECT 1"); err != context.Canceled { if _, err := dbt.db.PrepareContext(ctx, "SELECT 1"); err != context.Canceled {
...@@ -2544,10 +2802,10 @@ func TestContextCancelPrepare(t *testing.T) { ...@@ -2544,10 +2802,10 @@ func TestContextCancelPrepare(t *testing.T) {
} }
func TestContextCancelStmtExec(t *testing.T) { func TestContextCancelStmtExec(t *testing.T) {
runTests(t, dsn, func(dbt *DBTest) { runTestsParallel(t, dsn, func(dbt *DBTest, tbl string) {
dbt.mustExec("CREATE TABLE test (v INTEGER)") dbt.mustExec("CREATE TABLE " + tbl + " (v INTEGER)")
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
stmt, err := dbt.db.PrepareContext(ctx, "INSERT INTO test VALUES (SLEEP(1))") stmt, err := dbt.db.PrepareContext(ctx, "INSERT INTO "+tbl+" VALUES (SLEEP(1))")
if err != nil { if err != nil {
dbt.Fatalf("unexpected error: %v", err) dbt.Fatalf("unexpected error: %v", err)
} }
...@@ -2569,7 +2827,7 @@ func TestContextCancelStmtExec(t *testing.T) { ...@@ -2569,7 +2827,7 @@ func TestContextCancelStmtExec(t *testing.T) {
// Check how many times the query is executed. // Check how many times the query is executed.
var v int var v int
if err := dbt.db.QueryRow("SELECT COUNT(*) FROM test").Scan(&v); err != nil { if err := dbt.db.QueryRow("SELECT COUNT(*) FROM " + tbl).Scan(&v); err != nil {
dbt.Fatalf("%s", err.Error()) dbt.Fatalf("%s", err.Error())
} }
if v != 1 { // TODO: need to kill the query, and v should be 0. if v != 1 { // TODO: need to kill the query, and v should be 0.
...@@ -2579,10 +2837,10 @@ func TestContextCancelStmtExec(t *testing.T) { ...@@ -2579,10 +2837,10 @@ func TestContextCancelStmtExec(t *testing.T) {
} }
func TestContextCancelStmtQuery(t *testing.T) { func TestContextCancelStmtQuery(t *testing.T) {
runTests(t, dsn, func(dbt *DBTest) { runTestsParallel(t, dsn, func(dbt *DBTest, tbl string) {
dbt.mustExec("CREATE TABLE test (v INTEGER)") dbt.mustExec("CREATE TABLE " + tbl + " (v INTEGER)")
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
stmt, err := dbt.db.PrepareContext(ctx, "INSERT INTO test VALUES (SLEEP(1))") stmt, err := dbt.db.PrepareContext(ctx, "INSERT INTO "+tbl+" VALUES (SLEEP(1))")
if err != nil { if err != nil {
dbt.Fatalf("unexpected error: %v", err) dbt.Fatalf("unexpected error: %v", err)
} }
...@@ -2604,7 +2862,7 @@ func TestContextCancelStmtQuery(t *testing.T) { ...@@ -2604,7 +2862,7 @@ func TestContextCancelStmtQuery(t *testing.T) {
// Check how many times the query is executed. // Check how many times the query is executed.
var v int var v int
if err := dbt.db.QueryRow("SELECT COUNT(*) FROM test").Scan(&v); err != nil { if err := dbt.db.QueryRow("SELECT COUNT(*) FROM " + tbl).Scan(&v); err != nil {
dbt.Fatalf("%s", err.Error()) dbt.Fatalf("%s", err.Error())
} }
if v != 1 { // TODO: need to kill the query, and v should be 0. if v != 1 { // TODO: need to kill the query, and v should be 0.
...@@ -2618,8 +2876,8 @@ func TestContextCancelBegin(t *testing.T) { ...@@ -2618,8 +2876,8 @@ func TestContextCancelBegin(t *testing.T) {
t.Skip(`FIXME: it sometime fails with "expected driver.ErrBadConn, got sql: connection is already closed" on windows and macOS`) t.Skip(`FIXME: it sometime fails with "expected driver.ErrBadConn, got sql: connection is already closed" on windows and macOS`)
} }
runTests(t, dsn, func(dbt *DBTest) { runTestsParallel(t, dsn, func(dbt *DBTest, tbl string) {
dbt.mustExec("CREATE TABLE test (v INTEGER)") dbt.mustExec("CREATE TABLE " + tbl + " (v INTEGER)")
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
conn, err := dbt.db.Conn(ctx) conn, err := dbt.db.Conn(ctx)
if err != nil { if err != nil {
...@@ -2636,7 +2894,7 @@ func TestContextCancelBegin(t *testing.T) { ...@@ -2636,7 +2894,7 @@ func TestContextCancelBegin(t *testing.T) {
// This query will be canceled. // This query will be canceled.
startTime := time.Now() startTime := time.Now()
if _, err := tx.ExecContext(ctx, "INSERT INTO test VALUES (SLEEP(1))"); err != context.Canceled { if _, err := tx.ExecContext(ctx, "INSERT INTO "+tbl+" VALUES (SLEEP(1))"); err != context.Canceled {
dbt.Errorf("expected context.Canceled, got %v", err) dbt.Errorf("expected context.Canceled, got %v", err)
} }
if d := time.Since(startTime); d > 500*time.Millisecond { if d := time.Since(startTime); d > 500*time.Millisecond {
...@@ -2674,8 +2932,8 @@ func TestContextCancelBegin(t *testing.T) { ...@@ -2674,8 +2932,8 @@ func TestContextCancelBegin(t *testing.T) {
} }
func TestContextBeginIsolationLevel(t *testing.T) { func TestContextBeginIsolationLevel(t *testing.T) {
runTests(t, dsn, func(dbt *DBTest) { runTestsParallel(t, dsn, func(dbt *DBTest, tbl string) {
dbt.mustExec("CREATE TABLE test (v INTEGER)") dbt.mustExec("CREATE TABLE " + tbl + " (v INTEGER)")
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
defer cancel() defer cancel()
...@@ -2693,13 +2951,13 @@ func TestContextBeginIsolationLevel(t *testing.T) { ...@@ -2693,13 +2951,13 @@ func TestContextBeginIsolationLevel(t *testing.T) {
dbt.Fatal(err) dbt.Fatal(err)
} }
_, err = tx1.ExecContext(ctx, "INSERT INTO test VALUES (1)") _, err = tx1.ExecContext(ctx, "INSERT INTO "+tbl+" VALUES (1)")
if err != nil { if err != nil {
dbt.Fatal(err) dbt.Fatal(err)
} }
var v int var v int
row := tx2.QueryRowContext(ctx, "SELECT COUNT(*) FROM test") row := tx2.QueryRowContext(ctx, "SELECT COUNT(*) FROM "+tbl)
if err := row.Scan(&v); err != nil { if err := row.Scan(&v); err != nil {
dbt.Fatal(err) dbt.Fatal(err)
} }
...@@ -2713,7 +2971,7 @@ func TestContextBeginIsolationLevel(t *testing.T) { ...@@ -2713,7 +2971,7 @@ func TestContextBeginIsolationLevel(t *testing.T) {
dbt.Fatal(err) dbt.Fatal(err)
} }
row = tx2.QueryRowContext(ctx, "SELECT COUNT(*) FROM test") row = tx2.QueryRowContext(ctx, "SELECT COUNT(*) FROM "+tbl)
if err := row.Scan(&v); err != nil { if err := row.Scan(&v); err != nil {
dbt.Fatal(err) dbt.Fatal(err)
} }
...@@ -2726,8 +2984,8 @@ func TestContextBeginIsolationLevel(t *testing.T) { ...@@ -2726,8 +2984,8 @@ func TestContextBeginIsolationLevel(t *testing.T) {
} }
func TestContextBeginReadOnly(t *testing.T) { func TestContextBeginReadOnly(t *testing.T) {
runTests(t, dsn, func(dbt *DBTest) { runTestsParallel(t, dsn, func(dbt *DBTest, tbl string) {
dbt.mustExec("CREATE TABLE test (v INTEGER)") dbt.mustExec("CREATE TABLE " + tbl + " (v INTEGER)")
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
defer cancel() defer cancel()
...@@ -2742,14 +3000,14 @@ func TestContextBeginReadOnly(t *testing.T) { ...@@ -2742,14 +3000,14 @@ func TestContextBeginReadOnly(t *testing.T) {
} }
// INSERT queries fail in a READ ONLY transaction. // INSERT queries fail in a READ ONLY transaction.
_, err = tx.ExecContext(ctx, "INSERT INTO test VALUES (1)") _, err = tx.ExecContext(ctx, "INSERT INTO "+tbl+" VALUES (1)")
if _, ok := err.(*MySQLError); !ok { if _, ok := err.(*MySQLError); !ok {
dbt.Errorf("expected MySQLError, got %v", err) dbt.Errorf("expected MySQLError, got %v", err)
} }
// SELECT queries can be executed. // SELECT queries can be executed.
var v int var v int
row := tx.QueryRowContext(ctx, "SELECT COUNT(*) FROM test") row := tx.QueryRowContext(ctx, "SELECT COUNT(*) FROM "+tbl)
if err := row.Scan(&v); err != nil { if err := row.Scan(&v); err != nil {
dbt.Fatal(err) dbt.Fatal(err)
} }
...@@ -2778,13 +3036,18 @@ func TestRowsColumnTypes(t *testing.T) { ...@@ -2778,13 +3036,18 @@ func TestRowsColumnTypes(t *testing.T) {
nd1 := sql.NullTime{Time: time.Date(2006, 01, 02, 0, 0, 0, 0, time.UTC), Valid: true} nd1 := sql.NullTime{Time: time.Date(2006, 01, 02, 0, 0, 0, 0, time.UTC), Valid: true}
nd2 := sql.NullTime{Time: time.Date(2006, 03, 04, 0, 0, 0, 0, time.UTC), Valid: true} nd2 := sql.NullTime{Time: time.Date(2006, 03, 04, 0, 0, 0, 0, time.UTC), Valid: true}
ndNULL := sql.NullTime{Time: time.Time{}, Valid: false} ndNULL := sql.NullTime{Time: time.Time{}, Valid: false}
rbNULL := sql.RawBytes(nil) bNULL := []byte(nil)
rb0 := sql.RawBytes("0") nsNULL := sql.NullString{String: "", Valid: false}
rb42 := sql.RawBytes("42") // Helper function to build NullString from string literal.
rbTest := sql.RawBytes("Test") ns := func(s string) sql.NullString { return sql.NullString{String: s, Valid: true} }
rb0pad4 := sql.RawBytes("0\x00\x00\x00") // BINARY right-pads values with 0x00 ns0 := ns("0")
rbx0 := sql.RawBytes("\x00") b0 := []byte("0")
rbx42 := sql.RawBytes("\x42") b42 := []byte("42")
nsTest := ns("Test")
bTest := []byte("Test")
b0pad4 := []byte("0\x00\x00\x00") // BINARY right-pads values with 0x00
bx0 := []byte("\x00")
bx42 := []byte("\x42")
var columns = []struct { var columns = []struct {
name string name string
...@@ -2797,7 +3060,7 @@ func TestRowsColumnTypes(t *testing.T) { ...@@ -2797,7 +3060,7 @@ func TestRowsColumnTypes(t *testing.T) {
valuesIn [3]string valuesIn [3]string
valuesOut [3]interface{} valuesOut [3]interface{}
}{ }{
{"bit8null", "BIT(8)", "BIT", scanTypeRawBytes, true, 0, 0, [3]string{"0x0", "NULL", "0x42"}, [3]interface{}{rbx0, rbNULL, rbx42}}, {"bit8null", "BIT(8)", "BIT", scanTypeBytes, true, 0, 0, [3]string{"0x0", "NULL", "0x42"}, [3]interface{}{bx0, bNULL, bx42}},
{"boolnull", "BOOL", "TINYINT", scanTypeNullInt, true, 0, 0, [3]string{"NULL", "true", "0"}, [3]interface{}{niNULL, ni1, ni0}}, {"boolnull", "BOOL", "TINYINT", scanTypeNullInt, true, 0, 0, [3]string{"NULL", "true", "0"}, [3]interface{}{niNULL, ni1, ni0}},
{"bool", "BOOL NOT NULL", "TINYINT", scanTypeInt8, false, 0, 0, [3]string{"1", "0", "FALSE"}, [3]interface{}{int8(1), int8(0), int8(0)}}, {"bool", "BOOL NOT NULL", "TINYINT", scanTypeInt8, false, 0, 0, [3]string{"1", "0", "FALSE"}, [3]interface{}{int8(1), int8(0), int8(0)}},
{"intnull", "INTEGER", "INT", scanTypeNullInt, true, 0, 0, [3]string{"0", "NULL", "42"}, [3]interface{}{ni0, niNULL, ni42}}, {"intnull", "INTEGER", "INT", scanTypeNullInt, true, 0, 0, [3]string{"0", "NULL", "42"}, [3]interface{}{ni0, niNULL, ni42}},
...@@ -2811,35 +3074,38 @@ func TestRowsColumnTypes(t *testing.T) { ...@@ -2811,35 +3074,38 @@ func TestRowsColumnTypes(t *testing.T) {
{"tinyuint", "TINYINT UNSIGNED NOT NULL", "UNSIGNED TINYINT", scanTypeUint8, false, 0, 0, [3]string{"0", "255", "42"}, [3]interface{}{uint8(0), uint8(255), uint8(42)}}, {"tinyuint", "TINYINT UNSIGNED NOT NULL", "UNSIGNED TINYINT", scanTypeUint8, false, 0, 0, [3]string{"0", "255", "42"}, [3]interface{}{uint8(0), uint8(255), uint8(42)}},
{"smalluint", "SMALLINT UNSIGNED NOT NULL", "UNSIGNED SMALLINT", scanTypeUint16, false, 0, 0, [3]string{"0", "65535", "42"}, [3]interface{}{uint16(0), uint16(65535), uint16(42)}}, {"smalluint", "SMALLINT UNSIGNED NOT NULL", "UNSIGNED SMALLINT", scanTypeUint16, false, 0, 0, [3]string{"0", "65535", "42"}, [3]interface{}{uint16(0), uint16(65535), uint16(42)}},
{"biguint", "BIGINT UNSIGNED NOT NULL", "UNSIGNED BIGINT", scanTypeUint64, false, 0, 0, [3]string{"0", "65535", "42"}, [3]interface{}{uint64(0), uint64(65535), uint64(42)}}, {"biguint", "BIGINT UNSIGNED NOT NULL", "UNSIGNED BIGINT", scanTypeUint64, false, 0, 0, [3]string{"0", "65535", "42"}, [3]interface{}{uint64(0), uint64(65535), uint64(42)}},
{"mediumuint", "MEDIUMINT UNSIGNED NOT NULL", "UNSIGNED MEDIUMINT", scanTypeUint32, false, 0, 0, [3]string{"0", "16777215", "42"}, [3]interface{}{uint32(0), uint32(16777215), uint32(42)}},
{"uint13", "INT(13) UNSIGNED NOT NULL", "UNSIGNED INT", scanTypeUint32, false, 0, 0, [3]string{"0", "1337", "42"}, [3]interface{}{uint32(0), uint32(1337), uint32(42)}}, {"uint13", "INT(13) UNSIGNED NOT NULL", "UNSIGNED INT", scanTypeUint32, false, 0, 0, [3]string{"0", "1337", "42"}, [3]interface{}{uint32(0), uint32(1337), uint32(42)}},
{"float", "FLOAT NOT NULL", "FLOAT", scanTypeFloat32, false, math.MaxInt64, math.MaxInt64, [3]string{"0", "42", "13.37"}, [3]interface{}{float32(0), float32(42), float32(13.37)}}, {"float", "FLOAT NOT NULL", "FLOAT", scanTypeFloat32, false, math.MaxInt64, math.MaxInt64, [3]string{"0", "42", "13.37"}, [3]interface{}{float32(0), float32(42), float32(13.37)}},
{"floatnull", "FLOAT", "FLOAT", scanTypeNullFloat, true, math.MaxInt64, math.MaxInt64, [3]string{"0", "NULL", "13.37"}, [3]interface{}{nf0, nfNULL, nf1337}}, {"floatnull", "FLOAT", "FLOAT", scanTypeNullFloat, true, math.MaxInt64, math.MaxInt64, [3]string{"0", "NULL", "13.37"}, [3]interface{}{nf0, nfNULL, nf1337}},
{"float74null", "FLOAT(7,4)", "FLOAT", scanTypeNullFloat, true, math.MaxInt64, 4, [3]string{"0", "NULL", "13.37"}, [3]interface{}{nf0, nfNULL, nf1337}}, {"float74null", "FLOAT(7,4)", "FLOAT", scanTypeNullFloat, true, math.MaxInt64, 4, [3]string{"0", "NULL", "13.37"}, [3]interface{}{nf0, nfNULL, nf1337}},
{"double", "DOUBLE NOT NULL", "DOUBLE", scanTypeFloat64, false, math.MaxInt64, math.MaxInt64, [3]string{"0", "42", "13.37"}, [3]interface{}{float64(0), float64(42), float64(13.37)}}, {"double", "DOUBLE NOT NULL", "DOUBLE", scanTypeFloat64, false, math.MaxInt64, math.MaxInt64, [3]string{"0", "42", "13.37"}, [3]interface{}{float64(0), float64(42), float64(13.37)}},
{"doublenull", "DOUBLE", "DOUBLE", scanTypeNullFloat, true, math.MaxInt64, math.MaxInt64, [3]string{"0", "NULL", "13.37"}, [3]interface{}{nf0, nfNULL, nf1337}}, {"doublenull", "DOUBLE", "DOUBLE", scanTypeNullFloat, true, math.MaxInt64, math.MaxInt64, [3]string{"0", "NULL", "13.37"}, [3]interface{}{nf0, nfNULL, nf1337}},
{"decimal1", "DECIMAL(10,6) NOT NULL", "DECIMAL", scanTypeRawBytes, false, 10, 6, [3]string{"0", "13.37", "1234.123456"}, [3]interface{}{sql.RawBytes("0.000000"), sql.RawBytes("13.370000"), sql.RawBytes("1234.123456")}}, {"decimal1", "DECIMAL(10,6) NOT NULL", "DECIMAL", scanTypeString, false, 10, 6, [3]string{"0", "13.37", "1234.123456"}, [3]interface{}{"0.000000", "13.370000", "1234.123456"}},
{"decimal1null", "DECIMAL(10,6)", "DECIMAL", scanTypeRawBytes, true, 10, 6, [3]string{"0", "NULL", "1234.123456"}, [3]interface{}{sql.RawBytes("0.000000"), rbNULL, sql.RawBytes("1234.123456")}}, {"decimal1null", "DECIMAL(10,6)", "DECIMAL", scanTypeNullString, true, 10, 6, [3]string{"0", "NULL", "1234.123456"}, [3]interface{}{ns("0.000000"), nsNULL, ns("1234.123456")}},
{"decimal2", "DECIMAL(8,4) NOT NULL", "DECIMAL", scanTypeRawBytes, false, 8, 4, [3]string{"0", "13.37", "1234.123456"}, [3]interface{}{sql.RawBytes("0.0000"), sql.RawBytes("13.3700"), sql.RawBytes("1234.1235")}}, {"decimal2", "DECIMAL(8,4) NOT NULL", "DECIMAL", scanTypeString, false, 8, 4, [3]string{"0", "13.37", "1234.123456"}, [3]interface{}{"0.0000", "13.3700", "1234.1235"}},
{"decimal2null", "DECIMAL(8,4)", "DECIMAL", scanTypeRawBytes, true, 8, 4, [3]string{"0", "NULL", "1234.123456"}, [3]interface{}{sql.RawBytes("0.0000"), rbNULL, sql.RawBytes("1234.1235")}}, {"decimal2null", "DECIMAL(8,4)", "DECIMAL", scanTypeNullString, true, 8, 4, [3]string{"0", "NULL", "1234.123456"}, [3]interface{}{ns("0.0000"), nsNULL, ns("1234.1235")}},
{"decimal3", "DECIMAL(5,0) NOT NULL", "DECIMAL", scanTypeRawBytes, false, 5, 0, [3]string{"0", "13.37", "-12345.123456"}, [3]interface{}{rb0, sql.RawBytes("13"), sql.RawBytes("-12345")}}, {"decimal3", "DECIMAL(5,0) NOT NULL", "DECIMAL", scanTypeString, false, 5, 0, [3]string{"0", "13.37", "-12345.123456"}, [3]interface{}{"0", "13", "-12345"}},
{"decimal3null", "DECIMAL(5,0)", "DECIMAL", scanTypeRawBytes, true, 5, 0, [3]string{"0", "NULL", "-12345.123456"}, [3]interface{}{rb0, rbNULL, sql.RawBytes("-12345")}}, {"decimal3null", "DECIMAL(5,0)", "DECIMAL", scanTypeNullString, true, 5, 0, [3]string{"0", "NULL", "-12345.123456"}, [3]interface{}{ns0, nsNULL, ns("-12345")}},
{"char25null", "CHAR(25)", "CHAR", scanTypeRawBytes, true, 0, 0, [3]string{"0", "NULL", "'Test'"}, [3]interface{}{rb0, rbNULL, rbTest}}, {"char25null", "CHAR(25)", "CHAR", scanTypeNullString, true, 0, 0, [3]string{"0", "NULL", "'Test'"}, [3]interface{}{ns0, nsNULL, nsTest}},
{"varchar42", "VARCHAR(42) NOT NULL", "VARCHAR", scanTypeRawBytes, false, 0, 0, [3]string{"0", "'Test'", "42"}, [3]interface{}{rb0, rbTest, rb42}}, {"varchar42", "VARCHAR(42) NOT NULL", "VARCHAR", scanTypeString, false, 0, 0, [3]string{"0", "'Test'", "42"}, [3]interface{}{"0", "Test", "42"}},
{"binary4null", "BINARY(4)", "BINARY", scanTypeRawBytes, true, 0, 0, [3]string{"0", "NULL", "'Test'"}, [3]interface{}{rb0pad4, rbNULL, rbTest}}, {"binary4null", "BINARY(4)", "BINARY", scanTypeBytes, true, 0, 0, [3]string{"0", "NULL", "'Test'"}, [3]interface{}{b0pad4, bNULL, bTest}},
{"varbinary42", "VARBINARY(42) NOT NULL", "VARBINARY", scanTypeRawBytes, false, 0, 0, [3]string{"0", "'Test'", "42"}, [3]interface{}{rb0, rbTest, rb42}}, {"varbinary42", "VARBINARY(42) NOT NULL", "VARBINARY", scanTypeBytes, false, 0, 0, [3]string{"0", "'Test'", "42"}, [3]interface{}{b0, bTest, b42}},
{"tinyblobnull", "TINYBLOB", "BLOB", scanTypeRawBytes, true, 0, 0, [3]string{"0", "NULL", "'Test'"}, [3]interface{}{rb0, rbNULL, rbTest}}, {"tinyblobnull", "TINYBLOB", "BLOB", scanTypeBytes, true, 0, 0, [3]string{"0", "NULL", "'Test'"}, [3]interface{}{b0, bNULL, bTest}},
{"tinytextnull", "TINYTEXT", "TEXT", scanTypeRawBytes, true, 0, 0, [3]string{"0", "NULL", "'Test'"}, [3]interface{}{rb0, rbNULL, rbTest}}, {"tinytextnull", "TINYTEXT", "TEXT", scanTypeNullString, true, 0, 0, [3]string{"0", "NULL", "'Test'"}, [3]interface{}{ns0, nsNULL, nsTest}},
{"blobnull", "BLOB", "BLOB", scanTypeRawBytes, true, 0, 0, [3]string{"0", "NULL", "'Test'"}, [3]interface{}{rb0, rbNULL, rbTest}}, {"blobnull", "BLOB", "BLOB", scanTypeBytes, true, 0, 0, [3]string{"0", "NULL", "'Test'"}, [3]interface{}{b0, bNULL, bTest}},
{"textnull", "TEXT", "TEXT", scanTypeRawBytes, true, 0, 0, [3]string{"0", "NULL", "'Test'"}, [3]interface{}{rb0, rbNULL, rbTest}}, {"textnull", "TEXT", "TEXT", scanTypeNullString, true, 0, 0, [3]string{"0", "NULL", "'Test'"}, [3]interface{}{ns0, nsNULL, nsTest}},
{"mediumblob", "MEDIUMBLOB NOT NULL", "BLOB", scanTypeRawBytes, false, 0, 0, [3]string{"0", "'Test'", "42"}, [3]interface{}{rb0, rbTest, rb42}}, {"mediumblob", "MEDIUMBLOB NOT NULL", "BLOB", scanTypeBytes, false, 0, 0, [3]string{"0", "'Test'", "42"}, [3]interface{}{b0, bTest, b42}},
{"mediumtext", "MEDIUMTEXT NOT NULL", "TEXT", scanTypeRawBytes, false, 0, 0, [3]string{"0", "'Test'", "42"}, [3]interface{}{rb0, rbTest, rb42}}, {"mediumtext", "MEDIUMTEXT NOT NULL", "TEXT", scanTypeString, false, 0, 0, [3]string{"0", "'Test'", "42"}, [3]interface{}{"0", "Test", "42"}},
{"longblob", "LONGBLOB NOT NULL", "BLOB", scanTypeRawBytes, false, 0, 0, [3]string{"0", "'Test'", "42"}, [3]interface{}{rb0, rbTest, rb42}}, {"longblob", "LONGBLOB NOT NULL", "BLOB", scanTypeBytes, false, 0, 0, [3]string{"0", "'Test'", "42"}, [3]interface{}{b0, bTest, b42}},
{"longtext", "LONGTEXT NOT NULL", "TEXT", scanTypeRawBytes, false, 0, 0, [3]string{"0", "'Test'", "42"}, [3]interface{}{rb0, rbTest, rb42}}, {"longtext", "LONGTEXT NOT NULL", "TEXT", scanTypeString, false, 0, 0, [3]string{"0", "'Test'", "42"}, [3]interface{}{"0", "Test", "42"}},
{"datetime", "DATETIME", "DATETIME", scanTypeNullTime, true, 0, 0, [3]string{"'2006-01-02 15:04:05'", "'2006-01-02 15:04:05.1'", "'2006-01-02 15:04:05.111111'"}, [3]interface{}{nt0, nt0, nt0}}, {"datetime", "DATETIME", "DATETIME", scanTypeNullTime, true, 0, 0, [3]string{"'2006-01-02 15:04:05'", "'2006-01-02 15:04:05.1'", "'2006-01-02 15:04:05.111111'"}, [3]interface{}{nt0, nt0, nt0}},
{"datetime2", "DATETIME(2)", "DATETIME", scanTypeNullTime, true, 2, 2, [3]string{"'2006-01-02 15:04:05'", "'2006-01-02 15:04:05.1'", "'2006-01-02 15:04:05.111111'"}, [3]interface{}{nt0, nt1, nt2}}, {"datetime2", "DATETIME(2)", "DATETIME", scanTypeNullTime, true, 2, 2, [3]string{"'2006-01-02 15:04:05'", "'2006-01-02 15:04:05.1'", "'2006-01-02 15:04:05.111111'"}, [3]interface{}{nt0, nt1, nt2}},
{"datetime6", "DATETIME(6)", "DATETIME", scanTypeNullTime, true, 6, 6, [3]string{"'2006-01-02 15:04:05'", "'2006-01-02 15:04:05.1'", "'2006-01-02 15:04:05.111111'"}, [3]interface{}{nt0, nt1, nt6}}, {"datetime6", "DATETIME(6)", "DATETIME", scanTypeNullTime, true, 6, 6, [3]string{"'2006-01-02 15:04:05'", "'2006-01-02 15:04:05.1'", "'2006-01-02 15:04:05.111111'"}, [3]interface{}{nt0, nt1, nt6}},
{"date", "DATE", "DATE", scanTypeNullTime, true, 0, 0, [3]string{"'2006-01-02'", "NULL", "'2006-03-04'"}, [3]interface{}{nd1, ndNULL, nd2}}, {"date", "DATE", "DATE", scanTypeNullTime, true, 0, 0, [3]string{"'2006-01-02'", "NULL", "'2006-03-04'"}, [3]interface{}{nd1, ndNULL, nd2}},
{"year", "YEAR NOT NULL", "YEAR", scanTypeUint16, false, 0, 0, [3]string{"2006", "2000", "1994"}, [3]interface{}{uint16(2006), uint16(2000), uint16(1994)}}, {"year", "YEAR NOT NULL", "YEAR", scanTypeUint16, false, 0, 0, [3]string{"2006", "2000", "1994"}, [3]interface{}{uint16(2006), uint16(2000), uint16(1994)}},
{"enum", "ENUM('', 'v1', 'v2')", "ENUM", scanTypeNullString, true, 0, 0, [3]string{"''", "'v1'", "'v2'"}, [3]interface{}{ns(""), ns("v1"), ns("v2")}},
{"set", "set('', 'v1', 'v2')", "SET", scanTypeNullString, true, 0, 0, [3]string{"''", "'v1'", "'v1,v2'"}, [3]interface{}{ns(""), ns("v1"), ns("v1,v2")}},
} }
schema := "" schema := ""
...@@ -2945,7 +3211,10 @@ func TestRowsColumnTypes(t *testing.T) { ...@@ -2945,7 +3211,10 @@ func TestRowsColumnTypes(t *testing.T) {
continue continue
} }
} }
// Avoid panic caused by nil scantype.
if t.Failed() {
return
}
values := make([]interface{}, len(tt)) values := make([]interface{}, len(tt))
for i := range values { for i := range values {
values[i] = reflect.New(types[i]).Interface() values[i] = reflect.New(types[i]).Interface()
...@@ -2956,16 +3225,12 @@ func TestRowsColumnTypes(t *testing.T) { ...@@ -2956,16 +3225,12 @@ func TestRowsColumnTypes(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("failed to scan values in %v", err) t.Fatalf("failed to scan values in %v", err)
} }
for j := range values { for j, value := range values {
value := reflect.ValueOf(values[j]).Elem().Interface() value := reflect.ValueOf(value).Elem().Interface()
if !reflect.DeepEqual(value, columns[j].valuesOut[i]) { if !reflect.DeepEqual(value, columns[j].valuesOut[i]) {
if columns[j].scanType == scanTypeRawBytes {
t.Errorf("row %d, column %d: %v != %v", i, j, string(value.(sql.RawBytes)), string(columns[j].valuesOut[i].(sql.RawBytes)))
} else {
t.Errorf("row %d, column %d: %v != %v", i, j, value, columns[j].valuesOut[i]) t.Errorf("row %d, column %d: %v != %v", i, j, value, columns[j].valuesOut[i])
} }
} }
}
i++ i++
} }
if i != 3 { if i != 3 {
...@@ -2979,9 +3244,9 @@ func TestRowsColumnTypes(t *testing.T) { ...@@ -2979,9 +3244,9 @@ func TestRowsColumnTypes(t *testing.T) {
} }
func TestValuerWithValueReceiverGivenNilValue(t *testing.T) { func TestValuerWithValueReceiverGivenNilValue(t *testing.T) {
runTests(t, dsn, func(dbt *DBTest) { runTestsParallel(t, dsn, func(dbt *DBTest, tbl string) {
dbt.mustExec("CREATE TABLE test (value VARCHAR(255))") dbt.mustExec("CREATE TABLE " + tbl + " (value VARCHAR(255))")
dbt.db.Exec("INSERT INTO test VALUES (?)", (*testValuer)(nil)) dbt.db.Exec("INSERT INTO "+tbl+" VALUES (?)", (*testValuer)(nil))
// This test will panic on the INSERT if ConvertValue() does not check for typed nil before calling Value() // This test will panic on the INSERT if ConvertValue() does not check for typed nil before calling Value()
}) })
} }
...@@ -3015,14 +3280,17 @@ func TestRawBytesAreNotModified(t *testing.T) { ...@@ -3015,14 +3280,17 @@ func TestRawBytesAreNotModified(t *testing.T) {
rows, err := dbt.db.QueryContext(ctx, `SELECT id, value FROM test`) rows, err := dbt.db.QueryContext(ctx, `SELECT id, value FROM test`)
if err != nil { if err != nil {
t.Fatal(err) dbt.Fatal(err)
} }
defer rows.Close()
var b int var b int
var raw sql.RawBytes var raw sql.RawBytes
for rows.Next() { if !rows.Next() {
dbt.Fatal("expected at least one row")
}
if err := rows.Scan(&b, &raw); err != nil { if err := rows.Scan(&b, &raw); err != nil {
t.Fatal(err) dbt.Fatal(err)
} }
before := string(raw) before := string(raw)
...@@ -3032,10 +3300,8 @@ func TestRawBytesAreNotModified(t *testing.T) { ...@@ -3032,10 +3300,8 @@ func TestRawBytesAreNotModified(t *testing.T) {
after := string(raw) after := string(raw)
if before != after { if before != after {
t.Fatalf("the backing storage for sql.RawBytes has been modified (i=%v)", i) dbt.Fatalf("the backing storage for sql.RawBytes has been modified (i=%v)", i)
}
} }
rows.Close()
}() }()
} }
}) })
...@@ -3058,7 +3324,7 @@ func TestConnectorObeysDialTimeouts(t *testing.T) { ...@@ -3058,7 +3324,7 @@ func TestConnectorObeysDialTimeouts(t *testing.T) {
return d.DialContext(ctx, prot, addr) return d.DialContext(ctx, prot, addr)
}) })
db, err := sql.Open("mysql", fmt.Sprintf("%s:%s@dialctxtest(%s)/%s?timeout=30s", user, pass, addr, dbname)) db, err := sql.Open(driverNameTest, fmt.Sprintf("%s:%s@dialctxtest(%s)/%s?timeout=30s", user, pass, addr, dbname))
if err != nil { if err != nil {
t.Fatalf("error connecting: %s", err.Error()) t.Fatalf("error connecting: %s", err.Error())
} }
...@@ -3209,3 +3475,105 @@ func TestConnectorTimeoutsWatchCancel(t *testing.T) { ...@@ -3209,3 +3475,105 @@ func TestConnectorTimeoutsWatchCancel(t *testing.T) {
t.Errorf("connection not closed") t.Errorf("connection not closed")
} }
} }
func TestConnectionAttributes(t *testing.T) {
if !available {
t.Skipf("MySQL server not running on %s", netAddr)
}
defaultAttrs := []string{
connAttrClientName,
connAttrOS,
connAttrPlatform,
connAttrPid,
connAttrServerHost,
}
host, _, _ := net.SplitHostPort(addr)
defaultAttrValues := []string{
connAttrClientNameValue,
connAttrOSValue,
connAttrPlatformValue,
strconv.Itoa(os.Getpid()),
host,
}
customAttrs := []string{"attr1", "fo/o"}
customAttrValues := []string{"value1", "bo/o"}
customAttrStrs := make([]string, len(customAttrs))
for i := range customAttrs {
customAttrStrs[i] = fmt.Sprintf("%s:%s", customAttrs[i], customAttrValues[i])
}
dsn += "&connectionAttributes=" + url.QueryEscape(strings.Join(customAttrStrs, ","))
var db *sql.DB
if _, err := ParseDSN(dsn); err != errInvalidDSNUnsafeCollation {
db, err = sql.Open(driverNameTest, dsn)
if err != nil {
t.Fatalf("error connecting: %s", err.Error())
}
defer db.Close()
}
dbt := &DBTest{t, db}
queryString := "SELECT ATTR_NAME, ATTR_VALUE FROM performance_schema.session_account_connect_attrs WHERE PROCESSLIST_ID = CONNECTION_ID()"
rows := dbt.mustQuery(queryString)
defer rows.Close()
rowsMap := make(map[string]string)
for rows.Next() {
var attrName, attrValue string
rows.Scan(&attrName, &attrValue)
rowsMap[attrName] = attrValue
}
connAttrs := append(append([]string{}, defaultAttrs...), customAttrs...)
expectedAttrValues := append(append([]string{}, defaultAttrValues...), customAttrValues...)
for i := range connAttrs {
if gotValue := rowsMap[connAttrs[i]]; gotValue != expectedAttrValues[i] {
dbt.Errorf("expected %q, got %q", expectedAttrValues[i], gotValue)
}
}
}
func TestErrorInMultiResult(t *testing.T) {
// https://github.com/go-sql-driver/mysql/issues/1361
var db *sql.DB
if _, err := ParseDSN(dsn); err != errInvalidDSNUnsafeCollation {
db, err = sql.Open("mysql", dsn)
if err != nil {
t.Fatalf("error connecting: %s", err.Error())
}
defer db.Close()
}
dbt := &DBTest{t, db}
query := `
CREATE PROCEDURE test_proc1()
BEGIN
SELECT 1,2;
SELECT 3,4;
SIGNAL SQLSTATE '10000' SET MESSAGE_TEXT = "some error", MYSQL_ERRNO = 10000;
END;
`
runCallCommand(dbt, query, "test_proc1")
}
func runCallCommand(dbt *DBTest, query, name string) {
dbt.mustExec(fmt.Sprintf("DROP PROCEDURE IF EXISTS %s", name))
dbt.mustExec(query)
defer dbt.mustExec("DROP PROCEDURE " + name)
rows, err := dbt.db.Query(fmt.Sprintf("CALL %s", name))
if err != nil {
return
}
defer rows.Close()
for rows.Next() {
}
for rows.NextResultSet() {
for rows.Next() {
}
}
}
...@@ -10,6 +10,7 @@ package mysql ...@@ -10,6 +10,7 @@ package mysql
import ( import (
"bytes" "bytes"
"context"
"crypto/rsa" "crypto/rsa"
"crypto/tls" "crypto/tls"
"errors" "errors"
...@@ -34,22 +35,27 @@ var ( ...@@ -34,22 +35,27 @@ var (
// If a new Config is created instead of being parsed from a DSN string, // If a new Config is created instead of being parsed from a DSN string,
// the NewConfig function should be used, which sets default values. // the NewConfig function should be used, which sets default values.
type Config struct { type Config struct {
// non boolean fields
User string // Username User string // Username
Passwd string // Password (requires User) Passwd string // Password (requires User)
Net string // Network type Net string // Network (e.g. "tcp", "tcp6", "unix". default: "tcp")
Addr string // Network address (requires Net) Addr string // Address (default: "127.0.0.1:3306" for "tcp" and "/tmp/mysql.sock" for "unix")
DBName string // Database name DBName string // Database name
Params map[string]string // Connection parameters Params map[string]string // Connection parameters
ConnectionAttributes string // Connection Attributes, comma-delimited string of user-defined "key:value" pairs
Collation string // Connection collation Collation string // Connection collation
Loc *time.Location // Location for time.Time values Loc *time.Location // Location for time.Time values
MaxAllowedPacket int // Max packet size allowed MaxAllowedPacket int // Max packet size allowed
ServerPubKey string // Server public key name ServerPubKey string // Server public key name
pubKey *rsa.PublicKey // Server public key
TLSConfig string // TLS configuration name TLSConfig string // TLS configuration name
TLS *tls.Config // TLS configuration, its priority is higher than TLSConfig TLS *tls.Config // TLS configuration, its priority is higher than TLSConfig
Timeout time.Duration // Dial timeout Timeout time.Duration // Dial timeout
ReadTimeout time.Duration // I/O read timeout ReadTimeout time.Duration // I/O read timeout
WriteTimeout time.Duration // I/O write timeout WriteTimeout time.Duration // I/O write timeout
Logger Logger // Logger
// boolean fields
AllowAllFiles bool // Allow all files to be used with LOAD DATA LOCAL INFILE AllowAllFiles bool // Allow all files to be used with LOAD DATA LOCAL INFILE
AllowCleartextPasswords bool // Allows the cleartext client side plugin AllowCleartextPasswords bool // Allows the cleartext client side plugin
...@@ -63,17 +69,57 @@ type Config struct { ...@@ -63,17 +69,57 @@ type Config struct {
MultiStatements bool // Allow multiple statements in one query MultiStatements bool // Allow multiple statements in one query
ParseTime bool // Parse time values to time.Time ParseTime bool // Parse time values to time.Time
RejectReadOnly bool // Reject read-only connections RejectReadOnly bool // Reject read-only connections
// unexported fields. new options should be come here
beforeConnect func(context.Context, *Config) error // Invoked before a connection is established
pubKey *rsa.PublicKey // Server public key
timeTruncate time.Duration // Truncate time.Time values to the specified duration
} }
// Functional Options Pattern
// https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis
type Option func(*Config) error
// NewConfig creates a new Config and sets default values. // NewConfig creates a new Config and sets default values.
func NewConfig() *Config { func NewConfig() *Config {
return &Config{ cfg := &Config{
Collation: defaultCollation,
Loc: time.UTC, Loc: time.UTC,
MaxAllowedPacket: defaultMaxAllowedPacket, MaxAllowedPacket: defaultMaxAllowedPacket,
Logger: defaultLogger,
AllowNativePasswords: true, AllowNativePasswords: true,
CheckConnLiveness: true, CheckConnLiveness: true,
} }
return cfg
}
// Apply applies the given options to the Config object.
func (c *Config) Apply(opts ...Option) error {
for _, opt := range opts {
err := opt(c)
if err != nil {
return err
}
}
return nil
}
// TimeTruncate sets the time duration to truncate time.Time values in
// query parameters.
func TimeTruncate(d time.Duration) Option {
return func(cfg *Config) error {
cfg.timeTruncate = d
return nil
}
}
// BeforeConnect sets the function to be invoked before a connection is established.
func BeforeConnect(fn func(context.Context, *Config) error) Option {
return func(cfg *Config) error {
cfg.beforeConnect = fn
return nil
}
} }
func (cfg *Config) Clone() *Config { func (cfg *Config) Clone() *Config {
...@@ -97,7 +143,7 @@ func (cfg *Config) Clone() *Config { ...@@ -97,7 +143,7 @@ func (cfg *Config) Clone() *Config {
} }
func (cfg *Config) normalize() error { func (cfg *Config) normalize() error {
if cfg.InterpolateParams && unsafeCollations[cfg.Collation] { if cfg.InterpolateParams && cfg.Collation != "" && unsafeCollations[cfg.Collation] {
return errInvalidDSNUnsafeCollation return errInvalidDSNUnsafeCollation
} }
...@@ -153,6 +199,10 @@ func (cfg *Config) normalize() error { ...@@ -153,6 +199,10 @@ func (cfg *Config) normalize() error {
} }
} }
if cfg.Logger == nil {
cfg.Logger = defaultLogger
}
return nil return nil
} }
...@@ -171,6 +221,8 @@ func writeDSNParam(buf *bytes.Buffer, hasParam *bool, name, value string) { ...@@ -171,6 +221,8 @@ func writeDSNParam(buf *bytes.Buffer, hasParam *bool, name, value string) {
// FormatDSN formats the given Config into a DSN string which can be passed to // FormatDSN formats the given Config into a DSN string which can be passed to
// the driver. // the driver.
//
// Note: use [NewConnector] and [database/sql.OpenDB] to open a connection from a [*Config].
func (cfg *Config) FormatDSN() string { func (cfg *Config) FormatDSN() string {
var buf bytes.Buffer var buf bytes.Buffer
...@@ -196,7 +248,7 @@ func (cfg *Config) FormatDSN() string { ...@@ -196,7 +248,7 @@ func (cfg *Config) FormatDSN() string {
// /dbname // /dbname
buf.WriteByte('/') buf.WriteByte('/')
buf.WriteString(cfg.DBName) buf.WriteString(url.PathEscape(cfg.DBName))
// [?param1=value1&...&paramN=valueN] // [?param1=value1&...&paramN=valueN]
hasParam := false hasParam := false
...@@ -230,7 +282,7 @@ func (cfg *Config) FormatDSN() string { ...@@ -230,7 +282,7 @@ func (cfg *Config) FormatDSN() string {
writeDSNParam(&buf, &hasParam, "clientFoundRows", "true") writeDSNParam(&buf, &hasParam, "clientFoundRows", "true")
} }
if col := cfg.Collation; col != defaultCollation && len(col) > 0 { if col := cfg.Collation; col != "" {
writeDSNParam(&buf, &hasParam, "collation", col) writeDSNParam(&buf, &hasParam, "collation", col)
} }
...@@ -254,6 +306,10 @@ func (cfg *Config) FormatDSN() string { ...@@ -254,6 +306,10 @@ func (cfg *Config) FormatDSN() string {
writeDSNParam(&buf, &hasParam, "parseTime", "true") writeDSNParam(&buf, &hasParam, "parseTime", "true")
} }
if cfg.timeTruncate > 0 {
writeDSNParam(&buf, &hasParam, "timeTruncate", cfg.timeTruncate.String())
}
if cfg.ReadTimeout > 0 { if cfg.ReadTimeout > 0 {
writeDSNParam(&buf, &hasParam, "readTimeout", cfg.ReadTimeout.String()) writeDSNParam(&buf, &hasParam, "readTimeout", cfg.ReadTimeout.String())
} }
...@@ -358,7 +414,11 @@ func ParseDSN(dsn string) (cfg *Config, err error) { ...@@ -358,7 +414,11 @@ func ParseDSN(dsn string) (cfg *Config, err error) {
break break
} }
} }
cfg.DBName = dsn[i+1 : j]
dbname := dsn[i+1 : j]
if cfg.DBName, err = url.PathUnescape(dbname); err != nil {
return nil, fmt.Errorf("invalid dbname %q: %w", dbname, err)
}
break break
} }
...@@ -378,13 +438,13 @@ func ParseDSN(dsn string) (cfg *Config, err error) { ...@@ -378,13 +438,13 @@ func ParseDSN(dsn string) (cfg *Config, err error) {
// Values must be url.QueryEscape'ed // Values must be url.QueryEscape'ed
func parseDSNParams(cfg *Config, params string) (err error) { func parseDSNParams(cfg *Config, params string) (err error) {
for _, v := range strings.Split(params, "&") { for _, v := range strings.Split(params, "&") {
param := strings.SplitN(v, "=", 2) key, value, found := strings.Cut(v, "=")
if len(param) != 2 { if !found {
continue continue
} }
// cfg params // cfg params
switch value := param[1]; param[0] { switch key {
// Disable INFILE allowlist / enable all files // Disable INFILE allowlist / enable all files
case "allowAllFiles": case "allowAllFiles":
var isBool bool var isBool bool
...@@ -490,6 +550,13 @@ func parseDSNParams(cfg *Config, params string) (err error) { ...@@ -490,6 +550,13 @@ func parseDSNParams(cfg *Config, params string) (err error) {
return errors.New("invalid bool value: " + value) return errors.New("invalid bool value: " + value)
} }
// time.Time truncation
case "timeTruncate":
cfg.timeTruncate, err = time.ParseDuration(value)
if err != nil {
return fmt.Errorf("invalid timeTruncate value: %v, error: %w", value, err)
}
// I/O read Timeout // I/O read Timeout
case "readTimeout": case "readTimeout":
cfg.ReadTimeout, err = time.ParseDuration(value) cfg.ReadTimeout, err = time.ParseDuration(value)
...@@ -554,13 +621,22 @@ func parseDSNParams(cfg *Config, params string) (err error) { ...@@ -554,13 +621,22 @@ func parseDSNParams(cfg *Config, params string) (err error) {
if err != nil { if err != nil {
return return
} }
// Connection attributes
case "connectionAttributes":
connectionAttributes, err := url.QueryUnescape(value)
if err != nil {
return fmt.Errorf("invalid connectionAttributes value: %v", err)
}
cfg.ConnectionAttributes = connectionAttributes
default: default:
// lazy init // lazy init
if cfg.Params == nil { if cfg.Params == nil {
cfg.Params = make(map[string]string) cfg.Params = make(map[string]string)
} }
if cfg.Params[param[0]], err = url.QueryUnescape(value); err != nil { if cfg.Params[key], err = url.QueryUnescape(value); err != nil {
return return
} }
} }
......
//go:build go1.18
// +build go1.18
package mysql
import (
"net"
"testing"
)
func FuzzFormatDSN(f *testing.F) {
for _, test := range testDSNs { // See dsn_test.go
f.Add(test.in)
}
f.Fuzz(func(t *testing.T, dsn1 string) {
// Do not waste resources
if len(dsn1) > 1000 {
t.Skip("ignore: too long")
}
cfg1, err := ParseDSN(dsn1)
if err != nil {
t.Skipf("invalid DSN: %v", err)
}
dsn2 := cfg1.FormatDSN()
if dsn2 == dsn1 {
return
}
// Skip known cases of bad config that are not strictly checked by ParseDSN
if _, _, err := net.SplitHostPort(cfg1.Addr); err != nil {
t.Skipf("invalid addr %q: %v", cfg1.Addr, err)
}
cfg2, err := ParseDSN(dsn2)
if err != nil {
t.Fatalf("%q rewritten as %q: %v", dsn1, dsn2, err)
}
dsn3 := cfg2.FormatDSN()
if dsn3 != dsn2 {
t.Errorf("%q rewritten as %q", dsn2, dsn3)
}
})
}
...@@ -22,63 +22,71 @@ var testDSNs = []struct { ...@@ -22,63 +22,71 @@ var testDSNs = []struct {
out *Config out *Config
}{{ }{{
"username:password@protocol(address)/dbname?param=value", "username:password@protocol(address)/dbname?param=value",
&Config{User: "username", Passwd: "password", Net: "protocol", Addr: "address", DBName: "dbname", Params: map[string]string{"param": "value"}, Collation: "utf8mb4_general_ci", Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, AllowNativePasswords: true, CheckConnLiveness: true}, &Config{User: "username", Passwd: "password", Net: "protocol", Addr: "address", DBName: "dbname", Params: map[string]string{"param": "value"}, Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, Logger: defaultLogger, AllowNativePasswords: true, CheckConnLiveness: true},
}, { }, {
"username:password@protocol(address)/dbname?param=value&columnsWithAlias=true", "username:password@protocol(address)/dbname?param=value&columnsWithAlias=true",
&Config{User: "username", Passwd: "password", Net: "protocol", Addr: "address", DBName: "dbname", Params: map[string]string{"param": "value"}, Collation: "utf8mb4_general_ci", Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, AllowNativePasswords: true, CheckConnLiveness: true, ColumnsWithAlias: true}, &Config{User: "username", Passwd: "password", Net: "protocol", Addr: "address", DBName: "dbname", Params: map[string]string{"param": "value"}, Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, Logger: defaultLogger, AllowNativePasswords: true, CheckConnLiveness: true, ColumnsWithAlias: true},
}, { }, {
"username:password@protocol(address)/dbname?param=value&columnsWithAlias=true&multiStatements=true", "username:password@protocol(address)/dbname?param=value&columnsWithAlias=true&multiStatements=true",
&Config{User: "username", Passwd: "password", Net: "protocol", Addr: "address", DBName: "dbname", Params: map[string]string{"param": "value"}, Collation: "utf8mb4_general_ci", Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, AllowNativePasswords: true, CheckConnLiveness: true, ColumnsWithAlias: true, MultiStatements: true}, &Config{User: "username", Passwd: "password", Net: "protocol", Addr: "address", DBName: "dbname", Params: map[string]string{"param": "value"}, Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, Logger: defaultLogger, AllowNativePasswords: true, CheckConnLiveness: true, ColumnsWithAlias: true, MultiStatements: true},
}, { }, {
"user@unix(/path/to/socket)/dbname?charset=utf8", "user@unix(/path/to/socket)/dbname?charset=utf8",
&Config{User: "user", Net: "unix", Addr: "/path/to/socket", DBName: "dbname", Params: map[string]string{"charset": "utf8"}, Collation: "utf8mb4_general_ci", Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, AllowNativePasswords: true, CheckConnLiveness: true}, &Config{User: "user", Net: "unix", Addr: "/path/to/socket", DBName: "dbname", Params: map[string]string{"charset": "utf8"}, Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, Logger: defaultLogger, AllowNativePasswords: true, CheckConnLiveness: true},
}, { }, {
"user:password@tcp(localhost:5555)/dbname?charset=utf8&tls=true", "user:password@tcp(localhost:5555)/dbname?charset=utf8&tls=true",
&Config{User: "user", Passwd: "password", Net: "tcp", Addr: "localhost:5555", DBName: "dbname", Params: map[string]string{"charset": "utf8"}, Collation: "utf8mb4_general_ci", Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, AllowNativePasswords: true, CheckConnLiveness: true, TLSConfig: "true"}, &Config{User: "user", Passwd: "password", Net: "tcp", Addr: "localhost:5555", DBName: "dbname", Params: map[string]string{"charset": "utf8"}, Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, Logger: defaultLogger, AllowNativePasswords: true, CheckConnLiveness: true, TLSConfig: "true"},
}, { }, {
"user:password@tcp(localhost:5555)/dbname?charset=utf8mb4,utf8&tls=skip-verify", "user:password@tcp(localhost:5555)/dbname?charset=utf8mb4,utf8&tls=skip-verify",
&Config{User: "user", Passwd: "password", Net: "tcp", Addr: "localhost:5555", DBName: "dbname", Params: map[string]string{"charset": "utf8mb4,utf8"}, Collation: "utf8mb4_general_ci", Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, AllowNativePasswords: true, CheckConnLiveness: true, TLSConfig: "skip-verify"}, &Config{User: "user", Passwd: "password", Net: "tcp", Addr: "localhost:5555", DBName: "dbname", Params: map[string]string{"charset": "utf8mb4,utf8"}, Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, Logger: defaultLogger, AllowNativePasswords: true, CheckConnLiveness: true, TLSConfig: "skip-verify"},
}, { }, {
"user:password@/dbname?loc=UTC&timeout=30s&readTimeout=1s&writeTimeout=1s&allowAllFiles=1&clientFoundRows=true&allowOldPasswords=TRUE&collation=utf8mb4_unicode_ci&maxAllowedPacket=16777216&tls=false&allowCleartextPasswords=true&parseTime=true&rejectReadOnly=true", "user:password@/dbname?loc=UTC&timeout=30s&readTimeout=1s&writeTimeout=1s&allowAllFiles=1&clientFoundRows=true&allowOldPasswords=TRUE&collation=utf8mb4_unicode_ci&maxAllowedPacket=16777216&tls=false&allowCleartextPasswords=true&parseTime=true&rejectReadOnly=true",
&Config{User: "user", Passwd: "password", Net: "tcp", Addr: "127.0.0.1:3306", DBName: "dbname", Collation: "utf8mb4_unicode_ci", Loc: time.UTC, TLSConfig: "false", AllowCleartextPasswords: true, AllowNativePasswords: true, Timeout: 30 * time.Second, ReadTimeout: time.Second, WriteTimeout: time.Second, AllowAllFiles: true, AllowOldPasswords: true, CheckConnLiveness: true, ClientFoundRows: true, MaxAllowedPacket: 16777216, ParseTime: true, RejectReadOnly: true}, &Config{User: "user", Passwd: "password", Net: "tcp", Addr: "127.0.0.1:3306", DBName: "dbname", Collation: "utf8mb4_unicode_ci", Loc: time.UTC, TLSConfig: "false", AllowCleartextPasswords: true, AllowNativePasswords: true, Timeout: 30 * time.Second, ReadTimeout: time.Second, WriteTimeout: time.Second, Logger: defaultLogger, AllowAllFiles: true, AllowOldPasswords: true, CheckConnLiveness: true, ClientFoundRows: true, MaxAllowedPacket: 16777216, ParseTime: true, RejectReadOnly: true},
}, { }, {
"user:password@/dbname?allowNativePasswords=false&checkConnLiveness=false&maxAllowedPacket=0&allowFallbackToPlaintext=true", "user:password@/dbname?allowNativePasswords=false&checkConnLiveness=false&maxAllowedPacket=0&allowFallbackToPlaintext=true",
&Config{User: "user", Passwd: "password", Net: "tcp", Addr: "127.0.0.1:3306", DBName: "dbname", Collation: "utf8mb4_general_ci", Loc: time.UTC, MaxAllowedPacket: 0, AllowFallbackToPlaintext: true, AllowNativePasswords: false, CheckConnLiveness: false}, &Config{User: "user", Passwd: "password", Net: "tcp", Addr: "127.0.0.1:3306", DBName: "dbname", Loc: time.UTC, MaxAllowedPacket: 0, Logger: defaultLogger, AllowFallbackToPlaintext: true, AllowNativePasswords: false, CheckConnLiveness: false},
}, { }, {
"user:p@ss(word)@tcp([de:ad:be:ef::ca:fe]:80)/dbname?loc=Local", "user:p@ss(word)@tcp([de:ad:be:ef::ca:fe]:80)/dbname?loc=Local",
&Config{User: "user", Passwd: "p@ss(word)", Net: "tcp", Addr: "[de:ad:be:ef::ca:fe]:80", DBName: "dbname", Collation: "utf8mb4_general_ci", Loc: time.Local, MaxAllowedPacket: defaultMaxAllowedPacket, AllowNativePasswords: true, CheckConnLiveness: true}, &Config{User: "user", Passwd: "p@ss(word)", Net: "tcp", Addr: "[de:ad:be:ef::ca:fe]:80", DBName: "dbname", Loc: time.Local, MaxAllowedPacket: defaultMaxAllowedPacket, Logger: defaultLogger, AllowNativePasswords: true, CheckConnLiveness: true},
}, { }, {
"/dbname", "/dbname",
&Config{Net: "tcp", Addr: "127.0.0.1:3306", DBName: "dbname", Collation: "utf8mb4_general_ci", Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, AllowNativePasswords: true, CheckConnLiveness: true}, &Config{Net: "tcp", Addr: "127.0.0.1:3306", DBName: "dbname", Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, Logger: defaultLogger, AllowNativePasswords: true, CheckConnLiveness: true},
}, {
"/dbname%2Fwithslash",
&Config{Net: "tcp", Addr: "127.0.0.1:3306", DBName: "dbname/withslash", Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, Logger: defaultLogger, AllowNativePasswords: true, CheckConnLiveness: true},
}, { }, {
"@/", "@/",
&Config{Net: "tcp", Addr: "127.0.0.1:3306", Collation: "utf8mb4_general_ci", Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, AllowNativePasswords: true, CheckConnLiveness: true}, &Config{Net: "tcp", Addr: "127.0.0.1:3306", Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, Logger: defaultLogger, AllowNativePasswords: true, CheckConnLiveness: true},
}, { }, {
"/", "/",
&Config{Net: "tcp", Addr: "127.0.0.1:3306", Collation: "utf8mb4_general_ci", Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, AllowNativePasswords: true, CheckConnLiveness: true}, &Config{Net: "tcp", Addr: "127.0.0.1:3306", Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, Logger: defaultLogger, AllowNativePasswords: true, CheckConnLiveness: true},
}, { }, {
"", "",
&Config{Net: "tcp", Addr: "127.0.0.1:3306", Collation: "utf8mb4_general_ci", Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, AllowNativePasswords: true, CheckConnLiveness: true}, &Config{Net: "tcp", Addr: "127.0.0.1:3306", Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, Logger: defaultLogger, AllowNativePasswords: true, CheckConnLiveness: true},
}, { }, {
"user:p@/ssword@/", "user:p@/ssword@/",
&Config{User: "user", Passwd: "p@/ssword", Net: "tcp", Addr: "127.0.0.1:3306", Collation: "utf8mb4_general_ci", Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, AllowNativePasswords: true, CheckConnLiveness: true}, &Config{User: "user", Passwd: "p@/ssword", Net: "tcp", Addr: "127.0.0.1:3306", Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, Logger: defaultLogger, AllowNativePasswords: true, CheckConnLiveness: true},
}, { }, {
"unix/?arg=%2Fsome%2Fpath.ext", "unix/?arg=%2Fsome%2Fpath.ext",
&Config{Net: "unix", Addr: "/tmp/mysql.sock", Params: map[string]string{"arg": "/some/path.ext"}, Collation: "utf8mb4_general_ci", Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, AllowNativePasswords: true, CheckConnLiveness: true}, &Config{Net: "unix", Addr: "/tmp/mysql.sock", Params: map[string]string{"arg": "/some/path.ext"}, Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, Logger: defaultLogger, AllowNativePasswords: true, CheckConnLiveness: true},
}, { }, {
"tcp(127.0.0.1)/dbname", "tcp(127.0.0.1)/dbname",
&Config{Net: "tcp", Addr: "127.0.0.1:3306", DBName: "dbname", Collation: "utf8mb4_general_ci", Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, AllowNativePasswords: true, CheckConnLiveness: true}, &Config{Net: "tcp", Addr: "127.0.0.1:3306", DBName: "dbname", Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, Logger: defaultLogger, AllowNativePasswords: true, CheckConnLiveness: true},
}, { }, {
"tcp(de:ad:be:ef::ca:fe)/dbname", "tcp(de:ad:be:ef::ca:fe)/dbname",
&Config{Net: "tcp", Addr: "[de:ad:be:ef::ca:fe]:3306", DBName: "dbname", Collation: "utf8mb4_general_ci", Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, AllowNativePasswords: true, CheckConnLiveness: true}, &Config{Net: "tcp", Addr: "[de:ad:be:ef::ca:fe]:3306", DBName: "dbname", Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, Logger: defaultLogger, AllowNativePasswords: true, CheckConnLiveness: true},
}, {
"user:password@/dbname?loc=UTC&timeout=30s&parseTime=true&timeTruncate=1h",
&Config{User: "user", Passwd: "password", Net: "tcp", Addr: "127.0.0.1:3306", DBName: "dbname", Loc: time.UTC, Timeout: 30 * time.Second, ParseTime: true, MaxAllowedPacket: defaultMaxAllowedPacket, Logger: defaultLogger, AllowNativePasswords: true, CheckConnLiveness: true, timeTruncate: time.Hour},
}, },
} }
func TestDSNParser(t *testing.T) { func TestDSNParser(t *testing.T) {
for i, tst := range testDSNs { for i, tst := range testDSNs {
t.Run(tst.in, func(t *testing.T) {
cfg, err := ParseDSN(tst.in) cfg, err := ParseDSN(tst.in)
if err != nil { if err != nil {
t.Error(err.Error()) t.Error(err.Error())
return
} }
// pointer not static // pointer not static
...@@ -87,6 +95,7 @@ func TestDSNParser(t *testing.T) { ...@@ -87,6 +95,7 @@ func TestDSNParser(t *testing.T) {
if !reflect.DeepEqual(cfg, tst.out) { if !reflect.DeepEqual(cfg, tst.out) {
t.Errorf("%d. ParseDSN(%q) mismatch:\ngot %+v\nwant %+v", i, tst.in, cfg, tst.out) t.Errorf("%d. ParseDSN(%q) mismatch:\ngot %+v\nwant %+v", i, tst.in, cfg, tst.out)
} }
})
} }
} }
...@@ -113,20 +122,26 @@ func TestDSNParserInvalid(t *testing.T) { ...@@ -113,20 +122,26 @@ func TestDSNParserInvalid(t *testing.T) {
func TestDSNReformat(t *testing.T) { func TestDSNReformat(t *testing.T) {
for i, tst := range testDSNs { for i, tst := range testDSNs {
t.Run(tst.in, func(t *testing.T) {
dsn1 := tst.in dsn1 := tst.in
cfg1, err := ParseDSN(dsn1) cfg1, err := ParseDSN(dsn1)
if err != nil { if err != nil {
t.Error(err.Error()) t.Error(err.Error())
continue return
} }
cfg1.TLS = nil // pointer not static cfg1.TLS = nil // pointer not static
res1 := fmt.Sprintf("%+v", cfg1) res1 := fmt.Sprintf("%+v", cfg1)
dsn2 := cfg1.FormatDSN() dsn2 := cfg1.FormatDSN()
if dsn2 != dsn1 {
// Just log
t.Logf("%d. %q reformatted as %q", i, dsn1, dsn2)
}
cfg2, err := ParseDSN(dsn2) cfg2, err := ParseDSN(dsn2)
if err != nil { if err != nil {
t.Error(err.Error()) t.Error(err.Error())
continue return
} }
cfg2.TLS = nil // pointer not static cfg2.TLS = nil // pointer not static
res2 := fmt.Sprintf("%+v", cfg2) res2 := fmt.Sprintf("%+v", cfg2)
...@@ -134,6 +149,12 @@ func TestDSNReformat(t *testing.T) { ...@@ -134,6 +149,12 @@ func TestDSNReformat(t *testing.T) {
if res1 != res2 { if res1 != res2 {
t.Errorf("%d. %q does not match %q", i, res2, res1) t.Errorf("%d. %q does not match %q", i, res2, res1)
} }
dsn3 := cfg2.FormatDSN()
if dsn3 != dsn2 {
t.Errorf("%d. %q does not match %q", i, dsn2, dsn3)
}
})
} }
} }
......
...@@ -21,7 +21,7 @@ var ( ...@@ -21,7 +21,7 @@ var (
ErrMalformPkt = errors.New("malformed packet") ErrMalformPkt = errors.New("malformed packet")
ErrNoTLS = errors.New("TLS requested but server does not support TLS") ErrNoTLS = errors.New("TLS requested but server does not support TLS")
ErrCleartextPassword = errors.New("this user requires clear text authentication. If you still want to use it, please add 'allowCleartextPasswords=1' to your DSN") ErrCleartextPassword = errors.New("this user requires clear text authentication. If you still want to use it, please add 'allowCleartextPasswords=1' to your DSN")
ErrNativePassword = errors.New("this user requires mysql native password authentication.") ErrNativePassword = errors.New("this user requires mysql native password authentication")
ErrOldPassword = errors.New("this user requires old password authentication. If you still want to use it, please add 'allowOldPasswords=1' to your DSN. See also https://github.com/go-sql-driver/mysql/wiki/old_passwords") ErrOldPassword = errors.New("this user requires old password authentication. If you still want to use it, please add 'allowOldPasswords=1' to your DSN. See also https://github.com/go-sql-driver/mysql/wiki/old_passwords")
ErrUnknownPlugin = errors.New("this authentication plugin is not supported") ErrUnknownPlugin = errors.New("this authentication plugin is not supported")
ErrOldProtocol = errors.New("MySQL server does not support required protocol 41+") ErrOldProtocol = errors.New("MySQL server does not support required protocol 41+")
...@@ -37,20 +37,26 @@ var ( ...@@ -37,20 +37,26 @@ var (
errBadConnNoWrite = errors.New("bad connection") errBadConnNoWrite = errors.New("bad connection")
) )
var errLog = Logger(log.New(os.Stderr, "[mysql] ", log.Ldate|log.Ltime|log.Lshortfile)) var defaultLogger = Logger(log.New(os.Stderr, "[mysql] ", log.Ldate|log.Ltime|log.Lshortfile))
// Logger is used to log critical error messages. // Logger is used to log critical error messages.
type Logger interface { type Logger interface {
Print(v ...interface{}) Print(v ...interface{})
} }
// SetLogger is used to set the logger for critical errors. // NopLogger is a nop implementation of the Logger interface.
type NopLogger struct{}
// Print implements Logger interface.
func (nl *NopLogger) Print(_ ...interface{}) {}
// SetLogger is used to set the default logger for critical errors.
// The initial logger is os.Stderr. // The initial logger is os.Stderr.
func SetLogger(logger Logger) error { func SetLogger(logger Logger) error {
if logger == nil { if logger == nil {
return errors.New("logger is nil") return errors.New("logger is nil")
} }
errLog = logger defaultLogger = logger
return nil return nil
} }
......