Skip to content

Commit 89bdedf

Browse files
committed
Support optional resultset metadata
Allow optional resultset metadata. Can potentially improve the performance in many scenario. Issue #1105
1 parent 46351a8 commit 89bdedf

10 files changed

+171
-31
lines changed

AUTHORS

+1
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ Tan Jinhua <312841925 at qq.com>
9090
Thomas Wodarek <wodarekwebpage at gmail.com>
9191
Tim Ruffles <timruffles at gmail.com>
9292
Tom Jenkinson <tom at tjenkinson.me>
93+
Tzu-Chiao Yeh <su3g4284zo6y7 at gmail.com>
9394
Vladimir Kovpak <cn007b at gmail.com>
9495
Xiangyu Hu <xiangyu.hu at outlook.com>
9596
Xiaobing Jiang <s7v7nislands at gmail.com>

README.md

+11
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,17 @@ Allow multiple statements in one query. While this allows batch queries, it also
282282

283283
When `multiStatements` is used, `?` parameters must only be used in the first statement.
284284

285+
##### `resultSetMetadata`
286+
287+
```
288+
Type: string
289+
Valid Values: "full", "none"
290+
Default: empty
291+
```
292+
293+
Allow resultset metadata being optional.
294+
By making resultset metadata transfer being optional, can potentially improve queries performance.
295+
285296
##### `parseTime`
286297

