dm-mongo-adapter 0.2.0.pre1
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.
- data/.gitignore +9 -0
- data/LICENSE +20 -0
- data/README.rdoc +130 -0
- data/Rakefile +36 -0
- data/TODO +33 -0
- data/VERSION.yml +5 -0
- data/bin/console +31 -0
- data/dm-mongo-adapter.gemspec +154 -0
- data/lib/mongo_adapter.rb +71 -0
- data/lib/mongo_adapter/adapter.rb +255 -0
- data/lib/mongo_adapter/aggregates.rb +21 -0
- data/lib/mongo_adapter/conditions.rb +100 -0
- data/lib/mongo_adapter/embedded_model.rb +187 -0
- data/lib/mongo_adapter/embedded_resource.rb +134 -0
- data/lib/mongo_adapter/embedments/one_to_many.rb +139 -0
- data/lib/mongo_adapter/embedments/one_to_one.rb +53 -0
- data/lib/mongo_adapter/embedments/relationship.rb +258 -0
- data/lib/mongo_adapter/migrations.rb +45 -0
- data/lib/mongo_adapter/model.rb +89 -0
- data/lib/mongo_adapter/model/embedment.rb +215 -0
- data/lib/mongo_adapter/modifier.rb +85 -0
- data/lib/mongo_adapter/query.rb +252 -0
- data/lib/mongo_adapter/query/java_script.rb +145 -0
- data/lib/mongo_adapter/resource.rb +147 -0
- data/lib/mongo_adapter/types/date.rb +28 -0
- data/lib/mongo_adapter/types/date_time.rb +28 -0
- data/lib/mongo_adapter/types/db_ref.rb +65 -0
- data/lib/mongo_adapter/types/discriminator.rb +32 -0
- data/lib/mongo_adapter/types/object_id.rb +72 -0
- data/lib/mongo_adapter/types/objects.rb +31 -0
- data/script/performance.rb +260 -0
- data/spec/legacy/README +6 -0
- data/spec/legacy/adapter_shared_spec.rb +299 -0
- data/spec/legacy/adapter_spec.rb +174 -0
- data/spec/legacy/associations_spec.rb +140 -0
- data/spec/legacy/embedded_resource_spec.rb +157 -0
- data/spec/legacy/embedments_spec.rb +177 -0
- data/spec/legacy/modifier_spec.rb +81 -0
- data/spec/legacy/property_spec.rb +51 -0
- data/spec/legacy/spec_helper.rb +3 -0
- data/spec/legacy/sti_spec.rb +53 -0
- data/spec/lib/cleanup_models.rb +32 -0
- data/spec/lib/raw_connections.rb +11 -0
- data/spec/public/embedded_collection_spec.rb +61 -0
- data/spec/public/embedded_resource_spec.rb +220 -0
- data/spec/public/model/embedment_spec.rb +186 -0
- data/spec/public/model_spec.rb +37 -0
- data/spec/public/resource_spec.rb +564 -0
- data/spec/public/shared/model_embedments_spec.rb +338 -0
- data/spec/public/shared/object_id_shared_spec.rb +56 -0
- data/spec/public/types/df_ref_spec.rb +6 -0
- data/spec/public/types/discriminator_spec.rb +118 -0
- data/spec/public/types/embedded_array_spec.rb +55 -0
- data/spec/public/types/embedded_hash_spec.rb +83 -0
- data/spec/public/types/object_id_spec.rb +6 -0
- data/spec/rcov.opts +6 -0
- data/spec/semipublic/embedded_model_spec.rb +43 -0
- data/spec/semipublic/model/embedment_spec.rb +42 -0
- data/spec/semipublic/resource_spec.rb +70 -0
- data/spec/spec.opts +4 -0
- data/spec/spec_helper.rb +45 -0
- data/tasks/spec.rake +37 -0
- data/tasks/yard.rake +9 -0
- data/tasks/yardstick.rake +21 -0
- metadata +215 -0
@@ -0,0 +1,45 @@
|
|
1
|
+
module DataMapper
|
2
|
+
module Migrations
|
3
|
+
module SingletonMethods
|
4
|
+
private
|
5
|
+
|
6
|
+
def repository_execute(method, repository_name)
|
7
|
+
DataMapper::Model.descendants.each do |model|
|
8
|
+
model.send(method, repository_name || model.default_repository_name) unless model == DataMapper::Mongo::EmbeddedResource
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
module Mongo
|
15
|
+
module Migrations
|
16
|
+
def self.included(base)
|
17
|
+
DataMapper.extend(DataMapper::Migrations::SingletonMethods)
|
18
|
+
|
19
|
+
[ :Repository, :Model ].each do |name|
|
20
|
+
DataMapper.const_get(name).send(:include, DataMapper::Migrations.const_get(name))
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def storage_exists?(storage_name)
|
25
|
+
database.collections.map(&:name).include?(storage_name)
|
26
|
+
end
|
27
|
+
|
28
|
+
def create_model_storage(model)
|
29
|
+
return false if storage_exists?(model.storage_name)
|
30
|
+
database.create_collection(model.storage_name)
|
31
|
+
end
|
32
|
+
|
33
|
+
def upgrade_model_storage(model)
|
34
|
+
create_model_storage(model)
|
35
|
+
end
|
36
|
+
|
37
|
+
def destroy_model_storage(model)
|
38
|
+
database.drop_collection(model.storage_name)
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
DataMapper::Mongo.send(:include, DataMapper::Mongo::Migrations)
|
@@ -0,0 +1,89 @@
|
|
1
|
+
module DataMapper
|
2
|
+
module Mongo
|
3
|
+
module Model
|
4
|
+
def self.extended(model)
|
5
|
+
model.extend Embedment
|
6
|
+
end
|
7
|
+
|
8
|
+
# Defines a Property on the Resource
|
9
|
+
#
|
10
|
+
# Overrides the property method in dm-core so as to automatically map
|
11
|
+
# Array and Hash types to EmbeddedArray and EmbeddedHash respectively.
|
12
|
+
#
|
13
|
+
# @param [Symbol] name
|
14
|
+
# the name for which to call this property
|
15
|
+
# @param [Type] type
|
16
|
+
# the type to define this property ass
|
17
|
+
# @param [Hash(Symbol => String)] options
|
18
|
+
# a hash of available options
|
19
|
+
#
|
20
|
+
# @return [Property]
|
21
|
+
# the created Property
|
22
|
+
#
|
23
|
+
# @api public
|
24
|
+
def property(name, type, options = {})
|
25
|
+
if Array == type
|
26
|
+
type = DataMapper::Mongo::Types::EmbeddedArray
|
27
|
+
elsif Hash == type
|
28
|
+
type = DataMapper::Mongo::Types::EmbeddedHash
|
29
|
+
elsif DateTime == type
|
30
|
+
type = DataMapper::Mongo::Types::DateTime
|
31
|
+
elsif Date == type
|
32
|
+
type = DataMapper::Mongo::Types::Date
|
33
|
+
end
|
34
|
+
|
35
|
+
super(name, type, options)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Loads an instance of this Model, taking into account IdentityMap
|
39
|
+
# lookup, inheritance columns(s) and Property typecasting. Also loads
|
40
|
+
# the embedments on the Resource.
|
41
|
+
#
|
42
|
+
# @param [Enumerable<Object>] records
|
43
|
+
# An Array of Resource or Hashes to load a Resource with
|
44
|
+
# @param [DataMapper::Query] query
|
45
|
+
# The query used to load the Resource
|
46
|
+
#
|
47
|
+
# @return [Resource]
|
48
|
+
# The loaded Resource instance
|
49
|
+
#
|
50
|
+
# @overrides DataMapper::Model#load
|
51
|
+
#
|
52
|
+
# @api semipublic
|
53
|
+
def load(records, query)
|
54
|
+
if discriminator = properties(query.repository.name).discriminator
|
55
|
+
records.each do |record|
|
56
|
+
discriminator_key = discriminator.name.to_s
|
57
|
+
discriminator_value = discriminator.type.load(record[discriminator_key], discriminator)
|
58
|
+
|
59
|
+
record[discriminator_key] = discriminator_value
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
resources = super
|
64
|
+
|
65
|
+
# Load embedded resources
|
66
|
+
resources.each_with_index do |resource, index|
|
67
|
+
resource.model.embedments.each do |name, relationship|
|
68
|
+
unless (targets = records[index][name.to_s]).blank?
|
69
|
+
relationship.set(resource, targets, true)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
resources
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
# @api private
|
80
|
+
def const_missing(name)
|
81
|
+
if DataMapper::Mongo::Types.const_defined?(name)
|
82
|
+
DataMapper::Mongo::Types.const_get(name)
|
83
|
+
else
|
84
|
+
super
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end # Model
|
88
|
+
end # Mongo
|
89
|
+
end # DataMapper
|
@@ -0,0 +1,215 @@
|
|
1
|
+
module DataMapper
|
2
|
+
module Mongo
|
3
|
+
module Model
|
4
|
+
# Embedment extends Mongo-based resources to allow resources to be
|
5
|
+
# embedded within a document, while providing relationship-like `has 1`
|
6
|
+
# and `has n` functionality.
|
7
|
+
#
|
8
|
+
# @example
|
9
|
+
# class User
|
10
|
+
# include DataMapper::Mongo::Resource
|
11
|
+
#
|
12
|
+
# property :id, ObjectID
|
13
|
+
# property :name, String
|
14
|
+
#
|
15
|
+
# embeds n, :addresses, :model => Address
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# class Address
|
19
|
+
# include DataMapper::Mongo::EmbeddedResource
|
20
|
+
#
|
21
|
+
# property :street, String
|
22
|
+
# property :post_code, String
|
23
|
+
# end
|
24
|
+
#
|
25
|
+
module Embedment
|
26
|
+
extend Chainable
|
27
|
+
|
28
|
+
# Options which cannot be used on Embedments.
|
29
|
+
ILLEGAL_OPTS = [:through, :remote_name, :via, :inverse, :parent_key]
|
30
|
+
|
31
|
+
# @api private
|
32
|
+
def self.extended(model)
|
33
|
+
model.instance_variable_set(:@embedments, {})
|
34
|
+
end
|
35
|
+
|
36
|
+
chainable do
|
37
|
+
# @api private
|
38
|
+
def inherited(model)
|
39
|
+
model.instance_variable_set(:@embedments, {})
|
40
|
+
|
41
|
+
@embedments.each { |name, embedment| model.embedments[name] ||= embedment }
|
42
|
+
|
43
|
+
super
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
chainable do
|
48
|
+
# @api public
|
49
|
+
def method_missing(method, *args, &block)
|
50
|
+
if respond_to?(:embedments) && embedment = embedments[method]
|
51
|
+
return embedment
|
52
|
+
end
|
53
|
+
|
54
|
+
super
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# Returns the embedments on this model
|
59
|
+
#
|
60
|
+
# @return [Hash]
|
61
|
+
# Embedments on this model, where each hash key is the embedment
|
62
|
+
# name, and each value is the Embedments::Relationship instance.
|
63
|
+
#
|
64
|
+
# @api semipublic
|
65
|
+
def embedments
|
66
|
+
@embedments ||= {}
|
67
|
+
end
|
68
|
+
|
69
|
+
# A short-hand, clear syntax for defining one-to-one and one-to-many
|
70
|
+
# embedments -- where an embedded resource is held within the parent
|
71
|
+
# document in the database.
|
72
|
+
#
|
73
|
+
# @example
|
74
|
+
# embed 1, :friend # one friend
|
75
|
+
# embed n, :friends # many friends
|
76
|
+
# embed 1..3, :friends # many friends (at least 1, at most 3)
|
77
|
+
# embed 3, :friends # many friends (exactly 3)
|
78
|
+
# embed 1, :friend, 'User' # one friend with the class User
|
79
|
+
#
|
80
|
+
# @param cardinality [Integer, Range, Infinity]
|
81
|
+
# Cardinality that defines the embedment type and constraints
|
82
|
+
# @param name [Symbol]
|
83
|
+
# The name that the embedment will be referenced by
|
84
|
+
# @param model [Model, #to_str]
|
85
|
+
# The target model of the embedment
|
86
|
+
# @param opts [Hash]
|
87
|
+
# An options hash
|
88
|
+
#
|
89
|
+
# @option :model[Model, String] The name of the class to associate
|
90
|
+
# with, if omitted then the model class name is assumed to match the
|
91
|
+
# (optional) third parameter, or the embedment name.
|
92
|
+
#
|
93
|
+
# @return [Embedment::Relationship]
|
94
|
+
# The embedment that was created to reflect either a one-to-one or
|
95
|
+
# one-to-many embedment.
|
96
|
+
#
|
97
|
+
# @raise [ArgumentError]
|
98
|
+
# If the cardinality was not understood. Should be a Integer, Range
|
99
|
+
# or Infinity(n)
|
100
|
+
#
|
101
|
+
# @api public
|
102
|
+
def embeds(cardinality, name, *args)
|
103
|
+
assert_kind_of 'cardinality', cardinality, Integer, Range, Infinity.class
|
104
|
+
assert_kind_of 'name', name, Symbol
|
105
|
+
|
106
|
+
model = extract_model(args)
|
107
|
+
options = extract_options(args)
|
108
|
+
|
109
|
+
min, max = extract_min_max(cardinality)
|
110
|
+
options.update(:min => min, :max => max)
|
111
|
+
|
112
|
+
assert_valid_options(options)
|
113
|
+
|
114
|
+
if options.key?(:model) && model
|
115
|
+
raise ArgumentError, 'should not specify options[:model] if passing the model in the third argument'
|
116
|
+
end
|
117
|
+
|
118
|
+
model ||= options.delete(:model)
|
119
|
+
|
120
|
+
klass = if max > 1
|
121
|
+
Embedments::OneToMany::Relationship
|
122
|
+
else
|
123
|
+
Embedments::OneToOne::Relationship
|
124
|
+
end
|
125
|
+
|
126
|
+
embedment = embedments[name] = klass.new(name, model, self, options)
|
127
|
+
|
128
|
+
descendants.each do |descendant|
|
129
|
+
descendant.embedments[name] ||= embedment
|
130
|
+
end
|
131
|
+
|
132
|
+
create_embedment_reader(embedment)
|
133
|
+
create_embedment_writer(embedment)
|
134
|
+
|
135
|
+
embedment
|
136
|
+
end
|
137
|
+
|
138
|
+
# @todo Investigate as a candidate for removal.
|
139
|
+
# Added 26ae98e1 as an equivelent of belongs_to but _probably_ isn't
|
140
|
+
# of much use in embedded resources (since it would be perfectly
|
141
|
+
# acceptable for an embedment to be used in multiple models). My
|
142
|
+
# opinion is that embedments should always be declared from the
|
143
|
+
# parent side (DM::M::Resource), rather the child side
|
144
|
+
# (DM::M::EmbeddedResource).
|
145
|
+
#
|
146
|
+
# ~antw
|
147
|
+
#
|
148
|
+
# @api public
|
149
|
+
def embedded_in(name, *args)
|
150
|
+
return NotImplementedError
|
151
|
+
end
|
152
|
+
|
153
|
+
def assert_valid_options(options)
|
154
|
+
ILLEGAL_OPTS.each do |option|
|
155
|
+
raise ArgumentError, "+options[:#{option}]+ should not be " \
|
156
|
+
"specified on a relationship" if options.key?(option)
|
157
|
+
end
|
158
|
+
|
159
|
+
super
|
160
|
+
end
|
161
|
+
|
162
|
+
# Dynamically defines a reader method
|
163
|
+
#
|
164
|
+
# Creates a public method matching the name of the embedment which can
|
165
|
+
# be used to access the embedded resource(s).
|
166
|
+
#
|
167
|
+
# @param [Embedment::Relationship] embedment
|
168
|
+
# The embedment for which a reader should be created
|
169
|
+
#
|
170
|
+
# @api private
|
171
|
+
def create_embedment_reader(embedment)
|
172
|
+
name = embedment.name
|
173
|
+
reader_name = name.to_s
|
174
|
+
|
175
|
+
return if resource_method_defined?(reader_name)
|
176
|
+
|
177
|
+
reader_visibility = embedment.reader_visibility
|
178
|
+
|
179
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
180
|
+
#{reader_visibility} # public
|
181
|
+
def #{reader_name}(query = nil) # def author(query = nil)
|
182
|
+
embedments[#{name.inspect}].get(self, query) # embedment[:author].get(self, query)
|
183
|
+
end # end
|
184
|
+
RUBY
|
185
|
+
end
|
186
|
+
|
187
|
+
# Dynamically defines a writer method
|
188
|
+
#
|
189
|
+
# Creates a public method matching the name of the embedment which can
|
190
|
+
# be used to set the embedded resource(s).
|
191
|
+
#
|
192
|
+
# @param [Embedment::Relationship] embedment
|
193
|
+
# The embedment for which a writer should be created
|
194
|
+
#
|
195
|
+
# @api private
|
196
|
+
def create_embedment_writer(embedment)
|
197
|
+
name = embedment.name
|
198
|
+
writer_name = "#{name}="
|
199
|
+
|
200
|
+
return if resource_method_defined?(writer_name)
|
201
|
+
|
202
|
+
writer_visibility = embedment.writer_visibility
|
203
|
+
|
204
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
205
|
+
#{writer_visibility} # public
|
206
|
+
def #{writer_name}(target) # def author=(target)
|
207
|
+
embedments[#{name.inspect}].set(self, target) # embedment[:author].set(self, target)
|
208
|
+
end # end
|
209
|
+
RUBY
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
end # Model
|
214
|
+
end # Mongo
|
215
|
+
end # DataMapper
|
@@ -0,0 +1,85 @@
|
|
1
|
+
module DataMapper
|
2
|
+
module Mongo
|
3
|
+
module Modifier
|
4
|
+
# TODO: document
|
5
|
+
# @api public
|
6
|
+
def increment(property, value)
|
7
|
+
attribute_set(property, attribute_get(property) + value)
|
8
|
+
|
9
|
+
if modifier(:inc, property => value)
|
10
|
+
original_attributes.clear
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
# TODO: document
|
15
|
+
# @api public
|
16
|
+
def decrement(property, value)
|
17
|
+
attribute_set(property, attribute_get(property) - value)
|
18
|
+
|
19
|
+
if modifier(:inc, property => -value.abs)
|
20
|
+
original_attributes.clear
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# TODO: document
|
25
|
+
# @api public
|
26
|
+
def set(args)
|
27
|
+
modifier(:set, args)
|
28
|
+
|
29
|
+
args.keys.each do |key|
|
30
|
+
attribute_set(key, args[key])
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# TODO: document
|
35
|
+
# @api public
|
36
|
+
def unset(*args)
|
37
|
+
new_args = {}
|
38
|
+
|
39
|
+
args.each do |arg|
|
40
|
+
new_args[arg] = 1
|
41
|
+
end
|
42
|
+
|
43
|
+
modifier(:unset, new_args)
|
44
|
+
end
|
45
|
+
|
46
|
+
# TODO: document
|
47
|
+
# @api public
|
48
|
+
def push
|
49
|
+
|
50
|
+
end
|
51
|
+
|
52
|
+
# TODO: document
|
53
|
+
# @api public
|
54
|
+
def push_all
|
55
|
+
|
56
|
+
end
|
57
|
+
|
58
|
+
# TODO: document
|
59
|
+
# @api public
|
60
|
+
def pop
|
61
|
+
|
62
|
+
end
|
63
|
+
|
64
|
+
# TODO: document
|
65
|
+
# @api public
|
66
|
+
def pull
|
67
|
+
|
68
|
+
end
|
69
|
+
|
70
|
+
# TODO: document
|
71
|
+
# @api public
|
72
|
+
def pull_all
|
73
|
+
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
# TODO: document
|
79
|
+
# @api private
|
80
|
+
def modifier(operation, properties)
|
81
|
+
repository.adapter.execute([self], "$#{operation}" => properties)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,252 @@
|
|
1
|
+
module DataMapper
|
2
|
+
module Mongo
|
3
|
+
# This class is responsible for taking Query instances from DataMapper and
|
4
|
+
# formatting the query such that it can be performed by the Mongo library.
|
5
|
+
class Query
|
6
|
+
include Extlib::Assertions
|
7
|
+
include DataMapper::Query::Conditions
|
8
|
+
|
9
|
+
# Creates a new Query instance
|
10
|
+
#
|
11
|
+
# @param [Mongo::Collection] collection
|
12
|
+
# @param [DataMapper::Query] query
|
13
|
+
#
|
14
|
+
# @api semipublic
|
15
|
+
def initialize(collection, query)
|
16
|
+
assert_kind_of 'collection', collection, ::Mongo::Collection
|
17
|
+
assert_kind_of 'query', query, DataMapper::Query
|
18
|
+
|
19
|
+
@collection = collection
|
20
|
+
@query = query
|
21
|
+
@statements = {}
|
22
|
+
@conditions = Conditions.new(query.conditions)
|
23
|
+
end
|
24
|
+
|
25
|
+
# Applies the query to the collection
|
26
|
+
#
|
27
|
+
# Reads the query options, fetches the resources matching the query
|
28
|
+
# statements and filters according to the conditions.
|
29
|
+
#
|
30
|
+
# @return [Mongo::Collection]
|
31
|
+
#
|
32
|
+
# @api semipublic
|
33
|
+
def read
|
34
|
+
setup_conditions_and_options
|
35
|
+
find
|
36
|
+
end
|
37
|
+
|
38
|
+
# TODO: document
|
39
|
+
# @api semipublic
|
40
|
+
def count
|
41
|
+
setup_conditions_and_options
|
42
|
+
|
43
|
+
# TODO: atm ruby driver doesn't support count with statements,
|
44
|
+
# that's why we use find and size here as a tmp workaround
|
45
|
+
if @statements.blank?
|
46
|
+
[@collection.count]
|
47
|
+
else
|
48
|
+
[find.size]
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# TODO: document
|
53
|
+
# @api semipublic
|
54
|
+
def group
|
55
|
+
setup_conditions_and_options
|
56
|
+
|
57
|
+
property_names = []
|
58
|
+
operators = []
|
59
|
+
keys = []
|
60
|
+
|
61
|
+
@query.fields.each do |field|
|
62
|
+
if field.kind_of?(DataMapper::Query::Operator)
|
63
|
+
operators << field
|
64
|
+
property_names << field.target.name
|
65
|
+
else
|
66
|
+
keys << field.name
|
67
|
+
property_names << field.name
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
if operators.blank?
|
72
|
+
initial = {}
|
73
|
+
reduce = JavaScript::Reduce.new.to_s
|
74
|
+
finalize = nil
|
75
|
+
else
|
76
|
+
js_operation = JavaScript::Operation.new(operators)
|
77
|
+
|
78
|
+
initial = js_operation.initial
|
79
|
+
reduce = js_operation.reduce
|
80
|
+
finalize = js_operation.finalize
|
81
|
+
|
82
|
+
keys = keys - initial.keys
|
83
|
+
end
|
84
|
+
|
85
|
+
@collection.group(keys, @statements, initial, reduce, true, finalize).map do |records|
|
86
|
+
records.to_mash.symbolize_keys.only(*property_names)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
# TODO: document
|
93
|
+
# @api private
|
94
|
+
def setup_conditions_and_options
|
95
|
+
@options = {}
|
96
|
+
|
97
|
+
@options[:limit] = @query.limit if @query.limit
|
98
|
+
@options[:skip] = @query.offset if @query.offset
|
99
|
+
@options[:sort] = sort_statement(@query.order) unless @query.order.empty?
|
100
|
+
|
101
|
+
conditions_statement(@query.conditions)
|
102
|
+
end
|
103
|
+
|
104
|
+
# TODO: document
|
105
|
+
# @api private
|
106
|
+
def find
|
107
|
+
@conditions.filter_collection!(@collection.find(@statements, @options).to_a)
|
108
|
+
end
|
109
|
+
|
110
|
+
# Takes a condition and returns a Mongo-compatible hash
|
111
|
+
#
|
112
|
+
# @param [DataMapper::Query::Conditions::AbstractOperation, DataMapper::Query::Conditions::AbstractComparison] operation
|
113
|
+
# An operation to be made suitable for use with Mongo
|
114
|
+
# @param [Boolean] affirmative
|
115
|
+
# Are we looking for resources which match the condition (true), or
|
116
|
+
# those which do not match (false)
|
117
|
+
#
|
118
|
+
# @return [Hash]
|
119
|
+
#
|
120
|
+
# @api private
|
121
|
+
def conditions_statement(conditions, affirmative = true)
|
122
|
+
case conditions
|
123
|
+
when AbstractOperation then operation_statement(conditions, affirmative)
|
124
|
+
when AbstractComparison then comparison_statement(conditions, affirmative)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
# Takes a Operation condition and returns a Mongo-compatible hash
|
129
|
+
#
|
130
|
+
# @param [DataMapper::Query::Conditions::Operation] operation
|
131
|
+
# An operation to be made suitable for use with Mongo
|
132
|
+
# @param [Boolean] affirmative
|
133
|
+
# Are we looking for resources which match the condition (true), or
|
134
|
+
# those which do not match (false)
|
135
|
+
#
|
136
|
+
# @return [Hash]
|
137
|
+
#
|
138
|
+
# @api private
|
139
|
+
def operation_statement(operation, affirmative = true)
|
140
|
+
case operation
|
141
|
+
when NotOperation then conditions_statement(operation.first, !affirmative)
|
142
|
+
when AndOperation then operation.each{|op| conditions_statement(op, affirmative)}
|
143
|
+
when OrOperation then operation.each{|op| conditions_statement(op, affirmative)}
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
# TODO: document
|
148
|
+
# @api private
|
149
|
+
def comparison_statement_for_embedment(comparison, affirmative = true)
|
150
|
+
embedment = @query.model.embedments.values.detect { |e| e.target_model == comparison.subject.model }
|
151
|
+
|
152
|
+
field = "#{embedment.name}.#{comparison.subject.field}"
|
153
|
+
|
154
|
+
update_statements(comparison, field, affirmative)
|
155
|
+
end
|
156
|
+
|
157
|
+
# Takes a Comparison condition and returns a Mongo-compatible hash
|
158
|
+
#
|
159
|
+
# @param [DataMapper::Query::Conditions::Comparison] comparison
|
160
|
+
# An comparison to be made suitable for use with Mongo
|
161
|
+
# @param [Boolean] affirmative
|
162
|
+
# Are we looking for resources which match the condition (true), or
|
163
|
+
# those which do not match (false)
|
164
|
+
#
|
165
|
+
# @return [Hash]
|
166
|
+
#
|
167
|
+
# @api private
|
168
|
+
def comparison_statement(comparison, affirmative = true)
|
169
|
+
if comparison.relationship?
|
170
|
+
return conditions_statement(comparison.foreign_key_mapping, affirmative)
|
171
|
+
end
|
172
|
+
|
173
|
+
if comparison.subject.model && comparison.subject.model < EmbeddedResource
|
174
|
+
return comparison_statement_for_embedment(comparison, affirmative)
|
175
|
+
end
|
176
|
+
|
177
|
+
update_statements(comparison, comparison.subject.field, affirmative)
|
178
|
+
end
|
179
|
+
|
180
|
+
# TODO: document
|
181
|
+
# @api private
|
182
|
+
def update_statements(comparison, field, affirmative = true)
|
183
|
+
value = comparison.value
|
184
|
+
|
185
|
+
operator = if affirmative
|
186
|
+
case comparison
|
187
|
+
when EqualToComparison then value
|
188
|
+
when GreaterThanComparison then {'$gt' => value}
|
189
|
+
when LessThanComparison then {'$lt' => value}
|
190
|
+
when GreaterThanOrEqualToComparison then {'$gte' => value}
|
191
|
+
when LessThanOrEqualToComparison then {'$lte' => value}
|
192
|
+
when InclusionComparison then inclusion_comparison_operator(comparison, value)
|
193
|
+
when RegexpComparison then value
|
194
|
+
when LikeComparison then comparison.send(:expected)
|
195
|
+
else
|
196
|
+
raise NotImplementedError
|
197
|
+
end
|
198
|
+
else
|
199
|
+
case comparison
|
200
|
+
when EqualToComparison then {'$ne' => value}
|
201
|
+
when InclusionComparison then {'$nin' => value}
|
202
|
+
else
|
203
|
+
raise NotImplementedError
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
operator.is_a?(Hash) ?
|
208
|
+
(@statements[field.to_sym] ||= {}).merge!(operator) : @statements[field.to_sym] = operator
|
209
|
+
end
|
210
|
+
|
211
|
+
# Creates Mongo's equivalent of an IN() condition
|
212
|
+
#
|
213
|
+
# @param [DataMapper::Query::Conditions::Comparison] comparison
|
214
|
+
# An comparison to be made suitable for use with Mongo
|
215
|
+
# @param [Object] value
|
216
|
+
# The value to match against.
|
217
|
+
#
|
218
|
+
# @return [Hash]
|
219
|
+
#
|
220
|
+
# @api private
|
221
|
+
def inclusion_comparison_operator(comparison, value)
|
222
|
+
if value.kind_of?(Range)
|
223
|
+
{'$gte' => value.first, value.exclude_end? ? '$lt' : '$lte' => value.last}
|
224
|
+
elsif comparison.kind_of?(InclusionComparison) && value.size == 1
|
225
|
+
value.first
|
226
|
+
elsif comparison.subject.type == DataMapper::Mongo::Types::EmbeddedArray
|
227
|
+
value
|
228
|
+
else
|
229
|
+
{'$in' => value}
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
# Constructs a sort statement which can be used by the Mongo library
|
234
|
+
#
|
235
|
+
# Mongo::Collection#find requires that sort statements consist of an
|
236
|
+
# array where each element is another array containing the field name
|
237
|
+
# and the sort order.
|
238
|
+
#
|
239
|
+
# @param [Enumerable<DataMapper::Query::Direction>]
|
240
|
+
#
|
241
|
+
# @return [Array<Array>]
|
242
|
+
#
|
243
|
+
# @api private
|
244
|
+
def sort_statement(conditions)
|
245
|
+
conditions.inject([]) do |sort_arr, condition|
|
246
|
+
sort_arr << [condition.target.field, condition.operator == :asc ? 'ascending' : 'descending']
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
end # Query
|
251
|
+
end # Mongo
|
252
|
+
end # DataMapper
|