Skip to content
Snippets Groups Projects

Compare revisions

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

Source

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

Target

Select target project
  • oss/libraries/go/services/job-queues
1 result
Select Git revision
  • master
  • v1.0.0
  • v1.0.1
  • v1.1.0
  • v1.10.0
  • v1.10.1
  • v1.10.2
  • v1.11.0
  • v1.12.0
  • v1.12.1
  • v1.12.2
  • v1.12.3
  • v1.12.4
  • v1.12.5
  • v1.12.6
  • v1.12.7
  • v1.12.8
  • v1.13.0
  • v1.13.1
  • v1.13.2
  • v1.14.0
  • v1.15.0
  • v1.15.1
  • v1.15.10
  • v1.15.11
  • v1.15.12
  • v1.15.13
  • v1.15.14
  • v1.15.15
  • v1.15.16
  • v1.15.17
  • v1.15.2
  • v1.15.3
  • v1.15.4
  • v1.15.5
  • v1.15.6
  • v1.15.7
  • v1.15.8
  • v1.15.9
  • v1.16.0
  • v1.16.1
  • v1.17.0
  • v1.18.0
  • v1.18.1
  • v1.18.2
  • v1.19.0
  • v1.19.1
  • v1.19.2
  • v1.19.3
  • v1.19.4
  • v1.2.0
  • v1.20.0
  • v1.20.1
  • v1.20.2
  • v1.20.3
  • v1.21.0
  • v1.21.1
  • v1.22.0
  • v1.23.0
  • v1.23.1
  • v1.23.2
  • v1.3.0
  • v1.3.1
  • v1.3.2
  • v1.4.0
  • v1.5.0
  • v1.5.1
  • v1.6.0
  • v1.6.1
  • v1.7.0
  • v1.7.1
  • v1.7.2
  • v1.7.3
  • v1.8.0
  • v1.8.1
  • v1.9.0
