Skip to content

Commit 5b17c4e

Browse files
authored
Merge pull request #1 from jbub/encode
Optimize Attrs encoding by reducing allocations.
2 parents 8e663c4 + cf78aed commit 5b17c4e

7 files changed

Lines changed: 185 additions & 21 deletions

File tree

.drone.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,4 @@ steps:
99
- name: build
1010
image: golang:1.17
1111
commands:
12-
- go test -race -v ./...
12+
- go test -race -cover -v ./...

attrs.go

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@ package sqlcommenter
22

33
import (
44
"bytes"
5-
"net/url"
6-
"strings"
75
)
86

97
func AttrPairs(pairs ...string) Attrs {
@@ -39,25 +37,20 @@ func (a Attrs) encode(b *bytes.Buffer) {
3937
sortKeys(keys)
4038

4139
for i, key := range keys {
42-
b.WriteString(encodeKey(key))
40+
writeQueryEscape(key, b)
41+
4342
b.WriteByte('=')
4443
b.WriteByte('\'')
45-
b.WriteString(encodeValue(a[key]))
44+
45+
writePathEscape(a[key], b)
46+
4647
b.WriteByte('\'')
4748
if i < total-1 {
4849
b.WriteByte(',')
4950
}
5051
}
5152
}
5253

53-
func encodeKey(k string) string {
54-
return url.QueryEscape(k)
55-
}
56-
57-
func encodeValue(v string) string {
58-
return strings.ReplaceAll(url.PathEscape(v), "+", "%20")
59-
}
60-
6154
// sortKeys implements a simple insertion sort on string slice.
6255
// We save one alloc by not using sort.Strings.
6356
func sortKeys(keys []string) {

attrs_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,11 @@ func TestAttrsEncode(t *testing.T) {
2424
{
2525
name: "multiple attrs",
2626
attrs: map[string]string{
27-
"key": "value",
28-
"2key": "value 33",
29-
"key3": "44 value",
27+
"key": "DROP TABLE FOO",
28+
"2key": "/param first",
29+
"name": "1234",
3030
},
31-
want: "2key='value%2033',key='value',key3='44%20%20value'",
31+
want: "2key='%2Fparam%20first',key='DROP%20TABLE%20FOO',name='1234'",
3232
},
3333
}
3434

comment.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,6 @@ func (c *commenter) attrs(ctx context.Context) Attrs {
7575

7676
var bufPool = sync.Pool{
7777
New: func() interface{} {
78-
return &bytes.Buffer{}
78+
return bytes.NewBuffer(make([]byte, 0, 100))
7979
},
8080
}

comment_test.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,16 +77,17 @@ func TestCommentConcurrent(t *testing.T) {
7777
}
7878

7979
func BenchmarkComment(b *testing.B) {
80-
b.ReportAllocs()
81-
b.SetBytes(2)
82-
8380
ctx := context.Background()
8481
cmt := newCommenter(WithAttrs(map[string]string{
8582
"key": "value",
8683
"2key": "value 33",
8784
"key3": "44 value",
8885
}))
8986

87+
b.ReportAllocs()
88+
b.SetBytes(2)
89+
b.ResetTimer()
90+
9091
for i := 0; i < b.N; i++ {
9192
cmt.comment(ctx, "SELECT * FROM my_table WHERE column IS NOT NULL")
9293
}

escape.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package sqlcommenter
2+
3+
// Code is adapted from standard library package net/url.
4+
// Copyright (c) 2009 The Go Authors. All rights reserved.
5+
6+
import (
7+
"bytes"
8+
)
9+
10+
const upperhex = "0123456789ABCDEF"
11+
12+
func writeQueryEscape(s string, b *bytes.Buffer) {
13+
writeEscape(s, true, b)
14+
}
15+
16+
func writePathEscape(s string, b *bytes.Buffer) {
17+
writeEscape(s, false, b)
18+
}
19+
20+
func writeEscape(s string, query bool, b *bytes.Buffer) {
21+
spaceCount, hexCount := 0, 0
22+
for i := 0; i < len(s); i++ {
23+
c := s[i]
24+
if shouldEscape(c, query) {
25+
if c == ' ' && query {
26+
spaceCount++
27+
} else {
28+
hexCount++
29+
}
30+
}
31+
}
32+
33+
if spaceCount == 0 && hexCount == 0 {
34+
b.WriteString(s)
35+
return
36+
}
37+
38+
if hexCount == 0 {
39+
for i := 0; i < len(s); i++ {
40+
if s[i] == ' ' {
41+
b.WriteByte('+')
42+
} else {
43+
b.WriteByte(s[i])
44+
}
45+
}
46+
return
47+
}
48+
49+
for i := 0; i < len(s); i++ {
50+
switch c := s[i]; {
51+
case c == ' ' && query:
52+
b.WriteByte('+')
53+
case shouldEscape(c, query):
54+
b.WriteByte('%')
55+
b.WriteByte(upperhex[c>>4])
56+
b.WriteByte(upperhex[c&15])
57+
default:
58+
b.WriteByte(c)
59+
}
60+
}
61+
}
62+
63+
func shouldEscape(c byte, query bool) bool {
64+
if 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || '0' <= c && c <= '9' {
65+
return false
66+
}
67+
switch c {
68+
case '-', '_', '.', '~':
69+
return false
70+
case '$', '&', '+', ',', '/', ':', ';', '=', '?', '@':
71+
if query {
72+
return true
73+
}
74+
return c == '/' || c == ';' || c == ',' || c == '?'
75+
}
76+
return true
77+
}

escape_test.go

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package sqlcommenter
2+
3+
// Code is adapted from standard library package net/url.
4+
// Copyright (c) 2009 The Go Authors. All rights reserved.
5+
6+
import (
7+
"bytes"
8+
"testing"
9+
)
10+
11+
func TestQueryEscape(t *testing.T) {
12+
cases := []struct {
13+
in string
14+
want string
15+
}{
16+
{
17+
in: "",
18+
want: "",
19+
},
20+
{
21+
in: "abc",
22+
want: "abc",
23+
},
24+
{
25+
in: "one two",
26+
want: "one+two",
27+
},
28+
{
29+
in: "10%",
30+
want: "10%25",
31+
},
32+
{
33+
in: " ?&=#+%!<>#\"{}|\\^[]`☺\t:/@$'()*,;",
34+
want: "+%3F%26%3D%23%2B%25%21%3C%3E%23%22%7B%7D%7C%5C%5E%5B%5D%60%E2%98%BA%09%3A%2F%40%24%27%28%29%2A%2C%3B",
35+
},
36+
}
37+
38+
for _, cs := range cases {
39+
t.Run(cs.in, func(t *testing.T) {
40+
var b bytes.Buffer
41+
writeQueryEscape(cs.in, &b)
42+
if got := b.String(); cs.want != got {
43+
t.Errorf("got %q, want %q", got, cs.want)
44+
}
45+
})
46+
}
47+
}
48+
49+
func TestPathEscape(t *testing.T) {
50+
cases := []struct {
51+
in string
52+
want string
53+
}{
54+
{
55+
in: "",
56+
want: "",
57+
},
58+
{
59+
in: "abc",
60+
want: "abc",
61+
},
62+
{
63+
in: "abc+def",
64+
want: "abc+def",
65+
},
66+
{
67+
in: "a/b",
68+
want: "a%2Fb",
69+
},
70+
{
71+
in: "one two",
72+
want: "one%20two",
73+
},
74+
{
75+
in: "10%",
76+
want: "10%25",
77+
},
78+
{
79+
in: " ?&=#+%!<>#\"{}|\\^[]`☺\t:/@$'()*,;",
80+
want: "%20%3F&=%23+%25%21%3C%3E%23%22%7B%7D%7C%5C%5E%5B%5D%60%E2%98%BA%09:%2F@$%27%28%29%2A%2C%3B",
81+
},
82+
}
83+
84+
for _, cs := range cases {
85+
t.Run(cs.in, func(t *testing.T) {
86+
var b bytes.Buffer
87+
writePathEscape(cs.in, &b)
88+
if got := b.String(); cs.want != got {
89+
t.Errorf("got %q, want %q", got, cs.want)
90+
}
91+
})
92+
}
93+
}

0 commit comments

Comments
 (0)