Skip to content

Commit 64a96e2

Browse files
committed
Implement Go version of SQL Commenter
This implements the SQL Commenter specification for affixing comments to a SQL query. Loosely inspired by url.Values [1] in the Go standard library. The specification doesn't fully describe the following situation, so I'll list the intended behavior of the Go implementation here: - Empty keys: omitted and don't appear in the comment string. - Keys with single quotes: this Go library uses the valid URL encoding of a single quote `%27` instead of escaping with a backslash. The spec has the confusing example: `name''` serialized to `name=\'\'`. Where did the equal sign come from? Also, the website shows fancy quotes, not plain ascii quotes. - Already URL encoded values: this Go library double-encodes the value, meaning the key-pair `a=%3D` is encoded as `a='%253D'`. The spec exhibit doesn't double-encode, instead preserving the original percent encoding. That seems inadvisable since the parsing a serialized sql comment won't result in the same original values. Fixes google#95. [1]: https://pkg.go.dev/net/url#Values
1 parent 0ed325c commit 64a96e2

File tree

3 files changed

+133
-0
lines changed

3 files changed

+133
-0
lines changed

go/sqlcommenter/go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module github.com/google/sqlcommenter/go/sqlcommenter

go/sqlcommenter/sqlcommenter.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package sqlcommenter
2+
3+
import (
4+
"net/url"
5+
"sort"
6+
"strings"
7+
)
8+
9+
// Values maps a string key to a value for that key to attach to a SQL query
10+
// in a comment. Implements the SQL Commenter spec:
11+
// https://google.github.io/sqlcommenter
12+
type Values map[string]string
13+
14+
// String returns the string representing all values according to the SQL
15+
// Commenter spec.
16+
func (vs Values) String() string {
17+
if len(vs) == 0 {
18+
return ""
19+
}
20+
21+
pairs := make([]string, 0, len(vs))
22+
for k, v := range vs {
23+
if k == "" {
24+
continue
25+
}
26+
pairs = append(pairs, serializeKey(k)+"="+serializeValue(v))
27+
}
28+
29+
if len(pairs) == 0 {
30+
return "" // we might have dropped only empty keys
31+
}
32+
33+
// Spec requires sorted key-value pairs after running the serialization
34+
// algorithm.
35+
sort.Strings(pairs)
36+
37+
return "/*" + strings.Join(pairs, ",") + "*/"
38+
}
39+
40+
// https://google.github.io/sqlcommenter/spec/#key-serialization-algorithm
41+
func serializeKey(s string) string {
42+
esc := urlEncode(s)
43+
return escapeMeta(esc)
44+
}
45+
46+
// https://google.github.io/sqlcommenter/spec/#value-serialization-algorithm
47+
func serializeValue(s string) string {
48+
esc := urlEncode(s)
49+
return `'` + escapeMeta(esc) + `'`
50+
}
51+
52+
func urlEncode(s string) string {
53+
esc := url.QueryEscape(s)
54+
// Go encodes spaces as "+"; use more standard %20.
55+
return strings.Replace(esc, "+", "%20", -1)
56+
}
57+
58+
func escapeMeta(s string) string {
59+
return strings.Replace(s, `'`, `\'`, -1)
60+
}

go/sqlcommenter/sqlcommenter_test.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package sqlcommenter
2+
3+
import (
4+
"strings"
5+
"testing"
6+
)
7+
8+
func TestValues_String(t *testing.T) {
9+
tests := []struct {
10+
name string
11+
vs Values
12+
want string
13+
}{
14+
{name: "nil", vs: nil, want: ""},
15+
{name: "empty", vs: Values{}, want: ""},
16+
{name: "empty cast", vs: Values(map[string]string{}), want: ""},
17+
{
18+
name: "drop empty key",
19+
vs: Values(map[string]string{"": "val"}),
20+
want: "",
21+
},
22+
{
23+
name: "one",
24+
vs: Values(map[string]string{"key": "val"}),
25+
want: "/*key='val'*/",
26+
},
27+
{
28+
name: "two",
29+
vs: Values(map[string]string{"a": "1", "b": "2"}),
30+
want: "/*a='1',b='2'*/",
31+
},
32+
{
33+
name: "two reversed",
34+
vs: Values(map[string]string{"b": "2", "a": "1"}), // technically, Go map iteration is random
35+
want: "/*a='1',b='2'*/",
36+
},
37+
{
38+
name: "name=DROP TABLE FOO",
39+
vs: Values(map[string]string{"name": "DROP TABLE FOO"}),
40+
want: "/*name='DROP%20TABLE%20FOO'*/",
41+
},
42+
{
43+
name: `name''=DROP TABLE USERS'`,
44+
vs: Values(map[string]string{"name''": `DROP TABLE USERS'`}),
45+
want: `/*name%27%27='DROP%20TABLE%20USERS%27'*/`,
46+
},
47+
{
48+
name: `exhibit`, // https://google.github.io/sqlcommenter/spec/#sql-commenter-exhibit
49+
vs: Values(map[string]string{
50+
"action": `%2Fparam*d`,
51+
"controller": `index`,
52+
"framework": `spring`,
53+
"traceparent": `00-5bd66ef5095369c7b0d1f8f4bd33716a-c532cb4098ac3dd2-01`,
54+
"tracestate": `congo%3Dt61rcWkgMzE%2Crojo%3D00f067aa0ba902b7`,
55+
}),
56+
want: "/*" + strings.Join([]string{
57+
"action='%252Fparam%2Ad'",
58+
"controller='index'",
59+
"framework='spring'",
60+
"traceparent='00-5bd66ef5095369c7b0d1f8f4bd33716a-c532cb4098ac3dd2-01'",
61+
"tracestate='congo%253Dt61rcWkgMzE%252Crojo%253D00f067aa0ba902b7'",
62+
}, ",") + "*/",
63+
},
64+
}
65+
for _, tt := range tests {
66+
t.Run(tt.name, func(t *testing.T) {
67+
if got := tt.vs.String(); got != tt.want {
68+
t.Errorf("\nwant: %v\ngot: %v", tt.want, got)
69+
}
70+
})
71+
}
72+
}

0 commit comments

Comments
 (0)