Skip to content

Commit 128cc5d

Browse files
committed
Initial commit.
0 parents  commit 128cc5d

15 files changed

+742
-0
lines changed

.drone.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
kind: pipeline
3+
name: default
4+
5+
clone:
6+
depth: 50
7+
8+
steps:
9+
- name: build
10+
image: golang:1.17
11+
commands:
12+
- go test -v ./...

.gitignore

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# os
2+
.DS_Store
3+
Thumbs.db
4+
Desktop.ini
5+
6+
# Compiled Object files, Static and Dynamic libs (Shared Objects)
7+
*.o
8+
*.a
9+
*.so
10+
11+
# Folders
12+
_obj
13+
_test
14+
15+
# Architecture specific extensions/prefixes
16+
*.[568vq]
17+
[568vq].out
18+
19+
*.cgo1.go
20+
*.cgo2.c
21+
_cgo_defun.c
22+
_cgo_gotypes.go
23+
_cgo_export.*
24+
25+
_testmain.go
26+
27+
*.exe
28+
*.test
29+
*.prof
30+
31+
# coverage
32+
*.out
33+
34+
# goland
35+
.idea/

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
## v0.0.1
2+
3+
- Initial release.

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
The MIT License (MIT)
2+
3+
Copyright (c) 2021 Juraj Bubniak
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# sqlcommenter
2+
[![GoDoc](https://img.shields.io/badge/go-documentation-blue.svg?style=flat-square)](https://godoc.org/github.com/jbub/sqlcommenter)
3+
[![Build Status](https://cloud.drone.io/api/badges/jbub/sqlcommenter/status.svg)](https://cloud.drone.io/jbub/sqlcommenter)
4+
[![Go Report Card](https://goreportcard.com/badge/github.com/jbub/sqlcommenter)](https://goreportcard.com/report/github.com/jbub/sqlcommenter)
5+
6+
Go implementation of https://google.github.io/sqlcommenter/.
7+
8+
## Usage with pgx stdlib driver
9+
10+
```go
11+
package main
12+
13+
import (
14+
"context"
15+
"database/sql"
16+
17+
"github.com/jackc/pgx/v4/stdlib"
18+
"github.com/jbub/sqlcommenter"
19+
)
20+
21+
type contextKey int
22+
23+
const contextKeyUserID contextKey = 0
24+
25+
func withUserID(ctx context.Context, key string) context.Context {
26+
return context.WithValue(ctx, contextKeyUserID, key)
27+
}
28+
29+
func userIDFromContext(ctx context.Context) string {
30+
return ctx.Value(contextKeyUserID).(string)
31+
}
32+
33+
func main() {
34+
pgxDrv := stdlib.GetDefaultDriver()
35+
drv := sqlcommenter.WrapDriver(pgxDrv,
36+
sqlcommenter.WithAttrPairs("application", "hello-app"),
37+
sqlcommenter.WithAttrFunc(func(ctx context.Context) sqlcommenter.Attrs {
38+
return sqlcommenter.AttrPairs("user-id", userIDFromContext(ctx))
39+
}),
40+
)
41+
42+
sql.Register("pgx-sqlcommenter", drv)
43+
44+
db, err := sql.Open("pgx-sqlcommenter", "postgres://user@host:5432/db")
45+
if err != nil {
46+
// handle error
47+
}
48+
defer db.Close()
49+
50+
ctx := context.Background()
51+
52+
rows, err := db.QueryContext(withUserID(ctx, "22"), "SELECT 1")
53+
if err != nil {
54+
// handle error
55+
}
56+
defer rows.Close()
57+
58+
// will produce the following query: SELECT 1 /* application='hello-app',user-id='22' */
59+
}
60+
```

attrs.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package sqlcommenter
2+
3+
import (
4+
"net/url"
5+
"sort"
6+
"strings"
7+
)
8+
9+
func AttrPairs(pairs ...string) Attrs {
10+
if len(pairs)%2 == 1 {
11+
panic("got odd number of pairs")
12+
}
13+
attrs := make(Attrs, len(pairs)/2)
14+
for i := 0; i < len(pairs); i += 2 {
15+
attrs[pairs[i]] = pairs[i+1]
16+
}
17+
return attrs
18+
}
19+
20+
type Attr struct {
21+
Key string
22+
Value string
23+
}
24+
25+
type Attrs map[string]string
26+
27+
func (a Attrs) Encode() string {
28+
total := len(a)
29+
keys := make([]string, 0, total)
30+
for k := range a {
31+
keys = append(keys, k)
32+
}
33+
sort.Strings(keys)
34+
35+
var b strings.Builder
36+
for i, key := range keys {
37+
b.WriteString(encodeKey(key))
38+
b.WriteByte('=')
39+
b.WriteString(encodeValue(a[key]))
40+
if i < total-1 {
41+
b.WriteByte(',')
42+
}
43+
}
44+
return b.String()
45+
}
46+
47+
func (a Attrs) Update(other Attrs) {
48+
for k, v := range other {
49+
a[k] = v
50+
}
51+
}
52+
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+
}

attrs_test.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package sqlcommenter
2+
3+
import (
4+
"testing"
5+
)
6+
7+
func TestAttrsEncode(t *testing.T) {
8+
cases := []struct {
9+
name string
10+
attrs Attrs
11+
want string
12+
}{
13+
{
14+
name: "no attrs",
15+
},
16+
{
17+
name: "single attr",
18+
attrs: map[string]string{
19+
"key": "value",
20+
},
21+
want: "key='value'",
22+
},
23+
{
24+
name: "multiple attrs",
25+
attrs: map[string]string{
26+
"key": "value",
27+
"2key": "value 33",
28+
"key3": "44 value",
29+
},
30+
want: "2key='value%2033',key='value',key3='44%20%20value'",
31+
},
32+
}
33+
34+
for _, cs := range cases {
35+
t.Run(cs.name, func(t *testing.T) {
36+
got := cs.attrs.Encode()
37+
if want := cs.want; want != got {
38+
t.Errorf("got '%v', want '%v'", got, want)
39+
}
40+
})
41+
}
42+
}

comment.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package sqlcommenter
2+
3+
import (
4+
"context"
5+
"strings"
6+
)
7+
8+
const (
9+
commentStart = "/*"
10+
commentEnd = "*/"
11+
)
12+
13+
func Comment(query string, opts ...Option) string {
14+
if len(opts) == 0 {
15+
return query
16+
}
17+
if strings.Contains(query, commentStart) {
18+
return query
19+
}
20+
return newCommenter(opts...).comment(context.Background(), query)
21+
}
22+
23+
func newCommenter(opts ...Option) *commenter {
24+
cmt := &commenter{}
25+
for _, opt := range opts {
26+
opt(cmt)
27+
}
28+
return cmt
29+
}
30+
31+
type commenter struct {
32+
providers []AttrProvider
33+
}
34+
35+
func (c *commenter) comment(ctx context.Context, query string) string {
36+
attrs := c.attrs(ctx)
37+
if len(attrs) == 0 {
38+
return query
39+
}
40+
return query + " " + commentStart + " " + attrs.Encode() + " " + commentEnd
41+
}
42+
43+
func (c *commenter) attrs(ctx context.Context) Attrs {
44+
if len(c.providers) == 0 {
45+
return nil
46+
}
47+
attrs := make(Attrs)
48+
for _, prov := range c.providers {
49+
attrs.Update(prov.GetAttrs(ctx))
50+
}
51+
return attrs
52+
}

comment_test.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package sqlcommenter
2+
3+
import (
4+
"testing"
5+
)
6+
7+
func TestComment(t *testing.T) {
8+
cases := []struct {
9+
name string
10+
query string
11+
opts []Option
12+
want string
13+
}{
14+
{
15+
name: "empty query",
16+
},
17+
{
18+
name: "empty query with whitespace",
19+
query: " ",
20+
want: " ",
21+
},
22+
{
23+
name: "query with comment",
24+
query: "SELECT 1 /* comment */",
25+
want: "SELECT 1 /* comment */",
26+
},
27+
{
28+
name: "query without attrs",
29+
query: "SELECT 1",
30+
want: "SELECT 1",
31+
},
32+
{
33+
name: "query with single attr",
34+
query: "SELECT 1",
35+
opts: []Option{WithAttrPairs("key", "value")},
36+
want: "SELECT 1 /* key='value' */",
37+
},
38+
{
39+
name: "query with multiple attrs",
40+
query: "SELECT 1",
41+
opts: []Option{WithAttrPairs("key", "1value", "key2", " value 2")},
42+
want: "SELECT 1 /* key='1value',key2='%20%20value%202' */",
43+
},
44+
}
45+
46+
for _, cs := range cases {
47+
t.Run(cs.name, func(t *testing.T) {
48+
got := Comment(cs.query, cs.opts...)
49+
if want := cs.want; want != got {
50+
t.Fatalf("got '%v', want '%v'", got, want)
51+
}
52+
})
53+
}
54+
}

0 commit comments

Comments
 (0)