mongo_record 0.4.3 → 0.4.4

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc CHANGED
@@ -83,3 +83,13 @@ Jim Mulholland, jim at squeejee dot com
83
83
 
84
84
  Clinton R. Nixon, crnixon at gmail dot com
85
85
  * Ability to define and query indexes from models
86
+
87
+ Nate Wiger, http://github.com/nateware
88
+ * Optimization to first and last to close cursor and avoid expensive to_a
89
+ * Implemented Model.update_all leveraging Mongo collection.update
90
+ * Scoped dynamic finders to each instance, so rows with varying attributes work
91
+ * Added row.attributes helper to enable use of ActiveRecord::Callbacks if desired
92
+
93
+
94
+
95
+
@@ -18,6 +18,7 @@ require 'mongo/types/code'
18
18
  require 'mongo/cursor'
19
19
  require 'mongo_record/convert'
20
20
  require 'mongo_record/sql'
21
+ #require 'active_support/core_ext' # symbolize_keys!
21
22
 
22
23
  class String
23
24
  # Convert this String to an ObjectID.
@@ -110,6 +111,7 @@ module MongoRecord
110
111
  subclass.instance_variable_set("@field_names", []) # array of scalars names (symbols)
111
112
  subclass.instance_variable_set("@subobjects", {}) # key = name (symbol), value = class
112
113
  subclass.instance_variable_set("@arrays", {}) # key = name (symbol), value = class
114
+ subclass.field(:_id, :_ns)
113
115
  end
114
116
 
115
117
  # Call this method to set the Mongo collection name for this class.
@@ -117,7 +119,6 @@ module MongoRecord
117
119
  # lower_case_with_underscores.
118
120
  def collection_name(coll_name)
119
121
  @coll_name = coll_name
120
- field(:_id, :_ns)
121
122
  end
122
123
 
123
124
  # Creates one or more collection fields. Each field will be saved to
@@ -130,6 +131,7 @@ module MongoRecord
130
131
  field = field.to_sym
131
132
  unless @field_names.include?(field)
132
133
  ivar_name = "@" + field.to_s
134
+ # this is better than lambda because it's only eval'ed once
133
135
  define_method(field, lambda { instance_variable_get(ivar_name) })
134
136
  define_method("#{field}=".to_sym, lambda { |val| instance_variable_set(ivar_name, val) })
