diff options
Diffstat (limited to 'activerecord')
35 files changed, 299 insertions, 123 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 6e0f1f31dd..5f5c9e4915 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,5 +1,24 @@ ## Rails 4.0.0 (unreleased) ## +* Fix bug where sum(expression) returns string '0' for no matching records + Fixes #7439 + + *Tim Macfarlane* + +* PostgreSQL adapter correctly fetches default values when using multiple schemas and domains in a db. Fixes #7914 + + *Arturo Pie* + +* Learn ActiveRecord::QueryMethods#order work with hash arguments + + When symbol or hash passed we convert it to Arel::Nodes::Ordering. + If we pass invalid direction(like name: :DeSc) ActiveRecord::QueryMethods#order will raise an exception + + User.order(:name, email: :desc) + # SELECT "users".* FROM "users" ORDER BY "users"."name" ASC, "users"."email" DESC + + *Tima Maslyuchenko* + * Rename `ActiveRecord::Fixtures` class to `ActiveRecord::FixtureSet`. Instances of this class normally hold a collection of fixtures (records) loaded either from a single YAML file, or from a file and a folder 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 diff --git a/activerecord/test/cases/adapters/postgresql/quoting_test.rb b/activerecord/test/cases/adapters/postgresql/quoting_test.rb index f8a605b67c..685f0ea74f 100644 --- a/activerecord/test/cases/adapters/postgresql/quoting_test.rb +++ b/activerecord/test/cases/adapters/postgresql/quoting_test.rb @@ -1,4 +1,5 @@ require "cases/helper" +require 'ipaddr' module ActiveRecord module ConnectionAdapters @@ -20,6 +21,18 @@ module ActiveRecord assert_equal 'f', @conn.type_cast(false, c) end + def test_type_cast_cidr + ip = IPAddr.new('255.0.0.0/8') + c = Column.new(nil, ip, 'cidr') + assert_equal ip, @conn.type_cast(ip, c) + end + + def test_type_cast_inet + ip = IPAddr.new('255.1.0.0/8') + c = Column.new(nil, ip, 'inet') + assert_equal ip, @conn.type_cast(ip, c) + end + def test_quote_float_nan nan = 0.0/0 c = Column.new(nil, 1, 'float') diff --git a/activerecord/test/cases/adapters/postgresql/schema_test.rb b/activerecord/test/cases/adapters/postgresql/schema_test.rb index 9208f53997..cd31900d4e 100644 --- a/activerecord/test/cases/adapters/postgresql/schema_test.rb +++ b/activerecord/test/cases/adapters/postgresql/schema_test.rb @@ -72,7 +72,7 @@ class SchemaTest < ActiveRecord::TestCase end def test_schema_names - assert_equal ["public", "test_schema", "test_schema2"], @connection.schema_names + assert_equal ["public", "schema_1", "test_schema", "test_schema2"], @connection.schema_names end def test_create_schema @@ -97,7 +97,7 @@ class SchemaTest < ActiveRecord::TestCase def test_drop_schema begin - @connection.create_schema "test_schema3" + @connection.create_schema "test_schema3" ensure @connection.drop_schema "test_schema3" end diff --git a/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb b/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb index 7eef4ace81..74288a98d1 100644 --- a/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb @@ -59,7 +59,7 @@ class CopyTableTest < ActiveRecord::TestCase def test_copy_table_with_unconventional_primary_key test_copy_table('owners', 'owners_unconventional') do |from, to, options| - original_pk = @connection.primary_key('owners') + original_pk = @connection.primary_key('owners') copied_pk = @connection.primary_key('owners_unconventional') assert_equal original_pk, copied_pk end diff --git a/activerecord/test/cases/associations/has_many_through_associations_test.rb b/activerecord/test/cases/associations/has_many_through_associations_test.rb index f0582a3090..b2a5d9d6f7 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -19,7 +19,6 @@ require 'models/book' require 'models/subscription' require 'models/essay' require 'models/category' -require 'models/owner' require 'models/categorization' require 'models/member' require 'models/membership' diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb index d08b157011..c2b58fd7d1 100644 --- a/activerecord/test/cases/attribute_methods_test.rb +++ b/activerecord/test/cases/attribute_methods_test.rb @@ -542,10 +542,10 @@ class AttributeMethodsTest < ActiveRecord::TestCase val = t.send attr_name unless attr_name == "type" if attribute_gets_cached assert cached_columns.include?(attr_name) - assert_equal val, cache[attr_name.to_sym] + assert_equal val, cache[attr_name] else assert uncached_columns.include?(attr_name) - assert !cache.include?(attr_name.to_sym) + assert !cache.include?(attr_name) end end end diff --git a/activerecord/test/cases/autosave_association_test.rb b/activerecord/test/cases/autosave_association_test.rb index fd4f09ab36..4afd33f273 100644 --- a/activerecord/test/cases/autosave_association_test.rb +++ b/activerecord/test/cases/autosave_association_test.rb @@ -16,7 +16,6 @@ require 'models/ship_part' require 'models/tag' require 'models/tagging' require 'models/treasure' -require 'models/company' require 'models/eye' class TestAutosaveAssociationsInGeneral < ActiveRecord::TestCase diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb index 6cb6c469d2..de4902f89a 100644 --- a/activerecord/test/cases/calculations_test.rb +++ b/activerecord/test/cases/calculations_test.rb @@ -378,6 +378,10 @@ class CalculationsTest < ActiveRecord::TestCase end end + def test_sum_expression_returns_zero_when_no_records_to_sum + assert_equal 0, Account.where('1 = 2').sum("2 * credit_limit") + end + def test_count_with_from_option assert_equal Company.count(:all), Company.from('companies').count(:all) assert_equal Account.where("credit_limit = 50").count(:all), diff --git a/activerecord/test/cases/defaults_test.rb b/activerecord/test/cases/defaults_test.rb index deaf5252db..72f1c99ca0 100644 --- a/activerecord/test/cases/defaults_test.rb +++ b/activerecord/test/cases/defaults_test.rb @@ -109,3 +109,43 @@ if current_adapter?(:MysqlAdapter) or current_adapter?(:Mysql2Adapter) end end end + +if current_adapter?(:PostgreSQLAdapter) + class DefaultsUsingMultipleSchemasAndDomainTest < ActiveSupport::TestCase + def setup + @connection = ActiveRecord::Base.connection + + @old_search_path = @connection.schema_search_path + @connection.schema_search_path = "schema_1, pg_catalog" + @connection.create_table "defaults" do |t| + t.text "text_col", :default => "some value" + t.string "string_col", :default => "some value" + end + Default.reset_column_information + end + + def test_text_defaults_in_new_schema_when_overriding_domain + assert_equal "some value", Default.new.text_col, "Default of text column was not correctly parse" + end + + def test_string_defaults_in_new_schema_when_overriding_domain + assert_equal "some value", Default.new.string_col, "Default of string column was not correctly parse" + end + + def test_bpchar_defaults_in_new_schema_when_overriding_domain + @connection.execute "ALTER TABLE defaults ADD bpchar_col bpchar DEFAULT 'some value'" + Default.reset_column_information + assert_equal "some value", Default.new.bpchar_col, "Default of bpchar column was not correctly parse" + end + + def test_text_defaults_after_updating_column_default + @connection.execute "ALTER TABLE defaults ALTER COLUMN text_col SET DEFAULT 'some text'::schema_1.text" + assert_equal "some text", Default.new.text_col, "Default of text column was not correctly parse after updating default using '::text' since postgreSQL will add parens to the default in db" + end + + def teardown + @connection.schema_search_path = @old_search_path + Default.reset_column_information + end + end +end diff --git a/activerecord/test/cases/migration/change_schema_test.rb b/activerecord/test/cases/migration/change_schema_test.rb index ec4c554abb..17c1634444 100644 --- a/activerecord/test/cases/migration/change_schema_test.rb +++ b/activerecord/test/cases/migration/change_schema_test.rb @@ -293,7 +293,7 @@ module ActiveRecord connection.create_table :testings do |t| t.column :foo, :string, limit: 100 t.column :bar, :decimal, precision: 8, scale: 2 - t.column :taggable_id, :integer, null: false + t.column :taggable_id, :integer, null: false t.column :taggable_type, :string, default: 'Photo' end diff --git a/activerecord/test/cases/nested_attributes_test.rb b/activerecord/test/cases/nested_attributes_test.rb index 9120083eca..fe9eddbdec 100644 --- a/activerecord/test/cases/nested_attributes_test.rb +++ b/activerecord/test/cases/nested_attributes_test.rb @@ -156,7 +156,7 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase end def test_reject_if_with_blank_nested_attributes_id - # When using a select list to choose an existing 'ship' id, with :include_blank => true + # When using a select list to choose an existing 'ship' id, with include_blank: true Pirate.accepts_nested_attributes_for :ship, :reject_if => proc {|attributes| attributes[:id].blank? } pirate = Pirate.new(:catchphrase => "Stop wastin' me time") diff --git a/activerecord/test/cases/query_cache_test.rb b/activerecord/test/cases/query_cache_test.rb index 2d778e9e90..51a285a2b4 100644 --- a/activerecord/test/cases/query_cache_test.rb +++ b/activerecord/test/cases/query_cache_test.rb @@ -48,7 +48,7 @@ class QueryCacheTest < ActiveRecord::TestCase } assert_raises(RuntimeError) { mw.call({}) } - assert_equal connection_id, ActiveRecord::Base.connection_id + assert_equal connection_id, ActiveRecord::Base.connection_id end def test_middleware_delegates diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb index 588da68ec1..a9d46f4fba 100644 --- a/activerecord/test/cases/reflection_test.rb +++ b/activerecord/test/cases/reflection_test.rb @@ -3,7 +3,6 @@ require 'models/topic' require 'models/customer' require 'models/company' require 'models/company_in_module' -require 'models/subscriber' require 'models/ship' require 'models/pirate' require 'models/price_estimate' diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index b91423351e..fdbc0a3fdb 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -5,7 +5,6 @@ require 'models/post' require 'models/topic' require 'models/comment' require 'models/author' -require 'models/comment' require 'models/entrant' require 'models/developer' require 'models/reply' @@ -158,6 +157,22 @@ class RelationTest < ActiveRecord::TestCase assert_equal 4, topics.to_a.size assert_equal topics(:first).title, topics.first.title end + + def test_finding_with_assoc_order + topics = Topic.order(:id => :desc) + assert_equal 4, topics.to_a.size + assert_equal topics(:fourth).title, topics.first.title + end + + def test_finding_with_reverted_assoc_order + topics = Topic.order(:id => :asc).reverse_order + assert_equal 4, topics.to_a.size + assert_equal topics(:fourth).title, topics.first.title + end + + def test_raising_exception_on_invalid_hash_params + assert_raise(ArgumentError) { Topic.order(:name, "id DESC", :id => :DeSc) } + end def test_finding_last_with_arel_order topics = Topic.order(Topic.arel_table[:id].asc) diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb index 80f46c6b08..5f13124e5b 100644 --- a/activerecord/test/cases/schema_dumper_test.rb +++ b/activerecord/test/cases/schema_dumper_test.rb @@ -2,13 +2,9 @@ require "cases/helper" class SchemaDumperTest < ActiveRecord::TestCase - def initialize(*) - super - ActiveRecord::SchemaMigration.create_table - end - def setup super + ActiveRecord::SchemaMigration.create_table @stream = StringIO.new end diff --git a/activerecord/test/cases/validations/i18n_generate_message_validation_test.rb b/activerecord/test/cases/validations/i18n_generate_message_validation_test.rb index a8e513d81f..2f5ee32538 100644 --- a/activerecord/test/cases/validations/i18n_generate_message_validation_test.rb +++ b/activerecord/test/cases/validations/i18n_generate_message_validation_test.rb @@ -13,7 +13,7 @@ class I18nGenerateMessageValidationTest < ActiveRecord::TestCase I18n.load_path.clear I18n.backend = I18n::Backend::Simple.new yield - ensure + ensure I18n.load_path.replace @old_load_path I18n.backend = @old_backend end diff --git a/activerecord/test/cases/validations/i18n_validation_test.rb b/activerecord/test/cases/validations/i18n_validation_test.rb index 15b97c02c8..efa0c9b934 100644 --- a/activerecord/test/cases/validations/i18n_validation_test.rb +++ b/activerecord/test/cases/validations/i18n_validation_test.rb @@ -43,7 +43,7 @@ class I18nValidationTest < ActiveRecord::TestCase [ "given option that is not reserved", {:format => "jpg"}, {:format => "jpg" }] # TODO Add :on case, but below doesn't work, because then the validation isn't run for some reason # even when using .save instead .valid? - # [ "given on condition", {:on => :save}, {}] + # [ "given on condition", {on: :save}, {}] ] # validates_uniqueness_of w/ mocha diff --git a/activerecord/test/schema/postgresql_specific_schema.rb b/activerecord/test/schema/postgresql_specific_schema.rb index 2cd9f30b59..d0e7338f15 100644 --- a/activerecord/test/schema/postgresql_specific_schema.rb +++ b/activerecord/test/schema/postgresql_specific_schema.rb @@ -12,6 +12,8 @@ ActiveRecord::Schema.define do execute 'DROP FUNCTION IF EXISTS partitioned_insert_trigger()' + execute "DROP SCHEMA IF EXISTS schema_1 CASCADE" + %w(accounts_id_seq developers_id_seq projects_id_seq topics_id_seq customers_id_seq orders_id_seq).each do |seq_name| execute "SELECT setval('#{seq_name}', 100)" end @@ -37,7 +39,12 @@ ActiveRecord::Schema.define do ); _SQL - execute <<_SQL + execute "CREATE SCHEMA schema_1" + execute "CREATE DOMAIN schema_1.text AS text" + execute "CREATE DOMAIN schema_1.varchar AS varchar" + execute "CREATE DOMAIN schema_1.bpchar AS bpchar" + + execute <<_SQL CREATE TABLE geometrics ( id serial primary key, a_point point, |