mongodb-mongo-activerecord-ruby 0.0.1

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.
@@ -0,0 +1,49 @@
1
+ = Welcome to MongoRecord
2
+
3
+ MongoRecord is an ActiveRecord-like framework for the 10gen
4
+ Mongo[http://www.mongodb.org/] database.
5
+
6
+ This document assumes you have read the Mongo documentation.
7
+
8
+ == Installation
9
+
10
+ $ gem sources -a http://gems.github.com
11
+ $ sudo gem install mongodb-mongo-activerecord-ruby
12
+
13
+ MongoRecord depends on the Mongo Ruby Driver. Installing the MongoRecord gem
14
+ will also install the Mongo Ruby Driver if you don't have it already.
15
+
16
+ The source code is available at http://github.com/mongodb/mongo-ruby-driver.
17
+ You can either clone the git repository or download a tarball or zip file.
18
+ Once you have the source, you can use it from wherever you downloaded it or
19
+ you can install it as a gem from the source by typing
20
+
21
+ $ gem build mongo-activerecord-ruby.gemspec
22
+ # ignore "WARNING: no rubyforge_project specified"
23
+ $ sudo gem install mongo-activerecord-ruby-X.Y.Z.gem
24
+
25
+ ...where X.Y.Z is the gem version number. Remember that you need the Mongo
26
+ gem, too.
27
+
28
+
29
+ == Getting Started
30
+
31
+ See the examples, read the MongoRecord::Base and MongoRecord::Cursor
32
+ documentation, and look at tests/test_mongo.rb.
33
+
34
+ === Persistence
35
+
36
+ You can use MongoRecord::Base or talk to the database (stored in the $db
37
+ object) directly.
38
+
39
+ See MongoRecord::Base and MongoRecord::Cursor.
40
+
41
+ === Logger
42
+
43
+ See MongoRecord::LogDevice. When running outside of the cloud (for example,
44
+ during development), all log messages are echoed to $stderr which is normally
45
+ the console.
46
+
47
+ == To Do
48
+
49
+ * DBRefs
@@ -0,0 +1,39 @@
1
+ require 'rubygems'
2
+ require 'rubygems/specification'
3
+ require 'fileutils'
4
+ require 'rake'
5
+ require 'rake/testtask'
6
+ require 'rake/gempackagetask'
7
+ require 'rake/contrib/rubyforgepublisher'
8
+
9
+
10
+ # NOTE: some of the tests assume Mongo is running
11
+ Rake::TestTask.new do |t|
12
+ t.test_files = FileList['tests/test*.rb']
13
+ end
14
+
15
+ desc "Generate documentation"
16
+ task :rdoc do
17
+ FileUtils.rm_rf('html')
18
+ system "rdoc --main README.rdoc --op html --inline-source --quiet README.rdoc `find lib -name '*.rb'`"
19
+ end
20
+
21
+ namespace :gem do
22
+
23
+ desc "Install the gem locally"
24
+ task :install => [:package] do
25
+ sh %{sudo gem install mongo-ruby-driver.gemspec}
26
+ end
27
+
28
+ desc "Install the gem locally with ruby 1.9"
29
+ task :'install19' => [:package] do
30
+ sh %{sudo gem19 install mongo-ruby-driver.gemspec}
31
+ end
32
+
33
+ end
34
+
35
+ task :default => :list
36
+
37
+ task :list do
38
+ system 'rake -T'
39
+ end
@@ -0,0 +1,107 @@
1
+ # Use the local copy, even if this gem is already installed.
2
+ $LOAD_PATH[0,0] = File.join(File.dirname(__FILE__), '../lib')
3
+
4
+ require 'rubygems'
5
+ require 'mongo'
6
+ require 'mongo_record'
7
+
8
+ class Track < MongoRecord::Base
9
+ collection_name :tracks
10
+ fields :artist, :album, :song, :track
11
+ def to_s
12
+ # Uses both accessor methods and ivars themselves
13
+ "artist: #{artist}, album: #{album}, song: #@song, track: #{@track ? @track.to_i : nil}"
14
+ end
15
+ end
16
+
17
+ MongoRecord::Base.connection = XGen::Mongo::Driver::Mongo.new.db('mongorecord-test')
18
+
19
+ # Create data
20
+
21
+ puts "Creating 6 records using \"raw\" Mongo access..."
22
+ db = MongoRecord::Base.connection
23
+ coll = db.collection('tracks')
24
+ coll.remove({})
25
+ coll.insert({:_id => XGen::Mongo::Driver::ObjectID.new, :artist => 'Thomas Dolby', :album => 'Aliens Ate My Buick', :song => 'The Ability to Swing'})
26
+ coll.insert({:_id => XGen::Mongo::Driver::ObjectID.new, :artist => 'Thomas Dolby', :album => 'Aliens Ate My Buick', :song => 'Budapest by Blimp'})
27
+ coll.insert({:_id => XGen::Mongo::Driver::ObjectID.new, :artist => 'Thomas Dolby', :album => 'The Golden Age of Wireless', :song => 'Europa and the Pirate Twins'})
28
+ coll.insert({:_id => XGen::Mongo::Driver::ObjectID.new, :artist => 'XTC', :album => 'Oranges & Lemons', :song => 'Garden Of Earthly Delights', :track => 1})
29
+ coll.insert({:_id => XGen::Mongo::Driver::ObjectID.new, :artist => 'XTC', :album => 'Oranges & Lemons', :song => 'The Mayor Of Simpleton', :track => 2})
30
+ song_id = XGen::Mongo::Driver::ObjectID.new
31
+ coll.insert({:_id => song_id, :artist => 'XTC', :album => 'Oranges & Lemons', :song => 'King For A Day', :track => 3})
32
+ puts "Data created. One song_id = #{song_id}."
33
+ puts "There are #{coll.count()} records in the tracks collection."
34
+
35
+
36
+ puts "\nSimple find"
37
+ puts Track.find(song_id).to_s
38
+ puts Track.find(song_id, :select => :album).to_s
39
+ puts Track.find_by_id(song_id).to_s
40
+ puts Track.find_by_song("Budapest by Blimp").to_s
41
+
42
+ puts "\nCount"
43
+ puts "Yup; there are indeed #{Track.count} records in the tracks collection."
44
+
45
+ puts "\nUpdate"
46
+ x = Track.find_by_track(2)
47
+ x.track = 99
48
+ x.save
49
+ puts Track.find_by_track(99).to_s
50
+
51
+ puts "\nComplex find"
52
+ puts Track.find_by_song('The Mayor Of Simpleton').to_s
53
+
54
+ puts "\nFind all"
55
+ Track.find(:all).each { |t| puts t.to_s }
56
+
57
+ puts "\nFind song /to/"
58
+ Track.find(:all, :conditions => {:song => /to/}).each { |row| puts row.to_s }
59
+
60
+ puts "\nFind limit 2"
61
+ Track.find(:all, :limit => 2).each { |t| puts t.to_s }
62
+
63
+ puts "\nFind by album"
64
+ Track.find(:all, :conditions => {:album => 'Aliens Ate My Buick'}).each { |t| puts t.to_s }
65
+
66
+ puts "\nFind first"
67
+ puts Track.find(:first).to_s
68
+
69
+ puts "\nFind track 3"
70
+ puts Track.find(:first, :conditions => {:track => 3}).to_s
71
+
72
+ puts "\nfind_by_album"
73
+ Track.find_all_by_album('Oranges & Lemons').each { |t| puts t.to_s }
74
+
75
+ puts "\nSorting"
76
+ Track.find(:all, :order => 'album desc').each { |t| puts t.to_s }
77
+
78
+ puts "\nTrack.new"
79
+
80
+ puts Track.new.to_s
81
+
82
+ t = Track.new(:artist => 'Level 42', :album => 'Standing In The Light', :song => 'Micro-Kid', :track => 1)
83
+ puts t.to_s
84
+ puts "save returned #{t.save}"
85
+
86
+ puts "\nTrack.find_or_create_by_song"
87
+
88
+ s, a = 'The Ability to Swing', 'ignored because song found'
89
+ puts Track.find_or_create_by_song(s, :artist => a).to_s
90
+
91
+ s, ar, al = 'New Song', 'New Artist', 'New Album'
92
+ puts Track.find_or_create_by_song(s, :artist => ar, :album => al).to_s
93
+
94
+ puts "\nTrack.find(:first, :conditions => {:song => 'King For A Day'}).delete"
95
+ t = Track.find(:first, :conditions => {:song => 'King For A Day'}).delete
96
+ Track.find(:all).each { |t| puts t.to_s }
97
+
98
+ puts "\nTrack.find('bogus_id')"
99
+ puts "I should see an exception here:"
100
+ begin
101
+ Track.find('bogus_id')
102
+ rescue => ex
103
+ puts ex.to_s
104
+ end
105
+
106
+ puts "\nexplain()"
107
+ puts Track.find(:all, :conditions => {:song => 'King For A Day'}).explain().inspect
@@ -0,0 +1,23 @@
1
+ #--
2
+ # Copyright (C) 2009 10gen Inc.
3
+ #
4
+ # This program is free software: you can redistribute it and/or modify it
5
+ # under the terms of the GNU Affero General Public License, version 3, as
6
+ # published by the Free Software Foundation.
7
+ #
8
+ # This program is distributed in the hope that it will be useful, but WITHOUT
9
+ # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
10
+ # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License
11
+ # for more details.
12
+ #
13
+ # You should have received a copy of the GNU Affero General Public License
14
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
15
+ #++
16
+
17
+ # Include files for using Mongo, MongoRecord::Base, and a database logger.
18
+
19
+ require 'rubygems'
20
+ require 'mongo'
21
+ require 'mongo_record/base'
22
+ require 'mongo_record/subobject'
23
+ require 'mongo_record/log_device'
@@ -0,0 +1,827 @@
1
+ #--
2
+ # Copyright (C) 2009 10gen Inc.
3
+ #
4
+ # This program is free software: you can redistribute it and/or modify it
5
+ # under the terms of the GNU Affero General Public License, version 3, as
6
+ # published by the Free Software Foundation.
7
+ #
8
+ # This program is distributed in the hope that it will be useful, but WITHOUT
9
+ # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
10
+ # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License
11
+ # for more details.
12
+ #
13
+ # You should have received a copy of the GNU Affero General Public License
14
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
15
+ #++
16
+
17
+ require 'rubygems'
18
+ require 'mongo/types/objectid'
19
+ require 'mongo/cursor'
20
+ require 'mongo_record/convert'
21
+ require 'mongo_record/sql'
22
+
23
+ class String
24
+ # Convert this String to an ObjectID.
25
+ def to_oid
26
+ XGen::Mongo::Driver::ObjectID.from_string(self)
27
+ end
28
+ end
29
+
30
+ # An ObjectID. The primary key for all objects stored into Mongo through
31
+ # Babble, stored in the _id field.
32
+ #
33
+ # Normally, you don't have to worry about ObjectIDs. You can treat _id values
34
+ # as strings and XGen::Mongo::Base or Babble will covert them for you.
35
+ #
36
+ # The ObjectID class constructor and initialize methods are defined in Java.
37
+ class XGen::Mongo::Driver::ObjectID
38
+ # Convert this object to an ObjectID.
39
+ def to_oid
40
+ self
41
+ end
42
+ end
43
+
44
+ module MongoRecord
45
+
46
+ class MongoError < StandardError #:nodoc:
47
+ end
48
+ class PreparedStatementInvalid < MongoError #:nodoc:
49
+ end
50
+ class RecordNotFound < MongoError #:nodoc:
51
+ end
52
+ class RecordNotSaved < MongoError #:nodoc:
53
+ end
54
+
55
+ # A superclass for database collection instances. The API is very similar
56
+ # to ActiveRecord. See #find for examples.
57
+ #
58
+ # If you override initialize, make sure to call the superclass version,
59
+ # passing it the database row or hash that it was given.
60
+ #
61
+ # Example:
62
+ #
63
+ # class MP3Track < MongoRecord::Base
64
+ # collection_name :mp3_track
65
+ # fields :artist, :album, :song, :track
66
+ # def to_s
67
+ # "artist: #{self.artist}, album: #{self.album}, song: #{self.song}, track: #{track}"
68
+ # end
69
+ # end
70
+ #
71
+ # track = MP3Track.find_by_song('She Blinded Me With Science')
72
+ # puts track.to_s
73
+ #
74
+ # The database connection defaults to the global $db. You can set the
75
+ # connection using MongoRecord::Base.connection= and read it with
76
+ # MongoRecord::Base.connection.
77
+ #
78
+ # # Set the connection to something besides $db
79
+ # MongoRecord::Base.connection = connect('my-database')
80
+ class Base
81
+
82
+ @@connection = nil
83
+
84
+ class << self # Class methods
85
+
86
+ # Return the database connection. The default value is # <code>$db</code>.
87
+ def connection
88
+ conn = @@connection || $db
89
+ raise "connection not defined" unless conn
90
+ conn
91
+ end
92
+
93
+ # Set the database connection. If the connection is set to +nil+, then
94
+ # <code>$db</code> will be used.
95
+ def connection=(val)
96
+ @@connection = val
97
+ end
98
+
99
+ # This method only exists so that MongoRecord::Base and
100
+ # ActiveRecord::Base can live side by side.
101
+ def instantiate(row={})
102
+ new(row)
103
+ end
104
+
105
+ # Get ready to save information about +subclass+.
106
+ def inherited(subclass)
107
+ subclass.instance_variable_set("@coll_name", class_name_to_field_name(subclass.name)) # default name
108
+ subclass.instance_variable_set("@field_names", []) # array of scalars names (symbols)
109
+ subclass.instance_variable_set("@subobjects", {}) # key = name (symbol), value = class
110
+ subclass.instance_variable_set("@arrays", {}) # key = name (symbol), value = class
111
+ end
112
+
113
+ # Call this method to set the Mongo collection name for this class.
114
+ # The default value is the class name turned into
115
+ # lower_case_with_underscores.
116
+ def collection_name(coll_name)
117
+ @coll_name = coll_name
118
+ field(:_id, :_ns, :_update)
119
+ end
120
+
121
+ # Creates one or more collection fields. Each field will be saved to
122
+ # and loaded from the database. The fields named "_id" and "_ns" are
123
+ # automatically saved and loaded.
124
+ #
125
+ # The method "field" is also called "fields"; you can use either one.
126
+ def field(*fields)
127
+ fields.each { |field|
128
+ field = field.to_sym
129
+ unless @field_names.include?(field)
130
+ ivar_name = "@" + field.to_s
131
+ define_method(field, lambda { instance_variable_get(ivar_name) })
132
+ define_method("#{field}=".to_sym, lambda { |val| instance_variable_set(ivar_name, val) })
133
+ define_method("#{field}?".to_sym, lambda {
134
+ val = instance_variable_get(ivar_name)
135
+ val != nil && (!val.kind_of?(String) || val != '')
136
+ })
137
+ @field_names << field
138
+ end
139
+ }
140
+ end
141
+ alias_method :fields, :field
142
+
143
+ # Return the field names.
144
+ def field_names; @field_names; end
145
+
146
+ # Return the names of all instance variables that hold objects
147
+ # declared using has_one. The names do not start with '@'.
148
+ #
149
+ # These are not necessarily MongoRecord::Subobject subclasses.
150
+ def subobjects; @subobjects; end
151
+
152
+ # Return the names of all instance variables that hold objects
153
+ # declared using has_many. The names do not start with '@'.
154
+ def arrays; @arrays; end
155
+
156
+ # Return the names of all fields, subobjects, and arrays.
157
+ def mongo_ivar_names; @field_names + @subobjects.keys + @arrays.keys; end
158
+
159
+ # Tell Mongo about a subobject (which need not be a
160
+ # MongoRecord::Subobject).
161
+ #
162
+ # Options:
163
+ # <code>:class_name<code> - Name of the class of the subobject.
164
+ def has_one(name, options={})
165
+ name = name.to_sym
166
+ unless @subobjects[name]
167
+ ivar_name = "@" + name.to_s
168
+ define_method(name, lambda { instance_variable_get(ivar_name) })
169
+ define_method("#{name}=".to_sym, lambda { |val| instance_variable_set(ivar_name, val) })
170
+ define_method("#{name}?".to_sym, lambda {
171
+ val = instance_variable_get(ivar_name)
172
+ val != nil && (!val.kind_of?(String) || val != '')
173
+ })
174
+ klass_name = options[:class_name] || field_name_to_class_name(name)
175
+ @subobjects[name] = Kernel.const_get(klass_name)
176
+ end
177
+ end
178
+
179
+ # Tells Mongo about an array of subobjects (which need not be
180
+ # MongoRecord::Subobjects).
181
+ #
182
+ # Options:
183
+ # <code>:class_name</code> - Name of the class of the subobject.
184
+ def has_many(name, options={})
185
+ name = name.to_sym
186
+ unless @arrays[name]
187
+ ivar_name = "@" + name.to_s
188
+ define_method(name, lambda { instance_variable_get(ivar_name) })
189
+ define_method("#{name}=".to_sym, lambda { |val| instance_variable_set(ivar_name, val) })
190
+ define_method("#{name}?".to_sym, lambda { !instance_variable_get(ivar_name).empty? })
191
+ klass_name = options[:class_name] || field_name_to_class_name(name)
192
+ @arrays[name] = Kernel.const_get(klass_name)
193
+ end
194
+ end
195
+
196
+ # Tells Mongo that this object has and many belongs to another object.
197
+ # A no-op.
198
+ def has_and_belongs_to_many(name, options={})
199
+ end
200
+
201
+ # Tells Mongo that this object belongs to another. A no-op.
202
+ def belongs_to(name, options={})
203
+ end
204
+
205
+ # The collection object for this class, which will be different for
206
+ # every subclass of MongoRecord::Base.
207
+ def collection
208
+ connection.collection(@coll_name.to_s)
209
+ end
210
+
211
+ # Find one or more database objects.
212
+ #
213
+ # * Find by id (a single id or an array of ids) returns one record or a Cursor.
214
+ #
215
+ # * Find :first returns the first record that matches the options used
216
+ # or nil if not found.
217
+ #
218
+ # * Find :all records; returns a Cursor that can iterate over raw
219
+ # records.
220
+ #
221
+ # Options:
222
+ #
223
+ # <code>:conditions</code> - Hash where key is field name and value is
224
+ # field value. Value may be a simple value like a string, number, or
225
+ # regular expression.
226
+ #
227
+ # <code>:select</code> - Single field name or list of field names. If
228
+ # not specified, all fields are returned. Names may be symbols or
229
+ # strings. The database always returns _id and _ns fields.
230
+ #
231
+ # <code>:order</code> - If a symbol, orders by that field in ascending
232
+ # order. If a string like "field1 asc, field2 desc, field3", then
233
+ # sorts those fields in the specified order (default is ascending). If
234
+ # an array, each element is either a field name or symbol (which will
235
+ # be sorted in ascending order) or a hash where key =isfield and value
236
+ # is 'asc' or 'desc' (case-insensitive), 1 or -1, or if any other value
237
+ # then true == 1 and false/nil == -1.
238
+ #
239
+ # <code>:limit</code> - Maximum number of records to return.
240
+ #
241
+ # <code>:offset</code> - Number of records to skip.
242
+ #
243
+ # <code>:where</code> - A string containing a JavaScript expression.
244
+ # This expression is run by the database server against each record
245
+ # found after the :conditions are run.
246
+ #
247
+ # Examples for find by id:
248
+ # Person.find("48e5307114f4abdf00dfeb86") # returns the object for this ID
249
+ # Person.find(["a_hex_id", "another_hex_id"]) # returns a Cursor over these two objects
250
+ # Person.find(["a_hex_id"]) # returns a Cursor over the object with this ID
251
+ # Person.find("a_hex_id", :conditions => "admin = 1", :order => "created_on DESC")
252
+ #
253
+ # Examples for find first:
254
+ # Person.find(:first) # returns the first object in the collection
255
+ # Person.find(:first, :conditions => ["user_name = ?", user_name])
256
+ # Person.find(:first, :order => "created_on DESC", :offset => 5)
257
+ # Person.find(:first, :order => {:created_on => -1}, :offset => 5) # same as previous example
258
+ #
259
+ # Examples for find all:
260
+ # Person.find(:all) # returns a Cursor over all objects in the collection
261
+ # Person.find(:all, :conditions => ["category = ?, category], :limit => 50)
262
+ # Person.find(:all, :offset => 10, :limit => 10)
263
+ # Person.find(:all, :select => :name) # Only returns name (and _id) fields
264
+ #
265
+ # Find_by_*
266
+ # Person.find_by_name_and_age("Spongebob", 42)
267
+ # Person.find_all_by_name("Fred")
268
+ #
269
+ # Mongo-specific example:
270
+ # Person.find(:all, :where => "this.address.city == 'New York' || this.age = 42")
271
+ #
272
+ # As a side note, the :order, :limit, and :offset options are passed
273
+ # on to the Cursor (after the :order option is rewritten to be a
274
+ # hash). So
275
+ # Person.find(:all, :offset => 10, :limit => 10, :order => :created_on)
276
+ # is the same as
277
+ # Person.find(:all).skip(10).limit(10).sort({:created_on => 1})
278
+ def find(*args)
279
+ options = extract_options_from_args!(args)
280
+ case args.first
281
+ when :first
282
+ find_initial(options)
283
+ when :all
284
+ find_every(options)
285
+ else
286
+ find_from_ids(args, options)
287
+ end
288
+ end
289
+
290
+ # Returns all records matching mql. Not yet implemented.
291
+ def find_by_mql(mql) # :nodoc:
292
+ raise "not implemented"
293
+ end
294
+ alias_method :find_by_sql, :find_by_mql
295
+
296
+ # Returns the number of matching records.
297
+ def count(options={})
298
+ criteria = criteria_from(options[:conditions]).merge!(where_func(options[:where]))
299
+ collection.count(criteria)
300
+ end
301
+
302
+ # Deletes the record with the given id from the collection.
303
+ def delete(id)
304
+ collection.remove({:_id => id})
305
+ end
306
+ alias_method :remove, :delete
307
+
308
+ # Load the object with +id+ and delete it.
309
+ def destroy(id)
310
+ id.is_a?(Array) ? id.each { |oid| destroy(oid) } : find(id).destroy
311
+ end
312
+
313
+ # Not yet implemented.
314
+ def update_all(updates, conditions = nil)
315
+ # TODO
316
+ raise "not yet implemented"
317
+ end
318
+
319
+ # Destroy all objects that match +conditions+. Warning: if
320
+ # +conditions+ is +nil+, all records in the collection will be
321
+ # destroyed.
322
+ def destroy_all(conditions = nil)
323
+ find(:all, :conditions => conditions).each { |object| object.destroy }
324
+ end
325
+
326
+ # Deletes all records that match +condition+, which can be a
327
+ # Mongo-style hash or an ActiveRecord-like hash. Examples:
328
+ # Person.destroy_all "name like '%fred%' # SQL WHERE clause
329
+ # Person.destroy_all ["name = ?", 'Fred'] # Rails condition
330
+ # Person.destroy_all {:name => 'Fred'} # Mongo hash
331
+ def delete_all(conditions=nil)
332
+ collection.remove(criteria_from(conditions))
333
+ end
334
+
335
+ # Creates, saves, and returns a new database object.
336
+ def create(values_hash)
337
+ object = self.new(values_hash)
338
+ object.save
339
+ object
340
+ end
341
+
342
+ # Finds the record from the passed +id+, instantly saves it with the passed +attributes+ (if the validation permits it),
343
+ # and returns it. If the save fails under validations, the unsaved object is still returned.
344
+ #
345
+ # The arguments may also be given as arrays in which case the update method is called for each pair of +id+ and
346
+ # +attributes+ and an array of objects is returned.
347
+ # =>
348
+ # Example of updating one record:
349
+ # Person.update(15, {:user_name => 'Samuel', :group => 'expert'})
350
+ #
351
+ # Example of updating multiple records:
352
+ # people = { 1 => { "first_name" => "David" }, 2 => { "first_name" => "Jeremy"} }
353
+ # Person.update(people.keys, people.values)
354
+ def update(id, attributes)
355
+ if id.is_a?(Array)
356
+ i = -1
357
+ id.collect { |id| i += 1; update(id, attributes[i]) }
358
+ else
359
+ object = find(id)
360
+ object.update_attributes(attributes)
361
+ object
362
+ end
363
+ end
364
+
365
+ # Handles find_* methods such as find_by_name, find_all_by_shoe_size,
366
+ # and find_or_create_by_name.
367
+ def method_missing(sym, *args)
368
+ if match = /^find_(all_by|by)_([_a-zA-Z]\w*)$/.match(sym.to_s)
369
+ find_how_many = ($1 == 'all_by') ? :all : :first
370
+ field_names = $2.split(/_and_/)
371
+ super unless all_fields_exist?(field_names)
372
+ search = search_from_names_and_values(field_names, args)
373
+ self.find(find_how_many, {:conditions => search}, *args[field_names.length..-1])
374
+ elsif match = /^find_or_(initialize|create)_by_([_a-zA-Z]\w*)$/.match(sym.to_s)
375
+ create = $1 == 'create'
376
+ field_names = $2.split(/_and_/)
377
+ super unless all_fields_exist?(field_names)
378
+ search = search_from_names_and_values(field_names, args)
379
+ row = self.find(:first, {:conditions => search})
380
+ return self.new(row) if row # found
381
+ obj = self.new(search.merge(args[field_names.length] || {})) # new object using search and remainder of args
382
+ obj.save if create
383
+ obj
384
+ else
385
+ super
386
+ end
387
+ end
388
+
389
+ private
390
+
391
+ def extract_options_from_args!(args)
392
+ args.last.is_a?(Hash) ? args.pop : {}
393
+ end
394
+
395
+ def find_initial(options)
396
+ criteria = criteria_from(options[:conditions]).merge!(where_func(options[:where]))
397
+ fields = fields_from(options[:select])
398
+ row = collection.find(criteria, :fields => fields, :limit => 1).next_object
399
+ (row.nil? || row['_id'] == nil) ? nil : self.new(row)
400
+ end
401
+
402
+ def find_every(options)
403
+ criteria = criteria_from(options[:conditions]).merge!(where_func(options[:where]))
404
+
405
+ find_options = {}
406
+ find_options[:fields] = fields_from(options[:select]) if options[:select]
407
+ find_options[:limit] = options[:limit].to_i if options[:limit]
408
+ find_options[:offset] = options[:offset].to_i if options[:offset]
409
+ find_options[:sort] = sort_by_from(options[:order]) if options[:order]
410
+
411
+ cursor = collection.find(criteria, find_options)
412
+
413
+ # Override cursor.next_object so it returns a new instance of this class
414
+ eval "def cursor.next_object; #{self.name}.new(super()); end"
415
+ cursor
416
+ end
417
+
418
+ def find_from_ids(ids, options)
419
+ ids = ids.to_a.flatten.compact.uniq
420
+ raise RecordNotFound, "Couldn't find #{name} without an ID" unless ids.length > 0
421
+
422
+ criteria = criteria_from(options[:conditions]).merge!(where_func(options[:where]))
423
+ criteria[:_id] = ids_clause(ids)
424
+ fields = fields_from(options[:select])
425
+
426
+ if ids.length == 1
427
+ row = collection.find(criteria, :fields => fields, :limit => 1).next_object
428
+ raise RecordNotFound, "Couldn't find #{name} with ID=#{ids[0]} #{criteria.inspect}" if row == nil || row.empty?
429
+ self.new(row)
430
+ else
431
+ find_options = {}
432
+ find_options[:fields] = fields if fields
433
+ find_options[:sort] = sort_by_from(options[:order]) if options[:order]
434
+
435
+ cursor = collection.find(criteria, find_options)
436
+
437
+ # Override cursor.next_object so it returns a new instance of this class
438
+ eval "def cursor.next_object; #{self.name}.new(super()); end"
439
+ cursor
440
+ end
441
+ end
442
+
443
+ def ids_clause(ids)
444
+ ids.length == 1 ? ids[0].to_oid : {'$in' => ids.collect{|id| id.to_oid}}
445
+ end
446
+
447
+ # Returns true if all field_names are in @field_names.
448
+ def all_fields_exist?(field_names)
449
+ field_names.collect! {|f| f == 'id' ? '_id' : f}
450
+ (field_names - @field_names.collect{|f| f.to_s}).empty?
451
+ end
452
+
453
+ # Returns a db search hash, given field_names and values.
454
+ def search_from_names_and_values(field_names, values)
455
+ h = {}
456
+ field_names.each_with_index { |iv, i| h[iv.to_s] = values[i] }
457
+ h
458
+ end
459
+
460
+ # Given a "SymbolOrStringLikeThis", return the string "symbol_or_string_like_this".
461
+ def class_name_to_field_name(name)
462
+ name.gsub(/([A-Z])/, '_\1').downcase.sub(/^_/, '')
463
+ end
464
+
465
+ # Given a "symbol_or_string_like_this", return the string "SymbolOrStringLikeThis".
466
+ def field_name_to_class_name(name)
467
+ name = name.to_s.dup.gsub(/_([a-z])/) {$1.upcase}
468
+ name[0,1] = name[0,1].upcase
469
+ name
470
+ end
471
+
472
+ protected
473
+
474
+ # Turns array, string, or hash conditions into something useable by Mongo.
475
+ # ["name='%s' and group_id='%s'", "foo'bar", 4] returns {:name => 'foo''bar', :group_id => 4}
476
+ # "name='foo''bar' and group_id='4'" returns {:name => 'foo''bar', :group_id => 4}
477
+ # { :name => "foo'bar", :group_id => 4 } returns the hash, modified for Mongo
478
+ def criteria_from(condition) # :nodoc:
479
+ case condition
480
+ when Array
481
+ criteria_from_array(condition)
482
+ when String
483
+ criteria_from_string(condition)
484
+ when Hash
485
+ criteria_from_hash(condition)
486
+ else
487
+ {}
488
+ end
489
+ end
490
+
491
+ # Substitutes values at the end of an array into the string at its
492
+ # start, sanitizing strings in the values. Then passes the string on
493
+ # to criteria_from_string.
494
+ def criteria_from_array(condition) # :nodoc:
495
+ str, *values = condition
496
+ sql = if values.first.kind_of?(Hash) and str =~ /:\w+/
497
+ replace_named_bind_variables(str, values.first)
498
+ elsif str.include?('?')
499
+ replace_bind_variables(str, values)
500
+ else
501
+ str % values.collect {|value| quote(value) }
502
+ end
503
+ criteria_from_string(sql)
504
+ end
505
+
506
+ # Turns a string into a Mongo search condition hash.
507
+ def criteria_from_string(sql) # :nodoc:
508
+ MongoRecord::SQL::Parser.parse_where(sql)
509
+ end
510
+
511
+ # Turns a hash that ActiveRecord would expect into one for Mongo.
512
+ def criteria_from_hash(condition) # :nodoc:
513
+ h = {}
514
+ condition.each { |k,v|
515
+ h[k] = case v
516
+ when Array
517
+ {'$in' => k == 'id' || k == '_id' ? v.collect{ |val| val.to_oid} : v} # if id, can't pass in string; must be ObjectID
518
+ when Range
519
+ {'$gte' => v.first, '$lte' => v.last}
520
+ else
521
+ k == 'id' || k == '_id' ? v.to_oid : v
522
+ end
523
+ }
524
+ h
525
+ end
526
+
527
+ # Returns a hash useable by Mongo for applying +func+ on the db server.
528
+ # +func+ must be +nil+ or a JavaScript expression or function in a
529
+ # string.
530
+ def where_func(func) # :nodoc:
531
+ func ? {'$where' => func} : {}
532
+ end
533
+
534
+ def replace_named_bind_variables(str, h) # :nodoc:
535
+ str.gsub(/:(\w+)/) do
536
+ match = $1.to_sym
537
+ if h.include?(match)
538
+ quoted_bind_var(h[match])
539
+ else
540
+ raise PreparedStatementInvalid, "missing value for :#{match} in #{str}" # TODO this gets swallowed in find()
541
+ end
542
+ end
543
+ end
544
+
545
+ def replace_bind_variables(str, values) # :nodoc:
546
+ raise "parameter count does not match value count" unless str.count('?') == values.length
547
+ bound = values.dup
548
+ str.gsub('?') { quoted_bind_var(bound.shift) }
549
+ end
550
+
551
+ def quoted_bind_var(val) # :nodoc:
552
+ case val
553
+ when Array
554
+ val.collect{|v| quote(v)}.join(',')
555
+ else
556
+ quote(val)
557
+ end
558
+ end
559
+
560
+ # Returns value quoted if appropriate (if it's a string).
561
+ def quote(val) # :nodoc:
562
+ return val unless val.is_a?(String)
563
+ return "'#{val.gsub(/\'/, "\\\\'")}'" # " <= for Emacs font-lock
564
+ end
565
+
566
+ def fields_from(a) # :nodoc:
567
+ return nil unless a
568
+ a = [a] unless a.kind_of?(Array)
569
+ return nil unless a.length > 0
570
+ a.collect { |k| k.to_s }
571
+ end
572
+
573
+ def sort_by_from(option) # :nodoc:
574
+ return nil unless option
575
+ sort_by = []
576
+ case option
577
+ when Symbol # Single value
578
+ sort_by << {option.to_s => 1}
579
+ when String
580
+ # TODO order these by building an array of hashes
581
+ fields = option.split(',')
582
+ fields.each {|f|
583
+ name, order = f.split
584
+ order ||= 'asc'
585
+ sort_by << {name.to_s => sort_value_from_arg(order)}
586
+ }
587
+ when Array # Array of field names; assume ascending sort
588
+ # TODO order these by building an array of hashes
589
+ sort_by = option.collect {|o| {o.to_s => 1}}
590
+ else # Hash (order of sorts is not guaranteed)
591
+ sort_by = option.collect {|k, v| {k.to_s => sort_value_from_arg(v)}}
592
+ end
593
+ return nil unless sort_by.length > 0
594
+ sort_by
595
+ end
596
+
597
+ # Turns "asc" into 1, "desc" into -1, and other values into 1 or -1.
598
+ def sort_value_from_arg(arg) # :nodoc:
599
+ case arg
600
+ when /^asc/i
601
+ arg = 1
602
+ when /^desc/i
603
+ arg = -1
604
+ when Number
605
+ arg.to_i >= 0 ? 1 : -1
606
+ else
607
+ arg ? 1 : -1
608
+ end
609
+ end
610
+
611
+ # Overwrite the default class equality method to provide support for association proxies.
612
+ def ===(object)
613
+ object.is_a?(self)
614
+ end
615
+
616
+ end # End of class methods
617
+
618
+ public
619
+
620
+ # Initialize a new object with either a hash of values or a row returned
621
+ # from the database.
622
+ def initialize(row={})
623
+ case row
624
+ when Hash
625
+ row.each { |k, val|
626
+ k = '_id' if k == 'id' # Rails helper
627
+ init_ivar("@#{k}", val)
628
+ }
629
+ else
630
+ row.instance_variables.each { |iv|
631
+ init_ivar(iv, row.instance_variable_get(iv))
632
+ }
633
+ end
634
+ # Default values for remaining fields
635
+ (self.class.field_names + self.class.subobjects.keys).each { |iv|
636
+ iv = "@#{iv}"
637
+ instance_variable_set(iv, nil) unless instance_variable_defined?(iv)
638
+ }
639
+ self.class.arrays.keys.each { |iv|
640
+ iv = "@#{iv}"
641
+ instance_variable_set(iv, []) unless instance_variable_defined?(iv)
642
+ }
643
+ yield self if block_given?
644
+ end
645
+
646
+ # Set the id of this object. Normally not called by user code.
647
+ def id=(val); @_id = (val == '' ? nil : val); end
648
+
649
+ # Return this object's id.
650
+ def id; @_id ? @_id.to_s : nil; end
651
+
652
+ # Return true if the +comparison_object+ is the same object, or is of
653
+ # the same type and has the same id.
654
+ def ==(comparison_object)
655
+ comparison_object.equal?(self) ||
656
+ (comparison_object.instance_of?(self.class) &&
657
+ comparison_object.id == id &&
658
+ !comparison_object.new_record?)
659
+ end
660
+
661
+ # Delegate to ==
662
+ def eql?(comparison_object)
663
+ self == (comparison_object)
664
+ end
665
+
666
+ # Delegate to id in order to allow two records of the same type and id to work with something like:
667
+ # [ Person.find(1), Person.find(2), Person.find(3) ] & [ Person.find(1), Person.find(4) ] # => [ Person.find(1) ]
668
+ def hash
669
+ id.hash
670
+ end
671
+
672
+ # Rails convenience method. Return this object's id as a string.
673
+ def to_param
674
+ @_id.to_s
675
+ end
676
+
677
+ # Save self and returns true if the save was successful, false if not.
678
+ def save
679
+ create_or_update
680
+ end
681
+
682
+ # Save self and returns true if the save was successful and raises
683
+ # RecordNotSaved if not.
684
+ def save!
685
+ create_or_update || raise(RecordNotSaved)
686
+ end
687
+
688
+ # Return true if this object is new---that is, does not yet have an id.
689
+ def new_record?
690
+ @_id == nil
691
+ end
692
+
693
+ # Convert this object to a Mongo value suitable for saving to the
694
+ # database.
695
+ def to_mongo_value
696
+ h = {}
697
+ self.class.mongo_ivar_names.each {|iv| h[iv] = instance_variable_get("@#{iv}").to_mongo_value }
698
+ h
699
+ end
700
+
701
+ # Save self to the database and set the id.
702
+ def create
703
+ set_create_times
704
+ @_id = XGen::Mongo::Driver::ObjectID.new
705
+ self.class.collection.insert(to_mongo_value.merge({'_id' => @_id}))
706
+ self
707
+ end
708
+
709
+ # Save self to the database. Return +false+ if there was an error,
710
+ # +self+ if all is well.
711
+ def update
712
+ set_update_times
713
+ row = self.class.collection.insert(to_mongo_value)
714
+ if row['_id'].to_s != @_id.to_s
715
+ return false
716
+ end
717
+ self
718
+ end
719
+
720
+ # Remove self from the database and set @_id to nil. If self has no
721
+ # @_id, does nothing.
722
+ def delete
723
+ if @_id
724
+ self.class.collection.remove({:_id => self._id})
725
+ @_id = nil
726
+ end
727
+ end
728
+ alias_method :remove, :delete
729
+
730
+ # Delete and freeze self.
731
+ def destroy
732
+ delete
733
+ freeze
734
+ end
735
+
736
+ #--
737
+ # ================================================================
738
+ # These methods exist so we can plug in ActiveRecord validation, etc.
739
+ # ================================================================
740
+ #++
741
+
742
+ # Updates a single attribute and saves the record. This is especially
743
+ # useful for boolean flags on existing records. Note: This method is
744
+ # overwritten by the Validation module that'll make sure that updates
745
+ # made with this method doesn't get subjected to validation checks.
746
+ # Hence, attributes can be updated even if the full object isn't valid.
747
+ def update_attribute(name, value)
748
+ send(name.to_s + '=', value)
749
+ save
750
+ end
751
+
752
+ # Updates all the attributes from the passed-in Hash and saves the
753
+ # record. If the object is invalid, the saving will fail and false will
754
+ # be returned.
755
+ def update_attributes(attributes)
756
+ self.attributes = attributes
757
+ save
758
+ end
759
+
760
+ # Updates an object just like Base.update_attributes but calls save!
761
+ # instead of save so an exception is raised if the record is invalid.
762
+ def update_attributes!(attributes)
763
+ self.attributes = attributes
764
+ save!
765
+ end
766
+
767
+ # Does nothing.
768
+ def attributes_from_column_definition; end
769
+
770
+ # ================================================================
771
+
772
+ private
773
+
774
+ def create_or_update
775
+ result = new_record? ? create : update
776
+ result != false
777
+ end
778
+
779
+ # Initialize ivar. +name+ must include the leading '@'.
780
+ def init_ivar(ivar_name, val)
781
+ sym = ivar_name[1..-1].to_sym
782
+ if self.class.subobjects.keys.include?(sym)
783
+ instance_variable_set(ivar_name, self.class.subobjects[sym].new(val))
784
+ elsif self.class.arrays.keys.include?(sym)
785
+ klazz = self.class.arrays[sym]
786
+ val = [val] unless val.kind_of?(Array)
787
+ instance_variable_set(ivar_name, val.collect {|v| v.kind_of?(MongoRecord::Base) ? v : klazz.new(v)})
788
+ else
789
+ instance_variable_set(ivar_name, val)
790
+ end
791
+ end
792
+
793
+ def set_create_times(t=nil)
794
+ t ||= Time.now
795
+ self.class.field_names.each { |iv|
796
+ case iv
797
+ when :created_at
798
+ instance_variable_set("@#{iv}", t)
799
+ when :created_on
800
+ instance_variable_set("@#{iv}", Time.local(t.year, t.month, t.day))
801
+ end
802
+ }
803
+ self.class.subobjects.keys.each { |iv|
804
+ val = instance_variable_get("@#{iv}")
805
+ val.send(:set_create_times, t) if val
806
+ }
807
+ end
808
+
809
+ def set_update_times(t=nil)
810
+ t ||= Time.now
811
+ self.class.field_names.each { |iv|
812
+ case iv
813
+ when :updated_at
814
+ instance_variable_set("@#{iv}", t)
815
+ when :updated_on
816
+ instance_variable_set("@#{iv}", Time.local(t.year, t.month, t.day))
817
+ end
818
+ }
819
+ self.class.subobjects.keys.each { |iv|
820
+ val = instance_variable_get("@#{iv}")
821
+ val.send(:set_update_times, t) if val
822
+ }
823
+ end
824
+
825
+ end
826
+
827
+ end