diff options
Diffstat (limited to 'activerecord/lib/active_record')
28 files changed, 280 insertions, 76 deletions
diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 58725246c8..958821add6 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -1097,8 +1097,8 @@ module ActiveRecord # alongside this object by calling their +destroy+ method. If set to <tt>:delete_all</tt> all associated # objects are deleted *without* calling their +destroy+ method. If set to <tt>:nullify</tt> all associated # objects' foreign keys are set to +NULL+ *without* calling their +save+ callbacks. If set to - # <tt>:restrict</tt> this object raises an <tt>ActiveRecord::DeleteRestrictionError</tt> exception and - # cannot be deleted if it has any associated objects. + # <tt>:restrict</tt> an error will be added to the object, preventing its deletion, if any associated + # objects are present. # # If using with the <tt>:through</tt> option, the association on the join model must be # a +belongs_to+, and the records which get deleted are the join records, rather than @@ -1251,8 +1251,8 @@ module ActiveRecord # If set to <tt>:destroy</tt>, the associated object is destroyed when this object is. If set to # <tt>:delete</tt>, the associated object is deleted *without* calling its destroy method. # If set to <tt>:nullify</tt>, the associated object's foreign key is set to +NULL+. - # Also, association is assigned. If set to <tt>:restrict</tt> this object raises an - # <tt>ActiveRecord::DeleteRestrictionError</tt> exception and cannot be deleted if it has any associated object. + # If set to <tt>:restrict</tt>, an error will be added to the object, preventing its deletion, if an + # associated object is present. # [:foreign_key] # Specify the foreign key used for the association. By default this is guessed to be the name # of this class in lower-case and "_id" suffixed. So a Person class that makes a +has_one+ association diff --git a/activerecord/lib/active_record/associations/builder/association.rb b/activerecord/lib/active_record/associations/builder/association.rb index 6e2e5f9de0..c3fa4a05fd 100644 --- a/activerecord/lib/active_record/associations/builder/association.rb +++ b/activerecord/lib/active_record/associations/builder/association.rb @@ -51,5 +51,34 @@ module ActiveRecord::Associations::Builder association(name).writer(value) end end + + def dependent_restrict_raises? + ActiveRecord::Base.dependent_restrict_raises == true + end + + def dependent_restrict_deprecation_warning + if dependent_restrict_raises? + msg = "In the next release, `:dependent => :restrict` will not raise a `DeleteRestrictionError`. "\ + "Instead, it will add an error on the model. To fix this warning, make sure your code " \ + "isn't relying on a `DeleteRestrictionError` and then add " \ + "`config.active_record.dependent_restrict_raises = false` to your application config." + ActiveSupport::Deprecation.warn msg + end + end + + def define_restrict_dependency_method + name = self.name + mixin.redefine_method(dependency_method_name) do + # has_many or has_one associations + if send(name).respond_to?(:exists?) ? send(name).exists? : !send(name).nil? + if dependent_restrict_raises? + raise ActiveRecord::DeleteRestrictionError.new(name) + else + errors.add(:base, :restrict_dependent_destroy, :model => name.to_s.singularize) + return false + end + end + end + end end end diff --git a/activerecord/lib/active_record/associations/builder/has_many.rb b/activerecord/lib/active_record/associations/builder/has_many.rb index fc6799fb15..9ddfd433e4 100644 --- a/activerecord/lib/active_record/associations/builder/has_many.rb +++ b/activerecord/lib/active_record/associations/builder/has_many.rb @@ -21,6 +21,7 @@ module ActiveRecord::Associations::Builder ":nullify or :restrict (#{options[:dependent].inspect})" end + dependent_restrict_deprecation_warning if options[:dependent] == :restrict send("define_#{options[:dependent]}_dependency_method") model.before_destroy dependency_method_name end @@ -52,13 +53,6 @@ module ActiveRecord::Associations::Builder end end - def define_restrict_dependency_method - name = self.name - mixin.redefine_method(dependency_method_name) do - raise ActiveRecord::DeleteRestrictionError.new(name) unless send(name).empty? - end - end - def dependency_method_name "has_many_dependent_for_#{name}" end diff --git a/activerecord/lib/active_record/associations/builder/has_one.rb b/activerecord/lib/active_record/associations/builder/has_one.rb index 7a6cd3890f..bc8a212bee 100644 --- a/activerecord/lib/active_record/associations/builder/has_one.rb +++ b/activerecord/lib/active_record/associations/builder/has_one.rb @@ -34,15 +34,12 @@ module ActiveRecord::Associations::Builder ":nullify or :restrict (#{options[:dependent].inspect})" end + dependent_restrict_deprecation_warning if options[:dependent] == :restrict send("define_#{options[:dependent]}_dependency_method") model.before_destroy dependency_method_name end end - def dependency_method_name - "has_one_dependent_#{options[:dependent]}_for_#{name}" - end - def define_destroy_dependency_method name = self.name mixin.redefine_method(dependency_method_name) do @@ -52,11 +49,8 @@ module ActiveRecord::Associations::Builder 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 - mixin.redefine_method(dependency_method_name) do - raise ActiveRecord::DeleteRestrictionError.new(name) unless send(name).nil? - end + def dependency_method_name + "has_one_dependent_#{options[:dependent]}_for_#{name}" end end end diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index 7aed64d48c..b2136605e1 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -58,7 +58,7 @@ module ActiveRecord end end - relation.uniq.pluck(column) + relation.pluck(column) end end diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb index 3549cbb090..c129dc8c52 100644 --- a/activerecord/lib/active_record/attribute_methods/read.rb +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -92,11 +92,7 @@ module ActiveRecord end def internal_attribute_access_code(attr_name, cast_code) - if attr_name == primary_key - access_code = "v = @attributes[attr_name];" - else - access_code = "v = @attributes.fetch(attr_name) { missing_attribute(attr_name, caller) };" - end + access_code = "v = @attributes.fetch(attr_name) { missing_attribute(attr_name, caller) };" access_code << "v && #{cast_code};" diff --git a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb index 27ff13ad89..6ba64bb88f 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb @@ -65,18 +65,24 @@ module ActiveRecord end private - def cache_sql(sql, binds) - result = - if @query_cache[sql].key?(binds) - ActiveSupport::Notifications.instrument("sql.active_record", - :sql => sql, :name => "CACHE", :connection_id => object_id) - @query_cache[sql][binds] - else - @query_cache[sql][binds] = yield - end + def cache_sql(sql, binds) + result = + if @query_cache[sql].key?(binds) + ActiveSupport::Notifications.instrument("sql.active_record", + :sql => sql, :binds => binds, :name => "CACHE", :connection_id => object_id) + @query_cache[sql][binds] + else + @query_cache[sql][binds] = yield + end + # FIXME: we should guarantee that all cached items are Result + # objects. Then we can avoid this conditional + if ActiveRecord::Result === result + result.dup + else result.collect { |row| row.dup } end + end end 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 0cac6d1391..84c340770a 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -1,9 +1,12 @@ require 'active_support/deprecation/reporting' require 'active_record/schema_migration' +require 'active_record/migration/join_table' module ActiveRecord module ConnectionAdapters # :nodoc: module SchemaStatements + include ActiveRecord::Migration::JoinTable + # Returns a Hash of mappings from the abstract data types to the native # database types. See TableDefinition#column for details on the recognized # abstract data types. @@ -170,6 +173,45 @@ module ActiveRecord execute create_sql end + # Creates a new join table with the name created using the lexical order of the first two + # arguments. These arguments can be be a String or a Symbol. + # + # # Creates a table called 'assemblies_parts' with no id. + # create_join_table(:assemblies, :parts) + # + # You can pass a +options+ hash can include the following keys: + # [<tt>:table_name</tt>] + # Sets the table name overriding the default + # [<tt>:column_options</tt>] + # Any extra options you want appended to the columns definition. + # [<tt>:options</tt>] + # Any extra options you want appended to the table definition. + # [<tt>:temporary</tt>] + # Make a temporary table. + # [<tt>:force</tt>] + # Set to true to drop the table before creating it. + # Defaults to false. + # + # ===== Examples + # ====== Add a backend specific option to the generated SQL (MySQL) + # create_join_table(:assemblies, :parts, :options => 'ENGINE=InnoDB DEFAULT CHARSET=utf8') + # generates: + # CREATE TABLE assemblies_parts ( + # assembly_id int NOT NULL, + # part_id int NOT NULL, + # ) ENGINE=InnoDB DEFAULT CHARSET=utf8 + def create_join_table(table_1, table_2, options = {}) + join_table_name = find_join_table_name(table_1, table_2, options) + + column_options = options.delete(:column_options) || {} + column_options.reverse_merge!({:null => false}) + + create_table(join_table_name, options.merge!(:id => false)) do |td| + td.integer :"#{table_1.to_s.singularize}_id", column_options + td.integer :"#{table_2.to_s.singularize}_id", column_options + end + end + # A block for changing columns in +table+. # # === Example 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 201c05d8f5..9d9dbcc355 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -505,7 +505,7 @@ module ActiveRecord 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 = $1.split(",").map { |key| key.gsub(/[`"]/, "") } keys.length == 1 ? [keys.first, nil] : nil else nil diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index 6086c32dbe..c1332fde1a 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -225,7 +225,7 @@ module ActiveRecord # column values as values. def select(sql, name = nil, binds = []) binds = binds.dup - exec_query(sql.gsub("\0") { quote(*binds.shift.reverse) }, name).to_a + exec_query(sql.gsub("\0") { quote(*binds.shift.reverse) }, name) end def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index e432c5af32..5905242747 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -408,7 +408,7 @@ module ActiveRecord def select(sql, name = nil, binds = []) @connection.query_with_result = true - rows = exec_query(sql, name, binds).to_a + rows = exec_query(sql, name, binds) @connection.more_results && @connection.next_result # invoking stored procedures with CLIENT_MULTI_RESULTS requires this to tidy up else connection will be dropped rows end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index dbfb375ba8..1d8e5d813a 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -1249,7 +1249,7 @@ module ActiveRecord # Executes a SELECT query and returns the results, performing any data type # conversions that are required to be performed here instead of in PostgreSQLColumn. def select(sql, name = nil, binds = []) - exec_query(sql, name, binds).to_a + exec_query(sql, name, binds) end def select_raw(sql, name = nil) diff --git a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb index 69750a911d..0520fc8b62 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb @@ -454,7 +454,7 @@ module ActiveRecord protected def select(sql, name = nil, binds = []) #:nodoc: - exec_query(sql, name, binds).to_a + exec_query(sql, name, binds) end def table_structure(table_name) diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index adba3f710c..a2ce620354 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -72,6 +72,16 @@ module ActiveRecord # The connection handler config_attribute :connection_handler self.connection_handler = ConnectionAdapters::ConnectionHandler.new + + ## + # :singleton-method: + # Specifies wether or not has_many or has_one association option + # :dependent => :restrict raises an exception. If set to true, the + # ActiveRecord::DeleteRestrictionError exception will be raised + # along with a DEPRECATION WARNING. If set to false, an error would + # be added to the model instead. + config_attribute :dependent_restrict_raises, :global => true + self.dependent_restrict_raises = true end module ClassMethods @@ -202,6 +212,7 @@ module ActiveRecord cloned_attributes.delete(self.class.primary_key) @attributes = cloned_attributes + @attributes[self.class.primary_key] = nil run_callbacks(:initialize) if _initialize_callbacks.any? @@ -326,6 +337,10 @@ module ActiveRecord end def init_internals + pk = self.class.primary_key + + @attributes[pk] = nil unless @attributes.key?(pk) + @relation = nil @aggregation_cache = {} @association_cache = {} diff --git a/activerecord/lib/active_record/dynamic_finder_match.rb b/activerecord/lib/active_record/dynamic_finder_match.rb index b309df9b1b..38dbbef5fc 100644 --- a/activerecord/lib/active_record/dynamic_finder_match.rb +++ b/activerecord/lib/active_record/dynamic_finder_match.rb @@ -6,33 +6,23 @@ module ActiveRecord # class DynamicFinderMatch def self.match(method) - finder = :first - bang = false - instantiator = nil - - case method.to_s - when /^find_(all_|last_)?by_([_a-zA-Z]\w*)$/ - finder = :last if $1 == 'last_' - finder = :all if $1 == 'all_' - names = $2 - when /^find_by_([_a-zA-Z]\w*)\!$/ - bang = true - names = $1 - when /^find_or_(initialize|create)_by_([_a-zA-Z]\w*)$/ - instantiator = $1 == 'initialize' ? :new : :create - names = $2 - else - return nil + method = method.to_s + klass = [FindBy, FindByBang, FindOrInitializeCreateBy].find do |_klass| + _klass.matches?(method) end + klass.new(method) if klass + end - new(finder, instantiator, bang, names.split('_and_')) + def self.matches?(method) + method =~ self::METHOD_PATTERN end - def initialize(finder, instantiator, bang, attribute_names) - @finder = finder - @instantiator = instantiator - @bang = bang - @attribute_names = attribute_names + 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 @@ -41,16 +31,54 @@ module ActiveRecord @finder && !@instantiator end + def creator? + @finder == :first && @instantiator == :create + end + def instantiator? - @finder == :first && @instantiator + @instantiator end - def creator? - @finder == :first && @instantiator == :create + def bang? + false + end + + def valid_arguments?(arguments) + arguments.size >= @attribute_names.size 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? - @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 end diff --git a/activerecord/lib/active_record/dynamic_matchers.rb b/activerecord/lib/active_record/dynamic_matchers.rb index e9068089f0..60ce3dd4f1 100644 --- a/activerecord/lib/active_record/dynamic_matchers.rb +++ b/activerecord/lib/active_record/dynamic_matchers.rb @@ -25,7 +25,7 @@ module ActiveRecord 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 + 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 diff --git a/activerecord/lib/active_record/dynamic_scope_match.rb b/activerecord/lib/active_record/dynamic_scope_match.rb index c832e927d6..a502155aac 100644 --- a/activerecord/lib/active_record/dynamic_scope_match.rb +++ b/activerecord/lib/active_record/dynamic_scope_match.rb @@ -19,5 +19,9 @@ module ActiveRecord 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/explain.rb b/activerecord/lib/active_record/explain.rb index e502d7e52b..01cacf6153 100644 --- a/activerecord/lib/active_record/explain.rb +++ b/activerecord/lib/active_record/explain.rb @@ -19,6 +19,8 @@ module ActiveRecord # currently collected. A false value indicates collecting is turned # off. Otherwise it is an array of queries. def logging_query_plan # :nodoc: + return yield unless logger + threshold = auto_explain_threshold_in_seconds current = Thread.current if threshold && current[:available_queries_for_explain].nil? diff --git a/activerecord/lib/active_record/locale/en.yml b/activerecord/lib/active_record/locale/en.yml index 44328f63b6..8892f7ef2f 100644 --- a/activerecord/lib/active_record/locale/en.yml +++ b/activerecord/lib/active_record/locale/en.yml @@ -10,6 +10,7 @@ en: messages: taken: "has already been taken" record_invalid: "Validation failed: %{errors}" + restrict_dependent_destroy: "Cannot delete record because dependent %{model} exists" # Append your own errors here or at the model/attributes scope. # You can define own errors for models or model attributes. diff --git a/activerecord/lib/active_record/migration/command_recorder.rb b/activerecord/lib/active_record/migration/command_recorder.rb index 4e27293cb4..96b62fdd61 100644 --- a/activerecord/lib/active_record/migration/command_recorder.rb +++ b/activerecord/lib/active_record/migration/command_recorder.rb @@ -8,11 +8,14 @@ module ActiveRecord # * add_index # * add_timestamps # * create_table + # * create_join_table # * remove_timestamps # * rename_column # * rename_index # * rename_table class CommandRecorder + include JoinTable + attr_accessor :commands, :delegate def initialize(delegate = nil) @@ -48,7 +51,7 @@ module ActiveRecord super || delegate.respond_to?(*args) end - [:create_table, :change_table, :rename_table, :add_column, :remove_column, :rename_index, :rename_column, :add_index, :remove_index, :add_timestamps, :remove_timestamps, :change_column, :change_column_default].each do |method| + [:create_table, :create_join_table, :change_table, :rename_table, :add_column, :remove_column, :rename_index, :rename_column, :add_index, :remove_index, :add_timestamps, :remove_timestamps, :change_column, :change_column_default].each do |method| class_eval <<-EOV, __FILE__, __LINE__ + 1 def #{method}(*args) # def create_table(*args) record(:"#{method}", args) # record(:create_table, args) @@ -62,6 +65,12 @@ module ActiveRecord [:drop_table, [args.first]] end + def invert_create_join_table(args) + table_name = find_join_table_name(*args) + + [:drop_table, [table_name]] + end + def invert_rename_table(args) [:rename_table, args.reverse] end @@ -99,7 +108,6 @@ module ActiveRecord rescue NoMethodError => e raise e, e.message.sub(/ for #<.*$/, " via proxy for #{@delegate}") end - end end end diff --git a/activerecord/lib/active_record/migration/join_table.rb b/activerecord/lib/active_record/migration/join_table.rb new file mode 100644 index 0000000000..01a580781b --- /dev/null +++ b/activerecord/lib/active_record/migration/join_table.rb @@ -0,0 +1,17 @@ +module ActiveRecord + class Migration + module JoinTable #:nodoc: + private + + def find_join_table_name(table_1, table_2, options = {}) + options.delete(:table_name) { join_table_name(table_1, table_2) } + end + + def join_table_name(table_1, table_2) + tables_names = [table_1, table_2].map(&:to_s).sort + + tables_names.join("_").to_sym + end + end + end +end diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb index 261f6fa974..61f82af0c3 100644 --- a/activerecord/lib/active_record/model_schema.rb +++ b/activerecord/lib/active_record/model_schema.rb @@ -115,7 +115,7 @@ module ActiveRecord # the documentation for ActiveRecord::Base#table_name. def table_name=(value) @original_table_name = @table_name if defined?(@table_name) - @table_name = value + @table_name = value && value.to_s @quoted_table_name = nil @arel_table = nil @relation = Relation.new(self, arel_table) diff --git a/activerecord/lib/active_record/null_relation.rb b/activerecord/lib/active_record/null_relation.rb new file mode 100644 index 0000000000..60c37ac2b7 --- /dev/null +++ b/activerecord/lib/active_record/null_relation.rb @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- + +module ActiveRecord + # = Active Record Null Relation + class NullRelation < Relation + def exec_queries + @records = [] + end + end +end
\ No newline at end of file diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb index 94e34e1bd4..5945b05190 100644 --- a/activerecord/lib/active_record/querying.rb +++ b/activerecord/lib/active_record/querying.rb @@ -8,7 +8,7 @@ module ActiveRecord 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, :references, :to => :scoped + :having, :create_with, :uniq, :references, :none, :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 @@ -35,7 +35,8 @@ module ActiveRecord # > [#<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) } + result_set = connection.select_all(sanitize_sql(sql), "#{name} Load", binds) + result_set.map { |record| instantiate(record) } end end diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index bf9b4bf1c9..6bf3050af9 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -333,7 +333,7 @@ module ActiveRecord def select_for_count if @select_values.present? select = @select_values.join(", ") - select if select !~ /(,|\*)/ + select if select !~ /[,*]/ end end diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index a8ae7208fc..6a28d7b155 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -196,6 +196,39 @@ module ActiveRecord relation end + # Returns a chainable relation with zero records, specifically an + # instance of the NullRelation class. + # + # The returned NullRelation inherits from Relation and implements the + # Null Object pattern so it is an object with defined null behavior: + # it always returns an empty array of records and avoids any database query. + # + # Any subsequent condition chained to the returned relation will continue + # generating an empty relation and will not fire any query to the database. + # + # Used in cases where is needed a method or a scope that could return zero + # results but the response has to be chainable. + # + # For example: + # + # @posts = current_user.visible_posts.where(:name => params[:name]) + # # => the visible_post method response has to be a chainable Relation + # + # def visible_posts + # case role + # if 'Country Manager' + # Post.where(:country => country) + # if 'Reviewer' + # Post.published + # if 'Bad User' + # Post.none # => returning [] instead breaks the previous code + # end + # end + # + def none + NullRelation.new(@klass, @table) + end + def readonly(value = true) relation = clone relation.readonly_value = value diff --git a/activerecord/lib/active_record/result.rb b/activerecord/lib/active_record/result.rb index 9ceab2eabc..60a2e90e23 100644 --- a/activerecord/lib/active_record/result.rb +++ b/activerecord/lib/active_record/result.rb @@ -24,6 +24,31 @@ module ActiveRecord hash_rows end + alias :map! :map + alias :collect! :map + + def empty? + rows.empty? + end + + def to_ary + hash_rows + end + + def [](idx) + hash_rows[idx] + end + + def last + hash_rows.last + end + + def initialize_copy(other) + @columns = columns.dup + @rows = rows.dup + @hash_rows = nil + end + private def hash_rows @hash_rows ||= @rows.map { |row| diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index 3a741ba600..7fd5d76ba5 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -57,8 +57,7 @@ module ActiveRecord def build_relation(klass, table, attribute, value) #:nodoc: reflection = klass.reflect_on_association(attribute) - column = nil - if(reflection) + if reflection column = klass.columns_hash[reflection.foreign_key] attribute = reflection.foreign_key value = value.attributes[reflection.primary_key_column.name] |