135
137
  define_method("#{field}?".to_sym, lambda {
@@ -162,7 +164,7 @@ module MongoRecord
162
164
  end
163
165
 
164
166
  fields = fields.map do |field|
165
- field = field.respond_to?(:[]) ? field : [field, :asc]
167
+ field = field.is_a?(Array) ? field : [field, :asc]
166
168
  field[1] = (field[1] == :desc) ? Mongo::DESCENDING : Mongo::ASCENDING
167
169
  field
168
170
  end
@@ -395,7 +397,7 @@ module MongoRecord
395
397
  end
396
398
 
397
399
  def sum(column)
398
- x = self.find(:all, :select=>column)
400
+ x = all(:select => column)
399
401
  x.map {|p1| p1[column.to_sym]}.compact.inject(0) { |s,v| s += v }
400
402
  end
401
403
 
@@ -410,17 +412,24 @@ module MongoRecord
410
412
  id.is_a?(Array) ? id.each { |oid| destroy(oid) } : find(id).destroy
411
413
  end
412
414
 
413
- # Not yet implemented.
414
- def update_all(updates, conditions = nil)
415
- # TODO
416
- raise "not yet implemented"
415
+ # This updates all records matching the specified criteria. It leverages the
416
+ # db.update call from the Mongo core API to guarantee atomicity. You can
417
+ # specify either a hash for simplicity, or full Mongo API operators to the
418
+ # update part of the method call:
419
+ #
420
+ # Person.update_all({:name => 'Bob'}, {:name => 'Fred'})
421
+ # Person.update_all({'$set' => {:name => 'Bob'}, '$inc' => {:age => 1}}, {:name => 'Fred'})
422
+ def update_all(updates, conditions = nil, options = {})
423
+ all(:conditions => conditions).each do |row|
424
+ collection.update(criteria_from(conditions).merge(:_id => row.id.to_oid), update_fields_from(updates), options)
425
+ end
417
426
  end
418
427
 
419
428
  # Destroy all objects that match +conditions+. Warning: if
420
429
  # +conditions+ is +nil+, all records in the collection will be
421
430
  # destroyed.
422
431
  def destroy_all(conditions = nil)
423
- find(:all, :conditions => conditions).each { |object| object.destroy }
432
+ all(:conditions => conditions).each { |object| object.destroy }
424
433
  end
425
434
 
426
435
  # Deletes all records that match +condition+, which can be a
@@ -451,13 +460,13 @@ module MongoRecord
451
460
  # Example of updating multiple records:
452
461
  # people = { 1 => { "first_name" => "David" }, 2 => { "first_name" => "Jeremy"} }
453
462
  # Person.update(people.keys, people.values)
454
- def update(id, attributes)
463
+ def update(id, attrib)
455
464
  if id.is_a?(Array)
456
465
  i = -1
457
- id.collect { |id| i += 1; update(id, attributes[i]) }
466
+ id.collect { |id| i += 1; update(id, attrib[i]) }
458
467
  else
459
468
  object = find(id)
460
- object.update_attributes(attributes)
469
+ object.update_attributes(attrib)
461
470
  object
462
471
  end
463
472
  end
@@ -502,15 +511,21 @@ module MongoRecord
502
511
  def find_initial(options)
503
512
  options[:limit] = 1
504
513
  options[:order] = 'created_at asc'
505
- row = find_every(options)
506
- row.to_a[0]
514
+ find_one(options)
507
515
  end
508
516
 
509
517
  def find_last(options)
510
518
  options[:limit] = 1
511
519
  options[:order] = 'created_at desc'
512
- row = find_every(options)
513
- row.to_a[0]
520
+ find_one(options)
521
+ end
522
+
523
+ def find_one(options)
524
+ one = nil
525
+ cursor = find_every(options)
526
+ one = cursor.detect {|c| c}
527
+ cursor.close
528
+ one
514
529
  end
515
530
 
516
531
  def find_every(options)
@@ -523,7 +538,6 @@ module MongoRecord
523
538
  find_options[:offset] = options[:offset].to_i if options[:offset]
524
539
  find_options[:sort] = sort_by_from(options[:order]) if options[:order]
525
540
 
526
-
527
541
  cursor = collection.find(criteria, find_options)
528
542
 
529
543
  # Override cursor.next_object so it returns a new instance of this class
@@ -735,6 +749,26 @@ module MongoRecord
735
749
  end
736
750
  end
737
751
 
752
+ # Turns {:key => 'Value'} in update_all into the appropriate '$set' operator
753
+ def update_fields_from(arg)
754
+ raise "Update spec for #{self.name}.update_all must be a hash" unless arg.is_a?(Hash)
755
+ updates = {}
756
+ arg.each do |key,val|
757
+ case val
758
+ when Hash
759
+ # Assume something like $inc => {:num => 1}
760
+ updates[key] = val
761
+ when Array, Range
762
+ raise "Array/range not supported in value of update spec"
763
+ else
764
+ # Assume a simple value, so change to $set
765
+ updates['$set'] ||= {}
766
+ updates['$set'][key] = val
767
+ end
768
+ end
769
+ updates
770
+ end
771
+
738
772
  # Overwrite the default class equality method to provide support for association proxies.
739
773
  def ===(object)
740
774
  object.is_a?(self)
@@ -767,9 +801,20 @@ module MongoRecord
767
801
  iv = "@#{iv}"
768
802
  instance_variable_set(iv, []) unless instance_variable_defined?(iv)
769
803
  }
804
+
805
+ # Create accessors for any per-row dynamic fields we got from our schemaless store
806
+ self.instance_values.keys.each do |key|
807
+ next if respond_to?(key.to_sym) # exists
808
+ define_instance_accessors(key)
809
+ end
810
+
770
811
  yield self if block_given?
771
812
  end
772
813
 
814
+ def attributes
815
+ self.instance_values.inject({}){|h,iv| h[iv.first] = iv.last; h}
816
+ end
817
+
773
818
  # Set the id of this object. Normally not called by user code.
774
819
  def id=(val); @_id = (val == '' ? nil : val); end
775
820
 
@@ -882,13 +927,13 @@ module MongoRecord
882
927
  end
883
928
 
884
929
  def []=(attr_name, value)
885
- self.class.field(attr_name)
930
+ define_instance_accessors(attr_name)
886
931
  self.send(attr_name.to_s + '=', value)
887
932
  end
888
933
 
889
934
  def method_missing(sym, *args)
890
935
  if self.instance_variables.include?("@#{sym}")
891
- self.class.field(sym)
936
+ define_instance_accessors(sym)
892
937
  return self.send(sym)
893
938
  else
894
939
  super
@@ -927,6 +972,9 @@ module MongoRecord
927
972
  save!
928
973
  end
929
974
 
975
+ def valid?; true; end
976
+ alias_method :respond_to_without_attributes?, :respond_to?
977
+
930
978
  # Does nothing.
931
979
  def attributes_from_column_definition; end
932
980
 
@@ -943,6 +991,15 @@ module MongoRecord
943
991
  }
