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.
Files changed (65) hide show
  1. data/.gitignore +9 -0
  2. data/LICENSE +20 -0
  3. data/README.rdoc +130 -0
  4. data/Rakefile +36 -0
  5. data/TODO +33 -0
  6. data/VERSION.yml +5 -0
  7. data/bin/console +31 -0
  8. data/dm-mongo-adapter.gemspec +154 -0
  9. data/lib/mongo_adapter.rb +71 -0
  10. data/lib/mongo_adapter/adapter.rb +255 -0
  11. data/lib/mongo_adapter/aggregates.rb +21 -0
  12. data/lib/mongo_adapter/conditions.rb +100 -0
  13. data/lib/mongo_adapter/embedded_model.rb +187 -0
  14. data/lib/mongo_adapter/embedded_resource.rb +134 -0
  15. data/lib/mongo_adapter/embedments/one_to_many.rb +139 -0
  16. data/lib/mongo_adapter/embedments/one_to_one.rb +53 -0
  17. data/lib/mongo_adapter/embedments/relationship.rb +258 -0
  18. data/lib/mongo_adapter/migrations.rb +45 -0
  19. data/lib/mongo_adapter/model.rb +89 -0
  20. data/lib/mongo_adapter/model/embedment.rb +215 -0
  21. data/lib/mongo_adapter/modifier.rb +85 -0
  22. data/lib/mongo_adapter/query.rb +252 -0
  23. data/lib/mongo_adapter/query/java_script.rb +145 -0
  24. data/lib/mongo_adapter/resource.rb +147 -0
  25. data/lib/mongo_adapter/types/date.rb +28 -0
  26. data/lib/mongo_adapter/types/date_time.rb +28 -0
  27. data/lib/mongo_adapter/types/db_ref.rb +65 -0
  28. data/lib/mongo_adapter/types/discriminator.rb +32 -0
  29. data/lib/mongo_adapter/types/object_id.rb +72 -0
  30. data/lib/mongo_adapter/types/objects.rb +31 -0
  31. data/script/performance.rb +260 -0
  32. data/spec/legacy/README +6 -0
  33. data/spec/legacy/adapter_shared_spec.rb +299 -0
  34. data/spec/legacy/adapter_spec.rb +174 -0
  35. data/spec/legacy/associations_spec.rb +140 -0
  36. data/spec/legacy/embedded_resource_spec.rb +157 -0
  37. data/spec/legacy/embedments_spec.rb +177 -0
  38. data/spec/legacy/modifier_spec.rb +81 -0
  39. data/spec/legacy/property_spec.rb +51 -0
  40. data/spec/legacy/spec_helper.rb +3 -0
  41. data/spec/legacy/sti_spec.rb +53 -0
  42. data/spec/lib/cleanup_models.rb +32 -0
  43. data/spec/lib/raw_connections.rb +11 -0
  44. data/spec/public/embedded_collection_spec.rb +61 -0
  45. data/spec/public/embedded_resource_spec.rb +220 -0
  46. data/spec/public/model/embedment_spec.rb +186 -0
  47. data/spec/public/model_spec.rb +37 -0
  48. data/spec/public/resource_spec.rb +564 -0
  49. data/spec/public/shared/model_embedments_spec.rb +338 -0
  50. data/spec/public/shared/object_id_shared_spec.rb +56 -0
  51. data/spec/public/types/df_ref_spec.rb +6 -0
  52. data/spec/public/types/discriminator_spec.rb +118 -0
  53. data/spec/public/types/embedded_array_spec.rb +55 -0
  54. data/spec/public/types/embedded_hash_spec.rb +83 -0
  55. data/spec/public/types/object_id_spec.rb +6 -0
  56. data/spec/rcov.opts +6 -0
  57. data/spec/semipublic/embedded_model_spec.rb +43 -0
  58. data/spec/semipublic/model/embedment_spec.rb +42 -0
  59. data/spec/semipublic/resource_spec.rb +70 -0
  60. data/spec/spec.opts +4 -0
  61. data/spec/spec_helper.rb +45 -0
  62. data/tasks/spec.rake +37 -0
  63. data/tasks/yard.rake +9 -0
  64. data/tasks/yardstick.rake +21 -0
  65. metadata +215 -0