76 results
Show changes
Showing
with 0 additions and 8870 deletions
// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
//
// Copyright 2018 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/.
package mysql
import (
"bytes"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"encoding/pem"
"fmt"
"testing"
)
var testPubKey = []byte("-----BEGIN PUBLIC KEY-----\n" +
"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAol0Z8G8U+25Btxk/g/fm\n" +
"UAW/wEKjQCTjkibDE4B+qkuWeiumg6miIRhtilU6m9BFmLQSy1ltYQuu4k17A4tQ\n" +
"rIPpOQYZges/qsDFkZh3wyK5jL5WEFVdOasf6wsfszExnPmcZS4axxoYJfiuilrN\n" +
"hnwinBAqfi3S0sw5MpSI4Zl1AbOrHG4zDI62Gti2PKiMGyYDZTS9xPrBLbN95Kby\n" +
"FFclQLEzA9RJcS1nHFsWtRgHjGPhhjCQxEm9NQ1nePFhCfBfApyfH1VM2VCOQum6\n" +
"Ci9bMuHWjTjckC84mzF99kOxOWVU7mwS6gnJqBzpuz8t3zq8/iQ2y7QrmZV+jTJP\n" +
"WQIDAQAB\n" +
"-----END PUBLIC KEY-----\n")
var testPubKeyRSA *rsa.PublicKey
func init() {
block, _ := pem.Decode(testPubKey)
pub, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
panic(err)
}
testPubKeyRSA = pub.(*rsa.PublicKey)
}
func TestScrambleOldPass(t *testing.T) {
scramble := []byte{9, 8, 7, 6, 5, 4, 3, 2}
vectors := []struct {
pass string
out string
}{
{" pass", "47575c5a435b4251"},
{"pass ", "47575c5a435b4251"},
{"123\t456", "575c47505b5b5559"},
{"C0mpl!ca ted#PASS123", "5d5d554849584a45"},
}
for _, tuple := range vectors {
ours := scrambleOldPassword(scramble, tuple.pass)
if tuple.out != fmt.Sprintf("%x", ours) {
t.Errorf("Failed old password %q", tuple.pass)
}
}
}
func TestScrambleSHA256Pass(t *testing.T) {
scramble := []byte{10, 47, 74, 111, 75, 73, 34, 48, 88, 76, 114, 74, 37, 13, 3, 80, 82, 2, 23, 21}
vectors := []struct {
pass string
out string
}{
{"secret", "f490e76f66d9d86665ce54d98c78d0acfe2fb0b08b423da807144873d30b312c"},
{"secret2", "abc3934a012cf342e876071c8ee202de51785b430258a7a0138bc79c4d800bc6"},
}
for _, tuple := range vectors {
ours := scrambleSHA256Password(scramble, tuple.pass)
if tuple.out != fmt.Sprintf("%x", ours) {
t.Errorf("Failed SHA256 password %q", tuple.pass)
}
}
}
func TestAuthFastCachingSHA256PasswordCached(t *testing.T) {
conn, mc := newRWMockConn(1)
mc.cfg.User = "root"
mc.cfg.Passwd = "secret"
authData := []byte{90, 105, 74, 126, 30, 48, 37, 56, 3, 23, 115, 127, 69,
22, 41, 84, 32, 123, 43, 118}
plugin := "caching_sha2_password"
// Send Client Authentication Packet
authResp, err := mc.auth(authData, plugin)
if err != nil {
t.Fatal(err)
}
err = mc.writeHandshakeResponsePacket(authResp, plugin)
if err != nil {
t.Fatal(err)
}
// check written auth response
authRespStart := 4 + 4 + 4 + 1 + 23 + len(mc.cfg.User) + 1
authRespEnd := authRespStart + 1 + len(authResp)
writtenAuthRespLen := conn.written[authRespStart]
writtenAuthResp := conn.written[authRespStart+1 : authRespEnd]
expectedAuthResp := []byte{102, 32, 5, 35, 143, 161, 140, 241, 171, 232, 56,
139, 43, 14, 107, 196, 249, 170, 147, 60, 220, 204, 120, 178, 214, 15,
184, 150, 26, 61, 57, 235}
if writtenAuthRespLen != 32 || !bytes.Equal(writtenAuthResp, expectedAuthResp) {
t.Fatalf("unexpected written auth response (%d bytes): %v", writtenAuthRespLen, writtenAuthResp)
}
conn.written = nil
// auth response
conn.data = []byte{
2, 0, 0, 2, 1, 3, // Fast Auth Success
7, 0, 0, 3, 0, 0, 0, 2, 0, 0, 0, // OK
}
conn.maxReads = 1
// Handle response to auth packet
if err := mc.handleAuthResult(authData, plugin); err != nil {
t.Errorf("got error: %v", err)
}
}
func TestAuthFastCachingSHA256PasswordEmpty(t *testing.T) {
conn, mc := newRWMockConn(1)
mc.cfg.User = "root"
mc.cfg.Passwd = ""
authData := []byte{90, 105, 74, 126, 30, 48, 37, 56, 3, 23, 115, 127, 69,
22, 41, 84, 32, 123, 43, 118}
plugin := "caching_sha2_password"
// Send Client Authentication Packet
authResp, err := mc.auth(authData, plugin)
if err != nil {
t.Fatal(err)
}
err = mc.writeHandshakeResponsePacket(authResp, plugin)
if err != nil {
t.Fatal(err)
}
// check written auth response
authRespStart := 4 + 4 + 4 + 1 + 23 + len(mc.cfg.User) + 1
authRespEnd := authRespStart + 1 + len(authResp)
writtenAuthRespLen := conn.written[authRespStart]
writtenAuthResp := conn.written[authRespStart+1 : authRespEnd]
if writtenAuthRespLen != 0 {
t.Fatalf("unexpected written auth response (%d bytes): %v",
writtenAuthRespLen, writtenAuthResp)
}
conn.written = nil
// auth response
conn.data = []byte{
7, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, // OK
}
conn.maxReads = 1
// Handle response to auth packet
if err := mc.handleAuthResult(authData, plugin); err != nil {
t.Errorf("got error: %v", err)
}
}
func TestAuthFastCachingSHA256PasswordFullRSA(t *testing.T) {
conn, mc := newRWMockConn(1)
mc.cfg.User = "root"
mc.cfg.Passwd = "secret"
authData := []byte{6, 81, 96, 114, 14, 42, 50, 30, 76, 47, 1, 95, 126, 81,
62, 94, 83, 80, 52, 85}
plugin := "caching_sha2_password"
// Send Client Authentication Packet
authResp, err := mc.auth(authData, plugin)
if err != nil {
t.Fatal(err)
}
err = mc.writeHandshakeResponsePacket(authResp, plugin)
if err != nil {
t.Fatal(err)
}
// check written auth response
authRespStart := 4 + 4 + 4 + 1 + 23 + len(mc.cfg.User) + 1
authRespEnd := authRespStart + 1 + len(authResp)
writtenAuthRespLen := conn.written[authRespStart]
writtenAuthResp := conn.written[authRespStart+1 : authRespEnd]
expectedAuthResp := []byte{171, 201, 138, 146, 89, 159, 11, 170, 0, 67, 165,
49, 175, 94, 218, 68, 177, 109, 110, 86, 34, 33, 44, 190, 67, 240, 70,
110, 40, 139, 124, 41}
if writtenAuthRespLen != 32 || !bytes.Equal(writtenAuthResp, expectedAuthResp) {
t.Fatalf("unexpected written auth response (%d bytes): %v", writtenAuthRespLen, writtenAuthResp)
}
conn.written = nil
// auth response
conn.data = []byte{
2, 0, 0, 2, 1, 4, // Perform Full Authentication
}
conn.queuedReplies = [][]byte{
// pub key response
append([]byte{byte(1 + len(testPubKey)), 1, 0, 4, 1}, testPubKey...),
// OK
{7, 0, 0, 6, 0, 0, 0, 2, 0, 0, 0},
}
conn.maxReads = 3
// Handle response to auth packet
if err := mc.handleAuthResult(authData, plugin); err != nil {
t.Errorf("got error: %v", err)
}
if !bytes.HasPrefix(conn.written, []byte{1, 0, 0, 3, 2, 0, 1, 0, 5}) {
t.Errorf("unexpected written data: %v", conn.written)
}
}
func TestAuthFastCachingSHA256PasswordFullRSAWithKey(t *testing.T) {
conn, mc := newRWMockConn(1)
mc.cfg.User = "root"
mc.cfg.Passwd = "secret"
mc.cfg.pubKey = testPubKeyRSA
authData := []byte{6, 81, 96, 114, 14, 42, 50, 30, 76, 47, 1, 95, 126, 81,
62, 94, 83, 80, 52, 85}
plugin := "caching_sha2_password"
// Send Client Authentication Packet
authResp, err := mc.auth(authData, plugin)
if err != nil {
t.Fatal(err)
}
err = mc.writeHandshakeResponsePacket(authResp, plugin)
if err != nil {
t.Fatal(err)
}
// check written auth response
authRespStart := 4 + 4 + 4 + 1 + 23 + len(mc.cfg.User) + 1
authRespEnd := authRespStart + 1 + len(authResp)
writtenAuthRespLen := conn.written[authRespStart]
writtenAuthResp := conn.written[authRespStart+1 : authRespEnd]
expectedAuthResp := []byte{171, 201, 138, 146, 89, 159, 11, 170, 0, 67, 165,
49, 175, 94, 218, 68, 177, 109, 110, 86, 34, 33, 44, 190, 67, 240, 70,
110, 40, 139, 124, 41}
if writtenAuthRespLen != 32 || !bytes.Equal(writtenAuthResp, expectedAuthResp) {
t.Fatalf("unexpected written auth response (%d bytes): %v", writtenAuthRespLen, writtenAuthResp)
}
conn.written = nil
// auth response
conn.data = []byte{
2, 0, 0, 2, 1, 4, // Perform Full Authentication
}
conn.queuedReplies = [][]byte{
// OK
{7, 0, 0, 4, 0, 0, 0, 2, 0, 0, 0},
}
conn.maxReads = 2
// Handle response to auth packet
if err := mc.handleAuthResult(authData, plugin); err != nil {
t.Errorf("got error: %v", err)
}
if !bytes.HasPrefix(conn.written, []byte{0, 1, 0, 3}) {
t.Errorf("unexpected written data: %v", conn.written)
}
}
func TestAuthFastCachingSHA256PasswordFullSecure(t *testing.T) {
conn, mc := newRWMockConn(1)
mc.cfg.User = "root"
mc.cfg.Passwd = "secret"
authData := []byte{6, 81, 96, 114, 14, 42, 50, 30, 76, 47, 1, 95, 126, 81,
62, 94, 83, 80, 52, 85}
plugin := "caching_sha2_password"
// Send Client Authentication Packet
authResp, err := mc.auth(authData, plugin)
if err != nil {
t.Fatal(err)
}
err = mc.writeHandshakeResponsePacket(authResp, plugin)
if err != nil {
t.Fatal(err)
}
// Hack to make the caching_sha2_password plugin believe that the connection
// is secure
mc.cfg.TLS = &tls.Config{InsecureSkipVerify: true}
// check written auth response
authRespStart := 4 + 4 + 4 + 1 + 23 + len(mc.cfg.User) + 1
authRespEnd := authRespStart + 1 + len(authResp)
writtenAuthRespLen := conn.written[authRespStart]
writtenAuthResp := conn.written[authRespStart+1 : authRespEnd]
expectedAuthResp := []byte{171, 201, 138, 146, 89, 159, 11, 170, 0, 67, 165,
49, 175, 94, 218, 68, 177, 109, 110, 86, 34, 33, 44, 190, 67, 240, 70,
110, 40, 139, 124, 41}
if writtenAuthRespLen != 32 || !bytes.Equal(writtenAuthResp, expectedAuthResp) {
t.Fatalf("unexpected written auth response (%d bytes): %v", writtenAuthRespLen, writtenAuthResp)
}
conn.written = nil
// auth response
conn.data = []byte{
2, 0, 0, 2, 1, 4, // Perform Full Authentication
}
conn.queuedReplies = [][]byte{
// OK
{7, 0, 0, 4, 0, 0, 0, 2, 0, 0, 0},
}
conn.maxReads = 3
// Handle response to auth packet
if err := mc.handleAuthResult(authData, plugin); err != nil {
t.Errorf("got error: %v", err)
}
if !bytes.Equal(conn.written, []byte{7, 0, 0, 3, 115, 101, 99, 114, 101, 116, 0}) {
t.Errorf("unexpected written data: %v", conn.written)
}
}
func TestAuthFastCleartextPasswordNotAllowed(t *testing.T) {
_, mc := newRWMockConn(1)
mc.cfg.User = "root"
mc.cfg.Passwd = "secret"
authData := []byte{70, 114, 92, 94, 1, 38, 11, 116, 63, 114, 23, 101, 126,
103, 26, 95, 81, 17, 24, 21}
plugin := "mysql_clear_password"
// Send Client Authentication Packet
_, err := mc.auth(authData, plugin)
if err != ErrCleartextPassword {
t.Errorf("expected ErrCleartextPassword, got %v", err)
}
}
func TestAuthFastCleartextPassword(t *testing.T) {
conn, mc := newRWMockConn(1)
mc.cfg.User = "root"
mc.cfg.Passwd = "secret"
mc.cfg.AllowCleartextPasswords = true
authData := []byte{70, 114, 92, 94, 1, 38, 11, 116, 63, 114, 23, 101, 126,
103, 26, 95, 81, 17, 24, 21}
plugin := "mysql_clear_password"
// Send Client Authentication Packet
authResp, err := mc.auth(authData, plugin)
if err != nil {
t.Fatal(err)
}
err = mc.writeHandshakeResponsePacket(authResp, plugin)
if err != nil {
t.Fatal(err)
}
// check written auth response
authRespStart := 4 + 4 + 4 + 1 + 23 + len(mc.cfg.User) + 1
authRespEnd := authRespStart + 1 + len(authResp)
writtenAuthRespLen := conn.written[authRespStart]
writtenAuthResp := conn.written[authRespStart+1 : authRespEnd]
expectedAuthResp := []byte{115, 101, 99, 114, 101, 116, 0}
if writtenAuthRespLen != 7 || !bytes.Equal(writtenAuthResp, expectedAuthResp) {
t.Fatalf("unexpected written auth response (%d bytes): %v", writtenAuthRespLen, writtenAuthResp)
}
conn.written = nil
// auth response
conn.data = []byte{
7, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, // OK
}
conn.maxReads = 1
// Handle response to auth packet
if err := mc.handleAuthResult(authData, plugin); err != nil {
t.Errorf("got error: %v", err)
}
}
func TestAuthFastCleartextPasswordEmpty(t *testing.T) {
conn, mc := newRWMockConn(1)
mc.cfg.User = "root"
mc.cfg.Passwd = ""
mc.cfg.AllowCleartextPasswords = true
authData := []byte{70, 114, 92, 94, 1, 38, 11, 116, 63, 114, 23, 101, 126,
103, 26, 95, 81, 17, 24, 21}
plugin := "mysql_clear_password"
// Send Client Authentication Packet
authResp, err := mc.auth(authData, plugin)
if err != nil {
t.Fatal(err)
}
err = mc.writeHandshakeResponsePacket(authResp, plugin)
if err != nil {
t.Fatal(err)
}
// check written auth response
authRespStart := 4 + 4 + 4 + 1 + 23 + len(mc.cfg.User) + 1
authRespEnd := authRespStart + 1 + len(authResp)
writtenAuthRespLen := conn.written[authRespStart]
writtenAuthResp := conn.written[authRespStart+1 : authRespEnd]
expectedAuthResp := []byte{0}
if writtenAuthRespLen != 1 || !bytes.Equal(writtenAuthResp, expectedAuthResp) {
t.Fatalf("unexpected written auth response (%d bytes): %v", writtenAuthRespLen, writtenAuthResp)
}
conn.written = nil
// auth response
conn.data = []byte{
7, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, // OK
}
conn.maxReads = 1
// Handle response to auth packet
if err := mc.handleAuthResult(authData, plugin); err != nil {
t.Errorf("got error: %v", err)
}
}
func TestAuthFastNativePasswordNotAllowed(t *testing.T) {
_, mc := newRWMockConn(1)
mc.cfg.User = "root"
mc.cfg.Passwd = "secret"
mc.cfg.AllowNativePasswords = false
authData := []byte{70, 114, 92, 94, 1, 38, 11, 116, 63, 114, 23, 101, 126,
103, 26, 95, 81, 17, 24, 21}
plugin := "mysql_native_password"
// Send Client Authentication Packet
_, err := mc.auth(authData, plugin)
if err != ErrNativePassword {
t.Errorf("expected ErrNativePassword, got %v", err)
}
}
func TestAuthFastNativePassword(t *testing.T) {
conn, mc := newRWMockConn(1)
mc.cfg.User = "root"
mc.cfg.Passwd = "secret"
authData := []byte{70, 114, 92, 94, 1, 38, 11, 116, 63, 114, 23, 101, 126,
103, 26, 95, 81, 17, 24, 21}
plugin := "mysql_native_password"
// Send Client Authentication Packet
authResp, err := mc.auth(authData, plugin)
if err != nil {
t.Fatal(err)
}
err = mc.writeHandshakeResponsePacket(authResp, plugin)
if err != nil {
t.Fatal(err)
}
// check written auth response
authRespStart := 4 + 4 + 4 + 1 + 23 + len(mc.cfg.User) + 1
authRespEnd := authRespStart + 1 + len(authResp)
writtenAuthRespLen := conn.written[authRespStart]
writtenAuthResp := conn.written[authRespStart+1 : authRespEnd]
expectedAuthResp := []byte{53, 177, 140, 159, 251, 189, 127, 53, 109, 252,
172, 50, 211, 192, 240, 164, 26, 48, 207, 45}
if writtenAuthRespLen != 20 || !bytes.Equal(writtenAuthResp, expectedAuthResp) {
t.Fatalf("unexpected written auth response (%d bytes): %v", writtenAuthRespLen, writtenAuthResp)
}
conn.written = nil
// auth response
conn.data = []byte{
7, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, // OK
}
conn.maxReads = 1
// Handle response to auth packet
if err := mc.handleAuthResult(authData, plugin); err != nil {
t.Errorf("got error: %v", err)
}
}
func TestAuthFastNativePasswordEmpty(t *testing.T) {
conn, mc := newRWMockConn(1)
mc.cfg.User = "root"
mc.cfg.Passwd = ""
authData := []byte{70, 114, 92, 94, 1, 38, 11, 116, 63, 114, 23, 101, 126,
103, 26, 95, 81, 17, 24, 21}
plugin := "mysql_native_password"
// Send Client Authentication Packet
authResp, err := mc.auth(authData, plugin)
if err != nil {
t.Fatal(err)
}
err = mc.writeHandshakeResponsePacket(authResp, plugin)
if err != nil {
t.Fatal(err)
}
// check written auth response
authRespStart := 4 + 4 + 4 + 1 + 23 + len(mc.cfg.User) + 1
authRespEnd := authRespStart + 1 + len(authResp)
writtenAuthRespLen := conn.written[authRespStart]
writtenAuthResp := conn.written[authRespStart+1 : authRespEnd]
if writtenAuthRespLen != 0 {
t.Fatalf("unexpected written auth response (%d bytes): %v",
writtenAuthRespLen, writtenAuthResp)
}
conn.written = nil
// auth response
conn.data = []byte{
7, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, // OK
}
conn.maxReads = 1
// Handle response to auth packet
if err := mc.handleAuthResult(authData, plugin); err != nil {
t.Errorf("got error: %v", err)
}
}
func TestAuthFastSHA256PasswordEmpty(t *testing.T) {
conn, mc := newRWMockConn(1)
mc.cfg.User = "root"
mc.cfg.Passwd = ""
authData := []byte{6, 81, 96, 114, 14, 42, 50, 30, 76, 47, 1, 95, 126, 81,
62, 94, 83, 80, 52, 85}
plugin := "sha256_password"
// Send Client Authentication Packet
authResp, err := mc.auth(authData, plugin)
if err != nil {
t.Fatal(err)
}
err = mc.writeHandshakeResponsePacket(authResp, plugin)
if err != nil {
t.Fatal(err)
}
// check written auth response
authRespStart := 4 + 4 + 4 + 1 + 23 + len(mc.cfg.User) + 1
authRespEnd := authRespStart + 1 + len(authResp)
writtenAuthRespLen := conn.written[authRespStart]
writtenAuthResp := conn.written[authRespStart+1 : authRespEnd]
expectedAuthResp := []byte{0}
if writtenAuthRespLen != 1 || !bytes.Equal(writtenAuthResp, expectedAuthResp) {
t.Fatalf("unexpected written auth response (%d bytes): %v", writtenAuthRespLen, writtenAuthResp)
}
conn.written = nil
// auth response (pub key response)
conn.data = append([]byte{byte(1 + len(testPubKey)), 1, 0, 2, 1}, testPubKey...)
conn.queuedReplies = [][]byte{
// OK
{7, 0, 0, 4, 0, 0, 0, 2, 0, 0, 0},
}
conn.maxReads = 2
// Handle response to auth packet
if err := mc.handleAuthResult(authData, plugin); err != nil {
t.Errorf("got error: %v", err)
}
if !bytes.HasPrefix(conn.written, []byte{0, 1, 0, 3}) {
t.Errorf("unexpected written data: %v", conn.written)
}
}
func TestAuthFastSHA256PasswordRSA(t *testing.T) {
conn, mc := newRWMockConn(1)
mc.cfg.User = "root"
mc.cfg.Passwd = "secret"
authData := []byte{6, 81, 96, 114, 14, 42, 50, 30, 76, 47, 1, 95, 126, 81,
62, 94, 83, 80, 52, 85}
plugin := "sha256_password"
// Send Client Authentication Packet
authResp, err := mc.auth(authData, plugin)
if err != nil {
t.Fatal(err)
}
err = mc.writeHandshakeResponsePacket(authResp, plugin)
if err != nil {
t.Fatal(err)
}
// check written auth response
authRespStart := 4 + 4 + 4 + 1 + 23 + len(mc.cfg.User) + 1
authRespEnd := authRespStart + 1 + len(authResp)
writtenAuthRespLen := conn.written[authRespStart]
writtenAuthResp := conn.written[authRespStart+1 : authRespEnd]
expectedAuthResp := []byte{1}
if writtenAuthRespLen != 1 || !bytes.Equal(writtenAuthResp, expectedAuthResp) {
t.Fatalf("unexpected written auth response (%d bytes): %v", writtenAuthRespLen, writtenAuthResp)
}
conn.written = nil
// auth response (pub key response)
conn.data = append([]byte{byte(1 + len(testPubKey)), 1, 0, 2, 1}, testPubKey...)
conn.queuedReplies = [][]byte{
// OK
{7, 0, 0, 4, 0, 0, 0, 2, 0, 0, 0},
}
conn.maxReads = 2
// Handle response to auth packet
if err := mc.handleAuthResult(authData, plugin); err != nil {
t.Errorf("got error: %v", err)
}
if !bytes.HasPrefix(conn.written, []byte{0, 1, 0, 3}) {
t.Errorf("unexpected written data: %v", conn.written)
}
}
func TestAuthFastSHA256PasswordRSAWithKey(t *testing.T) {
conn, mc := newRWMockConn(1)
mc.cfg.User = "root"
mc.cfg.Passwd = "secret"
mc.cfg.pubKey = testPubKeyRSA
authData := []byte{6, 81, 96, 114, 14, 42, 50, 30, 76, 47, 1, 95, 126, 81,
62, 94, 83, 80, 52, 85}
plugin := "sha256_password"
// Send Client Authentication Packet
authResp, err := mc.auth(authData, plugin)
if err != nil {
t.Fatal(err)
}
err = mc.writeHandshakeResponsePacket(authResp, plugin)
if err != nil {
t.Fatal(err)
}
// auth response (OK)
conn.data = []byte{7, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0}
conn.maxReads = 1
// Handle response to auth packet
if err := mc.handleAuthResult(authData, plugin); err != nil {
t.Errorf("got error: %v", err)
}
}
func TestAuthFastSHA256PasswordSecure(t *testing.T) {
conn, mc := newRWMockConn(1)
mc.cfg.User = "root"
mc.cfg.Passwd = "secret"
// hack to make the caching_sha2_password plugin believe that the connection
// is secure
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}
plugin := "sha256_password"
// send Client Authentication Packet
authResp, err := mc.auth(authData, plugin)
if err != nil {
t.Fatal(err)
}
// unset TLS config to prevent the actual establishment of a TLS wrapper
mc.cfg.TLS = nil
err = mc.writeHandshakeResponsePacket(authResp, plugin)
if err != nil {
t.Fatal(err)
}
// check written auth response
authRespStart := 4 + 4 + 4 + 1 + 23 + len(mc.cfg.User) + 1
authRespEnd := authRespStart + 1 + len(authResp)
writtenAuthRespLen := conn.written[authRespStart]
writtenAuthResp := conn.written[authRespStart+1 : authRespEnd]
expectedAuthResp := []byte{115, 101, 99, 114, 101, 116, 0}
if writtenAuthRespLen != 7 || !bytes.Equal(writtenAuthResp, expectedAuthResp) {
t.Fatalf("unexpected written auth response (%d bytes): %v", writtenAuthRespLen, writtenAuthResp)
}
conn.written = nil
// auth response (OK)
conn.data = []byte{7, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0}
conn.maxReads = 1
// Handle response to auth packet
if err := mc.handleAuthResult(authData, plugin); err != nil {
t.Errorf("got error: %v", err)
}
if !bytes.Equal(conn.written, []byte{}) {
t.Errorf("unexpected written data: %v", conn.written)
}
}
func TestAuthSwitchCachingSHA256PasswordCached(t *testing.T) {
conn, mc := newRWMockConn(2)
mc.cfg.Passwd = "secret"
// auth switch request
conn.data = []byte{44, 0, 0, 2, 254, 99, 97, 99, 104, 105, 110, 103, 95,
115, 104, 97, 50, 95, 112, 97, 115, 115, 119, 111, 114, 100, 0, 101,
11, 26, 18, 94, 97, 22, 72, 2, 46, 70, 106, 29, 55, 45, 94, 76, 90, 84,
50, 0}
// auth response
conn.queuedReplies = [][]byte{
{7, 0, 0, 4, 0, 0, 0, 2, 0, 0, 0}, // OK
}
conn.maxReads = 3
authData := []byte{123, 87, 15, 84, 20, 58, 37, 121, 91, 117, 51, 24, 19,
47, 43, 9, 41, 112, 67, 110}
plugin := "mysql_native_password"
if err := mc.handleAuthResult(authData, plugin); err != nil {
t.Errorf("got error: %v", err)
}
expectedReply := []byte{
// 1. Packet: Hash
32, 0, 0, 3, 129, 93, 132, 95, 114, 48, 79, 215, 128, 62, 193, 118, 128,
54, 75, 208, 159, 252, 227, 215, 129, 15, 242, 97, 19, 159, 31, 20, 58,
153, 9, 130,
}
if !bytes.Equal(conn.written, expectedReply) {
t.Errorf("got unexpected data: %v", conn.written)
}
}
func TestAuthSwitchCachingSHA256PasswordEmpty(t *testing.T) {
conn, mc := newRWMockConn(2)
mc.cfg.Passwd = ""
// auth switch request
conn.data = []byte{44, 0, 0, 2, 254, 99, 97, 99, 104, 105, 110, 103, 95,
115, 104, 97, 50, 95, 112, 97, 115, 115, 119, 111, 114, 100, 0, 101,
11, 26, 18, 94, 97, 22, 72, 2, 46, 70, 106, 29, 55, 45, 94, 76, 90, 84,
50, 0}
// auth response
conn.queuedReplies = [][]byte{{7, 0, 0, 4, 0, 0, 0, 2, 0, 0, 0}}
conn.maxReads = 2
authData := []byte{123, 87, 15, 84, 20, 58, 37, 121, 91, 117, 51, 24, 19,
47, 43, 9, 41, 112, 67, 110}
plugin := "mysql_native_password"
if err := mc.handleAuthResult(authData, plugin); err != nil {
t.Errorf("got error: %v", err)
}
expectedReply := []byte{0, 0, 0, 3}
if !bytes.Equal(conn.written, expectedReply) {
t.Errorf("got unexpected data: %v", conn.written)
}
}
func TestAuthSwitchCachingSHA256PasswordFullRSA(t *testing.T) {
conn, mc := newRWMockConn(2)
mc.cfg.Passwd = "secret"
// auth switch request
conn.data = []byte{44, 0, 0, 2, 254, 99, 97, 99, 104, 105, 110, 103, 95,
115, 104, 97, 50, 95, 112, 97, 115, 115, 119, 111, 114, 100, 0, 101,
11, 26, 18, 94, 97, 22, 72, 2, 46, 70, 106, 29, 55, 45, 94, 76, 90, 84,
50, 0}
conn.queuedReplies = [][]byte{
// Perform Full Authentication
{2, 0, 0, 4, 1, 4},
// Pub Key Response
append([]byte{byte(1 + len(testPubKey)), 1, 0, 6, 1}, testPubKey...),
// OK
{7, 0, 0, 8, 0, 0, 0, 2, 0, 0, 0},
}
conn.maxReads = 4
authData := []byte{123, 87, 15, 84, 20, 58, 37, 121, 91, 117, 51, 24, 19,
47, 43, 9, 41, 112, 67, 110}
plugin := "mysql_native_password"
if err := mc.handleAuthResult(authData, plugin); err != nil {
t.Errorf("got error: %v", err)
}
expectedReplyPrefix := []byte{
// 1. Packet: Hash
32, 0, 0, 3, 129, 93, 132, 95, 114, 48, 79, 215, 128, 62, 193, 118, 128,
54, 75, 208, 159, 252, 227, 215, 129, 15, 242, 97, 19, 159, 31, 20, 58,
153, 9, 130,
// 2. Packet: Pub Key Request
1, 0, 0, 5, 2,
// 3. Packet: Encrypted Password
0, 1, 0, 7, // [changing bytes]
}
if !bytes.HasPrefix(conn.written, expectedReplyPrefix) {
t.Errorf("got unexpected data: %v", conn.written)
}
}
func TestAuthSwitchCachingSHA256PasswordFullRSAWithKey(t *testing.T) {
conn, mc := newRWMockConn(2)
mc.cfg.Passwd = "secret"
mc.cfg.pubKey = testPubKeyRSA
// auth switch request
conn.data = []byte{44, 0, 0, 2, 254, 99, 97, 99, 104, 105, 110, 103, 95,
115, 104, 97, 50, 95, 112, 97, 115, 115, 119, 111, 114, 100, 0, 101,
11, 26, 18, 94, 97, 22, 72, 2, 46, 70, 106, 29, 55, 45, 94, 76, 90, 84,
50, 0}
conn.queuedReplies = [][]byte{
// Perform Full Authentication
{2, 0, 0, 4, 1, 4},
// OK
{7, 0, 0, 6, 0, 0, 0, 2, 0, 0, 0},
}
conn.maxReads = 3
authData := []byte{123, 87, 15, 84, 20, 58, 37, 121, 91, 117, 51, 24, 19,
47, 43, 9, 41, 112, 67, 110}
plugin := "mysql_native_password"
if err := mc.handleAuthResult(authData, plugin); err != nil {
t.Errorf("got error: %v", err)
}
expectedReplyPrefix := []byte{
// 1. Packet: Hash
32, 0, 0, 3, 129, 93, 132, 95, 114, 48, 79, 215, 128, 62, 193, 118, 128,
54, 75, 208, 159, 252, 227, 215, 129, 15, 242, 97, 19, 159, 31, 20, 58,
153, 9, 130,
// 2. Packet: Encrypted Password
0, 1, 0, 5, // [changing bytes]
}
if !bytes.HasPrefix(conn.written, expectedReplyPrefix) {
t.Errorf("got unexpected data: %v", conn.written)
}
}
func TestAuthSwitchCachingSHA256PasswordFullSecure(t *testing.T) {
conn, mc := newRWMockConn(2)
mc.cfg.Passwd = "secret"
// Hack to make the caching_sha2_password plugin believe that the connection
// is secure
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,
115, 104, 97, 50, 95, 112, 97, 115, 115, 119, 111, 114, 100, 0, 101,
11, 26, 18, 94, 97, 22, 72, 2, 46, 70, 106, 29, 55, 45, 94, 76, 90, 84,
50, 0}
// auth response
conn.queuedReplies = [][]byte{
{2, 0, 0, 4, 1, 4}, // Perform Full Authentication
{7, 0, 0, 6, 0, 0, 0, 2, 0, 0, 0}, // OK
}
conn.maxReads = 3
authData := []byte{123, 87, 15, 84, 20, 58, 37, 121, 91, 117, 51, 24, 19,
47, 43, 9, 41, 112, 67, 110}
plugin := "mysql_native_password"
if err := mc.handleAuthResult(authData, plugin); err != nil {
t.Errorf("got error: %v", err)
}
expectedReply := []byte{
// 1. Packet: Hash
32, 0, 0, 3, 129, 93, 132, 95, 114, 48, 79, 215, 128, 62, 193, 118, 128,
54, 75, 208, 159, 252, 227, 215, 129, 15, 242, 97, 19, 159, 31, 20, 58,
153, 9, 130,
// 2. Packet: Cleartext password
7, 0, 0, 5, 115, 101, 99, 114, 101, 116, 0,
}
if !bytes.Equal(conn.written, expectedReply) {
t.Errorf("got unexpected data: %v", conn.written)
}
}
func TestAuthSwitchCleartextPasswordNotAllowed(t *testing.T) {
conn, mc := newRWMockConn(2)
conn.data = []byte{22, 0, 0, 2, 254, 109, 121, 115, 113, 108, 95, 99, 108,
101, 97, 114, 95, 112, 97, 115, 115, 119, 111, 114, 100, 0}
conn.maxReads = 1
authData := []byte{123, 87, 15, 84, 20, 58, 37, 121, 91, 117, 51, 24, 19,
47, 43, 9, 41, 112, 67, 110}
plugin := "mysql_native_password"
err := mc.handleAuthResult(authData, plugin)
if err != ErrCleartextPassword {
t.Errorf("expected ErrCleartextPassword, got %v", err)
}
}
func TestAuthSwitchCleartextPassword(t *testing.T) {
conn, mc := newRWMockConn(2)
mc.cfg.AllowCleartextPasswords = true
mc.cfg.Passwd = "secret"
// auth switch request
conn.data = []byte{22, 0, 0, 2, 254, 109, 121, 115, 113, 108, 95, 99, 108,
101, 97, 114, 95, 112, 97, 115, 115, 119, 111, 114, 100, 0}
// auth response
conn.queuedReplies = [][]byte{{7, 0, 0, 4, 0, 0, 0, 2, 0, 0, 0}}
conn.maxReads = 2
authData := []byte{123, 87, 15, 84, 20, 58, 37, 121, 91, 117, 51, 24, 19,
47, 43, 9, 41, 112, 67, 110}
plugin := "mysql_native_password"
if err := mc.handleAuthResult(authData, plugin); err != nil {
t.Errorf("got error: %v", err)
}
expectedReply := []byte{7, 0, 0, 3, 115, 101, 99, 114, 101, 116, 0}
if !bytes.Equal(conn.written, expectedReply) {
t.Errorf("got unexpected data: %v", conn.written)
}
}
func TestAuthSwitchCleartextPasswordEmpty(t *testing.T) {
conn, mc := newRWMockConn(2)
mc.cfg.AllowCleartextPasswords = true
mc.cfg.Passwd = ""
// auth switch request
conn.data = []byte{22, 0, 0, 2, 254, 109, 121, 115, 113, 108, 95, 99, 108,
101, 97, 114, 95, 112, 97, 115, 115, 119, 111, 114, 100, 0}
// auth response
conn.queuedReplies = [][]byte{{7, 0, 0, 4, 0, 0, 0, 2, 0, 0, 0}}
conn.maxReads = 2
authData := []byte{123, 87, 15, 84, 20, 58, 37, 121, 91, 117, 51, 24, 19,
47, 43, 9, 41, 112, 67, 110}
plugin := "mysql_native_password"
if err := mc.handleAuthResult(authData, plugin); err != nil {
t.Errorf("got error: %v", err)
}
expectedReply := []byte{1, 0, 0, 3, 0}
if !bytes.Equal(conn.written, expectedReply) {
t.Errorf("got unexpected data: %v", conn.written)
}
}
func TestAuthSwitchNativePasswordNotAllowed(t *testing.T) {
conn, mc := newRWMockConn(2)
mc.cfg.AllowNativePasswords = false
conn.data = []byte{44, 0, 0, 2, 254, 109, 121, 115, 113, 108, 95, 110, 97,
116, 105, 118, 101, 95, 112, 97, 115, 115, 119, 111, 114, 100, 0, 96,
71, 63, 8, 1, 58, 75, 12, 69, 95, 66, 60, 117, 31, 48, 31, 89, 39, 55,
31, 0}
conn.maxReads = 1
authData := []byte{96, 71, 63, 8, 1, 58, 75, 12, 69, 95, 66, 60, 117, 31,
48, 31, 89, 39, 55, 31}
plugin := "caching_sha2_password"
err := mc.handleAuthResult(authData, plugin)
if err != ErrNativePassword {
t.Errorf("expected ErrNativePassword, got %v", err)
}
}
func TestAuthSwitchNativePassword(t *testing.T) {
conn, mc := newRWMockConn(2)
mc.cfg.AllowNativePasswords = true
mc.cfg.Passwd = "secret"
// auth switch request
conn.data = []byte{44, 0, 0, 2, 254, 109, 121, 115, 113, 108, 95, 110, 97,
116, 105, 118, 101, 95, 112, 97, 115, 115, 119, 111, 114, 100, 0, 96,
71, 63, 8, 1, 58, 75, 12, 69, 95, 66, 60, 117, 31, 48, 31, 89, 39, 55,
31, 0}
// auth response
conn.queuedReplies = [][]byte{{7, 0, 0, 4, 0, 0, 0, 2, 0, 0, 0}}
conn.maxReads = 2
authData := []byte{96, 71, 63, 8, 1, 58, 75, 12, 69, 95, 66, 60, 117, 31,
48, 31, 89, 39, 55, 31}
plugin := "caching_sha2_password"
if err := mc.handleAuthResult(authData, plugin); err != nil {
t.Errorf("got error: %v", err)
}
expectedReply := []byte{20, 0, 0, 3, 202, 41, 195, 164, 34, 226, 49, 103,
21, 211, 167, 199, 227, 116, 8, 48, 57, 71, 149, 146}
if !bytes.Equal(conn.written, expectedReply) {
t.Errorf("got unexpected data: %v", conn.written)
}
}
func TestAuthSwitchNativePasswordEmpty(t *testing.T) {
conn, mc := newRWMockConn(2)
mc.cfg.AllowNativePasswords = true
mc.cfg.Passwd = ""
// auth switch request
conn.data = []byte{44, 0, 0, 2, 254, 109, 121, 115, 113, 108, 95, 110, 97,
116, 105, 118, 101, 95, 112, 97, 115, 115, 119, 111, 114, 100, 0, 96,
71, 63, 8, 1, 58, 75, 12, 69, 95, 66, 60, 117, 31, 48, 31, 89, 39, 55,
31, 0}
// auth response
conn.queuedReplies = [][]byte{{7, 0, 0, 4, 0, 0, 0, 2, 0, 0, 0}}
conn.maxReads = 2
authData := []byte{96, 71, 63, 8, 1, 58, 75, 12, 69, 95, 66, 60, 117, 31,
48, 31, 89, 39, 55, 31}
plugin := "caching_sha2_password"
if err := mc.handleAuthResult(authData, plugin); err != nil {
t.Errorf("got error: %v", err)
}
expectedReply := []byte{0, 0, 0, 3}
if !bytes.Equal(conn.written, expectedReply) {
t.Errorf("got unexpected data: %v", conn.written)
}
}
func TestAuthSwitchOldPasswordNotAllowed(t *testing.T) {
conn, mc := newRWMockConn(2)
conn.data = []byte{41, 0, 0, 2, 254, 109, 121, 115, 113, 108, 95, 111, 108,
100, 95, 112, 97, 115, 115, 119, 111, 114, 100, 0, 95, 84, 103, 43, 61,
49, 123, 61, 91, 50, 40, 113, 35, 84, 96, 101, 92, 123, 121, 107, 0}
conn.maxReads = 1
authData := []byte{95, 84, 103, 43, 61, 49, 123, 61, 91, 50, 40, 113, 35,
84, 96, 101, 92, 123, 121, 107}
plugin := "mysql_native_password"
err := mc.handleAuthResult(authData, plugin)
if err != ErrOldPassword {
t.Errorf("expected ErrOldPassword, got %v", err)
}
}
// Same to TestAuthSwitchOldPasswordNotAllowed, but use OldAuthSwitch request.
func TestOldAuthSwitchNotAllowed(t *testing.T) {
conn, mc := newRWMockConn(2)
// OldAuthSwitch request
conn.data = []byte{1, 0, 0, 2, 0xfe}
conn.maxReads = 1
authData := []byte{95, 84, 103, 43, 61, 49, 123, 61, 91, 50, 40, 113, 35,
84, 96, 101, 92, 123, 121, 107}
plugin := "mysql_native_password"
err := mc.handleAuthResult(authData, plugin)
if err != ErrOldPassword {
t.Errorf("expected ErrOldPassword, got %v", err)
}
}
func TestAuthSwitchOldPassword(t *testing.T) {
conn, mc := newRWMockConn(2)
mc.cfg.AllowOldPasswords = true
mc.cfg.Passwd = "secret"
// auth switch request
conn.data = []byte{41, 0, 0, 2, 254, 109, 121, 115, 113, 108, 95, 111, 108,
100, 95, 112, 97, 115, 115, 119, 111, 114, 100, 0, 95, 84, 103, 43, 61,
49, 123, 61, 91, 50, 40, 113, 35, 84, 96, 101, 92, 123, 121, 107, 0}
// auth response
conn.queuedReplies = [][]byte{{8, 0, 0, 4, 0, 0, 0, 2, 0, 0, 0, 0}}
conn.maxReads = 2
authData := []byte{95, 84, 103, 43, 61, 49, 123, 61, 91, 50, 40, 113, 35,
84, 96, 101, 92, 123, 121, 107}
plugin := "mysql_native_password"
if err := mc.handleAuthResult(authData, plugin); err != nil {
t.Errorf("got error: %v", err)
}
expectedReply := []byte{9, 0, 0, 3, 86, 83, 83, 79, 74, 78, 65, 66, 0}
if !bytes.Equal(conn.written, expectedReply) {
t.Errorf("got unexpected data: %v", conn.written)
}
}
// Same to TestAuthSwitchOldPassword, but use OldAuthSwitch request.
func TestOldAuthSwitch(t *testing.T) {
conn, mc := newRWMockConn(2)
mc.cfg.AllowOldPasswords = true
mc.cfg.Passwd = "secret"
// OldAuthSwitch request
conn.data = []byte{1, 0, 0, 2, 0xfe}
// auth response
conn.queuedReplies = [][]byte{{8, 0, 0, 4, 0, 0, 0, 2, 0, 0, 0, 0}}
conn.maxReads = 2
authData := []byte{95, 84, 103, 43, 61, 49, 123, 61, 91, 50, 40, 113, 35,
84, 96, 101, 92, 123, 121, 107}
plugin := "mysql_native_password"
if err := mc.handleAuthResult(authData, plugin); err != nil {
t.Errorf("got error: %v", err)
}
expectedReply := []byte{9, 0, 0, 3, 86, 83, 83, 79, 74, 78, 65, 66, 0}
if !bytes.Equal(conn.written, expectedReply) {
t.Errorf("got unexpected data: %v", conn.written)
}
}
func TestAuthSwitchOldPasswordEmpty(t *testing.T) {
conn, mc := newRWMockConn(2)
mc.cfg.AllowOldPasswords = true
mc.cfg.Passwd = ""
// auth switch request
conn.data = []byte{41, 0, 0, 2, 254, 109, 121, 115, 113, 108, 95, 111, 108,
100, 95, 112, 97, 115, 115, 119, 111, 114, 100, 0, 95, 84, 103, 43, 61,
49, 123, 61, 91, 50, 40, 113, 35, 84, 96, 101, 92, 123, 121, 107, 0}
// auth response
conn.queuedReplies = [][]byte{{8, 0, 0, 4, 0, 0, 0, 2, 0, 0, 0, 0}}
conn.maxReads = 2
authData := []byte{95, 84, 103, 43, 61, 49, 123, 61, 91, 50, 40, 113, 35,
84, 96, 101, 92, 123, 121, 107}
plugin := "mysql_native_password"
if err := mc.handleAuthResult(authData, plugin); err != nil {
t.Errorf("got error: %v", err)
}
expectedReply := []byte{0, 0, 0, 3}
if !bytes.Equal(conn.written, expectedReply) {
t.Errorf("got unexpected data: %v", conn.written)
}
}
// Same to TestAuthSwitchOldPasswordEmpty, but use OldAuthSwitch request.
func TestOldAuthSwitchPasswordEmpty(t *testing.T) {
conn, mc := newRWMockConn(2)
mc.cfg.AllowOldPasswords = true
mc.cfg.Passwd = ""
// OldAuthSwitch request.
conn.data = []byte{1, 0, 0, 2, 0xfe}
// auth response
conn.queuedReplies = [][]byte{{8, 0, 0, 4, 0, 0, 0, 2, 0, 0, 0, 0}}
conn.maxReads = 2
authData := []byte{95, 84, 103, 43, 61, 49, 123, 61, 91, 50, 40, 113, 35,
84, 96, 101, 92, 123, 121, 107}
plugin := "mysql_native_password"
if err := mc.handleAuthResult(authData, plugin); err != nil {
t.Errorf("got error: %v", err)
}
expectedReply := []byte{0, 0, 0, 3}
if !bytes.Equal(conn.written, expectedReply) {
t.Errorf("got unexpected data: %v", conn.written)
}
}
func TestAuthSwitchSHA256PasswordEmpty(t *testing.T) {
conn, mc := newRWMockConn(2)
mc.cfg.Passwd = ""
// auth switch request
conn.data = []byte{38, 0, 0, 2, 254, 115, 104, 97, 50, 53, 54, 95, 112, 97,
115, 115, 119, 111, 114, 100, 0, 78, 82, 62, 40, 100, 1, 59, 31, 44, 69,
33, 112, 8, 81, 51, 96, 65, 82, 16, 114, 0}
conn.queuedReplies = [][]byte{
// OK
{7, 0, 0, 4, 0, 0, 0, 2, 0, 0, 0},
}
conn.maxReads = 3
authData := []byte{123, 87, 15, 84, 20, 58, 37, 121, 91, 117, 51, 24, 19,
47, 43, 9, 41, 112, 67, 110}
plugin := "mysql_native_password"
if err := mc.handleAuthResult(authData, plugin); err != nil {
t.Errorf("got error: %v", err)
}
expectedReplyPrefix := []byte{
// 1. Packet: Empty Password
1, 0, 0, 3, 0,
}
if !bytes.HasPrefix(conn.written, expectedReplyPrefix) {
t.Errorf("got unexpected data: %v", conn.written)
}
}
func TestAuthSwitchSHA256PasswordRSA(t *testing.T) {
conn, mc := newRWMockConn(2)
mc.cfg.Passwd = "secret"
// auth switch request
conn.data = []byte{38, 0, 0, 2, 254, 115, 104, 97, 50, 53, 54, 95, 112, 97,
115, 115, 119, 111, 114, 100, 0, 78, 82, 62, 40, 100, 1, 59, 31, 44, 69,
33, 112, 8, 81, 51, 96, 65, 82, 16, 114, 0}
conn.queuedReplies = [][]byte{
// Pub Key Response
append([]byte{byte(1 + len(testPubKey)), 1, 0, 4, 1}, testPubKey...),
// OK
{7, 0, 0, 6, 0, 0, 0, 2, 0, 0, 0},
}
conn.maxReads = 3
authData := []byte{123, 87, 15, 84, 20, 58, 37, 121, 91, 117, 51, 24, 19,
47, 43, 9, 41, 112, 67, 110}
plugin := "mysql_native_password"
if err := mc.handleAuthResult(authData, plugin); err != nil {
t.Errorf("got error: %v", err)
}
expectedReplyPrefix := []byte{
// 1. Packet: Pub Key Request
1, 0, 0, 3, 1,
// 2. Packet: Encrypted Password
0, 1, 0, 5, // [changing bytes]
}
if !bytes.HasPrefix(conn.written, expectedReplyPrefix) {
t.Errorf("got unexpected data: %v", conn.written)
}
}
func TestAuthSwitchSHA256PasswordRSAWithKey(t *testing.T) {
conn, mc := newRWMockConn(2)
mc.cfg.Passwd = "secret"
mc.cfg.pubKey = testPubKeyRSA
// auth switch request
conn.data = []byte{38, 0, 0, 2, 254, 115, 104, 97, 50, 53, 54, 95, 112, 97,
115, 115, 119, 111, 114, 100, 0, 78, 82, 62, 40, 100, 1, 59, 31, 44, 69,
33, 112, 8, 81, 51, 96, 65, 82, 16, 114, 0}
conn.queuedReplies = [][]byte{
// OK
{7, 0, 0, 4, 0, 0, 0, 2, 0, 0, 0},
}
conn.maxReads = 2
authData := []byte{123, 87, 15, 84, 20, 58, 37, 121, 91, 117, 51, 24, 19,
47, 43, 9, 41, 112, 67, 110}
plugin := "mysql_native_password"
if err := mc.handleAuthResult(authData, plugin); err != nil {
t.Errorf("got error: %v", err)
}
expectedReplyPrefix := []byte{
// 1. Packet: Encrypted Password
0, 1, 0, 3, // [changing bytes]
}
if !bytes.HasPrefix(conn.written, expectedReplyPrefix) {
t.Errorf("got unexpected data: %v", conn.written)
}
}
func TestAuthSwitchSHA256PasswordSecure(t *testing.T) {
conn, mc := newRWMockConn(2)
mc.cfg.Passwd = "secret"
// Hack to make the caching_sha2_password plugin believe that the connection
// is secure
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,
115, 115, 119, 111, 114, 100, 0, 78, 82, 62, 40, 100, 1, 59, 31, 44, 69,
33, 112, 8, 81, 51, 96, 65, 82, 16, 114, 0}
conn.queuedReplies = [][]byte{
// OK
{7, 0, 0, 4, 0, 0, 0, 2, 0, 0, 0},
}
conn.maxReads = 2
authData := []byte{123, 87, 15, 84, 20, 58, 37, 121, 91, 117, 51, 24, 19,
47, 43, 9, 41, 112, 67, 110}
plugin := "mysql_native_password"
if err := mc.handleAuthResult(authData, plugin); err != nil {
t.Errorf("got error: %v", err)
}
expectedReplyPrefix := []byte{
// 1. Packet: Cleartext Password
7, 0, 0, 3, 115, 101, 99, 114, 101, 116, 0,
}
if !bytes.Equal(conn.written, expectedReplyPrefix) {
t.Errorf("got unexpected data: %v", conn.written)
}
}
// Derived from https://github.com/MariaDB/server/blob/6b2287fff23fbdc362499501c562f01d0d2db52e/plugin/auth_ed25519/ed25519-t.c
func TestEd25519Auth(t *testing.T) {
conn, mc := newRWMockConn(1)
mc.cfg.User = "root"
mc.cfg.Passwd = "foobar"
authData := []byte("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")
plugin := "client_ed25519"
// Send Client Authentication Packet
authResp, err := mc.auth(authData, plugin)
if err != nil {
t.Fatal(err)
}
err = mc.writeHandshakeResponsePacket(authResp, plugin)
if err != nil {
t.Fatal(err)
}
// check written auth response
authRespStart := 4 + 4 + 4 + 1 + 23 + len(mc.cfg.User) + 1
authRespEnd := authRespStart + 1 + len(authResp)
writtenAuthRespLen := conn.written[authRespStart]
writtenAuthResp := conn.written[authRespStart+1 : authRespEnd]
expectedAuthResp := []byte{
232, 61, 201, 63, 67, 63, 51, 53, 86, 73, 238, 35, 170, 117, 146,
214, 26, 17, 35, 9, 8, 132, 245, 141, 48, 99, 66, 58, 36, 228, 48,
84, 115, 254, 187, 168, 88, 162, 249, 57, 35, 85, 79, 238, 167, 106,
68, 117, 56, 135, 171, 47, 20, 14, 133, 79, 15, 229, 124, 160, 176,
100, 138, 14,
}
if writtenAuthRespLen != 64 {
t.Fatalf("expected 64 bytes from client, got %d", writtenAuthRespLen)
}
if !bytes.Equal(writtenAuthResp, expectedAuthResp) {
t.Fatalf("auth response did not match expected value:\n%v\n%v", writtenAuthResp, expectedAuthResp)
}
conn.written = nil
// auth response
conn.data = []byte{
7, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, // OK
}
conn.maxReads = 1
// Handle response to auth packet
if err := mc.handleAuthResult(authData, plugin); err != nil {
t.Errorf("got error: %v", err)
}
}
// 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/.
package mysql
import (
"bytes"
"context"
"database/sql"
"database/sql/driver"
"fmt"
"math"
"runtime"
"strings"
"sync"
"sync/atomic"
"testing"
"time"
)
type TB testing.B
func (tb *TB) check(err error) {
if err != nil {
tb.Fatal(err)
}
}
func (tb *TB) checkDB(db *sql.DB, err error) *sql.DB {
tb.check(err)
return db
}
func (tb *TB) checkRows(rows *sql.Rows, err error) *sql.Rows {
tb.check(err)
return rows
}
func (tb *TB) checkStmt(stmt *sql.Stmt, err error) *sql.Stmt {
tb.check(err)
return stmt
}
func initDB(b *testing.B, queries ...string) *sql.DB {
tb := (*TB)(b)
db := tb.checkDB(sql.Open(driverNameTest, dsn))
for _, query := range queries {
if _, err := db.Exec(query); err != nil {
b.Fatalf("error on %q: %v", query, err)
}
}
return db
}
const concurrencyLevel = 10
func BenchmarkQuery(b *testing.B) {
tb := (*TB)(b)
b.StopTimer()
b.ReportAllocs()
db := initDB(b,
"DROP TABLE IF EXISTS foo",
"CREATE TABLE foo (id INT PRIMARY KEY, val CHAR(50))",
`INSERT INTO foo VALUES (1, "one")`,
`INSERT INTO foo VALUES (2, "two")`,
)
db.SetMaxIdleConns(concurrencyLevel)
defer db.Close()
stmt := tb.checkStmt(db.Prepare("SELECT val FROM foo WHERE id=?"))
defer stmt.Close()
remain := int64(b.N)
var wg sync.WaitGroup
wg.Add(concurrencyLevel)
defer wg.Wait()
b.StartTimer()
for i := 0; i < concurrencyLevel; i++ {
go func() {
for {
if atomic.AddInt64(&remain, -1) < 0 {
wg.Done()
return
}
var got string
tb.check(stmt.QueryRow(1).Scan(&got))
if got != "one" {
b.Errorf("query = %q; want one", got)
wg.Done()
return
}
}
}()
}
}
func BenchmarkExec(b *testing.B) {
tb := (*TB)(b)
b.StopTimer()
b.ReportAllocs()
db := tb.checkDB(sql.Open(driverNameTest, dsn))
db.SetMaxIdleConns(concurrencyLevel)
defer db.Close()
stmt := tb.checkStmt(db.Prepare("DO 1"))
defer stmt.Close()
remain := int64(b.N)
var wg sync.WaitGroup
wg.Add(concurrencyLevel)
defer wg.Wait()
b.StartTimer()
for i := 0; i < concurrencyLevel; i++ {
go func() {
for {
if atomic.AddInt64(&remain, -1) < 0 {
wg.Done()
return
}
if _, err := stmt.Exec(); err != nil {
b.Logf("stmt.Exec failed: %v", err)
b.Fail()
}
}
}()
}
}
// data, but no db writes
var roundtripSample []byte
func initRoundtripBenchmarks() ([]byte, int, int) {
if roundtripSample == nil {
roundtripSample = []byte(strings.Repeat("0123456789abcdef", 1024*1024))
}
return roundtripSample, 16, len(roundtripSample)
}
func BenchmarkRoundtripTxt(b *testing.B) {
b.StopTimer()
sample, min, max := initRoundtripBenchmarks()
sampleString := string(sample)
b.ReportAllocs()
tb := (*TB)(b)
db := tb.checkDB(sql.Open(driverNameTest, dsn))
defer db.Close()
b.StartTimer()
var result string
for i := 0; i < b.N; i++ {
length := min + i
if length > max {
length = max
}
test := sampleString[0:length]
rows := tb.checkRows(db.Query(`SELECT "` + test + `"`))
if !rows.Next() {
rows.Close()
b.Fatalf("crashed")
}
err := rows.Scan(&result)
if err != nil {
rows.Close()
b.Fatalf("crashed")
}
if result != test {
rows.Close()
b.Errorf("mismatch")
}
rows.Close()
}
}
func BenchmarkRoundtripBin(b *testing.B) {
b.StopTimer()
sample, min, max := initRoundtripBenchmarks()
b.ReportAllocs()
tb := (*TB)(b)
db := tb.checkDB(sql.Open(driverNameTest, dsn))
defer db.Close()
stmt := tb.checkStmt(db.Prepare("SELECT ?"))
defer stmt.Close()
b.StartTimer()
var result sql.RawBytes
for i := 0; i < b.N; i++ {
length := min + i
if length > max {
length = max
}
test := sample[0:length]
rows := tb.checkRows(stmt.Query(test))
if !rows.Next() {
rows.Close()
b.Fatalf("crashed")
}
err := rows.Scan(&result)
if err != nil {
rows.Close()
b.Fatalf("crashed")
}
if !bytes.Equal(result, test) {
rows.Close()
b.Errorf("mismatch")
}
rows.Close()
}
}
func BenchmarkInterpolation(b *testing.B) {
mc := &mysqlConn{
cfg: &Config{
InterpolateParams: true,
Loc: time.UTC,
},
maxAllowedPacket: maxPacketSize,
maxWriteSize: maxPacketSize - 1,
buf: newBuffer(nil),
}
args := []driver.Value{
int64(42424242),
float64(math.Pi),
false,
time.Unix(1423411542, 807015000),
[]byte("bytes containing special chars ' \" \a \x00"),
"string containing special chars ' \" \a \x00",
}
q := "SELECT ?, ?, ?, ?, ?, ?"
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := mc.interpolateParams(q, args)
if err != nil {
b.Fatal(err)
}
}
}
func benchmarkQueryContext(b *testing.B, db *sql.DB, p int) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
db.SetMaxIdleConns(p * runtime.GOMAXPROCS(0))
tb := (*TB)(b)
stmt := tb.checkStmt(db.PrepareContext(ctx, "SELECT val FROM foo WHERE id=?"))
defer stmt.Close()
b.SetParallelism(p)
b.ReportAllocs()
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
var got string
for pb.Next() {
tb.check(stmt.QueryRow(1).Scan(&got))
if got != "one" {
b.Fatalf("query = %q; want one", got)
}
}
})
}
func BenchmarkQueryContext(b *testing.B) {
db := initDB(b,
"DROP TABLE IF EXISTS foo",
"CREATE TABLE foo (id INT PRIMARY KEY, val CHAR(50))",
`INSERT INTO foo VALUES (1, "one")`,
`INSERT INTO foo VALUES (2, "two")`,
)
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)
})
}
}
func benchmarkExecContext(b *testing.B, db *sql.DB, p int) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
db.SetMaxIdleConns(p * runtime.GOMAXPROCS(0))
tb := (*TB)(b)
stmt := tb.checkStmt(db.PrepareContext(ctx, "DO 1"))
defer stmt.Close()
b.SetParallelism(p)
b.ReportAllocs()
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
if _, err := stmt.ExecContext(ctx); err != nil {
b.Fatal(err)
}
}
})
}
func BenchmarkExecContext(b *testing.B) {
db := initDB(b,
"DROP TABLE IF EXISTS foo",
"CREATE TABLE foo (id INT PRIMARY KEY, val CHAR(50))",
`INSERT INTO foo VALUES (1, "one")`,
`INSERT INTO foo VALUES (2, "two")`,
)
defer db.Close()
for _, p := range []int{1, 2, 3, 4} {
b.Run(fmt.Sprintf("%d", p), func(b *testing.B) {
benchmarkExecContext(b, db, p)
})
}
}
// BenchmarkQueryRawBytes benchmarks fetching 100 blobs using sql.RawBytes.
// "size=" means size of each blobs.
func BenchmarkQueryRawBytes(b *testing.B) {
var sizes []int = []int{100, 1000, 2000, 4000, 8000, 12000, 16000, 32000, 64000, 256000}
db := initDB(b,
"DROP TABLE IF EXISTS bench_rawbytes",
"CREATE TABLE bench_rawbytes (id INT PRIMARY KEY, val LONGBLOB)",
)
defer db.Close()
blob := make([]byte, sizes[len(sizes)-1])
for i := range blob {
blob[i] = 42
}
for i := 0; i < 100; i++ {
_, err := db.Exec("INSERT INTO bench_rawbytes VALUES (?, ?)", i, blob)
if err != nil {
b.Fatal(err)
}
}
for _, s := range sizes {
b.Run(fmt.Sprintf("size=%v", s), func(b *testing.B) {
db.SetMaxIdleConns(0)
db.SetMaxIdleConns(1)
b.ReportAllocs()
b.ResetTimer()
for j := 0; j < b.N; j++ {
rows, err := db.Query("SELECT LEFT(val, ?) as v FROM bench_rawbytes", s)
if err != nil {
b.Fatal(err)
}
nrows := 0
for rows.Next() {
var buf sql.RawBytes
err := rows.Scan(&buf)
if err != nil {
b.Fatal(err)
}
if len(buf) != s {
b.Fatalf("size mismatch: expected %v, got %v", s, len(buf))
}
nrows++
}
rows.Close()
if nrows != 100 {
b.Fatalf("numbers of rows mismatch: expected %v, got %v", 100, nrows)
}
}
})
}
}
// BenchmarkReceiveMassiveRows measures performance of receiving large number of rows.
func BenchmarkReceiveMassiveRows(b *testing.B) {
// Setup -- prepare 10000 rows.
db := initDB(b,
"DROP TABLE IF EXISTS foo",
"CREATE TABLE foo (id INT PRIMARY KEY, val TEXT)")
defer db.Close()
sval := strings.Repeat("x", 50)
stmt, err := db.Prepare(`INSERT INTO foo (id, val) VALUES (?, ?)` + strings.Repeat(",(?,?)", 99))
if err != nil {
b.Errorf("failed to prepare query: %v", err)
return
}
for i := 0; i < 10000; i += 100 {
args := make([]any, 200)
for j := 0; j < 100; j++ {
args[j*2] = i + j
args[j*2+1] = sval
}
_, err := stmt.Exec(args...)
if err != nil {
b.Error(err)
return
}
}
stmt.Close()
// Use b.Run() to skip expensive setup.
b.Run("query", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
rows, err := db.Query(`SELECT id, val FROM foo`)
if err != nil {
b.Errorf("failed to select: %v", err)
return
}
for rows.Next() {
var i int
var s sql.RawBytes
err = rows.Scan(&i, &s)
if err != nil {
b.Errorf("failed to scan: %v", err)
_ = rows.Close()
return
}
}
if err = rows.Err(); err != nil {
b.Errorf("failed to read rows: %v", err)
}
_ = rows.Close()
}
})
}
// 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/.
package mysql
import (
"io"
"net"
"time"
)
const defaultBufSize = 4096
const maxCachedBufSize = 256 * 1024
// A buffer which is used for both reading and writing.
// This is possible since communication on each connection is synchronous.
// In other words, we can't write and read simultaneously on the same connection.
// The buffer is similar to bufio.Reader / Writer but zero-copy-ish
// Also highly optimized for this particular use case.
// This buffer is backed by two byte slices in a double-buffering scheme
type buffer struct {
buf []byte // buf is a byte buffer who's length and capacity are equal.
nc net.Conn
idx int
length int
timeout time.Duration
dbuf [2][]byte // dbuf is an array with the two byte slices that back this buffer
flipcnt uint // flipccnt is the current buffer counter for double-buffering
}
// newBuffer allocates and returns a new buffer.
func newBuffer(nc net.Conn) buffer {
fg := make([]byte, defaultBufSize)
return buffer{
buf: fg,
nc: nc,
dbuf: [2][]byte{fg, nil},
}
}
// flip replaces the active buffer with the background buffer
// this is a delayed flip that simply increases the buffer counter;
// the actual flip will be performed the next time we call `buffer.fill`
func (b *buffer) flip() {
b.flipcnt += 1
}
// fill reads into the buffer until at least _need_ bytes are in it
func (b *buffer) fill(need int) error {
n := b.length
// fill data into its double-buffering target: if we've called
// flip on this buffer, we'll be copying to the background buffer,
// and then filling it with network data; otherwise we'll just move
// the contents of the current buffer to the front before filling it
dest := b.dbuf[b.flipcnt&1]
// grow buffer if necessary to fit the whole packet.
if need > len(dest) {
// Round up to the next multiple of the default size
dest = make([]byte, ((need/defaultBufSize)+1)*defaultBufSize)
// if the allocated buffer is not too large, move it to backing storage
// to prevent extra allocations on applications that perform large reads
if len(dest) <= maxCachedBufSize {
b.dbuf[b.flipcnt&1] = dest
}
}
// if we're filling the fg buffer, move the existing data to the start of it.
// if we're filling the bg buffer, copy over the data
if n > 0 {
copy(dest[:n], b.buf[b.idx:])
}
b.buf = dest
b.idx = 0
for {
if b.timeout > 0 {
if err := b.nc.SetReadDeadline(time.Now().Add(b.timeout)); err != nil {
return err
}
}
nn, err := b.nc.Read(b.buf[n:])
n += nn
switch err {
case nil:
if n < need {
continue
}
b.length = n
return nil
case io.EOF:
if n >= need {
b.length = n
return nil
}
return io.ErrUnexpectedEOF
default:
return err
}
}
}
// returns next N bytes from buffer.
// The returned slice is only guaranteed to be valid until the next read
func (b *buffer) readNext(need int) ([]byte, error) {
if b.length < need {
// refill
if err := b.fill(need); err != nil {
return nil, err
}
}
offset := b.idx
b.idx += need
b.length -= need
return b.buf[offset:b.idx], nil
}
// takeBuffer returns a buffer with the requested size.
// If possible, a slice from the existing buffer is returned.
// Otherwise a bigger buffer is made.
// Only one buffer (total) can be used at a time.
func (b *buffer) takeBuffer(length int) ([]byte, error) {
if b.length > 0 {
return nil, ErrBusyBuffer
}
// test (cheap) general case first
if length <= cap(b.buf) {
return b.buf[:length], nil
}
if length < maxPacketSize {
b.buf = make([]byte, length)
return b.buf, nil
}
// buffer is larger than we want to store.
return make([]byte, length), nil
}
// takeSmallBuffer is shortcut which can be used if length is
// known to be smaller than defaultBufSize.
// Only one buffer (total) can be used at a time.
func (b *buffer) takeSmallBuffer(length int) ([]byte, error) {
if b.length > 0 {
return nil, ErrBusyBuffer
}
return b.buf[:length], nil
}
// takeCompleteBuffer returns the complete existing buffer.
// This can be used if the necessary buffer size is unknown.
// cap and len of the returned buffer will be equal.
// Only one buffer (total) can be used at a time.
func (b *buffer) takeCompleteBuffer() ([]byte, error) {
if b.length > 0 {
return nil, ErrBusyBuffer
}
return b.buf, nil
}
// store stores buf, an updated buffer, if its suitable to do so.
func (b *buffer) store(buf []byte) error {
if b.length > 0 {
return ErrBusyBuffer
} else if cap(buf) <= maxPacketSize && cap(buf) > cap(b.buf) {
b.buf = buf[:cap(buf)]
}
return nil
}
// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
//
// Copyright 2014 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/.
package mysql
const defaultCollation = "utf8mb4_general_ci"
const binaryCollationID = 63
// 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
//
// Handshake packet have only 1 byte for collation_id. So we can't use collations with ID > 255.
//
// ucs2, utf16, and utf32 can't be used for connection charset.
// https://dev.mysql.com/doc/refman/5.7/en/charset-connection.html#charset-connection-impermissible-client-charset
// They are commented out to reduce this map.
var collations = map[string]byte{
"big5_chinese_ci": 1,
"latin2_czech_cs": 2,
"dec8_swedish_ci": 3,
"cp850_general_ci": 4,
"latin1_german1_ci": 5,
"hp8_english_ci": 6,
"koi8r_general_ci": 7,
"latin1_swedish_ci": 8,
"latin2_general_ci": 9,
"swe7_swedish_ci": 10,
"ascii_general_ci": 11,
"ujis_japanese_ci": 12,
"sjis_japanese_ci": 13,
"cp1251_bulgarian_ci": 14,
"latin1_danish_ci": 15,
"hebrew_general_ci": 16,
"tis620_thai_ci": 18,
"euckr_korean_ci": 19,
"latin7_estonian_cs": 20,
"latin2_hungarian_ci": 21,
"koi8u_general_ci": 22,
"cp1251_ukrainian_ci": 23,
"gb2312_chinese_ci": 24,
"greek_general_ci": 25,
"cp1250_general_ci": 26,
"latin2_croatian_ci": 27,
"gbk_chinese_ci": 28,
"cp1257_lithuanian_ci": 29,
"latin5_turkish_ci": 30,
"latin1_german2_ci": 31,
"armscii8_general_ci": 32,
"utf8_general_ci": 33,
"cp1250_czech_cs": 34,
//"ucs2_general_ci": 35,
"cp866_general_ci": 36,
"keybcs2_general_ci": 37,
"macce_general_ci": 38,
"macroman_general_ci": 39,
"cp852_general_ci": 40,
"latin7_general_ci": 41,
"latin7_general_cs": 42,
"macce_bin": 43,
"cp1250_croatian_ci": 44,
"utf8mb4_general_ci": 45,
"utf8mb4_bin": 46,
"latin1_bin": 47,
"latin1_general_ci": 48,
"latin1_general_cs": 49,
"cp1251_bin": 50,
"cp1251_general_ci": 51,
"cp1251_general_cs": 52,
"macroman_bin": 53,
//"utf16_general_ci": 54,
//"utf16_bin": 55,
//"utf16le_general_ci": 56,
"cp1256_general_ci": 57,
"cp1257_bin": 58,
"cp1257_general_ci": 59,
//"utf32_general_ci": 60,
//"utf32_bin": 61,
//"utf16le_bin": 62,
"binary": 63,
"armscii8_bin": 64,
"ascii_bin": 65,
"cp1250_bin": 66,
"cp1256_bin": 67,
"cp866_bin": 68,
"dec8_bin": 69,
"greek_bin": 70,
"hebrew_bin": 71,
"hp8_bin": 72,
"keybcs2_bin": 73,
"koi8r_bin": 74,
"koi8u_bin": 75,
"utf8_tolower_ci": 76,
"latin2_bin": 77,
"latin5_bin": 78,
"latin7_bin": 79,
"cp850_bin": 80,
"cp852_bin": 81,
"swe7_bin": 82,
"utf8_bin": 83,
"big5_bin": 84,
"euckr_bin": 85,
"gb2312_bin": 86,
"gbk_bin": 87,
"sjis_bin": 88,
"tis620_bin": 89,
//"ucs2_bin": 90,
"ujis_bin": 91,
"geostd8_general_ci": 92,
"geostd8_bin": 93,
"latin1_spanish_ci": 94,
"cp932_japanese_ci": 95,
"cp932_bin": 96,
"eucjpms_japanese_ci": 97,
"eucjpms_bin": 98,
"cp1250_polish_ci": 99,
//"utf16_unicode_ci": 101,
//"utf16_icelandic_ci": 102,
//"utf16_latvian_ci": 103,
//"utf16_romanian_ci": 104,
//"utf16_slovenian_ci": 105,
//"utf16_polish_ci": 106,
//"utf16_estonian_ci": 107,
//"utf16_spanish_ci": 108,
//"utf16_swedish_ci": 109,
//"utf16_turkish_ci": 110,
//"utf16_czech_ci": 111,
//"utf16_danish_ci": 112,
//"utf16_lithuanian_ci": 113,
//"utf16_slovak_ci": 114,
//"utf16_spanish2_ci": 115,
//"utf16_roman_ci": 116,
//"utf16_persian_ci": 117,
//"utf16_esperanto_ci": 118,
//"utf16_hungarian_ci": 119,
//"utf16_sinhala_ci": 120,
//"utf16_german2_ci": 121,
//"utf16_croatian_ci": 122,
//"utf16_unicode_520_ci": 123,
//"utf16_vietnamese_ci": 124,
//"ucs2_unicode_ci": 128,
//"ucs2_icelandic_ci": 129,
//"ucs2_latvian_ci": 130,
//"ucs2_romanian_ci": 131,
//"ucs2_slovenian_ci": 132,
//"ucs2_polish_ci": 133,
//"ucs2_estonian_ci": 134,
//"ucs2_spanish_ci": 135,
//"ucs2_swedish_ci": 136,
//"ucs2_turkish_ci": 137,
//"ucs2_czech_ci": 138,
//"ucs2_danish_ci": 139,
//"ucs2_lithuanian_ci": 140,
//"ucs2_slovak_ci": 141,
//"ucs2_spanish2_ci": 142,
//"ucs2_roman_ci": 143,
//"ucs2_persian_ci": 144,
//"ucs2_esperanto_ci": 145,
//"ucs2_hungarian_ci": 146,
//"ucs2_sinhala_ci": 147,
//"ucs2_german2_ci": 148,
//"ucs2_croatian_ci": 149,
//"ucs2_unicode_520_ci": 150,
//"ucs2_vietnamese_ci": 151,
//"ucs2_general_mysql500_ci": 159,
//"utf32_unicode_ci": 160,
//"utf32_icelandic_ci": 161,
//"utf32_latvian_ci": 162,
//"utf32_romanian_ci": 163,
//"utf32_slovenian_ci": 164,
//"utf32_polish_ci": 165,
//"utf32_estonian_ci": 166,
//"utf32_spanish_ci": 167,
//"utf32_swedish_ci": 168,
//"utf32_turkish_ci": 169,
//"utf32_czech_ci": 170,
//"utf32_danish_ci": 171,
//"utf32_lithuanian_ci": 172,
//"utf32_slovak_ci": 173,
//"utf32_spanish2_ci": 174,
//"utf32_roman_ci": 175,
//"utf32_persian_ci": 176,
//"utf32_esperanto_ci": 177,
//"utf32_hungarian_ci": 178,
//"utf32_sinhala_ci": 179,
//"utf32_german2_ci": 180,
//"utf32_croatian_ci": 181,
//"utf32_unicode_520_ci": 182,
//"utf32_vietnamese_ci": 183,
"utf8_unicode_ci": 192,
"utf8_icelandic_ci": 193,
"utf8_latvian_ci": 194,
"utf8_romanian_ci": 195,
"utf8_slovenian_ci": 196,
"utf8_polish_ci": 197,
"utf8_estonian_ci": 198,
"utf8_spanish_ci": 199,
"utf8_swedish_ci": 200,
"utf8_turkish_ci": 201,
"utf8_czech_ci": 202,
"utf8_danish_ci": 203,
"utf8_lithuanian_ci": 204,
"utf8_slovak_ci": 205,
"utf8_spanish2_ci": 206,
"utf8_roman_ci": 207,
"utf8_persian_ci": 208,
"utf8_esperanto_ci": 209,
"utf8_hungarian_ci": 210,
"utf8_sinhala_ci": 211,
"utf8_german2_ci": 212,
"utf8_croatian_ci": 213,
"utf8_unicode_520_ci": 214,
"utf8_vietnamese_ci": 215,
"utf8_general_mysql500_ci": 223,
"utf8mb4_unicode_ci": 224,
"utf8mb4_icelandic_ci": 225,
"utf8mb4_latvian_ci": 226,
"utf8mb4_romanian_ci": 227,
"utf8mb4_slovenian_ci": 228,
"utf8mb4_polish_ci": 229,
"utf8mb4_estonian_ci": 230,
"utf8mb4_spanish_ci": 231,
"utf8mb4_swedish_ci": 232,
"utf8mb4_turkish_ci": 233,
"utf8mb4_czech_ci": 234,
"utf8mb4_danish_ci": 235,
"utf8mb4_lithuanian_ci": 236,
"utf8mb4_slovak_ci": 237,
"utf8mb4_spanish2_ci": 238,
"utf8mb4_roman_ci": 239,
"utf8mb4_persian_ci": 240,
"utf8mb4_esperanto_ci": 241,
"utf8mb4_hungarian_ci": 242,
"utf8mb4_sinhala_ci": 243,
"utf8mb4_german2_ci": 244,
"utf8mb4_croatian_ci": 245,
"utf8mb4_unicode_520_ci": 246,
"utf8mb4_vietnamese_ci": 247,
"gb18030_chinese_ci": 248,
"gb18030_bin": 249,
"gb18030_unicode_520_ci": 250,
"utf8mb4_0900_ai_ci": 255,
}
// 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,
"sjis_japanese_ci": true,
"gbk_chinese_ci": true,
"big5_bin": true,
"gb2312_bin": true,
"gbk_bin": true,
"sjis_bin": true,
"cp932_japanese_ci": true,
"cp932_bin": true,
"gb18030_chinese_ci": true,
"gb18030_bin": true,
"gb18030_unicode_520_ci": true,
}
// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
//
// Copyright 2019 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 linux || darwin || dragonfly || freebsd || netbsd || openbsd || solaris || illumos
// +build linux darwin dragonfly freebsd netbsd openbsd solaris illumos
package mysql
import (
"errors"
"io"
"net"
"syscall"
)
var errUnexpectedRead = errors.New("unexpected read from socket")
func connCheck(conn net.Conn) error {
var sysErr error
sysConn, ok := conn.(syscall.Conn)
if !ok {
return nil
}
rawConn, err := sysConn.SyscallConn()
if err != nil {
return err
}
err = rawConn.Read(func(fd uintptr) bool {
var buf [1]byte
n, err := syscall.Read(int(fd), buf[:])
switch {
case n == 0 && err == nil:
sysErr = io.EOF
case n > 0:
sysErr = errUnexpectedRead
case err == syscall.EAGAIN || err == syscall.EWOULDBLOCK:
sysErr = nil
default:
sysErr = err
}
return true
})
if err != nil {
return err
}
return sysErr
}
// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
//
// Copyright 2019 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 !linux && !darwin && !dragonfly && !freebsd && !netbsd && !openbsd && !solaris && !illumos
// +build !linux,!darwin,!dragonfly,!freebsd,!netbsd,!openbsd,!solaris,!illumos
package mysql
import "net"
func connCheck(conn net.Conn) error {
return nil
}
// 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/.
//go:build linux || darwin || dragonfly || freebsd || netbsd || openbsd || solaris || illumos
// +build linux darwin dragonfly freebsd netbsd openbsd solaris illumos
package mysql
import (
"testing"
"time"
)
func TestStaleConnectionChecks(t *testing.T) {
runTestsParallel(t, dsn, func(dbt *DBTest, _ string) {
dbt.mustExec("SET @@SESSION.wait_timeout = 2")
if err := dbt.db.Ping(); err != nil {
dbt.Fatal(err)
}
// wait for MySQL to close our connection
time.Sleep(3 * time.Second)
tx, err := dbt.db.Begin()
if err != nil {
dbt.Fatal(err)
}
if err := tx.Rollback(); err != nil {
dbt.Fatal(err)
}
})
}
// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
//
// Copyright 2012 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/.
package mysql
import (
"context"
"database/sql"
"database/sql/driver"
"encoding/json"
"io"
"net"
"strconv"
"strings"
"time"
)
type mysqlConn struct {
buf buffer
netConn net.Conn
rawConn net.Conn // underlying connection when netConn is TLS connection.
result mysqlResult // managed by clearResult() and handleOkPacket().
cfg *Config
connector *connector
maxAllowedPacket int
maxWriteSize int
writeTimeout time.Duration
flags clientFlag
status statusFlag
sequence uint8
parseTime bool
// for context support (Go 1.8+)
watching bool
watcher chan<- context.Context
closech chan struct{}
finished chan<- struct{}
canceled atomicError // set non-nil if conn is canceled
closed atomicBool // set when conn is closed, before closech is closed
}
// 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: character_set_connection, character_set_client, character_set_results
case "charset":
charsets := strings.Split(val, ",")
for _, cs := range charsets {
// ignore errors here - a charset may not exist
if mc.cfg.Collation != "" {
err = mc.exec("SET NAMES " + cs + " COLLATE " + mc.cfg.Collation)
} else {
err = mc.exec("SET NAMES " + cs)
}
if err == nil {
break
}
}
if err != nil {
return
}
// Other system vars accumulated in a single SET command
default:
if cmdSet.Len() == 0 {
// Heuristic: 29 chars for each other key=value to reduce reallocations
cmdSet.Grow(4 + len(param) + 3 + len(val) + 30*(len(mc.cfg.Params)-1))
cmdSet.WriteString("SET ")
} else {
cmdSet.WriteString(", ")
}
cmdSet.WriteString(param)
cmdSet.WriteString(" = ")
cmdSet.WriteString(val)
}
}
if cmdSet.Len() > 0 {
err = mc.exec(cmdSet.String())
if err != nil {
return
}
}
return
}
func (mc *mysqlConn) markBadConn(err error) error {
if mc == nil {
return err
}
if err != errBadConnNoWrite {
return err
}
return driver.ErrBadConn
}
func (mc *mysqlConn) Begin() (driver.Tx, error) {
return mc.begin(false)
}
func (mc *mysqlConn) begin(readOnly bool) (driver.Tx, error) {
if mc.closed.Load() {
mc.cfg.Logger.Print(ErrInvalidConn)
return nil, driver.ErrBadConn
}
var q string
if readOnly {
q = "START TRANSACTION READ ONLY"
} else {
q = "START TRANSACTION"
}
err := mc.exec(q)
if err == nil {
return &mysqlTx{mc}, err
}
return nil, mc.markBadConn(err)
}
func (mc *mysqlConn) Close() (err error) {
// Makes Close idempotent
if !mc.closed.Load() {
err = mc.writeCommandPacket(comQuit)
}
mc.cleanup()
return
}
// Closes the network connection and unsets internal variables. Do not call this
// function after successfully authentication, call Close instead. This function
// is called before auth or on auth failure because MySQL will have already
// closed the network connection.
func (mc *mysqlConn) cleanup() {
if mc.closed.Swap(true) {
return
}
// Makes cleanup idempotent
close(mc.closech)
if mc.netConn == nil {
return
}
if err := mc.netConn.Close(); err != nil {
mc.cfg.Logger.Print(err)
}
mc.clearResult()
}
func (mc *mysqlConn) error() error {
if mc.closed.Load() {
if err := mc.canceled.Value(); err != nil {
return err
}
return ErrInvalidConn
}
return nil
}
func (mc *mysqlConn) Prepare(query string) (driver.Stmt, error) {
if mc.closed.Load() {
mc.cfg.Logger.Print(ErrInvalidConn)
return nil, driver.ErrBadConn
}
// Send command
err := mc.writeCommandPacketStr(comStmtPrepare, query)
if err != nil {
// STMT_PREPARE is safe to retry. So we can return ErrBadConn here.
mc.cfg.Logger.Print(err)
return nil, driver.ErrBadConn
}
stmt := &mysqlStmt{
mc: mc,
}
// Read Result
columnCount, err := stmt.readPrepareResultPacket()
if err == nil {
if stmt.paramCount > 0 {
if err = mc.readUntilEOF(); err != nil {
return nil, err
}
}
if columnCount > 0 {
err = mc.readUntilEOF()
}
}
return stmt, err
}
func (mc *mysqlConn) interpolateParams(query string, args []driver.Value) (string, error) {
// Number of ? should be same to len(args)
if strings.Count(query, "?") != len(args) {
return "", driver.ErrSkip
}
buf, err := mc.buf.takeCompleteBuffer()
if err != nil {
// can not take the buffer. Something must be wrong with the connection
mc.cfg.Logger.Print(err)
return "", ErrInvalidConn
}
buf = buf[:0]
argPos := 0
for i := 0; i < len(query); i++ {
q := strings.IndexByte(query[i:], '?')
if q == -1 {
buf = append(buf, query[i:]...)
break
}
buf = append(buf, query[i:i+q]...)
i += q
arg := args[argPos]
argPos++
if arg == nil {
buf = append(buf, "NULL"...)
continue
}
switch v := arg.(type) {
case int64:
buf = strconv.AppendInt(buf, v, 10)
case uint64:
// Handle uint64 explicitly because our custom ConvertValue emits unsigned values
buf = strconv.AppendUint(buf, v, 10)
case float64:
buf = strconv.AppendFloat(buf, v, 'g', -1, 64)
case bool:
if v {
buf = append(buf, '1')
} else {
buf = append(buf, '0')
}
case time.Time:
if v.IsZero() {
buf = append(buf, "'0000-00-00'"...)
} else {
buf = append(buf, '\'')
buf, err = appendDateTime(buf, v.In(mc.cfg.Loc), mc.cfg.timeTruncate)
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"...)
} else {
buf = append(buf, "_binary'"...)
if mc.status&statusNoBackslashEscapes == 0 {
buf = escapeBytesBackslash(buf, v)
} else {
buf = escapeBytesQuotes(buf, v)
}
buf = append(buf, '\'')
}
case string:
buf = append(buf, '\'')
if mc.status&statusNoBackslashEscapes == 0 {
buf = escapeStringBackslash(buf, v)
} else {
buf = escapeStringQuotes(buf, v)
}
buf = append(buf, '\'')
default:
return "", driver.ErrSkip
}
if len(buf)+4 > mc.maxAllowedPacket {
return "", driver.ErrSkip
}
}
if argPos != len(args) {
return "", driver.ErrSkip
}
return string(buf), nil
}
func (mc *mysqlConn) Exec(query string, args []driver.Value) (driver.Result, error) {
if mc.closed.Load() {
mc.cfg.Logger.Print(ErrInvalidConn)
return nil, driver.ErrBadConn
}
if len(args) != 0 {
if !mc.cfg.InterpolateParams {
return nil, driver.ErrSkip
}
// try to interpolate the parameters to save extra roundtrips for preparing and closing a statement
prepared, err := mc.interpolateParams(query, args)
if err != nil {
return nil, err
}
query = prepared
}
err := mc.exec(query)
if err == nil {
copied := mc.result
return &copied, err
}
return nil, mc.markBadConn(err)
}
// Internal function to execute commands
func (mc *mysqlConn) exec(query string) error {
handleOk := mc.clearResult()
// Send command
if err := mc.writeCommandPacketStr(comQuery, query); err != nil {
return mc.markBadConn(err)
}
// Read Result
resLen, err := handleOk.readResultSetHeaderPacket()
if err != nil {
return err
}
if resLen > 0 {
// columns
if err := mc.readUntilEOF(); err != nil {
return err
}
// rows
if err := mc.readUntilEOF(); err != nil {
return err
}
}
return handleOk.discardResults()
}
func (mc *mysqlConn) Query(query string, args []driver.Value) (driver.Rows, error) {
return mc.query(query, args)
}
func (mc *mysqlConn) query(query string, args []driver.Value) (*textRows, error) {
handleOk := mc.clearResult()
if mc.closed.Load() {
mc.cfg.Logger.Print(ErrInvalidConn)
return nil, driver.ErrBadConn
}
if len(args) != 0 {
if !mc.cfg.InterpolateParams {
return nil, driver.ErrSkip
}
// try client-side prepare to reduce roundtrip
prepared, err := mc.interpolateParams(query, args)
if err != nil {
return nil, err
}
query = prepared
}
// Send command
err := mc.writeCommandPacketStr(comQuery, query)
if err == nil {
// Read Result
var resLen int
resLen, err = handleOk.readResultSetHeaderPacket()
if err == nil {
rows := new(textRows)
rows.mc = mc
if resLen == 0 {
rows.rs.done = true
switch err := rows.NextResultSet(); err {
case nil, io.EOF:
return rows, nil
default:
return nil, err
}
}
// Columns
rows.rs.columns, err = mc.readColumns(resLen)
return rows, err
}
}
return nil, mc.markBadConn(err)
}
// Gets the value of the given MySQL System Variable
// The returned byte slice is only valid until the next read
func (mc *mysqlConn) getSystemVar(name string) ([]byte, error) {
// Send command
handleOk := mc.clearResult()
if err := mc.writeCommandPacketStr(comQuery, "SELECT @@"+name); err != nil {
return nil, err
}
// Read Result
resLen, err := handleOk.readResultSetHeaderPacket()
if err == nil {
rows := new(textRows)
rows.mc = mc
rows.rs.columns = []mysqlField{{fieldType: fieldTypeVarChar}}
if resLen > 0 {
// Columns
if err := mc.readUntilEOF(); err != nil {
return nil, err
}
}
dest := make([]driver.Value, resLen)
if err = rows.readRow(dest); err == nil {
return dest[0].([]byte), mc.readUntilEOF()
}
}
return nil, err
}
// finish is called when the query has canceled.
func (mc *mysqlConn) cancel(err error) {
mc.canceled.Set(err)
mc.cleanup()
}
// finish is called when the query has succeeded.
func (mc *mysqlConn) finish() {
if !mc.watching || mc.finished == nil {
return
}
select {
case mc.finished <- struct{}{}:
mc.watching = false
case <-mc.closech:
}
}
// Ping implements driver.Pinger interface
func (mc *mysqlConn) Ping(ctx context.Context) (err error) {
if mc.closed.Load() {
mc.cfg.Logger.Print(ErrInvalidConn)
return driver.ErrBadConn
}
if err = mc.watchCancel(ctx); err != nil {
return
}
defer mc.finish()
handleOk := mc.clearResult()
if err = mc.writeCommandPacket(comPing); err != nil {
return mc.markBadConn(err)
}
return handleOk.readResultOK()
}
// 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
}
defer mc.finish()
if sql.IsolationLevel(opts.Isolation) != sql.LevelDefault {
level, err := mapIsolationLevel(opts.Isolation)
if err != nil {
return nil, err
}
err = mc.exec("SET TRANSACTION ISOLATION LEVEL " + level)
if err != nil {
return nil, err
}
}
return mc.begin(opts.ReadOnly)
}
func (mc *mysqlConn) QueryContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Rows, error) {
dargs, err := namedValueToValue(args)
if err != nil {
return nil, err
}
if err := mc.watchCancel(ctx); err != nil {
return nil, err
}
rows, err := mc.query(query, dargs)
if err != nil {
mc.finish()
return nil, err
}
rows.finish = mc.finish
return rows, err
}
func (mc *mysqlConn) ExecContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Result, error) {
dargs, err := namedValueToValue(args)
if err != nil {
return nil, err
}
if err := mc.watchCancel(ctx); err != nil {
return nil, err
}
defer mc.finish()
return mc.Exec(query, dargs)
}
func (mc *mysqlConn) PrepareContext(ctx context.Context, query string) (driver.Stmt, error) {
if err := mc.watchCancel(ctx); err != nil {
return nil, err
}
stmt, err := mc.Prepare(query)
mc.finish()
if err != nil {
return nil, err
}
select {
default:
case <-ctx.Done():
stmt.Close()
return nil, ctx.Err()
}
return stmt, nil
}
func (stmt *mysqlStmt) QueryContext(ctx context.Context, args []driver.NamedValue) (driver.Rows, error) {
dargs, err := namedValueToValue(args)
if err != nil {
return nil, err
}
if err := stmt.mc.watchCancel(ctx); err != nil {
return nil, err
}
rows, err := stmt.query(dargs)
if err != nil {
stmt.mc.finish()
return nil, err
}
rows.finish = stmt.mc.finish
return rows, err
}
func (stmt *mysqlStmt) ExecContext(ctx context.Context, args []driver.NamedValue) (driver.Result, error) {
dargs, err := namedValueToValue(args)
if err != nil {
return nil, err
}
if err := stmt.mc.watchCancel(ctx); err != nil {
return nil, err
}
defer stmt.mc.finish()
return stmt.Exec(dargs)
}
func (mc *mysqlConn) watchCancel(ctx context.Context) error {
if mc.watching {
// Reach here if canceled,
// so the connection is already invalid
mc.cleanup()
return nil
}
// When ctx is already cancelled, don't watch it.
if err := ctx.Err(); err != nil {
return err
}
// When ctx is not cancellable, don't watch it.
if ctx.Done() == nil {
return nil
}
// When watcher is not alive, can't watch it.
if mc.watcher == nil {
return nil
}
mc.watching = true
mc.watcher <- ctx
return nil
}
func (mc *mysqlConn) startWatcher() {
watcher := make(chan context.Context, 1)
mc.watcher = watcher
finished := make(chan struct{})
mc.finished = finished
go func() {
for {
var ctx context.Context
select {
case ctx = <-watcher:
case <-mc.closech:
return
}
select {
case <-ctx.Done():
mc.cancel(ctx.Err())
case <-finished:
case <-mc.closech:
return
}
}
}()
}
func (mc *mysqlConn) CheckNamedValue(nv *driver.NamedValue) (err error) {
nv.Value, err = converter{}.ConvertValue(nv.Value)
return
}
// ResetSession implements driver.SessionResetter.
// (From Go 1.10)
func (mc *mysqlConn) ResetSession(ctx context.Context) error {
if mc.closed.Load() {
return driver.ErrBadConn
}
// Perform a stale connection check. We only perform this check for
// the first query on a connection that has been checked out of the
// connection pool: a fresh connection from the pool is more likely
// to be stale, and it has not performed any previous writes that
// could cause data corruption, so it's safe to return ErrBadConn
// if the check fails.
if mc.cfg.CheckConnLiveness {
conn := mc.netConn
if mc.rawConn != nil {
conn = mc.rawConn
}
var err error
if mc.cfg.ReadTimeout != 0 {
err = conn.SetReadDeadline(time.Now().Add(mc.cfg.ReadTimeout))
}
if err == nil {
err = connCheck(conn)
}
if err != nil {
mc.cfg.Logger.Print("closing bad idle connection: ", err)
return driver.ErrBadConn
}
}
return nil
}
// IsValid implements driver.Validator interface
// (From Go 1.15)
func (mc *mysqlConn) IsValid() bool {
return !mc.closed.Load()
}
// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
//
// Copyright 2016 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/.
package mysql
import (
"context"
"database/sql/driver"
"encoding/json"
"errors"
"net"
"testing"
)
func TestInterpolateParams(t *testing.T) {
mc := &mysqlConn{
buf: newBuffer(nil),
maxAllowedPacket: maxPacketSize,
cfg: &Config{
InterpolateParams: true,
},
}
q, err := mc.interpolateParams("SELECT ?+?", []driver.Value{int64(42), "gopher"})
if err != nil {
t.Errorf("Expected err=nil, got %#v", err)
return
}
expected := `SELECT 42+'gopher'`
if q != expected {
t.Errorf("Expected: %q\nGot: %q", expected, q)
}
}
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),
maxAllowedPacket: maxPacketSize,
cfg: &Config{
InterpolateParams: true,
},
}
q, err := mc.interpolateParams("SELECT ?+?", []driver.Value{int64(42)})
if err != driver.ErrSkip {
t.Errorf("Expected err=driver.ErrSkip, got err=%#v, q=%#v", err, q)
}
}
// We don't support placeholder in string literal for now.
// https://github.com/go-sql-driver/mysql/pull/490
func TestInterpolateParamsPlaceholderInString(t *testing.T) {
mc := &mysqlConn{
buf: newBuffer(nil),
maxAllowedPacket: maxPacketSize,
cfg: &Config{
InterpolateParams: true,
},
}
q, err := mc.interpolateParams("SELECT 'abc?xyz',?", []driver.Value{int64(42)})
// When InterpolateParams support string literal, this should return `"SELECT 'abc?xyz', 42`
if err != driver.ErrSkip {
t.Errorf("Expected err=driver.ErrSkip, got err=%#v, q=%#v", err, q)
}
}
func TestInterpolateParamsUint64(t *testing.T) {
mc := &mysqlConn{
buf: newBuffer(nil),
maxAllowedPacket: maxPacketSize,
cfg: &Config{
InterpolateParams: true,
},
}
q, err := mc.interpolateParams("SELECT ?", []driver.Value{uint64(42)})
if err != nil {
t.Errorf("Expected err=nil, got err=%#v, q=%#v", err, q)
}
if q != "SELECT 42" {
t.Errorf("Expected uint64 interpolation to work, got q=%#v", q)
}
}
func TestCheckNamedValue(t *testing.T) {
value := driver.NamedValue{Value: ^uint64(0)}
x := &mysqlConn{}
err := x.CheckNamedValue(&value)
if err != nil {
t.Fatal("uint64 high-bit not convertible", err)
}
if value.Value != ^uint64(0) {
t.Fatalf("uint64 high-bit converted, got %#v %T", value.Value, value.Value)
}
}
// TestCleanCancel tests passed context is cancelled at start.
// No packet should be sent. Connection should keep current status.
func TestCleanCancel(t *testing.T) {
mc := &mysqlConn{
closech: make(chan struct{}),
}
mc.startWatcher()
defer mc.cleanup()
ctx, cancel := context.WithCancel(context.Background())
cancel()
for i := 0; i < 3; i++ { // Repeat same behavior
err := mc.Ping(ctx)
if err != context.Canceled {
t.Errorf("expected context.Canceled, got %#v", err)
}
if mc.closed.Load() {
t.Error("expected mc is not closed, closed actually")
}
if mc.watching {
t.Error("expected watching is false, but true")
}
}
}
func TestPingMarkBadConnection(t *testing.T) {
nc := badConnection{err: errors.New("boom")}
ms := &mysqlConn{
netConn: nc,
buf: newBuffer(nc),
maxAllowedPacket: defaultMaxAllowedPacket,
}
err := ms.Ping(context.Background())
if err != driver.ErrBadConn {
t.Errorf("expected driver.ErrBadConn, got %#v", err)
}
}
func TestPingErrInvalidConn(t *testing.T) {
nc := badConnection{err: errors.New("failed to write"), n: 10}
ms := &mysqlConn{
netConn: nc,
buf: newBuffer(nc),
maxAllowedPacket: defaultMaxAllowedPacket,
closech: make(chan struct{}),
cfg: NewConfig(),
}
err := ms.Ping(context.Background())
if err != ErrInvalidConn {
t.Errorf("expected ErrInvalidConn, got %#v", err)
}
}
type badConnection struct {
n int
err error
net.Conn
}
func (bc badConnection) Write(b []byte) (n int, err error) {
return bc.n, bc.err
}
func (bc badConnection) Close() error {
return nil
}
// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
//
// Copyright 2018 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/.
package mysql
import (
"context"
"database/sql/driver"
"net"
"os"
"strconv"
"strings"
)
type connector struct {
cfg *Config // immutable private copy.
encodedAttributes string // Encoded connection attributes.
}
func encodeConnectionAttributes(cfg *Config) string {
connAttrsBuf := make([]byte, 0)
// default connection attributes
connAttrsBuf = appendLengthEncodedString(connAttrsBuf, connAttrClientName)
connAttrsBuf = appendLengthEncodedString(connAttrsBuf, connAttrClientNameValue)
connAttrsBuf = appendLengthEncodedString(connAttrsBuf, connAttrOS)
connAttrsBuf = appendLengthEncodedString(connAttrsBuf, connAttrOSValue)
connAttrsBuf = appendLengthEncodedString(connAttrsBuf, connAttrPlatform)
connAttrsBuf = appendLengthEncodedString(connAttrsBuf, connAttrPlatformValue)
connAttrsBuf = appendLengthEncodedString(connAttrsBuf, connAttrPid)
connAttrsBuf = appendLengthEncodedString(connAttrsBuf, strconv.Itoa(os.Getpid()))
serverHost, _, _ := net.SplitHostPort(cfg.Addr)
if serverHost != "" {
connAttrsBuf = appendLengthEncodedString(connAttrsBuf, connAttrServerHost)
connAttrsBuf = appendLengthEncodedString(connAttrsBuf, serverHost)
}
// user-defined connection attributes
for _, connAttr := range strings.Split(cfg.ConnectionAttributes, ",") {
k, v, found := strings.Cut(connAttr, ":")
if !found {
continue
}
connAttrsBuf = appendLengthEncodedString(connAttrsBuf, k)
connAttrsBuf = appendLengthEncodedString(connAttrsBuf, v)
}
return string(connAttrsBuf)
}
func newConnector(cfg *Config) *connector {
encodedAttributes := encodeConnectionAttributes(cfg)
return &connector{
cfg: cfg,
encodedAttributes: encodedAttributes,
}
}
// Connect implements driver.Connector interface.
// Connect returns a connection to the database.
func (c *connector) Connect(ctx context.Context) (driver.Conn, error) {
var err error
// Invoke beforeConnect if present, with a copy of the configuration
cfg := c.cfg
if c.cfg.beforeConnect != nil {
cfg = c.cfg.Clone()
err = c.cfg.beforeConnect(ctx, cfg)
if err != nil {
return nil, err
}
}
// New mysqlConn
mc := &mysqlConn{
maxAllowedPacket: maxPacketSize,
maxWriteSize: maxPacketSize - 1,
closech: make(chan struct{}),
cfg: cfg,
connector: c,
}
mc.parseTime = mc.cfg.ParseTime
// Connect to Server
dialsLock.RLock()
dial, ok := dials[mc.cfg.Net]
dialsLock.RUnlock()
if ok {
dctx := ctx
if mc.cfg.Timeout > 0 {
var cancel context.CancelFunc
dctx, cancel = context.WithTimeout(ctx, c.cfg.Timeout)
defer cancel()
}
mc.netConn, err = dial(dctx, mc.cfg.Addr)
} else {
nd := net.Dialer{Timeout: mc.cfg.Timeout}
mc.netConn, err = nd.DialContext(ctx, mc.cfg.Net, mc.cfg.Addr)
}
if err != nil {
return nil, err
}
// Enable TCP Keepalives on TCP connections
if tc, ok := mc.netConn.(*net.TCPConn); ok {
if err := tc.SetKeepAlive(true); err != nil {
c.cfg.Logger.Print(err)
}
}
// Call startWatcher for context support (From Go 1.8)
mc.startWatcher()
if err := mc.watchCancel(ctx); err != nil {
mc.cleanup()
return nil, err
}
defer mc.finish()
mc.buf = newBuffer(mc.netConn)
// Set I/O timeouts
mc.buf.timeout = mc.cfg.ReadTimeout
mc.writeTimeout = mc.cfg.WriteTimeout
// Reading Handshake Initialization Packet
authData, plugin, err := mc.readHandshakePacket()
if err != nil {
mc.cleanup()
return nil, err
}
if plugin == "" {
plugin = defaultAuthPlugin
}
// Send Client Authentication Packet
authResp, err := mc.auth(authData, plugin)
if err != nil {
// try the default auth plugin, if using the requested plugin failed
c.cfg.Logger.Print("could not use requested auth plugin '"+plugin+"': ", err.Error())
plugin = defaultAuthPlugin
authResp, err = mc.auth(authData, plugin)
if err != nil {
mc.cleanup()
return nil, err
}
}
if err = mc.writeHandshakeResponsePacket(authResp, plugin); err != nil {
mc.cleanup()
return nil, err
}
// Handle response to auth packet, switch methods if possible
if err = mc.handleAuthResult(authData, plugin); err != nil {
// Authentication failed and MySQL has already closed the connection
// (https://dev.mysql.com/doc/internals/en/authentication-fails.html).
// Do not send COM_QUIT, just cleanup and return the error.
mc.cleanup()
return nil, err
}
if mc.cfg.MaxAllowedPacket > 0 {
mc.maxAllowedPacket = mc.cfg.MaxAllowedPacket
} else {
// Get max allowed packet size
maxap, err := mc.getSystemVar("max_allowed_packet")
if err != nil {
mc.Close()
return nil, err
}
mc.maxAllowedPacket = stringToInt(maxap) - 1
}
if mc.maxAllowedPacket < maxPacketSize {
mc.maxWriteSize = mc.maxAllowedPacket
}
// Handle DSN Params
err = mc.handleParams()
if err != nil {
mc.Close()
return nil, err
}
return mc, nil
}
// Driver implements driver.Connector interface.
// Driver returns &MySQLDriver{}.
func (c *connector) Driver() driver.Driver {
return &MySQLDriver{}
}
package mysql
import (
"context"
"net"
"testing"
"time"
)
func TestConnectorReturnsTimeout(t *testing.T) {
connector := newConnector(&Config{
Net: "tcp",
Addr: "1.1.1.1:1234",
Timeout: 10 * time.Millisecond,
})
_, err := connector.Connect(context.Background())
if err == nil {
t.Fatal("error expected")
}
if nerr, ok := err.(*net.OpError); ok {
expected := "dial tcp 1.1.1.1:1234: i/o timeout"
if nerr.Error() != expected {
t.Fatalf("expected %q, got %q", expected, nerr.Error())
}
} else {
t.Fatalf("expected %T, got %T", nerr, err)
}
}
// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
//
// Copyright 2012 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/.
package mysql
import "runtime"
const (
defaultAuthPlugin = "mysql_native_password"
defaultMaxAllowedPacket = 64 << 20 // 64 MiB. See https://github.com/go-sql-driver/mysql/issues/1355
minProtocolVersion = 10
maxPacketSize = 1<<24 - 1
timeFormat = "2006-01-02 15:04:05.999999"
// Connection attributes
// See https://dev.mysql.com/doc/refman/8.0/en/performance-schema-connection-attribute-tables.html#performance-schema-connection-attributes-available
connAttrClientName = "_client_name"
connAttrClientNameValue = "Go-MySQL-Driver"
connAttrOS = "_os"
connAttrOSValue = runtime.GOOS
connAttrPlatform = "_platform"
connAttrPlatformValue = runtime.GOARCH
connAttrPid = "_pid"
connAttrServerHost = "_server_host"
)
// MySQL constants documentation:
// http://dev.mysql.com/doc/internals/en/client-server-protocol.html
const (
iOK byte = 0x00
iAuthMoreData byte = 0x01
iLocalInFile byte = 0xfb
iEOF byte = 0xfe
iERR byte = 0xff
)
// https://dev.mysql.com/doc/internals/en/capability-flags.html#packet-Protocol::CapabilityFlags
type clientFlag uint32
const (
clientLongPassword clientFlag = 1 << iota
clientFoundRows
clientLongFlag
clientConnectWithDB
clientNoSchema
clientCompress
clientODBC
clientLocalFiles
clientIgnoreSpace
clientProtocol41
clientInteractive
clientSSL
clientIgnoreSIGPIPE
clientTransactions
clientReserved
clientSecureConn
clientMultiStatements
clientMultiResults
clientPSMultiResults
clientPluginAuth
clientConnectAttrs
clientPluginAuthLenEncClientData
clientCanHandleExpiredPasswords
clientSessionTrack
clientDeprecateEOF
)
const (
comQuit byte = iota + 1
comInitDB
comQuery
comFieldList
comCreateDB
comDropDB
comRefresh
comShutdown
comStatistics
comProcessInfo
comConnect
comProcessKill
comDebug
comPing
comTime
comDelayedInsert
comChangeUser
comBinlogDump
comTableDump
comConnectOut
comRegisterSlave
comStmtPrepare
comStmtExecute
comStmtSendLongData
comStmtClose
comStmtReset
comSetOption
comStmtFetch
)
// https://dev.mysql.com/doc/internals/en/com-query-response.html#packet-Protocol::ColumnType
type fieldType byte
const (
fieldTypeDecimal fieldType = iota
fieldTypeTiny
fieldTypeShort
fieldTypeLong
fieldTypeFloat
fieldTypeDouble
fieldTypeNULL
fieldTypeTimestamp
fieldTypeLongLong
fieldTypeInt24
fieldTypeDate
fieldTypeTime
fieldTypeDateTime
fieldTypeYear
fieldTypeNewDate
fieldTypeVarChar
fieldTypeBit
)
const (
fieldTypeJSON fieldType = iota + 0xf5
fieldTypeNewDecimal
fieldTypeEnum
fieldTypeSet
fieldTypeTinyBLOB
fieldTypeMediumBLOB
fieldTypeLongBLOB
fieldTypeBLOB
fieldTypeVarString
fieldTypeString
fieldTypeGeometry
)
type fieldFlag uint16
const (
flagNotNULL fieldFlag = 1 << iota
flagPriKey
flagUniqueKey
flagMultipleKey
flagBLOB
flagUnsigned
flagZeroFill
flagBinary
flagEnum
flagAutoIncrement
flagTimestamp
flagSet
flagUnknown1
flagUnknown2
flagUnknown3
flagUnknown4
)
// http://dev.mysql.com/doc/internals/en/status-flags.html
type statusFlag uint16
const (
statusInTrans statusFlag = 1 << iota
statusInAutocommit
statusReserved // Not in documentation
statusMoreResultsExists
statusNoGoodIndexUsed
statusNoIndexUsed
statusCursorExists
statusLastRowSent
statusDbDropped
statusNoBackslashEscapes
statusMetadataChanged
statusQueryWasSlow
statusPsOutParams
statusInTransReadonly
statusSessionStateChanged
)
const (
cachingSha2PasswordRequestPublicKey = 2
cachingSha2PasswordFastAuthSuccess = 3
cachingSha2PasswordPerformFullAuthentication = 4
)
// Copyright 2012 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/.
// Package mysql provides a MySQL driver for Go's database/sql package.
//
// The driver should be used via the database/sql package:
//
// import "database/sql"
// import _ "github.com/go-sql-driver/mysql"
//
// db, err := sql.Open("mysql", "user:password@/dbname")
//
// See https://github.com/go-sql-driver/mysql#usage for details
package mysql
import (
"context"
"database/sql"
"database/sql/driver"
"net"
"sync"
)
// MySQLDriver is exported to make the driver directly accessible.
// In general the driver is used via the database/sql package.
type MySQLDriver struct{}
// DialFunc is a function which can be used to establish the network connection.
// Custom dial functions must be registered with RegisterDial
//
// Deprecated: users should register a DialContextFunc instead
type DialFunc func(addr string) (net.Conn, error)
// DialContextFunc is a function which can be used to establish the network connection.
// Custom dial functions must be registered with RegisterDialContext
type DialContextFunc func(ctx context.Context, addr string) (net.Conn, error)
var (
dialsLock sync.RWMutex
dials map[string]DialContextFunc
)
// RegisterDialContext registers a custom dial function. It can then be used by the
// network address mynet(addr), where mynet is the registered new network.
// The current context for the connection and its address is passed to the dial function.
func RegisterDialContext(net string, dial DialContextFunc) {
dialsLock.Lock()
defer dialsLock.Unlock()
if dials == nil {
dials = make(map[string]DialContextFunc)
}
dials[net] = dial
}
// DeregisterDialContext removes the custom dial function registered with the given net.
func DeregisterDialContext(net string) {
dialsLock.Lock()
defer dialsLock.Unlock()
if dials != nil {
delete(dials, net)
}
}
// RegisterDial registers a custom dial function. It can then be used by the
// network address mynet(addr), where mynet is the registered new network.
// addr is passed as a parameter to the dial function.
//
// Deprecated: users should call RegisterDialContext instead
func RegisterDial(network string, dial DialFunc) {
RegisterDialContext(network, func(_ context.Context, addr string) (net.Conn, error) {
return dial(addr)
})
}
// Open new Connection.
// See https://github.com/go-sql-driver/mysql#dsn-data-source-name for how
// the DSN string is formatted
func (d MySQLDriver) Open(dsn string) (driver.Conn, error) {
cfg, err := ParseDSN(dsn)
if err != nil {
return nil, err
}
c := newConnector(cfg)
return c.Connect(context.Background())
}
// This variable can be replaced with -ldflags like below:
// go build "-ldflags=-X github.com/go-sql-driver/mysql.driverName=custom"
var driverName = "mysql"
func init() {
if driverName != "" {
sql.Register(driverName, &MySQLDriver{})
}
}
// NewConnector returns new driver.Connector.
func NewConnector(cfg *Config) (driver.Connector, error) {
cfg = cfg.Clone()
// normalize the contents of cfg so calls to NewConnector have the same
// behavior as MySQLDriver.OpenConnector
if err := cfg.normalize(); err != nil {
return nil, err
}
return newConnector(cfg), nil
}
// OpenConnector implements driver.DriverContext.
func (d MySQLDriver) OpenConnector(dsn string) (driver.Connector, error) {
cfg, err := ParseDSN(dsn)
if err != nil {
return nil, err
}
return newConnector(cfg), nil
}
// 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/.
package mysql
import (
"bytes"
"context"
"crypto/rand"
"crypto/tls"
"database/sql"
"database/sql/driver"
"encoding/json"
"fmt"
"io"
"log"
"math"
"net"
"net/url"
"os"
"reflect"
"runtime"
"strconv"
"strings"
"sync"
"sync/atomic"
"testing"
"time"
)
// This variable can be replaced with -ldflags like below:
// go test "-ldflags=-X github.com/go-sql-driver/mysql.driverNameTest=custom"
var driverNameTest string
func init() {
if driverNameTest == "" {
driverNameTest = driverName
}
}
// Ensure that all the driver interfaces are implemented
var (
_ driver.Rows = &binaryRows{}
_ driver.Rows = &textRows{}
)
var (
user string
pass string
prot string
addr string
dbname string
dsn string
netAddr string
available bool
)
var (
tDate = time.Date(2012, 6, 14, 0, 0, 0, 0, time.UTC)
sDate = "2012-06-14"
tDateTime = time.Date(2011, 11, 20, 21, 27, 37, 0, time.UTC)
sDateTime = "2011-11-20 21:27:37"
tDate0 = time.Time{}
sDate0 = "0000-00-00"
sDateTime0 = "0000-00-00 00:00:00"
)
// See https://github.com/go-sql-driver/mysql/wiki/Testing
func init() {
// get environment variables
env := func(key, defaultValue string) string {
if value := os.Getenv(key); value != "" {
return value
}
return defaultValue
}
user = env("MYSQL_TEST_USER", "root")
pass = env("MYSQL_TEST_PASS", "")
prot = env("MYSQL_TEST_PROT", "tcp")
addr = env("MYSQL_TEST_ADDR", "localhost:3306")
dbname = env("MYSQL_TEST_DBNAME", "gotest")
netAddr = fmt.Sprintf("%s(%s)", prot, addr)
dsn = fmt.Sprintf("%s:%s@%s/%s?timeout=30s", user, pass, netAddr, dbname)
c, err := net.Dial(prot, addr)
if err == nil {
available = true
c.Close()
}
}
type DBTest struct {
testing.TB
db *sql.DB
}
type netErrorMock struct {
temporary bool
timeout bool
}
func (e netErrorMock) Temporary() bool {
return e.temporary
}
func (e netErrorMock) Timeout() bool {
return e.timeout
}
func (e netErrorMock) Error() string {
return fmt.Sprintf("mock net error. Temporary: %v, Timeout %v", e.temporary, e.timeout)
}
func runTestsWithMultiStatement(t *testing.T, dsn string, tests ...func(dbt *DBTest)) {
if !available {
t.Skipf("MySQL server not running on %s", netAddr)
}
dsn += "&multiStatements=true"
var db *sql.DB
if _, err := ParseDSN(dsn); err != errInvalidDSNUnsafeCollation {
db, err = sql.Open(driverNameTest, dsn)
if err != nil {
t.Fatalf("error connecting: %s", err.Error())
}
defer db.Close()
}
// Previous test may be skipped without dropping the test table
db.Exec("DROP TABLE IF EXISTS test")
dbt := &DBTest{t, db}
for _, test := range tests {
test(dbt)
dbt.db.Exec("DROP TABLE IF EXISTS test")
}
}
func runTests(t *testing.T, dsn string, tests ...func(dbt *DBTest)) {
if !available {
t.Skipf("MySQL server not running on %s", netAddr)
}
db, err := sql.Open(driverNameTest, dsn)
if err != nil {
t.Fatalf("error connecting: %s", err.Error())
}
defer db.Close()
cleanup := func() {
db.Exec("DROP TABLE IF EXISTS test")
}
dsn2 := dsn + "&interpolateParams=true"
var db2 *sql.DB
if _, err := ParseDSN(dsn2); err != errInvalidDSNUnsafeCollation {
db2, err = sql.Open(driverNameTest, dsn2)
if err != nil {
t.Fatalf("error connecting: %s", err.Error())
}
defer db2.Close()
}
for _, test := range tests {
test := test
t.Run("default", func(t *testing.T) {
dbt := &DBTest{t, db}
t.Cleanup(cleanup)
test(dbt)
})
if db2 != nil {
t.Run("interpolateParams", func(t *testing.T) {
dbt2 := &DBTest{t, db2}
t.Cleanup(cleanup)
test(dbt2)
})
}
}
}
// runTestsParallel runs the tests in parallel with a separate database connection for each test.
func runTestsParallel(t *testing.T, dsn string, tests ...func(dbt *DBTest, tableName string)) {
if !available {
t.Skipf("MySQL server not running on %s", netAddr)
}
newTableName := func(t *testing.T) string {
t.Helper()
var buf [8]byte
if _, err := rand.Read(buf[:]); err != nil {
t.Fatal(err)
}
return fmt.Sprintf("test_%x", buf[:])
}
t.Parallel()
for _, test := range tests {
test := test
t.Run("default", func(t *testing.T) {
t.Parallel()
tableName := newTableName(t)
db, err := sql.Open("mysql", dsn)
if err != nil {
t.Fatalf("error connecting: %s", err.Error())
}
t.Cleanup(func() {
db.Exec("DROP TABLE IF EXISTS " + tableName)
db.Close()
})
dbt := &DBTest{t, db}
test(dbt, tableName)
})
dsn2 := dsn + "&interpolateParams=true"
if _, err := ParseDSN(dsn2); err == errInvalidDSNUnsafeCollation {
t.Run("interpolateParams", func(t *testing.T) {
t.Parallel()
tableName := newTableName(t)
db, err := sql.Open("mysql", dsn2)
if err != nil {
t.Fatalf("error connecting: %s", err.Error())
}
t.Cleanup(func() {
db.Exec("DROP TABLE IF EXISTS " + tableName)
db.Close()
})
dbt := &DBTest{t, db}
test(dbt, tableName)
})
}
}
}
func (dbt *DBTest) fail(method, query string, err error) {
dbt.Helper()
if len(query) > 300 {
query = "[query too large to print]"
}
dbt.Fatalf("error on %s %s: %s", method, query, err.Error())
}
func (dbt *DBTest) mustExec(query string, args ...interface{}) (res sql.Result) {
dbt.Helper()
res, err := dbt.db.Exec(query, args...)
if err != nil {
dbt.fail("exec", query, err)
}
return res
}
func (dbt *DBTest) mustQuery(query string, args ...interface{}) (rows *sql.Rows) {
dbt.Helper()
rows, err := dbt.db.Query(query, args...)
if err != nil {
dbt.fail("query", query, err)
}
return rows
}
func maybeSkip(t *testing.T, err error, skipErrno uint16) {
mySQLErr, ok := err.(*MySQLError)
if !ok {
return
}
if mySQLErr.Number == skipErrno {
t.Skipf("skipping test for error: %v", err)
}
}
func TestEmptyQuery(t *testing.T) {
runTestsParallel(t, dsn, func(dbt *DBTest, _ string) {
// just a comment, no query
rows := dbt.mustQuery("--")
defer rows.Close()
// will hang before #255
if rows.Next() {
dbt.Errorf("next on rows must be false")
}
})
}
func TestCRUD(t *testing.T) {
runTestsParallel(t, dsn, func(dbt *DBTest, tbl string) {
// Create Table
dbt.mustExec("CREATE TABLE " + tbl + " (value BOOL)")
// Test for unexpected data
var out bool
rows := dbt.mustQuery("SELECT * FROM " + tbl)
if rows.Next() {
dbt.Error("unexpected data in empty table")
}
rows.Close()
// Create Data
res := dbt.mustExec("INSERT INTO " + tbl + " VALUES (1)")
count, err := res.RowsAffected()
if err != nil {
dbt.Fatalf("res.RowsAffected() returned error: %s", err.Error())
}
if count != 1 {
dbt.Fatalf("expected 1 affected row, got %d", count)
}
id, err := res.LastInsertId()
if err != nil {
dbt.Fatalf("res.LastInsertId() returned error: %s", err.Error())
}
if id != 0 {
dbt.Fatalf("expected InsertId 0, got %d", id)
}
// Read
rows = dbt.mustQuery("SELECT value FROM " + tbl)
if rows.Next() {
rows.Scan(&out)
if true != out {
dbt.Errorf("true != %t", out)
}
if rows.Next() {
dbt.Error("unexpected data")
}
} else {
dbt.Error("no data")
}
rows.Close()
// Update
res = dbt.mustExec("UPDATE "+tbl+" SET value = ? WHERE value = ?", false, true)
count, err = res.RowsAffected()
if err != nil {
dbt.Fatalf("res.RowsAffected() returned error: %s", err.Error())
}
if count != 1 {
dbt.Fatalf("expected 1 affected row, got %d", count)
}
// Check Update
rows = dbt.mustQuery("SELECT value FROM " + tbl)
if rows.Next() {
rows.Scan(&out)
if false != out {
dbt.Errorf("false != %t", out)
}
if rows.Next() {
dbt.Error("unexpected data")
}
} else {
dbt.Error("no data")
}
rows.Close()
// Delete
res = dbt.mustExec("DELETE FROM "+tbl+" WHERE value = ?", false)
count, err = res.RowsAffected()
if err != nil {
dbt.Fatalf("res.RowsAffected() returned error: %s", err.Error())
}
if count != 1 {
dbt.Fatalf("expected 1 affected row, got %d", count)
}
// Check for unexpected rows
res = dbt.mustExec("DELETE FROM " + tbl)
count, err = res.RowsAffected()
if err != nil {
dbt.Fatalf("res.RowsAffected() returned error: %s", err.Error())
}
if count != 0 {
dbt.Fatalf("expected 0 affected row, got %d", count)
}
})
}
// TestNumbers test that selecting numeric columns.
// Both of textRows and binaryRows should return same type and value.
func TestNumbersToAny(t *testing.T) {
runTestsParallel(t, dsn, func(dbt *DBTest, tbl string) {
dbt.mustExec("CREATE TABLE " + tbl + " (id INT PRIMARY KEY, b BOOL, i8 TINYINT, " +
"i16 SMALLINT, i32 INT, i64 BIGINT, f32 FLOAT, f64 DOUBLE, iu32 INT UNSIGNED)")
dbt.mustExec("INSERT INTO " + tbl + " VALUES (1, true, 127, 32767, 2147483647, 9223372036854775807, 1.25, 2.5, 4294967295)")
// Use binaryRows for interpolateParams=false and textRows for interpolateParams=true.
rows := dbt.mustQuery("SELECT b, i8, i16, i32, i64, f32, f64, iu32 FROM "+tbl+" WHERE id=?", 1)
if !rows.Next() {
dbt.Fatal("no data")
}
var b, i8, i16, i32, i64, f32, f64, iu32 any
err := rows.Scan(&b, &i8, &i16, &i32, &i64, &f32, &f64, &iu32)
if err != nil {
dbt.Fatal(err)
}
if b.(int64) != 1 {
dbt.Errorf("b != 1")
}
if i8.(int64) != 127 {
dbt.Errorf("i8 != 127")
}
if i16.(int64) != 32767 {
dbt.Errorf("i16 != 32767")
}
if i32.(int64) != 2147483647 {
dbt.Errorf("i32 != 2147483647")
}
if i64.(int64) != 9223372036854775807 {
dbt.Errorf("i64 != 9223372036854775807")
}
if f32.(float32) != 1.25 {
dbt.Errorf("f32 != 1.25")
}
if f64.(float64) != 2.5 {
dbt.Errorf("f64 != 2.5")
}
if iu32.(int64) != 4294967295 {
dbt.Errorf("iu32 != 4294967295")
}
})
}
func TestMultiQuery(t *testing.T) {
runTestsWithMultiStatement(t, dsn, func(dbt *DBTest) {
// Create Table
dbt.mustExec("CREATE TABLE `test` (`id` int(11) NOT NULL, `value` int(11) NOT NULL) ")
// Create Data
res := dbt.mustExec("INSERT INTO test VALUES (1, 1)")
count, err := res.RowsAffected()
if err != nil {
dbt.Fatalf("res.RowsAffected() returned error: %s", err.Error())
}
if count != 1 {
dbt.Fatalf("expected 1 affected row, got %d", count)
}
// Update
res = dbt.mustExec("UPDATE test SET value = 3 WHERE id = 1; UPDATE test SET value = 4 WHERE id = 1; UPDATE test SET value = 5 WHERE id = 1;")
count, err = res.RowsAffected()
if err != nil {
dbt.Fatalf("res.RowsAffected() returned error: %s", err.Error())
}
if count != 1 {
dbt.Fatalf("expected 1 affected row, got %d", count)
}
// Read
var out int
rows := dbt.mustQuery("SELECT value FROM test WHERE id=1;")
if rows.Next() {
rows.Scan(&out)
if out != 5 {
dbt.Errorf("expected 5, got %d", out)
}
if rows.Next() {
dbt.Error("unexpected data")
}
} else {
dbt.Error("no data")
}
rows.Close()
})
}
func TestInt(t *testing.T) {
runTestsParallel(t, dsn, func(dbt *DBTest, tbl string) {
types := [5]string{"TINYINT", "SMALLINT", "MEDIUMINT", "INT", "BIGINT"}
in := int64(42)
var out int64
var rows *sql.Rows
// SIGNED
for _, v := range types {
dbt.mustExec("CREATE TABLE " + tbl + " (value " + v + ")")
dbt.mustExec("INSERT INTO "+tbl+" VALUES (?)", in)
rows = dbt.mustQuery("SELECT value FROM " + tbl)
if rows.Next() {
rows.Scan(&out)
if in != out {
dbt.Errorf("%s: %d != %d", v, in, out)
}
} else {
dbt.Errorf("%s: no data", v)
}
rows.Close()
dbt.mustExec("DROP TABLE IF EXISTS " + tbl)
}
// UNSIGNED ZEROFILL
for _, v := range types {
dbt.mustExec("CREATE TABLE " + tbl + " (value " + v + " ZEROFILL)")
dbt.mustExec("INSERT INTO "+tbl+" VALUES (?)", in)
rows = dbt.mustQuery("SELECT value FROM " + tbl)
if rows.Next() {
rows.Scan(&out)
if in != out {
dbt.Errorf("%s ZEROFILL: %d != %d", v, in, out)
}
} else {
dbt.Errorf("%s ZEROFILL: no data", v)
}
rows.Close()
dbt.mustExec("DROP TABLE IF EXISTS " + tbl)
}
})
}
func TestFloat32(t *testing.T) {
runTestsParallel(t, dsn, func(dbt *DBTest, tbl string) {
types := [2]string{"FLOAT", "DOUBLE"}
in := float32(42.23)
var out float32
var rows *sql.Rows
for _, v := range types {
dbt.mustExec("CREATE TABLE " + tbl + " (value " + v + ")")
dbt.mustExec("INSERT INTO "+tbl+" VALUES (?)", in)
rows = dbt.mustQuery("SELECT value FROM " + tbl)
if rows.Next() {
rows.Scan(&out)
if in != out {
dbt.Errorf("%s: %g != %g", v, in, out)
}
} else {
dbt.Errorf("%s: no data", v)
}
rows.Close()
dbt.mustExec("DROP TABLE IF EXISTS " + tbl)
}
})
}
func TestFloat64(t *testing.T) {
runTestsParallel(t, dsn, func(dbt *DBTest, tbl string) {
types := [2]string{"FLOAT", "DOUBLE"}
var expected float64 = 42.23
var out float64
var rows *sql.Rows
for _, v := range types {
dbt.mustExec("CREATE TABLE " + tbl + " (value " + v + ")")
dbt.mustExec("INSERT INTO " + tbl + " VALUES (42.23)")
rows = dbt.mustQuery("SELECT value FROM " + tbl)
if rows.Next() {
rows.Scan(&out)
if expected != out {
dbt.Errorf("%s: %g != %g", v, expected, out)
}
} else {
dbt.Errorf("%s: no data", v)
}
rows.Close()
dbt.mustExec("DROP TABLE IF EXISTS " + tbl)
}
})
}
func TestFloat64Placeholder(t *testing.T) {
runTestsParallel(t, dsn, func(dbt *DBTest, tbl string) {
types := [2]string{"FLOAT", "DOUBLE"}
var expected float64 = 42.23
var out float64
var rows *sql.Rows
for _, v := range types {
dbt.mustExec("CREATE TABLE " + tbl + " (id int, value " + v + ")")
dbt.mustExec("INSERT INTO " + tbl + " VALUES (1, 42.23)")
rows = dbt.mustQuery("SELECT value FROM "+tbl+" WHERE id = ?", 1)
if rows.Next() {
rows.Scan(&out)
if expected != out {
dbt.Errorf("%s: %g != %g", v, expected, out)
}
} else {
dbt.Errorf("%s: no data", v)
}
rows.Close()
dbt.mustExec("DROP TABLE IF EXISTS " + tbl)
}
})
}
func TestString(t *testing.T) {
runTestsParallel(t, dsn, func(dbt *DBTest, tbl string) {
types := [6]string{"CHAR(255)", "VARCHAR(255)", "TINYTEXT", "TEXT", "MEDIUMTEXT", "LONGTEXT"}
in := "κόσμε üöäßñóùéàâÿœ'îë Árvíztűrő いろはにほへとちりぬるを イロハニホヘト דג סקרן чащах น่าฟังเอย"
var out string
var rows *sql.Rows
for _, v := range types {
dbt.mustExec("CREATE TABLE " + tbl + " (value " + v + ") CHARACTER SET utf8")
dbt.mustExec("INSERT INTO "+tbl+" VALUES (?)", in)
rows = dbt.mustQuery("SELECT value FROM " + tbl)
if rows.Next() {
rows.Scan(&out)
if in != out {
dbt.Errorf("%s: %s != %s", v, in, out)
}
} else {
dbt.Errorf("%s: no data", v)
}
rows.Close()
dbt.mustExec("DROP TABLE IF EXISTS " + tbl)
}
// BLOB
dbt.mustExec("CREATE TABLE " + tbl + " (id int, value BLOB) CHARACTER SET utf8")
id := 2
in = "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, " +
"sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, " +
"sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. " +
"Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. " +
"Lorem ipsum dolor sit amet, consetetur sadipscing elitr, " +
"sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, " +
"sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. " +
"Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet."
dbt.mustExec("INSERT INTO "+tbl+" VALUES (?, ?)", id, in)
err := dbt.db.QueryRow("SELECT value FROM "+tbl+" WHERE id = ?", id).Scan(&out)
if err != nil {
dbt.Fatalf("Error on BLOB-Query: %s", err.Error())
} else if out != in {
dbt.Errorf("BLOB: %s != %s", in, out)
}
})
}
func TestRawBytes(t *testing.T) {
runTestsParallel(t, dsn, func(dbt *DBTest, _ string) {
v1 := []byte("aaa")
v2 := []byte("bbb")
rows := dbt.mustQuery("SELECT ?, ?", v1, v2)
defer rows.Close()
if rows.Next() {
var o1, o2 sql.RawBytes
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)
}
// https://github.com/go-sql-driver/mysql/issues/765
// Appending to RawBytes shouldn't overwrite next RawBytes.
o1 = append(o1, "xyzzy"...)
if !bytes.Equal(v2, o2) {
dbt.Errorf("expected %v, got %v", v2, o2)
}
} else {
dbt.Errorf("no data")
}
})
}
func TestRawMessage(t *testing.T) {
runTestsParallel(t, dsn, func(dbt *DBTest, _ string) {
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
}
func (tv testValuer) Value() (driver.Value, error) {
return tv.value, nil
}
func TestValuer(t *testing.T) {
runTestsParallel(t, dsn, func(dbt *DBTest, tbl string) {
in := testValuer{"a_value"}
var out string
var rows *sql.Rows
dbt.mustExec("CREATE TABLE " + tbl + " (value VARCHAR(255)) CHARACTER SET utf8")
dbt.mustExec("INSERT INTO "+tbl+" VALUES (?)", in)
rows = dbt.mustQuery("SELECT value FROM " + tbl)
if rows.Next() {
rows.Scan(&out)
if in.value != out {
dbt.Errorf("Valuer: %v != %s", in, out)
}
} else {
dbt.Errorf("Valuer: no data")
}
rows.Close()
})
}
type testValuerWithValidation struct {
value string
}
func (tv testValuerWithValidation) Value() (driver.Value, error) {
if len(tv.value) == 0 {
return nil, fmt.Errorf("Invalid string valuer. Value must not be empty")
}
return tv.value, nil
}
func TestValuerWithValidation(t *testing.T) {
runTestsParallel(t, dsn, func(dbt *DBTest, tbl string) {
in := testValuerWithValidation{"a_value"}
var out string
var rows *sql.Rows
dbt.mustExec("CREATE TABLE " + tbl + " (value VARCHAR(255)) CHARACTER SET utf8")
dbt.mustExec("INSERT INTO "+tbl+" VALUES (?)", in)
rows = dbt.mustQuery("SELECT value FROM " + tbl)
defer rows.Close()
if rows.Next() {
rows.Scan(&out)
if in.value != out {
dbt.Errorf("Valuer: %v != %s", in, out)
}
} else {
dbt.Errorf("Valuer: no data")
}
if _, err := dbt.db.Exec("INSERT INTO "+tbl+" VALUES (?)", testValuerWithValidation{""}); err == nil {
dbt.Errorf("Failed to check valuer error")
}
if _, err := dbt.db.Exec("INSERT INTO "+tbl+" VALUES (?)", nil); err != nil {
dbt.Errorf("Failed to check nil")
}
if _, err := dbt.db.Exec("INSERT INTO "+tbl+" VALUES (?)", map[string]bool{}); err == nil {
dbt.Errorf("Failed to check not valuer")
}
})
}
type timeTests struct {
dbtype string
tlayout string
tests []timeTest
}
type timeTest struct {
s string // leading "!": do not use t as value in queries
t time.Time
}
type timeMode byte
func (t timeMode) String() string {
switch t {
case binaryString:
return "binary:string"
case binaryTime:
return "binary:time.Time"
case textString:
return "text:string"
}
panic("unsupported timeMode")
}
func (t timeMode) Binary() bool {
switch t {
case binaryString, binaryTime:
return true
}
return false
}
const (
binaryString timeMode = iota
binaryTime
textString
)
func (t timeTest) genQuery(dbtype string, mode timeMode) string {
var inner string
if mode.Binary() {
inner = "?"
} else {
inner = `"%s"`
}
return `SELECT cast(` + inner + ` as ` + dbtype + `)`
}
func (t timeTest) run(dbt *DBTest, dbtype, tlayout string, mode timeMode) {
var rows *sql.Rows
query := t.genQuery(dbtype, mode)
switch mode {
case binaryString:
rows = dbt.mustQuery(query, t.s)
case binaryTime:
rows = dbt.mustQuery(query, t.t)
case textString:
query = fmt.Sprintf(query, t.s)
rows = dbt.mustQuery(query)
default:
panic("unsupported mode")
}
defer rows.Close()
var err error
if !rows.Next() {
err = rows.Err()
if err == nil {
err = fmt.Errorf("no data")
}
dbt.Errorf("%s [%s]: %s", dbtype, mode, err)
return
}
var dst interface{}
err = rows.Scan(&dst)
if err != nil {
dbt.Errorf("%s [%s]: %s", dbtype, mode, err)
return
}
switch val := dst.(type) {
case []uint8:
str := string(val)
if str == t.s {
return
}
if mode.Binary() && dbtype == "DATETIME" && len(str) == 26 && str[:19] == t.s {
// a fix mainly for TravisCI:
// accept full microsecond resolution in result for DATETIME columns
// where the binary protocol was used
return
}
dbt.Errorf("%s [%s] to string: expected %q, got %q",
dbtype, mode,
t.s, str,
)
case time.Time:
if val == t.t {
return
}
dbt.Errorf("%s [%s] to string: expected %q, got %q",
dbtype, mode,
t.s, val.Format(tlayout),
)
default:
fmt.Printf("%#v\n", []interface{}{dbtype, tlayout, mode, t.s, t.t})
dbt.Errorf("%s [%s]: unhandled type %T (is '%v')",
dbtype, mode,
val, val,
)
}
}
func TestDateTime(t *testing.T) {
afterTime := func(t time.Time, d string) time.Time {
dur, err := time.ParseDuration(d)
if err != nil {
panic(err)
}
return t.Add(dur)
}
// NOTE: MySQL rounds DATETIME(x) up - but that's not included in the tests
format := "2006-01-02 15:04:05.999999"
t0 := time.Time{}
tstr0 := "0000-00-00 00:00:00.000000"
testcases := []timeTests{
{"DATE", format[:10], []timeTest{
{t: time.Date(2011, 11, 20, 0, 0, 0, 0, time.UTC)},
{t: t0, s: tstr0[:10]},
}},
{"DATETIME", format[:19], []timeTest{
{t: time.Date(2011, 11, 20, 21, 27, 37, 0, time.UTC)},
{t: t0, s: tstr0[:19]},
}},
{"DATETIME(0)", format[:21], []timeTest{
{t: time.Date(2011, 11, 20, 21, 27, 37, 0, time.UTC)},
{t: t0, s: tstr0[:19]},
}},
{"DATETIME(1)", format[:21], []timeTest{
{t: time.Date(2011, 11, 20, 21, 27, 37, 100000000, time.UTC)},
{t: t0, s: tstr0[:21]},
}},
{"DATETIME(6)", format, []timeTest{
{t: time.Date(2011, 11, 20, 21, 27, 37, 123456000, time.UTC)},
{t: t0, s: tstr0},
}},
{"TIME", format[11:19], []timeTest{
{t: afterTime(t0, "12345s")},
{s: "!-12:34:56"},
{s: "!-838:59:59"},
{s: "!838:59:59"},
{t: t0, s: tstr0[11:19]},
}},
{"TIME(0)", format[11:19], []timeTest{
{t: afterTime(t0, "12345s")},
{s: "!-12:34:56"},
{s: "!-838:59:59"},
{s: "!838:59:59"},
{t: t0, s: tstr0[11:19]},
}},
{"TIME(1)", format[11:21], []timeTest{
{t: afterTime(t0, "12345600ms")},
{s: "!-12:34:56.7"},
{s: "!-838:59:58.9"},
{s: "!838:59:58.9"},
{t: t0, s: tstr0[11:21]},
}},
{"TIME(6)", format[11:], []timeTest{
{t: afterTime(t0, "1234567890123000ns")},
{s: "!-12:34:56.789012"},
{s: "!-838:59:58.999999"},
{s: "!838:59:58.999999"},
{t: t0, s: tstr0[11:]},
}},
}
dsns := []string{
dsn + "&parseTime=true",
dsn + "&parseTime=false",
}
for _, testdsn := range dsns {
runTests(t, testdsn, func(dbt *DBTest) {
microsecsSupported := false
zeroDateSupported := false
var rows *sql.Rows
var err error
rows, err = dbt.db.Query(`SELECT cast("00:00:00.1" as TIME(1)) = "00:00:00.1"`)
if err == nil {
rows.Scan(&microsecsSupported)
rows.Close()
}
rows, err = dbt.db.Query(`SELECT cast("0000-00-00" as DATE) = "0000-00-00"`)
if err == nil {
rows.Scan(&zeroDateSupported)
rows.Close()
}
for _, setups := range testcases {
if t := setups.dbtype; !microsecsSupported && t[len(t)-1:] == ")" {
// skip fractional second tests if unsupported by server
continue
}
for _, setup := range setups.tests {
allowBinTime := true
if setup.s == "" {
// fill time string wherever Go can reliable produce it
setup.s = setup.t.Format(setups.tlayout)
} else if setup.s[0] == '!' {
// skip tests using setup.t as source in queries
allowBinTime = false
// fix setup.s - remove the "!"
setup.s = setup.s[1:]
}
if !zeroDateSupported && setup.s == tstr0[:len(setup.s)] {
// skip disallowed 0000-00-00 date
continue
}
setup.run(dbt, setups.dbtype, setups.tlayout, textString)
setup.run(dbt, setups.dbtype, setups.tlayout, binaryString)
if allowBinTime {
setup.run(dbt, setups.dbtype, setups.tlayout, binaryTime)
}
}
}
})
}
}
func TestTimestampMicros(t *testing.T) {
format := "2006-01-02 15:04:05.999999"
f0 := format[:19]
f1 := format[:21]
f6 := format[:26]
runTestsParallel(t, dsn, func(dbt *DBTest, tbl string) {
// check if microseconds are supported.
// Do not use timestamp(x) for that check - before 5.5.6, x would mean display width
// and not precision.
// Se last paragraph at http://dev.mysql.com/doc/refman/5.6/en/fractional-seconds.html
microsecsSupported := false
if rows, err := dbt.db.Query(`SELECT cast("00:00:00.1" as TIME(1)) = "00:00:00.1"`); err == nil {
rows.Scan(&microsecsSupported)
rows.Close()
}
if !microsecsSupported {
// skip test
return
}
_, err := dbt.db.Exec(`
CREATE TABLE ` + tbl + ` (
value0 TIMESTAMP NOT NULL DEFAULT '` + f0 + `',
value1 TIMESTAMP(1) NOT NULL DEFAULT '` + f1 + `',
value6 TIMESTAMP(6) NOT NULL DEFAULT '` + f6 + `'
)`,
)
if err != nil {
dbt.Error(err)
}
defer dbt.mustExec("DROP TABLE IF EXISTS " + tbl)
dbt.mustExec("INSERT INTO "+tbl+" SET value0=?, value1=?, value6=?", f0, f1, f6)
var res0, res1, res6 string
rows := dbt.mustQuery("SELECT * FROM " + tbl)
defer rows.Close()
if !rows.Next() {
dbt.Errorf("test contained no selectable values")
}
err = rows.Scan(&res0, &res1, &res6)
if err != nil {
dbt.Error(err)
}
if res0 != f0 {
dbt.Errorf("expected %q, got %q", f0, res0)
}
if res1 != f1 {
dbt.Errorf("expected %q, got %q", f1, res1)
}
if res6 != f6 {
dbt.Errorf("expected %q, got %q", f6, res6)
}
})
}
func TestNULL(t *testing.T) {
runTestsParallel(t, dsn, func(dbt *DBTest, tbl string) {
nullStmt, err := dbt.db.Prepare("SELECT NULL")
if err != nil {
dbt.Fatal(err)
}
defer nullStmt.Close()
nonNullStmt, err := dbt.db.Prepare("SELECT 1")
if err != nil {
dbt.Fatal(err)
}
defer nonNullStmt.Close()
// NullBool
var nb sql.NullBool
// Invalid
if err = nullStmt.QueryRow().Scan(&nb); err != nil {
dbt.Fatal(err)
}
if nb.Valid {
dbt.Error("valid NullBool which should be invalid")
}
// Valid
if err = nonNullStmt.QueryRow().Scan(&nb); err != nil {
dbt.Fatal(err)
}
if !nb.Valid {
dbt.Error("invalid NullBool which should be valid")
} else if nb.Bool != true {
dbt.Errorf("Unexpected NullBool value: %t (should be true)", nb.Bool)
}
// NullFloat64
var nf sql.NullFloat64
// Invalid
if err = nullStmt.QueryRow().Scan(&nf); err != nil {
dbt.Fatal(err)
}
if nf.Valid {
dbt.Error("valid NullFloat64 which should be invalid")
}
// Valid
if err = nonNullStmt.QueryRow().Scan(&nf); err != nil {
dbt.Fatal(err)
}
if !nf.Valid {
dbt.Error("invalid NullFloat64 which should be valid")
} else if nf.Float64 != float64(1) {
dbt.Errorf("unexpected NullFloat64 value: %f (should be 1.0)", nf.Float64)
}
// NullInt64
var ni sql.NullInt64
// Invalid
if err = nullStmt.QueryRow().Scan(&ni); err != nil {
dbt.Fatal(err)
}
if ni.Valid {
dbt.Error("valid NullInt64 which should be invalid")
}
// Valid
if err = nonNullStmt.QueryRow().Scan(&ni); err != nil {
dbt.Fatal(err)
}
if !ni.Valid {
dbt.Error("invalid NullInt64 which should be valid")
} else if ni.Int64 != int64(1) {
dbt.Errorf("unexpected NullInt64 value: %d (should be 1)", ni.Int64)
}
// NullString
var ns sql.NullString
// Invalid
if err = nullStmt.QueryRow().Scan(&ns); err != nil {
dbt.Fatal(err)
}
if ns.Valid {
dbt.Error("valid NullString which should be invalid")
}
// Valid
if err = nonNullStmt.QueryRow().Scan(&ns); err != nil {
dbt.Fatal(err)
}
if !ns.Valid {
dbt.Error("invalid NullString which should be valid")
} else if ns.String != `1` {
dbt.Error("unexpected NullString value:" + ns.String + " (should be `1`)")
}
// nil-bytes
var b []byte
// Read nil
if err = nullStmt.QueryRow().Scan(&b); err != nil {
dbt.Fatal(err)
}
if b != nil {
dbt.Error("non-nil []byte which should be nil")
}
// Read non-nil
if err = nonNullStmt.QueryRow().Scan(&b); err != nil {
dbt.Fatal(err)
}
if b == nil {
dbt.Error("nil []byte which should be non-nil")
}
// Insert nil
b = nil
success := false
if err = dbt.db.QueryRow("SELECT ? IS NULL", b).Scan(&success); err != nil {
dbt.Fatal(err)
}
if !success {
dbt.Error("inserting []byte(nil) as NULL failed")
}
// Check input==output with input==nil
b = nil
if err = dbt.db.QueryRow("SELECT ?", b).Scan(&b); err != nil {
dbt.Fatal(err)
}
if b != nil {
dbt.Error("non-nil echo from nil input")
}
// Check input==output with input!=nil
b = []byte("")
if err = dbt.db.QueryRow("SELECT ?", b).Scan(&b); err != nil {
dbt.Fatal(err)
}
if b == nil {
dbt.Error("nil echo from non-nil input")
}
// Insert NULL
dbt.mustExec("CREATE TABLE " + tbl + " (dummmy1 int, value int, dummy2 int)")
dbt.mustExec("INSERT INTO "+tbl+" VALUES (?, ?, ?)", 1, nil, 2)
var out interface{}
rows := dbt.mustQuery("SELECT * FROM " + tbl)
defer rows.Close()
if rows.Next() {
rows.Scan(&out)
if out != nil {
dbt.Errorf("%v != nil", out)
}
} else {
dbt.Error("no data")
}
})
}
func TestUint64(t *testing.T) {
const (
u0 = uint64(0)
uall = ^u0
uhigh = uall >> 1
utop = ^uhigh
s0 = int64(0)
sall = ^s0
shigh = int64(uhigh)
stop = ^shigh
)
runTestsParallel(t, dsn, func(dbt *DBTest, _ string) {
stmt, err := dbt.db.Prepare(`SELECT ?, ?, ? ,?, ?, ?, ?, ?`)
if err != nil {
dbt.Fatal(err)
}
defer stmt.Close()
row := stmt.QueryRow(
u0, uhigh, utop, uall,
s0, shigh, stop, sall,
)
var ua, ub, uc, ud uint64
var sa, sb, sc, sd int64
err = row.Scan(&ua, &ub, &uc, &ud, &sa, &sb, &sc, &sd)
if err != nil {
dbt.Fatal(err)
}
switch {
case ua != u0,
ub != uhigh,
uc != utop,
ud != uall,
sa != s0,
sb != shigh,
sc != stop,
sd != sall:
dbt.Fatal("unexpected result value")
}
})
}
func TestLongData(t *testing.T) {
runTests(t, dsn+"&maxAllowedPacket=0", func(dbt *DBTest) {
var maxAllowedPacketSize int
err := dbt.db.QueryRow("select @@max_allowed_packet").Scan(&maxAllowedPacketSize)
if err != nil {
dbt.Fatal(err)
}
maxAllowedPacketSize--
// don't get too ambitious
if maxAllowedPacketSize > 1<<25 {
maxAllowedPacketSize = 1 << 25
}
dbt.mustExec("CREATE TABLE test (value LONGBLOB)")
in := strings.Repeat(`a`, maxAllowedPacketSize+1)
var out string
var rows *sql.Rows
// Long text data
const nonDataQueryLen = 28 // length query w/o value
inS := in[:maxAllowedPacketSize-nonDataQueryLen]
dbt.mustExec("INSERT INTO test VALUES('" + inS + "')")
rows = dbt.mustQuery("SELECT value FROM test")
defer rows.Close()
if rows.Next() {
rows.Scan(&out)
if inS != out {
dbt.Fatalf("LONGBLOB: length in: %d, length out: %d", len(inS), len(out))
}
if rows.Next() {
dbt.Error("LONGBLOB: unexpected row")
}
} else {
dbt.Fatalf("LONGBLOB: no data")
}
// Empty table
dbt.mustExec("TRUNCATE TABLE test")
// Long binary data
dbt.mustExec("INSERT INTO test VALUES(?)", in)
rows = dbt.mustQuery("SELECT value FROM test WHERE 1=?", 1)
defer rows.Close()
if rows.Next() {
rows.Scan(&out)
if in != out {
dbt.Fatalf("LONGBLOB: length in: %d, length out: %d", len(in), len(out))
}
if rows.Next() {
dbt.Error("LONGBLOB: unexpected row")
}
} else {
if err = rows.Err(); err != nil {
dbt.Fatalf("LONGBLOB: no data (err: %s)", err.Error())
} else {
dbt.Fatal("LONGBLOB: no data (err: <nil>)")
}
}
})
}
func TestLoadData(t *testing.T) {
runTests(t, dsn, func(dbt *DBTest) {
verifyLoadDataResult := func() {
rows, err := dbt.db.Query("SELECT * FROM test")
if err != nil {
dbt.Fatal(err.Error())
}
i := 0
values := [4]string{
"a string",
"a string containing a \t",
"a string containing a \n",
"a string containing both \t\n",
}
var id int
var value string
for rows.Next() {
i++
err = rows.Scan(&id, &value)
if err != nil {
dbt.Fatal(err.Error())
}
if i != id {
dbt.Fatalf("%d != %d", i, id)
}
if values[i-1] != value {
dbt.Fatalf("%q != %q", values[i-1], value)
}
}
err = rows.Err()
if err != nil {
dbt.Fatal(err.Error())
}
if i != 4 {
dbt.Fatalf("rows count mismatch. Got %d, want 4", i)
}
}
dbt.db.Exec("DROP TABLE IF EXISTS test")
dbt.mustExec("CREATE TABLE test (id INT NOT NULL PRIMARY KEY, value TEXT NOT NULL) CHARACTER SET utf8")
// Local File
file, err := os.CreateTemp("", "gotest")
defer os.Remove(file.Name())
if err != nil {
dbt.Fatal(err)
}
RegisterLocalFile(file.Name())
// Try first with empty file
dbt.mustExec(fmt.Sprintf("LOAD DATA LOCAL INFILE %q INTO TABLE test", file.Name()))
var count int
err = dbt.db.QueryRow("SELECT COUNT(*) FROM test").Scan(&count)
if err != nil {
dbt.Fatal(err.Error())
}
if count != 0 {
dbt.Fatalf("unexpected row count: got %d, want 0", count)
}
// Then fill File with data and try to load it
file.WriteString("1\ta string\n2\ta string containing a \\t\n3\ta string containing a \\n\n4\ta string containing both \\t\\n\n")
file.Close()
dbt.mustExec(fmt.Sprintf("LOAD DATA LOCAL INFILE %q INTO TABLE test", file.Name()))
verifyLoadDataResult()
// Try with non-existing file
_, err = dbt.db.Exec("LOAD DATA LOCAL INFILE 'doesnotexist' INTO TABLE test")
if err == nil {
dbt.Fatal("load non-existent file didn't fail")
} else if err.Error() != "local file 'doesnotexist' is not registered" {
dbt.Fatal(err.Error())
}
// Empty table
dbt.mustExec("TRUNCATE TABLE test")
// Reader
RegisterReaderHandler("test", func() io.Reader {
file, err = os.Open(file.Name())
if err != nil {
dbt.Fatal(err)
}
return file
})
dbt.mustExec("LOAD DATA LOCAL INFILE 'Reader::test' INTO TABLE test")
verifyLoadDataResult()
// negative test
_, err = dbt.db.Exec("LOAD DATA LOCAL INFILE 'Reader::doesnotexist' INTO TABLE test")
if err == nil {
dbt.Fatal("load non-existent Reader didn't fail")
} else if err.Error() != "reader 'doesnotexist' is not registered" {
dbt.Fatal(err.Error())
}
})
}
func TestFoundRows1(t *testing.T) {
runTestsParallel(t, dsn, func(dbt *DBTest, tbl string) {
dbt.mustExec("CREATE TABLE " + tbl + " (id INT NOT NULL ,data INT NOT NULL)")
dbt.mustExec("INSERT INTO " + tbl + " (id, data) VALUES (0, 0),(0, 0),(1, 0),(1, 0),(1, 1)")
res := dbt.mustExec("UPDATE " + tbl + " SET data = 1 WHERE id = 0")
count, err := res.RowsAffected()
if err != nil {
dbt.Fatalf("res.RowsAffected() returned error: %s", err.Error())
}
if count != 2 {
dbt.Fatalf("Expected 2 affected rows, got %d", count)
}
res = dbt.mustExec("UPDATE " + tbl + " SET data = 1 WHERE id = 1")
count, err = res.RowsAffected()
if err != nil {
dbt.Fatalf("res.RowsAffected() returned error: %s", err.Error())
}
if count != 2 {
dbt.Fatalf("Expected 2 affected rows, got %d", count)
}
})
}
func TestFoundRows2(t *testing.T) {
runTestsParallel(t, dsn+"&clientFoundRows=true", func(dbt *DBTest, tbl string) {
dbt.mustExec("CREATE TABLE " + tbl + " (id INT NOT NULL ,data INT NOT NULL)")
dbt.mustExec("INSERT INTO " + tbl + " (id, data) VALUES (0, 0),(0, 0),(1, 0),(1, 0),(1, 1)")
res := dbt.mustExec("UPDATE " + tbl + " SET data = 1 WHERE id = 0")
count, err := res.RowsAffected()
if err != nil {
dbt.Fatalf("res.RowsAffected() returned error: %s", err.Error())
}
if count != 2 {
dbt.Fatalf("Expected 2 matched rows, got %d", count)
}
res = dbt.mustExec("UPDATE " + tbl + " SET data = 1 WHERE id = 1")
count, err = res.RowsAffected()
if err != nil {
dbt.Fatalf("res.RowsAffected() returned error: %s", err.Error())
}
if count != 3 {
dbt.Fatalf("Expected 3 matched rows, got %d", count)
}
})
}
func TestTLS(t *testing.T) {
tlsTestReq := func(dbt *DBTest) {
if err := dbt.db.Ping(); err != nil {
if err == ErrNoTLS {
dbt.Skip("server does not support TLS")
} else {
dbt.Fatalf("error on Ping: %s", err.Error())
}
}
rows := dbt.mustQuery("SHOW STATUS LIKE 'Ssl_cipher'")
defer rows.Close()
var variable, value *sql.RawBytes
for rows.Next() {
if err := rows.Scan(&variable, &value); err != nil {
dbt.Fatal(err.Error())
}
if (*value == nil) || (len(*value) == 0) {
dbt.Fatalf("no Cipher")
} else {
dbt.Logf("Cipher: %s", *value)
}
}
}
tlsTestOpt := func(dbt *DBTest) {
if err := dbt.db.Ping(); err != nil {
dbt.Fatalf("error on Ping: %s", err.Error())
}
}
runTests(t, dsn+"&tls=preferred", tlsTestOpt)
runTests(t, dsn+"&tls=skip-verify", tlsTestReq)
// Verify that registering / using a custom cfg works
RegisterTLSConfig("custom-skip-verify", &tls.Config{
InsecureSkipVerify: true,
})
runTests(t, dsn+"&tls=custom-skip-verify", tlsTestReq)
}
func TestReuseClosedConnection(t *testing.T) {
// this test does not use sql.database, it uses the driver directly
if !available {
t.Skipf("MySQL server not running on %s", netAddr)
}
md := &MySQLDriver{}
conn, err := md.Open(dsn)
if err != nil {
t.Fatalf("error connecting: %s", err.Error())
}
stmt, err := conn.Prepare("DO 1")
if err != nil {
t.Fatalf("error preparing statement: %s", err.Error())
}
//lint:ignore SA1019 this is a test
_, err = stmt.Exec(nil)
if err != nil {
t.Fatalf("error executing statement: %s", err.Error())
}
err = conn.Close()
if err != nil {
t.Fatalf("error closing connection: %s", err.Error())
}
defer func() {
if err := recover(); err != nil {
t.Errorf("panic after reusing a closed connection: %v", err)
}
}()
//lint:ignore SA1019 this is a test
_, err = stmt.Exec(nil)
if err != nil && err != driver.ErrBadConn {
t.Errorf("unexpected error '%s', expected '%s'",
err.Error(), driver.ErrBadConn.Error())
}
}
func TestCharset(t *testing.T) {
if !available {
t.Skipf("MySQL server not running on %s", netAddr)
}
mustSetCharset := func(charsetParam, expected string) {
runTests(t, dsn+"&"+charsetParam, func(dbt *DBTest) {
rows := dbt.mustQuery("SELECT @@character_set_connection")
defer rows.Close()
if !rows.Next() {
dbt.Fatalf("error getting connection charset: %s", rows.Err())
}
var got string
rows.Scan(&got)
if got != expected {
dbt.Fatalf("expected connection charset %s but got %s", expected, got)
}
})
}
// non utf8 test
mustSetCharset("charset=ascii", "ascii")
// when the first charset is invalid, use the second
mustSetCharset("charset=none,utf8mb4", "utf8mb4")
// when the first charset is valid, use it
mustSetCharset("charset=ascii,utf8mb4", "ascii")
mustSetCharset("charset=utf8mb4,ascii", "utf8mb4")
}
func TestFailingCharset(t *testing.T) {
runTestsParallel(t, dsn+"&charset=none", func(dbt *DBTest, _ string) {
// run query to really establish connection...
_, err := dbt.db.Exec("SELECT 1")
if err == nil {
dbt.db.Close()
t.Fatalf("connection must not succeed without a valid charset")
}
})
}
func TestCollation(t *testing.T) {
if !available {
t.Skipf("MySQL server not running on %s", netAddr)
}
defaultCollation := "utf8mb4_general_ci"
testCollations := []string{
"", // do not set
defaultCollation, // driver default
"latin1_general_ci",
"binary",
"utf8mb4_unicode_ci",
"cp1257_bin",
}
for _, collation := range testCollations {
var expected, tdsn string
if collation != "" {
tdsn = dsn + "&collation=" + collation
expected = collation
} else {
tdsn = dsn
expected = defaultCollation
}
runTests(t, tdsn, func(dbt *DBTest) {
var got string
if err := dbt.db.QueryRow("SELECT @@collation_connection").Scan(&got); err != nil {
dbt.Fatal(err)
}
if got != expected {
dbt.Fatalf("expected connection collation %s but got %s", expected, got)
}
})
}
}
func TestColumnsWithAlias(t *testing.T) {
runTestsParallel(t, dsn+"&columnsWithAlias=true", func(dbt *DBTest, _ string) {
rows := dbt.mustQuery("SELECT 1 AS A")
defer rows.Close()
cols, _ := rows.Columns()
if len(cols) != 1 {
t.Fatalf("expected 1 column, got %d", len(cols))
}
if cols[0] != "A" {
t.Fatalf("expected column name \"A\", got \"%s\"", cols[0])
}
rows = dbt.mustQuery("SELECT * FROM (SELECT 1 AS one) AS A")
defer rows.Close()
cols, _ = rows.Columns()
if len(cols) != 1 {
t.Fatalf("expected 1 column, got %d", len(cols))
}
if cols[0] != "A.one" {
t.Fatalf("expected column name \"A.one\", got \"%s\"", cols[0])
}
})
}
func TestRawBytesResultExceedsBuffer(t *testing.T) {
runTestsParallel(t, dsn, func(dbt *DBTest, _ string) {
// defaultBufSize from buffer.go
expected := strings.Repeat("abc", defaultBufSize)
rows := dbt.mustQuery("SELECT '" + expected + "'")
defer rows.Close()
if !rows.Next() {
dbt.Error("expected result, got none")
}
var result sql.RawBytes
rows.Scan(&result)
if expected != string(result) {
dbt.Error("result did not match expected value")
}
})
}
func TestTimezoneConversion(t *testing.T) {
zones := []string{"UTC", "US/Central", "US/Pacific", "Local"}
// Regression test for timezone handling
tzTest := func(dbt *DBTest) {
// Create table
dbt.mustExec("CREATE TABLE test (ts TIMESTAMP)")
// Insert local time into database (should be converted)
usCentral, _ := time.LoadLocation("US/Central")
reftime := time.Date(2014, 05, 30, 18, 03, 17, 0, time.UTC).In(usCentral)
dbt.mustExec("INSERT INTO test VALUE (?)", reftime)
// Retrieve time from DB
rows := dbt.mustQuery("SELECT ts FROM test")
defer rows.Close()
if !rows.Next() {
dbt.Fatal("did not get any rows out")
}
var dbTime time.Time
err := rows.Scan(&dbTime)
if err != nil {
dbt.Fatal("Err", err)
}
// Check that dates match
if reftime.Unix() != dbTime.Unix() {
dbt.Errorf("times do not match.\n")
dbt.Errorf(" Now(%v)=%v\n", usCentral, reftime)
dbt.Errorf(" Now(UTC)=%v\n", dbTime)
}
}
for _, tz := range zones {
runTests(t, dsn+"&parseTime=true&loc="+url.QueryEscape(tz), tzTest)
}
}
// Special cases
func TestRowsClose(t *testing.T) {
runTestsParallel(t, dsn, func(dbt *DBTest, _ string) {
rows, err := dbt.db.Query("SELECT 1")
if err != nil {
dbt.Fatal(err)
}
err = rows.Close()
if err != nil {
dbt.Fatal(err)
}
if rows.Next() {
dbt.Fatal("unexpected row after rows.Close()")
}
err = rows.Err()
if err != nil {
dbt.Fatal(err)
}
})
}
// dangling statements
// http://code.google.com/p/go/issues/detail?id=3865
func TestCloseStmtBeforeRows(t *testing.T) {
runTestsParallel(t, dsn, func(dbt *DBTest, _ string) {
stmt, err := dbt.db.Prepare("SELECT 1")
if err != nil {
dbt.Fatal(err)
}
rows, err := stmt.Query()
if err != nil {
stmt.Close()
dbt.Fatal(err)
}
defer rows.Close()
err = stmt.Close()
if err != nil {
dbt.Fatal(err)
}
if !rows.Next() {
dbt.Fatal("getting row failed")
} else {
err = rows.Err()
if err != nil {
dbt.Fatal(err)
}
var out bool
err = rows.Scan(&out)
if err != nil {
dbt.Fatalf("error on rows.Scan(): %s", err.Error())
}
if out != true {
dbt.Errorf("true != %t", out)
}
}
})
}
// It is valid to have multiple Rows for the same Stmt
// http://code.google.com/p/go/issues/detail?id=3734
func TestStmtMultiRows(t *testing.T) {
runTestsParallel(t, dsn, func(dbt *DBTest, _ string) {
stmt, err := dbt.db.Prepare("SELECT 1 UNION SELECT 0")
if err != nil {
dbt.Fatal(err)
}
rows1, err := stmt.Query()
if err != nil {
stmt.Close()
dbt.Fatal(err)
}
defer rows1.Close()
rows2, err := stmt.Query()
if err != nil {
stmt.Close()
dbt.Fatal(err)
}
defer rows2.Close()
var out bool
// 1
if !rows1.Next() {
dbt.Fatal("first rows1.Next failed")
} else {
err = rows1.Err()
if err != nil {
dbt.Fatal(err)
}
err = rows1.Scan(&out)
if err != nil {
dbt.Fatalf("error on rows.Scan(): %s", err.Error())
}
if out != true {
dbt.Errorf("true != %t", out)
}
}
if !rows2.Next() {
dbt.Fatal("first rows2.Next failed")
} else {
err = rows2.Err()
if err != nil {
dbt.Fatal(err)
}
err = rows2.Scan(&out)
if err != nil {
dbt.Fatalf("error on rows.Scan(): %s", err.Error())
}
if out != true {
dbt.Errorf("true != %t", out)
}
}
// 2
if !rows1.Next() {
dbt.Fatal("second rows1.Next failed")
} else {
err = rows1.Err()
if err != nil {
dbt.Fatal(err)
}
err = rows1.Scan(&out)
if err != nil {
dbt.Fatalf("error on rows.Scan(): %s", err.Error())
}
if out != false {
dbt.Errorf("false != %t", out)
}
if rows1.Next() {
dbt.Fatal("unexpected row on rows1")
}
err = rows1.Close()
if err != nil {
dbt.Fatal(err)
}
}
if !rows2.Next() {
dbt.Fatal("second rows2.Next failed")
} else {
err = rows2.Err()
if err != nil {
dbt.Fatal(err)
}
err = rows2.Scan(&out)
if err != nil {
dbt.Fatalf("error on rows.Scan(): %s", err.Error())
}
if out != false {
dbt.Errorf("false != %t", out)
}
if rows2.Next() {
dbt.Fatal("unexpected row on rows2")
}
err = rows2.Close()
if err != nil {
dbt.Fatal(err)
}
}
})
}
// Regression test for
// * more than 32 NULL parameters (issue 209)
// * more parameters than fit into the buffer (issue 201)
// * parameters * 64 > max_allowed_packet (issue 734)
func TestPreparedManyCols(t *testing.T) {
numParams := 65535
runTests(t, dsn, func(dbt *DBTest) {
query := "SELECT ?" + strings.Repeat(",?", numParams-1)
stmt, err := dbt.db.Prepare(query)
if err != nil {
dbt.Fatal(err)
}
defer stmt.Close()
// create more parameters than fit into the buffer
// which will take nil-values
params := make([]interface{}, numParams)
rows, err := stmt.Query(params...)
if err != nil {
dbt.Fatal(err)
}
rows.Close()
// Create 0byte string which we can't send via STMT_LONG_DATA.
for i := 0; i < numParams; i++ {
params[i] = ""
}
rows, err = stmt.Query(params...)
if err != nil {
dbt.Fatal(err)
}
rows.Close()
})
}
func TestConcurrent(t *testing.T) {
if enabled, _ := readBool(os.Getenv("MYSQL_TEST_CONCURRENT")); !enabled {
t.Skip("MYSQL_TEST_CONCURRENT env var not set")
}
runTests(t, dsn, func(dbt *DBTest) {
// var version string
// if err := dbt.db.QueryRow("SELECT @@version").Scan(&version); err != nil {
// dbt.Fatal(err)
// }
// 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 {
dbt.Fatalf("%s", err.Error())
}
dbt.Logf("testing up to %d concurrent connections \r\n", max)
var remaining, succeeded int32 = int32(max), 0
var wg sync.WaitGroup
wg.Add(max)
var fatalError string
var once sync.Once
fatalf := func(s string, vals ...interface{}) {
once.Do(func() {
fatalError = fmt.Sprintf(s, vals...)
})
}
for i := 0; i < max; i++ {
go func(id int) {
defer wg.Done()
tx, err := dbt.db.Begin()
if err != nil {
if err.Error() != "Error 1040: Too many connections" {
fatalf("error on conn %d: %s", id, err.Error())
}
return
}
// keep the connection busy until all connections are open
for atomic.AddInt32(&remaining, -1) > 0 {
if _, err = tx.Exec("DO 1"); err != nil {
fatalf("error on conn %d: %s", id, err.Error())
return
}
}
if err = tx.Commit(); err != nil {
fatalf("error on conn %d: %s", id, err.Error())
return
}
// everything went fine with this connection
atomic.AddInt32(&succeeded, 1)
}(i)
}
// wait until all connections are open
wg.Wait()
if fatalError != "" {
dbt.Fatal(fatalError)
}
dbt.Logf("reached %d concurrent connections\r\n", succeeded)
})
}
func testDialError(t *testing.T, dialErr error, expectErr error) {
RegisterDialContext("mydial", func(ctx context.Context, addr string) (net.Conn, error) {
return nil, dialErr
})
db, err := sql.Open(driverNameTest, fmt.Sprintf("%s:%s@mydial(%s)/%s?timeout=30s", user, pass, addr, dbname))
if err != nil {
t.Fatalf("error connecting: %s", err.Error())
}
defer db.Close()
_, err = db.Exec("DO 1")
if err != expectErr {
t.Fatalf("was expecting %s. Got: %s", dialErr, err)
}
}
func TestDialUnknownError(t *testing.T) {
testErr := fmt.Errorf("test")
testDialError(t, testErr, testErr)
}
func TestDialNonRetryableNetErr(t *testing.T) {
testErr := netErrorMock{}
testDialError(t, testErr, testErr)
}
func TestDialTemporaryNetErr(t *testing.T) {
testErr := netErrorMock{temporary: true}
testDialError(t, testErr, testErr)
}
// Tests custom dial functions
func TestCustomDial(t *testing.T) {
if !available {
t.Skipf("MySQL server not running on %s", netAddr)
}
// our custom dial function which just wraps net.Dial here
RegisterDialContext("mydial", func(ctx context.Context, addr string) (net.Conn, error) {
var d net.Dialer
return d.DialContext(ctx, prot, addr)
})
db, err := sql.Open(driverNameTest, fmt.Sprintf("%s:%s@mydial(%s)/%s?timeout=30s", user, pass, addr, dbname))
if err != nil {
t.Fatalf("error connecting: %s", err.Error())
}
defer db.Close()
if _, err = db.Exec("DO 1"); err != nil {
t.Fatalf("connection failed: %s", err.Error())
}
}
func TestBeforeConnect(t *testing.T) {
if !available {
t.Skipf("MySQL server not running on %s", netAddr)
}
// dbname is set in the BeforeConnect handle
cfg, err := ParseDSN(fmt.Sprintf("%s:%s@%s/%s?timeout=30s", user, pass, netAddr, "_"))
if err != nil {
t.Fatalf("error parsing DSN: %v", err)
}
cfg.Apply(BeforeConnect(func(ctx context.Context, c *Config) error {
c.DBName = dbname
return nil
}))
connector, err := NewConnector(cfg)
if err != nil {
t.Fatalf("error creating connector: %v", err)
}
db := sql.OpenDB(connector)
defer db.Close()
var connectedDb string
err = db.QueryRow("SELECT DATABASE();").Scan(&connectedDb)
if err != nil {
t.Fatalf("error executing query: %v", err)
}
if connectedDb != dbname {
t.Fatalf("expected to connect to DB %s, but connected to %s instead", dbname, connectedDb)
}
}
func TestSQLInjection(t *testing.T) {
createTest := func(arg string) func(dbt *DBTest) {
return func(dbt *DBTest) {
dbt.mustExec("CREATE TABLE test (v INTEGER)")
dbt.mustExec("INSERT INTO test VALUES (?)", 1)
var v int
// NULL can't be equal to anything, the idea here is to inject query so it returns row
// This test verifies that escapeQuotes and escapeBackslash are working properly
err := dbt.db.QueryRow("SELECT v FROM test WHERE NULL = ?", arg).Scan(&v)
if err == sql.ErrNoRows {
return // success, sql injection failed
} else if err == nil {
dbt.Errorf("sql injection successful with arg: %s", arg)
} else {
dbt.Errorf("error running query with arg: %s; err: %s", arg, err.Error())
}
}
}
dsns := []string{
dsn,
dsn + "&sql_mode='NO_BACKSLASH_ESCAPES'",
}
for _, testdsn := range dsns {
runTests(t, testdsn, createTest("1 OR 1=1"))
runTests(t, testdsn, createTest("' OR '1'='1"))
}
}
// Test if inserted data is correctly retrieved after being escaped
func TestInsertRetrieveEscapedData(t *testing.T) {
testData := func(dbt *DBTest) {
dbt.mustExec("CREATE TABLE test (v VARCHAR(255))")
// All sequences that are escaped by escapeQuotes and escapeBackslash
v := "foo \x00\n\r\x1a\"'\\"
dbt.mustExec("INSERT INTO test VALUES (?)", v)
var out string
err := dbt.db.QueryRow("SELECT v FROM test").Scan(&out)
if err != nil {
dbt.Fatalf("%s", err.Error())
}
if out != v {
dbt.Errorf("%q != %q", out, v)
}
}
dsns := []string{
dsn,
dsn + "&sql_mode='NO_BACKSLASH_ESCAPES'",
}
for _, testdsn := range dsns {
runTests(t, testdsn, testData)
}
}
func TestUnixSocketAuthFail(t *testing.T) {
runTests(t, dsn, func(dbt *DBTest) {
// Save the current logger so we can restore it.
oldLogger := defaultLogger
// Set a new logger so we can capture its output.
buffer := bytes.NewBuffer(make([]byte, 0, 64))
newLogger := log.New(buffer, "prefix: ", 0)
SetLogger(newLogger)
// Restore the logger.
defer SetLogger(oldLogger)
// Make a new DSN that uses the MySQL socket file and a bad password, which
// we can make by simply appending any character to the real password.
badPass := pass + "x"
socket := ""
if prot == "unix" {
socket = addr
} else {
// Get socket file from MySQL.
err := dbt.db.QueryRow("SELECT @@socket").Scan(&socket)
if err != nil {
t.Fatalf("error on SELECT @@socket: %s", err.Error())
}
}
t.Logf("socket: %s", socket)
badDSN := fmt.Sprintf("%s:%s@unix(%s)/%s?timeout=30s", user, badPass, socket, dbname)
db, err := sql.Open(driverNameTest, badDSN)
if err != nil {
t.Fatalf("error connecting: %s", err.Error())
}
defer db.Close()
// Connect to MySQL for real. This will cause an auth failure.
err = db.Ping()
if err == nil {
t.Error("expected Ping() to return an error")
}
// The driver should not log anything.
if actual := buffer.String(); actual != "" {
t.Errorf("expected no output, got %q", actual)
}
})
}
// See Issue #422
func TestInterruptBySignal(t *testing.T) {
runTestsWithMultiStatement(t, dsn, func(dbt *DBTest) {
dbt.mustExec(`
DROP PROCEDURE IF EXISTS test_signal;
CREATE PROCEDURE test_signal(ret INT)
BEGIN
SELECT ret;
SIGNAL SQLSTATE
'45001'
SET
MESSAGE_TEXT = "an error",
MYSQL_ERRNO = 45001;
END
`)
defer dbt.mustExec("DROP PROCEDURE test_signal")
var val int
// text protocol
rows, err := dbt.db.Query("CALL test_signal(42)")
if err != nil {
dbt.Fatalf("error on text query: %s", err.Error())
}
for rows.Next() {
if err := rows.Scan(&val); err != nil {
dbt.Error(err)
} else if val != 42 {
dbt.Errorf("expected val to be 42")
}
}
rows.Close()
// binary protocol
rows, err = dbt.db.Query("CALL test_signal(?)", 42)
if err != nil {
dbt.Fatalf("error on binary query: %s", err.Error())
}
for rows.Next() {
if err := rows.Scan(&val); err != nil {
dbt.Error(err)
} else if val != 42 {
dbt.Errorf("expected val to be 42")
}
}
rows.Close()
})
}
func TestColumnsReusesSlice(t *testing.T) {
rows := mysqlRows{
rs: resultSet{
columns: []mysqlField{
{
tableName: "test",
name: "A",
},
{
tableName: "test",
name: "B",
},
},
},
}
allocs := testing.AllocsPerRun(1, func() {
cols := rows.Columns()
if len(cols) != 2 {
t.Fatalf("expected 2 columns, got %d", len(cols))
}
})
if allocs != 0 {
t.Fatalf("expected 0 allocations, got %d", int(allocs))
}
if rows.rs.columnNames == nil {
t.Fatalf("expected columnNames to be set, got nil")
}
}
func TestRejectReadOnly(t *testing.T) {
runTests(t, dsn, func(dbt *DBTest) {
// Create Table
dbt.mustExec("CREATE TABLE test (value BOOL)")
// Set the session to read-only. We didn't set the `rejectReadOnly`
// option, so any writes after this should fail.
_, err := dbt.db.Exec("SET SESSION TRANSACTION READ ONLY")
// Error 1193: Unknown system variable 'TRANSACTION' => skip test,
// MySQL server version is too old
maybeSkip(t, err, 1193)
if _, err := dbt.db.Exec("DROP TABLE test"); err == nil {
t.Fatalf("writing to DB in read-only session without " +
"rejectReadOnly did not error")
}
// Set the session back to read-write so runTests() can properly clean
// up the table `test`.
dbt.mustExec("SET SESSION TRANSACTION READ WRITE")
})
// Enable the `rejectReadOnly` option.
runTests(t, dsn+"&rejectReadOnly=true", func(dbt *DBTest) {
// Create Table
dbt.mustExec("CREATE TABLE test (value BOOL)")
// Set the session to read only. Any writes after this should error on
// a driver.ErrBadConn, and cause `database/sql` to initiate a new
// connection.
dbt.mustExec("SET SESSION TRANSACTION READ ONLY")
// This would error, but `database/sql` should automatically retry on a
// new connection which is not read-only, and eventually succeed.
dbt.mustExec("DROP TABLE test")
})
}
func TestPing(t *testing.T) {
ctx := context.Background()
runTests(t, dsn, func(dbt *DBTest) {
if err := dbt.db.Ping(); err != nil {
dbt.fail("Ping", "Ping", err)
}
})
runTests(t, dsn, func(dbt *DBTest) {
conn, err := dbt.db.Conn(ctx)
if err != nil {
dbt.fail("db", "Conn", err)
}
// Check that affectedRows and insertIds are cleared after each call.
conn.Raw(func(conn interface{}) error {
c := conn.(*mysqlConn)
// Issue a query that sets affectedRows and insertIds.
q, err := c.Query(`SELECT 1`, nil)
if err != nil {
dbt.fail("Conn", "Query", err)
}
if got, want := c.result.affectedRows, []int64{0}; !reflect.DeepEqual(got, want) {
dbt.Fatalf("bad affectedRows: got %v, want=%v", got, want)
}
if got, want := c.result.insertIds, []int64{0}; !reflect.DeepEqual(got, want) {
dbt.Fatalf("bad insertIds: got %v, want=%v", got, want)
}
q.Close()
// Verify that Ping() clears both fields.
for i := 0; i < 2; i++ {
if err := c.Ping(ctx); err != nil {
dbt.fail("Pinger", "Ping", err)
}
if got, want := c.result.affectedRows, []int64(nil); !reflect.DeepEqual(got, want) {
t.Errorf("bad affectedRows: got %v, want=%v", got, want)
}
if got, want := c.result.insertIds, []int64(nil); !reflect.DeepEqual(got, want) {
t.Errorf("bad affectedRows: got %v, want=%v", got, want)
}
}
return nil
})
})
}
// See Issue #799
func TestEmptyPassword(t *testing.T) {
if !available {
t.Skipf("MySQL server not running on %s", netAddr)
}
dsn := fmt.Sprintf("%s:%s@%s/%s?timeout=30s", user, "", netAddr, dbname)
db, err := sql.Open(driverNameTest, dsn)
if err == nil {
defer db.Close()
err = db.Ping()
}
if pass == "" {
if err != nil {
t.Fatal(err.Error())
}
} else {
if err == nil {
t.Fatal("expected authentication error")
}
if !strings.HasPrefix(err.Error(), "Error 1045") {
t.Fatal(err.Error())
}
}
}
// static interface implementation checks of mysqlConn
var (
_ driver.ConnBeginTx = &mysqlConn{}
_ driver.ConnPrepareContext = &mysqlConn{}
_ driver.ExecerContext = &mysqlConn{}
_ driver.Pinger = &mysqlConn{}
_ driver.QueryerContext = &mysqlConn{}
)
// static interface implementation checks of mysqlStmt
var (
_ driver.StmtExecContext = &mysqlStmt{}
_ driver.StmtQueryContext = &mysqlStmt{}
)
// Ensure that all the driver interfaces are implemented
var (
// _ driver.RowsColumnTypeLength = &binaryRows{}
// _ driver.RowsColumnTypeLength = &textRows{}
_ driver.RowsColumnTypeDatabaseTypeName = &binaryRows{}
_ driver.RowsColumnTypeDatabaseTypeName = &textRows{}
_ driver.RowsColumnTypeNullable = &binaryRows{}
_ driver.RowsColumnTypeNullable = &textRows{}
_ driver.RowsColumnTypePrecisionScale = &binaryRows{}
_ driver.RowsColumnTypePrecisionScale = &textRows{}
_ driver.RowsColumnTypeScanType = &binaryRows{}
_ driver.RowsColumnTypeScanType = &textRows{}
_ driver.RowsNextResultSet = &binaryRows{}
_ driver.RowsNextResultSet = &textRows{}
)
func TestMultiResultSet(t *testing.T) {
type result struct {
values [][]int
columns []string
}
// checkRows is a helper test function to validate rows containing 3 result
// sets with specific values and columns. The basic query would look like this:
//
// SELECT 1 AS col1, 2 AS col2 UNION SELECT 3, 4;
// SELECT 0 UNION SELECT 1;
// SELECT 1 AS col1, 2 AS col2, 3 AS col3 UNION SELECT 4, 5, 6;
//
// to distinguish test cases the first string argument is put in front of
// every error or fatal message.
checkRows := func(desc string, rows *sql.Rows, dbt *DBTest) {
expected := []result{
{
values: [][]int{{1, 2}, {3, 4}},
columns: []string{"col1", "col2"},
},
{
values: [][]int{{1, 2, 3}, {4, 5, 6}},
columns: []string{"col1", "col2", "col3"},
},
}
var res1 result
for rows.Next() {
var res [2]int
if err := rows.Scan(&res[0], &res[1]); err != nil {
dbt.Fatal(err)
}
res1.values = append(res1.values, res[:])
}
cols, err := rows.Columns()
if err != nil {
dbt.Fatal(desc, err)
}
res1.columns = cols
if !reflect.DeepEqual(expected[0], res1) {
dbt.Error(desc, "want =", expected[0], "got =", res1)
}
if !rows.NextResultSet() {
dbt.Fatal(desc, "expected next result set")
}
// ignoring one result set
if !rows.NextResultSet() {
dbt.Fatal(desc, "expected next result set")
}
var res2 result
cols, err = rows.Columns()
if err != nil {
dbt.Fatal(desc, err)
}
res2.columns = cols
for rows.Next() {
var res [3]int
if err := rows.Scan(&res[0], &res[1], &res[2]); err != nil {
dbt.Fatal(desc, err)
}
res2.values = append(res2.values, res[:])
}
if !reflect.DeepEqual(expected[1], res2) {
dbt.Error(desc, "want =", expected[1], "got =", res2)
}
if rows.NextResultSet() {
dbt.Error(desc, "unexpected next result set")
}
if err := rows.Err(); err != nil {
dbt.Error(desc, err)
}
}
runTestsWithMultiStatement(t, dsn, func(dbt *DBTest) {
rows := dbt.mustQuery(`DO 1;
SELECT 1 AS col1, 2 AS col2 UNION SELECT 3, 4;
DO 1;
SELECT 0 UNION SELECT 1;
SELECT 1 AS col1, 2 AS col2, 3 AS col3 UNION SELECT 4, 5, 6;`)
defer rows.Close()
checkRows("query: ", rows, dbt)
})
runTestsWithMultiStatement(t, dsn, func(dbt *DBTest) {
queries := []string{
`
DROP PROCEDURE IF EXISTS test_mrss;
CREATE PROCEDURE test_mrss()
BEGIN
DO 1;
SELECT 1 AS col1, 2 AS col2 UNION SELECT 3, 4;
DO 1;
SELECT 0 UNION SELECT 1;
SELECT 1 AS col1, 2 AS col2, 3 AS col3 UNION SELECT 4, 5, 6;
END
`,
`
DROP PROCEDURE IF EXISTS test_mrss;
CREATE PROCEDURE test_mrss()
BEGIN
SELECT 1 AS col1, 2 AS col2 UNION SELECT 3, 4;
SELECT 0 UNION SELECT 1;
SELECT 1 AS col1, 2 AS col2, 3 AS col3 UNION SELECT 4, 5, 6;
END
`,
}
defer dbt.mustExec("DROP PROCEDURE IF EXISTS test_mrss")
for i, query := range queries {
dbt.mustExec(query)
stmt, err := dbt.db.Prepare("CALL test_mrss()")
if err != nil {
dbt.Fatalf("%v (i=%d)", err, i)
}
defer stmt.Close()
for j := 0; j < 2; j++ {
rows, err := stmt.Query()
if err != nil {
dbt.Fatalf("%v (i=%d) (j=%d)", err, i, j)
}
checkRows(fmt.Sprintf("prepared stmt query (i=%d) (j=%d): ", i, j), rows, dbt)
}
}
})
}
func TestMultiResultSetNoSelect(t *testing.T) {
runTestsWithMultiStatement(t, dsn, func(dbt *DBTest) {
rows := dbt.mustQuery("DO 1; DO 2;")
defer rows.Close()
if rows.Next() {
dbt.Error("unexpected row")
}
if rows.NextResultSet() {
dbt.Error("unexpected next result set")
}
if err := rows.Err(); err != nil {
dbt.Error("expected nil; got ", err)
}
})
}
func TestExecMultipleResults(t *testing.T) {
ctx := context.Background()
runTestsWithMultiStatement(t, dsn, func(dbt *DBTest) {
dbt.mustExec(`
CREATE TABLE test (
id INT NOT NULL AUTO_INCREMENT,
value VARCHAR(255),
PRIMARY KEY (id)
)`)
conn, err := dbt.db.Conn(ctx)
if err != nil {
t.Fatalf("failed to connect: %v", err)
}
conn.Raw(func(conn interface{}) error {
//lint:ignore SA1019 this is a test
ex := conn.(driver.Execer)
res, err := ex.Exec(`
INSERT INTO test (value) VALUES ('a'), ('b');
INSERT INTO test (value) VALUES ('c'), ('d'), ('e');
`, nil)
if err != nil {
t.Fatalf("insert statements failed: %v", err)
}
mres := res.(Result)
if got, want := mres.AllRowsAffected(), []int64{2, 3}; !reflect.DeepEqual(got, want) {
t.Errorf("bad AllRowsAffected: got %v, want=%v", got, want)
}
// For INSERTs containing multiple rows, LAST_INSERT_ID() returns the
// first inserted ID, not the last.
if got, want := mres.AllLastInsertIds(), []int64{1, 3}; !reflect.DeepEqual(got, want) {
t.Errorf("bad AllLastInsertIds: got %v, want %v", got, want)
}
return nil
})
})
}
// tests if rows are set in a proper state if some results were ignored before
// calling rows.NextResultSet.
func TestSkipResults(t *testing.T) {
runTestsParallel(t, dsn, func(dbt *DBTest, _ string) {
rows := dbt.mustQuery("SELECT 1, 2")
defer rows.Close()
if !rows.Next() {
dbt.Error("expected row")
}
if rows.NextResultSet() {
dbt.Error("unexpected next result set")
}
if err := rows.Err(); err != nil {
dbt.Error("expected nil; got ", err)
}
})
}
func TestQueryMultipleResults(t *testing.T) {
ctx := context.Background()
runTestsWithMultiStatement(t, dsn, func(dbt *DBTest) {
dbt.mustExec(`
CREATE TABLE test (
id INT NOT NULL AUTO_INCREMENT,
value VARCHAR(255),
PRIMARY KEY (id)
)`)
conn, err := dbt.db.Conn(ctx)
if err != nil {
t.Fatalf("failed to connect: %v", err)
}
conn.Raw(func(conn interface{}) error {
//lint:ignore SA1019 this is a test
qr := conn.(driver.Queryer)
c := conn.(*mysqlConn)
// Demonstrate that repeated queries reset the affectedRows
for i := 0; i < 2; i++ {
_, err := qr.Query(`
INSERT INTO test (value) VALUES ('a'), ('b');
INSERT INTO test (value) VALUES ('c'), ('d'), ('e');
`, nil)
if err != nil {
t.Fatalf("insert statements failed: %v", err)
}
if got, want := c.result.affectedRows, []int64{2, 3}; !reflect.DeepEqual(got, want) {
t.Errorf("bad affectedRows: got %v, want=%v", got, want)
}
}
return nil
})
})
}
func TestPingContext(t *testing.T) {
runTestsParallel(t, dsn, func(dbt *DBTest, _ string) {
ctx, cancel := context.WithCancel(context.Background())
cancel()
if err := dbt.db.PingContext(ctx); err != context.Canceled {
dbt.Errorf("expected context.Canceled, got %v", err)
}
})
}
func TestContextCancelExec(t *testing.T) {
runTestsParallel(t, dsn, func(dbt *DBTest, tbl string) {
dbt.mustExec("CREATE TABLE " + tbl + " (v INTEGER)")
ctx, cancel := context.WithCancel(context.Background())
// Delay execution for just a bit until db.ExecContext has begun.
defer time.AfterFunc(250*time.Millisecond, cancel).Stop()
// This query will be canceled.
startTime := time.Now()
if _, err := dbt.db.ExecContext(ctx, "INSERT INTO "+tbl+" VALUES (SLEEP(1))"); err != context.Canceled {
dbt.Errorf("expected context.Canceled, got %v", err)
}
if d := time.Since(startTime); d > 500*time.Millisecond {
dbt.Errorf("too long execution time: %s", d)
}
// Wait for the INSERT query to be done.
time.Sleep(time.Second)
// Check how many times the query is executed.
var v int
if err := dbt.db.QueryRow("SELECT COUNT(*) FROM " + tbl).Scan(&v); err != nil {
dbt.Fatalf("%s", err.Error())
}
if v != 1 { // TODO: need to kill the query, and v should be 0.
dbt.Skipf("[WARN] expected val to be 1, got %d", v)
}
// Context is already canceled, so error should come before execution.
if _, err := dbt.db.ExecContext(ctx, "INSERT INTO "+tbl+" VALUES (1)"); err == nil {
dbt.Error("expected error")
} else if err.Error() != "context canceled" {
dbt.Fatalf("unexpected error: %s", err)
}
// The second insert query will fail, so the table has no changes.
if err := dbt.db.QueryRow("SELECT COUNT(*) FROM " + tbl).Scan(&v); err != nil {
dbt.Fatalf("%s", err.Error())
}
if v != 1 {
dbt.Skipf("[WARN] expected val to be 1, got %d", v)
}
})
}
func TestContextCancelQuery(t *testing.T) {
runTestsParallel(t, dsn, func(dbt *DBTest, tbl string) {
dbt.mustExec("CREATE TABLE " + tbl + " (v INTEGER)")
ctx, cancel := context.WithCancel(context.Background())
// Delay execution for just a bit until db.ExecContext has begun.
defer time.AfterFunc(250*time.Millisecond, cancel).Stop()
// This query will be canceled.
startTime := time.Now()
if _, err := dbt.db.QueryContext(ctx, "INSERT INTO "+tbl+" VALUES (SLEEP(1))"); err != context.Canceled {
dbt.Errorf("expected context.Canceled, got %v", err)
}
if d := time.Since(startTime); d > 500*time.Millisecond {
dbt.Errorf("too long execution time: %s", d)
}
// Wait for the INSERT query to be done.
time.Sleep(time.Second)
// Check how many times the query is executed.
var v int
if err := dbt.db.QueryRow("SELECT COUNT(*) FROM " + tbl).Scan(&v); err != nil {
dbt.Fatalf("%s", err.Error())
}
if v != 1 { // TODO: need to kill the query, and v should be 0.
dbt.Skipf("[WARN] expected val to be 1, got %d", v)
}
// Context is already canceled, so error should come before execution.
if _, err := dbt.db.QueryContext(ctx, "INSERT INTO "+tbl+" VALUES (1)"); err != context.Canceled {
dbt.Errorf("expected context.Canceled, got %v", err)
}
// The second insert query will fail, so the table has no changes.
if err := dbt.db.QueryRow("SELECT COUNT(*) FROM " + tbl).Scan(&v); err != nil {
dbt.Fatalf("%s", err.Error())
}
if v != 1 {
dbt.Skipf("[WARN] expected val to be 1, got %d", v)
}
})
}
func TestContextCancelQueryRow(t *testing.T) {
runTestsParallel(t, dsn, func(dbt *DBTest, tbl string) {
dbt.mustExec("CREATE TABLE " + tbl + " (v INTEGER)")
dbt.mustExec("INSERT INTO " + tbl + " VALUES (1), (2), (3)")
ctx, cancel := context.WithCancel(context.Background())
rows, err := dbt.db.QueryContext(ctx, "SELECT v FROM "+tbl)
if err != nil {
dbt.Fatalf("%s", err.Error())
}
// the first row will be succeed.
var v int
if !rows.Next() {
dbt.Fatalf("unexpected end")
}
if err := rows.Scan(&v); err != nil {
dbt.Fatalf("%s", err.Error())
}
cancel()
// make sure the driver receives the cancel request.
time.Sleep(100 * time.Millisecond)
if rows.Next() {
dbt.Errorf("expected end, but not")
}
if err := rows.Err(); err != context.Canceled {
dbt.Errorf("expected context.Canceled, got %v", err)
}
})
}
func TestContextCancelPrepare(t *testing.T) {
runTestsParallel(t, dsn, func(dbt *DBTest, _ string) {
ctx, cancel := context.WithCancel(context.Background())
cancel()
if _, err := dbt.db.PrepareContext(ctx, "SELECT 1"); err != context.Canceled {
dbt.Errorf("expected context.Canceled, got %v", err)
}
})
}
func TestContextCancelStmtExec(t *testing.T) {
runTestsParallel(t, dsn, func(dbt *DBTest, tbl string) {
dbt.mustExec("CREATE TABLE " + tbl + " (v INTEGER)")
ctx, cancel := context.WithCancel(context.Background())
stmt, err := dbt.db.PrepareContext(ctx, "INSERT INTO "+tbl+" VALUES (SLEEP(1))")
if err != nil {
dbt.Fatalf("unexpected error: %v", err)
}
// Delay execution for just a bit until db.ExecContext has begun.
defer time.AfterFunc(250*time.Millisecond, cancel).Stop()
// This query will be canceled.
startTime := time.Now()
if _, err := stmt.ExecContext(ctx); err != context.Canceled {
dbt.Errorf("expected context.Canceled, got %v", err)
}
if d := time.Since(startTime); d > 500*time.Millisecond {
dbt.Errorf("too long execution time: %s", d)
}
// Wait for the INSERT query to be done.
time.Sleep(time.Second)
// Check how many times the query is executed.
var v int
if err := dbt.db.QueryRow("SELECT COUNT(*) FROM " + tbl).Scan(&v); err != nil {
dbt.Fatalf("%s", err.Error())
}
if v != 1 { // TODO: need to kill the query, and v should be 0.
dbt.Skipf("[WARN] expected val to be 1, got %d", v)
}
})
}
func TestContextCancelStmtQuery(t *testing.T) {
runTestsParallel(t, dsn, func(dbt *DBTest, tbl string) {
dbt.mustExec("CREATE TABLE " + tbl + " (v INTEGER)")
ctx, cancel := context.WithCancel(context.Background())
stmt, err := dbt.db.PrepareContext(ctx, "INSERT INTO "+tbl+" VALUES (SLEEP(1))")
if err != nil {
dbt.Fatalf("unexpected error: %v", err)
}
// Delay execution for just a bit until db.ExecContext has begun.
defer time.AfterFunc(250*time.Millisecond, cancel).Stop()
// This query will be canceled.
startTime := time.Now()
if _, err := stmt.QueryContext(ctx); err != context.Canceled {
dbt.Errorf("expected context.Canceled, got %v", err)
}
if d := time.Since(startTime); d > 500*time.Millisecond {
dbt.Errorf("too long execution time: %s", d)
}
// Wait for the INSERT query has done.
time.Sleep(time.Second)
// Check how many times the query is executed.
var v int
if err := dbt.db.QueryRow("SELECT COUNT(*) FROM " + tbl).Scan(&v); err != nil {
dbt.Fatalf("%s", err.Error())
}
if v != 1 { // TODO: need to kill the query, and v should be 0.
dbt.Skipf("[WARN] expected val to be 1, got %d", v)
}
})
}
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`)
}
runTestsParallel(t, dsn, func(dbt *DBTest, tbl string) {
dbt.mustExec("CREATE TABLE " + tbl + " (v INTEGER)")
ctx, cancel := context.WithCancel(context.Background())
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)
}
// Delay execution for just a bit until db.ExecContext has begun.
defer time.AfterFunc(100*time.Millisecond, cancel).Stop()
// This query will be canceled.
startTime := time.Now()
if _, err := tx.ExecContext(ctx, "INSERT INTO "+tbl+" VALUES (SLEEP(1))"); err != context.Canceled {
dbt.Errorf("expected context.Canceled, got %v", err)
}
if d := time.Since(startTime); d > 500*time.Millisecond {
dbt.Errorf("too long execution time: %s", d)
}
// Transaction is canceled, so expect an error.
switch err := tx.Commit(); err {
case sql.ErrTxDone:
// because the transaction has already been rollbacked.
// the database/sql package watches ctx
// and rollbacks when ctx is canceled.
case context.Canceled:
// the database/sql package rollbacks on another goroutine,
// so the transaction may not be rollbacked depending on goroutine scheduling.
default:
dbt.Errorf("expected sql.ErrTxDone or context.Canceled, got %v", err)
}
// 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)
}
})
}
func TestContextBeginIsolationLevel(t *testing.T) {
runTestsParallel(t, dsn, func(dbt *DBTest, tbl string) {
dbt.mustExec("CREATE TABLE " + tbl + " (v INTEGER)")
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
tx1, err := dbt.db.BeginTx(ctx, &sql.TxOptions{
Isolation: sql.LevelRepeatableRead,
})
if err != nil {
dbt.Fatal(err)
}
tx2, err := dbt.db.BeginTx(ctx, &sql.TxOptions{
Isolation: sql.LevelReadCommitted,
})
if err != nil {
dbt.Fatal(err)
}
_, err = tx1.ExecContext(ctx, "INSERT INTO "+tbl+" VALUES (1)")
if err != nil {
dbt.Fatal(err)
}
var v int
row := tx2.QueryRowContext(ctx, "SELECT COUNT(*) FROM "+tbl)
if err := row.Scan(&v); err != nil {
dbt.Fatal(err)
}
// Because writer transaction wasn't committed yet, it should be available
if v != 0 {
dbt.Errorf("expected val to be 0, got %d", v)
}
err = tx1.Commit()
if err != nil {
dbt.Fatal(err)
}
row = tx2.QueryRowContext(ctx, "SELECT COUNT(*) FROM "+tbl)
if err := row.Scan(&v); err != nil {
dbt.Fatal(err)
}
// Data written by writer transaction is already committed, it should be selectable
if v != 1 {
dbt.Errorf("expected val to be 1, got %d", v)
}
tx2.Commit()
})
}
func TestContextBeginReadOnly(t *testing.T) {
runTestsParallel(t, dsn, func(dbt *DBTest, tbl string) {
dbt.mustExec("CREATE TABLE " + tbl + " (v INTEGER)")
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
tx, err := dbt.db.BeginTx(ctx, &sql.TxOptions{
ReadOnly: true,
})
if _, ok := err.(*MySQLError); ok {
dbt.Skip("It seems that your MySQL does not support READ ONLY transactions")
return
} else if err != nil {
dbt.Fatal(err)
}
// INSERT queries fail in a READ ONLY transaction.
_, err = tx.ExecContext(ctx, "INSERT INTO "+tbl+" VALUES (1)")
if _, ok := err.(*MySQLError); !ok {
dbt.Errorf("expected MySQLError, got %v", err)
}
// SELECT queries can be executed.
var v int
row := tx.QueryRowContext(ctx, "SELECT COUNT(*) FROM "+tbl)
if err := row.Scan(&v); err != nil {
dbt.Fatal(err)
}
if v != 0 {
dbt.Errorf("expected val to be 0, got %d", v)
}
if err := tx.Commit(); err != nil {
dbt.Fatal(err)
}
})
}
func TestRowsColumnTypes(t *testing.T) {
niNULL := sql.NullInt64{Int64: 0, Valid: false}
ni0 := sql.NullInt64{Int64: 0, Valid: true}
ni1 := sql.NullInt64{Int64: 1, Valid: true}
ni42 := sql.NullInt64{Int64: 42, Valid: true}
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 := 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}
bNULL := []byte(nil)
nsNULL := sql.NullString{String: "", Valid: false}
// Helper function to build NullString from string literal.
ns := func(s string) sql.NullString { return sql.NullString{String: s, Valid: true} }
ns0 := ns("0")
b0 := []byte("0")
b42 := []byte("42")
nsTest := ns("Test")
bTest := []byte("Test")
b0pad4 := []byte("0\x00\x00\x00") // BINARY right-pads values with 0x00
bx0 := []byte("\x00")
bx42 := []byte("\x42")
var columns = []struct {
name string
fieldType string // type used when creating table schema
databaseTypeName string // actual type used by MySQL
scanType reflect.Type
nullable bool
precision int64 // 0 if not ok
scale int64
valuesIn [3]string
valuesOut [3]interface{}
}{
{"bit8null", "BIT(8)", "BIT", scanTypeBytes, true, 0, 0, [3]string{"0x0", "NULL", "0x42"}, [3]interface{}{bx0, bNULL, bx42}},
{"boolnull", "BOOL", "TINYINT", scanTypeNullInt, true, 0, 0, [3]string{"NULL", "true", "0"}, [3]interface{}{niNULL, ni1, ni0}},
{"bool", "BOOL NOT NULL", "TINYINT", scanTypeInt8, false, 0, 0, [3]string{"1", "0", "FALSE"}, [3]interface{}{int8(1), int8(0), int8(0)}},
{"intnull", "INTEGER", "INT", scanTypeNullInt, true, 0, 0, [3]string{"0", "NULL", "42"}, [3]interface{}{ni0, niNULL, ni42}},
{"smallint", "SMALLINT NOT NULL", "SMALLINT", scanTypeInt16, false, 0, 0, [3]string{"0", "-32768", "32767"}, [3]interface{}{int16(0), int16(-32768), int16(32767)}},
{"smallintnull", "SMALLINT", "SMALLINT", scanTypeNullInt, true, 0, 0, [3]string{"0", "NULL", "42"}, [3]interface{}{ni0, niNULL, ni42}},
{"int3null", "INT(3)", "INT", scanTypeNullInt, true, 0, 0, [3]string{"0", "NULL", "42"}, [3]interface{}{ni0, niNULL, ni42}},
{"int7", "INT(7) NOT NULL", "INT", scanTypeInt32, false, 0, 0, [3]string{"0", "-1337", "42"}, [3]interface{}{int32(0), int32(-1337), int32(42)}},
{"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", "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)}},
{"mediumuint", "MEDIUMINT UNSIGNED NOT NULL", "UNSIGNED MEDIUMINT", scanTypeUint32, false, 0, 0, [3]string{"0", "16777215", "42"}, [3]interface{}{uint32(0), uint32(16777215), uint32(42)}},
{"uint13", "INT(13) UNSIGNED NOT NULL", "UNSIGNED INT", scanTypeUint32, false, 0, 0, [3]string{"0", "1337", "42"}, [3]interface{}{uint32(0), uint32(1337), uint32(42)}},
{"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}},
{"double", "DOUBLE NOT NULL", "DOUBLE", scanTypeFloat64, false, math.MaxInt64, math.MaxInt64, [3]string{"0", "42", "13.37"}, [3]interface{}{float64(0), float64(42), float64(13.37)}},
{"doublenull", "DOUBLE", "DOUBLE", scanTypeNullFloat, true, math.MaxInt64, math.MaxInt64, [3]string{"0", "NULL", "13.37"}, [3]interface{}{nf0, nfNULL, nf1337}},
{"decimal1", "DECIMAL(10,6) NOT NULL", "DECIMAL", scanTypeString, false, 10, 6, [3]string{"0", "13.37", "1234.123456"}, [3]interface{}{"0.000000", "13.370000", "1234.123456"}},
{"decimal1null", "DECIMAL(10,6)", "DECIMAL", scanTypeNullString, true, 10, 6, [3]string{"0", "NULL", "1234.123456"}, [3]interface{}{ns("0.000000"), nsNULL, ns("1234.123456")}},
{"decimal2", "DECIMAL(8,4) NOT NULL", "DECIMAL", scanTypeString, false, 8, 4, [3]string{"0", "13.37", "1234.123456"}, [3]interface{}{"0.0000", "13.3700", "1234.1235"}},
{"decimal2null", "DECIMAL(8,4)", "DECIMAL", scanTypeNullString, true, 8, 4, [3]string{"0", "NULL", "1234.123456"}, [3]interface{}{ns("0.0000"), nsNULL, ns("1234.1235")}},
{"decimal3", "DECIMAL(5,0) NOT NULL", "DECIMAL", scanTypeString, false, 5, 0, [3]string{"0", "13.37", "-12345.123456"}, [3]interface{}{"0", "13", "-12345"}},
{"decimal3null", "DECIMAL(5,0)", "DECIMAL", scanTypeNullString, true, 5, 0, [3]string{"0", "NULL", "-12345.123456"}, [3]interface{}{ns0, nsNULL, ns("-12345")}},
{"char25null", "CHAR(25)", "CHAR", scanTypeNullString, true, 0, 0, [3]string{"0", "NULL", "'Test'"}, [3]interface{}{ns0, nsNULL, nsTest}},
{"varchar42", "VARCHAR(42) NOT NULL", "VARCHAR", scanTypeString, false, 0, 0, [3]string{"0", "'Test'", "42"}, [3]interface{}{"0", "Test", "42"}},
{"binary4null", "BINARY(4)", "BINARY", scanTypeBytes, true, 0, 0, [3]string{"0", "NULL", "'Test'"}, [3]interface{}{b0pad4, bNULL, bTest}},
{"varbinary42", "VARBINARY(42) NOT NULL", "VARBINARY", scanTypeBytes, false, 0, 0, [3]string{"0", "'Test'", "42"}, [3]interface{}{b0, bTest, b42}},
{"tinyblobnull", "TINYBLOB", "BLOB", scanTypeBytes, true, 0, 0, [3]string{"0", "NULL", "'Test'"}, [3]interface{}{b0, bNULL, bTest}},
{"tinytextnull", "TINYTEXT", "TEXT", scanTypeNullString, true, 0, 0, [3]string{"0", "NULL", "'Test'"}, [3]interface{}{ns0, nsNULL, nsTest}},
{"blobnull", "BLOB", "BLOB", scanTypeBytes, true, 0, 0, [3]string{"0", "NULL", "'Test'"}, [3]interface{}{b0, bNULL, bTest}},
{"textnull", "TEXT", "TEXT", scanTypeNullString, true, 0, 0, [3]string{"0", "NULL", "'Test'"}, [3]interface{}{ns0, nsNULL, nsTest}},
{"mediumblob", "MEDIUMBLOB NOT NULL", "BLOB", scanTypeBytes, false, 0, 0, [3]string{"0", "'Test'", "42"}, [3]interface{}{b0, bTest, b42}},
{"mediumtext", "MEDIUMTEXT NOT NULL", "TEXT", scanTypeString, false, 0, 0, [3]string{"0", "'Test'", "42"}, [3]interface{}{"0", "Test", "42"}},
{"longblob", "LONGBLOB NOT NULL", "BLOB", scanTypeBytes, false, 0, 0, [3]string{"0", "'Test'", "42"}, [3]interface{}{b0, bTest, b42}},
{"longtext", "LONGTEXT NOT NULL", "TEXT", scanTypeString, false, 0, 0, [3]string{"0", "'Test'", "42"}, [3]interface{}{"0", "Test", "42"}},
{"datetime", "DATETIME", "DATETIME", scanTypeNullTime, true, 0, 0, [3]string{"'2006-01-02 15:04:05'", "'2006-01-02 15:04:05.1'", "'2006-01-02 15:04:05.111111'"}, [3]interface{}{nt0, nt0, nt0}},
{"datetime2", "DATETIME(2)", "DATETIME", scanTypeNullTime, true, 2, 2, [3]string{"'2006-01-02 15:04:05'", "'2006-01-02 15:04:05.1'", "'2006-01-02 15:04:05.111111'"}, [3]interface{}{nt0, nt1, nt2}},
{"datetime6", "DATETIME(6)", "DATETIME", scanTypeNullTime, true, 6, 6, [3]string{"'2006-01-02 15:04:05'", "'2006-01-02 15:04:05.1'", "'2006-01-02 15:04:05.111111'"}, [3]interface{}{nt0, nt1, nt6}},
{"date", "DATE", "DATE", scanTypeNullTime, true, 0, 0, [3]string{"'2006-01-02'", "NULL", "'2006-03-04'"}, [3]interface{}{nd1, ndNULL, nd2}},
{"year", "YEAR NOT NULL", "YEAR", scanTypeUint16, false, 0, 0, [3]string{"2006", "2000", "1994"}, [3]interface{}{uint16(2006), uint16(2000), uint16(1994)}},
{"enum", "ENUM('', 'v1', 'v2')", "ENUM", scanTypeNullString, true, 0, 0, [3]string{"''", "'v1'", "'v2'"}, [3]interface{}{ns(""), ns("v1"), ns("v2")}},
{"set", "set('', 'v1', 'v2')", "SET", scanTypeNullString, true, 0, 0, [3]string{"''", "'v1'", "'v1,v2'"}, [3]interface{}{ns(""), ns("v1"), ns("v1,v2")}},
}
schema := ""
values1 := ""
values2 := ""
values3 := ""
for _, column := range columns {
schema += fmt.Sprintf("`%s` %s, ", column.name, column.fieldType)
values1 += column.valuesIn[0] + ", "
values2 += column.valuesIn[1] + ", "
values3 += column.valuesIn[2] + ", "
}
schema = schema[:len(schema)-2]
values1 = values1[:len(values1)-2]
values2 = values2[:len(values2)-2]
values3 = values3[:len(values3)-2]
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)
}
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))
}
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
}
// 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
}
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("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
}
}
// Avoid panic caused by nil scantype.
if t.Failed() {
return
}
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)
}
for j, value := range values {
value := reflect.ValueOf(value).Elem().Interface()
if !reflect.DeepEqual(value, columns[j].valuesOut[i]) {
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)
}
if err := rows.Close(); err != nil {
t.Errorf("error closing rows: %s", err)
}
})
}
func TestValuerWithValueReceiverGivenNilValue(t *testing.T) {
runTestsParallel(t, dsn, func(dbt *DBTest, tbl string) {
dbt.mustExec("CREATE TABLE " + tbl + " (value VARCHAR(255))")
dbt.db.Exec("INSERT INTO "+tbl+" VALUES (?)", (*testValuer)(nil))
// This test will panic on the INSERT if ConvertValue() does not check for typed nil before calling Value()
})
}
// TestRawBytesAreNotModified checks for a race condition that arises when a query context
// is canceled while a user is calling rows.Scan. This is a more stringent test than the one
// proposed in https://github.com/golang/go/issues/23519. Here we're explicitly using
// `sql.RawBytes` to check the contents of our internal buffers are not modified after an implicit
// call to `Rows.Close`, so Context cancellation should **not** invalidate the backing buffers.
func TestRawBytesAreNotModified(t *testing.T) {
const blob = "abcdefghijklmnop"
const contextRaceIterations = 20
const blobSize = defaultBufSize * 3 / 4 // Second row overwrites first row.
const insertRows = 4
var sqlBlobs = [2]string{
strings.Repeat(blob, blobSize/len(blob)),
strings.Repeat(strings.ToUpper(blob), blobSize/len(blob)),
}
runTests(t, dsn, func(dbt *DBTest) {
dbt.mustExec("CREATE TABLE test (id int, value BLOB) CHARACTER SET utf8")
for i := 0; i < insertRows; i++ {
dbt.mustExec("INSERT INTO test VALUES (?, ?)", i+1, sqlBlobs[i&1])
}
for i := 0; i < contextRaceIterations; i++ {
func() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
rows, err := dbt.db.QueryContext(ctx, `SELECT id, value FROM test`)
if err != nil {
dbt.Fatal(err)
}
defer rows.Close()
var b int
var raw sql.RawBytes
if !rows.Next() {
dbt.Fatal("expected at least one row")
}
if err := rows.Scan(&b, &raw); err != nil {
dbt.Fatal(err)
}
before := string(raw)
// Ensure cancelling the query does not corrupt the contents of `raw`
cancel()
time.Sleep(time.Microsecond * 100)
after := string(raw)
if before != after {
dbt.Fatalf("the backing storage for sql.RawBytes has been modified (i=%v)", i)
}
}()
}
})
}
var _ driver.DriverContext = &MySQLDriver{}
type dialCtxKey struct{}
func TestConnectorObeysDialTimeouts(t *testing.T) {
if !available {
t.Skipf("MySQL server not running on %s", netAddr)
}
RegisterDialContext("dialctxtest", func(ctx context.Context, addr string) (net.Conn, error) {
var d net.Dialer
if !ctx.Value(dialCtxKey{}).(bool) {
return nil, fmt.Errorf("test error: query context is not propagated to our dialer")
}
return d.DialContext(ctx, prot, addr)
})
db, err := sql.Open(driverNameTest, fmt.Sprintf("%s:%s@dialctxtest(%s)/%s?timeout=30s", user, pass, addr, dbname))
if err != nil {
t.Fatalf("error connecting: %s", err.Error())
}
defer db.Close()
ctx := context.WithValue(context.Background(), dialCtxKey{}, true)
_, err = db.ExecContext(ctx, "DO 1")
if err != nil {
t.Fatal(err)
}
}
func configForTests(t *testing.T) *Config {
if !available {
t.Skipf("MySQL server not running on %s", netAddr)
}
mycnf := NewConfig()
mycnf.User = user
mycnf.Passwd = pass
mycnf.Addr = addr
mycnf.Net = prot
mycnf.DBName = dbname
return mycnf
}
func TestNewConnector(t *testing.T) {
mycnf := configForTests(t)
conn, err := NewConnector(mycnf)
if err != nil {
t.Fatal(err)
}
db := sql.OpenDB(conn)
defer db.Close()
if err := db.Ping(); err != nil {
t.Fatal(err)
}
}
type slowConnection struct {
net.Conn
slowdown time.Duration
}
func (sc *slowConnection) Read(b []byte) (int, error) {
time.Sleep(sc.slowdown)
return sc.Conn.Read(b)
}
type connectorHijack struct {
driver.Connector
connErr error
}
func (cw *connectorHijack) Connect(ctx context.Context) (driver.Conn, error) {
var conn driver.Conn
conn, cw.connErr = cw.Connector.Connect(ctx)
return conn, cw.connErr
}
func TestConnectorTimeoutsDuringOpen(t *testing.T) {
RegisterDialContext("slowconn", func(ctx context.Context, addr string) (net.Conn, error) {
var d net.Dialer
conn, err := d.DialContext(ctx, prot, addr)
if err != nil {
return nil, err
}
return &slowConnection{Conn: conn, slowdown: 100 * time.Millisecond}, nil
})
mycnf := configForTests(t)
mycnf.Net = "slowconn"
conn, err := NewConnector(mycnf)
if err != nil {
t.Fatal(err)
}
hijack := &connectorHijack{Connector: conn}
db := sql.OpenDB(hijack)
defer db.Close()
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
defer cancel()
_, err = db.ExecContext(ctx, "DO 1")
if err != context.DeadlineExceeded {
t.Fatalf("ExecContext should have timed out")
}
if hijack.connErr != context.DeadlineExceeded {
t.Fatalf("(*Connector).Connect should have timed out")
}
}
// A connection which can only be closed.
type dummyConnection struct {
net.Conn
closed bool
}
func (d *dummyConnection) Close() error {
d.closed = true
return nil
}
func TestConnectorTimeoutsWatchCancel(t *testing.T) {
var (
cancel func() // Used to cancel the context just after connecting.
created *dummyConnection // The created connection.
)
RegisterDialContext("TestConnectorTimeoutsWatchCancel", func(ctx context.Context, addr string) (net.Conn, error) {
// Canceling at this time triggers the watchCancel error branch in Connect().
cancel()
created = &dummyConnection{}
return created, nil
})
mycnf := NewConfig()
mycnf.User = "root"
mycnf.Addr = "foo"
mycnf.Net = "TestConnectorTimeoutsWatchCancel"
conn, err := NewConnector(mycnf)
if err != nil {
t.Fatal(err)
}
db := sql.OpenDB(conn)
defer db.Close()
var ctx context.Context
ctx, cancel = context.WithCancel(context.Background())
defer cancel()
if _, err := db.Conn(ctx); err != context.Canceled {
t.Errorf("got %v, want context.Canceled", err)
}
if created == nil {
t.Fatal("no connection created")
}
if !created.closed {
t.Errorf("connection not closed")
}
}
func TestConnectionAttributes(t *testing.T) {
if !available {
t.Skipf("MySQL server not running on %s", netAddr)
}
defaultAttrs := []string{
connAttrClientName,
connAttrOS,
connAttrPlatform,
connAttrPid,
connAttrServerHost,
}
host, _, _ := net.SplitHostPort(addr)
defaultAttrValues := []string{
connAttrClientNameValue,
connAttrOSValue,
connAttrPlatformValue,
strconv.Itoa(os.Getpid()),
host,
}
customAttrs := []string{"attr1", "fo/o"}
customAttrValues := []string{"value1", "bo/o"}
customAttrStrs := make([]string, len(customAttrs))
for i := range customAttrs {
customAttrStrs[i] = fmt.Sprintf("%s:%s", customAttrs[i], customAttrValues[i])
}
dsn += "&connectionAttributes=" + url.QueryEscape(strings.Join(customAttrStrs, ","))
var db *sql.DB
if _, err := ParseDSN(dsn); err != errInvalidDSNUnsafeCollation {
db, err = sql.Open(driverNameTest, dsn)
if err != nil {
t.Fatalf("error connecting: %s", err.Error())
}
defer db.Close()
}
dbt := &DBTest{t, db}
queryString := "SELECT ATTR_NAME, ATTR_VALUE FROM performance_schema.session_account_connect_attrs WHERE PROCESSLIST_ID = CONNECTION_ID()"
rows := dbt.mustQuery(queryString)
defer rows.Close()
rowsMap := make(map[string]string)
for rows.Next() {
var attrName, attrValue string
rows.Scan(&attrName, &attrValue)
rowsMap[attrName] = attrValue
}
connAttrs := append(append([]string{}, defaultAttrs...), customAttrs...)
expectedAttrValues := append(append([]string{}, defaultAttrValues...), customAttrValues...)
for i := range connAttrs {
if gotValue := rowsMap[connAttrs[i]]; gotValue != expectedAttrValues[i] {
dbt.Errorf("expected %q, got %q", expectedAttrValues[i], gotValue)
}
}
}
func TestErrorInMultiResult(t *testing.T) {
// https://github.com/go-sql-driver/mysql/issues/1361
var db *sql.DB
if _, err := ParseDSN(dsn); err != errInvalidDSNUnsafeCollation {
db, err = sql.Open("mysql", dsn)
if err != nil {
t.Fatalf("error connecting: %s", err.Error())
}
defer db.Close()
}
dbt := &DBTest{t, db}
query := `
CREATE PROCEDURE test_proc1()
BEGIN
SELECT 1,2;
SELECT 3,4;
SIGNAL SQLSTATE '10000' SET MESSAGE_TEXT = "some error", MYSQL_ERRNO = 10000;
END;
`
runCallCommand(dbt, query, "test_proc1")
}
func runCallCommand(dbt *DBTest, query, name string) {
dbt.mustExec(fmt.Sprintf("DROP PROCEDURE IF EXISTS %s", name))
dbt.mustExec(query)
defer dbt.mustExec("DROP PROCEDURE " + name)
rows, err := dbt.db.Query(fmt.Sprintf("CALL %s", name))
if err != nil {
return
}
defer rows.Close()
for rows.Next() {
}
for rows.NextResultSet() {
for rows.Next() {
}
}
}
// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
//
// Copyright 2016 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/.
package mysql
import (
"bytes"
"context"
"crypto/rsa"
"crypto/tls"
"errors"
"fmt"
"math/big"
"net"
"net/url"
"sort"
"strconv"
"strings"
"time"
)
var (
errInvalidDSNUnescaped = errors.New("invalid DSN: did you forget to escape a param value?")
errInvalidDSNAddr = errors.New("invalid DSN: network address not terminated (missing closing brace)")
errInvalidDSNNoSlash = errors.New("invalid DSN: missing the slash separating the database name")
errInvalidDSNUnsafeCollation = errors.New("invalid DSN: interpolateParams can not be used with unsafe collations")
)
// Config is a configuration parsed from a DSN string.
// If a new Config is created instead of being parsed from a DSN string,
// the NewConfig function should be used, which sets default values.
type Config struct {
// non boolean fields
User string // Username
Passwd string // Password (requires User)
Net string // Network (e.g. "tcp", "tcp6", "unix". default: "tcp")
Addr string // Address (default: "127.0.0.1:3306" for "tcp" and "/tmp/mysql.sock" for "unix")
DBName string // Database name
Params map[string]string // Connection parameters
ConnectionAttributes string // Connection Attributes, comma-delimited string of user-defined "key:value" pairs
Collation string // Connection collation
Loc *time.Location // Location for time.Time values
MaxAllowedPacket int // Max packet size allowed
ServerPubKey string // Server public key name
TLSConfig string // TLS configuration name
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
Logger Logger // Logger
// boolean fields
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
// unexported fields. new options should be come here
beforeConnect func(context.Context, *Config) error // Invoked before a connection is established
pubKey *rsa.PublicKey // Server public key
timeTruncate time.Duration // Truncate time.Time values to the specified duration
}
// Functional Options Pattern
// https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis
type Option func(*Config) error
// NewConfig creates a new Config and sets default values.
func NewConfig() *Config {
cfg := &Config{
Loc: time.UTC,
MaxAllowedPacket: defaultMaxAllowedPacket,
Logger: defaultLogger,
AllowNativePasswords: true,
CheckConnLiveness: true,
}
return cfg
}
// Apply applies the given options to the Config object.
func (c *Config) Apply(opts ...Option) error {
for _, opt := range opts {
err := opt(c)
if err != nil {
return err
}
}
return nil
}
// TimeTruncate sets the time duration to truncate time.Time values in
// query parameters.
func TimeTruncate(d time.Duration) Option {
return func(cfg *Config) error {
cfg.timeTruncate = d
return nil
}
}
// BeforeConnect sets the function to be invoked before a connection is established.
func BeforeConnect(fn func(context.Context, *Config) error) Option {
return func(cfg *Config) error {
cfg.beforeConnect = fn
return nil
}
}
func (cfg *Config) Clone() *Config {
cp := *cfg
if cp.TLS != nil {
cp.TLS = cfg.TLS.Clone()
}
if len(cp.Params) > 0 {
cp.Params = make(map[string]string, len(cfg.Params))
for k, v := range cfg.Params {
cp.Params[k] = v
}
}
if cfg.pubKey != nil {
cp.pubKey = &rsa.PublicKey{
N: new(big.Int).Set(cfg.pubKey.N),
E: cfg.pubKey.E,
}
}
return &cp
}
func (cfg *Config) normalize() error {
if cfg.InterpolateParams && cfg.Collation != "" && unsafeCollations[cfg.Collation] {
return errInvalidDSNUnsafeCollation
}
// Set default network if empty
if cfg.Net == "" {
cfg.Net = "tcp"
}
// Set default address if empty
if cfg.Addr == "" {
switch cfg.Net {
case "tcp":
cfg.Addr = "127.0.0.1:3306"
case "unix":
cfg.Addr = "/tmp/mysql.sock"
default:
return errors.New("default addr for network '" + cfg.Net + "' unknown")
}
} else if cfg.Net == "tcp" {
cfg.Addr = ensureHavePort(cfg.Addr)
}
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 {
host, _, err := net.SplitHostPort(cfg.Addr)
if err == nil {
cfg.TLS.ServerName = host
}
}
if cfg.ServerPubKey != "" {
cfg.pubKey = getServerPubKey(cfg.ServerPubKey)
if cfg.pubKey == nil {
return errors.New("invalid value / unknown server pub key name: " + cfg.ServerPubKey)
}
}
if cfg.Logger == nil {
cfg.Logger = defaultLogger
}
return nil
}
func writeDSNParam(buf *bytes.Buffer, hasParam *bool, name, value string) {
buf.Grow(1 + len(name) + 1 + len(value))
if !*hasParam {
*hasParam = true
buf.WriteByte('?')
} else {
buf.WriteByte('&')
}
buf.WriteString(name)
buf.WriteByte('=')
buf.WriteString(value)
}
// FormatDSN formats the given Config into a DSN string which can be passed to
// the driver.
//
// Note: use [NewConnector] and [database/sql.OpenDB] to open a connection from a [*Config].
func (cfg *Config) FormatDSN() string {
var buf bytes.Buffer
// [username[:password]@]
if len(cfg.User) > 0 {
buf.WriteString(cfg.User)
if len(cfg.Passwd) > 0 {
buf.WriteByte(':')
buf.WriteString(cfg.Passwd)
}
buf.WriteByte('@')
}
// [protocol[(address)]]
if len(cfg.Net) > 0 {
buf.WriteString(cfg.Net)
if len(cfg.Addr) > 0 {
buf.WriteByte('(')
buf.WriteString(cfg.Addr)
buf.WriteByte(')')
}
}
// /dbname
buf.WriteByte('/')
buf.WriteString(url.PathEscape(cfg.DBName))
// [?param1=value1&...&paramN=valueN]
hasParam := false
if cfg.AllowAllFiles {
hasParam = true
buf.WriteString("?allowAllFiles=true")
}
if cfg.AllowCleartextPasswords {
writeDSNParam(&buf, &hasParam, "allowCleartextPasswords", "true")
}
if cfg.AllowFallbackToPlaintext {
writeDSNParam(&buf, &hasParam, "allowFallbackToPlaintext", "true")
}
if !cfg.AllowNativePasswords {
writeDSNParam(&buf, &hasParam, "allowNativePasswords", "false")
}
if cfg.AllowOldPasswords {
writeDSNParam(&buf, &hasParam, "allowOldPasswords", "true")
}
if !cfg.CheckConnLiveness {
writeDSNParam(&buf, &hasParam, "checkConnLiveness", "false")
}
if cfg.ClientFoundRows {
writeDSNParam(&buf, &hasParam, "clientFoundRows", "true")
}
if col := cfg.Collation; col != "" {
writeDSNParam(&buf, &hasParam, "collation", col)
}
if cfg.ColumnsWithAlias {
writeDSNParam(&buf, &hasParam, "columnsWithAlias", "true")
}
if cfg.InterpolateParams {
writeDSNParam(&buf, &hasParam, "interpolateParams", "true")
}
if cfg.Loc != time.UTC && cfg.Loc != nil {
writeDSNParam(&buf, &hasParam, "loc", url.QueryEscape(cfg.Loc.String()))
}
if cfg.MultiStatements {
writeDSNParam(&buf, &hasParam, "multiStatements", "true")
}
if cfg.ParseTime {
writeDSNParam(&buf, &hasParam, "parseTime", "true")
}
if cfg.timeTruncate > 0 {
writeDSNParam(&buf, &hasParam, "timeTruncate", cfg.timeTruncate.String())
}
if cfg.ReadTimeout > 0 {
writeDSNParam(&buf, &hasParam, "readTimeout", cfg.ReadTimeout.String())
}
if cfg.RejectReadOnly {
writeDSNParam(&buf, &hasParam, "rejectReadOnly", "true")
}
if len(cfg.ServerPubKey) > 0 {
writeDSNParam(&buf, &hasParam, "serverPubKey", url.QueryEscape(cfg.ServerPubKey))
}
if cfg.Timeout > 0 {
writeDSNParam(&buf, &hasParam, "timeout", cfg.Timeout.String())
}
if len(cfg.TLSConfig) > 0 {
writeDSNParam(&buf, &hasParam, "tls", url.QueryEscape(cfg.TLSConfig))
}
if cfg.WriteTimeout > 0 {
writeDSNParam(&buf, &hasParam, "writeTimeout", cfg.WriteTimeout.String())
}
if cfg.MaxAllowedPacket != defaultMaxAllowedPacket {
writeDSNParam(&buf, &hasParam, "maxAllowedPacket", strconv.Itoa(cfg.MaxAllowedPacket))
}
// other params
if cfg.Params != nil {
var params []string
for param := range cfg.Params {
params = append(params, param)
}
sort.Strings(params)
for _, param := range params {
writeDSNParam(&buf, &hasParam, param, url.QueryEscape(cfg.Params[param]))
}
}
return buf.String()
}
// ParseDSN parses the DSN string to a Config
func ParseDSN(dsn string) (cfg *Config, err error) {
// New config with some default values
cfg = NewConfig()
// [user[:password]@][net[(addr)]]/dbname[?param1=value1&paramN=valueN]
// Find the last '/' (since the password or the net addr might contain a '/')
foundSlash := false
for i := len(dsn) - 1; i >= 0; i-- {
if dsn[i] == '/' {
foundSlash = true
var j, k int
// left part is empty if i <= 0
if i > 0 {
// [username[:password]@][protocol[(address)]]
// Find the last '@' in dsn[:i]
for j = i; j >= 0; j-- {
if dsn[j] == '@' {
// username[:password]
// Find the first ':' in dsn[:j]
for k = 0; k < j; k++ {
if dsn[k] == ':' {
cfg.Passwd = dsn[k+1 : j]
break
}
}
cfg.User = dsn[:k]
break
}
}
// [protocol[(address)]]
// Find the first '(' in dsn[j+1:i]
for k = j + 1; k < i; k++ {
if dsn[k] == '(' {
// dsn[i-1] must be == ')' if an address is specified
if dsn[i-1] != ')' {
if strings.ContainsRune(dsn[k+1:i], ')') {
return nil, errInvalidDSNUnescaped
}
return nil, errInvalidDSNAddr
}
cfg.Addr = dsn[k+1 : i-1]
break
}
}
cfg.Net = dsn[j+1 : k]
}
// dbname[?param1=value1&...&paramN=valueN]
// Find the first '?' in dsn[i+1:]
for j = i + 1; j < len(dsn); j++ {
if dsn[j] == '?' {
if err = parseDSNParams(cfg, dsn[j+1:]); err != nil {
return
}
break
}
}
dbname := dsn[i+1 : j]
if cfg.DBName, err = url.PathUnescape(dbname); err != nil {
return nil, fmt.Errorf("invalid dbname %q: %w", dbname, err)
}
break
}
}
if !foundSlash && len(dsn) > 0 {
return nil, errInvalidDSNNoSlash
}
if err = cfg.normalize(); err != nil {
return nil, err
}
return
}
// parseDSNParams parses the DSN "query string"
// Values must be url.QueryEscape'ed
func parseDSNParams(cfg *Config, params string) (err error) {
for _, v := range strings.Split(params, "&") {
key, value, found := strings.Cut(v, "=")
if !found {
continue
}
// cfg params
switch key {
// Disable INFILE allowlist / enable all files
case "allowAllFiles":
var isBool bool
cfg.AllowAllFiles, isBool = readBool(value)
if !isBool {
return errors.New("invalid bool value: " + value)
}
// Use cleartext authentication mode (MySQL 5.5.10+)
case "allowCleartextPasswords":
var isBool bool
cfg.AllowCleartextPasswords, isBool = readBool(value)
if !isBool {
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
cfg.AllowNativePasswords, isBool = readBool(value)
if !isBool {
return errors.New("invalid bool value: " + value)
}
// Use old authentication mode (pre MySQL 4.1)
case "allowOldPasswords":
var isBool bool
cfg.AllowOldPasswords, isBool = readBool(value)
if !isBool {
return errors.New("invalid bool value: " + value)
}
// Check connections for Liveness before using them
case "checkConnLiveness":
var isBool bool
cfg.CheckConnLiveness, isBool = readBool(value)
if !isBool {
return errors.New("invalid bool value: " + value)
}
// Switch "rowsAffected" mode
case "clientFoundRows":
var isBool bool
cfg.ClientFoundRows, isBool = readBool(value)
if !isBool {
return errors.New("invalid bool value: " + value)
}
// Collation
case "collation":
cfg.Collation = value
case "columnsWithAlias":
var isBool bool
cfg.ColumnsWithAlias, isBool = readBool(value)
if !isBool {
return errors.New("invalid bool value: " + value)
}
// Compression
case "compress":
return errors.New("compression not implemented yet")
// Enable client side placeholder substitution
case "interpolateParams":
var isBool bool
cfg.InterpolateParams, isBool = readBool(value)
if !isBool {
return errors.New("invalid bool value: " + value)
}
// Time Location
case "loc":
if value, err = url.QueryUnescape(value); err != nil {
return
}
cfg.Loc, err = time.LoadLocation(value)
if err != nil {
return
}
// multiple statements in one query
case "multiStatements":
var isBool bool
cfg.MultiStatements, isBool = readBool(value)
if !isBool {
return errors.New("invalid bool value: " + value)
}
// time.Time parsing
case "parseTime":
var isBool bool
cfg.ParseTime, isBool = readBool(value)
if !isBool {
return errors.New("invalid bool value: " + value)
}
// time.Time truncation
case "timeTruncate":
cfg.timeTruncate, err = time.ParseDuration(value)
if err != nil {
return fmt.Errorf("invalid timeTruncate value: %v, error: %w", value, err)
}
// I/O read Timeout
case "readTimeout":
cfg.ReadTimeout, err = time.ParseDuration(value)
if err != nil {
return
}
// Reject read-only connections
case "rejectReadOnly":
var isBool bool
cfg.RejectReadOnly, isBool = readBool(value)
if !isBool {
return errors.New("invalid bool value: " + value)
}
// Server public key
case "serverPubKey":
name, err := url.QueryUnescape(value)
if err != nil {
return fmt.Errorf("invalid value for server pub key name: %v", err)
}
cfg.ServerPubKey = name
// Strict mode
case "strict":
panic("strict mode has been removed. See https://github.com/go-sql-driver/mysql/wiki/strict-mode")
// Dial Timeout
case "timeout":
cfg.Timeout, err = time.ParseDuration(value)
if err != nil {
return
}
// TLS-Encryption
case "tls":
boolValue, isBool := readBool(value)
if isBool {
if boolValue {
cfg.TLSConfig = "true"
} else {
cfg.TLSConfig = "false"
}
} else if vl := strings.ToLower(value); vl == "skip-verify" || vl == "preferred" {
cfg.TLSConfig = vl
} else {
name, err := url.QueryUnescape(value)
if err != nil {
return fmt.Errorf("invalid value for TLS config name: %v", err)
}
cfg.TLSConfig = name
}
// I/O write Timeout
case "writeTimeout":
cfg.WriteTimeout, err = time.ParseDuration(value)
if err != nil {
return
}
case "maxAllowedPacket":
cfg.MaxAllowedPacket, err = strconv.Atoi(value)
if err != nil {
return
}
// Connection attributes
case "connectionAttributes":
connectionAttributes, err := url.QueryUnescape(value)
if err != nil {
return fmt.Errorf("invalid connectionAttributes value: %v", err)
}
cfg.ConnectionAttributes = connectionAttributes
default:
// lazy init
if cfg.Params == nil {
cfg.Params = make(map[string]string)
}
if cfg.Params[key], err = url.QueryUnescape(value); err != nil {
return
}
}
}
return
}
func ensureHavePort(addr string) string {
if _, _, err := net.SplitHostPort(addr); err != nil {
return net.JoinHostPort(addr, "3306")
}
return addr
}
//go:build go1.18
// +build go1.18
package mysql
import (
"net"
"testing"
)
func FuzzFormatDSN(f *testing.F) {
for _, test := range testDSNs { // See dsn_test.go
f.Add(test.in)
}
f.Fuzz(func(t *testing.T, dsn1 string) {
// Do not waste resources
if len(dsn1) > 1000 {
t.Skip("ignore: too long")
}
cfg1, err := ParseDSN(dsn1)
if err != nil {
t.Skipf("invalid DSN: %v", err)
}
dsn2 := cfg1.FormatDSN()
if dsn2 == dsn1 {
return
}
// Skip known cases of bad config that are not strictly checked by ParseDSN
if _, _, err := net.SplitHostPort(cfg1.Addr); err != nil {
t.Skipf("invalid addr %q: %v", cfg1.Addr, err)
}
cfg2, err := ParseDSN(dsn2)
if err != nil {
t.Fatalf("%q rewritten as %q: %v", dsn1, dsn2, err)
}
dsn3 := cfg2.FormatDSN()
if dsn3 != dsn2 {
t.Errorf("%q rewritten as %q", dsn2, dsn3)
}
})
}
// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
//
// Copyright 2016 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/.
package mysql
import (
"crypto/tls"
"fmt"
"net/url"
"reflect"
"testing"
"time"
)
var testDSNs = []struct {
in string
out *Config
}{{
"username:password@protocol(address)/dbname?param=value",
&Config{User: "username", Passwd: "password", Net: "protocol", Addr: "address", DBName: "dbname", Params: map[string]string{"param": "value"}, Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, Logger: defaultLogger, AllowNativePasswords: true, CheckConnLiveness: true},
}, {
"username:password@protocol(address)/dbname?param=value&columnsWithAlias=true",
&Config{User: "username", Passwd: "password", Net: "protocol", Addr: "address", DBName: "dbname", Params: map[string]string{"param": "value"}, Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, Logger: defaultLogger, AllowNativePasswords: true, CheckConnLiveness: true, ColumnsWithAlias: true},
}, {
"username:password@protocol(address)/dbname?param=value&columnsWithAlias=true&multiStatements=true",
&Config{User: "username", Passwd: "password", Net: "protocol", Addr: "address", DBName: "dbname", Params: map[string]string{"param": "value"}, Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, Logger: defaultLogger, AllowNativePasswords: true, CheckConnLiveness: true, ColumnsWithAlias: true, MultiStatements: true},
}, {
"user@unix(/path/to/socket)/dbname?charset=utf8",
&Config{User: "user", Net: "unix", Addr: "/path/to/socket", DBName: "dbname", Params: map[string]string{"charset": "utf8"}, Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, Logger: defaultLogger, AllowNativePasswords: true, CheckConnLiveness: true},
}, {
"user:password@tcp(localhost:5555)/dbname?charset=utf8&tls=true",
&Config{User: "user", Passwd: "password", Net: "tcp", Addr: "localhost:5555", DBName: "dbname", Params: map[string]string{"charset": "utf8"}, Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, Logger: defaultLogger, AllowNativePasswords: true, CheckConnLiveness: true, TLSConfig: "true"},
}, {
"user:password@tcp(localhost:5555)/dbname?charset=utf8mb4,utf8&tls=skip-verify",
&Config{User: "user", Passwd: "password", Net: "tcp", Addr: "localhost:5555", DBName: "dbname", Params: map[string]string{"charset": "utf8mb4,utf8"}, Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, Logger: defaultLogger, AllowNativePasswords: true, CheckConnLiveness: true, TLSConfig: "skip-verify"},
}, {
"user:password@/dbname?loc=UTC&timeout=30s&readTimeout=1s&writeTimeout=1s&allowAllFiles=1&clientFoundRows=true&allowOldPasswords=TRUE&collation=utf8mb4_unicode_ci&maxAllowedPacket=16777216&tls=false&allowCleartextPasswords=true&parseTime=true&rejectReadOnly=true",
&Config{User: "user", Passwd: "password", Net: "tcp", Addr: "127.0.0.1:3306", DBName: "dbname", Collation: "utf8mb4_unicode_ci", Loc: time.UTC, TLSConfig: "false", AllowCleartextPasswords: true, AllowNativePasswords: true, Timeout: 30 * time.Second, ReadTimeout: time.Second, WriteTimeout: time.Second, Logger: defaultLogger, AllowAllFiles: true, AllowOldPasswords: true, CheckConnLiveness: true, ClientFoundRows: true, MaxAllowedPacket: 16777216, ParseTime: true, RejectReadOnly: true},
}, {
"user:password@/dbname?allowNativePasswords=false&checkConnLiveness=false&maxAllowedPacket=0&allowFallbackToPlaintext=true",
&Config{User: "user", Passwd: "password", Net: "tcp", Addr: "127.0.0.1:3306", DBName: "dbname", Loc: time.UTC, MaxAllowedPacket: 0, Logger: defaultLogger, AllowFallbackToPlaintext: true, AllowNativePasswords: false, CheckConnLiveness: false},
}, {
"user:p@ss(word)@tcp([de:ad:be:ef::ca:fe]:80)/dbname?loc=Local",
&Config{User: "user", Passwd: "p@ss(word)", Net: "tcp", Addr: "[de:ad:be:ef::ca:fe]:80", DBName: "dbname", Loc: time.Local, MaxAllowedPacket: defaultMaxAllowedPacket, Logger: defaultLogger, AllowNativePasswords: true, CheckConnLiveness: true},
}, {
"/dbname",
&Config{Net: "tcp", Addr: "127.0.0.1:3306", DBName: "dbname", Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, Logger: defaultLogger, AllowNativePasswords: true, CheckConnLiveness: true},
}, {
"/dbname%2Fwithslash",
&Config{Net: "tcp", Addr: "127.0.0.1:3306", DBName: "dbname/withslash", Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, Logger: defaultLogger, AllowNativePasswords: true, CheckConnLiveness: true},
}, {
"@/",
&Config{Net: "tcp", Addr: "127.0.0.1:3306", Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, Logger: defaultLogger, AllowNativePasswords: true, CheckConnLiveness: true},
}, {
"/",
&Config{Net: "tcp", Addr: "127.0.0.1:3306", Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, Logger: defaultLogger, AllowNativePasswords: true, CheckConnLiveness: true},
}, {
"",
&Config{Net: "tcp", Addr: "127.0.0.1:3306", Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, Logger: defaultLogger, AllowNativePasswords: true, CheckConnLiveness: true},
}, {
"user:p@/ssword@/",
&Config{User: "user", Passwd: "p@/ssword", Net: "tcp", Addr: "127.0.0.1:3306", Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, Logger: defaultLogger, AllowNativePasswords: true, CheckConnLiveness: true},
}, {
"unix/?arg=%2Fsome%2Fpath.ext",
&Config{Net: "unix", Addr: "/tmp/mysql.sock", Params: map[string]string{"arg": "/some/path.ext"}, Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, Logger: defaultLogger, AllowNativePasswords: true, CheckConnLiveness: true},
}, {
"tcp(127.0.0.1)/dbname",
&Config{Net: "tcp", Addr: "127.0.0.1:3306", DBName: "dbname", Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, Logger: defaultLogger, AllowNativePasswords: true, CheckConnLiveness: true},
}, {
"tcp(de:ad:be:ef::ca:fe)/dbname",
&Config{Net: "tcp", Addr: "[de:ad:be:ef::ca:fe]:3306", DBName: "dbname", Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, Logger: defaultLogger, AllowNativePasswords: true, CheckConnLiveness: true},
}, {
"user:password@/dbname?loc=UTC&timeout=30s&parseTime=true&timeTruncate=1h",
&Config{User: "user", Passwd: "password", Net: "tcp", Addr: "127.0.0.1:3306", DBName: "dbname", Loc: time.UTC, Timeout: 30 * time.Second, ParseTime: true, MaxAllowedPacket: defaultMaxAllowedPacket, Logger: defaultLogger, AllowNativePasswords: true, CheckConnLiveness: true, timeTruncate: time.Hour},
},
}
func TestDSNParser(t *testing.T) {
for i, tst := range testDSNs {
t.Run(tst.in, func(t *testing.T) {
cfg, err := ParseDSN(tst.in)
if err != nil {
t.Error(err.Error())
return
}
// pointer not static
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)
}
})
}
}
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
"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",
}
for i, tst := range invalidDSNs {
if _, err := ParseDSN(tst); err == nil {
t.Errorf("invalid DSN #%d. (%s) didn't error!", i, tst)
}
}
}
func TestDSNReformat(t *testing.T) {
for i, tst := range testDSNs {
t.Run(tst.in, func(t *testing.T) {
dsn1 := tst.in
cfg1, err := ParseDSN(dsn1)
if err != nil {
t.Error(err.Error())
return
}
cfg1.TLS = nil // pointer not static
res1 := fmt.Sprintf("%+v", cfg1)
dsn2 := cfg1.FormatDSN()
if dsn2 != dsn1 {
// Just log
t.Logf("%d. %q reformatted as %q", i, dsn1, dsn2)
}
cfg2, err := ParseDSN(dsn2)
if err != nil {
t.Error(err.Error())
return
}
cfg2.TLS = nil // pointer not static
res2 := fmt.Sprintf("%+v", cfg2)
if res1 != res2 {
t.Errorf("%d. %q does not match %q", i, res2, res1)
}
dsn3 := cfg2.FormatDSN()
if dsn3 != dsn2 {
t.Errorf("%d. %q does not match %q", i, dsn2, dsn3)
}
})
}
}
func TestDSNServerPubKey(t *testing.T) {
baseDSN := "User:password@tcp(localhost:5555)/dbname?serverPubKey="
RegisterServerPubKey("testKey", testPubKeyRSA)
defer DeregisterServerPubKey("testKey")
tst := baseDSN + "testKey"
cfg, err := ParseDSN(tst)
if err != nil {
t.Error(err.Error())
}
if cfg.ServerPubKey != "testKey" {
t.Errorf("unexpected cfg.ServerPubKey value: %v", cfg.ServerPubKey)
}
if cfg.pubKey != testPubKeyRSA {
t.Error("pub key pointer doesn't match")
}
// Key is missing
tst = baseDSN + "invalid_name"
cfg, err = ParseDSN(tst)
if err == nil {
t.Errorf("invalid name in DSN (%s) but did not error. Got config: %#v", tst, cfg)
}
}
func TestDSNServerPubKeyQueryEscape(t *testing.T) {
const name = "&%!:"
dsn := "User:password@tcp(localhost:5555)/dbname?serverPubKey=" + url.QueryEscape(name)
RegisterServerPubKey(name, testPubKeyRSA)
defer DeregisterServerPubKey(name)
cfg, err := ParseDSN(dsn)
if err != nil {
t.Error(err.Error())
}
if cfg.pubKey != testPubKeyRSA {
t.Error("pub key pointer doesn't match")
}
}
func TestDSNWithCustomTLS(t *testing.T) {
baseDSN := "User:password@tcp(localhost:5555)/dbname?tls="
tlsCfg := tls.Config{}
RegisterTLSConfig("utils_test", &tlsCfg)
defer DeregisterTLSConfig("utils_test")
// Custom TLS is missing
tst := baseDSN + "invalid_tls"
cfg, err := ParseDSN(tst)
if err == nil {
t.Errorf("invalid custom TLS in DSN (%s) but did not error. Got config: %#v", tst, cfg)
}
tst = baseDSN + "utils_test"
// Custom TLS with a server name
name := "foohost"
tlsCfg.ServerName = name
cfg, err = ParseDSN(tst)
if err != nil {
t.Error(err.Error())
} else if cfg.TLS.ServerName != name {
t.Errorf("did not get the correct TLS ServerName (%s) parsing DSN (%s).", name, tst)
}
// Custom TLS without a server name
name = "localhost"
tlsCfg.ServerName = ""
cfg, err = ParseDSN(tst)
if err != nil {
t.Error(err.Error())
} 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)
}
}
func TestDSNTLSConfig(t *testing.T) {
expectedServerName := "example.com"
dsn := "tcp(example.com:1234)/?tls=true"
cfg, err := ParseDSN(dsn)
if err != nil {
t.Error(err.Error())
}
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)
}
dsn = "tcp(example.com)/?tls=true"
cfg, err = ParseDSN(dsn)
if err != nil {
t.Error(err.Error())
}
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)
}
}
func TestDSNWithCustomTLSQueryEscape(t *testing.T) {
const configKey = "&%!:"
dsn := "User:password@tcp(localhost:5555)/dbname?tls=" + url.QueryEscape(configKey)
name := "foohost"
tlsCfg := tls.Config{ServerName: name}
RegisterTLSConfig(configKey, &tlsCfg)
defer DeregisterTLSConfig(configKey)
cfg, err := ParseDSN(dsn)
if err != nil {
t.Error(err.Error())
} else if cfg.TLS.ServerName != name {
t.Errorf("did not get the correct TLS ServerName (%s) parsing DSN (%s).", name, dsn)
}
}
func TestDSNUnsafeCollation(t *testing.T) {
_, err := ParseDSN("/dbname?collation=gbk_chinese_ci&interpolateParams=true")
if err != errInvalidDSNUnsafeCollation {
t.Errorf("expected %v, got %v", errInvalidDSNUnsafeCollation, err)
}
_, err = ParseDSN("/dbname?collation=gbk_chinese_ci&interpolateParams=false")
if err != nil {
t.Errorf("expected %v, got %v", nil, err)
}
_, err = ParseDSN("/dbname?collation=gbk_chinese_ci")
if err != nil {
t.Errorf("expected %v, got %v", nil, err)
}
_, err = ParseDSN("/dbname?collation=ascii_bin&interpolateParams=true")
if err != nil {
t.Errorf("expected %v, got %v", nil, err)
}
_, err = ParseDSN("/dbname?collation=latin1_german1_ci&interpolateParams=true")
if err != nil {
t.Errorf("expected %v, got %v", nil, err)
}
_, err = ParseDSN("/dbname?collation=utf8_general_ci&interpolateParams=true")
if err != nil {
t.Errorf("expected %v, got %v", nil, err)
}
_, err = ParseDSN("/dbname?collation=utf8mb4_general_ci&interpolateParams=true")
if err != nil {
t.Errorf("expected %v, got %v", nil, err)
}
}
func TestParamsAreSorted(t *testing.T) {
expected := "/dbname?interpolateParams=true&foobar=baz&quux=loo"
cfg := NewConfig()
cfg.DBName = "dbname"
cfg.InterpolateParams = true
cfg.Params = map[string]string{
"quux": "loo",
"foobar": "baz",
}
actual := cfg.FormatDSN()
if actual != expected {
t.Errorf("generic Config.Params were not sorted: want %#v, got %#v", expected, actual)
}
}
func TestCloneConfig(t *testing.T) {
RegisterServerPubKey("testKey", testPubKeyRSA)
defer DeregisterServerPubKey("testKey")
expectedServerName := "example.com"
dsn := "tcp(example.com:1234)/?tls=true&foobar=baz&serverPubKey=testKey"
cfg, err := ParseDSN(dsn)
if err != nil {
t.Fatal(err.Error())
}
cfg2 := cfg.Clone()
if cfg == cfg2 {
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)
}
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")
}
if _, ok := cfg2.Params["foobar"]; !ok {
t.Errorf("cloned Config is missing custom params")
}
delete(cfg2.Params, "foobar")
if _, ok := cfg.Params["foobar"]; !ok {
t.Errorf("custom params in cloned Config should not propagate to original Config")
}
if !reflect.DeepEqual(cfg.pubKey, cfg2.pubKey) {
t.Errorf("public key in Config should be identical")
}
}
func TestNormalizeTLSConfig(t *testing.T) {
tt := []struct {
tlsConfig string
want *tls.Config
}{
{"", nil},
{"false", nil},
{"true", &tls.Config{ServerName: "myserver"}},
{"skip-verify", &tls.Config{InsecureSkipVerify: true}},
{"preferred", &tls.Config{InsecureSkipVerify: true}},
{"test_tls_config", &tls.Config{ServerName: "myServerName"}},
}
RegisterTLSConfig("test_tls_config", &tls.Config{ServerName: "myServerName"})
defer func() { DeregisterTLSConfig("test_tls_config") }()
for _, tc := range tt {
t.Run(tc.tlsConfig, func(t *testing.T) {
cfg := &Config{
Addr: "myserver:3306",
TLSConfig: tc.tlsConfig,
}
cfg.normalize()
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 {
t.Errorf("tls.ServerName doesn't match (want: '%s', got: '%s')",
tc.want.ServerName, cfg.TLS.ServerName)
}
if cfg.TLS.InsecureSkipVerify != tc.want.InsecureSkipVerify {
t.Errorf("tls.InsecureSkipVerify doesn't match (want: %T, got :%T)",
tc.want.InsecureSkipVerify, cfg.TLS.InsecureSkipVerify)
}
})
}
}
func BenchmarkParseDSN(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for _, tst := range testDSNs {
if _, err := ParseDSN(tst.in); err != nil {
b.Error(err.Error())
}
}
}
}
// 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/.
package mysql
import (
"errors"
"fmt"
"log"
"os"
)
// Various errors the driver might return. Can change between driver versions.
var (
ErrInvalidConn = errors.New("invalid connection")
ErrMalformPkt = errors.New("malformed packet")
ErrNoTLS = errors.New("TLS requested but server does not support TLS")
ErrCleartextPassword = errors.New("this user requires clear text authentication. If you still want to use it, please add 'allowCleartextPasswords=1' to your DSN")
ErrNativePassword = errors.New("this user requires mysql native password authentication")
ErrOldPassword = errors.New("this user requires old password authentication. If you still want to use it, please add 'allowOldPasswords=1' to your DSN. See also https://github.com/go-sql-driver/mysql/wiki/old_passwords")
ErrUnknownPlugin = errors.New("this authentication plugin is not supported")
ErrOldProtocol = errors.New("MySQL server does not support required protocol 41+")
ErrPktSync = errors.New("commands out of sync. You can't run this command now")
ErrPktSyncMul = errors.New("commands out of sync. Did you run multiple statements at once?")
ErrPktTooLarge = errors.New("packet for query is too large. Try adjusting the `Config.MaxAllowedPacket`")
ErrBusyBuffer = errors.New("busy buffer")
// errBadConnNoWrite is used for connection errors where nothing was sent to the database yet.
// If this happens first in a function starting a database interaction, it should be replaced by driver.ErrBadConn
// to trigger a resend.
// See https://github.com/go-sql-driver/mysql/pull/302
errBadConnNoWrite = errors.New("bad connection")
)
var defaultLogger = Logger(log.New(os.Stderr, "[mysql] ", log.Ldate|log.Ltime|log.Lshortfile))
// Logger is used to log critical error messages.
type Logger interface {
Print(v ...interface{})
}
// NopLogger is a nop implementation of the Logger interface.
type NopLogger struct{}
// Print implements Logger interface.
func (nl *NopLogger) Print(_ ...interface{}) {}
// SetLogger is used to set the default logger for critical errors.
// The initial logger is os.Stderr.
func SetLogger(logger Logger) error {
if logger == nil {
return errors.New("logger is nil")
}
defaultLogger = logger
return nil
}
// MySQLError is an error type which represents a single MySQL error
type MySQLError struct {
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
}
// 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/.
package mysql
import (
"bytes"
"errors"
"log"
"testing"
)
func TestErrorsSetLogger(t *testing.T) {
previous := defaultLogger
defer func() {
defaultLogger = previous
}()
// set up logger
const expected = "prefix: test\n"
buffer := bytes.NewBuffer(make([]byte, 0, 64))
logger := log.New(buffer, "prefix: ", 0)
// print
SetLogger(logger)
defaultLogger.Print("test")
// check result
if actual := buffer.String(); actual != expected {
t.Errorf("expected %q, got %q", expected, actual)
}
}
func TestErrorsStrictIgnoreNotes(t *testing.T) {
runTests(t, dsn+"&sql_notes=false", func(dbt *DBTest) {
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)
}
}
// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
//
// Copyright 2017 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/.
package mysql
import (
"database/sql"
"reflect"
)
func (mf *mysqlField) typeDatabaseName() string {
switch mf.fieldType {
case fieldTypeBit:
return "BIT"
case fieldTypeBLOB:
if mf.charSet != binaryCollationID {
return "TEXT"
}
return "BLOB"
case fieldTypeDate:
return "DATE"
case fieldTypeDateTime:
return "DATETIME"
case fieldTypeDecimal:
return "DECIMAL"
case fieldTypeDouble:
return "DOUBLE"
case fieldTypeEnum:
return "ENUM"
case fieldTypeFloat:
return "FLOAT"
case fieldTypeGeometry:
return "GEOMETRY"
case fieldTypeInt24:
if mf.flags&flagUnsigned != 0 {
return "UNSIGNED MEDIUMINT"
}
return "MEDIUMINT"
case fieldTypeJSON:
return "JSON"
case fieldTypeLong:
if mf.flags&flagUnsigned != 0 {
return "UNSIGNED INT"
}
return "INT"
case fieldTypeLongBLOB:
if mf.charSet != binaryCollationID {
return "LONGTEXT"
}
return "LONGBLOB"
case fieldTypeLongLong:
if mf.flags&flagUnsigned != 0 {
return "UNSIGNED BIGINT"
}
return "BIGINT"
case fieldTypeMediumBLOB:
if mf.charSet != binaryCollationID {
return "MEDIUMTEXT"
}
return "MEDIUMBLOB"
case fieldTypeNewDate:
return "DATE"
case fieldTypeNewDecimal:
return "DECIMAL"
case fieldTypeNULL:
return "NULL"
case fieldTypeSet:
return "SET"
case fieldTypeShort:
if mf.flags&flagUnsigned != 0 {
return "UNSIGNED SMALLINT"
}
return "SMALLINT"
case fieldTypeString:
if mf.flags&flagEnum != 0 {
return "ENUM"
} else if mf.flags&flagSet != 0 {
return "SET"
}
if mf.charSet == binaryCollationID {
return "BINARY"
}
return "CHAR"
case fieldTypeTime:
return "TIME"
case fieldTypeTimestamp:
return "TIMESTAMP"
case fieldTypeTiny:
if mf.flags&flagUnsigned != 0 {
return "UNSIGNED TINYINT"
}
return "TINYINT"
case fieldTypeTinyBLOB:
if mf.charSet != binaryCollationID {
return "TINYTEXT"
}
return "TINYBLOB"
case fieldTypeVarChar:
if mf.charSet == binaryCollationID {
return "VARBINARY"
}
return "VARCHAR"
case fieldTypeVarString:
if mf.charSet == binaryCollationID {
return "VARBINARY"
}
return "VARCHAR"
case fieldTypeYear:
return "YEAR"
default:
return ""
}
}
var (
scanTypeFloat32 = reflect.TypeOf(float32(0))
scanTypeFloat64 = reflect.TypeOf(float64(0))
scanTypeInt8 = reflect.TypeOf(int8(0))
scanTypeInt16 = reflect.TypeOf(int16(0))
scanTypeInt32 = reflect.TypeOf(int32(0))
scanTypeInt64 = reflect.TypeOf(int64(0))
scanTypeNullFloat = reflect.TypeOf(sql.NullFloat64{})
scanTypeNullInt = reflect.TypeOf(sql.NullInt64{})
scanTypeNullTime = reflect.TypeOf(sql.NullTime{})
scanTypeUint8 = reflect.TypeOf(uint8(0))
scanTypeUint16 = reflect.TypeOf(uint16(0))
scanTypeUint32 = reflect.TypeOf(uint32(0))
scanTypeUint64 = reflect.TypeOf(uint64(0))
scanTypeString = reflect.TypeOf("")
scanTypeNullString = reflect.TypeOf(sql.NullString{})
scanTypeBytes = reflect.TypeOf([]byte{})
scanTypeUnknown = reflect.TypeOf(new(interface{}))
)
type mysqlField struct {
tableName string
name string
length uint32
flags fieldFlag
fieldType fieldType
decimals byte
charSet uint8
}
func (mf *mysqlField) scanType() reflect.Type {
switch mf.fieldType {
case fieldTypeTiny:
if mf.flags&flagNotNULL != 0 {
if mf.flags&flagUnsigned != 0 {
return scanTypeUint8
}
return scanTypeInt8
}
return scanTypeNullInt
case fieldTypeShort, fieldTypeYear:
if mf.flags&flagNotNULL != 0 {
if mf.flags&flagUnsigned != 0 {
return scanTypeUint16
}
return scanTypeInt16
}
return scanTypeNullInt
case fieldTypeInt24, fieldTypeLong:
if mf.flags&flagNotNULL != 0 {
if mf.flags&flagUnsigned != 0 {
return scanTypeUint32
}
return scanTypeInt32
}
return scanTypeNullInt
case fieldTypeLongLong:
if mf.flags&flagNotNULL != 0 {
if mf.flags&flagUnsigned != 0 {
return scanTypeUint64
}
return scanTypeInt64
}
return scanTypeNullInt
case fieldTypeFloat:
if mf.flags&flagNotNULL != 0 {
return scanTypeFloat32
}
return scanTypeNullFloat
case fieldTypeDouble:
if mf.flags&flagNotNULL != 0 {
return scanTypeFloat64
}
return scanTypeNullFloat
case fieldTypeBit, fieldTypeTinyBLOB, fieldTypeMediumBLOB, fieldTypeLongBLOB,
fieldTypeBLOB, fieldTypeVarString, fieldTypeString, fieldTypeGeometry:
if mf.charSet == binaryCollationID {
return scanTypeBytes
}
fallthrough
case fieldTypeDecimal, fieldTypeNewDecimal, fieldTypeVarChar,
fieldTypeEnum, fieldTypeSet, fieldTypeJSON, fieldTypeTime:
if mf.flags&flagNotNULL != 0 {
return scanTypeString
}
return scanTypeNullString
case fieldTypeDate, fieldTypeNewDate,
fieldTypeTimestamp, fieldTypeDateTime:
// NullTime is always returned for more consistent behavior as it can
// handle both cases of parseTime regardless if the field is nullable.
return scanTypeNullTime
default:
return scanTypeUnknown
}
}