Skip to content

Commit

Permalink
Implement Go version of SQL Commenter
Browse files Browse the repository at this point in the history
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
  • Loading branch information
jschaf committed Mar 21, 2022
1 parent 0ed325c commit 64a96e2
Show file tree
Hide file tree
Showing 3 changed files with 133 additions and 0 deletions.
1 change: 1 addition & 0 deletions go/sqlcommenter/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module github.com/google/sqlcommenter/go/sqlcommenter
60 changes: 60 additions & 0 deletions go/sqlcommenter/sqlcommenter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package sqlcommenter

import (
"net/url"
"sort"
"strings"
)

// Values maps a string key to a value for that key to attach to a SQL query
// in a comment. Implements the SQL Commenter spec:
// https://google.github.io/sqlcommenter
type Values map[string]string

// String returns the string representing all values according to the SQL
// Commenter spec.
func (vs Values) String() string {
if len(vs) == 0 {
return ""
}

pairs := make([]string, 0, len(vs))
for k, v := range vs {
if k == "" {
continue
}
pairs = append(pairs, serializeKey(k)+"="+serializeValue(v))
}

if len(pairs) == 0 {
return "" // we might have dropped only empty keys
}

// Spec requires sorted key-value pairs after running the serialization
// algorithm.
sort.Strings(pairs)

return "/*" + strings.Join(pairs, ",") + "*/"
}

// https://google.github.io/sqlcommenter/spec/#key-serialization-algorithm
func serializeKey(s string) string {
esc := urlEncode(s)
return escapeMeta(esc)
}

// https://google.github.io/sqlcommenter/spec/#value-serialization-algorithm
func serializeValue(s string) string {
esc := urlEncode(s)
return `'` + escapeMeta(esc) + `'`
}

func urlEncode(s string) string {
esc := url.QueryEscape(s)
// Go encodes spaces as "+"; use more standard %20.
return strings.Replace(esc, "+", "%20", -1)
}

func escapeMeta(s string) string {
return strings.Replace(s, `'`, `\'`, -1)
}
72 changes: 72 additions & 0 deletions go/sqlcommenter/sqlcommenter_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package sqlcommenter

import (
"strings"
"testing"
)

func TestValues_String(t *testing.T) {
tests := []struct {
name string
vs Values
want string
}{
{name: "nil", vs: nil, want: ""},
{name: "empty", vs: Values{}, want: ""},
{name: "empty cast", vs: Values(map[string]string{}), want: ""},
{
name: "drop empty key",
vs: Values(map[string]string{"": "val"}),
want: "",
},
{
name: "one",
vs: Values(map[string]string{"key": "val"}),
want: "/*key='val'*/",
},
{
name: "two",
vs: Values(map[string]string{"a": "1", "b": "2"}),
want: "/*a='1',b='2'*/",
},
{
name: "two reversed",
vs: Values(map[string]string{"b": "2", "a": "1"}), // technically, Go map iteration is random
want: "/*a='1',b='2'*/",
},
{
name: "name=DROP TABLE FOO",
vs: Values(map[string]string{"name": "DROP TABLE FOO"}),
want: "/*name='DROP%20TABLE%20FOO'*/",
},
{
name: `name''=DROP TABLE USERS'`,
vs: Values(map[string]string{"name''": `DROP TABLE USERS'`}),
want: `/*name%27%27='DROP%20TABLE%20USERS%27'*/`,
},
{
name: `exhibit`, // https://google.github.io/sqlcommenter/spec/#sql-commenter-exhibit
vs: Values(map[string]string{
"action": `%2Fparam*d`,
"controller": `index`,
"framework": `spring`,
"traceparent": `00-5bd66ef5095369c7b0d1f8f4bd33716a-c532cb4098ac3dd2-01`,
"tracestate": `congo%3Dt61rcWkgMzE%2Crojo%3D00f067aa0ba902b7`,
}),
want: "/*" + strings.Join([]string{
"action='%252Fparam%2Ad'",
"controller='index'",
"framework='spring'",
"traceparent='00-5bd66ef5095369c7b0d1f8f4bd33716a-c532cb4098ac3dd2-01'",
"tracestate='congo%253Dt61rcWkgMzE%252Crojo%253D00f067aa0ba902b7'",
}, ",") + "*/",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.vs.String(); got != tt.want {
t.Errorf("\nwant: %v\ngot: %v", tt.want, got)
}
})
}
}

0 comments on commit 64a96e2

Please sign in to comment.