dm-mongo-adapter 0.2.0.pre1

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