-
Notifications
You must be signed in to change notification settings - Fork 2.3k
Placeholder interpolation #309
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 9 commits
e35fa00
c8c9bb1
cac6129
b7c2c47
f3b82fd
058ce87
42956fa
e6bf23a
b473259
42a1efd
3c8fa90
6c8484b
04866ee
dd7b87c
9faabe5
468b9e5
0297315
0b75396
9f84dfb
8826242
916a1f2
88aeb98
43536c7
0c7ae46
c285e39
fcea447
bfbe6c5
d65f96a
e11c825
b4f0315
1fd0514
20b75cd
e517683
0f22bc2
52a5860
2a634df
90cb6c3
9437b61
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,6 +13,7 @@ import ( | |
"database/sql/driver" | ||
"errors" | ||
"net" | ||
"strconv" | ||
"strings" | ||
"time" | ||
) | ||
|
@@ -26,26 +27,28 @@ type mysqlConn struct { | |
maxPacketAllowed int | ||
maxWriteSize int | ||
flags clientFlag | ||
status statusFlag | ||
sequence uint8 | ||
parseTime bool | ||
strict bool | ||
} | ||
|
||
type config struct { | ||
user string | ||
passwd string | ||
net string | ||
addr string | ||
dbname string | ||
params map[string]string | ||
loc *time.Location | ||
tls *tls.Config | ||
timeout time.Duration | ||
collation uint8 | ||
allowAllFiles bool | ||
allowOldPasswords bool | ||
clientFoundRows bool | ||
columnsWithAlias bool | ||
user string | ||
passwd string | ||
net string | ||
addr string | ||
dbname string | ||
params map[string]string | ||
loc *time.Location | ||
tls *tls.Config | ||
timeout time.Duration | ||
collation uint8 | ||
allowAllFiles bool | ||
allowOldPasswords bool | ||
clientFoundRows bool | ||
columnsWithAlias bool | ||
substitutePlaceholder bool | ||
} | ||
|
||
// Handles parameters set in DSN after the connection is established | ||
|
@@ -162,28 +165,101 @@ func (mc *mysqlConn) Prepare(query string) (driver.Stmt, error) { | |
return stmt, err | ||
} | ||
|
||
// https://github.com/mysql/mysql-server/blob/mysql-5.7.5/libmysql/libmysql.c#L1150-L1156 | ||
func (mc *mysqlConn) escapeBytes(v []byte) string { | ||
var escape func([]byte) []byte | ||
if mc.status&statusNoBackslashEscapes == 0 { | ||
escape = EscapeString | ||
} else { | ||
escape = EscapeQuotes | ||
} | ||
return "'" + string(escape(v)) + "'" | ||
} | ||
|
||
func (mc *mysqlConn) buildQuery(query string, args []driver.Value) (string, error) { | ||
chunks := strings.Split(query, "?") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To be taken with a grain of salt - this is only called once per query. Still... I'd like to reduce the number of allocations - that's also why I sent you the playground link. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'll rewrite it in two-pass way. First pass is for calculating required length. Second pass is for building There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've forgot about formatting float. |
||
if len(chunks) != len(args)+1 { | ||
return "", driver.ErrSkip | ||
} | ||
|
||
parts := make([]string, len(chunks)+len(args)) | ||
parts[0] = chunks[0] | ||
|
||
for i, arg := range args { | ||
pos := i*2 + 1 | ||
parts[pos+1] = chunks[i+1] | ||
if arg == nil { | ||
parts[pos] = "NULL" | ||
continue | ||
} | ||
switch v := arg.(type) { | ||
case int64: | ||
parts[pos] = strconv.FormatInt(v, 10) | ||
case float64: | ||
parts[pos] = strconv.FormatFloat(v, 'f', -1, 64) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 'g' instead of 'f'? That may be a little smaller in some cases. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fixed |
||
case bool: | ||
if v { | ||
parts[pos] = "1" | ||
} else { | ||
parts[pos] = "0" | ||
} | ||
case time.Time: | ||
if v.IsZero() { | ||
parts[pos] = "'0000-00-00'" | ||
} else { | ||
fmt := "'2006-01-02 15:04:05.999999'" | ||
parts[pos] = v.In(mc.cfg.loc).Format(fmt) | ||
} | ||
case []byte: | ||
if v == nil { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. not needed, handled in L191 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Required since |
||
parts[pos] = "NULL" | ||
} else { | ||
parts[pos] = mc.escapeBytes(v) | ||
} | ||
case string: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When []byte and string are essentially the same code, can they be in one There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh, I don't know that I can use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I can't... |
||
parts[pos] = mc.escapeBytes([]byte(v)) | ||
default: | ||
return "", driver.ErrSkip | ||
} | ||
} | ||
pktSize := len(query) + 4 // 4 bytes for header. | ||
for _, p := range parts { | ||
pktSize += len(p) | ||
} | ||
if pktSize > mc.maxPacketAllowed { | ||
return "", driver.ErrSkip | ||
} | ||
return strings.Join(parts, ""), nil | ||
} | ||
|
||
func (mc *mysqlConn) Exec(query string, args []driver.Value) (driver.Result, error) { | ||
if mc.netConn == nil { | ||
errLog.Print(ErrInvalidConn) | ||
return nil, driver.ErrBadConn | ||
} | ||
if len(args) == 0 { // no args, fastpath | ||
mc.affectedRows = 0 | ||
mc.insertId = 0 | ||
|
||
err := mc.exec(query) | ||
if err == nil { | ||
return &mysqlResult{ | ||
affectedRows: int64(mc.affectedRows), | ||
insertId: int64(mc.insertId), | ||
}, err | ||
if len(args) != 0 { | ||
if !mc.cfg.substitutePlaceholder { | ||
return nil, driver.ErrSkip | ||
} | ||
return nil, err | ||
// try client-side prepare to reduce roundtrip | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This comment is a bit confusing (same in L368). Maybe use something like this instead: |
||
prepared, err := mc.buildQuery(query, args) | ||
if err != nil { | ||
return nil, err | ||
} | ||
query = prepared | ||
args = nil | ||
} | ||
mc.affectedRows = 0 | ||
mc.insertId = 0 | ||
|
||
// with args, must use prepared stmt | ||
return nil, driver.ErrSkip | ||
|
||
err := mc.exec(query) | ||
if err == nil { | ||
return &mysqlResult{ | ||
affectedRows: int64(mc.affectedRows), | ||
insertId: int64(mc.insertId), | ||
}, err | ||
} | ||
return nil, err | ||
} | ||
|
||
// Internal function to execute commands | ||
|
@@ -212,31 +288,38 @@ func (mc *mysqlConn) Query(query string, args []driver.Value) (driver.Rows, erro | |
errLog.Print(ErrInvalidConn) | ||
return nil, driver.ErrBadConn | ||
} | ||
if len(args) == 0 { // no args, fastpath | ||
// Send command | ||
err := mc.writeCommandPacketStr(comQuery, query) | ||
if len(args) != 0 { | ||
if !mc.cfg.substitutePlaceholder { | ||
return nil, driver.ErrSkip | ||
} | ||
// try client-side prepare to reduce roundtrip | ||
prepared, err := mc.buildQuery(query, args) | ||
if err != nil { | ||
return nil, err | ||
} | ||
query = prepared | ||
args = nil | ||
} | ||
// Send command | ||
err := mc.writeCommandPacketStr(comQuery, query) | ||
if err == nil { | ||
// Read Result | ||
var resLen int | ||
resLen, err = mc.readResultSetHeaderPacket() | ||
if err == nil { | ||
// Read Result | ||
var resLen int | ||
resLen, err = mc.readResultSetHeaderPacket() | ||
if err == nil { | ||
rows := new(textRows) | ||
rows.mc = mc | ||
|
||
if resLen == 0 { | ||
// no columns, no more data | ||
return emptyRows{}, nil | ||
} | ||
// Columns | ||
rows.columns, err = mc.readColumns(resLen) | ||
return rows, err | ||
rows := new(textRows) | ||
rows.mc = mc | ||
|
||
if resLen == 0 { | ||
// no columns, no more data | ||
return emptyRows{}, nil | ||
} | ||
// Columns | ||
rows.columns, err = mc.readColumns(resLen) | ||
return rows, err | ||
} | ||
return nil, err | ||
} | ||
|
||
// with args, must use prepared stmt | ||
return nil, driver.ErrSkip | ||
return nil, err | ||
} | ||
|
||
// Gets the value of the given MySQL System Variable | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -130,3 +130,25 @@ const ( | |
flagUnknown3 | ||
flagUnknown4 | ||
) | ||
|
||
// http://dev.mysql.com/doc/internals/en/status-flags.html | ||
|
||
type statusFlag uint16 | ||
|
||
const ( | ||
statusInTrans statusFlag = 1 << iota | ||
statusInAutocommit | ||
statusUnknown1 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fixed |
||
statusMoreResultsExists | ||
statusNoGoodIndexUsed | ||
statusNoIndexUsed | ||
statusCursorExists | ||
statusLastRowSent | ||
statusDbDropped | ||
statusNoBackslashEscapes | ||
statusMetadataChanged | ||
statusQueryWasSlow | ||
statusPsOutParams | ||
statusInTransReadonly | ||
statusSessionStateChanged | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about
substitutePlaceholder
instead ofbuildQuery
?