diff options
Diffstat (limited to 'activerecord/lib/active_record')
15 files changed, 269 insertions, 246 deletions
diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index c7a24de11d..c30e8e08b8 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -1596,7 +1596,7 @@ module ActiveRecord # has_and_belongs_to_many :categories, :join_table => "prods_cats" # has_and_belongs_to_many :categories, :readonly => true # has_and_belongs_to_many :active_projects, :join_table => 'developers_projects', :delete_sql => - # "DELETE FROM developers_projects WHERE active=1 AND developer_id = #{id} AND project_id = #{record.id}" + # proc { |record| "DELETE FROM developers_projects WHERE active=1 AND developer_id = #{id} AND project_id = #{record.id}" } def has_and_belongs_to_many(name, options = {}, &extension) Builder::HasAndBelongsToMany.build(self, name, options, &extension) end diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb index ad029d1101..261a829281 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -79,9 +79,9 @@ module ActiveRecord end def method_missing(method, *args, &block) - match = DynamicFinderMatch.match(method) - if match && match.instantiator? - send(:find_or_instantiator_by_attributes, match, match.attribute_names, *args) do |r| + match = DynamicMatchers::Method.match(self, method) + if match && match.is_a?(DynamicMatchers::Instantiator) + scoped.send(method, *args) do |r| proxy_association.send :set_owner_attributes, r proxy_association.send :add_to_target, r yield(r) if block_given? @@ -101,7 +101,7 @@ module ActiveRecord end else - scoped.readonly(nil).send(method, *args, &block) + scoped.readonly(nil).public_send(method, *args, &block) end end 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 3546873550..f0b6ae2b7d 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -408,7 +408,7 @@ module ActiveRecord # t.remove(:qualification) # t.remove(:qualification, :experience) def remove(*column_names) - @base.remove_column(@table_name, column_names) + @base.remove_column(@table_name, *column_names) end # Removes the given index from the table. 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 30a4f9aa35..e7a4f061fd 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -618,8 +618,6 @@ module ActiveRecord end def columns_for_remove(table_name, *column_names) - column_names = column_names.flatten - raise ArgumentError.new("You must specify at least one column name. Example: remove_column(:people, :first_name)") if column_names.blank? column_names.map {|column_name| quote_column_name(column_name) } end diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb index b7e1513422..9af8e46120 100644 --- a/activerecord/lib/active_record/connection_adapters/column.rb +++ b/activerecord/lib/active_record/connection_adapters/column.rb @@ -95,7 +95,7 @@ module ActiveRecord case type when :string, :text then value - when :integer then value.to_i rescue value ? 1 : 0 + when :integer then value.to_i when :float then value.to_f when :decimal then klass.value_to_decimal(value) when :datetime, :timestamp then klass.string_to_time(value) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 273c165084..68cf495025 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -1233,7 +1233,10 @@ module ActiveRecord # Construct a clean list of column names from the ORDER BY clause, removing # any ASC/DESC modifiers - order_columns = orders.collect { |s| s.gsub(/\s+(ASC|DESC)\s*(NULLS\s+(FIRST|LAST)\s*)?/i, '') } + order_columns = orders.collect do |s| + s = s.to_sql unless s.is_a?(String) + s.gsub(/\s+(ASC|DESC)\s*(NULLS\s+(FIRST|LAST)\s*)?/i, '') + end order_columns.delete_if { |c| c.blank? } order_columns = order_columns.zip((0...order_columns.size).to_a).map { |s,i| "#{s} AS alias_#{i}" } diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index 83f75e3505..44e407a561 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -446,7 +446,7 @@ module ActiveRecord def remove_column(table_name, *column_names) #:nodoc: raise ArgumentError.new("You must specify at least one column name. Example: remove_column(:people, :first_name)") if column_names.empty? - column_names.flatten.each do |column_name| + column_names.each do |column_name| alter_table(table_name) do |definition| definition.columns.delete(definition[column_name]) end diff --git a/activerecord/lib/active_record/dynamic_finder_match.rb b/activerecord/lib/active_record/dynamic_finder_match.rb deleted file mode 100644 index 0473d6aafc..0000000000 --- a/activerecord/lib/active_record/dynamic_finder_match.rb +++ /dev/null @@ -1,108 +0,0 @@ -module ActiveRecord - - # = Active Record Dynamic Finder Match - # - # Refer to ActiveRecord::Base documentation for Dynamic attribute-based finders for detailed info - # - class DynamicFinderMatch - def self.match(method) - method = method.to_s - klass = klasses.find do |_klass| - _klass.matches?(method) - end - klass.new(method) if klass - end - - def self.matches?(method) - method =~ self::METHOD_PATTERN - end - - def self.klasses - [FindBy, FindByBang, FindOrInitializeCreateBy, FindOrCreateByBang] - end - - def initialize(method) - @finder = :first - @instantiator = nil - match_data = method.match(self.class::METHOD_PATTERN) - @attribute_names = match_data[-1].split("_and_") - initialize_from_match_data(match_data) - end - - attr_reader :finder, :attribute_names, :instantiator - - def finder? - @finder && !@instantiator - end - - def creator? - @finder == :first && @instantiator == :create - end - - def instantiator? - @instantiator - end - - def bang? - false - end - - def valid_arguments?(arguments) - arguments.size >= @attribute_names.size - end - - def save_record? - @instantiator == :create - end - - def save_method - bang? ? :save! : :save - end - - private - - def initialize_from_match_data(match_data) - end - end - - class FindBy < DynamicFinderMatch - METHOD_PATTERN = /^find_(all_|last_)?by_([_a-zA-Z]\w*)$/ - - def initialize_from_match_data(match_data) - @finder = :last if match_data[1] == 'last_' - @finder = :all if match_data[1] == 'all_' - end - end - - class FindByBang < DynamicFinderMatch - METHOD_PATTERN = /^find_by_([_a-zA-Z]\w*)\!$/ - - def bang? - true - end - end - - class FindOrInitializeCreateBy < DynamicFinderMatch - METHOD_PATTERN = /^find_or_(initialize|create)_by_([_a-zA-Z]\w*)$/ - - def initialize_from_match_data(match_data) - @instantiator = match_data[1] == 'initialize' ? :new : :create - end - - def valid_arguments?(arguments) - arguments.size == 1 && arguments.first.is_a?(Hash) || super - end - end - - class FindOrCreateByBang < DynamicFinderMatch - METHOD_PATTERN = /^find_or_create_by_([_a-zA-Z]\w*)\!$/ - - def initialize_from_match_data(match_data) - @instantiator = :create - end - - def bang? - true - end - end -end diff --git a/activerecord/lib/active_record/dynamic_matchers.rb b/activerecord/lib/active_record/dynamic_matchers.rb index e35b1c91a0..8726442d0b 100644 --- a/activerecord/lib/active_record/dynamic_matchers.rb +++ b/activerecord/lib/active_record/dynamic_matchers.rb @@ -1,10 +1,8 @@ module ActiveRecord module DynamicMatchers - def respond_to?(method_id, include_private = false) - match = find_dynamic_match(method_id) - valid_match = match && all_attributes_exists?(match.attribute_names) - - valid_match || super + def respond_to?(name, include_private = false) + match = Method.match(self, name) + match && match.valid? || super end private @@ -18,66 +16,217 @@ module ActiveRecord # # 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 = find_dynamic_match(method_id) - attribute_names = match.attribute_names - super unless all_attributes_exists?(attribute_names) - - unless match.valid_arguments?(arguments) - 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 + def method_missing(name, *arguments, &block) + match = Method.match(self, name) - if match.respond_to?(:scope?) && match.scope? - define_scope_method(method_id, attribute_names) - 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 + if match && match.valid? + match.define + send(name, *arguments, &block) else super end end - def define_scope_method(method_id, attribute_names) #:nodoc - self.class_eval <<-METHOD, __FILE__, __LINE__ + 1 - def self.#{method_id}(*args) # def self.scoped_by_user_name_and_password(*args) - conditions = Hash[[:#{attribute_names.join(',:')}].zip(args)] # conditions = Hash[[:user_name, :password].zip(args)] - where(conditions) # where(conditions) - end # end - METHOD + class Method + def self.match(model, name) + klass = klasses.find { |k| name =~ k.pattern } + klass.new(model, name) if klass + end + + def self.klasses + [ + FindBy, FindAllBy, FindLastBy, FindByBang, ScopedBy, + FindOrInitializeBy, FindOrCreateBy, FindOrCreateByBang + ] + end + + def self.pattern + /^#{prefix}_([_a-zA-Z]\w*)#{suffix}$/ + end + + def self.prefix + raise NotImplementedError + end + + def self.suffix + '' + end + + attr_reader :model, :name, :attribute_names + + def initialize(model, name) + @model = model + @name = name.to_s + @attribute_names = @name.match(self.class.pattern)[1].split('_and_') + end + + def valid? + attribute_names.all? { |name| model.columns_hash[name] || model.reflect_on_aggregation(name.to_sym) } + end + + def define + model.class_eval <<-CODE, __FILE__, __LINE__ + 1 + def self.#{name}(#{signature}) + #{body} + end + CODE + end + + def body + raise NotImplementedError + end end - def find_dynamic_match(method_id) #:nodoc: - DynamicFinderMatch.match(method_id) || DynamicScopeMatch.match(method_id) + class Finder < Method + def body + <<-CODE + result = #{result} + result && block_given? ? yield(result) : result + CODE + end + + def result + "scoped.apply_finder_options(options).#{finder}(#{attributes_hash})" + end + + def signature + attribute_names.join(', ') + ", options = {}" + end + + def attributes_hash + "{" + attribute_names.map { |name| ":#{name} => #{name}" }.join(',') + "}" + end + + def finder + raise NotImplementedError + end end - # Similar in purpose to +expand_hash_conditions_for_aggregates+. - def expand_attribute_names_for_aggregates(attribute_names) - attribute_names.map do |attribute_name| - if aggregation = reflect_on_aggregation(attribute_name.to_sym) - aggregate_mapping(aggregation).map do |field_attr, _| - field_attr.to_sym - end + class FindBy < Finder + def self.prefix + "find_by" + end + + def finder + "find_by" + end + end + + class FindByBang < Finder + def self.prefix + "find_by" + end + + def self.suffix + "!" + end + + def finder + "find_by!" + end + end + + class FindAllBy < Finder + def self.prefix + "find_all_by" + end + + def finder + "where" + end + + def result + "#{super}.to_a" + end + end + + class FindLastBy < Finder + def self.prefix + "find_last_by" + end + + def finder + "where" + end + + def result + "#{super}.last" + end + end + + class ScopedBy < Finder + def self.prefix + "scoped_by" + end + + def body + "where(#{attributes_hash})" + end + end + + class Instantiator < Method + # This is nasty, but it doesn't matter because it will be deprecated. + def self.dispatch(klass, attribute_names, instantiator, args, block) + if args.length == 1 && args.first.is_a?(Hash) + attributes = args.first.stringify_keys + conditions = attributes.slice(*attribute_names) + rest = [attributes.except(*attribute_names)] else - attribute_name.to_sym + raise ArgumentError, "too few arguments" unless args.length >= attribute_names.length + + conditions = Hash[attribute_names.map.with_index { |n, i| [n, args[i]] }] + rest = args.drop(attribute_names.length) end - end.flatten + + klass.where(conditions).first || + klass.create_with(conditions).send(instantiator, *rest, &block) + end + + def signature + "*args, &block" + end + + def body + "#{self.class}.dispatch(self, #{attribute_names.inspect}, #{instantiator.inspect}, args, block)" + end + + def instantiator + raise NotImplementedError + end end - def all_attributes_exists?(attribute_names) - (expand_attribute_names_for_aggregates(attribute_names) - - column_methods_hash.keys).empty? + class FindOrInitializeBy < Instantiator + def self.prefix + "find_or_initialize_by" + end + + def instantiator + "new" + end end - def aggregate_mapping(reflection) - mapping = reflection.options[:mapping] || [reflection.name, reflection.name] - mapping.first.is_a?(Array) ? mapping : [mapping] + class FindOrCreateBy < Instantiator + def self.prefix + "find_or_create_by" + end + + def instantiator + "create" + end + end + + class FindOrCreateByBang < Instantiator + def self.prefix + "find_or_create_by" + end + + def self.suffix + "!" + end + + def instantiator + "create!" + end end end end diff --git a/activerecord/lib/active_record/dynamic_scope_match.rb b/activerecord/lib/active_record/dynamic_scope_match.rb deleted file mode 100644 index 6c043d29c4..0000000000 --- a/activerecord/lib/active_record/dynamic_scope_match.rb +++ /dev/null @@ -1,30 +0,0 @@ -module ActiveRecord - - # = Active Record Dynamic Scope Match - # - # Provides dynamic attribute-based scopes such as <tt>scoped_by_price(4.99)</tt> - # if, for example, the <tt>Product</tt> has an attribute with that name. You can - # chain more <tt>scoped_by_* </tt> methods after the other. It acts like a named - # scope except that it's dynamic. - class DynamicScopeMatch - METHOD_PATTERN = /^scoped_by_([_a-zA-Z]\w*)$/ - - def self.match(method) - if method.to_s =~ METHOD_PATTERN - new(true, $1 && $1.split('_and_')) - end - end - - def initialize(scope, attribute_names) - @scope = scope - @attribute_names = attribute_names - end - - attr_reader :scope, :attribute_names - alias :scope? :scope - - def valid_arguments?(arguments) - arguments.size >= @attribute_names.size - end - end -end diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb index 29b8b2fb73..4d8283bcff 100644 --- a/activerecord/lib/active_record/querying.rb +++ b/activerecord/lib/active_record/querying.rb @@ -3,7 +3,7 @@ require 'active_support/deprecation' module ActiveRecord module Querying - delegate :find, :first, :first!, :last, :last!, :all, :exists?, :any?, :many?, :to => :scoped + delegate :find, :take, :take!, :first, :first!, :last, :last!, :all, :exists?, :any?, :many?, :to => :scoped delegate :first_or_create, :first_or_create!, :first_or_initialize, :to => :scoped delegate :find_by, :find_by!, :to => :scoped delegate :destroy, :destroy_all, :delete, :delete_all, :update, :update_all, :to => :scoped diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index d4f4d593c6..c380b5c029 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -153,6 +153,10 @@ module ActiveRecord # Holds all the meta-data about an aggregation as it was specified in the # Active Record class. class AggregateReflection < MacroReflection #:nodoc: + def mapping + mapping = options[:mapping] || [name, name] + mapping.first.is_a?(Array) ? mapping : [mapping] + end end # Holds all the meta-data about an association as it was specified in the diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index 333d31d8a3..779e052e3c 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -390,6 +390,8 @@ module ActiveRecord # If you need to destroy dependent associations or call your <tt>before_*</tt> or # +after_destroy+ callbacks, use the +destroy_all+ method instead. def delete_all(conditions = nil) + raise ActiveRecordError.new("delete_all doesn't support limit scope") if self.limit_value + if conditions where(conditions).delete_all else diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index 1ceb1949a4..a78e3d08e4 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -60,6 +60,28 @@ module ActiveRecord where(*args).first! end + # Gives a record (or N records if a parameter is supplied) without any implied + # order. The order will depend on the database implementation. + # If an order is supplied it will be respected. + # + # Examples: + # + # Person.take # returns an object fetched by SELECT * FROM people + # Person.take(5) # returns 5 objects fetched by SELECT * FROM people LIMIT 5 + # Person.where(["name LIKE '%?'", name]).take + def take(limit = nil) + limit ? limit(limit).to_a : find_take + end + + # Same as +take+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record + # is found. Note that <tt>take!</tt> accepts no arguments. + def take! + take or raise RecordNotFound + end + + # Find the first record (or first N records if a parameter is supplied). + # If no order is defined it will order by primary key. + # # Examples: # # Person.first # returns the first object fetched by SELECT * FROM people @@ -67,7 +89,15 @@ module ActiveRecord # Person.where(["user_name = :u", { :u => user_name }]).first # Person.order("created_on DESC").offset(5).first def first(limit = nil) - limit ? limit(limit).to_a : find_first + if limit + if order_values.empty? && primary_key + order(arel_table[primary_key].asc).limit(limit).to_a + else + limit(limit).to_a + end + else + find_first + end end # Same as +first+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record @@ -76,6 +106,9 @@ module ActiveRecord first or raise RecordNotFound end + # Find the last record (or last N records if a parameter is supplied). + # If no order is defined it will order by primary key. + # # Examples: # # Person.last # returns the last object fetched by SELECT * FROM people @@ -83,8 +116,8 @@ module ActiveRecord # Person.order("created_on DESC").offset(5).last def last(limit = nil) if limit - if order_values.empty? - order("#{primary_key} DESC").limit(limit).reverse + if order_values.empty? && primary_key + order(arel_table[primary_key].desc).limit(limit).reverse else to_a.last(limit) end @@ -210,47 +243,6 @@ module ActiveRecord ids_array.empty? ? raise(ThrowResult) : table[primary_key].in(ids_array) end - def find_by_attributes(match, attributes, *args) - conditions = Hash[attributes.map {|a| [a, args[attributes.index(a)]]}] - result = where(conditions).send(match.finder) - - if match.bang? && result.blank? - raise RecordNotFound, "Couldn't find #{@klass.name} with #{conditions.to_a.collect {|p| p.join(' = ')}.join(', ')}" - else - if block_given? && result - yield(result) - else - result - end - end - end - - def find_or_instantiator_by_attributes(match, attributes, *args) - options = args.size > 1 && args.last(2).all?{ |a| a.is_a?(Hash) } ? args.extract_options! : {} - protected_attributes_for_create, unprotected_attributes_for_create = {}, {} - args.each_with_index do |arg, i| - if arg.is_a?(Hash) - protected_attributes_for_create = args[i].with_indifferent_access - else - unprotected_attributes_for_create[attributes[i]] = args[i] - end - end - - conditions = (protected_attributes_for_create.merge(unprotected_attributes_for_create)).slice(*attributes).symbolize_keys - - record = where(conditions).first - - unless record - record = @klass.new(protected_attributes_for_create, options) do |r| - r.assign_attributes(unprotected_attributes_for_create, :without_protection => true) - end - yield(record) if block_given? - record.send(match.save_method) if match.save_record? - end - - record - end - def find_with_ids(*ids) return to_a.find { |*block_args| yield(*block_args) } if block_given? @@ -315,11 +307,24 @@ module ActiveRecord end end + def find_take + if loaded? + @records.take(1).first + else + @take ||= limit(1).to_a.first + end + end + def find_first if loaded? @records.first else - @first ||= limit(1).to_a[0] + @first ||= + if order_values.empty? && primary_key + order(arel_table[primary_key].asc).limit(1).to_a.first + else + limit(1).to_a.first + end end end @@ -331,7 +336,7 @@ module ActiveRecord if offset_value || limit_value to_a.last else - reverse_order.limit(1).to_a[0] + reverse_order.limit(1).to_a.first end end end diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb index 81b13fe529..5530be3219 100644 --- a/activerecord/lib/active_record/sanitization.rb +++ b/activerecord/lib/active_record/sanitization.rb @@ -58,7 +58,7 @@ module ActiveRecord expanded_attrs = {} attrs.each do |attr, value| if aggregation = reflect_on_aggregation(attr.to_sym) - mapping = aggregate_mapping(aggregation) + mapping = aggregation.mapping mapping.each do |field_attr, aggregate_attr| if mapping.size == 1 && !value.respond_to?(aggregate_attr) expanded_attrs[field_attr] = value |