mongo_record 0.4.2

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,988 @@
1
+ # Copyright 2009 10gen, Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ require 'rubygems'
16
+ require 'mongo/types/objectid'
17
+ require 'mongo/types/code'
18
+ require 'mongo/cursor'
19
+ require 'mongo_record/convert'
20
+ require 'mongo_record/sql'
21
+
22
+ class String
23
+ # Convert this String to an ObjectID.
24
+ def to_oid
25
+ Mongo::ObjectID.legal?(self) ? XGen::Mongo::Driver::ObjectID.from_string(self) : self
26
+ end
27
+ end
28
+
29
+ class Mongo::ObjectID
30
+ # Convert this object to an ObjectID.
31
+ def to_oid
32
+ self
33
+ end
34
+ end
35
+
36
+ module MongoRecord
37
+
38
+ class PKFactory
39
+ def create_pk(row)
40
+ return row if row[:_id]
41
+ row.delete(:_id) # in case it is nil
42
+ row['_id'] ||= Mongo::ObjectID.new
43
+ row
44
+ end
45
+ end
46
+
47
+ class MongoError < StandardError #:nodoc:
48
+ end
49
+ class PreparedStatementInvalid < MongoError #:nodoc:
50
+ end
51
+ class RecordNotFound < MongoError #:nodoc:
52
+ end
53
+ class RecordNotSaved < MongoError #:nodoc:
54
+ end
55
+
56
+ # A superclass for database collection instances. The API is very similar
57
+ # to ActiveRecord. See #find for examples.
58
+ #
59
+ # If you override initialize, make sure to call the superclass version,
60
+ # passing it the database row or hash that it was given.
61
+ #
62
+ # Example:
63
+ #
64
+ # class MP3Track < MongoRecord::Base
65
+ # collection_name :mp3_track
66
+ # fields :artist, :album, :song, :track
67
+ # def to_s
68
+ # "artist: #{self.artist}, album: #{self.album}, song: #{self.song}, track: #{track}"
69
+ # end
70
+ # end
71
+ #
72
+ # track = MP3Track.find_by_song('She Blinded Me With Science')
73
+ # puts track.to_s
74
+ #
75
+ # The database connection defaults to the global $db. You can set the
76
+ # connection using MongoRecord::Base.connection= and read it with
77
+ # MongoRecord::Base.connection.
78
+ #
79
+ # # Set the connection to something besides $db
80
+ # MongoRecord::Base.connection = connect('my-database')
81
+ class Base
82
+
83
+ @@connection = nil
84
+
85
+ class << self # Class methods
86
+
87
+ # Return the database connection. The default value is # <code>$db</code>.
88
+ def connection
89
+ conn = @@connection || $db
90
+ raise "connection not defined" unless conn
91
+ conn
92
+ end
93
+
94
+ # Set the database connection. If the connection is set to +nil+, then
95
+ # <code>$db</code> will be used.
96
+ def connection=(val)
97
+ @@connection = val
98
+ @@connection.pk_factory = PKFactory.new unless @@connection.pk_factory
99
+ end
100
+
101
+ # This method only exists so that MongoRecord::Base and
102
+ # ActiveRecord::Base can live side by side.
103
+ def instantiate(row={})
104
+ new(row)
105
+ end
106
+
107
+ # Get ready to save information about +subclass+.
108
+ def inherited(subclass)
109
+ subclass.instance_variable_set("@coll_name", class_name_to_field_name(subclass.name)) # default name
110
+ subclass.instance_variable_set("@field_names", []) # array of scalars names (symbols)
111
+ subclass.instance_variable_set("@subobjects", {}) # key = name (symbol), value = class
112
+ subclass.instance_variable_set("@arrays", {}) # key = name (symbol), value = class
113
+ end
114
+
115
+ # Call this method to set the Mongo collection name for this class.
116
+ # The default value is the class name turned into
117
+ # lower_case_with_underscores.
118
+ def collection_name(coll_name)
119
+ @coll_name = coll_name
120
+ field(:_id, :_ns)
121
+ end
122
+
123
+ # Creates one or more collection fields. Each field will be saved to
124
+ # and loaded from the database. The fields named "_id" and "_ns" are
125
+ # automatically saved and loaded.
126
+ #
127
+ # The method "field" is also called "fields"; you can use either one.
128
+ def field(*fields)
129
+ fields.each { |field|
130
+ field = field.to_sym
131
+ unless @field_names.include?(field)
132
+ ivar_name = "@" + field.to_s
133
+ define_method(field, lambda { instance_variable_get(ivar_name) })
134
+ define_method("#{field}=".to_sym, lambda { |val| instance_variable_set(ivar_name, val) })
135
+ define_method("#{field}?".to_sym, lambda {
136
+ val = instance_variable_get(ivar_name)
137
+ val != nil && (!val.kind_of?(String) || val != '')
138
+ })
139
+ @field_names << field
140
+ end
141
+ }
142
+ end
143
+ alias_method :fields, :field
144
+
145
+ # Return the field names.
146
+ def field_names; @field_names; end
147
+
148
+ # Creates an index for this collection.
149
+ # +fields+ should be either a single field name (:title)
150
+ # or an array of fields ([:title, :author, :date])
151
+ # or an array of a field name and direction ([:title, :asc] or [:title, :desc])
152
+ # or an array of field names and directions ([[:title, :asc], [:author, :desc]])
153
+ # +unique+ should be true or false and indicates whether this index
154
+ # should enforce a uniqueness constraint.
155
+ def index(fields, unique = false)
156
+ fields = Array(fields)
157
+
158
+ if fields.length == 2 &&
159
+ ( fields[1].to_s == 'asc' || fields[1].to_s == 'desc' ||
160
+ fields[1] == Mongo::ASCENDING || fields[1] == Mongo::DESCENDING )
161
+ fields = [fields]
162
+ end
163
+
164
+ fields = fields.map do |field|
165
+ field = field.respond_to?(:[]) ? field : [field, :asc]
166
+ field[1] = (field[1] == :desc) ? Mongo::DESCENDING : Mongo::ASCENDING
167
+ field
168
+ end
169
+
170
+ collection.create_index(fields, unique)
171
+ end
172
+
173
+ # Returns list of indexes for model, unless fields are passed.
174
+ # In that case, creates an index.
175
+ def indexes(*fields)
176
+ if fields.empty?
177
+ collection.index_information
178
+ else
179
+ index(*fields)
180
+ end
181
+ end
182
+
183
+ # Return the names of all instance variables that hold objects
184
+ # declared using has_one. The names do not start with '@'.
185
+ #
186
+ # These are not necessarily MongoRecord::Subobject subclasses.
187
+ def subobjects; @subobjects; end
188
+
189
+ # Return the names of all instance variables that hold objects
190
+ # declared using has_many. The names do not start with '@'.
191
+ def arrays; @arrays; end
192
+
193
+ # Return the names of all fields, subobjects, and arrays.
194
+ def mongo_ivar_names; @field_names + @subobjects.keys + @arrays.keys; end
195
+
196
+ # Tell Mongo about a subobject (which can be either a
197
+ # MongoRecord::Subobject or MongoRecord::Base instance).
198
+ #
199
+ # Options:
200
+ # <code>:class_name<code> - Name of the class of the subobject.
201
+ def has_one(name, options={})
202
+ name = name.to_sym
203
+ unless @subobjects[name]
204
+ ivar_name = "@" + name.to_s
205
+ define_method(name, lambda { instance_variable_get(ivar_name) })
206
+ define_method("#{name}=".to_sym, lambda { |val| instance_variable_set(ivar_name, val) })
207
+ define_method("#{name}?".to_sym, lambda {
208
+ val = instance_variable_get(ivar_name)
209
+ val != nil && (!val.kind_of?(String) || val != '')
210
+ })
211
+ klass_name = options[:class_name] || field_name_to_class_name(name)
212
+ @subobjects[name] = eval(klass_name)
213
+ end
214
+ end
215
+
216
+ # Tells Mongo about an array of subobjects (which can be either a
217
+ # MongoRecord::Subobject or MongoRecord::Base instance).
218
+ #
219
+ # Options:
220
+ # <code>:class_name</code> - Name of the class of the subobject.
221
+ def has_many(name, options={})
222
+ name = name.to_sym
223
+ unless @arrays[name]
224
+ ivar_name = "@" + name.to_s
225
+ define_method(name, lambda { instance_variable_get(ivar_name) })
226
+ define_method("#{name}=".to_sym, lambda { |val| instance_variable_set(ivar_name, val) })
227
+ define_method("#{name}?".to_sym, lambda { !instance_variable_get(ivar_name).empty? })
228
+ klass_name = options[:class_name] || field_name_to_class_name(name)
229
+ @arrays[name] = eval(klass_name)
230
+ end
231
+ end
232
+
233
+ # Tells Mongo that this object has and many belongs to another object.
234
+ # A no-op.
235
+ def has_and_belongs_to_many(name, options={})
236
+ end
237
+
238
+ # Tells Mongo that this object belongs to another. A no-op.
239
+ def belongs_to(name, options={})
240
+ end
241
+
242
+ # The collection object for this class, which will be different for
243
+ # every subclass of MongoRecord::Base.
244
+ def collection
245
+ connection.collection(@coll_name.to_s)
246
+ end
247
+
248
+ # Find one or more database objects.
249
+ #
250
+ # * Find by id (a single id or an array of ids) returns one record or a Cursor.
251
+ #
252
+ # * Find :first returns the first record that matches the options used
253
+ # or nil if not found.
254
+ #
255
+ # * Find :all records; returns a Cursor that can iterate over raw
256
+ # records.
257
+ #
258
+ # Options:
259
+ #
260
+ # <code>:conditions</code> - Hash where key is field name and value is
261
+ # field value. Value may be a simple value like a string, number, or
262
+ # regular expression.
263
+ #
264
+ # <code>:select</code> - Single field name or list of field names. If
265
+ # not specified, all fields are returned. Names may be symbols or
266
+ # strings. The database always returns _id and _ns fields.
267
+ #
268
+ # <code>:order</code> - If a symbol, orders by that field in ascending
269
+ # order. If a string like "field1 asc, field2 desc, field3", then
270
+ # sorts those fields in the specified order (default is ascending). If
271
+ # an array, each element is either a field name or symbol (which will
272
+ # be sorted in ascending order) or a hash where key =isfield and value
273
+ # is 'asc' or 'desc' (case-insensitive), 1 or -1, or if any other value
274
+ # then true == 1 and false/nil == -1.
275
+ #
276
+ # <code>:limit</code> - Maximum number of records to return.
277
+ #
278
+ # <code>:offset</code> - Number of records to skip.
279
+ #
280
+ # <code>:where</code> - A string containing a JavaScript expression.
281
+ # This expression is run by the database server against each record
282
+ # found after the :conditions are run.
283
+ #
284
+ # This expression is run by the database server against each record
285
+ # found after the :conditions are run.
286
+ #
287
+ # <code>:criteria</code> - A hash field to pass in MongoDB conditional operators
288
+ # in a hash format. [$gt, $lt, $gte, $lte, $in, ect.]
289
+ #
290
+ # Examples for find by id:
291
+ # Person.find("48e5307114f4abdf00dfeb86") # returns the object for this ID
292
+ # Person.find(["a_hex_id", "another_hex_id"]) # returns a Cursor over these two objects
293
+ # Person.find(["a_hex_id"]) # returns a Cursor over the object with this ID
294
+ # Person.find("a_hex_id", :conditions => "admin = 1", :order => "created_on DESC")
295
+ #
296
+ # Examples for find first:
297
+ # Person.find(:first) # returns the first object in the collection
298
+ # Person.find(:first, :conditions => ["user_name = ?", user_name])
299
+ # Person.find(:first, :order => "created_on DESC", :offset => 5)
300
+ # Person.find(:first, :order => {:created_on => -1}, :offset => 5) # same as previous example
301
+ #
302
+ # Examples for find all:
303
+ # Person.find(:all) # returns a Cursor over all objects in the collection
304
+ # Person.find(:all, :conditions => ["category = ?, category], :limit => 50)
305
+ # Person.find(:all, :offset => 10, :limit => 10)
306
+ # Person.find(:all, :select => :name) # Only returns name (and _id) fields
307
+ #
308
+ # Find_by_*
309
+ # Person.find_by_name_and_age("Spongebob", 42)
310
+ # Person.find_all_by_name("Fred")
311
+ #
312
+ # Mongo-specific example:
313
+ # Person.find(:all, :where => "this.address.city == 'New York' || this.age = 42")
314
+ # Person.find(:all, :criteria => {"followers_count"=>{"$gte"=>410}})
315
+ #
316
+ # As a side note, the :order, :limit, and :offset options are passed
317
+ # on to the Cursor (after the :order option is rewritten to be a
318
+ # hash). So
319
+ # Person.find(:all, :offset => 10, :limit => 10, :order => :created_on)
320
+ # is the same as
321
+ # Person.find(:all).skip(10).limit(10).sort({:created_on => 1})
322
+ #
323
+
324
+
325
+ def find(*args)
326
+ options = extract_options_from_args!(args)
327
+ options.symbolize_keys!
328
+ case args.first
329
+ when :first
330
+ find_initial(options)
331
+ when :all
332
+ find_every(options)
333
+ when :last
334
+ find_last(options)
335
+ else
336
+ find_from_ids(args, options)
337
+ end
338
+ end
339
+
340
+ # Yields each record that was found by the find +options+. The find is
341
+ # performed by find.
342
+ #
343
+ # Example:
344
+ #
345
+ # Person.find_each(:conditions => "age > 21") do |person|
346
+ # person.party_all_night!
347
+ # end
348
+
349
+ def find_each(*args)
350
+ options = extract_options_from_args!(args)
351
+ options.symbolize_keys!
352
+ find_every(options).each do |record|
353
+ yield record
354
+ end
355
+ self
356
+ end
357
+
358
+ def all(*args)
359
+ options = extract_options_from_args!(args)
360
+ find_every(options)
361
+ end
362
+
363
+ def first(*args)
364
+ # args = ([:first]<<args).flatten
365
+ options = extract_options_from_args!(args)
366
+ find_initial(options)
367
+ end
368
+
369
+ def last(*args)
370
+ options = extract_options_from_args!(args)
371
+ find_last(options)
372
+ end
373
+
374
+ # Returns all records matching mql. Not yet implemented.
375
+ def find_by_mql(mql) # :nodoc:
376
+ raise "not implemented"
377
+ end
378
+ alias_method :find_by_sql, :find_by_mql
379
+
380
+ # Returns the number of matching records.
381
+ def count(options={})
382
+ criteria = criteria_from(options[:conditions],options[:criteria]).merge!(where_func(options[:where]))
383
+ begin
384
+ collection.find(criteria).count()
385
+ rescue => ex
386
+ if ex.to_s =~ /Error with count command.*ns missing/
387
+ # Return 0 because we will graciously assume that we are being
388
+ # called from a subclass that has been initialized properly, and
389
+ # is therefore mentioned in the schema.
390
+ 0
391
+ else
392
+ raise ex
393
+ end
394
+ end
395
+ end
396
+
397
+ def sum(column)
398
+ x = self.find(:all, :select=>column)
399
+ x.map {|p1| p1[column.to_sym]}.compact.inject(0) { |s,v| s += v }
400
+ end
401
+
402
+ # Deletes the record with the given id from the collection.
403
+ def delete(id)
404
+ collection.remove({:_id => id})
405
+ end
406
+ alias_method :remove, :delete
407
+
408
+ # Load the object with +id+ and delete it.
409
+ def destroy(id)
410
+ id.is_a?(Array) ? id.each { |oid| destroy(oid) } : find(id).destroy
411
+ end
412
+
413
+ # Not yet implemented.
414
+ def update_all(updates, conditions = nil)
415
+ # TODO
416
+ raise "not yet implemented"
417
+ end
418
+
419
+ # Destroy all objects that match +conditions+. Warning: if
420
+ # +conditions+ is +nil+, all records in the collection will be
421
+ # destroyed.
422
+ def destroy_all(conditions = nil)
423
+ find(:all, :conditions => conditions).each { |object| object.destroy }
424
+ end
425
+
426
+ # Deletes all records that match +condition+, which can be a
427
+ # Mongo-style hash or an ActiveRecord-like hash. Examples:
428
+ # Person.destroy_all "name like '%fred%' # SQL WHERE clause
429
+ # Person.destroy_all ["name = ?", 'Fred'] # Rails condition
430
+ # Person.destroy_all {:name => 'Fred'} # Mongo hash
431
+ def delete_all(conditions=nil)
432
+ collection.remove(criteria_from(conditions))
433
+ end
434
+
435
+ # Creates, saves, and returns a new database object.
436
+ def create(values_hash)
437
+ object = self.new(values_hash)
438
+ object.save
439
+ object
440
+ end
441
+
442
+ # Finds the record from the passed +id+, instantly saves it with the passed +attributes+ (if the validation permits it),
443
+ # and returns it. If the save fails under validations, the unsaved object is still returned.
444
+ #
445
+ # The arguments may also be given as arrays in which case the update method is called for each pair of +id+ and
446
+ # +attributes+ and an array of objects is returned.
447
+ # =>
448
+ # Example of updating one record:
449
+ # Person.update(15, {:user_name => 'Samuel', :group => 'expert'})
450
+ #
451
+ # Example of updating multiple records:
452
+ # people = { 1 => { "first_name" => "David" }, 2 => { "first_name" => "Jeremy"} }
453
+ # Person.update(people.keys, people.values)
454
+ def update(id, attributes)
455
+ if id.is_a?(Array)
456
+ i = -1
457
+ id.collect { |id| i += 1; update(id, attributes[i]) }
458
+ else
459
+ object = find(id)
460
+ object.update_attributes(attributes)
461
+ object
462
+ end
463
+ end
464
+
465
+ # Handles find_* methods such as find_by_name, find_all_by_shoe_size,
466
+ # and find_or_create_by_name.
467
+ def method_missing(sym, *args)
468
+ if match = /^find_(all_by|by)_([_a-zA-Z]\w*)$/.match(sym.to_s)
469
+ find_how_many = ($1 == 'all_by') ? :all : :first
470
+ field_names = $2.split(/_and_/)
471
+ super unless all_fields_exist?(field_names)
472
+ search = search_from_names_and_values(field_names, args)
473
+ self.find(find_how_many, {:conditions => search}, *args[field_names.length..-1])
474
+ elsif match = /^find_or_(initialize|create)_by_([_a-zA-Z]\w*)$/.match(sym.to_s)
475
+ create = $1 == 'create'
476
+ field_names = $2.split(/_and_/)
477
+ super unless all_fields_exist?(field_names)
478
+ search = search_from_names_and_values(field_names, args)
479
+ row = self.find(:first, {:conditions => search})
480
+ return self.new(row) if row # found
481
+ obj = self.new(search.merge(args[field_names.length] || {})) # new object using search and remainder of args
482
+ obj.save if create
483
+ obj
484
+ else
485
+ super
486
+ end
487
+ end
488
+
489
+ private
490
+
491
+ def extract_options_from_args!(args)
492
+ args.last.is_a?(Hash) ? args.pop : {}
493
+ end
494
+
495
+ # def find_initial(options)
496
+ # criteria = criteria_from(options[:conditions]).merge!(where_func(options[:where]))
497
+ # fields = fields_from(options[:select])
498
+ # row = collection.find_one(criteria, :fields => fields)
499
+ # (row.nil? || row['_id'] == nil) ? nil : self.new(row)
500
+ # end
501
+
502
+ def find_initial(options)
503
+ options[:limit] = 1
504
+ options[:order] = 'created_at asc'
505
+ row = find_every(options)
506
+ row.to_a[0]
507
+ end
508
+
509
+ def find_last(options)
510
+ options[:limit] = 1
511
+ options[:order] = 'created_at desc'
512
+ row = find_every(options)
513
+ row.to_a[0]
514
+ end
515
+
516
+ def find_every(options)
517
+ options.symbolize_keys!
518
+ criteria = criteria_from(options[:conditions], options[:criteria]).merge!(where_func(options[:where]))
519
+
520
+ find_options = {}
521
+ find_options[:fields] = fields_from(options[:select]) if options[:select]
522
+ find_options[:limit] = options[:limit].to_i if options[:limit]
523
+ find_options[:offset] = options[:offset].to_i if options[:offset]
524
+ find_options[:sort] = sort_by_from(options[:order]) if options[:order]
525
+
526
+
527
+ cursor = collection.find(criteria, find_options)
528
+
529
+ # Override cursor.next_object so it returns a new instance of this class
530
+ eval "def cursor.next_object; #{self.name}.new(super()); end"
531
+ cursor
532
+ end
533
+
534
+ def find_from_ids(ids, options)
535
+ ids = ids.to_a.flatten.compact.uniq
536
+ raise RecordNotFound, "Couldn't find #{name} without an ID" unless ids.length > 0
537
+
538
+ criteria = criteria_from(options[:conditions], options[:criteria]).merge!(where_func(options[:where]))
539
+ criteria.merge!(options[:criteria]) unless options[:criteria].nil?
540
+ criteria[:_id] = ids_clause(ids)
541
+ fields = fields_from(options[:select])
542
+
543
+ if ids.length == 1
544
+ row = collection.find_one(criteria, :fields => fields)
545
+ raise RecordNotFound, "Couldn't find #{name} with ID=#{ids[0]} #{criteria.inspect}" if row == nil || row.empty?
546
+ self.new(row)
547
+ else
548
+ find_options = {}
549
+ find_options[:fields] = fields if fields
550
+ find_options[:sort] = sort_by_from(options[:order]) if options[:order]
551
+ find_options[:limit] = options[:limit].to_i if options[:limit]
552
+ find_options[:offset] = options[:offset].to_i if options[:offset]
553
+
554
+ cursor = collection.find(criteria, find_options)
555
+
556
+ # Override cursor.next_object so it returns a new instance of this class
557
+ eval "def cursor.next_object; #{self.name}.new(super()); end"
558
+ cursor
559
+ end
560
+ end
561
+
562
+ def ids_clause(ids)
563
+ #ids.length == 1 ? ids[0].to_oid : {'$in' => ids.collect{|id| id.to_oid}}
564
+
565
+ if ids.length == 1
566
+ ids[0].is_a?(String) ? ids[0].to_oid : ids[0]
567
+ else
568
+ {'$in' => ids.collect{|id| id.is_a?(String) ? id.to_oid : id}}
569
+ end
570
+ end
571
+
572
+ # Returns true if all field_names are in @field_names.
573
+ def all_fields_exist?(field_names)
574
+ field_names.collect! {|f| f == 'id' ? '_id' : f}
575
+ (field_names - @field_names.collect{|f| f.to_s}).empty?
576
+ end
577
+
578
+ # Returns a db search hash, given field_names and values.
579
+ def search_from_names_and_values(field_names, values)
580
+ h = {}
581
+ field_names.each_with_index { |iv, i| h[iv.to_s] = values[i] }
582
+ h
583
+ end
584
+
585
+ # Given a "SymbolOrStringLikeThis", return the string "symbol_or_string_like_this".
586
+ def class_name_to_field_name(name)
587
+ name.gsub(/([A-Z])/, '_\1').downcase.sub(/^_/, '')
588
+ end
589
+
590
+ # Given a "symbol_or_string_like_this", return the string "SymbolOrStringLikeThis".
591
+ def field_name_to_class_name(name)
592
+ name = name.to_s.dup.gsub(/_([a-z])/) {$1.upcase}
593
+ name[0,1] = name[0,1].upcase
594
+ name
595
+ end
596
+
597
+ protected
598
+
599
+ # Turns array, string, or hash conditions into something useable by Mongo.
600
+ # ["name='%s' and group_id='%s'", "foo'bar", 4] returns {:name => 'foo''bar', :group_id => 4}
601
+ # "name='foo''bar' and group_id='4'" returns {:name => 'foo''bar', :group_id => 4}
602
+ # { :name => "foo'bar", :group_id => 4 } returns the hash, modified for Mongo
603
+ def criteria_from(condition, criteria={}) # :nodoc:
604
+ case condition
605
+ when Array
606
+ opts = criteria_from_array(condition)
607
+ when String
608
+ opts = criteria_from_string(condition)
609
+ when Hash
610
+ opts = criteria_from_hash(condition)
611
+ else
612
+ opts = {}
613
+ end
614
+ opts ||= {}
615
+ criteria ||= {}
616
+ opts.merge!(criteria)
617
+ end
618
+
619
+ # Substitutes values at the end of an array into the string at its
620
+ # start, sanitizing strings in the values. Then passes the string on
621
+ # to criteria_from_string.
622
+ def criteria_from_array(condition) # :nodoc:
623
+ str, *values = condition
624
+ sql = if values.first.kind_of?(Hash) and str =~ /:\w+/
625
+ replace_named_bind_variables(str, values.first)
626
+ elsif str.include?('?')
627
+ replace_bind_variables(str, values)
628
+ else
629
+ str % values.collect {|value| quote(value) }
630
+ end
631
+ criteria_from_string(sql)
632
+ end
633
+
634
+ # Turns a string into a Mongo search condition hash.
635
+ def criteria_from_string(sql) # :nodoc:
636
+ MongoRecord::SQL::Parser.parse_where(sql)
637
+ end
638
+
639
+ # Turns a hash that ActiveRecord would expect into one for Mongo.
640
+ def criteria_from_hash(condition) # :nodoc:
641
+ h = {}
642
+ condition.each { |k,v|
643
+ h[k] = case v
644
+ when Array
645
+ {'$in' => v}
646
+ when Range
647
+ {'$gte' => v.first, '$lte' => v.last}
648
+ else
649
+ v
650
+ end
651
+ }
652
+ h
653
+ end
654
+
655
+ # Returns a hash useable by Mongo for applying +func+ on the db server.
656
+ # +func+ must be +nil+ or a JavaScript expression or function in a
657
+ # string.
658
+ def where_func(func) # :nodoc:
659
+ func ? {'$where' => Mongo::Code.new(func)} : {}
660
+ end
661
+
662
+ def replace_named_bind_variables(str, h) # :nodoc:
663
+ str.gsub(/:(\w+)/) do
664
+ match = $1.to_sym
665
+ if h.include?(match)
666
+ quoted_bind_var(h[match])
667
+ else
668
+ raise PreparedStatementInvalid, "missing value for :#{match} in #{str}" # TODO this gets swallowed in find()
669
+ end
670
+ end
671
+ end
672
+
673
+ def replace_bind_variables(str, values) # :nodoc:
674
+ raise "parameter count does not match value count" unless str.count('?') == values.length
675
+ bound = values.dup
676
+ str.gsub('?') { quoted_bind_var(bound.shift) }
677
+ end
678
+
679
+ def quoted_bind_var(val) # :nodoc:
680
+ case val
681
+ when Array
682
+ val.collect{|v| quote(v)}.join(',')
683
+ else
684
+ quote(val)
685
+ end
686
+ end
687
+
688
+ # Returns value quoted if appropriate (if it's a string).
689
+ def quote(val) # :nodoc:
690
+ return val unless val.is_a?(String)
691
+ return "'#{val.gsub(/\'/, "\\\\'")}'" # " <= for Emacs font-lock
692
+ end
693
+
694
+ def fields_from(a) # :nodoc:
695
+ return nil unless a
696
+ a = [a] unless a.kind_of?(Array)
697
+ a += ['_id'] # always return _id
698
+ a.uniq.collect { |k| k.to_s }
699
+ end
700
+
701
+ def sort_by_from(option) # :nodoc:
702
+ return nil unless option
703
+ sort_by = []
704
+ case option
705
+ when Symbol # Single value
706
+ sort_by << {option.to_s => 1}
707
+ when String
708
+ # TODO order these by building an array of hashes
709
+ fields = option.split(',')
710
+ fields.each {|f|
711
+ name, order = f.split
712
+ order ||= 'asc'
713
+ sort_by << {name.to_s => sort_value_from_arg(order)}
714
+ }
715
+ when Array # Array of field names; assume ascending sort
716
+ # TODO order these by building an array of hashes
717
+ sort_by = option.collect {|o| {o.to_s => 1}}
718
+ else # Hash (order of sorts is not guaranteed)
719
+ sort_by = option.collect {|k, v| {k.to_s => sort_value_from_arg(v)}}
720
+ end
721
+ return nil unless sort_by.length > 0
722
+ sort_by
723
+ end
724
+
725
+ # Turns "asc" into 1, "desc" into -1, and other values into 1 or -1.
726
+ def sort_value_from_arg(arg) # :nodoc:
727
+ case arg
728
+ when /^asc/i
729
+ arg = 1
730
+ when /^desc/i
731
+ arg = -1
732
+ when Number
733
+ arg.to_i >= 0 ? 1 : -1
734
+ else
735
+ arg ? 1 : -1
736
+ end
737
+ end
738
+
739
+ # Overwrite the default class equality method to provide support for association proxies.
740
+ def ===(object)
741
+ object.is_a?(self)
742
+ end
743
+
744
+ end # End of class methods
745
+
746
+ public
747
+
748
+ # Initialize a new object with either a hash of values or a row returned
749
+ # from the database.
750
+ def initialize(row={})
751
+ case row
752
+ when Hash
753
+ row.each { |k, val|
754
+ k = '_id' if k == 'id' # Rails helper
755
+ init_ivar("@#{k}", val)
756
+ }
757
+ else
758
+ row.instance_variables.each { |iv|
759
+ init_ivar(iv, row.instance_variable_get(iv))
760
+ }
761
+ end
762
+ # Default values for remaining fields
763
+ (self.class.field_names + self.class.subobjects.keys).each { |iv|
764
+ iv = "@#{iv}"
765
+ instance_variable_set(iv, nil) unless instance_variable_defined?(iv)
766
+ }
767
+ self.class.arrays.keys.each { |iv|
768
+ iv = "@#{iv}"
769
+ instance_variable_set(iv, []) unless instance_variable_defined?(iv)
770
+ }
771
+ yield self if block_given?
772
+ end
773
+
774
+ # Set the id of this object. Normally not called by user code.
775
+ def id=(val); @_id = (val == '' ? nil : val); end
776
+
777
+ # Return this object's id.
778
+ def id; @_id ? @_id.to_s : nil; end
779
+
780
+ # Return true if the +comparison_object+ is the same object, or is of
781
+ # the same type and has the same id.
782
+ def ==(comparison_object)
783
+ comparison_object.equal?(self) ||
784
+ (comparison_object.instance_of?(self.class) &&
785
+ comparison_object.id == id &&
786
+ !comparison_object.new_record?)
787
+ end
788
+
789
+ # Delegate to ==
790
+ def eql?(comparison_object)
791
+ self == (comparison_object)
792
+ end
793
+
794
+ # Delegate to id in order to allow two records of the same type and id to work with something like:
795
+ # [ Person.find(1), Person.find(2), Person.find(3) ] & [ Person.find(1), Person.find(4) ] # => [ Person.find(1) ]
796
+ def hash
797
+ id.hash
798
+ end
799
+
800
+ # Rails convenience method. Return this object's id as a string.
801
+ def to_param
802
+ @_id.to_s
803
+ end
804
+
805
+ # Save self and returns true if the save was successful, false if not.
806
+ def save
807
+ create_or_update
808
+ end
809
+
810
+ # Save self and returns true if the save was successful and raises
811
+ # RecordNotSaved if not.
812
+ def save!
813
+ create_or_update || raise(RecordNotSaved)
814
+ end
815
+
816
+ # Return true if this object is new---that is, does not yet have an id.
817
+ def new_record?
818
+ @_id.nil? || self.class.collection.find_one("_id" => @_id).nil?
819
+ end
820
+
821
+ # Convert this object to a Mongo value suitable for saving to the
822
+ # database.
823
+ def to_mongo_value
824
+ h = {}
825
+ key_names = self.instance_values.keys
826
+ key_names.each {|key|
827
+ value = instance_variable_get("@#{key}").to_mongo_value
828
+ if value.instance_of? Hash and value["_ns"]
829
+ value = Mongo::DBRef.new(value["_ns"], value["_id"])
830
+ elsif value.instance_of? Array
831
+ value = value.map {|v|
832
+ if v.instance_of? Hash and v["_ns"]
833
+ Mongo::DBRef.new(v["_ns"], v["_id"])
834
+ else
835
+ v
836
+ end
837
+ }
838
+ end
839
+ h[key] = value
840
+ }
841
+ h
842
+ end
843
+
844
+ # Save self to the database and set the id.
845
+ def create
846
+ create_date = self.instance_variable_defined?("@created_at") ? self.created_at : nil
847
+ set_create_times(create_date)
848
+ @_ns = self.class.collection.name
849
+ value = to_mongo_value
850
+ @_id = self.class.collection.insert(value)
851
+ value.merge(:_id => @_id)
852
+ end
853
+
854
+ # Save self to the database. Return +false+ if there was an error,
855
+ # +self+ if all is well.
856
+ def update
857
+ set_update_times
858
+ self.class.collection.update({:_id => @_id}, to_mongo_value)
859
+ if self.class.collection.db.error?
860
+ return false
861
+ end
862
+ self
863
+ end
864
+
865
+ # Remove self from the database and set @_id to nil. If self has no
866
+ # @_id, does nothing.
867
+ def delete
868
+ if @_id
869
+ self.class.collection.remove({:_id => self._id})
870
+ @_id = nil
871
+ end
872
+ end
873
+ alias_method :remove, :delete
874
+
875
+ # Delete and freeze self.
876
+ def destroy
877
+ delete
878
+ freeze
879
+ end
880
+
881
+ def [](attr_name)
882
+ self.send(attr_name)
883
+ end
884
+
885
+ def []=(attr_name, value)
886
+ self.class.field(attr_name)
887
+ self.send(attr_name.to_s + '=', value)
888
+ end
889
+
890
+ def method_missing(sym, *args)
891
+ if self.instance_variables.include?("@#{sym}")
892
+ self.class.field(sym)
893
+ return self.send(sym)
894
+ else
895
+ super
896
+ end
897
+ end
898
+
899
+ #--
900
+ # ================================================================
901
+ # These methods exist so we can plug in ActiveRecord validation, etc.
902
+ # ================================================================
903
+ #++
904
+
905
+ # Updates a single attribute and saves the record. This is especially
906
+ # useful for boolean flags on existing records. Note: This method is
907
+ # overwritten by the Validation module that'll make sure that updates
908
+ # made with this method doesn't get subjected to validation checks.
909
+ # Hence, attributes can be updated even if the full object isn't valid.
910
+ def update_attribute(name, value)
911
+ self[name] = value
912
+ save
913
+ end
914
+
915
+ # Updates all the attributes from the passed-in Hash and saves the
916
+ # record. If the object is invalid, the saving will fail and false will
917
+ # be returned.
918
+ def update_attributes(attributes)
919
+ attributes.each do |name, value|
920
+ update_attribute(name, value)
921
+ end
922
+ end
923
+
924
+ # Updates an object just like Base.update_attributes but calls save!
925
+ # instead of save so an exception is raised if the record is invalid.
926
+ def update_attributes!(attributes)
927
+ self.attributes = attributes
928
+ save!
929
+ end
930
+
931
+ # Does nothing.
932
+ def attributes_from_column_definition; end
933
+
934
+ # ================================================================
935
+
936
+ def set_create_times(t=nil)
937
+ t ||= Time.now
938
+ t = Time.parse(t) if t.is_a?(String)
939
+ self["created_at"] = t
940
+ self["created_on"] = Time.local(t.year, t.month, t.day)
941
+ self.class.subobjects.keys.each { |iv|
942
+ val = instance_variable_get("@#{iv}")
943
+ val.send(:set_create_times, t) if val
944
+ }
945
+ end
946
+
947
+ private
948
+
949
+ def create_or_update
950
+ result = new_record? ? create : update
951
+ result != false
952
+ end
953
+
954
+ # Initialize ivar. +name+ must include the leading '@'.
955
+ def init_ivar(ivar_name, val)
956
+ sym = ivar_name[1..-1].to_sym
957
+ if self.class.subobjects.keys.include?(sym)
958
+ if val.instance_of? Mongo::DBRef
959
+ val = self.class.collection.db.dereference(val)
960
+ end
961
+ instance_variable_set(ivar_name, self.class.subobjects[sym].new(val))
962
+ elsif self.class.arrays.keys.include?(sym)
963
+ klazz = self.class.arrays[sym]
964
+ val = [val] unless val.kind_of?(Array)
965
+ instance_variable_set(ivar_name, val.collect {|v|
966
+ if v.instance_of? Mongo::DBRef
967
+ v = self.class.collection.db.dereference(v)
968
+ end
969
+ v.kind_of?(MongoRecord::Base) ? v : klazz.new(v)
970
+ })
971
+ else
972
+ instance_variable_set(ivar_name, val)
973
+ end
974
+ end
975
+
976
+ def set_update_times(t=nil)
977
+ t ||= Time.now
978
+ self["updated_at"] = t
979
+ self["updated_on"] = Time.local(t.year, t.month, t.day)
980
+ self.class.subobjects.keys.each { |iv|
981
+ val = instance_variable_get("@#{iv}")
982
+ val.send(:set_update_times, t) if val
983
+ }
984
+ end
985
+
986
+ end
987
+
988
+ end