-
Notifications
You must be signed in to change notification settings - Fork 2
/
text.go
112 lines (98 loc) · 2.78 KB
/
text.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
package sol
import (
"reflect"
"regexp"
"strings"
"github.com/aodin/sol/dialect"
)
// paramsRegex will match any words with a leading colon.
// Since Go's re2 has no lookbehind / lookahead assertions, we'll match
// any number of leading colons - which will include type casts -
// and filter afterwards
// TODO this regular expression should be replaced by a parser
var paramsRegex = regexp.MustCompile(`(:)+(\w+)`)
// TextStmt allows the creation of custom SQL statements
type TextStmt struct {
Stmt
text string
values Values
}
// String outputs the parameter-less statement in a neutral dialect.
func (stmt TextStmt) String() string {
compiled, _ := stmt.Compile(&defaultDialect{}, Params())
return compiled
}
// Compile outputs the statement using the given dialect and parameters.
func (stmt TextStmt) Compile(d dialect.Dialect, ps *Parameters) (string, error) {
// Select the parameters from the statement and replace them
// with dialect specific parameters
// Also alias the values (TODO hacky and probably too friendly)
aliases := Values{}
for key, value := range stmt.values {
aliases[camelToSnake(key)] = value
}
replacer := func(match string) string {
// Remove any matches with more than one leading colon
if strings.LastIndex(match, ":") != 0 {
return match
}
key := match[1:]
// Parameter names must match value keys exactly or with
// camel to snake conversion
value, exists := stmt.values[key]
if !exists {
if value, exists = aliases[key]; !exists {
stmt.AddMeta("sol: missing value for parameter '%s'", key)
}
}
param := &Parameter{Value: value}
replacement, err := param.Compile(d, ps)
if err != nil {
stmt.AddMeta(err.Error())
}
return replacement
}
compiled := paramsRegex.ReplaceAllStringFunc(stmt.text, replacer)
return compiled, stmt.Error()
}
// Values sets the values of the statement. They can be given as either
// Values or struct types
func (stmt TextStmt) Values(obj interface{}) TextStmt {
elem := reflect.Indirect(reflect.ValueOf(obj))
// Examine allowed types
var unsupported bool
switch elem.Kind() {
case reflect.Map:
switch converted := obj.(type) {
case Values:
stmt.values = converted
case *Values:
stmt.values = *converted
default:
unsupported = true
}
case reflect.Struct:
var err error
if stmt.values, err = ValuesOf(obj); err != nil {
stmt.AddMeta(err.Error())
return stmt
}
default:
unsupported = true
}
if unsupported {
stmt.AddMeta(
"sol: unsupported type %T for inserted values - accepted types: struct types or Values",
obj,
)
}
return stmt
}
// Text creates a TextStmt with custom SQL
func Text(text string, values ...Values) TextStmt {
merged := Values{}
for _, val := range values {
merged = merged.Merge(val)
}
return TextStmt{text: text, values: merged}
}