-
Notifications
You must be signed in to change notification settings - Fork 3
/
schema.go
179 lines (157 loc) · 5.21 KB
/
schema.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
package papergres
import (
"errors"
"fmt"
"strings"
"unicode"
)
// Schema holds the schema to query along with the Database
type Schema struct {
Name string
Database *Database
}
// GenerateInsert generates the insert query for the given object
func (s *Schema) GenerateInsert(obj interface{}) *Query {
return s.generateInsertQuery(obj, false)
}
// GenerateInsertWithPK generates the insert query for the given object in which
// PrimaryKey value is also supposed to be populated during insert.
func (s *Schema) GenerateInsertWithPK(obj interface{}) *Query {
return s.generateInsertQuery(obj, true)
}
// Insert inserts the passed in object in DB.
//
// NOTE: It does not account for client side generated value for a
// primary key and expects that the logic for populating value
// of primary key should reside in the database as sequence.
//
// DO NOT use Insert() if you wish to populate a client side generated
// value in primary key, use InsertWithPK() instead.
func (s *Schema) Insert(obj interface{}) *Result {
sql := insertSQL(obj, s.Name, false)
args := insertArgs(obj, false)
return s.Database.Query(sql, args...).Exec()
}
// InsertWithPK performs inserts on objects and persists the Primary key value
// to DB as well. It will fail to insert duplicate values to DB.
//
// NOTE: Proceed with caution!
// Only use this method you wish to persist a client side generated value in
// Primary key and don't rely on database sequenece to autogenerate
// PrimaryKey values.
func (s *Schema) InsertWithPK(obj interface{}) *Result {
sql := insertSQL(obj, s.Name, true)
args := insertArgs(obj, true)
return s.Database.Query(sql, args...).ExecNonQuery()
}
// InsertAll inserts a slice of objects concurrently.
// objs must be a slice with items in it.
// the Result slice will be in the same order as objs
// so a simple loop will set all the primary keys if needed:
// for i, r := range results {
// objs[i].Id = r.LastInsertId.ID
// }
func (s *Schema) InsertAll(objs interface{}) ([]*Result, error) {
slice, err := convertToSlice(objs)
if err != nil {
return nil, err
}
if len(slice) == 0 {
return nil, errors.New("empty slice")
}
// now turn the objs into a repeat query and exec
return s.GenerateInsert(slice[0]).Repeat(len(slice),
func(i int) (dest interface{}, args []interface{}) {
args = insertArgs(slice[i], false)
return
}).Exec()
}
// generateInsertQuery constructs an insert query for the given object
func (s *Schema) generateInsertQuery(obj interface{}, withPK bool) *Query {
sql := insertSQL(obj, s.Name, withPK)
args := insertArgs(obj, withPK)
q := s.Database.Query(sql, args...)
q.insert = true
return q
}
// insertSQL generates insert SQL string for a given object and schema
func insertSQL(obj interface{}, schema string, withPK bool) string {
// Construct the table name prefixed with schema name
tname := goToSQLName(getTypeName(obj))
tname = fmt.Sprintf("%s.%s", schema, tname)
// Construct the first component of insert statement
sql := fmt.Sprintf("INSERT INTO %s (", tname)
// NOTE: An object is represented as a slice of Fields,
// where each Field represents a column.
// Get list of columns to populate.
fields, primary := prepareFields(obj, withPK)
// Based on the number of columns, create value placeholders
var values string
for i, f := range fields {
sql += fmt.Sprintf("\n\t%s,", getColumnName(f))
values += fmt.Sprintf("\n\t$%v,", i+1)
}
// Add source data placeholders
// something like this: `VALUES ($1, $2, $3, $4, $5, $6)`
sql = strings.TrimRight(sql, ",")
sql += "\n)\nVALUES ("
sql += values
sql = strings.TrimRight(sql, ",")
sql += "\n)\n"
// Add last line to capture primary key
sql += fmt.Sprintf("RETURNING %s as LastInsertId;", getColumnName(primary))
return sql
}
// getColumnName returns a Field's associated Tag name if it is supplied.
// Else, it constructs a snake_case value from Field.Name value and returns it.
// Example:
// For a Field with `Name` as 'TransactionSource' if `db: transaction_source` is
// present in Tag then it'll be used else it'll be constructed.
func getColumnName(f *Field) string {
var columnName string
if f.Tag != "" {
columnName = f.Tag
return columnName
}
columnName = goToSQLName(f.Name)
return columnName
}
// goToSQLName converts a string from camel case to snake case
// e.g. TransactionSource to transaction_source
func goToSQLName(name string) string {
var s string
for _, c := range name {
if unicode.IsUpper(c) {
if s != "" {
s += "_"
}
}
s += strings.ToLower(string(c))
}
return s
}
// insertArgs creates the insert arg slice for an object
func insertArgs(obj interface{}, withPK bool) []interface{} {
final, _ := prepareFields(obj, withPK)
args := make([]interface{}, len(final))
for i, f := range final {
args[i] = f.Value
}
return args
}
// prepareFields performs necessary transformations for the insert statement.
// If `withPK` is false: It does not account a primary key Field to list of
// fields to append, else, primary key is also considered.
func prepareFields(obj interface{}, withPK bool) (nfields []*Field, primary *Field) {
fields := fields(obj)
for _, f := range fields {
if f.IsPrimary {
primary = f
if !withPK {
continue
}
}
nfields = append(nfields, f)
}
return
}