This repository has been archived by the owner on Jun 13, 2019. It is now read-only.
forked from shanna/dm-mongo-adapter
/
query.rb
270 lines (237 loc) · 8.95 KB
/
query.rb
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
module DataMapper
module Mongo
# This class is responsible for taking Query instances from DataMapper and
# formatting the query such that it can be performed by the Mongo library.
class Query
include DataMapper::Assertions
include DataMapper::Query::Conditions
# Creates a new Query instance
#
# @param [Mongo::Collection] collection
# @param [DataMapper::Query] query
#
# @api semipublic
def initialize(collection, query)
assert_kind_of 'collection', collection, ::Mongo::Collection
assert_kind_of 'query', query, DataMapper::Query
@collection = collection
@query = query
@statements = {}
end
# Applies the query to the collection
#
# Reads the query options, fetches the resources matching the query
# statements and filters according to the conditions.
#
# @return [Mongo::Collection]
#
# @api semipublic
def read
setup_conditions_and_options
find
end
# TODO: document
# @api semipublic
def count
setup_conditions_and_options
[@collection.count({:query => @statements}.merge(@options))]
end
# TODO: document
# @api semipublic
def group
setup_conditions_and_options
property_names = []
operators = []
keys = []
@query.fields.each do |field|
if field.kind_of?(DataMapper::Query::Operator)
operators << field
if field.target != :all
property_names << field.target.name
end
else
keys << field.name
property_names << field.name
end
end
if operators.empty?
initial = {}
reduce = JavaScript::Reduce.new.to_s
finalize = nil
else
js_operation = JavaScript::Operation.new(operators)
initial = js_operation.initial
reduce = js_operation.reduce
finalize = js_operation.finalize
keys = keys - initial.keys
end
opts = {
:key => keys, :cond => @statements, :initial => initial,
:reduce => reduce, :finalize => finalize
}
@collection.group(opts).map do |records|
DataMapper::Ext::Hash.to_mash(records).symbolize_keys
end
end
private
# TODO: document
# @api private
def setup_conditions_and_options
@options = {}
@options[:limit] = @query.limit if @query.limit
@options[:skip] = @query.offset if @query.offset
@options[:sort] = sort_statement(@query.order) unless @query.order.nil?
conditions_statement(@query.conditions)
end
# TODO: document
# @api private
def find
@collection.find(@statements, @options).to_a
end
# Takes a condition and returns a Mongo-compatible hash
#
# @param [DataMapper::Query::Conditions::AbstractOperation, DataMapper::Query::Conditions::AbstractComparison] operation
# An operation to be made suitable for use with Mongo
# @param [Boolean] affirmative
# Are we looking for resources which match the condition (true), or
# those which do not match (false)
#
# @return [Hash]
#
# @api private
def conditions_statement(conditions, affirmative = true)
case conditions
when AbstractOperation then operation_statement(conditions, affirmative)
when AbstractComparison then comparison_statement(conditions, affirmative)
end
end
# Takes a Operation condition and returns a Mongo-compatible hash
#
# @param [DataMapper::Query::Conditions::Operation] operation
# An operation to be made suitable for use with Mongo
# @param [Boolean] affirmative
# Are we looking for resources which match the condition (true), or
# those which do not match (false)
#
# @return [Hash]
#
# @api private
def operation_statement(operation, affirmative = true)
case operation
when NotOperation then conditions_statement(operation.first, !affirmative)
when AndOperation then operation.each{|op| conditions_statement(op, affirmative)}
when OrOperation then or_statement(operation, affirmative)
end
end
# Takes a Comparison condition and returns a Mongo-compatible hash
#
# @param [DataMapper::Query::Conditions::Comparison] comparison
# An comparison to be made suitable for use with Mongo
# @param [Boolean] affirmative
# Are we looking for resources which match the condition (true), or
# those which do not match (false)
#
# @return [Hash]
#
# @api private
def comparison_statement(comparison, affirmative = true)
if comparison.relationship?
return conditions_statement(comparison.foreign_key_mapping, affirmative)
end
update_statements(comparison, comparison.subject.field, affirmative)
end
# @api private
def or_statement(operation, affirmative = true)
statement = { '$or' => [] }
operation.each do |operand|
statement['$or'] << { operand.subject.field => build_statement(operand, affirmative) }
end
@statements.merge!(statement)
end
def build_statement(comparison, affirmative)
value = if comparison.value.kind_of?(Array)
comparison.value.map { |value| value.class.to_mongo(value) }
else
comparison.value.class.to_mongo(comparison.value)
end
operator = if affirmative
case comparison
when EqualToComparison then value
when GreaterThanComparison then {'$gt' => value}
when LessThanComparison then {'$lt' => value}
when GreaterThanOrEqualToComparison then {'$gte' => value}
when LessThanOrEqualToComparison then {'$lte' => value}
when InclusionComparison then inclusion_comparison_operator(comparison, value)
when RegexpComparison then value
when LikeComparison then comparison.send(:expected)
else
raise NotImplementedError
end
else
case comparison
when EqualToComparison then {'$ne' => value}
when InclusionComparison then inclusion_comparison_operator(comparison, value, affirmative)
when RegexpComparison then {'$not' => value}
else
raise NotImplementedError
end
end
operator
end
# TODO: document
# @api private
def update_statements(comparison, field, affirmative = true)
operator = build_statement(comparison, affirmative)
case operator
when ::Hash
(@statements[field.to_sym] ||= {}).merge!(operator)
when ::Array
(@statements['$or'] ||= []).concat(operator.map { |conditions| { field => conditions } })
else
@statements[field.to_sym] = operator
end
end
# Creates Mongo's equivalent of an IN() condition
#
# @param [DataMapper::Query::Conditions::Comparison] comparison
# An comparison to be made suitable for use with Mongo
# @param [Object] value
# The value to match against.
#
# @return [Hash]
#
# @api private
def inclusion_comparison_operator(comparison, value, affirmative = true)
if value.kind_of?(Range)
if affirmative
{ '$gte' => value.first, value.exclude_end? ? '$lt' : '$lte' => value.last }
else
[ { '$lt' => value.first }, { value.exclude_end? ? '$gte' : '$gt' => value.last } ]
end
elsif comparison.kind_of?(InclusionComparison) && value.size == 1
value.first
elsif comparison.subject.kind_of?(DataMapper::Mongo::Property::Array)
value
else
{ affirmative ? '$in' : '$nin' => value}
end
end
# Constructs a sort statement which can be used by the Mongo library
#
# Mongo::Collection#find requires that sort statements consist of an
# array where each element is another array containing the field name
# and the sort order.
#
# @param [Enumerable<DataMapper::Query::Direction>]
#
# @return [Array<Array>]
#
# @api private
def sort_statement(conditions)
conditions.inject([]) do |sort_arr, condition|
sort_arr << [condition.target.field, condition.operator == :asc ? 'ascending' : 'descending']
end
end
end # Query
end # Mongo
end # DataMapper