diff options
Diffstat (limited to 'activerecord/lib')
97 files changed, 4493 insertions, 3233 deletions
diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index 132dc12680..4f194cb6cf 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -39,6 +39,7 @@ module ActiveRecord autoload :Aggregations autoload :Associations autoload :AttributeMethods + autoload :AttributeAssignment autoload :AutosaveAssociation autoload :Relation @@ -50,30 +51,45 @@ module ActiveRecord autoload :PredicateBuilder autoload :SpawnMethods autoload :Batches + autoload :Explain + autoload :Delegation end autoload :Base autoload :Callbacks + autoload :Core autoload :CounterCache + autoload :ConnectionHandling + autoload :DynamicMatchers autoload :DynamicFinderMatch autoload :DynamicScopeMatch + autoload :Explain + autoload :IdentityMap + autoload :Inheritance + autoload :Integration autoload :Migration autoload :Migrator, 'active_record/migration' - autoload :NamedScope + autoload :Model + autoload :ModelSchema autoload :NestedAttributes autoload :Observer autoload :Persistence autoload :QueryCache + autoload :Querying + autoload :ReadonlyAttributes autoload :Reflection autoload :Result + autoload :Sanitization autoload :Schema autoload :SchemaDumper + autoload :Scoping autoload :Serialization autoload :SessionStore + autoload :Store autoload :Timestamp autoload :Transactions + autoload :Translation autoload :Validations - autoload :IdentityMap end module Coders @@ -91,6 +107,7 @@ module ActiveRecord autoload :Read autoload :TimeZoneConversion autoload :Write + autoload :Serialization end end @@ -112,6 +129,15 @@ module ActiveRecord end end + module Scoping + extend ActiveSupport::Autoload + + eager_autoload do + autoload :Named + autoload :Default + end + end + autoload :TestCase autoload :TestFixtures, 'active_record/fixtures' end diff --git a/activerecord/lib/active_record/aggregations.rb b/activerecord/lib/active_record/aggregations.rb index 81ddbba51e..5a8addc4e4 100644 --- a/activerecord/lib/active_record/aggregations.rb +++ b/activerecord/lib/active_record/aggregations.rb @@ -172,8 +172,8 @@ module ActiveRecord # with this option. # * <tt>:mapping</tt> - Specifies the mapping of entity attributes to attributes of the value # object. Each mapping is represented as an array where the first item is the name of the - # entity attribute and the second item is the name the attribute in the value object. The - # order in which mappings are defined determine the order in which attributes are sent to the + # entity attribute and the second item is the name of the attribute in the value object. The + # order in which mappings are defined determines the order in which attributes are sent to the # value class constructor. # * <tt>:allow_nil</tt> - Specifies that the value object will not be instantiated when all mapped # attributes are +nil+. Setting the value object to +nil+ has the effect of writing +nil+ to all @@ -191,7 +191,8 @@ module ActiveRecord # # Option examples: # composed_of :temperature, :mapping => %w(reading celsius) - # composed_of :balance, :class_name => "Money", :mapping => %w(balance amount), :converter => Proc.new { |balance| balance.to_money } + # composed_of :balance, :class_name => "Money", :mapping => %w(balance amount), + # :converter => Proc.new { |balance| balance.to_money } # composed_of :address, :mapping => [ %w(address_street street), %w(address_city city) ] # composed_of :gps_location # composed_of :gps_location, :allow_nil => true diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 0952ea2829..0efa111d12 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -70,7 +70,7 @@ module ActiveRecord end end - class HasManyThroughNestedAssociationsAreReadonly < ActiveRecordError #:nodoc + class HasManyThroughNestedAssociationsAreReadonly < ActiveRecordError #:nodoc: def initialize(owner, reflection) super("Cannot modify association '#{owner.class.name}##{reflection.name}' because it goes through more than one other association.") end @@ -196,6 +196,26 @@ module ActiveRecord # * <tt>Project#categories.empty?, Project#categories.size, Project#categories, Project#categories<<(category1),</tt> # <tt>Project#categories.delete(category1)</tt> # + # === Overriding generated methods + # + # Association methods are generated in a module that is included into the model class, + # which allows you to easily override with your own methods and call the original + # generated method with +super+. For example: + # + # class Car < ActiveRecord::Base + # belongs_to :owner + # belongs_to :old_owner + # def owner=(new_owner) + # self.old_owner = self.owner + # super + # end + # end + # + # If your model class is <tt>Project</tt>, the module is + # named <tt>Project::GeneratedFeatureMethods</tt>. The GeneratedFeatureMethods module is + # included in the model class immediately after the (anonymous) generated attributes methods + # module, meaning an association will override the methods for an attribute with the same name. + # # === A word of warning # # Don't create associations that have the same name as instance methods of @@ -1165,7 +1185,7 @@ module ActiveRecord # has_many :subscribers, :through => :subscriptions, :source => :user # has_many :subscribers, :class_name => "Person", :finder_sql => Proc.new { # %Q{ - # SELECT DISTINCT people.* + # SELECT DISTINCT * # FROM people p, post_subscriptions ps # WHERE ps.post_id = #{id} AND ps.person_id = p.id # ORDER BY p.first_name @@ -1424,18 +1444,18 @@ module ActiveRecord # join table with a migration such as this: # # class CreateDevelopersProjectsJoinTable < ActiveRecord::Migration - # def self.up + # def change # create_table :developers_projects, :id => false do |t| # t.integer :developer_id # t.integer :project_id # end # end - # - # def self.down - # drop_table :developers_projects - # end # end # + # It's also a good idea to add indexes to each of those columns to speed up the joins process. + # However, in MySQL it is advised to add a compound index for both of the columns as MySQL only + # uses one index per table during the lookup. + # # Adds the following methods for retrieval and query: # # [collection(force_reload = false)] diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb index d1e3ff8e38..7887d59aad 100644 --- a/activerecord/lib/active_record/associations/association.rb +++ b/activerecord/lib/active_record/associations/association.rb @@ -231,10 +231,7 @@ module ActiveRecord def build_record(attributes, options) reflection.build_association(attributes, options) do |record| - record.assign_attributes( - create_scope.except(*record.changed), - :without_protection => true - ) + record.assign_attributes(create_scope.except(*record.changed), :without_protection => true) end end end diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb index 6cc401e6cc..090f690f0d 100644 --- a/activerecord/lib/active_record/associations/association_scope.rb +++ b/activerecord/lib/active_record/associations/association_scope.rb @@ -20,31 +20,19 @@ module ActiveRecord # It's okay to just apply all these like this. The options will only be present if the # association supports that option; this is enforced by the association builder. scope = scope.apply_finder_options(options.slice( - :readonly, :include, :order, :limit, :joins, :group, :having, :offset)) + :readonly, :include, :eager_load, :order, :limit, :joins, :group, :having, :offset, :select)) if options[:through] && !options[:include] scope = scope.includes(source_options[:include]) end - if select = select_value - scope = scope.select(select) - end + scope = scope.uniq if options[:uniq] add_constraints(scope) end private - def select_value - select_value = options[:select] - - if reflection.collection? - select_value ||= options[:uniq] && "DISTINCT #{reflection.quoted_table_name}.*" - end - - select_value - end - def add_constraints(scope) tables = construct_tables diff --git a/activerecord/lib/active_record/associations/builder/association.rb b/activerecord/lib/active_record/associations/builder/association.rb index 96fca97440..776f0d0469 100644 --- a/activerecord/lib/active_record/associations/builder/association.rb +++ b/activerecord/lib/active_record/associations/builder/association.rb @@ -1,7 +1,7 @@ module ActiveRecord::Associations::Builder class Association #:nodoc: class_attribute :valid_options - self.valid_options = [:class_name, :foreign_key, :select, :conditions, :include, :extend, :readonly, :validate] + self.valid_options = [:class_name, :foreign_key, :select, :conditions, :include, :eager_load, :extend, :readonly, :validate] # Set by subclasses class_attribute :macro @@ -16,6 +16,10 @@ module ActiveRecord::Associations::Builder @model, @name, @options = model, name, options end + def mixin + @model.generated_feature_methods + end + def build validate_options reflection = model.create_reflection(self.class.macro, name, options, model) @@ -36,16 +40,14 @@ module ActiveRecord::Associations::Builder def define_readers name = self.name - - model.redefine_method(name) do |*params| + mixin.redefine_method(name) do |*params| association(name).reader(*params) end end def define_writers name = self.name - - model.redefine_method("#{name}=") do |value| + mixin.redefine_method("#{name}=") do |value| association(name).writer(value) end end diff --git a/activerecord/lib/active_record/associations/builder/belongs_to.rb b/activerecord/lib/active_record/associations/builder/belongs_to.rb index f6d26840c2..1759a41d93 100644 --- a/activerecord/lib/active_record/associations/builder/belongs_to.rb +++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb @@ -25,14 +25,14 @@ module ActiveRecord::Associations::Builder name = self.name method_name = "belongs_to_counter_cache_after_create_for_#{name}" - model.redefine_method(method_name) do + mixin.redefine_method(method_name) do record = send(name) record.class.increment_counter(cache_column, record.id) unless record.nil? end model.after_create(method_name) method_name = "belongs_to_counter_cache_before_destroy_for_#{name}" - model.redefine_method(method_name) do + mixin.redefine_method(method_name) do record = send(name) record.class.decrement_counter(cache_column, record.id) unless record.nil? end @@ -48,7 +48,7 @@ module ActiveRecord::Associations::Builder method_name = "belongs_to_touch_after_save_or_destroy_for_#{name}" touch = options[:touch] - model.redefine_method(method_name) do + mixin.redefine_method(method_name) do record = send(name) unless record.nil? diff --git a/activerecord/lib/active_record/associations/builder/collection_association.rb b/activerecord/lib/active_record/associations/builder/collection_association.rb index f62209a226..35f9a3ae8e 100644 --- a/activerecord/lib/active_record/associations/builder/collection_association.rb +++ b/activerecord/lib/active_record/associations/builder/collection_association.rb @@ -58,7 +58,7 @@ module ActiveRecord::Associations::Builder super name = self.name - model.redefine_method("#{name.to_s.singularize}_ids") do + mixin.redefine_method("#{name.to_s.singularize}_ids") do association(name).ids_reader end end @@ -67,7 +67,7 @@ module ActiveRecord::Associations::Builder super name = self.name - model.redefine_method("#{name.to_s.singularize}_ids=") do |ids| + mixin.redefine_method("#{name.to_s.singularize}_ids=") do |ids| association(name).ids_writer(ids) end end diff --git a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb index 30fc44b4c2..0b634ab944 100644 --- a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb +++ b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb @@ -18,7 +18,7 @@ module ActiveRecord::Associations::Builder model.send(:include, Module.new { class_eval <<-RUBY, __FILE__, __LINE__ + 1 def destroy_associations - association(#{name.to_sym.inspect}).delete_all + association(#{name.to_sym.inspect}).delete_all_on_destroy super end RUBY diff --git a/activerecord/lib/active_record/associations/builder/has_many.rb b/activerecord/lib/active_record/associations/builder/has_many.rb index ecbc70888f..9c24f40690 100644 --- a/activerecord/lib/active_record/associations/builder/has_many.rb +++ b/activerecord/lib/active_record/associations/builder/has_many.rb @@ -28,7 +28,7 @@ module ActiveRecord::Associations::Builder def define_destroy_dependency_method name = self.name - model.send(:define_method, dependency_method_name) do + mixin.redefine_method(dependency_method_name) do send(name).each do |o| # No point in executing the counter update since we're going to destroy the parent anyway counter_method = ('belongs_to_counter_cache_before_destroy_for_' + self.class.name.downcase).to_sym @@ -45,15 +45,21 @@ module ActiveRecord::Associations::Builder def define_delete_all_dependency_method name = self.name - model.send(:define_method, dependency_method_name) do + mixin.redefine_method(dependency_method_name) do + association(name).delete_all_on_destroy + end + end + + def define_nullify_dependency_method + name = self.name + mixin.redefine_method(dependency_method_name) do send(name).delete_all end end - alias :define_nullify_dependency_method :define_delete_all_dependency_method def define_restrict_dependency_method name = self.name - model.send(:define_method, dependency_method_name) do + mixin.redefine_method(dependency_method_name) do raise ActiveRecord::DeleteRestrictionError.new(name) unless send(name).empty? end end diff --git a/activerecord/lib/active_record/associations/builder/has_one.rb b/activerecord/lib/active_record/associations/builder/has_one.rb index 88c0d3e90f..7a6cd3890f 100644 --- a/activerecord/lib/active_record/associations/builder/has_one.rb +++ b/activerecord/lib/active_record/associations/builder/has_one.rb @@ -44,18 +44,17 @@ module ActiveRecord::Associations::Builder end def define_destroy_dependency_method - model.send(:class_eval, <<-eoruby, __FILE__, __LINE__ + 1) - def #{dependency_method_name} - association(#{name.to_sym.inspect}).delete - end - eoruby + name = self.name + mixin.redefine_method(dependency_method_name) do + association(name).delete + end end alias :define_delete_dependency_method :define_destroy_dependency_method alias :define_nullify_dependency_method :define_destroy_dependency_method def define_restrict_dependency_method name = self.name - model.redefine_method(dependency_method_name) do + mixin.redefine_method(dependency_method_name) do raise ActiveRecord::DeleteRestrictionError.new(name) unless send(name).nil? end end diff --git a/activerecord/lib/active_record/associations/builder/singular_association.rb b/activerecord/lib/active_record/associations/builder/singular_association.rb index 0cbbba041a..436b6c1524 100644 --- a/activerecord/lib/active_record/associations/builder/singular_association.rb +++ b/activerecord/lib/active_record/associations/builder/singular_association.rb @@ -16,15 +16,15 @@ module ActiveRecord::Associations::Builder def define_constructors name = self.name - model.redefine_method("build_#{name}") do |*params, &block| + mixin.redefine_method("build_#{name}") do |*params, &block| association(name).build(*params, &block) end - model.redefine_method("create_#{name}") do |*params, &block| + mixin.redefine_method("create_#{name}") do |*params, &block| association(name).create(*params, &block) end - model.redefine_method("create_#{name}!") do |*params, &block| + mixin.redefine_method("create_#{name}!") do |*params, &block| association(name).create!(*params, &block) end end diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index cec876149c..fe9f30bd2a 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -49,10 +49,18 @@ module ActiveRecord end else column = "#{reflection.quoted_table_name}.#{reflection.association_primary_key}" + relation = scoped - scoped.select(column).map! do |record| - record.send(reflection.association_primary_key) + including = (relation.eager_load_values + relation.includes_values).uniq + + if including.any? + join_dependency = ActiveRecord::Associations::JoinDependency.new(reflection.klass, including, []) + relation = join_dependency.join_associations.inject(relation) do |r, association| + association.join_relation(r) + end end + + relation.uniq.pluck(column) end end @@ -152,6 +160,13 @@ module ActiveRecord end end + # Called when the association is declared as :dependent => :delete_all. This is + # an optimised version which avoids loading the records into memory. Not really + # for public consumption. + def delete_all_on_destroy + scoped.delete_all + end + # Destroy all the records from this association. # # See destroy for more info. @@ -235,7 +250,7 @@ module ActiveRecord # This method is abstract in the sense that it relies on # +count_records+, which is a method descendants have to provide. def size - if owner.new_record? || (loaded? && !options[:uniq]) + if !find_target? || (loaded? && !options[:uniq]) target.size elsif !loaded? && options[:group] load_target.size @@ -344,8 +359,12 @@ module ActiveRecord if options[:counter_sql] interpolate(options[:counter_sql]) else - # replace the SELECT clause with COUNT(*), preserving any hints within /* ... */ - interpolate(options[:finder_sql]).sub(/SELECT\b(\/\*.*?\*\/ )?(.*)\bFROM\b/im) { "SELECT #{$1}COUNT(*) FROM" } + # replace the SELECT clause with COUNT(SELECTS), preserving any hints within /* ... */ + interpolate(options[:finder_sql]).sub(/SELECT\b(\/\*.*?\*\/ )?(.*)\bFROM\b/im) do + count_with = $2.to_s + count_with = '*' if count_with.blank? || count_with =~ /,/ + "SELECT #{$1}COUNT(#{count_with}) FROM" + end end end diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb index 3181ca9a32..eb320bc774 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -39,10 +39,9 @@ module ActiveRecord instance_methods.each { |m| undef_method m unless m.to_s =~ /^(?:nil\?|send|object_id|to_a)$|^__|^respond_to|proxy_/ } delegate :group, :order, :limit, :joins, :where, :preload, :eager_load, :includes, :from, - :lock, :readonly, :having, :to => :scoped + :lock, :readonly, :having, :pluck, :to => :scoped - delegate :target, :load_target, :loaded?, :scoped, - :to => :@association + delegate :target, :load_target, :loaded?, :to => :@association delegate :select, :find, :first, :last, :build, :create, :create!, @@ -62,6 +61,13 @@ module ActiveRecord @association end + def scoped + association = @association + association.scoped.extending do + define_method(:proxy_association) { association } + end + end + def respond_to?(name, include_private = false) super || (load_target && target.respond_to?(name, include_private)) || 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 index 1f917f58f2..a4cea99372 100644 --- 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 @@ -32,6 +32,10 @@ module ActiveRecord record end + # ActiveRecord::Relation#delete_all needs to support joins before we can use a + # SQL-only implementation. + alias delete_all_on_destroy delete_all + private def count_records diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index 50ee60284c..059e6c77bc 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -99,6 +99,10 @@ module ActiveRecord end end end + + def foreign_key_present? + owner.attribute_present?(reflection.association_primary_key) + end end end end diff --git a/activerecord/lib/active_record/associations/has_many_through_association.rb b/activerecord/lib/active_record/associations/has_many_through_association.rb index 2e818dca5d..9657cb081d 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -6,6 +6,13 @@ module ActiveRecord class HasManyThroughAssociation < HasManyAssociation #:nodoc: include ThroughAssociation + def initialize(owner, reflection) + super + + @through_records = {} + @through_association = nil + end + # Returns the size of the collection by executing a SELECT COUNT(*) query if the collection hasn't been # loaded and calling collection.size if it has. If it's more likely than not that the collection does # have a size larger than zero, and you need to fetch that collection afterwards, it'll take one fewer @@ -42,27 +49,40 @@ module ActiveRecord end end - through_record(record).save! + save_through_record(record) update_counter(1) record end - private + # ActiveRecord::Relation#delete_all needs to support joins before we can use a + # SQL-only implementation. + alias delete_all_on_destroy delete_all - def through_record(record) - through_association = owner.association(through_reflection.name) - attributes = construct_join_attributes(record) + private - through_record = Array.wrap(through_association.target).find { |candidate| - candidate.attributes.slice(*attributes.keys) == attributes - } + def through_association + @through_association ||= owner.association(through_reflection.name) + end - unless through_record - through_record = through_association.build(attributes) + # We temporarily cache through record that has been build, because if we build a + # through record in build_record and then subsequently call insert_record, then we + # want to use the exact same object. + # + # However, after insert_record has been called, we clear the cache entry because + # we want it to be possible to have multiple instances of the same record in an + # association + def build_through_record(record) + @through_records[record.object_id] ||= begin + through_record = through_association.build(construct_join_attributes(record)) through_record.send("#{source_reflection.name}=", record) + through_record end + end - through_record + def save_through_record(record) + build_through_record(record).save! + ensure + @through_records.delete(record.object_id) end def build_record(attributes, options = {}) @@ -73,9 +93,9 @@ module ActiveRecord inverse = source_reflection.inverse_of if inverse if inverse.macro == :has_many - record.send(inverse.name) << through_record(record) + record.send(inverse.name) << build_through_record(record) elsif inverse.macro == :has_one - record.send("#{inverse.name}=", through_record(record)) + record.send("#{inverse.name}=", build_through_record(record)) end end @@ -104,8 +124,7 @@ module ActiveRecord def delete_records(records, method) ensure_not_nested - through = owner.association(through_reflection.name) - scope = through.scoped.where(construct_join_attributes(*records)) + scope = through_association.scoped.where(construct_join_attributes(*records)) case method when :destroy @@ -116,7 +135,7 @@ module ActiveRecord count = scope.delete_all end - delete_through_records(through, records) + delete_through_records(records) if through_reflection.macro == :has_many && update_through_counter?(method) update_counter(-count, through_reflection) @@ -125,15 +144,25 @@ module ActiveRecord update_counter(-count) end - def delete_through_records(through, records) - if through_reflection.macro == :has_many - records.each do |record| - through.target.delete(through_record(record)) - end - else - records.each do |record| - through.target = nil if through.target == through_record(record) + def through_records_for(record) + attributes = construct_join_attributes(record) + candidates = Array.wrap(through_association.target) + candidates.find_all { |c| c.attributes.slice(*attributes.keys) == attributes } + end + + def delete_through_records(records) + records.each do |record| + through_records = through_records_for(record) + + if through_reflection.macro == :has_many + through_records.each { |r| through_association.target.delete(r) } + else + if through_records.include?(through_association.target) + through_association.target = nil + end end + + @through_records.delete(record.object_id) end end diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb index 6c878f0f00..827b01c5ac 100644 --- a/activerecord/lib/active_record/associations/join_dependency.rb +++ b/activerecord/lib/active_record/associations/join_dependency.rb @@ -184,7 +184,7 @@ module ActiveRecord macro = join_part.reflection.macro if macro == :has_one - return if record.association_cache.key?(join_part.reflection.name) + return record.association(join_part.reflection.name).target if record.association_cache.key?(join_part.reflection.name) association = join_part.instantiate(row) unless row[join_part.aliased_primary_key].nil? set_target_and_inverse(join_part, association, record) else diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb index 779f8164cc..298decb0f1 100644 --- a/activerecord/lib/active_record/associations/preloader/association.rb +++ b/activerecord/lib/active_record/associations/preloader/association.rb @@ -100,6 +100,7 @@ module ActiveRecord scope = scope.select(preload_options[:select] || options[:select] || table[Arel.star]) scope = scope.includes(preload_options[:include] || options[:include]) + scope = scope.eager_load(preload_options[:eager_load] || options[:eager_load]) if options[:as] scope = scope.where( diff --git a/activerecord/lib/active_record/associations/preloader/through_association.rb b/activerecord/lib/active_record/associations/preloader/through_association.rb index ad6374d09a..97898c53ae 100644 --- a/activerecord/lib/active_record/associations/preloader/through_association.rb +++ b/activerecord/lib/active_record/associations/preloader/through_association.rb @@ -53,6 +53,7 @@ module ActiveRecord else if options[:conditions] through_options[:include] = options[:include] || options[:source] + through_options[:eager_load] = options[:eager_load] || options[:source] through_options[:conditions] = options[:conditions] end diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb index b347a94978..f95e5337c2 100644 --- a/activerecord/lib/active_record/associations/through_association.rb +++ b/activerecord/lib/active_record/associations/through_association.rb @@ -44,7 +44,7 @@ module ActiveRecord join_attributes = { source_reflection.foreign_key => records.map { |record| - record.send(source_reflection.association_primary_key) + record.send(source_reflection.association_primary_key(reflection.klass)) } } diff --git a/activerecord/lib/active_record/attribute_assignment.rb b/activerecord/lib/active_record/attribute_assignment.rb new file mode 100644 index 0000000000..bf9fe70b31 --- /dev/null +++ b/activerecord/lib/active_record/attribute_assignment.rb @@ -0,0 +1,221 @@ +require 'active_support/concern' + +module ActiveRecord + module AttributeAssignment + extend ActiveSupport::Concern + include ActiveModel::MassAssignmentSecurity + + module ClassMethods + private + + # The primary key and inheritance column can never be set by mass-assignment for security reasons. + def attributes_protected_by_default + default = [ primary_key, inheritance_column ] + default << 'id' unless primary_key.eql? 'id' + default + end + 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). + # + # If any attributes are protected by either +attr_protected+ or + # +attr_accessible+ then only settable attributes will be assigned. + # + # class User < ActiveRecord::Base + # attr_protected :is_admin + # end + # + # user = User.new + # user.attributes = { :username => 'Phusion', :is_admin => true } + # user.username # => "Phusion" + # user.is_admin? # => false + def attributes=(new_attributes) + return unless new_attributes.is_a?(Hash) + + assign_attributes(new_attributes) + end + + # Allows you to set all the attributes for a particular mass-assignment + # security role by passing in a hash of attributes with keys matching + # the attribute names (which again matches the column names) and the role + # name using the :as option. + # + # To bypass mass-assignment security you can use the :without_protection => true + # option. + # + # class User < ActiveRecord::Base + # attr_accessible :name + # attr_accessible :name, :is_admin, :as => :admin + # end + # + # user = User.new + # user.assign_attributes({ :name => 'Josh', :is_admin => true }) + # user.name # => "Josh" + # user.is_admin? # => false + # + # user = User.new + # user.assign_attributes({ :name => 'Josh', :is_admin => true }, :as => :admin) + # user.name # => "Josh" + # user.is_admin? # => true + # + # user = User.new + # user.assign_attributes({ :name => 'Josh', :is_admin => true }, :without_protection => true) + # user.name # => "Josh" + # user.is_admin? # => true + def assign_attributes(new_attributes, options = {}) + return unless new_attributes + + attributes = new_attributes.stringify_keys + multi_parameter_attributes = [] + nested_parameter_attributes = [] + @mass_assignment_options = options + + unless options[:without_protection] + attributes = sanitize_for_mass_assignment(attributes, mass_assignment_role) + end + + attributes.each do |k, v| + if k.include?("(") + multi_parameter_attributes << [ k, v ] + elsif respond_to?("#{k}=") + if v.is_a?(Hash) + nested_parameter_attributes << [ k, v ] + else + send("#{k}=", v) + end + else + raise(UnknownAttributeError, "unknown attribute: #{k}") + end + end + + # assign any deferred nested attributes after the base attributes have been set + nested_parameter_attributes.each do |k,v| + send("#{k}=", v) + end + + @mass_assignment_options = nil + assign_multiparameter_attributes(multi_parameter_attributes) + end + + protected + + def mass_assignment_options + @mass_assignment_options ||= {} + end + + def mass_assignment_role + mass_assignment_options[:as] || :default + end + + private + + # 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 + # parentheses 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 are 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 + + def instantiate_time_object(name, values) + if self.class.send(:create_time_zone_conversion_attribute?, name, column_for_attribute(name)) + Time.zone.local(*values) + else + Time.time_with_datetime_fallback(self.class.default_timezone, *values) + end + end + + def execute_callstack_for_multiparameter_attributes(callstack) + errors = [] + callstack.each do |name, values_with_empty_parameters| + begin + send(name + "=", read_value_from_parameter(name, values_with_empty_parameters)) + rescue => ex + errors << AttributeAssignmentError.new("error on assignment #{values_with_empty_parameters.values.inspect} to #{name}", ex, name) + end + end + unless errors.empty? + raise MultiparameterAssignmentErrors.new(errors), "#{errors.size} error(s) on assignment of multiparameter attributes" + end + end + + def read_value_from_parameter(name, values_hash_from_param) + klass = (self.class.reflect_on_aggregation(name.to_sym) || column_for_attribute(name)).klass + if values_hash_from_param.values.all?{|v|v.nil?} + nil + elsif klass == Time + read_time_parameter_value(name, values_hash_from_param) + elsif klass == Date + read_date_parameter_value(name, values_hash_from_param) + else + read_other_parameter_value(klass, name, values_hash_from_param) + end + end + + def read_time_parameter_value(name, values_hash_from_param) + # If Date bits were not provided, error + raise "Missing Parameter" if [1,2,3].any?{|position| !values_hash_from_param.has_key?(position)} + max_position = extract_max_param_for_multiparameter_attributes(values_hash_from_param, 6) + # If Date bits were provided but blank, then return nil + return nil if (1..3).any? {|position| values_hash_from_param[position].blank?} + + set_values = (1..max_position).collect{|position| values_hash_from_param[position] } + # If Time bits are not there, then default to 0 + (3..5).each {|i| set_values[i] = set_values[i].blank? ? 0 : set_values[i]} + instantiate_time_object(name, set_values) + end + + def read_date_parameter_value(name, values_hash_from_param) + return nil if (1..3).any? {|position| values_hash_from_param[position].blank?} + set_values = [values_hash_from_param[1], values_hash_from_param[2], values_hash_from_param[3]] + begin + Date.new(*set_values) + rescue ArgumentError # if Date.new raises an exception on an invalid date + instantiate_time_object(name, set_values).to_date # we instantiate Time object and convert it back to a date thus using Time's logic in handling invalid dates + end + end + + def read_other_parameter_value(klass, name, values_hash_from_param) + max_position = extract_max_param_for_multiparameter_attributes(values_hash_from_param) + values = (1..max_position).collect do |position| + raise "Missing Parameter" if !values_hash_from_param.has_key?(position) + values_hash_from_param[position] + end + klass.new(*values) + end + + def extract_max_param_for_multiparameter_attributes(values_hash_from_param, upper_cap = 100) + [values_hash_from_param.keys.max,upper_cap].min + end + + def extract_callstack_for_multiparameter_attributes(pairs) + attributes = { } + + pairs.each do |pair| + multiparameter_name, value = pair + attribute_name = multiparameter_name.split("(").first + attributes[attribute_name] = {} unless attributes.include?(attribute_name) + + parameter_value = value.empty? ? nil : type_cast_attribute_value(multiparameter_name, value) + attributes[attribute_name][find_parameter_position(multiparameter_name)] ||= parameter_value + end + + attributes + end + + def type_cast_attribute_value(multiparameter_name, value) + multiparameter_name =~ /\([0-9]*([if])\)/ ? value.send("to_" + $1) : value + end + + def find_parameter_position(multiparameter_name) + multiparameter_name.scan(/\(([0-9]*).*\)/).first.first.to_i + end + + end +end diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index d7bfaa5655..c5834e2fef 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -7,35 +7,53 @@ module ActiveRecord extend ActiveSupport::Concern include ActiveModel::AttributeMethods + included do + include Read + include Write + include BeforeTypeCast + include Query + include PrimaryKey + include TimeZoneConversion + include Dirty + include Serialization + + # Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (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). + alias [] read_attribute + + # Updates the attribute identified by <tt>attr_name</tt> with the specified +value+. + # (Alias for the protected write_attribute method). + alias []= write_attribute + + public :[], :[]= + end + module ClassMethods # Generates all the attribute related methods for columns in the database # accessors, mutators and query methods. def define_attribute_methods return if attribute_methods_generated? - - if base_class == self - super(column_names) - @attribute_methods_generated = true - else - base_class.define_attribute_methods - end + superclass.define_attribute_methods unless self == base_class + super(column_names) + @attribute_methods_generated = true end def attribute_methods_generated? - if base_class == self - @attribute_methods_generated ||= false - else - base_class.attribute_methods_generated? - end + @attribute_methods_generated ||= false end - def undefine_attribute_methods(*args) - if base_class == self - super - @attribute_methods_generated = false - else - base_class.undefine_attribute_methods(*args) - end + # We will define the methods as instance methods, but will call them as singleton + # methods. This allows us to use method_defined? to check if the method exists, + # which is fast and won't give any false positives from the ancestors (because + # there are no ancestors). + def generated_external_attribute_methods + @generated_external_attribute_methods ||= Module.new { extend self } + end + + def undefine_attribute_methods + super if attribute_methods_generated? + @attribute_methods_generated = false end def instance_method_already_implemented?(method_name) @@ -43,19 +61,46 @@ module ActiveRecord raise DangerousAttributeError, "#{method_name} is defined by ActiveRecord" end - super + if [Base, Model].include?(active_record_super) + super + else + # If B < A and A defines its own attribute method, then we don't want to overwrite that. + defined = method_defined_within?(method_name, superclass, superclass.generated_attribute_methods) + defined && !ActiveRecord::Base.method_defined?(method_name) || super + end end # A method name is 'dangerous' if it is already defined by Active Record, but # not by any ancestors. (So 'puts' is not dangerous but 'save' is.) - def dangerous_attribute_method?(method_name) - active_record = ActiveRecord::Base - superclass = ActiveRecord::Base.superclass - - (active_record.method_defined?(method_name) || - active_record.private_method_defined?(method_name)) && - !superclass.method_defined?(method_name) && - !superclass.private_method_defined?(method_name) + def dangerous_attribute_method?(name) + method_defined_within?(name, Base) + end + + def method_defined_within?(name, klass, sup = klass.superclass) + if klass.method_defined?(name) || klass.private_method_defined?(name) + if sup.method_defined?(name) || sup.private_method_defined?(name) + klass.instance_method(name).owner != sup.instance_method(name).owner + else + true + end + else + false + end + end + + def attribute_method?(attribute) + super || (table_exists? && column_names.include?(attribute.to_s.sub(/=$/, ''))) + end + + # Returns an array of column names as strings if it's not + # an abstract class and table exists. + # Otherwise it returns an empty array. + def attribute_names + @attribute_names ||= if !abstract_class? && table_exists? + column_names + else + [] + end end end @@ -94,9 +139,105 @@ module ActiveRecord super end + # Returns true if the given attribute is in the attributes hash + def has_attribute?(attr_name) + @attributes.has_key?(attr_name.to_s) + end + + # Returns an array of names for the attributes available on this object. + def attribute_names + @attributes.keys + end + + # Returns a hash of all the attributes with their names as keys and the values of the attributes as values. + def attributes + Hash[@attributes.map { |name, _| [name, read_attribute(name)] }] + end + + # Returns an <tt>#inspect</tt>-like string for the value of the + # attribute +attr_name+. String attributes are truncated upto 50 + # characters, and Date and Time attributes are returned in the + # <tt>:db</tt> format. Other attributes return the value of + # <tt>#inspect</tt> without modification. + # + # person = Person.create!(:name => "David Heinemeier Hansson " * 3) + # + # person.attribute_for_inspect(:name) + # # => '"David Heinemeier Hansson David Heinemeier Hansson D..."' + # + # person.attribute_for_inspect(:created_at) + # # => '"2009-01-12 04:48:57"' + def attribute_for_inspect(attr_name) + value = read_attribute(attr_name) + + if value.is_a?(String) && value.length > 50 + "#{value[0..50]}...".inspect + elsif value.is_a?(Date) || value.is_a?(Time) + %("#{value.to_s(:db)}") + else + value.inspect + end + 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 respond to empty?, most notably Strings). + def attribute_present?(attribute) + value = read_attribute(attribute) + !value.nil? || (value.respond_to?(:empty?) && !value.empty?) + end + + # Returns the column object for the named attribute. + def column_for_attribute(name) + self.class.columns_hash[name.to_s] + end + protected - def attribute_method?(attr_name) - attr_name == 'id' || (defined?(@attributes) && @attributes.include?(attr_name)) + + def clone_attributes(reader_method = :read_attribute, attributes = {}) + attribute_names.each do |name| + attributes[name] = clone_attribute_value(reader_method, name) end + attributes + end + + def clone_attribute_value(reader_method, attribute_name) + value = send(reader_method, attribute_name) + value.duplicable? ? value.clone : value + rescue TypeError, NoMethodError + value + end + + # Returns a copy of the attributes hash where all the values have been safely quoted for use in + # an Arel insert/update method. + def arel_attributes_values(include_primary_key = true, include_readonly_attributes = true, attribute_names = @attributes.keys) + attrs = {} + klass = self.class + arel_table = klass.arel_table + + attribute_names.each do |name| + if (column = column_for_attribute(name)) && (include_primary_key || !column.primary) + + if include_readonly_attributes || !self.class.readonly_attributes.include?(name) + + value = if klass.serialized_attributes.include?(name) + @attributes[name].serialized_value + else + # FIXME: we need @attributes to be used consistently. + # If the values stored in @attributes were already type + # casted, this code could be simplified + read_attribute(name) + end + + attrs[arel_table[name]] = value + end + end + end + + attrs + end + + def attribute_method?(attr_name) + attr_name == 'id' || (defined?(@attributes) && @attributes.include?(attr_name)) + end end end diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb index 3eff3d54e3..40e4a97e73 100644 --- a/activerecord/lib/active_record/attribute_methods/dirty.rb +++ b/activerecord/lib/active_record/attribute_methods/dirty.rb @@ -13,7 +13,7 @@ module ActiveRecord raise "You cannot include Dirty after Timestamp" end - class_attribute :partial_updates + config_attribute :partial_updates self.partial_updates = true end @@ -34,9 +34,9 @@ module ActiveRecord @previously_changed = changes @changed_attributes.clear end - rescue - IdentityMap.remove(self) if IdentityMap.enabled? - raise + rescue + IdentityMap.remove(self) if IdentityMap.enabled? + raise end # <tt>reload</tt> the record and clears changed attributes. diff --git a/activerecord/lib/active_record/attribute_methods/primary_key.rb b/activerecord/lib/active_record/attribute_methods/primary_key.rb index 36d7f4ad11..a7785f8786 100644 --- a/activerecord/lib/active_record/attribute_methods/primary_key.rb +++ b/activerecord/lib/active_record/attribute_methods/primary_key.rb @@ -5,16 +5,48 @@ module ActiveRecord # Returns this record's primary key value wrapped in an Array if one is available def to_key - key = send(self.class.primary_key) + key = self.id [key] if key end + # Returns the primary key value + def id + read_attribute(self.class.primary_key) + end + + # Sets the primary key value + def id=(value) + write_attribute(self.class.primary_key, value) + end + + # Queries the primary key value + def id? + query_attribute(self.class.primary_key) + end + module ClassMethods + def define_method_attribute(attr_name) + super + + if attr_name == primary_key && attr_name != 'id' + generated_attribute_methods.send(:alias_method, :id, primary_key) + generated_external_attribute_methods.module_eval <<-CODE, __FILE__, __LINE__ + def id(v, attributes, attributes_cache, attr_name) + attr_name = '#{primary_key}' + send(attr_name, attributes[attr_name], attributes, attributes_cache, attr_name) + end + CODE + end + end + + def dangerous_attribute_method?(method_name) + super && !['id', 'id=', 'id?'].include?(method_name) + end + # Defines the primary key field -- can be overridden in subclasses. Overwriting will negate any effect of the # primary_key_prefix_type setting, though. def primary_key - @primary_key ||= reset_primary_key - raise ActiveRecord::UnknownPrimaryKey.new(self) unless @primary_key + @primary_key = reset_primary_key unless defined? @primary_key @primary_key end @@ -24,16 +56,11 @@ module ActiveRecord end def reset_primary_key #:nodoc: - key = self == base_class ? get_primary_key(base_class.name) : - base_class.primary_key - - set_primary_key(key) - key - end - - def primary_key? #:nodoc: - @primary_key ||= reset_primary_key - !@primary_key.nil? + if self == base_class + self.primary_key = get_primary_key(base_class.name) + else + self.primary_key = base_class.primary_key + end end def get_primary_key(base_name) #:nodoc: @@ -45,35 +72,32 @@ module ActiveRecord when :table_name_with_underscore base_name.foreign_key else - if ActiveRecord::Base != self && connection.table_exists?(table_name) - connection.primary_key(table_name) + if ActiveRecord::Base != self && table_exists? + connection.schema_cache.primary_keys[table_name] else 'id' end end end - attr_accessor :original_primary_key - - # Attribute writer for the primary key column - def primary_key=(value) - @quoted_primary_key = nil - @primary_key = value - end - - # Sets the name of the primary key column to use to the given value, - # or (if the value is nil or false) to the value returned by the given - # block. + # Sets the name of the primary key column. + # + # class Project < ActiveRecord::Base + # self.primary_key = "sysid" + # end + # + # You can also define the primary_key method yourself: # # class Project < ActiveRecord::Base - # set_primary_key "sysid" + # def self.primary_key + # "foo_" + super + # end # end - def set_primary_key(value = nil, &block) - @quoted_primary_key = nil - @primary_key ||= '' - self.original_primary_key = @primary_key - value &&= value.to_s - self.primary_key = block_given? ? instance_eval(&block) : value + # Project.primary_key # => "foo_id" + def primary_key=(value) + @original_primary_key = @primary_key if defined?(@primary_key) + @primary_key = value && value.to_s + @quoted_primary_key = nil end end end diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb index 8566ecad14..964c4123ef 100644 --- a/activerecord/lib/active_record/attribute_methods/read.rb +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -6,11 +6,8 @@ module ActiveRecord ATTRIBUTE_TYPES_CACHED_BY_DEFAULT = [:datetime, :timestamp, :time, :date] included do - cattr_accessor :attribute_types_cached_by_default, :instance_writer => false + config_attribute :attribute_types_cached_by_default, :global => true self.attribute_types_cached_by_default = ATTRIBUTE_TYPES_CACHED_BY_DEFAULT - - # Undefine id so it can be used as an attribute name - undef_method(:id) if method_defined?(:id) end module ClassMethods @@ -32,107 +29,101 @@ module ActiveRecord cached_attributes.include?(attr_name) end + def undefine_attribute_methods + generated_external_attribute_methods.module_eval do + instance_methods.each { |m| undef_method(m) } + end + + super + end + + def type_cast_attribute(attr_name, attributes, cache = {}) #:nodoc: + return unless attr_name + attr_name = attr_name.to_s + + if generated_external_attribute_methods.method_defined?(attr_name) + if attributes.has_key?(attr_name) || attr_name == 'id' + generated_external_attribute_methods.send(attr_name, attributes[attr_name], attributes, cache, attr_name) + end + elsif !attribute_methods_generated? + # If we haven't generated the caster methods yet, do that and + # then try again + define_attribute_methods + type_cast_attribute(attr_name, attributes, cache) + else + # If we get here, the attribute has no associated DB column, so + # just return it verbatim. + attributes[attr_name] + end + end + protected + # We want to generate the methods via module_eval rather than define_method, + # because define_method is slower on dispatch and uses more memory (because it + # creates a closure). + # + # But sometimes the database might return columns with characters that are not + # allowed in normal method names (like 'my_column(omg)'. So to work around this + # we first define with the __temp__ identifier, and then use alias method to + # rename it to what we want. def define_method_attribute(attr_name) - if serialized_attributes.include?(attr_name) - define_read_method_for_serialized_attribute(attr_name) - else - define_read_method(attr_name, attr_name, columns_hash[attr_name]) - end + cast_code = attribute_cast_code(attr_name) - if primary_key? && attr_name == primary_key && attr_name != "id" - define_read_method('id', attr_name, columns_hash[attr_name]) - end + generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1 + def __temp__ + #{internal_attribute_access_code(attr_name, cast_code)} + end + alias_method '#{attr_name}', :__temp__ + undef_method :__temp__ + STR + + generated_external_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1 + def __temp__(v, attributes, attributes_cache, attr_name) + #{external_attribute_access_code(attr_name, cast_code)} + end + alias_method '#{attr_name}', :__temp__ + undef_method :__temp__ + STR end private def cacheable_column?(column) - serialized_attributes.include?(column.name) || attribute_types_cached_by_default.include?(column.type) - end - - # Define read method for serialized attribute. - def define_read_method_for_serialized_attribute(attr_name) - access_code = "@attributes_cache['#{attr_name}'] ||= @attributes['#{attr_name}']" - generated_attribute_methods.module_eval("def _#{attr_name}; #{access_code}; end; alias #{attr_name} _#{attr_name}", __FILE__, __LINE__) + attribute_types_cached_by_default.include?(column.type) end - # Define an attribute reader method. Cope with nil column. - # method_name is the same as attr_name except when a non-standard primary key is used, - # we still define #id as an accessor for the key - def define_read_method(method_name, attr_name, column) - cast_code = column.type_cast_code('v') - access_code = "(v=@attributes['#{attr_name}']) && #{cast_code}" + def internal_attribute_access_code(attr_name, cast_code) + access_code = "(v=@attributes[attr_name]) && #{cast_code}" - unless primary_key? && attr_name.to_s == primary_key.to_s - access_code.insert(0, "missing_attribute('#{attr_name}', caller) unless @attributes.has_key?('#{attr_name}'); ") + unless attr_name == primary_key + access_code.insert(0, "missing_attribute(attr_name, caller) unless @attributes.has_key?(attr_name); ") end if cache_attribute?(attr_name) - access_code = "@attributes_cache['#{attr_name}'] ||= (#{access_code})" + access_code = "@attributes_cache[attr_name] ||= (#{access_code})" end - # Where possible, generate the method by evalling a string, as this will result in - # faster accesses because it avoids the block eval and then string eval incurred - # by the second branch. - # - # The second, slower, branch is necessary to support instances where the database - # returns columns with extra stuff in (like 'my_column(omg)'). - if method_name =~ ActiveModel::AttributeMethods::COMPILABLE_REGEXP - generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ - def _#{method_name} - #{access_code} - end - - alias #{method_name} _#{method_name} - STR - else - generated_attribute_methods.module_eval do - define_method("_#{method_name}") { eval(access_code) } - alias_method(method_name, "_#{method_name}") - end - end + "attr_name = '#{attr_name}'; #{access_code}" end - end - # Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (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) - method = "_#{attr_name}" - if respond_to? method - send method if @attributes.has_key?(attr_name.to_s) - else - _read_attribute attr_name - end - end + def external_attribute_access_code(attr_name, cast_code) + access_code = "v && #{cast_code}" - def _read_attribute(attr_name) - attr_name = attr_name.to_s - attr_name = self.class.primary_key? && self.class.primary_key if attr_name == 'id' - value = @attributes[attr_name] - unless value.nil? - if column = column_for_attribute(attr_name) - if unserializable_attribute?(attr_name, column) - unserialize_attribute(attr_name) - else - column.type_cast(value) + if cache_attribute?(attr_name) + access_code = "attributes_cache[attr_name] ||= (#{access_code})" end - else - value + + access_code end - end - end - # Returns true if the attribute is of a text column and marked for serialization. - def unserializable_attribute?(attr_name, column) - column.text? && self.class.serialized_attributes.include?(attr_name) + def attribute_cast_code(attr_name) + columns_hash[attr_name].type_cast_code('v') + end end - # Returns the unserialized object of the attribute. - def unserialize_attribute(attr_name) - coder = self.class.serialized_attributes[attr_name] - unserialized_object = coder.load(@attributes[attr_name]) - - @attributes.frozen? ? unserialized_object : @attributes[attr_name] = unserialized_object + # Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (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) + self.class.type_cast_attribute(attr_name, @attributes, @attributes_cache) end private diff --git a/activerecord/lib/active_record/attribute_methods/serialization.rb b/activerecord/lib/active_record/attribute_methods/serialization.rb new file mode 100644 index 0000000000..0c8e4e4b9a --- /dev/null +++ b/activerecord/lib/active_record/attribute_methods/serialization.rb @@ -0,0 +1,93 @@ +module ActiveRecord + module AttributeMethods + module Serialization + extend ActiveSupport::Concern + + included do + # Returns a hash of all the attributes that have been specified for serialization as + # keys and their class restriction as values. + config_attribute :serialized_attributes + self.serialized_attributes = {} + end + + class Attribute < Struct.new(:coder, :value, :state) + def unserialized_value + state == :serialized ? unserialize : value + end + + def serialized_value + state == :unserialized ? serialize : value + end + + def unserialize + self.state = :unserialized + self.value = coder.load(value) + end + + def serialize + self.state = :serialized + self.value = coder.dump(value) + end + end + + module ClassMethods + # If you have an attribute that needs to be saved to the database as an object, and retrieved as the same object, + # then specify the name of that attribute using this method and it will be handled automatically. + # The serialization is done through YAML. If +class_name+ is specified, the serialized object must be of that + # class on retrieval or SerializationTypeMismatch will be raised. + # + # ==== Parameters + # + # * +attr_name+ - The field name that should be serialized. + # * +class_name+ - Optional, class name that the object type should be equal to. + # + # ==== Example + # # Serialize a preferences attribute + # class User < ActiveRecord::Base + # serialize :preferences + # end + def serialize(attr_name, class_name = Object) + coder = if [:load, :dump].all? { |x| class_name.respond_to?(x) } + class_name + else + Coders::YAMLColumn.new(class_name) + end + + # merge new serialized attribute and create new hash to ensure that each class in inheritance hierarchy + # has its own hash of own serialized attributes + self.serialized_attributes = serialized_attributes.merge(attr_name.to_s => coder) + end + + def initialize_attributes(attributes) #:nodoc: + super + + serialized_attributes.each do |key, coder| + if attributes.key?(key) + attributes[key] = Attribute.new(coder, attributes[key], :serialized) + end + end + + attributes + end + + private + + def attribute_cast_code(attr_name) + if serialized_attributes.include?(attr_name) + "v.unserialized_value" + else + super + end + end + end + + def type_cast_attribute_for_write(column, value) + if column && coder = self.class.serialized_attributes[column.name] + Attribute.new(coder, value, :unserialized) + else + super + end + end + end + end +end diff --git a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb index 62a3cfa9a5..2f86e32f41 100644 --- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb +++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb @@ -7,30 +7,25 @@ module ActiveRecord extend ActiveSupport::Concern included do - cattr_accessor :time_zone_aware_attributes, :instance_writer => false + config_attribute :time_zone_aware_attributes, :global => true self.time_zone_aware_attributes = false - class_attribute :skip_time_zone_conversion_for_attributes, :instance_writer => false + config_attribute :skip_time_zone_conversion_for_attributes self.skip_time_zone_conversion_for_attributes = [] end module ClassMethods protected - # Defined for all +datetime+ and +timestamp+ attributes when +time_zone_aware_attributes+ are enabled. - # This enhanced read method automatically converts the UTC time stored in the database to the time + # The enhanced read method automatically converts the UTC time stored in the database to the time # zone stored in Time.zone. - def define_method_attribute(attr_name) - if create_time_zone_conversion_attribute?(attr_name, columns_hash[attr_name]) - method_body, line = <<-EOV, __LINE__ + 1 - def _#{attr_name} - cached = @attributes_cache['#{attr_name}'] - return cached if cached - time = _read_attribute('#{attr_name}') - @attributes_cache['#{attr_name}'] = time.acts_like?(:time) ? time.in_time_zone : time - end - alias #{attr_name} _#{attr_name} - EOV - generated_attribute_methods.module_eval(method_body, __FILE__, line) + def attribute_cast_code(attr_name) + column = columns_hash[attr_name] + + if create_time_zone_conversion_attribute?(attr_name, column) + typecast = "v = #{super}" + time_zone_conversion = "v.acts_like?(:time) ? v.in_time_zone : v" + + "((#{typecast}) && (#{time_zone_conversion}))" else super end diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb index 4db6d71ba6..fde55b95da 100644 --- a/activerecord/lib/active_record/attribute_methods/write.rb +++ b/activerecord/lib/active_record/attribute_methods/write.rb @@ -10,17 +10,13 @@ module ActiveRecord module ClassMethods protected def define_method_attribute=(attr_name) - if attr_name =~ ActiveModel::AttributeMethods::COMPILABLE_REGEXP + if attr_name =~ ActiveModel::AttributeMethods::NAME_COMPILABLE_REGEXP generated_attribute_methods.module_eval("def #{attr_name}=(new_value); write_attribute('#{attr_name}', new_value); end", __FILE__, __LINE__) else generated_attribute_methods.send(:define_method, "#{attr_name}=") do |new_value| write_attribute(attr_name, new_value) end end - - if primary_key? && attr_name == primary_key && attr_name != "id" - generated_attribute_methods.module_eval("alias :id= :'#{primary_key}='") - end end end @@ -32,10 +28,8 @@ module ActiveRecord @attributes_cache.delete(attr_name) column = column_for_attribute(attr_name) - if column && column.number? - @attributes[attr_name] = convert_number_column_value(value) - elsif column || @attributes.has_key?(attr_name) - @attributes[attr_name] = value + if column || @attributes.has_key?(attr_name) + @attributes[attr_name] = type_cast_attribute_for_write(column, value) else raise ActiveModel::MissingAttributeError, "can't write unknown attribute `#{attr_name}'" end @@ -47,6 +41,26 @@ module ActiveRecord def attribute=(attribute_name, value) write_attribute(attribute_name, value) end + + def type_cast_attribute_for_write(column, value) + if column && column.number? + convert_number_column_value(value) + else + value + end + end + + def convert_number_column_value(value) + if value == false + 0 + elsif value == true + 1 + elsif value.is_a?(String) && value.blank? + nil + else + value + end + end end end end diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index 056170d82a..c86eaba498 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -21,6 +21,21 @@ module ActiveRecord # Note that <tt>:autosave => false</tt> is not same as not declaring <tt>:autosave</tt>. # When the <tt>:autosave</tt> option is not present new associations are saved. # + # == Validation + # + # Children records are validated unless <tt>:validate</tt> is +false+. + # + # == Callbacks + # + # Association with autosave option defines several callbacks on your + # model (before_save, after_create, after_update). Please note that + # callbacks are executed in the order they were defined in + # model. You should avoid modyfing the association content, before + # autosave callbacks are executed. Placing your callbacks after + # associations is usually a good practice. + # + # == Examples + # # === One-to-one Example # # class Post @@ -109,10 +124,7 @@ module ActiveRecord # Now it _is_ removed from the database: # # Comment.find_by_id(id).nil? # => true - # - # === Validation - # - # Children records are validated unless <tt>:validate</tt> is +false+. + module AutosaveAssociation extend ActiveSupport::Concern @@ -264,7 +276,7 @@ module ActiveRecord # turned on for the association. def validate_single_association(reflection) association = association_instance_get(reflection.name) - record = association && association.target + record = association && association.reader association_valid?(reflection, record) if record end @@ -331,7 +343,7 @@ module ActiveRecord if autosave saved = association.insert_record(record, false) else - association.insert_record(record) + association.insert_record(record) unless reflection.nested? end elsif autosave saved = record.save(:validate => false) diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 137b4c6534..d29cf82dad 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -23,9 +23,11 @@ require 'active_support/core_ext/module/delegation' require 'active_support/core_ext/module/introspection' require 'active_support/core_ext/object/duplicable' require 'active_support/core_ext/object/blank' +require 'active_support/deprecation' require 'arel' require 'active_record/errors' require 'active_record/log_subscriber' +require 'active_record/explain_subscriber' module ActiveRecord #:nodoc: # = Active Record @@ -115,8 +117,8 @@ module ActiveRecord #:nodoc: # When joining tables, nested hashes or keys written in the form 'table_name.column_name' # can be used to qualify the table name of a particular condition. For instance: # - # Student.joins(:schools).where(:schools => { :type => 'public' }) - # Student.joins(:schools).where('schools.type' => 'public' ) + # Student.joins(:schools).where(:schools => { :category => 'public' }) + # Student.joins(:schools).where('schools.category' => 'public' ) # # == Overwriting default accessors # @@ -325,1843 +327,8 @@ module ActiveRecord #:nodoc: # So it's possible to assign a logger to the class through <tt>Base.logger=</tt> which will then be used by all # instances in the current object space. class Base - ## - # :singleton-method: - # 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, :instance_writer => false - - ## - # :singleton-method: - # Contains the database configuration - as is typically stored in config/database.yml - - # as a Hash. - # - # For example, the following database.yml... - # - # development: - # adapter: sqlite3 - # database: db/development.sqlite3 - # - # production: - # adapter: sqlite3 - # database: db/production.sqlite3 - # - # ...would result in ActiveRecord::Base.configurations to look like this: - # - # { - # 'development' => { - # 'adapter' => 'sqlite3', - # 'database' => 'db/development.sqlite3' - # }, - # 'production' => { - # 'adapter' => 'sqlite3', - # 'database' => 'db/production.sqlite3' - # } - # } - cattr_accessor :configurations, :instance_writer => false - @@configurations = {} - - ## - # :singleton-method: - # 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, :instance_writer => false - @@primary_key_prefix_type = nil - - ## - # :singleton-method: - # 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 convenient way of creating a namespace for tables in a shared database. - # By default, the prefix is the empty string. - # - # If you are organising your models within modules you can add a prefix to the models within - # a namespace by defining a singleton method in the parent module called table_name_prefix which - # returns your chosen prefix. - class_attribute :table_name_prefix, :instance_writer => false - self.table_name_prefix = "" - - ## - # :singleton-method: - # 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. - class_attribute :table_name_suffix, :instance_writer => false - self.table_name_suffix = "" - - ## - # :singleton-method: - # Indicates whether table names should be the pluralized versions of the corresponding class names. - # If true, 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. - class_attribute :pluralize_table_names, :instance_writer => false - self.pluralize_table_names = true - - ## - # :singleton-method: - # Determines whether to use Time.local (using :local) or Time.utc (using :utc) when pulling - # dates and times from the database. This is set to :local by default. - cattr_accessor :default_timezone, :instance_writer => false - @@default_timezone = :local - - ## - # :singleton-method: - # Specifies the format to use when dumping the database schema with Rails' - # Rakefile. If :sql, the schema is dumped as (potentially database- - # specific) SQL statements. If :ruby, the schema is dumped as an - # ActiveRecord::Schema file which can be loaded into any database that - # supports migrations. Use :ruby if you want to have different database - # adapters for, e.g., your development and test environments. - cattr_accessor :schema_format , :instance_writer => false - @@schema_format = :ruby - - ## - # :singleton-method: - # Specify whether or not to use timestamps for migration versions - cattr_accessor :timestamped_migrations , :instance_writer => false - @@timestamped_migrations = true - - # Determine whether to store the full constant name including namespace when using STI - class_attribute :store_full_sti_class - self.store_full_sti_class = true - - # Stores the default scope for the class - class_attribute :default_scopes, :instance_writer => false - self.default_scopes = [] - - # Returns a hash of all the attributes that have been specified for serialization as - # keys and their class restriction as values. - class_attribute :serialized_attributes - self.serialized_attributes = {} - - class_attribute :_attr_readonly, :instance_writer => false - self._attr_readonly = [] - - class << self # Class methods - delegate :find, :first, :first!, :last, :last!, :all, :exists?, :any?, :many?, :to => :scoped - delegate :first_or_create, :first_or_create!, :first_or_initialize, :to => :scoped - delegate :destroy, :destroy_all, :delete, :delete_all, :update, :update_all, :to => :scoped - delegate :find_each, :find_in_batches, :to => :scoped - delegate :select, :group, :order, :except, :reorder, :limit, :offset, :joins, :where, :preload, :eager_load, :includes, :from, :lock, :readonly, :having, :create_with, :to => :scoped - delegate :count, :average, :minimum, :maximum, :sum, :calculate, :to => :scoped - - # Executes a custom SQL query against your database and returns all the results. The results will - # be returned as an array with columns requested encapsulated as attributes of the model you call - # this method from. If you call <tt>Product.find_by_sql</tt> then the results will be returned in - # a Product object with the attributes you specified in the SQL query. - # - # If you call a complicated SQL query which spans multiple tables the columns specified by the - # SELECT will be attributes of the model, whether or not they are columns of the corresponding - # table. - # - # The +sql+ parameter is a full SQL query as a string. It will be called as is, there will be - # no database agnostic conversions performed. This should be a last resort because using, for example, - # MySQL specific terms will lock you to using that particular database engine or require you to - # change your call if you switch engines. - # - # ==== Examples - # # A simple SQL query spanning multiple tables - # Post.find_by_sql "SELECT p.title, c.author FROM posts p, comments c WHERE p.id = c.post_id" - # > [#<Post:0x36bff9c @attributes={"title"=>"Ruby Meetup", "first_name"=>"Quentin"}>, ...] - # - # # You can use the same string replacement techniques as you can with ActiveRecord#find - # Post.find_by_sql ["SELECT title FROM posts WHERE author = ? AND created > ?", author_id, start_date] - # > [#<Post:0x36bff9c @attributes={"title"=>"The Cheap Man Buys Twice"}>, ...] - def find_by_sql(sql, binds = []) - connection.select_all(sanitize_sql(sql), "#{name} Load", binds).collect! { |record| instantiate(record) } - end - - # Creates an object (or multiple objects) and saves it to the database, if validations pass. - # The resulting object is returned whether the object was saved successfully to the database or not. - # - # The +attributes+ parameter can be either be a Hash or an Array of Hashes. These Hashes describe the - # attributes on the objects that are to be created. - # - # +create+ respects mass-assignment security and accepts either +:as+ or +:without_protection+ options - # in the +options+ parameter. - # - # ==== Examples - # # Create a single new object - # User.create(:first_name => 'Jamie') - # - # # Create a single new object using the :admin mass-assignment security role - # User.create({ :first_name => 'Jamie', :is_admin => true }, :as => :admin) - # - # # Create a single new object bypassing mass-assignment security - # User.create({ :first_name => 'Jamie', :is_admin => true }, :without_protection => true) - # - # # Create an Array of new objects - # User.create([{ :first_name => 'Jamie' }, { :first_name => 'Jeremy' }]) - # - # # Create a single object and pass it into a block to set other attributes. - # User.create(:first_name => 'Jamie') do |u| - # u.is_admin = false - # end - # - # # Creating an Array of new objects using a block, where the block is executed for each object: - # User.create([{ :first_name => 'Jamie' }, { :first_name => 'Jeremy' }]) do |u| - # u.is_admin = false - # end - def create(attributes = nil, options = {}, &block) - if attributes.is_a?(Array) - attributes.collect { |attr| create(attr, options, &block) } - else - object = new(attributes, options, &block) - object.save - object - end - end - - # Returns the result of an SQL statement that should only include a COUNT(*) in the SELECT part. - # The use of this method should be restricted to complicated SQL queries that can't be executed - # using the ActiveRecord::Calculations class methods. Look into those before using this. - # - # ==== Parameters - # - # * +sql+ - An SQL statement which should return a count query from the database, see the example below. - # - # ==== Examples - # - # Product.count_by_sql "SELECT COUNT(*) FROM sales s, customers c WHERE s.customer_id = c.id" - def count_by_sql(sql) - sql = sanitize_conditions(sql) - connection.select_value(sql, "#{name} Count").to_i - end - - # Attributes listed as readonly will be used to create a new record but update operations will - # ignore these fields. - def attr_readonly(*attributes) - self._attr_readonly = Set.new(attributes.map { |a| a.to_s }) + (self._attr_readonly || []) - end - - # Returns an array of all the attributes that have been specified as readonly. - def readonly_attributes - self._attr_readonly - end - - # If you have an attribute that needs to be saved to the database as an object, and retrieved as the same object, - # then specify the name of that attribute using this method and it will be handled automatically. - # The serialization is done through YAML. If +class_name+ is specified, the serialized object must be of that - # class on retrieval or SerializationTypeMismatch will be raised. - # - # ==== Parameters - # - # * +attr_name+ - The field name that should be serialized. - # * +class_name+ - Optional, class name that the object type should be equal to. - # - # ==== Example - # # Serialize a preferences attribute - # class User < ActiveRecord::Base - # serialize :preferences - # end - def serialize(attr_name, class_name = Object) - coder = if [:load, :dump].all? { |x| class_name.respond_to?(x) } - class_name - else - Coders::YAMLColumn.new(class_name) - end - - # merge new serialized attribute and create new hash to ensure that each class in inheritance hierarchy - # has its own hash of own serialized attributes - self.serialized_attributes = serialized_attributes.merge(attr_name.to_s => coder) - end - - # Guesses the table name (in forced lower-case) based on the name of the class in the - # inheritance hierarchy descending directly from ActiveRecord::Base. So if the hierarchy - # looks like: Reply < Message < ActiveRecord::Base, then Message is used - # to guess the table name even when called on Reply. The rules used to do the guess - # are handled by the Inflector class in Active Support, which knows almost all common - # English inflections. You can add new inflections in config/initializers/inflections.rb. - # - # Nested classes are given table names prefixed by the singular form of - # the parent's table name. Enclosing modules are not considered. - # - # ==== Examples - # - # class Invoice < ActiveRecord::Base - # end - # - # file class table_name - # invoice.rb Invoice invoices - # - # class Invoice < ActiveRecord::Base - # class Lineitem < ActiveRecord::Base - # end - # end - # - # file class table_name - # invoice.rb Invoice::Lineitem invoice_lineitems - # - # module Invoice - # class Lineitem < ActiveRecord::Base - # end - # end - # - # file class table_name - # invoice/lineitem.rb Invoice::Lineitem lineitems - # - # Additionally, the class-level +table_name_prefix+ is prepended and the - # +table_name_suffix+ is appended. So if you have "myapp_" as a prefix, - # the table name guess for an Invoice class becomes "myapp_invoices". - # Invoice::Lineitem becomes "myapp_invoice_lineitems". - # - # 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 - # set_table_name "mice" - # end - def table_name - reset_table_name - end - - # Returns a quoted version of the table name, used to construct SQL statements. - def quoted_table_name - @quoted_table_name ||= connection.quote_table_name(table_name) - end - - # Computes the table name, (re)sets it internally, and returns it. - def reset_table_name #:nodoc: - return if abstract_class? - - self.table_name = compute_table_name - end - - def full_table_name_prefix #:nodoc: - (parents.detect{ |p| p.respond_to?(:table_name_prefix) } || self).table_name_prefix - end - - # Defines the column name for use with single table inheritance. Use - # <tt>set_inheritance_column</tt> to set a different value. - def inheritance_column - @inheritance_column ||= "type" - end - - # Lazy-set the sequence name to the connection's default. This method - # is only ever called once since set_sequence_name overrides it. - def sequence_name #:nodoc: - reset_sequence_name - end - - def reset_sequence_name #:nodoc: - default = connection.default_sequence_name(table_name, primary_key) - set_sequence_name(default) - default - end - - # Sets the table name. If the value is nil or false then the value returned by the given - # block is used. - # - # class Project < ActiveRecord::Base - # set_table_name "project" - # end - def set_table_name(value = nil, &block) - @quoted_table_name = nil - define_attr_method :table_name, value, &block - @arel_table = nil - - @relation = Relation.new(self, arel_table) - end - alias :table_name= :set_table_name - - # Sets the name of the inheritance column to use to the given value, - # or (if the value # is nil or false) to the value returned by the - # given block. - # - # class Project < ActiveRecord::Base - # set_inheritance_column do - # original_inheritance_column + "_id" - # end - # end - def set_inheritance_column(value = nil, &block) - define_attr_method :inheritance_column, value, &block - end - alias :inheritance_column= :set_inheritance_column - - # Sets the name of the sequence to use when generating ids to the given - # value, or (if the value is nil or false) to the value returned by the - # given block. This is required for Oracle and is useful for any - # database which relies on sequences for primary key generation. - # - # If a sequence name is not explicitly set when using Oracle or Firebird, - # it will default to the commonly used pattern of: #{table_name}_seq - # - # If a sequence name is not explicitly set when using PostgreSQL, it - # will discover the sequence corresponding to your primary key for you. - # - # class Project < ActiveRecord::Base - # set_sequence_name "projectseq" # default would have been "project_seq" - # end - def set_sequence_name(value = nil, &block) - define_attr_method :sequence_name, value, &block - end - alias :sequence_name= :set_sequence_name - - # Indicates whether the table associated with this class exists - def table_exists? - connection.table_exists?(table_name) - end - - # Returns an array of column objects for the table associated with this class. - def columns - if defined?(@primary_key) - connection_pool.primary_keys[table_name] ||= @primary_key - end - - connection_pool.columns[table_name] - end - - # Returns a hash of column objects for the table associated with this class. - def columns_hash - connection_pool.columns_hash[table_name] - end - - # Returns a hash where the keys are column names and the values are - # default values when instantiating the AR object for this table. - def column_defaults - connection_pool.column_defaults[table_name] - end - - # Returns an array of column names as strings. - def column_names - @column_names ||= columns.map { |column| column.name } - end - - # Returns an array of column objects where the primary id, all columns ending in "_id" or "_count", - # and columns used for single table inheritance have been removed. - def content_columns - @content_columns ||= columns.reject { |c| c.primary || 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 #:nodoc: - @dynamic_methods_hash ||= column_names.inject(Hash.new(false)) do |methods, attr| - attr_name = attr.to_s - methods[attr.to_sym] = attr_name - methods["#{attr}=".to_sym] = attr_name - methods["#{attr}?".to_sym] = attr_name - methods["#{attr}_before_type_cast".to_sym] = attr_name - methods - end - end - - # Resets all the cached information about columns, which will cause them - # to be reloaded on the next request. - # - # The most common usage pattern for this method is probably in a migration, - # when just after creating a table you want to populate it with some default - # values, eg: - # - # class CreateJobLevels < ActiveRecord::Migration - # def self.up - # create_table :job_levels do |t| - # t.integer :id - # t.string :name - # - # t.timestamps - # end - # - # JobLevel.reset_column_information - # %w{assistant executive manager director}.each do |type| - # JobLevel.create(:name => type) - # end - # end - # - # def self.down - # drop_table :job_levels - # end - # end - def reset_column_information - connection.clear_cache! - undefine_attribute_methods - connection_pool.clear_table_cache!(table_name) if table_exists? - - @column_names = @content_columns = @dynamic_methods_hash = @inheritance_column = nil - @arel_engine = @relation = nil - end - - def clear_cache! # :nodoc: - connection_pool.clear_cache! - end - - def attribute_method?(attribute) - super || (table_exists? && column_names.include?(attribute.to_s.sub(/=$/, ''))) - end - - # Returns an array of column names as strings if it's not - # an abstract class and table exists. - # Otherwise it returns an empty array. - def attribute_names - @attribute_names ||= if !abstract_class? && table_exists? - column_names - else - [] - end - end - - # Set the lookup ancestors for ActiveModel. - def lookup_ancestors #:nodoc: - klass = self - classes = [klass] - return classes if klass == ActiveRecord::Base - - while klass != klass.base_class - classes << klass = klass.superclass - end - classes - end - - # Set the i18n scope to overwrite ActiveModel. - def i18n_scope #:nodoc: - :activerecord - end - - # True if this isn't a concrete subclass needing a STI type condition. - def descends_from_active_record? - if superclass.abstract_class? - superclass.descends_from_active_record? - else - superclass == Base || !columns_hash.include?(inheritance_column) - end - end - - def finder_needs_type_condition? #:nodoc: - # This is like this because benchmarking justifies the strange :false stuff - :true == (@finder_needs_type_condition ||= descends_from_active_record? ? :false : :true) - end - - # Returns a string like 'Post(id:integer, title:string, body:text)' - def inspect - if self == Base - super - elsif abstract_class? - "#{super}(abstract)" - elsif table_exists? - attr_list = columns.map { |c| "#{c.name}: #{c.type}" } * ', ' - "#{super}(#{attr_list})" - else - "#{super}(Table doesn't exist)" - end - end - - def quote_value(value, column = nil) #:nodoc: - connection.quote(value,column) - end - - # Used to sanitize objects before they're used in an SQL SELECT statement. Delegates to <tt>connection.quote</tt>. - def sanitize(object) #:nodoc: - connection.quote(object) - end - - # Overwrite the default class equality method to provide support for association proxies. - def ===(object) - object.is_a?(self) - end - - def symbolized_base_class - @symbolized_base_class ||= base_class.to_s.to_sym - end - - def symbolized_sti_name - @symbolized_sti_name ||= sti_name.present? ? sti_name.to_sym : symbolized_base_class - end - - # Returns the base AR subclass that this class descends from. If A - # extends AR::Base, A.base_class will return A. If B descends from A - # through some arbitrarily deep hierarchy, B.base_class will return A. - # - # If B < A and C < B and if A is an abstract_class then both B.base_class - # and C.base_class would return B as the answer since A is an abstract_class. - def base_class - class_of_active_record_descendant(self) - end - - # Set this to true if this is an abstract class (see <tt>abstract_class?</tt>). - attr_accessor :abstract_class - - # Returns whether this class is an abstract class or not. - def abstract_class? - defined?(@abstract_class) && @abstract_class == true - end - - def respond_to?(method_id, include_private = false) - if match = DynamicFinderMatch.match(method_id) - return true if all_attributes_exists?(match.attribute_names) - elsif match = DynamicScopeMatch.match(method_id) - return true if all_attributes_exists?(match.attribute_names) - end - - super - end - - def sti_name - store_full_sti_class ? name : name.demodulize - end - - def arel_table - @arel_table ||= Arel::Table.new(table_name, arel_engine) - end - - def arel_engine - @arel_engine ||= begin - if self == ActiveRecord::Base - ActiveRecord::Base - else - connection_handler.connection_pools[name] ? self : superclass.arel_engine - end - end - end - - # Returns a scope for this class without taking into account the default_scope. - # - # class Post < ActiveRecord::Base - # def self.default_scope - # where :published => true - # end - # end - # - # Post.all # Fires "SELECT * FROM posts WHERE published = true" - # Post.unscoped.all # Fires "SELECT * FROM posts" - # - # This method also accepts a block meaning that all queries inside the block will - # not use the default_scope: - # - # Post.unscoped { - # Post.limit(10) # Fires "SELECT * FROM posts LIMIT 10" - # } - # - # It is recommended to use block form of unscoped because chaining unscoped with <tt>scope</tt> - # does not work. Assuming that <tt>published</tt> is a <tt>scope</tt> following two statements are same. - # - # Post.unscoped.published - # Post.published - def unscoped #:nodoc: - block_given? ? relation.scoping { yield } : relation - end - - def before_remove_const #:nodoc: - self.current_scope = nil - end - - # 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) - sti_class = find_sti_class(record[inheritance_column]) - record_id = sti_class.primary_key? && record[sti_class.primary_key] - - if ActiveRecord::IdentityMap.enabled? && record_id - if (column = sti_class.columns_hash[sti_class.primary_key]) && column.number? - record_id = record_id.to_i - end - if instance = IdentityMap.get(sti_class, record_id) - instance.reinit_with('attributes' => record) - else - instance = sti_class.allocate.init_with('attributes' => record) - IdentityMap.add(instance) - end - else - instance = sti_class.allocate.init_with('attributes' => record) - end - - instance - end - - private - - def relation #:nodoc: - @relation ||= Relation.new(self, arel_table) - - if finder_needs_type_condition? - @relation.where(type_condition).create_with(inheritance_column.to_sym => sti_name) - else - @relation - end - end - - def find_sti_class(type_name) - if type_name.blank? || !columns_hash.include?(inheritance_column) - self - else - begin - if store_full_sti_class - ActiveSupport::Dependencies.constantize(type_name) - else - compute_type(type_name) - end - rescue NameError - raise SubclassNotFound, - "The single-table inheritance mechanism failed to locate the subclass: '#{type_name}'. " + - "This error is raised because the column '#{inheritance_column}' is reserved for storing the class in case of inheritance. " + - "Please rename this column if you didn't intend it to be used for storing the inheritance class " + - "or overwrite #{name}.inheritance_column to use another column for that information." - end - end - end - - def construct_finder_arel(options = {}, scope = nil) - relation = options.is_a?(Hash) ? unscoped.apply_finder_options(options) : options - relation = scope.merge(relation) if scope - relation - end - - def type_condition(table = arel_table) - sti_column = table[inheritance_column.to_sym] - sti_names = ([self] + descendants).map { |model| model.sti_name } - - sti_column.in(sti_names) - end - - # Guesses the table name, but does not decorate it with prefix and suffix information. - def undecorated_table_name(class_name = base_class.name) - table_name = class_name.to_s.demodulize.underscore - table_name = table_name.pluralize if pluralize_table_names - table_name - end - - # Computes and returns a table name according to default conventions. - def compute_table_name - base = base_class - if self == base - # Nested classes are prefixed with singular parent table name. - if parent < ActiveRecord::Base && !parent.abstract_class? - contained = parent.table_name - contained = contained.singularize if parent.pluralize_table_names - contained += '_' - end - "#{full_table_name_prefix}#{contained}#{undecorated_table_name(name)}#{table_name_suffix}" - else - # STI subclasses always use their superclass' table. - base.table_name - end - end - - # Enables dynamic finders like <tt>User.find_by_user_name(user_name)</tt> and - # <tt>User.scoped_by_user_name(user_name). Refer to Dynamic attribute-based finders - # section at the top of this file for more detailed information. - # - # It's even possible to use all the additional parameters to +find+. For example, the - # full interface for +find_all_by_amount+ is actually <tt>find_all_by_amount(amount, options)</tt>. - # - # Each dynamic finder using <tt>scoped_by_*</tt> is also defined in the class after it - # is first invoked, so that future attempts to use it do not run through method_missing. - def method_missing(method_id, *arguments, &block) - if match = (DynamicFinderMatch.match(method_id) || DynamicScopeMatch.match(method_id)) - attribute_names = match.attribute_names - super unless all_attributes_exists?(attribute_names) - if arguments.size < attribute_names.size - method_trace = "#{__FILE__}:#{__LINE__}:in `#{method_id}'" - backtrace = [method_trace] + caller - raise ArgumentError, "wrong number of arguments (#{arguments.size} for #{attribute_names.size})", backtrace - end - if match.respond_to?(:scope?) && match.scope? - self.class_eval <<-METHOD, __FILE__, __LINE__ + 1 - def self.#{method_id}(*args) # def self.scoped_by_user_name_and_password(*args) - attributes = Hash[[:#{attribute_names.join(',:')}].zip(args)] # attributes = Hash[[:user_name, :password].zip(args)] - # - scoped(:conditions => attributes) # scoped(:conditions => attributes) - end # end - METHOD - send(method_id, *arguments) - elsif match.finder? - options = arguments.extract_options! - relation = options.any? ? scoped(options) : scoped - relation.send :find_by_attributes, match, attribute_names, *arguments, &block - elsif match.instantiator? - scoped.send :find_or_instantiator_by_attributes, match, attribute_names, *arguments, &block - end - else - super - end - end - - # Similar in purpose to +expand_hash_conditions_for_aggregates+. - def expand_attribute_names_for_aggregates(attribute_names) - attribute_names.map { |attribute_name| - unless (aggregation = reflect_on_aggregation(attribute_name.to_sym)).nil? - aggregate_mapping(aggregation).map do |field_attr, _| - field_attr.to_sym - end - else - attribute_name.to_sym - end - }.flatten - end - - def all_attributes_exists?(attribute_names) - (expand_attribute_names_for_aggregates(attribute_names) - - column_methods_hash.keys).empty? - end - - protected - # with_scope lets you apply options to inner block incrementally. It takes a hash and the keys must be - # <tt>:find</tt> or <tt>:create</tt>. <tt>:find</tt> parameter is <tt>Relation</tt> while - # <tt>:create</tt> parameters are an attributes hash. - # - # class Article < ActiveRecord::Base - # def self.create_with_scope - # with_scope(:find => where(:blog_id => 1), :create => { :blog_id => 1 }) do - # find(1) # => SELECT * from articles WHERE blog_id = 1 AND id = 1 - # a = create(1) - # a.blog_id # => 1 - # end - # end - # end - # - # In nested scopings, all previous parameters are overwritten by the innermost rule, with the exception of - # <tt>where</tt>, <tt>includes</tt>, and <tt>joins</tt> operations in <tt>Relation</tt>, which are merged. - # - # <tt>joins</tt> operations are uniqued so multiple scopes can join in the same table without table aliasing - # problems. If you need to join multiple tables, but still want one of the tables to be uniqued, use the - # array of strings format for your joins. - # - # class Article < ActiveRecord::Base - # def self.find_with_scope - # with_scope(:find => where(:blog_id => 1).limit(1), :create => { :blog_id => 1 }) do - # with_scope(:find => limit(10)) do - # all # => SELECT * from articles WHERE blog_id = 1 LIMIT 10 - # end - # with_scope(:find => where(:author_id => 3)) do - # all # => SELECT * from articles WHERE blog_id = 1 AND author_id = 3 LIMIT 1 - # end - # end - # end - # end - # - # You can ignore any previous scopings by using the <tt>with_exclusive_scope</tt> method. - # - # class Article < ActiveRecord::Base - # def self.find_with_exclusive_scope - # with_scope(:find => where(:blog_id => 1).limit(1)) do - # with_exclusive_scope(:find => limit(10)) do - # all # => SELECT * from articles LIMIT 10 - # end - # end - # end - # end - # - # *Note*: the +:find+ scope also has effect on update and deletion methods, like +update_all+ and +delete_all+. - def with_scope(scope = {}, action = :merge, &block) - # If another Active Record class has been passed in, get its current scope - scope = scope.current_scope if !scope.is_a?(Relation) && scope.respond_to?(:current_scope) - - previous_scope = self.current_scope - - if scope.is_a?(Hash) - # Dup first and second level of hash (method and params). - scope = scope.dup - scope.each do |method, params| - scope[method] = params.dup unless params == true - end - - scope.assert_valid_keys([ :find, :create ]) - relation = construct_finder_arel(scope[:find] || {}) - relation.default_scoped = true unless action == :overwrite - - if previous_scope && previous_scope.create_with_value && scope[:create] - scope_for_create = if action == :merge - previous_scope.create_with_value.merge(scope[:create]) - else - scope[:create] - end - - relation = relation.create_with(scope_for_create) - else - scope_for_create = scope[:create] - scope_for_create ||= previous_scope.create_with_value if previous_scope - relation = relation.create_with(scope_for_create) if scope_for_create - end - - scope = relation - end - - scope = previous_scope.merge(scope) if previous_scope && action == :merge - - self.current_scope = scope - begin - yield - ensure - self.current_scope = previous_scope - end - end - - # Works like with_scope, but discards any nested properties. - def with_exclusive_scope(method_scoping = {}, &block) - if method_scoping.values.any? { |e| e.is_a?(ActiveRecord::Relation) } - raise ArgumentError, <<-MSG -New finder API can not be used with_exclusive_scope. You can either call unscoped to get an anonymous scope not bound to the default_scope: - - User.unscoped.where(:active => true) - -Or call unscoped with a block: - - User.unscoped do - User.where(:active => true).all - end - -MSG - end - with_scope(method_scoping, :overwrite, &block) - end - - def current_scope #:nodoc: - Thread.current["#{self}_current_scope"] - end - - def current_scope=(scope) #:nodoc: - Thread.current["#{self}_current_scope"] = scope - end - - # Use this macro in your model to set a default scope for all operations on - # the model. - # - # class Article < ActiveRecord::Base - # default_scope where(:published => true) - # end - # - # Article.all # => SELECT * FROM articles WHERE published = true - # - # The <tt>default_scope</tt> is also applied while creating/building a record. It is not - # applied while updating a record. - # - # Article.new.published # => true - # Article.create.published # => true - # - # You can also use <tt>default_scope</tt> with a block, in order to have it lazily evaluated: - # - # class Article < ActiveRecord::Base - # default_scope { where(:published_at => Time.now - 1.week) } - # end - # - # (You can also pass any object which responds to <tt>call</tt> to the <tt>default_scope</tt> - # macro, and it will be called when building the default scope.) - # - # If you use multiple <tt>default_scope</tt> declarations in your model then they will - # be merged together: - # - # class Article < ActiveRecord::Base - # default_scope where(:published => true) - # default_scope where(:rating => 'G') - # end - # - # Article.all # => SELECT * FROM articles WHERE published = true AND rating = 'G' - # - # This is also the case with inheritance and module includes where the parent or module - # defines a <tt>default_scope</tt> and the child or including class defines a second one. - # - # If you need to do more complex things with a default scope, you can alternatively - # define it as a class method: - # - # class Article < ActiveRecord::Base - # def self.default_scope - # # Should return a scope, you can call 'super' here etc. - # end - # end - def default_scope(scope = {}) - scope = Proc.new if block_given? - self.default_scopes = default_scopes + [scope] - end - - def build_default_scope #:nodoc: - if method(:default_scope).owner != Base.singleton_class - evaluate_default_scope { default_scope } - elsif default_scopes.any? - evaluate_default_scope do - default_scopes.inject(relation) do |default_scope, scope| - if scope.is_a?(Hash) - default_scope.apply_finder_options(scope) - elsif !scope.is_a?(Relation) && scope.respond_to?(:call) - default_scope.merge(scope.call) - else - default_scope.merge(scope) - end - end - end - end - end - - def ignore_default_scope? #:nodoc: - Thread.current["#{self}_ignore_default_scope"] - end - - def ignore_default_scope=(ignore) #:nodoc: - Thread.current["#{self}_ignore_default_scope"] = ignore - end - - # The ignore_default_scope flag is used to prevent an infinite recursion situation where - # a default scope references a scope which has a default scope which references a scope... - def evaluate_default_scope - return if ignore_default_scope? - - begin - self.ignore_default_scope = true - yield - ensure - self.ignore_default_scope = false - end - end - - # Returns the class type of the record using the current module as a prefix. So descendants of - # MyApp::Business::Account would appear as MyApp::Business::AccountSubclass. - def compute_type(type_name) - if type_name.match(/^::/) - # If the type is prefixed with a scope operator then we assume that - # the type_name is an absolute reference. - ActiveSupport::Dependencies.constantize(type_name) - else - # Build a list of candidates to search for - candidates = [] - name.scan(/::|$/) { candidates.unshift "#{$`}::#{type_name}" } - candidates << type_name - - candidates.each do |candidate| - begin - constant = ActiveSupport::Dependencies.constantize(candidate) - return constant if candidate == constant.to_s - rescue NameError => e - # We don't want to swallow NoMethodError < NameError errors - raise e unless e.instance_of?(NameError) - end - end - - raise NameError, "uninitialized constant #{candidates.first}" - end - end - - # Returns the class descending directly from ActiveRecord::Base or an - # abstract class, if any, in the inheritance hierarchy. - def class_of_active_record_descendant(klass) - if klass == Base || klass.superclass == Base || klass.superclass.abstract_class? - klass - elsif klass.superclass.nil? - raise ActiveRecordError, "#{name} doesn't belong in a hierarchy descending from ActiveRecord" - else - class_of_active_record_descendant(klass.superclass) - end - end - - # Accepts an array, hash, or string of SQL conditions and sanitizes - # them into a valid SQL fragment for a WHERE clause. - # ["name='%s' and group_id='%s'", "foo'bar", 4] returns "name='foo''bar' and group_id='4'" - # { :name => "foo'bar", :group_id => 4 } returns "name='foo''bar' and group_id='4'" - # "name='foo''bar' and group_id='4'" returns "name='foo''bar' and group_id='4'" - def sanitize_sql_for_conditions(condition, table_name = self.table_name) - return nil if condition.blank? - - case condition - when Array; sanitize_sql_array(condition) - when Hash; sanitize_sql_hash_for_conditions(condition, table_name) - else condition - end - end - alias_method :sanitize_sql, :sanitize_sql_for_conditions - - # Accepts an array, hash, or string of SQL conditions and sanitizes - # them into a valid SQL fragment for a SET clause. - # { :name => nil, :group_id => 4 } returns "name = NULL , group_id='4'" - def sanitize_sql_for_assignment(assignments) - case assignments - when Array; sanitize_sql_array(assignments) - when Hash; sanitize_sql_hash_for_assignment(assignments) - else assignments - end - end - - def aggregate_mapping(reflection) - mapping = reflection.options[:mapping] || [reflection.name, reflection.name] - mapping.first.is_a?(Array) ? mapping : [mapping] - end - - # Accepts a hash of SQL conditions and replaces those attributes - # that correspond to a +composed_of+ relationship with their expanded - # aggregate attribute values. - # Given: - # class Person < ActiveRecord::Base - # composed_of :address, :class_name => "Address", - # :mapping => [%w(address_street street), %w(address_city city)] - # end - # Then: - # { :address => Address.new("813 abc st.", "chicago") } - # # => { :address_street => "813 abc st.", :address_city => "chicago" } - def expand_hash_conditions_for_aggregates(attrs) - expanded_attrs = {} - attrs.each do |attr, value| - unless (aggregation = reflect_on_aggregation(attr.to_sym)).nil? - mapping = aggregate_mapping(aggregation) - mapping.each do |field_attr, aggregate_attr| - if mapping.size == 1 && !value.respond_to?(aggregate_attr) - expanded_attrs[field_attr] = value - else - expanded_attrs[field_attr] = value.send(aggregate_attr) - end - end - else - expanded_attrs[attr] = value - end - end - expanded_attrs - end - - # Sanitizes a hash of attribute/value pairs into SQL conditions for a WHERE clause. - # { :name => "foo'bar", :group_id => 4 } - # # => "name='foo''bar' and group_id= 4" - # { :status => nil, :group_id => [1,2,3] } - # # => "status IS NULL and group_id IN (1,2,3)" - # { :age => 13..18 } - # # => "age BETWEEN 13 AND 18" - # { 'other_records.id' => 7 } - # # => "`other_records`.`id` = 7" - # { :other_records => { :id => 7 } } - # # => "`other_records`.`id` = 7" - # And for value objects on a composed_of relationship: - # { :address => Address.new("123 abc st.", "chicago") } - # # => "address_street='123 abc st.' and address_city='chicago'" - def sanitize_sql_hash_for_conditions(attrs, default_table_name = self.table_name) - attrs = expand_hash_conditions_for_aggregates(attrs) - - table = Arel::Table.new(table_name).alias(default_table_name) - PredicateBuilder.build_from_hash(arel_engine, attrs, table).map { |b| - connection.visitor.accept b - }.join(' AND ') - end - alias_method :sanitize_sql_hash, :sanitize_sql_hash_for_conditions - - # Sanitizes a hash of attribute/value pairs into SQL conditions for a SET clause. - # { :status => nil, :group_id => 1 } - # # => "status = NULL , group_id = 1" - def sanitize_sql_hash_for_assignment(attrs) - attrs.map do |attr, value| - "#{connection.quote_column_name(attr)} = #{quote_bound_value(value)}" - end.join(', ') - end - - # Accepts an array of conditions. The array has each value - # sanitized and interpolated into the SQL statement. - # ["name='%s' and group_id='%s'", "foo'bar", 4] returns "name='foo''bar' and group_id='4'" - def sanitize_sql_array(ary) - statement, *values = ary - if values.first.is_a?(Hash) && statement =~ /:\w+/ - replace_named_bind_variables(statement, values.first) - elsif statement.include?('?') - replace_bind_variables(statement, values) - elsif statement.blank? - statement - else - statement % values.collect { |value| connection.quote_string(value.to_s) } - end - end - - alias_method :sanitize_conditions, :sanitize_sql - - def replace_bind_variables(statement, values) #:nodoc: - raise_if_bind_arity_mismatch(statement, statement.count('?'), values.size) - bound = values.dup - c = connection - statement.gsub('?') { quote_bound_value(bound.shift, c) } - end - - def replace_named_bind_variables(statement, bind_vars) #:nodoc: - statement.gsub(/(:?):([a-zA-Z]\w*)/) do - if $1 == ':' # skip postgresql casts - $& # return the whole match - elsif bind_vars.include?(match = $2.to_sym) - quote_bound_value(bind_vars[match]) - else - raise PreparedStatementInvalid, "missing value for :#{match} in #{statement}" - end - end - end - - def expand_range_bind_variables(bind_vars) #:nodoc: - expanded = [] - - bind_vars.each do |var| - next if var.is_a?(Hash) - - if var.is_a?(Range) - expanded << var.first - expanded << var.last - else - expanded << var - end - end - - expanded - end - - def quote_bound_value(value, c = connection) #:nodoc: - if value.respond_to?(:map) && !value.acts_like?(:string) - if value.respond_to?(:empty?) && value.empty? - c.quote(nil) - else - value.map { |v| c.quote(v) }.join(',') - end - else - c.quote(value) - end - end - - def raise_if_bind_arity_mismatch(statement, expected, provided) #:nodoc: - unless expected == provided - raise PreparedStatementInvalid, "wrong number of bind variables (#{provided} for #{expected}) in: #{statement}" - end - end - - def encode_quoted_value(value) #:nodoc: - quoted_value = connection.quote(value) - quoted_value = "'#{quoted_value[1..-2].gsub(/\'/, "\\\\'")}'" if quoted_value.include?("\\\'") # (for ruby mode) " - quoted_value - 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. - # - # +initialize+ respects mass-assignment security and accepts either +:as+ or +:without_protection+ options - # in the +options+ parameter. - # - # ==== Examples - # # Instantiates a single new object - # User.new(:first_name => 'Jamie') - # - # # Instantiates a single new object using the :admin mass-assignment security role - # User.new({ :first_name => 'Jamie', :is_admin => true }, :as => :admin) - # - # # Instantiates a single new object bypassing mass-assignment security - # User.new({ :first_name => 'Jamie', :is_admin => true }, :without_protection => true) - def initialize(attributes = nil, options = {}) - @attributes = attributes_from_column_definition - @association_cache = {} - @aggregation_cache = {} - @attributes_cache = {} - @new_record = true - @readonly = false - @destroyed = false - @marked_for_destruction = false - @previously_changed = {} - @changed_attributes = {} - @relation = nil - - ensure_proper_type - set_serialized_attributes - - populate_with_current_scope_attributes - - assign_attributes(attributes, options) if attributes - - yield self if block_given? - run_callbacks :initialize - end - - # Populate +coder+ with attributes about this record that should be - # serialized. The structure of +coder+ defined in this method is - # guaranteed to match the structure of +coder+ passed to the +init_with+ - # method. - # - # Example: - # - # class Post < ActiveRecord::Base - # end - # coder = {} - # Post.new.encode_with(coder) - # coder # => { 'id' => nil, ... } - def encode_with(coder) - coder['attributes'] = attributes - end - - # Initialize an empty model object from +coder+. +coder+ must contain - # the attributes necessary for initializing an empty model object. For - # example: - # - # class Post < ActiveRecord::Base - # end - # - # post = Post.allocate - # post.init_with('attributes' => { 'title' => 'hello world' }) - # post.title # => 'hello world' - def init_with(coder) - @attributes = coder['attributes'] - @relation = nil - - set_serialized_attributes - - @attributes_cache, @previously_changed, @changed_attributes = {}, {}, {} - @association_cache = {} - @aggregation_cache = {} - @readonly = @destroyed = @marked_for_destruction = false - @new_record = false - run_callbacks :find - run_callbacks :initialize - - self - end - - # Returns a String, which Action Pack uses for constructing an URL to this - # object. The default implementation returns this record's id as a String, - # or nil if this record's unsaved. - # - # For example, suppose that you have a User model, and that you have a - # <tt>resources :users</tt> route. Normally, +user_path+ will - # construct a path with the user object's 'id' in it: - # - # user = User.find_by_name('Phusion') - # user_path(user) # => "/users/1" - # - # You can override +to_param+ in your model to make +user_path+ construct - # a path using the user's name instead of the user's id: - # - # class User < ActiveRecord::Base - # def to_param # overridden - # name - # end - # end - # - # user = User.find_by_name('Phusion') - # user_path(user) # => "/users/Phusion" - def to_param - # We can't use alias_method here, because method 'id' optimizes itself on the fly. - id && id.to_s # Be sure to stringify the id for routes - end - - # Returns a cache key that can be used to identify this record. - # - # ==== Examples - # - # Product.new.cache_key # => "products/new" - # Product.find(5).cache_key # => "products/5" (updated_at not available) - # Person.find(5).cache_key # => "people/5-20071224150000" (updated_at available) - def cache_key - case - when new_record? - "#{self.class.model_name.cache_key}/new" - when timestamp = self[:updated_at] - timestamp = timestamp.utc.to_s(:number) - "#{self.class.model_name.cache_key}/#{id}-#{timestamp}" - else - "#{self.class.model_name.cache_key}/#{id}" - end - end - - def quoted_id #:nodoc: - quote_value(id, column_for_attribute(self.class.primary_key)) - end - - # Returns true if the given attribute is in the attributes hash - def has_attribute?(attr_name) - @attributes.has_key?(attr_name.to_s) - end - - # Returns an array of names for the attributes available on this object. - def attribute_names - @attributes.keys - 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). - # - # If any attributes are protected by either +attr_protected+ or - # +attr_accessible+ then only settable attributes will be assigned. - # - # class User < ActiveRecord::Base - # attr_protected :is_admin - # end - # - # user = User.new - # user.attributes = { :username => 'Phusion', :is_admin => true } - # user.username # => "Phusion" - # user.is_admin? # => false - def attributes=(new_attributes) - return unless new_attributes.is_a?(Hash) - - assign_attributes(new_attributes) - end - - # Allows you to set all the attributes for a particular mass-assignment - # security role by passing in a hash of attributes with keys matching - # the attribute names (which again matches the column names) and the role - # name using the :as option. - # - # To bypass mass-assignment security you can use the :without_protection => true - # option. - # - # class User < ActiveRecord::Base - # attr_accessible :name - # attr_accessible :name, :is_admin, :as => :admin - # end - # - # user = User.new - # user.assign_attributes({ :name => 'Josh', :is_admin => true }) - # user.name # => "Josh" - # user.is_admin? # => false - # - # user = User.new - # user.assign_attributes({ :name => 'Josh', :is_admin => true }, :as => :admin) - # user.name # => "Josh" - # user.is_admin? # => true - # - # user = User.new - # user.assign_attributes({ :name => 'Josh', :is_admin => true }, :without_protection => true) - # user.name # => "Josh" - # user.is_admin? # => true - def assign_attributes(new_attributes, options = {}) - return unless new_attributes - - attributes = new_attributes.stringify_keys - multi_parameter_attributes = [] - @mass_assignment_options = options - - unless options[:without_protection] - attributes = sanitize_for_mass_assignment(attributes, mass_assignment_role) - end - - attributes.each do |k, v| - if k.include?("(") - multi_parameter_attributes << [ k, v ] - elsif respond_to?("#{k}=") - send("#{k}=", v) - else - raise(UnknownAttributeError, "unknown attribute: #{k}") - end - end - - @mass_assignment_options = nil - assign_multiparameter_attributes(multi_parameter_attributes) - end - - # Returns a hash of all the attributes with their names as keys and the values of the attributes as values. - def attributes - Hash[@attributes.map { |name, _| [name, read_attribute(name)] }] - end - - # Returns an <tt>#inspect</tt>-like string for the value of the - # attribute +attr_name+. String attributes are truncated upto 50 - # characters, and Date and Time attributes are returned in the - # <tt>:db</tt> format. Other attributes return the value of - # <tt>#inspect</tt> without modification. - # - # person = Person.create!(:name => "David Heinemeier Hansson " * 3) - # - # person.attribute_for_inspect(:name) - # # => '"David Heinemeier Hansson David Heinemeier Hansson D..."' - # - # person.attribute_for_inspect(:created_at) - # # => '"2009-01-12 04:48:57"' - def attribute_for_inspect(attr_name) - value = read_attribute(attr_name) - - if value.is_a?(String) && value.length > 50 - "#{value[0..50]}...".inspect - elsif value.is_a?(Date) || value.is_a?(Time) - %("#{value.to_s(:db)}") - else - value.inspect - end - 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 respond to empty?, most notably Strings). - def attribute_present?(attribute) - !_read_attribute(attribute).blank? - end - - # Returns the column object for the named attribute. - def column_for_attribute(name) - self.class.columns_hash[name.to_s] - end - - # Returns true if +comparison_object+ is the same exact object, or +comparison_object+ - # is of the same type and +self+ has an ID and it is equal to +comparison_object.id+. - # - # Note that new records are different from any other record by definition, unless the - # other record is the receiver itself. Besides, if you fetch existing records with - # +select+ and leave the ID out, you're on your own, this predicate will return false. - # - # Note also that destroying a record preserves its ID in the model instance, so deleted - # models are still comparable. - def ==(comparison_object) - super || - comparison_object.instance_of?(self.class) && - id.present? && - comparison_object.id == id - end - alias :eql? :== - - # 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.hash - end - - # Freeze the attributes hash such that associations are still accessible, even on destroyed records. - def freeze - @attributes.freeze; self - end - - # Returns +true+ if the attributes hash has been frozen. - def frozen? - @attributes.frozen? - end - - # Allows sort on objects - def <=>(other_object) - if other_object.is_a?(self.class) - self.to_key <=> other_object.to_key - else - nil - end - end - - # Backport dup from 1.9 so that initialize_dup() gets called - unless Object.respond_to?(:initialize_dup) - def dup # :nodoc: - copy = super - copy.initialize_dup(self) - copy - end - end - - # Duped objects have no id assigned and are treated as new records. Note - # that this is a "shallow" copy as it copies the object's attributes - # only, not its associations. The extent of a "deep" copy is application - # specific and is therefore left to the application to implement according - # to its need. - # The dup method does not preserve the timestamps (created|updated)_(at|on). - def initialize_dup(other) - cloned_attributes = other.clone_attributes(:read_attribute_before_type_cast) - cloned_attributes.delete(self.class.primary_key) - - @attributes = cloned_attributes - - _run_after_initialize_callbacks if respond_to?(:_run_after_initialize_callbacks) - - @changed_attributes = {} - attributes_from_column_definition.each do |attr, orig_value| - @changed_attributes[attr] = orig_value if field_changed?(attr, orig_value, @attributes[attr]) - end - - @aggregation_cache = {} - @association_cache = {} - @attributes_cache = {} - @new_record = true - - ensure_proper_type - populate_with_current_scope_attributes - super - end - - # Returns +true+ if the record is read only. Records loaded through joins with piggy-back - # attributes will be marked as read only since they cannot be saved. - def readonly? - @readonly - end - - # Marks this record as read only. - def readonly! - @readonly = true - end - - # Returns the contents of the record as a nicely formatted string. - def inspect - inspection = if @attributes - self.class.column_names.collect { |name| - if has_attribute?(name) - "#{name}: #{attribute_for_inspect(name)}" - end - }.compact.join(", ") - else - "not initialized" - end - "#<#{self.class} #{inspection}>" - end - - protected - def clone_attributes(reader_method = :read_attribute, attributes = {}) - attribute_names.each do |name| - attributes[name] = clone_attribute_value(reader_method, name) - end - attributes - end - - def clone_attribute_value(reader_method, attribute_name) - value = send(reader_method, attribute_name) - value.duplicable? ? value.clone : value - rescue TypeError, NoMethodError - value - end - - def mass_assignment_options - @mass_assignment_options ||= {} - end - - def mass_assignment_role - mass_assignment_options[:as] || :default - end - - private - - # Under Ruby 1.9, Array#flatten will call #to_ary (recursively) on each of the elements - # of the array, and then rescues from the possible NoMethodError. If those elements are - # ActiveRecord::Base's, then this triggers the various method_missing's that we have, - # which significantly impacts upon performance. - # - # So we can avoid the method_missing hit by explicitly defining #to_ary as nil here. - # - # See also http://tenderlovemaking.com/2011/06/28/til-its-ok-to-return-nil-from-to_ary/ - def to_ary # :nodoc: - nil - end - - def set_serialized_attributes - sattrs = self.class.serialized_attributes - - sattrs.each do |key, coder| - @attributes[key] = coder.load @attributes[key] if @attributes.key?(key) - end - end - - # Sets the attribute used for single table inheritance to this class name if this is not the - # ActiveRecord::Base descendant. - # Considering the hierarchy Reply < Message < ActiveRecord::Base, this makes it possible to - # do Reply.new without having to set <tt>Reply[Reply.inheritance_column] = "Reply"</tt> yourself. - # No such attribute would be set for objects of the Message class in that example. - def ensure_proper_type - klass = self.class - if klass.finder_needs_type_condition? - write_attribute(klass.inheritance_column, klass.sti_name) - end - end - - # The primary key and inheritance column can never be set by mass-assignment for security reasons. - def self.attributes_protected_by_default - default = [ inheritance_column ] - default << primary_key if primary_key? - default << 'id' unless primary_key? && primary_key == 'id' - default - end - - # Returns a copy of the attributes hash where all the values have been safely quoted for use in - # an Arel insert/update method. - def arel_attributes_values(include_primary_key = true, include_readonly_attributes = true, attribute_names = @attributes.keys) - attrs = {} - klass = self.class - arel_table = klass.arel_table - - attribute_names.each do |name| - if (column = column_for_attribute(name)) && (include_primary_key || !column.primary) - - if include_readonly_attributes || (!include_readonly_attributes && !self.class.readonly_attributes.include?(name)) - - value = if coder = klass.serialized_attributes[name] - coder.dump @attributes[name] - else - # FIXME: we need @attributes to be used consistently. - # If the values stored in @attributes were already type - # casted, this code could be simplified - read_attribute(name) - end - - attrs[arel_table[name]] = value - end - end - end - attrs - end - - # Quote strings appropriately for SQL statements. - def quote_value(value, column = nil) - self.class.connection.quote(value, column) - 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 - # parentheses 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 are 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 - - def instantiate_time_object(name, values) - if self.class.send(:create_time_zone_conversion_attribute?, name, column_for_attribute(name)) - Time.zone.local(*values) - else - Time.time_with_datetime_fallback(@@default_timezone, *values) - end - end - - def execute_callstack_for_multiparameter_attributes(callstack) - errors = [] - callstack.each do |name, values_with_empty_parameters| - begin - send(name + "=", read_value_from_parameter(name, values_with_empty_parameters)) - rescue => ex - errors << AttributeAssignmentError.new("error on assignment #{values_with_empty_parameters.values.inspect} to #{name}", ex, name) - end - end - unless errors.empty? - raise MultiparameterAssignmentErrors.new(errors), "#{errors.size} error(s) on assignment of multiparameter attributes" - end - end - - def read_value_from_parameter(name, values_hash_from_param) - klass = (self.class.reflect_on_aggregation(name.to_sym) || column_for_attribute(name)).klass - if values_hash_from_param.values.all?{|v|v.nil?} - nil - elsif klass == Time - read_time_parameter_value(name, values_hash_from_param) - elsif klass == Date - read_date_parameter_value(name, values_hash_from_param) - else - read_other_parameter_value(klass, name, values_hash_from_param) - end - end - - def read_time_parameter_value(name, values_hash_from_param) - # If Date bits were not provided, error - raise "Missing Parameter" if [1,2,3].any?{|position| !values_hash_from_param.has_key?(position)} - max_position = extract_max_param_for_multiparameter_attributes(values_hash_from_param, 6) - # If Date bits were provided but blank, then return nil - return nil if (1..3).any? {|position| values_hash_from_param[position].blank?} - - set_values = (1..max_position).collect{|position| values_hash_from_param[position] } - # If Time bits are not there, then default to 0 - (3..5).each {|i| set_values[i] = set_values[i].blank? ? 0 : set_values[i]} - instantiate_time_object(name, set_values) - end - - def read_date_parameter_value(name, values_hash_from_param) - return nil if (1..3).any? {|position| values_hash_from_param[position].blank?} - set_values = [values_hash_from_param[1], values_hash_from_param[2], values_hash_from_param[3]] - begin - Date.new(*set_values) - rescue ArgumentError # if Date.new raises an exception on an invalid date - instantiate_time_object(name, set_values).to_date # we instantiate Time object and convert it back to a date thus using Time's logic in handling invalid dates - end - end - - def read_other_parameter_value(klass, name, values_hash_from_param) - max_position = extract_max_param_for_multiparameter_attributes(values_hash_from_param) - values = (1..max_position).collect do |position| - raise "Missing Parameter" if !values_hash_from_param.has_key?(position) - values_hash_from_param[position] - end - klass.new(*values) - end - - def extract_max_param_for_multiparameter_attributes(values_hash_from_param, upper_cap = 100) - [values_hash_from_param.keys.max,upper_cap].min - end - - def extract_callstack_for_multiparameter_attributes(pairs) - attributes = { } - - pairs.each do |pair| - multiparameter_name, value = pair - attribute_name = multiparameter_name.split("(").first - attributes[attribute_name] = {} unless attributes.include?(attribute_name) - - parameter_value = value.empty? ? nil : type_cast_attribute_value(multiparameter_name, value) - attributes[attribute_name][find_parameter_position(multiparameter_name)] ||= parameter_value - end - - attributes - end - - def type_cast_attribute_value(multiparameter_name, value) - multiparameter_name =~ /\([0-9]*([if])\)/ ? value.send("to_" + $1) : value - end - - def find_parameter_position(multiparameter_name) - multiparameter_name.scan(/\(([0-9]*).*\)/).first.first.to_i - end - - # Returns a comma-separated pair list, like "key1 = val1, key2 = val2". - def comma_pair_list(hash) - hash.map { |k,v| "#{k} = #{v}" }.join(", ") - end - - def quote_columns(quoter, hash) - Hash[hash.map { |name, value| [quoter.quote_column_name(name), value] }] - end - - def quoted_comma_pair_list(quoter, hash) - comma_pair_list(quote_columns(quoter, hash)) - end - - def convert_number_column_value(value) - if value == false - 0 - elsif value == true - 1 - elsif value.is_a?(String) && value.blank? - nil - else - value - end - end - - def populate_with_current_scope_attributes - return unless self.class.scope_attributes? - - self.class.scope_attributes.each do |att,value| - send("#{att}=", value) if respond_to?("#{att}=") - end - end - end - - Base.class_eval do - include ActiveRecord::Persistence - extend ActiveModel::Naming - extend QueryCache::ClassMethods - extend ActiveSupport::Benchmarkable - extend ActiveSupport::DescendantsTracker - - include ActiveModel::Conversion - include Validations - extend CounterCache - include Locking::Optimistic, Locking::Pessimistic - include AttributeMethods - include AttributeMethods::Read, AttributeMethods::Write, AttributeMethods::BeforeTypeCast, AttributeMethods::Query - include AttributeMethods::PrimaryKey - include AttributeMethods::TimeZoneConversion - include AttributeMethods::Dirty - include ActiveModel::MassAssignmentSecurity - include Callbacks, ActiveModel::Observing, Timestamp - include Associations, NamedScope - include IdentityMap - include ActiveModel::SecurePassword - - # AutosaveAssociation needs to be included before Transactions, because we want - # #save_with_autosave_associations to be wrapped inside a transaction. - include AutosaveAssociation, NestedAttributes - include Aggregations, Transactions, Reflection, Serialization - - NilClass.add_whiner(self) if NilClass.respond_to?(:add_whiner) - - # Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (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). - alias [] read_attribute - - # Updates the attribute identified by <tt>attr_name</tt> with the specified +value+. - # (Alias for the protected write_attribute method). - alias []= write_attribute - - public :[], :[]= + include ActiveRecord::Model end end -require 'active_record/connection_adapters/abstract/connection_specification' -ActiveSupport.run_load_hooks(:active_record, ActiveRecord::Base) +ActiveSupport.run_load_hooks(:active_record, ActiveRecord::Model::DeprecationProxy) diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb index a175bf003c..8b2fc69b00 100644 --- a/activerecord/lib/active_record/callbacks.rb +++ b/activerecord/lib/active_record/callbacks.rb @@ -25,7 +25,7 @@ module ActiveRecord # Check out <tt>ActiveRecord::Transactions</tt> for more details about <tt>after_commit</tt> and # <tt>after_rollback</tt>. # - # Lastly an <tt>after_find</tt> and <tt>after_initialize</tt> callback is triggered for each object that + # Lastly an <tt>after_find</tt> and <tt>after_initialize</tt> callback is triggered for each object that # is found and instantiated by a finder, with <tt>after_initialize</tt> being triggered after new objects # are instantiated as well. # @@ -215,24 +215,48 @@ module ActiveRecord # instead of quietly returning +false+. # # == Debugging callbacks - # - # The callback chain is accessible via the <tt>_*_callbacks</tt> method on an object. ActiveModel Callbacks support + # + # The callback chain is accessible via the <tt>_*_callbacks</tt> method on an object. ActiveModel Callbacks support # <tt>:before</tt>, <tt>:after</tt> and <tt>:around</tt> as values for the <tt>kind</tt> property. The <tt>kind</tt> property # defines what part of the chain the callback runs in. - # - # To find all callbacks in the before_save callback chain: - # + # + # To find all callbacks in the before_save callback chain: + # # Topic._save_callbacks.select { |cb| cb.kind.eql?(:before) } - # + # # Returns an array of callback objects that form the before_save chain. - # + # # To further check if the before_save chain contains a proc defined as <tt>rest_when_dead</tt> use the <tt>filter</tt> property of the callback object: - # + # # Topic._save_callbacks.select { |cb| cb.kind.eql?(:before) }.collect(&:filter).include?(:rest_when_dead) - # + # # Returns true or false depending on whether the proc is contained in the before_save callback chain on a Topic model. - # + # module Callbacks + # We can't define callbacks directly on ActiveRecord::Model because + # it is a module. So we queue up the definitions and execute them + # when ActiveRecord::Model is included. + module Register #:nodoc: + def self.extended(base) + base.config_attribute :_callbacks_register + base._callbacks_register = [] + end + + def self.setup(base) + base._callbacks_register.each do |item| + base.send(*item) + end + end + + def define_callbacks(*args) + self._callbacks_register << [:define_callbacks, *args] + end + + def define_model_callbacks(*args) + self._callbacks_register << [:define_model_callbacks, *args] + end + end + extend ActiveSupport::Concern CALLBACKS = [ @@ -242,8 +266,11 @@ module ActiveRecord :before_destroy, :around_destroy, :after_destroy, :after_commit, :after_rollback ] + module ClassMethods + include ActiveModel::Callbacks + end + included do - extend ActiveModel::Callbacks include ActiveModel::Validations::Callbacks define_model_callbacks :initialize, :find, :touch, :only => :after diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb index 20863e73aa..b8f99adc22 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -1,7 +1,7 @@ require 'thread' require 'monitor' require 'set' -require 'active_support/core_ext/module/synchronization' +require 'active_support/core_ext/module/deprecation' module ActiveRecord # Raised when a connection could not be obtained within the connection @@ -9,6 +9,13 @@ module ActiveRecord class ConnectionTimeoutError < ConnectionNotEstablished end + # Raised when a connection pool is full and another connection is requested + class PoolFullError < ConnectionNotEstablished + def initialize size, timeout + super("Connection pool of size #{size} and timeout #{timeout}s is full") + end + end + module ConnectionAdapters # Connection pool base class for managing Active Record database # connections. @@ -57,10 +64,35 @@ module ActiveRecord # * +wait_timeout+: number of seconds to block and wait for a connection # before giving up and raising a timeout error (default 5 seconds). class ConnectionPool - attr_accessor :automatic_reconnect - attr_reader :spec, :connections - attr_reader :columns, :columns_hash, :primary_keys, :tables - attr_reader :column_defaults + # Every +frequency+ seconds, the reaper will call +reap+ on +pool+. + # A reaper instantiated with a nil frequency will never reap the + # connection pool. + # + # Configure the frequency by setting "reaping_frequency" in your + # database yaml file. + class Reaper + attr_reader :pool, :frequency + + def initialize(pool, frequency) + @pool = pool + @frequency = frequency + end + + def run + return unless frequency + Thread.new(frequency, pool) { |t, p| + while true + sleep t + p.reap + end + } + end + end + + include MonitorMixin + + attr_accessor :automatic_reconnect, :timeout + attr_reader :spec, :connections, :size, :reaper # Creates a new ConnectionPool object. +spec+ is a ConnectionSpecification # object which describes database connection information (e.g. adapter, @@ -69,88 +101,22 @@ module ActiveRecord # # The default ConnectionPool maximum size is 5. def initialize(spec) + super() + @spec = spec # The cache of reserved connections mapped to threads @reserved_connections = {} - # The mutex used to synchronize pool access - @connection_mutex = Monitor.new - @queue = @connection_mutex.new_cond @timeout = spec.config[:wait_timeout] || 5 + @reaper = Reaper.new self, spec.config[:reaping_frequency] + @reaper.run # default max pool size to 5 @size = (spec.config[:pool] && spec.config[:pool].to_i) || 5 @connections = [] - @checked_out = [] @automatic_reconnect = true - @tables = {} - @visitor = nil - - @columns = Hash.new do |h, table_name| - h[table_name] = with_connection do |conn| - - # Fetch a list of columns - conn.columns(table_name, "#{table_name} Columns").tap do |columns| - - # set primary key information - columns.each do |column| - column.primary = column.name == primary_keys[table_name] - end - end - end - end - - @columns_hash = Hash.new do |h, table_name| - h[table_name] = Hash[columns[table_name].map { |col| - [col.name, col] - }] - end - - @column_defaults = Hash.new do |h, table_name| - h[table_name] = Hash[columns[table_name].map { |col| - [col.name, col.default] - }] - end - - @primary_keys = Hash.new do |h, table_name| - h[table_name] = with_connection do |conn| - table_exists?(table_name) ? conn.primary_key(table_name) : 'id' - end - end - end - - # A cached lookup for table existence. - def table_exists?(name) - return true if @tables.key? name - - with_connection do |conn| - conn.tables.each { |table| @tables[table] = true } - @tables[name] = true if !@tables.key?(name) && conn.table_exists?(name) - end - - @tables.key? name - end - - # Clears out internal caches: - # - # * columns - # * columns_hash - # * tables - def clear_cache! - @columns.clear - @columns_hash.clear - @column_defaults.clear - @tables.clear - end - - # Clear out internal caches for table with +table_name+. - def clear_table_cache!(table_name) - @columns.delete table_name - @columns_hash.delete table_name - @column_defaults.delete table_name - @primary_keys.delete table_name end # Retrieve the connection associated with the current thread, or call @@ -165,7 +131,7 @@ module ActiveRecord # Check to see if there is an active connection in this connection # pool. def active_connection? - @reserved_connections.key? current_connection_id + active_connections.any? end # Signal that the thread is finished with the current connection. @@ -181,7 +147,7 @@ module ActiveRecord # connection when finished. def with_connection connection_id = current_connection_id - fresh_connection = true unless @reserved_connections[connection_id] + fresh_connection = true unless active_connection? yield connection ensure release_connection(connection_id) if fresh_connection @@ -189,94 +155,80 @@ module ActiveRecord # Returns true if a connection has already been opened. def connected? - !@connections.empty? + synchronize { @connections.any? } end # Disconnects all connections in the pool, and clears the pool. def disconnect! - @reserved_connections.each do |name,conn| - checkin conn - end - @reserved_connections = {} - @connections.each do |conn| - conn.disconnect! + synchronize do + @reserved_connections = {} + @connections.each do |conn| + checkin conn + conn.disconnect! + end + @connections = [] end - @connections = [] end # Clears the cache which maps classes. def clear_reloadable_connections! - @reserved_connections.each do |name, conn| - checkin conn - end - @reserved_connections = {} - @connections.each do |conn| - conn.disconnect! if conn.requires_reloading? - end - @connections.delete_if do |conn| - conn.requires_reloading? + synchronize do + @reserved_connections = {} + @connections.each do |conn| + checkin conn + conn.disconnect! if conn.requires_reloading? + end + @connections.delete_if do |conn| + conn.requires_reloading? + end end end # Verify active connections and remove and disconnect connections # associated with stale threads. def verify_active_connections! #:nodoc: - clear_stale_cached_connections! - @connections.each do |connection| - connection.verify! + synchronize do + @connections.each do |connection| + connection.verify! + end end end - # Return any checked-out connections back to the pool by threads that - # are no longer alive. - def clear_stale_cached_connections! - keys = @reserved_connections.keys - Thread.list.find_all { |t| - t.alive? - }.map { |thread| thread.object_id } - keys.each do |key| - checkin @reserved_connections[key] - @reserved_connections.delete(key) - end + def clear_stale_cached_connections! # :nodoc: end + deprecate :clear_stale_cached_connections! # Check-out a database connection from the pool, indicating that you want # to use it. You should call #checkin when you no longer need this. # - # This is done by either returning an existing connection, or by creating - # a new connection. If the maximum number of connections for this pool has - # already been reached, but the pool is empty (i.e. they're all being used), - # then this method will wait until a thread has checked in a connection. - # The wait time is bounded however: if no connection can be checked out - # within the timeout specified for this pool, then a ConnectionTimeoutError - # exception will be raised. + # This is done by either returning and leasing existing connection, or by + # creating a new connection and leasing it. + # + # If all connections are leased and the pool is at capacity (meaning the + # number of currently leased connections is greater than or equal to the + # size limit set), an ActiveRecord::PoolFullError exception will be raised. # # Returns: an AbstractAdapter object. # # Raises: - # - ConnectionTimeoutError: no connection can be obtained from the pool - # within the timeout period. + # - PoolFullError: no connection can be obtained from the pool. def checkout # Checkout an available connection - @connection_mutex.synchronize do - loop do - conn = if @checked_out.size < @connections.size - checkout_existing_connection - elsif @connections.size < @size - checkout_new_connection - end - return conn if conn - - @queue.wait(@timeout) - - if(@checked_out.size < @connections.size) - next - else - clear_stale_cached_connections! - if @size == @checked_out.size - raise ConnectionTimeoutError, "could not obtain a database connection#{" within #{@timeout} seconds" if @timeout}. The max pool size is currently #{@size}; consider increasing it." - end - end + synchronize do + # Try to find a connection that hasn't been leased, and lease it + conn = connections.find { |c| c.lease } + + # If all connections were leased, and we have room to expand, + # create a new connection and lease it. + if !conn && connections.size < size + conn = checkout_new_connection + conn.lease + end + if conn + checkout_and_verify conn + else + raise PoolFullError.new(size, timeout) end end end @@ -287,56 +239,69 @@ module ActiveRecord # +conn+: an AbstractAdapter object, which was obtained by earlier by # calling +checkout+ on this pool. def checkin(conn) - @connection_mutex.synchronize do + synchronize do conn.run_callbacks :checkin do - @checked_out.delete conn - @queue.signal + conn.expire end end end - synchronize :clear_reloadable_connections!, :verify_active_connections!, - :connected?, :disconnect!, :with => :@connection_mutex + # Remove a connection from the connection pool. The connection will + # remain open and active but will no longer be managed by this pool. + def remove(conn) + synchronize do + @connections.delete conn - private + # FIXME: we might want to store the key on the connection so that removing + # from the reserved hash will be a little easier. + thread_id = @reserved_connections.keys.find { |k| + @reserved_connections[k] == conn + } + @reserved_connections.delete thread_id if thread_id + end + end - def new_connection - connection = ActiveRecord::Base.send(spec.adapter_method, spec.config) + # Removes dead connections from the pool. A dead connection can occur + # if a programmer forgets to close a connection at the end of a thread + # or a thread dies unexpectedly. + def reap + synchronize do + stale = Time.now - @timeout + connections.dup.each do |conn| + remove conn if conn.in_use? && stale > conn.last_use && !conn.active? + end + end + end - # TODO: This is a bit icky, and in the long term we may want to change the method - # signature for connections. Also, if we switch to have one visitor per - # connection (and therefore per thread), we can get rid of the thread-local - # variable in Arel::Visitors::ToSql. - @visitor ||= connection.class.visitor_for(self) - connection.visitor = @visitor + private - connection + def new_connection + ActiveRecord::Base.send(spec.adapter_method, spec.config) end def current_connection_id #:nodoc: - Thread.current.object_id + ActiveRecord::Base.connection_id ||= Thread.current.object_id end def checkout_new_connection raise ConnectionNotEstablished unless @automatic_reconnect c = new_connection + c.pool = self @connections << c - checkout_and_verify(c) - end - - def checkout_existing_connection - c = (@connections - @checked_out).first - checkout_and_verify(c) + c end def checkout_and_verify(c) c.run_callbacks :checkout do c.verify! - @checked_out << c end c end + + def active_connections + @connections.find_all { |c| c.in_use? } + end end # ConnectionHandler is a collection of ConnectionPool objects. It is used @@ -367,10 +332,12 @@ module ActiveRecord def initialize(pools = {}) @connection_pools = pools + @class_to_pool = {} end def establish_connection(name, spec) - @connection_pools[name] = ConnectionAdapters::ConnectionPool.new(spec) + @connection_pools[spec] ||= ConnectionAdapters::ConnectionPool.new(spec) + @class_to_pool[name] = @connection_pools[spec] end # Returns true if there are any active connections among the connection @@ -421,19 +388,20 @@ module ActiveRecord # can be used as an argument for establish_connection, for easily # re-establishing the connection. def remove_connection(klass) - pool = @connection_pools.delete(klass.name) + pool = @class_to_pool.delete(klass.name) return nil unless pool + @connection_pools.delete pool.spec pool.automatic_reconnect = false pool.disconnect! pool.spec.config end def retrieve_connection_pool(klass) - pool = @connection_pools[klass.name] + pool = @class_to_pool[klass.name] return pool if pool - return nil if ActiveRecord::Base == klass - retrieve_connection_pool klass.superclass + return nil if ActiveRecord::Model == klass + retrieve_connection_pool klass.active_record_super end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb deleted file mode 100644 index c08c0263b9..0000000000 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb +++ /dev/null @@ -1,153 +0,0 @@ -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 - - ## - # :singleton-method: - # The connection handler - class_attribute :connection_handler, :instance_writer => false - self.connection_handler = ConnectionAdapters::ConnectionHandler.new - - # 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 - - # Establishes the connection to the database. Accepts a hash as input where - # the <tt>:adapter</tt> 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", - # :database => "path/to/dbfile" - # ) - # - # Also accepts keys as strings (for parsing from YAML for example): - # - # ActiveRecord::Base.establish_connection( - # "adapter" => "sqlite", - # "database" => "path/to/dbfile" - # ) - # - # Or a URL: - # - # ActiveRecord::Base.establish_connection( - # "postgres://myuser:mypass@localhost/somedatabase" - # ) - # - # The exceptions AdapterNotSpecified, AdapterNotFound and ArgumentError - # may be returned on an error. - def self.establish_connection(spec = ENV["DATABASE_URL"]) - case spec - when nil - raise AdapterNotSpecified unless defined?(Rails.env) - establish_connection(Rails.env) - when ConnectionSpecification - self.connection_handler.establish_connection(name, spec) - when Symbol, String - if configuration = configurations[spec.to_s] - establish_connection(configuration) - elsif spec.is_a?(String) && hash = connection_url_to_hash(spec) - establish_connection(hash) - else - raise AdapterNotSpecified, "#{spec} database is not configured" - end - else - spec = spec.symbolize_keys - unless spec.key?(:adapter) then raise AdapterNotSpecified, "database configuration does not specify adapter" end - - begin - require "active_record/connection_adapters/#{spec[:adapter]}_adapter" - rescue LoadError => e - raise "Please install the #{spec[:adapter]} adapter: `gem install activerecord-#{spec[:adapter]}-adapter` (#{e})" - end - - adapter_method = "#{spec[:adapter]}_connection" - unless respond_to?(adapter_method) - raise AdapterNotFound, "database configuration specifies nonexistent #{spec[:adapter]} adapter" - end - - remove_connection - establish_connection(ConnectionSpecification.new(spec, adapter_method)) - end - end - - def self.connection_url_to_hash(url) # :nodoc: - config = URI.parse url - adapter = config.scheme - adapter = "postgresql" if adapter == "postgres" - spec = { :adapter => adapter, - :username => config.user, - :password => config.password, - :port => config.port, - :database => config.path.sub(%r{^/},""), - :host => config.host } - spec.reject!{ |_,value| !value } - if config.query - options = Hash[config.query.split("&").map{ |pair| pair.split("=") }].symbolize_keys - spec.merge!(options) - end - spec - end - - class << self - # 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 connection - retrieve_connection - end - - # Returns the configuration of the associated connection as a hash: - # - # ActiveRecord::Base.connection_config - # # => {:pool=>5, :timeout=>5000, :database=>"db/development.sqlite3", :adapter=>"sqlite3"} - # - # Please use only for reading. - def connection_config - connection_pool.spec.config - end - - def connection_pool - connection_handler.retrieve_connection_pool(self) or raise ConnectionNotEstablished - end - - def retrieve_connection - connection_handler.retrieve_connection(self) - end - - # Returns true if Active Record is connected. - def connected? - connection_handler.connected?(self) - end - - def remove_connection(klass = self) - connection_handler.remove_connection(klass) - end - - def clear_active_connections! - connection_handler.clear_active_connections! - end - - delegate :clear_reloadable_connections!, - :clear_all_connections!,:verify_active_connections!, :to => :connection_handler - end - end -end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb index dc4a53034b..eb8cff9610 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -130,7 +130,7 @@ module ActiveRecord # # In order to get around this problem, #transaction will emulate the effect # of nested transactions, by using savepoints: - # http://dev.mysql.com/doc/refman/5.0/en/savepoints.html + # http://dev.mysql.com/doc/refman/5.0/en/savepoint.html # Savepoints are supported by MySQL and PostgreSQL, but not SQLite3. # # It is safe to call this method if a database transaction is already open, @@ -341,7 +341,7 @@ module ActiveRecord # Send a rollback message to all records after they have been rolled back. If rollback # is false, only rollback records since the last save point. - def rollback_transaction_records(rollback) #:nodoc + def rollback_transaction_records(rollback) if rollback records = @_current_transaction_records.flatten @_current_transaction_records.clear @@ -361,7 +361,7 @@ module ActiveRecord end # Send a commit message to all records after they have been committed. - def commit_transaction_records #:nodoc + def commit_transaction_records records = @_current_transaction_records.flatten @_current_transaction_records.clear unless records.blank? diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb index 82f564e41d..132ca10f79 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -6,7 +6,7 @@ require 'bigdecimal/util' module ActiveRecord module ConnectionAdapters #:nodoc: - class IndexDefinition < Struct.new(:table, :name, :unique, :columns, :lengths) #:nodoc: + class IndexDefinition < Struct.new(:table, :name, :unique, :columns, :lengths, :orders) #:nodoc: end # Abstract representation of a column definition. Instances of this type @@ -46,13 +46,13 @@ module ActiveRecord # +change_table+ is actually of this type: # # class SomeMigration < ActiveRecord::Migration - # def self.up + # def up # create_table :foo do |t| # puts t.class # => "ActiveRecord::ConnectionAdapters::TableDefinition" # end # end # - # def self.down + # def down # ... # end # end @@ -66,6 +66,7 @@ module ActiveRecord def initialize(base) @columns = [] + @columns_hash = {} @base = base end @@ -86,7 +87,7 @@ module ActiveRecord # Returns a ColumnDefinition for the column with name +name+. def [](name) - @columns.find {|column| column.name.to_s == name.to_s} + @columns_hash[name.to_s] end # Instantiates a new column for the table. @@ -224,35 +225,38 @@ module ActiveRecord # t.references :taggable, :polymorphic => { :default => 'Photo' } # end def column(name, type, options = {}) - column = self[name] || ColumnDefinition.new(@base, name, type) - if options[:limit] - column.limit = options[:limit] - elsif native[type.to_sym].is_a?(Hash) - column.limit = native[type.to_sym][:limit] + name = name.to_s + type = type.to_sym + + column = self[name] || new_column_definition(@base, name, type) + + limit = options.fetch(:limit) do + native[type][:limit] if native[type].is_a?(Hash) end + + column.limit = limit column.precision = options[:precision] - column.scale = options[:scale] - column.default = options[:default] - column.null = options[:null] - @columns << column unless @columns.include? column + column.scale = options[:scale] + column.default = options[:default] + column.null = options[:null] self end %w( string text integer float decimal datetime timestamp time date binary boolean ).each do |column_type| class_eval <<-EOV, __FILE__, __LINE__ + 1 - def #{column_type}(*args) # def string(*args) - options = args.extract_options! # options = args.extract_options! - column_names = args # column_names = args - # - column_names.each { |name| column(name, '#{column_type}', options) } # column_names.each { |name| column(name, 'string', options) } - end # end + def #{column_type}(*args) # def string(*args) + options = args.extract_options! # options = args.extract_options! + column_names = args # column_names = args + type = :'#{column_type}' # type = :string + column_names.each { |name| column(name, type, options) } # column_names.each { |name| column(name, type, options) } + end # end EOV end # Appends <tt>:datetime</tt> columns <tt>:created_at</tt> and # <tt>:updated_at</tt> to the table. def timestamps(*args) - options = args.extract_options! + options = { :null => false }.merge(args.extract_options!) column(:created_at, :datetime, options) column(:updated_at, :datetime, options) end @@ -275,9 +279,16 @@ module ActiveRecord end private - def native - @base.native_database_types - end + def new_column_definition(base, name, type) + definition = ColumnDefinition.new base, name, type + @columns << definition + @columns_hash[name] = definition + definition + end + + def native + @base.native_database_types + end end # Represents an SQL table in an abstract way for updating a table. @@ -453,13 +464,13 @@ module ActiveRecord def #{column_type}(*args) # def string(*args) options = args.extract_options! # options = args.extract_options! column_names = args # column_names = args - # + type = :'#{column_type}' # type = :string column_names.each do |name| # column_names.each do |name| - column = ColumnDefinition.new(@base, name, '#{column_type}') # column = ColumnDefinition.new(@base, name, 'string') + column = ColumnDefinition.new(@base, name.to_s, type) # column = ColumnDefinition.new(@base, name, type) if options[:limit] # if options[:limit] column.limit = options[:limit] # column.limit = options[:limit] - elsif native['#{column_type}'.to_sym].is_a?(Hash) # elsif native['string'.to_sym].is_a?(Hash) - column.limit = native['#{column_type}'.to_sym][:limit] # column.limit = native['string'.to_sym][:limit] + elsif native[type].is_a?(Hash) # elsif native[type].is_a?(Hash) + column.limit = native[type][:limit] # column.limit = native[type][:limit] end # end column.precision = options[:precision] # column.precision = options[:precision] column.scale = options[:scale] # column.scale = options[:scale] @@ -479,4 +490,3 @@ module ActiveRecord end end - diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb index 8e3ba1297e..20d3b4a1ef 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -1,4 +1,5 @@ require 'active_support/core_ext/array/wrap' +require 'active_support/deprecation/reporting' module ActiveRecord module ConnectionAdapters # :nodoc: @@ -15,8 +16,6 @@ module ActiveRecord table_name[0...table_alias_length].gsub(/\./, '_') end - # def tables(name = nil) end - # Checks to see if the table +table_name+ exists on the database. # # === Example @@ -113,7 +112,7 @@ module ActiveRecord # Defaults to +id+. If <tt>:id</tt> is false this option is ignored. # # Also note that this just sets the primary key in the table. You additionally - # need to configure the primary key in the model via the +set_primary_key+ macro. + # need to configure the primary key in the model via +self.primary_key=+. # Models do NOT auto-detect the primary key from their table definition. # # [<tt>:options</tt>] @@ -302,15 +301,8 @@ module ActiveRecord # Adds a new index to the table. +column_name+ can be a single Symbol, or # an Array of Symbols. # - # The index will be named after the table and the first column name, - # unless you pass <tt>:name</tt> as an option. - # - # When creating an index on multiple columns, the first column is used as a name - # for the index. For example, when you specify an index on two columns - # [<tt>:first</tt>, <tt>:last</tt>], the DBMS creates an index for both columns as well as an - # index for the first column <tt>:first</tt>. Using just the first name for this index - # makes sense, because you will never have to create a singular index with this - # name. + # The index will be named after the table and the column name(s), unless + # you pass <tt>:name</tt> as an option. # # ===== Examples # @@ -339,6 +331,14 @@ module ActiveRecord # CREATE INDEX by_name_surname ON accounts(name(10), surname(15)) # # Note: SQLite doesn't support index length + # + # ====== Creating an index with a sort order (desc or asc, asc is the default) + # add_index(:accounts, [:branch_id, :party_id, :surname], :order => {:branch_id => :desc, :part_id => :asc}) + # generates + # CREATE INDEX by_branch_desc_party ON accounts(branch_id DESC, party_id ASC, surname) + # + # Note: mysql doesn't yet support index order (it accepts the syntax but ignores it) + # def add_index(table_name, column_name, options = {}) index_name, index_type, index_columns = add_index_options(table_name, column_name, options) execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} (#{index_columns})" @@ -405,7 +405,7 @@ module ActiveRecord def dump_schema_information #:nodoc: sm_table = ActiveRecord::Migrator.schema_migrations_table_name - migrated = select_values("SELECT version FROM #{sm_table}") + migrated = select_values("SELECT version FROM #{sm_table} ORDER BY version") migrated.map { |v| "INSERT INTO #{sm_table} (version) VALUES ('#{v}');" }.join("\n\n") end @@ -426,6 +426,7 @@ module ActiveRecord si_table = Base.table_name_prefix + 'schema_info' + Base.table_name_suffix if table_exists?(si_table) + ActiveRecord::Deprecation.warn "Usage of the schema table `#{si_table}` is deprecated. Please switch to using `schema_migrations` table" old_version = select_value("SELECT version FROM #{quote_table_name(si_table)}").to_i assume_migrated_upto_version(old_version) @@ -507,8 +508,8 @@ module ActiveRecord # ===== Examples # add_timestamps(:suppliers) def add_timestamps(table_name) - add_column table_name, :created_at, :datetime - add_column table_name, :updated_at, :datetime + add_column table_name, :created_at, :datetime, :null => false + add_column table_name, :updated_at, :datetime, :null => false end # Removes the timestamp columns (created_at and updated_at) from the table definition. @@ -520,9 +521,29 @@ module ActiveRecord end protected + def add_index_sort_order(option_strings, column_names, options = {}) + if options.is_a?(Hash) && order = options[:order] + case order + when Hash + column_names.each {|name| option_strings[name] += " #{order[name].to_s.upcase}" if order.has_key?(name)} + when String + column_names.each {|name| option_strings[name] += " #{order.upcase}"} + end + end + + return option_strings + end + # Overridden by the mysql adapter for supporting index lengths def quoted_columns_for_index(column_names, options = {}) - column_names.map {|name| quote_column_name(name) } + option_strings = Hash[column_names.map {|name| [name, '']}] + + # add index sort order if supported + if supports_index_sort_order? + option_strings = add_index_sort_order(option_strings, column_names, options) + end + + column_names.map {|name| quote_column_name(name) + option_strings[name]} end def options_include_default?(options) diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index 443e61b527..edea414db7 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -3,12 +3,15 @@ require 'bigdecimal' require 'bigdecimal/util' require 'active_support/core_ext/benchmark' require 'active_support/deprecation' +require 'active_record/connection_adapters/schema_cache' +require 'monitor' module ActiveRecord module ConnectionAdapters # :nodoc: extend ActiveSupport::Autoload autoload :Column + autoload :ConnectionSpecification autoload_under 'abstract' do autoload :IndexDefinition, 'active_record/connection_adapters/abstract/schema_definitions' @@ -24,7 +27,6 @@ module ActiveRecord autoload :ConnectionPool autoload :ConnectionHandler, 'active_record/connection_adapters/abstract/connection_pool' autoload :ConnectionManagement, 'active_record/connection_adapters/abstract/connection_pool' - autoload :ConnectionSpecification autoload :QueryCache end @@ -47,39 +49,44 @@ module ActiveRecord include DatabaseLimits include QueryCache include ActiveSupport::Callbacks + include MonitorMixin define_callbacks :checkout, :checkin - attr_accessor :visitor - - def initialize(connection, logger = nil) #:nodoc: - @active = nil - @connection, @logger = connection, logger + attr_accessor :visitor, :pool + attr_reader :schema_cache, :last_use, :in_use + alias :in_use? :in_use + + def initialize(connection, logger = nil, pool = nil) #:nodoc: + super() + + @active = nil + @connection = connection + @in_use = false + @instrumenter = ActiveSupport::Notifications.instrumenter + @last_use = false + @logger = logger + @open_transactions = 0 + @pool = pool + @query_cache = Hash.new { |h,sql| h[sql] = {} } @query_cache_enabled = false - @query_cache = Hash.new { |h,sql| h[sql] = {} } - @open_transactions = 0 - @instrumenter = ActiveSupport::Notifications.instrumenter - @visitor = nil - end - - # Returns a visitor instance for this adaptor, which conforms to the Arel::ToSql interface - def self.visitor_for(pool) # :nodoc: - adapter = pool.spec.config[:adapter] - - if Arel::Visitors::VISITORS[adapter] - ActiveSupport::Deprecation.warn( - "Arel::Visitors::VISITORS is deprecated and will be removed. Database adapters " \ - "should define a visitor_for method which returns the appropriate visitor for " \ - "the database. For example, MysqlAdapter.visitor_for(pool) returns " \ - "Arel::Visitors::MySQL.new(pool)." - ) - - Arel::Visitors::VISITORS[adapter].new(pool) - else - Arel::Visitors::ToSql.new(pool) + @schema_cache = SchemaCache.new self + @visitor = nil + end + + def lease + synchronize do + unless in_use + @in_use = true + @last_use = Time.now + end end end + def expire + @in_use = false + end + # Returns the human-readable name of the adapter. Use mixed case - one # can always use downcase if needed. def adapter_name @@ -130,6 +137,17 @@ module ActiveRecord false end + # Does this adapter support index sort order? + def supports_index_sort_order? + false + end + + # Does this adapter support explain? As of this writing sqlite3, + # mysql2, and postgresql are the only ones that do. + def supports_explain? + false + end + # QUOTING ================================================== # Override to return the quoted table name. Defaults to column quoting. @@ -238,10 +256,19 @@ module ActiveRecord node end + def case_insensitive_comparison(table, attribute, column, value) + table[attribute].lower.eq(table.lower(value)) + end + def current_savepoint_name "active_record_#{open_transactions}" end + # Check the connection back in to the connection pool + def close + pool.checkin self + end + protected def log(sql, name = "SQL", binds = []) diff --git a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb index 4b7c74e0b8..560773ca86 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -4,6 +4,13 @@ module ActiveRecord module ConnectionAdapters class AbstractMysqlAdapter < AbstractAdapter class Column < ConnectionAdapters::Column # :nodoc: + attr_reader :collation + + def initialize(name, default, sql_type = nil, null = true, collation = nil) + super(name, default, sql_type, null) + @collation = collation + end + def extract_default(default) if sql_type =~ /blob/i || type == :text if default.blank? @@ -28,6 +35,10 @@ module ActiveRecord raise NotImplementedError end + def case_sensitive? + collation && !collation.match(/_ci$/) + end + private def simplified_type(field_type) @@ -116,10 +127,7 @@ module ActiveRecord super(connection, logger) @connection_options, @config = connection_options, config @quoted_column_names, @quoted_table_names = {}, {} - end - - def self.visitor_for(pool) # :nodoc: - Arel::Visitors::MySQL.new(pool) + @visitor = Arel::Visitors::MySQL.new self end def adapter_name #:nodoc: @@ -144,6 +152,12 @@ module ActiveRecord true end + # Technically MySQL allows to create indexes with the sort order syntax + # but at the moment (5.5) it doesn't yet implement them + def supports_index_sort_order? + true + end + def native_database_types NATIVE_DATABASE_TYPES end @@ -157,8 +171,8 @@ module ActiveRecord end # Overridden by the adapters to instantiate their specific Column type. - def new_column(field, default, type, null) # :nodoc: - Column.new(field, default, type, null) + def new_column(field, default, type, null, collation) # :nodoc: + Column.new(field, default, type, null, collation) end # Must return the Mysql error number from the exception, if the exception has an @@ -298,11 +312,11 @@ module ActiveRecord sql = "SHOW TABLES" end - select_all(sql).map do |table| + select_all(sql).map { |table| table.delete('Table_type') sql = "SHOW CREATE TABLE #{quote_table_name(table.to_a.first.last)}" exec_without_stmt(sql).first['Create Table'] + ";\n\n" - end.join("") + }.join end # Drops the database specified on the +name+ attribute @@ -349,8 +363,10 @@ module ActiveRecord show_variable 'collation_database' end - def tables(name = nil, database = nil) #:nodoc: - sql = ["SHOW TABLES", database].compact.join(' IN ') + def tables(name = nil, database = nil, like = nil) #:nodoc: + sql = "SHOW TABLES " + sql << "IN #{database} " if database + sql << "LIKE #{quote(like)}" if like execute_and_free(sql, 'SCHEMA') do |result| result.collect { |field| field.first } @@ -358,7 +374,8 @@ module ActiveRecord end def table_exists?(name) - return true if super + return false unless name + return true if tables(nil, nil, name).any? name = name.to_s schema, table = name.split('.', 2) @@ -368,7 +385,7 @@ module ActiveRecord schema = nil end - tables(nil, schema).include? table + tables(nil, schema, table).any? end # Returns an array of indexes for the given table. @@ -393,10 +410,10 @@ module ActiveRecord # Returns an array of +Column+ objects for the table specified by +table_name+. def columns(table_name, name = nil)#:nodoc: - sql = "SHOW FIELDS FROM #{quote_table_name(table_name)}" + sql = "SHOW FULL FIELDS FROM #{quote_table_name(table_name)}" execute_and_free(sql, 'SCHEMA') do |result| each_hash(result).map do |field| - new_column(field[:Field], field[:Default], field[:Type], field[:Null] == "YES") + new_column(field[:Field], field[:Default], field[:Type], field[:Null] == "YES", field[:Collation]) end end end @@ -485,9 +502,14 @@ module ActiveRecord # Returns a table's primary key and belonging sequence. def pk_and_sequence_for(table) - execute_and_free("DESCRIBE #{quote_table_name(table)}", 'SCHEMA') do |result| - keys = each_hash(result).select { |row| row[:Key] == 'PRI' }.map { |row| row[:Field] } - keys.length == 1 ? [keys.first, nil] : nil + execute_and_free("SHOW CREATE TABLE #{quote_table_name(table)}", 'SCHEMA') do |result| + create_table = each_hash(result).first[:"Create Table"] + if create_table.to_s =~ /PRIMARY KEY\s+\((.+)\)/ + keys = $1.split(",").map { |key| key.gsub(/`/, "") } + keys.length == 1 ? [keys.first, nil] : nil + else + nil + end end end @@ -501,23 +523,43 @@ module ActiveRecord Arel::Nodes::Bin.new(node) end + def case_insensitive_comparison(table, attribute, column, value) + if column.case_sensitive? + super + else + table[attribute].eq(value) + end + end + def limited_update_conditions(where_sql, quoted_table_name, quoted_primary_key) where_sql end protected + def add_index_length(option_strings, column_names, options = {}) + if options.is_a?(Hash) && length = options[:length] + case length + when Hash + column_names.each {|name| option_strings[name] += "(#{length[name]})" if length.has_key?(name)} + when Fixnum + column_names.each {|name| option_strings[name] += "(#{length})"} + end + end + + return option_strings + end + def quoted_columns_for_index(column_names, options = {}) - length = options[:length] if options.is_a?(Hash) + option_strings = Hash[column_names.map {|name| [name, '']}] - case length - when Hash - column_names.map {|name| length[name] ? "#{quote_column_name(name)}(#{length[name]})" : quote_column_name(name) } - when Fixnum - column_names.map {|name| "#{quote_column_name(name)}(#{length})"} - else - column_names.map {|name| quote_column_name(name) } - end + # add index length + option_strings = add_index_length(option_strings, column_names, options) + + # add index sort order + option_strings = add_index_sort_order(option_strings, column_names, options) + + column_names.map {|name| quote_column_name(name) + option_strings[name]} end def translate_exception(exception, message) diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb index a7856539b7..6aa4a2c5b3 100644 --- a/activerecord/lib/active_record/connection_adapters/column.rb +++ b/activerecord/lib/active_record/connection_adapters/column.rb @@ -80,9 +80,10 @@ module ActiveRecord when :decimal then klass.value_to_decimal(value) when :datetime, :timestamp then klass.string_to_time(value) when :time then klass.string_to_dummy_time(value) - when :date then klass.string_to_date(value) + when :date then klass.value_to_date(value) when :binary then klass.binary_to_string(value) when :boolean then klass.value_to_boolean(value) + when :hstore then klass.cast_hstore(value) else value end end @@ -97,9 +98,10 @@ module ActiveRecord when :decimal then "#{klass}.value_to_decimal(#{var_name})" when :datetime, :timestamp then "#{klass}.string_to_time(#{var_name})" when :time then "#{klass}.string_to_dummy_time(#{var_name})" - when :date then "#{klass}.string_to_date(#{var_name})" + when :date then "#{klass}.value_to_date(#{var_name})" when :binary then "#{klass}.binary_to_string(#{var_name})" when :boolean then "#{klass}.value_to_boolean(#{var_name})" + when :hstore then "#{klass}.cast_hstore(#{var_name})" else var_name end end @@ -132,11 +134,15 @@ module ActiveRecord value end - def string_to_date(string) - return string unless string.is_a?(String) - return nil if string.empty? - - fast_string_to_date(string) || fallback_string_to_date(string) + def value_to_date(value) + if value.is_a?(String) + return nil if value.empty? + fast_string_to_date(value) || fallback_string_to_date(value) + elsif value.respond_to?(:to_date) + value.to_date + else + value + end end def string_to_time(string) diff --git a/activerecord/lib/active_record/connection_adapters/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/connection_specification.rb new file mode 100644 index 0000000000..8491d42b86 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/connection_specification.rb @@ -0,0 +1,83 @@ +module ActiveRecord + module ConnectionAdapters + class ConnectionSpecification #:nodoc: + attr_reader :config, :adapter_method + + def initialize(config, adapter_method) + @config, @adapter_method = config, adapter_method + end + + def initialize_dup(original) + @config = original.config.dup + end + + ## + # Builds a ConnectionSpecification from user input + class Resolver # :nodoc: + attr_reader :config, :klass, :configurations + + def initialize(config, configurations) + @config = config + @configurations = configurations + end + + def spec + case config + when nil + raise AdapterNotSpecified unless defined?(Rails.env) + resolve_string_connection Rails.env + when Symbol, String + resolve_string_connection config.to_s + when Hash + resolve_hash_connection config + end + end + + private + def resolve_string_connection(spec) # :nodoc: + hash = configurations.fetch(spec) do |k| + connection_url_to_hash(k) + end + + raise(AdapterNotSpecified, "#{spec} database is not configured") unless hash + + resolve_hash_connection hash + end + + def resolve_hash_connection(spec) # :nodoc: + spec = spec.symbolize_keys + + raise(AdapterNotSpecified, "database configuration does not specify adapter") unless spec.key?(:adapter) + + begin + require "active_record/connection_adapters/#{spec[:adapter]}_adapter" + rescue LoadError => e + raise LoadError, "Please install the #{spec[:adapter]} adapter: `gem install activerecord-#{spec[:adapter]}-adapter` (#{e.message})", e.backtrace + end + + adapter_method = "#{spec[:adapter]}_connection" + + ConnectionSpecification.new(spec, adapter_method) + end + + def connection_url_to_hash(url) # :nodoc: + config = URI.parse url + adapter = config.scheme + adapter = "postgresql" if adapter == "postgres" + spec = { :adapter => adapter, + :username => config.user, + :password => config.password, + :port => config.port, + :database => config.path.sub(%r{^/},""), + :host => config.host } + spec.reject!{ |_,value| !value } + if config.query + options = Hash[config.query.split("&").map{ |pair| pair.split("=") }].symbolize_keys + spec.merge!(options) + end + spec + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index 8b574518e5..6086c32dbe 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -1,12 +1,12 @@ require 'active_record/connection_adapters/abstract_mysql_adapter' -gem 'mysql2', '~> 0.3.6' +gem 'mysql2', '~> 0.3.10' require 'mysql2' module ActiveRecord - class Base + module ConnectionHandling # Establishes a connection to the database that's used by all Active Record objects. - def self.mysql2_connection(config) + def mysql2_connection(config) config[:username] = 'root' if config[:username].nil? if Mysql2::Client.const_defined? :FOUND_ROWS @@ -35,6 +35,10 @@ module ActiveRecord configure_connection end + def supports_explain? + true + end + # HELPER METHODS =========================================== def each_hash(result) # :nodoc: @@ -47,8 +51,8 @@ module ActiveRecord end end - def new_column(field, default, type, null) # :nodoc: - Column.new(field, default, type, null) + def new_column(field, default, type, null, collation) # :nodoc: + Column.new(field, default, type, null, collation) end def error_number(exception) @@ -93,6 +97,80 @@ module ActiveRecord # DATABASE STATEMENTS ====================================== + def explain(arel, binds = []) + sql = "EXPLAIN #{to_sql(arel)}" + start = Time.now + result = exec_query(sql, 'EXPLAIN', binds) + elapsed = Time.now - start + + ExplainPrettyPrinter.new.pp(result, elapsed) + end + + class ExplainPrettyPrinter # :nodoc: + # Pretty prints the result of a EXPLAIN in a way that resembles the output of the + # MySQL shell: + # + # +----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+ + # | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | + # +----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+ + # | 1 | SIMPLE | users | const | PRIMARY | PRIMARY | 4 | const | 1 | | + # | 1 | SIMPLE | posts | ALL | NULL | NULL | NULL | NULL | 1 | Using where | + # +----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+ + # 2 rows in set (0.00 sec) + # + # This is an exercise in Ruby hyperrealism :). + def pp(result, elapsed) + widths = compute_column_widths(result) + separator = build_separator(widths) + + pp = [] + + pp << separator + pp << build_cells(result.columns, widths) + pp << separator + + result.rows.each do |row| + pp << build_cells(row, widths) + end + + pp << separator + pp << build_footer(result.rows.length, elapsed) + + pp.join("\n") + "\n" + end + + private + + def compute_column_widths(result) + [].tap do |widths| + result.columns.each_with_index do |column, i| + cells_in_column = [column] + result.rows.map {|r| r[i].nil? ? 'NULL' : r[i].to_s} + widths << cells_in_column.map(&:length).max + end + end + end + + def build_separator(widths) + padding = 1 + '+' + widths.map {|w| '-' * (w + (padding*2))}.join('+') + '+' + end + + def build_cells(items, widths) + cells = [] + items.each_with_index do |item, i| + item = 'NULL' if item.nil? + justifier = item.is_a?(Numeric) ? 'rjust' : 'ljust' + cells << item.to_s.send(justifier, widths[i]) + end + '| ' + cells.join(' | ') + ' |' + end + + def build_footer(nrows, elapsed) + rows_label = nrows == 1 ? 'row' : 'rows' + "#{nrows} #{rows_label} in set (%.2f sec)" % elapsed + end + end + # FIXME: re-enable the following once a "better" query_cache solution is in core # # The overrides below perform much better than the originals in AbstractAdapter diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index a1824fe396..e432c5af32 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -18,9 +18,9 @@ class Mysql end module ActiveRecord - class Base + module ConnectionHandling # Establishes a connection to the database that's used by all Active Record objects. - def self.mysql_connection(config) # :nodoc: + def mysql_connection(config) # :nodoc: config = config.symbolize_keys host = config[:host] port = config[:port] @@ -150,8 +150,8 @@ module ActiveRecord end end - def new_column(field, default, type, null) # :nodoc: - Column.new(field, default, type, null) + def new_column(field, default, type, null, collation) # :nodoc: + Column.new(field, default, type, null, collation) end def error_number(exception) # :nodoc: @@ -224,52 +224,48 @@ module ActiveRecord @statements.clear end - if "<3".respond_to?(:encode) - # Taken from here: - # https://github.com/tmtm/ruby-mysql/blob/master/lib/mysql/charset.rb - # Author: TOMITA Masahiro <tommy@tmtm.org> - ENCODINGS = { - "armscii8" => nil, - "ascii" => Encoding::US_ASCII, - "big5" => Encoding::Big5, - "binary" => Encoding::ASCII_8BIT, - "cp1250" => Encoding::Windows_1250, - "cp1251" => Encoding::Windows_1251, - "cp1256" => Encoding::Windows_1256, - "cp1257" => Encoding::Windows_1257, - "cp850" => Encoding::CP850, - "cp852" => Encoding::CP852, - "cp866" => Encoding::IBM866, - "cp932" => Encoding::Windows_31J, - "dec8" => nil, - "eucjpms" => Encoding::EucJP_ms, - "euckr" => Encoding::EUC_KR, - "gb2312" => Encoding::EUC_CN, - "gbk" => Encoding::GBK, - "geostd8" => nil, - "greek" => Encoding::ISO_8859_7, - "hebrew" => Encoding::ISO_8859_8, - "hp8" => nil, - "keybcs2" => nil, - "koi8r" => Encoding::KOI8_R, - "koi8u" => Encoding::KOI8_U, - "latin1" => Encoding::ISO_8859_1, - "latin2" => Encoding::ISO_8859_2, - "latin5" => Encoding::ISO_8859_9, - "latin7" => Encoding::ISO_8859_13, - "macce" => Encoding::MacCentEuro, - "macroman" => Encoding::MacRoman, - "sjis" => Encoding::SHIFT_JIS, - "swe7" => nil, - "tis620" => Encoding::TIS_620, - "ucs2" => Encoding::UTF_16BE, - "ujis" => Encoding::EucJP_ms, - "utf8" => Encoding::UTF_8, - "utf8mb4" => Encoding::UTF_8, - } - else - ENCODINGS = Hash.new { |h,k| h[k] = k } - end + # Taken from here: + # https://github.com/tmtm/ruby-mysql/blob/master/lib/mysql/charset.rb + # Author: TOMITA Masahiro <tommy@tmtm.org> + ENCODINGS = { + "armscii8" => nil, + "ascii" => Encoding::US_ASCII, + "big5" => Encoding::Big5, + "binary" => Encoding::ASCII_8BIT, + "cp1250" => Encoding::Windows_1250, + "cp1251" => Encoding::Windows_1251, + "cp1256" => Encoding::Windows_1256, + "cp1257" => Encoding::Windows_1257, + "cp850" => Encoding::CP850, + "cp852" => Encoding::CP852, + "cp866" => Encoding::IBM866, + "cp932" => Encoding::Windows_31J, + "dec8" => nil, + "eucjpms" => Encoding::EucJP_ms, + "euckr" => Encoding::EUC_KR, + "gb2312" => Encoding::EUC_CN, + "gbk" => Encoding::GBK, + "geostd8" => nil, + "greek" => Encoding::ISO_8859_7, + "hebrew" => Encoding::ISO_8859_8, + "hp8" => nil, + "keybcs2" => nil, + "koi8r" => Encoding::KOI8_R, + "koi8u" => Encoding::KOI8_U, + "latin1" => Encoding::ISO_8859_1, + "latin2" => Encoding::ISO_8859_2, + "latin5" => Encoding::ISO_8859_9, + "latin7" => Encoding::ISO_8859_13, + "macce" => Encoding::MacCentEuro, + "macroman" => Encoding::MacRoman, + "sjis" => Encoding::SHIFT_JIS, + "swe7" => nil, + "tis620" => Encoding::TIS_620, + "ucs2" => Encoding::UTF_16BE, + "ujis" => Encoding::EucJP_ms, + "utf8" => Encoding::UTF_8, + "utf8mb4" => Encoding::UTF_8, + } # Get the client encoding for this database def client_encoding diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 5402918b1d..d2361b5378 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -7,9 +7,9 @@ gem 'pg', '~> 0.11' require 'pg' module ActiveRecord - class Base + module ConnectionHandling # Establishes a connection to the database that's used by all Active Record objects - def self.postgresql_connection(config) # :nodoc: + def postgresql_connection(config) # :nodoc: config = config.symbolize_keys host = config[:host] port = config[:port] || 5432 @@ -49,6 +49,42 @@ module ActiveRecord super end end + + def cast_hstore(object) + if Hash === object + object.map { |k,v| + "#{escape_hstore(k)}=>#{escape_hstore(v)}" + }.join ', ' + else + kvs = object.scan(/(?<!\\)".*?(?<!\\)"/).map { |o| + unescape_hstore(o[1...-1]) + } + Hash[kvs.each_slice(2).to_a] + end + end + + private + HSTORE_ESCAPE = { + ' ' => '\\ ', + '\\' => '\\\\', + '"' => '\\"', + '=' => '\\=', + } + HSTORE_ESCAPE_RE = Regexp.union(HSTORE_ESCAPE.keys) + HSTORE_UNESCAPE = HSTORE_ESCAPE.invert + HSTORE_UNESCAPE_RE = Regexp.union(HSTORE_UNESCAPE.keys) + + def unescape_hstore(value) + value.gsub(HSTORE_UNESCAPE_RE) do |match| + HSTORE_UNESCAPE[match] + end + end + + def escape_hstore(value) + value.gsub(HSTORE_ESCAPE_RE) do |match| + HSTORE_ESCAPE[match] + end + end end # :startdoc: @@ -79,53 +115,55 @@ module ActiveRecord # Maps PostgreSQL-specific data types to logical Rails types. def simplified_type(field_type) case field_type - # Numeric and monetary types - when /^(?:real|double precision)$/ - :float - # Monetary types - when 'money' - :decimal - # Character types - when /^(?:character varying|bpchar)(?:\(\d+\))?$/ - :string - # Binary data types - when 'bytea' - :binary - # Date/time types - when /^timestamp with(?:out)? time zone$/ - :datetime - when 'interval' - :string - # Geometric types - when /^(?:point|line|lseg|box|"?path"?|polygon|circle)$/ - :string - # Network address types - when /^(?:cidr|inet|macaddr)$/ - :string - # Bit strings - when /^bit(?: varying)?(?:\(\d+\))?$/ - :string - # XML type - when 'xml' - :xml - # tsvector type - when 'tsvector' - :tsvector - # Arrays - when /^\D+\[\]$/ - :string - # Object identifier types - when 'oid' - :integer - # UUID type - when 'uuid' - :string - # Small and big integer types - when /^(?:small|big)int$/ - :integer - # Pass through all types that are not specific to PostgreSQL. - else - super + # Numeric and monetary types + when /^(?:real|double precision)$/ + :float + # Monetary types + when 'money' + :decimal + when 'hstore' + :hstore + # Character types + when /^(?:character varying|bpchar)(?:\(\d+\))?$/ + :string + # Binary data types + when 'bytea' + :binary + # Date/time types + when /^timestamp with(?:out)? time zone$/ + :datetime + when 'interval' + :string + # Geometric types + when /^(?:point|line|lseg|box|"?path"?|polygon|circle)$/ + :string + # Network address types + when /^(?:cidr|inet|macaddr)$/ + :string + # Bit strings + when /^bit(?: varying)?(?:\(\d+\))?$/ + :string + # XML type + when 'xml' + :xml + # tsvector type + when 'tsvector' + :tsvector + # Arrays + when /^\D+\[\]$/ + :string + # Object identifier types + when 'oid' + :integer + # UUID type + when 'uuid' + :string + # Small and big integer types + when /^(?:small|big)int$/ + :integer + # Pass through all types that are not specific to PostgreSQL. + else + super end end @@ -215,6 +253,10 @@ module ActiveRecord options = args.extract_options! column(args[0], 'tsvector', options) end + + def hstore(name, options = {}) + column(name, 'hstore', options) + end end ADAPTER_NAME = 'PostgreSQL' @@ -247,6 +289,10 @@ module ActiveRecord true end + def supports_index_sort_order? + true + end + class StatementPool < ConnectionAdapters::StatementPool def initialize(connection, max) super @@ -278,13 +324,24 @@ module ActiveRecord cache.clear end + def delete(sql_key) + dealloc cache[sql_key] + cache.delete sql_key + end + private def cache @cache[$$] end def dealloc(key) - @connection.query "DEALLOCATE #{key}" + @connection.query "DEALLOCATE #{key}" if connection_active? + end + + def connection_active? + @connection.status == PGconn::CONNECTION_OK + rescue PGError + false end end @@ -292,6 +349,7 @@ module ActiveRecord def initialize(connection, logger, connection_parameters, config) super(connection, logger) @connection_parameters, @config = connection_parameters, config + @visitor = Arel::Visitors::PostgreSQL.new self # @local_tz is initialized as nil to avoid warnings when connect tries to use it @local_tz = nil @@ -308,10 +366,6 @@ module ActiveRecord @local_tz = execute('SHOW TIME ZONE', 'SCHEMA').first["TimeZone"] end - def self.visitor_for(pool) # :nodoc: - Arel::Visitors::PostgreSQL.new(pool) - end - # Clears the prepared statements cache. def clear_cache! @statements.clear @@ -378,6 +432,11 @@ module ActiveRecord true end + # Returns true. + def supports_explain? + true + end + # Returns the configured supported identifier length supported by PostgreSQL def table_alias_length @table_alias_length ||= query('SHOW max_identifier_length')[0][0].to_i @@ -502,6 +561,48 @@ module ActiveRecord # DATABASE STATEMENTS ====================================== + def explain(arel, binds = []) + sql = "EXPLAIN #{to_sql(arel)}" + ExplainPrettyPrinter.new.pp(exec_query(sql, 'EXPLAIN', binds)) + end + + class ExplainPrettyPrinter # :nodoc: + # Pretty prints the result of a EXPLAIN in a way that resembles the output of the + # PostgreSQL shell: + # + # QUERY PLAN + # ------------------------------------------------------------------------------ + # Nested Loop Left Join (cost=0.00..37.24 rows=8 width=0) + # Join Filter: (posts.user_id = users.id) + # -> Index Scan using users_pkey on users (cost=0.00..8.27 rows=1 width=4) + # Index Cond: (id = 1) + # -> Seq Scan on posts (cost=0.00..28.88 rows=8 width=4) + # Filter: (posts.user_id = 1) + # (6 rows) + # + def pp(result) + header = result.columns.first + lines = result.rows.map(&:first) + + # We add 2 because there's one char of padding at both sides, note + # the extra hyphens in the example above. + width = [header, *lines].map(&:length).max + 2 + + pp = [] + + pp << header.center(width).rstrip + pp << '-' * width + + pp += lines.map {|line| " #{line}"} + + nrows = result.rows.length + rows_label = nrows == 1 ? 'row' : 'rows' + pp << "(#{nrows} #{rows_label})" + + pp.join("\n") + "\n" + end + end + # Executes a SELECT query and returns an array of rows. Each row is an # array of field values. def select_rows(sql, name = nil) @@ -743,16 +844,15 @@ module ActiveRecord # Returns an array of indexes for the given table. def indexes(table_name, name = nil) - schemas = schema_search_path.split(/,/).map { |p| quote(p) }.join(',') result = query(<<-SQL, name) - SELECT distinct i.relname, d.indisunique, d.indkey, t.oid + SELECT distinct i.relname, d.indisunique, d.indkey, pg_get_indexdef(d.indexrelid), t.oid FROM pg_class t INNER JOIN pg_index d ON t.oid = d.indrelid INNER JOIN pg_class i ON d.indexrelid = i.oid WHERE i.relkind = 'i' AND d.indisprimary = 'f' AND t.relname = '#{table_name}' - AND i.relnamespace IN (SELECT oid FROM pg_namespace WHERE nspname IN (#{schemas}) ) + AND i.relnamespace IN (SELECT oid FROM pg_namespace WHERE nspname = ANY (current_schemas(false)) ) ORDER BY i.relname SQL @@ -761,7 +861,8 @@ module ActiveRecord index_name = row[0] unique = row[1] == 't' indkey = row[2].split(" ") - oid = row[3] + inddef = row[3] + oid = row[4] columns = Hash[query(<<-SQL, "Columns for index #{row[0]} on #{table_name}")] SELECT a.attnum, a.attname @@ -771,7 +872,12 @@ module ActiveRecord SQL column_names = columns.values_at(*indkey).compact - column_names.empty? ? nil : IndexDefinition.new(table_name, index_name, unique, column_names) + + # add info on sort order for columns (only desc order is explicitly specified, asc is the default) + desc_order_columns = inddef.scan(/(\w+) DESC/).flatten + orders = desc_order_columns.any? ? Hash[desc_order_columns.map {|order_column| [order_column, :desc]}] : {} + + column_names.empty? ? nil : IndexDefinition.new(table_name, index_name, unique, column_names, [], orders) end.compact end @@ -815,7 +921,7 @@ module ActiveRecord # Returns the active schema search path. def schema_search_path - @schema_search_path ||= query('SHOW search_path')[0][0] + @schema_search_path ||= query('SHOW search_path', 'SCHEMA')[0][0] end # Returns the current client message level. @@ -911,12 +1017,14 @@ module ActiveRecord # Example: # rename_table('octopuses', 'octopi') def rename_table(name, new_name) + clear_cache! execute "ALTER TABLE #{quote_table_name(name)} RENAME TO #{quote_table_name(new_name)}" end # Adds a new column to the named table. # See TableDefinition#column for details of the options you can use. def add_column(table_name, column_name, type, options = {}) + clear_cache! add_column_sql = "ALTER TABLE #{quote_table_name(table_name)} ADD COLUMN #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}" add_column_options!(add_column_sql, options) @@ -925,6 +1033,7 @@ module ActiveRecord # Changes the column of a table. def change_column(table_name, column_name, type, options = {}) + clear_cache! quoted_table_name = quote_table_name(table_name) execute "ALTER TABLE #{quoted_table_name} ALTER COLUMN #{quote_column_name(column_name)} TYPE #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}" @@ -935,10 +1044,12 @@ module ActiveRecord # Changes the default value of a table column. def change_column_default(table_name, column_name, default) + clear_cache! execute "ALTER TABLE #{quote_table_name(table_name)} ALTER COLUMN #{quote_column_name(column_name)} SET DEFAULT #{quote(default)}" end def change_column_null(table_name, column_name, null, default = nil) + clear_cache! unless null || default.nil? execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL") end @@ -947,6 +1058,7 @@ module ActiveRecord # Renames a column in a table. def rename_column(table_name, column_name, new_column_name) + clear_cache! execute "ALTER TABLE #{quote_table_name(table_name)} RENAME COLUMN #{quote_column_name(column_name)} TO #{quote_column_name(new_column_name)}" end @@ -1030,26 +1142,54 @@ module ActiveRecord end private + FEATURE_NOT_SUPPORTED = "0A000" # :nodoc: + def exec_no_cache(sql, binds) @connection.async_exec(sql) end def exec_cache(sql, binds) - unless @statements.key? sql - nextkey = @statements.next_key - @connection.prepare nextkey, sql - @statements[sql] = nextkey + begin + stmt_key = prepare_statement sql + + # Clear the queue + @connection.get_last_result + @connection.send_query_prepared(stmt_key, binds.map { |col, val| + type_cast(val, col) + }) + @connection.block + @connection.get_last_result + rescue PGError => e + # Get the PG code for the failure. Annoyingly, the code for + # prepared statements whose return value may have changed is + # FEATURE_NOT_SUPPORTED. Check here for more details: + # http://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/utils/cache/plancache.c#l573 + code = e.result.result_error_field(PGresult::PG_DIAG_SQLSTATE) + if FEATURE_NOT_SUPPORTED == code + @statements.delete sql_key(sql) + retry + else + raise e + end end + end - key = @statements[sql] + # Returns the statement identifier for the client side cache + # of statements + def sql_key(sql) + "#{schema_search_path}-#{sql}" + end - # Clear the queue - @connection.get_last_result - @connection.send_query_prepared(key, binds.map { |col, val| - type_cast(val, col) - }) - @connection.block - @connection.get_last_result + # Prepare the statement if it hasn't been prepared, return + # the statement key. + def prepare_statement(sql) + sql_key = sql_key(sql) + unless @statements.key? sql_key + nextkey = @statements.next_key + @connection.prepare nextkey, sql + @statements[sql_key] = nextkey + end + @statements[sql_key] end # The internal PostgreSQL identifier of the money data type. diff --git a/activerecord/lib/active_record/connection_adapters/schema_cache.rb b/activerecord/lib/active_record/connection_adapters/schema_cache.rb new file mode 100644 index 0000000000..4e8932a695 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/schema_cache.rb @@ -0,0 +1,50 @@ +module ActiveRecord + module ConnectionAdapters + class SchemaCache + attr_reader :columns, :columns_hash, :primary_keys, :tables + attr_reader :connection + + def initialize(conn) + @connection = conn + @tables = {} + + @columns = Hash.new do |h, table_name| + h[table_name] = conn.columns(table_name, "#{table_name} Columns") + end + + @columns_hash = Hash.new do |h, table_name| + h[table_name] = Hash[columns[table_name].map { |col| + [col.name, col] + }] + end + + @primary_keys = Hash.new do |h, table_name| + h[table_name] = table_exists?(table_name) ? conn.primary_key(table_name) : nil + end + end + + # A cached lookup for table existence. + def table_exists?(name) + return @tables[name] if @tables.key? name + + @tables[name] = connection.table_exists?(name) + end + + # Clears out internal caches + def clear! + @columns.clear + @columns_hash.clear + @primary_keys.clear + @tables.clear + end + + # Clear out internal caches for table with +table_name+. + def clear_table_cache!(table_name) + @columns.delete table_name + @columns_hash.delete table_name + @primary_keys.delete table_name + @tables.delete table_name + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index 0a0da0b5d3..ee5d10859c 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -1,12 +1,12 @@ require 'active_record/connection_adapters/sqlite_adapter' -gem 'sqlite3', '~> 1.3.4' +gem 'sqlite3', '~> 1.3.5' require 'sqlite3' module ActiveRecord - class Base + module ConnectionHandling # sqlite3 adapter reuses sqlite_connection. - def self.sqlite3_connection(config) # :nodoc: + def sqlite3_connection(config) # :nodoc: # Require database. unless config[:database] raise ArgumentError, "No database file specified. Missing argument: database" @@ -47,11 +47,7 @@ module ActiveRecord # Returns the current database encoding format as a string, eg: 'UTF-8' def encoding - if @connection.respond_to?(:encoding) - @connection.encoding.to_s - else - @connection.execute('PRAGMA encoding')[0]['encoding'] - end + @connection.encoding.to_s end end diff --git a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb index 1932a849ee..69750a911d 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb @@ -16,7 +16,7 @@ module ActiveRecord end def binary_to_string(value) - if value.respond_to?(:force_encoding) && value.encoding != Encoding::ASCII_8BIT + if value.encoding != Encoding::ASCII_8BIT value = value.force_encoding(Encoding::ASCII_8BIT) end @@ -89,10 +89,7 @@ module ActiveRecord @statements = StatementPool.new(@connection, config.fetch(:statement_limit) { 1000 }) @config = config - end - - def self.visitor_for(pool) # :nodoc: - Arel::Visitors::SQLite.new(pool) + @visitor = Arel::Visitors::SQLite.new self end def adapter_name #:nodoc: @@ -125,6 +122,11 @@ module ActiveRecord true end + # Returns true. + def supports_explain? + true + end + def requires_reloading? true end @@ -157,6 +159,10 @@ module ActiveRecord sqlite_version >= '3.1.0' end + def supports_index_sort_order? + sqlite_version >= '3.3.0' + end + def native_database_types #:nodoc: { :primary_key => default_primary_key_type, @@ -195,29 +201,40 @@ module ActiveRecord end end - if "<3".encoding_aware? - def type_cast(value, column) # :nodoc: - return value.to_f if BigDecimal === value - return super unless String === value - return super unless column && value - - value = super - if column.type == :string && value.encoding == Encoding::ASCII_8BIT - @logger.error "Binary data inserted for `string` type on column `#{column.name}`" - value.encode! 'utf-8' - end - value - end - else - def type_cast(value, column) # :nodoc: - return super unless BigDecimal === value + def type_cast(value, column) # :nodoc: + return value.to_f if BigDecimal === value + return super unless String === value + return super unless column && value - value.to_f + value = super + if column.type == :string && value.encoding == Encoding::ASCII_8BIT + @logger.error "Binary data inserted for `string` type on column `#{column.name}`" + value.encode! 'utf-8' end + value end # DATABASE STATEMENTS ====================================== + def explain(arel, binds = []) + sql = "EXPLAIN QUERY PLAN #{to_sql(arel)}" + ExplainPrettyPrinter.new.pp(exec_query(sql, 'EXPLAIN', binds)) + end + + class ExplainPrettyPrinter + # Pretty prints the result of a EXPLAIN QUERY PLAN in a way that resembles + # the output of the SQLite shell: + # + # 0|0|0|SEARCH TABLE users USING INTEGER PRIMARY KEY (rowid=?) (~1 rows) + # 0|1|1|SCAN TABLE posts (~100000 rows) + # + def pp(result) # :nodoc: + result.rows.map do |row| + row.join('|') + end.join("\n") + "\n" + end + end + def exec_query(sql, name = nil, binds = []) log(sql, name, binds) do @@ -304,18 +321,23 @@ module ActiveRecord # SCHEMA STATEMENTS ======================================== - def tables(name = 'SCHEMA') #:nodoc: + def tables(name = 'SCHEMA', table_name = nil) #:nodoc: sql = <<-SQL SELECT name FROM sqlite_master WHERE type = 'table' AND NOT name = 'sqlite_sequence' SQL + sql << " AND name = #{quote_table_name(table_name)}" if table_name exec_query(sql, name).map do |row| row['name'] end end + def table_exists?(name) + name && tables('SCHEMA', name).any? + end + # Returns an array of +SQLiteColumn+ objects for the table specified by +table_name+. def columns(table_name, name = nil) #:nodoc: table_structure(table_name).map do |field| @@ -413,6 +435,8 @@ module ActiveRecord self.limit = options[:limit] if options.include?(:limit) self.default = options[:default] if include_default self.null = options[:null] if options.include?(:null) + self.precision = options[:precision] if options.include?(:precision) + self.scale = options[:scale] if options.include?(:scale) end end end @@ -467,6 +491,7 @@ module ActiveRecord @definition.column(column_name, column.type, :limit => column.limit, :default => column.default, + :precision => column.precision, :scale => column.scale, :null => column.null) end @definition.primary_key(primary_key(from)) if primary_key(from) diff --git a/activerecord/lib/active_record/connection_handling.rb b/activerecord/lib/active_record/connection_handling.rb new file mode 100644 index 0000000000..e9cd3d5e57 --- /dev/null +++ b/activerecord/lib/active_record/connection_handling.rb @@ -0,0 +1,100 @@ +require 'active_support/core_ext/module/delegation' + +module ActiveRecord + module ConnectionHandling + # Establishes the connection to the database. Accepts a hash as input where + # the <tt>:adapter</tt> 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", + # :database => "path/to/dbfile" + # ) + # + # Also accepts keys as strings (for parsing from YAML for example): + # + # ActiveRecord::Base.establish_connection( + # "adapter" => "sqlite", + # "database" => "path/to/dbfile" + # ) + # + # Or a URL: + # + # ActiveRecord::Base.establish_connection( + # "postgres://myuser:mypass@localhost/somedatabase" + # ) + # + # The exceptions AdapterNotSpecified, AdapterNotFound and ArgumentError + # may be returned on an error. + def establish_connection(spec = ENV["DATABASE_URL"]) + resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new spec, configurations + spec = resolver.spec + + unless respond_to?(spec.adapter_method) + raise AdapterNotFound, "database configuration specifies nonexistent #{spec.config[:adapter]} adapter" + end + + remove_connection + connection_handler.establish_connection name, spec + end + + # 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 connection + retrieve_connection + end + + def connection_id + Thread.current['ActiveRecord::Base.connection_id'] + end + + def connection_id=(connection_id) + Thread.current['ActiveRecord::Base.connection_id'] = connection_id + end + + # Returns the configuration of the associated connection as a hash: + # + # ActiveRecord::Base.connection_config + # # => {:pool=>5, :timeout=>5000, :database=>"db/development.sqlite3", :adapter=>"sqlite3"} + # + # Please use only for reading. + def connection_config + connection_pool.spec.config + end + + def connection_pool + connection_handler.retrieve_connection_pool(self) or raise ConnectionNotEstablished + end + + def retrieve_connection + connection_handler.retrieve_connection(self) + end + + # Returns true if Active Record is connected. + def connected? + connection_handler.connected?(self) + end + + def remove_connection(klass = self) + connection_handler.remove_connection(klass) + end + + def clear_active_connections! + connection_handler.clear_active_connections! + end + + delegate :clear_reloadable_connections!, + :clear_all_connections!,:verify_active_connections!, :to => :connection_handler + end +end diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb new file mode 100644 index 0000000000..980f8fe50f --- /dev/null +++ b/activerecord/lib/active_record/core.rb @@ -0,0 +1,354 @@ +require 'active_support/concern' + +module ActiveRecord + module Core + extend ActiveSupport::Concern + + included do + ## + # :singleton-method: + # 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+. + config_attribute :logger, :global => true + + ## + # :singleton-method: + # Contains the database configuration - as is typically stored in config/database.yml - + # as a Hash. + # + # For example, the following database.yml... + # + # development: + # adapter: sqlite3 + # database: db/development.sqlite3 + # + # production: + # adapter: sqlite3 + # database: db/production.sqlite3 + # + # ...would result in ActiveRecord::Base.configurations to look like this: + # + # { + # 'development' => { + # 'adapter' => 'sqlite3', + # 'database' => 'db/development.sqlite3' + # }, + # 'production' => { + # 'adapter' => 'sqlite3', + # 'database' => 'db/production.sqlite3' + # } + # } + config_attribute :configurations, :global => true + self.configurations = {} + + ## + # :singleton-method: + # Determines whether to use Time.local (using :local) or Time.utc (using :utc) when pulling + # dates and times from the database. This is set to :local by default. + config_attribute :default_timezone, :global => true + self.default_timezone = :local + + ## + # :singleton-method: + # Specifies the format to use when dumping the database schema with Rails' + # Rakefile. If :sql, the schema is dumped as (potentially database- + # specific) SQL statements. If :ruby, the schema is dumped as an + # ActiveRecord::Schema file which can be loaded into any database that + # supports migrations. Use :ruby if you want to have different database + # adapters for, e.g., your development and test environments. + config_attribute :schema_format, :global => true + self.schema_format = :ruby + + ## + # :singleton-method: + # Specify whether or not to use timestamps for migration versions + config_attribute :timestamped_migrations, :global => true + self.timestamped_migrations = true + + ## + # :singleton-method: + # The connection handler + config_attribute :connection_handler + self.connection_handler = ConnectionAdapters::ConnectionHandler.new + end + + module ClassMethods + def inherited(child_class) #:nodoc: + child_class.initialize_generated_modules + super + end + + def initialize_generated_modules + # force attribute methods to be higher in inheritance hierarchy than other generated methods + generated_attribute_methods + generated_feature_methods + end + + def generated_feature_methods + @generated_feature_methods ||= begin + mod = const_set(:GeneratedFeatureMethods, Module.new) + include mod + mod + end + end + + # Returns a string like 'Post(id:integer, title:string, body:text)' + def inspect + if self == Base + super + elsif abstract_class? + "#{super}(abstract)" + elsif table_exists? + attr_list = columns.map { |c| "#{c.name}: #{c.type}" } * ', ' + "#{super}(#{attr_list})" + else + "#{super}(Table doesn't exist)" + end + end + + # Overwrite the default class equality method to provide support for association proxies. + def ===(object) + object.is_a?(self) + end + + def arel_table + @arel_table ||= Arel::Table.new(table_name, arel_engine) + end + + def arel_engine + @arel_engine ||= connection_handler.connection_pools[name] ? self : active_record_super.arel_engine + end + + private + + def relation #:nodoc: + @relation ||= Relation.new(self, arel_table) + + if finder_needs_type_condition? + @relation.where(type_condition).create_with(inheritance_column.to_sym => sti_name) + else + @relation + end + end + end + + # 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. + # + # +initialize+ respects mass-assignment security and accepts either +:as+ or +:without_protection+ options + # in the +options+ parameter. + # + # ==== Examples + # # Instantiates a single new object + # User.new(:first_name => 'Jamie') + # + # # Instantiates a single new object using the :admin mass-assignment security role + # User.new({ :first_name => 'Jamie', :is_admin => true }, :as => :admin) + # + # # Instantiates a single new object bypassing mass-assignment security + # User.new({ :first_name => 'Jamie', :is_admin => true }, :without_protection => true) + def initialize(attributes = nil, options = {}) + @attributes = self.class.initialize_attributes(self.class.column_defaults.dup) + @association_cache = {} + @aggregation_cache = {} + @attributes_cache = {} + @new_record = true + @readonly = false + @destroyed = false + @marked_for_destruction = false + @previously_changed = {} + @changed_attributes = {} + @relation = nil + + ensure_proper_type + + populate_with_current_scope_attributes + + assign_attributes(attributes, options) if attributes + + yield self if block_given? + run_callbacks :initialize + end + + # Initialize an empty model object from +coder+. +coder+ must contain + # the attributes necessary for initializing an empty model object. For + # example: + # + # class Post < ActiveRecord::Base + # end + # + # post = Post.allocate + # post.init_with('attributes' => { 'title' => 'hello world' }) + # post.title # => 'hello world' + def init_with(coder) + @attributes = self.class.initialize_attributes(coder['attributes']) + @relation = nil + + @attributes_cache, @previously_changed, @changed_attributes = {}, {}, {} + @association_cache = {} + @aggregation_cache = {} + @readonly = @destroyed = @marked_for_destruction = false + @new_record = false + run_callbacks :find + run_callbacks :initialize + + self + end + + # Duped objects have no id assigned and are treated as new records. Note + # that this is a "shallow" copy as it copies the object's attributes + # only, not its associations. The extent of a "deep" copy is application + # specific and is therefore left to the application to implement according + # to its need. + # The dup method does not preserve the timestamps (created|updated)_(at|on). + def initialize_dup(other) + cloned_attributes = other.clone_attributes(:read_attribute_before_type_cast) + cloned_attributes.delete(self.class.primary_key) + + @attributes = cloned_attributes + + _run_after_initialize_callbacks if respond_to?(:_run_after_initialize_callbacks) + + @changed_attributes = {} + self.class.column_defaults.each do |attr, orig_value| + @changed_attributes[attr] = orig_value if field_changed?(attr, orig_value, @attributes[attr]) + end + + @aggregation_cache = {} + @association_cache = {} + @attributes_cache = {} + @new_record = true + + ensure_proper_type + populate_with_current_scope_attributes + super + end + + # Populate +coder+ with attributes about this record that should be + # serialized. The structure of +coder+ defined in this method is + # guaranteed to match the structure of +coder+ passed to the +init_with+ + # method. + # + # Example: + # + # class Post < ActiveRecord::Base + # end + # coder = {} + # Post.new.encode_with(coder) + # coder # => { 'id' => nil, ... } + def encode_with(coder) + coder['attributes'] = attributes + end + + # Returns true if +comparison_object+ is the same exact object, or +comparison_object+ + # is of the same type and +self+ has an ID and it is equal to +comparison_object.id+. + # + # Note that new records are different from any other record by definition, unless the + # other record is the receiver itself. Besides, if you fetch existing records with + # +select+ and leave the ID out, you're on your own, this predicate will return false. + # + # Note also that destroying a record preserves its ID in the model instance, so deleted + # models are still comparable. + def ==(comparison_object) + super || + comparison_object.instance_of?(self.class) && + id.present? && + comparison_object.id == id + end + alias :eql? :== + + # 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.hash + end + + # Freeze the attributes hash such that associations are still accessible, even on destroyed records. + def freeze + @attributes.freeze; self + end + + # Returns +true+ if the attributes hash has been frozen. + def frozen? + @attributes.frozen? + end + + # Allows sort on objects + def <=>(other_object) + if other_object.is_a?(self.class) + self.to_key <=> other_object.to_key + else + nil + end + end + + # Returns +true+ if the record is read only. Records loaded through joins with piggy-back + # attributes will be marked as read only since they cannot be saved. + def readonly? + @readonly + end + + # Marks this record as read only. + def readonly! + @readonly = true + 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 + + # Returns the contents of the record as a nicely formatted string. + def inspect + inspection = if @attributes + self.class.column_names.collect { |name| + if has_attribute?(name) + "#{name}: #{attribute_for_inspect(name)}" + end + }.compact.join(", ") + else + "not initialized" + end + "#<#{self.class} #{inspection}>" + end + + # Hackery to accomodate Syck. Remove for 4.0. + def to_yaml(opts = {}) #:nodoc: + if YAML.const_defined?(:ENGINE) && !YAML::ENGINE.syck? + super + else + coder = {} + encode_with(coder) + YAML.quick_emit(self, opts) do |out| + out.map(taguri, to_yaml_style) do |map| + coder.each { |k, v| map.add(k, v) } + end + end + end + end + + # Hackery to accomodate Syck. Remove for 4.0. + def yaml_initialize(tag, coder) #:nodoc: + init_with(coder) + end + + private + + # Under Ruby 1.9, Array#flatten will call #to_ary (recursively) on each of the elements + # of the array, and then rescues from the possible NoMethodError. If those elements are + # ActiveRecord::Base's, then this triggers the various method_missing's that we have, + # which significantly impacts upon performance. + # + # So we can avoid the method_missing hit by explicitly defining #to_ary as nil here. + # + # See also http://tenderlovemaking.com/2011/06/28/til-its-ok-to-return-nil-from-to_ary/ + def to_ary # :nodoc: + nil + end + end +end diff --git a/activerecord/lib/active_record/counter_cache.rb b/activerecord/lib/active_record/counter_cache.rb index 3c7defedac..031918712a 100644 --- a/activerecord/lib/active_record/counter_cache.rb +++ b/activerecord/lib/active_record/counter_cache.rb @@ -65,7 +65,7 @@ module ActiveRecord # Post.update_counters [10, 15], :comment_count => 1 # # Executes the following SQL: # # UPDATE posts - # # SET comment_count = COALESCE(comment_count, 0) + 1, + # # SET comment_count = COALESCE(comment_count, 0) + 1 # # WHERE id IN (10, 15) def update_counters(id, counters) updates = counters.map do |counter_name, value| diff --git a/activerecord/lib/active_record/dynamic_matchers.rb b/activerecord/lib/active_record/dynamic_matchers.rb new file mode 100644 index 0000000000..e9068089f0 --- /dev/null +++ b/activerecord/lib/active_record/dynamic_matchers.rb @@ -0,0 +1,79 @@ +module ActiveRecord + module DynamicMatchers + def respond_to?(method_id, include_private = false) + if match = DynamicFinderMatch.match(method_id) + return true if all_attributes_exists?(match.attribute_names) + elsif match = DynamicScopeMatch.match(method_id) + return true if all_attributes_exists?(match.attribute_names) + end + + super + end + + private + + # Enables dynamic finders like <tt>User.find_by_user_name(user_name)</tt> and + # <tt>User.scoped_by_user_name(user_name). Refer to Dynamic attribute-based finders + # section at the top of this file for more detailed information. + # + # It's even possible to use all the additional parameters to +find+. For example, the + # full interface for +find_all_by_amount+ is actually <tt>find_all_by_amount(amount, options)</tt>. + # + # Each dynamic finder using <tt>scoped_by_*</tt> is also defined in the class after it + # is first invoked, so that future attempts to use it do not run through method_missing. + def method_missing(method_id, *arguments, &block) + if match = (DynamicFinderMatch.match(method_id) || DynamicScopeMatch.match(method_id)) + attribute_names = match.attribute_names + super unless all_attributes_exists?(attribute_names) + if arguments.size < attribute_names.size + method_trace = "#{__FILE__}:#{__LINE__}:in `#{method_id}'" + backtrace = [method_trace] + caller + raise ArgumentError, "wrong number of arguments (#{arguments.size} for #{attribute_names.size})", backtrace + end + if match.respond_to?(:scope?) && match.scope? + self.class_eval <<-METHOD, __FILE__, __LINE__ + 1 + def self.#{method_id}(*args) # def self.scoped_by_user_name_and_password(*args) + attributes = Hash[[:#{attribute_names.join(',:')}].zip(args)] # attributes = Hash[[:user_name, :password].zip(args)] + # + scoped(:conditions => attributes) # scoped(:conditions => attributes) + end # end + METHOD + send(method_id, *arguments) + elsif match.finder? + options = arguments.extract_options! + relation = options.any? ? scoped(options) : scoped + relation.send :find_by_attributes, match, attribute_names, *arguments, &block + elsif match.instantiator? + scoped.send :find_or_instantiator_by_attributes, match, attribute_names, *arguments, &block + end + else + super + end + end + + # Similar in purpose to +expand_hash_conditions_for_aggregates+. + def expand_attribute_names_for_aggregates(attribute_names) + attribute_names.map { |attribute_name| + unless (aggregation = reflect_on_aggregation(attribute_name.to_sym)).nil? + aggregate_mapping(aggregation).map do |field_attr, _| + field_attr.to_sym + end + else + attribute_name.to_sym + end + }.flatten + end + + def all_attributes_exists?(attribute_names) + (expand_attribute_names_for_aggregates(attribute_names) - + column_methods_hash.keys).empty? + end + + def aggregate_mapping(reflection) + mapping = reflection.options[:mapping] || [reflection.name, reflection.name] + mapping.first.is_a?(Array) ? mapping : [mapping] + end + + + end +end diff --git a/activerecord/lib/active_record/errors.rb b/activerecord/lib/active_record/errors.rb index 8262b60f6e..fc80f3081e 100644 --- a/activerecord/lib/active_record/errors.rb +++ b/activerecord/lib/active_record/errors.rb @@ -99,6 +99,16 @@ module ActiveRecord # # Read more about optimistic locking in ActiveRecord::Locking module RDoc. class StaleObjectError < ActiveRecordError + attr_reader :record, :attempted_action + + def initialize(record, attempted_action) + @record = record + @attempted_action = attempted_action + end + + def message + "Attempted to #{attempted_action} a stale object: #{record.class.name}" + end end # Raised when association is being configured improperly or @@ -170,8 +180,7 @@ module ActiveRecord end end - # Raised when a model attempts to fetch its primary key from the database, but the table - # has no primary key declared. + # Raised when a primary key is needed, but there is not one specified in the schema or model. class UnknownPrimaryKey < ActiveRecordError attr_reader :model diff --git a/activerecord/lib/active_record/explain.rb b/activerecord/lib/active_record/explain.rb new file mode 100644 index 0000000000..e502d7e52b --- /dev/null +++ b/activerecord/lib/active_record/explain.rb @@ -0,0 +1,80 @@ +require 'active_support/core_ext/class/attribute' + +module ActiveRecord + module Explain + def self.extended(base) + # If a query takes longer than these many seconds we log its query plan + # automatically. nil disables this feature. + base.config_attribute :auto_explain_threshold_in_seconds, :global => true + end + + # If auto explain is enabled, this method triggers EXPLAIN logging for the + # queries triggered by the block if it takes more than the threshold as a + # whole. That is, the threshold is not checked against each individual + # query, but against the duration of the entire block. This approach is + # convenient for relations. + # + # The available_queries_for_explain thread variable collects the queries + # to be explained. If the value is nil, it means queries are not being + # currently collected. A false value indicates collecting is turned + # off. Otherwise it is an array of queries. + def logging_query_plan # :nodoc: + threshold = auto_explain_threshold_in_seconds + current = Thread.current + if threshold && current[:available_queries_for_explain].nil? + begin + queries = current[:available_queries_for_explain] = [] + start = Time.now + result = yield + logger.warn(exec_explain(queries)) if Time.now - start > threshold + result + ensure + current[:available_queries_for_explain] = nil + end + else + yield + end + end + + # Relation#explain needs to be able to collect the queries regardless of + # whether auto explain is enabled. This method serves that purpose. + def collecting_queries_for_explain # :nodoc: + current = Thread.current + original, current[:available_queries_for_explain] = current[:available_queries_for_explain], [] + return yield, current[:available_queries_for_explain] + ensure + # Note that the return value above does not depend on this assigment. + current[:available_queries_for_explain] = original + end + + # Makes the adapter execute EXPLAIN for the tuples of queries and bindings. + # Returns a formatted string ready to be logged. + def exec_explain(queries) # :nodoc: + queries && queries.map do |sql, bind| + [].tap do |msg| + msg << "EXPLAIN for: #{sql}" + unless bind.empty? + bind_msg = bind.map {|col, val| [col.name, val]}.inspect + msg.last << " #{bind_msg}" + end + msg << connection.explain(sql, bind) + end.join("\n") + end.join("\n") + end + + # Silences automatic EXPLAIN logging for the duration of the block. + # + # This has high priority, no EXPLAINs will be run even if downwards + # the threshold is set to 0. + # + # As the name of the method suggests this only applies to automatic + # EXPLAINs, manual calls to +ActiveRecord::Relation#explain+ run. + def silence_auto_explain + current = Thread.current + original, current[:available_queries_for_explain] = current[:available_queries_for_explain], false + yield + ensure + current[:available_queries_for_explain] = original + end + end +end diff --git a/activerecord/lib/active_record/explain_subscriber.rb b/activerecord/lib/active_record/explain_subscriber.rb new file mode 100644 index 0000000000..fc76410499 --- /dev/null +++ b/activerecord/lib/active_record/explain_subscriber.rb @@ -0,0 +1,21 @@ +require 'active_support/notifications' + +module ActiveRecord + class ExplainSubscriber # :nodoc: + def call(*args) + if queries = Thread.current[:available_queries_for_explain] + payload = args.last + queries << payload.values_at(:sql, :binds) unless ignore_payload?(payload) + end + end + + # SCHEMA queries cannot be EXPLAINed, also we do not want to run EXPLAIN on + # our own EXPLAINs now matter how loopingly beautiful that would be. + IGNORED_PAYLOADS = %w(SCHEMA EXPLAIN) + def ignore_payload?(payload) + payload[:exception] || IGNORED_PAYLOADS.include?(payload[:name]) + end + + ActiveSupport::Notifications.subscribe("sql.active_record", new) + end +end diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index 97af15c9e8..c59c00f424 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -10,7 +10,6 @@ require 'zlib' require 'active_support/dependencies' require 'active_support/core_ext/array/wrap' require 'active_support/core_ext/object/blank' -require 'active_support/core_ext/logger' require 'active_support/ordered_hash' require 'active_record/fixtures/file' @@ -22,8 +21,6 @@ else end end -class FixturesFileNotFound < StandardError; end - module ActiveRecord # \Fixtures are a way of organizing data that you want to test against; in short, sample data. # @@ -391,10 +388,15 @@ module ActiveRecord @@all_cached_fixtures = Hash.new { |h,k| h[k] = {} } - def self.find_table_name(table_name) # :nodoc: + def self.default_fixture_model_name(fixture_name) # :nodoc: ActiveRecord::Base.pluralize_table_names ? - table_name.to_s.singularize.camelize : - table_name.to_s.camelize + fixture_name.singularize.camelize : + fixture_name.camelize + end + + def self.default_fixture_table_name(fixture_name) # :nodoc: + "#{ActiveRecord::Base.table_name_prefix}"\ + "#{fixture_name.tr('/', '_')}#{ActiveRecord::Base.table_name_suffix}".to_sym end def self.reset_cache @@ -444,9 +446,6 @@ module ActiveRecord def self.create_fixtures(fixtures_directory, table_names, class_names = {}) table_names = [table_names].flatten.map { |n| n.to_s } - table_names.each { |n| - class_names[n.tr('/', '_').to_sym] = n.classify if n.include?('/') - } # FIXME: Apparently JK uses this. connection = block_given? ? yield : ActiveRecord::Base.connection @@ -460,12 +459,12 @@ module ActiveRecord fixtures_map = {} fixture_files = files_to_read.map do |path| - table_name = path.tr '/', '_' + fixture_name = path - fixtures_map[path] = ActiveRecord::Fixtures.new( + fixtures_map[fixture_name] = new( # ActiveRecord::Fixtures.new connection, - table_name, - class_names[table_name.to_sym] || table_name.classify, + fixture_name, + class_names[fixture_name] || default_fixture_model_name(fixture_name), ::File.join(fixtures_directory, path)) end @@ -489,8 +488,8 @@ module ActiveRecord # Cap primary key sequences to max(pk). if connection.respond_to?(:reset_pk_sequence!) - table_names.each do |table_name| - connection.reset_pk_sequence!(table_name.tr('/', '_')) + fixture_files.each do |ff| + connection.reset_pk_sequence!(ff.table_name) end end end @@ -509,25 +508,27 @@ module ActiveRecord attr_reader :table_name, :name, :fixtures, :model_class - def initialize(connection, table_name, class_name, fixture_path) + def initialize(connection, fixture_name, class_name, fixture_path) @connection = connection - @table_name = table_name @fixture_path = fixture_path - @name = table_name # preserve fixture base name + @name = fixture_name @class_name = class_name @fixtures = ActiveSupport::OrderedHash.new - @table_name = "#{ActiveRecord::Base.table_name_prefix}#{@table_name}#{ActiveRecord::Base.table_name_suffix}" # Should be an AR::Base type class if class_name.is_a?(Class) - @table_name = class_name.table_name - @connection = class_name.connection - @model_class = class_name + @model_class = class_name else - @model_class = class_name.constantize rescue nil + @model_class = class_name.constantize rescue nil end + @connection = model_class.connection if model_class && model_class.respond_to?(:connection) + + @table_name = ( model_class.respond_to?(:table_name) ? + model_class.table_name : + self.class.default_fixture_table_name(fixture_name) ) + read_fixture_files end @@ -562,7 +563,7 @@ module ActiveRecord rows[table_name] = fixtures.map do |label, fixture| row = fixture.to_hash - if model_class && model_class < ActiveRecord::Base + if model_class && model_class < ActiveRecord::Model # fill in timestamp columns if they aren't specified and the model is set to record_timestamps if model_class.record_timestamps timestamp_column_names.each do |name| @@ -622,7 +623,7 @@ module ActiveRecord private def primary_key_name - @primary_key_name ||= model_class && model_class.primary_key? && model_class.primary_key + @primary_key_name ||= model_class && model_class.primary_key end def has_primary_key_column? @@ -644,14 +645,6 @@ module ActiveRecord end def read_fixture_files - if ::File.file?(yaml_file_path) - read_yaml_fixture_files - else - raise FixturesFileNotFound, "Could not find #{yaml_file_path}" - end - end - - def read_yaml_fixture_files yaml_files = Dir["#{@fixture_path}/**/*.yml"].select { |f| ::File.file?(f) } + [yaml_file_path] @@ -734,14 +727,29 @@ module ActiveRecord self.use_instantiated_fixtures = false self.pre_loaded_fixtures = false - self.fixture_class_names = Hash.new do |h, table_name| - h[table_name] = ActiveRecord::Fixtures.find_table_name(table_name) + self.fixture_class_names = Hash.new do |h, fixture_name| + h[fixture_name] = ActiveRecord::Fixtures.default_fixture_model_name(fixture_name) end end module ClassMethods + # Sets the model class for a fixture when the class name cannot be inferred from the fixture name. + # + # Examples: + # + # set_fixture_class :some_fixture => SomeModel, + # 'namespaced/fixture' => Another::Model + # + # The keys must be the fixture names, that coincide with the short paths to the fixture files. + #-- + # It is also possible to pass the class name instead of the class: + # set_fixture_class 'some_fixture' => 'SomeModel' + # I think this option is redundant, i propose to deprecate it. + # Isn't it easier to always pass the class itself? + # (2011-12-20 alexeymuranov) + #++ def set_fixture_class(class_names = {}) - self.fixture_class_names = self.fixture_class_names.merge(class_names) + self.fixture_class_names = self.fixture_class_names.merge(class_names.stringify_keys) end def fixtures(*fixture_names) @@ -781,9 +789,9 @@ module ActiveRecord fixture_names = Array.wrap(fixture_names || fixture_table_names) methods = Module.new do fixture_names.each do |fixture_name| - fixture_name = fixture_name.to_s.tr('./', '_') + accessor_name = fixture_name.tr('/', '_').to_sym - define_method(fixture_name) do |*fixtures| + define_method(accessor_name) do |*fixtures| force_reload = fixtures.pop if fixtures.last == true || fixtures.last == :reload @fixture_cache[fixture_name] ||= {} @@ -796,13 +804,13 @@ module ActiveRecord @fixture_cache[fixture_name][fixture] ||= @loaded_fixtures[fixture_name][fixture.to_s].find end else - raise StandardError, "No fixture with name '#{fixture}' found for table '#{fixture_name}'" + raise StandardError, "No entry named '#{fixture}' found for fixture collection '#{fixture_name}'" end end instances.size == 1 ? instances.first : instances end - private fixture_name + private accessor_name end end include methods @@ -832,6 +840,7 @@ module ActiveRecord end @fixture_cache = {} + @fixture_connections = [] @@already_loaded_fixtures ||= {} # Load fixtures once and begin transaction. @@ -842,9 +851,12 @@ module ActiveRecord @loaded_fixtures = load_fixtures @@already_loaded_fixtures[self.class] = @loaded_fixtures end - ActiveRecord::Base.connection.increment_open_transactions - ActiveRecord::Base.connection.transaction_joinable = false - ActiveRecord::Base.connection.begin_db_transaction + @fixture_connections = enlist_fixture_connections + @fixture_connections.each do |connection| + connection.increment_open_transactions + connection.transaction_joinable = false + connection.begin_db_transaction + end # Load fixtures for every test. else ActiveRecord::Fixtures.reset_cache @@ -864,13 +876,22 @@ module ActiveRecord end # Rollback changes if a transaction is active. - if run_in_transaction? && ActiveRecord::Base.connection.open_transactions != 0 - ActiveRecord::Base.connection.rollback_db_transaction - ActiveRecord::Base.connection.decrement_open_transactions + if run_in_transaction? + @fixture_connections.each do |connection| + if connection.open_transactions != 0 + connection.rollback_db_transaction + connection.decrement_open_transactions + end + end + @fixture_connections.clear end ActiveRecord::Base.clear_active_connections! end + def enlist_fixture_connections + ActiveRecord::Base.connection_handler.connection_pools.values.map(&:connection) + end + private def load_fixtures fixtures = ActiveRecord::Fixtures.create_fixtures(fixture_path, fixture_table_names, fixture_class_names) diff --git a/activerecord/lib/active_record/identity_map.rb b/activerecord/lib/active_record/identity_map.rb index b15b5a8133..680d9ffea0 100644 --- a/activerecord/lib/active_record/identity_map.rb +++ b/activerecord/lib/active_record/identity_map.rb @@ -111,13 +111,12 @@ module ActiveRecord # model object. def reinit_with(coder) @attributes_cache = {} - dirty = @changed_attributes.keys - @attributes.update(coder['attributes'].except(*dirty)) + dirty = @changed_attributes.keys + attributes = self.class.initialize_attributes(coder['attributes'].except(*dirty)) + @attributes.update(attributes) @changed_attributes.update(coder['attributes'].slice(*dirty)) @changed_attributes.delete_if{|k,v| v.eql? @attributes[k]} - set_serialized_attributes - run_callbacks :find self diff --git a/activerecord/lib/active_record/inheritance.rb b/activerecord/lib/active_record/inheritance.rb new file mode 100644 index 0000000000..eaa7deac5a --- /dev/null +++ b/activerecord/lib/active_record/inheritance.rb @@ -0,0 +1,182 @@ +require 'active_support/concern' + +module ActiveRecord + module Inheritance + extend ActiveSupport::Concern + + included do + # Determine whether to store the full constant name including namespace when using STI + config_attribute :store_full_sti_class + self.store_full_sti_class = true + end + + module ClassMethods + # True if this isn't a concrete subclass needing a STI type condition. + def descends_from_active_record? + sup = active_record_super + + if sup.abstract_class? + sup.descends_from_active_record? + elsif self == Base + false + else + [Base, Model].include?(sup) || !columns_hash.include?(inheritance_column) + end + end + + def finder_needs_type_condition? #:nodoc: + # This is like this because benchmarking justifies the strange :false stuff + :true == (@finder_needs_type_condition ||= descends_from_active_record? ? :false : :true) + end + + def symbolized_base_class + @symbolized_base_class ||= base_class.to_s.to_sym + end + + def symbolized_sti_name + @symbolized_sti_name ||= sti_name.present? ? sti_name.to_sym : symbolized_base_class + end + + # Returns the base AR subclass that this class descends from. If A + # extends AR::Base, A.base_class will return A. If B descends from A + # through some arbitrarily deep hierarchy, B.base_class will return A. + # + # If B < A and C < B and if A is an abstract_class then both B.base_class + # and C.base_class would return B as the answer since A is an abstract_class. + def base_class + class_of_active_record_descendant(self) + end + + # Set this to true if this is an abstract class (see <tt>abstract_class?</tt>). + attr_accessor :abstract_class + + # Returns whether this class is an abstract class or not. + def abstract_class? + defined?(@abstract_class) && @abstract_class == true + end + + def sti_name + store_full_sti_class ? name : name.demodulize + end + + # 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) + sti_class = find_sti_class(record[inheritance_column]) + record_id = sti_class.primary_key && record[sti_class.primary_key] + + if ActiveRecord::IdentityMap.enabled? && record_id + if (column = sti_class.columns_hash[sti_class.primary_key]) && column.number? + record_id = record_id.to_i + end + if instance = IdentityMap.get(sti_class, record_id) + instance.reinit_with('attributes' => record) + else + instance = sti_class.allocate.init_with('attributes' => record) + IdentityMap.add(instance) + end + else + instance = sti_class.allocate.init_with('attributes' => record) + end + + instance + end + + # For internal use. + # + # If this class includes ActiveRecord::Model then it won't have a + # superclass. So this provides a way to get to the 'root' (ActiveRecord::Model). + def active_record_super #:nodoc: + superclass < Model ? superclass : Model + end + + protected + + # Returns the class descending directly from ActiveRecord::Base or an + # abstract class, if any, in the inheritance hierarchy. + def class_of_active_record_descendant(klass) + unless klass < Model + raise ActiveRecordError, "#{name} doesn't belong in a hierarchy descending from ActiveRecord" + end + + sup = klass.active_record_super + if [Base, Model].include?(klass) || [Base, Model].include?(sup) || sup.abstract_class? + klass + else + class_of_active_record_descendant(sup) + end + end + + # Returns the class type of the record using the current module as a prefix. So descendants of + # MyApp::Business::Account would appear as MyApp::Business::AccountSubclass. + def compute_type(type_name) + if type_name.match(/^::/) + # If the type is prefixed with a scope operator then we assume that + # the type_name is an absolute reference. + ActiveSupport::Dependencies.constantize(type_name) + else + # Build a list of candidates to search for + candidates = [] + name.scan(/::|$/) { candidates.unshift "#{$`}::#{type_name}" } + candidates << type_name + + candidates.each do |candidate| + begin + constant = ActiveSupport::Dependencies.constantize(candidate) + return constant if candidate == constant.to_s + rescue NameError => e + # We don't want to swallow NoMethodError < NameError errors + raise e unless e.instance_of?(NameError) + end + end + + raise NameError, "uninitialized constant #{candidates.first}" + end + end + + private + + def find_sti_class(type_name) + if type_name.blank? || !columns_hash.include?(inheritance_column) + self + else + begin + if store_full_sti_class + ActiveSupport::Dependencies.constantize(type_name) + else + compute_type(type_name) + end + rescue NameError + raise SubclassNotFound, + "The single-table inheritance mechanism failed to locate the subclass: '#{type_name}'. " + + "This error is raised because the column '#{inheritance_column}' is reserved for storing the class in case of inheritance. " + + "Please rename this column if you didn't intend it to be used for storing the inheritance class " + + "or overwrite #{name}.inheritance_column to use another column for that information." + end + end + end + + def type_condition(table = arel_table) + sti_column = table[inheritance_column.to_sym] + sti_names = ([self] + descendants).map { |model| model.sti_name } + + sti_column.in(sti_names) + end + end + + private + + # Sets the attribute used for single table inheritance to this class name if this is not the + # ActiveRecord::Base descendant. + # Considering the hierarchy Reply < Message < ActiveRecord::Base, this makes it possible to + # do Reply.new without having to set <tt>Reply[Reply.inheritance_column] = "Reply"</tt> yourself. + # No such attribute would be set for objects of the Message class in that example. + def ensure_proper_type + klass = self.class + if klass.finder_needs_type_condition? + write_attribute(klass.inheritance_column, klass.sti_name) + end + end + end +end diff --git a/activerecord/lib/active_record/integration.rb b/activerecord/lib/active_record/integration.rb new file mode 100644 index 0000000000..2c42f4cca5 --- /dev/null +++ b/activerecord/lib/active_record/integration.rb @@ -0,0 +1,49 @@ +module ActiveRecord + module Integration + # Returns a String, which Action Pack uses for constructing an URL to this + # object. The default implementation returns this record's id as a String, + # or nil if this record's unsaved. + # + # For example, suppose that you have a User model, and that you have a + # <tt>resources :users</tt> route. Normally, +user_path+ will + # construct a path with the user object's 'id' in it: + # + # user = User.find_by_name('Phusion') + # user_path(user) # => "/users/1" + # + # You can override +to_param+ in your model to make +user_path+ construct + # a path using the user's name instead of the user's id: + # + # class User < ActiveRecord::Base + # def to_param # overridden + # name + # end + # end + # + # user = User.find_by_name('Phusion') + # user_path(user) # => "/users/Phusion" + def to_param + # We can't use alias_method here, because method 'id' optimizes itself on the fly. + id && id.to_s # Be sure to stringify the id for routes + end + + # Returns a cache key that can be used to identify this record. + # + # ==== Examples + # + # Product.new.cache_key # => "products/new" + # Product.find(5).cache_key # => "products/5" (updated_at not available) + # Person.find(5).cache_key # => "people/5-20071224150000" (updated_at available) + def cache_key + case + when new_record? + "#{self.class.model_name.cache_key}/new" + when timestamp = self[:updated_at] + timestamp = timestamp.utc.to_s(:number) + "#{self.class.model_name.cache_key}/#{id}-#{timestamp}" + else + "#{self.class.model_name.cache_key}/#{id}" + end + end + end +end diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb index d9ad7e4132..e643c0d437 100644 --- a/activerecord/lib/active_record/locking/optimistic.rb +++ b/activerecord/lib/active_record/locking/optimistic.rb @@ -37,6 +37,9 @@ module ActiveRecord # You're then responsible for dealing with the conflict by rescuing the exception and either rolling back, merging, # or otherwise apply the business logic needed to resolve the conflict. # + # This locking mechanism will function inside a single Ruby process. To make it work across all + # web requests, the recommended approach is to add +lock_version+ as a hidden field to your form. + # # You must ensure that your database schema defaults the +lock_version+ column to 0. # # This behavior can be turned off by setting <tt>ActiveRecord::Base.lock_optimistically = false</tt>. @@ -46,12 +49,8 @@ module ActiveRecord extend ActiveSupport::Concern included do - cattr_accessor :lock_optimistically, :instance_writer => false + config_attribute :lock_optimistically, :global => true self.lock_optimistically = true - - class << self - alias_method :locking_column=, :set_locking_column - end end def locking_enabled? #:nodoc: @@ -65,21 +64,6 @@ module ActiveRecord send(lock_col + '=', previous_lock_value + 1) end - def attributes_from_column_definition - result = super - - # If the locking column has no default value set, - # start the lock version at zero. Note we can't use - # <tt>locking_enabled?</tt> at this point as - # <tt>@attributes</tt> may not have been initialized yet. - - if result.key?(self.class.locking_column) && lock_optimistically - result[self.class.locking_column] ||= 0 - end - - result - end - def update(attribute_names = @attributes.keys) #:nodoc: return super unless locking_enabled? return 0 if attribute_names.empty? @@ -103,7 +87,7 @@ module ActiveRecord affected_rows = connection.update stmt unless affected_rows == 1 - raise ActiveRecord::StaleObjectError, "Attempted to update a stale object: #{self.class.name}" + raise ActiveRecord::StaleObjectError.new(self, "update") end affected_rows @@ -127,7 +111,7 @@ module ActiveRecord affected_rows = self.class.unscoped.where(predicate).delete_all unless affected_rows == 1 - raise ActiveRecord::StaleObjectError, "Attempted to delete a stale object: #{self.class.name}" + raise ActiveRecord::StaleObjectError.new(self, "destroy") end end @@ -146,14 +130,15 @@ module ActiveRecord end # Set the column to use for optimistic locking. Defaults to +lock_version+. - def set_locking_column(value = nil, &block) - define_attr_method :locking_column, value, &block - value + def locking_column=(value) + @original_locking_column = @locking_column if defined?(@locking_column) + @locking_column = value.to_s end # The version column used for optimistic locking. Defaults to +lock_version+. def locking_column - reset_locking_column + reset_locking_column unless defined?(@locking_column) + @locking_column end # Quote the column name used for optimistic locking. @@ -163,7 +148,7 @@ module ActiveRecord # Reset the column used for optimistic locking back to the +lock_version+ default. def reset_locking_column - set_locking_column DEFAULT_LOCKING_COLUMN + self.locking_column = DEFAULT_LOCKING_COLUMN end # Make sure the lock version column gets updated when counters are @@ -172,6 +157,18 @@ module ActiveRecord counters = counters.merge(locking_column => 1) if locking_enabled? super end + + # If the locking column has no default value set, + # start the lock version at zero. Note we can't use + # <tt>locking_enabled?</tt> at this point as + # <tt>@attributes</tt> may not have been initialized yet. + def initialize_attributes(attributes) #:nodoc: + if attributes.key?(locking_column) && lock_optimistically + attributes[locking_column] ||= 0 + end + + attributes + end end end end diff --git a/activerecord/lib/active_record/log_subscriber.rb b/activerecord/lib/active_record/log_subscriber.rb index 3a015ee8c2..a25f2c7bca 100644 --- a/activerecord/lib/active_record/log_subscriber.rb +++ b/activerecord/lib/active_record/log_subscriber.rb @@ -26,9 +26,9 @@ module ActiveRecord return if 'SCHEMA' == payload[:name] - name = '%s (%.1fms)' % [payload[:name], event.duration] - sql = payload[:sql].squeeze(' ') - binds = nil + name = '%s (%.1fms)' % [payload[:name], event.duration] + sql = payload[:sql].squeeze(' ') + binds = nil unless (payload[:binds] || []).empty? binds = " " + payload[:binds].map { |col,v| diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index 7166f1b82a..46464783fd 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -112,12 +112,13 @@ module ActiveRecord # a column but keeps the type and content. # * <tt>change_column(table_name, column_name, type, options)</tt>: Changes # the column to a different type using the same parameters as add_column. - # * <tt>remove_column(table_name, column_name)</tt>: Removes the column named - # +column_name+ from the table called +table_name+. + # * <tt>remove_column(table_name, column_names)</tt>: Removes the column listed in + # +column_names+ from the table called +table_name+. # * <tt>add_index(table_name, column_names, options)</tt>: Adds a new index # with the name of the column. Other options include - # <tt>:name</tt> and <tt>:unique</tt> (e.g. - # <tt>{ :name => "users_name_index", :unique => true }</tt>). + # <tt>:name</tt>, <tt>:unique</tt> (e.g. + # <tt>{ :name => "users_name_index", :unique => true }</tt>) and <tt>:order</tt> + # (e.g. { :order => {:name => :desc} }</tt>). # * <tt>remove_index(table_name, :column => column_name)</tt>: Removes the index # specified by +column_name+. # * <tt>remove_index(table_name, :name => index_name)</tt>: Removes the index @@ -301,7 +302,7 @@ module ActiveRecord # # class TenderloveMigration < ActiveRecord::Migration # def change - # create_table(:horses) do + # create_table(:horses) do |t| # t.column :content, :text # t.column :remind_at, :datetime # end @@ -442,6 +443,7 @@ module ActiveRecord say_with_time "#{method}(#{arg_list})" do unless arguments.empty? || method == :execute arguments[0] = Migrator.proper_table_name(arguments.first) + arguments[1] = Migrator.proper_table_name(arguments.second) if method == :rename_table end return super unless connection.respond_to?(method) connection.send(method, *arguments, &block) @@ -455,26 +457,28 @@ module ActiveRecord destination_migrations = ActiveRecord::Migrator.migrations(destination) last = destination_migrations.last - sources.each do |name, path| + sources.each do |scope, path| source_migrations = ActiveRecord::Migrator.migrations(path) source_migrations.each do |migration| source = File.read(migration.filename) - source = "# This migration comes from #{name} (originally #{migration.version})\n#{source}" + source = "# This migration comes from #{scope} (originally #{migration.version})\n#{source}" if duplicate = destination_migrations.detect { |m| m.name == migration.name } - options[:on_skip].call(name, migration) if File.read(duplicate.filename) != source && options[:on_skip] + if options[:on_skip] && duplicate.scope != scope.to_s + options[:on_skip].call(scope, migration) + end next end migration.version = next_migration_number(last ? last.version + 1 : 0).to_i - new_path = File.join(destination, "#{migration.version}_#{migration.name.underscore}.rb") + new_path = File.join(destination, "#{migration.version}_#{migration.name.underscore}.#{scope}.rb") old_path, migration.filename = migration.filename, new_path last = migration - FileUtils.cp(old_path, migration.filename) + File.open(migration.filename, "w") { |f| f.write source } copied << migration - options[:on_copy].call(name, migration, old_path) if options[:on_copy] + options[:on_copy].call(scope, migration, old_path) if options[:on_copy] destination_migrations << migration end end @@ -493,9 +497,9 @@ module ActiveRecord # MigrationProxy is used to defer loading of the actual migration classes # until they are needed - class MigrationProxy < Struct.new(:name, :version, :filename) + class MigrationProxy < Struct.new(:name, :version, :filename, :scope) - def initialize(name, version, filename) + def initialize(name, version, filename, scope) super @migration = nil end @@ -524,16 +528,16 @@ module ActiveRecord attr_writer :migrations_paths alias :migrations_path= :migrations_paths= - def migrate(migrations_paths, target_version = nil) + def migrate(migrations_paths, target_version = nil, &block) case when target_version.nil? - up(migrations_paths, target_version) + up(migrations_paths, target_version, &block) when current_version == 0 && target_version == 0 [] when current_version > target_version - down(migrations_paths, target_version) + down(migrations_paths, target_version, &block) else - up(migrations_paths, target_version) + up(migrations_paths, target_version, &block) end end @@ -545,12 +549,12 @@ module ActiveRecord move(:up, migrations_paths, steps) end - def up(migrations_paths, target_version = nil) - self.new(:up, migrations_paths, target_version).migrate + def up(migrations_paths, target_version = nil, &block) + self.new(:up, migrations_paths, target_version).migrate(&block) end - def down(migrations_paths, target_version = nil) - self.new(:down, migrations_paths, target_version).migrate + def down(migrations_paths, target_version = nil, &block) + self.new(:down, migrations_paths, target_version).migrate(&block) end def run(direction, migrations_paths, target_version) @@ -590,15 +594,16 @@ module ActiveRecord migrations_paths.first end - def migrations(paths) + def migrations(paths, subdirectories = true) paths = Array.wrap(paths) - files = Dir[*paths.map { |p| "#{p}/[0-9]*_*.rb" }] + glob = subdirectories ? "**/" : "" + files = Dir[*paths.map { |p| "#{p}/#{glob}[0-9]*_*.rb" }] seen = Hash.new false migrations = files.map do |file| - version, name = file.scan(/([0-9]+)_([_a-z0-9]*).rb/).first + version, name, scope = file.scan(/([0-9]+)_([_a-z0-9]*)\.?([_a-z0-9]*)?.rb/).first raise IllegalMigrationNameError.new(file) unless version version = version.to_i @@ -609,7 +614,7 @@ module ActiveRecord seen[version] = seen[name] = true - MigrationProxy.new(name, version, file) + MigrationProxy.new(name, version, file, scope) end migrations.sort_by(&:version) @@ -652,7 +657,7 @@ module ActiveRecord end end - def migrate + def migrate(&block) current = migrations.detect { |m| m.version == current_version } target = migrations.detect { |m| m.version == @target_version } @@ -669,6 +674,10 @@ module ActiveRecord ran = [] runnable.each do |migration| + if block && !block.call(migration) + next + end + Base.logger.info "Migrating to #{migration.name} (#{migration.version})" if Base.logger seen = migrated.include?(migration.version.to_i) diff --git a/activerecord/lib/active_record/model.rb b/activerecord/lib/active_record/model.rb new file mode 100644 index 0000000000..86de5ab2fa --- /dev/null +++ b/activerecord/lib/active_record/model.rb @@ -0,0 +1,109 @@ +require 'active_support/deprecation' + +module ActiveRecord + # <tt>ActiveRecord::Model</tt> can be included into a class to add Active Record persistence. + # This is an alternative to inheriting from <tt>ActiveRecord::Base</tt>. Example: + # + # class Post + # include ActiveRecord::Model + # end + # + module Model + module ClassMethods #:nodoc: + include ActiveSupport::Callbacks::ClassMethods + include ActiveModel::Naming + include QueryCache::ClassMethods + include ActiveSupport::Benchmarkable + include ActiveSupport::DescendantsTracker + + include Querying + include Translation + include DynamicMatchers + include CounterCache + include Explain + include ConnectionHandling + end + + def self.included(base) + return if base.singleton_class < ClassMethods + + base.class_eval do + extend ClassMethods + Callbacks::Register.setup(self) + initialize_generated_modules unless self == Base + end + end + + extend ActiveModel::Configuration + extend ActiveModel::Callbacks + extend ActiveModel::MassAssignmentSecurity::ClassMethods + extend ActiveModel::AttributeMethods::ClassMethods + extend Callbacks::Register + extend Explain + extend ConnectionHandling + + def self.extend(*modules) + ClassMethods.send(:include, *modules) + end + + include Persistence + include ReadonlyAttributes + include ModelSchema + include Inheritance + include Scoping + include Sanitization + include Integration + include AttributeAssignment + include ActiveModel::Conversion + include Validations + include Locking::Optimistic, Locking::Pessimistic + include AttributeMethods + include Callbacks, ActiveModel::Observing, Timestamp + include Associations + include IdentityMap + include ActiveModel::SecurePassword + include AutosaveAssociation, NestedAttributes + include Aggregations, Transactions, Reflection, Serialization, Store + include Core + + class << self + def arel_engine + self + end + + def abstract_class? + false + end + + def inheritance_column + 'type' + end + end + + module DeprecationProxy #:nodoc: + class << self + instance_methods.each { |m| undef_method m unless m =~ /^__|^object_id$|^instance_eval$/ } + + def method_missing(name, *args, &block) + if Model.respond_to?(name) + Model.send(name, *args, &block) + else + ActiveSupport::Deprecation.warn( + "The object passed to the active_record load hook was previously ActiveRecord::Base " \ + "(a Class). Now it is ActiveRecord::Model (a Module). You have called `#{name}' which " \ + "is only defined on ActiveRecord::Base. Please change your code so that it works with " \ + "a module rather than a class. (Model is included in Base, so anything added to Model " \ + "will be available on Base as well.)" + ) + Base.send(name, *args, &block) + end + end + + alias send method_missing + end + end + end + + # Load Base at this point, because the active_record load hook is run in that file. + Base +end diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb new file mode 100644 index 0000000000..c1a8583119 --- /dev/null +++ b/activerecord/lib/active_record/model_schema.rb @@ -0,0 +1,303 @@ +require 'active_support/concern' +require 'active_support/core_ext/class/attribute_accessors' + +module ActiveRecord + module ModelSchema + extend ActiveSupport::Concern + + included do + ## + # :singleton-method: + # 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. + config_attribute :primary_key_prefix_type, :global => true + + ## + # :singleton-method: + # 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 convenient way of creating a namespace for tables in a shared database. + # By default, the prefix is the empty string. + # + # If you are organising your models within modules you can add a prefix to the models within + # a namespace by defining a singleton method in the parent module called table_name_prefix which + # returns your chosen prefix. + config_attribute :table_name_prefix + self.table_name_prefix = "" + + ## + # :singleton-method: + # 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. + config_attribute :table_name_suffix + self.table_name_suffix = "" + + ## + # :singleton-method: + # Indicates whether table names should be the pluralized versions of the corresponding class names. + # If true, 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. + config_attribute :pluralize_table_names + self.pluralize_table_names = true + end + + module ClassMethods + # Guesses the table name (in forced lower-case) based on the name of the class in the + # inheritance hierarchy descending directly from ActiveRecord::Base. So if the hierarchy + # looks like: Reply < Message < ActiveRecord::Base, then Message is used + # to guess the table name even when called on Reply. The rules used to do the guess + # are handled by the Inflector class in Active Support, which knows almost all common + # English inflections. You can add new inflections in config/initializers/inflections.rb. + # + # Nested classes are given table names prefixed by the singular form of + # the parent's table name. Enclosing modules are not considered. + # + # ==== Examples + # + # class Invoice < ActiveRecord::Base + # end + # + # file class table_name + # invoice.rb Invoice invoices + # + # class Invoice < ActiveRecord::Base + # class Lineitem < ActiveRecord::Base + # end + # end + # + # file class table_name + # invoice.rb Invoice::Lineitem invoice_lineitems + # + # module Invoice + # class Lineitem < ActiveRecord::Base + # end + # end + # + # file class table_name + # invoice/lineitem.rb Invoice::Lineitem lineitems + # + # Additionally, the class-level +table_name_prefix+ is prepended and the + # +table_name_suffix+ is appended. So if you have "myapp_" as a prefix, + # the table name guess for an Invoice class becomes "myapp_invoices". + # Invoice::Lineitem becomes "myapp_invoice_lineitems". + # + # You can also set your own table name explicitly: + # + # class Mouse < ActiveRecord::Base + # self.table_name = "mice" + # end + # + # Alternatively, you can override the table_name method to define your + # own computation. (Possibly using <tt>super</tt> to manipulate the default + # table name.) Example: + # + # class Post < ActiveRecord::Base + # def self.table_name + # "special_" + super + # end + # end + # Post.table_name # => "special_posts" + def table_name + reset_table_name unless defined?(@table_name) + @table_name + end + + # Sets the table name explicitly. Example: + # + # class Project < ActiveRecord::Base + # self.table_name = "project" + # end + # + # You can also just define your own <tt>self.table_name</tt> method; see + # the documentation for ActiveRecord::Base#table_name. + def table_name=(value) + @original_table_name = @table_name if defined?(@table_name) + @table_name = value + @quoted_table_name = nil + @arel_table = nil + @relation = Relation.new(self, arel_table) + end + + # Returns a quoted version of the table name, used to construct SQL statements. + def quoted_table_name + @quoted_table_name ||= connection.quote_table_name(table_name) + end + + # Computes the table name, (re)sets it internally, and returns it. + def reset_table_name #:nodoc: + if active_record_super.abstract_class? + self.table_name = active_record_super.table_name || compute_table_name + elsif abstract_class? + self.table_name = active_record_super == Base ? nil : active_record_super.table_name + else + self.table_name = compute_table_name + end + end + + def full_table_name_prefix #:nodoc: + (parents.detect{ |p| p.respond_to?(:table_name_prefix) } || self).table_name_prefix + end + + # The name of the column containing the object's class when Single Table Inheritance is used + def inheritance_column + (@inheritance_column ||= nil) || active_record_super.inheritance_column + end + + # Sets the value of inheritance_column + def inheritance_column=(value) + @original_inheritance_column = inheritance_column + @inheritance_column = value.to_s + end + + def sequence_name + if base_class == self + @sequence_name ||= reset_sequence_name + else + (@sequence_name ||= nil) || base_class.sequence_name + end + end + + def reset_sequence_name #:nodoc: + self.sequence_name = connection.default_sequence_name(table_name, primary_key) + end + + # Sets the name of the sequence to use when generating ids to the given + # value, or (if the value is nil or false) to the value returned by the + # given block. This is required for Oracle and is useful for any + # database which relies on sequences for primary key generation. + # + # If a sequence name is not explicitly set when using Oracle or Firebird, + # it will default to the commonly used pattern of: #{table_name}_seq + # + # If a sequence name is not explicitly set when using PostgreSQL, it + # will discover the sequence corresponding to your primary key for you. + # + # class Project < ActiveRecord::Base + # self.sequence_name = "projectseq" # default would have been "project_seq" + # end + def sequence_name=(value) + @original_sequence_name = @sequence_name if defined?(@sequence_name) + @sequence_name = value.to_s + end + + # Indicates whether the table associated with this class exists + def table_exists? + connection.schema_cache.table_exists?(table_name) + end + + # Returns an array of column objects for the table associated with this class. + def columns + @columns ||= connection.schema_cache.columns[table_name].map do |col| + col = col.dup + col.primary = (col.name == primary_key) + col + end + end + + # Returns a hash of column objects for the table associated with this class. + def columns_hash + @columns_hash ||= Hash[columns.map { |c| [c.name, c] }] + end + + # Returns a hash where the keys are column names and the values are + # default values when instantiating the AR object for this table. + def column_defaults + @column_defaults ||= Hash[columns.map { |c| [c.name, c.default] }] + end + + # Returns an array of column names as strings. + def column_names + @column_names ||= columns.map { |column| column.name } + end + + # Returns an array of column objects where the primary id, all columns ending in "_id" or "_count", + # and columns used for single table inheritance have been removed. + def content_columns + @content_columns ||= columns.reject { |c| c.primary || 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 #:nodoc: + @dynamic_methods_hash ||= column_names.inject(Hash.new(false)) do |methods, attr| + attr_name = attr.to_s + methods[attr.to_sym] = attr_name + methods["#{attr}=".to_sym] = attr_name + methods["#{attr}?".to_sym] = attr_name + methods["#{attr}_before_type_cast".to_sym] = attr_name + methods + end + end + + # Resets all the cached information about columns, which will cause them + # to be reloaded on the next request. + # + # The most common usage pattern for this method is probably in a migration, + # when just after creating a table you want to populate it with some default + # values, eg: + # + # class CreateJobLevels < ActiveRecord::Migration + # def up + # create_table :job_levels do |t| + # t.integer :id + # t.string :name + # + # t.timestamps + # end + # + # JobLevel.reset_column_information + # %w{assistant executive manager director}.each do |type| + # JobLevel.create(:name => type) + # end + # end + # + # def down + # drop_table :job_levels + # end + # end + def reset_column_information + connection.clear_cache! + undefine_attribute_methods + connection.schema_cache.clear_table_cache!(table_name) if table_exists? + + @column_names = @content_columns = @column_defaults = @columns = @columns_hash = nil + @dynamic_methods_hash = @inheritance_column = nil + @arel_engine = @relation = nil + end + + def clear_cache! # :nodoc: + connection.schema_cache.clear! + end + + private + + # Guesses the table name, but does not decorate it with prefix and suffix information. + def undecorated_table_name(class_name = base_class.name) + table_name = class_name.to_s.demodulize.underscore + table_name = table_name.pluralize if pluralize_table_names + table_name + end + + # Computes and returns a table name according to default conventions. + def compute_table_name + base = base_class + if self == base + # Nested classes are prefixed with singular parent table name. + if parent < ActiveRecord::Model && !parent.abstract_class? + contained = parent.table_name + contained = contained.singularize if parent.pluralize_table_names + contained += '_' + end + "#{full_table_name_prefix}#{contained}#{undecorated_table_name(name)}#{table_name_suffix}" + else + # STI subclasses always use their superclass' table. + base.table_name + end + end + end + end +end diff --git a/activerecord/lib/active_record/named_scope.rb b/activerecord/lib/active_record/named_scope.rb deleted file mode 100644 index 0313abe456..0000000000 --- a/activerecord/lib/active_record/named_scope.rb +++ /dev/null @@ -1,200 +0,0 @@ -require 'active_support/core_ext/array' -require 'active_support/core_ext/hash/except' -require 'active_support/core_ext/kernel/singleton_class' -require 'active_support/core_ext/object/blank' -require 'active_support/core_ext/class/attribute' - -module ActiveRecord - # = Active Record Named \Scopes - module NamedScope - extend ActiveSupport::Concern - - module ClassMethods - # Returns an anonymous \scope. - # - # posts = Post.scoped - # posts.size # Fires "select count(*) from posts" and returns the count - # posts.each {|p| puts p.name } # Fires "select * from posts" and loads post objects - # - # fruits = Fruit.scoped - # fruits = fruits.where(:color => 'red') if options[:red_only] - # fruits = fruits.limit(10) if limited? - # - # Anonymous \scopes tend to be useful when procedurally generating complex - # queries, where passing intermediate values (\scopes) around as first-class - # objects is convenient. - # - # You can define a \scope that applies to all finders using - # ActiveRecord::Base.default_scope. - def scoped(options = nil) - if options - scoped.apply_finder_options(options) - else - if current_scope - current_scope.clone - else - scope = relation.clone - scope.default_scoped = true - scope - end - end - end - - ## - # Collects attributes from scopes that should be applied when creating - # an AR instance for the particular class this is called on. - def scope_attributes # :nodoc: - if current_scope - current_scope.scope_for_create - else - scope = relation.clone - scope.default_scoped = true - scope.scope_for_create - end - end - - ## - # Are there default attributes associated with this scope? - def scope_attributes? # :nodoc: - current_scope || default_scopes.any? - end - - # Adds a class method for retrieving and querying objects. A \scope represents a narrowing of a database query, - # such as <tt>where(:color => :red).select('shirts.*').includes(:washing_instructions)</tt>. - # - # class Shirt < ActiveRecord::Base - # scope :red, where(:color => 'red') - # scope :dry_clean_only, joins(:washing_instructions).where('washing_instructions.dry_clean_only = ?', true) - # end - # - # The above calls to <tt>scope</tt> define class methods Shirt.red and Shirt.dry_clean_only. Shirt.red, - # in effect, represents the query <tt>Shirt.where(:color => 'red')</tt>. - # - # Note that this is simply 'syntactic sugar' for defining an actual class method: - # - # class Shirt < ActiveRecord::Base - # def self.red - # where(:color => 'red') - # end - # end - # - # Unlike <tt>Shirt.find(...)</tt>, however, the object returned by Shirt.red is not an Array; it - # resembles the association object constructed by a <tt>has_many</tt> declaration. For instance, - # you can invoke <tt>Shirt.red.first</tt>, <tt>Shirt.red.count</tt>, <tt>Shirt.red.where(:size => 'small')</tt>. - # Also, just as with the association objects, named \scopes act like an Array, implementing Enumerable; - # <tt>Shirt.red.each(&block)</tt>, <tt>Shirt.red.first</tt>, and <tt>Shirt.red.inject(memo, &block)</tt> - # all behave as if Shirt.red really was an Array. - # - # These named \scopes are composable. For instance, <tt>Shirt.red.dry_clean_only</tt> will produce - # all shirts that are both red and dry clean only. - # Nested finds and calculations also work with these compositions: <tt>Shirt.red.dry_clean_only.count</tt> - # returns the number of garments for which these criteria obtain. Similarly with - # <tt>Shirt.red.dry_clean_only.average(:thread_count)</tt>. - # - # All \scopes are available as class methods on the ActiveRecord::Base descendant upon which - # the \scopes were defined. But they are also available to <tt>has_many</tt> associations. If, - # - # class Person < ActiveRecord::Base - # has_many :shirts - # end - # - # then <tt>elton.shirts.red.dry_clean_only</tt> will return all of Elton's red, dry clean - # only shirts. - # - # Named \scopes can also be procedural: - # - # class Shirt < ActiveRecord::Base - # scope :colored, lambda { |color| where(:color => color) } - # end - # - # In this example, <tt>Shirt.colored('puce')</tt> finds all puce shirts. - # - # On Ruby 1.9 you can use the 'stabby lambda' syntax: - # - # scope :colored, ->(color) { where(:color => color) } - # - # Note that scopes defined with \scope will be evaluated when they are defined, rather than - # when they are used. For example, the following would be incorrect: - # - # class Post < ActiveRecord::Base - # scope :recent, where('published_at >= ?', Time.now - 1.week) - # end - # - # The example above would be 'frozen' to the <tt>Time.now</tt> value when the <tt>Post</tt> - # class was defined, and so the resultant SQL query would always be the same. The correct - # way to do this would be via a lambda, which will re-evaluate the scope each time - # it is called: - # - # class Post < ActiveRecord::Base - # scope :recent, lambda { where('published_at >= ?', Time.now - 1.week) } - # end - # - # Named \scopes can also have extensions, just as with <tt>has_many</tt> declarations: - # - # class Shirt < ActiveRecord::Base - # scope :red, where(:color => 'red') do - # def dom_id - # 'red_shirts' - # end - # end - # end - # - # Scopes can also be used while creating/building a record. - # - # class Article < ActiveRecord::Base - # scope :published, where(:published => true) - # end - # - # Article.published.new.published # => true - # Article.published.create.published # => true - # - # Class methods on your model are automatically available - # on scopes. Assuming the following setup: - # - # class Article < ActiveRecord::Base - # scope :published, where(:published => true) - # scope :featured, where(:featured => true) - # - # def self.latest_article - # order('published_at desc').first - # end - # - # def self.titles - # map(&:title) - # end - # - # end - # - # We are able to call the methods like this: - # - # Article.published.featured.latest_article - # Article.featured.titles - - def scope(name, scope_options = {}) - name = name.to_sym - valid_scope_name?(name) - extension = Module.new(&Proc.new) if block_given? - - scope_proc = lambda do |*args| - options = scope_options.respond_to?(:call) ? scope_options.call(*args) : scope_options - options = scoped.apply_finder_options(options) if options.is_a?(Hash) - - relation = scoped.merge(options) - - extension ? relation.extending(extension) : relation - end - - singleton_class.send(:redefine_method, name, &scope_proc) - end - - protected - - def valid_scope_name?(name) - if respond_to?(name, true) - logger.warn "Creating scope :#{name}. " \ - "Overwriting existing method #{self.name}.#{name}." - end - end - end - end -end diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb index 2dbebfcaf8..a8ee43b598 100644 --- a/activerecord/lib/active_record/nested_attributes.rb +++ b/activerecord/lib/active_record/nested_attributes.rb @@ -12,7 +12,7 @@ module ActiveRecord extend ActiveSupport::Concern included do - class_attribute :nested_attributes_options, :instance_writer => false + config_attribute :nested_attributes_options self.nested_attributes_options = {} end @@ -220,7 +220,7 @@ module ActiveRecord # validates_presence_of :member # end module ClassMethods - REJECT_ALL_BLANK_PROC = proc { |attributes| attributes.all? { |_, value| value.blank? } } + REJECT_ALL_BLANK_PROC = proc { |attributes| attributes.all? { |key, value| key == '_destroy' || value.blank? } } # Defines an attributes writer for the specified association(s). If you # are using <tt>attr_protected</tt> or <tt>attr_accessible</tt>, then you @@ -239,7 +239,8 @@ module ActiveRecord # is specified, a record will be built for all attribute hashes that # do not have a <tt>_destroy</tt> value that evaluates to true. # Passing <tt>:all_blank</tt> instead of a Proc will create a proc - # that will reject a record where all the attributes are blank. + # that will reject a record where all the attributes are blank excluding + # any value for _destroy. # [:limit] # Allows you to specify the maximum number of the associated records that # can be processed with the nested attributes. If the size of the diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index b5dadb929d..09ee2ba61d 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -1,6 +1,53 @@ +require 'active_support/concern' + module ActiveRecord # = Active Record Persistence module Persistence + extend ActiveSupport::Concern + + module ClassMethods + # Creates an object (or multiple objects) and saves it to the database, if validations pass. + # The resulting object is returned whether the object was saved successfully to the database or not. + # + # The +attributes+ parameter can be either a Hash or an Array of Hashes. These Hashes describe the + # attributes on the objects that are to be created. + # + # +create+ respects mass-assignment security and accepts either +:as+ or +:without_protection+ options + # in the +options+ parameter. + # + # ==== Examples + # # Create a single new object + # User.create(:first_name => 'Jamie') + # + # # Create a single new object using the :admin mass-assignment security role + # User.create({ :first_name => 'Jamie', :is_admin => true }, :as => :admin) + # + # # Create a single new object bypassing mass-assignment security + # User.create({ :first_name => 'Jamie', :is_admin => true }, :without_protection => true) + # + # # Create an Array of new objects + # User.create([{ :first_name => 'Jamie' }, { :first_name => 'Jeremy' }]) + # + # # Create a single object and pass it into a block to set other attributes. + # User.create(:first_name => 'Jamie') do |u| + # u.is_admin = false + # end + # + # # Creating an Array of new objects using a block, where the block is executed for each object: + # User.create([{ :first_name => 'Jamie' }, { :first_name => 'Jeremy' }]) do |u| + # u.is_admin = false + # end + def create(attributes = nil, options = {}, &block) + if attributes.is_a?(Array) + attributes.collect { |attr| create(attr, options, &block) } + else + object = new(attributes, options, &block) + object.save + object + end + end + end + # Returns true if this object hasn't been saved yet -- that is, a record # for the object doesn't exist in the data store yet; otherwise, returns false. def new_record? @@ -12,8 +59,8 @@ module ActiveRecord @destroyed end - # Returns if the record is persisted, i.e. it's not a new record and it was - # not destroyed. + # Returns true if the record is persisted, i.e. it's not a new record and it was + # not destroyed, otherwise returns false. def persisted? !(new_record? || destroyed?) end @@ -114,6 +161,7 @@ module ActiveRecord became.instance_variable_set("@attributes_cache", @attributes_cache) became.instance_variable_set("@new_record", new_record?) became.instance_variable_set("@destroyed", destroyed?) + became.instance_variable_set("@errors", errors) became.type = klass.name unless self.class.descends_from_active_record? became end @@ -161,7 +209,7 @@ module ActiveRecord # The following transaction covers any possible database side-effects of the # attributes assignment. For example, setting the IDs of a child collection. with_transaction_returning_status do - self.assign_attributes(attributes, options) + assign_attributes(attributes, options) save end end @@ -172,7 +220,7 @@ module ActiveRecord # The following transaction covers any possible database side-effects of the # attributes assignment. For example, setting the IDs of a child collection. with_transaction_returning_status do - self.assign_attributes(attributes, options) + assign_attributes(attributes, options) save! end end @@ -237,7 +285,7 @@ module ActiveRecord clear_association_cache IdentityMap.without do - fresh_object = self.class.unscoped { self.class.find(self.id, options) } + fresh_object = self.class.unscoped { self.class.find(id, options) } @attributes.update(fresh_object.instance_variable_get('@attributes')) end @@ -314,19 +362,11 @@ module ActiveRecord new_id = self.class.unscoped.insert attributes_values - self.id ||= new_id if self.class.primary_key? + self.id ||= new_id if self.class.primary_key IdentityMap.add(self) if IdentityMap.enabled? @new_record = false id 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 - self.class.column_defaults.dup - end end end diff --git a/activerecord/lib/active_record/query_cache.rb b/activerecord/lib/active_record/query_cache.rb index 10c0dc6f2a..466d148901 100644 --- a/activerecord/lib/active_record/query_cache.rb +++ b/activerecord/lib/active_record/query_cache.rb @@ -28,9 +28,10 @@ module ActiveRecord end class BodyProxy # :nodoc: - def initialize(original_cache_value, target) + def initialize(original_cache_value, target, connection_id) @original_cache_value = original_cache_value @target = target + @connection_id = connection_id end def method_missing(method_sym, *arguments, &block) @@ -48,6 +49,7 @@ module ActiveRecord def close @target.close if @target.respond_to?(:close) ensure + ActiveRecord::Base.connection_id = @connection_id ActiveRecord::Base.connection.clear_query_cache unless @original_cache_value ActiveRecord::Base.connection.disable_query_cache! @@ -60,7 +62,7 @@ module ActiveRecord ActiveRecord::Base.connection.enable_query_cache! status, headers, body = @app.call(env) - [status, headers, BodyProxy.new(old, body)] + [status, headers, BodyProxy.new(old, body, ActiveRecord::Base.connection_id)] rescue Exception => e ActiveRecord::Base.connection.clear_query_cache unless old diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb new file mode 100644 index 0000000000..09da9ad1d1 --- /dev/null +++ b/activerecord/lib/active_record/querying.rb @@ -0,0 +1,58 @@ +require 'active_support/core_ext/module/delegation' + +module ActiveRecord + module Querying + delegate :find, :first, :first!, :last, :last!, :all, :exists?, :any?, :many?, :to => :scoped + delegate :first_or_create, :first_or_create!, :first_or_initialize, :to => :scoped + delegate :destroy, :destroy_all, :delete, :delete_all, :update, :update_all, :to => :scoped + delegate :find_each, :find_in_batches, :to => :scoped + delegate :select, :group, :order, :except, :reorder, :limit, :offset, :joins, + :where, :preload, :eager_load, :includes, :from, :lock, :readonly, + :having, :create_with, :uniq, :to => :scoped + delegate :count, :average, :minimum, :maximum, :sum, :calculate, :pluck, :to => :scoped + + # Executes a custom SQL query against your database and returns all the results. The results will + # be returned as an array with columns requested encapsulated as attributes of the model you call + # this method from. If you call <tt>Product.find_by_sql</tt> then the results will be returned in + # a Product object with the attributes you specified in the SQL query. + # + # If you call a complicated SQL query which spans multiple tables the columns specified by the + # SELECT will be attributes of the model, whether or not they are columns of the corresponding + # table. + # + # The +sql+ parameter is a full SQL query as a string. It will be called as is, there will be + # no database agnostic conversions performed. This should be a last resort because using, for example, + # MySQL specific terms will lock you to using that particular database engine or require you to + # change your call if you switch engines. + # + # ==== Examples + # # A simple SQL query spanning multiple tables + # Post.find_by_sql "SELECT p.title, c.author FROM posts p, comments c WHERE p.id = c.post_id" + # > [#<Post:0x36bff9c @attributes={"title"=>"Ruby Meetup", "first_name"=>"Quentin"}>, ...] + # + # # You can use the same string replacement techniques as you can with ActiveRecord#find + # Post.find_by_sql ["SELECT title FROM posts WHERE author = ? AND created > ?", author_id, start_date] + # > [#<Post:0x36bff9c @attributes={"title"=>"The Cheap Man Buys Twice"}>, ...] + def find_by_sql(sql, binds = []) + logging_query_plan do + connection.select_all(sanitize_sql(sql), "#{name} Load", binds).collect! { |record| instantiate(record) } + end + end + + # Returns the result of an SQL statement that should only include a COUNT(*) in the SELECT part. + # The use of this method should be restricted to complicated SQL queries that can't be executed + # using the ActiveRecord::Calculations class methods. Look into those before using this. + # + # ==== Parameters + # + # * +sql+ - An SQL statement which should return a count query from the database, see the example below. + # + # ==== Examples + # + # Product.count_by_sql "SELECT COUNT(*) FROM sales s, customers c WHERE s.customer_id = c.id" + def count_by_sql(sql) + sql = sanitize_conditions(sql) + connection.select_value(sql, "#{name} Count").to_i + end + end +end diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb index 47133e77e8..165d0750dd 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -22,6 +22,13 @@ module ActiveRecord config.app_middleware.insert_after "::ActionDispatch::Callbacks", "ActiveRecord::ConnectionAdapters::ConnectionManagement" + config.action_dispatch.rescue_responses.merge!( + 'ActiveRecord::RecordNotFound' => :not_found, + 'ActiveRecord::StaleObjectError' => :conflict, + 'ActiveRecord::RecordInvalid' => :unprocessable_entity, + 'ActiveRecord::RecordNotSaved' => :unprocessable_entity + ) + rake_tasks do load "active_record/railties/databases.rake" end @@ -78,18 +85,30 @@ module ActiveRecord end end - initializer "active_record.set_dispatch_hooks", :before => :set_clear_dependencies_hook do |app| - ActiveSupport.on_load(:active_record) do - ActionDispatch::Reloader.to_cleanup do - ActiveRecord::Base.clear_reloadable_connections! - ActiveRecord::Base.clear_cache! + initializer "active_record.set_reloader_hooks" do |app| + hook = lambda do + ActiveRecord::Base.clear_reloadable_connections! + ActiveRecord::Base.clear_cache! + end + + if app.config.reload_classes_only_on_change + ActiveSupport.on_load(:active_record) do + ActionDispatch::Reloader.to_prepare(&hook) + end + else + ActiveSupport.on_load(:active_record) do + ActionDispatch::Reloader.to_cleanup(&hook) end end end + initializer "active_record.add_watchable_files" do |app| + config.watchable_files.concat ["#{app.root}/db/schema.rb", "#{app.root}/db/structure.sql"] + end + config.after_initialize do ActiveSupport.on_load(:active_record) do - instantiate_observers + ActiveRecord::Base.instantiate_observers ActionDispatch::Reloader.to_prepare do ActiveRecord::Base.instantiate_observers diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index f4a813d704..180424641a 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -1,8 +1,8 @@ require 'active_support/core_ext/object/inclusion' +require 'active_record' db_namespace = namespace :db do task :load_config => :rails_env do - require 'active_record' ActiveRecord::Base.configurations = Rails.application.config.database_configuration ActiveRecord::Migrator.migrations_paths = Rails.application.paths['db/migrate'].to_a @@ -37,11 +37,8 @@ db_namespace = namespace :db do desc 'Create the database from config/database.yml for the current Rails.env (use db:create:all to create all dbs in the config)' task :create => :load_config do - # Make the test database at the same time as the development one, if it exists - if Rails.env.development? && ActiveRecord::Base.configurations['test'] - create_database(ActiveRecord::Base.configurations['test']) - end - create_database(ActiveRecord::Base.configurations[Rails.env]) + configs_for_environment.each { |config| create_database(config) } + ActiveRecord::Base.establish_connection(configs_for_environment.first) end def mysql_creation_options(config) @@ -115,8 +112,7 @@ db_namespace = namespace :db do end end else - # Bug with 1.9.2 Calling return within begin still executes else - $stderr.puts "#{config['database']} already exists" unless config['adapter'] =~ /sqlite/ + $stderr.puts "#{config['database']} already exists" end end @@ -138,12 +134,7 @@ db_namespace = namespace :db do desc 'Drops the database for the current Rails.env (use db:drop:all to drop all databases)' task :drop => :load_config do - config = ActiveRecord::Base.configurations[Rails.env || 'development'] - begin - drop_database(config) - rescue Exception => e - $stderr.puts "Couldn't drop #{config['database']} : #{e.inspect}" - end + configs_for_environment.each { |config| drop_database_and_rescue(config) } end def local_database?(config, &block) @@ -158,8 +149,19 @@ db_namespace = namespace :db do desc "Migrate the database (options: VERSION=x, VERBOSE=false)." task :migrate => [:environment, :load_config] do ActiveRecord::Migration.verbose = ENV["VERBOSE"] ? ENV["VERBOSE"] == "true" : true - ActiveRecord::Migrator.migrate(ActiveRecord::Migrator.migrations_paths, ENV["VERSION"] ? ENV["VERSION"].to_i : nil) - db_namespace["schema:dump"].invoke if ActiveRecord::Base.schema_format == :ruby + ActiveRecord::Migrator.migrate(ActiveRecord::Migrator.migrations_paths, ENV["VERSION"] ? ENV["VERSION"].to_i : nil) do |migration| + ENV["SCOPE"].blank? || (ENV["SCOPE"] == migration.scope) + end + db_namespace['_dump'].invoke + end + + task :_dump do + case ActiveRecord::Base.schema_format + when :ruby then db_namespace["schema:dump"].invoke + when :sql then db_namespace["structure:dump"].invoke + else + raise "unknown schema format #{ActiveRecord::Base.schema_format}" + end end namespace :migrate do @@ -182,7 +184,7 @@ db_namespace = namespace :db do version = ENV['VERSION'] ? ENV['VERSION'].to_i : nil raise 'VERSION is required' unless version ActiveRecord::Migrator.run(:up, ActiveRecord::Migrator.migrations_paths, version) - db_namespace['schema:dump'].invoke if ActiveRecord::Base.schema_format == :ruby + db_namespace['_dump'].invoke end # desc 'Runs the "down" for a given migration VERSION.' @@ -190,7 +192,7 @@ db_namespace = namespace :db do version = ENV['VERSION'] ? ENV['VERSION'].to_i : nil raise 'VERSION is required' unless version ActiveRecord::Migrator.run(:down, ActiveRecord::Migrator.migrations_paths, version) - db_namespace['schema:dump'].invoke if ActiveRecord::Base.schema_format == :ruby + db_namespace['_dump'].invoke end desc 'Display status of migrations' @@ -230,18 +232,21 @@ db_namespace = namespace :db do task :rollback => [:environment, :load_config] do step = ENV['STEP'] ? ENV['STEP'].to_i : 1 ActiveRecord::Migrator.rollback(ActiveRecord::Migrator.migrations_paths, step) - db_namespace['schema:dump'].invoke if ActiveRecord::Base.schema_format == :ruby + db_namespace['_dump'].invoke end # desc 'Pushes the schema to the next version (specify steps w/ STEP=n).' task :forward => [:environment, :load_config] do step = ENV['STEP'] ? ENV['STEP'].to_i : 1 ActiveRecord::Migrator.forward(ActiveRecord::Migrator.migrations_paths, step) - db_namespace['schema:dump'].invoke if ActiveRecord::Base.schema_format == :ruby + db_namespace['_dump'].invoke end # desc 'Drops and recreates the database from db/schema.rb for the current environment and loads the seeds.' - task :reset => [ 'db:drop', 'db:setup' ] + task :reset => :environment do + db_namespace["drop"].invoke + db_namespace["setup"].invoke + end # desc "Retrieves the charset for the current environment's database" task :charset => :environment do @@ -280,24 +285,23 @@ db_namespace = namespace :db do # desc "Raises an error if there are pending migrations" task :abort_if_pending_migrations => :environment do - if defined? ActiveRecord - pending_migrations = ActiveRecord::Migrator.new(:up, ActiveRecord::Migrator.migrations_paths).pending_migrations + pending_migrations = ActiveRecord::Migrator.new(:up, ActiveRecord::Migrator.migrations_paths).pending_migrations - if pending_migrations.any? - puts "You have #{pending_migrations.size} pending migrations:" - pending_migrations.each do |pending_migration| - puts ' %4d %s' % [pending_migration.version, pending_migration.name] - end - abort %{Run "rake db:migrate" to update your database then try again.} + if pending_migrations.any? + puts "You have #{pending_migrations.size} pending migrations:" + pending_migrations.each do |pending_migration| + puts ' %4d %s' % [pending_migration.version, pending_migration.name] end + abort %{Run `rake db:migrate` to update your database then try again.} end end desc 'Create the database, load the schema, and initialize with the seed data (use db:reset to also drop the db first)' - task :setup => [ 'db:create', 'db:schema:load', 'db:seed' ] + task :setup => ['db:schema:load_if_ruby', 'db:structure:load_if_sql', :seed] desc 'Load the seed data from db/seeds.rb' - task :seed => 'db:abort_if_pending_migrations' do + task :seed do + db_namespace['abort_if_pending_migrations'].invoke Rails.application.load_seed end @@ -357,93 +361,125 @@ db_namespace = namespace :db do if File.exists?(file) load(file) else - abort %{#{file} doesn't exist yet. Run "rake db:migrate" to create it then try again. If you do not intend to use a database, you should instead alter #{Rails.root}/config/application.rb to limit the frameworks that will be loaded} + abort %{#{file} doesn't exist yet. Run `rake db:migrate` to create it then try again. If you do not intend to use a database, you should instead alter #{Rails.root}/config/application.rb to limit the frameworks that will be loaded} end end + + task :load_if_ruby => 'db:create' do + db_namespace["schema:load"].invoke if ActiveRecord::Base.schema_format == :ruby + end end namespace :structure do - desc 'Dump the database structure to an SQL file' + desc 'Dump the database structure to db/structure.sql. Specify another file with DB_STRUCTURE=db/my_structure.sql' task :dump => :environment do abcs = ActiveRecord::Base.configurations + filename = ENV['DB_STRUCTURE'] || File.join(Rails.root, "db", "structure.sql") case abcs[Rails.env]['adapter'] when /mysql/, 'oci', 'oracle' ActiveRecord::Base.establish_connection(abcs[Rails.env]) - File.open("#{Rails.root}/db/#{Rails.env}_structure.sql", "w+") { |f| f << ActiveRecord::Base.connection.structure_dump } + File.open(filename, "w:utf-8") { |f| f << ActiveRecord::Base.connection.structure_dump } when /postgresql/ - ENV['PGHOST'] = abcs[Rails.env]['host'] if abcs[Rails.env]['host'] - ENV['PGPORT'] = abcs[Rails.env]["port"].to_s if abcs[Rails.env]['port'] - ENV['PGPASSWORD'] = abcs[Rails.env]['password'].to_s if abcs[Rails.env]['password'] + set_psql_env(abcs[Rails.env]) search_path = abcs[Rails.env]['schema_search_path'] unless search_path.blank? search_path = search_path.split(",").map{|search_path_part| "--schema=#{search_path_part.strip}" }.join(" ") end - `pg_dump -i -U "#{abcs[Rails.env]['username']}" -s -x -O -f db/#{Rails.env}_structure.sql #{search_path} #{abcs[Rails.env]['database']}` + `pg_dump -i -s -x -O -f #{filename} #{search_path} #{abcs[Rails.env]['database']}` raise 'Error dumping database' if $?.exitstatus == 1 when /sqlite/ - dbfile = abcs[Rails.env]['database'] || abcs[Rails.env]['dbfile'] - `sqlite3 #{dbfile} .schema > db/#{Rails.env}_structure.sql` + dbfile = abcs[Rails.env]['database'] + `sqlite3 #{dbfile} .schema > #{filename}` when 'sqlserver' - `smoscript -s #{abcs[Rails.env]['host']} -d #{abcs[Rails.env]['database']} -u #{abcs[Rails.env]['username']} -p #{abcs[Rails.env]['password']} -f db\\#{Rails.env}_structure.sql -A -U` + `smoscript -s #{abcs[Rails.env]['host']} -d #{abcs[Rails.env]['database']} -u #{abcs[Rails.env]['username']} -p #{abcs[Rails.env]['password']} -f #{filename} -A -U` when "firebird" set_firebird_env(abcs[Rails.env]) db_string = firebird_db_string(abcs[Rails.env]) - sh "isql -a #{db_string} > #{Rails.root}/db/#{Rails.env}_structure.sql" + sh "isql -a #{db_string} > #{filename}" else raise "Task not supported by '#{abcs[Rails.env]["adapter"]}'" end if ActiveRecord::Base.connection.supports_migrations? - File.open("#{Rails.root}/db/#{Rails.env}_structure.sql", "a") { |f| f << ActiveRecord::Base.connection.dump_schema_information } + File.open(filename, "a") { |f| f << ActiveRecord::Base.connection.dump_schema_information } end end - end - - namespace :test do - # desc "Recreate the test database from the current schema.rb" - task :load => 'db:test:purge' do - ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations['test']) - ActiveRecord::Schema.verbose = false - db_namespace['schema:load'].invoke - end - # desc "Recreate the test database from the current environment's database schema" - task :clone => %w(db:schema:dump db:test:load) + # desc "Recreate the databases from the structure.sql file" + task :load => [:environment, :load_config] do + env = ENV['RAILS_ENV'] || 'test' - # desc "Recreate the test databases from the development structure" - task :clone_structure => [ 'db:structure:dump', 'db:test:purge' ] do abcs = ActiveRecord::Base.configurations - case abcs['test']['adapter'] + filename = ENV['DB_STRUCTURE'] || File.join(Rails.root, "db", "structure.sql") + case abcs[env]['adapter'] when /mysql/ - ActiveRecord::Base.establish_connection(:test) + ActiveRecord::Base.establish_connection(abcs[env]) ActiveRecord::Base.connection.execute('SET foreign_key_checks = 0') - IO.readlines("#{Rails.root}/db/#{Rails.env}_structure.sql").join.split("\n\n").each do |table| + IO.read(filename).split("\n\n").each do |table| ActiveRecord::Base.connection.execute(table) end when /postgresql/ - ENV['PGHOST'] = abcs['test']['host'] if abcs['test']['host'] - ENV['PGPORT'] = abcs['test']['port'].to_s if abcs['test']['port'] - ENV['PGPASSWORD'] = abcs['test']['password'].to_s if abcs['test']['password'] - `psql -U "#{abcs['test']['username']}" -f #{Rails.root}/db/#{Rails.env}_structure.sql #{abcs['test']['database']} #{abcs['test']['template']}` + set_psql_env(abcs[env]) + `psql -f "#{filename}" #{abcs[env]['database']} #{abcs[env]['template']}` when /sqlite/ - dbfile = abcs['test']['database'] || abcs['test']['dbfile'] - `sqlite3 #{dbfile} < #{Rails.root}/db/#{Rails.env}_structure.sql` + dbfile = abcs[env]['database'] + `sqlite3 #{dbfile} < "#{filename}"` when 'sqlserver' - `sqlcmd -S #{abcs['test']['host']} -d #{abcs['test']['database']} -U #{abcs['test']['username']} -P #{abcs['test']['password']} -i db\\#{Rails.env}_structure.sql` + `sqlcmd -S #{abcs[env]['host']} -d #{abcs[env]['database']} -U #{abcs[env]['username']} -P #{abcs[env]['password']} -i #{filename}` when 'oci', 'oracle' - ActiveRecord::Base.establish_connection(:test) - IO.readlines("#{Rails.root}/db/#{Rails.env}_structure.sql").join.split(";\n\n").each do |ddl| + ActiveRecord::Base.establish_connection(abcs[env]) + IO.read(filename).split(";\n\n").each do |ddl| ActiveRecord::Base.connection.execute(ddl) end when 'firebird' - set_firebird_env(abcs['test']) - db_string = firebird_db_string(abcs['test']) - sh "isql -i #{Rails.root}/db/#{Rails.env}_structure.sql #{db_string}" + set_firebird_env(abcs[env]) + db_string = firebird_db_string(abcs[env]) + sh "isql -i #{filename} #{db_string}" else - raise "Task not supported by '#{abcs['test']['adapter']}'" + raise "Task not supported by '#{abcs[env]['adapter']}'" end end + task :load_if_sql => 'db:create' do + db_namespace["structure:load"].invoke if ActiveRecord::Base.schema_format == :sql + end + end + + namespace :test do + + # desc "Recreate the test database from the current schema" + task :load => 'db:test:purge' do + case ActiveRecord::Base.schema_format + when :ruby + db_namespace["test:load_schema"].invoke + when :sql + db_namespace["test:load_structure"].invoke + end + end + + # desc "Recreate the test database from an existent structure.sql file" + task :load_structure => 'db:test:purge' do + begin + old_env, ENV['RAILS_ENV'] = ENV['RAILS_ENV'], 'test' + db_namespace["structure:load"].invoke + ensure + ENV['RAILS_ENV'] = old_env + end + end + + # desc "Recreate the test database from an existent schema.rb file" + task :load_schema => 'db:test:purge' do + ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations['test']) + ActiveRecord::Schema.verbose = false + db_namespace["schema:load"].invoke + end + + # desc "Recreate the test database from a fresh schema.rb file" + task :clone => %w(db:schema:dump db:test:load_schema) + + # desc "Recreate the test database from a fresh structure.sql file" + task :clone_structure => [ "db:structure:dump", "db:test:load_structure" ] + # desc "Empty the test database" task :purge => :environment do abcs = ActiveRecord::Base.configurations @@ -456,7 +492,7 @@ db_namespace = namespace :db do drop_database(abcs['test']) create_database(abcs['test']) when /sqlite/ - dbfile = abcs['test']['database'] || abcs['test']['dbfile'] + dbfile = abcs['test']['database'] File.delete(dbfile) if File.exist?(dbfile) when 'sqlserver' test = abcs.deep_dup['test'] @@ -479,7 +515,7 @@ db_namespace = namespace :db do # desc 'Check for pending migrations and load the test schema' task :prepare => 'db:abort_if_pending_migrations' do - if defined?(ActiveRecord) && !ActiveRecord::Base.configurations.blank? + unless ActiveRecord::Base.configurations.blank? db_namespace[{ :sql => 'test:clone_structure', :ruby => 'test:load' }[ActiveRecord::Base.schema_format]].invoke end end @@ -548,6 +584,20 @@ def drop_database(config) end end +def drop_database_and_rescue(config) + begin + drop_database(config) + rescue Exception => e + $stderr.puts "Couldn't drop #{config['database']} : #{e.inspect}" + end +end + +def configs_for_environment + environments = [Rails.env] + environments << 'test' if Rails.env.development? + ActiveRecord::Base.configurations.values_at(*environments).compact.reject { |config| config['database'].blank? } +end + def session_table_name ActiveRecord::SessionStore::Session.table_name end @@ -560,3 +610,10 @@ end def firebird_db_string(config) FireRuby::Database.db_string_for(config.symbolize_keys) end + +def set_psql_env(config) + ENV['PGHOST'] = config['host'] if config['host'] + ENV['PGPORT'] = config['port'].to_s if config['port'] + ENV['PGPASSWORD'] = config['password'].to_s if config['password'] + ENV['PGUSER'] = config['username'].to_s if config['username'] +end diff --git a/activerecord/lib/active_record/readonly_attributes.rb b/activerecord/lib/active_record/readonly_attributes.rb new file mode 100644 index 0000000000..836b15e2ce --- /dev/null +++ b/activerecord/lib/active_record/readonly_attributes.rb @@ -0,0 +1,26 @@ +require 'active_support/concern' +require 'active_support/core_ext/class/attribute' + +module ActiveRecord + module ReadonlyAttributes + extend ActiveSupport::Concern + + included do + config_attribute :_attr_readonly + self._attr_readonly = [] + end + + module ClassMethods + # Attributes listed as readonly will be used to create a new record but update operations will + # ignore these fields. + def attr_readonly(*attributes) + self._attr_readonly = Set.new(attributes.map { |a| a.to_s }) + (self._attr_readonly || []) + end + + # Returns an array of all the attributes that have been specified as readonly. + def readonly_attributes + self._attr_readonly + end + end + end +end diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 120ff0cac6..60b73e9fe5 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -7,7 +7,8 @@ module ActiveRecord extend ActiveSupport::Concern included do - class_attribute :reflections + extend ActiveModel::Configuration + config_attribute :reflections self.reflections = {} end @@ -124,7 +125,7 @@ module ActiveRecord # <tt>composed_of :balance, :class_name => 'Money'</tt> returns <tt>'Money'</tt> # <tt>has_many :clients</tt> returns <tt>'Client'</tt> def class_name - @class_name ||= options[:class_name] || derive_class_name + @class_name ||= (options[:class_name] || derive_class_name).to_s end # Returns +true+ if +self+ and +other_aggregation+ have the same +name+ attribute, +active_record+ attribute, @@ -213,11 +214,11 @@ module ActiveRecord # klass option is necessary to support loading polymorphic associations def association_primary_key(klass = nil) - options[:primary_key] || (klass || self.klass).primary_key + options[:primary_key] || primary_key(klass || self.klass) end def active_record_primary_key - @active_record_primary_key ||= options[:primary_key] || active_record.primary_key + @active_record_primary_key ||= options[:primary_key] || primary_key(active_record) end def counter_cache_column @@ -262,6 +263,10 @@ module ActiveRecord [self] end + def nested? + false + end + # An array of arrays of conditions. Each item in the outside array corresponds to a reflection # in the #chain. The inside arrays are simply conditions (and each condition may itself be # a hash, array, arel predicate, etc...) @@ -357,6 +362,10 @@ module ActiveRecord active_record.name.foreign_key end end + + def primary_key(klass) + klass.primary_key || raise(UnknownPrimaryKey.new(klass)) + end end # Holds all the meta-data about a :through association as it was specified @@ -429,7 +438,7 @@ module ActiveRecord # of relevant reflections, plus any :source_type or polymorphic :as constraints. def conditions @conditions ||= begin - conditions = source_reflection.conditions + conditions = source_reflection.conditions.map { |c| c.dup } # Add to it the conditions from this reflection if necessary. conditions.first << options[:conditions] if options[:conditions] @@ -453,7 +462,7 @@ module ActiveRecord source_reflection.source_macro end - # A through association is nested iff there would be more than one join table + # A through association is nested if there would be more than one join table def nested? chain.length > 2 || through_reflection.macro == :has_and_belongs_to_many end @@ -461,7 +470,7 @@ module ActiveRecord # We want to use the klass from this reflection, rather than just delegate straight to # the source_reflection, because the source_reflection may be polymorphic. We still # need to respect the source_reflection's :primary_key option, though. - def association_primary_key(klass = self.klass) + def association_primary_key(klass = nil) # Get the "actual" source reflection if the immediate source reflection has a # source reflection itself source_reflection = self.source_reflection @@ -469,7 +478,7 @@ module ActiveRecord source_reflection = source_reflection.source_reflection end - source_reflection.options[:primary_key] || klass.primary_key + source_reflection.options[:primary_key] || primary_key(klass || self.klass) end # Gets an array of possible <tt>:through</tt> source reflection names: diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index bf61d79a2c..6f2248fa21 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -1,5 +1,6 @@ +# -*- coding: utf-8 -*- require 'active_support/core_ext/object/blank' -require 'active_support/core_ext/module/delegation' +require 'active_support/deprecation' module ActiveRecord # = Active Record Relation @@ -7,13 +8,9 @@ module ActiveRecord JoinOperation = Struct.new(:relation, :join_class, :on) ASSOCIATION_METHODS = [:includes, :eager_load, :preload] MULTI_VALUE_METHODS = [:select, :group, :order, :joins, :where, :having, :bind] - SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :from, :reorder, :reverse_order] + SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :from, :reorder, :reverse_order, :uniq] - include FinderMethods, Calculations, SpawnMethods, QueryMethods, Batches - - # These are explicitly delegated to improve performance (avoids method_missing) - delegate :to_xml, :to_yaml, :length, :collect, :map, :each, :all?, :include?, :to => :to_a - delegate :table_name, :quoted_table_name, :primary_key, :primary_key?, :quoted_primary_key, :connection, :column_hash,:to => :klass + include FinderMethods, Calculations, SpawnMethods, QueryMethods, Batches, Explain, Delegation attr_reader :table, :klass, :loaded attr_accessor :extensions, :default_scoped @@ -36,7 +33,7 @@ module ActiveRecord def insert(values) primary_key_value = nil - if primary_key? && Hash === values + if primary_key && Hash === values primary_key_value = values[values.keys.find { |k| k.name == primary_key }] @@ -70,7 +67,7 @@ module ActiveRecord conn.insert( im, 'SQL', - primary_key? && primary_key, + primary_key, primary_key_value, nil, binds) @@ -136,14 +133,35 @@ module ActiveRecord first || new(attributes, options, &block) end - def respond_to?(method, include_private = false) - arel.respond_to?(method, include_private) || - Array.method_defined?(method) || - @klass.respond_to?(method, include_private) || - super + # Runs EXPLAIN on the query or queries triggered by this relation and + # returns the result as a string. The string is formatted imitating the + # ones printed by the database shell. + # + # Note that this method actually runs the queries, since the results of some + # are needed by the next ones when eager loading is going on. + # + # Please see further details in the + # {Active Record Query Interface guide}[http://edgeguides.rubyonrails.org/active_record_querying.html#running-explain]. + def explain + _, queries = collecting_queries_for_explain { exec_queries } + exec_explain(queries) end def to_a + # We monitor here the entire execution rather than individual SELECTs + # because from the point of view of the user fetching the records of a + # relation is a single unit of work. You want to know if this call takes + # too long, not if the individual queries take too long. + # + # It could be the case that none of the queries involved surpass the + # threshold, and at the same time the sum of them all does. The user + # should get a query plan logged in that case. + logging_query_plan do + exec_queries + end + end + + def exec_queries return @records if loaded? default_scoped = with_default_scope @@ -174,6 +192,7 @@ module ActiveRecord @loaded = true @records end + private :exec_queries def as_json(options = nil) #:nodoc: to_a.as_json(options) @@ -219,7 +238,7 @@ module ActiveRecord # Please check unscoped if you want to remove all previous scopes (including # the default_scope) during the execution of a block. def scoping - @klass.send(:with_scope, self, :overwrite) { yield } + @klass.with_scope(self, :overwrite) { yield } end # Updates all records with details given if they match a set of conditions supplied, limits and order can @@ -337,7 +356,7 @@ module ActiveRecord end end - # Destroy an object (or multiple objects) that has the given id, the object is instantiated first, + # Destroy an object (or multiple objects) that has the given id. The object is instantiated first, # therefore all callbacks and filters are fired off before the object is deleted. This method is # less efficient than ActiveRecord#delete but allows cleanup methods and other actions to be run. # @@ -487,20 +506,6 @@ module ActiveRecord end end - protected - - def method_missing(method, *args, &block) - if Array.method_defined?(method) - to_a.send(method, *args, &block) - elsif @klass.respond_to?(method) - scoping { @klass.send(method, *args, &block) } - elsif arel.respond_to?(method) - arel.send(method, *args, &block) - else - super - end - end - private def references_eager_loaded_tables? @@ -517,7 +522,18 @@ module ActiveRecord # always convert table names to downcase as in Oracle quoted table names are in uppercase joined_tables = joined_tables.flatten.compact.map { |t| t.downcase }.uniq - (tables_in_string(to_sql) - joined_tables).any? + referenced_tables = (tables_in_string(to_sql) - joined_tables) + if referenced_tables.any? + ActiveSupport::Deprecation.warn( + "Your query appears to reference tables (#{referenced_tables.join(', ')}) that are not " \ + "explicitly joined. This implicit joining is deprecated, so you must explicitly " \ + "reference the tables. For example, instead of Author.includes(:posts).where(\"posts.name = 'foo'\"), " \ + "you should write Author.eager_load(:posts).where(\"posts.name = 'foo'\")." + ) + true + else + false + end end def tables_in_string(string) @@ -526,6 +542,5 @@ module ActiveRecord # ignore raw_sql_ that is used by Oracle adapter as alias for limit/offset subqueries string.scan(/([a-zA-Z_][.\w]+).?\./).flatten.map{ |s| s.downcase }.uniq - ['raw_sql_'] end - end end diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index af86771d2d..bf9b4bf1c9 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -166,6 +166,22 @@ module ActiveRecord 0 end + # This method is designed to perform select by a single column as direct SQL query + # Returns <tt>Array</tt> with values of the specified column name + # The values has same data type as column. + # + # Examples: + # + # Person.pluck(:id) # SELECT people.id FROM people + # Person.uniq.pluck(:role) # SELECT DISTINCT role FROM people + # Person.where(:confirmed => true).limit(5).pluck(:id) + # + def pluck(column_name) + klass.connection.select_all(select(column_name).arel).map! do |attributes| + klass.type_cast_attribute(attributes.keys.first, klass.initialize_attributes(attributes)) + end + end + private def perform_calculation(operation, column_name, options = {}) diff --git a/activerecord/lib/active_record/relation/delegation.rb b/activerecord/lib/active_record/relation/delegation.rb new file mode 100644 index 0000000000..f5fdf437bf --- /dev/null +++ b/activerecord/lib/active_record/relation/delegation.rb @@ -0,0 +1,49 @@ +require 'active_support/core_ext/module/delegation' + +module ActiveRecord + module Delegation + # Set up common delegations for performance (avoids method_missing) + delegate :to_xml, :to_yaml, :length, :collect, :map, :each, :all?, :include?, :to_ary, :to => :to_a + delegate :table_name, :quoted_table_name, :primary_key, :quoted_primary_key, + :connection, :columns_hash, :auto_explain_threshold_in_seconds, :to => :klass + + def self.delegate_to_scoped_klass(method) + if method.to_s =~ /\A[a-zA-Z_]\w*[!?]?\z/ + module_eval <<-RUBY, __FILE__, __LINE__ + 1 + def #{method}(*args, &block) + scoping { @klass.#{method}(*args, &block) } + end + RUBY + else + module_eval <<-RUBY, __FILE__, __LINE__ + 1 + def #{method}(*args, &block) + scoping { @klass.send(#{method.inspect}, *args, &block) } + end + RUBY + end + end + + def respond_to?(method, include_private = false) + super || Array.method_defined?(method) || + @klass.respond_to?(method, include_private) || + arel.respond_to?(method, include_private) + end + + protected + + def method_missing(method, *args, &block) + if Array.method_defined?(method) + ::ActiveRecord::Delegation.delegate method, :to => :to_a + to_a.send(method, *args, &block) + elsif @klass.respond_to?(method) + ::ActiveRecord::Delegation.delegate_to_scoped_klass(method) + scoping { @klass.send(method, *args, &block) } + elsif arel.respond_to?(method) + ::ActiveRecord::Delegation.delegate method, :to => :arel + arel.send(method, *args, &block) + else + super + end + end + end +end
\ No newline at end of file diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index 7eeb3dde70..e58c726e09 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -187,11 +187,11 @@ module ActiveRecord def exists?(id = false) return false if id.nil? - id = id.id if ActiveRecord::Base === id + id = id.id if ActiveRecord::Model === id join_dependency = construct_join_dependency_for_association_find relation = construct_relation_for_association_find(join_dependency) - relation = relation.except(:select).select("1").limit(1) + relation = relation.except(:select, :order).select("1").limit(1) case id when Array, Hash @@ -249,7 +249,7 @@ module ActiveRecord end def construct_limited_ids_condition(relation) - orders = relation.order_values.map { |val| val.presence }.compact + orders = (relation.reorder_value || relation.order_values).map { |val| val.presence }.compact values = @klass.connection.distinct("#{@klass.connection.quote_table_name table_name}.#{primary_key}", orders) relation = relation.dup diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb index 7e8ddd1b5d..88081edae2 100644 --- a/activerecord/lib/active_record/relation/predicate_builder.rb +++ b/activerecord/lib/active_record/relation/predicate_builder.rb @@ -15,42 +15,49 @@ module ActiveRecord table = Arel::Table.new(table_name, engine) end - attribute = table[column.to_sym] + build(table[column.to_sym], value) + end + end + predicates.flatten + end + + private + def self.build(attribute, value) + case value + when ActiveRecord::Relation + value = value.select(value.klass.arel_table[value.klass.primary_key]) if value.select_values.empty? + attribute.in(value.arel.ast) + when Array, ActiveRecord::Associations::CollectionProxy + values = value.to_a.map {|x| x.is_a?(ActiveRecord::Model) ? x.id : x} + ranges, values = values.partition {|v| v.is_a?(Range) || v.is_a?(Arel::Relation)} - case value - when ActiveRecord::Relation - value = value.select(value.klass.arel_table[value.klass.primary_key]) if value.select_values.empty? - attribute.in(value.arel.ast) - when Array, ActiveRecord::Associations::CollectionProxy - values = value.to_a.map { |x| - x.is_a?(ActiveRecord::Base) ? x.id : x - } + array_predicates = ranges.map {|range| attribute.in(range)} - if values.include?(nil) - values = values.compact - if values.empty? - attribute.eq nil - else - attribute.in(values.compact).or attribute.eq(nil) - end + if values.include?(nil) + values = values.compact + case values.length + when 0 + array_predicates << attribute.eq(nil) + when 1 + array_predicates << attribute.eq(values.first).or(attribute.eq(nil)) else - attribute.in(values) + array_predicates << attribute.in(values).or(attribute.eq(nil)) end - - when Range, Arel::Relation - attribute.in(value) - when ActiveRecord::Base - attribute.eq(value.id) - when Class - # FIXME: I think we need to deprecate this behavior - attribute.eq(value.name) else - attribute.eq(value) + array_predicates << attribute.in(values) end + + array_predicates.inject {|composite, predicate| composite.or(predicate)} + when Range, Arel::Relation + attribute.in(value) + when ActiveRecord::Model + attribute.eq(value.id) + when Class + # FIXME: I think we need to deprecate this behavior + attribute.eq(value.name) + else + attribute.eq(value) end end - - predicates.flatten - end end end diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 670ba0987d..c281bead0d 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -9,7 +9,8 @@ module ActiveRecord :select_values, :group_values, :order_values, :joins_values, :where_values, :having_values, :bind_values, :limit_value, :offset_value, :lock_value, :readonly_value, :create_with_value, - :from_value, :reorder_value, :reverse_order_value + :from_value, :reorder_value, :reverse_order_value, + :uniq_value def includes(*args) args.reject! {|a| a.blank? } @@ -38,7 +39,7 @@ module ActiveRecord end # Works in two unique ways. - # + # # First: takes a block so it can be used just like Array#select. # # Model.scoped.select { |m| m.field == value } @@ -176,9 +177,25 @@ module ActiveRecord relation end + # Specifies whether the records should be unique or not. For example: + # + # User.select(:name) + # # => Might return two records with the same name + # + # User.select(:name).uniq + # # => Returns 1 record per unique name + # + # User.select(:name).uniq.uniq(false) + # # => You can also remove the uniqueness + def uniq(value = true) + relation = clone + relation.uniq_value = value + relation + end + # Used to extend a scope with additional methods, either through - # a module or through a block provided. - # + # a module or through a block provided. + # # The object returned is a relation, which can be further extended. # # === Using a module @@ -200,7 +217,7 @@ module ActiveRecord # # scope = Model.scoped.extending do # def page(number) - # # pagination code goes here + # # pagination code goes here # end # end # scope.page(params[:page]) @@ -209,7 +226,7 @@ module ActiveRecord # # scope = Model.scoped.extending(Pagination) do # def per_page(number) - # # pagination code goes here + # # pagination code goes here # end # end def extending(*modules) @@ -252,6 +269,7 @@ module ActiveRecord build_select(arel, @select_values.uniq) + arel.distinct(@uniq_value) arel.from(@from_value) if @from_value arel.lock(@lock_value) if @lock_value diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb index ba882beca9..a5194beae5 100644 --- a/activerecord/lib/active_record/relation/spawn_methods.rb +++ b/activerecord/lib/active_record/relation/spawn_methods.rb @@ -113,7 +113,7 @@ module ActiveRecord result end - VALID_FIND_OPTIONS = [ :conditions, :include, :joins, :limit, :offset, :extend, + VALID_FIND_OPTIONS = [ :conditions, :include, :joins, :limit, :offset, :extend, :eager_load, :order, :select, :readonly, :group, :having, :from, :lock ] def apply_finder_options(options) @@ -124,7 +124,7 @@ module ActiveRecord finders = options.dup finders.delete_if { |key, value| value.nil? && key != :limit } - ([:joins, :select, :group, :order, :having, :limit, :offset, :from, :lock, :readonly] & finders.keys).each do |finder| + ([:joins, :select, :group, :order, :having, :limit, :offset, :from, :lock, :readonly, :eager_load] & finders.keys).each do |finder| relation = relation.send(finder, finders[finder]) end diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb new file mode 100644 index 0000000000..2d7d83d160 --- /dev/null +++ b/activerecord/lib/active_record/sanitization.rb @@ -0,0 +1,194 @@ +require 'active_support/concern' + +module ActiveRecord + module Sanitization + extend ActiveSupport::Concern + + module ClassMethods + def quote_value(value, column = nil) #:nodoc: + connection.quote(value,column) + end + + # Used to sanitize objects before they're used in an SQL SELECT statement. Delegates to <tt>connection.quote</tt>. + def sanitize(object) #:nodoc: + connection.quote(object) + end + + protected + + # Accepts an array, hash, or string of SQL conditions and sanitizes + # them into a valid SQL fragment for a WHERE clause. + # ["name='%s' and group_id='%s'", "foo'bar", 4] returns "name='foo''bar' and group_id='4'" + # { :name => "foo'bar", :group_id => 4 } returns "name='foo''bar' and group_id='4'" + # "name='foo''bar' and group_id='4'" returns "name='foo''bar' and group_id='4'" + def sanitize_sql_for_conditions(condition, table_name = self.table_name) + return nil if condition.blank? + + case condition + when Array; sanitize_sql_array(condition) + when Hash; sanitize_sql_hash_for_conditions(condition, table_name) + else condition + end + end + alias_method :sanitize_sql, :sanitize_sql_for_conditions + + # Accepts an array, hash, or string of SQL conditions and sanitizes + # them into a valid SQL fragment for a SET clause. + # { :name => nil, :group_id => 4 } returns "name = NULL , group_id='4'" + def sanitize_sql_for_assignment(assignments) + case assignments + when Array; sanitize_sql_array(assignments) + when Hash; sanitize_sql_hash_for_assignment(assignments) + else assignments + end + end + + # Accepts a hash of SQL conditions and replaces those attributes + # that correspond to a +composed_of+ relationship with their expanded + # aggregate attribute values. + # Given: + # class Person < ActiveRecord::Base + # composed_of :address, :class_name => "Address", + # :mapping => [%w(address_street street), %w(address_city city)] + # end + # Then: + # { :address => Address.new("813 abc st.", "chicago") } + # # => { :address_street => "813 abc st.", :address_city => "chicago" } + def expand_hash_conditions_for_aggregates(attrs) + expanded_attrs = {} + attrs.each do |attr, value| + unless (aggregation = reflect_on_aggregation(attr.to_sym)).nil? + mapping = aggregate_mapping(aggregation) + mapping.each do |field_attr, aggregate_attr| + if mapping.size == 1 && !value.respond_to?(aggregate_attr) + expanded_attrs[field_attr] = value + else + expanded_attrs[field_attr] = value.send(aggregate_attr) + end + end + else + expanded_attrs[attr] = value + end + end + expanded_attrs + end + + # Sanitizes a hash of attribute/value pairs into SQL conditions for a WHERE clause. + # { :name => "foo'bar", :group_id => 4 } + # # => "name='foo''bar' and group_id= 4" + # { :status => nil, :group_id => [1,2,3] } + # # => "status IS NULL and group_id IN (1,2,3)" + # { :age => 13..18 } + # # => "age BETWEEN 13 AND 18" + # { 'other_records.id' => 7 } + # # => "`other_records`.`id` = 7" + # { :other_records => { :id => 7 } } + # # => "`other_records`.`id` = 7" + # And for value objects on a composed_of relationship: + # { :address => Address.new("123 abc st.", "chicago") } + # # => "address_street='123 abc st.' and address_city='chicago'" + def sanitize_sql_hash_for_conditions(attrs, default_table_name = self.table_name) + attrs = expand_hash_conditions_for_aggregates(attrs) + + table = Arel::Table.new(table_name).alias(default_table_name) + PredicateBuilder.build_from_hash(arel_engine, attrs, table).map { |b| + connection.visitor.accept b + }.join(' AND ') + end + alias_method :sanitize_sql_hash, :sanitize_sql_hash_for_conditions + + # Sanitizes a hash of attribute/value pairs into SQL conditions for a SET clause. + # { :status => nil, :group_id => 1 } + # # => "status = NULL , group_id = 1" + def sanitize_sql_hash_for_assignment(attrs) + attrs.map do |attr, value| + "#{connection.quote_column_name(attr)} = #{quote_bound_value(value)}" + end.join(', ') + end + + # Accepts an array of conditions. The array has each value + # sanitized and interpolated into the SQL statement. + # ["name='%s' and group_id='%s'", "foo'bar", 4] returns "name='foo''bar' and group_id='4'" + def sanitize_sql_array(ary) + statement, *values = ary + if values.first.is_a?(Hash) && statement =~ /:\w+/ + replace_named_bind_variables(statement, values.first) + elsif statement.include?('?') + replace_bind_variables(statement, values) + elsif statement.blank? + statement + else + statement % values.collect { |value| connection.quote_string(value.to_s) } + end + end + + alias_method :sanitize_conditions, :sanitize_sql + + def replace_bind_variables(statement, values) #:nodoc: + raise_if_bind_arity_mismatch(statement, statement.count('?'), values.size) + bound = values.dup + c = connection + statement.gsub('?') { quote_bound_value(bound.shift, c) } + end + + def replace_named_bind_variables(statement, bind_vars) #:nodoc: + statement.gsub(/(:?):([a-zA-Z]\w*)/) do + if $1 == ':' # skip postgresql casts + $& # return the whole match + elsif bind_vars.include?(match = $2.to_sym) + quote_bound_value(bind_vars[match]) + else + raise PreparedStatementInvalid, "missing value for :#{match} in #{statement}" + end + end + end + + def expand_range_bind_variables(bind_vars) #:nodoc: + expanded = [] + + bind_vars.each do |var| + next if var.is_a?(Hash) + + if var.is_a?(Range) + expanded << var.first + expanded << var.last + else + expanded << var + end + end + + expanded + end + + def quote_bound_value(value, c = connection) #:nodoc: + if value.respond_to?(:map) && !value.acts_like?(:string) + if value.respond_to?(:empty?) && value.empty? + c.quote(nil) + else + value.map { |v| c.quote(v) }.join(',') + end + else + c.quote(value) + end + end + + def raise_if_bind_arity_mismatch(statement, expected, provided) #:nodoc: + unless expected == provided + raise PreparedStatementInvalid, "wrong number of bind variables (#{provided} for #{expected}) in: #{statement}" + end + end + end + + # TODO: Deprecate this + def quoted_id #:nodoc: + quote_value(id, column_for_attribute(self.class.primary_key)) + end + + private + + # Quote strings appropriately for SQL statements. + def quote_value(value, column = nil) + self.class.connection.quote(value, column) + end + end +end diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb index 6fe305f843..cdde5cf3b9 100644 --- a/activerecord/lib/active_record/schema_dumper.rb +++ b/activerecord/lib/active_record/schema_dumper.rb @@ -190,6 +190,9 @@ HEADER index_lengths = (index.lengths || []).compact statement_parts << (':length => ' + Hash[index.columns.zip(index.lengths)].inspect) unless index_lengths.empty? + index_orders = (index.orders || {}) + statement_parts << (':order => ' + index.orders.inspect) unless index_orders.empty? + ' ' + statement_parts.join(', ') end diff --git a/activerecord/lib/active_record/scoping.rb b/activerecord/lib/active_record/scoping.rb new file mode 100644 index 0000000000..a8f5e96190 --- /dev/null +++ b/activerecord/lib/active_record/scoping.rb @@ -0,0 +1,152 @@ +require 'active_support/concern' + +module ActiveRecord + module Scoping + extend ActiveSupport::Concern + + included do + include Default + include Named + end + + module ClassMethods + # with_scope lets you apply options to inner block incrementally. It takes a hash and the keys must be + # <tt>:find</tt> or <tt>:create</tt>. <tt>:find</tt> parameter is <tt>Relation</tt> while + # <tt>:create</tt> parameters are an attributes hash. + # + # class Article < ActiveRecord::Base + # def self.create_with_scope + # with_scope(:find => where(:blog_id => 1), :create => { :blog_id => 1 }) do + # find(1) # => SELECT * from articles WHERE blog_id = 1 AND id = 1 + # a = create(1) + # a.blog_id # => 1 + # end + # end + # end + # + # In nested scopings, all previous parameters are overwritten by the innermost rule, with the exception of + # <tt>where</tt>, <tt>includes</tt>, and <tt>joins</tt> operations in <tt>Relation</tt>, which are merged. + # + # <tt>joins</tt> operations are uniqued so multiple scopes can join in the same table without table aliasing + # problems. If you need to join multiple tables, but still want one of the tables to be uniqued, use the + # array of strings format for your joins. + # + # class Article < ActiveRecord::Base + # def self.find_with_scope + # with_scope(:find => where(:blog_id => 1).limit(1), :create => { :blog_id => 1 }) do + # with_scope(:find => limit(10)) do + # all # => SELECT * from articles WHERE blog_id = 1 LIMIT 10 + # end + # with_scope(:find => where(:author_id => 3)) do + # all # => SELECT * from articles WHERE blog_id = 1 AND author_id = 3 LIMIT 1 + # end + # end + # end + # end + # + # You can ignore any previous scopings by using the <tt>with_exclusive_scope</tt> method. + # + # class Article < ActiveRecord::Base + # def self.find_with_exclusive_scope + # with_scope(:find => where(:blog_id => 1).limit(1)) do + # with_exclusive_scope(:find => limit(10)) do + # all # => SELECT * from articles LIMIT 10 + # end + # end + # end + # end + # + # *Note*: the +:find+ scope also has effect on update and deletion methods, like +update_all+ and +delete_all+. + def with_scope(scope = {}, action = :merge, &block) + # If another Active Record class has been passed in, get its current scope + scope = scope.current_scope if !scope.is_a?(Relation) && scope.respond_to?(:current_scope) + + previous_scope = self.current_scope + + if scope.is_a?(Hash) + # Dup first and second level of hash (method and params). + scope = scope.dup + scope.each do |method, params| + scope[method] = params.dup unless params == true + end + + scope.assert_valid_keys([ :find, :create ]) + relation = construct_finder_arel(scope[:find] || {}) + relation.default_scoped = true unless action == :overwrite + + if previous_scope && previous_scope.create_with_value && scope[:create] + scope_for_create = if action == :merge + previous_scope.create_with_value.merge(scope[:create]) + else + scope[:create] + end + + relation = relation.create_with(scope_for_create) + else + scope_for_create = scope[:create] + scope_for_create ||= previous_scope.create_with_value if previous_scope + relation = relation.create_with(scope_for_create) if scope_for_create + end + + scope = relation + end + + scope = previous_scope.merge(scope) if previous_scope && action == :merge + + self.current_scope = scope + begin + yield + ensure + self.current_scope = previous_scope + end + end + + protected + + # Works like with_scope, but discards any nested properties. + def with_exclusive_scope(method_scoping = {}, &block) + if method_scoping.values.any? { |e| e.is_a?(ActiveRecord::Relation) } + raise ArgumentError, <<-MSG + New finder API can not be used with_exclusive_scope. You can either call unscoped to get an anonymous scope not bound to the default_scope: + + User.unscoped.where(:active => true) + + Or call unscoped with a block: + + User.unscoped do + User.where(:active => true).all + end + + MSG + end + with_scope(method_scoping, :overwrite, &block) + end + + def current_scope #:nodoc: + Thread.current["#{self}_current_scope"] + end + + def current_scope=(scope) #:nodoc: + Thread.current["#{self}_current_scope"] = scope + end + + private + + def construct_finder_arel(options = {}, scope = nil) + relation = options.is_a?(Hash) ? unscoped.apply_finder_options(options) : options + relation = scope.merge(relation) if scope + relation + end + + end + + def populate_with_current_scope_attributes + return unless self.class.scope_attributes? + + self.class.scope_attributes.each do |att,value| + send("#{att}=", value) if respond_to?("#{att}=") + end + end + + end +end diff --git a/activerecord/lib/active_record/scoping/default.rb b/activerecord/lib/active_record/scoping/default.rb new file mode 100644 index 0000000000..5f05d146f2 --- /dev/null +++ b/activerecord/lib/active_record/scoping/default.rb @@ -0,0 +1,142 @@ +require 'active_support/concern' + +module ActiveRecord + module Scoping + module Default + extend ActiveSupport::Concern + + included do + # Stores the default scope for the class + config_attribute :default_scopes + self.default_scopes = [] + end + + module ClassMethods + # Returns a scope for the model without the default_scope. + # + # class Post < ActiveRecord::Base + # def self.default_scope + # where :published => true + # end + # end + # + # Post.all # Fires "SELECT * FROM posts WHERE published = true" + # Post.unscoped.all # Fires "SELECT * FROM posts" + # + # This method also accepts a block. All queries inside the block will + # not use the default_scope: + # + # Post.unscoped { + # Post.limit(10) # Fires "SELECT * FROM posts LIMIT 10" + # } + # + # It is recommended to use the block form of unscoped because chaining + # unscoped with <tt>scope</tt> does not work. Assuming that + # <tt>published</tt> is a <tt>scope</tt>, the following two statements + # are equal: the default_scope is applied on both. + # + # Post.unscoped.published + # Post.published + def unscoped #:nodoc: + block_given? ? relation.scoping { yield } : relation + end + + def before_remove_const #:nodoc: + self.current_scope = nil + end + + protected + + # Use this macro in your model to set a default scope for all operations on + # the model. + # + # class Article < ActiveRecord::Base + # default_scope where(:published => true) + # end + # + # Article.all # => SELECT * FROM articles WHERE published = true + # + # The <tt>default_scope</tt> is also applied while creating/building a record. It is not + # applied while updating a record. + # + # Article.new.published # => true + # Article.create.published # => true + # + # You can also use <tt>default_scope</tt> with a block, in order to have it lazily evaluated: + # + # class Article < ActiveRecord::Base + # default_scope { where(:published_at => Time.now - 1.week) } + # end + # + # (You can also pass any object which responds to <tt>call</tt> to the <tt>default_scope</tt> + # macro, and it will be called when building the default scope.) + # + # If you use multiple <tt>default_scope</tt> declarations in your model then they will + # be merged together: + # + # class Article < ActiveRecord::Base + # default_scope where(:published => true) + # default_scope where(:rating => 'G') + # end + # + # Article.all # => SELECT * FROM articles WHERE published = true AND rating = 'G' + # + # This is also the case with inheritance and module includes where the parent or module + # defines a <tt>default_scope</tt> and the child or including class defines a second one. + # + # If you need to do more complex things with a default scope, you can alternatively + # define it as a class method: + # + # class Article < ActiveRecord::Base + # def self.default_scope + # # Should return a scope, you can call 'super' here etc. + # end + # end + def default_scope(scope = {}) + scope = Proc.new if block_given? + self.default_scopes = default_scopes + [scope] + end + + def build_default_scope #:nodoc: + if method(:default_scope).owner != ActiveRecord::Scoping::Default::ClassMethods + evaluate_default_scope { default_scope } + elsif default_scopes.any? + evaluate_default_scope do + default_scopes.inject(relation) do |default_scope, scope| + if scope.is_a?(Hash) + default_scope.apply_finder_options(scope) + elsif !scope.is_a?(Relation) && scope.respond_to?(:call) + default_scope.merge(scope.call) + else + default_scope.merge(scope) + end + end + end + end + end + + def ignore_default_scope? #:nodoc: + Thread.current["#{self}_ignore_default_scope"] + end + + def ignore_default_scope=(ignore) #:nodoc: + Thread.current["#{self}_ignore_default_scope"] = ignore + end + + # The ignore_default_scope flag is used to prevent an infinite recursion situation where + # a default scope references a scope which has a default scope which references a scope... + def evaluate_default_scope + return if ignore_default_scope? + + begin + self.ignore_default_scope = true + yield + ensure + self.ignore_default_scope = false + end + end + + end + end + end +end diff --git a/activerecord/lib/active_record/scoping/named.rb b/activerecord/lib/active_record/scoping/named.rb new file mode 100644 index 0000000000..0edc3f1dcc --- /dev/null +++ b/activerecord/lib/active_record/scoping/named.rb @@ -0,0 +1,202 @@ +require 'active_support/core_ext/array' +require 'active_support/core_ext/hash/except' +require 'active_support/core_ext/kernel/singleton_class' +require 'active_support/core_ext/object/blank' +require 'active_support/core_ext/class/attribute' + +module ActiveRecord + # = Active Record Named \Scopes + module Scoping + module Named + extend ActiveSupport::Concern + + module ClassMethods + # Returns an anonymous \scope. + # + # posts = Post.scoped + # posts.size # Fires "select count(*) from posts" and returns the count + # posts.each {|p| puts p.name } # Fires "select * from posts" and loads post objects + # + # fruits = Fruit.scoped + # fruits = fruits.where(:color => 'red') if options[:red_only] + # fruits = fruits.limit(10) if limited? + # + # Anonymous \scopes tend to be useful when procedurally generating complex + # queries, where passing intermediate values (\scopes) around as first-class + # objects is convenient. + # + # You can define a \scope that applies to all finders using + # ActiveRecord::Base.default_scope. + def scoped(options = nil) + if options + scoped.apply_finder_options(options) + else + if current_scope + current_scope.clone + else + scope = relation.clone + scope.default_scoped = true + scope + end + end + end + + ## + # Collects attributes from scopes that should be applied when creating + # an AR instance for the particular class this is called on. + def scope_attributes # :nodoc: + if current_scope + current_scope.scope_for_create + else + scope = relation.clone + scope.default_scoped = true + scope.scope_for_create + end + end + + ## + # Are there default attributes associated with this scope? + def scope_attributes? # :nodoc: + current_scope || default_scopes.any? + end + + # Adds a class method for retrieving and querying objects. A \scope represents a narrowing of a database query, + # such as <tt>where(:color => :red).select('shirts.*').includes(:washing_instructions)</tt>. + # + # class Shirt < ActiveRecord::Base + # scope :red, where(:color => 'red') + # scope :dry_clean_only, joins(:washing_instructions).where('washing_instructions.dry_clean_only = ?', true) + # end + # + # The above calls to <tt>scope</tt> define class methods Shirt.red and Shirt.dry_clean_only. Shirt.red, + # in effect, represents the query <tt>Shirt.where(:color => 'red')</tt>. + # + # Note that this is simply 'syntactic sugar' for defining an actual class method: + # + # class Shirt < ActiveRecord::Base + # def self.red + # where(:color => 'red') + # end + # end + # + # Unlike <tt>Shirt.find(...)</tt>, however, the object returned by Shirt.red is not an Array; it + # resembles the association object constructed by a <tt>has_many</tt> declaration. For instance, + # you can invoke <tt>Shirt.red.first</tt>, <tt>Shirt.red.count</tt>, <tt>Shirt.red.where(:size => 'small')</tt>. + # Also, just as with the association objects, named \scopes act like an Array, implementing Enumerable; + # <tt>Shirt.red.each(&block)</tt>, <tt>Shirt.red.first</tt>, and <tt>Shirt.red.inject(memo, &block)</tt> + # all behave as if Shirt.red really was an Array. + # + # These named \scopes are composable. For instance, <tt>Shirt.red.dry_clean_only</tt> will produce + # all shirts that are both red and dry clean only. + # Nested finds and calculations also work with these compositions: <tt>Shirt.red.dry_clean_only.count</tt> + # returns the number of garments for which these criteria obtain. Similarly with + # <tt>Shirt.red.dry_clean_only.average(:thread_count)</tt>. + # + # All \scopes are available as class methods on the ActiveRecord::Base descendant upon which + # the \scopes were defined. But they are also available to <tt>has_many</tt> associations. If, + # + # class Person < ActiveRecord::Base + # has_many :shirts + # end + # + # then <tt>elton.shirts.red.dry_clean_only</tt> will return all of Elton's red, dry clean + # only shirts. + # + # Named \scopes can also be procedural: + # + # class Shirt < ActiveRecord::Base + # scope :colored, lambda { |color| where(:color => color) } + # end + # + # In this example, <tt>Shirt.colored('puce')</tt> finds all puce shirts. + # + # On Ruby 1.9 you can use the 'stabby lambda' syntax: + # + # scope :colored, ->(color) { where(:color => color) } + # + # Note that scopes defined with \scope will be evaluated when they are defined, rather than + # when they are used. For example, the following would be incorrect: + # + # class Post < ActiveRecord::Base + # scope :recent, where('published_at >= ?', Time.current - 1.week) + # end + # + # The example above would be 'frozen' to the <tt>Time.current</tt> value when the <tt>Post</tt> + # class was defined, and so the resultant SQL query would always be the same. The correct + # way to do this would be via a lambda, which will re-evaluate the scope each time + # it is called: + # + # class Post < ActiveRecord::Base + # scope :recent, lambda { where('published_at >= ?', Time.current - 1.week) } + # end + # + # Named \scopes can also have extensions, just as with <tt>has_many</tt> declarations: + # + # class Shirt < ActiveRecord::Base + # scope :red, where(:color => 'red') do + # def dom_id + # 'red_shirts' + # end + # end + # end + # + # Scopes can also be used while creating/building a record. + # + # class Article < ActiveRecord::Base + # scope :published, where(:published => true) + # end + # + # Article.published.new.published # => true + # Article.published.create.published # => true + # + # Class methods on your model are automatically available + # on scopes. Assuming the following setup: + # + # class Article < ActiveRecord::Base + # scope :published, where(:published => true) + # scope :featured, where(:featured => true) + # + # def self.latest_article + # order('published_at desc').first + # end + # + # def self.titles + # map(&:title) + # end + # + # end + # + # We are able to call the methods like this: + # + # Article.published.featured.latest_article + # Article.featured.titles + + def scope(name, scope_options = {}) + name = name.to_sym + valid_scope_name?(name) + extension = Module.new(&Proc.new) if block_given? + + scope_proc = lambda do |*args| + options = scope_options.respond_to?(:call) ? unscoped { scope_options.call(*args) } : scope_options + options = scoped.apply_finder_options(options) if options.is_a?(Hash) + + relation = scoped.merge(options) + + extension ? relation.extending(extension) : relation + end + + singleton_class.send(:redefine_method, name, &scope_proc) + end + + protected + + def valid_scope_name?(name) + if respond_to?(name, true) + logger.warn "Creating scope :#{name}. " \ + "Overwriting existing method #{self.name}.#{name}." + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/session_store.rb b/activerecord/lib/active_record/session_store.rb index 76c37cc367..e3bbd06f7e 100644 --- a/activerecord/lib/active_record/session_store.rb +++ b/activerecord/lib/active_record/session_store.rb @@ -59,12 +59,12 @@ module ActiveRecord end def drop_table! - connection_pool.clear_table_cache!(table_name) + connection.schema_cache.clear_table_cache!(table_name) connection.drop_table table_name end def create_table! - connection_pool.clear_table_cache!(table_name) + connection.schema_cache.clear_table_cache!(table_name) connection.create_table(table_name) do |t| t.string session_id_column, :limit => 255 t.text data_column_name diff --git a/activerecord/lib/active_record/store.rb b/activerecord/lib/active_record/store.rb new file mode 100644 index 0000000000..8cc84f81d0 --- /dev/null +++ b/activerecord/lib/active_record/store.rb @@ -0,0 +1,50 @@ +module ActiveRecord + # Store gives you a thin wrapper around serialize for the purpose of storing hashes in a single column. + # It's like a simple key/value store backed into your record when you don't care about being able to + # query that store outside the context of a single record. + # + # You can then declare accessors to this store that are then accessible just like any other attribute + # of the model. This is very helpful for easily exposing store keys to a form or elsewhere that's + # already built around just accessing attributes on the model. + # + # Make sure that you declare the database column used for the serialized store as a text, so there's + # plenty of room. + # + # Examples: + # + # class User < ActiveRecord::Base + # store :settings, accessors: [ :color, :homepage ] + # end + # + # u = User.new(color: 'black', homepage: '37signals.com') + # u.color # Accessor stored attribute + # u.settings[:country] = 'Denmark' # Any attribute, even if not specified with an accessor + # + # # Add additional accessors to an existing store through store_accessor + # class SuperUser < User + # store_accessor :settings, :privileges, :servants + # end + module Store + extend ActiveSupport::Concern + + module ClassMethods + def store(store_attribute, options = {}) + serialize store_attribute, Hash + store_accessor(store_attribute, options[:accessors]) if options.has_key? :accessors + end + + def store_accessor(store_attribute, *keys) + Array(keys).flatten.each do |key| + define_method("#{key}=") do |value| + send(store_attribute)[key] = value + send("#{store_attribute}_will_change!") + end + + define_method(key) do + send(store_attribute)[key] + end + end + end + end + end +end
\ No newline at end of file diff --git a/activerecord/lib/active_record/test_case.rb b/activerecord/lib/active_record/test_case.rb index ffe9b08dce..5398a14fc6 100644 --- a/activerecord/lib/active_record/test_case.rb +++ b/activerecord/lib/active_record/test_case.rb @@ -13,13 +13,6 @@ module ActiveRecord ActiveRecord::IdentityMap.clear end - # Backport skip to Ruby 1.8. test/unit doesn't support it, so just - # make it a noop. - unless instance_methods.map(&:to_s).include?("skip") - def skip(message) - end - end - def assert_date_from_db(expected, actual, message = nil) # SybaseAdapter doesn't have a separate column type just for dates, # so the time is in the string and incorrectly formatted @@ -56,18 +49,5 @@ module ActiveRecord ensure ActiveRecord::SQLCounter.ignored_sql = prev_ignored_sql end - - def with_kcode(kcode) - if RUBY_VERSION < '1.9' - orig_kcode, $KCODE = $KCODE, kcode - begin - yield - ensure - $KCODE = orig_kcode - end - else - yield - end - end end end diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb index 4d5e469a7f..c717fdea47 100644 --- a/activerecord/lib/active_record/timestamp.rb +++ b/activerecord/lib/active_record/timestamp.rb @@ -33,7 +33,7 @@ module ActiveRecord extend ActiveSupport::Concern included do - class_attribute :record_timestamps, :instance_writer => false + config_attribute :record_timestamps, :instance_writer => true self.record_timestamps = true end diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index d4870dd3f2..2c70d31b94 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -301,9 +301,9 @@ module ActiveRecord protected # Save the new record state and id of a record so it can be restored later if a transaction fails. - def remember_transaction_record_state #:nodoc + def remember_transaction_record_state #:nodoc: @_start_transaction_state ||= {} - @_start_transaction_state[:id] = id if self.class.primary_key? + @_start_transaction_state[:id] = id if has_attribute?(self.class.primary_key) unless @_start_transaction_state.include?(:new_record) @_start_transaction_state[:new_record] = @new_record end @@ -314,7 +314,7 @@ module ActiveRecord end # Clear the new record state and id of a record. - def clear_transaction_record_state #:nodoc + def clear_transaction_record_state #:nodoc: if defined?(@_start_transaction_state) @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1 remove_instance_variable(:@_start_transaction_state) if @_start_transaction_state[:level] < 1 @@ -322,7 +322,7 @@ module ActiveRecord end # Restore the new record state and id of a record that was previously saved by a call to save_record_state. - def restore_transaction_record_state(force = false) #:nodoc + def restore_transaction_record_state(force = false) #:nodoc: if defined?(@_start_transaction_state) @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1 if @_start_transaction_state[:level] < 1 @@ -341,12 +341,12 @@ module ActiveRecord end # Determine if a record was created or destroyed in a transaction. State should be one of :new_record or :destroyed. - def transaction_record_state(state) #:nodoc + def transaction_record_state(state) #:nodoc: @_start_transaction_state[state] if defined?(@_start_transaction_state) end # Determine if a transaction included an action for :create, :update, or :destroy. Used in filtering callbacks. - def transaction_include_action?(action) #:nodoc + def transaction_include_action?(action) #:nodoc: case action when :create transaction_record_state(:new_record) diff --git a/activerecord/lib/active_record/translation.rb b/activerecord/lib/active_record/translation.rb new file mode 100644 index 0000000000..ddcb5f2a7a --- /dev/null +++ b/activerecord/lib/active_record/translation.rb @@ -0,0 +1,22 @@ +module ActiveRecord + module Translation + include ActiveModel::Translation + + # Set the lookup ancestors for ActiveModel. + def lookup_ancestors #:nodoc: + klass = self + classes = [klass] + return classes if klass == ActiveRecord::Base + + while klass != klass.base_class + classes << klass = klass.superclass + end + classes + end + + # Set the i18n scope to overwrite ActiveModel. + def i18n_scope #:nodoc: + :activerecord + end + end +end diff --git a/activerecord/lib/active_record/validations/associated.rb b/activerecord/lib/active_record/validations/associated.rb index 7af0352a31..9f072c4c39 100644 --- a/activerecord/lib/active_record/validations/associated.rb +++ b/activerecord/lib/active_record/validations/associated.rb @@ -2,8 +2,9 @@ module ActiveRecord module Validations class AssociatedValidator < ActiveModel::EachValidator def validate_each(record, attribute, value) - return if (value.is_a?(Array) ? value : [value]).collect{ |r| r.nil? || r.valid? }.all? - record.errors.add(attribute, :invalid, options.merge(:value => value)) + if Array.wrap(value).reject {|r| r.marked_for_destruction? || r.valid?}.any? + record.errors.add(attribute, :invalid, options.merge(:value => value)) + end end end diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index 484b1d369b..2e2ea8c42b 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -57,8 +57,8 @@ module ActiveRecord value = column.limit ? value.to_s.mb_chars[0, column.limit] : value.to_s if column.text? if !options[:case_sensitive] && value && column.text? - # will use SQL LOWER function before comparison - relation = table[attribute].lower.eq(table.lower(value)) + # will use SQL LOWER function before comparison, unless it detects a case insensitive collation + relation = klass.connection.case_insensitive_comparison(table, attribute, column, value) else value = klass.connection.case_sensitive_modifier(value) relation = table[attribute].eq(value) diff --git a/activerecord/lib/active_record/version.rb b/activerecord/lib/active_record/version.rb index 838aa8fb1e..0c35adc11d 100644 --- a/activerecord/lib/active_record/version.rb +++ b/activerecord/lib/active_record/version.rb @@ -1,7 +1,7 @@ module ActiveRecord module VERSION #:nodoc: - MAJOR = 3 - MINOR = 2 + MAJOR = 4 + MINOR = 0 TINY = 0 PRE = "beta" diff --git a/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb b/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb index f6159deeeb..1509e34473 100644 --- a/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb +++ b/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb @@ -3,7 +3,7 @@ require 'rails/generators/active_record' module ActiveRecord module Generators class MigrationGenerator < Base - argument :attributes, :type => :array, :default => [], :banner => "field:type field:type" + argument :attributes, :type => :array, :default => [], :banner => "field[:type][:index] field[:type][:index]" def create_migration_file set_local_assigns! diff --git a/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb b/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb index ce8d7eed42..d084a00ed7 100644 --- a/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb +++ b/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb @@ -2,14 +2,20 @@ class <%= migration_class_name %> < ActiveRecord::Migration <%- if migration_action == 'add' -%> def change <% attributes.each do |attribute| -%> - add_column :<%= table_name %>, :<%= attribute.name %>, :<%= attribute.type %> + add_column :<%= table_name %>, :<%= attribute.name %>, :<%= attribute.type %><%= attribute.inject_options %> + <%- if attribute.has_index? -%> + add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %> + <%- end %> <%- end -%> end <%- else -%> def up <% attributes.each do |attribute| -%> <%- if migration_action -%> - <%= migration_action %>_column :<%= table_name %>, :<%= attribute.name %><% if migration_action == 'add' %>, :<%= attribute.type %><% end %> + <%= migration_action %>_column :<%= table_name %>, :<%= attribute.name %><% if migration_action == 'add' %>, :<%= attribute.type %><%= attribute.inject_options %><% end %> + <% if attribute.has_index? && migration_action == 'add' %> + add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %> + <% end -%> <%- end -%> <%- end -%> end @@ -17,7 +23,7 @@ class <%= migration_class_name %> < ActiveRecord::Migration def down <% attributes.reverse.each do |attribute| -%> <%- if migration_action -%> - <%= migration_action == 'add' ? 'remove' : 'add' %>_column :<%= table_name %>, :<%= attribute.name %><% if migration_action == 'remove' %>, :<%= attribute.type %><% end %> + <%= migration_action == 'add' ? 'remove' : 'add' %>_column :<%= table_name %>, :<%= attribute.name %><% if migration_action == 'remove' %>, :<%= attribute.type %><%= attribute.inject_options %><% end %> <%- end -%> <%- end -%> end diff --git a/activerecord/lib/rails/generators/active_record/model/model_generator.rb b/activerecord/lib/rails/generators/active_record/model/model_generator.rb index f7caa43ac8..99a022461e 100644 --- a/activerecord/lib/rails/generators/active_record/model/model_generator.rb +++ b/activerecord/lib/rails/generators/active_record/model/model_generator.rb @@ -3,7 +3,7 @@ require 'rails/generators/active_record' module ActiveRecord module Generators class ModelGenerator < Base - argument :attributes, :type => :array, :default => [], :banner => "field:type field:type" + argument :attributes, :type => :array, :default => [], :banner => "field[:type][:index] field[:type][:index]" check_class_collision @@ -26,6 +26,10 @@ module ActiveRecord template 'module.rb', File.join('app/models', "#{class_path.join('/')}.rb") if behavior == :invoke end + def attributes_with_index + attributes.select { |a| a.has_index? || (a.reference? && options[:indexes]) } + end + hook_for :test_framework protected diff --git a/activerecord/lib/rails/generators/active_record/model/templates/migration.rb b/activerecord/lib/rails/generators/active_record/model/templates/migration.rb index 851930344a..3a3cf86d73 100644 --- a/activerecord/lib/rails/generators/active_record/model/templates/migration.rb +++ b/activerecord/lib/rails/generators/active_record/model/templates/migration.rb @@ -2,16 +2,14 @@ class <%= migration_class_name %> < ActiveRecord::Migration def change create_table :<%= table_name %> do |t| <% attributes.each do |attribute| -%> - t.<%= attribute.type %> :<%= attribute.name %> + t.<%= attribute.type %> :<%= attribute.name %><%= attribute.inject_options %> <% end -%> <% if options[:timestamps] %> t.timestamps <% end -%> end -<% if options[:indexes] -%> -<% attributes.select {|attr| attr.reference? }.each do |attribute| -%> - add_index :<%= table_name %>, :<%= attribute.name %>_id -<% end -%> +<% attributes_with_index.each do |attribute| -%> + add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %> <% end -%> end end |