This repository has been archived by the owner on Dec 29, 2018. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 1
/
collection.go
584 lines (487 loc) · 14.4 KB
/
collection.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
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
package database
import (
"database/sql"
"fmt"
"log"
"reflect"
"strings"
)
// Collection represents a table. You can apply further filters and operations
// to the collection and then query it with one of our read methods (Get, GetAll, ...)
// or use it to store new items (Put).
type Collection struct {
sess *sql.DB
debug bool
conditions []Condition
orders []string
offset, limit int64
model Model
props []*Property
alias string
}
func newCollection(db *Database, model Model) *Collection {
props, err := extractModelProps(model)
if err != nil {
panic(err)
}
c := &Collection{
sess: db.sess,
debug: db.debug,
model: model,
props: props,
}
return c
}
// Clone returns a new collection with the same filters and configuration of
// the original one.
func (c *Collection) Clone() *Collection {
return &Collection{
sess: c.sess,
conditions: c.conditions,
orders: c.orders,
offset: c.offset,
limit: c.limit,
model: c.model,
props: c.props,
alias: c.alias,
debug: c.debug,
}
}
// Alias changes the name of the table in the SQL query. It is useful in combination
// with FilterExists() to have a stable name for the tables that should be filtered.
func (c *Collection) Alias(alias string) *Collection {
c.alias = alias
return c
}
// Get retrieves the model matching the collection filters and the model primary key.
// If no model is found ErrNoSuchEntity will be returned and the model won't be touched.
func (c *Collection) Get(instance Model) error {
modelProps := updatedProps(c.props, instance)
b := &sqlBuilder{
table: c.model.TableName(),
conditions: c.conditions,
alias: c.alias,
props: modelProps,
}
for _, prop := range modelProps {
if prop.PrimaryKey {
b.conditions = append(b.conditions, Filter(prop.Name, prop.Value))
}
}
statement, values := b.SelectSQL()
if c.debug {
log.Println("database [Get]:", statement)
}
var pointers []interface{}
for _, prop := range modelProps {
pointers = append(pointers, prop.Pointer)
}
if err := c.sess.QueryRow(statement, values...).Scan(pointers...); err != nil {
if err == sql.ErrNoRows {
return ErrNoSuchEntity
}
return err
}
modelProps = updatedProps(c.props, instance)
return instance.Tracking().AfterGet(modelProps)
}
// Put stores a new item of the collection. Any filter or limit of the
// collection won't be applied.
func (c *Collection) Put(instance Model) error {
modelt := reflect.TypeOf(c.model)
instancet := reflect.TypeOf(instance)
if modelt != instancet {
return fmt.Errorf("database: expected instance of %s and got a instance of %s", modelt, instancet)
}
if h, ok := instance.(OnBeforePutHooker); ok {
if err := h.OnBeforePutHook(); err != nil {
return err
}
}
b := &sqlBuilder{
table: c.model.TableName(),
}
modelProps := updatedProps(c.props, instance)
var q string
var values []interface{}
if instance.Tracking().IsInserted() {
b.conditions = append(b.conditions, c.conditions...)
b.conditions = append(b.conditions, Filter("revision", instance.Tracking().StoredRevision()))
for _, prop := range modelProps {
if prop.PrimaryKey {
b.conditions = append(b.conditions, Filter(prop.Name, prop.Value))
continue
}
if prop.OmitEmpty && isZero(prop.Value) {
continue
}
b.props = append(b.props, prop)
}
q, values = b.UpdateSQL()
} else {
for _, prop := range modelProps {
if prop.OmitEmpty && isZero(prop.Value) {
continue
}
b.props = append(b.props, prop)
}
q, values = b.InsertSQL()
}
if c.debug {
log.Println("database [Put]:", q)
}
result, err := c.sess.Exec(q, values...)
if err != nil {
return err
}
var pks int
for _, prop := range modelProps {
if prop.PrimaryKey {
pks++
}
}
if pks == 1 && !instance.Tracking().IsInserted() {
id, err := result.LastInsertId()
if err != nil {
return fmt.Errorf("database: cannot get last inserted id: %s", err)
}
for _, prop := range modelProps {
if prop.PrimaryKey {
if _, ok := prop.Value.(int64); ok {
reflect.ValueOf(prop.Pointer).Elem().Set(reflect.ValueOf(id))
}
}
}
}
rows, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("database: cannot get rows affected: %s", err)
}
if rows == 0 {
return ErrConcurrentTransaction
}
if err := instance.Tracking().AfterPut(modelProps); err != nil {
return err
}
if h, ok := instance.(OnAfterPutHooker); ok {
if err := h.OnAfterPutHook(); err != nil {
return err
}
}
return nil
}
// Filter applies a new simple filter to the collection. See the global Filter
// function for documentation.
func (c *Collection) Filter(sql string, value interface{}) *Collection {
return c.FilterCond(Filter(sql, value))
}
// FilterIsNil applies a new NULL filter to the collection. See the global FilterIsNil
// function for documentation.
func (c *Collection) FilterIsNil(column string) *Collection {
return c.FilterCond(FilterIsNil(column))
}
// FilterIsNotNil applies a new NOT NULL filter to the collection. See the
// global FilterIsNotNil function for documentation.
func (c *Collection) FilterIsNotNil(column string) *Collection {
return c.FilterCond(FilterIsNotNil(column))
}
// FilterCond applies a generic condition to the collection. We have some helpers
// in this library to build conditions; and other libraries (like github.com/altipla-consulting/geo)
// can implement their own conditions too.
func (c *Collection) FilterCond(condition Condition) *Collection {
if condition.SQL() == "" {
return c
}
c.conditions = append(c.conditions, condition)
return c
}
// Offset moves the initial position of the query. In combination with Limit
// it allows you to paginate the results.
func (c *Collection) Offset(offset int64) *Collection {
c.offset = offset
return c
}
// Limit adds a maximum number of results to the query.
func (c *Collection) Limit(limit int64) *Collection {
c.limit = limit
return c
}
// Order the collection of items. You can pass "column" for ascendent order or "-column"
// for descendent order. If you want to order by mutliple columns call Order multiple
// times for each column, the will be joined.
func (c *Collection) Order(column string) *Collection {
if strings.Contains(column, ",") {
panic("call Order multiple times, do not pass multiple columns")
}
if strings.Contains(column, "ASC") {
panic("do not call Order with `foo ASC`, use plain `foo` instead")
}
if strings.Contains(column, "DESC") {
panic("do not call Order with `foo DESC`, use plain `-foo` instead")
}
if strings.HasPrefix(column, "-") {
column = fmt.Sprintf("`%s` DESC", column[1:])
} else {
column = fmt.Sprintf("`%s` ASC", column)
}
c.orders = append(c.orders, column)
return c
}
// OrderSorter sorts the collection of items. We have some helpers
// in this library to build sorters; and other libraries (like github.com/altipla-consulting/geo)
// can implement their own sorters too.
func (c *Collection) OrderSorter(sorter Sorter) *Collection {
c.orders = append(c.orders, sorter.SQL())
return c
}
// Delete removes a model from a collection. It uses the filters and the model
// primary key to find the row to remove, so it can return an error even if the
// PK exists when the filters do not match. Limits won't be applied but the offset
// of the collection will.
func (c *Collection) Delete(instance Model) error {
b := &sqlBuilder{
table: c.model.TableName(),
conditions: c.conditions,
limit: 1,
offset: c.offset,
alias: c.alias,
}
modelProps := updatedProps(c.props, instance)
for _, prop := range modelProps {
if prop.PrimaryKey {
b.conditions = append(b.conditions, Filter(prop.Name, prop.Value))
}
}
statement, values := b.DeleteSQL()
if c.debug {
log.Println("database [Delete]:", statement)
}
if _, err := c.sess.Exec(statement, values...); err != nil {
return err
}
return instance.Tracking().AfterDelete(modelProps)
}
// Iterator returns a new iterator that can be used to extract models one by one in a loop.
// You should close the Iterator after you are done with it.
func (c *Collection) Iterator() (*Iterator, error) {
b := &sqlBuilder{
table: c.model.TableName(),
conditions: c.conditions,
props: c.props,
limit: c.limit,
offset: c.offset,
orders: c.orders,
alias: c.alias,
}
sql, values := b.SelectSQL()
if c.debug {
log.Println("database [Iterator]:", sql)
}
rows, err := c.sess.Query(sql, values...)
if err != nil {
return nil, err
}
return &Iterator{rows, c.props}, nil
}
// GetAll receives a pointer to an empty slice of models and retrieves all the
// models that match the filters of the collection. Take care to avoid fetching large
// collections of models or you will run out of memory.
func (c *Collection) GetAll(models interface{}) error {
v := reflect.ValueOf(models)
t := reflect.TypeOf(models)
if v.Kind() != reflect.Ptr {
return fmt.Errorf("database: pass a pointer to a slice to GetAll")
}
v = v.Elem()
t = t.Elem()
if v.Kind() != reflect.Slice {
return fmt.Errorf("database: pass a slice to GetAll")
}
modelt := reflect.TypeOf(c.model)
if t.Elem() != modelt {
return fmt.Errorf("database: expected a slice of %s and got a slice of %s", modelt, t.Elem())
}
dest := reflect.MakeSlice(t, 0, 0)
it, err := c.Iterator()
if err != nil {
return err
}
defer it.Close()
for {
model := reflect.New(t.Elem().Elem())
if err := it.Next(model.Interface().(Model)); err != nil {
if err == ErrDone {
break
}
return err
}
dest = reflect.Append(dest, model)
}
v.Set(dest)
return nil
}
// First returns the first model that matches the collection. If no one is found
// it will return ErrNoSuchEntity and it won't touch model.
func (c *Collection) First(instance Model) error {
c = c.Limit(1)
modelProps := updatedProps(c.props, instance)
b := &sqlBuilder{
table: c.model.TableName(),
conditions: c.conditions,
props: modelProps,
limit: c.limit,
offset: c.offset,
orders: c.orders,
alias: c.alias,
}
statement, values := b.SelectSQL()
if c.debug {
log.Println("database [First]:", statement)
}
var pointers []interface{}
for _, prop := range modelProps {
pointers = append(pointers, prop.Pointer)
}
if err := c.sess.QueryRow(statement, values...).Scan(pointers...); err != nil {
if err == sql.ErrNoRows {
return ErrNoSuchEntity
}
return err
}
modelProps = updatedProps(c.props, instance)
return instance.Tracking().AfterGet(modelProps)
}
// Count queries the number of rows that the collection matches.
func (c *Collection) Count() (int64, error) {
b := &sqlBuilder{
table: c.model.TableName(),
conditions: c.conditions,
alias: c.alias,
}
sql, values := b.SelectSQLCols("COUNT(*)")
if c.debug {
log.Println("database [Count]:", sql)
}
var n int64
if err := c.sess.QueryRow(sql, values...).Scan(&n); err != nil {
return 0, err
}
return n, nil
}
// GetMulti queries multiple rows and return all of them in a list. Keys should
// be a list of primary keys to retrieve and models should be a pointer to an empty
// slice of models. If any of the primary keys is not found a MultiError will be returned.
// You can check the error type for MultiError and then loop over the list of errors, they will
// be in the same order as the keys and they will have nil's when the row is found. The result
// list will also have the same length as keys with nil's filled when the row is not found.
func (c *Collection) GetMulti(keys interface{}, models interface{}) error {
v := reflect.ValueOf(models)
t := reflect.TypeOf(models)
keyst := reflect.TypeOf(keys)
keysv := reflect.ValueOf(keys)
if v.Kind() != reflect.Ptr {
return fmt.Errorf("database: pass a pointer to a slice of models to GetMulti")
}
v = v.Elem()
t = t.Elem()
if v.Kind() != reflect.Slice {
return fmt.Errorf("database: pass a slice of models to GetMulti")
}
if keyst.Kind() != reflect.Slice {
return fmt.Errorf("database: pass a slice of keys to GetMulti")
}
keyst = keyst.Elem()
if keyst.Kind() != reflect.Int64 && keyst.Kind() != reflect.String {
return fmt.Errorf("database: pass a slice of string/int64 keys to GetMulti")
}
if keysv.Len() == 0 {
return nil
}
var pk *Property
for _, prop := range c.props {
if prop.PrimaryKey {
if pk != nil {
return fmt.Errorf("database: cannot use GetMulti with multiple primary keys")
}
pk = prop
}
}
c = c.Filter(fmt.Sprintf("%s IN", pk.Name), keys)
fetch := reflect.New(t)
fetch.Elem().Set(reflect.MakeSlice(t, 0, 0))
if err := c.GetAll(fetch.Interface()); err != nil {
return err
}
stringKeys := map[string]reflect.Value{}
intKeys := map[int64]reflect.Value{}
var merr MultiError
for i := 0; i < fetch.Elem().Len(); i++ {
model := fetch.Elem().Index(i)
pk := model.Elem().FieldByName(pk.Field).Interface()
switch v := pk.(type) {
case string:
stringKeys[v] = model
case int64:
intKeys[v] = model
default:
panic("should not reach here")
}
}
results := reflect.MakeSlice(t, 0, 0)
for i := 0; i < keysv.Len(); i++ {
switch v := keysv.Index(i).Interface().(type) {
case string:
model, ok := stringKeys[v]
if !ok {
merr = append(merr, ErrNoSuchEntity)
results = reflect.Append(results, reflect.Zero(t.Elem()))
continue
}
merr = append(merr, nil)
results = reflect.Append(results, model)
case int64:
model, ok := intKeys[v]
if !ok {
merr = append(merr, ErrNoSuchEntity)
results = reflect.Append(results, reflect.Zero(t.Elem()))
continue
}
merr = append(merr, nil)
results = reflect.Append(results, model)
default:
panic("should not reach here")
}
}
v.Set(results)
if merr.HasError() {
return merr
}
return nil
}
// Truncate removes every single row of a table. It also resets any autoincrement
// value it may have to the value "1".
func (c *Collection) Truncate() error {
b := &sqlBuilder{
table: c.model.TableName(),
}
statement := b.TruncateSQL()
if c.debug {
log.Println("database [Truncate]:", statement)
}
if _, err := c.sess.Exec(statement); err != nil {
return err
}
statement = b.ResetAutoIncrementSQL()
if c.debug {
log.Println("database [Truncate]:", statement)
}
if _, err := c.sess.Exec(statement); err != nil {
return err
}
return nil
}
// FilterExists applies the global FilterExists condition to this collection. See
// the global function for documentation.
func (c *Collection) FilterExists(sub *Collection, join string) *Collection {
return c.FilterCond(FilterExists(sub, join))
}