944
992
  end
945
993
 
994
+ #--
995
+ # ================================================================
996
+ # "Dirty" attribute tracking, adapted from ActiveRecord. This is
997
+ # a big performance boost, plus it avoids issues if two people
998
+ # are updating a record concurrently.
999
+ # ================================================================
1000
+ #++
1001
+
1002
+
946
1003
  private
947
1004
 
948
1005
  def create_or_update
@@ -982,6 +1039,28 @@ module MongoRecord
982
1039
  }
983
1040
  end
984
1041
 
1042
+ # Per-object accessors, since row-to-row attributes can change
1043
+ # Use instance_eval so that they don't bleed over to other objects that lack the fields
1044
+ def define_instance_accessors(*fields)
1045
+ fields = Array(fields)
1046
+ fields.each do |field|
1047
+ ivar_name = "@" + field.to_s
1048
+ instance_eval <<-EndAccessors
1049
+ def #{field}
1050
+ instance_variable_get('#{ivar_name}')
1051
+ end
1052
+ def #{field}=(val)
1053
+ old = instance_variable_get('#{ivar_name}')
1054
+ instance_variable_set('#{ivar_name}', val)
1055
+ instance_variable_set('#{ivar_name}', val)
1056
+ end
1057
+ def #{field}?
1058
+ val = instance_variable_get('#{ivar_name}')
1059
+ val != nil && (!val.kind_of?(String) || val != '')
1060
+ end
1061
+ EndAccessors
1062
+ end
1063
+ end
985
1064
  end
986
1065
 
987
1066
  end
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = 'mongo_record'
3
- s.version = '0.4.3'
3
+ s.version = '0.4.4'
4
4
  s.platform = Gem::Platform::RUBY
5
5
  s.summary = 'ActiveRecord-like models for the MongoDB'
6
6
  s.description = 'MongoRecord is an ActiveRecord-like framework for MongoDB. For more information about Mongo, see http://www.mongodb.org.'
data/tests/test_mongo.rb CHANGED
@@ -59,10 +59,10 @@ class MongoTest < Test::Unit::TestCase
59
59
  super
60
60
  MongoRecord::Base.connection = @@db
61
61
 
62
- @@students.clear
63
- @@courses.clear
64
- @@tracks.clear
65
- @@playlists.clear
62
+ @@students.remove
63
+ @@courses.remove
64
+ @@tracks.remove
65
+ @@playlists.remove
66
66
 
67
67
  # Manually insert data without using MongoRecord::Base
68
68
  @@tracks.insert({:_id => Mongo::ObjectID.new, :artist => 'Thomas Dolby', :album => 'Aliens Ate My Buick', :song => 'The Ability to Swing'})
@@ -85,10 +85,10 @@ class MongoTest < Test::Unit::TestCase
85
85
  end
86
86
 
87
87
  def teardown
88
- @@students.clear
89
- @@courses.clear
90
- @@tracks.clear
91
- @@playlists.clear
88
+ @@students.remove
89
+ @@courses.remove
90
+ @@tracks.remove
91
+ @@playlists.remove
92
92
  super
93
93
  end
94
94
 
@@ -98,6 +98,11 @@ class MongoTest < Test::Unit::TestCase
98
98
  assert t.instance_variable_defined?("@#{iv}")
99
99
  }
100
100
  end
