usergrid_ironhorse 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,890 @@
1
+ # http://guides.rubyonrails.org/active_record_querying.html
2
+
3
+ module Usergrid
4
+ module Ironhorse
5
+
6
+ class Query
7
+
8
+ RecordNotFound = ActiveRecord::RecordNotFound
9
+
10
+ def initialize(model_class)
11
+ @model_class = model_class
12
+ @options = {}
13
+ end
14
+
15
+ ## Initializes new record from relation while maintaining the current
16
+ ## scope.
17
+ ##
18
+ ## Expects arguments in the same format as +Base.new+.
19
+ ##
20
+ ## users = User.where(name: 'DHH')
21
+ ## user = users.new # => #<User id: nil, name: "DHH", created_at: nil, updated_at: nil>
22
+ ##
23
+ ## You can also pass a block to new with the new record as argument:
24
+ ##
25
+ ## user = users.new { |user| user.name = 'Oscar' }
26
+ ## user.name # => Oscar
27
+ #def new(*args, &block)
28
+ # scoping { @model_class.new(*args, &block) }
29
+ #end
30
+
31
+ # Find by uuid or name - This can either be a specific uuid or name (1), a list of uuids
32
+ # or names (1, 5, 6), or an array of uuids or names ([5, 6, 10]).
33
+ # If no record can be found for all of the listed ids, then RecordNotFound will be raised.
34
+ #
35
+ # Person.find(1) # returns the object for ID = 1
36
+ # Person.find("1") # returns the object for ID = 1
37
+ # Person.find(1, 2, 6) # returns an array for objects with IDs in (1, 2, 6)
38
+ # Person.find([7, 17]) # returns an array for objects with IDs in (7, 17)
39
+ # Person.find([1]) # returns an array for the object with ID = 1
40
+ # Person.where("administrator = 1").order("created_on DESC").find(1)
41
+ #
42
+ def find(*ids)
43
+ raise RecordNotFound unless ids
44
+ ids = ids.first if ids.first.is_a? Array
45
+ @records = ids.collect { |id| find_one! id } # todo: can this be optimized in one call?
46
+ #entities = @model_class.resource[ids.join '&'].get.entities
47
+ #raise RecordNotFound unless (entities.size == ids.size)
48
+ #@records = entities.collect {|entity| @model_class.model_name.constantize.new(entity.data) }
49
+ @records.size == 1 ? @records.first : @records
50
+ end
51
+
52
+ # Finds the first record matching the specified conditions. There
53
+ # is no implied ordering so if order matters, you should specify it
54
+ # yourself.
55
+ #
56
+ # If no record is found, returns <tt>nil</tt>.
57
+ #
58
+ # Post.find_by name: 'Spartacus', rating: 4
59
+ # Post.find_by "published_at < ?", 2.weeks.ago
60
+ def find_by(*conditions)
61
+ where(*conditions).take
62
+ end
63
+
64
+ # Like <tt>find_by</tt>, except that if no record is found, raises
65
+ # an <tt>ActiveRecord::RecordNotFound</tt> error.
66
+ def find_by!(*conditions)
67
+ where(*conditions).take!
68
+ end
69
+
70
+ # Gives a record (or N records if a parameter is supplied) without any implied
71
+ # order.
72
+ #
73
+ # Person.take # returns an object fetched by SELECT * FROM people
74
+ # Person.take(5) # returns 5 objects fetched by SELECT * FROM people LIMIT 5
75
+ # Person.where(["name LIKE '%?'", name]).take
76
+ def take(limit=1)
77
+ limit(limit).to_a
78
+ end
79
+
80
+ # Same as +take+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record
81
+ # is found. Note that <tt>take!</tt> accepts no arguments.
82
+ def take!
83
+ take or raise RecordNotFound
84
+ end
85
+
86
+ # Find the first record (or first N records if a parameter is supplied).
87
+ # If no order is defined it will order by primary key.
88
+ #
89
+ # Person.first # returns the first object fetched by SELECT * FROM people
90
+ # Person.where(["user_name = ?", user_name]).first
91
+ # Person.where(["user_name = :u", { :u => user_name }]).first
92
+ # Person.order("created_on DESC").offset(5).first
93
+ # Person.first(3) # returns the first three objects fetched by SELECT * FROM people LIMIT 3
94
+ def first(limit=1)
95
+ limit(limit).load.first
96
+ end
97
+
98
+ # Same as +first+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record
99
+ # is found. Note that <tt>first!</tt> accepts no arguments.
100
+ def first!
101
+ first or raise RecordNotFound
102
+ end
103
+
104
+ # Find the last record (or last N records if a parameter is supplied).
105
+ # If no order is defined it will order by primary key.
106
+ #
107
+ # Person.last # returns the last object fetched by SELECT * FROM people
108
+ # Person.where(["user_name = ?", user_name]).last
109
+ # Person.order("created_on DESC").offset(5).last
110
+ # Person.last(3) # returns the last three objects fetched by SELECT * FROM people.
111
+ #
112
+ # Take note that in that last case, the results are sorted in ascending order:
113
+ #
114
+ # [#<Person id:2>, #<Person id:3>, #<Person id:4>]
115
+ #
116
+ # and not:
117
+ #
118
+ # [#<Person id:4>, #<Person id:3>, #<Person id:2>]
119
+ def last(limit=1)
120
+ limit(limit).reverse_order.load.first
121
+ end
122
+
123
+ # Same as +last+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record
124
+ # is found. Note that <tt>last!</tt> accepts no arguments.
125
+ def last!
126
+ last or raise RecordNotFound
127
+ end
128
+
129
+ def each
130
+ to_a.each { |*block_args| yield(*block_args) }
131
+ end
132
+
133
+ # Returns +true+ if a record exists in the table that matches the +id+ or
134
+ # conditions given, or +false+ otherwise. The argument can take six forms:
135
+ #
136
+ # * String - Finds the record with a primary key corresponding to this
137
+ # string (such as <tt>'5'</tt>).
138
+ # * Array - Finds the record that matches these +find+-style conditions
139
+ # (such as <tt>['color = ?', 'red']</tt>).
140
+ # * Hash - Finds the record that matches these +find+-style conditions
141
+ # (such as <tt>{color: 'red'}</tt>).
142
+ # * +false+ - Returns always +false+.
143
+ # * No args - Returns +false+ if the table is empty, +true+ otherwise.
144
+ #
145
+ # For more information about specifying conditions as a Hash or Array,
146
+ # see the Conditions section in the introduction to ActiveRecord::Base.
147
+ def exists?(conditions=nil)
148
+ # todo: does not yet handle all conditions described above
149
+ case conditions
150
+ when Array, Hash
151
+ pluck :uuid
152
+ !where(conditions).take.empty?
153
+ else
154
+ !!find_one(conditions)
155
+ end
156
+ end
157
+ alias_method :any?, :exists?
158
+ alias_method :many?, :exists?
159
+
160
+ def limit(limit=1)
161
+ @options[:limit] = limit
162
+ self
163
+ end
164
+
165
+ def offset(num)
166
+ @options[:offset] = num
167
+ self
168
+ end
169
+
170
+ # Removes from the query the condition(s) specified in +skips+.
171
+ #
172
+ # Example:
173
+ #
174
+ # Post.order('id asc').except(:order) # discards the order condition
175
+ # Post.where('id > 10').order('id asc').except(:where) # discards the where condition but keeps the order
176
+ #
177
+ def except(*skips)
178
+ skips.each {|option| @options.delete option}
179
+ end
180
+
181
+ # Removes any condition from the query other than the one(s) specified in +onlies+.
182
+ #
183
+ # Example:
184
+ #
185
+ # Post.order('id asc').only(:where) # discards the order condition
186
+ # Post.order('id asc').only(:where, :order) # uses the specified order
187
+ #
188
+ def only(*onlies)
189
+ @options.keys do |k|
190
+ unless onlines.include? k
191
+ @options.delete k
192
+ end
193
+ end
194
+ end
195
+
196
+ # Allows to specify an order attribute:
197
+ #
198
+ # User.order('name')
199
+ # => SELECT "users".* FROM "users" ORDER BY name
200
+ #
201
+ # User.order('name DESC')
202
+ # => SELECT "users".* FROM "users" ORDER BY name DESC
203
+ #
204
+ # User.order('name DESC, email')
205
+ # => SELECT "users".* FROM "users" ORDER BY name DESC, email
206
+ def order(*args)
207
+ @options[:order] << args
208
+ end
209
+
210
+ # Replaces any existing order defined on the relation with the specified order.
211
+ #
212
+ # User.order('email DESC').reorder('id ASC') # generated SQL has 'ORDER BY id ASC'
213
+ #
214
+ # Subsequent calls to order on the same relation will be appended. For example:
215
+ #
216
+ # User.order('email DESC').reorder('id ASC').order('name ASC')
217
+ #
218
+ # generates a query with 'ORDER BY name ASC, id ASC'.
219
+ def reorder(*args)
220
+ @options[:order] = args
221
+ end
222
+
223
+ def all
224
+ @options[:conditions] = nil
225
+ self
226
+ end
227
+
228
+ # Works in two unique ways.
229
+ #
230
+ # First: takes a block so it can be used just like Array#select.
231
+ #
232
+ # Model.all.select { |m| m.field == value }
233
+ #
234
+ # This will build an array of objects from the database for the scope,
235
+ # converting them into an array and iterating through them using Array#select.
236
+ #
237
+ # Second: Modifies the SELECT statement for the query so that only certain
238
+ # fields are retrieved:
239
+ #
240
+ # Model.select(:field)
241
+ # # => [#<Model field:value>]
242
+ #
243
+ # Although in the above example it looks as though this method returns an
244
+ # array, it actually returns a relation object and can have other query
245
+ # methods appended to it, such as the other methods in ActiveRecord::QueryMethods.
246
+ #
247
+ # The argument to the method can also be an array of fields.
248
+ #
249
+ # Model.select(:field, :other_field, :and_one_more)
250
+ # # => [#<Model field: "value", other_field: "value", and_one_more: "value">]
251
+ #
252
+ def select(*fields)
253
+ if block_given?
254
+ to_a.select { |*block_args| yield(*block_args) }
255
+ else
256
+ raise ArgumentError, 'Call this with at least one field' if fields.empty?
257
+ clone.select!(*fields)
258
+ end
259
+ end
260
+
261
+ # Like #select, but modifies relation in place.
262
+ def select!(*fields)
263
+ @options[:select] ||= fields.join ','
264
+ self
265
+ end
266
+ alias_method :pluck, :select!
267
+
268
+ def reverse_order
269
+ @options[:reversed] = true
270
+ self
271
+ end
272
+
273
+ def readonly
274
+ @options[:readonly] = true
275
+ self
276
+ end
277
+
278
+ def to_a
279
+ load
280
+ @records
281
+ end
282
+
283
+ def as_json(options = nil) #:nodoc:
284
+ to_a.as_json(options)
285
+ end
286
+
287
+ # Returns size of the results (not size of the stored collection)
288
+ def size
289
+ loaded? ? @records.length : count
290
+ end
291
+
292
+ # true if there are no records
293
+ def empty?
294
+ return @records.empty? if loaded?
295
+
296
+ c = count
297
+ c.respond_to?(:zero?) ? c.zero? : c.empty?
298
+ end
299
+
300
+ # true if there are any records
301
+ def any?
302
+ if block_given?
303
+ to_a.any? { |*block_args| yield(*block_args) }
304
+ else
305
+ !empty?
306
+ end
307
+ end
308
+
309
+ # true if there is more than one record
310
+ def many?
311
+ if block_given?
312
+ to_a.many? { |*block_args| yield(*block_args) }
313
+ else
314
+ limit_value ? to_a.many? : size > 1
315
+ end
316
+ end
317
+
318
+ # find_all_by_
319
+ # find_by_
320
+ # find_first_by_
321
+ # find_last_by_
322
+ def method_missing(method_name, *args)
323
+
324
+ method = method_name.to_s
325
+ if method.start_with? 'find_all_by_'
326
+ attribs = method.gsub /^find_all_by_/, ''
327
+ elsif method.start_with? 'find_by_'
328
+ attribs = method.gsub /^find_by_/, ''
329
+ limit(1)
330
+ elsif method.start_with? 'find_first_by_'
331
+ limit(1)
332
+ find_first = true
333
+ attribs = method.gsub /^find_first_by_/, ''
334
+ elsif method.start_with? 'find_last_by_'
335
+ limit(1)
336
+ find_last = true
337
+ attribs = method.gsub /^find_last_by_/, ''
338
+ else
339
+ super
340
+ end
341
+
342
+ if method.end_with? '!'
343
+ method.chop!
344
+ error_on_empty = true
345
+ end
346
+
347
+ attribs = attribs.split '_and_'
348
+ conditions = {}
349
+ attribs.each { |attr| conditions[attr] = args.shift }
350
+
351
+ where(conditions, *args)
352
+ load
353
+ raise RecordNotFound if error_on_empty && @records.empty?
354
+ return @records.first if limit_value == 1
355
+ @records
356
+ end
357
+
358
+ # Tries to create a new record with the same scoped attributes
359
+ # defined in the relation. Returns the initialized object if validation fails.
360
+ #
361
+ # Expects arguments in the same format as +Base.create+.
362
+ #
363
+ # ==== Examples
364
+ # users = User.where(name: 'Oscar')
365
+ # users.create # #<User id: 3, name: "oscar", ...>
366
+ #
367
+ # users.create(name: 'fxn')
368
+ # users.create # #<User id: 4, name: "fxn", ...>
369
+ #
370
+ # users.create { |user| user.name = 'tenderlove' }
371
+ # # #<User id: 5, name: "tenderlove", ...>
372
+ #
373
+ # users.create(name: nil) # validation on name
374
+ # # #<User id: nil, name: nil, ...>
375
+ def create(*args, &block)
376
+ @model_class.create(*args, &block)
377
+ end
378
+
379
+ # Similar to #create, but calls +create!+ on the base class. Raises
380
+ # an exception if a validation error occurs.
381
+ #
382
+ # Expects arguments in the same format as <tt>Base.create!</tt>.
383
+ def create!(*args, &block)
384
+ @model_class.create!(*args, &block)
385
+ end
386
+
387
+ # Tries to load the first record; if it fails, then <tt>create</tt> is called with the same arguments as this method.
388
+ #
389
+ # Expects arguments in the same format as +Base.create+.
390
+ #
391
+ # ==== Examples
392
+ # # Find the first user named Penélope or create a new one.
393
+ # User.where(:first_name => 'Penélope').first_or_create
394
+ # # => <User id: 1, first_name: 'Penélope', last_name: nil>
395
+ #
396
+ # # Find the first user named Penélope or create a new one.
397
+ # # We already have one so the existing record will be returned.
398
+ # User.where(:first_name => 'Penélope').first_or_create
399
+ # # => <User id: 1, first_name: 'Penélope', last_name: nil>
400
+ #
401
+ # # Find the first user named Scarlett or create a new one with a particular last name.
402
+ # User.where(:first_name => 'Scarlett').first_or_create(:last_name => 'Johansson')
403
+ # # => <User id: 2, first_name: 'Scarlett', last_name: 'Johansson'>
404
+ #
405
+ # # Find the first user named Scarlett or create a new one with a different last name.
406
+ # # We already have one so the existing record will be returned.
407
+ # User.where(:first_name => 'Scarlett').first_or_create do |user|
408
+ # user.last_name = "O'Hara"
409
+ # end
410
+ # # => <User id: 2, first_name: 'Scarlett', last_name: 'Johansson'>
411
+ def first_or_create(attributes={}, &block)
412
+ result = first
413
+ unless result
414
+ attributes = @options[:hash].merge(attributes) if @options[:hash]
415
+ result = create(attributes, &block)
416
+ end
417
+ result
418
+ end
419
+
420
+ # Like <tt>first_or_create</tt> but calls <tt>create!</tt> so an exception is raised if the created record is invalid.
421
+ #
422
+ # Expects arguments in the same format as <tt>Base.create!</tt>.
423
+ def first_or_create!(attributes={}, &block)
424
+ result = first
425
+ unless result
426
+ attributes = @options[:hash].merge(attributes) if @options[:hash]
427
+ result = create!(attributes, &block)
428
+ end
429
+ result
430
+ end
431
+
432
+ # Like <tt>first_or_create</tt> but calls <tt>new</tt> instead of <tt>create</tt>.
433
+ #
434
+ # Expects arguments in the same format as <tt>Base.new</tt>.
435
+ def first_or_initialize(attributes={}, &block)
436
+ result = first
437
+ unless result
438
+ attributes = @options[:hash].merge(attributes) if @options[:hash]
439
+ result = @model_class.new(attributes, &block)
440
+ end
441
+ result
442
+ end
443
+
444
+ # Destroys the records matching +conditions+ by instantiating each
445
+ # record and calling its +destroy+ method. Each object's callbacks are
446
+ # executed (including <tt>:dependent</tt> association options and
447
+ # +before_destroy+/+after_destroy+ Observer methods). Returns the
448
+ # collection of objects that were destroyed; each will be frozen, to
449
+ # reflect that no changes should be made (since they can't be
450
+ # persisted).
451
+ #
452
+ # Note: Instantiation, callback execution, and deletion of each
453
+ # record can be time consuming when you're removing many records at
454
+ # once. It generates at least one SQL +DELETE+ query per record (or
455
+ # possibly more, to enforce your callbacks). If you want to delete many
456
+ # rows quickly, without concern for their associations or callbacks, use
457
+ # +delete_all+ instead.
458
+ #
459
+ # ==== Parameters
460
+ #
461
+ # * +conditions+ - A string, array, or hash that specifies which records
462
+ # to destroy. If omitted, all records are destroyed. See the
463
+ # Conditions section in the introduction to ActiveRecord::Base for
464
+ # more information.
465
+ #
466
+ # ==== Examples
467
+ #
468
+ # Person.destroy_all("last_login < '2004-04-04'")
469
+ # Person.destroy_all(status: "inactive")
470
+ # Person.where(:age => 0..18).destroy_all
471
+ def destroy_all(conditions=nil)
472
+ if conditions
473
+ where(conditions).destroy_all
474
+ else
475
+ to_a.each {|object| object.destroy}
476
+ @records
477
+ end
478
+ end
479
+
480
+ # Destroy an object (or multiple objects) that has the given id. The object is instantiated first,
481
+ # therefore all callbacks and filters are fired off before the object is deleted. This method is
482
+ # less efficient than ActiveRecord#delete but allows cleanup methods and other actions to be run.
483
+ #
484
+ # This essentially finds the object (or multiple objects) with the given id, creates a new object
485
+ # from the attributes, and then calls destroy on it.
486
+ #
487
+ # ==== Parameters
488
+ #
489
+ # * +id+ - Can be either an Integer or an Array of Integers.
490
+ #
491
+ # ==== Examples
492
+ #
493
+ # # Destroy a single object
494
+ # Foo.destroy(1)
495
+ #
496
+ # # Destroy multiple objects
497
+ # foos = [1,2,3]
498
+ # Foo.destroy(foos)
499
+ def destroy(id)
500
+ if id.is_a?(Array)
501
+ id.map {|one_id| destroy(one_id)}
502
+ else
503
+ find(id).destroy
504
+ end
505
+ end
506
+
507
+ # Deletes the records matching +conditions+ without instantiating the records
508
+ # first, and hence not calling the +destroy+ method nor invoking callbacks. This
509
+ # is a single SQL DELETE statement that goes straight to the database, much more
510
+ # efficient than +destroy_all+. Be careful with relations though, in particular
511
+ # <tt>:dependent</tt> rules defined on associations are not honored. Returns the
512
+ # number of rows affected.
513
+ #
514
+ # Post.delete_all("person_id = 5 AND (category = 'Something' OR category = 'Else')")
515
+ # Post.delete_all(["person_id = ? AND (category = ? OR category = ?)", 5, 'Something', 'Else'])
516
+ # Post.where(:person_id => 5).where(:category => ['Something', 'Else']).delete_all
517
+ #
518
+ # Both calls delete the affected posts all at once with a single DELETE statement.
519
+ # If you need to destroy dependent associations or call your <tt>before_*</tt> or
520
+ # +after_destroy+ callbacks, use the +destroy_all+ method instead.
521
+ #
522
+ # If a limit scope is supplied, +delete_all+ raises an ActiveRecord error:
523
+ #
524
+ # Post.limit(100).delete_all
525
+ # # => ActiveRecord::ActiveRecordError: delete_all doesn't support limit scope
526
+ def delete_all(conditions=nil)
527
+ raise ActiveRecordError.new("delete_all doesn't support limit scope") if self.limit_value
528
+
529
+ if conditions
530
+ where(conditions).delete_all
531
+ else
532
+ pluck :uuid
533
+ response = run_query
534
+ response.entities.each {|entity| entity.delete} # todo: can this be optimized into one call?
535
+ response.entities.size
536
+ end
537
+ end
538
+
539
+ # Deletes the row with a primary key matching the +id+ argument, using a
540
+ # SQL +DELETE+ statement, and returns the number of rows deleted. Active
541
+ # Record objects are not instantiated, so the object's callbacks are not
542
+ # executed, including any <tt>:dependent</tt> association options or
543
+ # Observer methods.
544
+ #
545
+ # You can delete multiple rows at once by passing an Array of <tt>id</tt>s.
546
+ #
547
+ # Note: Although it is often much faster than the alternative,
548
+ # <tt>#destroy</tt>, skipping callbacks might bypass business logic in
549
+ # your application that ensures referential integrity or performs other
550
+ # essential jobs.
551
+ #
552
+ # ==== Examples
553
+ #
554
+ # # Delete a single row
555
+ # Foo.delete(1)
556
+ #
557
+ # # Delete multiple rows
558
+ # Foo.delete([2,3,4])
559
+ def delete(id_or_array)
560
+ if id_or_array.is_a? Array
561
+ id_or_array.each {|id| @model_class.resource[id].delete} # todo: can this be optimized into one call?
562
+ else
563
+ @model_class.resource[id_or_array].delete
564
+ end
565
+ end
566
+
567
+ # Updates all records with details given if they match a set of conditions supplied, limits and order can
568
+ # also be supplied. This method sends a single update straight to the database. It does not instantiate
569
+ # the involved models and it does not trigger Active Record callbacks or validations.
570
+ #
571
+ # ==== Parameters
572
+ #
573
+ # * +updates+ - hash of attribute updates
574
+ #
575
+ # ==== Examples
576
+ #
577
+ # # Update all customers with the given attributes
578
+ # Customer.update_all wants_email: true
579
+ #
580
+ # # Update all books with 'Rails' in their title
581
+ # Book.where('title LIKE ?', '%Rails%').update_all(author: 'David')
582
+ #
583
+ # # Update all books that match conditions, but limit it to 5 ordered by date
584
+ # Book.where('title LIKE ?', '%Rails%').order(:created).limit(5).update_all(:author => 'David')
585
+ def update_all(updates)
586
+ raise ArgumentError, "Empty list of attributes to change" if updates.blank?
587
+ raise ArgumentError, "updates must be a Hash" unless updates.is_a? Hash
588
+ run_update(updates)
589
+ end
590
+
591
+ # Looping through a collection of records from the database
592
+ # (using the +all+ method, for example) is very inefficient
593
+ # since it will try to instantiate all the objects at once.
594
+ #
595
+ # In that case, batch processing methods allow you to work
596
+ # with the records in batches, thereby greatly reducing memory consumption.
597
+ #
598
+ # The #find_each method uses #find_in_batches with a batch size of 1000 (or as
599
+ # specified by the +:batch_size+ option).
600
+ #
601
+ # Person.all.find_each do |person|
602
+ # person.do_awesome_stuff
603
+ # end
604
+ #
605
+ # Person.where("age > 21").find_each do |person|
606
+ # person.party_all_night!
607
+ # end
608
+ #
609
+ # You can also pass the +:start+ option to specify
610
+ # an offset to control the starting point.
611
+ def find_each(options = {})
612
+ find_in_batches(options) do |records|
613
+ records.each { |record| yield record }
614
+ end
615
+ end
616
+
617
+ # Yields each batch of records that was found by the find +options+ as
618
+ # an array. The size of each batch is set by the +:batch_size+
619
+ # option; the default is 1000.
620
+ #
621
+ # You can control the starting point for the batch processing by
622
+ # supplying the +:start+ option. This is especially useful if you
623
+ # want multiple workers dealing with the same processing queue. You can
624
+ # make worker 1 handle all the records between id 0 and 10,000 and
625
+ # worker 2 handle from 10,000 and beyond (by setting the +:start+
626
+ # option on that worker).
627
+ #
628
+ # It's not possible to set the order. That is automatically set to
629
+ # ascending on the primary key ("id ASC") to make the batch ordering
630
+ # work. This also mean that this method only works with integer-based
631
+ # primary keys. You can't set the limit either, that's used to control
632
+ # the batch sizes.
633
+ #
634
+ # Person.where("age > 21").find_in_batches do |group|
635
+ # sleep(50) # Make sure it doesn't get too crowded in there!
636
+ # group.each { |person| person.party_all_night! }
637
+ # end
638
+ #
639
+ # # Let's process the next 2000 records
640
+ # Person.all.find_in_batches(start: 2000, batch_size: 2000) do |group|
641
+ # group.each { |person| person.party_all_night! }
642
+ # end
643
+ def find_in_batches(options={})
644
+ options.assert_valid_keys(:start, :batch_size)
645
+
646
+ raise "Not yet implemented" # todo
647
+
648
+ start = options.delete(:start) || 0
649
+ batch_size = options.delete(:batch_size) || 1000
650
+
651
+ while records.any?
652
+ records_size = records.size
653
+ primary_key_offset = records.last.id
654
+
655
+ yield records
656
+
657
+ break if records_size < batch_size
658
+
659
+ if primary_key_offset
660
+ records = relation.where(table[primary_key].gt(primary_key_offset)).to_a
661
+ else
662
+ raise "Primary key not included in the custom select clause"
663
+ end
664
+ end
665
+ end
666
+
667
+ # Updates an object (or multiple objects) and saves it to the database, if validations pass.
668
+ # The resulting object is returned whether the object was saved successfully to the database or not.
669
+ #
670
+ # ==== Parameters
671
+ #
672
+ # * +id+ - This should be the id or an array of ids to be updated.
673
+ # * +attributes+ - This should be a hash of attributes or an array of hashes.
674
+ #
675
+ # ==== Examples
676
+ #
677
+ # # Updates one record
678
+ # Person.update(15, user_name: 'Samuel', group: 'expert')
679
+ #
680
+ # # Updates multiple records
681
+ # people = { 1 => { "first_name" => "David" }, 2 => { "first_name" => "Jeremy" } }
682
+ # Person.update(people.keys, people.values)
683
+ def update(id, attributes)
684
+ if id.is_a?(Array)
685
+ id.map.with_index { |one_id, idx| update(one_id, attributes[idx]) }
686
+ else
687
+ object = find(id)
688
+ object.update_attributes(attributes)
689
+ object
690
+ end
691
+ end
692
+
693
+ ## todo: scoping
694
+ ## Scope all queries to the current scope.
695
+ ##
696
+ ## Comment.where(:post_id => 1).scoping do
697
+ ## Comment.first # SELECT * FROM comments WHERE post_id = 1
698
+ ## end
699
+ ##
700
+ ## Please check unscoped if you want to remove all previous scopes (including
701
+ ## the default_scope) during the execution of a block.
702
+ #def scoping
703
+ # previous, @model_class.current_scope = @model_class.current_scope, self
704
+ # yield
705
+ #ensure
706
+ # klass.current_scope = previous
707
+ #end
708
+
709
+
710
+ # #where accepts conditions in one of several formats.
711
+ #
712
+ # === string
713
+ #
714
+ # A single string, without additional arguments, is used in the where clause of the query.
715
+ #
716
+ # Client.where("orders_count = '2'")
717
+ # # SELECT * where orders_count = '2';
718
+ #
719
+ # Note that building your own string from user input may expose your application
720
+ # to injection attacks if not done properly. As an alternative, it is recommended
721
+ # to use one of the following methods.
722
+ #
723
+ # === array
724
+ #
725
+ # If an array is passed, then the first element of the array is treated as a template, and
726
+ # the remaining elements are inserted into the template to generate the condition.
727
+ # Active Record takes care of building the query to avoid injection attacks, and will
728
+ # convert from the ruby type to the database type where needed. Elements are inserted
729
+ # into the string in the order in which they appear.
730
+ #
731
+ # User.where(["name = ? and email = ?", "Joe", "joe@example.com"])
732
+ # # SELECT * WHERE name = 'Joe' AND email = 'joe@example.com';
733
+ #
734
+ # Alternatively, you can use named placeholders in the template, and pass a hash as the
735
+ # second element of the array. The names in the template are replaced with the corresponding
736
+ # values from the hash.
737
+ #
738
+ # User.where(["name = :name and email = :email", { name: "Joe", email: "joe@example.com" }])
739
+ # # SELECT * WHERE name = 'Joe' AND email = 'joe@example.com';
740
+ #
741
+ # This can make for more readable code in complex queries.
742
+ #
743
+ # Lastly, you can use sprintf-style % escapes in the template. This works slightly differently
744
+ # than the previous methods; you are responsible for ensuring that the values in the template
745
+ # are properly quoted. The values are passed to the connector for quoting, but the caller
746
+ # is responsible for ensuring they are enclosed in quotes in the resulting SQL. After quoting,
747
+ # the values are inserted using the same escapes as the Ruby core method <tt>Kernel::sprintf</tt>.
748
+ #
749
+ # User.where(["name = '%s' and email = '%s'", "Joe", "joe@example.com"])
750
+ # # SELECT * WHERE name = 'Joe' AND email = 'joe@example.com';
751
+ #
752
+ # If #where is called with multiple arguments, these are treated as if they were passed as
753
+ # the elements of a single array.
754
+ #
755
+ # User.where("name = :name and email = :email", { name: "Joe", email: "joe@example.com" })
756
+ # # SELECT * WHERE name = 'Joe' AND email = 'joe@example.com';
757
+ #
758
+ # When using strings to specify conditions, you can use any operator available from
759
+ # the database. While this provides the most flexibility, you can also unintentionally introduce
760
+ # dependencies on the underlying database. If your code is intended for general consumption,
761
+ # test with multiple database backends.
762
+ #
763
+ # === hash
764
+ #
765
+ # #where will also accept a hash condition, in which the keys are fields and the values
766
+ # are values to be searched for.
767
+ #
768
+ # Fields can be symbols or strings. Values can be single values, arrays, or ranges.
769
+ #
770
+ # User.where({ name: "Joe", email: "joe@example.com" })
771
+ # # SELECT * WHERE name = 'Joe' AND email = 'joe@example.com'
772
+ #
773
+ # User.where({ name: ["Alice", "Bob"]})
774
+ # # SELECT * WHERE name IN ('Alice', 'Bob')
775
+ #
776
+ # User.where({ created_at: (Time.now.midnight - 1.day)..Time.now.midnight })
777
+ # # SELECT * WHERE (created_at BETWEEN '2012-06-09 07:00:00.000000' AND '2012-06-10 07:00:00.000000')
778
+ #
779
+ # In the case of a belongs_to relationship, an association key can be used
780
+ # to specify the model if an ActiveRecord object is used as the value.
781
+ #
782
+ # author = Author.find(1)
783
+ #
784
+ # # The following queries will be equivalent:
785
+ # Post.where(:author => author)
786
+ # Post.where(:author_id => author)
787
+ #
788
+ # === empty condition
789
+ #
790
+ # If the condition returns true for blank?, then where is a no-op and returns the current relation.
791
+ #
792
+ def where(opts, *rest)
793
+ return self if opts.blank?
794
+ case opts
795
+ when Hash
796
+ @options[:hash] = opts # keep around for first_or_create stuff...
797
+ opts.each do |k,v|
798
+ # todo: can we support IN and BETWEEN syntax as documented above?
799
+ v = "'#{v}'" if v.is_a? String
800
+ query_conditions << "#{k} = #{v}"
801
+ end
802
+ when String
803
+ query_conditions << opts
804
+ when Array
805
+ query = opts.shift.gsub '?', "'%s'"
806
+ query = query % opts
807
+ query_conditions << query
808
+ end
809
+ self
810
+ end
811
+
812
+
813
+ protected
814
+
815
+
816
+ def limit_value
817
+ @options[:limit]
818
+ end
819
+
820
+ def query_conditions
821
+ @options[:conditions] ||= []
822
+ end
823
+
824
+ def loaded?
825
+ !!@records
826
+ end
827
+
828
+ def reversed?
829
+ !!@options[:reversed]
830
+ end
831
+
832
+ def find_one(id_or_name=nil)
833
+ begin
834
+ entity = @model_class.resource[id_or_name].query(nil, limit: 1).entity
835
+ @model_class.model_name.constantize.new(entity.data) if entity
836
+ rescue RestClient::ResourceNotFound
837
+ nil
838
+ end
839
+ end
840
+
841
+ def find_one!(id_or_name=nil)
842
+ find_one(id_or_name) or raise RecordNotFound
843
+ end
844
+
845
+ # Server-side options:
846
+ # Xql string Query in the query language
847
+ # type string Entity type to return
848
+ # Xreversed string Return results in reverse order
849
+ # connection string Connection type (e.g., "likes")
850
+ # start string First entity's UUID to return
851
+ # cursor string Encoded representation of the query position for paging
852
+ # Xlimit integer Number of results to return
853
+ # permission string Permission type
854
+ # Xfilter string Condition on which to filter
855
+ def query_options
856
+ # todo: support more options?
857
+ options = {}
858
+ options.merge!({:limit => limit_value.to_json}) if limit_value
859
+ options.merge!({:skip => @options[:skip].to_json}) if @options[:skip]
860
+ options.merge!({:reversed => reversed?.to_json}) if reversed?
861
+ options.merge!({:order => @options[:order]}) if @options[:order]
862
+ options
863
+ end
864
+
865
+ def create_query
866
+ select = @options[:select] || '*'
867
+ where = ('where ' + query_conditions.join(' and ')) unless query_conditions.blank?
868
+ "select #{select} #{where}"
869
+ end
870
+
871
+ def run_query
872
+ @model_class.resource.query(create_query, query_options)
873
+ end
874
+
875
+ def run_update(attributes)
876
+ @model_class.resource.update_query(attributes, create_query, query_options)
877
+ end
878
+
879
+ def load
880
+ return if loaded?
881
+ begin
882
+ response = run_query
883
+ @records = response.entities.collect {|r| @model_class.model_name.constantize.new(r.data)}
884
+ rescue RestClient::ResourceNotFound
885
+ @records = []
886
+ end
887
+ end
888
+ end
889
+ end
890
+ end