mongo_record 0.4.3 → 0.4.4

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/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