├── History.md ├── Makefile ├── Readme.md ├── escape.go ├── escape_test.go └── reserved.go /History.md: -------------------------------------------------------------------------------- 1 | 2 | v1.1.0 / 2014-11-19 3 | =================== 4 | 5 | * add WITHIN 6 | 7 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | test: 3 | @go test 4 | 5 | .PHONY: test -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | 2 | # go-pg-escape 3 | 4 | Escape Postgres queries. 5 | 6 | View the [docs](http://godoc.org/github.com/tj/go-pg-escape). 7 | 8 | ## Installation 9 | 10 | ``` 11 | $ go get github.com/tj/go-pg-escape 12 | ``` 13 | 14 | ## Example 15 | 16 | ```go 17 | s := Escape("SELECT %I FROM %I WHERE %I=%L", "some stuff", "some table", "some column", "some value") 18 | exp := `SELECT "some stuff" FROM "some table" WHERE "some column"='some value'` 19 | assert.Equal(t, exp, s) 20 | ``` 21 | 22 | # License 23 | 24 | MIT -------------------------------------------------------------------------------- /escape.go: -------------------------------------------------------------------------------- 1 | package escape 2 | 3 | import "strings" 4 | import "regexp" 5 | 6 | var ident = regexp.MustCompile(`(?i)^[a-z_][a-z0-9_$]*$`) 7 | var params = regexp.MustCompile(`%([sIL])`) 8 | 9 | // Escape the given `query` with positional `args`. 10 | func Escape(query string, args ...string) string { 11 | matches := params.FindAllStringSubmatch(query, -1) 12 | 13 | length := len(matches) 14 | argc := len(args) 15 | 16 | if argc > length { 17 | panic("too many arguments for escaped query") 18 | } 19 | 20 | if argc < length { 21 | panic("too few arguments for escaped query") 22 | } 23 | 24 | for i, match := range matches { 25 | arg := args[i] 26 | switch match[1] { 27 | case "L": 28 | query = strings.Replace(query, "%L", Literal(arg), 1) 29 | case "I": 30 | query = strings.Replace(query, "%I", Ident(arg), 1) 31 | case "s": 32 | query = strings.Replace(query, "%s", arg, 1) 33 | } 34 | } 35 | 36 | return query 37 | } 38 | 39 | // Literal escape the given string. 40 | func Literal(s string) string { 41 | p := "" 42 | 43 | if strings.Contains(s, `\`) { 44 | p = "E" 45 | } 46 | 47 | s = strings.Replace(s, `'`, `''`, -1) 48 | s = strings.Replace(s, `\`, `\\`, -1) 49 | return p + `'` + s + `'` 50 | } 51 | 52 | // Ident quotes the given identifier if required. 53 | func Ident(s string) string { 54 | if IdentNeedsQuotes(s) { 55 | return QuoteIdent(s) 56 | } else { 57 | return s 58 | } 59 | } 60 | 61 | // IdentNeedsQuotes checks if the given identifier requires quoting 62 | func IdentNeedsQuotes(s string) bool { 63 | if Reserved[strings.ToUpper(s)] { 64 | return true 65 | } 66 | 67 | return !ident.MatchString(s) 68 | } 69 | 70 | // QuoteIdent quotes the given identifier string. 71 | func QuoteIdent(s string) string { 72 | s = strings.Replace(s, `"`, `""`, -1) 73 | return `"` + s + `"` 74 | } 75 | -------------------------------------------------------------------------------- /escape_test.go: -------------------------------------------------------------------------------- 1 | package escape 2 | 3 | import "github.com/bmizerany/assert" 4 | import "testing" 5 | 6 | func TestEscape(t *testing.T) { 7 | { 8 | s := Escape("SELECT %I FROM %I WHERE %I=%L", "some stuff", "some table", "some column", "some value") 9 | exp := `SELECT "some stuff" FROM "some table" WHERE "some column"='some value'` 10 | assert.Equal(t, exp, s) 11 | } 12 | 13 | { 14 | s := Escape("COPY %s", "something") 15 | assert.Equal(t, "COPY something", s) 16 | } 17 | 18 | { 19 | s := Escape("COPY something") 20 | assert.Equal(t, "COPY something", s) 21 | } 22 | } 23 | 24 | func TestLiteral(t *testing.T) { 25 | { 26 | s := Literal(`hey`) 27 | assert.Equal(t, `'hey'`, s) 28 | } 29 | 30 | { 31 | s := Literal(`Hello World`) 32 | assert.Equal(t, `'Hello World'`, s) 33 | } 34 | 35 | { 36 | s := Literal(`O'Reilly`) 37 | assert.Equal(t, `'O''Reilly'`, s) 38 | } 39 | 40 | { 41 | s := Literal(`\\whoop\\`) 42 | assert.Equal(t, `E'\\\\whoop\\\\'`, s) 43 | } 44 | } 45 | 46 | func TestIdent(t *testing.T) { 47 | { 48 | s := Ident(`foo`) 49 | assert.Equal(t, `foo`, s) 50 | } 51 | 52 | { 53 | s := Ident(`_foo`) 54 | assert.Equal(t, `_foo`, s) 55 | } 56 | 57 | { 58 | s := Ident(`foo_$_bar`) 59 | assert.Equal(t, `foo_$_bar`, s) 60 | } 61 | 62 | { 63 | s := Ident(`test.some.stuff`) 64 | assert.Equal(t, `"test.some.stuff"`, s) 65 | } 66 | 67 | { 68 | s := Ident(`test."some".stuff`) 69 | assert.Equal(t, `"test.""some"".stuff"`, s) 70 | } 71 | 72 | { 73 | s := Ident(`join`) 74 | assert.Equal(t, `"join"`, s) 75 | } 76 | 77 | { 78 | s := Ident(`desc`) 79 | assert.Equal(t, `"desc"`, s) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /reserved.go: -------------------------------------------------------------------------------- 1 | package escape 2 | 3 | var Reserved = map[string]bool{ 4 | "AES128": true, 5 | "AES256": true, 6 | "ALL": true, 7 | "ALLOWOVERWRITE": true, 8 | "ANALYSE": true, 9 | "ANALYZE": true, 10 | "AND": true, 11 | "ANY": true, 12 | "ARRAY": true, 13 | "AS": true, 14 | "ASC": true, 15 | "AUTHORIZATION": true, 16 | "BACKUP": true, 17 | "BETWEEN": true, 18 | "BINARY": true, 19 | "BLANKSASNULL": true, 20 | "BOTH": true, 21 | "BYTEDICT": true, 22 | "CASE": true, 23 | "CAST": true, 24 | "CHECK": true, 25 | "COLLATE": true, 26 | "COLUMN": true, 27 | "CONSTRAINT": true, 28 | "CREATE": true, 29 | "CREDENTIALS": true, 30 | "CROSS": true, 31 | "CURRENT_DATE": true, 32 | "CURRENT_TIME": true, 33 | "CURRENT_TIMESTAMP": true, 34 | "CURRENT_USER": true, 35 | "CURRENT_USER_ID": true, 36 | "DEFAULT": true, 37 | "DEFERRABLE": true, 38 | "DEFLATE": true, 39 | "DEFRAG": true, 40 | "DELTA": true, 41 | "DELTA32K": true, 42 | "DESC": true, 43 | "DISABLE": true, 44 | "DISTINCT": true, 45 | "DO": true, 46 | "ELSE": true, 47 | "EMPTYASNULL": true, 48 | "ENABLE": true, 49 | "ENCODE": true, 50 | "ENCRYPT": true, 51 | "ENCRYPTION": true, 52 | "END": true, 53 | "EXCEPT": true, 54 | "EXPLICIT": true, 55 | "FALSE": true, 56 | "FOR": true, 57 | "FOREIGN": true, 58 | "FREEZE": true, 59 | "FROM": true, 60 | "FULL": true, 61 | "GLOBALDICT256": true, 62 | "GLOBALDICT64K": true, 63 | "GRANT": true, 64 | "GROUP": true, 65 | "GZIP": true, 66 | "HAVING": true, 67 | "IDENTITY": true, 68 | "IGNORE": true, 69 | "ILIKE": true, 70 | "IN": true, 71 | "INITIALLY": true, 72 | "INNER": true, 73 | "INTERSECT": true, 74 | "INTO": true, 75 | "IS": true, 76 | "ISNULL": true, 77 | "JOIN": true, 78 | "LEADING": true, 79 | "LEFT": true, 80 | "LIKE": true, 81 | "LIMIT": true, 82 | "LOCALTIME": true, 83 | "LOCALTIMESTAMP": true, 84 | "LUN": true, 85 | "LUNS": true, 86 | "LZO": true, 87 | "LZOP": true, 88 | "MINUS": true, 89 | "MOSTLY13": true, 90 | "MOSTLY32": true, 91 | "MOSTLY8": true, 92 | "NATURAL": true, 93 | "NEW": true, 94 | "NOT": true, 95 | "NOTNULL": true, 96 | "NULL": true, 97 | "NULLS": true, 98 | "OFF": true, 99 | "OFFLINE": true, 100 | "OFFSET": true, 101 | "OLD": true, 102 | "ON": true, 103 | "ONLY": true, 104 | "OPEN": true, 105 | "OR": true, 106 | "ORDER": true, 107 | "OUTER": true, 108 | "OVERLAPS": true, 109 | "PARALLEL": true, 110 | "PARTITION": true, 111 | "PERCENT": true, 112 | "PLACING": true, 113 | "PRIMARY": true, 114 | "RAW": true, 115 | "READRATIO": true, 116 | "RECOVER": true, 117 | "REFERENCES": true, 118 | "REJECTLOG": true, 119 | "RESORT": true, 120 | "RESTORE": true, 121 | "RIGHT": true, 122 | "SELECT": true, 123 | "SESSION_USER": true, 124 | "SIMILAR": true, 125 | "SOME": true, 126 | "SYSDATE": true, 127 | "SYSTEM": true, 128 | "TABLE": true, 129 | "TAG": true, 130 | "TDES": true, 131 | "TEXT255": true, 132 | "TEXT32K": true, 133 | "THEN": true, 134 | "TO": true, 135 | "TOP": true, 136 | "TRAILING": true, 137 | "TRUE": true, 138 | "TRUNCATECOLUMNS": true, 139 | "UNION": true, 140 | "UNIQUE": true, 141 | "USER": true, 142 | "USING": true, 143 | "VERBOSE": true, 144 | "WALLET": true, 145 | "WHEN": true, 146 | "WHERE": true, 147 | "WITH": true, 148 | "WITHIN": true, 149 | "WITHOUT": true, 150 | } 151 | --------------------------------------------------------------------------------