-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Expand file tree
/
Copy pathcriteria.rb
More file actions
647 lines (598 loc) · 20.3 KB
/
Copy pathcriteria.rb
File metadata and controls
647 lines (598 loc) · 20.3 KB
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
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
# frozen_string_literal: true
# rubocop:todo all
require "mongoid/criteria/findable"
require "mongoid/criteria/includable"
require "mongoid/criteria/inspectable"
require "mongoid/criteria/marshalable"
require "mongoid/criteria/modifiable"
require "mongoid/criteria/queryable"
require "mongoid/criteria/scopable"
require "mongoid/criteria/options"
require "mongoid/criteria/translator"
module Mongoid
# The +Criteria+ class is the core object needed in Mongoid to retrieve
# objects from the database. It is a DSL that essentially sets up the
# selector and options arguments that get passed on to a Mongo::Collection
# in the Ruby driver. Each method on the +Criteria+ returns self to they
# can be chained in order to create a readable criterion to be executed
# against the database.
class Criteria
include Enumerable
# @api private
alias :_enumerable_find :find
include Contextual
include Queryable
include Findable
# @api private
alias :_findable_find :find
include Inspectable
include Includable
include Marshalable
include Modifiable
include Scopable
include Clients::Options
include Clients::Sessions
include Options
class << self
# Convert the given hash to a criteria. Will iterate over each keys in the
# hash which must correspond to method on a criteria object. The hash
# must also include a "klass" key.
#
# @example Convert the hash to a criteria.
# Criteria.from_hash({ klass: Band, where: { name: "Depeche Mode" })
#
# @param [ Hash ] hash The hash to convert.
#
# @return [ Criteria ] The criteria.
def from_hash(hash)
criteria = Criteria.new(hash.delete(:klass) || hash.delete('klass'))
hash.each_pair do |method, args|
criteria = criteria.__send__(method, args)
end
criteria
end
end
# Static array used to check with method missing - we only need to ever
# instantiate once.
CHECK = []
attr_accessor :embedded, :klass, :parent_document, :association
# Returns true if the supplied +Enumerable+ or +Criteria+ is equal to the results
# of this +Criteria+ or the criteria itself.
#
# @note This will force a database load when called if an enumerable is passed.
#
# @param [ Object ] other The other +Enumerable+ or +Criteria+ to compare to.
#
# @return [ true | false ] If the objects are equal.
def ==(other)
return super if other.respond_to?(:selector)
entries == other
end
# Finds one or many documents given the provided _id values, or filters
# the documents in the current scope in the application process space
# after loading them if needed.
#
# If this method is not given a block, it delegates to +Findable#find+
# and finds one or many documents for the provided _id values.
#
# If this method is given a block, it delegates to +Enumerable#find+ and
# returns the first document of those found by the current Criteria object
# for which the block returns a truthy value.
#
# Note that the "default proc" argument of Enumerable is not specially
# treated by Mongoid - the decision between delegating to +Findable+ vs
# +Enumerable+ is made solely based on whether +find+ is passed a block.
#
# @note Each argument can be an individual id, an array of ids or
# a nested array. Each array will be flattened.
#
# @example Finds a document by its _id, invokes Findable#find.
# criteria.find("1234")
#
# @example Finds the first matching document using a block, invokes Enumerable#find.
# criteria.find { |item| item.name == "Depeche Mode" }
#
# @example Finds the first matching document using a block using the default Proc, invokes Enumerable#find.
# criteria.find(-> { "Default Band" }) { |item| item.name == "Milwaukee Mode" }
#
# @example Tries to find a document whose _id is the stringification of the provided Proc, typically failing.
# enumerator = criteria.find(-> { "Default Band" })
#
# @param [ [ Object | Array<Object> ]... ] *args The id(s).
# @param &block Optional block to pass.
# @yield [ Object ] Yields each enumerable element to the block.
#
# @return [ Document | Array<Document> | nil ] A document or matching documents.
#
# @raise Errors::DocumentNotFound If the parameters were _id values and
# not all documents are found and the +raise_not_found_error+
# Mongoid configuration option is truthy.
#
# @see https://ruby-doc.org/core/Enumerable.html#method-i-find
def find(*args, &block)
if block_given?
_enumerable_find(*args, &block)
else
_findable_find(*args)
end
end
# Needed to properly get a criteria back as json
#
# @example Get the criteria as json.
# Person.where(:title => "Sir").as_json
#
# @param [ Hash ] options Options to pass through to the serializer.
#
# @return [ String ] The JSON string.
def as_json(options = nil)
entries.as_json(options)
end
# Get the documents from the embedded criteria.
#
# @example Get the documents.
# criteria.documents
#
# @return [ Array<Document> ] The documents.
def documents
@documents ||= []
end
# Set the embedded documents on the criteria.
#
# @example Set the documents.
#
# @param [ Array<Document> ] docs The embedded documents.
#
# @return [ Array<Document> ] The embedded documents.
def documents=(docs)
@documents = docs
end
# Is the criteria for embedded documents?
#
# @example Is the criteria for embedded documents?
# criteria.embedded?
#
# @return [ true | false ] If the criteria is embedded.
def embedded?
!!@embedded
end
# Produce a clone of the current criteria object with it's "raw"
# setting set to the given value. A criteria set to "raw" will return
# all results as raw hashes. If `typed` is true, the values in the hashes
# will be typecast according to the fields that they correspond to.
#
# When "raw" is not set (or if `raw_results` is false), the criteria will
# return all results as instantiated Document instances.
#
# @example Return query results as raw hashes:
# Person.where(city: 'Boston').raw
#
# @param [ true | false ] raw_results Whether the new criteria should be
# placed in "raw" mode or not.
# @param [ true | false ] typed Whether the raw results should be typecast
# before being returned. Default is true if raw_results is false, and
# false otherwise.
#
# @return [ Criteria ] the cloned criteria object.
def raw(raw_results = true, typed: nil)
# default for typed is true when raw_results is false, and false when
# raw_results is true.
typed = !raw_results if typed.nil?
if !typed && !raw_results
raise ArgumentError, 'instantiated results must be typecast'
end
clone.tap do |criteria|
criteria._raw_results = { raw: raw_results, typed: typed }
end
end
# An internal helper for getting/setting the "raw" flag on a given criteria
# object.
#
# @return [ nil | Hash ] If set, it is a hash with two keys, :raw and :typed,
# that describe whether raw results should be returned, and whether they
# ought to be typecast.
#
# @api private
attr_accessor :_raw_results
# Predicate that answers the question: is this criteria object currently
# in raw mode? (See #raw for a description of raw mode.)
#
# @return [ true | false ] whether the criteria is in raw mode or not.
def raw_results?
_raw_results && _raw_results[:raw]
end
# Predicate that answers the question: should the results returned by
# this criteria object be typecast? (See #raw for a description of this.)
# The answer is meaningless unless #raw_results? is true, since if
# instantiated document objects are returned they will always be typecast.
#
# @return [ true | false ] whether the criteria should return typecast
# results.
def typecast_results?
_raw_results && _raw_results[:typed]
end
# Extract a single id from the provided criteria. Could be in an $and
# query or a straight _id query.
#
# @example Extract the id.
# criteria.extract_id
#
# @return [ Object ] The id.
def extract_id
selector['_id'] || selector[:_id] || selector['id'] || selector[:id]
end
# Adds a criterion to the +Criteria+ that specifies additional options
# to be passed to the Ruby driver, in the exact format for the driver.
#
# @example Add extra params to the criteria.
# criteria.extras(:limit => 20, :skip => 40)
#
# @param [ Hash ] extras The extra driver options.
#
# @return [ Criteria ] The cloned criteria.
def extras(extras)
crit = clone
crit.options.merge!(extras)
crit
end
# Get the list of included fields.
#
# @example Get the field list.
# criteria.field_list
#
# @return [ Array<String> ] The fields.
def field_list
if options[:fields]
options[:fields].keys.reject{ |key| key == klass.discriminator_key }
else
[]
end
end
# When freezing a criteria we need to initialize the context first
# otherwise the setting of the context on attempted iteration will raise a
# runtime error.
#
# @example Freeze the criteria.
# criteria.freeze
#
# @return [ Criteria ] The frozen criteria.
def freeze
context and inclusions and super
end
# Initialize the new criteria.
#
# @example Init the new criteria.
# Criteria.new(Band)
#
# @param [ Class ] klass The model class.
def initialize(klass)
@klass = klass
@embedded = nil
@none = nil
klass ? super(klass.aliased_fields, klass.fields, klass.relations, klass.aliased_associations) : super({}, {}, {}, {})
end
# Merges another object with this +Criteria+ and returns a new criteria.
# The other object may be a +Criteria+ or a +Hash+. This is used to
# combine multiple scopes together, where a chained scope situation
# may be desired.
#
# @example Merge the criteria with another criteria.
# criteria.merge(other_criteria)
#
# @example Merge the criteria with a hash. The hash must contain a klass
# key and the key/value pairs correspond to method names/args.
#
# criteria.merge({
# klass: Band,
# where: { name: "Depeche Mode" },
# order_by: { name: 1 }
# })
#
# @param [ Criteria ] other The other criterion to merge with.
#
# @return [ Criteria ] A cloned self.
def merge(other)
crit = clone
crit.merge!(other)
crit
end
# Merge the other criteria into this one.
#
# @example Merge another criteria into this criteria.
# criteria.merge(Person.where(name: "bob"))
#
# @param [ Criteria | Hash ] other The criteria to merge in.
#
# @return [ Criteria ] The merged criteria.
def merge!(other)
other = self.class.from_hash(other) if other.is_a?(Hash)
selector.merge!(other.selector)
options.merge!(other.options)
self.documents = other.documents.dup unless other.documents.empty?
self.scoping_options = other.scoping_options
self.inclusions = (inclusions + other.inclusions).uniq
self._raw_results = self._raw_results || other._raw_results
self
end
# Returns a criteria that will always contain zero results and never hits
# the database.
#
# @example Return a none criteria.
# criteria.none
#
# @return [ Criteria ] The none criteria.
def none
@none = true and self
end
# Is the criteria an empty but chainable criteria?
#
# @example Is the criteria a none criteria?
# criteria.empty_and_chainable?
#
# @return [ true | false ] If the criteria is a none.
def empty_and_chainable?
!!@none
end
# Overridden to include _type in the fields.
#
# @example Limit the fields returned from the database.
# Band.only(:name)
#
# @param [ [ Symbol | Array<Symbol> ]... ] *args The field name(s).
#
# @return [ Criteria ] The cloned criteria.
def only(*args)
args = args.flatten
return clone if args.empty?
if (args & Fields::IDS).empty?
args.unshift(:_id)
end
if klass.hereditary?
args.push(klass.discriminator_key.to_sym)
end
super(*args)
end
# Set the read preference for the criteria.
#
# @example Set the read preference.
# criteria.read(mode: :primary_preferred)
#
# @param [ Hash ] value The mode preference.
#
# @return [ Criteria ] The cloned criteria.
def read(value = nil)
clone.tap do |criteria|
criteria.options.merge!(read: value)
end
end
# Overridden to exclude _id from the fields.
#
# @example Exclude fields returned from the database.
# Band.without(:name)
#
# @param [ Symbol... ] *args The field name(s).
#
# @return [ Criteria ] The cloned criteria.
def without(*args)
args -= id_fields
super(*args)
end
# Returns true if criteria responds to the given method.
#
# @example Does the criteria respond to the method?
# criteria.respond_to?(:each)
#
# @param [ Symbol ] name The name of the class method on the +Document+.
# @param [ true | false ] include_private Whether to include privates.
#
# @return [ true | false ] If the criteria responds to the method.
def respond_to?(name, include_private = false)
super || klass.respond_to?(name) || CHECK.respond_to?(name, include_private)
end
alias :to_ary :to_a
# Convert the criteria to a proc.
#
# @example Convert the criteria to a proc.
# criteria.to_proc
#
# @return [ Proc ] The wrapped criteria.
def to_proc
->{ self }
end
# Adds a criterion to the +Criteria+ that specifies a type or an Array of
# types that must be matched.
#
# @example Match only specific models.
# criteria.type('Browser')
# criteria.type(['Firefox', 'Browser'])
#
# @param [ Array<String> ] types The types to match against.
#
# @return [ Criteria ] The cloned criteria.
def type(types)
any_in(self.discriminator_key.to_sym => Array(types))
end
# This is the general entry point for most MongoDB queries. This either
# creates a standard field: value selection, and expanded selection with
# the use of hash methods, or a $where selection if a string is provided.
#
# @example Add a standard selection.
# criteria.where(name: "syd")
#
# @example Add a javascript selection.
# criteria.where("this.name == 'syd'")
#
# @param [ [ Hash | String ]... ] *args The standard selection
# or javascript string.
#
# @raise [ UnsupportedJavascript ] If provided a string and the criteria
# is embedded.
#
# @return [ Criteria ] The cloned selectable.
def where(*args)
# Historically this method required exactly one argument.
# As of https://jira.mongodb.org/browse/MONGOID-4804 it also accepts
# zero arguments.
# The underlying where implementation that super invokes supports
# any number of arguments, but we don't presently allow multiple
# arguments through this method. This API can be reconsidered in the
# future.
if args.length > 1
raise ArgumentError, "Criteria#where requires zero or one arguments (given #{args.length})"
end
if args.length == 1
expression = args.first
if expression.is_a?(::String) && embedded?
raise Errors::UnsupportedJavascript.new(klass, expression)
end
end
super
end
# Get a version of this criteria without the options.
#
# @example Get the criteria without options.
# criteria.without_options
#
# @return [ Criteria ] The cloned criteria.
def without_options
crit = clone
crit.options.clear
crit
end
# Find documents by the provided javascript and scope. Uses a $where but is
# different from +Criteria#where+ in that it will pass a code object to the
# query instead of a pure string. Safe against Javascript injection
# attacks.
#
# @example Find by javascript.
# Band.for_js("this.name = param", param: "Tool")
#
# @param [ String ] javascript The javascript to execute in the $where.
# @param [ Hash ] scope The scope for the code.
#
# @return [ Criteria ] The criteria.
#
# @deprecated
def for_js(javascript, scope = {})
code = if scope.empty?
# CodeWithScope is not supported for $where as of MongoDB 4.4
BSON::Code.new(javascript)
else
BSON::CodeWithScope.new(javascript, scope)
end
js_query(code)
end
Mongoid.deprecate(self, :for_js)
private
# Are documents in the query missing, and are we configured to raise an
# error?
#
# @api private
#
# @example Check for missing documents.
# criteria.check_for_missing_documents!([], [ 1 ])
#
# @param [ Array<Document> ] result The result.
# @param [ Array<Object> ] ids The ids.
#
# @raise [ Errors::DocumentNotFound ] If none are found and raising an
# error.
def check_for_missing_documents!(result, ids)
if (result.size < ids.size) && Mongoid.raise_not_found_error
raise Errors::DocumentNotFound.new(klass, ids, ids - result.map(&:_id))
end
end
# Clone or dup the current +Criteria+. This will return a new criteria with
# the selector, options, klass, embedded options, etc intact.
#
# @api private
#
# @example Clone a criteria.
# criteria.clone
#
# @example Dup a criteria.
# criteria.dup
#
# @param [ Criteria ] other The criteria getting cloned.
#
# @return [ nil ] nil.
def initialize_copy(other)
@inclusions = other.inclusions.dup
@scoping_options = other.scoping_options
@documents = other.documents.dup
self._raw_results = other._raw_results
@context = nil
super
end
# Used for chaining +Criteria+ scopes together in the for of class methods
# on the +Document+ the criteria is for.
#
# @example Handle method missing.
# criteria.method_missing(:name)
#
# @param [ Symbol ] name The method name.
# @param [ Object... ] *args The arguments.
#
# @return [ Object ] The result of the method call.
ruby2_keywords def method_missing(name, *args, &block)
if klass.respond_to?(name)
klass.send(:with_scope, self) do
klass.send(name, *args, &block)
end
elsif CHECK.respond_to?(name)
return entries.send(name, *args, &block)
else
super
end
end
# For models where inheritance is at play we need to add the type
# selection.
#
# @example Add the type selection.
# criteria.merge_type_selection
#
# @return [ true | false ] If type selection was added.
def merge_type_selection
selector.merge!(type_selection) if type_selectable?
end
# Is the criteria type selectable?
#
# @api private
#
# @example If the criteria type selectable?
# criteria.type_selectable?
#
# @return [ true | false ] If type selection should be added.
def type_selectable?
klass.hereditary? &&
!selector.keys.include?(self.discriminator_key) &&
!selector.keys.include?(self.discriminator_key.to_sym)
end
# Get the selector for type selection.
#
# @api private
#
# @example Get a type selection hash.
# criteria.type_selection
#
# @return [ Hash ] The type selection.
def type_selection
klasses = klass._types
if klasses.size > 1
{ klass.discriminator_key.to_sym => { "$in" => klass._types }}
else
{ klass.discriminator_key.to_sym => klass._types[0] }
end
end
# Get a new selector with type selection in it.
#
# @api private
#
# @example Get a selector with type selection.
# criteria.selector_with_type_selection
#
# @return [ Hash ] The selector.
def selector_with_type_selection
type_selectable? ? selector.merge(type_selection) : selector
end
end
end