usergrid_ironhorse 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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