Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

initial checkin of plugin code

  • Loading branch information...
commit f8715c7d314f0cefb301a484f7dd2bda101e5917 1 parent d3f304e
Jim Menard authored
View
0  README
No changes.
View
97 README.rdoc
@@ -0,0 +1,97 @@
+= ActiveRecord for Mongo
+
+This plugin provides an ActiveRecord connection adapter for Mongo
+(http://www.mongodb.org/).
+
+This plugin relies on the "mongo" Ruby Gem, which can be found at
+http://github.com/mongodb/mongo-ruby-driver. See the README file there for
+installation instructions.
+
+After installing this plugin, you will need a db/schema.rb file and a
+database.yml file. See below for instructions.
+
+
+== Installing
+
+As noted above, this plugin requires the "mongo" Ruby Gem.
+
+To add this plugin to your Rails app, move (or link) this directory into your
+Rails app's vendor/plugins directory and name it mongo_record. In other words,
+this README.rdoc file should be
+
+ RAILS_ROOT/vendor/plugins/mongo_record/README.rdoc
+
+
+== Schema
+
+ActiveRecord requires a schema, but Mongo is a schema-free database. This
+means that you have to supply this plugin with a schema for your Mongo
+database that ActiveRecord can use.
+
+This plugin reads the file named by ENV['SCHEMA'] or, if that is not defined,
+db/schema.rb to get the database schema the application wants to use.
+
+=== Sample schema.rb
+
+In case you need to hand-generate a schema file (for example, you don't have a
+db/schema.rb file generated by "rake db:schema:dump", here is a small sample.
+
+ ActiveRecord::Schema.define(:version => 0) do
+
+ create_table "students", :force => true do |t|
+ t.column "name", :string, :null => false
+ end
+
+ create_table "addresses", :force => true do |t|
+ t.column "student_id", :integer, :null => false
+ t.column "street", :string
+ t.column "city", :string
+ t.column "state", :string
+ t.column "postal_code", :string
+ end
+
+ add_index "addresses", ["student_id"], :name => "fk_addresses_students"
+
+ create_table "courses", :force => true do |t|
+ t.column "name", :string
+ end
+
+ create_table "scores", :force => true do |t|
+ t.column "student_id", :integer, :null => false
+ t.column "course_id", :integer, :null => false
+ t.column "grade", :float, :null => false
+ end
+
+ end
+
+
+== Sample database.yml
+
+ development:
+ adapter: mongo
+ database: foo_development
+
+ test:
+ adapter: mongo
+ database: foo_test
+
+ production:
+ adapter: mongo
+ database: foo_production
+
+
+== Copyright
+
+Copyright (C) 2009 10gen Inc.
+
+This program is free software: you can redistribute it and/or modify it
+under the terms of the GNU Affero General Public License, version 3, as
+published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful, but WITHOUT
+ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License
+for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
View
22 Rakefile
@@ -0,0 +1,22 @@
+require 'rake'
+require 'rake/testtask'
+require 'rake/rdoctask'
+
+desc 'Default: run unit tests.'
+task :default => :test
+
+desc 'Test the mongo_record plugin. Note: Mongo must be running.'
+Rake::TestTask.new(:test) do |t|
+ t.libs << 'lib'
+ t.libs << 'test'
+ t.pattern = 'test/**/*_test.rb'
+end
+
+desc 'Generate documentation for the mongo_record plugin.'
+Rake::RDocTask.new(:rdoc) do |rdoc|
+ rdoc.rdoc_dir = 'rdoc'
+ rdoc.title = 'ActiveRecord for Mongo'
+ rdoc.options << '--line-numbers' << '--inline-source'
+ rdoc.rdoc_files.include('README.rdoc')
+ rdoc.rdoc_files.include('lib/**/*.rb')
+end
View
12 init.rb
@@ -0,0 +1,12 @@
+require 'mongo'
+require 'yaml'
+require 'mongo_record/pk_factory'
+
+db_config = File.open(File.join(RAILS_ROOT, 'config/database.yml'), 'r') {|f|
+ YAML.load(f)
+}
+db_config = db_config[RAILS_ENV]
+if db_config['adapter'] == 'mongo'
+ $db = XGen::Mongo::Driver::Mongo.new(db_config['host'], db_config['port']).db(db_config['database'], :pk => MongoRecord::PKFactory.new)
+ require 'mongo_record'
+end
View
1  install.rb
@@ -0,0 +1 @@
+# Install hook code here
View
7 lib/active_record/connection_adapters/mongo_adapter.rb
@@ -0,0 +1,7 @@
+module ActiveRecord
+ class Base
+ def self.mongo_connection
+ end
+ end
+end
+
View
28 lib/mongo_record.rb
@@ -0,0 +1,28 @@
+#--
+# Copyright (C) 2009 10gen Inc.
+#
+# This program is free software: you can redistribute it and/or modify it
+# under the terms of the GNU Affero General Public License, version 3, as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License
+# for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#++
+
+require 'logger'
+require 'mongo_record/log_device'
+# Default LogDevice capped collection size is 10 Mb.
+RAILS_DEFAULT_LOGGER = Logger.new(MongoRecord::LogDevice.new("rails_log_#{ENV['RAILS_ENV']}")) unless defined?(RAILS_DEFAULT_LOGGER)
+
+# Patch Rails
+require 'mongo_record/active_record'
+
+# (Normal Rails config here).
+
+# Use $db (defined in init.rb) as the database connection
+ActiveRecord::Base.connection = $db
View
19 lib/mongo_record/active_record.rb
@@ -0,0 +1,19 @@
+#--
+# Copyright (C) 2009 10gen Inc.
+#
+# This program is free software: you can redistribute it and/or modify it
+# under the terms of the GNU Affero General Public License, version 3, as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License
+# for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#++
+
+require 'mongo_record/active_record/connection_adapters'
+require 'mongo_record/active_record/base'
+require 'mongo_record/active_record/schema'
View
403 lib/mongo_record/active_record/base.rb
@@ -0,0 +1,403 @@
+#--
+# Copyright (C) 2009 10gen Inc.
+#
+# This program is free software: you can redistribute it and/or modify it
+# under the terms of the GNU Affero General Public License, version 3, as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License
+# for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#++
+
+require 'mongo_record/sql'
+require 'mongo_record/objectid'
+require 'mongo_record/cursor'
+require 'mongo_record/convert'
+
+module ActiveRecord
+
+ # We override a number of ActiveRecord::Base methods to make it work with Mongo.
+ class Base
+
+ @@mongo_connection = nil
+
+ class << self # Class methods
+
+ # Return information about the schema defined in the file named by
+ # ENV['SCHEMA'] or, if that is not defined, db/schema.rb.
+ def collection_info
+ unless defined? @@collection_info
+ file = ENV['SCHEMA'] || 'db/schema.rb'
+ load(file)
+ @@collection_info = ActiveRecord::Schema.collection_info
+ end
+ @@collection_info
+ end
+
+ # Return the Mongo collection for this class.
+ def collection
+ connection.db.collection(table_name)
+ end
+
+ # ================ relational database connection handling ================
+
+ # Return the database connection. The default value is an
+ # ActiveRecord::ConnectionAdapters::MongoPseudoConnection that uses
+ # <code>$db</code>.
+ def connection
+ @@mongo_connection ||= ActiveRecord::ConnectionAdapters::MongoPseudoConnection.new($db)
+ end
+
+ # Set the database connection. If the connection is set to +nil+, then
+ # an ActiveRecord::ConnectionAdapters::MongoPseudoConnection that uses
+ # <code>$db</code> will be used.
+ def connection=(db)
+ @@mongo_connection = ActiveRecord::ConnectionAdapters::MongoPseudoConnection.new(db || $db)
+ end
+
+ # Does nothing.
+ def establish_connection(spec = nil); end
+
+ # Return the connection.
+ def retrieve_connection; connection; end
+
+ # Always returns +true+.
+ def connected?; true; end
+
+ # Does nothing.
+ def remove_connection; end
+
+ # ================
+
+ # Works like find(:all), but requires a complete SQL string. Examples:
+ # Post.find_by_sql "SELECT p.*, c.author FROM posts p, comments c WHERE p.id = c.post_id"
+ # Post.find_by_sql ["SELECT * FROM posts WHERE author = ? AND created > ?", author_id, start_date]
+ #
+ # Note: this method is not implemented. It will raise "not implemented".
+ def find_by_sql(sql)
+ raise "not implemented"
+ end
+
+ # Deletes the record with the given +id+ without instantiating an object first. If an array of ids is provided, all of them
+ # are deleted.
+ def delete(id)
+ collection.remove({'_id' => id})
+ end
+
+ # Updates all records with the SET-part of an SQL update statement in +updates+ and returns an integer with the number of rows updated.
+ # A subset of the records can be selected by specifying +conditions+. Example:
+ # Billing.update_all "category = 'authorized', approved = 1", "author = 'David'"
+ def update_all(updates, conditions = nil)
+ # TODO
+ raise "not implemented"
+# sql = "UPDATE #{table_name} SET #{sanitize_sql(updates)} "
+# add_conditions!(sql, conditions, scope(:find))
+# connection.update(sql, "#{name} Update")
+ end
+
+ # Deletes all the records that match the +condition+ without instantiating the objects first (and hence not
+ # calling the destroy method). Example:
+ # Post.delete_all "person_id = 5 AND (category = 'Something' OR category = 'Else')"
+ def delete_all(conditions = "")
+ collection.remove(MongoRecord::SQL::Parser.parse_where(conditions, true) || {})
+ end
+
+ # Count operates using two different approaches.
+ #
+ # * Count all: By not passing any parameters to count, it will return a count of all the rows for the model.
+ # * Count using options will find the row count matched by the options used.
+ #
+ # The last approach, count using options, accepts an option hash as the only parameter. The options are:
+ #
+ # * <tt>:conditions</tt> - An SQL fragment like "administrator = 1" or [ "user_name = ?", username ]. See conditions in the intro.
+ # * <tt>:joins</tt> - An SQL fragment for additional joins like "LEFT JOIN comments ON comments.post_id = id". (Rarely needed).
+ # The records will be returned read-only since they will have attributes that do not correspond to the table's columns.
+ # * <tt>:include</tt> - Named associations that should be loaded alongside using LEFT OUTER JOINs. The symbols named refer
+ # to already defined associations. When using named associations count returns the number DISTINCT items for the model you're counting.
+ # See eager loading under Associations.
+ # * <tt>:order</tt> - An SQL fragment like "created_at DESC, name" (really only used with GROUP BY calculations).
+ # * <tt>:group</tt> - An attribute name by which the result should be grouped. Uses the GROUP BY SQL-clause.
+ # * <tt>:select</tt> - By default, this is * as in SELECT * FROM, but can be changed if you for example want to do a join, but not
+ # include the joined columns.
+ # * <tt>:distinct</tt> - Set this to true to make this a distinct calculation, such as SELECT COUNT(DISTINCT posts.id) ...
+ #
+ # Examples for counting all:
+ # Person.count # returns the total count of all people
+ #
+ # Examples for count by +conditions+ and +joins+ (this has been deprecated):
+ # Person.count("age > 26") # returns the number of people older than 26
+ # Person.find("age > 26 AND job.salary > 60000", "LEFT JOIN jobs on jobs.person_id = person.id") # returns the total number of rows matching the conditions and joins fetched by SELECT COUNT(*).
+ #
+ # Examples for count with options:
+ # Person.count(:conditions => "age > 26")
+ # Person.count(:conditions => "age > 26 AND job.salary > 60000", :include => :job) # because of the named association, it finds the DISTINCT count using LEFT OUTER JOIN.
+ # Person.count(:conditions => "age > 26 AND job.salary > 60000", :joins => "LEFT JOIN jobs on jobs.person_id = person.id") # finds the number of rows matching the conditions and joins.
+ # Person.count('id', :conditions => "age > 26") # Performs a COUNT(id)
+ # Person.count(:all, :conditions => "age > 26") # Performs a COUNT(*) (:all is an alias for '*')
+ #
+ # Note: Person.count(:all) will not work because it will use :all as the condition. Use Person.count instead.
+ def count(*args)
+ # Ignore first arg if it is not a Hash
+ a = self.respond_to?(:construct_count_options_from_legacy_args) ?
+ construct_count_options_from_legacy_args(*args) :
+ construct_count_options_from_args(*args)
+ column_name, options = *a
+ find_every(options).count()
+ end
+
+ # Returns the result of an SQL statement that should only include a COUNT(*) in the SELECT part.
+ # Product.count_by_sql "SELECT COUNT(*) FROM sales s, customers c WHERE s.customer_id = c.id"
+ def count_by_sql(sql)
+ count(sql)
+ sql =~ /.*\bwhere\b(.*)/i
+ count(:conditions => $1 || "")
+ end
+
+ # Increments the specified counter by one. So <tt>DiscussionBoard.increment_counter("post_count",
+ # discussion_board_id)</tt> would increment the "post_count" counter on the board responding to discussion_board_id.
+ # This is used for caching aggregate values, so that they don't need to be computed every time. Especially important
+ # for looping over a collection where each element require a number of aggregate values. Like the DiscussionBoard
+ # that needs to list both the number of posts and comments.
+ def increment_counter(counter_name, id)
+ rec = collection.find({:_id => id}, :limit => 1).next_object
+ rec[counter_name] += 1
+ collection.insert(rec)
+ end
+
+ # Works like increment_counter, but decrements instead.
+ def decrement_counter(counter_name, id)
+ rec = collection.find({:_id => id}, :limit => 1).next_object
+ rec[counter_name] -= 1
+ collection.insert(rec)
+ end
+
+ # Defines the primary key field -- can be overridden in subclasses. Overwriting will negate any effect of the
+ # primary_key_prefix_type setting, though.
+ def primary_key
+ '_id'
+ end
+
+ def reset_sequence_name #:nodoc:
+ default = nil
+ set_sequence_name(default)
+ default
+ end
+
+ # Indicates whether the table associated with this class exists
+ def table_exists?
+ true
+ end
+
+ # Returns an array of column objects for the table associated with this class.
+ def columns
+ unless @columns
+ @columns = collection_info[table_name].columns.collect { |col_def|
+ col = ActiveRecord::ConnectionAdapters::Column.new(col_def.name, col_def.default, col_def.sql_type, col_def.null)
+ col.primary = col.name == primary_key
+ col
+ }
+ end
+ @columns
+ end
+
+ # Used to sanitize objects before they're used in an SELECT SQL-statement.
+ def sanitize(object) #:nodoc:
+ quote_value(object)
+ end
+
+ private
+
+ def find_initial(options)
+ criteria = criteria_from(options[:conditions]).merge(where_func(options[:where]))
+ fields = fields_from(options[:select])
+ row = collection.find(criteria, :fields => fields, :limit => 1).next_object
+ (row.nil? || row['_id'] == nil) ? nil : self.send(:instantiate, row)
+ end
+
+ def find_every(options)
+ criteria = criteria_from(options[:conditions]).merge(where_func(options[:where]))
+ fields = fields_from(options[:select])
+ db_cursor = collection.find(criteria, :fields => fields)
+ db_cursor.limit(options[:limit].to_i) if options[:limit]
+ db_cursor.skip(options[:offset].to_i) if options[:offset]
+ sort_by = sort_by_from(options[:order]) if options[:order]
+ db_cursor.sort(sort_by) if sort_by
+ MongoRecord::Cursor.new(db_cursor, self)
+ end
+
+ def find_from_ids(ids, options)
+ ids = ids.to_a.flatten.compact.uniq
+ criteria = criteria_from(options[:conditions]).merge(where_func(options[:where]))
+ criteria[:_id] = ids_clause(ids)
+ fields = fields_from(options[:select])
+ db_cursor = collection.find(criteria, :fields => fields)
+ sort_by = sort_by_from(options[:order]) if options[:order]
+ db_cursor.sort(sort_by) if sort_by
+ ids.length == 1 ? instantiate(db_cursor.next_object) : MongoRecord::Cursor.new(db_cursor, self)
+ end
+
+ def ids_clause(ids)
+ ids.length == 1 ? ids[0].to_oid : {:$in => ids.collect{|id| id.to_oid}}
+ end
+
+ # Turns array, string, or hash conditions into something useable by Mongo.
+ # ["name='%s' and group_id='%s'", "foo'bar", 4] returns {:name => 'foo''bar', :group_id => 4}
+ # "name='foo''bar' and group_id='4'" returns {:name => 'foo''bar', :group_id => 4}
+ # { :name => "foo'bar", :group_id => 4 } returns the hash, modified for Mongo
+ def criteria_from(condition) # :nodoc:
+ case condition
+ when Array
+ criteria_from_array(condition)
+ when String
+ criteria_from_string(condition)
+ when Hash
+ criteria_from_hash(condition)
+ else
+ {}
+ end
+ end
+
+ # Substitutes values at the end of an array into the string at its
+ # start, sanitizing strings in the values. Then passes the string on
+ # to criteria_from_string.
+ def criteria_from_array(condition) # :nodoc:
+ str, *values = condition
+ sql = if values.first.kind_of?(Hash) and str =~ /:\w+/
+ replace_named_bind_variables(str, values.first)
+ elsif str.include?('?')
+ replace_bind_variables(str, values)
+ else
+ str % values.collect {|value| quote_value(value) }
+ end
+ criteria_from_string(sql)
+ end
+
+ # Turns a string into a Mongo search condition hash.
+ def criteria_from_string(sql) # :nodoc:
+ MongoRecord::SQL::Parser.parse_where(sql, true)
+ end
+
+ # Turns a hash that ActiveRecord would expect into one for Mongo.
+ def criteria_from_hash(condition) # :nodoc:
+ h = {}
+ condition.each { |k,v|
+ h[k] = case v
+ when Array
+ {:$in => k == 'id' || k == '_id' ? v.collect{ |val| val.to_oid} : v} # if id, can't pass in string; must be ObjectId
+ when Range
+ {:$gte => v.first, :$lte => v.last}
+ else
+ v
+ end
+ }
+ h
+ end
+
+ # Returns a hash useable by Mongo for applying +func+ on the db
+ # server. +func+ must be a JavaScript function in a string.
+ def where_func(func) # :nodoc:
+ func ? {:$where => func} : {}
+ end
+
+ def fields_from(a) # :nodoc:
+ return nil unless a
+ a = [a] unless a.kind_of?(Array)
+ return nil unless a.length > 0
+ fields = {}
+ a.each { |k| fields[k.to_sym] = 1 }
+ fields
+ end
+
+ def sort_by_from(option) # :nodoc:
+ return nil unless option
+ sort_by = []
+ case option
+ when Symbol # Single value
+ sort_by << {option.to_sym => 1}
+ when String
+ # TODO order these by building an array of hashes
+ fields = option.split(',')
+ fields.each {|f|
+ name, order = f.split
+ order ||= 'asc'
+ sort_by << {name.to_sym => sort_value_from_arg(order)}
+ }
+ when Array # Array of field names; assume ascending sort
+ # TODO order these by building an array of hashes
+ sort_by = option.collect {|o| {o.to_sym => 1}}
+ else # Hash (order of sorts is not guaranteed)
+ sort_by = option.collect {|k, v| {k.to_sym => sort_value_from_arg(v)}}
+ end
+ return nil unless sort_by.length > 0
+ sort_by
+ end
+
+ # Turns "asc" into 1, "desc" into -1, and other values into 1 or -1.
+ def sort_value_from_arg(arg) # :nodoc:
+ case arg
+ when /^asc/i
+ arg = 1
+ when /^desc/i
+ arg = -1
+ when Number
+ arg.to_i >= 0 ? 1 : -1
+ else
+ arg ? 1 : -1
+ end
+ end
+
+ # Default implementation doesn't work for "_id".
+ def all_attributes_exists?(attribute_names)
+ attribute_names.collect! {|n| n == 'id' ? '_id' : n}
+ attribute_names.all? { |name| column_methods_hash.include?(name.to_sym) }
+ end
+
+ end # End of class methods
+
+ public
+
+ # Deletes the record in the database and freezes this instance to reflect that no changes should
+ # be made (since they can't be persisted).
+ def destroy
+ unless new_record?
+ self.class.collection.remove({:_id => self.id})
+ end
+ freeze
+ end
+
+ # Convert this object to a Mongo value suitable for saving to the
+ # database.
+ def to_mongo_value
+ h = {}
+ self.class.column_names.each {|iv|
+ val = read_attribute(iv)
+ h[iv] = val == nil ? nil : val.to_mongo_value
+ }
+ h
+ end
+
+ private
+
+ # Updates the associated record with values matching those of the instance attributes.
+ # Returns the number of affected rows.
+ def update_without_callbacks
+ self.class.collection.insert(to_mongo_value)
+ end
+
+ # Creates a record with values matching those of the instance attributes
+ # and returns its id.
+ def create_without_callbacks
+ row = self.class.collection.insert(to_mongo_value)
+ self.id = row['_id']
+ @new_record = false
+ id
+ end
+
+ end
+
+end
View
104 lib/mongo_record/active_record/connection_adapters.rb
@@ -0,0 +1,104 @@
+#--
+# Copyright (C) 2009 10gen Inc.
+#
+# This program is free software: you can redistribute it and/or modify it
+# under the terms of the GNU Affero General Public License, version 3, as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License
+# for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#++
+
+module ActiveRecord
+
+ module ConnectionAdapters
+
+ # Override a few methods because Mongo doesn't use SQL.
+ class ColumnDefinition
+ def sql_type; type; end
+ def to_sql; ''; end
+ def add_column_options!(sql, options); ''; end
+ end
+
+ # Override a few methods because Mongo doesn't use SQL.
+ class TableDefinition
+ def native; {}; end
+ end
+
+ # The database connection object used by ActiveRecord to talk to Mongo.
+ # Most of the actual communications with the database happens in the
+ # mongo_record modifications to ActiveRecord::Base, because it is that
+ # class (and its subclasses) that know what collection to talk to.
+ class MongoPseudoConnection
+
+ attr_reader :db
+
+ def initialize(db)
+ @runtime = 0
+ @db = db
+ end
+
+ # We output all unknown method calls to $stderr. There shouldn't be
+ # many.
+ def method_missing(sym, *args)
+ if $DEBUG
+ $stderr.puts "#{sym}(#{args.inspect}) sent to conn"
+ caller(0).each { |s| $stderr.puts s }
+ end
+ end
+
+ # Return a quoted value.
+ def quote(val, column=nil)
+ return val unless val.is_a?(String)
+ "'#{val.gsub(/\'/, "\\\\'")}'" # " <= for Emacs font-lock
+ end
+
+ # Return a quoted table name.
+ def quote_table_name(str)
+ str.to_s
+ end
+
+ # Return a quoted column name.
+ def quote_column_name(str)
+ str.to_s
+ end
+
+ # Used by ActiveRecord to record statement runtimes.
+ def reset_runtime
+ rt, @runtime = @runtime, 0
+ rt
+ end
+
+ # Return the alias for +table_name+.
+ def table_alias_for(table_name)
+ table_name.gsub(/\./, '_')
+ end
+
+ # Return +false+.
+ def supports_count_distinct?
+ false
+ end
+
+ # Transactions are not yet supported by Mongo, so this method simply
+ # yields to the given block.
+ def transaction(start_db_transaction=true)
+ yield
+ end
+
+ # Enable the query cache within the block. Ignored.
+ def cache
+ yield
+ end
+
+ # Disable the query cache within the block. Ignored.
+ def uncached
+ yield
+ end
+ end
+ end
+end
View
55 lib/mongo_record/active_record/schema.rb
@@ -0,0 +1,55 @@
+#--
+# Copyright (C) 2009 10gen Inc.
+#
+# This program is free software: you can redistribute it and/or modify it
+# under the terms of the GNU Affero General Public License, version 3, as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License
+# for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#++
+
+module ActiveRecord
+
+ # The Mongo implementation of ActiveRecord needs to read the file named by
+ # ENV['SCHEMA'] or, if that is not defined, db/schema.rb to get the database
+ # schema the application wants to use, because Mongo is schema-free.
+ #
+ # Here we override some ActiveRecord::Schema methods here that get used when
+ # reading the schema file.
+ #
+ # Since this class is only used by the Mongo ActiveRecord code to read a
+ # schema file, we don't have to worry about handling database and table
+ # modification statements like drop_table or remove_column.
+ class Schema
+
+ cattr_reader :collection_info
+ @@collection_info = {}
+
+ class << self
+
+ def define(info={}, &block)
+ self.verbose = false
+ @collection_info = {}
+ instance_eval(&block)
+ end
+
+ def create_table(name, options)
+ t = ActiveRecord::ConnectionAdapters::TableDefinition.new(self)
+ t.primary_key('_id')
+ @@collection_info[name] = t
+ yield t
+ end
+
+ def add_index(table_name, column_name, options = {})
+ ActiveRecord::Base.connection.db.collection(table_name).create_index(column_name, column_name)
+ end
+ end
+
+ end
+end
View
22 lib/mongo_record/convert.rb
@@ -0,0 +1,22 @@
+class Object
+ # Convert an Object to a Mongo value. Used when saving data to Mongo.
+ def to_mongo_value
+ self
+ end
+end
+
+class Array
+ # Convert an Array to a Mongo value. Used when saving data to Mongo.
+ def to_mongo_value
+ self.collect {|v| v.to_mongo_value}
+ end
+end
+
+class Hash
+ # Convert an Hash to a Mongo value. Used when saving data to Mongo.
+ def to_mongo_value
+ h = {}
+ self.each {|k,v| h[k] = v.to_mongo_value}
+ h
+ end
+end
View
39 lib/mongo_record/cursor.rb
@@ -0,0 +1,39 @@
+module MongoRecord
+
+ # A wrapper around a Mongo database cursor. MongoRecord::Cursor is
+ # Enumerable.
+ #
+ # Example:
+ # Person.find(:all).sort({:created_on => 1}).each { |p| puts p.to_s }
+ # n = Thing.find(:all).count()
+ # # note that you can just call Thing.count() instead
+ #
+ # The sort, limit, and skip methods must be called before resolving the
+ # quantum state of a cursor.
+ #
+ # See ActiveRecord::Base#find for more information.
+ class Cursor
+ include Enumerable
+
+ # Forward missing methods to the cursor itself.
+ def method_missing(sym, *args, &block)
+ return @cursor.send(sym, *args)
+ end
+
+ def initialize(db_cursor, model_class)
+ @cursor, @model_class = db_cursor, model_class
+ end
+
+ # Iterate over the records returned by the query. Each row is turned
+ # into the proper ActiveRecord::Base subclass instance.
+ def each
+ @cursor.each { |row| yield @model_class.send(:instantiate, row) }
+ end
+
+ # Sort, limit, and skip methods that return self (the cursor) instead of
+ # whatever those methods return.
+ %w(sort limit skip).each { |name|
+ eval "def #{name}(*args); @cursor.#{name}(*args); return self; end"
+ }
+ end
+end
View
114 lib/mongo_record/log_device.rb
@@ -0,0 +1,114 @@
+#--
+# Copyright (C) 2008 10gen Inc.
+#
+# This program is free software: you can redistribute it and/or modify it
+# under the terms of the GNU Affero General Public License, version 3, as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License
+# for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#++
+
+module MongoRecord
+
+ # A destination for Ruby's built-in Logger class. It writes log messages
+ # to a Mongo database collection. Each item in the collection consists
+ # of two fields (besides the _id): +time+ and +msg+. +time+ is
+ # automatically generated when +write+ is called.
+ #
+ # If we are running outside of the cloud, all log messages are echoed to
+ # $stderr.
+ #
+ # The collection is capped, which means after the limit is reached old
+ # records are deleted when new ones are inserted. See the new method and the
+ # Mongo documentation for details.
+ #
+ # Example:
+ #
+ # logger = Logger.new(MongoRecord::LogDevice('my_log_name'))
+ #
+ # The database connection defaults to the global $db. You can set the
+ # connection using MongoRecord::LogDevice.connection= and read it with
+ # MongoRecord::LogDevice.connection.
+ #
+ # # Set the connection to something besides $db
+ # MongoRecord::LogDevice.connection = connect('my-database')
+ class LogDevice
+
+ DEFAULT_CAP_SIZE = (10 * 1024 * 1024)
+
+ @@connection = nil
+
+ class << self # Class methods
+
+ # Return the database connection. The default value is <code>$db</code>.
+ def connection
+ conn = @@connection || $db
+ raise "connection not defined" unless conn
+ conn
+ end
+
+ # Set the database connection. If the connection is set to +nil+, then
+ # <code>$db</code> will be used.
+ def connection=(val)
+ @@connection = val
+ end
+
+ end
+
+ # +name+ is the name of the Mongo database collection that will hold all
+ # log messages. +options+ is a hash that may have the following entries:
+ #
+ # <code>:size</code> - Optional. The max size of the collection, in
+ # bytes. If it is nil or negative then +DEFAULT_CAP_SIZE+ is used.
+ #
+ # <code>:max</code> - Optional. Specifies the maximum number of log
+ # records, after which the oldest items are deleted as new ones are
+ # inserted.
+ #
+ # Note: a non-nil :max requires a :size value. The collection will never
+ # grow above :size. If you leave :size nil then it will be
+ # +DEFAULT_CAP_SIZE+.
+ #
+ # Note: once a capped collection has been created, you can't redefine the
+ # size or max falues for that collection. To do so, you must drop and
+ # recreate (or let a LogDevice object recreate) the collection.
+ def initialize(name, options = {})
+ @collection_name = name
+ options[:size] ||= DEFAULT_CAP_SIZE
+ options[:size] = DEFAULT_CAP_SIZE if options[:size] <= 0
+ options[:capped] = true
+
+ # It's OK to call createCollection if the collection already exists.
+ # Size and max won't change, though.
+ #
+ # Note we can't use the name "create_collection" because a DB JSObject
+ # does not have normal keys and returns collection objects as the value
+ # of all unknown names.
+ self.class.connection.createCollection(@collection_name, options)
+
+ # If we are running outside of the cloud, echo all log messages to
+ # $stderr. If app_context is nil we are outside the cloud, too, but
+ # we don't write to the console because if app_context is null then
+ # we are probably running unit tests.
+ app_context = $scope['__instance__']
+ @console = app_context != nil && app_context.getEnvironmentName() == nil
+ end
+
+ # Write a log message to the database. We save the message and a timestamp.
+ def write(str)
+ $stderr.puts str if @console
+ self.class.connection[@collection_name].save({:time => Time.now, :msg => str})
+ end
+
+ # Close the log. This method is a sham. Nothing happens. You may
+ # continue to use this LogDevice.
+ def close
+ end
+ end
+end
View
46 lib/mongo_record/objectid.rb
@@ -0,0 +1,46 @@
+#--
+# Copyright (C) 2009 10gen Inc.
+#
+# This program is free software: you can redistribute it and/or modify it
+# under the terms of the GNU Affero General Public License, version 3, as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License
+# for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#++
+
+require 'mongo/types/objectid'
+
+class String
+ # Convert this String to an ObjectID.
+ def to_oid
+ XGen::Mongo::Driver::ObjectID.from_string(self)
+ end
+end
+
+# Normally, you don't have to worry about ObjectIDs. You can treat _id values
+# as strings and this code will convert them for you.
+class XGen::Mongo::Driver::ObjectID
+ # Convert this object to an ObjectId.
+ def to_oid
+ self
+ end
+
+ # Tells Marshal how to dump this object. This was used in code that stored
+ # sessions in Mongo. It is unused for now.
+ def marshal_dump
+ to_s
+ end
+
+ # Tells Marshal how to load this object. This was used in code that stored
+ # sessions in Mongo. It is unused for now.
+ def marshal_load(oid)
+ XGen::Mongo::Driver::ObjectID.from_string(oid)
+ end
+end
+
View
10 lib/mongo_record/pk_factory.rb
@@ -0,0 +1,10 @@
+module MongoRecord
+ class PKFactory
+ def create_pk(row)
+ return row if row[:_id]
+ row.delete(:_id) # in case it exists but the value is nil
+ row['_id'] ||= XGen::Mongo::Driver::ObjectID.new
+ row
+ end
+ end
+end
View
237 lib/mongo_record/sql.rb
@@ -0,0 +1,237 @@
+#--
+# Copyright (C) 2008 10gen Inc.
+#
+# This program is free software: you can redistribute it and/or modify it
+# under the terms of the GNU Affero General Public License, version 3, as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License
+# for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#++
+
+module MongoRecord
+
+ module SQL
+
+ # A simple tokenizer for SQL.
+ class Tokenizer
+
+ attr_reader :sql
+
+ def initialize(sql)
+ @sql = sql
+ @length = sql.length
+ @pos = 0
+ @extra_tokens = []
+ end
+
+ # Push +tok+ onto the stack.
+ def add_extra_token(tok)
+ @extra_tokens.push(tok)
+ end
+
+ # Skips whitespace, setting @pos to the position of the next
+ # non-whitespace character. If there is none, @pos will == @length.
+ def skip_whitespace
+ while @pos < @length && [" ", "\n", "\r", "\t"].include?(@sql[@pos,1])
+ @pos += 1
+ end
+ end
+
+ # Return +true+ if there are more non-whitespace characters.
+ def more?
+ skip_whitespace
+ @pos < @length
+ end
+
+ # Return the next string without its surrounding quotes. Assumes we have
+ # already seen a quote character.
+ def next_string(c)
+ q = c
+ @pos += 1
+ t = ''
+ while @pos < @length
+ c = @sql[@pos, 1]
+ case c
+ when q
+ if @pos + 1 < @length && @sql[@pos + 1, 1] == q # double quote
+ t += q
+ @pos += 1
+ else
+ @pos += 1
+ return t
+ end
+ when '\\'
+ @pos += 1
+ return t if @pos >= @length
+ t << @sql[@pos, 1]
+ else
+ t << c
+ end
+ @pos += 1
+ end
+ raise "unterminated string in SQL: #{@sql}"
+ end
+
+ # Return +true+ if the next character is a legal starting identifier
+ # character.
+ def identifier_char?(c)
+ c =~ /[\.a-zA-Z0-9_]/ ? true : false
+ end
+
+ # Return +true+ if +c+ is a single or double quote character.
+ def quote?(c)
+ c == '"' || c == "'"
+ end
+
+ # Return the next token, or +nil+ if there are no more.
+ def next_token
+ return @extra_tokens.pop unless @extra_tokens.empty?
+
+ skip_whitespace
+ c = @sql[@pos, 1]
+ return next_string(c) if quote?(c)
+
+ first_is_identifier_char = identifier_char?(c)
+ t = c
+ @pos += 1
+ while @pos < @length
+ c = @sql[@pos, 1]
+ break if c == ' '
+
+ this_is_identifier_char = identifier_char?(c)
+ break if first_is_identifier_char != this_is_identifier_char && @length > 0
+ break if !this_is_identifier_char && quote?(c)
+
+ t << c
+ @pos += 1
+ end
+
+ case t
+ when ''
+ nil
+ when /^\d+$/
+ t.to_i
+ else
+ t
+ end
+ end
+
+ end
+
+ # Only parses simple WHERE clauses right now. The parser returns a query
+ # Hash suitable for use by Mongo.
+ class Parser
+
+ # Parse a WHERE clause (without the "WHERE") ane return a query Hash
+ # suitable for use by Mongo.
+ def self.parse_where(sql, remove_table_names=false)
+ Parser.new(Tokenizer.new(sql)).parse_where(remove_table_names)
+ end
+
+ def initialize(tokenizer)
+ @tokenizer = tokenizer
+ end
+
+ # Given a regexp string like '%foo%', return a Regexp object. We set
+ # Regexp::IGNORECASE so that all regex matches are case-insensitive.
+ def regexp_from_string(str)
+ if str[0,1] == '%'
+ str = str[1..-1]
+ else
+ str = '^' + str
+ end
+
+ if str[-1,1] == '%'
+ str = str[0..-2]
+ else
+ str = str + '$'
+ end
+ Regexp.new(str, Regexp::IGNORECASE)
+ end
+
+ # Parse a WHERE clause (without the "WHERE") and return a query Hash
+ # suitable for use by Mongo.
+ def parse_where(remove_table_names=false)
+ filters = {}
+ done = false
+ while !done && @tokenizer.more?
+ name = @tokenizer.next_token
+ raise "sql parser can't handle nested stuff yet: #{@tokenizer.sql}" if name == '('
+ name.sub!(/.*\./, '') if remove_table_names # Remove "schema.table." from "schema.table.col"
+
+ op = @tokenizer.next_token
+ op += (' ' + @tokenizer.next_token) if op.downcase == 'not'
+ op = op.downcase
+
+ val = @tokenizer.next_token
+
+ case op
+ when "="
+ filters[name] = val
+ when "<"
+ filters[name] = { :$lt => val }
+ when "<="
+ filters[name] = { :$lte => val }
+ when ">"
+ filters[name] = { :$gt => val }
+ when ">="
+ filters[name] = { :$gte => val }
+ when "<>", "!="
+ filters[name] = { :$ne => val }
+ when "like"
+ filters[name] = regexp_from_string(val)
+ when "in"
+ raise "'in' must be followed by a list of values: #{@tokenizer.sql}" unless val == '('
+ filters[name] = { :$in => read_array }
+ when "between"
+ conjunction = @tokenizer.next_token.downcase
+ raise "syntax error: expected 'between X and Y', but saw '" + conjunction + "' instead of 'and'" unless conjunction == 'and'
+ val2 = @tokenizer.next_token
+ val2, val = val, val2 if val > val2 # Make sure val <= val2
+ filters[name] = { :$gte => val, :$lte => val2 }
+ else
+ raise "can't handle sql operator [#{op}] yet: #{@tokenizer.sql}"
+ end
+
+ break unless @tokenizer.more?
+
+ tok = @tokenizer.next_token.downcase
+ case tok
+ when 'and'
+ next
+ when 'or'
+ raise "sql parser can't handle ors yet: #{@tokenizer.sql}"
+ when 'order', 'group', 'limit'
+ @tokenizer.add_extra_token(tok)
+ done = true
+ else
+ raise "can't handle [#{tok}] yet"
+ end
+ end
+ filters
+ end
+
+ private
+
+ # Read and return an array of values from a clause like "('a', 'b',
+ # 'c')". We have already read the first '('.
+ def read_array
+ vals = []
+ while @tokenizer.more?
+ vals.push(@tokenizer.next_token)
+ sep = @tokenizer.next_token
+ return vals if sep == ')'
+ raise "missing ',' in 'in' list of values: #{@tokenizer.sql}" unless sep == ','
+ end
+ raise "missing ')' at end of 'in' list of values: #{@tokenizer.sql}"
+ end
+ end
+
+ end
+end
View
4 tasks/mongo_record_tasks.rake
@@ -0,0 +1,4 @@
+# desc "Explaining what the task does"
+# task :mongo_record do
+# # Task goes here
+# end
View
24 test/mongo_record_test.rb
@@ -0,0 +1,24 @@
+require 'test_helper'
+require 'mongo'
+
+class MongoRecordTest < ActiveSupport::TestCase
+
+ def setup
+ @mongo = XGen::Mongo::Driver::Mongo.new
+ end
+
+ test "can see database names" do
+ list = @mongo.database_names
+ assert_not_nil list
+ assert list.size > 0
+ assert list.include?('admin')
+ end
+
+ test "can connect to database" do
+ db = @mongo.db('mongo_record_test')
+ assert_not_nil db
+ list = db.collection_names
+ assert_not_nil list
+ assert_kind_of Array, list
+ end
+end
View
4 test/test_helper.rb
@@ -0,0 +1,4 @@
+require 'rubygems'
+require 'active_support'
+require 'active_support/test_case'
+require 'test/unit'
View
1  uninstall.rb
@@ -0,0 +1 @@
+# Uninstall hook code here
Please sign in to comment.
Something went wrong with that request. Please try again.