101
+
102
+ def test_new_record_set_correctly
103
+ t = Track.new(:_id => 12345, :artist => 'Alice In Chains')
104
+ assert_equal true, t.new_record?
105
+ end
101
106
 
102
107
  def test_method_generation
103
108
  x = Track.new({:artist => 1, :album => 2})
@@ -124,6 +129,94 @@ class MongoTest < Test::Unit::TestCase
124
129
  assert_nil(x.track)
125
130
  end
126
131
 
132
+ def test_dynamic_methods_in_new
133
+ x = Track.new({:foo => 1, :bar => 2})
134
+ y = Track.new({:artist => 3, :song => 4})
135
+
136
+ assert x.respond_to?(:_id)
137
+ assert x.respond_to?(:artist)
138
+ assert x.respond_to?(:album)
139
+ assert x.respond_to?(:song)
140
+ assert x.respond_to?(:track)
141
+ assert x.respond_to?(:_id=)
142
+ assert x.respond_to?(:artist=)
143
+ assert x.respond_to?(:album=)
144
+ assert x.respond_to?(:song=)
145
+ assert x.respond_to?(:track=)
146
+ assert x.respond_to?(:_id?)
147
+ assert x.respond_to?(:artist?)
148
+ assert x.respond_to?(:album?)
149
+ assert x.respond_to?(:song?)
150
+ assert x.respond_to?(:track?)
151
+
152
+ # dynamic fields
153
+ assert x.respond_to?(:foo)
154
+ assert x.respond_to?(:bar)
155
+ assert x.respond_to?(:foo=)
156
+ assert x.respond_to?(:bar=)
157
+ assert x.respond_to?(:foo?)
158
+ assert x.respond_to?(:bar?)
159
+
160
+ # make sure accessors only per-object
161
+ assert !y.respond_to?(:foo)
162
+ assert !y.respond_to?(:bar)
163
+ assert !y.respond_to?(:foo=)
164
+ assert !y.respond_to?(:bar=)
165
+ assert !y.respond_to?(:foo?)
166
+ assert !y.respond_to?(:bar?)
167
+
168
+ assert_equal(1, x.foo)
169
+ assert_equal(2, x.bar)
170
+ assert_nil(x.song)
171
+ assert_nil(x.track)
172
+ assert_equal(3, y.artist)
173
+ assert_equal(4, y.song)
174
+ end
175
+
176
+ def test_dynamic_methods_in_find
177
+ @@tracks.insert({:_id => 909, :artist => 'Faith No More', :album => 'Album Of The Year', :song => 'Stripsearch', :track => 2,
178
+ :vocals => 'Mike Patton', :drums => 'Mike Bordin', :producers => ['Roli Mosimann', 'Billy Gould']})
179
+ x = Track.find_by_id(909)
180
+
181
+ # defined
182
+ assert x.respond_to?(:_id)
183
+ assert x.respond_to?(:artist)
184
+ assert x.respond_to?(:album)
185
+ assert x.respond_to?(:song)
186
+ assert x.respond_to?(:track)
187
+ assert x.respond_to?(:_id=)
188
+ assert x.respond_to?(:artist=)
189
+ assert x.respond_to?(:album=)
190
+ assert x.respond_to?(:song=)
191
+ assert x.respond_to?(:track=)
192
+ assert x.respond_to?(:_id?)
193
+ assert x.respond_to?(:artist?)
194
+ assert x.respond_to?(:album?)
195
+ assert x.respond_to?(:song?)
196
+ assert x.respond_to?(:track?)
197
+
198
+ # dynamic fields
199
+ assert x.respond_to?(:vocals)
200
+ assert x.respond_to?(:drums)
201
+ assert x.respond_to?(:producers)
202
+ assert x.respond_to?(:vocals=)
203
+ assert x.respond_to?(:drums=)
204
+ assert x.respond_to?(:producers=)
205
+ assert x.respond_to?(:vocals?)
206
+ assert x.respond_to?(:drums?)
207
+ assert x.respond_to?(:producers?)
208
+
209
+ assert_equal 'Faith No More', x.artist
210
+ assert_equal 'Album Of The Year', x.album
211
+ assert_equal 'Stripsearch', x.song
212
+ assert_equal 2, x.track
213
+ assert_equal 'Mike Patton', x.vocals
214
+ assert_equal 'Mike Bordin', x.drums
215
+ assert_equal ['Roli Mosimann', 'Billy Gould'], x.producers
216
+
217
+ x.destroy
218
+ end
219
+
127
220
  def test_initialize_block
