From db045dbbf60b53dbe013ef25554fd013baf88134 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Wed, 24 Nov 2004 01:04:44 +0000 Subject: Initial git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@4 5ecf4fe2-1ee6-0310-87b1-e25e094e27de --- activerecord/lib/active_record.rb | 50 + activerecord/lib/active_record/aggregations.rb | 165 +++ activerecord/lib/active_record/associations.rb | 576 ++++++++++ .../associations/association_collection.rb | 129 +++ .../has_and_belongs_to_many_association.rb | 107 ++ .../associations/has_many_association.rb | 102 ++ activerecord/lib/active_record/base.rb | 1051 ++++++++++++++++++ activerecord/lib/active_record/callbacks.rb | 337 ++++++ .../connection_adapters/abstract_adapter.rb | 371 +++++++ .../connection_adapters/mysql_adapter.rb | 131 +++ .../connection_adapters/postgresql_adapter.rb | 170 +++ .../connection_adapters/sqlite_adapter.rb | 105 ++ .../connection_adapters/sqlserver_adapter.rb | 298 ++++++ .../lib/active_record/deprecated_associations.rb | 70 ++ activerecord/lib/active_record/fixtures.rb | 208 ++++ activerecord/lib/active_record/observer.rb | 71 ++ activerecord/lib/active_record/reflection.rb | 126 +++ .../support/class_attribute_accessors.rb | 43 + .../support/class_inheritable_attributes.rb | 37 + .../lib/active_record/support/clean_logger.rb | 10 + .../lib/active_record/support/inflector.rb | 78 ++ activerecord/lib/active_record/transactions.rb | 119 +++ activerecord/lib/active_record/validations.rb | 205 ++++ activerecord/lib/active_record/vendor/mysql.rb | 1117 ++++++++++++++++++++ activerecord/lib/active_record/vendor/simple.rb | 702 ++++++++++++ .../lib/active_record/wrappers/yaml_wrapper.rb | 15 + activerecord/lib/active_record/wrappings.rb | 59 ++ 27 files changed, 6452 insertions(+) create mode 100755 activerecord/lib/active_record.rb create mode 100644 activerecord/lib/active_record/aggregations.rb create mode 100755 activerecord/lib/active_record/associations.rb create mode 100644 activerecord/lib/active_record/associations/association_collection.rb create mode 100644 activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb create mode 100644 activerecord/lib/active_record/associations/has_many_association.rb create mode 100755 activerecord/lib/active_record/base.rb create mode 100755 activerecord/lib/active_record/callbacks.rb create mode 100755 activerecord/lib/active_record/connection_adapters/abstract_adapter.rb create mode 100755 activerecord/lib/active_record/connection_adapters/mysql_adapter.rb create mode 100644 activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb create mode 100644 activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb create mode 100644 activerecord/lib/active_record/connection_adapters/sqlserver_adapter.rb create mode 100644 activerecord/lib/active_record/deprecated_associations.rb create mode 100755 activerecord/lib/active_record/fixtures.rb create mode 100644 activerecord/lib/active_record/observer.rb create mode 100644 activerecord/lib/active_record/reflection.rb create mode 100644 activerecord/lib/active_record/support/class_attribute_accessors.rb create mode 100644 activerecord/lib/active_record/support/class_inheritable_attributes.rb create mode 100644 activerecord/lib/active_record/support/clean_logger.rb create mode 100644 activerecord/lib/active_record/support/inflector.rb create mode 100644 activerecord/lib/active_record/transactions.rb create mode 100755 activerecord/lib/active_record/validations.rb create mode 100644 activerecord/lib/active_record/vendor/mysql.rb create mode 100644 activerecord/lib/active_record/vendor/simple.rb create mode 100644 activerecord/lib/active_record/wrappers/yaml_wrapper.rb create mode 100644 activerecord/lib/active_record/wrappings.rb (limited to 'activerecord/lib') 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: + # * Customer#balance, Customer#balance=(money) + # * Customer#address, Customer#address=(address) + # + # 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 + # composed_of :address would add address and address=(new_address). + # + # Options are: + # * :class_name - specify the class name of the association. Use it only if that name can't be infered + # from the part id. So composed_of :address 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. + # * :mapping - 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: + # * Project#portfolio, Project#portfolio=(portfolio), Project#portfolio.nil?, Project#portfolio?(portfolio) + # * Project#project_manager, Project#project_manager=(project_manager), Project#project_manager.nil?, + # Project#project_manager?(project_manager), Project#build_project_manager, Project#create_project_manager + # * Project#milestones.empty?, Project#milestones.size, Project#milestones, Project#milestones<<(milestone), + # Project#milestones.delete(milestone), Project#milestones.find(milestone_id), Project#milestones.find_all(conditions), + # Project#milestones.build, Project#milestones.create + # * Project#categories.empty?, Project#categories.size, Project#categories, Project#categories<<(category1), + # Project#categories.delete(category1) + # + # == 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 MyApplication::Business::Company.find(firm.id). 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 :class_name, 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 + # has_many :clients would add among others has_clients?. + # * collection(force_reload = false) - returns an array of all the associated objects. + # An empty array is returned if none are found. + # * collection<<(object, ...) - adds one or more objects to the collection by setting their foreign keys to the collection's primary key. + # * collection.delete(object, ...) - removes one or more objects from the collection by setting their foreign keys to NULL. This does not destroy the objects. + # * collection.clear - removes every object from the collection. This does not destroy the objects. + # * collection.empty? - returns true if there are no associated objects. + # * collection.size - returns the number of associated objects. + # * collection.find(id) - finds an associated object responding to the +id+ and that + # meets the condition that it has to be associated with this object. + # * collection.find_all(conditions = nil, orderings = nil, limit = nil, joins = nil) - 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. + # * collection.build(attributes = {}) - 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. + # * collection.create(attributes = {}) - 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 has_many :clients, which will add: + # * Firm#clients (similar to Clients.find_all "firm_id = #{id}") + # * Firm#clients<< + # * Firm#clients.delete + # * Firm#clients.clear + # * Firm#clients.empty? (similar to firm.clients.size == 0) + # * Firm#clients.size (similar to Client.count "firm_id = #{id}") + # * Firm#clients.find (similar to Client.find_on_conditions(id, "firm_id = #{id}")) + # * Firm#clients.find_all (similar to Client.find_all "firm_id = #{id}") + # * Firm#clients.build (similar to Client.new("firm_id" => id)) + # * Firm#clients.create (similar to c = Client.new("client_id" => id); c.save; c) + # The declaration can also include an options hash to specialize the behavior of the association. + # + # Options are: + # * :class_name - specify the class name of the association. Use it only if that name can't be infered + # from the association name. So has_many :products 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. + # * :conditions - 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%'". + # * :order - specify the order in which the associated objects are returned as a "ORDER BY" sql fragment, + # such as "last_name, first_name DESC" + # * :foreign_key - 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. + # * :dependent - if set to true all the associated object are destroyed alongside this object. + # May not be set if :exclusively_dependent is also set. + # * :exclusively_dependent - 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. + # * :finder_sql - 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 + # has_one :manager would add among others has_manager?. + # * association(force_reload = false) - returns the associated object. Nil is returned if none is found. + # * association=(associate) - assigns the associate object, extracts the primary key, sets it as the foreign key, + # and saves the associate object. + # * association?(object, force_reload = false) - returns true if the +object+ is of the same type and has the + # same id as the associated object. + # * association.nil? - returns true if there is no associated object. + # * build_association(attributes = {}) - 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. + # * create_association(attributes = {}) - 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 has_one :beneficiary, which will add: + # * Account#beneficiary (similar to Beneficiary.find_first "account_id = #{id}") + # * Account#beneficiary=(beneficiary) (similar to beneficiary.account_id = account.id; beneficiary.save) + # * Account#beneficiary? (similar to account.beneficiary == some_beneficiary) + # * Account#beneficiary.nil? + # * Account#build_beneficiary (similar to Beneficiary.new("account_id" => id)) + # * Account#create_beneficiary (similar to b = Beneficiary.new("account_id" => id); b.save; b) + # The declaration can also include an options hash to specialize the behavior of the association. + # + # Options are: + # * :class_name - specify the class name of the association. Use it only if that name can't be infered + # from the association name. So has_one :manager 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. + # * :conditions - specify the conditions that the associated object must meet in order to be included as a "WHERE" + # sql fragment, such as "rank = 5". + # * :order - 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" + # * :dependent - if set to true the associated object is destroyed alongside this object + # * :foreign_key - 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 + # belongs_to :author would add among others has_author?. + # * association(force_reload = false) - returns the associated object. Nil is returned if none is found. + # * association=(associate) - assigns the associate object, extracts the primary key, and sets it as the foreign key. + # * association?(object, force_reload = false) - returns true if the +object+ is of the same type and has the + # same id as the associated object. + # * association.nil? - returns true if there is no associated object. + # + # Example: An Post class declares has_one :author, which will add: + # * Post#author (similar to Author.find(author_id)) + # * Post#author=(author) (similar to post.author_id = author.id) + # * Post#author? (similar to post.author == some_author) + # * Post#author.nil? + # The declaration can also include an options hash to specialize the behavior of the association. + # + # Options are: + # * :class_name - specify the class name of the association. Use it only if that name can't be infered + # from the association name. So has_one :author 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. + # * :conditions - specify the conditions that the associated object must meet in order to be included as a "WHERE" + # sql fragment, such as "authorized = 1". + # * :order - 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" + # * :foreign_key - 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. + # * :counter_cache - 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 + # has_and_belongs_to_many :categories would add among others +add_categories+. + # * collection(force_reload = false) - returns an array of all the associated objects. + # An empty array is returned if none is found. + # * collection<<(object, ...) - 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). + # * collection.push_with_attributes(object, join_attributes) - adds one to the collection by creating an association in the join table that + # also holds the attributes from join_attributes (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). + # * collection.delete(object, ...) - removes one or more objects from the collection by removing their associations from the join table. + # This does not destroy the objects. + # * collection.clear - removes every object from the collection. This does not destroy the objects. + # * collection.empty? - returns true if there are no associated objects. + # * collection.size - returns the number of associated objects. + # + # Example: An Developer class declares has_and_belongs_to_many :projects, which will add: + # * Developer#projects + # * Developer#projects<< + # * Developer#projects.delete + # * Developer#projects.clear + # * Developer#projects.empty? + # * Developer#projects.size + # * Developer#projects.find(id) + # The declaration may include an options hash to specialize the behavior of the association. + # + # Options are: + # * :class_name - specify the class name of the association. Use it only if that name can't be infered + # from the association name. So has_and_belongs_to_many :projects 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. + # * :join_table - 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. + # * :foreign_key - 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. + # * :association_foreign_key - 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. + # * :conditions - specify the conditions that the associated object must meet in order to be included as a "WHERE" + # sql fragment, such as "authorized = 1". + # * :order - specify the order in which the associated objects are returned as a "ORDER BY" sql fragment, such as "last_name, first_name DESC" + # * :uniq - if set to true, duplicate associated objects will be ignored by accessors and query methods + # * :finder_sql - overwrite the default generated SQL used to fetch the association with a manual one + # * :delete_sql - overwrite the default generated SQL used to remove links between the associated + # classes with a manual one + # * :insert_sql - 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 Base.inheritance_column). 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 establish_connection didn't include a + # :adapter key. + # * +AdapterNotSpecified+ -- the :adapter key used in establish_connection 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 :class_name option of + # the serialize definition. + # * +ConnectionNotEstablished+ -- no connection has been established. Use establish_connection 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 DiscussionBoard.increment_counter("post_count", + # discussion_board_id) 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 new(attributes) and + # attributes=(attributes). 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 + # new(attributes) and attributes=(attributes). 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 file_name 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 attr_name 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 attr_name 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 attr_name 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 attr_name 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 +# 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 + +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 +# 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 = < 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 @developers = Fixtures.create_fixtures(".", "developers") 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 @developers hash, so @developers["david"]["name"] will return +# "David Heinemeier Hansson" and @developers["david"]["birthday"] will return Date.new(1979, 10, 15). +# +# 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 on(attribute) + # 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 on(attribute). + # 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 attribute_present?). + 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 Marshaled + # (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 Marshaled 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 -- cgit v1.2.3