Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -482,6 +482,8 @@ Rules:
* The values for string variables must be quoted with `'`.
* The values must also be [url.QueryEscape](http://golang.org/pkg/net/url/#QueryEscape)'ed!
(which implies values of string variables must be wrapped with `%27`).
* System variables are set and retained by `FormatDSN` in the order they appear in the DSN.
Use `Config.Apply(AddParam(name, value))` to preserve order when adding them programmatically.

Examples:
* `autocommit=1`: `SET autocommit=1`
Expand Down
20 changes: 14 additions & 6 deletions connection.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,13 +103,21 @@ func (mc *mysqlConn) syncSequence() {
}

// Handles parameters set in DSN after the connection is established
func (mc *mysqlConn) handleParams() (err error) {
func (mc *mysqlConn) handleParams() error {
cmdSet := mc.cfg.setParamsCommand()
if cmdSet == "" {
return nil
}
return mc.exec(cmdSet)
}

func (cfg *Config) setParamsCommand() string {
var cmdSet strings.Builder

for param, val := range mc.cfg.Params {
writeParam := func(param, val string) {
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.Grow(4 + len(param) + 3 + len(val) + 30*(len(cfg.Params)-1))
cmdSet.WriteString("SET ")
} else {
cmdSet.WriteString(", ")
Expand All @@ -119,11 +127,11 @@ func (mc *mysqlConn) handleParams() (err error) {
cmdSet.WriteString(val)
}

if cmdSet.Len() > 0 {
err = mc.exec(cmdSet.String())
for _, param := range cfg.orderedParams() {
writeParam(param, cfg.Params[param])
}

return
return cmdSet.String()
}

// markBadConn replaces errBadConnNoWrite with driver.ErrBadConn.
Expand Down
76 changes: 64 additions & 12 deletions dsn.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"math/big"
"net"
"net/url"
"slices"
"sort"
"strconv"
"strings"
Expand All @@ -43,7 +44,7 @@ type Config struct {
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
Params map[string]string // Connection parameters. Use Apply(AddParam(...)) to preserve insertion order.
ConnectionAttributes string // Connection Attributes, comma-delimited string of user-defined "key:value" pairs
Collation string // Connection collation. When set, this will be set in SET NAMES <charset> COLLATE <collation> query
Loc *time.Location // Location for time.Time values
Expand Down Expand Up @@ -80,6 +81,7 @@ type Config struct {
tinyInt1IsBool bool // Treat signed TINYINT(1) as boolean

beforeConnect func(context.Context, *Config) error // Invoked before a connection is established
paramOrder []string // Order of connection parameters parsed from the DSN
pubKey *rsa.PublicKey // Server public key
timeTruncate time.Duration // Truncate time.Time values to the specified duration
charsets []string // Connection charset. When set, this will be set in SET NAMES <charset> query
Expand Down Expand Up @@ -113,6 +115,27 @@ func (c *Config) Apply(opts ...Option) error {
return nil
}

// AddParam adds a connection parameter.
//
// Parameters added with AddParam retain their order in [Config.FormatDSN] and
// are applied to the connection in that order. If name already exists, it is
// moved to the end of the parameter order.
func AddParam(name, value string) Option {
return func(cfg *Config) error {
params := cfg.orderedParams()
if i := slices.Index(params, name); i >= 0 {
params = slices.Delete(params, i, i+1)
}

if cfg.Params == nil {
cfg.Params = make(map[string]string)
}
cfg.Params[name] = value
cfg.paramOrder = append(params, name)
return nil
}
}

// TimeTruncate sets the time duration to truncate time.Time values in
// query parameters.
func TimeTruncate(d time.Duration) Option {
Expand Down Expand Up @@ -166,10 +189,11 @@ func (cfg *Config) Clone() *Config {
if cp.TLS != nil {
cp.TLS = cfg.TLS.Clone()
}
if len(cp.Params) > 0 {
if cp.Params != nil {
cp.Params = make(map[string]string, len(cfg.Params))
maps.Copy(cp.Params, cfg.Params)
}
cp.paramOrder = slices.Clone(cfg.paramOrder)
if cfg.pubKey != nil {
cp.pubKey = &rsa.PublicKey{
N: new(big.Int).Set(cfg.pubKey.N),
Expand Down Expand Up @@ -398,18 +422,37 @@ func (cfg *Config) FormatDSN() string {
}

// other params
if cfg.Params != nil {
var params []string
for param := range cfg.Params {
params = append(params, param)
for _, param := range cfg.orderedParams() {
writeDSNParam(&buf, &hasParam, param, url.QueryEscape(cfg.Params[param]))
}

return buf.String()
}

// orderedParams returns connection parameter names in their recorded order,
// followed by any names added directly to Config.Params in sorted order.
func (cfg *Config) orderedParams() []string {
params := make([]string, 0, len(cfg.Params))
seen := make(map[string]struct{}, len(cfg.Params))
for _, param := range cfg.paramOrder {
if _, ok := cfg.Params[param]; !ok {
continue
}
sort.Strings(params)
for _, param := range params {
writeDSNParam(&buf, &hasParam, param, url.QueryEscape(cfg.Params[param]))
if _, ok := seen[param]; ok {
continue
}
params = append(params, param)
seen[param] = struct{}{}
}

return buf.String()
unordered := make([]string, 0, len(cfg.Params)-len(params))
for param := range cfg.Params {
if _, ok := seen[param]; !ok {
unordered = append(unordered, param)
}
}
sort.Strings(unordered)
return append(params, unordered...)
}

// ParseDSN parses the DSN string to a Config
Expand Down Expand Up @@ -706,14 +749,23 @@ func parseDSNParams(cfg *Config, params string) (err error) {
cfg.ConnectionAttributes = connectionAttributes

default:
value, err = url.QueryUnescape(value)
if err != nil {
return
}

// lazy init
if cfg.Params == nil {
cfg.Params = make(map[string]string)
}

if cfg.Params[key], err = url.QueryUnescape(value); err != nil {
return
// Keep the last occurrence of a duplicate parameter, matching the
// existing map semantics, and place it at its last position.
if i := slices.Index(cfg.paramOrder, key); i >= 0 {
cfg.paramOrder = slices.Delete(cfg.paramOrder, i, i+1)
}
cfg.Params[key] = value
cfg.paramOrder = append(cfg.paramOrder, key)
}
}

Expand Down
83 changes: 79 additions & 4 deletions dsn_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,13 @@ var testDSNs = []struct {
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, tinyInt1IsBool: 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, tinyInt1IsBool: true, paramOrder: []string{"param"}},
}, {
"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, tinyInt1IsBool: true, 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, tinyInt1IsBool: true, ColumnsWithAlias: true, paramOrder: []string{"param"}},
}, {
"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, tinyInt1IsBool: true, 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, tinyInt1IsBool: true, ColumnsWithAlias: true, MultiStatements: true, paramOrder: []string{"param"}},
}, {
"user@unix(/path/to/socket)/dbname?charset=utf8",
&Config{User: "user", Net: "unix", Addr: "/path/to/socket", DBName: "dbname", charsets: []string{"utf8"}, Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, Logger: defaultLogger, AllowNativePasswords: true, CheckConnLiveness: true, tinyInt1IsBool: true},
Expand Down Expand Up @@ -67,7 +67,7 @@ var testDSNs = []struct {
&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, tinyInt1IsBool: 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, tinyInt1IsBool: true},
&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, tinyInt1IsBool: true, paramOrder: []string{"arg"}},
}, {
"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, tinyInt1IsBool: true},
Expand Down Expand Up @@ -382,6 +382,76 @@ func TestParamsAreSorted(t *testing.T) {
}
}

func TestFormatDSNPreservesParamOrder(t *testing.T) {
dsn := "/dbname?second=2&first=1"
cfg, err := ParseDSN(dsn)
if err != nil {
t.Fatal(err)
}
want := "tcp(127.0.0.1:3306)" + dsn
if got := cfg.FormatDSN(); got != want {
t.Fatalf("FormatDSN() = %q, want %q", got, want)
}
}

func TestAddParam(t *testing.T) {
cfg := NewConfig()
if err := cfg.Apply(
AddParam("second", "2"),
AddParam("first", "1"),
AddParam("second", "new"),
); err != nil {
t.Fatal(err)
}

if got, want := cfg.FormatDSN(), "/?first=1&second=new"; got != want {
t.Fatalf("FormatDSN() = %q, want %q", got, want)
}
if got, want := cfg.setParamsCommand(), "SET first = 1, second = new"; got != want {
t.Fatalf("setParamsCommand() = %q, want %q", got, want)
}
}

func TestAddParamAfterDirectParams(t *testing.T) {
cfg := NewConfig()
cfg.Params = map[string]string{"second": "2", "first": "1"}
if err := cfg.Apply(AddParam("third", "3")); err != nil {
t.Fatal(err)
}

if got, want := cfg.FormatDSN(), "/?first=1&second=2&third=3"; got != want {
t.Fatalf("FormatDSN() = %q, want %q", got, want)
}
}

func TestDSNParamOrder(t *testing.T) {
dsn := "/?aurora_read_replica_read_committed=1&transaction_isolation=%27READ-COMMITTED%27"
cfg, err := ParseDSN(dsn)
if err != nil {
t.Fatal(err)
}

want := "SET aurora_read_replica_read_committed = 1, transaction_isolation = 'READ-COMMITTED'"
if got := cfg.setParamsCommand(); got != want {
t.Fatalf("setParamsCommand() = %q, want %q", got, want)
}
}

func TestDuplicateDSNParamOrder(t *testing.T) {
cfg, err := ParseDSN("/?first=old&second=2&first=new")
if err != nil {
t.Fatal(err)
}

want := "SET second = 2, first = new"
if got := cfg.setParamsCommand(); got != want {
t.Fatalf("setParamsCommand() = %q, want %q", got, want)
}
if got, want := cfg.FormatDSN(), "tcp(127.0.0.1:3306)/?second=2&first=new"; got != want {
t.Fatalf("FormatDSN() = %q, want %q", got, want)
}
}

func TestCloneConfig(t *testing.T) {
RegisterServerPubKey("testKey", testPubKeyRSA)
defer DeregisterServerPubKey("testKey")
Expand Down Expand Up @@ -417,6 +487,11 @@ func TestCloneConfig(t *testing.T) {
t.Errorf("custom params in cloned Config should not propagate to original Config")
}

cfg2.paramOrder[0] = "changed"
if cfg.paramOrder[0] == cfg2.paramOrder[0] {
t.Errorf("param order 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")
}
Expand Down
Loading