From b36cd86ebcb680d317f962679780f5877a0b91e1 Mon Sep 17 00:00:00 2001 From: Ichinose Shogo Date: Tue, 18 May 2021 10:33:08 +0900 Subject: [PATCH 01/26] Drop support of Go 1.12 (#1211) * Drop support of Go 1.12 * bump Go version in go.mod * remove nulltime_legacy --- .github/workflows/test.yml | 2 -- README.md | 2 +- driver_test.go | 14 ++++++------- fields.go | 2 +- go.mod | 2 +- nulltime.go | 21 ++++++++++++++++++++ nulltime_go113.go | 40 -------------------------------------- nulltime_legacy.go | 39 ------------------------------------- 8 files changed, 31 insertions(+), 91 deletions(-) delete mode 100644 nulltime_go113.go delete mode 100644 nulltime_legacy.go diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 886002143..fce4cf670 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -27,8 +27,6 @@ jobs: '1.15', '1.14', '1.13', - '1.12', - '1.11', ] mysql = [ '8.0', diff --git a/README.md b/README.md index 0b13154fc..9c8284cd1 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ A MySQL-Driver for Go's [database/sql](https://golang.org/pkg/database/sql/) pac * 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+) --------------------------------------- diff --git a/driver_test.go b/driver_test.go index 54f7cd10c..3ae379b26 100644 --- a/driver_test.go +++ b/driver_test.go @@ -2771,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") diff --git a/fields.go b/fields.go index ed6c7a37d..d82154ce8 100644 --- a/fields.go +++ b/fields.go @@ -106,7 +106,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/go.mod b/go.mod index fffbf6a90..251110478 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/nulltime.go b/nulltime.go index 651723a96..17af92ddc 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. diff --git a/nulltime_go113.go b/nulltime_go113.go deleted file mode 100644 index 453b4b394..000000000 --- a/nulltime_go113.go +++ /dev/null @@ -1,40 +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 -// -// 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 - -// for internal use. -// the mysql package uses sql.NullTime if it is available. -// if not, the package uses mysql.NullTime. -type nullTime = sql.NullTime // sql.NullTime is available diff --git a/nulltime_legacy.go b/nulltime_legacy.go deleted file mode 100644 index 9f7ae27a8..000000000 --- a/nulltime_legacy.go +++ /dev/null @@ -1,39 +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 -} - -// for internal use. -// the mysql package uses sql.NullTime if it is available. -// if not, the package uses mysql.NullTime. -type nullTime = NullTime // sql.NullTime is not available From 9942e21775f58b4a6339c3a814f7c1865d2007d9 Mon Sep 17 00:00:00 2001 From: lowang-bh Date: Wed, 26 May 2021 13:13:55 +0800 Subject: [PATCH 02/26] Fix readme: MaxIdle is same or less than MaxOpen (#1215) Co-authored-by: wanglonghui7 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9c8284cd1..eb9614cbd 100644 --- a/README.md +++ b/README.md @@ -85,7 +85,7 @@ db.SetMaxIdleConns(10) `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 (or greater than) `db.SetMaxOpenConns()`. When it is smaller than `SetMaxOpenConns()`, connections can be opened and closed very 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. +`db.SetMaxIdleConns()` is recommended to be set same to `db.SetMaxOpenConns()`. When it is smaller than `SetMaxOpenConns()`, connections can be opened and closed very 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) From 4b653727060f5799e4e4bd96ad693046885b375e Mon Sep 17 00:00:00 2001 From: Ichinose Shogo Date: Wed, 26 May 2021 20:46:06 +0900 Subject: [PATCH 03/26] noCopy implements sync.Locker (#1216) noCopy is used by -copylocks checker from `go vet`. see https://github.com/golang/go/issues/8005#issuecomment-190753527 for details. but it doesn't work from Go 1.11, because of https://github.com/golang/go/commit/c2eba53e7f80df21d51285879d51ab81bcfbf6bc and https://github.com/golang/go/issues/26165 -copylock now works with structs that implement sync.Locker. So, noCopy should implement sync.Locker. --- utils.go | 6 ++++++ utils_test.go | 4 +++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/utils.go b/utils.go index d6545f5be..b400cf99f 100644 --- a/utils.go +++ b/utils.go @@ -790,6 +790,12 @@ type noCopy struct{} // Lock is a no-op used by -copylocks checker from `go vet`. func (*noCopy) Lock() {} +// 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() {} + // atomicBool is a wrapper around uint32 for usage as a boolean value with // atomic access. type atomicBool struct { diff --git a/utils_test.go b/utils_test.go index 67b132d2b..b0069251e 100644 --- a/utils_test.go +++ b/utils_test.go @@ -228,7 +228,9 @@ func TestAtomicBool(t *testing.T) { t.Fatal("Expected value to be false") } - ab._noCopy.Lock() // we've "tested" it ¯\_(ツ)_/¯ + // we've "tested" them ¯\_(ツ)_/¯ + ab._noCopy.Lock() + defer ab._noCopy.Unlock() } func TestAtomicError(t *testing.T) { From e74ba5c13fd26850019c51afb1c4acf71f688aec Mon Sep 17 00:00:00 2001 From: Janek Vedock <83283832+jvedock@users.noreply.github.com> Date: Thu, 3 Jun 2021 04:45:34 -0400 Subject: [PATCH 04/26] Wording correction in README (#1218) * fixed grammatical error * Update AUTHORS --- AUTHORS | 1 + README.md | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/AUTHORS b/AUTHORS index 50afa2c85..900dfec06 100644 --- a/AUTHORS +++ b/AUTHORS @@ -45,6 +45,7 @@ Ilia Cimpoes INADA Naoki Jacek Szwec James Harr +Janek Vedock Jeff Hodges Jeffrey Charles Jerome Meyer diff --git a/README.md b/README.md index eb9614cbd..f056e614b 100644 --- a/README.md +++ b/README.md @@ -85,7 +85,7 @@ db.SetMaxIdleConns(10) `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 very 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. +`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) From 417641ad42910e50863a0caf4740ce319262f2f9 Mon Sep 17 00:00:00 2001 From: Chris Kirkland Date: Thu, 3 Jun 2021 18:34:57 -0500 Subject: [PATCH 05/26] support Is comparison on MySQLError (#1210) * support Is comparison on MySQLError * add myself to authors * skip error tests for go 1.12 * remove test build tag --- AUTHORS | 1 + errors.go | 7 +++++++ errors_test.go | 19 +++++++++++++++++++ 3 files changed, 27 insertions(+) diff --git a/AUTHORS b/AUTHORS index 900dfec06..e3370e025 100644 --- a/AUTHORS +++ b/AUTHORS @@ -23,6 +23,7 @@ Asta Xie Bulat Gaifullin Caine Jette Carlos Nieto +Chris Kirkland Chris Moos Craig Wilson Daniel Montoya diff --git a/errors.go b/errors.go index 760782ff2..92cc9a361 100644 --- a/errors.go +++ b/errors.go @@ -63,3 +63,10 @@ type MySQLError struct { func (me *MySQLError) Error() string { 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 96f9126d6..3a1aef74d 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{1234, "the server is on fire"} + otherInfraErr := &MySQLError{1234, "the datacenter is flooded"} + if !errors.Is(infraErr, otherInfraErr) { + t.Errorf("expected errors to be the same: %+v %+v", infraErr, otherInfraErr) + } + + differentCodeErr := &MySQLError{5678, "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) + } +} From 21f789cd2353b7ac81538f41426e9cfd2b1fcc87 Mon Sep 17 00:00:00 2001 From: ziheng Date: Wed, 30 Jun 2021 08:27:49 +0800 Subject: [PATCH 06/26] improve readability follows go-staticcheck (#1227) sign in AUTHORS --- AUTHORS | 1 + dsn.go | 1 - statement_test.go | 2 +- utils.go | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) diff --git a/AUTHORS b/AUTHORS index e3370e025..fee2d5ccf 100644 --- a/AUTHORS +++ b/AUTHORS @@ -101,6 +101,7 @@ Xiuming Chen Xuehong Chan Zhenye Xie Zhixin Wen +Ziheng Lyu # Organizations diff --git a/dsn.go b/dsn.go index 93f3548cb..a306d66a3 100644 --- a/dsn.go +++ b/dsn.go @@ -426,7 +426,6 @@ func parseDSNParams(cfg *Config, params string) (err error) { // Collation case "collation": cfg.Collation = value - break case "columnsWithAlias": var isBool bool diff --git a/statement_test.go b/statement_test.go index ac6b92de9..2563ece55 100644 --- a/statement_test.go +++ b/statement_test.go @@ -36,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) } } diff --git a/utils.go b/utils.go index b400cf99f..bcdee1b46 100644 --- a/utils.go +++ b/utils.go @@ -199,7 +199,7 @@ func parseByteYear(b []byte) (int, error) { return 0, err } year += v * n - n = n / 10 + n /= 10 } return year, nil } From a34e090a4648ec0ec682e87966cdcd4e43006a79 Mon Sep 17 00:00:00 2001 From: Ichinose Shogo Date: Mon, 12 Jul 2021 13:36:52 +0900 Subject: [PATCH 07/26] use utf8mb4 instead of utf8 in TestCharset (#1228) From MySQL 8.0.24, `SELECT @@character_set_connection` reports utf8mb3 or utf8mb4 instead of utf8. Because utf8 is currently an alias for utf8mb3, however at some point utf8 is expected to become a reference to utf8mb4. > ref. https://dev.mysql.com/doc/relnotes/mysql/8.0/en/news-8-0-24.html#mysqld-8-0-24-bug > Important Note: When a utf8mb3 collation was specified in a CREATE TABLE statement, SHOW CREATE TABLE, DEFAULT CHARSET, > the values of system variables containing character set names, > and the binary log all subsequently displayed the character set as utf8 which is becoming a synonym for utf8mb4. > Now in such cases, utf8mb3 is shown instead, and CREATE TABLE raises the warning 'collation_name' is a collation of the deprecated character set UTF8MB3. > Please consider using UTF8MB4 with an appropriate collation instead. (Bug #27225287, Bug #32085357, Bug #32122844) > > References: See also: Bug #30624990. The document says that we should use utf8mb4 instead of utf8, so we should follow it. --- driver_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/driver_test.go b/driver_test.go index 3ae379b26..f1e4ad71e 100644 --- a/driver_test.go +++ b/driver_test.go @@ -1450,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) { From 75d09acc46ea1a7074058d31da50293052248047 Mon Sep 17 00:00:00 2001 From: ziheng Date: Mon, 12 Jul 2021 17:58:03 +0800 Subject: [PATCH 08/26] refactoring (*textRows).readRow in a more clear way (#1230) --- packets.go | 52 ++++++++++++++++++++++++++-------------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/packets.go b/packets.go index 6664e5ae5..1867ecab2 100644 --- a/packets.go +++ b/packets.go @@ -761,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( - 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 From 6a88ab97c64c79be016f34d11e3295b8d291d50b Mon Sep 17 00:00:00 2001 From: ziheng Date: Mon, 19 Jul 2021 07:00:59 +0800 Subject: [PATCH 09/26] add an invalid DSN test case (#1235) --- dsn_test.go | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/dsn_test.go b/dsn_test.go index 89815b341..fc6eea9c8 100644 --- a/dsn_test.go +++ b/dsn_test.go @@ -92,13 +92,14 @@ 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 //"/dbname?arg=/some/unescaped/path", } From e8f8fcdbd3e06fc74b6cf745c13eaebedff68c49 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Mon, 19 Jul 2021 07:01:45 +0800 Subject: [PATCH 10/26] return unsigned in database type name when necessary (#1238) * return unsigned in database type name when necessary * Fix test * Update README * Add myself in AUTHORS --- AUTHORS | 1 + README.md | 2 +- driver_test.go | 8 ++++---- fields.go | 12 ++++++++++++ 4 files changed, 18 insertions(+), 5 deletions(-) diff --git a/AUTHORS b/AUTHORS index fee2d5ccf..80199cf2c 100644 --- a/AUTHORS +++ b/AUTHORS @@ -67,6 +67,7 @@ Linh Tran Tuan Lion Yang Luca Looz Lucas Liu +Lunny Xiao Luke Scott Maciej Zimnoch Michael Woolnough diff --git a/README.md b/README.md index f056e614b..ded6e3b16 100644 --- a/README.md +++ b/README.md @@ -454,7 +454,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. diff --git a/driver_test.go b/driver_test.go index f1e4ad71e..7b4ab84f3 100644 --- a/driver_test.go +++ b/driver_test.go @@ -2808,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}}, diff --git a/fields.go b/fields.go index d82154ce8..e0654a83d 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] { From a5bb8074f2718b6f49530d588c5ee8649cf0a58a Mon Sep 17 00:00:00 2001 From: Santhosh Kumar Tekuri Date: Tue, 24 Aug 2021 10:20:01 +0530 Subject: [PATCH 11/26] Fix sha256_password auth plugin on unix transport (#1246) caching_sha2_password uses cleartext password when the connection is Unix or sharedmemory protocol. but sha256_password do not. --- AUTHORS | 1 + auth.go | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/AUTHORS b/AUTHORS index 80199cf2c..876b2964a 100644 --- a/AUTHORS +++ b/AUTHORS @@ -82,6 +82,7 @@ Reed Allman Richard Wilkes Robert Russell Runrioter Wung +Santhosh Kumar Tekuri Sho Iizuka Sho Ikeda Shuode Li diff --git a/auth.go b/auth.go index b2f19e8f0..f610b5f49 100644 --- a/auth.go +++ b/auth.go @@ -274,7 +274,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 } From 6cf3092b0e12f6e197de3ed6aa2acfeac322a9bb Mon Sep 17 00:00:00 2001 From: Ichinose Shogo Date: Wed, 25 Aug 2021 17:32:50 +0900 Subject: [PATCH 12/26] Go 1.17 and MariaDB 10.6 are released (#1253) * Go 1.17 and MariaDB 10.6 are released * use `utf8mb4_unicode_ci` instead of `utf8_unicode_ci` https://mariadb.com/kb/en/changes-improvements-in-mariadb-106/#character-sets > The utf8 character set (and related collations) is now by default an alias for utf8mb3 rather than the other way around. --- .github/workflows/test.yml | 4 +++- driver_test.go | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fce4cf670..6bf3524d4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -22,8 +22,9 @@ jobs: import json go = [ # Keep the most recent production release at the top - '1.16', + '1.17', # Older production releases + '1.16', '1.15', '1.14', '1.13', @@ -32,6 +33,7 @@ jobs: '8.0', '5.7', '5.6', + 'mariadb-10.6', 'mariadb-10.5', 'mariadb-10.4', 'mariadb-10.3', diff --git a/driver_test.go b/driver_test.go index 7b4ab84f3..4850498d0 100644 --- a/driver_test.go +++ b/driver_test.go @@ -1479,7 +1479,7 @@ func TestCollation(t *testing.T) { defaultCollation, // driver default "latin1_general_ci", "binary", - "utf8_unicode_ci", + "utf8mb4_unicode_ci", "cp1257_bin", } From 0d7b91a8e9b24de9f7fa55d475b8b43f0bccfe48 Mon Sep 17 00:00:00 2001 From: zjj Date: Mon, 10 Jan 2022 16:54:06 +0800 Subject: [PATCH 13/26] Fixed SetReadDeadline before connCheck (#1299) there's no need to set conn's readline if CheckConnLiveness is false, and the ReadTimeout shall work with rawConn.Read inside conncheck. buffer.fill will do SetReadDeadline if needed --- packets.go | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/packets.go b/packets.go index 1867ecab2..ab30601ae 100644 --- a/packets.go +++ b/packets.go @@ -110,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) From 217d05049e5a88d529b9a2d5fe5675120831efab Mon Sep 17 00:00:00 2001 From: zjj Date: Wed, 19 Jan 2022 16:52:20 +0800 Subject: [PATCH 14/26] enhancement for mysqlConn handleAuthResult (#1250) --- auth.go | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/auth.go b/auth.go index f610b5f49..a25353429 100644 --- a/auth.go +++ b/auth.go @@ -367,13 +367,20 @@ 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 } + 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) @@ -406,6 +413,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 From c1aa6812e4756a364458fbef765f098421090372 Mon Sep 17 00:00:00 2001 From: cui fliter Date: Thu, 3 Mar 2022 08:13:32 +0800 Subject: [PATCH 15/26] utils: fix typo (#1312) Signed-off-by: cuishuang --- utils.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils.go b/utils.go index bcdee1b46..1176d743b 100644 --- a/utils.go +++ b/utils.go @@ -542,7 +542,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) { From ce134bfccee8f827a591ba127ee4b004e0ee0a5c Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Fri, 11 Mar 2022 21:47:08 +0900 Subject: [PATCH 16/26] util: Reduce boundery check in escape functions. (#1316) --- utils.go | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/utils.go b/utils.go index 1176d743b..5a024aa0a 100644 --- a/utils.go +++ b/utils.go @@ -652,32 +652,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 @@ -697,32 +697,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 @@ -744,8 +744,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 @@ -764,8 +764,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 From 90e813fe43edc87a66650b570e8362da44041a4c Mon Sep 17 00:00:00 2001 From: Mikhail Faraponov <11322032+moredure@users.noreply.github.com> Date: Fri, 18 Mar 2022 06:23:21 +0200 Subject: [PATCH 17/26] Add go1.18 to test matrix (#1317) --- .github/workflows/test.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6bf3524d4..f8c472832 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -22,8 +22,9 @@ jobs: import json go = [ # Keep the most recent production release at the top - '1.17', + '1.18', # Older production releases + '1.17', '1.16', '1.15', '1.14', From eff3908a822d24d8952cc3cd547eeb36f2affa7f Mon Sep 17 00:00:00 2001 From: Mikhail Faraponov <11322032+moredure@users.noreply.github.com> Date: Fri, 25 Mar 2022 02:07:47 +0200 Subject: [PATCH 18/26] Move default packet size to constant (#1319) --- infile.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/infile.go b/infile.go index 60effdfc2..e6323aea4 100644 --- a/infile.go +++ b/infile.go @@ -93,10 +93,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 } From ad9fa14acdcf7d0533e7fbe58728f3d216213ade Mon Sep 17 00:00:00 2001 From: Thomas Posch <55388669+thopos@users.noreply.github.com> Date: Wed, 13 Apr 2022 09:25:45 +0200 Subject: [PATCH 19/26] Add SQLState to MySQLError (#1321) Report SQLState in MySQLError to allow library users to distinguish user-defined from client / server errors. --- AUTHORS | 1 + errors.go | 9 +++++++-- errors_test.go | 6 +++--- packets.go | 11 ++++++----- 4 files changed, 17 insertions(+), 10 deletions(-) diff --git a/AUTHORS b/AUTHORS index 876b2964a..50b9593f0 100644 --- a/AUTHORS +++ b/AUTHORS @@ -110,6 +110,7 @@ Ziheng Lyu Barracuda Networks, Inc. Counting Ltd. DigitalOcean Inc. +dyves labs AG Facebook Inc. GitHub Inc. Google Inc. diff --git a/errors.go b/errors.go index 92cc9a361..7c037e7d6 100644 --- a/errors.go +++ b/errors.go @@ -56,11 +56,16 @@ 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) } diff --git a/errors_test.go b/errors_test.go index 3a1aef74d..43213f98e 100644 --- a/errors_test.go +++ b/errors_test.go @@ -43,13 +43,13 @@ func TestErrorsStrictIgnoreNotes(t *testing.T) { } func TestMySQLErrIs(t *testing.T) { - infraErr := &MySQLError{1234, "the server is on fire"} - otherInfraErr := &MySQLError{1234, "the datacenter is flooded"} + 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{5678, "the server is on fire"} + 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) } diff --git a/packets.go b/packets.go index ab30601ae..003584c25 100644 --- a/packets.go +++ b/packets.go @@ -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 { From 0c62bb2791485d4260371bcc6017321de93b2430 Mon Sep 17 00:00:00 2001 From: ICHINOSE Shogo Date: Fri, 19 Aug 2022 16:29:14 +0900 Subject: [PATCH 20/26] Go1.19 is released (#1350) --- .github/workflows/test.yml | 3 +- atomic_bool.go | 19 ++++++++++ atomic_bool_go118.go | 47 +++++++++++++++++++++++++ atomic_bool_test.go | 71 +++++++++++++++++++++++++++++++++++++ auth.go | 35 +++++++++--------- collations.go | 3 +- conncheck.go | 1 + conncheck_dummy.go | 1 + conncheck_test.go | 1 + connection.go | 22 ++++++------ connection_test.go | 2 +- driver.go | 6 ++-- fuzz.go | 1 + infile.go | 28 +++++++-------- nulltime.go | 18 +++++----- statement.go | 8 ++--- transaction.go | 4 +-- utils.go | 72 +++++++++++--------------------------- utils_test.go | 60 ------------------------------- 19 files changed, 226 insertions(+), 176 deletions(-) create mode 100644 atomic_bool.go create mode 100644 atomic_bool_go118.go create mode 100644 atomic_bool_test.go diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f8c472832..b558eba28 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -22,8 +22,9 @@ jobs: import json go = [ # Keep the most recent production release at the top - '1.18', + '1.19', # Older production releases + '1.18', '1.17', '1.16', '1.15', diff --git a/atomic_bool.go b/atomic_bool.go new file mode 100644 index 000000000..1b7e19f3e --- /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 000000000..2e9a7f0b6 --- /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 000000000..a3b4ea0e8 --- /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 a25353429..26f8723f5 100644 --- a/auth.go +++ b/auth.go @@ -33,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 { diff --git a/collations.go b/collations.go index 326a9f7fa..295bfbe52 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. // diff --git a/conncheck.go b/conncheck.go index 024eb2858..0ea721720 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 ea7fb607a..a56c138f2 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 53995517b..f7e025680 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 835f89729..9539077cb 100644 --- a/connection.go +++ b/connection.go @@ -104,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 } @@ -123,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) } @@ -137,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 } @@ -152,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 } @@ -162,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 } @@ -295,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 } @@ -356,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 } @@ -450,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 } @@ -469,7 +469,7 @@ 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.IsSet() { + if mc.closed.Load() { return nil, driver.ErrBadConn } @@ -636,7 +636,7 @@ 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 @@ -646,5 +646,5 @@ func (mc *mysqlConn) ResetSession(ctx context.Context) error { // IsValid implements driver.Validator interface // (From Go 1.15) func (mc *mysqlConn) IsValid() bool { - return !mc.closed.IsSet() + return !mc.closed.Load() } diff --git a/connection_test.go b/connection_test.go index a6d677308..b6764a2f6 100644 --- a/connection_test.go +++ b/connection_test.go @@ -147,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/driver.go b/driver.go index c1bdf1199..ad7aec215 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/fuzz.go b/fuzz.go index fa75adf6a..3a4ec25a9 100644 --- a/fuzz.go +++ b/fuzz.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 gofuzz // +build gofuzz package mysql diff --git a/infile.go b/infile.go index e6323aea4..3279dcffd 100644 --- a/infile.go +++ b/infile.go @@ -28,12 +28,11 @@ var ( // 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 @@ -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 diff --git a/nulltime.go b/nulltime.go index 17af92ddc..36c8a42c5 100644 --- a/nulltime.go +++ b/nulltime.go @@ -19,16 +19,16 @@ import ( // 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 -// } +// 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 +// # 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. diff --git a/statement.go b/statement.go index 18a3ae498..10ece8bd6 100644 --- a/statement.go +++ b/statement.go @@ -23,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. @@ -50,7 +50,7 @@ func (stmt *mysqlStmt) CheckNamedValue(nv *driver.NamedValue) (err error) { } 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 } @@ -98,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 } @@ -157,7 +157,7 @@ func (c converter) ConvertValue(v interface{}) (driver.Value, error) { if driver.IsValue(sv) { return sv, nil } - // A value returend from the Valuer interface can be "a type handled by + // 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 { diff --git a/transaction.go b/transaction.go index 417d72793..4a4b61001 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 5a024aa0a..60f1a91c6 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) @@ -796,39 +795,10 @@ func (*noCopy) Lock() {} // https://github.com/golang/go/issues/26165 func (*noCopy) Unlock() {} -// 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 -} - // 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 b0069251e..8296ac2aa 100644 --- a/utils_test.go +++ b/utils_test.go @@ -173,66 +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") - } - - // we've "tested" them ¯\_(ツ)_/¯ - ab._noCopy.Lock() - defer ab._noCopy.Unlock() -} - func TestAtomicError(t *testing.T) { var ae atomicError if ae.Value() != nil { From a477f69f3c2abaf4646680bdc3a65d5172a6566e Mon Sep 17 00:00:00 2001 From: ICHINOSE Shogo Date: Sat, 20 Aug 2022 00:19:54 +0900 Subject: [PATCH 21/26] fix: benchmarkExecContext is unused (#1351) --- benchmark_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/benchmark_test.go b/benchmark_test.go index 1030ddc52..97ed781f8 100644 --- a/benchmark_test.go +++ b/benchmark_test.go @@ -314,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) }) } } From 803c0e06f2b703d30b18e168e7349ffc66e7fa86 Mon Sep 17 00:00:00 2001 From: ICHINOSE Shogo Date: Mon, 7 Nov 2022 21:26:59 +0900 Subject: [PATCH 22/26] migrate set-output to the environment file (#1368) --- .github/workflows/test.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b558eba28..703203258 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -20,6 +20,7 @@ jobs: id: set-matrix run: | import json + import os go = [ # Keep the most recent production release at the top '1.19', @@ -55,7 +56,8 @@ jobs: 'include': includes } output = json.dumps(matrix, separators=(',', ':')) - print('::set-output name=matrix::{0}'.format(output)) + with open(os.environ["GITHUB_OUTPUT"], 'a', encoding="utf-8") as f: + f.write('matrix={0}\n'.format(output)) shell: python test: needs: list From 05bed834d054b8361595c6544146567d70713dc1 Mon Sep 17 00:00:00 2001 From: "lgtm-com[bot]" <43144390+lgtm-com[bot]@users.noreply.github.com> Date: Thu, 10 Nov 2022 06:19:39 +0900 Subject: [PATCH 23/26] Add CodeQL workflow for GitHub code scanning (#1369) Co-authored-by: LGTM Migrator --- .github/workflows/codeql.yml | 41 ++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 .github/workflows/codeql.yml diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 000000000..d9d29a8b7 --- /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 }}" From fa1e4ed592daa59bcd70003263b5fc72e3de0137 Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Thu, 10 Nov 2022 15:01:30 +0900 Subject: [PATCH 24/26] Fix parsing 0 year. (#1257) Fix #1252 --- utils.go | 10 ---------- utils_test.go | 27 +++++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/utils.go b/utils.go index 60f1a91c6..15dbd8d16 100644 --- a/utils.go +++ b/utils.go @@ -117,10 +117,6 @@ func parseDateTime(b []byte, loc *time.Location) (time.Time, error) { if err != nil { return time.Time{}, err } - if year <= 0 { - year = 1 - } - if b[4] != '-' { return time.Time{}, fmt.Errorf("bad value for field: `%c`", b[4]) } @@ -129,9 +125,6 @@ func parseDateTime(b []byte, loc *time.Location) (time.Time, error) { if err != nil { return time.Time{}, err } - if m <= 0 { - m = 1 - } month := time.Month(m) if b[7] != '-' { @@ -142,9 +135,6 @@ func parseDateTime(b []byte, loc *time.Location) (time.Time, error) { if err != nil { return time.Time{}, err } - if day <= 0 { - day = 1 - } if len(b) == 10 { return time.Date(year, month, day, 0, 0, 0, 0, loc), nil } diff --git a/utils_test.go b/utils_test.go index 8296ac2aa..4e5fc3cb7 100644 --- a/utils_test.go +++ b/utils_test.go @@ -380,6 +380,33 @@ func TestParseDateTime(t *testing.T) { } } +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 From 41dd159e6ec9afad00d2b90144bbc083ea860db1 Mon Sep 17 00:00:00 2001 From: lance6716 Date: Mon, 28 Nov 2022 14:26:20 +0800 Subject: [PATCH 25/26] Add `AllowFallbackToPlaintext` and `TLS` to config (#1370) --- AUTHORS | 1 + README.md | 11 ++++++++ auth.go | 4 +-- auth_test.go | 10 ++++---- dsn.go | 72 ++++++++++++++++++++++++++++++++-------------------- dsn_test.go | 47 +++++++++++++++++----------------- packets.go | 12 ++++----- 7 files changed, 94 insertions(+), 63 deletions(-) diff --git a/AUTHORS b/AUTHORS index 50b9593f0..051327519 100644 --- a/AUTHORS +++ b/AUTHORS @@ -61,6 +61,7 @@ Kamil Dziedzic Kei Kamikawa Kevin Malachowski Kieron Woodhouse +Lance Tian Lennart Rudolph Leonardo YongUk Kim Linh Tran Tuan diff --git a/README.md b/README.md index ded6e3b16..25de2e5aa 100644 --- a/README.md +++ b/README.md @@ -157,6 +157,17 @@ Default: false `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` ``` diff --git a/auth.go b/auth.go index 26f8723f5..1ff203e57 100644 --- a/auth.go +++ b/auth.go @@ -275,7 +275,7 @@ func (mc *mysqlConn) auth(authData []byte, plugin string) ([]byte, error) { } // unlike caching_sha2_password, sha256_password does not accept // cleartext password on unix transport. - if mc.cfg.tls != nil { + if mc.cfg.TLS != nil { // write cleartext auth packet return append([]byte(mc.cfg.Passwd), 0), nil } @@ -351,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 { diff --git a/auth_test.go b/auth_test.go index 3bce7fe22..3ce0ea6e0 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, @@ -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/dsn.go b/dsn.go index a306d66a3..4b71aaab0 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") } @@ -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 diff --git a/dsn_test.go b/dsn_test.go index fc6eea9c8..41a6a29fa 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) @@ -100,6 +100,7 @@ func TestDSNParserInvalid(t *testing.T) { "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", } @@ -118,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() @@ -127,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 { @@ -203,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) } @@ -214,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) @@ -229,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" @@ -241,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) } } @@ -262,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) } } @@ -335,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") } @@ -384,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/packets.go b/packets.go index 003584c25..ee05c95a8 100644 --- a/packets.go +++ b/packets.go @@ -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 } @@ -356,14 +356,14 @@ func (mc *mysqlConn) writeHandshakeResponsePacket(authResp []byte, plugin string // 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 } From 5cee457661043566c72c86b89aadbab7b88cce7a Mon Sep 17 00:00:00 2001 From: ICHINOSE Shogo Date: Fri, 2 Dec 2022 20:40:24 +0900 Subject: [PATCH 26/26] update changelog for Version 1.7 (#1376) --- CHANGELOG.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 72a738ed5..77024a820 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,24 @@ +## 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: