-
Notifications
You must be signed in to change notification settings - Fork 7
/
Copy pathnamedParameterQuery.go
290 lines (223 loc) · 8.38 KB
/
namedParameterQuery.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
/*
Provides support for named parameters in SQL queries used by Go / golang programs and libraries.
Named parameters are not supported by all SQL query engines, and their standards are scattered.
But positional parameters have wide adoption across all databases.
This package translates SQL queries which use named parameters into queries which use positional parameters.
Example usage:
query := NewNamedParameterQuery("
SELECT * FROM table
WHERE col1 = :foo
")
query.SetValue("foo", "bar")
connection, _ := sql.Open("mysql", "user:pass@tcp(localhost:3306)/db")
connection.QueryRow(query.GetParsedQuery(), (query.GetParsedParameters())...)
In the example above, note the format of "QueryRow". In order to use named parameter queries,
you will need to use this exact format, including the variadic symbol "..."
Note that the example above uses "QueryRow", but named parameters used in this fashion
work equally well for "Query" and "Exec".
It's also possible to pass in a map, instead of defining each parameter individually:
query := NewNamedParameterQuery("
SELECT * FROM table
WHERE col1 = :foo
AND col2 IN(:firstName, :middleName, :lastName)
")
var parameterMap = map[string]interface{} {
"foo": "bar",
"firstName": "Alice",
"lastName": "Bob"
"middleName": "Eve",
}
query.SetValuesFromMap(parameterMap)
connection, _ := sql.Open("mysql", "user:pass@tcp(localhost:3306)/db")
connection.QueryRow(query.GetParsedQuery(), (query.GetParsedParameters())...)
But of course, sometimes you just want to pass in an entire struct. No problem:
type QueryValues struct {
Foo string `sqlParameterName:"foo"`
FirstName string `sqlParameterName:"firstName"`
MiddleName string `sqlParameterName:"middleName"`
LastName string `sqlParameterName:"lirstName"`
}
query := NewNamedParameterQuery("
SELECT * FROM table
WHERE col1 = :foo
AND col2 IN(:firstName, :middleName, :lastName)
")
parameter = new(QueryValues)
query.SetValuesFromStruct(parameter)
connection, _ := sql.Open("mysql", "user:pass@tcp(localhost:3306)/db")
connection.QueryRow(query.GetParsedQuery(), (query.GetParsedParameters())...)
*/
package namedParameterQuery
import (
"bytes"
"errors"
"reflect"
"unicode"
"unicode/utf8"
)
/*
NamedParameterQuery handles the translation of named parameters to positional parameters, for SQL statements.
It is not recommended to create zero-valued NamedParameterQuery objects by yourself;
instead use NewNamedParameterQuery
*/
type NamedParameterQuery struct {
// A map of parameter names as keys, with value as a slice of positional indices which match
// that parameter.
positions map[string][]int
// Contains all positional parameters, in order, ready to be used in the positional query.
parameters []interface{}
// The query containing named parameters, as passed in by NewNamedParameterQuery
originalQuery string
// The query containing positional parameters, as generated by setQuery
revisedQuery string
}
/*
NewNamedParameterQuery creates a new named parameter query using the given [queryText] as a SQL query which
contains named parameters. Named parameters are identified by starting with a ":"
e.g., ":name" refers to the parameter "name", and ":foo" refers to the parameter "foo".
Except for their names, named parameters follow all the same rules as positional parameters;
they cannot be inside quoted strings, and cannot inject statements into a query. They can only
be used to insert values.
*/
func NewNamedParameterQuery(queryText string) (*NamedParameterQuery) {
var ret *NamedParameterQuery
// TODO: I don't like using a map for such a small amount of elements.
// If this becomes a bottleneck for anyone, the first thing to do would
// be to make a slice and search routine for parameter positions.
ret = new(NamedParameterQuery)
ret.positions = make(map[string][]int, 8)
ret.setQuery(queryText)
return ret
}
/*
setQuery parses out all named parameters, stores their locations, and
builds a "revised" query which uses positional parameters.
*/
func (this *NamedParameterQuery) setQuery(queryText string) {
var revisedBuilder bytes.Buffer
var parameterBuilder bytes.Buffer
var position []int
var character rune
var parameterName string
var width int
var positionIndex int
this.originalQuery = queryText
positionIndex = 0
for i := 0; i < len(queryText); {
character, width = utf8.DecodeRuneInString(queryText[i:])
i += width
// if it's a colon, do not write to builder, but grab name
if(character == ':') {
for ;; {
character, width = utf8.DecodeRuneInString(queryText[i:])
i += width
if unicode.IsLetter(character) || unicode.IsDigit(character) {
parameterBuilder.WriteString(string(character))
} else {
break
}
}
// add to positions
parameterName = parameterBuilder.String()
position = this.positions[parameterName]
this.positions[parameterName] = append(position, positionIndex)
positionIndex++
revisedBuilder.WriteString("?")
parameterBuilder.Reset()
if(width <= 0) {
break
}
}
// otherwise write.
revisedBuilder.WriteString(string(character))
// if it's a quote, continue writing to builder, but do not search for parameters.
if(character == '\'') {
for ;; {
character, width = utf8.DecodeRuneInString(queryText[i:])
i += width
revisedBuilder.WriteString(string(character))
if(character == '\'') {
break
}
}
}
}
this.revisedQuery = revisedBuilder.String()
this.parameters = make([]interface{}, positionIndex)
}
/*
GetParsedQuery returns a version of the original query text
whose named parameters have been replaced by positional parameters.
*/
func (this *NamedParameterQuery) GetParsedQuery() (string) {
return this.revisedQuery
}
/*
GetParsedParameters returns an array of parameter objects that match the positional parameter list
from GetParsedQuery
*/
func (this *NamedParameterQuery) GetParsedParameters() ([]interface{}) {
return this.parameters
}
/*
SetValue sets the value of the given [parameterName] to the given [parameterValue].
If the parsed query does not have a placeholder for the given [parameterName],
this method does nothing.
*/
func (this *NamedParameterQuery) SetValue(parameterName string, parameterValue interface{}) {
for _, position := range this.positions[parameterName] {
this.parameters[position] = parameterValue
}
}
/*
SetValuesFromMap uses every key/value pair in the given [parameters] as a parameter replacement
for this query. This is equivalent to calling SetValue for every key/value pair
in the given [parameters] map.
If there are any keys/values present in the map that aren't part of the query,
they are ignored.
*/
func (this *NamedParameterQuery) SetValuesFromMap(parameters map[string]interface{}) {
for name, value := range parameters {
this.SetValue(name, value)
}
}
/*
SetValuesFromStruct uses reflection to find every public field of the given struct [parameters]
and set their key/value as named parameters in this query.
If the given [parameters] is not a struct, this will return an error.
If you do not wish for a field in the struct to be added by its literal name,
The struct may optionally specify the sqlParameterName as a tag on the field.
e.g., a struct field may say something like:
type Test struct {
Foo string `sqlParameterName:"foobar"`
}
*/
func (this *NamedParameterQuery) SetValuesFromStruct(parameters interface{}) (error) {
var fieldValues reflect.Value
var fieldValue reflect.Value
var parameterType reflect.Type
var parameterField reflect.StructField
var queryTag string
var visibilityCharacter rune
fieldValues = reflect.ValueOf(parameters)
if(fieldValues.Kind() != reflect.Struct) {
return errors.New("Unable to add query values from parameter: parameter is not a struct")
}
parameterType = fieldValues.Type()
for i := 0; i < fieldValues.NumField(); i++ {
fieldValue = fieldValues.Field(i)
parameterField = parameterType.Field(i)
// public field?
visibilityCharacter, _ = utf8.DecodeRuneInString(parameterField.Name[0:])
if(fieldValue.CanSet() || unicode.IsUpper(visibilityCharacter)) {
// check to see if this has a tag indicating a different query name
queryTag = parameterField.Tag.Get("sqlParameterName")
// otherwise just add the struct's name.
if(len(queryTag) <= 0) {
queryTag = parameterField.Name
}
this.SetValue(queryTag, fieldValue.Interface())
}
}
return nil
}