128
221
  track = Track.new { |t|
129
222
  t.artist = "Me'Shell Ndegeocello"
@@ -293,6 +386,27 @@ class MongoTest < Test::Unit::TestCase
293
386
  assert_no_match(/song: The Mayor Of Simpleton/, Track.find(:all).inject('') { |str, t| str + t.to_s })
294
387
  end
295
388
 
389
+ def test_update_all
390
+ Track.update_all({:track => 919}, {:artist => 'XTC'})
391
+ Track.all.each{|r| assert_equal(919, r.track) if r.artist == 'XTC' }
392
+
393
+ # Should fail (can't $inc/$set) - remove this test once Mongo 1.2 is out
394
+ error = nil
395
+ begin
396
+ Track.update_all({:song => 'Just Drums'}, {}, :safe => true)
397
+ rescue Mongo::OperationFailure => error
398
+ end
399
+ assert_instance_of Mongo::OperationFailure, error
400
+
401
+ @@tracks.drop_index 'song_-1' # otherwise update_all $set fails
402
+ Track.update_all({:song => 'Just Drums'}, {}, :safe => true)
403
+
404
+ assert_no_match(/song: Budapest by Blimp/, Track.all.inject('') { |str, t| str + t.to_s })
405
+
406
+ assert_equal 6, Track.count
407
+ Track.index [:song, :desc], true # reindex
408
+ end
409
+
296
410
  def test_delete_all
297
411
  Track.delete_all({:artist => 'XTC'})
298
412
  assert_no_match(/artist: XTC/, Track.find(:all).inject('') { |str, t| str + t.to_s })
@@ -635,7 +749,7 @@ class MongoTest < Test::Unit::TestCase
635
749
  # Make sure collection exists
636
750
  coll = alt_db.collection('students')
637
751
  coll.insert('name' => 'foo')
638
- coll.clear
752
+ coll.remove
639
753
 
640
754
  assert_equal 0, coll.count()
641
755
  s = Student.new(:name => 'Spongebob Squarepants', :address => @spongebob_addr)
@@ -730,7 +844,30 @@ class MongoTest < Test::Unit::TestCase
730
844
  opts = {:artist => 'The Outfield', :album => 'Play Deep', :song => 'Your Love', :year => 1986}
731
845
  playlist = Playlist.new
732
846
  playlist.update_attributes(opts)
733
- p = Playlist.find_by_artist("The Outfield")
847
+
848
+ # We *want* the following to fail, because otherwise MongoRecord is buggy in the following
849
+ # situation:
850
+ #
851
+ # Rails/Sinatra/etc server instance #1 does: playlist.custom_field = 'foo'
852
+ # Rails/Sinatra/etc instance #2 attempts to do: Playlist.find_by_custom_field
853
+ #
854
+ # This will fail because, in previous versions of MongoRecord, the instance would callback
855
+ # into class.field(), the changing the class definition, then use this modified definition to
856
+ # determine whether dynamic finders work.
857
+ #
858
+ # The biggest issue is you'll never catch this in dev, since everything is a single instance
859
+ # in memory. It will manifest as mysterious "undefined method" production bugs. As such, we *must*
860
+ # restrict dynamic accessors to only modifying the instance for each row, or else they corrupt
861
+ # the class. This means find_by_whatever only works for fields defined via fields()
862
+ error = nil
863
+ begin
864
+ p = Playlist.find_by_artist("The Outfield")
865
+ rescue NoMethodError => error
866
+ end
867
+ assert_instance_of NoMethodError, error
868
+
869
+ # This should work though
870
+ p = Playlist.first(:conditions => {:artist => 'The Outfield'})
734
871
  assert_equal(p.year, 1986)
735
872
  end
736
873
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mongo_record
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.3
4
+ version: 0.4.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jim Menard
@@ -10,7 +10,7 @@ autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
12
 
13
- date: 2009-10-08 00:00:00 -04:00
13
+ date: 2009-12-29 00:00:00 -05:00
14
14
  default_executable:
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency