diff options
Diffstat (limited to 'activerecord/lib/active_record')
17 files changed, 188 insertions, 103 deletions
diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 258d602afa..69b95f814c 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -190,10 +190,10 @@ module ActiveRecord # * <tt>Project#portfolio, Project#portfolio=(portfolio), Project#portfolio.nil?</tt> # * <tt>Project#project_manager, Project#project_manager=(project_manager), Project#project_manager.nil?,</tt> # * <tt>Project#milestones.empty?, Project#milestones.size, Project#milestones, Project#milestones<<(milestone),</tt> - # <tt>Project#milestones.delete(milestone), Project#milestones.find(milestone_id), Project#milestones.all(options),</tt> - # <tt>Project#milestones.build, Project#milestones.create</tt> + # <tt>Project#milestones.delete(milestone), Project#milestones.destroy(mileston), Project#milestones.find(milestone_id),</tt> + # <tt>Project#milestones.all(options), Project#milestones.build, Project#milestones.create</tt> # * <tt>Project#categories.empty?, Project#categories.size, Project#categories, Project#categories<<(category1),</tt> - # <tt>Project#categories.delete(category1)</tt> + # <tt>Project#categories.delete(category1), Project#categories.destroy(category1)</tt> # # === A word of warning # @@ -236,6 +236,7 @@ module ActiveRecord # others.clear | X | X | X # others.delete(other,other,...) | X | X | X # others.delete_all | X | X | X + # others.destroy(other,other,...) | X | X | X # others.destroy_all | X | X | X # others.find(*args) | X | X | X # others.exists? | X | X | X @@ -1031,6 +1032,12 @@ module ActiveRecord # If the <tt>:through</tt> option is used, then the join records are deleted (rather than # nullified) by default, but you can specify <tt>:dependent => :destroy</tt> or # <tt>:dependent => :nullify</tt> to override this. + # [collection.destroy(object, ...)] + # Removes one or more objects from the collection by running <tt>destroy</tt> on + # each record, regardless of any dependent option, ensuring callbacks are run. + # + # If the <tt>:through</tt> option is used, then the join records are destroyed + # instead, not the objects themselves. # [collection=objects] # Replaces the collections content by deleting and adding objects as appropriate. If the <tt>:through</tt> # option is true callbacks in the join models are triggered except destroy callbacks, since deletion is @@ -1074,6 +1081,7 @@ module ActiveRecord # * <tt>Firm#clients</tt> (similar to <tt>Clients.all :conditions => ["firm_id = ?", id]</tt>) # * <tt>Firm#clients<<</tt> # * <tt>Firm#clients.delete</tt> + # * <tt>Firm#clients.destroy</tt> # * <tt>Firm#clients=</tt> # * <tt>Firm#client_ids</tt> # * <tt>Firm#client_ids=</tt> @@ -1425,6 +1433,9 @@ module ActiveRecord # [collection.delete(object, ...)] # Removes one or more objects from the collection by removing their associations from the join table. # This does not destroy the objects. + # [collection.destroy(object, ...)] + # Removes one or more objects from the collection by running destroy on each association in the join table, overriding any dependent option. + # This does not destroy the objects. # [collection=objects] # Replaces the collection's content by deleting and adding objects as appropriate. # [collection_singular_ids] @@ -1461,6 +1472,7 @@ module ActiveRecord # * <tt>Developer#projects</tt> # * <tt>Developer#projects<<</tt> # * <tt>Developer#projects.delete</tt> + # * <tt>Developer#projects.destroy</tt> # * <tt>Developer#projects=</tt> # * <tt>Developer#project_ids</tt> # * <tt>Developer#project_ids=</tt> diff --git a/activerecord/lib/active_record/associations/preloader.rb b/activerecord/lib/active_record/associations/preloader.rb index ce5bf15f10..c1cd3a4ae3 100644 --- a/activerecord/lib/active_record/associations/preloader.rb +++ b/activerecord/lib/active_record/associations/preloader.rb @@ -30,17 +30,21 @@ module ActiveRecord # option references an association's column), it will fallback to the table # join strategy. class Preloader #:nodoc: - autoload :Association, 'active_record/associations/preloader/association' - autoload :SingularAssociation, 'active_record/associations/preloader/singular_association' - autoload :CollectionAssociation, 'active_record/associations/preloader/collection_association' - autoload :ThroughAssociation, 'active_record/associations/preloader/through_association' + extend ActiveSupport::Autoload - autoload :HasMany, 'active_record/associations/preloader/has_many' - autoload :HasManyThrough, 'active_record/associations/preloader/has_many_through' - autoload :HasOne, 'active_record/associations/preloader/has_one' - autoload :HasOneThrough, 'active_record/associations/preloader/has_one_through' - autoload :HasAndBelongsToMany, 'active_record/associations/preloader/has_and_belongs_to_many' - autoload :BelongsTo, 'active_record/associations/preloader/belongs_to' + eager_autoload do + autoload :Association, 'active_record/associations/preloader/association' + autoload :SingularAssociation, 'active_record/associations/preloader/singular_association' + autoload :CollectionAssociation, 'active_record/associations/preloader/collection_association' + autoload :ThroughAssociation, 'active_record/associations/preloader/through_association' + + autoload :HasMany, 'active_record/associations/preloader/has_many' + autoload :HasManyThrough, 'active_record/associations/preloader/has_many_through' + autoload :HasOne, 'active_record/associations/preloader/has_one' + autoload :HasOneThrough, 'active_record/associations/preloader/has_one_through' + autoload :HasAndBelongsToMany, 'active_record/associations/preloader/has_and_belongs_to_many' + autoload :BelongsTo, 'active_record/associations/preloader/belongs_to' + end attr_reader :records, :associations, :preload_scope, :model diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb index 6213b5dcd5..46fd6ebfb3 100644 --- a/activerecord/lib/active_record/attribute_methods/read.rb +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -35,21 +35,36 @@ module ActiveRecord 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). + # 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) + # 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. + # + # We are also defining a constant to hold the frozen string of + # the attribute name. Using a constant means that we do not have + # to allocate an object on each call to the attribute method. + # Making it frozen means that it doesn't get duped when used to + # key the @attributes_cache in read_attribute. + def define_method_attribute(name) + safe_name = name.unpack('h*').first generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1 - def __temp__ - read_attribute(:'#{attr_name}') { |n| missing_attribute(n, caller) } + module AttrNames + unless defined? ATTR_#{safe_name} + ATTR_#{safe_name} = #{name.inspect}.freeze + end + end + + def __temp__#{safe_name} + read_attribute(AttrNames::ATTR_#{safe_name}) { |n| missing_attribute(n, caller) } end - alias_method '#{attr_name}', :__temp__ - undef_method :__temp__ + + alias_method #{name.inspect}, :__temp__#{safe_name} + undef_method :__temp__#{safe_name} STR end @@ -70,17 +85,13 @@ module ActiveRecord # 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) - return unless attr_name - name_sym = attr_name.to_sym - # If it's cached, just return it # We use #[] first as a perf optimization for non-nil values. See https://gist.github.com/3552829. - @attributes_cache[name_sym] || @attributes_cache.fetch(name_sym) { - name = attr_name.to_s - + name = attr_name.to_s + @attributes_cache[name] || @attributes_cache.fetch(name) { column = @columns_hash.fetch(name) { return @attributes.fetch(name) { - if name_sym == :id && self.class.primary_key != name + if name == 'id' && self.class.primary_key != name read_attribute(self.class.primary_key) end } @@ -91,7 +102,7 @@ module ActiveRecord } if self.class.cache_attribute?(name) - @attributes_cache[name_sym] = column.type_cast(value) + @attributes_cache[name] = column.type_cast(value) else column.type_cast value 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 b9a69cdb0a..f36a5806a9 100644 --- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb +++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb @@ -50,7 +50,7 @@ module ActiveRecord if (rounded_value != rounded_time) || (!rounded_value && original_time) write_attribute("#{attr_name}", original_time) #{attr_name}_will_change! - @attributes_cache[:"#{attr_name}"] = zoned_time + @attributes_cache["#{attr_name}"] = zoned_time end end EOV diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb index 6eb9e25fd9..cd33494cc3 100644 --- a/activerecord/lib/active_record/attribute_methods/write.rb +++ b/activerecord/lib/active_record/attribute_methods/write.rb @@ -9,15 +9,19 @@ module ActiveRecord module ClassMethods protected - def define_method_attribute=(attr_name) - 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 + + # See define_method_attribute in read.rb for an explanation of + # this code. + def define_method_attribute=(name) + safe_name = name.unpack('h*').first + generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1 + def __temp__#{safe_name}=(value) + write_attribute(AttrNames::ATTR_#{safe_name}, value) end - end + alias_method #{(name + '=').inspect}, :__temp__#{safe_name}= + undef_method :__temp__#{safe_name}= + STR + end end # Updates the attribute identified by <tt>attr_name</tt> with the @@ -26,13 +30,13 @@ module ActiveRecord def write_attribute(attr_name, value) attr_name = attr_name.to_s attr_name = self.class.primary_key if attr_name == 'id' && self.class.primary_key - @attributes_cache.delete(attr_name.to_sym) + @attributes_cache.delete(attr_name) column = column_for_attribute(attr_name) # If we're dealing with a binary column, write the data to the cache # so we don't attempt to typecast multiple times. if column && column.binary? - @attributes_cache[attr_name.to_sym] = value + @attributes_cache[attr_name] = value end if column || @attributes.has_key?(attr_name) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb index 37d43d891d..9d3fa18e3a 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb @@ -90,7 +90,7 @@ module ActiveRecord else super(value, column) end when IPAddr - return super(value, column) unless ['inet','cidr'].includes? column.sql_type + return super(value, column) unless ['inet','cidr'].include? column.sql_type PostgreSQLColumn.cidr_to_string(value) else super(value, column) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb index 2264595751..7cad8f94cf 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -280,16 +280,13 @@ module ActiveRecord end_sql if result.nil? or result.empty? - # If that fails, try parsing the primary key's default value. - # Support the 7.x and 8.0 nextval('foo'::text) as well as - # the 8.1+ nextval('foo'::regclass). result = query(<<-end_sql, 'SCHEMA')[0] SELECT attr.attname, CASE - WHEN split_part(def.adsrc, '''', 2) ~ '.' THEN - substr(split_part(def.adsrc, '''', 2), - strpos(split_part(def.adsrc, '''', 2), '.')+1) - ELSE split_part(def.adsrc, '''', 2) + WHEN split_part(pg_get_expr(def.adbin, def.adrelid), '''', 2) ~ '.' THEN + substr(split_part(pg_get_expr(def.adbin, def.adrelid), '''', 2), + strpos(split_part(pg_get_expr(def.adbin, def.adrelid), '''', 2), '.')+1) + ELSE split_part(pg_get_expr(def.adbin, def.adrelid), '''', 2) END FROM pg_class t JOIN pg_attribute attr ON (t.oid = attrelid) @@ -297,7 +294,7 @@ module ActiveRecord JOIN pg_constraint cons ON (conrelid = adrelid AND adnum = conkey[1]) WHERE t.oid = '#{quote_table_name(table)}'::regclass AND cons.contype = 'p' - AND def.adsrc ~* 'nextval' + AND pg_get_expr(def.adbin, def.adrelid) ~* 'nextval' end_sql end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 5e35f472c7..e18464fa35 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -78,11 +78,8 @@ module ActiveRecord when /\A\(?(-?\d+(\.\d*)?\)?)\z/ $1 # Character types - when /\A'(.*)'::(?:character varying|bpchar|text)\z/m + when /\A\(?'(.*)'::.*\b(?:character varying|bpchar|text)\z/m $1 - # Character types (8.1 formatting) - when /\AE'(.*)'::(?:character varying|bpchar|text)\z/m - $1.gsub(/\\(\d\d\d)/) { $1.oct.chr } # Binary data types when /\A'(.*)'::bytea\z/m $1 @@ -241,7 +238,7 @@ module ActiveRecord # <encoding></tt> call on the connection. # * <tt>:min_messages</tt> - An optional client min messages that is used in a # <tt>SET client_min_messages TO <min_messages></tt> call on the connection. - # * <tt>:insert_returning</tt> - An optional boolean to control the use or <tt>RETURNING</tt> for <tt>INSERT<tt> statements + # * <tt>:insert_returning</tt> - An optional boolean to control the use or <tt>RETURNING</tt> for <tt>INSERT</tt> statements # defaults to true. # # Any further options are used as connection parameters to libpq. See @@ -763,7 +760,8 @@ module ActiveRecord # - ::regclass is a function that gives the id for a table name def column_definitions(table_name) #:nodoc: exec_query(<<-end_sql, 'SCHEMA').rows - SELECT a.attname, format_type(a.atttypid, a.atttypmod), d.adsrc, a.attnotnull, a.atttypid, a.atttypmod + SELECT a.attname, format_type(a.atttypid, a.atttypmod), + pg_get_expr(d.adbin, d.adrelid), a.attnotnull, a.atttypid, a.atttypmod FROM pg_attribute a LEFT JOIN pg_attrdef d ON a.attrelid = d.adrelid AND a.attnum = d.adnum WHERE a.attrelid = '#{quote_table_name(table_name)}'::regclass diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index 899e89a6f5..413bd147de 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -657,7 +657,7 @@ module ActiveRecord #-- # Deprecate 'Fixtures' in favor of 'FixtureSet'. #++ - Fixtures = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('ActiveRecord::Fixtures', '::ActiveRecord::FixtureSet') + Fixtures = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('ActiveRecord::Fixtures', 'ActiveRecord::FixtureSet') class Fixture #:nodoc: include Enumerable diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index c1d57855a9..d5ee98382d 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -50,7 +50,7 @@ module ActiveRecord # # class AddSsl < ActiveRecord::Migration # def up - # add_column :accounts, :ssl_enabled, :boolean, :default => true + # add_column :accounts, :ssl_enabled, :boolean, default: true # end # # def down @@ -62,7 +62,7 @@ module ActiveRecord # if you're backing out of the migration. It shows how all migrations have # two methods +up+ and +down+ that describes the transformations # required to implement or remove the migration. These methods can consist - # of both the migration specific methods like add_column and remove_column, + # of both the migration specific methods like +add_column+ and +remove_column+, # but may also contain regular Ruby code for generating data needed for the # transformations. # @@ -78,9 +78,9 @@ module ActiveRecord # t.integer :position # end # - # SystemSetting.create :name => "notice", - # :label => "Use notice?", - # :value => 1 + # SystemSetting.create name: 'notice', + # label: 'Use notice?', + # value: 1 # end # # def down @@ -88,19 +88,22 @@ module ActiveRecord # end # end # - # This migration first adds the system_settings table, then creates the very + # This migration first adds the +system_settings+ table, then creates the very # first row in it using the Active Record model that relies on the table. It - # also uses the more advanced create_table syntax where you can specify a + # also uses the more advanced +create_table+ syntax where you can specify a # complete table schema in one block call. # # == Available transformations # - # * <tt>create_table(name, options)</tt> Creates a table called +name+ and + # * <tt>create_table(name, options)</tt>: Creates a table called +name+ and # makes the table object available to a block that can then add columns to it, - # following the same format as add_column. See example above. The options hash + # following the same format as +add_column+. See example above. The options hash # is for fragments like "DEFAULT CHARSET=UTF-8" that are appended to the create # table definition. # * <tt>drop_table(name)</tt>: Drops the table called +name+. + # * <tt>change_table(name, options)</tt>: Allows to make column alterations to + # the table called +name+. It makes the table object availabe to a block that + # can then add/remove columns, indexes or foreign keys to it. # * <tt>rename_table(old_name, new_name)</tt>: Renames the table called +old_name+ # to +new_name+. # * <tt>add_column(table_name, column_name, type, options)</tt>: Adds a new column @@ -109,9 +112,9 @@ module ActiveRecord # <tt>:string</tt>, <tt>:text</tt>, <tt>:integer</tt>, <tt>:float</tt>, # <tt>:decimal</tt>, <tt>:datetime</tt>, <tt>:timestamp</tt>, <tt>:time</tt>, # <tt>:date</tt>, <tt>:binary</tt>, <tt>:boolean</tt>. A default value can be - # specified by passing an +options+ hash like <tt>{ :default => 11 }</tt>. + # specified by passing an +options+ hash like <tt>{ default: 11 }</tt>. # Other options include <tt>:limit</tt> and <tt>:null</tt> (e.g. - # <tt>{ :limit => 50, :null => false }</tt>) -- see + # <tt>{ limit: 50, null: false }</tt>) -- see # ActiveRecord::ConnectionAdapters::TableDefinition#column for details. # * <tt>rename_column(table_name, column_name, new_column_name)</tt>: Renames # a column but keeps the type and content. @@ -122,11 +125,11 @@ module ActiveRecord # * <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>, <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 + # <tt>{ name: 'users_name_index', unique: true }</tt>) and <tt>:order</tt> + # (e.g. <tt>{ 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 + # * <tt>remove_index(table_name, name: index_name)</tt>: Removes the index # specified by +index_name+. # # == Irreversible transformations diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index 0107667fbe..611d3d97c3 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -197,7 +197,7 @@ module ActiveRecord end end - # Updates a single attribute of an object, without having to call save on that object. + # Updates a single attribute of an object, without having to explicitly call save on that object. # # * Validation is skipped. # * Callbacks are skipped. @@ -209,7 +209,7 @@ module ActiveRecord update_columns(name => value) end - # Updates the attributes from the passed-in hash, without having to call save on that object. + # Updates the attributes from the passed-in hash, without having to explicitly call save on that object. # # * Validation is skipped. # * Callbacks are skipped. diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index ed80422336..ecce7c703b 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -91,8 +91,10 @@ module ActiveRecord end def initialize_copy(other) - @values = @values.dup - @values[:bind] = @values[:bind].dup if @values[:bind] + # This method is a hot spot, so for now, use Hash[] to dup the hash. + # https://bugs.ruby-lang.org/issues/7166 + @values = Hash[@values] + @values[:bind] = @values[:bind].dup if @values.key? :bind reset end @@ -540,7 +542,7 @@ module ActiveRecord end def values - @values.dup + Hash[@values] end def inspect diff --git a/activerecord/lib/active_record/relation/batches.rb b/activerecord/lib/active_record/relation/batches.rb index 28aab6d92b..8af0c6a8ef 100644 --- a/activerecord/lib/active_record/relation/batches.rb +++ b/activerecord/lib/active_record/relation/batches.rb @@ -40,7 +40,7 @@ module ActiveRecord # # It's not possible to set the order. That is automatically set to # ascending on the primary key ("id ASC") to make the batch ordering - # work. This also mean that this method only works with integer-based + # work. This also means that this method only works with integer-based # primary keys. You can't set the limit either, that's used to control # the batch sizes. # diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index 7c43d844d0..6317004631 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -349,7 +349,7 @@ module ActiveRecord def type_cast_calculated_value(value, column, operation = nil) case operation when 'count' then value.to_i - when 'sum' then type_cast_using_column(value || '0', column) + when 'sum' then type_cast_using_column(value || 0, column) when 'average' then value.respond_to?(:to_d) ? value.to_d : value else type_cast_using_column(value, column) end diff --git a/activerecord/lib/active_record/relation/merger.rb b/activerecord/lib/active_record/relation/merger.rb index e5b50673da..59226d316e 100644 --- a/activerecord/lib/active_record/relation/merger.rb +++ b/activerecord/lib/active_record/relation/merger.rb @@ -22,7 +22,17 @@ module ActiveRecord # the values. def other other = Relation.new(relation.klass, relation.table) - hash.each { |k, v| other.send("#{k}!", v) } + hash.each { |k, v| + if k == :joins + if Hash === v + other.joins!(v) + else + other.joins!(*v) + end + else + other.send("#{k}!", v) + end + } other end end @@ -39,16 +49,18 @@ module ActiveRecord @values = other.values end + NORMAL_VALUES = Relation::SINGLE_VALUE_METHODS + + Relation::MULTI_VALUE_METHODS - + [:where, :order, :bind, :reverse_order, :lock, :create_with, :reordering, :from] # :nodoc: + def normal_values - Relation::SINGLE_VALUE_METHODS + - Relation::MULTI_VALUE_METHODS - - [:where, :order, :bind, :reverse_order, :lock, :create_with, :reordering, :from] + NORMAL_VALUES end def merge normal_values.each do |name| value = values[name] - relation.send("#{name}!", value) unless value.blank? + relation.send("#{name}!", *value) unless value.blank? end merge_multi_values diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 3c59bd8a68..14bcb337e9 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -202,6 +202,15 @@ module ActiveRecord # # User.order('name DESC, email') # => SELECT "users".* FROM "users" ORDER BY name DESC, email + # + # User.order(:name) + # => SELECT "users".* FROM "users" ORDER BY "users"."name" ASC + # + # User.order(email: :desc) + # => SELECT "users".* FROM "users" ORDER BY "users"."email" DESC + # + # User.order(:name, email: :desc) + # => SELECT "users".* FROM "users" ORDER BY "users"."name" ASC, "users"."email" DESC def order(*args) args.blank? ? self : spawn.order!(*args) end @@ -210,6 +219,8 @@ module ActiveRecord def order!(*args) args.flatten! + validate_order_args args + references = args.reject { |arg| Arel::Node === arg } references.map! { |arg| arg =~ /^([a-zA-Z]\w*)\.(\w+)/ && $1 }.compact! references!(references) if references.any? @@ -235,6 +246,8 @@ module ActiveRecord def reorder!(*args) args.flatten! + validate_order_args args + self.reordering_value = true self.order_values = args self @@ -245,13 +258,11 @@ module ActiveRecord # User.joins(:posts) # => SELECT "users".* FROM "users" INNER JOIN "posts" ON "posts"."user_id" = "users"."id" def joins(*args) - args.compact.blank? ? self : spawn.joins!(*args) + args.compact.blank? ? self : spawn.joins!(*args.flatten) end # Like #joins, but modifies relation in place. def joins!(*args) - args.flatten! - self.joins_values += args self end @@ -658,9 +669,7 @@ module ActiveRecord arel.group(*group_values.uniq.reject{|g| g.blank?}) unless group_values.empty? - order = order_values - order = reverse_sql_order(order) if reverse_order_value - arel.order(*order.uniq.reject{|o| o.blank?}) unless order.empty? + build_order(arel) build_select(arel, select_values.uniq) @@ -729,22 +738,22 @@ module ActiveRecord buckets = joins.group_by do |join| case join when String - 'string_join' + :string_join when Hash, Symbol, Array - 'association_join' + :association_join when ActiveRecord::Associations::JoinDependency::JoinAssociation - 'stashed_join' + :stashed_join when Arel::Nodes::Join - 'join_node' + :join_node else raise 'unknown class: %s' % join.class.name end end - association_joins = buckets['association_join'] || [] - stashed_association_joins = buckets['stashed_join'] || [] - join_nodes = (buckets['join_node'] || []).uniq - string_joins = (buckets['string_join'] || []).map { |x| + association_joins = buckets[:association_join] || [] + stashed_association_joins = buckets[:stashed_join] || [] + join_nodes = (buckets[:join_node] || []).uniq + string_joins = (buckets[:string_join] || []).map { |x| x.strip }.uniq @@ -786,11 +795,17 @@ module ActiveRecord case o when Arel::Nodes::Ordering o.reverse - when String, Symbol + when String o.to_s.split(',').collect do |s| s.strip! s.gsub!(/\sasc\Z/i, ' DESC') || s.gsub!(/\sdesc\Z/i, ' ASC') || s.concat(' DESC') end + when Symbol + { o => :desc } + when Hash + o.each_with_object({}) do |(field, dir), memo| + memo[field] = (dir == :asc ? :desc : :asc ) + end else o end @@ -801,5 +816,31 @@ module ActiveRecord o.is_a?(Array) && o.all?{|obj| obj.is_a?(String)} end + def build_order(arel) + orders = order_values + orders = reverse_sql_order(orders) if reverse_order_value + + orders = orders.uniq.reject(&:blank?).map do |order| + case order + when Symbol + table[order].asc + when Hash + order.map { |field, dir| table[field].send(dir) } + else + order + end + end.flatten + + arel.order(*orders) unless orders.empty? + end + + def validate_order_args(args) + args.select { |a| Hash === a }.each do |h| + unless (h.values - [:asc, :desc]).empty? + raise ArgumentError, 'Direction should be :asc or :desc' + end + end + end + end end diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index 5dece1cb36..5fa6a0b892 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -26,11 +26,12 @@ module ActiveRecord relation = relation.and(table[finder_class.primary_key.to_sym].not_eq(record.send(:id))) if record.persisted? Array(options[:scope]).each do |scope_item| - scope_value = record.read_attribute(scope_item) reflection = record.class.reflect_on_association(scope_item) if reflection scope_value = record.send(reflection.foreign_key) scope_item = reflection.foreign_key + else + scope_value = record.read_attribute(scope_item) end relation = relation.and(table[scope_item].eq(scope_value)) end |