287298
```

connection.go

+34-14
Original file line numberDiff line numberDiff line change
@@ -21,20 +21,21 @@ 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+
resultSetMetadata uint8
3839

3940
// for context support (Go 1.8+)
4041
watching bool
@@ -392,6 +393,10 @@ func (mc *mysqlConn) query(query string, args []driver.Value) (*textRows, error)
392393
}
393394
}
394395

396+
if mc.cfg.ResultSetMetadata != "" && mc.resultSetMetadata == resultSetMetadataNone {
397+
return mc.readIgnoreColumns(rows, resLen)
398+
}
399+
395400
// Columns
396401
rows.rs.columns, err = mc.readColumns(resLen)
397402
return rows, err
@@ -400,6 +405,21 @@ func (mc *mysqlConn) query(query string, args []driver.Value) (*textRows, error)
400405
return nil, mc.markBadConn(err)
401406
}
402407

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

connector.go

+18
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,24 @@ func (c *connector) Connect(ctx context.Context) (driver.Conn, error) {
129129
mc.maxWriteSize = mc.maxAllowedPacket
130130
}
131131

132+
// Additional handling for result set optional metadata
133+
if mc.cfg.ResultSetMetadata != "" {
134+
err = mc.exec("SET resultset_metadata=" + mc.cfg.ResultSetMetadata)
135+
if err != nil {
136+
mc.Close()
137+
return nil, err
138+
}
139+
switch mc.cfg.ResultSetMetadata {
140+
case resultSetMetadataSysVarNone:
141+
mc.resultSetMetadata = resultSetMetadataNone
142+
case resultSetMetadataSysVarFull:
143+
mc.resultSetMetadata = resultSetMetadataFull
144+
default:
145+
mc.Close()
146+
return nil, ErrOptionalResultSet
147+
}
148+
}
149+
132150
// Handle DSN Params
133151
err = mc.handleParams()
134152
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

+40
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ var (
4343
prot string
4444
addr string
4545
dbname string
46+
vendor string
4647
dsn string
4748
netAddr string
4849
available bool
@@ -1344,6 +1345,45 @@ func TestFoundRows(t *testing.T) {
13441345
})
13451346
}
13461347

1348+
func TestOptionalResultSetMetadata(t *testing.T) {
1349+
runTests(t, dsn+"&resultSetMetadata=none", func(dbt *DBTest) {
1350+
_, err := dbt.db.Exec("CREATE TABLE test (id INT NOT NULL ,data INT NOT NULL)")
1351+
// Error 1193: Unknown system variable 'resultset_metadata' => skip test,
1352+
// MySQL server version is too old
1353+
maybeSkip(t, err, 1193)
1354+
dbt.mustExec("INSERT INTO test (id, data) VALUES (0, 0),(0, 0),(1, 0),(1, 0),(1, 1)")
1355+
1356+
row := dbt.db.QueryRow("SELECT id, data FROM test WHERE id = 1")
1357+
id, data := 0, 0
1358+
err = row.Scan(&id, &data)
1359+
if err != nil {
1360+
dbt.Fatal(err)
1361+
}
1362+
1363+
if id != 1 && data != 0 {
1364+
dbt.Fatal("invalid result")
1365+
}
1366+
})
1367+
runTests(t, dsn+"&resultSetMetadata=full", func(dbt *DBTest) {
1368+
_, err := dbt.db.Exec("CREATE TABLE test (id INT NOT NULL ,data INT NOT NULL)")
1369+
// Error 1193: Unknown system variable 'resultset_metadata' => skip test,
1370+
// MySQL server version is too old
1371+
maybeSkip(t, err, 1193)
1372+
dbt.mustExec("INSERT INTO test (id, data) VALUES (0, 0),(0, 0),(1, 0),(1, 0),(1, 1)")
1373+
1374+
row := dbt.db.QueryRow("SELECT id, data FROM test WHERE id = 1")
1375+
id, data := 0, 0
1376+
err = row.Scan(&id, &data)
1377+
if err != nil {
1378+
dbt.Fatal(err)
1379+
}
1380+
1381+
if id != 1 && data != 0 {
1382+
dbt.Fatal("invalid result")
1383+
}
1384+
})
1385+
}
1386+
13471387
func TestTLS(t *testing.T) {
13481388
tlsTestReq := func(dbt *DBTest) {
13491389
if err := dbt.db.Ping(); err != nil {

dsn.go

+33-16
Original file line numberDiff line numberDiff line change
@@ -34,22 +34,23 @@ 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
53+
ResultSetMetadata string // Allow optional resultset metadata
5354

5455
AllowAllFiles bool // Allow all files to be used with LOAD DATA LOCAL INFILE
5556
AllowCleartextPasswords bool // Allows the cleartext client side plugin
@@ -240,6 +241,10 @@ func (cfg *Config) FormatDSN() string {
240241
writeDSNParam(&buf, &hasParam, "multiStatements", "true")
241242
}
242243

244+
if cfg.ResultSetMetadata != "" {
245+
writeDSNParam(&buf, &hasParam, "resultSetMetadata", strings.ToLower(cfg.ResultSetMetadata))
246+
}
247+
243248
if cfg.ParseTime {
244249
writeDSNParam(&buf, &hasParam, "parseTime", "true")
245250
}
@@ -465,6 +470,18 @@ func parseDSNParams(cfg *Config, params string) (err error) {
465470
return errors.New("invalid bool value: " + value)
466471
}
467472

473+
// allow resultset metadata being optional
474+
case "resultSetMetadata":
475+
// Pre-check resultSetMetadata.
476+
// Although so far there's only two modes FULL and NONE, in the future it may be extended.
477+
// Because if any potential extensions introduced will force us do the read path change,
478+
// failed earlier when parsing DSN.
479+
upperVal := strings.ToUpper(value)
480+
if upperVal != resultSetMetadataSysVarFull && upperVal != resultSetMetadataSysVarNone {
481+
return errors.New("invalid resultset metadata, allow FULL and NONE only")
482+
}
483+
cfg.ResultSetMetadata = upperVal
484+
468485
// time.Time parsing
469486
case "parseTime":
470487
var isBool bool

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&resultSetMetadata=none",
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, ResultSetMetadata: "NONE"},
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

+2-1
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,15 @@ var (
2121
ErrMalformPkt = errors.New("malformed packet")
2222
ErrNoTLS = errors.New("TLS requested but server does not support TLS")
2323
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.")
24+
ErrNativePassword = errors.New("this user requires mysql native password authentication")
2525
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")
2626
ErrUnknownPlugin = errors.New("this authentication plugin is not supported")
2727
ErrOldProtocol = errors.New("MySQL server does not support required protocol 41+")
2828
ErrPktSync = errors.New("commands out of sync. You can't run this command now")
2929
ErrPktSyncMul = errors.New("commands out of sync. Did you run multiple statements at once?")
3030
ErrPktTooLarge = errors.New("packet for query is too large. Try adjusting the 'max_allowed_packet' variable on the server")
3131
ErrBusyBuffer = errors.New("busy buffer")
32+
ErrOptionalResultSet = errors.New("malformed optional resultset metadata packets")
3233

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

packets.go

+15
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,10 @@ func (mc *mysqlConn) writeHandshakeResponsePacket(authResp []byte, plugin string
301301
clientFlags |= clientMultiStatements
302302
}
303303

304+
if mc.cfg.ResultSetMetadata != "" {
305+
clientFlags |= clientOptionalResultSetMetadata
306+
}
307+
304308
// encode length of the auth plugin data
305309
var authRespLEIBuf [9]byte
306310
authRespLen := len(authResp)
@@ -555,6 +559,17 @@ func (mc *mysqlConn) readResultSetHeaderPacket() (int, error) {
555559
return int(num), nil
556560
}
557561

562+
// Sniff one extra byte for resultset metadata if we set capability
563+
// CLIENT_OPTIONAL_RESULTSET_METADTA
564+
// https://dev.mysql.com/worklog/task/?id=8134
565+
if len(data) == 2 {
566+
// ResultSet metadata flag check
567+
if mc.resultSetMetadata != data[1] {
568+
return 0, ErrOptionalResultSet
569+
}
570+
return int(num), nil
571+
}
572+
558573
return 0, ErrMalformPkt
559574
}
560575
return 0, err

0 commit comments

Comments
 (0)