New Upstream Release - golang-github-go-sql-driver-mysql
Ready changes
Summary
Merged new upstream version: 1.7.0 (was: 1.5.0).
Resulting package
Built on 2022-12-18T04:49 (took 3m39s)
The resulting binary packages can be installed (if you have the apt repository enabled) by running one of:
apt install -t fresh-releases golang-github-go-sql-driver-mysql-dev
Lintian Result
Diff
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
new file mode 100644
index 0000000..d9d29a8
--- /dev/null
+++ b/.github/workflows/codeql.yml
@@ -0,0 +1,41 @@
+name: "CodeQL"
+
+on:
+ push:
+ branches: [ "master" ]
+ pull_request:
+ branches: [ "master" ]
+ schedule:
+ - cron: "18 19 * * 1"
+
+jobs:
+ analyze:
+ name: Analyze
+ runs-on: ubuntu-latest
+ permissions:
+ actions: read
+ contents: read
+ security-events: write
+
+ strategy:
+ fail-fast: false
+ matrix:
+ language: [ go ]
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+
+ - name: Initialize CodeQL
+ uses: github/codeql-action/init@v2
+ with:
+ languages: ${{ matrix.language }}
+ queries: +security-and-quality
+
+ - name: Autobuild
+ uses: github/codeql-action/autobuild@v2
+
+ - name: Perform CodeQL Analysis
+ uses: github/codeql-action/analyze@v2
+ with:
+ category: "/language:${{ matrix.language }}"
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 0000000..7032032
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,108 @@
+name: test
+on:
+ pull_request:
+ push:
+ workflow_dispatch:
+
+env:
+ MYSQL_TEST_USER: gotest
+ MYSQL_TEST_PASS: secret
+ MYSQL_TEST_ADDR: 127.0.0.1:3306
+ MYSQL_TEST_CONCURRENT: 1
+
+jobs:
+ list:
+ runs-on: ubuntu-latest
+ outputs:
+ matrix: ${{ steps.set-matrix.outputs.matrix }}
+ steps:
+ - name: list
+ id: set-matrix
+ run: |
+ import json
+ import os
+ go = [
+ # Keep the most recent production release at the top
+ '1.19',
+ # Older production releases
+ '1.18',
+ '1.17',
+ '1.16',
+ '1.15',
+ '1.14',
+ '1.13',
+ ]
+ mysql = [
+ '8.0',
+ '5.7',
+ '5.6',
+ 'mariadb-10.6',
+ 'mariadb-10.5',
+ 'mariadb-10.4',
+ 'mariadb-10.3',
+ ]
+
+ includes = []
+ # Go versions compatibility check
+ for v in go[1:]:
+ includes.append({'os': 'ubuntu-latest', 'go': v, 'mysql': mysql[0]})
+
+ matrix = {
+ # OS vs MySQL versions
+ 'os': [ 'ubuntu-latest', 'macos-latest', 'windows-latest' ],
+ 'go': [ go[0] ],
+ 'mysql': mysql,
+
+ 'include': includes
+ }
+ output = json.dumps(matrix, separators=(',', ':'))
+ with open(os.environ["GITHUB_OUTPUT"], 'a', encoding="utf-8") as f:
+ f.write('matrix={0}\n'.format(output))
+ shell: python
+ test:
+ needs: list
+ runs-on: ${{ matrix.os }}
+ strategy:
+ fail-fast: false
+ matrix: ${{ fromJSON(needs.list.outputs.matrix) }}
+ steps:
+ - uses: actions/checkout@v2
+ - uses: actions/setup-go@v2
+ with:
+ go-version: ${{ matrix.go }}
+ - uses: shogo82148/actions-setup-mysql@v1
+ with:
+ mysql-version: ${{ matrix.mysql }}
+ user: ${{ env.MYSQL_TEST_USER }}
+ password: ${{ env.MYSQL_TEST_PASS }}
+ my-cnf: |
+ innodb_log_file_size=256MB
+ innodb_buffer_pool_size=512MB
+ max_allowed_packet=16MB
+ ; TestConcurrent fails if max_connections is too large
+ max_connections=50
+ local_infile=1
+ - name: setup database
+ run: |
+ mysql --user 'root' --host '127.0.0.1' -e 'create database gotest;'
+
+ - name: test
+ run: |
+ go test -v '-covermode=count' '-coverprofile=coverage.out'
+
+ - name: Send coverage
+ uses: shogo82148/actions-goveralls@v1
+ with:
+ path-to-profile: coverage.out
+ flag-name: ${{ runner.os }}-Go-${{ matrix.go }}-DB-${{ matrix.mysql }}
+ parallel: true
+
+ # notifies that all test jobs are finished.
+ finish:
+ needs: test
+ if: always()
+ runs-on: ubuntu-latest
+ steps:
+ - uses: shogo82148/actions-goveralls@v1
+ with:
+ parallel-finished: true
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 56fcf25..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,129 +0,0 @@
-sudo: false
-language: go
-go:
- - 1.10.x
- - 1.11.x
- - 1.12.x
- - 1.13.x
- - master
-
-before_install:
- - go get golang.org/x/tools/cmd/cover
- - go get github.com/mattn/goveralls
-
-before_script:
- - echo -e "[server]\ninnodb_log_file_size=256MB\ninnodb_buffer_pool_size=512MB\nmax_allowed_packet=16MB" | sudo tee -a /etc/mysql/my.cnf
- - sudo service mysql restart
- - .travis/wait_mysql.sh
- - mysql -e 'create database gotest;'
-
-matrix:
- include:
- - env: DB=MYSQL8
- sudo: required
- dist: trusty
- go: 1.10.x
- services:
- - docker
- before_install:
- - go get golang.org/x/tools/cmd/cover
- - go get github.com/mattn/goveralls
- - docker pull mysql:8.0
- - docker run -d -p 127.0.0.1:3307:3306 --name mysqld -e MYSQL_DATABASE=gotest -e MYSQL_USER=gotest -e MYSQL_PASSWORD=secret -e MYSQL_ROOT_PASSWORD=verysecret
- mysql:8.0 --innodb_log_file_size=256MB --innodb_buffer_pool_size=512MB --max_allowed_packet=16MB --local-infile=1
- - cp .travis/docker.cnf ~/.my.cnf
- - .travis/wait_mysql.sh
- before_script:
- - export MYSQL_TEST_USER=gotest
- - export MYSQL_TEST_PASS=secret
- - export MYSQL_TEST_ADDR=127.0.0.1:3307
- - export MYSQL_TEST_CONCURRENT=1
-
- - env: DB=MYSQL57
- sudo: required
- dist: trusty
- go: 1.10.x
- services:
- - docker
- before_install:
- - go get golang.org/x/tools/cmd/cover
- - go get github.com/mattn/goveralls
- - docker pull mysql:5.7
- - docker run -d -p 127.0.0.1:3307:3306 --name mysqld -e MYSQL_DATABASE=gotest -e MYSQL_USER=gotest -e MYSQL_PASSWORD=secret -e MYSQL_ROOT_PASSWORD=verysecret
- mysql:5.7 --innodb_log_file_size=256MB --innodb_buffer_pool_size=512MB --max_allowed_packet=16MB --local-infile=1
- - cp .travis/docker.cnf ~/.my.cnf
- - .travis/wait_mysql.sh
- before_script:
- - export MYSQL_TEST_USER=gotest
- - export MYSQL_TEST_PASS=secret
- - export MYSQL_TEST_ADDR=127.0.0.1:3307
- - export MYSQL_TEST_CONCURRENT=1
-
- - env: DB=MARIA55
- sudo: required
- dist: trusty
- go: 1.10.x
- services:
- - docker
- before_install:
- - go get golang.org/x/tools/cmd/cover
- - go get github.com/mattn/goveralls
- - docker pull mariadb:5.5
- - docker run -d -p 127.0.0.1:3307:3306 --name mysqld -e MYSQL_DATABASE=gotest -e MYSQL_USER=gotest -e MYSQL_PASSWORD=secret -e MYSQL_ROOT_PASSWORD=verysecret
- mariadb:5.5 --innodb_log_file_size=256MB --innodb_buffer_pool_size=512MB --max_allowed_packet=16MB --local-infile=1
- - cp .travis/docker.cnf ~/.my.cnf
- - .travis/wait_mysql.sh
- before_script:
- - export MYSQL_TEST_USER=gotest
- - export MYSQL_TEST_PASS=secret
- - export MYSQL_TEST_ADDR=127.0.0.1:3307
- - export MYSQL_TEST_CONCURRENT=1
-
- - env: DB=MARIA10_1
- sudo: required
- dist: trusty
- go: 1.10.x
- services:
- - docker
- before_install:
- - go get golang.org/x/tools/cmd/cover
- - go get github.com/mattn/goveralls
- - docker pull mariadb:10.1
- - docker run -d -p 127.0.0.1:3307:3306 --name mysqld -e MYSQL_DATABASE=gotest -e MYSQL_USER=gotest -e MYSQL_PASSWORD=secret -e MYSQL_ROOT_PASSWORD=verysecret
- mariadb:10.1 --innodb_log_file_size=256MB --innodb_buffer_pool_size=512MB --max_allowed_packet=16MB --local-infile=1
- - cp .travis/docker.cnf ~/.my.cnf
- - .travis/wait_mysql.sh
- before_script:
- - export MYSQL_TEST_USER=gotest
- - export MYSQL_TEST_PASS=secret
- - export MYSQL_TEST_ADDR=127.0.0.1:3307
- - export MYSQL_TEST_CONCURRENT=1
-
- - os: osx
- osx_image: xcode10.1
- addons:
- homebrew:
- packages:
- - mysql
- update: true
- go: 1.12.x
- before_install:
- - go get golang.org/x/tools/cmd/cover
- - go get github.com/mattn/goveralls
- before_script:
- - echo -e "[server]\ninnodb_log_file_size=256MB\ninnodb_buffer_pool_size=512MB\nmax_allowed_packet=16MB\nlocal_infile=1" >> /usr/local/etc/my.cnf
- - mysql.server start
- - mysql -uroot -e 'CREATE USER gotest IDENTIFIED BY "secret"'
- - mysql -uroot -e 'GRANT ALL ON *.* TO gotest'
- - mysql -uroot -e 'create database gotest;'
- - export MYSQL_TEST_USER=gotest
- - export MYSQL_TEST_PASS=secret
- - export MYSQL_TEST_ADDR=127.0.0.1:3306
- - export MYSQL_TEST_CONCURRENT=1
-
-script:
- - go test -v -covermode=count -coverprofile=coverage.out
- - go vet ./...
- - .travis/gofmt.sh
-after_script:
- - $HOME/gopath/bin/goveralls -coverprofile=coverage.out -service=travis-ci
diff --git a/.travis/docker.cnf b/.travis/docker.cnf
deleted file mode 100644
index e57754e..0000000
--- a/.travis/docker.cnf
+++ /dev/null
@@ -1,5 +0,0 @@
-[client]
-user = gotest
-password = secret
-host = 127.0.0.1
-port = 3307
diff --git a/.travis/gofmt.sh b/.travis/gofmt.sh
deleted file mode 100755
index 9bf0d16..0000000
--- a/.travis/gofmt.sh
+++ /dev/null
@@ -1,7 +0,0 @@
-#!/bin/bash
-set -ev
-
-# Only check for go1.10+ since the gofmt style changed
-if [[ $(go version) =~ go1\.([0-9]+) ]] && ((${BASH_REMATCH[1]} >= 10)); then
- test -z "$(gofmt -d -s . | tee /dev/stderr)"
-fi
diff --git a/.travis/wait_mysql.sh b/.travis/wait_mysql.sh
deleted file mode 100755
index e87993e..0000000
--- a/.travis/wait_mysql.sh
+++ /dev/null
@@ -1,8 +0,0 @@
-#!/bin/sh
-while :
-do
- if mysql -e 'select version()' 2>&1 | grep 'version()\|ERROR 2059 (HY000):'; then
- break
- fi
- sleep 3
-done
diff --git a/AUTHORS b/AUTHORS
index ad59898..0513275 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -13,12 +13,17 @@
Aaron Hopkins <go-sql-driver at die.net>
Achille Roussel <achille.roussel at gmail.com>
+Alex Snast <alexsn at fb.com>
Alexey Palazhchenko <alexey.palazhchenko at gmail.com>
Andrew Reid <andrew.reid at tixtrack.com>
+Animesh Ray <mail.rayanimesh at gmail.com>
Arne Hormann <arnehormann at gmail.com>
+Ariel Mashraki <ariel at mashraki.co.il>
Asta Xie <xiemengjun at gmail.com>
Bulat Gaifullin <gaifullinbf at gmail.com>
+Caine Jette <jette at alum.mit.edu>
Carlos Nieto <jose.carlos at menteslibres.net>
+Chris Kirkland <chriskirkland at github.com>
Chris Moos <chris at tech9computers.com>
Craig Wilson <craiggwilson at gmail.com>
Daniel Montoya <dsmontoyam at gmail.com>
@@ -41,6 +46,7 @@ Ilia Cimpoes <ichimpoesh at gmail.com>
INADA Naoki <songofacandy at gmail.com>
Jacek Szwec <szwec.jacek at gmail.com>
James Harr <james.harr at gmail.com>
+Janek Vedock <janekvedock at comcast.net>
Jeff Hodges <jeff at somethingsimilar.com>
Jeffrey Charles <jeffreycharles at gmail.com>
Jerome Meyer <jxmeyer at gmail.com>
@@ -52,14 +58,17 @@ Julien Schmidt <go-sql-driver at julienschmidt.com>
Justin Li <jli at j-li.net>
Justin Nuß <nuss.justin at gmail.com>
Kamil Dziedzic <kamil at klecza.pl>
+Kei Kamikawa <x00.x7f.x86 at gmail.com>
Kevin Malachowski <kevin at chowski.com>
Kieron Woodhouse <kieron.woodhouse at infosum.com>
+Lance Tian <lance6716 at gmail.com>
Lennart Rudolph <lrudolph at hmc.edu>
Leonardo YongUk Kim <dalinaum at gmail.com>
Linh Tran Tuan <linhduonggnu at gmail.com>
Lion Yang <lion at aosc.xyz>
Luca Looz <luca.looz92 at gmail.com>
Lucas Liu <extrafliu at gmail.com>
+Lunny Xiao <xiaolunwen at gmail.com>
Luke Scott <luke at webconnex.com>
Maciej Zimnoch <maciej.zimnoch at codilime.com>
Michael Woolnough <michael.woolnough at gmail.com>
@@ -74,26 +83,35 @@ Reed Allman <rdallman10 at gmail.com>
Richard Wilkes <wilkes at me.com>
Robert Russell <robert at rrbrussell.com>
Runrioter Wung <runrioter at gmail.com>
+Santhosh Kumar Tekuri <santhosh.tekuri at gmail.com>
+Sho Iizuka <sho.i518 at gmail.com>
+Sho Ikeda <suicaicoca at gmail.com>
Shuode Li <elemount at qq.com>
Simon J Mudd <sjmudd at pobox.com>
Soroush Pour <me at soroushjp.com>
Stan Putrya <root.vagner at gmail.com>
Stanley Gunawan <gunawan.stanley at gmail.com>
Steven Hartland <steven.hartland at multiplay.co.uk>
+Tan Jinhua <312841925 at qq.com>
Thomas Wodarek <wodarekwebpage at gmail.com>
Tim Ruffles <timruffles at gmail.com>
Tom Jenkinson <tom at tjenkinson.me>
Vladimir Kovpak <cn007b at gmail.com>
+Vladyslav Zhelezniak <zhvladi at gmail.com>
Xiangyu Hu <xiangyu.hu at outlook.com>
Xiaobing Jiang <s7v7nislands at gmail.com>
Xiuming Chen <cc at cxm.cc>
+Xuehong Chan <chanxuehong at gmail.com>
Zhenye Xie <xiezhenye at gmail.com>
+Zhixin Wen <john.wenzhixin at gmail.com>
+Ziheng Lyu <zihenglv at gmail.com>
# Organizations
Barracuda Networks, Inc.
Counting Ltd.
DigitalOcean Inc.
+dyves labs AG
Facebook Inc.
GitHub Inc.
Google Inc.
@@ -103,3 +121,4 @@ Multiplay Ltd.
Percona LLC
Pivotal Inc.
Stripe Inc.
+Zendesk Inc.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9cb97b3..77024a8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,50 @@
+## Version 1.7 (2022-11-29)
+
+Changes:
+
+ - Drop support of Go 1.12 (#1211)
+ - Refactoring `(*textRows).readRow` in a more clear way (#1230)
+ - util: Reduce boundary check in escape functions. (#1316)
+ - enhancement for mysqlConn handleAuthResult (#1250)
+
+New Features:
+
+ - support Is comparison on MySQLError (#1210)
+ - return unsigned in database type name when necessary (#1238)
+ - Add API to express like a --ssl-mode=PREFERRED MySQL client (#1370)
+ - Add SQLState to MySQLError (#1321)
+
+Bugfixes:
+
+ - Fix parsing 0 year. (#1257)
+
+
+## Version 1.6 (2021-04-01)
+
+Changes:
+
+ - Migrate the CI service from travis-ci to GitHub Actions (#1176, #1183, #1190)
+ - `NullTime` is deprecated (#960, #1144)
+ - Reduce allocations when building SET command (#1111)
+ - Performance improvement for time formatting (#1118)
+ - Performance improvement for time parsing (#1098, #1113)
+
+New Features:
+
+ - Implement `driver.Validator` interface (#1106, #1174)
+ - Support returning `uint64` from `Valuer` in `ConvertValue` (#1143)
+ - Add `json.RawMessage` for converter and prepared statement (#1059)
+ - Interpolate `json.RawMessage` as `string` (#1058)
+ - Implements `CheckNamedValue` (#1090)
+
+Bugfixes:
+
+ - Stop rounding times (#1121, #1172)
+ - Put zero filler into the SSL handshake packet (#1066)
+ - Fix checking cancelled connections back into the connection pool (#1095)
+ - Fix remove last 0 byte for mysql_old_password when password is empty (#1133)
+
+
## Version 1.5 (2020-01-07)
Changes:
diff --git a/README.md b/README.md
index d2627a4..25de2e5 100644
--- a/README.md
+++ b/README.md
@@ -35,12 +35,12 @@ A MySQL-Driver for Go's [database/sql](https://golang.org/pkg/database/sql/) pac
* Supports queries larger than 16MB
* Full [`sql.RawBytes`](https://golang.org/pkg/database/sql/#RawBytes) support.
* Intelligent `LONG DATA` handling in prepared statements
- * Secure `LOAD DATA LOCAL INFILE` support with file Whitelisting and `io.Reader` support
+ * Secure `LOAD DATA LOCAL INFILE` support with file allowlisting and `io.Reader` support
* Optional `time.Time` parsing
* Optional placeholder interpolation
## Requirements
- * Go 1.10 or higher. We aim to support the 3 latest versions of Go.
+ * 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+)
---------------------------------------
@@ -56,15 +56,37 @@ Make sure [Git is installed](https://git-scm.com/downloads) on your machine and
_Go MySQL Driver_ is an implementation of Go's `database/sql/driver` interface. You only need to import the driver and can use the full [`database/sql`](https://golang.org/pkg/database/sql/) API then.
Use `mysql` as `driverName` and a valid [DSN](#dsn-data-source-name) as `dataSourceName`:
+
```go
-import "database/sql"
-import _ "github.com/go-sql-driver/mysql"
+import (
+ "database/sql"
+ "time"
+
+ _ "github.com/go-sql-driver/mysql"
+)
+
+// ...
db, err := sql.Open("mysql", "user:password@/dbname")
+if err != nil {
+ panic(err)
+}
+// See "Important settings" section.
+db.SetConnMaxLifetime(time.Minute * 3)
+db.SetMaxOpenConns(10)
+db.SetMaxIdleConns(10)
```
[Examples are available in our Wiki](https://github.com/go-sql-driver/mysql/wiki/Examples "Go-MySQL-Driver Examples").
+### Important settings
+
+`db.SetConnMaxLifetime()` is required to ensure connections are closed by the driver safely before connection is closed by MySQL server, OS, or other middlewares. Since some middlewares close idle connections by 5 minutes, we recommend timeout shorter than 5 minutes. This setting helps load balancing and changing system variables too.
+
+`db.SetMaxOpenConns()` is highly recommended to limit the number of connection used by the application. There is no recommended limit number because it depends on application and MySQL server.
+
+`db.SetMaxIdleConns()` is recommended to be set same to `db.SetMaxOpenConns()`. When it is smaller than `SetMaxOpenConns()`, connections can be opened and closed much more frequently than you expect. Idle connections can be closed by the `db.SetConnMaxLifetime()`. If you want to close idle connections more rapidly, you can use `db.SetConnMaxIdleTime()` since Go 1.15.
+
### DSN (Data Source Name)
@@ -122,7 +144,7 @@ Valid Values: true, false
Default: false
```
-`allowAllFiles=true` disables the file Whitelist 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)
##### `allowCleartextPasswords`
@@ -133,7 +155,18 @@ Valid Values: true, false
Default: false
```
-`allowCleartextPasswords=true` allows using the [cleartext client side plugin](http://dev.mysql.com/doc/en/cleartext-authentication-plugin.html) if required by an account, such as one defined with the [PAM authentication plugin](http://dev.mysql.com/doc/en/pam-authentication-plugin.html). Sending passwords in clear text may be a security problem in some configurations. To avoid problems if there is any possibility that the password would be intercepted, clients should connect to MySQL Server using a method that protects the password. Possibilities include [TLS / SSL](#tls), IPsec, or a private network.
+`allowCleartextPasswords=true` allows using the [cleartext client side plugin](https://dev.mysql.com/doc/en/cleartext-pluggable-authentication.html) if required by an account, such as one defined with the [PAM authentication plugin](http://dev.mysql.com/doc/en/pam-authentication-plugin.html). Sending passwords in clear text may be a security problem in some configurations. To avoid problems if there is any possibility that the password would be intercepted, clients should connect to MySQL Server using a method that protects the password. Possibilities include [TLS / SSL](#tls), IPsec, or a private network.
+
+
+##### `allowFallbackToPlaintext`
+
+```
+Type: bool
+Valid Values: true, false
+Default: false
+```
+
+`allowFallbackToPlaintext=true` acts like a `--ssl-mode=PREFERRED` MySQL client as described in [Command Options for Connecting to the Server](https://dev.mysql.com/doc/refman/5.7/en/connection-options.html#option_general_ssl-mode)
##### `allowNativePasswords`
@@ -230,7 +263,7 @@ Default: false
If `interpolateParams` is true, placeholders (`?`) in calls to `db.Query()` and `db.Exec()` are interpolated into a single query string with given parameters. This reduces the number of roundtrips, since the driver has to prepare a statement, execute it with given parameters and close the statement again with `interpolateParams=false`.
-*This can not be used together with the multibyte encodings BIG5, CP932, GB2312, GBK or SJIS. These are blacklisted as they may [introduce a SQL injection vulnerability](http://stackoverflow.com/a/12118602/3430118)!*
+*This can not be used together with the multibyte encodings BIG5, CP932, GB2312, GBK or SJIS. These are rejected as they may [introduce a SQL injection vulnerability](http://stackoverflow.com/a/12118602/3430118)!*
##### `loc`
@@ -376,7 +409,7 @@ Rules:
Examples:
* `autocommit=1`: `SET autocommit=1`
* [`time_zone=%27Europe%2FParis%27`](https://dev.mysql.com/doc/refman/5.5/en/time-zone-support.html): `SET time_zone='Europe/Paris'`
- * [`tx_isolation=%27REPEATABLE-READ%27`](https://dev.mysql.com/doc/refman/5.5/en/server-system-variables.html#sysvar_tx_isolation): `SET tx_isolation='REPEATABLE-READ'`
+ * [`transaction_isolation=%27REPEATABLE-READ%27`](https://dev.mysql.com/doc/refman/5.7/en/server-system-variables.html#sysvar_transaction_isolation): `SET transaction_isolation='REPEATABLE-READ'`
#### Examples
@@ -432,7 +465,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.
## `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.
+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`.
## `context.Context` Support
Go 1.8 added `database/sql` support for `context.Context`. This driver supports query timeouts and cancellation via contexts.
@@ -445,7 +478,7 @@ For this feature you need direct access to the package. Therefore you must chang
import "github.com/go-sql-driver/mysql"
```
-Files must be whitelisted by registering them with `mysql.RegisterLocalFile(filepath)` (recommended) or the Whitelist 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!*](http://dev.mysql.com/doc/refman/5.7/en/load-data-local.html)).
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.
@@ -459,8 +492,6 @@ However, many want to scan MySQL `DATE` and `DATETIME` values into `time.Time` v
**Caution:** As of Go 1.1, this makes `time.Time` the only variable type you can scan `DATE` and `DATETIME` values into. This breaks for example [`sql.RawBytes` support](https://github.com/go-sql-driver/mysql/wiki/Examples#rawbytes).
-Alternatively you can use the [`NullTime`](https://godoc.org/github.com/go-sql-driver/mysql#NullTime) type as the scan destination, which works with both `time.Time` and `string` / `[]byte`.
-
### Unicode support
Since version 1.5 Go-MySQL-Driver automatically uses the collation ` utf8mb4_general_ci` by default.
@@ -477,7 +508,7 @@ To run the driver tests you may need to adjust the configuration. See the [Testi
Go-MySQL-Driver is not feature-complete yet. Your help is very appreciated.
If you want to contribute, you can work on an [open issue](https://github.com/go-sql-driver/mysql/issues?state=open) or review a [pull request](https://github.com/go-sql-driver/mysql/pulls).
-See the [Contribution Guidelines](https://github.com/go-sql-driver/mysql/blob/master/CONTRIBUTING.md) for details.
+See the [Contribution Guidelines](https://github.com/go-sql-driver/mysql/blob/master/.github/CONTRIBUTING.md) for details.
---------------------------------------
@@ -498,4 +529,3 @@ Please read the [MPL 2.0 FAQ](https://www.mozilla.org/en-US/MPL/2.0/FAQ/) if you
You can read the full terms here: [LICENSE](https://raw.github.com/go-sql-driver/mysql/master/LICENSE).
![Go Gopher and MySQL Dolphin](https://raw.github.com/wiki/go-sql-driver/mysql/go-mysql-driver_m.jpg "Golang Gopher transporting the MySQL Dolphin in a wheelbarrow")
-
diff --git a/atomic_bool.go b/atomic_bool.go
new file mode 100644
index 0000000..1b7e19f
--- /dev/null
+++ b/atomic_bool.go
@@ -0,0 +1,19 @@
+// Go MySQL Driver - A MySQL-Driver for Go's database/sql package.
+//
+// Copyright 2022 The Go-MySQL-Driver Authors. All rights reserved.
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
+// You can obtain one at http://mozilla.org/MPL/2.0/.
+//go:build go1.19
+// +build go1.19
+
+package mysql
+
+import "sync/atomic"
+
+/******************************************************************************
+* Sync utils *
+******************************************************************************/
+
+type atomicBool = atomic.Bool
diff --git a/atomic_bool_go118.go b/atomic_bool_go118.go
new file mode 100644
index 0000000..2e9a7f0
--- /dev/null
+++ b/atomic_bool_go118.go
@@ -0,0 +1,47 @@
+// Go MySQL Driver - A MySQL-Driver for Go's database/sql package.
+//
+// Copyright 2022 The Go-MySQL-Driver Authors. All rights reserved.
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
+// You can obtain one at http://mozilla.org/MPL/2.0/.
+//go:build !go1.19
+// +build !go1.19
+
+package mysql
+
+import "sync/atomic"
+
+/******************************************************************************
+* Sync utils *
+******************************************************************************/
+
+// atomicBool is an implementation of atomic.Bool for older version of Go.
+// it is a wrapper around uint32 for usage as a boolean value with
+// atomic access.
+type atomicBool struct {
+ _ noCopy
+ value uint32
+}
+
+// Load returns whether the current boolean value is true
+func (ab *atomicBool) Load() bool {
+ return atomic.LoadUint32(&ab.value) > 0
+}
+
+// Store sets the value of the bool regardless of the previous value
+func (ab *atomicBool) Store(value bool) {
+ if value {
+ atomic.StoreUint32(&ab.value, 1)
+ } else {
+ atomic.StoreUint32(&ab.value, 0)
+ }
+}
+
+// Swap sets the value of the bool and returns the old value.
+func (ab *atomicBool) Swap(value bool) bool {
+ if value {
+ return atomic.SwapUint32(&ab.value, 1) > 0
+ }
+ return atomic.SwapUint32(&ab.value, 0) > 0
+}
diff --git a/atomic_bool_test.go b/atomic_bool_test.go
new file mode 100644
index 0000000..a3b4ea0
--- /dev/null
+++ b/atomic_bool_test.go
@@ -0,0 +1,71 @@
+// Go MySQL Driver - A MySQL-Driver for Go's database/sql package.
+//
+// Copyright 2022 The Go-MySQL-Driver Authors. All rights reserved.
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
+// You can obtain one at http://mozilla.org/MPL/2.0/.
+//go:build !go1.19
+// +build !go1.19
+
+package mysql
+
+import (
+ "testing"
+)
+
+func TestAtomicBool(t *testing.T) {
+ var ab atomicBool
+ if ab.Load() {
+ t.Fatal("Expected value to be false")
+ }
+
+ ab.Store(true)
+ if ab.value != 1 {
+ t.Fatal("Set(true) did not set value to 1")
+ }
+ if !ab.Load() {
+ t.Fatal("Expected value to be true")
+ }
+
+ ab.Store(true)
+ if !ab.Load() {
+ t.Fatal("Expected value to be true")
+ }
+
+ ab.Store(false)
+ if ab.value != 0 {
+ t.Fatal("Set(false) did not set value to 0")
+ }
+ if ab.Load() {
+ t.Fatal("Expected value to be false")
+ }
+
+ ab.Store(false)
+ if ab.Load() {
+ t.Fatal("Expected value to be false")
+ }
+ if ab.Swap(false) {
+ t.Fatal("Expected the old value to be false")
+ }
+ if ab.Swap(true) {
+ t.Fatal("Expected the old value to be false")
+ }
+ if !ab.Load() {
+ t.Fatal("Expected value to be true")
+ }
+
+ ab.Store(true)
+ if !ab.Load() {
+ t.Fatal("Expected value to be true")
+ }
+ if !ab.Swap(true) {
+ t.Fatal("Expected the old value to be true")
+ }
+ if !ab.Swap(false) {
+ t.Fatal("Expected the old value to be true")
+ }
+ if ab.Load() {
+ t.Fatal("Expected value to be false")
+ }
+}
diff --git a/auth.go b/auth.go
index fec7040..1ff203e 100644
--- a/auth.go
+++ b/auth.go
@@ -15,6 +15,7 @@ import (
"crypto/sha256"
"crypto/x509"
"encoding/pem"
+ "fmt"
"sync"
)
@@ -32,27 +33,26 @@ var (
// Note: The provided rsa.PublicKey instance is exclusively owned by the driver
// after registering it and may not be modified.
//
-// data, err := ioutil.ReadFile("mykey.pem")
-// if err != nil {
-// log.Fatal(err)
-// }
+// data, err := ioutil.ReadFile("mykey.pem")
+// if err != nil {
+// log.Fatal(err)
+// }
//
-// block, _ := pem.Decode(data)
-// if block == nil || block.Type != "PUBLIC KEY" {
-// log.Fatal("failed to decode PEM block containing public key")
-// }
+// block, _ := pem.Decode(data)
+// if block == nil || block.Type != "PUBLIC KEY" {
+// log.Fatal("failed to decode PEM block containing public key")
+// }
//
-// pub, err := x509.ParsePKIXPublicKey(block.Bytes)
-// if err != nil {
-// log.Fatal(err)
-// }
-//
-// if rsaPubKey, ok := pub.(*rsa.PublicKey); ok {
-// mysql.RegisterServerPubKey("mykey", rsaPubKey)
-// } else {
-// log.Fatal("not a RSA public key")
-// }
+// pub, err := x509.ParsePKIXPublicKey(block.Bytes)
+// if err != nil {
+// log.Fatal(err)
+// }
//
+// if rsaPubKey, ok := pub.(*rsa.PublicKey); ok {
+// mysql.RegisterServerPubKey("mykey", rsaPubKey)
+// } else {
+// log.Fatal("not a RSA public key")
+// }
func RegisterServerPubKey(name string, pubKey *rsa.PublicKey) {
serverPubKeyLock.Lock()
if serverPubKeyRegistry == nil {
@@ -136,10 +136,6 @@ func pwHash(password []byte) (result [2]uint32) {
// Hash password using insecure pre 4.1 method
func scrambleOldPassword(scramble []byte, password string) []byte {
- if len(password) == 0 {
- return nil
- }
-
scramble = scramble[:8]
hashPw := pwHash([]byte(password))
@@ -247,6 +243,9 @@ func (mc *mysqlConn) auth(authData []byte, plugin string) ([]byte, error) {
if !mc.cfg.AllowOldPasswords {
return nil, ErrOldPassword
}
+ if len(mc.cfg.Passwd) == 0 {
+ return nil, nil
+ }
// Note: there are edge cases where this should work but doesn't;
// this is currently "wontfix":
// https://github.com/go-sql-driver/mysql/issues/184
@@ -274,7 +273,9 @@ func (mc *mysqlConn) auth(authData []byte, plugin string) ([]byte, error) {
if len(mc.cfg.Passwd) == 0 {
return []byte{0}, nil
}
- if mc.cfg.tls != nil || mc.cfg.Net == "unix" {
+ // unlike caching_sha2_password, sha256_password does not accept
+ // cleartext password on unix transport.
+ if mc.cfg.TLS != nil {
// write cleartext auth packet
return append([]byte(mc.cfg.Passwd), 0), nil
}
@@ -350,7 +351,7 @@ func (mc *mysqlConn) handleAuthResult(oldAuthData []byte, plugin string) error {
}
case cachingSha2PasswordPerformFullAuthentication:
- if mc.cfg.tls != nil || mc.cfg.Net == "unix" {
+ if mc.cfg.TLS != nil || mc.cfg.Net == "unix" {
// write cleartext auth packet
err = mc.writeAuthSwitchPacket(append([]byte(mc.cfg.Passwd), 0))
if err != nil {
@@ -365,14 +366,24 @@ func (mc *mysqlConn) handleAuthResult(oldAuthData []byte, plugin string) error {
return err
}
data[4] = cachingSha2PasswordRequestPublicKey
- mc.writePacket(data)
+ err = mc.writePacket(data)
+ if err != nil {
+ return err
+ }
- // parse public key
if data, err = mc.readPacket(); err != nil {
return err
}
- block, _ := pem.Decode(data[1:])
+ if data[0] != iAuthMoreData {
+ return fmt.Errorf("unexpect resp from server for caching_sha2_password perform full authentication")
+ }
+
+ // parse public key
+ block, rest := pem.Decode(data[1:])
+ if block == nil {
+ return fmt.Errorf("No Pem data found, data: %s", rest)
+ }
pkix, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
return err
@@ -401,6 +412,10 @@ func (mc *mysqlConn) handleAuthResult(oldAuthData []byte, plugin string) error {
return nil // auth successful
default:
block, _ := pem.Decode(authData)
+ if block == nil {
+ return fmt.Errorf("no Pem data found, data: %s", authData)
+ }
+
pub, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
return err
diff --git a/auth_test.go b/auth_test.go
index 1920ef3..3ce0ea6 100644
--- a/auth_test.go
+++ b/auth_test.go
@@ -291,7 +291,7 @@ func TestAuthFastCachingSHA256PasswordFullSecure(t *testing.T) {
// Hack to make the caching_sha2_password plugin believe that the connection
// is secure
- mc.cfg.tls = &tls.Config{InsecureSkipVerify: true}
+ mc.cfg.TLS = &tls.Config{InsecureSkipVerify: true}
// check written auth response
authRespStart := 4 + 4 + 4 + 1 + 23 + len(mc.cfg.User) + 1
@@ -663,7 +663,7 @@ func TestAuthFastSHA256PasswordSecure(t *testing.T) {
// hack to make the caching_sha2_password plugin believe that the connection
// is secure
- mc.cfg.tls = &tls.Config{InsecureSkipVerify: true}
+ mc.cfg.TLS = &tls.Config{InsecureSkipVerify: true}
authData := []byte{6, 81, 96, 114, 14, 42, 50, 30, 76, 47, 1, 95, 126, 81,
62, 94, 83, 80, 52, 85}
@@ -676,7 +676,7 @@ func TestAuthFastSHA256PasswordSecure(t *testing.T) {
}
// unset TLS config to prevent the actual establishment of a TLS wrapper
- mc.cfg.tls = nil
+ mc.cfg.TLS = nil
err = mc.writeHandshakeResponsePacket(authResp, plugin)
if err != nil {
@@ -866,7 +866,7 @@ func TestAuthSwitchCachingSHA256PasswordFullSecure(t *testing.T) {
// Hack to make the caching_sha2_password plugin believe that the connection
// is secure
- mc.cfg.tls = &tls.Config{InsecureSkipVerify: true}
+ mc.cfg.TLS = &tls.Config{InsecureSkipVerify: true}
// auth switch request
conn.data = []byte{44, 0, 0, 2, 254, 99, 97, 99, 104, 105, 110, 103, 95,
@@ -1157,7 +1157,7 @@ func TestAuthSwitchOldPasswordEmpty(t *testing.T) {
t.Errorf("got error: %v", err)
}
- expectedReply := []byte{1, 0, 0, 3, 0}
+ expectedReply := []byte{0, 0, 0, 3}
if !bytes.Equal(conn.written, expectedReply) {
t.Errorf("got unexpected data: %v", conn.written)
}
@@ -1184,7 +1184,7 @@ func TestOldAuthSwitchPasswordEmpty(t *testing.T) {
t.Errorf("got error: %v", err)
}
- expectedReply := []byte{1, 0, 0, 3, 0}
+ expectedReply := []byte{0, 0, 0, 3}
if !bytes.Equal(conn.written, expectedReply) {
t.Errorf("got unexpected data: %v", conn.written)
}
@@ -1299,7 +1299,7 @@ func TestAuthSwitchSHA256PasswordSecure(t *testing.T) {
// Hack to make the caching_sha2_password plugin believe that the connection
// is secure
- mc.cfg.tls = &tls.Config{InsecureSkipVerify: true}
+ mc.cfg.TLS = &tls.Config{InsecureSkipVerify: true}
// auth switch request
conn.data = []byte{38, 0, 0, 2, 254, 115, 104, 97, 50, 53, 54, 95, 112, 97,
diff --git a/benchmark_test.go b/benchmark_test.go
index 3e25a3b..97ed781 100644
--- a/benchmark_test.go
+++ b/benchmark_test.go
@@ -127,7 +127,8 @@ func BenchmarkExec(b *testing.B) {
}
if _, err := stmt.Exec(); err != nil {
- b.Fatal(err.Error())
+ b.Logf("stmt.Exec failed: %v", err)
+ b.Fail()
}
}
}()
@@ -313,7 +314,7 @@ func BenchmarkExecContext(b *testing.B) {
defer db.Close()
for _, p := range []int{1, 2, 3, 4} {
b.Run(fmt.Sprintf("%d", p), func(b *testing.B) {
- benchmarkQueryContext(b, db, p)
+ benchmarkExecContext(b, db, p)
})
}
}
diff --git a/collations.go b/collations.go
index 8d2b556..295bfbe 100644
--- a/collations.go
+++ b/collations.go
@@ -13,7 +13,8 @@ const binaryCollation = "binary"
// A list of available collations mapped to the internal ID.
// To update this map use the following MySQL query:
-// SELECT COLLATION_NAME, ID FROM information_schema.COLLATIONS WHERE ID<256 ORDER BY ID
+//
+// SELECT COLLATION_NAME, ID FROM information_schema.COLLATIONS WHERE ID<256 ORDER BY ID
//
// Handshake packet have only 1 byte for collation_id. So we can't use collations with ID > 255.
//
@@ -247,7 +248,7 @@ var collations = map[string]byte{
"utf8mb4_0900_ai_ci": 255,
}
-// A blacklist of collations which is unsafe to interpolate parameters.
+// A denylist of collations which is unsafe to interpolate parameters.
// These multibyte encodings may contains 0x5c (`\`) in their trailing bytes.
var unsafeCollations = map[string]bool{
"big5_chinese_ci": true,
diff --git a/conncheck.go b/conncheck.go
index 024eb28..0ea7217 100644
--- a/conncheck.go
+++ b/conncheck.go
@@ -6,6 +6,7 @@
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.
+//go:build linux || darwin || dragonfly || freebsd || netbsd || openbsd || solaris || illumos
// +build linux darwin dragonfly freebsd netbsd openbsd solaris illumos
package mysql
diff --git a/conncheck_dummy.go b/conncheck_dummy.go
index ea7fb60..a56c138 100644
--- a/conncheck_dummy.go
+++ b/conncheck_dummy.go
@@ -6,6 +6,7 @@
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.
+//go:build !linux && !darwin && !dragonfly && !freebsd && !netbsd && !openbsd && !solaris && !illumos
// +build !linux,!darwin,!dragonfly,!freebsd,!netbsd,!openbsd,!solaris,!illumos
package mysql
diff --git a/conncheck_test.go b/conncheck_test.go
index 5399551..f7e0256 100644
--- a/conncheck_test.go
+++ b/conncheck_test.go
@@ -6,6 +6,7 @@
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.
+//go:build linux || darwin || dragonfly || freebsd || netbsd || openbsd || solaris || illumos
// +build linux darwin dragonfly freebsd netbsd openbsd solaris illumos
package mysql
diff --git a/connection.go b/connection.go
index e4bb59e..9539077 100644
--- a/connection.go
+++ b/connection.go
@@ -12,6 +12,7 @@ import (
"context"
"database/sql"
"database/sql/driver"
+ "encoding/json"
"io"
"net"
"strconv"
@@ -46,9 +47,10 @@ type mysqlConn struct {
// Handles parameters set in DSN after the connection is established
func (mc *mysqlConn) handleParams() (err error) {
+ var cmdSet strings.Builder
for param, val := range mc.cfg.Params {
switch param {
- // Charset
+ // Charset: character_set_connection, character_set_client, character_set_results
case "charset":
charsets := strings.Split(val, ",")
for i := range charsets {
@@ -62,12 +64,25 @@ func (mc *mysqlConn) handleParams() (err error) {
return
}
- // System Vars
+ // Other system vars accumulated in a single SET command
default:
- err = mc.exec("SET " + param + "=" + val + "")
- if err != nil {
- return
+ if cmdSet.Len() == 0 {
+ // 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.WriteString("SET ")
+ } else {
+ cmdSet.WriteByte(',')
}
+ cmdSet.WriteString(param)
+ cmdSet.WriteByte('=')
+ cmdSet.WriteString(val)
+ }
+ }
+
+ if cmdSet.Len() > 0 {
+ err = mc.exec(cmdSet.String())
+ if err != nil {
+ return
}
}
@@ -89,7 +104,7 @@ func (mc *mysqlConn) Begin() (driver.Tx, error) {
}
func (mc *mysqlConn) begin(readOnly bool) (driver.Tx, error) {
- if mc.closed.IsSet() {
+ if mc.closed.Load() {
errLog.Print(ErrInvalidConn)
return nil, driver.ErrBadConn
}
@@ -108,7 +123,7 @@ func (mc *mysqlConn) begin(readOnly bool) (driver.Tx, error) {
func (mc *mysqlConn) Close() (err error) {
// Makes Close idempotent
- if !mc.closed.IsSet() {
+ if !mc.closed.Load() {
err = mc.writeCommandPacket(comQuit)
}
@@ -122,7 +137,7 @@ func (mc *mysqlConn) Close() (err error) {
// is called before auth or on auth failure because MySQL will have already
// closed the network connection.
func (mc *mysqlConn) cleanup() {
- if !mc.closed.TrySet(true) {
+ if mc.closed.Swap(true) {
return
}
@@ -137,7 +152,7 @@ func (mc *mysqlConn) cleanup() {
}
func (mc *mysqlConn) error() error {
- if mc.closed.IsSet() {
+ if mc.closed.Load() {
if err := mc.canceled.Value(); err != nil {
return err
}
@@ -147,7 +162,7 @@ func (mc *mysqlConn) error() error {
}
func (mc *mysqlConn) Prepare(query string) (driver.Stmt, error) {
- if mc.closed.IsSet() {
+ if mc.closed.Load() {
errLog.Print(ErrInvalidConn)
return nil, driver.ErrBadConn
}
@@ -230,47 +245,21 @@ func (mc *mysqlConn) interpolateParams(query string, args []driver.Value) (strin
if v.IsZero() {
buf = append(buf, "'0000-00-00'"...)
} else {
- v := v.In(mc.cfg.Loc)
- v = v.Add(time.Nanosecond * 500) // To round under microsecond
- year := v.Year()
- year100 := year / 100
- year1 := year % 100
- month := v.Month()
- day := v.Day()
- hour := v.Hour()
- minute := v.Minute()
- second := v.Second()
- micro := v.Nanosecond() / 1000
-
- buf = append(buf, []byte{
- '\'',
- digits10[year100], digits01[year100],
- digits10[year1], digits01[year1],
- '-',
- digits10[month], digits01[month],
- '-',
- digits10[day], digits01[day],
- ' ',
- digits10[hour], digits01[hour],
- ':',
- digits10[minute], digits01[minute],
- ':',
- digits10[second], digits01[second],
- }...)
-
- if micro != 0 {
- micro10000 := micro / 10000
- micro100 := micro / 100 % 100
- micro1 := micro % 100
- buf = append(buf, []byte{
- '.',
- digits10[micro10000], digits01[micro10000],
- digits10[micro100], digits01[micro100],
- digits10[micro1], digits01[micro1],
- }...)
+ buf = append(buf, '\'')
+ buf, err = appendDateTime(buf, v.In(mc.cfg.Loc))
+ if err != nil {
+ return "", err
}
buf = append(buf, '\'')
}
+ case json.RawMessage:
+ buf = append(buf, '\'')
+ if mc.status&statusNoBackslashEscapes == 0 {
+ buf = escapeBytesBackslash(buf, v)
+ } else {
+ buf = escapeBytesQuotes(buf, v)
+ }
+ buf = append(buf, '\'')
case []byte:
if v == nil {
buf = append(buf, "NULL"...)
@@ -306,7 +295,7 @@ func (mc *mysqlConn) interpolateParams(query string, args []driver.Value) (strin
}
func (mc *mysqlConn) Exec(query string, args []driver.Value) (driver.Result, error) {
- if mc.closed.IsSet() {
+ if mc.closed.Load() {
errLog.Print(ErrInvalidConn)
return nil, driver.ErrBadConn
}
@@ -367,7 +356,7 @@ func (mc *mysqlConn) Query(query string, args []driver.Value) (driver.Rows, erro
}
func (mc *mysqlConn) query(query string, args []driver.Value) (*textRows, error) {
- if mc.closed.IsSet() {
+ if mc.closed.Load() {
errLog.Print(ErrInvalidConn)
return nil, driver.ErrBadConn
}
@@ -461,7 +450,7 @@ func (mc *mysqlConn) finish() {
// Ping implements driver.Pinger interface
func (mc *mysqlConn) Ping(ctx context.Context) (err error) {
- if mc.closed.IsSet() {
+ if mc.closed.Load() {
errLog.Print(ErrInvalidConn)
return driver.ErrBadConn
}
@@ -480,6 +469,10 @@ func (mc *mysqlConn) Ping(ctx context.Context) (err error) {
// BeginTx implements driver.ConnBeginTx interface
func (mc *mysqlConn) BeginTx(ctx context.Context, opts driver.TxOptions) (driver.Tx, error) {
+ if mc.closed.Load() {
+ return nil, driver.ErrBadConn
+ }
+
if err := mc.watchCancel(ctx); err != nil {
return nil, err
}
@@ -643,9 +636,15 @@ func (mc *mysqlConn) CheckNamedValue(nv *driver.NamedValue) (err error) {
// ResetSession implements driver.SessionResetter.
// (From Go 1.10)
func (mc *mysqlConn) ResetSession(ctx context.Context) error {
- if mc.closed.IsSet() {
+ if mc.closed.Load() {
return driver.ErrBadConn
}
mc.reset = true
return nil
}
+
+// IsValid implements driver.Validator interface
+// (From Go 1.15)
+func (mc *mysqlConn) IsValid() bool {
+ return !mc.closed.Load()
+}
diff --git a/connection_test.go b/connection_test.go
index 19c17ff..b6764a2 100644
--- a/connection_test.go
+++ b/connection_test.go
@@ -11,6 +11,7 @@ package mysql
import (
"context"
"database/sql/driver"
+ "encoding/json"
"errors"
"net"
"testing"
@@ -36,6 +37,33 @@ func TestInterpolateParams(t *testing.T) {
}
}
+func TestInterpolateParamsJSONRawMessage(t *testing.T) {
+ mc := &mysqlConn{
+ buf: newBuffer(nil),
+ maxAllowedPacket: maxPacketSize,
+ cfg: &Config{
+ InterpolateParams: true,
+ },
+ }
+
+ buf, err := json.Marshal(struct {
+ Value int `json:"value"`
+ }{Value: 42})
+ if err != nil {
+ t.Errorf("Expected err=nil, got %#v", err)
+ return
+ }
+ q, err := mc.interpolateParams("SELECT ?", []driver.Value{json.RawMessage(buf)})
+ if err != nil {
+ t.Errorf("Expected err=nil, got %#v", err)
+ return
+ }
+ expected := `SELECT '{\"value\":42}'`
+ if q != expected {
+ t.Errorf("Expected: %q\nGot: %q", expected, q)
+ }
+}
+
func TestInterpolateParamsTooManyPlaceholders(t *testing.T) {
mc := &mysqlConn{
buf: newBuffer(nil),
@@ -119,7 +147,7 @@ func TestCleanCancel(t *testing.T) {
t.Errorf("expected context.Canceled, got %#v", err)
}
- if mc.closed.IsSet() {
+ if mc.closed.Load() {
t.Error("expected mc is not closed, closed actually")
}
diff --git a/debian/changelog b/debian/changelog
index 783fc79..389e105 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,9 @@
+golang-github-go-sql-driver-mysql (1.7.0-1) UNRELEASED; urgency=low
+
+ * New upstream release.
+
+ -- Debian Janitor <janitor@jelmer.uk> Sun, 18 Dec 2022 04:46:35 -0000
+
golang-github-go-sql-driver-mysql (1.5.0-2) unstable; urgency=medium
[ Debian Janitor ]
diff --git a/driver.go b/driver.go
index c1bdf11..ad7aec2 100644
--- a/driver.go
+++ b/driver.go
@@ -8,10 +8,10 @@
//
// The driver should be used via the database/sql package:
//
-// import "database/sql"
-// import _ "github.com/go-sql-driver/mysql"
+// import "database/sql"
+// import _ "github.com/go-sql-driver/mysql"
//
-// db, err := sql.Open("mysql", "user:password@/dbname")
+// db, err := sql.Open("mysql", "user:password@/dbname")
//
// See https://github.com/go-sql-driver/mysql#usage for details
package mysql
diff --git a/driver_test.go b/driver_test.go
index ace083d..4850498 100644
--- a/driver_test.go
+++ b/driver_test.go
@@ -14,6 +14,7 @@ import (
"crypto/tls"
"database/sql"
"database/sql/driver"
+ "encoding/json"
"fmt"
"io"
"io/ioutil"
@@ -23,6 +24,7 @@ import (
"net/url"
"os"
"reflect"
+ "runtime"
"strings"
"sync"
"sync/atomic"
@@ -559,6 +561,29 @@ func TestRawBytes(t *testing.T) {
})
}
+func TestRawMessage(t *testing.T) {
+ runTests(t, dsn, func(dbt *DBTest) {
+ v1 := json.RawMessage("{}")
+ v2 := json.RawMessage("[]")
+ rows := dbt.mustQuery("SELECT ?, ?", v1, v2)
+ defer rows.Close()
+ if rows.Next() {
+ var o1, o2 json.RawMessage
+ if err := rows.Scan(&o1, &o2); err != nil {
+ dbt.Errorf("Got error: %v", err)
+ }
+ if !bytes.Equal(v1, o1) {
+ dbt.Errorf("expected %v, got %v", v1, o1)
+ }
+ if !bytes.Equal(v2, o2) {
+ dbt.Errorf("expected %v, got %v", v2, o2)
+ }
+ } else {
+ dbt.Errorf("no data")
+ }
+ })
+}
+
type testValuer struct {
value string
}
@@ -1425,11 +1450,11 @@ func TestCharset(t *testing.T) {
mustSetCharset("charset=ascii", "ascii")
// when the first charset is invalid, use the second
- mustSetCharset("charset=none,utf8", "utf8")
+ mustSetCharset("charset=none,utf8mb4", "utf8mb4")
// when the first charset is valid, use it
- mustSetCharset("charset=ascii,utf8", "ascii")
- mustSetCharset("charset=utf8,ascii", "utf8")
+ mustSetCharset("charset=ascii,utf8mb4", "ascii")
+ mustSetCharset("charset=utf8mb4,ascii", "utf8mb4")
}
func TestFailingCharset(t *testing.T) {
@@ -1454,7 +1479,7 @@ func TestCollation(t *testing.T) {
defaultCollation, // driver default
"latin1_general_ci",
"binary",
- "utf8_unicode_ci",
+ "utf8mb4_unicode_ci",
"cp1257_bin",
}
@@ -1782,6 +1807,14 @@ func TestConcurrent(t *testing.T) {
}
runTests(t, dsn, func(dbt *DBTest) {
+ var version string
+ if err := dbt.db.QueryRow("SELECT @@version").Scan(&version); err != nil {
+ dbt.Fatalf("%s", err.Error())
+ }
+ if strings.Contains(strings.ToLower(version), "mariadb") {
+ t.Skip(`TODO: "fix commands out of sync. Did you run multiple statements at once?" on MariaDB`)
+ }
+
var max int
err := dbt.db.QueryRow("SELECT @@max_connections").Scan(&max)
if err != nil {
@@ -2581,10 +2614,19 @@ func TestContextCancelStmtQuery(t *testing.T) {
}
func TestContextCancelBegin(t *testing.T) {
+ if runtime.GOOS == "windows" || runtime.GOOS == "darwin" {
+ 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) {
dbt.mustExec("CREATE TABLE test (v INTEGER)")
ctx, cancel := context.WithCancel(context.Background())
- tx, err := dbt.db.BeginTx(ctx, nil)
+ conn, err := dbt.db.Conn(ctx)
+ if err != nil {
+ dbt.Fatal(err)
+ }
+ defer conn.Close()
+ tx, err := conn.BeginTx(ctx, nil)
if err != nil {
dbt.Fatal(err)
}
@@ -2614,7 +2656,17 @@ func TestContextCancelBegin(t *testing.T) {
dbt.Errorf("expected sql.ErrTxDone or context.Canceled, got %v", err)
}
- // Context is canceled, so cannot begin a transaction.
+ // The connection is now in an inoperable state - so performing other
+ // operations should fail with ErrBadConn
+ // Important to exercise isolation level too - it runs SET TRANSACTION ISOLATION
+ // LEVEL XXX first, which needs to return ErrBadConn if the connection's context
+ // is cancelled
+ _, err = conn.BeginTx(context.Background(), &sql.TxOptions{Isolation: sql.LevelReadCommitted})
+ if err != driver.ErrBadConn {
+ dbt.Errorf("expected driver.ErrBadConn, got %v", err)
+ }
+
+ // cannot begin a transaction (on a different conn) with a canceled context
if _, err := dbt.db.BeginTx(ctx, nil); err != context.Canceled {
dbt.Errorf("expected context.Canceled, got %v", err)
}
@@ -2719,13 +2771,13 @@ func TestRowsColumnTypes(t *testing.T) {
nfNULL := sql.NullFloat64{Float64: 0.0, Valid: false}
nf0 := sql.NullFloat64{Float64: 0.0, Valid: true}
nf1337 := sql.NullFloat64{Float64: 13.37, Valid: true}
- nt0 := NullTime{Time: time.Date(2006, 01, 02, 15, 04, 05, 0, time.UTC), Valid: true}
- nt1 := NullTime{Time: time.Date(2006, 01, 02, 15, 04, 05, 100000000, time.UTC), Valid: true}
- nt2 := NullTime{Time: time.Date(2006, 01, 02, 15, 04, 05, 110000000, time.UTC), Valid: true}
- nt6 := NullTime{Time: time.Date(2006, 01, 02, 15, 04, 05, 111111000, time.UTC), Valid: true}
- nd1 := NullTime{Time: time.Date(2006, 01, 02, 0, 0, 0, 0, time.UTC), Valid: true}
- nd2 := NullTime{Time: time.Date(2006, 03, 04, 0, 0, 0, 0, time.UTC), Valid: true}
- ndNULL := NullTime{Time: time.Time{}, Valid: false}
+ nt0 := sql.NullTime{Time: time.Date(2006, 01, 02, 15, 04, 05, 0, time.UTC), Valid: true}
+ nt1 := sql.NullTime{Time: time.Date(2006, 01, 02, 15, 04, 05, 100000000, time.UTC), Valid: true}
+ nt2 := sql.NullTime{Time: time.Date(2006, 01, 02, 15, 04, 05, 110000000, time.UTC), Valid: true}
+ nt6 := sql.NullTime{Time: time.Date(2006, 01, 02, 15, 04, 05, 111111000, 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}
+ ndNULL := sql.NullTime{Time: time.Time{}, Valid: false}
rbNULL := sql.RawBytes(nil)
rb0 := sql.RawBytes("0")
rb42 := sql.RawBytes("42")
@@ -2756,10 +2808,10 @@ func TestRowsColumnTypes(t *testing.T) {
{"mediumintnull", "MEDIUMINT", "MEDIUMINT", scanTypeNullInt, true, 0, 0, [3]string{"0", "42", "NULL"}, [3]interface{}{ni0, ni42, niNULL}},
{"bigint", "BIGINT NOT NULL", "BIGINT", scanTypeInt64, false, 0, 0, [3]string{"0", "65535", "-42"}, [3]interface{}{int64(0), int64(65535), int64(-42)}},
{"bigintnull", "BIGINT", "BIGINT", scanTypeNullInt, true, 0, 0, [3]string{"NULL", "1", "42"}, [3]interface{}{niNULL, ni1, ni42}},
- {"tinyuint", "TINYINT UNSIGNED NOT NULL", "TINYINT", scanTypeUint8, false, 0, 0, [3]string{"0", "255", "42"}, [3]interface{}{uint8(0), uint8(255), uint8(42)}},
- {"smalluint", "SMALLINT UNSIGNED NOT NULL", "SMALLINT", scanTypeUint16, false, 0, 0, [3]string{"0", "65535", "42"}, [3]interface{}{uint16(0), uint16(65535), uint16(42)}},
- {"biguint", "BIGINT UNSIGNED NOT NULL", "BIGINT", scanTypeUint64, false, 0, 0, [3]string{"0", "65535", "42"}, [3]interface{}{uint64(0), uint64(65535), uint64(42)}},
- {"uint13", "INT(13) UNSIGNED NOT NULL", "INT", scanTypeUint32, false, 0, 0, [3]string{"0", "1337", "42"}, [3]interface{}{uint32(0), uint32(1337), uint32(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)}},
+ {"biguint", "BIGINT UNSIGNED NOT NULL", "UNSIGNED BIGINT", scanTypeUint64, false, 0, 0, [3]string{"0", "65535", "42"}, [3]interface{}{uint64(0), uint64(65535), uint64(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)}},
{"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}},
@@ -2805,131 +2857,125 @@ func TestRowsColumnTypes(t *testing.T) {
values2 = values2[:len(values2)-2]
values3 = values3[:len(values3)-2]
- dsns := []string{
- dsn + "&parseTime=true",
- dsn + "&parseTime=false",
- }
- for _, testdsn := range dsns {
- runTests(t, testdsn, func(dbt *DBTest) {
- dbt.mustExec("CREATE TABLE test (" + schema + ")")
- dbt.mustExec("INSERT INTO test VALUES (" + values1 + "), (" + values2 + "), (" + values3 + ")")
+ runTests(t, dsn+"&parseTime=true", func(dbt *DBTest) {
+ dbt.mustExec("CREATE TABLE test (" + schema + ")")
+ dbt.mustExec("INSERT INTO test VALUES (" + values1 + "), (" + values2 + "), (" + values3 + ")")
- rows, err := dbt.db.Query("SELECT * FROM test")
- if err != nil {
- t.Fatalf("Query: %v", err)
- }
+ rows, err := dbt.db.Query("SELECT * FROM test")
+ if err != nil {
+ t.Fatalf("Query: %v", err)
+ }
- tt, err := rows.ColumnTypes()
- if err != nil {
- t.Fatalf("ColumnTypes: %v", err)
- }
+ tt, err := rows.ColumnTypes()
+ if err != nil {
+ t.Fatalf("ColumnTypes: %v", err)
+ }
- if len(tt) != len(columns) {
- t.Fatalf("unexpected number of columns: expected %d, got %d", len(columns), len(tt))
- }
+ if len(tt) != len(columns) {
+ t.Fatalf("unexpected number of columns: expected %d, got %d", len(columns), len(tt))
+ }
- types := make([]reflect.Type, len(tt))
- for i, tp := range tt {
- column := columns[i]
+ types := make([]reflect.Type, len(tt))
+ for i, tp := range tt {
+ column := columns[i]
- // Name
- name := tp.Name()
- if name != column.name {
- t.Errorf("column name mismatch %s != %s", name, column.name)
- continue
- }
+ // Name
+ name := tp.Name()
+ if name != column.name {
+ t.Errorf("column name mismatch %s != %s", name, column.name)
+ continue
+ }
- // DatabaseTypeName
- databaseTypeName := tp.DatabaseTypeName()
- if databaseTypeName != column.databaseTypeName {
- t.Errorf("databasetypename name mismatch for column %q: %s != %s", name, databaseTypeName, column.databaseTypeName)
- continue
- }
+ // DatabaseTypeName
+ databaseTypeName := tp.DatabaseTypeName()
+ if databaseTypeName != column.databaseTypeName {
+ t.Errorf("databasetypename name mismatch for column %q: %s != %s", name, databaseTypeName, column.databaseTypeName)
+ continue
+ }
- // ScanType
- scanType := tp.ScanType()
- if scanType != column.scanType {
- if scanType == nil {
- t.Errorf("scantype is null for column %q", name)
- } else {
- t.Errorf("scantype mismatch for column %q: %s != %s", name, scanType.Name(), column.scanType.Name())
- }
- continue
+ // ScanType
+ scanType := tp.ScanType()
+ if scanType != column.scanType {
+ if scanType == nil {
+ t.Errorf("scantype is null for column %q", name)
+ } else {
+ t.Errorf("scantype mismatch for column %q: %s != %s", name, scanType.Name(), column.scanType.Name())
}
- types[i] = scanType
-
- // Nullable
- nullable, ok := tp.Nullable()
+ continue
+ }
+ types[i] = scanType
+
+ // Nullable
+ nullable, ok := tp.Nullable()
+ if !ok {
+ t.Errorf("nullable not ok %q", name)
+ continue
+ }
+ if nullable != column.nullable {
+ t.Errorf("nullable mismatch for column %q: %t != %t", name, nullable, column.nullable)
+ }
+
+ // Length
+ // length, ok := tp.Length()
+ // if length != column.length {
+ // if !ok {
+ // t.Errorf("length not ok for column %q", name)
+ // } else {
+ // t.Errorf("length mismatch for column %q: %d != %d", name, length, column.length)
+ // }
+ // continue
+ // }
+
+ // Precision and Scale
+ precision, scale, ok := tp.DecimalSize()
+ if precision != column.precision {
if !ok {
- t.Errorf("nullable not ok %q", name)
- continue
+ t.Errorf("precision not ok for column %q", name)
+ } else {
+ t.Errorf("precision mismatch for column %q: %d != %d", name, precision, column.precision)
}
- if nullable != column.nullable {
- t.Errorf("nullable mismatch for column %q: %t != %t", name, nullable, column.nullable)
- }
-
- // Length
- // length, ok := tp.Length()
- // if length != column.length {
- // if !ok {
- // t.Errorf("length not ok for column %q", name)
- // } else {
- // t.Errorf("length mismatch for column %q: %d != %d", name, length, column.length)
- // }
- // continue
- // }
-
- // Precision and Scale
- precision, scale, ok := tp.DecimalSize()
- if precision != column.precision {
- if !ok {
- t.Errorf("precision not ok for column %q", name)
- } else {
- t.Errorf("precision mismatch for column %q: %d != %d", name, precision, column.precision)
- }
- continue
- }
- if scale != column.scale {
- if !ok {
- t.Errorf("scale not ok for column %q", name)
- } else {
- t.Errorf("scale mismatch for column %q: %d != %d", name, scale, column.scale)
- }
- continue
+ continue
+ }
+ if scale != column.scale {
+ if !ok {
+ t.Errorf("scale not ok for column %q", name)
+ } else {
+ t.Errorf("scale mismatch for column %q: %d != %d", name, scale, column.scale)
}
+ continue
}
+ }
- values := make([]interface{}, len(tt))
- for i := range values {
- values[i] = reflect.New(types[i]).Interface()
+ values := make([]interface{}, len(tt))
+ for i := range values {
+ values[i] = reflect.New(types[i]).Interface()
+ }
+ i := 0
+ for rows.Next() {
+ err = rows.Scan(values...)
+ if err != nil {
+ t.Fatalf("failed to scan values in %v", err)
}
- i := 0
- for rows.Next() {
- err = rows.Scan(values...)
- if err != nil {
- t.Fatalf("failed to scan values in %v", err)
- }
- for j := range values {
- value := reflect.ValueOf(values[j]).Elem().Interface()
- 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])
- }
+ for j := range values {
+ value := reflect.ValueOf(values[j]).Elem().Interface()
+ 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])
}
}
- i++
- }
- if i != 3 {
- t.Errorf("expected 3 rows, got %d", i)
}
+ i++
+ }
+ if i != 3 {
+ t.Errorf("expected 3 rows, got %d", i)
+ }
- if err := rows.Close(); err != nil {
- t.Errorf("error closing rows: %s", err)
- }
- })
- }
+ if err := rows.Close(); err != nil {
+ t.Errorf("error closing rows: %s", err)
+ }
+ })
}
func TestValuerWithValueReceiverGivenNilValue(t *testing.T) {
diff --git a/dsn.go b/dsn.go
index 75c8c24..4b71aaa 100644
--- a/dsn.go
+++ b/dsn.go
@@ -46,22 +46,23 @@ type Config struct {
ServerPubKey string // Server public key name
pubKey *rsa.PublicKey // Server public key
TLSConfig string // TLS configuration name
- tls *tls.Config // TLS configuration
+ TLS *tls.Config // TLS configuration, its priority is higher than TLSConfig
Timeout time.Duration // Dial timeout
ReadTimeout time.Duration // I/O read timeout
WriteTimeout time.Duration // I/O write timeout
- AllowAllFiles bool // Allow all files to be used with LOAD DATA LOCAL INFILE
- AllowCleartextPasswords bool // Allows the cleartext client side plugin
- AllowNativePasswords bool // Allows the native password authentication method
- AllowOldPasswords bool // Allows the old insecure password method
- CheckConnLiveness bool // Check connections for liveness before using them
- ClientFoundRows bool // Return number of matching rows instead of rows changed
- ColumnsWithAlias bool // Prepend table alias to column names
- InterpolateParams bool // Interpolate placeholders into query string
- MultiStatements bool // Allow multiple statements in one query
- ParseTime bool // Parse time values to time.Time
- RejectReadOnly bool // Reject read-only connections
+ AllowAllFiles bool // Allow all files to be used with LOAD DATA LOCAL INFILE
+ AllowCleartextPasswords bool // Allows the cleartext client side plugin
+ AllowFallbackToPlaintext bool // Allows fallback to unencrypted connection if server does not support TLS
+ AllowNativePasswords bool // Allows the native password authentication method
+ AllowOldPasswords bool // Allows the old insecure password method
+ CheckConnLiveness bool // Check connections for liveness before using them
+ ClientFoundRows bool // Return number of matching rows instead of rows changed
+ ColumnsWithAlias bool // Prepend table alias to column names
+ InterpolateParams bool // Interpolate placeholders into query string
+ MultiStatements bool // Allow multiple statements in one query
+ ParseTime bool // Parse time values to time.Time
+ RejectReadOnly bool // Reject read-only connections
}
// NewConfig creates a new Config and sets default values.
@@ -77,8 +78,8 @@ func NewConfig() *Config {
func (cfg *Config) Clone() *Config {
cp := *cfg
- if cp.tls != nil {
- cp.tls = cfg.tls.Clone()
+ if cp.TLS != nil {
+ cp.TLS = cfg.TLS.Clone()
}
if len(cp.Params) > 0 {
cp.Params = make(map[string]string, len(cfg.Params))
@@ -119,24 +120,29 @@ func (cfg *Config) normalize() error {
cfg.Addr = ensureHavePort(cfg.Addr)
}
- switch cfg.TLSConfig {
- case "false", "":
- // don't set anything
- case "true":
- cfg.tls = &tls.Config{}
- case "skip-verify", "preferred":
- cfg.tls = &tls.Config{InsecureSkipVerify: true}
- default:
- cfg.tls = getTLSConfigClone(cfg.TLSConfig)
- if cfg.tls == nil {
- return errors.New("invalid value / unknown config name: " + cfg.TLSConfig)
+ if cfg.TLS == nil {
+ switch cfg.TLSConfig {
+ case "false", "":
+ // don't set anything
+ case "true":
+ cfg.TLS = &tls.Config{}
+ case "skip-verify":
+ cfg.TLS = &tls.Config{InsecureSkipVerify: true}
+ case "preferred":
+ cfg.TLS = &tls.Config{InsecureSkipVerify: true}
+ cfg.AllowFallbackToPlaintext = true
+ default:
+ cfg.TLS = getTLSConfigClone(cfg.TLSConfig)
+ if cfg.TLS == nil {
+ return errors.New("invalid value / unknown config name: " + cfg.TLSConfig)
+ }
}
}
- if cfg.tls != nil && cfg.tls.ServerName == "" && !cfg.tls.InsecureSkipVerify {
+ if cfg.TLS != nil && cfg.TLS.ServerName == "" && !cfg.TLS.InsecureSkipVerify {
host, _, err := net.SplitHostPort(cfg.Addr)
if err == nil {
- cfg.tls.ServerName = host
+ cfg.TLS.ServerName = host
}
}
@@ -204,6 +210,10 @@ func (cfg *Config) FormatDSN() string {
writeDSNParam(&buf, &hasParam, "allowCleartextPasswords", "true")
}
+ if cfg.AllowFallbackToPlaintext {
+ writeDSNParam(&buf, &hasParam, "allowFallbackToPlaintext", "true")
+ }
+
if !cfg.AllowNativePasswords {
writeDSNParam(&buf, &hasParam, "allowNativePasswords", "false")
}
@@ -375,7 +385,7 @@ func parseDSNParams(cfg *Config, params string) (err error) {
// cfg params
switch value := param[1]; param[0] {
- // Disable INFILE whitelist / enable all files
+ // Disable INFILE allowlist / enable all files
case "allowAllFiles":
var isBool bool
cfg.AllowAllFiles, isBool = readBool(value)
@@ -391,6 +401,14 @@ func parseDSNParams(cfg *Config, params string) (err error) {
return errors.New("invalid bool value: " + value)
}
+ // Allow fallback to unencrypted connection if server does not support TLS
+ case "allowFallbackToPlaintext":
+ var isBool bool
+ cfg.AllowFallbackToPlaintext, isBool = readBool(value)
+ if !isBool {
+ return errors.New("invalid bool value: " + value)
+ }
+
// Use native password authentication
case "allowNativePasswords":
var isBool bool
@@ -426,7 +444,6 @@ func parseDSNParams(cfg *Config, params string) (err error) {
// Collation
case "collation":
cfg.Collation = value
- break
case "columnsWithAlias":
var isBool bool
diff --git a/dsn_test.go b/dsn_test.go
index 89815b3..41a6a29 100644
--- a/dsn_test.go
+++ b/dsn_test.go
@@ -42,8 +42,8 @@ var testDSNs = []struct {
"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},
}, {
- "user:password@/dbname?allowNativePasswords=false&checkConnLiveness=false&maxAllowedPacket=0",
- &Config{User: "user", Passwd: "password", Net: "tcp", Addr: "127.0.0.1:3306", DBName: "dbname", Collation: "utf8mb4_general_ci", Loc: time.UTC, MaxAllowedPacket: 0, AllowNativePasswords: false, CheckConnLiveness: false},
+ "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},
}, {
"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},
@@ -82,7 +82,7 @@ func TestDSNParser(t *testing.T) {
}
// pointer not static
- cfg.tls = nil
+ cfg.TLS = nil
if !reflect.DeepEqual(cfg, tst.out) {
t.Errorf("%d. ParseDSN(%q) mismatch:\ngot %+v\nwant %+v", i, tst.in, cfg, tst.out)
@@ -92,13 +92,15 @@ func TestDSNParser(t *testing.T) {
func TestDSNParserInvalid(t *testing.T) {
var invalidDSNs = []string{
- "@net(addr/", // no closing brace
- "@tcp(/", // no closing brace
- "tcp(/", // no closing brace
- "(/", // no closing brace
- "net(addr)//", // unescaped
- "User:pass@tcp(1.2.3.4:3306)", // no trailing slash
- "net()/", // unknown default addr
+ "@net(addr/", // no closing brace
+ "@tcp(/", // no closing brace
+ "tcp(/", // no closing brace
+ "(/", // no closing brace
+ "net(addr)//", // unescaped
+ "User:pass@tcp(1.2.3.4:3306)", // no trailing slash
+ "net()/", // unknown default addr
+ "user:pass@tcp(127.0.0.1:3306)/db/name", // invalid dbname
+ "user:password@/dbname?allowFallbackToPlaintext=PREFERRED", // wrong bool flag
//"/dbname?arg=/some/unescaped/path",
}
@@ -117,7 +119,7 @@ func TestDSNReformat(t *testing.T) {
t.Error(err.Error())
continue
}
- cfg1.tls = nil // pointer not static
+ cfg1.TLS = nil // pointer not static
res1 := fmt.Sprintf("%+v", cfg1)
dsn2 := cfg1.FormatDSN()
@@ -126,7 +128,7 @@ func TestDSNReformat(t *testing.T) {
t.Error(err.Error())
continue
}
- cfg2.tls = nil // pointer not static
+ cfg2.TLS = nil // pointer not static
res2 := fmt.Sprintf("%+v", cfg2)
if res1 != res2 {
@@ -202,7 +204,7 @@ func TestDSNWithCustomTLS(t *testing.T) {
if err != nil {
t.Error(err.Error())
- } else if cfg.tls.ServerName != name {
+ } else if cfg.TLS.ServerName != name {
t.Errorf("did not get the correct TLS ServerName (%s) parsing DSN (%s).", name, tst)
}
@@ -213,7 +215,7 @@ func TestDSNWithCustomTLS(t *testing.T) {
if err != nil {
t.Error(err.Error())
- } else if cfg.tls.ServerName != name {
+ } else if cfg.TLS.ServerName != name {
t.Errorf("did not get the correct ServerName (%s) parsing DSN (%s).", name, tst)
} else if tlsCfg.ServerName != "" {
t.Errorf("tlsCfg was mutated ServerName (%s) should be empty parsing DSN (%s).", name, tst)
@@ -228,11 +230,11 @@ func TestDSNTLSConfig(t *testing.T) {
if err != nil {
t.Error(err.Error())
}
- if cfg.tls == nil {
+ if cfg.TLS == nil {
t.Error("cfg.tls should not be nil")
}
- if cfg.tls.ServerName != expectedServerName {
- t.Errorf("cfg.tls.ServerName should be %q, got %q (host with port)", expectedServerName, cfg.tls.ServerName)
+ if cfg.TLS.ServerName != expectedServerName {
+ t.Errorf("cfg.tls.ServerName should be %q, got %q (host with port)", expectedServerName, cfg.TLS.ServerName)
}
dsn = "tcp(example.com)/?tls=true"
@@ -240,11 +242,11 @@ func TestDSNTLSConfig(t *testing.T) {
if err != nil {
t.Error(err.Error())
}
- if cfg.tls == nil {
+ if cfg.TLS == nil {
t.Error("cfg.tls should not be nil")
}
- if cfg.tls.ServerName != expectedServerName {
- t.Errorf("cfg.tls.ServerName should be %q, got %q (host without port)", expectedServerName, cfg.tls.ServerName)
+ if cfg.TLS.ServerName != expectedServerName {
+ t.Errorf("cfg.tls.ServerName should be %q, got %q (host without port)", expectedServerName, cfg.TLS.ServerName)
}
}
@@ -261,7 +263,7 @@ func TestDSNWithCustomTLSQueryEscape(t *testing.T) {
if err != nil {
t.Error(err.Error())
- } else if cfg.tls.ServerName != name {
+ } else if cfg.TLS.ServerName != name {
t.Errorf("did not get the correct TLS ServerName (%s) parsing DSN (%s).", name, dsn)
}
}
@@ -334,12 +336,12 @@ func TestCloneConfig(t *testing.T) {
t.Errorf("Config.Clone did not create a separate config struct")
}
- if cfg2.tls.ServerName != expectedServerName {
- t.Errorf("cfg.tls.ServerName should be %q, got %q (host with port)", expectedServerName, cfg.tls.ServerName)
+ if cfg2.TLS.ServerName != expectedServerName {
+ t.Errorf("cfg.tls.ServerName should be %q, got %q (host with port)", expectedServerName, cfg.TLS.ServerName)
}
- cfg2.tls.ServerName = "example2.com"
- if cfg.tls.ServerName == cfg2.tls.ServerName {
+ cfg2.TLS.ServerName = "example2.com"
+ if cfg.TLS.ServerName == cfg2.TLS.ServerName {
t.Errorf("changed cfg.tls.Server name should not propagate to original Config")
}
@@ -383,20 +385,20 @@ func TestNormalizeTLSConfig(t *testing.T) {
cfg.normalize()
- if cfg.tls == nil {
+ if cfg.TLS == nil {
if tc.want != nil {
t.Fatal("wanted a tls config but got nil instead")
}
return
}
- if cfg.tls.ServerName != tc.want.ServerName {
+ if cfg.TLS.ServerName != tc.want.ServerName {
t.Errorf("tls.ServerName doesn't match (want: '%s', got: '%s')",
- tc.want.ServerName, cfg.tls.ServerName)
+ tc.want.ServerName, cfg.TLS.ServerName)
}
- if cfg.tls.InsecureSkipVerify != tc.want.InsecureSkipVerify {
+ if cfg.TLS.InsecureSkipVerify != tc.want.InsecureSkipVerify {
t.Errorf("tls.InsecureSkipVerify doesn't match (want: %T, got :%T)",
- tc.want.InsecureSkipVerify, cfg.tls.InsecureSkipVerify)
+ tc.want.InsecureSkipVerify, cfg.TLS.InsecureSkipVerify)
}
})
}
diff --git a/errors.go b/errors.go
index 760782f..7c037e7 100644
--- a/errors.go
+++ b/errors.go
@@ -56,10 +56,22 @@ func SetLogger(logger Logger) error {
// MySQLError is an error type which represents a single MySQL error
type MySQLError struct {
- Number uint16
- Message string
+ Number uint16
+ SQLState [5]byte
+ Message string
}
func (me *MySQLError) Error() string {
+ if me.SQLState != [5]byte{} {
+ return fmt.Sprintf("Error %d (%s): %s", me.Number, me.SQLState, me.Message)
+ }
+
return fmt.Sprintf("Error %d: %s", me.Number, me.Message)
}
+
+func (me *MySQLError) Is(err error) bool {
+ if merr, ok := err.(*MySQLError); ok {
+ return merr.Number == me.Number
+ }
+ return false
+}
diff --git a/errors_test.go b/errors_test.go
index 96f9126..43213f9 100644
--- a/errors_test.go
+++ b/errors_test.go
@@ -10,6 +10,7 @@ package mysql
import (
"bytes"
+ "errors"
"log"
"testing"
)
@@ -40,3 +41,21 @@ func TestErrorsStrictIgnoreNotes(t *testing.T) {
dbt.mustExec("DROP TABLE IF EXISTS does_not_exist")
})
}
+
+func TestMySQLErrIs(t *testing.T) {
+ infraErr := &MySQLError{Number: 1234, Message: "the server is on fire"}
+ otherInfraErr := &MySQLError{Number: 1234, Message: "the datacenter is flooded"}
+ if !errors.Is(infraErr, otherInfraErr) {
+ t.Errorf("expected errors to be the same: %+v %+v", infraErr, otherInfraErr)
+ }
+
+ differentCodeErr := &MySQLError{Number: 5678, Message: "the server is on fire"}
+ if errors.Is(infraErr, differentCodeErr) {
+ t.Fatalf("expected errors to be different: %+v %+v", infraErr, differentCodeErr)
+ }
+
+ nonMysqlErr := errors.New("not a mysql error")
+ if errors.Is(infraErr, nonMysqlErr) {
+ t.Fatalf("expected errors to be different: %+v %+v", infraErr, nonMysqlErr)
+ }
+}
diff --git a/fields.go b/fields.go
index e1e2ece..e0654a8 100644
--- a/fields.go
+++ b/fields.go
@@ -41,6 +41,9 @@ func (mf *mysqlField) typeDatabaseName() string {
case fieldTypeJSON:
return "JSON"
case fieldTypeLong:
+ if mf.flags&flagUnsigned != 0 {
+ return "UNSIGNED INT"
+ }
return "INT"
case fieldTypeLongBLOB:
if mf.charSet != collations[binaryCollation] {
@@ -48,6 +51,9 @@ func (mf *mysqlField) typeDatabaseName() string {
}
return "LONGBLOB"
case fieldTypeLongLong:
+ if mf.flags&flagUnsigned != 0 {
+ return "UNSIGNED BIGINT"
+ }
return "BIGINT"
case fieldTypeMediumBLOB:
if mf.charSet != collations[binaryCollation] {
@@ -63,6 +69,9 @@ func (mf *mysqlField) typeDatabaseName() string {
case fieldTypeSet:
return "SET"
case fieldTypeShort:
+ if mf.flags&flagUnsigned != 0 {
+ return "UNSIGNED SMALLINT"
+ }
return "SMALLINT"
case fieldTypeString:
if mf.charSet == collations[binaryCollation] {
@@ -74,6 +83,9 @@ func (mf *mysqlField) typeDatabaseName() string {
case fieldTypeTimestamp:
return "TIMESTAMP"
case fieldTypeTiny:
+ if mf.flags&flagUnsigned != 0 {
+ return "UNSIGNED TINYINT"
+ }
return "TINYINT"
case fieldTypeTinyBLOB:
if mf.charSet != collations[binaryCollation] {
@@ -106,7 +118,7 @@ var (
scanTypeInt64 = reflect.TypeOf(int64(0))
scanTypeNullFloat = reflect.TypeOf(sql.NullFloat64{})
scanTypeNullInt = reflect.TypeOf(sql.NullInt64{})
- scanTypeNullTime = reflect.TypeOf(NullTime{})
+ scanTypeNullTime = reflect.TypeOf(sql.NullTime{})
scanTypeUint8 = reflect.TypeOf(uint8(0))
scanTypeUint16 = reflect.TypeOf(uint16(0))
scanTypeUint32 = reflect.TypeOf(uint32(0))
diff --git a/fuzz.go b/fuzz.go
new file mode 100644
index 0000000..3a4ec25
--- /dev/null
+++ b/fuzz.go
@@ -0,0 +1,25 @@
+// Go MySQL Driver - A MySQL-Driver for Go's database/sql package.
+//
+// Copyright 2020 The Go-MySQL-Driver Authors. All rights reserved.
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
+// You can obtain one at http://mozilla.org/MPL/2.0/.
+
+//go:build gofuzz
+// +build gofuzz
+
+package mysql
+
+import (
+ "database/sql"
+)
+
+func Fuzz(data []byte) int {
+ db, err := sql.Open("mysql", string(data))
+ if err != nil {
+ return 0
+ }
+ db.Close()
+ return 1
+}
diff --git a/go.mod b/go.mod
index fffbf6a..2511104 100644
--- a/go.mod
+++ b/go.mod
@@ -1,3 +1,3 @@
module github.com/go-sql-driver/mysql
-go 1.10
+go 1.13
diff --git a/infile.go b/infile.go
index 273cb0b..3279dcf 100644
--- a/infile.go
+++ b/infile.go
@@ -23,17 +23,16 @@ var (
readerRegisterLock sync.RWMutex
)
-// RegisterLocalFile adds the given file to the file whitelist,
+// RegisterLocalFile adds the given file to the file allowlist,
// so that it can be used by "LOAD DATA LOCAL INFILE <filepath>".
// Alternatively you can allow the use of all local files with
// the DSN parameter 'allowAllFiles=true'
//
-// filePath := "/home/gopher/data.csv"
-// mysql.RegisterLocalFile(filePath)
-// err := db.Exec("LOAD DATA LOCAL INFILE '" + filePath + "' INTO TABLE foo")
-// if err != nil {
-// ...
-//
+// filePath := "/home/gopher/data.csv"
+// mysql.RegisterLocalFile(filePath)
+// err := db.Exec("LOAD DATA LOCAL INFILE '" + filePath + "' INTO TABLE foo")
+// if err != nil {
+// ...
func RegisterLocalFile(filePath string) {
fileRegisterLock.Lock()
// lazy map init
@@ -45,7 +44,7 @@ func RegisterLocalFile(filePath string) {
fileRegisterLock.Unlock()
}
-// DeregisterLocalFile removes the given filepath from the whitelist.
+// DeregisterLocalFile removes the given filepath from the allowlist.
func DeregisterLocalFile(filePath string) {
fileRegisterLock.Lock()
delete(fileRegister, strings.Trim(filePath, `"`))
@@ -58,15 +57,14 @@ func DeregisterLocalFile(filePath string) {
// If the handler returns a io.ReadCloser Close() is called when the
// request is finished.
//
-// mysql.RegisterReaderHandler("data", func() io.Reader {
-// var csvReader io.Reader // Some Reader that returns CSV data
-// ... // Open Reader here
-// return csvReader
-// })
-// err := db.Exec("LOAD DATA LOCAL INFILE 'Reader::data' INTO TABLE foo")
-// if err != nil {
-// ...
-//
+// mysql.RegisterReaderHandler("data", func() io.Reader {
+// var csvReader io.Reader // Some Reader that returns CSV data
+// ... // Open Reader here
+// return csvReader
+// })
+// err := db.Exec("LOAD DATA LOCAL INFILE 'Reader::data' INTO TABLE foo")
+// if err != nil {
+// ...
func RegisterReaderHandler(name string, handler func() io.Reader) {
readerRegisterLock.Lock()
// lazy map init
@@ -93,10 +91,12 @@ func deferredClose(err *error, closer io.Closer) {
}
}
+const defaultPacketSize = 16 * 1024 // 16KB is small enough for disk readahead and large enough for TCP
+
func (mc *mysqlConn) handleInFileRequest(name string) (err error) {
var rdr io.Reader
var data []byte
- packetSize := 16 * 1024 // 16KB is small enough for disk readahead and large enough for TCP
+ packetSize := defaultPacketSize
if mc.maxWriteSize < packetSize {
packetSize = mc.maxWriteSize
}
diff --git a/nulltime.go b/nulltime.go
index afa8a89..36c8a42 100644
--- a/nulltime.go
+++ b/nulltime.go
@@ -9,11 +9,32 @@
package mysql
import (
+ "database/sql"
"database/sql/driver"
"fmt"
"time"
)
+// NullTime represents a time.Time that may be NULL.
+// NullTime implements the Scanner interface so
+// it can be used as a scan destination:
+//
+// var nt NullTime
+// err := db.QueryRow("SELECT time FROM foo WHERE id=?", id).Scan(&nt)
+// ...
+// if nt.Valid {
+// // use nt.Time
+// } else {
+// // NULL value
+// }
+//
+// # This NullTime implementation is not driver-specific
+//
+// Deprecated: NullTime doesn't honor the loc DSN parameter.
+// NullTime.Scan interprets a time as UTC, not the loc DSN parameter.
+// Use sql.NullTime instead.
+type NullTime sql.NullTime
+
// Scan implements the Scanner interface.
// The value type must be time.Time or string / []byte (formatted time-string),
// otherwise Scan fails.
@@ -28,11 +49,11 @@ func (nt *NullTime) Scan(value interface{}) (err error) {
nt.Time, nt.Valid = v, true
return
case []byte:
- nt.Time, err = parseDateTime(string(v), time.UTC)
+ nt.Time, err = parseDateTime(v, time.UTC)
nt.Valid = (err == nil)
return
case string:
- nt.Time, err = parseDateTime(v, time.UTC)
+ nt.Time, err = parseDateTime([]byte(v), time.UTC)
nt.Valid = (err == nil)
return
}
diff --git a/nulltime_go113.go b/nulltime_go113.go
deleted file mode 100644
index c392594..0000000
--- a/nulltime_go113.go
+++ /dev/null
@@ -1,31 +0,0 @@
-// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
-//
-// Copyright 2013 The Go-MySQL-Driver Authors. All rights reserved.
-//
-// This Source Code Form is subject to the terms of the Mozilla Public
-// License, v. 2.0. If a copy of the MPL was not distributed with this file,
-// You can obtain one at http://mozilla.org/MPL/2.0/.
-
-// +build go1.13
-
-package mysql
-
-import (
- "database/sql"
-)
-
-// NullTime represents a time.Time that may be NULL.
-// NullTime implements the Scanner interface so
-// it can be used as a scan destination:
-//
-// var nt NullTime
-// err := db.QueryRow("SELECT time FROM foo WHERE id=?", id).Scan(&nt)
-// ...
-// if nt.Valid {
-// // use nt.Time
-// } else {
-// // NULL value
-// }
-//
-// This NullTime implementation is not driver-specific
-type NullTime sql.NullTime
diff --git a/nulltime_legacy.go b/nulltime_legacy.go
deleted file mode 100644
index 86d159d..0000000
--- a/nulltime_legacy.go
+++ /dev/null
@@ -1,34 +0,0 @@
-// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
-//
-// Copyright 2013 The Go-MySQL-Driver Authors. All rights reserved.
-//
-// This Source Code Form is subject to the terms of the Mozilla Public
-// License, v. 2.0. If a copy of the MPL was not distributed with this file,
-// You can obtain one at http://mozilla.org/MPL/2.0/.
-
-// +build !go1.13
-
-package mysql
-
-import (
- "time"
-)
-
-// NullTime represents a time.Time that may be NULL.
-// NullTime implements the Scanner interface so
-// it can be used as a scan destination:
-//
-// var nt NullTime
-// err := db.QueryRow("SELECT time FROM foo WHERE id=?", id).Scan(&nt)
-// ...
-// if nt.Valid {
-// // use nt.Time
-// } else {
-// // NULL value
-// }
-//
-// This NullTime implementation is not driver-specific
-type NullTime struct {
- Time time.Time
- Valid bool // Valid is true if Time is not NULL
-}
diff --git a/packets.go b/packets.go
index 82ad7a2..ee05c95 100644
--- a/packets.go
+++ b/packets.go
@@ -13,6 +13,7 @@ import (
"crypto/tls"
"database/sql/driver"
"encoding/binary"
+ "encoding/json"
"errors"
"fmt"
"io"
@@ -109,14 +110,13 @@ func (mc *mysqlConn) writePacket(data []byte) error {
conn = mc.rawConn
}
var err error
- // If this connection has a ReadTimeout which we've been setting on
- // reads, reset it to its default value before we attempt a non-blocking
- // read, otherwise the scheduler will just time us out before we can read
- if mc.cfg.ReadTimeout != 0 {
- err = conn.SetReadDeadline(time.Time{})
- }
- if err == nil && mc.cfg.CheckConnLiveness {
- err = connCheck(conn)
+ if mc.cfg.CheckConnLiveness {
+ if mc.cfg.ReadTimeout != 0 {
+ err = conn.SetReadDeadline(time.Now().Add(mc.cfg.ReadTimeout))
+ }
+ if err == nil {
+ err = connCheck(conn)
+ }
}
if err != nil {
errLog.Print("closing bad idle connection: ", err)
@@ -222,9 +222,9 @@ func (mc *mysqlConn) readHandshakePacket() (data []byte, plugin string, err erro
if mc.flags&clientProtocol41 == 0 {
return nil, "", ErrOldProtocol
}
- if mc.flags&clientSSL == 0 && mc.cfg.tls != nil {
- if mc.cfg.TLSConfig == "preferred" {
- mc.cfg.tls = nil
+ if mc.flags&clientSSL == 0 && mc.cfg.TLS != nil {
+ if mc.cfg.AllowFallbackToPlaintext {
+ mc.cfg.TLS = nil
} else {
return nil, "", ErrNoTLS
}
@@ -292,7 +292,7 @@ func (mc *mysqlConn) writeHandshakeResponsePacket(authResp []byte, plugin string
}
// To enable TLS / SSL
- if mc.cfg.tls != nil {
+ if mc.cfg.TLS != nil {
clientFlags |= clientSSL
}
@@ -348,16 +348,22 @@ func (mc *mysqlConn) writeHandshakeResponsePacket(authResp []byte, plugin string
return errors.New("unknown collation")
}
+ // Filler [23 bytes] (all 0x00)
+ pos := 13
+ for ; pos < 13+23; pos++ {
+ data[pos] = 0
+ }
+
// SSL Connection Request Packet
// http://dev.mysql.com/doc/internals/en/connection-phase-packets.html#packet-Protocol::SSLRequest
- if mc.cfg.tls != nil {
+ if mc.cfg.TLS != nil {
// Send TLS / SSL request packet
if err := mc.writePacket(data[:(4+4+1+23)+4]); err != nil {
return err
}
// Switch to TLS
- tlsConn := tls.Client(mc.netConn, mc.cfg.tls)
+ tlsConn := tls.Client(mc.netConn, mc.cfg.TLS)
if err := tlsConn.Handshake(); err != nil {
return err
}
@@ -366,12 +372,6 @@ func (mc *mysqlConn) writeHandshakeResponsePacket(authResp []byte, plugin string
mc.buf.nc = tlsConn
}
- // Filler [23 bytes] (all 0x00)
- pos := 13
- for ; pos < 13+23; pos++ {
- data[pos] = 0
- }
-
// User [null terminated string]
if len(mc.cfg.User) > 0 {
pos += copy(data[pos:], mc.cfg.User)
@@ -587,19 +587,20 @@ func (mc *mysqlConn) handleErrorPacket(data []byte) error {
return driver.ErrBadConn
}
+ me := &MySQLError{Number: errno}
+
pos := 3
// SQL State [optional: # + 5bytes string]
if data[3] == 0x23 {
- //sqlstate := string(data[4 : 4+5])
+ copy(me.SQLState[:], data[4:4+5])
pos = 9
}
// Error Message [string]
- return &MySQLError{
- Number: errno,
- Message: string(data[pos:]),
- }
+ me.Message = string(data[pos:])
+
+ return me
}
func readStatus(b []byte) statusFlag {
@@ -760,40 +761,40 @@ func (rows *textRows) readRow(dest []driver.Value) error {
}
// RowSet Packet
- var n int
- var isNull bool
- pos := 0
+ var (
+ n int
+ isNull bool
+ pos int = 0
+ )
for i := range dest {
// Read bytes and convert to string
dest[i], isNull, n, err = readLengthEncodedString(data[pos:])
pos += n
- if err == nil {
- if !isNull {
- if !mc.parseTime {
- continue
- } else {
- switch rows.rs.columns[i].fieldType {
- case fieldTypeTimestamp, fieldTypeDateTime,
- fieldTypeDate, fieldTypeNewDate:
- dest[i], err = parseDateTime(
- string(dest[i].([]byte)),
- mc.cfg.Loc,
- )
- if err == nil {
- continue
- }
- default:
- continue
- }
- }
- } else {
- dest[i] = nil
- continue
+ if err != nil {
+ return err
+ }
+
+ if isNull {
+ dest[i] = nil
+ continue
+ }
+
+ if !mc.parseTime {
+ continue
+ }
+
+ // Parse time field
+ switch rows.rs.columns[i].fieldType {
+ case fieldTypeTimestamp,
+ fieldTypeDateTime,
+ fieldTypeDate,
+ fieldTypeNewDate:
+ if dest[i], err = parseDateTime(dest[i].([]byte), mc.cfg.Loc); err != nil {
+ return err
}
}
- return err // err != nil
}
return nil
@@ -1003,6 +1004,9 @@ func (stmt *mysqlStmt) writeExecutePacket(args []driver.Value) error {
continue
}
+ if v, ok := arg.(json.RawMessage); ok {
+ arg = []byte(v)
+ }
// cache types and values
switch v := arg.(type) {
case int64:
@@ -1112,7 +1116,10 @@ func (stmt *mysqlStmt) writeExecutePacket(args []driver.Value) error {
if v.IsZero() {
b = append(b, "0000-00-00"...)
} else {
- b = v.In(mc.cfg.Loc).AppendFormat(b, timeFormat)
+ b, err = appendDateTime(b, v.In(mc.cfg.Loc))
+ if err != nil {
+ return err
+ }
}
paramValues = appendLengthEncodedInteger(paramValues,
diff --git a/statement.go b/statement.go
index f7e3709..10ece8b 100644
--- a/statement.go
+++ b/statement.go
@@ -10,6 +10,7 @@ package mysql
import (
"database/sql/driver"
+ "encoding/json"
"fmt"
"io"
"reflect"
@@ -22,7 +23,7 @@ type mysqlStmt struct {
}
func (stmt *mysqlStmt) Close() error {
- if stmt.mc == nil || stmt.mc.closed.IsSet() {
+ if stmt.mc == nil || stmt.mc.closed.Load() {
// driver.Stmt.Close can be called more than once, thus this function
// has to be idempotent.
// See also Issue #450 and golang/go#16019.
@@ -43,8 +44,13 @@ func (stmt *mysqlStmt) ColumnConverter(idx int) driver.ValueConverter {
return converter{}
}
+func (stmt *mysqlStmt) CheckNamedValue(nv *driver.NamedValue) (err error) {
+ nv.Value, err = converter{}.ConvertValue(nv.Value)
+ return
+}
+
func (stmt *mysqlStmt) Exec(args []driver.Value) (driver.Result, error) {
- if stmt.mc.closed.IsSet() {
+ if stmt.mc.closed.Load() {
errLog.Print(ErrInvalidConn)
return nil, driver.ErrBadConn
}
@@ -92,7 +98,7 @@ func (stmt *mysqlStmt) Query(args []driver.Value) (driver.Rows, error) {
}
func (stmt *mysqlStmt) query(args []driver.Value) (*binaryRows, error) {
- if stmt.mc.closed.IsSet() {
+ if stmt.mc.closed.Load() {
errLog.Print(ErrInvalidConn)
return nil, driver.ErrBadConn
}
@@ -129,6 +135,8 @@ func (stmt *mysqlStmt) query(args []driver.Value) (*binaryRows, error) {
return rows, err
}
+var jsonType = reflect.TypeOf(json.RawMessage{})
+
type converter struct{}
// ConvertValue mirrors the reference/default converter in database/sql/driver
@@ -146,12 +154,17 @@ func (c converter) ConvertValue(v interface{}) (driver.Value, error) {
if err != nil {
return nil, err
}
- if !driver.IsValue(sv) {
- return nil, fmt.Errorf("non-Value type %T returned from Value", sv)
+ if driver.IsValue(sv) {
+ return sv, nil
}
- return sv, nil
+ // A value returned from the Valuer interface can be "a type handled by
+ // a database driver's NamedValueChecker interface" so we should accept
+ // uint64 here as well.
+ if u, ok := sv.(uint64); ok {
+ return u, nil
+ }
+ return nil, fmt.Errorf("non-Value type %T returned from Value", sv)
}
-
rv := reflect.ValueOf(v)
switch rv.Kind() {
case reflect.Ptr:
@@ -170,11 +183,14 @@ func (c converter) ConvertValue(v interface{}) (driver.Value, error) {
case reflect.Bool:
return rv.Bool(), nil
case reflect.Slice:
- ek := rv.Type().Elem().Kind()
- if ek == reflect.Uint8 {
+ switch t := rv.Type(); {
+ case t == jsonType:
+ return v, nil
+ case t.Elem().Kind() == reflect.Uint8:
return rv.Bytes(), nil
+ default:
+ return nil, fmt.Errorf("unsupported type %T, a slice of %s", v, t.Elem().Kind())
}
- return nil, fmt.Errorf("unsupported type %T, a slice of %s", v, ek)
case reflect.String:
return rv.String(), nil
}
diff --git a/statement_test.go b/statement_test.go
index 4b9914f..2563ece 100644
--- a/statement_test.go
+++ b/statement_test.go
@@ -10,6 +10,8 @@ package mysql
import (
"bytes"
+ "database/sql/driver"
+ "encoding/json"
"testing"
)
@@ -34,7 +36,7 @@ func TestConvertDerivedByteSlice(t *testing.T) {
t.Fatal("Byte slice not convertible", err)
}
- if bytes.Compare(output.([]byte), []byte("value")) != 0 {
+ if !bytes.Equal(output.([]byte), []byte("value")) {
t.Fatalf("Byte slice not converted, got %#v %T", output, output)
}
}
@@ -95,6 +97,14 @@ func TestConvertSignedIntegers(t *testing.T) {
}
}
+type myUint64 struct {
+ value uint64
+}
+
+func (u myUint64) Value() (driver.Value, error) {
+ return u.value, nil
+}
+
func TestConvertUnsignedIntegers(t *testing.T) {
values := []interface{}{
uint8(42),
@@ -102,6 +112,7 @@ func TestConvertUnsignedIntegers(t *testing.T) {
uint32(42),
uint64(42),
uint(42),
+ myUint64{uint64(42)},
}
for _, value := range values {
@@ -124,3 +135,17 @@ func TestConvertUnsignedIntegers(t *testing.T) {
t.Fatalf("uint64 high-bit converted, got %#v %T", output, output)
}
}
+
+func TestConvertJSON(t *testing.T) {
+ raw := json.RawMessage("{}")
+
+ out, err := converter{}.ConvertValue(raw)
+
+ if err != nil {
+ t.Fatal("json.RawMessage was failed in convert", err)
+ }
+
+ if _, ok := out.(json.RawMessage); !ok {
+ t.Fatalf("json.RawMessage converted, got %#v %T", out, out)
+ }
+}
diff --git a/transaction.go b/transaction.go
index 417d727..4a4b610 100644
--- a/transaction.go
+++ b/transaction.go
@@ -13,7 +13,7 @@ type mysqlTx struct {
}
func (tx *mysqlTx) Commit() (err error) {
- if tx.mc == nil || tx.mc.closed.IsSet() {
+ if tx.mc == nil || tx.mc.closed.Load() {
return ErrInvalidConn
}
err = tx.mc.exec("COMMIT")
@@ -22,7 +22,7 @@ func (tx *mysqlTx) Commit() (err error) {
}
func (tx *mysqlTx) Rollback() (err error) {
- if tx.mc == nil || tx.mc.closed.IsSet() {
+ if tx.mc == nil || tx.mc.closed.Load() {
return ErrInvalidConn
}
err = tx.mc.exec("ROLLBACK")
diff --git a/utils.go b/utils.go
index 9552e80..15dbd8d 100644
--- a/utils.go
+++ b/utils.go
@@ -35,26 +35,25 @@ var (
// Note: The provided tls.Config is exclusively owned by the driver after
// registering it.
//
-// rootCertPool := x509.NewCertPool()
-// pem, err := ioutil.ReadFile("/path/ca-cert.pem")
-// if err != nil {
-// log.Fatal(err)
-// }
-// if ok := rootCertPool.AppendCertsFromPEM(pem); !ok {
-// log.Fatal("Failed to append PEM.")
-// }
-// clientCert := make([]tls.Certificate, 0, 1)
-// certs, err := tls.LoadX509KeyPair("/path/client-cert.pem", "/path/client-key.pem")
-// if err != nil {
-// log.Fatal(err)
-// }
-// clientCert = append(clientCert, certs)
-// mysql.RegisterTLSConfig("custom", &tls.Config{
-// RootCAs: rootCertPool,
-// Certificates: clientCert,
-// })
-// db, err := sql.Open("mysql", "user@tcp(localhost:3306)/test?tls=custom")
-//
+// rootCertPool := x509.NewCertPool()
+// pem, err := ioutil.ReadFile("/path/ca-cert.pem")
+// if err != nil {
+// log.Fatal(err)
+// }
+// if ok := rootCertPool.AppendCertsFromPEM(pem); !ok {
+// log.Fatal("Failed to append PEM.")
+// }
+// clientCert := make([]tls.Certificate, 0, 1)
+// certs, err := tls.LoadX509KeyPair("/path/client-cert.pem", "/path/client-key.pem")
+// if err != nil {
+// log.Fatal(err)
+// }
+// clientCert = append(clientCert, certs)
+// mysql.RegisterTLSConfig("custom", &tls.Config{
+// RootCAs: rootCertPool,
+// Certificates: clientCert,
+// })
+// db, err := sql.Open("mysql", "user@tcp(localhost:3306)/test?tls=custom")
func RegisterTLSConfig(key string, config *tls.Config) error {
if _, isBool := readBool(key); isBool || strings.ToLower(key) == "skip-verify" || strings.ToLower(key) == "preferred" {
return fmt.Errorf("key '%s' is reserved", key)
@@ -106,27 +105,126 @@ func readBool(input string) (value bool, valid bool) {
* Time related utils *
******************************************************************************/
-func parseDateTime(str string, loc *time.Location) (t time.Time, err error) {
- base := "0000-00-00 00:00:00.0000000"
- switch len(str) {
+func parseDateTime(b []byte, loc *time.Location) (time.Time, error) {
+ const base = "0000-00-00 00:00:00.000000"
+ switch len(b) {
case 10, 19, 21, 22, 23, 24, 25, 26: // up to "YYYY-MM-DD HH:MM:SS.MMMMMM"
- if str == base[:len(str)] {
- return
+ if string(b) == base[:len(b)] {
+ return time.Time{}, nil
+ }
+
+ year, err := parseByteYear(b)
+ if err != nil {
+ return time.Time{}, err
+ }
+ if b[4] != '-' {
+ return time.Time{}, fmt.Errorf("bad value for field: `%c`", b[4])
+ }
+
+ m, err := parseByte2Digits(b[5], b[6])
+ if err != nil {
+ return time.Time{}, err
+ }
+ month := time.Month(m)
+
+ if b[7] != '-' {
+ return time.Time{}, fmt.Errorf("bad value for field: `%c`", b[7])
+ }
+
+ day, err := parseByte2Digits(b[8], b[9])
+ if err != nil {
+ return time.Time{}, err
+ }
+ if len(b) == 10 {
+ return time.Date(year, month, day, 0, 0, 0, 0, loc), nil
+ }
+
+ if b[10] != ' ' {
+ return time.Time{}, fmt.Errorf("bad value for field: `%c`", b[10])
+ }
+
+ hour, err := parseByte2Digits(b[11], b[12])
+ if err != nil {
+ return time.Time{}, err
+ }
+ if b[13] != ':' {
+ return time.Time{}, fmt.Errorf("bad value for field: `%c`", b[13])
+ }
+
+ min, err := parseByte2Digits(b[14], b[15])
+ if err != nil {
+ return time.Time{}, err
+ }
+ if b[16] != ':' {
+ return time.Time{}, fmt.Errorf("bad value for field: `%c`", b[16])
+ }
+
+ sec, err := parseByte2Digits(b[17], b[18])
+ if err != nil {
+ return time.Time{}, err
}
- t, err = time.Parse(timeFormat[:len(str)], str)
+ if len(b) == 19 {
+ return time.Date(year, month, day, hour, min, sec, 0, loc), nil
+ }
+
+ if b[19] != '.' {
+ return time.Time{}, fmt.Errorf("bad value for field: `%c`", b[19])
+ }
+ nsec, err := parseByteNanoSec(b[20:])
+ if err != nil {
+ return time.Time{}, err
+ }
+ return time.Date(year, month, day, hour, min, sec, nsec, loc), nil
default:
- err = fmt.Errorf("invalid time string: %s", str)
- return
+ return time.Time{}, fmt.Errorf("invalid time bytes: %s", b)
}
+}
- // Adjust location
- if err == nil && loc != time.UTC {
- y, mo, d := t.Date()
- h, mi, s := t.Clock()
- t, err = time.Date(y, mo, d, h, mi, s, t.Nanosecond(), loc), nil
+func parseByteYear(b []byte) (int, error) {
+ year, n := 0, 1000
+ for i := 0; i < 4; i++ {
+ v, err := bToi(b[i])
+ if err != nil {
+ return 0, err
+ }
+ year += v * n
+ n /= 10
}
+ return year, nil
+}
- return
+func parseByte2Digits(b1, b2 byte) (int, error) {
+ d1, err := bToi(b1)
+ if err != nil {
+ return 0, err
+ }
+ d2, err := bToi(b2)
+ if err != nil {
+ return 0, err
+ }
+ return d1*10 + d2, nil
+}
+
+func parseByteNanoSec(b []byte) (int, error) {
+ ns, digit := 0, 100000 // max is 6-digits
+ for i := 0; i < len(b); i++ {
+ v, err := bToi(b[i])
+ if err != nil {
+ return 0, err
+ }
+ ns += v * digit
+ digit /= 10
+ }
+ // nanoseconds has 10-digits. (needs to scale digits)
+ // 10 - 6 = 4, so we have to multiple 1000.
+ return ns * 1000, nil
+}
+
+func bToi(b byte) (int, error) {
+ if b < '0' || b > '9' {
+ return 0, errors.New("not [0-9]")
+ }
+ return int(b - '0'), nil
}
func parseBinaryDateTime(num uint64, data []byte, loc *time.Location) (driver.Value, error) {
@@ -167,6 +265,64 @@ func parseBinaryDateTime(num uint64, data []byte, loc *time.Location) (driver.Va
return nil, fmt.Errorf("invalid DATETIME packet length %d", num)
}
+func appendDateTime(buf []byte, t time.Time) ([]byte, error) {
+ year, month, day := t.Date()
+ hour, min, sec := t.Clock()
+ nsec := t.Nanosecond()
+
+ if year < 1 || year > 9999 {
+ return buf, errors.New("year is not in the range [1, 9999]: " + strconv.Itoa(year)) // use errors.New instead of fmt.Errorf to avoid year escape to heap
+ }
+ year100 := year / 100
+ year1 := year % 100
+
+ var localBuf [len("2006-01-02T15:04:05.999999999")]byte // does not escape
+ localBuf[0], localBuf[1], localBuf[2], localBuf[3] = digits10[year100], digits01[year100], digits10[year1], digits01[year1]
+ localBuf[4] = '-'
+ localBuf[5], localBuf[6] = digits10[month], digits01[month]
+ localBuf[7] = '-'
+ localBuf[8], localBuf[9] = digits10[day], digits01[day]
+
+ if hour == 0 && min == 0 && sec == 0 && nsec == 0 {
+ return append(buf, localBuf[:10]...), nil
+ }
+
+ localBuf[10] = ' '
+ localBuf[11], localBuf[12] = digits10[hour], digits01[hour]
+ localBuf[13] = ':'
+ localBuf[14], localBuf[15] = digits10[min], digits01[min]
+ localBuf[16] = ':'
+ localBuf[17], localBuf[18] = digits10[sec], digits01[sec]
+
+ if nsec == 0 {
+ return append(buf, localBuf[:19]...), nil
+ }
+ nsec100000000 := nsec / 100000000
+ nsec1000000 := (nsec / 1000000) % 100
+ nsec10000 := (nsec / 10000) % 100
+ nsec100 := (nsec / 100) % 100
+ nsec1 := nsec % 100
+ localBuf[19] = '.'
+
+ // milli second
+ localBuf[20], localBuf[21], localBuf[22] =
+ digits01[nsec100000000], digits10[nsec1000000], digits01[nsec1000000]
+ // micro second
+ localBuf[23], localBuf[24], localBuf[25] =
+ digits10[nsec10000], digits01[nsec10000], digits10[nsec100]
+ // nano second
+ localBuf[26], localBuf[27], localBuf[28] =
+ digits01[nsec100], digits10[nsec1], digits01[nsec1]
+
+ // trim trailing zeros
+ n := len(localBuf)
+ for n > 0 && localBuf[n-1] == '0' {
+ n--
+ }
+
+ return append(buf, localBuf[:n]...), nil
+}
+
// zeroDateTime is used in formatBinaryDateTime to avoid an allocation
// if the DATE or DATETIME has the zero value.
// It must never be changed.
@@ -375,7 +531,7 @@ func stringToInt(b []byte) int {
return val
}
-// returns the string read as a bytes slice, wheter the value is NULL,
+// returns the string read as a bytes slice, whether the value is NULL,
// the number of bytes read and an error, in case the string is longer than
// the input slice
func readLengthEncodedString(b []byte) ([]byte, bool, int, error) {
@@ -485,32 +641,32 @@ func escapeBytesBackslash(buf, v []byte) []byte {
for _, c := range v {
switch c {
case '\x00':
- buf[pos] = '\\'
buf[pos+1] = '0'
+ buf[pos] = '\\'
pos += 2
case '\n':
- buf[pos] = '\\'
buf[pos+1] = 'n'
+ buf[pos] = '\\'
pos += 2
case '\r':
- buf[pos] = '\\'
buf[pos+1] = 'r'
+ buf[pos] = '\\'
pos += 2
case '\x1a':
- buf[pos] = '\\'
buf[pos+1] = 'Z'
+ buf[pos] = '\\'
pos += 2
case '\'':
- buf[pos] = '\\'
buf[pos+1] = '\''
+ buf[pos] = '\\'
pos += 2
case '"':
- buf[pos] = '\\'
buf[pos+1] = '"'
+ buf[pos] = '\\'
pos += 2
case '\\':
- buf[pos] = '\\'
buf[pos+1] = '\\'
+ buf[pos] = '\\'
pos += 2
default:
buf[pos] = c
@@ -530,32 +686,32 @@ func escapeStringBackslash(buf []byte, v string) []byte {
c := v[i]
switch c {
case '\x00':
- buf[pos] = '\\'
buf[pos+1] = '0'
+ buf[pos] = '\\'
pos += 2
case '\n':
- buf[pos] = '\\'
buf[pos+1] = 'n'
+ buf[pos] = '\\'
pos += 2
case '\r':
- buf[pos] = '\\'
buf[pos+1] = 'r'
+ buf[pos] = '\\'
pos += 2
case '\x1a':
- buf[pos] = '\\'
buf[pos+1] = 'Z'
+ buf[pos] = '\\'
pos += 2
case '\'':
- buf[pos] = '\\'
buf[pos+1] = '\''
+ buf[pos] = '\\'
pos += 2
case '"':
- buf[pos] = '\\'
buf[pos+1] = '"'
+ buf[pos] = '\\'
pos += 2
case '\\':
- buf[pos] = '\\'
buf[pos+1] = '\\'
+ buf[pos] = '\\'
pos += 2
default:
buf[pos] = c
@@ -577,8 +733,8 @@ func escapeBytesQuotes(buf, v []byte) []byte {
for _, c := range v {
if c == '\'' {
- buf[pos] = '\''
buf[pos+1] = '\''
+ buf[pos] = '\''
pos += 2
} else {
buf[pos] = c
@@ -597,8 +753,8 @@ func escapeStringQuotes(buf []byte, v string) []byte {
for i := 0; i < len(v); i++ {
c := v[i]
if c == '\'' {
- buf[pos] = '\''
buf[pos+1] = '\''
+ buf[pos] = '\''
pos += 2
} else {
buf[pos] = c
@@ -623,39 +779,16 @@ type noCopy struct{}
// Lock is a no-op used by -copylocks checker from `go vet`.
func (*noCopy) Lock() {}
-// atomicBool is a wrapper around uint32 for usage as a boolean value with
-// atomic access.
-type atomicBool struct {
- _noCopy noCopy
- value uint32
-}
-
-// IsSet returns whether the current boolean value is true
-func (ab *atomicBool) IsSet() bool {
- return atomic.LoadUint32(&ab.value) > 0
-}
-
-// Set sets the value of the bool regardless of the previous value
-func (ab *atomicBool) Set(value bool) {
- if value {
- atomic.StoreUint32(&ab.value, 1)
- } else {
- atomic.StoreUint32(&ab.value, 0)
- }
-}
-
-// TrySet sets the value of the bool and returns whether the value changed
-func (ab *atomicBool) TrySet(value bool) bool {
- if value {
- return atomic.SwapUint32(&ab.value, 1) == 0
- }
- return atomic.SwapUint32(&ab.value, 0) > 0
-}
+// Unlock is a no-op used by -copylocks checker from `go vet`.
+// noCopy should implement sync.Locker from Go 1.11
+// https://github.com/golang/go/commit/c2eba53e7f80df21d51285879d51ab81bcfbf6bc
+// https://github.com/golang/go/issues/26165
+func (*noCopy) Unlock() {}
// atomicError is a wrapper for atomically accessed error values
type atomicError struct {
- _noCopy noCopy
- value atomic.Value
+ _ noCopy
+ value atomic.Value
}
// Set sets the error value regardless of the previous value.
diff --git a/utils_test.go b/utils_test.go
index 10a60c2..4e5fc3c 100644
--- a/utils_test.go
+++ b/utils_test.go
@@ -14,6 +14,7 @@ import (
"database/sql/driver"
"encoding/binary"
"testing"
+ "time"
)
func TestLengthEncodedInteger(t *testing.T) {
@@ -172,64 +173,6 @@ func TestEscapeQuotes(t *testing.T) {
expect("foo\"bar", "foo\"bar") // not affected
}
-func TestAtomicBool(t *testing.T) {
- var ab atomicBool
- if ab.IsSet() {
- t.Fatal("Expected value to be false")
- }
-
- ab.Set(true)
- if ab.value != 1 {
- t.Fatal("Set(true) did not set value to 1")
- }
- if !ab.IsSet() {
- t.Fatal("Expected value to be true")
- }
-
- ab.Set(true)
- if !ab.IsSet() {
- t.Fatal("Expected value to be true")
- }
-
- ab.Set(false)
- if ab.value != 0 {
- t.Fatal("Set(false) did not set value to 0")
- }
- if ab.IsSet() {
- t.Fatal("Expected value to be false")
- }
-
- ab.Set(false)
- if ab.IsSet() {
- t.Fatal("Expected value to be false")
- }
- if ab.TrySet(false) {
- t.Fatal("Expected TrySet(false) to fail")
- }
- if !ab.TrySet(true) {
- t.Fatal("Expected TrySet(true) to succeed")
- }
- if !ab.IsSet() {
- t.Fatal("Expected value to be true")
- }
-
- ab.Set(true)
- if !ab.IsSet() {
- t.Fatal("Expected value to be true")
- }
- if ab.TrySet(true) {
- t.Fatal("Expected TrySet(true) to fail")
- }
- if !ab.TrySet(false) {
- t.Fatal("Expected TrySet(false) to succeed")
- }
- if ab.IsSet() {
- t.Fatal("Expected value to be false")
- }
-
- ab._noCopy.Lock() // we've "tested" it ¯\_(ツ)_/¯
-}
-
func TestAtomicError(t *testing.T) {
var ae atomicError
if ae.Value() != nil {
@@ -291,3 +234,244 @@ func TestIsolationLevelMapping(t *testing.T) {
t.Fatalf("Expected error to be %q, got %q", expectedErr, err)
}
}
+
+func TestAppendDateTime(t *testing.T) {
+ tests := []struct {
+ t time.Time
+ str string
+ }{
+ {
+ t: time.Date(1234, 5, 6, 0, 0, 0, 0, time.UTC),
+ str: "1234-05-06",
+ },
+ {
+ t: time.Date(4567, 12, 31, 12, 0, 0, 0, time.UTC),
+ str: "4567-12-31 12:00:00",
+ },
+ {
+ t: time.Date(2020, 5, 30, 12, 34, 0, 0, time.UTC),
+ str: "2020-05-30 12:34:00",
+ },
+ {
+ t: time.Date(2020, 5, 30, 12, 34, 56, 0, time.UTC),
+ str: "2020-05-30 12:34:56",
+ },
+ {
+ t: time.Date(2020, 5, 30, 22, 33, 44, 123000000, time.UTC),
+ str: "2020-05-30 22:33:44.123",
+ },
+ {
+ t: time.Date(2020, 5, 30, 22, 33, 44, 123456000, time.UTC),
+ str: "2020-05-30 22:33:44.123456",
+ },
+ {
+ t: time.Date(2020, 5, 30, 22, 33, 44, 123456789, time.UTC),
+ str: "2020-05-30 22:33:44.123456789",
+ },
+ {
+ t: time.Date(9999, 12, 31, 23, 59, 59, 999999999, time.UTC),
+ str: "9999-12-31 23:59:59.999999999",
+ },
+ {
+ t: time.Date(1, 1, 1, 0, 0, 0, 0, time.UTC),
+ str: "0001-01-01",
+ },
+ }
+ for _, v := range tests {
+ buf := make([]byte, 0, 32)
+ buf, _ = appendDateTime(buf, v.t)
+ if str := string(buf); str != v.str {
+ t.Errorf("appendDateTime(%v), have: %s, want: %s", v.t, str, v.str)
+ }
+ }
+
+ // year out of range
+ {
+ v := time.Date(0, 1, 1, 0, 0, 0, 0, time.UTC)
+ buf := make([]byte, 0, 32)
+ _, err := appendDateTime(buf, v)
+ if err == nil {
+ t.Error("want an error")
+ return
+ }
+ }
+ {
+ v := time.Date(10000, 1, 1, 0, 0, 0, 0, time.UTC)
+ buf := make([]byte, 0, 32)
+ _, err := appendDateTime(buf, v)
+ if err == nil {
+ t.Error("want an error")
+ return
+ }
+ }
+}
+
+func TestParseDateTime(t *testing.T) {
+ cases := []struct {
+ name string
+ str string
+ }{
+ {
+ name: "parse date",
+ str: "2020-05-13",
+ },
+ {
+ name: "parse null date",
+ str: sDate0,
+ },
+ {
+ name: "parse datetime",
+ str: "2020-05-13 21:30:45",
+ },
+ {
+ name: "parse null datetime",
+ str: sDateTime0,
+ },
+ {
+ name: "parse datetime nanosec 1-digit",
+ str: "2020-05-25 23:22:01.1",
+ },
+ {
+ name: "parse datetime nanosec 2-digits",
+ str: "2020-05-25 23:22:01.15",
+ },
+ {
+ name: "parse datetime nanosec 3-digits",
+ str: "2020-05-25 23:22:01.159",
+ },
+ {
+ name: "parse datetime nanosec 4-digits",
+ str: "2020-05-25 23:22:01.1594",
+ },
+ {
+ name: "parse datetime nanosec 5-digits",
+ str: "2020-05-25 23:22:01.15949",
+ },
+ {
+ name: "parse datetime nanosec 6-digits",
+ str: "2020-05-25 23:22:01.159491",
+ },
+ }
+
+ for _, loc := range []*time.Location{
+ time.UTC,
+ time.FixedZone("test", 8*60*60),
+ } {
+ for _, cc := range cases {
+ t.Run(cc.name+"-"+loc.String(), func(t *testing.T) {
+ var want time.Time
+ if cc.str != sDate0 && cc.str != sDateTime0 {
+ var err error
+ want, err = time.ParseInLocation(timeFormat[:len(cc.str)], cc.str, loc)
+ if err != nil {
+ t.Fatal(err)
+ }
+ }
+ got, err := parseDateTime([]byte(cc.str), loc)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if !want.Equal(got) {
+ t.Fatalf("want: %v, but got %v", want, got)
+ }
+ })
+ }
+ }
+}
+
+func TestInvalidDateTime(t *testing.T) {
+ cases := []struct {
+ name string
+ str string
+ want time.Time
+ }{
+ {
+ name: "parse datetime without day",
+ str: "0000-00-00 21:30:45",
+ want: time.Date(0, 0, 0, 21, 30, 45, 0, time.UTC),
+ },
+ }
+
+ for _, cc := range cases {
+ t.Run(cc.name, func(t *testing.T) {
+ got, err := parseDateTime([]byte(cc.str), time.UTC)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if !cc.want.Equal(got) {
+ t.Fatalf("want: %v, but got %v", cc.want, got)
+ }
+ })
+ }
+}
+
+func TestParseDateTimeFail(t *testing.T) {
+ cases := []struct {
+ name string
+ str string
+ wantErr string
+ }{
+ {
+ name: "parse invalid time",
+ str: "hello",
+ wantErr: "invalid time bytes: hello",
+ },
+ {
+ name: "parse year",
+ str: "000!-00-00 00:00:00.000000",
+ wantErr: "not [0-9]",
+ },
+ {
+ name: "parse month",
+ str: "0000-!0-00 00:00:00.000000",
+ wantErr: "not [0-9]",
+ },
+ {
+ name: `parse "-" after parsed year`,
+ str: "0000:00-00 00:00:00.000000",
+ wantErr: "bad value for field: `:`",
+ },
+ {
+ name: `parse "-" after parsed month`,
+ str: "0000-00:00 00:00:00.000000",
+ wantErr: "bad value for field: `:`",
+ },
+ {
+ name: `parse " " after parsed date`,
+ str: "0000-00-00+00:00:00.000000",
+ wantErr: "bad value for field: `+`",
+ },
+ {
+ name: `parse ":" after parsed date`,
+ str: "0000-00-00 00-00:00.000000",
+ wantErr: "bad value for field: `-`",
+ },
+ {
+ name: `parse ":" after parsed hour`,
+ str: "0000-00-00 00:00-00.000000",
+ wantErr: "bad value for field: `-`",
+ },
+ {
+ name: `parse "." after parsed sec`,
+ str: "0000-00-00 00:00:00?000000",
+ wantErr: "bad value for field: `?`",
+ },
+ }
+
+ for _, cc := range cases {
+ t.Run(cc.name, func(t *testing.T) {
+ got, err := parseDateTime([]byte(cc.str), time.UTC)
+ if err == nil {
+ t.Fatal("want error")
+ }
+ if cc.wantErr != err.Error() {
+ t.Fatalf("want `%s`, but got `%s`", cc.wantErr, err)
+ }
+ if !got.IsZero() {
+ t.Fatal("want zero time")
+ }
+ })
+ }
+}
Debdiff
[The following lists of changes regard files as different if they have different names, permissions or owners.]
Files in second set of .debs but not in first
-rw-r--r-- root/root /usr/share/gocode/src/github.com/go-sql-driver/mysql/atomic_bool.go -rw-r--r-- root/root /usr/share/gocode/src/github.com/go-sql-driver/mysql/atomic_bool_go118.go -rw-r--r-- root/root /usr/share/gocode/src/github.com/go-sql-driver/mysql/atomic_bool_test.go -rw-r--r-- root/root /usr/share/gocode/src/github.com/go-sql-driver/mysql/fuzz.go
Files in first set of .debs but not in second
-rw-r--r-- root/root /usr/share/gocode/src/github.com/go-sql-driver/mysql/nulltime_go113.go -rw-r--r-- root/root /usr/share/gocode/src/github.com/go-sql-driver/mysql/nulltime_legacy.go
No differences were encountered in the control files