From 64a96e228c8aeb7ac90d61dd97e055734193d929 Mon Sep 17 00:00:00 2001 From: Joe Schafer Date: Mon, 21 Mar 2022 01:00:28 -0700 Subject: [PATCH] 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 #95. [1]: https://pkg.go.dev/net/url#Values --- go/sqlcommenter/go.mod | 1 + go/sqlcommenter/sqlcommenter.go | 60 +++++++++++++++++++++++ go/sqlcommenter/sqlcommenter_test.go | 72 ++++++++++++++++++++++++++++ 3 files changed, 133 insertions(+) create mode 100644 go/sqlcommenter/go.mod create mode 100644 go/sqlcommenter/sqlcommenter.go create mode 100644 go/sqlcommenter/sqlcommenter_test.go diff --git a/go/sqlcommenter/go.mod b/go/sqlcommenter/go.mod new file mode 100644 index 00000000..b71fd7d4 --- /dev/null +++ b/go/sqlcommenter/go.mod @@ -0,0 +1 @@ +module github.com/google/sqlcommenter/go/sqlcommenter diff --git a/go/sqlcommenter/sqlcommenter.go b/go/sqlcommenter/sqlcommenter.go new file mode 100644 index 00000000..c4a34505 --- /dev/null +++ b/go/sqlcommenter/sqlcommenter.go @@ -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) +} diff --git a/go/sqlcommenter/sqlcommenter_test.go b/go/sqlcommenter/sqlcommenter_test.go new file mode 100644 index 00000000..5026869a --- /dev/null +++ b/go/sqlcommenter/sqlcommenter_test.go @@ -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) + } + }) + } +}