aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib
diff options
context:
space:
mode:
authorDavid Heinemeier Hansson <david@loudthinking.com>2004-11-24 01:04:44 +0000
committerDavid Heinemeier Hansson <david@loudthinking.com>2004-11-24 01:04:44 +0000
commitdb045dbbf60b53dbe013ef25554fd013baf88134 (patch)
tree257830e3c76458c8ff3d1329de83f32b23926028 /activerecord/lib
downloadrails-db045dbbf60b53dbe013ef25554fd013baf88134.tar.gz
rails-db045dbbf60b53dbe013ef25554fd013baf88134.tar.bz2
rails-db045dbbf60b53dbe013ef25554fd013baf88134.zip
Initial
git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@4 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
Diffstat (limited to 'activerecord/lib')
-rwxr-xr-xactiverecord/lib/active_record.rb50
-rw-r--r--activerecord/lib/active_record/aggregations.rb165
-rwxr-xr-xactiverecord/lib/active_record/associations.rb576
-rw-r--r--activerecord/lib/active_record/associations/association_collection.rb129
-rw-r--r--activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb107
-rw-r--r--activerecord/lib/active_record/associations/has_many_association.rb102
-rwxr-xr-xactiverecord/lib/active_record/base.rb1051
-rwxr-xr-xactiverecord/lib/active_record/callbacks.rb337
-rwxr-xr-xactiverecord/lib/active_record/connection_adapters/abstract_adapter.rb371
-rwxr-xr-xactiverecord/lib/active_record/connection_adapters/mysql_adapter.rb131
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb170
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb105
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlserver_adapter.rb298
-rw-r--r--activerecord/lib/active_record/deprecated_associations.rb70
-rwxr-xr-xactiverecord/lib/active_record/fixtures.rb208
-rw-r--r--activerecord/lib/active_record/observer.rb71
-rw-r--r--activerecord/lib/active_record/reflection.rb126
-rw-r--r--activerecord/lib/active_record/support/class_attribute_accessors.rb43
-rw-r--r--activerecord/lib/active_record/support/class_inheritable_attributes.rb37
-rw-r--r--activerecord/lib/active_record/support/clean_logger.rb10
-rw-r--r--activerecord/lib/active_record/support/inflector.rb78
-rw-r--r--activerecord/lib/active_record/transactions.rb119
-rwxr-xr-xactiverecord/lib/active_record/validations.rb205
-rw-r--r--activerecord/lib/active_record/vendor/mysql.rb1117
-rw-r--r--activerecord/lib/active_record/vendor/simple.rb702
-rw-r--r--activerecord/lib/active_record/wrappers/yaml_wrapper.rb15
-rw-r--r--activerecord/lib/active_record/wrappings.rb59
27 files changed, 6452 insertions, 0 deletions
diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb
new file mode 100755
index 0000000000..9ce79284cd
--- /dev/null
+++ b/activerecord/lib/active_record.rb
@@ -0,0 +1,50 @@
+#--
+# Copyright (c) 2004 David Heinemeier Hansson
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#++
+
+
+$:.unshift(File.dirname(__FILE__))
+
+require 'active_record/support/clean_logger'
+
+require 'active_record/base'
+require 'active_record/observer'
+require 'active_record/validations'
+require 'active_record/callbacks'
+require 'active_record/associations'
+require 'active_record/aggregations'
+require 'active_record/transactions'
+require 'active_record/reflection'
+
+ActiveRecord::Base.class_eval do
+ include ActiveRecord::Validations
+ include ActiveRecord::Callbacks
+ include ActiveRecord::Associations
+ include ActiveRecord::Aggregations
+ include ActiveRecord::Transactions
+ include ActiveRecord::Reflection
+end
+
+require 'active_record/connection_adapters/mysql_adapter'
+require 'active_record/connection_adapters/postgresql_adapter'
+require 'active_record/connection_adapters/sqlite_adapter'
+require 'active_record/connection_adapters/sqlserver_adapter' \ No newline at end of file
diff --git a/activerecord/lib/active_record/aggregations.rb b/activerecord/lib/active_record/aggregations.rb
new file mode 100644
index 0000000000..82011018a2
--- /dev/null
+++ b/activerecord/lib/active_record/aggregations.rb
@@ -0,0 +1,165 @@
+module ActiveRecord
+ module Aggregations # :nodoc:
+ def self.append_features(base)
+ super
+ base.extend(ClassMethods)
+ end
+
+ # Active Record implements aggregation through a macro-like class method called +composed_of+ for representing attributes
+ # as value objects. It expresses relationships like "Account [is] composed of Money [among other things]" or "Person [is]
+ # composed of [an] address". Each call to the macro adds a description on how the value objects are created from the
+ # attributes of the entity object (when the entity is initialized either as a new object or from finding an existing)
+ # and how it can be turned back into attributes (when the entity is saved to the database). Example:
+ #
+ # class Customer < ActiveRecord::Base
+ # composed_of :balance, :class_name => "Money", :mapping => %w(balance amount)
+ # composed_of :address, :mapping => [ %w(address_street street), %w(address_city city) ]
+ # end
+ #
+ # The customer class now has the following methods to manipulate the value objects:
+ # * <tt>Customer#balance, Customer#balance=(money)</tt>
+ # * <tt>Customer#address, Customer#address=(address)</tt>
+ #
+ # These methods will operate with value objects like the ones described below:
+ #
+ # class Money
+ # include Comparable
+ # attr_reader :amount, :currency
+ # EXCHANGE_RATES = { "USD_TO_DKK" => 6 }
+ #
+ # def initialize(amount, currency = "USD")
+ # @amount, @currency = amount, currency
+ # end
+ #
+ # def exchange_to(other_currency)
+ # exchanged_amount = (amount * EXCHANGE_RATES["#{currency}_TO_#{other_currency}"]).floor
+ # Money.new(exchanged_amount, other_currency)
+ # end
+ #
+ # def ==(other_money)
+ # amount == other_money.amount && currency == other_money.currency
+ # end
+ #
+ # def <=>(other_money)
+ # if currency == other_money.currency
+ # amount <=> amount
+ # else
+ # amount <=> other_money.exchange_to(currency).amount
+ # end
+ # end
+ # end
+ #
+ # class Address
+ # attr_reader :street, :city
+ # def initialize(street, city)
+ # @street, @city = street, city
+ # end
+ #
+ # def close_to?(other_address)
+ # city == other_address.city
+ # end
+ #
+ # def ==(other_address)
+ # city == other_address.city && street == other_address.street
+ # end
+ # end
+ #
+ # Now it's possible to access attributes from the database through the value objects instead. If you choose to name the
+ # composition the same as the attributes name, it will be the only way to access that attribute. That's the case with our
+ # +balance+ attribute. You interact with the value objects just like you would any other attribute, though:
+ #
+ # customer.balance = Money.new(20) # sets the Money value object and the attribute
+ # customer.balance # => Money value object
+ # customer.balance.exchanged_to("DKK") # => Money.new(120, "DKK")
+ # customer.balance > Money.new(10) # => true
+ # customer.balance == Money.new(20) # => true
+ # customer.balance < Money.new(5) # => false
+ #
+ # Value objects can also be composed of multiple attributes, such as the case of Address. The order of the mappings will
+ # determine the order of the parameters. Example:
+ #
+ # customer.address_street = "Hyancintvej"
+ # customer.address_city = "Copenhagen"
+ # customer.address # => Address.new("Hyancintvej", "Copenhagen")
+ # customer.address = Address.new("May Street", "Chicago")
+ # customer.address_street # => "May Street"
+ # customer.address_city # => "Chicago"
+ #
+ # == Writing value objects
+ #
+ # Value objects are immutable and interchangeable objects that represent a given value, such as a Money object representing
+ # $5. Two Money objects both representing $5 should be equal (through methods such == and <=> from Comparable if ranking makes
+ # sense). This is unlike a entity objects where equality is determined by identity. An entity class such as Customer can
+ # easily have two different objects that both have an address on Hyancintvej. Entity identity is determined by object or
+ # relational unique identifiers (such as primary keys). Normal ActiveRecord::Base classes are entity objects.
+ #
+ # It's also important to treat the value objects as immutable. Don't allow the Money object to have its amount changed after
+ # creation. Create a new money object with the new value instead. This is examplified by the Money#exchanged_to method that
+ # returns a new value object instead of changing its own values. Active Record won't persist value objects that have been
+ # changed through other means than the writer method.
+ #
+ # The immutable requirement is enforced by Active Record by freezing any object assigned as a value object. Attempting to
+ # change it afterwards will result in a TypeError.
+ #
+ # Read more about value objects on http://c2.com/cgi/wiki?ValueObject and on the dangers of not keeping value objects
+ # immutable on http://c2.com/cgi/wiki?ValueObjectsShouldBeImmutable
+ module ClassMethods
+ # Adds the a reader and writer method for manipulating a value object, so
+ # <tt>composed_of :address</tt> would add <tt>address</tt> and <tt>address=(new_address)</tt>.
+ #
+ # Options are:
+ # * <tt>:class_name</tt> - specify the class name of the association. Use it only if that name can't be infered
+ # from the part id. So <tt>composed_of :address</tt> will by default be linked to the +Address+ class, but
+ # if the real class name is +CompanyAddress+, you'll have to specify it with this option.
+ # * <tt>:mapping</tt> - specifies a number of mapping arrays (attribute, parameter) that bind an attribute name
+ # to a constructor parameter on the value class.
+ #
+ # Option examples:
+ # composed_of :temperature, :mapping => %w(reading celsius)
+ # composed_of :balance, :class_name => "Money", :mapping => %w(balance amount)
+ # composed_of :address, :mapping => [ %w(address_street street), %w(address_city city) ]
+ def composed_of(part_id, options = {})
+ validate_options([ :class_name, :mapping ], options.keys)
+
+ name = part_id.id2name
+ class_name = options[:class_name] || name_to_class_name(name)
+ mapping = options[:mapping]
+
+ reader_method(name, class_name, mapping)
+ writer_method(name, class_name, mapping)
+ end
+
+ private
+ # Raises an exception if an invalid option has been specified to prevent misspellings from slipping through
+ def validate_options(valid_option_keys, supplied_option_keys)
+ unknown_option_keys = supplied_option_keys - valid_option_keys
+ raise(ActiveRecordError, "Unknown options: #{unknown_option_keys}") unless unknown_option_keys.empty?
+ end
+
+ def name_to_class_name(name)
+ name.capitalize.gsub(/_(.)/) { |s| $1.capitalize }
+ end
+
+ def reader_method(name, class_name, mapping)
+ module_eval <<-end_eval
+ def #{name}(force_reload = false)
+ if @#{name}.nil? || force_reload
+ @#{name} = #{class_name}.new(#{(Array === mapping.first ? mapping : [ mapping ]).collect{ |pair| "read_attribute(\"#{pair.first}\")"}.join(", ")})
+ end
+
+ return @#{name}
+ end
+ end_eval
+ end
+
+ def writer_method(name, class_name, mapping)
+ module_eval <<-end_eval
+ def #{name}=(part)
+ @#{name} = part.freeze
+ #{(Array === mapping.first ? mapping : [ mapping ]).collect{ |pair| "@attributes[\"#{pair.first}\"] = part.#{pair.last}" }.join("\n")}
+ end
+ end_eval
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb
new file mode 100755
index 0000000000..6285a59882
--- /dev/null
+++ b/activerecord/lib/active_record/associations.rb
@@ -0,0 +1,576 @@
+require 'active_record/associations/association_collection'
+require 'active_record/associations/has_many_association'
+require 'active_record/associations/has_and_belongs_to_many_association'
+require 'active_record/deprecated_associations'
+
+module ActiveRecord
+ module Associations # :nodoc:
+ def self.append_features(base)
+ super
+ base.extend(ClassMethods)
+ end
+
+ # Associations are a set of macro-like class methods for tying objects together through foreign keys. They express relationships like
+ # "Project has one Project Manager" or "Project belongs to a Portfolio". Each macro adds a number of methods to the class which are
+ # specialized according to the collection or association symbol and the options hash. It works much the same was as Ruby's own attr*
+ # methods. Example:
+ #
+ # class Project < ActiveRecord::Base
+ # belongs_to :portfolio
+ # has_one :project_manager
+ # has_many :milestones
+ # has_and_belongs_to_many :categories
+ # end
+ #
+ # The project class now has the following methods (and more) to ease the traversal and manipulation of its relationships:
+ # * <tt>Project#portfolio, Project#portfolio=(portfolio), Project#portfolio.nil?, Project#portfolio?(portfolio)</tt>
+ # * <tt>Project#project_manager, Project#project_manager=(project_manager), Project#project_manager.nil?,</tt>
+ # <tt>Project#project_manager?(project_manager), Project#build_project_manager, Project#create_project_manager</tt>
+ # * <tt>Project#milestones.empty?, Project#milestones.size, Project#milestones, Project#milestones<<(milestone),</tt>
+ # <tt>Project#milestones.delete(milestone), Project#milestones.find(milestone_id), Project#milestones.find_all(conditions),</tt>
+ # <tt>Project#milestones.build, Project#milestones.create</tt>
+ # * <tt>Project#categories.empty?, Project#categories.size, Project#categories, Project#categories<<(category1),</tt>
+ # <tt>Project#categories.delete(category1)</tt>
+ #
+ # == Example
+ #
+ # link:../examples/associations.png
+ #
+ # == Is it belongs_to or has_one?
+ #
+ # Both express a 1-1 relationship, the difference is mostly where to place the foreign key, which goes on the table for the class
+ # saying belongs_to. Example:
+ #
+ # class Post < ActiveRecord::Base
+ # has_one :author
+ # end
+ #
+ # class Author < ActiveRecord::Base
+ # belongs_to :post
+ # end
+ #
+ # The tables for these classes could look something like:
+ #
+ # CREATE TABLE posts (
+ # id int(11) NOT NULL auto_increment,
+ # title varchar default NULL,
+ # PRIMARY KEY (id)
+ # )
+ #
+ # CREATE TABLE authors (
+ # id int(11) NOT NULL auto_increment,
+ # post_id int(11) default NULL,
+ # name varchar default NULL,
+ # PRIMARY KEY (id)
+ # )
+ #
+ # == Caching
+ #
+ # All of the methods are built on a simple caching principle that will keep the result of the last query around unless specifically
+ # instructed not to. The cache is even shared across methods to make it even cheaper to use the macro-added methods without
+ # worrying too much about performance at the first go. Example:
+ #
+ # project.milestones # fetches milestones from the database
+ # project.milestones.size # uses the milestone cache
+ # project.milestones.empty? # uses the milestone cache
+ # project.milestones(true).size # fetches milestones from the database
+ # project.milestones # uses the milestone cache
+ #
+ # == Modules
+ #
+ # By default, associations will look for objects within the current module scope. Consider:
+ #
+ # module MyApplication
+ # module Business
+ # class Firm < ActiveRecord::Base
+ # has_many :clients
+ # end
+ #
+ # class Company < ActiveRecord::Base; end
+ # end
+ # end
+ #
+ # When Firm#clients is called, it'll in turn call <tt>MyApplication::Business::Company.find(firm.id)</tt>. If you want to associate
+ # with a class in another module scope this can be done by specifying the complete class name, such as:
+ #
+ # module MyApplication
+ # module Business
+ # class Firm < ActiveRecord::Base; end
+ # end
+ #
+ # module Billing
+ # class Account < ActiveRecord::Base
+ # belongs_to :firm, :class_name => "MyApplication::Business::Firm"
+ # end
+ # end
+ # end
+ #
+ # == Type safety with ActiveRecord::AssociationTypeMismatch
+ #
+ # If you attempt to assign an object to an association that doesn't match the inferred or specified <tt>:class_name</tt>, you'll
+ # get a ActiveRecord::AssociationTypeMismatch.
+ #
+ # == Options
+ #
+ # All of the association macros can be specialized through options which makes more complex cases than the simple and guessable ones
+ # possible.
+ module ClassMethods
+ # Adds the following methods for retrival and query of collections of associated objects.
+ # +collection+ is replaced with the symbol passed as the first argument, so
+ # <tt>has_many :clients</tt> would add among others <tt>has_clients?</tt>.
+ # * <tt>collection(force_reload = false)</tt> - returns an array of all the associated objects.
+ # An empty array is returned if none are found.
+ # * <tt>collection<<(object, ...)</tt> - adds one or more objects to the collection by setting their foreign keys to the collection's primary key.
+ # * <tt>collection.delete(object, ...)</tt> - removes one or more objects from the collection by setting their foreign keys to NULL. This does not destroy the objects.
+ # * <tt>collection.clear</tt> - removes every object from the collection. This does not destroy the objects.
+ # * <tt>collection.empty?</tt> - returns true if there are no associated objects.
+ # * <tt>collection.size</tt> - returns the number of associated objects.
+ # * <tt>collection.find(id)</tt> - finds an associated object responding to the +id+ and that
+ # meets the condition that it has to be associated with this object.
+ # * <tt>collection.find_all(conditions = nil, orderings = nil, limit = nil, joins = nil)</tt> - finds all associated objects responding
+ # criterias mentioned (like in the standard find_all) and that meets the condition that it has to be associated with this object.
+ # * <tt>collection.build(attributes = {})</tt> - returns a new object of the collection type that has been instantiated
+ # with +attributes+ and linked to this object through a foreign key but has not yet been saved.
+ # * <tt>collection.create(attributes = {})</tt> - returns a new object of the collection type that has been instantiated
+ # with +attributes+ and linked to this object through a foreign key and that has already been saved (if it passed the validation).
+ #
+ # Example: A Firm class declares <tt>has_many :clients</tt>, which will add:
+ # * <tt>Firm#clients</tt> (similar to <tt>Clients.find_all "firm_id = #{id}"</tt>)
+ # * <tt>Firm#clients<<</tt>
+ # * <tt>Firm#clients.delete</tt>
+ # * <tt>Firm#clients.clear</tt>
+ # * <tt>Firm#clients.empty?</tt> (similar to <tt>firm.clients.size == 0</tt>)
+ # * <tt>Firm#clients.size</tt> (similar to <tt>Client.count "firm_id = #{id}"</tt>)
+ # * <tt>Firm#clients.find</tt> (similar to <tt>Client.find_on_conditions(id, "firm_id = #{id}")</tt>)
+ # * <tt>Firm#clients.find_all</tt> (similar to <tt>Client.find_all "firm_id = #{id}"</tt>)
+ # * <tt>Firm#clients.build</tt> (similar to <tt>Client.new("firm_id" => id)</tt>)
+ # * <tt>Firm#clients.create</tt> (similar to <tt>c = Client.new("client_id" => id); c.save; c</tt>)
+ # The declaration can also include an options hash to specialize the behavior of the association.
+ #
+ # Options are:
+ # * <tt>:class_name</tt> - specify the class name of the association. Use it only if that name can't be infered
+ # from the association name. So <tt>has_many :products</tt> will by default be linked to the +Product+ class, but
+ # if the real class name is +SpecialProduct+, you'll have to specify it with this option.
+ # * <tt>:conditions</tt> - specify the conditions that the associated objects must meet in order to be included as a "WHERE"
+ # sql fragment, such as "price > 5 AND name LIKE 'B%'".
+ # * <tt>:order</tt> - specify the order in which the associated objects are returned as a "ORDER BY" sql fragment,
+ # such as "last_name, first_name DESC"
+ # * <tt>:foreign_key</tt> - specify the foreign key used for the association. By default this is guessed to be the name
+ # of this class in lower-case and "_id" suffixed. So a +Person+ class that makes a has_many association will use "person_id"
+ # as the default foreign_key.
+ # * <tt>:dependent</tt> - if set to true all the associated object are destroyed alongside this object.
+ # May not be set if :exclusively_dependent is also set.
+ # * <tt>:exclusively_dependent</tt> - if set to true all the associated object are deleted in one SQL statement without having their
+ # before_destroy callback run. This should only be used on associations that depend solely on this class and don't need to do any
+ # clean-up in before_destroy. The upside is that it's much faster, especially if there's a counter_cache involved.
+ # May not be set if :dependent is also set.
+ # * <tt>:finder_sql</tt> - specify a complete SQL statement to fetch the association. This is a good way to go for complex
+ # associations that depends on multiple tables. Note: When this option is used, +find_in_collection+ is _not_ added.
+ #
+ # Option examples:
+ # has_many :comments, :order => "posted_on"
+ # has_many :people, :class_name => "Person", :conditions => "deleted = 0", :order => "name"
+ # has_many :tracks, :order => "position", :dependent => true
+ # has_many :subscribers, :class_name => "Person", :finder_sql =>
+ # 'SELECT DISTINCT people.* ' +
+ # 'FROM people p, post_subscriptions ps ' +
+ # 'WHERE ps.post_id = #{id} AND ps.person_id = p.id ' +
+ # 'ORDER BY p.first_name'
+ def has_many(association_id, options = {})
+ validate_options([ :foreign_key, :class_name, :exclusively_dependent, :dependent, :conditions, :order, :finder_sql ], options.keys)
+ association_name, association_class_name, association_class_primary_key_name =
+ associate_identification(association_id, options[:class_name], options[:foreign_key])
+
+ require_association_class(association_class_name)
+
+ if options[:dependent] and options[:exclusively_dependent]
+ raise ArgumentError, ':dependent and :exclusively_dependent are mutually exclusive options. You may specify one or the other.' # ' ruby-mode
+ elsif options[:dependent]
+ module_eval "before_destroy '#{association_name}.each { |o| o.destroy }'"
+ elsif options[:exclusively_dependent]
+ module_eval "before_destroy { |record| #{association_class_name}.delete_all(%(#{association_class_primary_key_name} = '\#{record.id}')) }"
+ end
+
+ define_method(association_name) do |*params|
+ force_reload = params.first unless params.empty?
+ association = instance_variable_get("@#{association_name}")
+ if association.nil?
+ association = HasManyAssociation.new(self,
+ association_name, association_class_name,
+ association_class_primary_key_name, options)
+ instance_variable_set("@#{association_name}", association)
+ end
+ association.reload if force_reload
+ association
+ end
+
+ # deprecated api
+ deprecated_collection_count_method(association_name)
+ deprecated_add_association_relation(association_name)
+ deprecated_remove_association_relation(association_name)
+ deprecated_has_collection_method(association_name)
+ deprecated_find_in_collection_method(association_name)
+ deprecated_find_all_in_collection_method(association_name)
+ deprecated_create_method(association_name)
+ deprecated_build_method(association_name)
+ end
+
+ # Adds the following methods for retrival and query of a single associated object.
+ # +association+ is replaced with the symbol passed as the first argument, so
+ # <tt>has_one :manager</tt> would add among others <tt>has_manager?</tt>.
+ # * <tt>association(force_reload = false)</tt> - returns the associated object. Nil is returned if none is found.
+ # * <tt>association=(associate)</tt> - assigns the associate object, extracts the primary key, sets it as the foreign key,
+ # and saves the associate object.
+ # * <tt>association?(object, force_reload = false)</tt> - returns true if the +object+ is of the same type and has the
+ # same id as the associated object.
+ # * <tt>association.nil?</tt> - returns true if there is no associated object.
+ # * <tt>build_association(attributes = {})</tt> - returns a new object of the associated type that has been instantiated
+ # with +attributes+ and linked to this object through a foreign key but has not yet been saved.
+ # * <tt>create_association(attributes = {})</tt> - returns a new object of the associated type that has been instantiated
+ # with +attributes+ and linked to this object through a foreign key and that has already been saved (if it passed the validation).
+ #
+ # Example: An Account class declares <tt>has_one :beneficiary</tt>, which will add:
+ # * <tt>Account#beneficiary</tt> (similar to <tt>Beneficiary.find_first "account_id = #{id}"</tt>)
+ # * <tt>Account#beneficiary=(beneficiary)</tt> (similar to <tt>beneficiary.account_id = account.id; beneficiary.save</tt>)
+ # * <tt>Account#beneficiary?</tt> (similar to <tt>account.beneficiary == some_beneficiary</tt>)
+ # * <tt>Account#beneficiary.nil?</tt>
+ # * <tt>Account#build_beneficiary</tt> (similar to <tt>Beneficiary.new("account_id" => id)</tt>)
+ # * <tt>Account#create_beneficiary</tt> (similar to <tt>b = Beneficiary.new("account_id" => id); b.save; b</tt>)
+ # The declaration can also include an options hash to specialize the behavior of the association.
+ #
+ # Options are:
+ # * <tt>:class_name</tt> - specify the class name of the association. Use it only if that name can't be infered
+ # from the association name. So <tt>has_one :manager</tt> will by default be linked to the +Manager+ class, but
+ # if the real class name is +Person+, you'll have to specify it with this option.
+ # * <tt>:conditions</tt> - specify the conditions that the associated object must meet in order to be included as a "WHERE"
+ # sql fragment, such as "rank = 5".
+ # * <tt>:order</tt> - specify the order from which the associated object will be picked at the top. Specified as
+ # an "ORDER BY" sql fragment, such as "last_name, first_name DESC"
+ # * <tt>:dependent</tt> - if set to true the associated object is destroyed alongside this object
+ # * <tt>:foreign_key</tt> - specify the foreign key used for the association. By default this is guessed to be the name
+ # of this class in lower-case and "_id" suffixed. So a +Person+ class that makes a has_one association will use "person_id"
+ # as the default foreign_key.
+ #
+ # Option examples:
+ # has_one :credit_card, :dependent => true
+ # has_one :last_comment, :class_name => "Comment", :order => "posted_on"
+ # has_one :project_manager, :class_name => "Person", :conditions => "role = 'project_manager'"
+ def has_one(association_id, options = {})
+ options.merge!({ :remote => true })
+ belongs_to(association_id, options)
+
+ association_name, association_class_name, class_primary_key_name =
+ associate_identification(association_id, options[:class_name], options[:foreign_key], false)
+
+ require_association_class(association_class_name)
+
+ has_one_writer_method(association_name, association_class_name, class_primary_key_name)
+ build_method("build_", association_name, association_class_name, class_primary_key_name)
+ create_method("create_", association_name, association_class_name, class_primary_key_name)
+
+ module_eval "before_destroy '#{association_name}.destroy if has_#{association_name}?'" if options[:dependent]
+ end
+
+ # Adds the following methods for retrival and query for a single associated object that this object holds an id to.
+ # +association+ is replaced with the symbol passed as the first argument, so
+ # <tt>belongs_to :author</tt> would add among others <tt>has_author?</tt>.
+ # * <tt>association(force_reload = false)</tt> - returns the associated object. Nil is returned if none is found.
+ # * <tt>association=(associate)</tt> - assigns the associate object, extracts the primary key, and sets it as the foreign key.
+ # * <tt>association?(object, force_reload = false)</tt> - returns true if the +object+ is of the same type and has the
+ # same id as the associated object.
+ # * <tt>association.nil?</tt> - returns true if there is no associated object.
+ #
+ # Example: An Post class declares <tt>has_one :author</tt>, which will add:
+ # * <tt>Post#author</tt> (similar to <tt>Author.find(author_id)</tt>)
+ # * <tt>Post#author=(author)</tt> (similar to <tt>post.author_id = author.id</tt>)
+ # * <tt>Post#author?</tt> (similar to <tt>post.author == some_author</tt>)
+ # * <tt>Post#author.nil?</tt>
+ # The declaration can also include an options hash to specialize the behavior of the association.
+ #
+ # Options are:
+ # * <tt>:class_name</tt> - specify the class name of the association. Use it only if that name can't be infered
+ # from the association name. So <tt>has_one :author</tt> will by default be linked to the +Author+ class, but
+ # if the real class name is +Person+, you'll have to specify it with this option.
+ # * <tt>:conditions</tt> - specify the conditions that the associated object must meet in order to be included as a "WHERE"
+ # sql fragment, such as "authorized = 1".
+ # * <tt>:order</tt> - specify the order from which the associated object will be picked at the top. Specified as
+ # an "ORDER BY" sql fragment, such as "last_name, first_name DESC"
+ # * <tt>:foreign_key</tt> - specify the foreign key used for the association. By default this is guessed to be the name
+ # of the associated class in lower-case and "_id" suffixed. So a +Person+ class that makes a belongs_to association to a
+ # +Boss+ class will use "boss_id" as the default foreign_key.
+ # * <tt>:counter_cache</tt> - caches the number of belonging objects on the associate class through use of increment_counter
+ # and decrement_counter. The counter cache is incremented when an object of this class is created and decremented when it's
+ # destroyed. This requires that a column named "#{table_name}_count" (such as comments_count for a belonging Comment class)
+ # is used on the associate class (such as a Post class).
+ #
+ # Option examples:
+ # belongs_to :firm, :foreign_key => "client_of"
+ # belongs_to :author, :class_name => "Person", :foreign_key => "author_id"
+ # belongs_to :valid_coupon, :class_name => "Coupon", :foreign_key => "coupon_id",
+ # :conditions => 'discounts > #{payments_count}'
+ def belongs_to(association_id, options = {})
+ validate_options([ :class_name, :foreign_key, :remote, :conditions, :order, :dependent, :counter_cache ], options.keys)
+
+ association_name, association_class_name, class_primary_key_name =
+ associate_identification(association_id, options[:class_name], options[:foreign_key], false)
+
+ require_association_class(association_class_name)
+
+ association_class_primary_key_name = options[:foreign_key] || Inflector.underscore(Inflector.demodulize(association_class_name)) + "_id"
+
+ if options[:remote]
+ association_finder = <<-"end_eval"
+ #{association_class_name}.find_first(
+ "#{class_primary_key_name} = '\#{id}'#{options[:conditions] ? " AND " + options[:conditions] : ""}",
+ #{options[:order] ? "\"" + options[:order] + "\"" : "nil" }
+ )
+ end_eval
+ else
+ association_finder = options[:conditions] ?
+ "#{association_class_name}.find_on_conditions(#{association_class_primary_key_name}, \"#{options[:conditions]}\")" :
+ "#{association_class_name}.find(#{association_class_primary_key_name})"
+ end
+
+ has_association_method(association_name)
+ association_reader_method(association_name, association_finder)
+ belongs_to_writer_method(association_name, association_class_name, association_class_primary_key_name)
+ association_comparison_method(association_name, association_class_name)
+
+ if options[:counter_cache]
+ module_eval(
+ "after_create '#{association_class_name}.increment_counter(\"#{Inflector.pluralize(self.to_s.downcase). + "_count"}\", #{association_class_primary_key_name})" +
+ " if has_#{association_name}?'"
+ )
+
+ module_eval(
+ "before_destroy '#{association_class_name}.decrement_counter(\"#{Inflector.pluralize(self.to_s.downcase) + "_count"}\", #{association_class_primary_key_name})" +
+ " if has_#{association_name}?'"
+ )
+ end
+ end
+
+ # Associates two classes via an intermediate join table. Unless the join table is explicitly specified as
+ # an option, it is guessed using the lexical order of the class names. So a join between Developer and Project
+ # will give the default join table name of "developers_projects" because "D" outranks "P".
+ #
+ # Any additional fields added to the join table will be placed as attributes when pulling records out through
+ # has_and_belongs_to_many associations. This is helpful when have information about the association itself
+ # that you want available on retrival.
+ #
+ # Adds the following methods for retrival and query.
+ # +collection+ is replaced with the symbol passed as the first argument, so
+ # <tt>has_and_belongs_to_many :categories</tt> would add among others +add_categories+.
+ # * <tt>collection(force_reload = false)</tt> - returns an array of all the associated objects.
+ # An empty array is returned if none is found.
+ # * <tt>collection<<(object, ...)</tt> - adds one or more objects to the collection by creating associations in the join table
+ # (collection.push and collection.concat are aliases to this method).
+ # * <tt>collection.push_with_attributes(object, join_attributes)</tt> - adds one to the collection by creating an association in the join table that
+ # also holds the attributes from <tt>join_attributes</tt> (should be a hash with the column names as keys). This can be used to have additional
+ # attributes on the join, which will be injected into the associated objects when they are retrieved through the collection.
+ # (collection.concat_with_attributes is an alias to this method).
+ # * <tt>collection.delete(object, ...)</tt> - removes one or more objects from the collection by removing their associations from the join table.
+ # This does not destroy the objects.
+ # * <tt>collection.clear</tt> - removes every object from the collection. This does not destroy the objects.
+ # * <tt>collection.empty?</tt> - returns true if there are no associated objects.
+ # * <tt>collection.size</tt> - returns the number of associated objects.
+ #
+ # Example: An Developer class declares <tt>has_and_belongs_to_many :projects</tt>, which will add:
+ # * <tt>Developer#projects</tt>
+ # * <tt>Developer#projects<<</tt>
+ # * <tt>Developer#projects.delete</tt>
+ # * <tt>Developer#projects.clear</tt>
+ # * <tt>Developer#projects.empty?</tt>
+ # * <tt>Developer#projects.size</tt>
+ # * <tt>Developer#projects.find(id)</tt>
+ # The declaration may include an options hash to specialize the behavior of the association.
+ #
+ # Options are:
+ # * <tt>:class_name</tt> - specify the class name of the association. Use it only if that name can't be infered
+ # from the association name. So <tt>has_and_belongs_to_many :projects</tt> will by default be linked to the
+ # +Project+ class, but if the real class name is +SuperProject+, you'll have to specify it with this option.
+ # * <tt>:join_table</tt> - specify the name of the join table if the default based on lexical order isn't what you want.
+ # WARNING: If you're overwriting the table name of either class, the table_name method MUST be declared underneath any
+ # has_and_belongs_to_many declaration in order to work.
+ # * <tt>:foreign_key</tt> - specify the foreign key used for the association. By default this is guessed to be the name
+ # of this class in lower-case and "_id" suffixed. So a +Person+ class that makes a has_and_belongs_to_many association
+ # will use "person_id" as the default foreign_key.
+ # * <tt>:association_foreign_key</tt> - specify the association foreign key used for the association. By default this is
+ # guessed to be the name of the associated class in lower-case and "_id" suffixed. So the associated class is +Project+
+ # that makes a has_and_belongs_to_many association will use "project_id" as the default association foreign_key.
+ # * <tt>:conditions</tt> - specify the conditions that the associated object must meet in order to be included as a "WHERE"
+ # sql fragment, such as "authorized = 1".
+ # * <tt>:order</tt> - specify the order in which the associated objects are returned as a "ORDER BY" sql fragment, such as "last_name, first_name DESC"
+ # * <tt>:uniq</tt> - if set to true, duplicate associated objects will be ignored by accessors and query methods
+ # * <tt>:finder_sql</tt> - overwrite the default generated SQL used to fetch the association with a manual one
+ # * <tt>:delete_sql</tt> - overwrite the default generated SQL used to remove links between the associated
+ # classes with a manual one
+ # * <tt>:insert_sql</tt> - overwrite the default generated SQL used to add links between the associated classes
+ # with a manual one
+ #
+ # Option examples:
+ # has_and_belongs_to_many :projects
+ # has_and_belongs_to_many :nations, :class_name => "Country"
+ # has_and_belongs_to_many :categories, :join_table => "prods_cats"
+ def has_and_belongs_to_many(association_id, options = {})
+ validate_options([ :class_name, :table_name, :foreign_key, :association_foreign_key, :conditions,
+ :join_table, :finder_sql, :delete_sql, :insert_sql, :order, :uniq ], options.keys)
+ association_name, association_class_name, association_class_primary_key_name =
+ associate_identification(association_id, options[:class_name], options[:foreign_key])
+
+ require_association_class(association_class_name)
+
+ join_table = options[:join_table] ||
+ join_table_name(undecorated_table_name(self.to_s), undecorated_table_name(association_class_name))
+
+ define_method(association_name) do |*params|
+ force_reload = params.first unless params.empty?
+ association = instance_variable_get("@#{association_name}")
+ if association.nil?
+ association = HasAndBelongsToManyAssociation.new(self,
+ association_name, association_class_name,
+ association_class_primary_key_name, join_table, options)
+ instance_variable_set("@#{association_name}", association)
+ end
+ association.reload if force_reload
+ association
+ end
+
+ before_destroy_sql = "DELETE FROM #{join_table} WHERE #{association_class_primary_key_name} = '\\\#{self.id}'"
+ module_eval(%{before_destroy "self.connection.delete(%{#{before_destroy_sql}})"}) # "
+
+ # deprecated api
+ deprecated_collection_count_method(association_name)
+ deprecated_add_association_relation(association_name)
+ deprecated_remove_association_relation(association_name)
+ deprecated_has_collection_method(association_name)
+ end
+
+ private
+ # Raises an exception if an invalid option has been specified to prevent misspellings from slipping through
+ def validate_options(valid_option_keys, supplied_option_keys)
+ unknown_option_keys = supplied_option_keys - valid_option_keys
+ raise(ActiveRecord::ActiveRecordError, "Unknown options: #{unknown_option_keys}") unless unknown_option_keys.empty?
+ end
+
+ def join_table_name(first_table_name, second_table_name)
+ if first_table_name < second_table_name
+ join_table = "#{first_table_name}_#{second_table_name}"
+ else
+ join_table = "#{second_table_name}_#{first_table_name}"
+ end
+
+ table_name_prefix + join_table + table_name_suffix
+ end
+
+ def associate_identification(association_id, association_class_name, foreign_key, plural = true)
+ if association_class_name !~ /::/
+ association_class_name = type_name_with_module(
+ association_class_name ||
+ Inflector.camelize(plural ? Inflector.singularize(association_id.id2name) : association_id.id2name)
+ )
+ end
+
+ primary_key_name = foreign_key || Inflector.underscore(Inflector.demodulize(name)) + "_id"
+
+ return association_id.id2name, association_class_name, primary_key_name
+ end
+
+ def association_comparison_method(association_name, association_class_name)
+ module_eval <<-"end_eval", __FILE__, __LINE__
+ def #{association_name}?(comparison_object, force_reload = false)
+ if comparison_object.kind_of?(#{association_class_name})
+ #{association_name}(force_reload) == comparison_object
+ else
+ raise "Comparison object is a #{association_class_name}, should have been \#{comparison_object.class.name}"
+ end
+ end
+ end_eval
+ end
+
+ def association_reader_method(association_name, association_finder)
+ module_eval <<-"end_eval", __FILE__, __LINE__
+ def #{association_name}(force_reload = false)
+ if @#{association_name}.nil? || force_reload
+ begin
+ @#{association_name} = #{association_finder}
+ rescue ActiveRecord::StatementInvalid, ActiveRecord::RecordNotFound
+ nil
+ end
+ end
+
+ return @#{association_name}
+ end
+ end_eval
+ end
+
+ def has_one_writer_method(association_name, association_class_name, class_primary_key_name)
+ module_eval <<-"end_eval", __FILE__, __LINE__
+ def #{association_name}=(association)
+ if association.nil?
+ @#{association_name}.#{class_primary_key_name} = nil
+ @#{association_name}.save(false)
+ @#{association_name} = nil
+ else
+ raise ActiveRecord::AssociationTypeMismatch unless #{association_class_name} === association
+ association.#{class_primary_key_name} = id
+ association.save(false)
+ @#{association_name} = association
+ end
+ end
+ end_eval
+ end
+
+ def belongs_to_writer_method(association_name, association_class_name, association_class_primary_key_name)
+ module_eval <<-"end_eval", __FILE__, __LINE__
+ def #{association_name}=(association)
+ if association.nil?
+ @#{association_name} = self.#{association_class_primary_key_name} = nil
+ else
+ raise ActiveRecord::AssociationTypeMismatch unless #{association_class_name} === association
+ @#{association_name} = association
+ self.#{association_class_primary_key_name} = association.id
+ end
+ end
+ end_eval
+ end
+
+ def has_association_method(association_name)
+ module_eval <<-"end_eval", __FILE__, __LINE__
+ def has_#{association_name}?(force_reload = false)
+ !#{association_name}(force_reload).nil?
+ end
+ end_eval
+ end
+
+ def build_method(method_prefix, collection_name, collection_class_name, class_primary_key_name)
+ module_eval <<-"end_eval", __FILE__, __LINE__
+ def #{method_prefix + collection_name}(attributes = {})
+ association = #{collection_class_name}.new
+ association.attributes = attributes.merge({ "#{class_primary_key_name}" => id})
+ association
+ end
+ end_eval
+ end
+
+ def create_method(method_prefix, collection_name, collection_class_name, class_primary_key_name)
+ module_eval <<-"end_eval", __FILE__, __LINE__
+ def #{method_prefix + collection_name}(attributes = nil)
+ #{collection_class_name}.create((attributes || {}).merge({ "#{class_primary_key_name}" => id}))
+ end
+ end_eval
+ end
+
+ def require_association_class(class_name)
+ begin
+ require(Inflector.underscore(class_name))
+ rescue LoadError
+ if logger
+ logger.info "#{self.to_s} failed to require #{class_name}"
+ else
+ STDERR << "#{self.to_s} failed to require #{class_name}\n"
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb
new file mode 100644
index 0000000000..a60b9ddab5
--- /dev/null
+++ b/activerecord/lib/active_record/associations/association_collection.rb
@@ -0,0 +1,129 @@
+module ActiveRecord
+ module Associations
+ class AssociationCollection #:nodoc:
+ alias_method :proxy_respond_to?, :respond_to?
+ instance_methods.each { |m| undef_method m unless m =~ /(^__|^nil\?|^proxy_respond_to\?)/ }
+
+ def initialize(owner, association_name, association_class_name, association_class_primary_key_name, options)
+ @owner = owner
+ @options = options
+ @association_name = association_name
+ @association_class = eval(association_class_name)
+ @association_class_primary_key_name = association_class_primary_key_name
+ end
+
+ def method_missing(symbol, *args, &block)
+ load_collection
+ @collection.send(symbol, *args, &block)
+ end
+
+ def to_ary
+ load_collection
+ @collection.to_ary
+ end
+
+ def respond_to?(symbol)
+ proxy_respond_to?(symbol) || [].respond_to?(symbol)
+ end
+
+ def loaded?
+ !@collection.nil?
+ end
+
+ def reload
+ @collection = nil
+ end
+
+ # Add +records+ to this association. Returns +self+ so method calls may be chained.
+ # Since << flattens its argument list and inserts each record, +push+ and +concat+ behave identically.
+ def <<(*records)
+ flatten_deeper(records).each do |record|
+ raise_on_type_mismatch(record)
+ insert_record(record)
+ @collection << record if loaded?
+ end
+ self
+ end
+
+ alias_method :push, :<<
+ alias_method :concat, :<<
+
+ # Remove +records+ from this association. Does not destroy +records+.
+ def delete(*records)
+ records = flatten_deeper(records)
+ records.each { |record| raise_on_type_mismatch(record) }
+ delete_records(records)
+ records.each { |record| @collection.delete(record) } if loaded?
+ end
+
+ def destroy_all
+ each { |record| record.destroy }
+ @collection = []
+ end
+
+ def size
+ if loaded? then @collection.size else count_records end
+ end
+
+ def empty?
+ size == 0
+ end
+
+ def uniq(collection = self)
+ collection.inject([]) { |uniq_records, record| uniq_records << record unless uniq_records.include?(record); uniq_records }
+ end
+
+ alias_method :length, :size
+
+ protected
+ def loaded?
+ not @collection.nil?
+ end
+
+ def quoted_record_ids(records)
+ records.map { |record| "'#{@association_class.send(:sanitize, record.id)}'" }.join(',')
+ end
+
+ def interpolate_sql_options!(options, *keys)
+ keys.each { |key| options[key] &&= interpolate_sql(options[key]) }
+ end
+
+ def interpolate_sql(sql, record = nil)
+ @owner.send(:interpolate_sql, sql, record)
+ end
+
+ private
+ def load_collection
+ begin
+ @collection = find_all_records unless loaded?
+ rescue ActiveRecord::RecordNotFound
+ @collection = []
+ end
+ end
+
+ def raise_on_type_mismatch(record)
+ raise ActiveRecord::AssociationTypeMismatch, "#{@association_class} expected, got #{record.class}" unless record.is_a?(@association_class)
+ end
+
+
+ def load_collection_to_array
+ return unless @collection_array.nil?
+ begin
+ @collection_array = find_all_records
+ rescue ActiveRecord::StatementInvalid, ActiveRecord::RecordNotFound
+ @collection_array = []
+ end
+ end
+
+ def duplicated_records_array(records)
+ records = [records] unless records.is_a?(Array) || records.is_a?(ActiveRecord::Associations::AssociationCollection)
+ records.dup
+ end
+
+ # Array#flatten has problems with rescursive arrays. Going one level deeper solves the majority of the problems.
+ def flatten_deeper(array)
+ array.collect { |element| element.respond_to?(:flatten) ? element.flatten : element }.flatten
+ end
+ end
+ end
+end \ No newline at end of file
diff --git a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb
new file mode 100644
index 0000000000..946f238f21
--- /dev/null
+++ b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb
@@ -0,0 +1,107 @@
+module ActiveRecord
+ module Associations
+ class HasAndBelongsToManyAssociation < AssociationCollection #:nodoc:
+ def initialize(owner, association_name, association_class_name, association_class_primary_key_name, join_table, options)
+ super(owner, association_name, association_class_name, association_class_primary_key_name, options)
+
+ @association_foreign_key = options[:association_foreign_key] || Inflector.underscore(Inflector.demodulize(association_class_name.downcase)) + "_id"
+ association_table_name = options[:table_name] || @association_class.table_name(association_class_name)
+ @join_table = join_table
+ @order = options[:order] || "t.#{@owner.class.primary_key}"
+
+ interpolate_sql_options!(options, :finder_sql, :delete_sql)
+ @finder_sql = options[:finder_sql] ||
+ "SELECT t.*, j.* FROM #{association_table_name} t, #{@join_table} j " +
+ "WHERE t.#{@owner.class.primary_key} = j.#{@association_foreign_key} AND " +
+ "j.#{association_class_primary_key_name} = '#{@owner.id}' " +
+ (options[:conditions] ? " AND " + options[:conditions] : "") + " " +
+ "ORDER BY #{@order}"
+ end
+
+ # Removes all records from this association. Returns +self+ so method calls may be chained.
+ def clear
+ return self if size == 0 # forces load_collection if hasn't happened already
+
+ if sql = @options[:delete_sql]
+ each { |record| @owner.connection.execute(sql) }
+ elsif @options[:conditions]
+ sql =
+ "DELETE FROM #{@join_table} WHERE #{@association_class_primary_key_name} = '#{@owner.id}' " +
+ "AND #{@association_foreign_key} IN (#{collect { |record| record.id }.join(", ")})"
+ @owner.connection.execute(sql)
+ else
+ sql = "DELETE FROM #{@join_table} WHERE #{@association_class_primary_key_name} = '#{@owner.id}'"
+ @owner.connection.execute(sql)
+ end
+
+ @collection = []
+ self
+ end
+
+ def find(association_id = nil, &block)
+ if block_given? || @options[:finder_sql]
+ load_collection
+ @collection.find(&block)
+ else
+ if loaded?
+ find_all { |record| record.id == association_id.to_i }.first
+ else
+ find_all_records(@finder_sql.sub(/ORDER BY/, "AND j.#{@association_foreign_key} = '#{association_id}' ORDER BY")).first
+ end
+ end
+ end
+
+ def push_with_attributes(record, join_attributes = {})
+ raise_on_type_mismatch(record)
+ insert_record_with_join_attributes(record, join_attributes)
+ join_attributes.each { |key, value| record.send(:write_attribute, key, value) }
+ @collection << record if loaded?
+ self
+ end
+
+ alias :concat_with_attributes :push_with_attributes
+
+ def size
+ @options[:uniq] ? count_records : super
+ end
+
+ protected
+ def find_all_records(sql = @finder_sql)
+ records = @association_class.find_by_sql(sql)
+ @options[:uniq] ? uniq(records) : records
+ end
+
+ def count_records
+ load_collection
+ @collection.size
+ end
+
+ def insert_record(record)
+ if @options[:insert_sql]
+ @owner.connection.execute(interpolate_sql(@options[:insert_sql], record))
+ else
+ sql = "INSERT INTO #{@join_table} (#{@association_class_primary_key_name}, #{@association_foreign_key}) VALUES ('#{@owner.id}','#{record.id}')"
+ @owner.connection.execute(sql)
+ end
+ end
+
+ def insert_record_with_join_attributes(record, join_attributes)
+ attributes = { @association_class_primary_key_name => @owner.id, @association_foreign_key => record.id }.update(join_attributes)
+ sql =
+ "INSERT INTO #{@join_table} (#{@owner.send(:quoted_column_names, attributes).join(', ')}) " +
+ "VALUES (#{attributes.values.collect { |value| @owner.send(:quote, value) }.join(', ')})"
+ @owner.connection.execute(sql)
+ end
+
+ def delete_records(records)
+ if sql = @options[:delete_sql]
+ records.each { |record| @owner.connection.execute(sql) }
+ else
+ ids = quoted_record_ids(records)
+ sql = "DELETE FROM #{@join_table} WHERE #{@association_class_primary_key_name} = '#{@owner.id}' AND #{@association_foreign_key} IN (#{ids})"
+ @owner.connection.execute(sql)
+ end
+ end
+ end
+ end
+end \ No newline at end of file
diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb
new file mode 100644
index 0000000000..947862ad37
--- /dev/null
+++ b/activerecord/lib/active_record/associations/has_many_association.rb
@@ -0,0 +1,102 @@
+module ActiveRecord
+ module Associations
+ class HasManyAssociation < AssociationCollection #:nodoc:
+ def initialize(owner, association_name, association_class_name, association_class_primary_key_name, options)
+ super(owner, association_name, association_class_name, association_class_primary_key_name, options)
+ @conditions = @association_class.send(:sanitize_conditions, options[:conditions])
+
+ if options[:finder_sql]
+ @finder_sql = interpolate_sql(options[:finder_sql])
+ @counter_sql = @finder_sql.gsub(/SELECT (.*) FROM/i, "SELECT COUNT(*) FROM")
+ else
+ @finder_sql = "#{@association_class_primary_key_name} = '#{@owner.id}' #{@conditions ? " AND " + interpolate_sql(@conditions) : ""}"
+ @counter_sql = "#{@association_class_primary_key_name} = '#{@owner.id}'#{@conditions ? " AND " + interpolate_sql(@conditions) : ""}"
+ end
+ end
+
+ def create(attributes = {})
+ # Can't use Base.create since the foreign key may be a protected attribute.
+ record = build(attributes)
+ record.save
+ @collection << record if loaded?
+ record
+ end
+
+ def build(attributes = {})
+ record = @association_class.new(attributes)
+ record[@association_class_primary_key_name] = @owner.id
+ record
+ end
+
+ def find_all(runtime_conditions = nil, orderings = nil, limit = nil, joins = nil, &block)
+ if block_given? || @options[:finder_sql]
+ load_collection
+ @collection.find_all(&block)
+ else
+ @association_class.find_all(
+ "#{@association_class_primary_key_name} = '#{@owner.id}' " +
+ "#{@conditions ? " AND " + @conditions : ""} #{runtime_conditions ? " AND " + @association_class.send(:sanitize_conditions, runtime_conditions) : ""}",
+ orderings,
+ limit,
+ joins
+ )
+ end
+ end
+
+ def find(association_id = nil, &block)
+ if block_given? || @options[:finder_sql]
+ load_collection
+ @collection.find(&block)
+ else
+ @association_class.find_on_conditions(association_id,
+ "#{@association_class_primary_key_name} = '#{@owner.id}' #{@conditions ? " AND " + @conditions : ""}"
+ )
+ end
+ end
+
+ # Removes all records from this association. Returns +self+ so
+ # method calls may be chained.
+ def clear
+ @association_class.update_all("#{@association_class_primary_key_name} = NULL", "#{@association_class_primary_key_name} = '#{@owner.id}'")
+ @collection = []
+ self
+ end
+
+ protected
+ def find_all_records
+ if @options[:finder_sql]
+ @association_class.find_by_sql(@finder_sql)
+ else
+ @association_class.find_all(@finder_sql, @options[:order] ? @options[:order] : nil)
+ end
+ end
+
+ def count_records
+ if has_cached_counter?
+ @owner.send(:read_attribute, cached_counter_attribute_name)
+ elsif @options[:finder_sql]
+ @association_class.count_by_sql(@counter_sql)
+ else
+ @association_class.count(@counter_sql)
+ end
+ end
+
+ def has_cached_counter?
+ @owner.attribute_present?(cached_counter_attribute_name)
+ end
+
+ def cached_counter_attribute_name
+ "#{@association_name}_count"
+ end
+
+ def insert_record(record)
+ record.update_attribute(@association_class_primary_key_name, @owner.id)
+ end
+
+ def delete_records(records)
+ ids = quoted_record_ids(records)
+ @association_class.update_all("#{@association_class_primary_key_name} = NULL", "#{@association_class_primary_key_name} = '#{@owner.id}' AND #{@association_class.primary_key} IN (#{ids})")
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb
new file mode 100755
index 0000000000..3312d41d06
--- /dev/null
+++ b/activerecord/lib/active_record/base.rb
@@ -0,0 +1,1051 @@
+require 'active_record/support/class_attribute_accessors'
+require 'active_record/support/class_inheritable_attributes'
+require 'active_record/support/inflector'
+require 'yaml'
+
+module ActiveRecord #:nodoc:
+ class ActiveRecordError < StandardError #:nodoc:
+ end
+ class AssociationTypeMismatch < ActiveRecordError #:nodoc:
+ end
+ class SerializationTypeMismatch < ActiveRecordError #:nodoc:
+ end
+ class AdapterNotSpecified < ActiveRecordError # :nodoc:
+ end
+ class AdapterNotFound < ActiveRecordError # :nodoc:
+ end
+ class ConnectionNotEstablished < ActiveRecordError #:nodoc:
+ end
+ class ConnectionFailed < ActiveRecordError #:nodoc:
+ end
+ class RecordNotFound < ActiveRecordError #:nodoc:
+ end
+ class StatementInvalid < ActiveRecordError #:nodoc:
+ end
+
+ # Active Record objects doesn't specify their attributes directly, but rather infer them from the table definition with
+ # which they're linked. Adding, removing, and changing attributes and their type is done directly in the database. Any change
+ # is instantly reflected in the Active Record objects. The mapping that binds a given Active Record class to a certain
+ # database table will happen automatically in most common cases, but can be overwritten for the uncommon ones.
+ #
+ # See the mapping rules in table_name and the full example in link:files/README.html for more insight.
+ #
+ # == Creation
+ #
+ # Active Records accepts constructor parameters either in a hash or as a block. The hash method is especially useful when
+ # you're receiving the data from somewhere else, like a HTTP request. It works like this:
+ #
+ # user = User.new("name" => "David", "occupation" => "Code Artist")
+ # user.name # => "David"
+ #
+ # You can also use block initialization:
+ #
+ # user = User.new do |u|
+ # u.name = "David"
+ # u.occupation = "Code Artist"
+ # end
+ #
+ # And of course you can just create a bare object and specify the attributes after the fact:
+ #
+ # user = User.new
+ # user.name = "David"
+ # user.occupation = "Code Artist"
+ #
+ # == Conditions
+ #
+ # Conditions can either be specified as a string or an array representing the WHERE-part of an SQL statement.
+ # The array form is to be used when the condition input is tainted and requires sanitization. The string form can
+ # be used for statements that doesn't involve tainted data. Examples:
+ #
+ # User < ActiveRecord::Base
+ # def self.authenticate_unsafely(user_name, password)
+ # find_first("user_name = '#{user_name}' AND password = '#{password}'")
+ # end
+ #
+ # def self.authenticate_safely(user_name, password)
+ # find_first([ "user_name = '%s' AND password = '%s'", user_name, password ])
+ # end
+ # end
+ #
+ # The +authenticate_unsafely+ method inserts the parameters directly into the query and is thus susceptible to SQL-injection
+ # attacks if the +user_name+ and +password+ parameters come directly from a HTTP request. The +authenticate_safely+ method, on
+ # the other hand, will sanitize the +user_name+ and +password+ before inserting them in the query, which will ensure that
+ # an attacker can't escape the query and fake the login (or worse).
+ #
+ # == Overwriting default accessors
+ #
+ # All column values are automatically available through basic accessors on the Active Record object, but some times you
+ # want to specialize this behavior. This can be done by either by overwriting the default accessors (using the same
+ # name as the attribute) calling read_attribute(attr_name) and write_attribute(attr_name, value) to actually change things.
+ # Example:
+ #
+ # class Song < ActiveRecord::Base
+ # # Uses an integer of seconds to hold the length of the song
+ #
+ # def length=(minutes)
+ # write_attribute("length", minutes * 60)
+ # end
+ #
+ # def length
+ # read_attribute("length") / 60
+ # end
+ # end
+ #
+ # == Saving arrays, hashes, and other non-mappeable objects in text columns
+ #
+ # Active Record can serialize any object in text columns using YAML. To do so, you must specify this with a call to the class method +serialize+.
+ # This makes it possible to store arrays, hashes, and other non-mappeable objects without doing any additional work. Example:
+ #
+ # class User < ActiveRecord::Base
+ # serialize :preferences
+ # end
+ #
+ # user = User.create("preferences" => { "background" => "black", "display" => large })
+ # User.find(user.id).preferences # => { "background" => "black", "display" => large }
+ #
+ # You can also specify an optional :class_name option that'll raise an exception if a serialized object is retrieved as a
+ # descendent of a class not in the hierarchy. Example:
+ #
+ # class User < ActiveRecord::Base
+ # serialize :preferences, :class_name => "Hash"
+ # end
+ #
+ # user = User.create("preferences" => %w( one two three ))
+ # User.find(user.id).preferences # raises SerializationTypeMismatch
+ #
+ # == Single table inheritance
+ #
+ # Active Record allows inheritance by storing the name of the class in a column that by default is called "type" (can be changed
+ # by overwriting <tt>Base.inheritance_column</tt>). This means that an inheritance looking like this:
+ #
+ # class Company < ActiveRecord::Base; end
+ # class Firm < Company; end
+ # class Client < Company; end
+ # class PriorityClient < Client; end
+ #
+ # When you do Firm.create("name" => "37signals"), this record with be saved in the companies table with type = "Firm". You can then
+ # fetch this row again using Company.find_first "name = '37signals'" and it will return a Firm object.
+ #
+ # Note, all the attributes for all the cases are kept in the same table. Read more:
+ # http://www.martinfowler.com/eaaCatalog/singleTableInheritance.html
+ #
+ # == Connection to multiple databases in different models
+ #
+ # Connections are usually created through ActiveRecord::Base.establish_connection and retrieved by ActiveRecord::Base.connection.
+ # All classes inheriting from ActiveRecord::Base will use this connection. But you can also set a class-specific connection.
+ # For example, if Course is a ActiveRecord::Base, but resides in a different database you can just say Course.establish_connection
+ # and Course *and all its subclasses* will use this connection instead.
+ #
+ # This feature is implemented by keeping a connection pool in ActiveRecord::Base that is a Hash indexed by the class. If a connection is
+ # requested, the retrieve_connection method will go up the class-hierarchy until a connection is found in the connection pool.
+ #
+ # == Exceptions
+ #
+ # * +ActiveRecordError+ -- generic error class and superclass of all other errors raised by Active Record
+ # * +AdapterNotSpecified+ -- the configuration hash used in <tt>establish_connection</tt> didn't include a
+ # <tt>:adapter</tt> key.
+ # * +AdapterNotSpecified+ -- the <tt>:adapter</tt> key used in <tt>establish_connection</tt> specified an unexisting adapter
+ # (or a bad spelling of an existing one).
+ # * +AssociationTypeMismatch+ -- the object assigned to the association wasn't of the type specified in the association definition.
+ # * +SerializationTypeMismatch+ -- the object serialized wasn't of the class specified in the <tt>:class_name</tt> option of
+ # the serialize definition.
+ # * +ConnectionNotEstablished+ -- no connection has been established. Use <tt>establish_connection</tt> before querying.
+ # * +RecordNotFound+ -- no record responded to the find* method.
+ # Either the row with the given ID doesn't exist or the row didn't meet the additional restrictions.
+ # * +StatementInvalid+ -- the database server rejected the SQL statement. The precise error is added in the message.
+ # Either the record with the given ID doesn't exist or the record didn't meet the additional restrictions.
+ #
+ # *Note*: The attributes listed are class-level attributes (accessible from both the class and instance level).
+ # So it's possible to assign a logger to the class through Base.logger= which will then be used by all
+ # instances in the current object space.
+ class Base
+ include ClassInheritableAttributes
+
+ # Accepts a logger conforming to the interface of Log4r or the default Ruby 1.8+ Logger class, which is then passed
+ # on to any new database connections made and which can be retrieved on both a class and instance level by calling +logger+.
+ cattr_accessor :logger
+
+ # Returns the connection currently associated with the class. This can
+ # also be used to "borrow" the connection to do database work unrelated
+ # to any of the specific Active Records.
+ def self.connection
+ retrieve_connection
+ end
+
+ # Returns the connection currently associated with the class. This can
+ # also be used to "borrow" the connection to do database work that isn't
+ # easily done without going straight to SQL.
+ def connection
+ self.class.connection
+ end
+
+ def self.inherited(child) #:nodoc:
+ @@subclasses[self] ||= []
+ @@subclasses[self] << child
+ super
+ end
+
+ @@subclasses = {}
+
+ cattr_accessor :configurations
+ @@primary_key_prefix_type = {}
+
+ # Accessor for the prefix type that will be prepended to every primary key column name. The options are :table_name and
+ # :table_name_with_underscore. If the first is specified, the Product class will look for "productid" instead of "id" as
+ # the primary column. If the latter is specified, the Product class will look for "product_id" instead of "id". Remember
+ # that this is a global setting for all Active Records.
+ cattr_accessor :primary_key_prefix_type
+ @@primary_key_prefix_type = nil
+
+ # Accessor for the name of the prefix string to prepend to every table name. So if set to "basecamp_", all
+ # table names will be named like "basecamp_projects", "basecamp_people", etc. This is a convinient way of creating a namespace
+ # for tables in a shared database. By default, the prefix is the empty string.
+ cattr_accessor :table_name_prefix
+ @@table_name_prefix = ""
+
+ # Works like +table_name_prefix+, but appends instead of prepends (set to "_basecamp" gives "projects_basecamp",
+ # "people_basecamp"). By default, the suffix is the empty string.
+ cattr_accessor :table_name_suffix
+ @@table_name_suffix = ""
+
+ # Indicate whether or not table names should be the pluralized versions of the corresponding class names.
+ # If true, this the default table name for a +Product+ class will be +products+. If false, it would just be +product+.
+ # See table_name for the full rules on table/class naming. This is true, by default.
+ cattr_accessor :pluralize_table_names
+ @@pluralize_table_names = true
+
+ # When turned on (which is default), all associations are included using "load". This mean that any change is instant in cached
+ # environments like mod_ruby or FastCGI. When set to false, "require" is used, which is faster but requires server restart to
+ # be effective.
+ @@reload_associations = true
+ cattr_accessor :reload_associations
+
+ @@associations_loaded = []
+ cattr_accessor :associations_loaded
+
+ class << self # Class methods
+ # Returns objects for the records responding to either a specific id (1), a list of ids (1, 5, 6) or an array of ids.
+ # If only one ID is specified, that object is returned directly. If more than one ID is specified, an array is returned.
+ # Examples:
+ # Person.find(1) # returns the object for ID = 1
+ # Person.find(1, 2, 6) # returns an array for objects with IDs in (1, 2, 6)
+ # Person.find([7, 17]) # returns an array for objects with IDs in (7, 17)
+ # +RecordNotFound+ is raised if no record can be found.
+ def find(*ids)
+ ids = ids.flatten.compact.uniq
+
+ if ids.length > 1
+ ids_list = ids.map{ |id| "'#{sanitize(id)}'" }.join(", ")
+ objects = find_all("#{primary_key} IN (#{ids_list})", primary_key)
+
+ if objects.length == ids.length
+ return objects
+ else
+ raise RecordNotFound, "Couldn't find #{name} with ID in (#{ids_list})"
+ end
+ elsif ids.length == 1
+ id = ids.first
+ sql = "SELECT * FROM #{table_name} WHERE #{primary_key} = '#{sanitize(id)}'"
+ sql << " AND #{type_condition}" unless descends_from_active_record?
+
+ if record = connection.select_one(sql, "#{name} Find")
+ instantiate(record)
+ else
+ raise RecordNotFound, "Couldn't find #{name} with ID = #{id}"
+ end
+ else
+ raise RecordNotFound, "Couldn't find #{name} without an ID"
+ end
+ end
+
+ # Works like find, but the record matching +id+ must also meet the +conditions+.
+ # +RecordNotFound+ is raised if no record can be found matching the +id+ or meeting the condition.
+ # Example:
+ # Person.find_on_conditions 5, "first_name LIKE '%dav%' AND last_name = 'heinemeier'"
+ def find_on_conditions(id, conditions)
+ find_first("#{primary_key} = '#{sanitize(id)}' AND #{sanitize_conditions(conditions)}") ||
+ raise(RecordNotFound, "Couldn't find #{name} with #{primary_key} = #{id} on the condition of #{conditions}")
+ end
+
+ # Returns an array of all the objects that could be instantiated from the associated
+ # table in the database. The +conditions+ can be used to narrow the selection of objects (WHERE-part),
+ # such as by "color = 'red'", and arrangement of the selection can be done through +orderings+ (ORDER BY-part),
+ # such as by "last_name, first_name DESC". A maximum of returned objects can be specified in +limit+. Example:
+ # Project.find_all "category = 'accounts'", "last_accessed DESC", 15
+ def find_all(conditions = nil, orderings = nil, limit = nil, joins = nil)
+ sql = "SELECT * FROM #{table_name} "
+ sql << "#{joins} " if joins
+ add_conditions!(sql, conditions)
+ sql << "ORDER BY #{orderings} " unless orderings.nil?
+ sql << "LIMIT #{limit} " unless limit.nil?
+
+ find_by_sql(sql)
+ end
+
+ # Works like find_all, but requires a complete SQL string. Example:
+ # Post.find_by_sql "SELECT p.*, c.author FROM posts p, comments c WHERE p.id = c.post_id"
+ def find_by_sql(sql)
+ connection.select_all(sql, "#{name} Load").inject([]) { |objects, record| objects << instantiate(record) }
+ end
+
+ # Returns the object for the first record responding to the conditions in +conditions+,
+ # such as "group = 'master'". If more than one record is returned from the query, it's the first that'll
+ # be used to create the object. In such cases, it might be beneficial to also specify
+ # +orderings+, like "income DESC, name", to control exactly which record is to be used. Example:
+ # Employee.find_first "income > 50000", "income DESC, name"
+ def find_first(conditions = nil, orderings = nil)
+ sql = "SELECT * FROM #{table_name} "
+ add_conditions!(sql, conditions)
+ sql << "ORDER BY #{orderings} " unless orderings.nil?
+ sql << "LIMIT 1"
+
+ record = connection.select_one(sql, "#{name} Load First")
+ instantiate(record) unless record.nil?
+ end
+
+ # Creates an object, instantly saves it as a record (if the validation permits it), and returns it. If the save
+ # fail under validations, the unsaved object is still returned.
+ def create(attributes = nil)
+ object = new(attributes)
+ object.save
+ object
+ end
+
+ # Finds the record from the passed +id+, instantly saves it with the passed +attributes+ (if the validation permits it),
+ # and returns it. If the save fail under validations, the unsaved object is still returned.
+ def update(id, attributes)
+ object = find(id)
+ object.attributes = attributes
+ object.save
+ object
+ end
+
+ # Updates all records with the SET-part of an SQL update statement in +updates+. 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)
+ sql = "UPDATE #{table_name} SET #{updates} "
+ add_conditions!(sql, conditions)
+ connection.update(sql, "#{name} Update")
+ end
+
+ # Destroys the objects for all the records that matches the +condition+ by instantiating each object and calling
+ # the destroy method. Example:
+ # Person.destroy_all "last_login < '2004-04-04'"
+ def destroy_all(conditions = nil)
+ find_all(conditions).each { |object| object.destroy }
+ end
+
+ # Deletes all the records that matches the +condition+ without instantiating the objects first (and hence not
+ # calling the destroy method). Example:
+ # Post.destroy_all "person_id = 5 AND (category = 'Something' OR category = 'Else')"
+ def delete_all(conditions = nil)
+ sql = "DELETE FROM #{table_name} "
+ add_conditions!(sql, conditions)
+ connection.delete(sql, "#{name} Delete all")
+ end
+
+ # Returns the number of records that meets the +conditions+. Zero is returned if no records match. Example:
+ # Product.count "sales > 1"
+ def count(conditions = nil)
+ sql = "SELECT COUNT(*) FROM #{table_name} "
+ add_conditions!(sql, conditions)
+ count_by_sql(sql)
+ end
+
+ # Returns the result of an SQL statement that should only include a COUNT(*) in the SELECT part.
+ # Product.count "SELECT COUNT(*) FROM sales s, customers c WHERE s.customer_id = c.id"
+ def count_by_sql(sql)
+ count = connection.select_one(sql, "#{name} Count").values.first
+ return count ? count.to_i : 0
+ 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 doesn'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)
+ update_all "#{counter_name} = #{counter_name} + 1", "#{primary_key} = #{id}"
+ end
+
+ # Works like increment_counter, but decrements instead.
+ def decrement_counter(counter_name, id)
+ update_all "#{counter_name} = #{counter_name} - 1", "#{primary_key} = #{id}"
+ end
+
+ # Attributes named in this macro are protected from mass-assignment, such as <tt>new(attributes)</tt> and
+ # <tt>attributes=(attributes)</tt>. Their assignment will simply be ignored. Instead, you can use the direct writer
+ # methods to do assignment. This is meant to protect sensitive attributes to be overwritten by URL/form hackers. Example:
+ #
+ # class Customer < ActiveRecord::Base
+ # attr_protected :credit_rating
+ # end
+ #
+ # customer = Customer.new("name" => David, "credit_rating" => "Excellent")
+ # customer.credit_rating # => nil
+ # customer.attributes = { "description" => "Jolly fellow", "credit_rating" => "Superb" }
+ # customer.credit_rating # => nil
+ #
+ # customer.credit_rating = "Average"
+ # customer.credit_rating # => "Average"
+ def attr_protected(*attributes)
+ write_inheritable_array("attr_protected", attributes)
+ end
+
+ # Returns an array of all the attributes that have been protected from mass-assigment.
+ def protected_attributes # :nodoc:
+ read_inheritable_attribute("attr_protected")
+ end
+
+ # If this macro is used, only those attributed named in it will be accessible for mass-assignment, such as
+ # <tt>new(attributes)</tt> and <tt>attributes=(attributes)</tt>. This is the more conservative choice for mass-assignment
+ # protection. If you'd rather start from an all-open default and restrict attributes as needed, have a look at
+ # attr_protected.
+ def attr_accessible(*attributes)
+ write_inheritable_array("attr_accessible", attributes)
+ end
+
+ # Returns an array of all the attributes that have been made accessible to mass-assigment.
+ def accessible_attributes # :nodoc:
+ read_inheritable_attribute("attr_accessible")
+ end
+
+ # Specifies that the attribute by the name of +attr_name+ should be serialized before saving to the database and unserialized
+ # after loading from the database. The serialization is done through YAML. If +class_name+ is specified, the serialized
+ # object must be of that class on retrival or +SerializationTypeMismatch+ will be raised.
+ def serialize(attr_name, class_name = Object)
+ write_inheritable_attribute("attr_serialized", serialized_attributes.update(attr_name.to_s => class_name))
+ end
+
+ # Returns a hash of all the attributes that have been specified for serialization as keys and their class restriction as values.
+ def serialized_attributes
+ read_inheritable_attribute("attr_serialized") || { }
+ end
+
+ # Guesses the table name (in forced lower-case) based on the name of the class in the inheritance hierarchy descending
+ # directly from ActiveRecord. So if the hierarchy looks like: Reply < Message < ActiveRecord, then Message is used
+ # to guess the table name from even when called on Reply. The guessing rules are as follows:
+ #
+ # * Class name ends in "x", "ch" or "ss": "es" is appended, so a Search class becomes a searches table.
+ # * Class name ends in "y" preceded by a consonant or "qu": The "y" is replaced with "ies", so a Category class becomes a categories table.
+ # * Class name ends in "fe": The "fe" is replaced with "ves", so a Wife class becomes a wives table.
+ # * Class name ends in "lf" or "rf": The "f" is replaced with "ves", so a Half class becomes a halves table.
+ # * Class name ends in "person": The "person" is replaced with "people", so a Salesperson class becomes a salespeople table.
+ # * Class name ends in "man": The "man" is replaced with "men", so a Spokesman class becomes a spokesmen table.
+ # * Class name ends in "sis": The "i" is replaced with an "e", so a Basis class becomes a bases table.
+ # * Class name ends in "tum" or "ium": The "um" is replaced with an "a", so a Datum class becomes a data table.
+ # * Class name ends in "child": The "child" is replaced with "children", so a NodeChild class becomes a node_children table.
+ # * Class name ends in an "s": No additional characters are added or removed.
+ # * Class name doesn't end in "s": An "s" is appended, so a Comment class becomes a comments table.
+ # * Class name with word compositions: Compositions are underscored, so CreditCard class becomes a credit_cards table.
+ #
+ # Additionally, the class-level table_name_prefix is prepended to the table_name and the table_name_suffix is appended.
+ # So if you have "myapp_" as a prefix, the table name guess for an Account class becomes "myapp_accounts".
+ #
+ # You can also overwrite this class method to allow for unguessable links, such as a Mouse class with a link to a
+ # "mice" table. Example:
+ #
+ # class Mouse < ActiveRecord::Base
+ # def self.table_name() "mice" end
+ # end
+ def table_name(class_name = nil)
+ if class_name.nil?
+ class_name = class_name_of_active_record_descendant(self)
+ table_name_prefix + undecorated_table_name(class_name) + table_name_suffix
+ else
+ table_name_prefix + undecorated_table_name(class_name) + table_name_suffix
+ end
+ end
+
+ # Defines the primary key field -- can be overridden in subclasses. Overwritting will negate any effect of the
+ # primary_key_prefix_type setting, though.
+ def primary_key
+ case primary_key_prefix_type
+ when :table_name
+ Inflector.foreign_key(class_name_of_active_record_descendant(self), false)
+ when :table_name_with_underscore
+ Inflector.foreign_key(class_name_of_active_record_descendant(self))
+ else
+ "id"
+ end
+ end
+
+ # Defines the column name for use with single table inheritance -- can be overridden in subclasses.
+ def inheritance_column
+ "type"
+ end
+
+ # Turns the +table_name+ back into a class name following the reverse rules of +table_name+.
+ def class_name(table_name = table_name) # :nodoc:
+ # remove any prefix and/or suffix from the table name
+ class_name = Inflector.camelize(table_name[table_name_prefix.length..-(table_name_suffix.length + 1)])
+ class_name = Inflector.singularize(class_name) if pluralize_table_names
+ return class_name
+ end
+
+ # Returns an array of column objects for the table associated with this class.
+ def columns
+ @columns ||= connection.columns(table_name, "#{name} Columns")
+ end
+
+ # Returns an array of column objects for the table associated with this class.
+ def columns_hash
+ @columns_hash ||= columns.inject({}) { |hash, column| hash[column.name] = column; hash }
+ end
+
+ # Returns an array of columns objects where the primary id, all columns ending in "_id" or "_count",
+ # and columns used for single table inheritance has been removed.
+ def content_columns
+ @content_columns ||= columns.reject { |c| c.name == primary_key || c.name =~ /(_id|_count)$/ || c.name == inheritance_column }
+ end
+
+ # Returns a hash of all the methods added to query each of the columns in the table with the name of the method as the key
+ # and true as the value. This makes it possible to do O(1) lookups in respond_to? to check if a given method for attribute
+ # is available.
+ def column_methods_hash
+ @dynamic_methods_hash ||= columns_hash.keys.inject(Hash.new(false)) do |methods, attr|
+ methods[attr.to_sym] = true
+ methods["#{attr}=".to_sym] = true
+ methods["#{attr}?".to_sym] = true
+ methods
+ end
+ end
+
+ # Transforms attribute key names into a more humane format, such as "First name" instead of "first_name". Example:
+ # Person.human_attribute_name("first_name") # => "First name"
+ def human_attribute_name(attribute_key_name)
+ attribute_key_name.gsub(/_/, " ").capitalize unless attribute_key_name.nil?
+ end
+
+ def descends_from_active_record? # :nodoc:
+ superclass == Base
+ end
+
+ # Used to sanitize objects before they're used in an SELECT SQL-statement.
+ def sanitize(object) # :nodoc:
+ return object if Fixnum === object
+ object.to_s.gsub(/([;:])/, "").gsub('##', '\#\#').gsub(/'/, "''") # ' (for ruby-mode)
+ end
+
+ # Used to aggregate logging and benchmark, so you can measure and represent multiple statements in a single block.
+ # Usage (hides all the SQL calls for the individual actions and calculates total runtime for them all):
+ #
+ # Project.benchmark("Creating project") do
+ # project = Project.create("name" => "stuff")
+ # project.create_manager("name" => "David")
+ # project.milestones << Milestone.find_all
+ # end
+ def benchmark(title)
+ result = nil
+ logger.level = Logger::ERROR
+ bm = Benchmark.measure { result = yield }
+ logger.level = Logger::DEBUG
+ logger.info "#{title} (#{sprintf("%f", bm.real)})"
+ return result
+ end
+
+ # Loads the <tt>file_name</tt> if reload_associations is true or requires if it's false.
+ def require_or_load(file_name)
+ if !associations_loaded.include?(file_name)
+ associations_loaded << file_name
+ reload_associations ? load("#{file_name}.rb") : require(file_name)
+ end
+ end
+
+ # Resets the list of dependencies loaded (typically to be called by the end of a request), so when require_or_load is
+ # called for that dependency it'll be loaded anew.
+ def reset_associations_loaded
+ associations_loaded = []
+ end
+
+ private
+ # Finder methods must instantiate through this method to work with the single-table inheritance model
+ # that makes it possible to create objects of different types from the same table.
+ def instantiate(record)
+ object = record_with_type?(record) ? compute_type(record[inheritance_column]).allocate : allocate
+ object.instance_variable_set("@attributes", record)
+ return object
+ end
+
+ # Returns true if the +record+ has a single table inheritance column and is using it.
+ def record_with_type?(record)
+ record.include?(inheritance_column) && !record[inheritance_column].nil? &&
+ !record[inheritance_column].empty?
+ end
+
+ # Returns the name of the type of the record using the current module as a prefix. So descendents of
+ # MyApp::Business::Account would be appear as "MyApp::Business::AccountSubclass".
+ def type_name_with_module(type_name)
+ self.name =~ /::/ ? self.name.scan(/(.*)::/).first.first + "::" + type_name : type_name
+ end
+
+ # Adds a sanitized version of +conditions+ to the +sql+ string. Note that it's the passed +sql+ string is changed.
+ def add_conditions!(sql, conditions)
+ sql << "WHERE #{sanitize_conditions(conditions)} " unless conditions.nil?
+ sql << (conditions.nil? ? "WHERE " : " AND ") + type_condition unless descends_from_active_record?
+ end
+
+ def type_condition
+ " (" + subclasses.inject("#{inheritance_column} = '#{Inflector.demodulize(name)}' ") do |condition, subclass|
+ condition << "OR #{inheritance_column} = '#{Inflector.demodulize(subclass.name)}'"
+ end + ") "
+ end
+
+ # Guesses the table name, but does not decorate it with prefix and suffix information.
+ def undecorated_table_name(class_name = class_name_of_active_record_descendant(self))
+ table_name = Inflector.underscore(Inflector.demodulize(class_name))
+ table_name = Inflector.pluralize(table_name) if pluralize_table_names
+ return table_name
+ end
+
+
+ protected
+ def subclasses
+ @@subclasses[self] ||= []
+ @@subclasses[self] + extra = @@subclasses[self].inject([]) {|list, subclass| list + subclass.subclasses }
+ end
+
+ # Returns the class type of the record using the current module as a prefix. So descendents of
+ # MyApp::Business::Account would be appear as MyApp::Business::AccountSubclass.
+ def compute_type(type_name)
+ type_name_with_module(type_name).split("::").inject(Object) do |final_type, part|
+ final_type = final_type.const_get(part)
+ end
+ end
+
+ # Returns the name of the class descending directly from ActiveRecord in the inheritance hierarchy.
+ def class_name_of_active_record_descendant(klass)
+ if klass.superclass == Base
+ return klass.name
+ elsif klass.superclass.nil?
+ raise ActiveRecordError, "#{name} doesn't belong in a hierarchy descending from ActiveRecord"
+ else
+ class_name_of_active_record_descendant(klass.superclass)
+ end
+ end
+
+ # Accepts either a condition array or string. The string is returned untouched, but the array has each of
+ # the condition values sanitized.
+ def sanitize_conditions(conditions)
+ if Array === conditions
+ statement, values = conditions[0], conditions[1..-1]
+ values.collect! { |value| sanitize(value) }
+ conditions = statement % values
+ end
+
+ return conditions
+ end
+ end
+
+ public
+ # New objects can be instantiated as either empty (pass no construction parameter) or pre-set with
+ # attributes but not yet saved (pass a hash with key names matching the associated table column names).
+ # In both instances, valid attribute keys are determined by the column names of the associated table --
+ # hence you can't have attributes that aren't part of the table columns.
+ def initialize(attributes = nil)
+ @attributes = attributes_from_column_definition
+ @new_record = true
+ ensure_proper_type
+ self.attributes = attributes unless attributes.nil?
+ yield self if block_given?
+ end
+
+ # Every Active Record class must use "id" as their primary ID. This getter overwrites the native
+ # id method, which isn't being used in this context.
+ def id
+ read_attribute(self.class.primary_key)
+ end
+
+ # Sets the primary ID.
+ def id=(value)
+ write_attribute(self.class.primary_key, value)
+ end
+
+ # Returns true if this object hasn't been saved yet -- that is, a record for the object doesn't exist yet.
+ def new_record?
+ @new_record
+ end
+
+ # * No record exists: Creates a new record with values matching those of the object attributes.
+ # * A record does exist: Updates the record with values matching those of the object attributes.
+ def save
+ create_or_update
+ return true
+ end
+
+ # 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?
+ connection.delete(
+ "DELETE FROM #{self.class.table_name} " +
+ "WHERE #{self.class.primary_key} = '#{id}'",
+ "#{self.class.name} Destroy"
+ )
+ end
+
+ freeze
+ end
+
+ # Returns a clone of the record that hasn't been assigned an id yet and is treated as a new record.
+ def clone
+ attr = Hash.new
+
+ self.attribute_names.each do |name|
+ begin
+ attr[name] = read_attribute(name).clone
+ rescue TypeError
+ attr[name] = read_attribute(name)
+ end
+ end
+
+ cloned_record = self.class.new(attr)
+ cloned_record.instance_variable_set "@new_record", true
+ cloned_record.id = nil
+ cloned_record
+ end
+
+ # Updates a single attribute and saves the record. This is especially useful for boolean flags on existing records.
+ def update_attribute(name, value)
+ self[name] = value
+ save
+ end
+
+ # Returns the value of attribute identified by <tt>attr_name</tt> after it has been type cast (for example,
+ # "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)).
+ # (Alias for the protected read_attribute method).
+ def [](attr_name)
+ read_attribute(attr_name)
+ end
+
+ # Updates the attribute identified by <tt>attr_name</tt> with the specified +value+.
+ # (Alias for the protected write_attribute method).
+ def []= (attr_name, value)
+ write_attribute(attr_name, value)
+ end
+
+ # Allows you to set all the attributes at once by passing in a hash with keys
+ # matching the attribute names (which again matches the column names). Sensitive attributes can be protected
+ # from this form of mass-assignment by using the +attr_protected+ macro. Or you can alternatively
+ # specify which attributes *can* be accessed in with the +attr_accessible+ macro. Then all the
+ # attributes not included in that won't be allowed to be mass-assigned.
+ def attributes=(attributes)
+ return if attributes.nil?
+
+ multi_parameter_attributes = []
+ remove_attributes_protected_from_mass_assignment(attributes).each do |k, v|
+ k.include?("(") ? multi_parameter_attributes << [ k, v ] : send(k + "=", v)
+ end
+ assign_multiparameter_attributes(multi_parameter_attributes)
+ end
+
+ # Returns true if the specified +attribute+ has been set by the user or by a database load and is neither
+ # nil nor empty? (the latter only applies to objects that responds to empty?, most notably Strings).
+ def attribute_present?(attribute)
+ is_empty = read_attribute(attribute).respond_to?("empty?") ? read_attribute(attribute).empty? : false
+ @attributes.include?(attribute) && !@attributes[attribute].nil? && !is_empty
+ end
+
+ # Returns an array of names for the attributes available on this object sorted alphabetically.
+ def attribute_names
+ @attributes.keys.sort
+ end
+
+ # Returns the column object for the named attribute.
+ def column_for_attribute(name)
+ self.class.columns_hash[name]
+ end
+
+ # Returns true if the +comparison_object+ is of the same type and has the same id.
+ def ==(comparison_object)
+ comparison_object.instance_of?(self.class) && comparison_object.id == id
+ end
+
+ # Delegates to ==
+ def eql?(comparison_object)
+ self == (comparison_object)
+ end
+
+ # Delegates to id in order to allow two records of the same type and id to work with something like:
+ # [ Person.find(1), Person.find(2), Person.find(3) ] & [ Person.find(1), Person.find(4) ] # => [ Person.find(1) ]
+ def hash
+ id
+ end
+
+ # For checking respond_to? without searching the attributes (which is faster).
+ alias_method :respond_to_without_attributes?, :respond_to?
+
+ # A Person object with a name attribute can ask person.respond_to?("name"), person.respond_to?("name="), and
+ # person.respond_to?("name?") which will all return true.
+ def respond_to?(method)
+ self.class.column_methods_hash[method.to_sym] || respond_to_without_attributes?(method)
+ end
+
+ def require_or_load(file_name)
+ self.class.require_or_load(file_name)
+ end
+
+ private
+ def create_or_update
+ if new_record? then create else update end
+ end
+
+ # Updates the associated record with values matching those of the instant attributes.
+ def update
+ connection.update(
+ "UPDATE #{self.class.table_name} " +
+ "SET #{quoted_comma_pair_list(connection, attributes_with_quotes)} " +
+ "WHERE #{self.class.primary_key} = '#{id}'",
+ "#{self.class.name} Update"
+ )
+ end
+
+ # Creates a new record with values matching those of the instant attributes.
+ def create
+ self.id = connection.insert(
+ "INSERT INTO #{self.class.table_name} " +
+ "(#{quoted_column_names.join(', ')}) " +
+ "VALUES(#{attributes_with_quotes.values.join(', ')})",
+ "#{self.class.name} Create",
+ self.class.primary_key, self.id
+ )
+
+ @new_record = false
+ end
+
+ # Sets the attribute used for single table inheritance to this class name if this is not the ActiveRecord descendant.
+ # Considering the hierarchy Reply < Message < ActiveRecord, this makes it possible to do Reply.new without having to
+ # set Reply[Reply.inheritance_column] = "Reply" yourself. No such attribute would be set for objects of the
+ # Message class in that example.
+ def ensure_proper_type
+ unless self.class.descends_from_active_record?
+ write_attribute(self.class.inheritance_column, Inflector.demodulize(self.class.name))
+ end
+ end
+
+ # Allows access to the object attributes, which are held in the @attributes hash, as were
+ # they first-class methods. So a Person class with a name attribute can use Person#name and
+ # Person#name= and never directly use the attributes hash -- except for multiple assigns with
+ # ActiveRecord#attributes=. A Milestone class can also ask Milestone#completed? to test that
+ # the completed attribute is not nil or 0.
+ #
+ # It's also possible to instantiate related objects, so a Client class belonging to the clients
+ # table with a master_id foreign key can instantiate master through Client#master.
+ def method_missing(method_id, *arguments)
+ method_name = method_id.id2name
+
+
+
+ if method_name =~ read_method? && @attributes.include?($1)
+ return read_attribute($1)
+ elsif method_name =~ write_method? && @attributes.include?($1)
+ write_attribute($1, arguments[0])
+ elsif method_name =~ query_method? && @attributes.include?($1)
+ return query_attribute($1)
+ else
+ super
+ end
+ end
+
+ def read_method?() /^([a-zA-Z][-_\w]*)[^=?]*$/ end
+ def write_method?() /^([a-zA-Z][-_\w]*)=.*$/ end
+ def query_method?() /^([a-zA-Z][-_\w]*)\?$/ end
+
+ # Returns the value of attribute identified by <tt>attr_name</tt> after it has been type cast (for example,
+ # "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)).
+ def read_attribute(attr_name) #:doc:
+ if @attributes.keys.include? attr_name
+ if column = column_for_attribute(attr_name)
+ @attributes[attr_name] = unserializable_attribute?(attr_name, column) ?
+ unserialize_attribute(attr_name) : column.type_cast(@attributes[attr_name])
+ end
+
+ @attributes[attr_name]
+ else
+ nil
+ end
+ end
+
+ # Returns true if the attribute is of a text column and marked for serialization.
+ def unserializable_attribute?(attr_name, column)
+ @attributes[attr_name] && column.send(:type) == :text && @attributes[attr_name].is_a?(String) && self.class.serialized_attributes[attr_name]
+ end
+
+ # Returns the unserialized object of the attribute.
+ def unserialize_attribute(attr_name)
+ unserialized_object = object_from_yaml(@attributes[attr_name])
+
+ if unserialized_object.is_a?(self.class.serialized_attributes[attr_name])
+ @attributes[attr_name] = unserialized_object
+ else
+ raise(
+ SerializationTypeMismatch,
+ "#{attr_name} was supposed to be a #{self.class.serialized_attributes[attr_name]}, " +
+ "but was a #{unserialized_object.class.to_s}"
+ )
+ end
+ end
+
+ # Updates the attribute identified by <tt>attr_name</tt> with the specified +value+. Empty strings for fixnum and float
+ # columns are turned into nil.
+ def write_attribute(attr_name, value) #:doc:
+ @attributes[attr_name] = empty_string_for_number_column?(attr_name, value) ? nil : value
+ end
+
+ def empty_string_for_number_column?(attr_name, value)
+ column = column_for_attribute(attr_name)
+ column && (column.klass == Fixnum || column.klass == Float) && value == ""
+ end
+
+ def query_attribute(attr_name)
+ attribute = @attributes[attr_name]
+ if attribute.kind_of?(Fixnum) && attribute == 0
+ false
+ elsif attribute.kind_of?(String) && attribute == "0"
+ false
+ elsif attribute.kind_of?(String) && attribute.empty?
+ false
+ elsif attribute.nil?
+ false
+ elsif attribute == false
+ false
+ elsif attribute == "f"
+ false
+ elsif attribute == "false"
+ false
+ else
+ true
+ end
+ end
+
+ def remove_attributes_protected_from_mass_assignment(attributes)
+ if self.class.accessible_attributes.nil? && self.class.protected_attributes.nil?
+ attributes.reject { |key, value| key == self.class.primary_key }
+ elsif self.class.protected_attributes.nil?
+ attributes.reject { |key, value| !self.class.accessible_attributes.include?(key.intern) || key == self.class.primary_key }
+ elsif self.class.accessible_attributes.nil?
+ attributes.reject { |key, value| self.class.protected_attributes.include?(key.intern) || key == self.class.primary_key }
+ end
+ end
+
+ # Returns copy of the attributes hash where all the values have been safely quoted for use in
+ # an SQL statement.
+ def attributes_with_quotes
+ columns_hash = self.class.columns_hash
+ @attributes.inject({}) do |attrs_quoted, pair|
+ attrs_quoted[pair.first] = quote(pair.last, columns_hash[pair.first])
+ attrs_quoted
+ end
+ end
+
+ # Quote strings appropriately for SQL statements.
+ def quote(value, column = nil)
+ connection.quote(value, column)
+ end
+
+ # Interpolate custom sql string in instance context.
+ # Optional record argument is meant for custom insert_sql.
+ def interpolate_sql(sql, record = nil)
+ instance_eval("%(#{sql})")
+ end
+
+ # Initializes the attributes array with keys matching the columns from the linked table and
+ # the values matching the corresponding default value of that column, so
+ # that a new instance, or one populated from a passed-in Hash, still has all the attributes
+ # that instances loaded from the database would.
+ def attributes_from_column_definition
+ connection.columns(self.class.table_name, "#{self.class.name} Columns").inject({}) do |attributes, column|
+ attributes[column.name] = column.default unless column.name == self.class.primary_key
+ attributes
+ end
+ end
+
+ # Instantiates objects for all attribute classes that needs more than one constructor parameter. This is done
+ # by calling new on the column type or aggregation type (through composed_of) object with these parameters.
+ # So having the pairs written_on(1) = "2004", written_on(2) = "6", written_on(3) = "24", will instantiate
+ # written_on (a date type) with Date.new("2004", "6", "24"). You can also specify a typecast character in the
+ # parenteses to have the parameters typecasted before they're used in the constructor. Use i for Fixnum, f for Float,
+ # s for String, and a for Array. If all the values for a given attribute is empty, the attribute will be set to nil.
+ def assign_multiparameter_attributes(pairs)
+ execute_callstack_for_multiparameter_attributes(
+ extract_callstack_for_multiparameter_attributes(pairs)
+ )
+ end
+
+ # Includes an ugly hack for Time.local instead of Time.new because the latter is reserved by Time itself.
+ def execute_callstack_for_multiparameter_attributes(callstack)
+ callstack.each do |name, values|
+ klass = (self.class.reflect_on_aggregation(name) || column_for_attribute(name)).klass
+ if values.empty?
+ send(name + "=", nil)
+ else
+ send(name + "=", Time == klass ? klass.local(*values) : klass.new(*values))
+ end
+ end
+ end
+
+ def extract_callstack_for_multiparameter_attributes(pairs)
+ attributes = { }
+
+ for pair in pairs
+ multiparameter_name, value = pair
+ attribute_name = multiparameter_name.split("(").first
+ attributes[attribute_name] = [] unless attributes.include?(attribute_name)
+
+ unless value.empty?
+ attributes[attribute_name] <<
+ [find_parameter_position(multiparameter_name), type_cast_attribute_value(multiparameter_name, value)]
+ end
+ end
+
+ attributes.each { |name, values| attributes[name] = values.sort_by{ |v| v.first }.collect { |v| v.last } }
+ end
+
+ def type_cast_attribute_value(multiparameter_name, value)
+ multiparameter_name =~ /\([0-9]*([a-z])\)/ ? value.send("to_" + $1) : value
+ end
+
+ def find_parameter_position(multiparameter_name)
+ multiparameter_name.scan(/\(([0-9]*).*\)/).first.first
+ end
+
+ # Returns a comma-separated pair list, like "key1 = val1, key2 = val2".
+ def comma_pair_list(hash)
+ hash.inject([]) { |list, pair| list << "#{pair.first} = #{pair.last}" }.join(", ")
+ end
+
+ def quoted_column_names(attributes = attributes_with_quotes)
+ attributes.keys.collect { |column_name| connection.quote_column_name(column_name) }
+ end
+
+ def quote_columns(column_quoter, hash)
+ hash.inject({}) {|list, pair|
+ list[column_quoter.quote_column_name(pair.first)] = pair.last
+ list
+ }
+ end
+
+ def quoted_comma_pair_list(column_quoter, hash)
+ comma_pair_list(quote_columns(column_quoter, hash))
+ end
+
+ def object_from_yaml(string)
+ return string unless String === string
+ if has_yaml_encoding_header?(string)
+ begin
+ YAML::load(string)
+ rescue Object
+ # Apparently wasn't YAML anyway
+ string
+ end
+ else
+ string
+ end
+ end
+
+ def has_yaml_encoding_header?(string)
+ string[0..3] == "--- "
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb
new file mode 100755
index 0000000000..fc013ba743
--- /dev/null
+++ b/activerecord/lib/active_record/callbacks.rb
@@ -0,0 +1,337 @@
+require 'observer'
+
+module ActiveRecord
+ # Callbacks are hooks into the lifecycle of an Active Record object that allows you to trigger logic
+ # before or after an alteration of the object state. This can be used to make sure that associated and
+ # dependent objects are deleted when destroy is called (by overwriting before_destroy) or to massage attributes
+ # before they're validated (by overwriting before_validation). As an example of the callbacks initiated, consider
+ # the Base#save call:
+ #
+ # * (-) save
+ # * (-) valid?
+ # * (1) before_validation
+ # * (2) before_validation_on_create
+ # * (-) validate
+ # * (-) validate_on_create
+ # * (4) after_validation
+ # * (5) after_validation_on_create
+ # * (6) before_save
+ # * (7) before_create
+ # * (-) create
+ # * (8) after_create
+ # * (9) after_save
+ #
+ # That's a total of nine callbacks, which gives you immense power to react and prepare for each state in the
+ # Active Record lifecyle.
+ #
+ # Examples:
+ # class CreditCard < ActiveRecord::Base
+ # # Strip everything but digits, so the user can specify "555 234 34" or
+ # # "5552-3434" or both will mean "55523434"
+ # def before_validation_on_create
+ # self.number = number.gsub(/[^0-9]/, "") if attribute_present?("number")
+ # end
+ # end
+ #
+ # class Subscription < ActiveRecord::Base
+ # # Automatically assign the signup date
+ # def before_create
+ # self.signed_up_on = Date.today
+ # end
+ # end
+ #
+ # class Firm < ActiveRecord::Base
+ # # Destroys the associated clients and people when the firm is destroyed
+ # def before_destroy
+ # Client.destroy_all "client_of = #{id}"
+ # Person.destroy_all "firm_id = #{id}"
+ # end
+ #
+ # == Inheritable callback queues
+ #
+ # Besides the overwriteable callback methods, it's also possible to register callbacks through the use of the callback macros.
+ # Their main advantage is that the macros add behavior into a callback queue that is kept intact down through an inheritance
+ # hierarchy. Example:
+ #
+ # class Topic < ActiveRecord::Base
+ # before_destroy :destroy_author
+ # end
+ #
+ # class Reply < Topic
+ # before_destroy :destroy_readers
+ # end
+ #
+ # Now, when Topic#destroy is run only +destroy_author+ is called. When Reply#destroy is run both +destroy_author+ and
+ # +destroy_readers+ is called. Contrast this to the situation where we've implemented the save behavior through overwriteable
+ # methods:
+ #
+ # class Topic < ActiveRecord::Base
+ # def before_destroy() destroy_author end
+ # end
+ #
+ # class Reply < Topic
+ # def before_destroy() destroy_readers end
+ # end
+ #
+ # In that case, Reply#destroy would only run +destroy_readers+ and _not_ +destroy_author+. So use the callback macros when
+ # you want to ensure that a certain callback is called for the entire hierarchy and the regular overwriteable methods when you
+ # want to leave it up to each descendent to decide whether they want to call +super+ and trigger the inherited callbacks.
+ #
+ # == Types of callbacks
+ #
+ # There are four types of callbacks accepted by the callback macros: Method references (symbol), callback objects,
+ # inline methods (using a proc), and inline eval methods (using a string). Method references and callback objects are the
+ # recommended approaches, inline methods using a proc is some times appropriate (such as for creating mix-ins), and inline
+ # eval methods are deprecated.
+ #
+ # The method reference callbacks work by specifying a protected or private method available in the object, like this:
+ #
+ # class Topic < ActiveRecord::Base
+ # before_destroy :delete_parents
+ #
+ # private
+ # def delete_parents
+ # self.class.delete_all "parent_id = #{id}"
+ # end
+ # end
+ #
+ # The callback objects have methods named after the callback called with the record as the only parameter, such as:
+ #
+ # class BankAccount < ActiveRecord::Base
+ # before_save EncryptionWrapper.new("credit_card_number")
+ # after_save EncryptionWrapper.new("credit_card_number")
+ # after_initialize EncryptionWrapper.new("credit_card_number")
+ # end
+ #
+ # class EncryptionWrapper
+ # def initialize(attribute)
+ # @attribute = attribute
+ # end
+ #
+ # def before_save(record)
+ # record.credit_card_number = encrypt(record.credit_card_number)
+ # end
+ #
+ # def after_save(record)
+ # record.credit_card_number = decrypt(record.credit_card_number)
+ # end
+ #
+ # alias_method :after_initialize, :after_save
+ #
+ # private
+ # def encrypt(value)
+ # # Secrecy is committed
+ # end
+ #
+ # def decrypt(value)
+ # # Secrecy is unvieled
+ # end
+ # end
+ #
+ # So you specify the object you want messaged on a given callback. When that callback is triggered, the object has
+ # a method by the name of the callback messaged.
+ #
+ # The callback macros usually accept a symbol for the method they're supposed to run, but you can also pass a "method string",
+ # which will then be evaluated within the binding of the callback. Example:
+ #
+ # class Topic < ActiveRecord::Base
+ # before_destroy 'self.class.delete_all "parent_id = #{id}"'
+ # end
+ #
+ # Notice that single plings (') are used so the #{id} part isn't evaluated until the callback is triggered. Also note that these
+ # inline callbacks can be stacked just like the regular ones:
+ #
+ # class Topic < ActiveRecord::Base
+ # before_destroy 'self.class.delete_all "parent_id = #{id}"',
+ # 'puts "Evaluated after parents are destroyed"'
+ # end
+ #
+ # == The after_find and after_initialize exceptions
+ #
+ # Because after_find and after_initialize is called for each object instantiated found by a finder, such as Base.find_all, we've had
+ # to implement a simple performance constraint (50% more speed on a simple test case). Unlike all the other callbacks, after_find and
+ # after_initialize can only be declared using an explicit implementation. So using the inheritable callback queue for after_find and
+ # after_initialize won't work.
+ module Callbacks
+ CALLBACKS = %w(
+ after_find after_initialize before_save after_save before_create after_create before_update after_update before_validation
+ after_validation before_validation_on_create after_validation_on_create before_validation_on_update
+ after_validation_on_update before_destroy after_destroy
+ )
+
+ def self.append_features(base) #:nodoc:
+ super
+
+ base.extend(ClassMethods)
+ base.class_eval do
+ class << self
+ include Observable
+ alias_method :instantiate_without_callbacks, :instantiate
+ alias_method :instantiate, :instantiate_with_callbacks
+ end
+ end
+
+ base.class_eval do
+ alias_method :initialize_without_callbacks, :initialize
+ alias_method :initialize, :initialize_with_callbacks
+
+ alias_method :create_or_update_without_callbacks, :create_or_update
+ alias_method :create_or_update, :create_or_update_with_callbacks
+
+ alias_method :valid_without_callbacks, :valid?
+ alias_method :valid?, :valid_with_callbacks
+
+ alias_method :create_without_callbacks, :create
+ alias_method :create, :create_with_callbacks
+
+ alias_method :update_without_callbacks, :update
+ alias_method :update, :update_with_callbacks
+
+ alias_method :destroy_without_callbacks, :destroy
+ alias_method :destroy, :destroy_with_callbacks
+ end
+
+ CALLBACKS.each { |cb| base.class_eval("def self.#{cb}(*methods) write_inheritable_array(\"#{cb}\", methods) end") }
+ end
+
+ module ClassMethods #:nodoc:
+ def instantiate_with_callbacks(record)
+ object = instantiate_without_callbacks(record)
+ object.callback(:after_find) if object.respond_to_without_attributes?(:after_find)
+ object.callback(:after_initialize) if object.respond_to_without_attributes?(:after_initialize)
+ object
+ end
+ end
+
+ # Is called when the object was instantiated by one of the finders, like Base.find.
+ # def after_find() end
+
+ # Is called after the object has been instantiated by a call to Base.new.
+ # def after_initialize() end
+ def initialize_with_callbacks(attributes = nil) #:nodoc:
+ initialize_without_callbacks(attributes)
+ yield self if block_given?
+ after_initialize if respond_to_without_attributes?(:after_initialize)
+ end
+
+ # Is called _before_ Base.save (regardless of whether it's a create or update save).
+ def before_save() end
+
+ # Is called _after_ Base.save (regardless of whether it's a create or update save).
+ def after_save() end
+ def create_or_update_with_callbacks #:nodoc:
+ callback(:before_save)
+ create_or_update_without_callbacks
+ callback(:after_save)
+ end
+
+ # Is called _before_ Base.save on new objects that haven't been saved yet (no record exists).
+ def before_create() end
+
+ # Is called _after_ Base.save on new objects that haven't been saved yet (no record exists).
+ def after_create() end
+ def create_with_callbacks #:nodoc:
+ callback(:before_create)
+ create_without_callbacks
+ callback(:after_create)
+ end
+
+ # Is called _before_ Base.save on existing objects that has a record.
+ def before_update() end
+
+ # Is called _after_ Base.save on existing objects that has a record.
+ def after_update() end
+
+ def update_with_callbacks #:nodoc:
+ callback(:before_update)
+ update_without_callbacks
+ callback(:after_update)
+ end
+
+ # Is called _before_ Validations.validate (which is part of the Base.save call).
+ def before_validation() end
+
+ # Is called _after_ Validations.validate (which is part of the Base.save call).
+ def after_validation() end
+
+ # Is called _before_ Validations.validate (which is part of the Base.save call) on new objects
+ # that haven't been saved yet (no record exists).
+ def before_validation_on_create() end
+
+ # Is called _after_ Validations.validate (which is part of the Base.save call) on new objects
+ # that haven't been saved yet (no record exists).
+ def after_validation_on_create() end
+
+ # Is called _before_ Validations.validate (which is part of the Base.save call) on
+ # existing objects that has a record.
+ def before_validation_on_update() end
+
+ # Is called _after_ Validations.validate (which is part of the Base.save call) on
+ # existing objects that has a record.
+ def after_validation_on_update() end
+
+ def valid_with_callbacks #:nodoc:
+ callback(:before_validation)
+ if new_record? then callback(:before_validation_on_create) else callback(:before_validation_on_update) end
+
+ result = valid_without_callbacks
+
+ callback(:after_validation)
+ if new_record? then callback(:after_validation_on_create) else callback(:after_validation_on_update) end
+
+ return result
+ end
+
+ # Is called _before_ Base.destroy.
+ def before_destroy() end
+
+ # Is called _after_ Base.destroy (and all the attributes have been frozen).
+ def after_destroy() end
+ def destroy_with_callbacks #:nodoc:
+ callback(:before_destroy)
+ destroy_without_callbacks
+ callback(:after_destroy)
+ end
+
+ def callback(callback_method) #:nodoc:
+ run_callbacks(callback_method)
+ send(callback_method)
+ notify(callback_method)
+ end
+
+ def run_callbacks(callback_method)
+ filters = self.class.read_inheritable_attribute(callback_method.to_s)
+ if filters.nil? then return end
+ filters.each do |filter|
+ if Symbol === filter
+ self.send(filter)
+ elsif String === filter
+ eval(filter, binding)
+ elsif filter_block?(filter)
+ filter.call(self)
+ elsif filter_class?(filter, callback_method)
+ filter.send(callback_method, self)
+ else
+ raise(
+ ActiveRecordError,
+ "Filters need to be either a symbol, string (to be eval'ed), proc/method, or " +
+ "class implementing a static filter method"
+ )
+ end
+ end
+ end
+
+ def filter_block?(filter)
+ filter.respond_to?("call") && (filter.arity == 1 || filter.arity == -1)
+ end
+
+ def filter_class?(filter, callback_method)
+ filter.respond_to?(callback_method)
+ end
+
+ def notify(callback_method) #:nodoc:
+ self.class.changed
+ self.class.notify_observers(callback_method, self)
+ end
+ end
+end \ No newline at end of file
diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
new file mode 100755
index 0000000000..54fdfd25cd
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
@@ -0,0 +1,371 @@
+require 'benchmark'
+require 'date'
+
+# Method that requires a library, ensuring that rubygems is loaded
+# This is used in the database adaptors to require DB drivers. Reasons:
+# (1) database drivers are the only third-party library that Rails depend upon
+# (2) they are often installed as gems
+def require_library_or_gem(library_name)
+ begin
+ require library_name
+ rescue LoadError => cannot_require
+ # 1. Requiring the module is unsuccessful, maybe it's a gem and nobody required rubygems yet. Try.
+ begin
+ require 'rubygems'
+ rescue LoadError => rubygems_not_installed
+ raise cannot_require
+ end
+ # 2. Rubygems is installed and loaded. Try to load the library again
+ begin
+ require library_name
+ rescue LoadError => gem_not_installed
+ raise cannot_require
+ end
+ end
+end
+
+module ActiveRecord
+ class Base
+ class ConnectionSpecification #:nodoc:
+ attr_reader :config, :adapter_method
+ def initialize (config, adapter_method)
+ @config, @adapter_method = config, adapter_method
+ end
+ end
+
+ # The class -> [adapter_method, config] map
+ @@defined_connections = {}
+
+ # Establishes the connection to the database. Accepts a hash as input where
+ # the :adapter key must be specified with the name of a database adapter (in lower-case)
+ # example for regular databases (MySQL, Postgresql, etc):
+ #
+ # ActiveRecord::Base.establish_connection(
+ # :adapter => "mysql",
+ # :host => "localhost",
+ # :username => "myuser",
+ # :password => "mypass",
+ # :database => "somedatabase"
+ # )
+ #
+ # Example for SQLite database:
+ #
+ # ActiveRecord::Base.establish_connection(
+ # :adapter => "sqlite",
+ # :dbfile => "path/to/dbfile"
+ # )
+ #
+ # Also accepts keys as strings (for parsing from yaml for example):
+ # ActiveRecord::Base.establish_connection(
+ # "adapter" => "sqlite",
+ # "dbfile" => "path/to/dbfile"
+ # )
+ #
+ # The exceptions AdapterNotSpecified, AdapterNotFound and ArgumentError
+ # may be returned on an error.
+ #
+ # == Connecting to another database for a single model
+ #
+ # To support different connections for different classes, you can
+ # simply call establish_connection with the classes you wish to have
+ # different connections for:
+ #
+ # class Courses < ActiveRecord::Base
+ # ...
+ # end
+ #
+ # Courses.establish_connection( ... )
+ def self.establish_connection(spec)
+ if spec.instance_of? ConnectionSpecification
+ @@defined_connections[self] = spec
+ elsif spec.is_a?(Symbol)
+ establish_connection(configurations[spec.to_s])
+ else
+ if spec.nil? then raise AdapterNotSpecified end
+ symbolize_strings_in_hash(spec)
+ unless spec.key?(:adapter) then raise AdapterNotSpecified end
+
+ adapter_method = "#{spec[:adapter]}_connection"
+ unless methods.include?(adapter_method) then raise AdapterNotFound end
+ remove_connection
+ @@defined_connections[self] = ConnectionSpecification.new(spec, adapter_method)
+ end
+ end
+
+ # Locate the connection of the nearest super class. This can be an
+ # active or defined connections: if it is the latter, it will be
+ # opened and set as the active connection for the class it was defined
+ # for (not necessarily the current class).
+ def self.retrieve_connection #:nodoc:
+ klass = self
+ until klass == ActiveRecord::Base.superclass
+ Thread.current['active_connections'] ||= {}
+ if Thread.current['active_connections'][klass]
+ return Thread.current['active_connections'][klass]
+ elsif @@defined_connections[klass]
+ klass.connection = @@defined_connections[klass]
+ return self.connection
+ end
+ klass = klass.superclass
+ end
+ raise ConnectionNotEstablished
+ end
+
+ # Returns true if a connection that's accessible to this class have already been opened.
+ def self.connected?
+ klass = self
+ until klass == ActiveRecord::Base.superclass
+ if Thread.current['active_connections'].is_a?(Hash) && Thread.current['active_connections'][klass]
+ return true
+ else
+ klass = klass.superclass
+ end
+ end
+ return false
+ end
+
+ # Remove the connection for this class. This will close the active
+ # connection and the defined connection (if they exist). The result
+ # can be used as argument for establish_connection, for easy
+ # re-establishing of the connection.
+ def self.remove_connection(klass=self)
+ conn = @@defined_connections[klass]
+ @@defined_connections.delete(klass)
+ Thread.current['active_connections'] ||= {}
+ Thread.current['active_connections'][klass] = nil
+ conn.config if conn
+ end
+
+ # Set the connection for the class.
+ def self.connection=(spec)
+ raise ConnectionNotEstablished unless spec
+ conn = self.send(spec.adapter_method, spec.config)
+ Thread.current['active_connections'] ||= {}
+ Thread.current['active_connections'][self] = conn
+ end
+
+ # Converts all strings in a hash to symbols.
+ def self.symbolize_strings_in_hash(hash)
+ hash.each do |key, value|
+ if key.class == String
+ hash.delete key
+ hash[key.intern] = value
+ end
+ end
+ end
+ end
+
+ module ConnectionAdapters # :nodoc:
+ class Column # :nodoc:
+ attr_reader :name, :default, :type, :limit
+ # The name should contain the name of the column, such as "name" in "name varchar(250)"
+ # The default should contain the type-casted default of the column, such as 1 in "count int(11) DEFAULT 1"
+ # The type parameter should either contain :integer, :float, :datetime, :date, :text, or :string
+ # The sql_type is just used for extracting the limit, such as 10 in "varchar(10)"
+ def initialize(name, default, sql_type = nil)
+ @name, @default, @type = name, default, simplified_type(sql_type)
+ @limit = extract_limit(sql_type) unless sql_type.nil?
+ end
+
+ def default
+ type_cast(@default)
+ end
+
+ def klass
+ case type
+ when :integer then Fixnum
+ when :float then Float
+ when :datetime then Time
+ when :date then Date
+ when :text, :string then String
+ when :boolean then Object
+ end
+ end
+
+ def type_cast(value)
+ if value.nil? then return nil end
+ case type
+ when :string then value
+ when :text then value
+ when :integer then value.to_i
+ when :float then value.to_f
+ when :datetime then string_to_time(value)
+ when :date then string_to_date(value)
+ when :boolean then (value == "t" or value == true ? true : false)
+ else value
+ end
+ end
+
+ def human_name
+ Base.human_attribute_name(@name)
+ end
+
+ private
+ def string_to_date(string)
+ return string if Date === string
+ date_array = ParseDate.parsedate(string)
+ # treat 0000-00-00 as nil
+ Date.new(date_array[0], date_array[1], date_array[2]) rescue nil
+ end
+
+ def string_to_time(string)
+ return string if Time === string
+ time_array = ParseDate.parsedate(string).compact
+ # treat 0000-00-00 00:00:00 as nil
+ Time.local(*time_array) rescue nil
+ end
+
+ def extract_limit(sql_type)
+ $1.to_i if sql_type =~ /\((.*)\)/
+ end
+
+ def simplified_type(field_type)
+ case field_type
+ when /int/i
+ :integer
+ when /float|double|decimal|numeric/i
+ :float
+ when /time/i
+ :datetime
+ when /date/i
+ :date
+ when /(c|b)lob/i, /text/i
+ :text
+ when /char/i, /string/i
+ :string
+ when /boolean/i
+ :boolean
+ end
+ end
+ end
+
+ # All the concrete database adapters follow the interface laid down in this class.
+ # You can use this interface directly by borrowing the database connection from the Base with
+ # Base.connection.
+ class AbstractAdapter
+ @@row_even = true
+
+ include Benchmark
+
+ def initialize(connection, logger = nil) # :nodoc:
+ @connection, @logger = connection, logger
+ @runtime = 0
+ end
+
+ # Returns an array of record hashes with the column names as a keys and fields as values.
+ def select_all(sql, name = nil) end
+
+ # Returns a record hash with the column names as a keys and fields as values.
+ def select_one(sql, name = nil) end
+
+ # Returns an array of column objects for the table specified by +table_name+.
+ def columns(table_name, name = nil) end
+
+ # Returns the last auto-generated ID from the affected table.
+ def insert(sql, name = nil, pk = nil, id_value = nil) end
+
+ # Executes the update statement.
+ def update(sql, name = nil) end
+
+ # Executes the delete statement.
+ def delete(sql, name = nil) end
+
+ def reset_runtime # :nodoc:
+ rt = @runtime
+ @runtime = 0
+ return rt
+ end
+
+ # Wrap a block in a transaction. Returns result of block.
+ def transaction
+ begin
+ if block_given?
+ begin_db_transaction
+ result = yield
+ commit_db_transaction
+ result
+ end
+ rescue Exception => database_transaction_rollback
+ rollback_db_transaction
+ raise
+ end
+ end
+
+ # Begins the transaction (and turns off auto-committing).
+ def begin_db_transaction() end
+
+ # Commits the transaction (and turns on auto-committing).
+ def commit_db_transaction() end
+
+ # Rollsback the transaction (and turns on auto-committing). Must be done if the transaction block
+ # raises an exception or returns false.
+ def rollback_db_transaction() end
+
+ def quote(value, column = nil)
+ case value
+ when String then "'#{quote_string(value)}'" # ' (for ruby-mode)
+ when NilClass then "NULL"
+ when TrueClass then (column && column.type == :boolean ? "'t'" : "1")
+ when FalseClass then (column && column.type == :boolean ? "'f'" : "0")
+ when Float, Fixnum, Bignum, Date then "'#{value.to_s}'"
+ when Time, DateTime then "'#{value.strftime("%Y-%m-%d %H:%M:%S")}'"
+ else "'#{quote_string(value.to_yaml)}'"
+ end
+ end
+
+ def quote_string(s)
+ s.gsub(/\\/, '\&\&').gsub(/'/, "''") # ' (for ruby-mode)
+ end
+
+ def quote_column_name(name)
+ return name
+ end
+
+ # Returns a string of the CREATE TABLE SQL statements for recreating the entire structure of the database.
+ def structure_dump() end
+
+ protected
+ def log(sql, name, connection, &action)
+ begin
+ if @logger.nil?
+ action.call(connection)
+ else
+ result = nil
+ bm = measure { result = action.call(connection) }
+ @runtime += bm.real
+ log_info(sql, name, bm.real)
+ result
+ end
+ rescue => e
+ log_info("#{e.message}: #{sql}", name, 0)
+ raise ActiveRecord::StatementInvalid, "#{e.message}: #{sql}"
+ end
+ end
+
+ def log_info(sql, name, runtime)
+ if @logger.nil? then return end
+
+ @logger.info(
+ format_log_entry(
+ "#{name.nil? ? "SQL" : name} (#{sprintf("%f", runtime)})",
+ sql.gsub(/ +/, " ")
+ )
+ )
+ end
+
+ def format_log_entry(message, dump = nil)
+ if @@row_even then
+ @@row_even = false; caller_color = "1;32"; message_color = "4;33"; dump_color = "1;37"
+ else
+ @@row_even = true; caller_color = "1;36"; message_color = "4;35"; dump_color = "0;37"
+ end
+
+ log_entry = " \e[#{message_color}m#{message}\e[m"
+ log_entry << " \e[#{dump_color}m%s\e[m" % dump if dump.kind_of?(String) && !dump.nil?
+ log_entry << " \e[#{dump_color}m%p\e[m" % dump if !dump.kind_of?(String) && !dump.nil?
+ log_entry
+ end
+ end
+
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
new file mode 100755
index 0000000000..5dcdded5bc
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
@@ -0,0 +1,131 @@
+require 'active_record/connection_adapters/abstract_adapter'
+require 'parsedate'
+
+module ActiveRecord
+ class Base
+ # Establishes a connection to the database that's used by all Active Record objects
+ def self.mysql_connection(config) # :nodoc:
+ unless self.class.const_defined?(:Mysql)
+ begin
+ # Only include the MySQL driver if one hasn't already been loaded
+ require_library_or_gem 'mysql'
+ rescue LoadError => cannot_require_mysql
+ # Only use the supplied backup Ruby/MySQL driver if no driver is already in place
+ begin
+ require 'active_record/vendor/mysql'
+ rescue LoadError
+ raise cannot_require_mysql
+ end
+ end
+ end
+ symbolize_strings_in_hash(config)
+ host = config[:host]
+ port = config[:port]
+ socket = config[:socket]
+ username = config[:username] ? config[:username].to_s : 'root'
+ password = config[:password].to_s
+
+ if config.has_key?(:database)
+ database = config[:database]
+ else
+ raise ArgumentError, "No database specified. Missing argument: database."
+ end
+
+ ConnectionAdapters::MysqlAdapter.new(
+ Mysql::real_connect(host, username, password, database, port, socket), logger
+ )
+ end
+ end
+
+ module ConnectionAdapters
+ class MysqlAdapter < AbstractAdapter # :nodoc:
+ def select_all(sql, name = nil)
+ select(sql, name)
+ end
+
+ def select_one(sql, name = nil)
+ result = select(sql, name)
+ result.nil? ? nil : result.first
+ end
+
+ def columns(table_name, name = nil)
+ sql = "SHOW FIELDS FROM #{table_name}"
+ result = nil
+ log(sql, name, @connection) { |connection| result = connection.query(sql) }
+
+ columns = []
+ result.each { |field| columns << Column.new(field[0], field[4], field[1]) }
+ columns
+ end
+
+ def insert(sql, name = nil, pk = nil, id_value = nil)
+ execute(sql, name = nil)
+ return id_value || @connection.insert_id
+ end
+
+ def execute(sql, name = nil)
+ log(sql, name, @connection) { |connection| connection.query(sql) }
+ end
+
+ alias_method :update, :execute
+ alias_method :delete, :execute
+
+ def begin_db_transaction
+ begin
+ execute "BEGIN"
+ rescue Exception
+ # Transactions aren't supported
+ end
+ end
+
+ def commit_db_transaction
+ begin
+ execute "COMMIT"
+ rescue Exception
+ # Transactions aren't supported
+ end
+ end
+
+ def rollback_db_transaction
+ begin
+ execute "ROLLBACK"
+ rescue Exception
+ # Transactions aren't supported
+ end
+ end
+
+ def quote_column_name(name)
+ return "`#{name}`"
+ end
+
+ def structure_dump
+ select_all("SHOW TABLES").inject("") do |structure, table|
+ structure += select_one("SHOW CREATE TABLE #{table.to_a.first.last}")["Create Table"] + ";\n\n"
+ end
+ end
+
+ def recreate_database(name)
+ drop_database(name)
+ create_database(name)
+ end
+
+ def drop_database(name)
+ execute "DROP DATABASE IF EXISTS #{name}"
+ end
+
+ def create_database(name)
+ execute "CREATE DATABASE #{name}"
+ end
+
+ private
+ def select(sql, name = nil)
+ result = nil
+ log(sql, name, @connection) { |connection| connection.query_with_result = true; result = connection.query(sql) }
+ rows = []
+ all_fields_initialized = result.fetch_fields.inject({}) { |all_fields, f| all_fields[f.name] = nil; all_fields }
+ result.each_hash { |row| rows << all_fields_initialized.dup.update(row) }
+ rows
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
new file mode 100644
index 0000000000..fb54642d3a
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
@@ -0,0 +1,170 @@
+
+# postgresql_adaptor.rb
+# author: Luke Holden <lholden@cablelan.net>
+# notes: Currently this adaptor does not pass the test_zero_date_fields
+# and test_zero_datetime_fields unit tests in the BasicsTest test
+# group.
+#
+# This is due to the fact that, in postgresql you can not have a
+# totally zero timestamp. Instead null/nil should be used to
+# represent no value.
+#
+
+require 'active_record/connection_adapters/abstract_adapter'
+require 'parsedate'
+
+module ActiveRecord
+ class Base
+ # Establishes a connection to the database that's used by all Active Record objects
+ def self.postgresql_connection(config) # :nodoc:
+ require_library_or_gem 'postgres' unless self.class.const_defined?(:PGconn)
+ symbolize_strings_in_hash(config)
+ host = config[:host]
+ port = config[:port] || 5432 unless host.nil?
+ username = config[:username].to_s
+ password = config[:password].to_s
+
+ if config.has_key?(:database)
+ database = config[:database]
+ else
+ raise ArgumentError, "No database specified. Missing argument: database."
+ end
+
+ ConnectionAdapters::PostgreSQLAdapter.new(
+ PGconn.connect(host, port, "", "", database, username, password), logger
+ )
+ end
+ end
+
+ module ConnectionAdapters
+ class PostgreSQLAdapter < AbstractAdapter # :nodoc:
+ def select_all(sql, name = nil)
+ select(sql, name)
+ end
+
+ def select_one(sql, name = nil)
+ result = select(sql, name)
+ result.nil? ? nil : result.first
+ end
+
+ def columns(table_name, name = nil)
+ table_structure(table_name).inject([]) do |columns, field|
+ columns << Column.new(field[0], field[2], field[1])
+ columns
+ end
+ end
+
+ def insert(sql, name = nil, pk = nil, id_value = nil)
+ execute(sql, name = nil)
+ table = sql.split(" ", 4)[2]
+ return id_value || last_insert_id(table, pk)
+ end
+
+ def execute(sql, name = nil)
+ log(sql, name, @connection) { |connection| connection.query(sql) }
+ end
+
+ alias_method :update, :execute
+ alias_method :delete, :execute
+
+ def begin_db_transaction() execute "BEGIN" end
+ def commit_db_transaction() execute "COMMIT" end
+ def rollback_db_transaction() execute "ROLLBACK" end
+
+ def quote_column_name(name)
+ return "\"#{name}\""
+ end
+
+ private
+ def last_insert_id(table, column = "id")
+ sequence_name = "#{table}_#{column || 'id'}_seq"
+ @connection.exec("SELECT currval('#{sequence_name}')")[0][0].to_i
+ end
+
+ def select(sql, name = nil)
+ res = nil
+ log(sql, name, @connection) { |connection| res = connection.exec(sql) }
+
+ results = res.result
+ rows = []
+ if results.length > 0
+ fields = res.fields
+ results.each do |row|
+ hashed_row = {}
+ row.each_index { |cel_index| hashed_row[fields[cel_index]] = row[cel_index] }
+ rows << hashed_row
+ end
+ end
+ return rows
+ end
+
+ def split_table_schema(table_name)
+ schema_split = table_name.split('.')
+ schema_name = "public"
+ if schema_split.length > 1
+ schema_name = schema_split.first.strip
+ table_name = schema_split.last.strip
+ end
+ return [schema_name, table_name]
+ end
+
+ def table_structure(table_name)
+ database_name = @connection.db
+ schema_name, table_name = split_table_schema(table_name)
+
+ # Grab a list of all the default values for the columns.
+ sql = "SELECT column_name, column_default, character_maximum_length, data_type "
+ sql << " FROM information_schema.columns "
+ sql << " WHERE table_catalog = '#{database_name}' "
+ sql << " AND table_schema = '#{schema_name}' "
+ sql << " AND table_name = '#{table_name}';"
+
+ column_defaults = nil
+ log(sql, nil, @connection) { |connection| column_defaults = connection.query(sql) }
+ column_defaults.collect do |row|
+ field = row[0]
+ type = type_as_string(row[3], row[2])
+ default = default_value(row[1])
+ length = row[2]
+
+ [field, type, default, length]
+ end
+ end
+
+ def type_as_string(field_type, field_length)
+ type = case field_type
+ when 'numeric', 'real', 'money' then 'float'
+ when 'character varying', 'interval' then 'string'
+ when 'timestamp without time zone' then 'datetime'
+ else field_type
+ end
+
+ size = field_length.nil? ? "" : "(#{field_length})"
+
+ return type + size
+ end
+
+ def default_value(value)
+ # Boolean types
+ return "t" if value =~ /true/i
+ return "f" if value =~ /false/i
+
+ # Char/String type values
+ return $1 if value =~ /^'(.*)'::(bpchar|text|character varying)$/
+
+ # Numeric values
+ return value if value =~ /^[0-9]+(\.[0-9]*)?/
+
+ # Date / Time magic values
+ return Time.now.to_s if value =~ /^\('now'::text\)::(date|timestamp)/
+
+ # Fixed dates / times
+ return $1 if value =~ /^'(.+)'::(date|timestamp)/
+
+ # Anything else is blank, some user type, or some function
+ # and we can't know the value of that, so return nil.
+ return nil
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb
new file mode 100644
index 0000000000..1f3845e6a8
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb
@@ -0,0 +1,105 @@
+# sqlite_adapter.rb
+# author: Luke Holden <lholden@cablelan.net>
+
+require 'active_record/connection_adapters/abstract_adapter'
+
+module ActiveRecord
+ class Base
+ # Establishes a connection to the database that's used by all Active Record objects
+ def self.sqlite_connection(config) # :nodoc:
+ require_library_or_gem('sqlite') unless self.class.const_defined?(:SQLite)
+ symbolize_strings_in_hash(config)
+ unless config.has_key?(:dbfile)
+ raise ArgumentError, "No database file specified. Missing argument: dbfile"
+ end
+
+ db = SQLite::Database.new(config[:dbfile], 0)
+
+ db.show_datatypes = "ON" if !defined? SQLite::Version
+ db.results_as_hash = true if defined? SQLite::Version
+ db.type_translation = false
+
+ ConnectionAdapters::SQLiteAdapter.new(db, logger)
+ end
+ end
+
+ module ConnectionAdapters
+ class SQLiteAdapter < AbstractAdapter # :nodoc:
+ def select_all(sql, name = nil)
+ select(sql, name)
+ end
+
+ def select_one(sql, name = nil)
+ result = select(sql, name)
+ result.nil? ? nil : result.first
+ end
+
+ def columns(table_name, name = nil)
+ table_structure(table_name).inject([]) do |columns, field|
+ columns << Column.new(field['name'], field['dflt_value'], field['type'])
+ columns
+ end
+ end
+
+ def insert(sql, name = nil, pk = nil, id_value = nil)
+ execute(sql, name = nil)
+ id_value || @connection.send( defined?( SQLite::Version ) ? :last_insert_row_id : :last_insert_rowid )
+ end
+
+ def execute(sql, name = nil)
+ log(sql, name, @connection) do |connection|
+ if defined?( SQLite::Version )
+ case sql
+ when "BEGIN" then connection.transaction
+ when "COMMIT" then connection.commit
+ when "ROLLBACK" then connection.rollback
+ else connection.execute(sql)
+ end
+ else
+ connection.execute( sql )
+ end
+ end
+ end
+
+ alias_method :update, :execute
+ alias_method :delete, :execute
+
+ def begin_db_transaction() execute "BEGIN" end
+ def commit_db_transaction() execute "COMMIT" end
+ def rollback_db_transaction() execute "ROLLBACK" end
+
+ def quote_string(s)
+ SQLite::Database.quote(s)
+ end
+
+ def quote_column_name(name)
+ return "'#{name}'"
+ end
+
+ private
+ def select(sql, name = nil)
+ results = nil
+ log(sql, name, @connection) { |connection| results = connection.execute(sql) }
+
+ rows = []
+
+ results.each do |row|
+ hash_only_row = {}
+ row.each_key do |key|
+ hash_only_row[key.sub(/\w+\./, "")] = row[key] unless key.class == Fixnum
+ end
+ rows << hash_only_row
+ end
+
+ return rows
+ end
+
+ def table_structure(table_name)
+ sql = "PRAGMA table_info(#{table_name});"
+ results = nil
+ log(sql, nil, @connection) { |connection| results = connection.execute(sql) }
+ return results
+ end
+ end
+ end
+end \ No newline at end of file
diff --git a/activerecord/lib/active_record/connection_adapters/sqlserver_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlserver_adapter.rb
new file mode 100644
index 0000000000..5cd5f5a0be
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/sqlserver_adapter.rb
@@ -0,0 +1,298 @@
+require 'active_record/connection_adapters/abstract_adapter'
+
+# sqlserver_adapter.rb -- ActiveRecord adapter for Microsoft SQL Server
+#
+# Author: Joey Gibson <joey@joeygibson.com>
+# Date: 10/14/2004
+#
+# REQUIREMENTS:
+#
+# This adapter will ONLY work on Windows systems, since it relies on Win32OLE, which,
+# to my knowledge, is only available on Window.
+#
+# It relies on the ADO support in the DBI module. If you are using the
+# one-click installer of Ruby, then you already have DBI installed, but
+# the ADO module is *NOT* installed. You will need to get the latest
+# source distribution of Ruby-DBI from http://ruby-dbi.sourceforge.net/
+# unzip it, and copy the file src/lib/dbd_ado/ADO.rb to
+# X:/Ruby/lib/ruby/site_ruby/1.8/DBD/ADO/ADO.rb (you will need to create
+# the ADO directory). Once you've installed that file, you are ready to go.
+#
+# This module uses the ADO-style DSNs for connection. For example:
+# "DBI:ADO:Provider=SQLOLEDB;Data Source=(local);Initial Catalog=test;User Id=sa;Password=password;"
+# with User Id replaced with your proper login, and Password with your
+# password.
+#
+# I have tested this code on a WindowsXP Pro SP1 system,
+# ruby 1.8.2 (2004-07-29) [i386-mswin32], SQL Server 2000.
+#
+module ActiveRecord
+ class Base
+ def self.sqlserver_connection(config)
+ require_library_or_gem 'dbi' unless self.class.const_defined?(:DBI)
+ class_eval { include ActiveRecord::SQLServerBaseExtensions }
+
+ symbolize_strings_in_hash(config)
+
+ if config.has_key? :dsn
+ dsn = config[:dsn]
+ else
+ raise ArgumentError, "No DSN specified"
+ end
+
+ conn = DBI.connect(dsn)
+ conn["AutoCommit"] = true
+
+ ConnectionAdapters::SQLServerAdapter.new(conn, logger)
+ end
+ end
+
+ module SQLServerBaseExtensions #:nodoc:
+ def self.append_features(base)
+ super
+ base.extend(ClassMethods)
+ end
+
+ module ClassMethods
+ def find_first(conditions = nil, orderings = nil)
+ sql = "SELECT TOP 1 * FROM #{table_name} "
+ add_conditions!(sql, conditions)
+ sql << "ORDER BY #{orderings} " unless orderings.nil?
+
+ record = connection.select_one(sql, "#{name} Load First")
+ instantiate(record) unless record.nil?
+ end
+
+ def find_all(conditions = nil, orderings = nil, limit = nil, joins = nil)
+ sql = "SELECT "
+ sql << "TOP #{limit} " unless limit.nil?
+ sql << " * FROM #{table_name} "
+ sql << "#{joins} " if joins
+ add_conditions!(sql, conditions)
+ sql << "ORDER BY #{orderings} " unless orderings.nil?
+
+ find_by_sql(sql)
+ end
+ end
+
+ def attributes_with_quotes
+ columns_hash = self.class.columns_hash
+
+ attrs = @attributes.dup
+
+ attrs = attrs.reject do |name, value|
+ columns_hash[name].identity
+ end
+
+ attrs.inject({}) do |attrs_quoted, pair|
+ attrs_quoted[pair.first] = quote(pair.last, columns_hash[pair.first])
+ attrs_quoted
+ end
+ end
+ end
+
+ module ConnectionAdapters
+ class ColumnWithIdentity < Column
+ attr_reader :identity
+
+ def initialize(name, default, sql_type = nil, is_identity = false)
+ super(name, default, sql_type)
+
+ @identity = is_identity
+ end
+ end
+
+ class SQLServerAdapter < AbstractAdapter # :nodoc:
+ def quote_column_name(name)
+ " [#{name}] "
+ end
+
+ def select_all(sql, name = nil)
+ select(sql, name)
+ end
+
+ def select_one(sql, name = nil)
+ result = select(sql, name)
+ result.nil? ? nil : result.first
+ end
+
+ def columns(table_name, name = nil)
+ sql = <<EOL
+SELECT s.name AS TableName, c.id AS ColId, c.name AS ColName, t.name AS ColType, c.length AS Length,
+c.AutoVal AS IsIdentity,
+c.cdefault AS DefaultId, com.text AS DefaultValue
+FROM syscolumns AS c
+JOIN systypes AS t ON (c.xtype = t.xtype AND c.usertype = t.usertype)
+JOIN sysobjects AS s ON (c.id = s.id)
+LEFT OUTER JOIN syscomments AS com ON (c.cdefault = com.id)
+WHERE s.name = '#{table_name}'
+EOL
+
+ columns = []
+
+ log(sql, name, @connection) do |conn|
+ conn.select_all(sql) do |row|
+ default_value = row[:DefaultValue]
+
+ if default_value =~ /null/i
+ default_value = nil
+ else
+ default_value =~ /\(([^)]+)\)/
+ default_value = $1
+ end
+
+ col = ColumnWithIdentity.new(row[:ColName], default_value, "#{row[:ColType]}(#{row[:Length]})", row[:IsIdentity] != nil)
+
+ columns << col
+ end
+ end
+
+ columns
+ end
+
+ def insert(sql, name = nil, pk = nil, id_value = nil)
+ begin
+ table_name = get_table_name(sql)
+
+ col = get_identity_column(table_name)
+
+ ii_enabled = false
+
+ if col != nil
+ if query_contains_identity_column(sql, col)
+ begin
+ execute enable_identity_insert(table_name, true)
+ ii_enabled = true
+ rescue Exception => e
+ # Coulnd't turn on IDENTITY_INSERT
+ end
+ end
+ end
+
+ log(sql, name, @connection) do |conn|
+ conn.execute(sql)
+
+ select_one("SELECT @@IDENTITY AS Ident")["Ident"]
+ end
+ ensure
+ if ii_enabled
+ begin
+ execute enable_identity_insert(table_name, false)
+
+ rescue Exception => e
+ # Couldn't turn off IDENTITY_INSERT
+ end
+ end
+ end
+ end
+
+ def execute(sql, name = nil)
+ if sql =~ /^INSERT/i
+ insert(sql, name)
+ else
+ log(sql, name, @connection) do |conn|
+ conn.execute(sql)
+ end
+ end
+ end
+
+ alias_method :update, :execute
+ alias_method :delete, :execute
+
+ def begin_db_transaction
+ begin
+ @connection["AutoCommit"] = false
+ rescue Exception => e
+ @connection["AutoCommit"] = true
+ end
+ end
+
+ def commit_db_transaction
+ begin
+ @connection.commit
+ ensure
+ @connection["AutoCommit"] = true
+ end
+ end
+
+ def rollback_db_transaction
+ begin
+ @connection.rollback
+ ensure
+ @connection["AutoCommit"] = true
+ end
+ end
+
+ def recreate_database(name)
+ drop_database(name)
+ create_database(name)
+ end
+
+ def drop_database(name)
+ execute "DROP DATABASE #{name}"
+ end
+
+ def create_database(name)
+ execute "CREATE DATABASE #{name}"
+ end
+
+ private
+ def select(sql, name = nil)
+ rows = []
+
+ log(sql, name, @connection) do |conn|
+ conn.select_all(sql) do |row|
+ record = {}
+
+ row.column_names.each do |col|
+ record[col] = row[col]
+ end
+
+ rows << record
+ end
+ end
+
+ rows
+ end
+
+ def enable_identity_insert(table_name, enable = true)
+ if has_identity_column(table_name)
+ "SET IDENTITY_INSERT #{table_name} #{enable ? 'ON' : 'OFF'}"
+ end
+ end
+
+ def get_table_name(sql)
+ if sql =~ /into\s*([^\s]+)\s*/i or
+ sql =~ /update\s*([^\s]+)\s*/i
+ $1
+ else
+ nil
+ end
+ end
+
+ def has_identity_column(table_name)
+ return get_identity_column(table_name) != nil
+ end
+
+ def get_identity_column(table_name)
+ if not @table_columns
+ @table_columns = {}
+ end
+
+ if @table_columns[table_name] == nil
+ @table_columns[table_name] = columns(table_name)
+ end
+
+ @table_columns[table_name].each do |col|
+ return col.name if col.identity
+ end
+
+ return nil
+ end
+
+ def query_contains_identity_column(sql, col)
+ return sql =~ /[\(\.\,]\s*#{col}/
+ end
+ end
+ end
+end \ No newline at end of file
diff --git a/activerecord/lib/active_record/deprecated_associations.rb b/activerecord/lib/active_record/deprecated_associations.rb
new file mode 100644
index 0000000000..481b66bf0a
--- /dev/null
+++ b/activerecord/lib/active_record/deprecated_associations.rb
@@ -0,0 +1,70 @@
+module ActiveRecord
+ module Associations # :nodoc:
+ module ClassMethods
+ def deprecated_collection_count_method(collection_name)# :nodoc:
+ module_eval <<-"end_eval", __FILE__, __LINE__
+ def #{collection_name}_count(force_reload = false)
+ #{collection_name}.reload if force_reload
+ #{collection_name}.size
+ end
+ end_eval
+ end
+
+ def deprecated_add_association_relation(association_name)# :nodoc:
+ module_eval <<-"end_eval", __FILE__, __LINE__
+ def add_#{association_name}(*items)
+ #{association_name}.concat(items)
+ end
+ end_eval
+ end
+
+ def deprecated_remove_association_relation(association_name)# :nodoc:
+ module_eval <<-"end_eval", __FILE__, __LINE__
+ def remove_#{association_name}(*items)
+ #{association_name}.delete(items)
+ end
+ end_eval
+ end
+
+ def deprecated_has_collection_method(collection_name)# :nodoc:
+ module_eval <<-"end_eval", __FILE__, __LINE__
+ def has_#{collection_name}?(force_reload = false)
+ !#{collection_name}(force_reload).empty?
+ end
+ end_eval
+ end
+
+ def deprecated_find_in_collection_method(collection_name)# :nodoc:
+ module_eval <<-"end_eval", __FILE__, __LINE__
+ def find_in_#{collection_name}(association_id)
+ #{collection_name}.find(association_id)
+ end
+ end_eval
+ end
+
+ def deprecated_find_all_in_collection_method(collection_name)# :nodoc:
+ module_eval <<-"end_eval", __FILE__, __LINE__
+ def find_all_in_#{collection_name}(runtime_conditions = nil, orderings = nil, limit = nil, joins = nil)
+ #{collection_name}.find_all(runtime_conditions, orderings, limit, joins)
+ end
+ end_eval
+ end
+
+ def deprecated_create_method(collection_name)# :nodoc:
+ module_eval <<-"end_eval", __FILE__, __LINE__
+ def create_in_#{collection_name}(attributes = {})
+ #{collection_name}.create(attributes)
+ end
+ end_eval
+ end
+
+ def deprecated_build_method(collection_name)# :nodoc:
+ module_eval <<-"end_eval", __FILE__, __LINE__
+ def build_to_#{collection_name}(attributes = {})
+ #{collection_name}.build(attributes)
+ end
+ end_eval
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb
new file mode 100755
index 0000000000..f17768e1f2
--- /dev/null
+++ b/activerecord/lib/active_record/fixtures.rb
@@ -0,0 +1,208 @@
+require 'erb'
+require 'yaml'
+require 'active_record/support/class_inheritable_attributes'
+require 'active_record/support/inflector'
+
+# Fixtures are a way of organizing data that you want to test against. You normally have one YAML file with fixture
+# definitions per model. They're just hashes of hashes with the first-level key being the name of fixture (try to keep
+# that name unique across all fixtures in the system for reasons that will follow). The value to that key is a hash
+# where the keys are column names and the values the fixture data you want to insert into it. Example for developers.yml:
+#
+# david:
+# id: 1
+# name: David Heinemeier Hansson
+# birthday: 1979-10-15
+# profession: Systems development
+#
+# steve:
+# id: 2
+# name: Steve Ross Kellock
+# birthday: 1974-09-27
+# profession: guy with keyboard
+#
+# So this YAML file includes two fixtures. T
+#
+# Now when we call <tt>@developers = Fixtures.create_fixtures(".", "developers")</tt> both developers will get inserted into
+# the "developers" table through the active Active Record connection (that must be setup before-hand). And we can now query
+# the fixture data through the <tt>@developers</tt> hash, so <tt>@developers["david"]["name"]</tt> will return
+# <tt>"David Heinemeier Hansson"</tt> and <tt>@developers["david"]["birthday"]</tt> will return <tt>Date.new(1979, 10, 15)</tt>.
+#
+# In addition to getting the raw data, we can also get the Developer object by doing @developers["david"].find. This can then
+# be used for comparison in a unit test. Something like:
+#
+# def test_find
+# assert_equal @developers["david"]["name"], @developers["david"].find.name
+# end
+#
+# Comparing that the data we have on the name is also what the object returns when we ask for it.
+#
+# == Automatic fixture setup and instance variable availability
+#
+# Fixtures can also be automatically instantiated in instance variables relating to their names using the following style:
+#
+# class FixturesTest < Test::Unit::TestCase
+# fixtures :developers # you can add more with comma separation
+#
+# def test_developers
+# assert_equal 3, @developers.size # the container for all the fixtures is automatically set
+# assert_kind_of Developer, @david # works like @developers["david"].find
+# assert_equal "David Heinemeier Hansson", @david.name
+# end
+# end
+class Fixtures < Hash
+ def self.instantiate_fixtures(object, fixtures_directory, *table_names)
+ [ create_fixtures(fixtures_directory, *table_names) ].flatten.each_with_index do |fixtures, idx|
+ object.instance_variable_set "@#{table_names[idx]}", fixtures
+ fixtures.each { |name, fixture| object.instance_variable_set "@#{name}", fixture.find }
+ end
+ end
+
+ def self.create_fixtures(fixtures_directory, *table_names)
+ connection = block_given? ? yield : ActiveRecord::Base.connection
+ old_logger_level = ActiveRecord::Base.logger.level
+
+ begin
+ ActiveRecord::Base.logger.level = Logger::ERROR
+ fixtures = connection.transaction do
+ table_names.flatten.map do |table_name|
+ Fixtures.new(connection, table_name.to_s, File.join(fixtures_directory, table_name.to_s))
+ end
+ end
+ return fixtures.size > 1 ? fixtures : fixtures.first
+ ensure
+ ActiveRecord::Base.logger.level = old_logger_level
+ end
+ end
+
+ def initialize(connection, table_name, fixture_path, file_filter = /^\.|CVS|\.yml/)
+ @connection, @table_name, @fixture_path, @file_filter = connection, table_name, fixture_path, file_filter
+ @class_name = Inflector.classify(@table_name)
+
+ read_fixture_files
+ delete_existing_fixtures
+ insert_fixtures
+ end
+
+ private
+ def read_fixture_files
+ if File.exists?(yaml_file_path)
+ YAML::load(erb_render(IO.read(yaml_file_path))).each do |name, data|
+ self[name] = Fixture.new(data, @class_name)
+ end
+ else
+ Dir.entries(@fixture_path).each do |file|
+ self[file] = Fixture.new(File.join(@fixture_path, file), @class_name) unless file =~ @file_filter
+ end
+ end
+ end
+
+ def delete_existing_fixtures
+ @connection.delete "DELETE FROM #{@table_name}"
+ end
+
+ def insert_fixtures
+ values.each do |fixture|
+ @connection.execute "INSERT INTO #{@table_name} (#{fixture.key_list}) VALUES(#{fixture.value_list})"
+ end
+ end
+
+ def yaml_file_path
+ @fixture_path + ".yml"
+ end
+
+ def yaml_fixtures_key(path)
+ File.basename(@fixture_path).split(".").first
+ end
+
+ def erb_render(fixture_content)
+ ERB.new(fixture_content).result
+ end
+end
+
+class Fixture #:nodoc:
+ include Enumerable
+ class FixtureError < StandardError; end
+ class FormatError < FixtureError; end
+
+ def initialize(fixture, class_name)
+ @fixture = fixture.is_a?(Hash) ? fixture : read_fixture_file(fixture)
+ @class_name = class_name
+ end
+
+ def each
+ @fixture.each { |item| yield item }
+ end
+
+ def [](key)
+ @fixture[key]
+ end
+
+ def to_hash
+ @fixture
+ end
+
+ def key_list
+ @fixture.keys.join(", ")
+ end
+
+ def value_list
+ @fixture.values.map { |v| ActiveRecord::Base.connection.quote(v).gsub('\\n', "\n").gsub('\\r', "\r") }.join(", ")
+ end
+
+ def find
+ Object.const_get(@class_name).find(self["id"])
+ end
+
+ private
+ def read_fixture_file(fixture_file_path)
+ IO.readlines(fixture_file_path).inject({}) do |fixture, line|
+ # Mercifully skip empty lines.
+ next if line.empty?
+
+ # Use the same regular expression for attributes as Active Record.
+ unless md = /^\s*([a-zA-Z][-_\w]*)\s*=>\s*(.+)\s*$/.match(line)
+ raise FormatError, "#{path}: fixture format error at '#{line}'. Expecting 'key => value'."
+ end
+ key, value = md.captures
+
+ # Disallow duplicate keys to catch typos.
+ raise FormatError, "#{path}: duplicate '#{key}' in fixture." if fixture[key]
+ fixture[key] = value.strip
+ fixture
+ end
+ end
+end
+
+class Test::Unit::TestCase #:nodoc:
+ include ClassInheritableAttributes
+
+ cattr_accessor :fixture_path
+ cattr_accessor :fixture_table_names
+
+ def self.fixtures(*table_names)
+ write_inheritable_attribute("fixture_table_names", table_names)
+ end
+
+ def setup
+ instantiate_fixtures(*fixture_table_names) if fixture_table_names
+ end
+
+ def self.method_added(method_symbol)
+ if method_symbol == :setup && !method_defined?(:setup_without_fixtures)
+ alias_method :setup_without_fixtures, :setup
+ define_method(:setup) do
+ instantiate_fixtures(*fixture_table_names) if fixture_table_names
+ setup_without_fixtures
+ end
+ end
+ end
+
+ private
+ def instantiate_fixtures(*table_names)
+ Fixtures.instantiate_fixtures(self, fixture_path, *table_names)
+ end
+
+ def fixture_table_names
+ self.class.read_inheritable_attribute("fixture_table_names")
+ end
+end \ No newline at end of file
diff --git a/activerecord/lib/active_record/observer.rb b/activerecord/lib/active_record/observer.rb
new file mode 100644
index 0000000000..ceba78b043
--- /dev/null
+++ b/activerecord/lib/active_record/observer.rb
@@ -0,0 +1,71 @@
+require 'singleton'
+
+module ActiveRecord
+ # Observers can be programmed to react to lifecycle callbacks in another class to implement
+ # trigger-like behavior outside the original class. This is a great way to reduce the clutter that
+ # normally comes when the model class is burdened with excess responsibility that doesn't pertain to
+ # the core and nature of the class. Example:
+ #
+ # class CommentObserver < ActiveRecord::Observer
+ # def after_save(comment)
+ # Notifications.deliver_comment("admin@do.com", "New comment was posted", comment)
+ # end
+ # end
+ #
+ # This Observer is triggered when a Comment#save is finished and sends a notification about it to the administrator.
+ #
+ # == Observing a class that can't be infered
+ #
+ # Observers will by default be mapped to the class with which they share a name. So CommentObserver will
+ # be tied to observing Comment, ProductManagerObserver to ProductManager, and so on. If you want to name your observer
+ # something else than the class you're interested in observing, you can implement the observed_class class method. Like this:
+ #
+ # class AuditObserver < ActiveRecord::Observer
+ # def self.observed_class() Account end
+ # def after_update(account)
+ # AuditTrail.new(account, "UPDATED")
+ # end
+ # end
+ #
+ # == Observing multiple classes at once
+ #
+ # If the audit observer needs to watch more than one kind of object, this can be specified in an array, like this:
+ #
+ # class AuditObserver < ActiveRecord::Observer
+ # def self.observed_class() [ Account, Balance ] end
+ # def after_update(record)
+ # AuditTrail.new(record, "UPDATED")
+ # end
+ # end
+ #
+ # The AuditObserver will now act on both updates to Account and Balance by treating them both as records.
+ #
+ # The observer can implement callback methods for each of the methods described in the Callbacks module.
+ class Observer
+ include Singleton
+
+ def initialize
+ [ observed_class ].flatten.each do |klass|
+ klass.add_observer(self)
+ klass.send(:define_method, :after_find) unless klass.respond_to?(:after_find)
+ end
+ end
+
+ def update(callback_method, object)
+ send(callback_method, object) if respond_to?(callback_method)
+ end
+
+ private
+ def observed_class
+ if self.class.respond_to? "observed_class"
+ self.class.observed_class
+ else
+ Object.const_get(infer_observed_class_name)
+ end
+ end
+
+ def infer_observed_class_name
+ self.class.name.scan(/(.*)Observer/)[0][0]
+ end
+ end
+end \ No newline at end of file
diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb
new file mode 100644
index 0000000000..036200a200
--- /dev/null
+++ b/activerecord/lib/active_record/reflection.rb
@@ -0,0 +1,126 @@
+module ActiveRecord
+ module Reflection # :nodoc:
+ def self.append_features(base)
+ super
+ base.extend(ClassMethods)
+
+ base.class_eval do
+ class << self
+ alias_method :composed_of_without_reflection, :composed_of
+
+ def composed_of_with_reflection(part_id, options = {})
+ composed_of_without_reflection(part_id, options)
+ write_inheritable_array "aggregations", [ AggregateReflection.new(part_id, options, self) ]
+ end
+
+ alias_method :composed_of, :composed_of_with_reflection
+ end
+ end
+
+ for association_type in %w( belongs_to has_one has_many has_and_belongs_to_many )
+ base.module_eval <<-"end_eval"
+ class << self
+ alias_method :#{association_type}_without_reflection, :#{association_type}
+
+ def #{association_type}_with_reflection(association_id, options = {})
+ #{association_type}_without_reflection(association_id, options)
+ write_inheritable_array "associations", [ AssociationReflection.new(association_id, options, self) ]
+ end
+
+ alias_method :#{association_type}, :#{association_type}_with_reflection
+ end
+ end_eval
+ end
+ end
+
+ # Reflection allows you to interrogate Active Record classes and objects about their associations and aggregations.
+ # This information can for example be used in a form builder that took an Active Record object and created input
+ # fields for all of the attributes depending on their type and displayed the associations to other objects.
+ #
+ # You can find the interface for the AggregateReflection and AssociationReflection classes in the abstract MacroReflection class.
+ module ClassMethods
+ # Returns an array of AggregateReflection objects for all the aggregations in the class.
+ def reflect_on_all_aggregations
+ read_inheritable_attribute "aggregations"
+ end
+
+ # Returns the AggregateReflection object for the named +aggregation+ (use the symbol). Example:
+ # Account.reflect_on_aggregation(:balance) # returns the balance AggregateReflection
+ def reflect_on_aggregation(aggregation)
+ reflect_on_all_aggregations.find { |reflection| reflection.name == aggregation } unless reflect_on_all_aggregations.nil?
+ end
+
+ # Returns an array of AssociationReflection objects for all the aggregations in the class.
+ def reflect_on_all_associations
+ read_inheritable_attribute "associations"
+ end
+
+ # Returns the AssociationReflection object for the named +aggregation+ (use the symbol). Example:
+ # Account.reflect_on_association(:owner) # returns the owner AssociationReflection
+ def reflect_on_association(association)
+ reflect_on_all_associations.find { |reflection| reflection.name == association } unless reflect_on_all_associations.nil?
+ end
+ end
+
+
+ # Abstract base class for AggregateReflection and AssociationReflection that describes the interface available for both of
+ # those classes. Objects of AggregateReflection and AssociationReflection are returned by the Reflection::ClassMethods.
+ class MacroReflection
+ attr_reader :active_record
+ def initialize(name, options, active_record)
+ @name, @options, @active_record = name, options, active_record
+ end
+
+ # Returns the name of the macro, so it would return :balance for "composed_of :balance, :class_name => 'Money'" or
+ # :clients for "has_many :clients".
+ def name
+ @name
+ end
+
+ # Returns the hash of options used for the macro, so it would return { :class_name => "Money" } for
+ # "composed_of :balance, :class_name => 'Money'" or {} for "has_many :clients".
+ def options
+ @options
+ end
+
+ # Returns the class for the macro, so "composed_of :balance, :class_name => 'Money'" would return the Money class and
+ # "has_many :clients" would return the Client class.
+ def klass() end
+
+ def ==(other_aggregation)
+ name == other_aggregation.name && other_aggregation.options && active_record == other_aggregation.active_record
+ end
+ end
+
+
+ # Holds all the meta-data about an aggregation as it was specified in the Active Record class.
+ class AggregateReflection < MacroReflection #:nodoc:
+ def klass
+ Object.const_get(options[:class_name] || name_to_class_name(name.id2name))
+ end
+
+ private
+ def name_to_class_name(name)
+ name.capitalize.gsub(/_(.)/) { |s| $1.capitalize }
+ end
+ end
+
+ # Holds all the meta-data about an association as it was specified in the Active Record class.
+ class AssociationReflection < MacroReflection #:nodoc:
+ def klass
+ active_record.send(:compute_type, (name_to_class_name(name.id2name)))
+ end
+
+ private
+ def name_to_class_name(name)
+ if name !~ /::/
+ class_name = active_record.send(
+ :type_name_with_module,
+ (options[:class_name] || active_record.class_name(active_record.table_name_prefix + name + active_record.table_name_suffix))
+ )
+ end
+ return class_name || name
+ end
+ end
+ end
+end \ No newline at end of file
diff --git a/activerecord/lib/active_record/support/class_attribute_accessors.rb b/activerecord/lib/active_record/support/class_attribute_accessors.rb
new file mode 100644
index 0000000000..0e269165a6
--- /dev/null
+++ b/activerecord/lib/active_record/support/class_attribute_accessors.rb
@@ -0,0 +1,43 @@
+# attr_* style accessors for class-variables that can accessed both on an instance and class level.
+class Class #:nodoc:
+ def cattr_reader(*syms)
+ syms.each do |sym|
+ class_eval <<-EOS
+ if ! defined? @@#{sym.id2name}
+ @@#{sym.id2name} = nil
+ end
+
+ def self.#{sym.id2name}
+ @@#{sym}
+ end
+
+ def #{sym.id2name}
+ self.class.#{sym.id2name}
+ end
+ EOS
+ end
+ end
+
+ def cattr_writer(*syms)
+ syms.each do |sym|
+ class_eval <<-EOS
+ if ! defined? @@#{sym.id2name}
+ @@#{sym.id2name} = nil
+ end
+
+ def self.#{sym.id2name}=(obj)
+ @@#{sym.id2name} = obj
+ end
+
+ def #{sym.id2name}=(obj)
+ self.class.#{sym.id2name}=(obj)
+ end
+ EOS
+ end
+ end
+
+ def cattr_accessor(*syms)
+ cattr_reader(*syms)
+ cattr_writer(*syms)
+ end
+end \ No newline at end of file
diff --git a/activerecord/lib/active_record/support/class_inheritable_attributes.rb b/activerecord/lib/active_record/support/class_inheritable_attributes.rb
new file mode 100644
index 0000000000..ee69646da0
--- /dev/null
+++ b/activerecord/lib/active_record/support/class_inheritable_attributes.rb
@@ -0,0 +1,37 @@
+# Allows attributes to be shared within an inheritance hierarchy, but where each descentent gets a copy of
+# their parents' attributes, instead of just a pointer to the same. This means that the child can add elements
+# to, for example, an array without those additions being shared with either their parent, siblings, or
+# children, which is unlike the regular class-level attributes that are shared across the entire hierarchy.
+module ClassInheritableAttributes # :nodoc:
+ def self.append_features(base)
+ super
+ base.extend(ClassMethods)
+ end
+
+ module ClassMethods # :nodoc:
+ @@classes ||= {}
+
+ def inheritable_attributes
+ @@classes[self] ||= {}
+ end
+
+ def write_inheritable_attribute(key, value)
+ inheritable_attributes[key] = value
+ end
+
+ def write_inheritable_array(key, elements)
+ write_inheritable_attribute(key, []) if read_inheritable_attribute(key).nil?
+ write_inheritable_attribute(key, read_inheritable_attribute(key) + elements)
+ end
+
+ def read_inheritable_attribute(key)
+ inheritable_attributes[key]
+ end
+
+ private
+ def inherited(child)
+ @@classes[child] = inheritable_attributes.dup
+ end
+
+ end
+end
diff --git a/activerecord/lib/active_record/support/clean_logger.rb b/activerecord/lib/active_record/support/clean_logger.rb
new file mode 100644
index 0000000000..1a36562892
--- /dev/null
+++ b/activerecord/lib/active_record/support/clean_logger.rb
@@ -0,0 +1,10 @@
+require 'logger'
+
+class Logger #:nodoc:
+ private
+ remove_const "Format"
+ Format = "%s\n"
+ def format_message(severity, timestamp, msg, progname)
+ Format % [msg]
+ end
+end \ No newline at end of file
diff --git a/activerecord/lib/active_record/support/inflector.rb b/activerecord/lib/active_record/support/inflector.rb
new file mode 100644
index 0000000000..05ff4fede9
--- /dev/null
+++ b/activerecord/lib/active_record/support/inflector.rb
@@ -0,0 +1,78 @@
+# The Inflector transforms words from singular to plural, class names to table names, modulized class names to ones without,
+# and class names to foreign keys.
+module Inflector
+ extend self
+
+ def pluralize(word)
+ result = word.dup
+ plural_rules.each do |(rule, replacement)|
+ break if result.gsub!(rule, replacement)
+ end
+ return result
+ end
+
+ def singularize(word)
+ result = word.dup
+ singular_rules.each do |(rule, replacement)|
+ break if result.gsub!(rule, replacement)
+ end
+ return result
+ end
+
+ def camelize(lower_case_and_underscored_word)
+ lower_case_and_underscored_word.gsub(/(^|_)(.)/){$2.upcase}
+ end
+
+ def underscore(camel_cased_word)
+ camel_cased_word.gsub(/([A-Z]+)([A-Z])/,'\1_\2').gsub(/([a-z])([A-Z])/,'\1_\2').downcase
+ end
+
+ def demodulize(class_name_in_module)
+ class_name_in_module.gsub(/^.*::/, '')
+ end
+
+ def tableize(class_name)
+ pluralize(underscore(class_name))
+ end
+
+ def classify(table_name)
+ camelize(singularize(table_name))
+ end
+
+ def foreign_key(class_name, separate_class_name_and_id_with_underscore = true)
+ Inflector.underscore(Inflector.demodulize(class_name)) +
+ (separate_class_name_and_id_with_underscore ? "_id" : "id")
+ end
+
+ private
+ def plural_rules #:doc:
+ [
+ [/(x|ch|ss)$/, '\1es'], # search, switch, fix, box, process, address
+ [/([^aeiouy]|qu)y$/, '\1ies'], # query, ability, agency
+ [/(?:([^f])fe|([lr])f)$/, '\1\2ves'], # half, safe, wife
+ [/sis$/, 'ses'], # basis, diagnosis
+ [/([ti])um$/, '\1a'], # datum, medium
+ [/person$/, 'people'], # person, salesperson
+ [/man$/, 'men'], # man, woman, spokesman
+ [/child$/, 'children'], # child
+ [/s$/, 's'], # no change (compatibility)
+ [/$/, 's']
+ ]
+ end
+
+ def singular_rules #:doc:
+ [
+ [/(x|ch|ss)es$/, '\1'],
+ [/([^aeiouy]|qu)ies$/, '\1y'],
+ [/([lr])ves$/, '\1f'],
+ [/([^f])ves$/, '\1fe'],
+ [/(analy|ba|diagno|parenthe|progno|synop|the)ses$/, '\1sis'],
+ [/([ti])a$/, '\1um'],
+ [/people$/, 'person'],
+ [/men$/, 'man'],
+ [/status$/, 'status'],
+ [/children$/, 'child'],
+ [/s$/, '']
+ ]
+ end
+end \ No newline at end of file
diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb
new file mode 100644
index 0000000000..d440e74346
--- /dev/null
+++ b/activerecord/lib/active_record/transactions.rb
@@ -0,0 +1,119 @@
+require 'active_record/vendor/simple.rb'
+require 'thread'
+
+module ActiveRecord
+ module Transactions # :nodoc:
+ TRANSACTION_MUTEX = Mutex.new
+
+ def self.append_features(base)
+ super
+ base.extend(ClassMethods)
+
+ base.class_eval do
+ alias_method :destroy_without_transactions, :destroy
+ alias_method :destroy, :destroy_with_transactions
+
+ alias_method :save_without_transactions, :save
+ alias_method :save, :save_with_transactions
+ end
+ end
+
+ # Transactions are protective blocks where SQL statements are only permanent if they can all succeed as one atomic action.
+ # The classic example is a transfer between two accounts where you can only have a deposit if the withdrawal succedded and
+ # vice versa. Transaction enforce the integrity of the database and guards the data against program errors or database break-downs.
+ # So basically you should use transaction blocks whenever you have a number of statements that must be executed together or
+ # not at all. Example:
+ #
+ # transaction do
+ # david.withdrawal(100)
+ # mary.deposit(100)
+ # end
+ #
+ # This example will only take money from David and give to Mary if neither +withdrawal+ nor +deposit+ raises an exception.
+ # Exceptions will force a ROLLBACK that returns the database to the state before the transaction was begun. Be aware, though,
+ # that the objects by default will _not_ have their instance data returned to their pre-transactional state.
+ #
+ # == Transactions are not distributed across database connections
+ #
+ # A transaction acts on a single database connection. If you have
+ # multiple class-specific databases, the transaction will not protect
+ # interaction among them. One workaround is to begin a transaction
+ # on each class whose models you alter:
+ #
+ # Student.transaction do
+ # Course.transaction do
+ # course.enroll(student)
+ # student.units += course.units
+ # end
+ # end
+ #
+ # This is a poor solution, but full distributed transactions are beyond
+ # the scope of Active Record.
+ #
+ # == Save and destroy are automatically wrapped in a transaction
+ #
+ # Both Base#save and Base#destroy come wrapped in a transaction that ensures that whatever you do in validations or callbacks
+ # will happen under the protected cover of a transaction. So you can use validations to check for values that the transaction
+ # depend on or you can raise exceptions in the callbacks to rollback.
+ #
+ # == Object-level transactions
+ #
+ # You can enable object-level transactions for Active Record objects, though. You do this by naming the each of the Active Records
+ # that you want to enable object-level transactions for, like this:
+ #
+ # Account.transaction(david, mary) do
+ # david.withdrawal(100)
+ # mary.deposit(100)
+ # end
+ #
+ # If the transaction fails, David and Mary will be returned to their pre-transactional state. No money will have changed hands in
+ # neither object nor database.
+ #
+ # == Exception handling
+ #
+ # Also have in mind that exceptions thrown within a transaction block will be propagated (after triggering the ROLLBACK), so you
+ # should be ready to catch those in your application code.
+ #
+ # Tribute: Object-level transactions are implemented by Transaction::Simple by Austin Ziegler.
+ module ClassMethods
+ def transaction(*objects, &block)
+ TRANSACTION_MUTEX.lock
+
+ begin
+ objects.each { |o| o.extend(Transaction::Simple) }
+ objects.each { |o| o.start_transaction }
+
+ result = connection.transaction(&block)
+
+ objects.each { |o| o.commit_transaction }
+ return result
+ rescue Exception => object_transaction_rollback
+ objects.each { |o| o.abort_transaction }
+ raise
+ ensure
+ TRANSACTION_MUTEX.unlock
+ end
+ end
+ end
+
+ def transaction(*objects, &block)
+ self.class.transaction(*objects, &block)
+ end
+
+ def destroy_with_transactions #:nodoc:
+ if TRANSACTION_MUTEX.locked?
+ destroy_without_transactions
+ else
+ transaction { destroy_without_transactions }
+ end
+ end
+
+ def save_with_transactions(perform_validation = true) #:nodoc:
+ if TRANSACTION_MUTEX.locked?
+ save_without_transactions(perform_validation)
+ else
+ transaction { save_without_transactions(perform_validation) }
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb
new file mode 100755
index 0000000000..07bc7b99b5
--- /dev/null
+++ b/activerecord/lib/active_record/validations.rb
@@ -0,0 +1,205 @@
+module ActiveRecord
+ # Active Records implement validation by overwriting Base#validate (or the variations, +validate_on_create+ and
+ # +validate_on_update+). Each of these methods can inspect the state of the object, which usually means ensuring
+ # that a number of attributes have a certain value (such as not empty, within a given range, matching a certain regular expression).
+ #
+ # Example:
+ #
+ # class Person < ActiveRecord::Base
+ # protected
+ # def validate
+ # errors.add_on_empty %w( first_name last_name )
+ # errors.add("phone_number", "has invalid format") unless phone_number =~ /[0-9]*/
+ # end
+ #
+ # def validate_on_create # is only run the first time a new object is saved
+ # unless valid_discount?(membership_discount)
+ # errors.add("membership_discount", "has expired")
+ # end
+ # end
+ #
+ # def validate_on_update
+ # errors.add_to_base("No changes have occured") if unchanged_attributes?
+ # end
+ # end
+ #
+ # person = Person.new("first_name" => "David", "phone_number" => "what?")
+ # person.save # => false (and doesn't do the save)
+ # person.errors.empty? # => false
+ # person.count # => 2
+ # person.errors.on "last_name" # => "can't be empty"
+ # person.errors.on "phone_number" # => "has invalid format"
+ # person.each_full { |msg| puts msg } # => "Last name can't be empty\n" +
+ # "Phone number has invalid format"
+ #
+ # person.attributes = { "last_name" => "Heinemeier", "phone_number" => "555-555" }
+ # person.save # => true (and person is now saved in the database)
+ #
+ # An +Errors+ object is automatically created for every Active Record.
+ module Validations
+ def self.append_features(base) # :nodoc:
+ super
+
+ base.class_eval do
+ alias_method :save_without_validation, :save
+ alias_method :save, :save_with_validation
+
+ alias_method :update_attribute_without_validation_skipping, :update_attribute
+ alias_method :update_attribute, :update_attribute_with_validation_skipping
+ end
+ end
+
+ # The validation process on save can be skipped by passing false. The regular Base#save method is
+ # replaced with this when the validations module is mixed in, which it is by default.
+ def save_with_validation(perform_validation = true)
+ if perform_validation && valid? || !perform_validation then save_without_validation else false end
+ end
+
+ # Updates a single attribute and saves the record without going through the normal validation procedure.
+ # This is especially useful for boolean flags on existing records. The regular +update_attribute+ method
+ # in Base is replaced with this when the validations module is mixed in, which it is by default.
+ def update_attribute_with_validation_skipping(name, value)
+ @attributes[name] = value
+ save(false)
+ end
+
+ # Runs validate and validate_on_create or validate_on_update and returns true if no errors were added otherwise false.
+ def valid?
+ errors.clear
+ validate
+ if new_record? then validate_on_create else validate_on_update end
+ errors.empty?
+ end
+
+ # Returns the Errors object that holds all information about attribute error messages.
+ def errors
+ @errors = Errors.new(self) if @errors.nil?
+ @errors
+ end
+
+ protected
+ # Overwrite this method for validation checks on all saves and use Errors.add(field, msg) for invalid attributes.
+ def validate #:doc:
+ end
+
+ # Overwrite this method for validation checks used only on creation.
+ def validate_on_create #:doc:
+ end
+
+ # Overwrite this method for validation checks used only on updates.
+ def validate_on_update # :doc:
+ end
+ end
+
+ # Active Record validation is reported to and from this object, which is used by Base#save to
+ # determine whether the object in a valid state to be saved. See usage example in Validations.
+ class Errors
+ def initialize(base) # :nodoc:
+ @base, @errors = base, {}
+ end
+
+ # Adds an error to the base object instead of any particular attribute. This is used
+ # to report errors that doesn't tie to any specific attribute, but rather to the object
+ # as a whole. These error messages doesn't get prepended with any field name when iterating
+ # with each_full, so they should be complete sentences.
+ def add_to_base(msg)
+ add(:base, msg)
+ end
+
+ # Adds an error message (+msg+) to the +attribute+, which will be returned on a call to <tt>on(attribute)</tt>
+ # for the same attribute and ensure that this error object returns false when asked if +empty?+. More than one
+ # error can be added to the same +attribute+ in which case an array will be returned on a call to <tt>on(attribute)</tt>.
+ # If no +msg+ is supplied, "invalid" is assumed.
+ def add(attribute, msg = "invalid")
+ @errors[attribute] = [] if @errors[attribute].nil?
+ @errors[attribute] << msg
+ end
+
+ # Will add an error message to each of the attributes in +attributes+ that is empty (defined by <tt>attribute_present?</tt>).
+ def add_on_empty(attributes, msg = "can't be empty")
+ [attributes].flatten.each { |attr| add(attr, msg) unless @base.attribute_present?(attr) }
+ end
+
+ # Will add an error message to each of the attributes in +attributes+ that has a length outside of the passed boundary +range+.
+ # If the length is above the boundary, the too_long_msg message will be used. If below, the too_short_msg.
+ def add_on_boundary_breaking(attributes, range, too_long_msg = "is too long (max is %d characters)", too_short_msg = "is too short (min is %d characters)")
+ for attr in [attributes].flatten
+ add(attr, too_short_msg % range.begin) if @base.attribute_present?(attr) && @base.send(attr).length < range.begin
+ add(attr, too_long_msg % range.end) if @base.attribute_present?(attr) && @base.send(attr).length > range.end
+ end
+ end
+
+ alias :add_on_boundry_breaking :add_on_boundary_breaking
+
+ # Returns true if the specified +attribute+ has errors associated with it.
+ def invalid?(attribute)
+ !@errors[attribute].nil?
+ end
+
+ # * Returns nil, if no errors are associated with the specified +attribute+.
+ # * Returns the error message, if one error is associated with the specified +attribute+.
+ # * Returns an array of error messages, if more than one error is associated with the specified +attribute+.
+ def on(attribute)
+ if @errors[attribute].nil?
+ nil
+ elsif @errors[attribute].length == 1
+ @errors[attribute].first
+ else
+ @errors[attribute]
+ end
+ end
+
+ alias :[] :on
+
+ # Returns errors assigned to base object through add_to_base according to the normal rules of on(attribute).
+ def on_base
+ on(:base)
+ end
+
+ # Yields each attribute and associated message per error added.
+ def each
+ @errors.each_key { |attr| @errors[attr].each { |msg| yield attr, msg } }
+ end
+
+ # Yields each full error message added. So Person.errors.add("first_name", "can't be empty") will be returned
+ # through iteration as "First name can't be empty".
+ def each_full
+ full_messages.each { |msg| yield msg }
+ end
+
+ # Returns all the full error messages in an array.
+ def full_messages
+ full_messages = []
+
+ @errors.each_key do |attr|
+ @errors[attr].each do |msg|
+ if attr == :base
+ full_messages << msg
+ else
+ full_messages << @base.class.human_attribute_name(attr) + " " + msg
+ end
+ end
+ end
+
+ return full_messages
+ end
+
+ # Returns true if no errors have been added.
+ def empty?
+ return @errors.empty?
+ end
+
+ # Removes all the errors that have been added.
+ def clear
+ @errors = {}
+ end
+
+ # Returns the total number of errors added. Two errors added to the same attribute will be counted as such
+ # with this as well.
+ def count
+ error_count = 0
+ @errors.each_value { |attribute| error_count += attribute.length }
+ error_count
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/vendor/mysql.rb b/activerecord/lib/active_record/vendor/mysql.rb
new file mode 100644
index 0000000000..4970f77bd3
--- /dev/null
+++ b/activerecord/lib/active_record/vendor/mysql.rb
@@ -0,0 +1,1117 @@
+# $Id: mysql.rb,v 1.1 2004/02/24 15:42:29 webster132 Exp $
+#
+# Copyright (C) 2003 TOMITA Masahiro
+# tommy@tmtm.org
+#
+
+class Mysql
+
+ VERSION = "4.0-ruby-0.2.4"
+
+ require "socket"
+
+ MAX_PACKET_LENGTH = 256*256*256-1
+ MAX_ALLOWED_PACKET = 1024*1024*1024
+
+ MYSQL_UNIX_ADDR = "/tmp/mysql.sock"
+ MYSQL_PORT = 3306
+ PROTOCOL_VERSION = 10
+
+ # Command
+ COM_SLEEP = 0
+ COM_QUIT = 1
+ COM_INIT_DB = 2
+ COM_QUERY = 3
+ COM_FIELD_LIST = 4
+ COM_CREATE_DB = 5
+ COM_DROP_DB = 6
+ COM_REFRESH = 7
+ COM_SHUTDOWN = 8
+ COM_STATISTICS = 9
+ COM_PROCESS_INFO = 10
+ COM_CONNECT = 11
+ COM_PROCESS_KILL = 12
+ COM_DEBUG = 13
+ COM_PING = 14
+ COM_TIME = 15
+ COM_DELAYED_INSERT = 16
+ COM_CHANGE_USER = 17
+ COM_BINLOG_DUMP = 18
+ COM_TABLE_DUMP = 19
+ COM_CONNECT_OUT = 20
+ COM_REGISTER_SLAVE = 21
+
+ # Client flag
+ CLIENT_LONG_PASSWORD = 1
+ CLIENT_FOUND_ROWS = 1 << 1
+ CLIENT_LONG_FLAG = 1 << 2
+ CLIENT_CONNECT_WITH_DB= 1 << 3
+ CLIENT_NO_SCHEMA = 1 << 4
+ CLIENT_COMPRESS = 1 << 5
+ CLIENT_ODBC = 1 << 6
+ CLIENT_LOCAL_FILES = 1 << 7
+ CLIENT_IGNORE_SPACE = 1 << 8
+ CLIENT_INTERACTIVE = 1 << 10
+ CLIENT_SSL = 1 << 11
+ CLIENT_IGNORE_SIGPIPE = 1 << 12
+ CLIENT_TRANSACTIONS = 1 << 13
+ CLIENT_CAPABILITIES = CLIENT_LONG_PASSWORD|CLIENT_LONG_FLAG|CLIENT_TRANSACTIONS
+
+ # Connection Option
+ OPT_CONNECT_TIMEOUT = 0
+ OPT_COMPRESS = 1
+ OPT_NAMED_PIPE = 2
+ INIT_COMMAND = 3
+ READ_DEFAULT_FILE = 4
+ READ_DEFAULT_GROUP = 5
+ SET_CHARSET_DIR = 6
+ SET_CHARSET_NAME = 7
+ OPT_LOCAL_INFILE = 8
+
+ # Server Status
+ SERVER_STATUS_IN_TRANS = 1
+ SERVER_STATUS_AUTOCOMMIT = 2
+
+ # Refresh parameter
+ REFRESH_GRANT = 1
+ REFRESH_LOG = 2
+ REFRESH_TABLES = 4
+ REFRESH_HOSTS = 8
+ REFRESH_STATUS = 16
+ REFRESH_THREADS = 32
+ REFRESH_SLAVE = 64
+ REFRESH_MASTER = 128
+
+ def initialize(*args)
+ @client_flag = 0
+ @max_allowed_packet = MAX_ALLOWED_PACKET
+ @query_with_result = true
+ @status = :STATUS_READY
+ if args[0] != :INIT then
+ real_connect(*args)
+ end
+ end
+
+ def real_connect(host=nil, user=nil, passwd=nil, db=nil, port=nil, socket=nil, flag=nil)
+ @server_status = SERVER_STATUS_AUTOCOMMIT
+ if (host == nil or host == "localhost") and defined? UNIXSocket then
+ unix_socket = socket || ENV["MYSQL_UNIX_PORT"] || MYSQL_UNIX_ADDR
+ sock = UNIXSocket::new(unix_socket)
+ @host_info = Error::err(Error::CR_LOCALHOST_CONNECTION)
+ @unix_socket = unix_socket
+ else
+ sock = TCPSocket::new(host, port||ENV["MYSQL_TCP_PORT"]||(Socket::getservbyname("mysql","tcp") rescue MYSQL_PORT))
+ @host_info = sprintf Error::err(Error::CR_TCP_CONNECTION), host
+ end
+ @host = host ? host.dup : nil
+ sock.setsockopt Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true
+ @net = Net::new sock
+
+ a = read
+ @protocol_version = a.slice!(0)
+ @server_version, a = a.split(/\0/,2)
+ @thread_id, @scramble_buff = a.slice!(0,13).unpack("La8")
+ if a.size >= 2 then
+ @server_capabilities, = a.slice!(0,2).unpack("v")
+ end
+ if a.size >= 16 then
+ @server_language, @server_status = a.unpack("cv")
+ end
+
+ flag = 0 if flag == nil
+ flag |= @client_flag | CLIENT_CAPABILITIES
+ flag |= CLIENT_CONNECT_WITH_DB if db
+ data = Net::int2str(flag)+Net::int3str(@max_allowed_packet)+(user||"")+"\0"+scramble(passwd, @scramble_buff, @protocol_version==9)
+ if db and @server_capabilities & CLIENT_CONNECT_WITH_DB != 0 then
+ data << "\0"+db
+ @db = db.dup
+ end
+ write data
+ read
+ self
+ end
+ alias :connect :real_connect
+
+ def escape_string(str)
+ Mysql::escape_string str
+ end
+ alias :quote :escape_string
+
+ def get_client_info()
+ VERSION
+ end
+ alias :client_info :get_client_info
+
+ def options(option, arg=nil)
+ if option == OPT_LOCAL_INFILE then
+ if arg == false or arg == 0 then
+ @client_flag &= ~CLIENT_LOCAL_FILES
+ else
+ @client_flag |= CLIENT_LOCAL_FILES
+ end
+ else
+ raise "not implemented"
+ end
+ end
+
+ def real_query(query)
+ command COM_QUERY, query, true
+ read_query_result
+ self
+ end
+
+ def use_result()
+ if @status != :STATUS_GET_RESULT then
+ error Error::CR_COMMANDS_OUT_OF_SYNC
+ end
+ res = Result::new self, @fields, @field_count
+ @status = :STATUS_USE_RESULT
+ res
+ end
+
+ def store_result()
+ if @status != :STATUS_GET_RESULT then
+ error Error::CR_COMMANDS_OUT_OF_SYNC
+ end
+ @status = :STATUS_READY
+ data = read_rows @field_count
+ res = Result::new self, @fields, @field_count, data
+ @fields = nil
+ @affected_rows = data.length
+ res
+ end
+
+ def change_user(user="", passwd="", db="")
+ data = user+"\0"+scramble(passwd, @scramble_buff, @protocol_version==9)+"\0"+db
+ command COM_CHANGE_USER, data
+ @user = user
+ @passwd = passwd
+ @db = db
+ end
+
+ def character_set_name()
+ raise "not implemented"
+ end
+
+ def close()
+ @status = :STATUS_READY
+ command COM_QUIT, nil, true
+ @net.close
+ self
+ end
+
+ def create_db(db)
+ command COM_CREATE_DB, db
+ self
+ end
+
+ def drop_db(db)
+ command COM_DROP_DB, db
+ self
+ end
+
+ def dump_debug_info()
+ command COM_DEBUG
+ self
+ end
+
+ def get_host_info()
+ @host_info
+ end
+ alias :host_info :get_host_info
+
+ def get_proto_info()
+ @protocol_version
+ end
+ alias :proto_info :get_proto_info
+
+ def get_server_info()
+ @server_version
+ end
+ alias :server_info :get_server_info
+
+ def kill(id)
+ command COM_PROCESS_KILL, Net::int4str(id)
+ self
+ end
+
+ def list_dbs(db=nil)
+ real_query "show databases #{db}"
+ @status = :STATUS_READY
+ read_rows(1).flatten
+ end
+
+ def list_fields(table, field=nil)
+ command COM_FIELD_LIST, "#{table}\0#{field}", true
+ f = read_rows 6
+ fields = unpack_fields(f, @server_capabilities & CLIENT_LONG_FLAG != 0)
+ res = Result::new self, fields, f.length
+ res.eof = true
+ res
+ end
+
+ def list_processes()
+ data = command COM_PROCESS_INFO
+ @field_count = get_length data
+ fields = read_rows 5
+ @fields = unpack_fields(fields, @server_capabilities & CLIENT_LONG_FLAG != 0)
+ @status = :STATUS_GET_RESULT
+ store_result
+ end
+
+ def list_tables(table=nil)
+ real_query "show tables #{table}"
+ @status = :STATUS_READY
+ read_rows(1).flatten
+ end
+
+ def ping()
+ command COM_PING
+ self
+ end
+
+ def query(query)
+ real_query query
+ if not @query_with_result then
+ return self
+ end
+ if @field_count == 0 then
+ return nil
+ end
+ store_result
+ end
+
+ def refresh(r)
+ command COM_REFRESH, r.chr
+ self
+ end
+
+ def reload()
+ refresh REFRESH_GRANT
+ self
+ end
+
+ def select_db(db)
+ command COM_INIT_DB, db
+ @db = db
+ self
+ end
+
+ def shutdown()
+ command COM_SHUTDOWN
+ self
+ end
+
+ def stat()
+ command COM_STATISTICS
+ end
+
+ attr_reader :info, :insert_id, :affected_rows, :field_count, :thread_id
+ attr_accessor :query_with_result, :status
+
+ def read_one_row(field_count)
+ data = read
+ return if data[0] == 254 and data.length == 1
+ rec = []
+ field_count.times do
+ len = get_length data
+ if len == nil then
+ rec << len
+ else
+ rec << data.slice!(0,len)
+ end
+ end
+ rec
+ end
+
+ def skip_result()
+ if @status == :STATUS_USE_RESULT then
+ loop do
+ data = read
+ break if data[0] == 254 and data.length == 1
+ end
+ @status = :STATUS_READY
+ end
+ end
+
+ def inspect()
+ "#<#{self.class}>"
+ end
+
+ private
+
+ def read_query_result()
+ data = read
+ @field_count = get_length(data)
+ if @field_count == nil then # LOAD DATA LOCAL INFILE
+ File::open(data) do |f|
+ write f.read
+ end
+ write "" # mark EOF
+ data = read
+ @field_count = get_length(data)
+ end
+ if @field_count == 0 then
+ @affected_rows = get_length(data, true)
+ @insert_id = get_length(data, true)
+ if @server_capabilities & CLIENT_TRANSACTIONS != 0 then
+ a = data.slice!(0,2)
+ @server_status = a[0]+a[1]*256
+ end
+ if data.size > 0 and get_length(data) then
+ @info = data
+ end
+ else
+ @extra_info = get_length(data, true)
+ fields = read_rows 5
+ @fields = unpack_fields(fields, @server_capabilities & CLIENT_LONG_FLAG != 0)
+ @status = :STATUS_GET_RESULT
+ end
+ self
+ end
+
+ def unpack_fields(data, long_flag_protocol)
+ ret = []
+ data.each do |f|
+ table = org_table = f[0]
+ name = f[1]
+ length = f[2][0]+f[2][1]*256+f[2][2]*256*256
+ type = f[3][0]
+ if long_flag_protocol then
+ flags = f[4][0]+f[4][1]*256
+ decimals = f[4][2]
+ else
+ flags = f[4][0]
+ decimals = f[4][1]
+ end
+ def_value = f[5]
+ max_length = 0
+ ret << Field::new(table, org_table, name, length, type, flags, decimals, def_value, max_length)
+ end
+ ret
+ end
+
+ def read_rows(field_count)
+ ret = []
+ while rec = read_one_row(field_count) do
+ ret << rec
+ end
+ ret
+ end
+
+ def get_length(data, longlong=nil)
+ return if data.length == 0
+ c = data.slice!(0)
+ case c
+ when 251
+ return nil
+ when 252
+ a = data.slice!(0,2)
+ return a[0]+a[1]*256
+ when 253
+ a = data.slice!(0,3)
+ return a[0]+a[1]*256+a[2]*256**2
+ when 254
+ a = data.slice!(0,8)
+ if longlong then
+ return a[0]+a[1]*256+a[2]*256**2+a[3]*256**3+
+ a[4]*256**4+a[5]*256**5+a[6]*256**6+a[7]*256**7
+ else
+ return a[0]+a[1]*256+a[2]*256**2+a[3]*256**3
+ end
+ else
+ c
+ end
+ end
+
+ def command(cmd, arg=nil, skip_check=nil)
+ unless @net then
+ error Error::CR_SERVER_GONE_ERROR
+ end
+ if @status != :STATUS_READY then
+ error Error::CR_COMMANDS_OUT_OF_SYNC
+ end
+ @net.clear
+ write cmd.chr+(arg||"")
+ read unless skip_check
+ end
+
+ def read()
+ unless @net then
+ error Error::CR_SERVER_GONE_ERROR
+ end
+ a = @net.read
+ if a[0] == 255 then
+ if a.length > 3 then
+ @errno = a[1]+a[2]*256
+ @error = a[3 .. -1]
+ else
+ @errno = Error::CR_UNKNOWN_ERROR
+ @error = Error::err @errno
+ end
+ raise Error::new(@errno, @error)
+ end
+ a
+ end
+
+ def write(arg)
+ unless @net then
+ error Error::CR_SERVER_GONE_ERROR
+ end
+ @net.write arg
+ end
+
+ def hash_password(password)
+ nr = 1345345333
+ add = 7
+ nr2 = 0x12345671
+ password.each_byte do |i|
+ next if i == 0x20 or i == 9
+ nr ^= (((nr & 63) + add) * i) + (nr << 8)
+ nr2 += (nr2 << 8) ^ nr
+ add += i
+ end
+ [nr & ((1 << 31) - 1), nr2 & ((1 << 31) - 1)]
+ end
+
+ def scramble(password, message, old_ver)
+ return "" if password == nil or password == ""
+ raise "old version password is not implemented" if old_ver
+ hash_pass = hash_password password
+ hash_message = hash_password message
+ rnd = Random::new hash_pass[0] ^ hash_message[0], hash_pass[1] ^ hash_message[1]
+ to = []
+ 1.upto(message.length) do
+ to << ((rnd.rnd*31)+64).floor
+ end
+ extra = (rnd.rnd*31).floor
+ to.map! do |t| (t ^ extra).chr end
+ to.join
+ end
+
+ def error(errno)
+ @errno = errno
+ @error = Error::err errno
+ raise Error::new(@errno, @error)
+ end
+
+ class Result
+ def initialize(mysql, fields, field_count, data=nil)
+ @handle = mysql
+ @fields = fields
+ @field_count = field_count
+ @data = data
+ @current_field = 0
+ @current_row = 0
+ @eof = false
+ @row_count = 0
+ end
+ attr_accessor :eof
+
+ def data_seek(n)
+ @current_row = n
+ end
+
+ def fetch_field()
+ return if @current_field >= @field_count
+ f = @fields[@current_field]
+ @current_field += 1
+ f
+ end
+
+ def fetch_fields()
+ @fields
+ end
+
+ def fetch_field_direct(n)
+ @fields[n]
+ end
+
+ def fetch_lengths()
+ @data ? @data[@current_row].map{|i| i ? i.length : 0} : @lengths
+ end
+
+ def fetch_row()
+ if @data then
+ if @current_row >= @data.length then
+ @handle.status = :STATUS_READY
+ return
+ end
+ ret = @data[@current_row]
+ @current_row += 1
+ else
+ return if @eof
+ ret = @handle.read_one_row @field_count
+ if ret == nil then
+ @eof = true
+ return
+ end
+ @lengths = ret.map{|i| i ? i.length : 0}
+ @row_count += 1
+ end
+ ret
+ end
+
+ def fetch_hash(with_table=nil)
+ row = fetch_row
+ return if row == nil
+ hash = {}
+ @fields.each_index do |i|
+ f = with_table ? @fields[i].table+"."+@fields[i].name : @fields[i].name
+ hash[f] = row[i]
+ end
+ hash
+ end
+
+ def field_seek(n)
+ @current_field = n
+ end
+
+ def field_tell()
+ @current_field
+ end
+
+ def free()
+ @handle.skip_result
+ @handle = @fields = @data = nil
+ GC::start
+ end
+
+ def num_fields()
+ @field_count
+ end
+
+ def num_rows()
+ @data ? @data.length : @row_count
+ end
+
+ def row_seek(n)
+ @current_row = n
+ end
+
+ def row_tell()
+ @current_row
+ end
+
+ def each()
+ while row = fetch_row do
+ yield row
+ end
+ end
+
+ def each_hash(with_table=nil)
+ while hash = fetch_hash(with_table) do
+ yield hash
+ end
+ end
+
+ def inspect()
+ "#<#{self.class}>"
+ end
+
+ end
+
+ class Field
+ # Field type
+ TYPE_DECIMAL = 0
+ TYPE_TINY = 1
+ TYPE_SHORT = 2
+ TYPE_LONG = 3
+ TYPE_FLOAT = 4
+ TYPE_DOUBLE = 5
+ TYPE_NULL = 6
+ TYPE_TIMESTAMP = 7
+ TYPE_LONGLONG = 8
+ TYPE_INT24 = 9
+ TYPE_DATE = 10
+ TYPE_TIME = 11
+ TYPE_DATETIME = 12
+ TYPE_YEAR = 13
+ TYPE_NEWDATE = 14
+ TYPE_ENUM = 247
+ TYPE_SET = 248
+ TYPE_TINY_BLOB = 249
+ TYPE_MEDIUM_BLOB = 250
+ TYPE_LONG_BLOB = 251
+ TYPE_BLOB = 252
+ TYPE_VAR_STRING = 253
+ TYPE_STRING = 254
+ TYPE_GEOMETRY = 255
+ TYPE_CHAR = TYPE_TINY
+ TYPE_INTERVAL = TYPE_ENUM
+
+ # Flag
+ NOT_NULL_FLAG = 1
+ PRI_KEY_FLAG = 2
+ UNIQUE_KEY_FLAG = 4
+ MULTIPLE_KEY_FLAG = 8
+ BLOB_FLAG = 16
+ UNSIGNED_FLAG = 32
+ ZEROFILL_FLAG = 64
+ BINARY_FLAG = 128
+ ENUM_FLAG = 256
+ AUTO_INCREMENT_FLAG = 512
+ TIMESTAMP_FLAG = 1024
+ SET_FLAG = 2048
+ NUM_FLAG = 32768
+ PART_KEY_FLAG = 16384
+ GROUP_FLAG = 32768
+ UNIQUE_FLAG = 65536
+
+ def initialize(table, org_table, name, length, type, flags, decimals, def_value, max_length)
+ @table = table
+ @org_table = org_table
+ @name = name
+ @length = length
+ @type = type
+ @flags = flags
+ @decimals = decimals
+ @def = def_value
+ @max_length = max_length
+ if (type <= TYPE_INT24 and (type != TYPE_TIMESTAMP or length == 14 or length == 8)) or type == TYPE_YEAR then
+ @flags |= NUM_FLAG
+ end
+ end
+ attr_reader :table, :org_table, :name, :length, :type, :flags, :decimals, :def, :max_length
+
+ def inspect()
+ "#<#{self.class}:#{@name}>"
+ end
+ end
+
+ class Error < StandardError
+ # Server Error
+ ER_HASHCHK = 1000
+ ER_NISAMCHK = 1001
+ ER_NO = 1002
+ ER_YES = 1003
+ ER_CANT_CREATE_FILE = 1004
+ ER_CANT_CREATE_TABLE = 1005
+ ER_CANT_CREATE_DB = 1006
+ ER_DB_CREATE_EXISTS = 1007
+ ER_DB_DROP_EXISTS = 1008
+ ER_DB_DROP_DELETE = 1009
+ ER_DB_DROP_RMDIR = 1010
+ ER_CANT_DELETE_FILE = 1011
+ ER_CANT_FIND_SYSTEM_REC = 1012
+ ER_CANT_GET_STAT = 1013
+ ER_CANT_GET_WD = 1014
+ ER_CANT_LOCK = 1015
+ ER_CANT_OPEN_FILE = 1016
+ ER_FILE_NOT_FOUND = 1017
+ ER_CANT_READ_DIR = 1018
+ ER_CANT_SET_WD = 1019
+ ER_CHECKREAD = 1020
+ ER_DISK_FULL = 1021
+ ER_DUP_KEY = 1022
+ ER_ERROR_ON_CLOSE = 1023
+ ER_ERROR_ON_READ = 1024
+ ER_ERROR_ON_RENAME = 1025
+ ER_ERROR_ON_WRITE = 1026
+ ER_FILE_USED = 1027
+ ER_FILSORT_ABORT = 1028
+ ER_FORM_NOT_FOUND = 1029
+ ER_GET_ERRNO = 1030
+ ER_ILLEGAL_HA = 1031
+ ER_KEY_NOT_FOUND = 1032
+ ER_NOT_FORM_FILE = 1033
+ ER_NOT_KEYFILE = 1034
+ ER_OLD_KEYFILE = 1035
+ ER_OPEN_AS_READONLY = 1036
+ ER_OUTOFMEMORY = 1037
+ ER_OUT_OF_SORTMEMORY = 1038
+ ER_UNEXPECTED_EOF = 1039
+ ER_CON_COUNT_ERROR = 1040
+ ER_OUT_OF_RESOURCES = 1041
+ ER_BAD_HOST_ERROR = 1042
+ ER_HANDSHAKE_ERROR = 1043
+ ER_DBACCESS_DENIED_ERROR = 1044
+ ER_ACCESS_DENIED_ERROR = 1045
+ ER_NO_DB_ERROR = 1046
+ ER_UNKNOWN_COM_ERROR = 1047
+ ER_BAD_NULL_ERROR = 1048
+ ER_BAD_DB_ERROR = 1049
+ ER_TABLE_EXISTS_ERROR = 1050
+ ER_BAD_TABLE_ERROR = 1051
+ ER_NON_UNIQ_ERROR = 1052
+ ER_SERVER_SHUTDOWN = 1053
+ ER_BAD_FIELD_ERROR = 1054
+ ER_WRONG_FIELD_WITH_GROUP = 1055
+ ER_WRONG_GROUP_FIELD = 1056
+ ER_WRONG_SUM_SELECT = 1057
+ ER_WRONG_VALUE_COUNT = 1058
+ ER_TOO_LONG_IDENT = 1059
+ ER_DUP_FIELDNAME = 1060
+ ER_DUP_KEYNAME = 1061
+ ER_DUP_ENTRY = 1062
+ ER_WRONG_FIELD_SPEC = 1063
+ ER_PARSE_ERROR = 1064
+ ER_EMPTY_QUERY = 1065
+ ER_NONUNIQ_TABLE = 1066
+ ER_INVALID_DEFAULT = 1067
+ ER_MULTIPLE_PRI_KEY = 1068
+ ER_TOO_MANY_KEYS = 1069
+ ER_TOO_MANY_KEY_PARTS = 1070
+ ER_TOO_LONG_KEY = 1071
+ ER_KEY_COLUMN_DOES_NOT_EXITS = 1072
+ ER_BLOB_USED_AS_KEY = 1073
+ ER_TOO_BIG_FIELDLENGTH = 1074
+ ER_WRONG_AUTO_KEY = 1075
+ ER_READY = 1076
+ ER_NORMAL_SHUTDOWN = 1077
+ ER_GOT_SIGNAL = 1078
+ ER_SHUTDOWN_COMPLETE = 1079
+ ER_FORCING_CLOSE = 1080
+ ER_IPSOCK_ERROR = 1081
+ ER_NO_SUCH_INDEX = 1082
+ ER_WRONG_FIELD_TERMINATORS = 1083
+ ER_BLOBS_AND_NO_TERMINATED = 1084
+ ER_TEXTFILE_NOT_READABLE = 1085
+ ER_FILE_EXISTS_ERROR = 1086
+ ER_LOAD_INFO = 1087
+ ER_ALTER_INFO = 1088
+ ER_WRONG_SUB_KEY = 1089
+ ER_CANT_REMOVE_ALL_FIELDS = 1090
+ ER_CANT_DROP_FIELD_OR_KEY = 1091
+ ER_INSERT_INFO = 1092
+ ER_INSERT_TABLE_USED = 1093
+ ER_NO_SUCH_THREAD = 1094
+ ER_KILL_DENIED_ERROR = 1095
+ ER_NO_TABLES_USED = 1096
+ ER_TOO_BIG_SET = 1097
+ ER_NO_UNIQUE_LOGFILE = 1098
+ ER_TABLE_NOT_LOCKED_FOR_WRITE = 1099
+ ER_TABLE_NOT_LOCKED = 1100
+ ER_BLOB_CANT_HAVE_DEFAULT = 1101
+ ER_WRONG_DB_NAME = 1102
+ ER_WRONG_TABLE_NAME = 1103
+ ER_TOO_BIG_SELECT = 1104
+ ER_UNKNOWN_ERROR = 1105
+ ER_UNKNOWN_PROCEDURE = 1106
+ ER_WRONG_PARAMCOUNT_TO_PROCEDURE = 1107
+ ER_WRONG_PARAMETERS_TO_PROCEDURE = 1108
+ ER_UNKNOWN_TABLE = 1109
+ ER_FIELD_SPECIFIED_TWICE = 1110
+ ER_INVALID_GROUP_FUNC_USE = 1111
+ ER_UNSUPPORTED_EXTENSION = 1112
+ ER_TABLE_MUST_HAVE_COLUMNS = 1113
+ ER_RECORD_FILE_FULL = 1114
+ ER_UNKNOWN_CHARACTER_SET = 1115
+ ER_TOO_MANY_TABLES = 1116
+ ER_TOO_MANY_FIELDS = 1117
+ ER_TOO_BIG_ROWSIZE = 1118
+ ER_STACK_OVERRUN = 1119
+ ER_WRONG_OUTER_JOIN = 1120
+ ER_NULL_COLUMN_IN_INDEX = 1121
+ ER_CANT_FIND_UDF = 1122
+ ER_CANT_INITIALIZE_UDF = 1123
+ ER_UDF_NO_PATHS = 1124
+ ER_UDF_EXISTS = 1125
+ ER_CANT_OPEN_LIBRARY = 1126
+ ER_CANT_FIND_DL_ENTRY = 1127
+ ER_FUNCTION_NOT_DEFINED = 1128
+ ER_HOST_IS_BLOCKED = 1129
+ ER_HOST_NOT_PRIVILEGED = 1130
+ ER_PASSWORD_ANONYMOUS_USER = 1131
+ ER_PASSWORD_NOT_ALLOWED = 1132
+ ER_PASSWORD_NO_MATCH = 1133
+ ER_UPDATE_INFO = 1134
+ ER_CANT_CREATE_THREAD = 1135
+ ER_WRONG_VALUE_COUNT_ON_ROW = 1136
+ ER_CANT_REOPEN_TABLE = 1137
+ ER_INVALID_USE_OF_NULL = 1138
+ ER_REGEXP_ERROR = 1139
+ ER_MIX_OF_GROUP_FUNC_AND_FIELDS = 1140
+ ER_NONEXISTING_GRANT = 1141
+ ER_TABLEACCESS_DENIED_ERROR = 1142
+ ER_COLUMNACCESS_DENIED_ERROR = 1143
+ ER_ILLEGAL_GRANT_FOR_TABLE = 1144
+ ER_GRANT_WRONG_HOST_OR_USER = 1145
+ ER_NO_SUCH_TABLE = 1146
+ ER_NONEXISTING_TABLE_GRANT = 1147
+ ER_NOT_ALLOWED_COMMAND = 1148
+ ER_SYNTAX_ERROR = 1149
+ ER_DELAYED_CANT_CHANGE_LOCK = 1150
+ ER_TOO_MANY_DELAYED_THREADS = 1151
+ ER_ABORTING_CONNECTION = 1152
+ ER_NET_PACKET_TOO_LARGE = 1153
+ ER_NET_READ_ERROR_FROM_PIPE = 1154
+ ER_NET_FCNTL_ERROR = 1155
+ ER_NET_PACKETS_OUT_OF_ORDER = 1156
+ ER_NET_UNCOMPRESS_ERROR = 1157
+ ER_NET_READ_ERROR = 1158
+ ER_NET_READ_INTERRUPTED = 1159
+ ER_NET_ERROR_ON_WRITE = 1160
+ ER_NET_WRITE_INTERRUPTED = 1161
+ ER_TOO_LONG_STRING = 1162
+ ER_TABLE_CANT_HANDLE_BLOB = 1163
+ ER_TABLE_CANT_HANDLE_AUTO_INCREMENT = 1164
+ ER_DELAYED_INSERT_TABLE_LOCKED = 1165
+ ER_WRONG_COLUMN_NAME = 1166
+ ER_WRONG_KEY_COLUMN = 1167
+ ER_WRONG_MRG_TABLE = 1168
+ ER_DUP_UNIQUE = 1169
+ ER_BLOB_KEY_WITHOUT_LENGTH = 1170
+ ER_PRIMARY_CANT_HAVE_NULL = 1171
+ ER_TOO_MANY_ROWS = 1172
+ ER_REQUIRES_PRIMARY_KEY = 1173
+ ER_NO_RAID_COMPILED = 1174
+ ER_UPDATE_WITHOUT_KEY_IN_SAFE_MODE = 1175
+ ER_KEY_DOES_NOT_EXITS = 1176
+ ER_CHECK_NO_SUCH_TABLE = 1177
+ ER_CHECK_NOT_IMPLEMENTED = 1178
+ ER_CANT_DO_THIS_DURING_AN_TRANSACTION = 1179
+ ER_ERROR_DURING_COMMIT = 1180
+ ER_ERROR_DURING_ROLLBACK = 1181
+ ER_ERROR_DURING_FLUSH_LOGS = 1182
+ ER_ERROR_DURING_CHECKPOINT = 1183
+ ER_NEW_ABORTING_CONNECTION = 1184
+ ER_DUMP_NOT_IMPLEMENTED = 1185
+ ER_FLUSH_MASTER_BINLOG_CLOSED = 1186
+ ER_INDEX_REBUILD = 1187
+ ER_MASTER = 1188
+ ER_MASTER_NET_READ = 1189
+ ER_MASTER_NET_WRITE = 1190
+ ER_FT_MATCHING_KEY_NOT_FOUND = 1191
+ ER_LOCK_OR_ACTIVE_TRANSACTION = 1192
+ ER_UNKNOWN_SYSTEM_VARIABLE = 1193
+ ER_CRASHED_ON_USAGE = 1194
+ ER_CRASHED_ON_REPAIR = 1195
+ ER_WARNING_NOT_COMPLETE_ROLLBACK = 1196
+ ER_TRANS_CACHE_FULL = 1197
+ ER_SLAVE_MUST_STOP = 1198
+ ER_SLAVE_NOT_RUNNING = 1199
+ ER_BAD_SLAVE = 1200
+ ER_MASTER_INFO = 1201
+ ER_SLAVE_THREAD = 1202
+ ER_TOO_MANY_USER_CONNECTIONS = 1203
+ ER_SET_CONSTANTS_ONLY = 1204
+ ER_LOCK_WAIT_TIMEOUT = 1205
+ ER_LOCK_TABLE_FULL = 1206
+ ER_READ_ONLY_TRANSACTION = 1207
+ ER_DROP_DB_WITH_READ_LOCK = 1208
+ ER_CREATE_DB_WITH_READ_LOCK = 1209
+ ER_WRONG_ARGUMENTS = 1210
+ ER_NO_PERMISSION_TO_CREATE_USER = 1211
+ ER_UNION_TABLES_IN_DIFFERENT_DIR = 1212
+ ER_LOCK_DEADLOCK = 1213
+ ER_TABLE_CANT_HANDLE_FULLTEXT = 1214
+ ER_CANNOT_ADD_FOREIGN = 1215
+ ER_NO_REFERENCED_ROW = 1216
+ ER_ROW_IS_REFERENCED = 1217
+ ER_CONNECT_TO_MASTER = 1218
+ ER_QUERY_ON_MASTER = 1219
+ ER_ERROR_WHEN_EXECUTING_COMMAND = 1220
+ ER_WRONG_USAGE = 1221
+ ER_WRONG_NUMBER_OF_COLUMNS_IN_SELECT = 1222
+ ER_CANT_UPDATE_WITH_READLOCK = 1223
+ ER_MIXING_NOT_ALLOWED = 1224
+ ER_DUP_ARGUMENT = 1225
+ ER_USER_LIMIT_REACHED = 1226
+ ER_SPECIFIC_ACCESS_DENIED_ERROR = 1227
+ ER_LOCAL_VARIABLE = 1228
+ ER_GLOBAL_VARIABLE = 1229
+ ER_NO_DEFAULT = 1230
+ ER_WRONG_VALUE_FOR_VAR = 1231
+ ER_WRONG_TYPE_FOR_VAR = 1232
+ ER_VAR_CANT_BE_READ = 1233
+ ER_CANT_USE_OPTION_HERE = 1234
+ ER_NOT_SUPPORTED_YET = 1235
+ ER_MASTER_FATAL_ERROR_READING_BINLOG = 1236
+ ER_SLAVE_IGNORED_TABLE = 1237
+ ER_ERROR_MESSAGES = 238
+
+ # Client Error
+ CR_MIN_ERROR = 2000
+ CR_MAX_ERROR = 2999
+ CR_UNKNOWN_ERROR = 2000
+ CR_SOCKET_CREATE_ERROR = 2001
+ CR_CONNECTION_ERROR = 2002
+ CR_CONN_HOST_ERROR = 2003
+ CR_IPSOCK_ERROR = 2004
+ CR_UNKNOWN_HOST = 2005
+ CR_SERVER_GONE_ERROR = 2006
+ CR_VERSION_ERROR = 2007
+ CR_OUT_OF_MEMORY = 2008
+ CR_WRONG_HOST_INFO = 2009
+ CR_LOCALHOST_CONNECTION = 2010
+ CR_TCP_CONNECTION = 2011
+ CR_SERVER_HANDSHAKE_ERR = 2012
+ CR_SERVER_LOST = 2013
+ CR_COMMANDS_OUT_OF_SYNC = 2014
+ CR_NAMEDPIPE_CONNECTION = 2015
+ CR_NAMEDPIPEWAIT_ERROR = 2016
+ CR_NAMEDPIPEOPEN_ERROR = 2017
+ CR_NAMEDPIPESETSTATE_ERROR = 2018
+ CR_CANT_READ_CHARSET = 2019
+ CR_NET_PACKET_TOO_LARGE = 2020
+ CR_EMBEDDED_CONNECTION = 2021
+ CR_PROBE_SLAVE_STATUS = 2022
+ CR_PROBE_SLAVE_HOSTS = 2023
+ CR_PROBE_SLAVE_CONNECT = 2024
+ CR_PROBE_MASTER_CONNECT = 2025
+ CR_SSL_CONNECTION_ERROR = 2026
+ CR_MALFORMED_PACKET = 2027
+
+ CLIENT_ERRORS = [
+ "Unknown MySQL error",
+ "Can't create UNIX socket (%d)",
+ "Can't connect to local MySQL server through socket '%-.64s' (%d)",
+ "Can't connect to MySQL server on '%-.64s' (%d)",
+ "Can't create TCP/IP socket (%d)",
+ "Unknown MySQL Server Host '%-.64s' (%d)",
+ "MySQL server has gone away",
+ "Protocol mismatch. Server Version = %d Client Version = %d",
+ "MySQL client run out of memory",
+ "Wrong host info",
+ "Localhost via UNIX socket",
+ "%-.64s via TCP/IP",
+ "Error in server handshake",
+ "Lost connection to MySQL server during query",
+ "Commands out of sync; You can't run this command now",
+ "%-.64s via named pipe",
+ "Can't wait for named pipe to host: %-.64s pipe: %-.32s (%lu)",
+ "Can't open named pipe to host: %-.64s pipe: %-.32s (%lu)",
+ "Can't set state of named pipe to host: %-.64s pipe: %-.32s (%lu)",
+ "Can't initialize character set %-.64s (path: %-.64s)",
+ "Got packet bigger than 'max_allowed_packet'",
+ "Embedded server",
+ "Error on SHOW SLAVE STATUS:",
+ "Error on SHOW SLAVE HOSTS:",
+ "Error connecting to slave:",
+ "Error connecting to master:",
+ "SSL connection error",
+ "Malformed packet"
+ ]
+
+ def initialize(errno, error)
+ @errno = errno
+ @error = error
+ super error
+ end
+ attr_reader :errno, :error
+
+ def Error::err(errno)
+ CLIENT_ERRORS[errno - Error::CR_MIN_ERROR]
+ end
+ end
+
+ class Net
+ def initialize(sock)
+ @sock = sock
+ @pkt_nr = 0
+ end
+
+ def clear()
+ @pkt_nr = 0
+ end
+
+ def read()
+ buf = []
+ len = nil
+ @sock.sync = false
+ while len == nil or len == MAX_PACKET_LENGTH do
+ a = @sock.read(4)
+ len = a[0]+a[1]*256+a[2]*256*256
+ pkt_nr = a[3]
+ if @pkt_nr != pkt_nr then
+ raise "Packets out of order: #{@pkt_nr}<>#{pkt_nr}"
+ end
+ @pkt_nr = @pkt_nr + 1 & 0xff
+ buf << @sock.read(len)
+ end
+ @sock.sync = true
+ buf.join
+ end
+
+ def write(data)
+ if data.is_a? Array then
+ data = data.join
+ end
+ @sock.sync = false
+ ptr = 0
+ while data.length >= MAX_PACKET_LENGTH do
+ @sock.write Net::int3str(MAX_PACKET_LENGTH)+@pkt_nr.chr+data[ptr, MAX_PACKET_LENGTH]
+ @pkt_nr = @pkt_nr + 1 & 0xff
+ ptr += MAX_PACKET_LENGTH
+ end
+ @sock.write Net::int3str(data.length-ptr)+@pkt_nr.chr+data[ptr .. -1]
+ @pkt_nr = @pkt_nr + 1 & 0xff
+ @sock.sync = true
+ @sock.flush
+ end
+
+ def close()
+ @sock.close
+ end
+
+ def Net::int2str(n)
+ [n].pack("v")
+ end
+
+ def Net::int3str(n)
+ [n%256, n>>8].pack("cv")
+ end
+
+ def Net::int4str(n)
+ [n].pack("V")
+ end
+
+ end
+
+ class Random
+ def initialize(seed1, seed2)
+ @max_value = 0x3FFFFFFF
+ @seed1 = seed1 % @max_value
+ @seed2 = seed2 % @max_value
+ end
+
+ def rnd()
+ @seed1 = (@seed1*3+@seed2) % @max_value
+ @seed2 = (@seed1+@seed2+33) % @max_value
+ @seed1.to_f / @max_value
+ end
+ end
+
+end
+
+class << Mysql
+ def init()
+ Mysql::new :INIT
+ end
+
+ def real_connect(*args)
+ Mysql::new(*args)
+ end
+ alias :connect :real_connect
+
+ def escape_string(str)
+ str.gsub(/([\0\n\r\032\'\"\\])/) do
+ case $1
+ when "\0" then "\\0"
+ when "\n" then "\\n"
+ when "\r" then "\\r"
+ when "\032" then "\Z"
+ else "\\"+$1
+ end
+ end
+ end
+ alias :quote :escape_string
+
+ def get_client_info()
+ Mysql::VERSION
+ end
+ alias :client_info :get_client_info
+
+ def debug(str)
+ raise "not implemented"
+ end
+end
+
+#
+# for compatibility
+#
+
+MysqlRes = Mysql::Result
+MysqlField = Mysql::Field
+MysqlError = Mysql::Error
diff --git a/activerecord/lib/active_record/vendor/simple.rb b/activerecord/lib/active_record/vendor/simple.rb
new file mode 100644
index 0000000000..1bd332c882
--- /dev/null
+++ b/activerecord/lib/active_record/vendor/simple.rb
@@ -0,0 +1,702 @@
+# :title: Transaction::Simple
+#
+# == Licence
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to
+# deal in the Software without restriction, including without limitation the
+# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+# sell copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+# IN THE SOFTWARE.
+#--
+# Transaction::Simple
+# Simple object transaction support for Ruby
+# Version 1.11
+#
+# Copyright (c) 2003 Austin Ziegler
+#
+# $Id: simple.rb,v 1.2 2004/08/20 13:56:37 webster132 Exp $
+#
+# ==========================================================================
+# Revision History ::
+# YYYY.MM.DD Change ID Developer
+# Description
+# --------------------------------------------------------------------------
+# 2003.07.29 Austin Ziegler
+# Added debugging capabilities and VERSION string.
+# 2003.08.21 Austin Ziegler
+# Added named transactions.
+#
+# ==========================================================================
+#++
+require 'thread'
+
+ # The "Transaction" namespace can be used for additional transactional
+ # support objects and modules.
+module Transaction
+
+ # A standard exception for transactional errors.
+ class TransactionError < StandardError; end
+ # A standard exception for transactional errors involving the acquisition
+ # of locks for Transaction::Simple::ThreadSafe.
+ class TransactionThreadError < StandardError; end
+
+ # = Transaction::Simple for Ruby
+ # Simple object transaction support for Ruby
+ #
+ # == Introduction
+ #
+ # Transaction::Simple provides a generic way to add active transactional
+ # support to objects. The transaction methods added by this module will
+ # work with most objects, excluding those that cannot be <i>Marshal</i>ed
+ # (bindings, procedure objects, IO instances, or singleton objects).
+ #
+ # The transactions supported by Transaction::Simple are not backed
+ # transactions; that is, they have nothing to do with any sort of data
+ # store. They are "live" transactions occurring in memory and in the
+ # object itself. This is to allow "test" changes to be made to an object
+ # before making the changes permanent.
+ #
+ # Transaction::Simple can handle an "infinite" number of transactional
+ # levels (limited only by memory). If I open two transactions, commit the
+ # first, but abort the second, the object will revert to the original
+ # version.
+ #
+ # Transaction::Simple supports "named" transactions, so that multiple
+ # levels of transactions can be committed, aborted, or rewound by
+ # referring to the appropriate name of the transaction. Names may be any
+ # object *except* +nil+.
+ #
+ # Copyright:: Copyright © 2003 by Austin Ziegler
+ # Version:: 1.1
+ # Licence:: MIT-Style
+ #
+ # Thanks to David Black for help with the initial concept that led to this
+ # library.
+ #
+ # == Usage
+ # include 'transaction/simple'
+ #
+ # v = "Hello, you." # => "Hello, you."
+ # v.extend(Transaction::Simple) # => "Hello, you."
+ #
+ # v.start_transaction # => ... (a Marshal string)
+ # v.transaction_open? # => true
+ # v.gsub!(/you/, "world") # => "Hello, world."
+ #
+ # v.rewind_transaction # => "Hello, you."
+ # v.transaction_open? # => true
+ #
+ # v.gsub!(/you/, "HAL") # => "Hello, HAL."
+ # v.abort_transaction # => "Hello, you."
+ # v.transaction_open? # => false
+ #
+ # v.start_transaction # => ... (a Marshal string)
+ # v.start_transaction # => ... (a Marshal string)
+ #
+ # v.transaction_open? # => true
+ # v.gsub!(/you/, "HAL") # => "Hello, HAL."
+ #
+ # v.commit_transaction # => "Hello, HAL."
+ # v.transaction_open? # => true
+ # v.abort_transaction # => "Hello, you."
+ # v.transaction_open? # => false
+ #
+ # == Named Transaction Usage
+ # v = "Hello, you." # => "Hello, you."
+ # v.extend(Transaction::Simple) # => "Hello, you."
+ #
+ # v.start_transaction(:first) # => ... (a Marshal string)
+ # v.transaction_open? # => true
+ # v.transaction_open?(:first) # => true
+ # v.transaction_open?(:second) # => false
+ # v.gsub!(/you/, "world") # => "Hello, world."
+ #
+ # v.start_transaction(:second) # => ... (a Marshal string)
+ # v.gsub!(/world/, "HAL") # => "Hello, HAL."
+ # v.rewind_transaction(:first) # => "Hello, you."
+ # v.transaction_open? # => true
+ # v.transaction_open?(:first) # => true
+ # v.transaction_open?(:second) # => false
+ #
+ # v.gsub!(/you/, "world") # => "Hello, world."
+ # v.start_transaction(:second) # => ... (a Marshal string)
+ # v.gsub!(/world/, "HAL") # => "Hello, HAL."
+ # v.transaction_name # => :second
+ # v.abort_transaction(:first) # => "Hello, you."
+ # v.transaction_open? # => false
+ #
+ # v.start_transaction(:first) # => ... (a Marshal string)
+ # v.gsub!(/you/, "world") # => "Hello, world."
+ # v.start_transaction(:second) # => ... (a Marshal string)
+ # v.gsub!(/world/, "HAL") # => "Hello, HAL."
+ #
+ # v.commit_transaction(:first) # => "Hello, HAL."
+ # v.transaction_open? # => false
+ #
+ # == Contraindications
+ #
+ # While Transaction::Simple is very useful, it has some severe limitations
+ # that must be understood. Transaction::Simple:
+ #
+ # * uses Marshal. Thus, any object which cannot be <i>Marshal</i>ed cannot
+ # use Transaction::Simple.
+ # * does not manage resources. Resources external to the object and its
+ # instance variables are not managed at all. However, all instance
+ # variables and objects "belonging" to those instance variables are
+ # managed. If there are object reference counts to be handled,
+ # Transaction::Simple will probably cause problems.
+ # * is not inherently thread-safe. In the ACID ("atomic, consistent,
+ # isolated, durable") test, Transaction::Simple provides CD, but it is
+ # up to the user of Transaction::Simple to provide isolation and
+ # atomicity. Transactions should be considered "critical sections" in
+ # multi-threaded applications. If thread safety and atomicity is
+ # absolutely required, use Transaction::Simple::ThreadSafe, which uses a
+ # Mutex object to synchronize the accesses on the object during the
+ # transactional operations.
+ # * does not necessarily maintain Object#__id__ values on rewind or abort.
+ # This may change for future versions that will be Ruby 1.8 or better
+ # *only*. Certain objects that support #replace will maintain
+ # Object#__id__.
+ # * Can be a memory hog if you use many levels of transactions on many
+ # objects.
+ #
+ module Simple
+ VERSION = '1.1.1.0';
+
+ # Sets the Transaction::Simple debug object. It must respond to #<<.
+ # Sets the transaction debug object. Debugging will be performed
+ # automatically if there's a debug object. The generic transaction error
+ # class.
+ def self.debug_io=(io)
+ raise TransactionError, "Transaction Error: the transaction debug object must respond to #<<" unless io.respond_to?(:<<)
+ @tdi = io
+ end
+
+ # Returns the Transaction::Simple debug object. It must respond to #<<.
+ def self.debug_io
+ @tdi
+ end
+
+ # If +name+ is +nil+ (default), then returns +true+ if there is
+ # currently a transaction open.
+ #
+ # If +name+ is specified, then returns +true+ if there is currently a
+ # transaction that responds to +name+ open.
+ def transaction_open?(name = nil)
+ if name.nil?
+ Transaction::Simple.debug_io << "Transaction [#{(@__transaction_checkpoint__.nil?) ? 'closed' : 'open'}]\n" unless Transaction::Simple.debug_io.nil?
+ return (not @__transaction_checkpoint__.nil?)
+ else
+ Transaction::Simple.debug_io << "Transaction(#{name.inspect}) [#{(@__transaction_checkpoint__.nil?) ? 'closed' : 'open'}]\n" unless Transaction::Simple.debug_io.nil?
+ return ((not @__transaction_checkpoint__.nil?) and @__transaction_names__.include?(name))
+ end
+ end
+
+ # Returns the current name of the transaction. Transactions not
+ # explicitly named are named +nil+.
+ def transaction_name
+ raise TransactionError, "Transaction Error: No transaction open." if @__transaction_checkpoint__.nil?
+ Transaction::Simple.debug_io << "#{'|' * @__transaction_level__} Transaction Name: #{@__transaction_names__[-1].inspect}\n" unless Transaction::Simple.debug_io.nil?
+ @__transaction_names__[-1]
+ end
+
+ # Starts a transaction. Stores the current object state. If a
+ # transaction name is specified, the transaction will be named.
+ # Transaction names must be unique. Transaction names of +nil+ will be
+ # treated as unnamed transactions.
+ def start_transaction(name = nil)
+ @__transaction_level__ ||= 0
+ @__transaction_names__ ||= []
+
+ if name.nil?
+ @__transaction_names__ << nil
+ s = ""
+ else
+ raise TransactionError, "Transaction Error: Named transactions must be unique." if @__transaction_names__.include?(name)
+ @__transaction_names__ << name
+ s = "(#{name.inspect})"
+ end
+
+ @__transaction_level__ += 1
+
+ Transaction::Simple.debug_io << "#{'>' * @__transaction_level__} Start Transaction#{s}\n" unless Transaction::Simple.debug_io.nil?
+
+ @__transaction_checkpoint__ = Marshal.dump(self)
+ end
+
+ # Rewinds the transaction. If +name+ is specified, then the intervening
+ # transactions will be aborted and the named transaction will be
+ # rewound. Otherwise, only the current transaction is rewound.
+ def rewind_transaction(name = nil)
+ raise TransactionError, "Transaction Error: Cannot rewind. There is no current transaction." if @__transaction_checkpoint__.nil?
+ if name.nil?
+ __rewind_this_transaction
+ s = ""
+ else
+ raise TransactionError, "Transaction Error: Cannot rewind to transaction #{name.inspect} because it does not exist." unless @__transaction_names__.include?(name)
+ s = "(#{name})"
+
+ while @__transaction_names__[-1] != name
+ @__transaction_checkpoint__ = __rewind_this_transaction
+ Transaction::Simple.debug_io << "#{'|' * @__transaction_level__} Rewind Transaction#{s}\n" unless Transaction::Simple.debug_io.nil?
+ @__transaction_level__ -= 1
+ @__transaction_names__.pop
+ end
+ __rewind_this_transaction
+ end
+ Transaction::Simple.debug_io << "#{'|' * @__transaction_level__} Rewind Transaction#{s}\n" unless Transaction::Simple.debug_io.nil?
+ self
+ end
+
+ # Aborts the transaction. Resets the object state to what it was before
+ # the transaction was started and closes the transaction. If +name+ is
+ # specified, then the intervening transactions and the named transaction
+ # will be aborted. Otherwise, only the current transaction is aborted.
+ def abort_transaction(name = nil)
+ raise TransactionError, "Transaction Error: Cannot abort. There is no current transaction." if @__transaction_checkpoint__.nil?
+ if name.nil?
+ __abort_transaction(name)
+ else
+ raise TransactionError, "Transaction Error: Cannot abort nonexistant transaction #{name.inspect}." unless @__transaction_names__.include?(name)
+
+ __abort_transaction(name) while @__transaction_names__.include?(name)
+ end
+ self
+ end
+
+ # If +name+ is +nil+ (default), the current transaction level is closed
+ # out and the changes are committed.
+ #
+ # If +name+ is specified and +name+ is in the list of named
+ # transactions, then all transactions are closed and committed until the
+ # named transaction is reached.
+ def commit_transaction(name = nil)
+ raise TransactionError, "Transaction Error: Cannot commit. There is no current transaction." if @__transaction_checkpoint__.nil?
+
+ if name.nil?
+ s = ""
+ __commit_transaction
+ Transaction::Simple.debug_io << "#{'<' * @__transaction_level__} Commit Transaction#{s}\n" unless Transaction::Simple.debug_io.nil?
+ else
+ raise TransactionError, "Transaction Error: Cannot commit nonexistant transaction #{name.inspect}." unless @__transaction_names__.include?(name)
+ s = "(#{name})"
+
+ while @__transaction_names__[-1] != name
+ Transaction::Simple.debug_io << "#{'<' * @__transaction_level__} Commit Transaction#{s}\n" unless Transaction::Simple.debug_io.nil?
+ __commit_transaction
+ end
+ Transaction::Simple.debug_io << "#{'<' * @__transaction_level__} Commit Transaction#{s}\n" unless Transaction::Simple.debug_io.nil?
+ __commit_transaction
+ end
+ self
+ end
+
+ # Alternative method for calling the transaction methods. An optional
+ # name can be specified for named transaction support.
+ #
+ # #transaction(:start):: #start_transaction
+ # #transaction(:rewind):: #rewind_transaction
+ # #transaction(:abort):: #abort_transaction
+ # #transaction(:commit):: #commit_transaction
+ # #transaction(:name):: #transaction_name
+ # #transaction:: #transaction_open?
+ def transaction(action = nil, name = nil)
+ case action
+ when :start
+ start_transaction(name)
+ when :rewind
+ rewind_transaction(name)
+ when :abort
+ abort_transaction(name)
+ when :commit
+ commit_transaction(name)
+ when :name
+ transaction_name
+ when nil
+ transaction_open?(name)
+ end
+ end
+
+ def __abort_transaction(name = nil) #:nodoc:
+ @__transaction_checkpoint__ = __rewind_this_transaction
+
+ if name.nil?
+ s = ""
+ else
+ s = "(#{name.inspect})"
+ end
+
+ Transaction::Simple.debug_io << "#{'<' * @__transaction_level__} Abort Transaction#{s}\n" unless Transaction::Simple.debug_io.nil?
+ @__transaction_level__ -= 1
+ @__transaction_names__.pop
+ if @__transaction_level__ < 1
+ @__transaction_level__ = 0
+ @__transaction_names__ = []
+ end
+ end
+
+ TRANSACTION_CHECKPOINT = "@__transaction_checkpoint__" #:nodoc:
+ SKIP_TRANSACTION_VARS = [TRANSACTION_CHECKPOINT, "@__transaction_level__"] #:nodoc:
+
+ def __rewind_this_transaction #:nodoc:
+ r = Marshal.restore(@__transaction_checkpoint__)
+
+ begin
+ self.replace(r) if respond_to?(:replace)
+ rescue
+ nil
+ end
+
+ r.instance_variables.each do |i|
+ next if SKIP_TRANSACTION_VARS.include?(i)
+ if respond_to?(:instance_variable_get)
+ instance_variable_set(i, r.instance_variable_get(i))
+ else
+ instance_eval(%q|#{i} = r.instance_eval("#{i}")|)
+ end
+ end
+
+ if respond_to?(:instance_variable_get)
+ return r.instance_variable_get(TRANSACTION_CHECKPOINT)
+ else
+ return r.instance_eval(TRANSACTION_CHECKPOINT)
+ end
+ end
+
+ def __commit_transaction #:nodoc:
+ if respond_to?(:instance_variable_get)
+ @__transaction_checkpoint__ = Marshal.restore(@__transaction_checkpoint__).instance_variable_get(TRANSACTION_CHECKPOINT)
+ else
+ @__transaction_checkpoint__ = Marshal.restore(@__transaction_checkpoint__).instance_eval(TRANSACTION_CHECKPOINT)
+ end
+
+ @__transaction_level__ -= 1
+ @__transaction_names__.pop
+ if @__transaction_level__ < 1
+ @__transaction_level__ = 0
+ @__transaction_names__ = []
+ end
+ end
+
+ private :__abort_transaction, :__rewind_this_transaction, :__commit_transaction
+
+ # = Transaction::Simple::ThreadSafe
+ # Thread-safe simple object transaction support for Ruby.
+ # Transaction::Simple::ThreadSafe is used in the same way as
+ # Transaction::Simple. Transaction::Simple::ThreadSafe uses a Mutex
+ # object to ensure atomicity at the cost of performance in threaded
+ # applications.
+ #
+ # Transaction::Simple::ThreadSafe will not wait to obtain a lock; if the
+ # lock cannot be obtained immediately, a
+ # Transaction::TransactionThreadError will be raised.
+ #
+ # Thanks to Mauricio Fernández for help with getting this part working.
+ module ThreadSafe
+ VERSION = '1.1.1.0';
+
+ include Transaction::Simple
+
+ SKIP_TRANSACTION_VARS = Transaction::Simple::SKIP_TRANSACTION_VARS.dup #:nodoc:
+ SKIP_TRANSACTION_VARS << "@__transaction_mutex__"
+
+ Transaction::Simple.instance_methods(false) do |meth|
+ next if meth == "transaction"
+ arg = "(name = nil)" unless meth == "transaction_name"
+ module_eval <<-EOS
+ def #{meth}#{arg}
+ if (@__transaction_mutex__ ||= Mutex.new).try_lock
+ result = super
+ @__transaction_mutex__.unlock
+ return result
+ else
+ raise TransactionThreadError, "Transaction Error: Cannot obtain lock for ##{meth}"
+ end
+ ensure
+ @__transaction_mutex__.unlock
+ end
+ EOS
+ end
+ end
+ end
+end
+
+if $0 == __FILE__
+ require 'test/unit'
+
+ class Test__Transaction_Simple < Test::Unit::TestCase #:nodoc:
+ VALUE = "Now is the time for all good men to come to the aid of their country."
+
+ def setup
+ @value = VALUE.dup
+ @value.extend(Transaction::Simple)
+ end
+
+ def test_extended
+ assert_respond_to(@value, :start_transaction)
+ end
+
+ def test_started
+ assert_equal(false, @value.transaction_open?)
+ assert_nothing_raised { @value.start_transaction }
+ assert_equal(true, @value.transaction_open?)
+ end
+
+ def test_rewind
+ assert_equal(false, @value.transaction_open?)
+ assert_raises(Transaction::TransactionError) { @value.rewind_transaction }
+ assert_nothing_raised { @value.start_transaction }
+ assert_equal(true, @value.transaction_open?)
+ assert_nothing_raised { @value.gsub!(/men/, 'women') }
+ assert_not_equal(VALUE, @value)
+ assert_nothing_raised { @value.rewind_transaction }
+ assert_equal(true, @value.transaction_open?)
+ assert_equal(VALUE, @value)
+ end
+
+ def test_abort
+ assert_equal(false, @value.transaction_open?)
+ assert_raises(Transaction::TransactionError) { @value.abort_transaction }
+ assert_nothing_raised { @value.start_transaction }
+ assert_equal(true, @value.transaction_open?)
+ assert_nothing_raised { @value.gsub!(/men/, 'women') }
+ assert_not_equal(VALUE, @value)
+ assert_nothing_raised { @value.abort_transaction }
+ assert_equal(false, @value.transaction_open?)
+ assert_equal(VALUE, @value)
+ end
+
+ def test_commit
+ assert_equal(false, @value.transaction_open?)
+ assert_raises(Transaction::TransactionError) { @value.commit_transaction }
+ assert_nothing_raised { @value.start_transaction }
+ assert_equal(true, @value.transaction_open?)
+ assert_nothing_raised { @value.gsub!(/men/, 'women') }
+ assert_not_equal(VALUE, @value)
+ assert_equal(true, @value.transaction_open?)
+ assert_nothing_raised { @value.commit_transaction }
+ assert_equal(false, @value.transaction_open?)
+ assert_not_equal(VALUE, @value)
+ end
+
+ def test_multilevel
+ assert_equal(false, @value.transaction_open?)
+ assert_nothing_raised { @value.start_transaction }
+ assert_equal(true, @value.transaction_open?)
+ assert_nothing_raised { @value.gsub!(/men/, 'women') }
+ assert_equal(VALUE.gsub(/men/, 'women'), @value)
+ assert_equal(true, @value.transaction_open?)
+ assert_nothing_raised { @value.start_transaction }
+ assert_nothing_raised { @value.gsub!(/country/, 'nation-state') }
+ assert_nothing_raised { @value.commit_transaction }
+ assert_equal(VALUE.gsub(/men/, 'women').gsub(/country/, 'nation-state'), @value)
+ assert_equal(true, @value.transaction_open?)
+ assert_nothing_raised { @value.abort_transaction }
+ assert_equal(VALUE, @value)
+ end
+
+ def test_multilevel_named
+ assert_equal(false, @value.transaction_open?)
+ assert_raises(Transaction::TransactionError) { @value.transaction_name }
+ assert_nothing_raised { @value.start_transaction(:first) } # 1
+ assert_raises(Transaction::TransactionError) { @value.start_transaction(:first) }
+ assert_equal(true, @value.transaction_open?)
+ assert_equal(true, @value.transaction_open?(:first))
+ assert_equal(:first, @value.transaction_name)
+ assert_nothing_raised { @value.start_transaction } # 2
+ assert_not_equal(:first, @value.transaction_name)
+ assert_equal(nil, @value.transaction_name)
+ assert_raises(Transaction::TransactionError) { @value.abort_transaction(:second) }
+ assert_nothing_raised { @value.abort_transaction(:first) }
+ assert_equal(false, @value.transaction_open?)
+ assert_nothing_raised do
+ @value.start_transaction(:first)
+ @value.gsub!(/men/, 'women')
+ @value.start_transaction(:second)
+ @value.gsub!(/women/, 'people')
+ @value.start_transaction
+ @value.gsub!(/people/, 'sentients')
+ end
+ assert_nothing_raised { @value.abort_transaction(:second) }
+ assert_equal(true, @value.transaction_open?(:first))
+ assert_equal(VALUE.gsub(/men/, 'women'), @value)
+ assert_nothing_raised do
+ @value.start_transaction(:second)
+ @value.gsub!(/women/, 'people')
+ @value.start_transaction
+ @value.gsub!(/people/, 'sentients')
+ end
+ assert_raises(Transaction::TransactionError) { @value.rewind_transaction(:foo) }
+ assert_nothing_raised { @value.rewind_transaction(:second) }
+ assert_equal(VALUE.gsub(/men/, 'women'), @value)
+ assert_nothing_raised do
+ @value.gsub!(/women/, 'people')
+ @value.start_transaction
+ @value.gsub!(/people/, 'sentients')
+ end
+ assert_raises(Transaction::TransactionError) { @value.commit_transaction(:foo) }
+ assert_nothing_raised { @value.commit_transaction(:first) }
+ assert_equal(VALUE.gsub(/men/, 'sentients'), @value)
+ assert_equal(false, @value.transaction_open?)
+ end
+
+ def test_array
+ assert_nothing_raised do
+ @orig = ["first", "second", "third"]
+ @value = ["first", "second", "third"]
+ @value.extend(Transaction::Simple)
+ end
+ assert_equal(@orig, @value)
+ assert_nothing_raised { @value.start_transaction }
+ assert_equal(true, @value.transaction_open?)
+ assert_nothing_raised { @value[1].gsub!(/second/, "fourth") }
+ assert_not_equal(@orig, @value)
+ assert_nothing_raised { @value.abort_transaction }
+ assert_equal(@orig, @value)
+ end
+ end
+
+ class Test__Transaction_Simple_ThreadSafe < Test::Unit::TestCase #:nodoc:
+ VALUE = "Now is the time for all good men to come to the aid of their country."
+
+ def setup
+ @value = VALUE.dup
+ @value.extend(Transaction::Simple::ThreadSafe)
+ end
+
+ def test_extended
+ assert_respond_to(@value, :start_transaction)
+ end
+
+ def test_started
+ assert_equal(false, @value.transaction_open?)
+ assert_nothing_raised { @value.start_transaction }
+ assert_equal(true, @value.transaction_open?)
+ end
+
+ def test_rewind
+ assert_equal(false, @value.transaction_open?)
+ assert_raises(Transaction::TransactionError) { @value.rewind_transaction }
+ assert_nothing_raised { @value.start_transaction }
+ assert_equal(true, @value.transaction_open?)
+ assert_nothing_raised { @value.gsub!(/men/, 'women') }
+ assert_not_equal(VALUE, @value)
+ assert_nothing_raised { @value.rewind_transaction }
+ assert_equal(true, @value.transaction_open?)
+ assert_equal(VALUE, @value)
+ end
+
+ def test_abort
+ assert_equal(false, @value.transaction_open?)
+ assert_raises(Transaction::TransactionError) { @value.abort_transaction }
+ assert_nothing_raised { @value.start_transaction }
+ assert_equal(true, @value.transaction_open?)
+ assert_nothing_raised { @value.gsub!(/men/, 'women') }
+ assert_not_equal(VALUE, @value)
+ assert_nothing_raised { @value.abort_transaction }
+ assert_equal(false, @value.transaction_open?)
+ assert_equal(VALUE, @value)
+ end
+
+ def test_commit
+ assert_equal(false, @value.transaction_open?)
+ assert_raises(Transaction::TransactionError) { @value.commit_transaction }
+ assert_nothing_raised { @value.start_transaction }
+ assert_equal(true, @value.transaction_open?)
+ assert_nothing_raised { @value.gsub!(/men/, 'women') }
+ assert_not_equal(VALUE, @value)
+ assert_equal(true, @value.transaction_open?)
+ assert_nothing_raised { @value.commit_transaction }
+ assert_equal(false, @value.transaction_open?)
+ assert_not_equal(VALUE, @value)
+ end
+
+ def test_multilevel
+ assert_equal(false, @value.transaction_open?)
+ assert_nothing_raised { @value.start_transaction }
+ assert_equal(true, @value.transaction_open?)
+ assert_nothing_raised { @value.gsub!(/men/, 'women') }
+ assert_equal(VALUE.gsub(/men/, 'women'), @value)
+ assert_equal(true, @value.transaction_open?)
+ assert_nothing_raised { @value.start_transaction }
+ assert_nothing_raised { @value.gsub!(/country/, 'nation-state') }
+ assert_nothing_raised { @value.commit_transaction }
+ assert_equal(VALUE.gsub(/men/, 'women').gsub(/country/, 'nation-state'), @value)
+ assert_equal(true, @value.transaction_open?)
+ assert_nothing_raised { @value.abort_transaction }
+ assert_equal(VALUE, @value)
+ end
+
+ def test_multilevel_named
+ assert_equal(false, @value.transaction_open?)
+ assert_raises(Transaction::TransactionError) { @value.transaction_name }
+ assert_nothing_raised { @value.start_transaction(:first) } # 1
+ assert_raises(Transaction::TransactionError) { @value.start_transaction(:first) }
+ assert_equal(true, @value.transaction_open?)
+ assert_equal(true, @value.transaction_open?(:first))
+ assert_equal(:first, @value.transaction_name)
+ assert_nothing_raised { @value.start_transaction } # 2
+ assert_not_equal(:first, @value.transaction_name)
+ assert_equal(nil, @value.transaction_name)
+ assert_raises(Transaction::TransactionError) { @value.abort_transaction(:second) }
+ assert_nothing_raised { @value.abort_transaction(:first) }
+ assert_equal(false, @value.transaction_open?)
+ assert_nothing_raised do
+ @value.start_transaction(:first)
+ @value.gsub!(/men/, 'women')
+ @value.start_transaction(:second)
+ @value.gsub!(/women/, 'people')
+ @value.start_transaction
+ @value.gsub!(/people/, 'sentients')
+ end
+ assert_nothing_raised { @value.abort_transaction(:second) }
+ assert_equal(true, @value.transaction_open?(:first))
+ assert_equal(VALUE.gsub(/men/, 'women'), @value)
+ assert_nothing_raised do
+ @value.start_transaction(:second)
+ @value.gsub!(/women/, 'people')
+ @value.start_transaction
+ @value.gsub!(/people/, 'sentients')
+ end
+ assert_raises(Transaction::TransactionError) { @value.rewind_transaction(:foo) }
+ assert_nothing_raised { @value.rewind_transaction(:second) }
+ assert_equal(VALUE.gsub(/men/, 'women'), @value)
+ assert_nothing_raised do
+ @value.gsub!(/women/, 'people')
+ @value.start_transaction
+ @value.gsub!(/people/, 'sentients')
+ end
+ assert_raises(Transaction::TransactionError) { @value.commit_transaction(:foo) }
+ assert_nothing_raised { @value.commit_transaction(:first) }
+ assert_equal(VALUE.gsub(/men/, 'sentients'), @value)
+ assert_equal(false, @value.transaction_open?)
+ end
+
+ def test_array
+ assert_nothing_raised do
+ @orig = ["first", "second", "third"]
+ @value = ["first", "second", "third"]
+ @value.extend(Transaction::Simple::ThreadSafe)
+ end
+ assert_equal(@orig, @value)
+ assert_nothing_raised { @value.start_transaction }
+ assert_equal(true, @value.transaction_open?)
+ assert_nothing_raised { @value[1].gsub!(/second/, "fourth") }
+ assert_not_equal(@orig, @value)
+ assert_nothing_raised { @value.abort_transaction }
+ assert_equal(@orig, @value)
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/wrappers/yaml_wrapper.rb b/activerecord/lib/active_record/wrappers/yaml_wrapper.rb
new file mode 100644
index 0000000000..74f40a507c
--- /dev/null
+++ b/activerecord/lib/active_record/wrappers/yaml_wrapper.rb
@@ -0,0 +1,15 @@
+require 'yaml'
+
+module ActiveRecord
+ module Wrappings #:nodoc:
+ class YamlWrapper < AbstractWrapper #:nodoc:
+ def wrap(attribute) attribute.to_yaml end
+ def unwrap(attribute) YAML::load(attribute) end
+ end
+
+ module ClassMethods #:nodoc:
+ # Wraps the attribute in Yaml encoding
+ def wrap_in_yaml(*attributes) wrap_with(YamlWrapper, attributes) end
+ end
+ end
+end \ No newline at end of file
diff --git a/activerecord/lib/active_record/wrappings.rb b/activerecord/lib/active_record/wrappings.rb
new file mode 100644
index 0000000000..43e5e3151d
--- /dev/null
+++ b/activerecord/lib/active_record/wrappings.rb
@@ -0,0 +1,59 @@
+module ActiveRecord
+ # A plugin framework for wrapping attribute values before they go in and unwrapping them after they go out of the database.
+ # This was intended primarily for YAML wrapping of arrays and hashes, but this behavior is now native in the Base class.
+ # So for now this framework is laying dorment until a need pops up.
+ module Wrappings #:nodoc:
+ module ClassMethods #:nodoc:
+ def wrap_with(wrapper, *attributes)
+ [ attributes ].flat.each { |attribute| wrapper.wrap(attribute) }
+ end
+ end
+
+ def self.append_features(base)
+ super
+ base.extend(ClassMethods)
+ end
+
+ class AbstractWrapper #:nodoc:
+ def self.wrap(attribute, record_binding) #:nodoc:
+ %w( before_save after_save after_initialize ).each do |callback|
+ eval "#{callback} #{name}.new('#{attribute}')", record_binding
+ end
+ end
+
+ def initialize(attribute) #:nodoc:
+ @attribute = attribute
+ end
+
+ def save_wrapped_attribute(record) #:nodoc:
+ if record.attribute_present?(@attribute)
+ record.send(
+ "write_attribute",
+ @attribute,
+ wrap(record.send("read_attribute", @attribute))
+ )
+ end
+ end
+
+ def load_wrapped_attribute(record) #:nodoc:
+ if record.attribute_present?(@attribute)
+ record.send(
+ "write_attribute",
+ @attribute,
+ unwrap(record.send("read_attribute", @attribute))
+ )
+ end
+ end
+
+ alias_method :before_save, :save_wrapped_attribute #:nodoc:
+ alias_method :after_save, :load_wrapped_attribute #:nodoc:
+ alias_method :after_initialize, :after_save #:nodoc:
+
+ # Overwrite to implement the logic that'll take the regular attribute and wrap it.
+ def wrap(attribute) end
+
+ # Overwrite to implement the logic that'll take the wrapped attribute and unwrap it.
+ def unwrap(attribute) end
+ end
+ end
+end \ No newline at end of file