Skip to content

Commit acbdeec

Browse files
tz70sTzu-Chiao Yeh
authored and
Tzu-Chiao Yeh
committed
Support optional resultset metadata
Allow optional resultset metadata. Can potentially improve the performance in many scenarios. Issue #1105
1 parent 217d050 commit acbdeec

10 files changed

+168
-43
lines changed

AUTHORS

+1
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ Tan Jinhua <312841925 at qq.com>
9595
Thomas Wodarek <wodarekwebpage at gmail.com>
9696
Tim Ruffles <timruffles at gmail.com>
9797
Tom Jenkinson <tom at tjenkinson.me>
98+
Tzu-Chiao Yeh <su3g4284zo6y7 at gmail.com>
9899
Vladimir Kovpak <cn007b at gmail.com>
99100
Vladyslav Zhelezniak <zhvladi at gmail.com>
100101
Xiangyu Hu <xiangyu.hu at outlook.com>

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,7 @@ Examples:
399399
* `autocommit=1`: `SET autocommit=1`
400400
* [`time_zone=%27Europe%2FParis%27`](https://dev.mysql.com/doc/refman/5.5/en/time-zone-support.html): `SET time_zone='Europe/Paris'`
401401
* [`transaction_isolation=%27REPEATABLE-READ%27`](https://dev.mysql.com/doc/refman/5.7/en/server-system-variables.html#sysvar_transaction_isolation): `SET transaction_isolation='REPEATABLE-READ'`
402+
* metata=none`](https://dev.mysql.com/doc/refman/8.0/en/server-system-variables.html#sysvar_resultset_metadata): `SET resultset_metadata=none` (note that this is only applicable to MySQL 8.0+ versions).
402403

403404

404405
#### Examples

connection.go

+35-14
Original file line numberDiff line numberDiff line change
@@ -21,20 +21,22 @@ import (
2121
)
2222

2323
type mysqlConn struct {
24-
buf buffer
25-
netConn net.Conn
26-
rawConn net.Conn // underlying connection when netConn is TLS connection.
27-
affectedRows uint64
28-
insertId uint64
29-
cfg *Config
30-
maxAllowedPacket int
31-
maxWriteSize int
32-
writeTimeout time.Duration
33-
flags clientFlag
34-
status statusFlag
35-
sequence uint8
36-
parseTime bool
37-
reset bool // set when the Go SQL package calls ResetSession
24+
buf buffer
25+
netConn net.Conn
26+
rawConn net.Conn // underlying connection when netConn is TLS connection.
27+
affectedRows uint64
28+
insertId uint64
29+
cfg *Config
30+
maxAllowedPacket int
31+
maxWriteSize int
32+
writeTimeout time.Duration
33+
flags clientFlag
34+
status statusFlag
35+
sequence uint8
36+
parseTime bool
37+
reset bool // set when the Go SQL package calls ResetSession
38+
optionalResultSetMetadata bool
39+
resultSetMetadata uint8
3840

3941
// for context support (Go 1.8+)
4042
watching bool
@@ -392,6 +394,10 @@ func (mc *mysqlConn) query(query string, args []driver.Value) (*textRows, error)
392394
}
393395
}
394396

397+
if mc.optionalResultSetMetadata && mc.resultSetMetadata == resultSetMetadataNone {
398+
return mc.readIgnoreColumns(rows, resLen)
399+
}
400+
395401
// Columns
396402
rows.rs.columns, err = mc.readColumns(resLen)
397403
return rows, err
@@ -400,6 +406,21 @@ func (mc *mysqlConn) query(query string, args []driver.Value) (*textRows, error)
400406
return nil, mc.markBadConn(err)
401407
}
402408

409+
func (mc *mysqlConn) readIgnoreColumns(rows *textRows, resLen int) (*textRows, error) {
410+
data, err := mc.readPacket()
411+
if err != nil {
412+
errLog.Print(err)
413+
return nil, err
414+
}
415+
// Expected an EOF packet
416+
if data[0] == iEOF && (len(data) == 5 || len(data) == 1) {
417+
// Set empty columnNames, we will first read these columnNames via rows.Columns().
418+
rows.rs.columnNames = make([]string, resLen)
419+
return rows, nil
420+
}
421+
return nil, ErrOptionalResultSetMetadataPkt
422+
}
423+
403424
// Gets the value of the given MySQL System Variable
404425
// The returned byte slice is only valid until the next read
405426
func (mc *mysqlConn) getSystemVar(name string) ([]byte, error) {

connector.go

+15
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"context"
1313
"database/sql/driver"
1414
"net"
15+
"strings"
1516
)
1617

1718
type connector struct {
@@ -88,6 +89,20 @@ func (c *connector) Connect(ctx context.Context) (driver.Conn, error) {
8889
plugin = defaultAuthPlugin
8990
}
9091

92+
// Set the optionalResultSetMetadata ahead to set the client capability flag.
93+
if resultSetMetadata, ok := mc.cfg.Params["resultset_metadata"]; ok {
94+
upperVal := strings.ToUpper(resultSetMetadata)
95+
switch upperVal {
96+
case resultSetMetadataSysVarNone:
97+
mc.optionalResultSetMetadata = true
98+
mc.resultSetMetadata = resultSetMetadataNone
99+
case resultSetMetadataSysVarFull:
100+
mc.optionalResultSetMetadata = true
101+
mc.resultSetMetadata = resultSetMetadataFull
102+
}
103+
// To be consistent with other params, in case the param is passed wrongly still send to MySQL to let the server side rejects it.
104+
}
105+
91106
// Send Client Authentication Packet
92107
authResp, err := mc.auth(authData, plugin)
93108
if err != nil {

const.go

+14
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ const (
5656
clientCanHandleExpiredPasswords
5757
clientSessionTrack
5858
clientDeprecateEOF
59+
clientOptionalResultSetMetadata
5960
)
6061

6162
const (
@@ -172,3 +173,16 @@ const (
172173
cachingSha2PasswordFastAuthSuccess = 3
173174
cachingSha2PasswordPerformFullAuthentication = 4
174175
)
176+
177+
const (
178+
// One-byte metadata flag
179+
// https://dev.mysql.com/worklog/task/?id=8134
180+
resultSetMetadataNone uint8 = iota
181+
resultSetMetadataFull
182+
)
183+
184+
const (
185+
// ResultSet Metadata system var
186+
resultSetMetadataSysVarNone = "NONE"
187+
resultSetMetadataSysVarFull = "FULL"
188+
)

driver_test.go

+45
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ var (
4444
prot string
4545
addr string
4646
dbname string
47+
vendor string
4748
dsn string
4849
netAddr string
4950
available bool
@@ -202,6 +203,7 @@ func (dbt *DBTest) mustQuery(query string, args ...interface{}) (rows *sql.Rows)
202203
func maybeSkip(t *testing.T, err error, skipErrno uint16) {
203204
mySQLErr, ok := err.(*MySQLError)
204205
if !ok {
206+
errLog.Print("non match")
205207
return
206208
}
207209

@@ -1345,6 +1347,49 @@ func TestFoundRows(t *testing.T) {
13451347
})
13461348
}
13471349

1350+
func TestOptionalResultSetMetadata(t *testing.T) {
1351+
runTests(t, dsn+"&resultSetMetadata=none", func(dbt *DBTest) {
1352+
_, err := dbt.db.Exec("CREATE TABLE test (id INT NOT NULL ,data INT NOT NULL)")
1353+
if err == ErrNoOptionalResultMetadataSet {
1354+
t.Skip("server does not support resultset metadata")
1355+
} else if err != nil {
1356+
dbt.Fatal(err)
1357+
}
1358+
dbt.mustExec("INSERT INTO test (id, data) VALUES (0, 0),(0, 0),(1, 0),(1, 0),(1, 1)")
1359+
1360+
row := dbt.db.QueryRow("SELECT id, data FROM test WHERE id = 1")
1361+
id, data := 0, 0
1362+
err = row.Scan(&id, &data)
1363+
if err != nil {
1364+
dbt.Fatal(err)
1365+
}
1366+
1367+
if id != 1 && data != 0 {
1368+
dbt.Fatal("invalid result")
1369+
}
1370+
})
1371+
runTests(t, dsn+"&resultSetMetadata=full", func(dbt *DBTest) {
1372+
_, err := dbt.db.Exec("CREATE TABLE test (id INT NOT NULL ,data INT NOT NULL)")
1373+
if err == ErrNoOptionalResultMetadataSet {
1374+
t.Skip("server does not support resultset metadata")
1375+
} else if err != nil {
1376+
dbt.Fatal(err)
1377+
}
1378+
dbt.mustExec("INSERT INTO test (id, data) VALUES (0, 0),(0, 0),(1, 0),(1, 0),(1, 1)")
1379+
1380+
row := dbt.db.QueryRow("SELECT id, data FROM test WHERE id = 1")
1381+
id, data := 0, 0
1382+
err = row.Scan(&id, &data)
1383+
if err != nil {
1384+
dbt.Fatal(err)
1385+
}
1386+
1387+
if id != 1 && data != 0 {
1388+
dbt.Fatal("invalid result")
1389+
}
1390+
})
1391+
}
1392+
13481393
func TestTLS(t *testing.T) {
13491394
tlsTestReq := func(dbt *DBTest) {
13501395
if err := dbt.db.Ping(); err != nil {

dsn.go

+16-16
Original file line numberDiff line numberDiff line change
@@ -34,22 +34,22 @@ var (
3434
// If a new Config is created instead of being parsed from a DSN string,
3535
// the NewConfig function should be used, which sets default values.
3636
type Config struct {
37-
User string // Username
38-
Passwd string // Password (requires User)
39-
Net string // Network type
40-
Addr string // Network address (requires Net)
41-
DBName string // Database name
42-
Params map[string]string // Connection parameters
43-
Collation string // Connection collation
44-
Loc *time.Location // Location for time.Time values
45-
MaxAllowedPacket int // Max packet size allowed
46-
ServerPubKey string // Server public key name
47-
pubKey *rsa.PublicKey // Server public key
48-
TLSConfig string // TLS configuration name
49-
tls *tls.Config // TLS configuration
50-
Timeout time.Duration // Dial timeout
51-
ReadTimeout time.Duration // I/O read timeout
52-
WriteTimeout time.Duration // I/O write timeout
37+
User string // Username
38+
Passwd string // Password (requires User)
39+
Net string // Network type
40+
Addr string // Network address (requires Net)
41+
DBName string // Database name
42+
Params map[string]string // Connection parameters
43+
Collation string // Connection collation
44+
Loc *time.Location // Location for time.Time values
45+
MaxAllowedPacket int // Max packet size allowed
46+
ServerPubKey string // Server public key name
47+
pubKey *rsa.PublicKey // Server public key
48+
TLSConfig string // TLS configuration name
49+
tls *tls.Config // TLS configuration
50+
Timeout time.Duration // Dial timeout
51+
ReadTimeout time.Duration // I/O read timeout
52+
WriteTimeout time.Duration // I/O write timeout
5353

5454
AllowAllFiles bool // Allow all files to be used with LOAD DATA LOCAL INFILE
5555
AllowCleartextPasswords bool // Allows the cleartext client side plugin

dsn_test.go

+3
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ var testDSNs = []struct {
4444
}, {
4545
"user:password@/dbname?allowNativePasswords=false&checkConnLiveness=false&maxAllowedPacket=0",
4646
&Config{User: "user", Passwd: "password", Net: "tcp", Addr: "127.0.0.1:3306", DBName: "dbname", Collation: "utf8mb4_general_ci", Loc: time.UTC, MaxAllowedPacket: 0, AllowNativePasswords: false, CheckConnLiveness: false},
47+
}, {
48+
"user:password@/dbname?allowNativePasswords=false&checkConnLiveness=false&maxAllowedPacket=0",
49+
&Config{User: "user", Passwd: "password", Net: "tcp", Addr: "127.0.0.1:3306", DBName: "dbname", Collation: "utf8mb4_general_ci", Loc: time.UTC, MaxAllowedPacket: 0, AllowNativePasswords: false, CheckConnLiveness: false},
4750
}, {
4851
"user:p@ss(word)@tcp([de:ad:be:ef::ca:fe]:80)/dbname?loc=Local",
4952
&Config{User: "user", Passwd: "p@ss(word)", Net: "tcp", Addr: "[de:ad:be:ef::ca:fe]:80", DBName: "dbname", Collation: "utf8mb4_general_ci", Loc: time.Local, MaxAllowedPacket: defaultMaxAllowedPacket, AllowNativePasswords: true, CheckConnLiveness: true},

errors.go

+14-12
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,20 @@ import (
1717

1818
// Various errors the driver might return. Can change between driver versions.
1919
var (
20-
ErrInvalidConn = errors.New("invalid connection")
21-
ErrMalformPkt = errors.New("malformed packet")
22-
ErrNoTLS = errors.New("TLS requested but server does not support TLS")
23-
ErrCleartextPassword = errors.New("this user requires clear text authentication. If you still want to use it, please add 'allowCleartextPasswords=1' to your DSN")
24-
ErrNativePassword = errors.New("this user requires mysql native password authentication.")
25-
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")
26-
ErrUnknownPlugin = errors.New("this authentication plugin is not supported")
27-
ErrOldProtocol = errors.New("MySQL server does not support required protocol 41+")
28-
ErrPktSync = errors.New("commands out of sync. You can't run this command now")
29-
ErrPktSyncMul = errors.New("commands out of sync. Did you run multiple statements at once?")
30-
ErrPktTooLarge = errors.New("packet for query is too large. Try adjusting the 'max_allowed_packet' variable on the server")
31-
ErrBusyBuffer = errors.New("busy buffer")
20+
ErrInvalidConn = errors.New("invalid connection")
21+
ErrMalformPkt = errors.New("malformed packet")
22+
ErrNoTLS = errors.New("TLS requested but server does not support TLS")
23+
ErrCleartextPassword = errors.New("this user requires clear text authentication. If you still want to use it, please add 'allowCleartextPasswords=1' to your DSN")
24+
ErrNativePassword = errors.New("this user requires mysql native password authentication")
25+
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")
26+
ErrUnknownPlugin = errors.New("this authentication plugin is not supported")
27+
ErrOldProtocol = errors.New("MySQL server does not support required protocol 41+")
28+
ErrPktSync = errors.New("commands out of sync. You can't run this command now")
29+
ErrPktSyncMul = errors.New("commands out of sync. Did you run multiple statements at once?")
30+
ErrPktTooLarge = errors.New("packet for query is too large. Try adjusting the 'max_allowed_packet' variable on the server")
31+
ErrBusyBuffer = errors.New("busy buffer")
32+
ErrNoOptionalResultMetadataSet = errors.New("requested optional resultset metadata but server does not support")
33+
ErrOptionalResultSetMetadataPkt = errors.New("malformed optional resultset metadata packets")
3234

3335
// errBadConnNoWrite is used for connection errors where nothing was sent to the database yet.
3436
// If this happens first in a function starting a database interaction, it should be replaced by driver.ErrBadConn

packets.go

+24-1
Original file line numberDiff line numberDiff line change
@@ -234,10 +234,18 @@ func (mc *mysqlConn) readHandshakePacket() (data []byte, plugin string, err erro
234234
if len(data) > pos {
235235
// character set [1 byte]
236236
// status flags [2 bytes]
237+
pos += 1 + 2
237238
// capability flags (upper 2 bytes) [2 bytes]
239+
upperFlags := clientFlag(binary.LittleEndian.Uint16(data[pos : pos+2]))
240+
mc.flags |= upperFlags << 16
241+
pos += 2
242+
if mc.flags&clientOptionalResultSetMetadata == 0 && mc.optionalResultSetMetadata {
243+
return nil, "", ErrNoOptionalResultMetadataSet
244+
}
245+
238246
// length of auth-plugin-data [1 byte]
239247
// reserved (all [00]) [10 bytes]
240-
pos += 1 + 2 + 2 + 1 + 10
248+
pos += 1 + 10
241249

242250
// second part of the password cipher [mininum 13 bytes],
243251
// where len=MAX(13, length of auth-plugin-data - 8)
@@ -300,6 +308,10 @@ func (mc *mysqlConn) writeHandshakeResponsePacket(authResp []byte, plugin string
300308
clientFlags |= clientMultiStatements
301309
}
302310

311+
if mc.optionalResultSetMetadata {
312+
clientFlags |= clientOptionalResultSetMetadata
313+
}
314+
303315
// encode length of the auth plugin data
304316
var authRespLEIBuf [9]byte
305317
authRespLen := len(authResp)
@@ -554,6 +566,17 @@ func (mc *mysqlConn) readResultSetHeaderPacket() (int, error) {
554566
return int(num), nil
555567
}
556568

569+
// Sniff one extra byte for resultset metadata if we set capability
570+
// CLIENT_OPTIONAL_RESULTSET_METADTA
571+
// https://dev.mysql.com/worklog/task/?id=8134
572+
if len(data) == 2 && mc.flags&clientOptionalResultSetMetadata != 0 {
573+
// ResultSet metadata flag check
574+
if mc.resultSetMetadata != data[1] {
575+
return 0, ErrOptionalResultSetMetadataPkt
576+
}
577+
return int(num), nil
578+
}
579+
557580
return 0, ErrMalformPkt
558581
}
559582
return 0, err

0 commit comments

Comments
 (0)