@@ -0,0 +1,255 @@
1
+ module DataMapper
2
+ module Mongo
3
+ class Adapter < DataMapper::Adapters::AbstractAdapter
4
+ class ConnectionError < StandardError; end
5
+
6
+ # Persists one or more new resources
7
+ #
8
+ # @example
9
+ # adapter.create(collection) # => 1
10
+ #
11
+ # @param [Enumerable<Resource>] resources
12
+ # The list of resources (model instances) to create
13
+ #
14
+ # @return [Integer]
15
+ # The number of records that were actually saved into the data-store
16
+ #
17
+ # @api semipublic
18
+ def create(resources)
19
+ resources.map do |resource|
20
+ with_collection(resource.model) do |collection|
21
+ resource.model.key.set(resource, [collection.insert(attributes_as_fields(resource))])
22
+ end
23
+ end.size
24
+ end
25
+
26
+ # Reads one or many resources from a datastore
27
+ #
28
+ # @example
29
+ # adapter.read(query) # => [ { 'name' => 'Dan Kubb' } ]
30
+ #
31
+ # @param [Query] query
32
+ # The query to match resources in the datastore
33
+ #
34
+ # @return [Enumerable<Hash>]
35
+ # An array of hashes to become resources
36
+ #
37
+ # @api semipublic
38
+ def read(query)
39
+ with_collection(query.model) do |collection|
40
+ Query.new(collection, query).read
41
+ end
42
+ end
43
+
44
+ # Updates one or many existing resources
45
+ #
46
+ # @example
47
+ # adapter.update(attributes, collection) # => 1
48
+ #
49
+ # @param [Hash(Property => Object)] attributes
50
+ # Hash of attribute values to set, keyed by Property
51
+ # @param [Collection] resources
52
+ # Collection of records to be updated
53
+ #
54
+ # @return [Integer]
55
+ # The number of records updated
56
+ #
57
+ # @api semipublic
58
+ def update(attributes, resources)
59
+ with_collection(resources.query.model) do |collection|
60
+ resources.each do |resource|
61
+ collection.update(key(resource),
62
+ attributes_as_fields(resource).merge(attributes_as_fields(attributes)))
63
+ end.size
64
+ end
65
+ end
66
+
67
+ # Deletes one or many existing resources
68
+ #
69
+ # @example
70
+ # adapter.delete(collection) # => 1
71
+ #
72
+ # @param [Collection] resources
73
+ # Collection of records to be deleted
74
+ #
75
+ # @return [Integer]
76
+ # The number of records deleted
77
+ #
78
+ # @api semipublic
79
+ def delete(resources)
80
+ with_collection(resources.query.model) do |collection|
81
+ resources.each do |resource|
82
+ collection.remove(key(resource))
83
+ end.size
84
+ end
85
+ end
86
+
87
+ # TODO: document
88
+ # @api semipublic
89
+ def execute(resources, document, options={})
90
+ resources.map do |resource|
91
+ with_collection(resource.model) do |collection|
92
+ collection.update(key(resource), document, options)
93
+ end
94
+ end.size
95
+ end
96
+
97
+ private
98
+
99
+ def initialize(name, options = {})
100
+ # When giving a repository URI rather than a hash, the database name
101
+ # is :path, with a leading slash.
102
+ if options[:path] && options[:database].nil?
103
+ options[:database] = options[:path].sub(/^\//, '')
104
+ end
105
+
106
+ super
107
+ end
108
+
109
+ # Retrieves the key for a given resource as a hash.
110
+ #
111
+ # @param [Resource] resource
112
+ # The resource whose key is to be retrieved
113
+ #
114
+ # @return [Hash{Symbol => Object}]
115
+ # Returns a hash where each hash key/value corresponds to a key name
116
+ # and value on the resource.
117
+ #
118
+ # @api private
119
+ def key(resource)
120
+ resource.model.key(name).map{ |key| [key.field, key.value(resource.__send__(key.name))] }.to_hash
121
+ end
122
+
123
+ # Retrieves all of a records attributes and returns them as a Hash.
124
+ #
125
+ # The resulting hash can then be used with the Mongo library for
126
+ # inserting new -- and updating existing -- documents in the database.
127
+ #
128
+ # @param [Resource, Hash] record
129
+ # A DataMapper resource, or a hash containing fields and values.
130
+ #
131
+ # @return [Hash]
132
+ # Returns a hash containing the values for each of the fields in the
133
+ # given resource as raw (dumped) values suitable for use with the
134
+ # Mongo library.
135
+ #
136
+ # @api private
137
+ def attributes_as_fields(record)
138
+ attributes = case record
139
+ when DataMapper::Resource
140
+ attributes_from_resource(record)
141
+ when Hash
142
+ attributes_from_properties_hash(record)
143
+ end
144
+
145
+ attributes.except('_id') unless attributes.nil?
146
+ end
147
+
148
+ # TODO: document
149
+ def attributes_from_resource(record)
150
+ attributes = {}
151
+
152
+ model = record.model
153
+
154
+ model.properties.each do |property|
155
+ name = property.name
156
+ if model.public_method_defined?(name)
157
+ value = record.__send__(name)
158
+ attributes[property.field] = property.custom? ?
159
+ property.type.dump(value, property) : value
160
+ end
161
+ end
162
+
163
+ if model.respond_to?(:embedments)
164
+ model.embedments.each do |name, embedment|
165
+ if model.public_method_defined?(name)
166
+ value = record.__send__(name)
167
+
168
+ if embedment.kind_of?(Embedments::OneToMany::Relationship)
169
+ attributes[name] = value.map{ |resource| attributes_as_fields(resource) }
170
+ else
171
+ attributes[name] = attributes_as_fields(value)
172
+ end
173
+ end
174
+ end
175
+ end
176
+
177
+ attributes
178
+ end
179
+
180
+ # TODO: document
181
+ def attributes_from_properties_hash(record)
182
+ attributes = {}
183
+
184
+ record.each do |key, value|
185
+ case key
186
+ when DataMapper::Property
187
+ attributes[key.field] = key.custom? ? key.type.dump(value, key) : value
188
+ when Embedments::Relationship
189
+ attributes[key.name] = attributes_as_fields(value)
190
+ end
191
+ end
192
+
193
+ attributes
194
+ end
195
+
196
+ # Runs the given block within the context of a Mongo collection.
197
+ #
198
+ # @param [Model] model
199
+ # The model whose collection is to be scoped.
200
+ #
201
+ # @yieldparam [Mongo::Collection]
202
+ # The Mongo::Collection instance for the given model
203
+ #
204
+ # @api private
205
+ def with_collection(model)
206
+ begin
207
+ yield database.collection(model.storage_name(name))
208
+ rescue Exception => exception
209
+ DataMapper.logger.error(exception.to_s)
210
+ raise exception
211
+ end
212
+ end
213
+
214
+ # Returns the Mongo::DB instance for this process.
215
+ #
216
+ # @return [Mongo::DB]
217
+ #
218
+ # @raise [ConnectionError]
219
+ # If the database requires you to authenticate, and the given username
220
+ # or password was not correct, a ConnectionError exception will be
221
+ # raised.
222
+ #
223
+ # @api semipublic
224
+ def database
225
+ unless defined?(@database)
226
+ @database = connection.db(@options[:database])
227
+
228
+ if @options[:username] and not @database.authenticate(
229
+ @options[:username], @options[:password])
230
+ raise ConnectionError,
231
+ 'MongoDB did not recognize the given username and/or ' \
232
+ 'password; see the server logs for more information'
233
+ end
234
+ end
235
+
236
+ @database
237
+ end
238
+
239
+ # Returns the Mongo::Connection instance for this process
240
+ #
241
+ # @todo Reconnect if the connection has timed out, or if the process has
242
+ # been forked.
243
+ #
244
+ # @return [Mongo::Connection]
245
+ #
246
+ # @api semipublic
247
+ def connection
248
+ @connection ||= ::Mongo::Connection.new(*@options.values_at(:host, :port))
249
+ end
250
+ end # Adapter
251
+ end # Mongo
252
+
253
+ Adapters::MongoAdapter = DataMapper::Mongo::Adapter
254
+ Adapters.const_added(:MongoAdapter)
255
+ end # DataMapper
@@ -0,0 +1,21 @@
1
+ module DataMapper
2
+ module Mongo
3
+ module Aggregates
4
+ # TODO: document
5
+ # @api semipublic
6
+ def aggregate(query)
7
+ operator = if query.fields.size == 1 && query.fields.first.target == :all
8
+ :count
9
+ else
10
+ :group
11
+ end
12
+
13
+ with_collection(query.model) do |collection|
14
+ Query.new(collection, query).send(operator)
15
+ end
16
+ end
17
+ end # class Aggregates
18
+ end # module Mongo
19
+
20
+ Aggregates::MongoAdapter = DataMapper::Mongo::Aggregates
21
+ end # module DataMapper
@@ -0,0 +1,100 @@
1
+ module DataMapper
2
+ module Mongo
3
+ # Used to filter records from Mongo::Collection according to the
4
+ # conditions defined in a DataMapper::Query. Warns when attempting to use
5
+ # operations which aren't supported by MongoDB.
6
+ class Conditions
7
+ include DataMapper::Query::Conditions
8
+
9
+ # Returns the query operation
10
+ #
11
+ # @return [DataMapper::Query::AbstractOperation]
12
+ # The operation which will be used to filter the collection.
13
+ #
14
+ # @api semipublic
15
+ attr_reader :operation
16
+
17
+ # Creates a new Conditions instance
18
+ #
19
+ # @param [DataMapper::Query::AbstractOperation] query_operation
20
+ # The top-level operation from DataMapper::Query#conditions
21
+ #
22
+ # @api private
23
+ def initialize(query_operation)
24
+ @operation = verify_operation(query_operation)
25
+ end
26
+
27
+ # Filters a collection according to the Conditions
28
+ #
29
+ # @param [Enumerable<Hash>] collection
30
+ # A collection of hashes which correspond to resource values
31
+ #
32
+ # @return [Enumerable<Hash>]
33
+ # Returns the collection without modification if the condition has no
34
+ # operations, otherwise it returns a copy of the collection with only
35
+ # the matching records.
36
+ #
37
+ # @api private
38
+ def filter_collection!(collection)
39
+ @operation.operands.empty? ? collection : collection.delete_if {|record| !@operation.matches?(record)}
40
+ end
41
+
42
+ private
43
+
44
+ # Returns a copy of the operation, removing _from the original_ any
45
+ # operands which are incompatible with MongoDB.
46
+ #
47
+ # @param [DataMapper::Query::AbstractOperation] query_operation
48
+ # The top-level operation from DataMapper::Query#conditions
49
+ #
50
+ # @return [DataMapper::Query::AbstractOperation]
51
+ #
52
+ # @api private
53
+ def verify_operation(query_operation)
54
+ operation = query_operation.dup.clear
55
+
56
+ query_operation.each do |operand|
57
+ if not_supported?(operand)
58
+ query_operation.operands.delete(operand)
59
+ operation << operand
60
+ elsif operand.kind_of?(AbstractOperation)
61
+ operation << verify_operation(operand)
62
+ end
63
+ end
64
+
65
+ operation
66
+ end
67
+
68
+ # Checks if a given operand is supported by MongoDB.
69
+ #
70
+ # Comparisons not current supported are:
71
+ #
72
+ # * $nin with range
73
+ # * negated regexp comparison (see: http://jira.mongodb.org/browse/SERVER-251)
74
+ #
75
+ # @param [DataMapper::Query::Conditions::AbstractOperation, DataMapper::Query::Conditions::AbstractComparison] operand
76
+ # An operation to be made suitable for use with Mongo
77
+ #
78
+ # @return [Boolean]
79
+ #
80
+ # @api private
81
+ def not_supported?(operand)
82
+ case operand
83
+ when OrOperation
84
+ true
85
+ when RegexpComparison
86
+ if operand.negated?
87
+ true
88
+ end
89
+ when InclusionComparison
90
+ if operand.negated?
91
+ true
92
+ end
93
+ else
94
+ false
95
+ end
96
+ end
97
+
98
+ end # Conditions
99
+ end # Mongo
100
+ end # DataMapper
@@ -0,0 +1,187 @@
1
+ module DataMapper
2
+ module Mongo
3
+ module EmbeddedModel
4
+ extend Chainable
5
+ include DataMapper::Model
6
+
7
+ # Creates a new Model class with default_storage_name +storage_name+
8
+ #
9
+ # If a block is passed, it will be eval'd in the context of the new Model
10
+ #
11
+ # @param [Proc] block
12
+ # a block that will be eval'd in the context of the new Model class
13
+ #
14
+ # @return [Model]
15
+ # the newly created Model class
16
+ #
17
+ # @api semipublic
18
+ def self.new(&block)
19
+ model = Class.new
20
+
21
+ model.class_eval <<-RUBY, __FILE__, __LINE__ + 1
22
+ include DataMapper::Mongo::EmbeddedResource
23
+
24
+ def self.name
25
+ to_s
26
+ end
27
+ RUBY
28
+
29
+ model.instance_eval(&block) if block
30
+ model
31
+ end
32
+
33
+ # Methods copied from DataMapper::Model
34
+
35
+ # Return all models that extend the Model module
36
+ #
37
+ # class Foo
38
+ # include DataMapper::Resource
39
+ # end
40
+ #
41
+ # DataMapper::Model.descendants.first #=> Foo
42
+ #
43
+ # @return [DescendantSet]
44
+ # Set containing the descendant models
45
+ #
46
+ # @api semipublic
47
+ def self.descendants
48
+ @descendants ||= DescendantSet.new
49
+ end
50
+
51
+ # Return all models that inherit from a Model
52
+ #
53
+ # class Foo
54
+ # include DataMapper::Resource
55
+ # end
56
+ #
57
+ # class Bar < Foo
58
+ # end
59
+ #
60
+ # Foo.descendants.first #=> Bar
61
+ #
62
+ # @return [Set]
63
+ # Set containing the descendant classes
64
+ #
65
+ # @api semipublic
66
+ attr_reader :descendants
67
+
68
+ # Appends a module for inclusion into the model class after Resource.
69
+ #
70
+ # This is a useful way to extend Resource while still retaining a
71
+ # self.included method.
72
+ #
73
+ # @param [Module] inclusions
74
+ # the module that is to be appended to the module after Resource
75
+ #
76
+ # @return [Boolean]
77
+ # true if the inclusions have been successfully appended to the list
78
+ #
79
+ # @api semipublic
80
+ def self.append_inclusions(*inclusions)
81
+ extra_inclusions.concat inclusions
82
+
83
+ # Add the inclusion to existing descendants
84
+ descendants.each do |model|
85
+ inclusions.each { |inclusion| model.send :include, inclusion }
86
+ end
87
+
88
+ true
89
+ end
90
+
91
+ # The current registered extra inclusions
92
+ #
93
+ # @return [Set]
94
+ #
95
+ # @api private
96
+ def self.extra_inclusions
97
+ @extra_inclusions ||= []
98
+ end
99
+
100
+ # Extends the model with this module after Resource has been included.
101
+ #
102
+ # This is a useful way to extend Model while still retaining a self.extended method.
103
+ #
104
+ # @param [Module] extensions
105
+ # List of modules that will extend the model after it is extended by Model
106
+ #
107
+ # @return [Boolean]
108
+ # whether or not the inclusions have been successfully appended to the list
109
+ #
110
+ # @api semipublic
111
+ def self.append_extensions(*extensions)
112
+ extra_extensions.concat extensions
113
+
114
+ # Add the extension to existing descendants
115
+ descendants.each do |model|
116
+ extensions.each { |extension| model.extend(extension) }
117
+ end
118
+
119
+ true
120
+ end
121
+
122
+ # The current registered extra extensions
123
+ #
124
+ # @return [Set]
125
+ #
126
+ # @api private
127
+ def self.extra_extensions
128
+ @extra_extensions ||= []
129
+ end
130
+
131
+ # @api private
132
+ def self.extended(model)
133
+ descendants = self.descendants
134
+
135
+ descendants << model
136
+
137
+ model.instance_variable_set(:@valid, false)
138
+ model.instance_variable_set(:@base_model, model)
139
+ model.instance_variable_set(:@storage_names, {})
140
+ model.instance_variable_set(:@default_order, {})
141
+ model.instance_variable_set(:@descendants, descendants.class.new(model, descendants))
142
+
143
+ extra_extensions.each { |mod| model.extend(mod) }
144
+ extra_inclusions.each { |mod| model.send(:include, mod) }
145
+ end
146
+
147
+ # @api private
148
+ chainable do
149
+ def inherited(model)
150
+ descendants = self.descendants
151
+
152
+ descendants << model
153
+
154
+ model.instance_variable_set(:@valid, false)
155
+ model.instance_variable_set(:@base_model, base_model)
156
+ model.instance_variable_set(:@storage_names, @storage_names.dup)
157
+ model.instance_variable_set(:@default_order, @default_order.dup)
158
+ model.instance_variable_set(:@descendants, descendants.class.new(model, descendants))
159
+
160
+ # TODO: move this into dm-validations
161
+ if respond_to?(:validators)
162
+ validators.contexts.each do |context, validators|
163
+ model.validators.context(context).concat(validators)
164
+ end
165
+ end
166
+ end
167
+ end
168
+
169
+ # end of DataMapper::Model methods
170
+
171
+ append_extensions DataMapper::Model::Hook
172
+ append_extensions DataMapper::Model::Property
173
+ append_extensions DataMapper::Model::Relationship
174
+
175
+ # @overrides DataMapper::Model#assert_valid
176
+ def assert_valid
177
+ return if @valid
178
+ @valid = true
179
+
180
+ if properties.empty?
181
+ raise IncompleteModelError, "#{self.name} must have at least one property to be valid"
182
+ end
183
+ end
184
+
185
+ end # EmbeddedModel
186
+ end # Mongo
187
+ end # DataMapper