diff options
12 files changed, 279 insertions, 258 deletions
diff --git a/actionpack/lib/action_controller/metal/strong_parameters.rb b/actionpack/lib/action_controller/metal/strong_parameters.rb index aff083b502..d86d49c9dc 100644 --- a/actionpack/lib/action_controller/metal/strong_parameters.rb +++ b/actionpack/lib/action_controller/metal/strong_parameters.rb @@ -502,7 +502,7 @@ module ActionController # end # end # - # In order to use <tt>accepts_nested_attribute_for</tt> with Strong \Parameters, you + # In order to use <tt>accepts_nested_attributes_for</tt> with Strong \Parameters, you # will need to specify which nested attributes should be whitelisted. # # class Person diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index fe18ae995c..18ec203fa3 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,9 @@ +* Fixed error when specifying a non-empty default value on a PostgreSQL array column. + + Fixes #10613. + + *Luke Steensen* + * Make possible to change `record_timestamps` inside Callbacks. *Tieg Zaharia* diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 860e76fa18..4abe2ad0a0 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -4,6 +4,12 @@ require 'active_support/core_ext/module/remove_method' require 'active_record/errors' module ActiveRecord + class AssociationNotFoundError < ConfigurationError #:nodoc: + def initialize(record, association_name) + super("Association named '#{association_name}' was not found on #{record.class.name}; perhaps you misspelled it?") + end + end + class InverseOfAssociationNotFoundError < ActiveRecordError #:nodoc: def initialize(reflection, associated_class = nil) super("Could not find the inverse association for #{reflection.name} (#{reflection.options[:inverse_of].inspect} in #{associated_class.nil? ? reflection.class_name : associated_class.name})") @@ -145,7 +151,7 @@ module ActiveRecord association = association_instance_get(name) if association.nil? - reflection = self.class.reflect_on_association(name) + raise AssociationNotFoundError.new(self, name) unless reflection = self.class.reflect_on_association(name) association = reflection.association_class.new(self, reflection) association_instance_set(name, association) end diff --git a/activerecord/lib/active_record/associations/preloader.rb b/activerecord/lib/active_record/associations/preloader.rb index 83637a0409..31ddf4e0fc 100644 --- a/activerecord/lib/active_record/associations/preloader.rb +++ b/activerecord/lib/active_record/associations/preloader.rb @@ -140,36 +140,13 @@ module ActiveRecord end def grouped_records(association, records) - reflection_records = records_by_reflection(association, records) - - reflection_records.each_with_object({}) do |(reflection, r_records),h| - h[reflection] = r_records.group_by { |record| - association_klass(reflection, record) - } - end - end - - def records_by_reflection(association, records) - records.group_by do |record| - reflection = record.class.reflect_on_association(association) - - reflection || raise_config_error(record, association) - end - end - - def raise_config_error(record, association) - raise ActiveRecord::ConfigurationError, - "Association named '#{association}' was not found on #{record.class.name}; " \ - "perhaps you misspelled it?" - end - - def association_klass(reflection, record) - if reflection.macro == :belongs_to && reflection.options[:polymorphic] - klass = record.read_attribute(reflection.foreign_type.to_s) - klass && klass.constantize - else - reflection.klass + h = {} + records.each do |record| + assoc = record.association(association) + klasses = h[assoc.reflection] ||= {} + (klasses[assoc.klass] ||= []) << record end + h end class AlreadyLoaded diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb index 8ac20441da..47fe501752 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb @@ -64,7 +64,7 @@ module ActiveRecord end def add_column_options!(sql, options) - sql << " DEFAULT #{@conn.quote(options[:default], options[:column])}" if options_include_default?(options) + sql << " DEFAULT #{quote_value(options[:default], options[:column])}" if options_include_default?(options) # must explicitly check for :null to allow change_column to work on migrations if options[:null] == false sql << " NOT NULL" @@ -75,6 +75,12 @@ module ActiveRecord sql end + def quote_value(value, column) + column.sql_type ||= type_to_sql(column.type, column.limit, column.precision, column.scale) + + @conn.quote(value, column) + end + def options_include_default?(options) options.include?(:default) && !(options[:null] == false && options[:default].nil?) end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb index c39bf15e83..71c3a4378b 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -15,7 +15,7 @@ module ActiveRecord # are typically created by methods in TableDefinition, and added to the # +columns+ attribute of said TableDefinition object, in order to be used # for generating a number of table creation or table changing SQL statements. - class ColumnDefinition < Struct.new(:name, :type, :limit, :precision, :scale, :default, :null, :first, :after, :primary_key) #:nodoc: + class ColumnDefinition < Struct.new(:name, :type, :limit, :precision, :scale, :default, :null, :first, :after, :primary_key, :sql_type) #:nodoc: def primary_key? primary_key || type.to_sym == :primary_key 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 e4c6502bb7..20eea208ec 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -30,7 +30,7 @@ module ActiveRecord options = o.options sql_type = type_to_sql(o.type, options[:limit], options[:precision], options[:scale]) change_column_sql = "CHANGE #{quote_column_name(column.name)} #{quote_column_name(options[:name])} #{sql_type}" - add_column_options!(change_column_sql, options) + add_column_options!(change_column_sql, options.merge(column: column)) add_column_position!(change_column_sql, options) end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/column.rb b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb new file mode 100644 index 0000000000..823189934d --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb @@ -0,0 +1,221 @@ +module ActiveRecord + module ConnectionAdapters + # PostgreSQL-specific extensions to column definitions in a table. + class PostgreSQLColumn < Column #:nodoc: + attr_accessor :array + + def initialize(name, default, oid_type, sql_type = nil, null = true) + @oid_type = oid_type + default_value = self.class.extract_value_from_default(default) + + if sql_type =~ /\[\]$/ + @array = true + super(name, default_value, sql_type[0..sql_type.length - 3], null) + else + @array = false + super(name, default_value, sql_type, null) + end + + @default_function = default if has_default_function?(default_value, default) + end + + def number? + !array && super + end + + def text? + !array && super + end + + # :stopdoc: + class << self + include ConnectionAdapters::PostgreSQLColumn::Cast + include ConnectionAdapters::PostgreSQLColumn::ArrayParser + attr_accessor :money_precision + end + # :startdoc: + + # Extracts the value from a PostgreSQL column default definition. + def self.extract_value_from_default(default) + # This is a performance optimization for Ruby 1.9.2 in development. + # If the value is nil, we return nil straight away without checking + # the regular expressions. If we check each regular expression, + # Regexp#=== will call NilClass#to_str, which will trigger + # method_missing (defined by whiny nil in ActiveSupport) which + # makes this method very very slow. + return default unless default + + case default + when /\A'(.*)'::(num|date|tstz|ts|int4|int8)range\z/m + $1 + # Numeric types + when /\A\(?(-?\d+(\.\d*)?\)?(::bigint)?)\z/ + $1 + # Character types + when /\A\(?'(.*)'::.*\b(?:character varying|bpchar|text)\z/m + $1.gsub(/''/, "'") + # Binary data types + when /\A'(.*)'::bytea\z/m + $1 + # Date/time types + when /\A'(.+)'::(?:time(?:stamp)? with(?:out)? time zone|date)\z/ + $1 + when /\A'(.*)'::interval\z/ + $1 + # Boolean type + when 'true' + true + when 'false' + false + # Geometric types + when /\A'(.*)'::(?:point|line|lseg|box|"?path"?|polygon|circle)\z/ + $1 + # Network address types + when /\A'(.*)'::(?:cidr|inet|macaddr)\z/ + $1 + # Bit string types + when /\AB'(.*)'::"?bit(?: varying)?"?\z/ + $1 + # XML type + when /\A'(.*)'::xml\z/m + $1 + # Arrays + when /\A'(.*)'::"?\D+"?\[\]\z/ + $1 + # Hstore + when /\A'(.*)'::hstore\z/ + $1 + # JSON + when /\A'(.*)'::json\z/ + $1 + # Object identifier types + when /\A-?\d+\z/ + $1 + else + # Anything else is blank, some user type, or some function + # and we can't know the value of that, so return nil. + nil + end + end + + def type_cast_for_write(value) + if @oid_type.respond_to?(:type_cast_for_write) + @oid_type.type_cast_for_write(value) + else + super + end + end + + def type_cast(value) + return if value.nil? + return super if encoded? + + @oid_type.type_cast value + end + + def accessor + @oid_type.accessor + end + + private + + def has_default_function?(default_value, default) + !default_value && (%r{\w+\(.*\)} === default) + end + + def extract_limit(sql_type) + case sql_type + when /^bigint/i; 8 + when /^smallint/i; 2 + when /^timestamp/i; nil + else super + end + end + + # Extracts the scale from PostgreSQL-specific data types. + def extract_scale(sql_type) + # Money type has a fixed scale of 2. + sql_type =~ /^money/ ? 2 : super + end + + # Extracts the precision from PostgreSQL-specific data types. + def extract_precision(sql_type) + if sql_type == 'money' + self.class.money_precision + elsif sql_type =~ /timestamp/i + $1.to_i if sql_type =~ /\((\d+)\)/ + else + super + end + end + + # Maps PostgreSQL-specific data types to logical Rails types. + def simplified_type(field_type) + case field_type + # Numeric and monetary types + when /^(?:real|double precision)$/ + :float + # Monetary types + when 'money' + :decimal + when 'hstore' + :hstore + when 'ltree' + :ltree + # Network address types + when 'inet' + :inet + when 'cidr' + :cidr + when 'macaddr' + :macaddr + # Character types + when /^(?:character varying|bpchar)(?:\(\d+\))?$/ + :string + when /^citext(?:\(\d+\))?$/ + :citext + # Binary data types + when 'bytea' + :binary + # Date/time types + when /^timestamp with(?:out)? time zone$/ + :datetime + when /^interval(?:|\(\d+\))$/ + :string + # Geometric types + when /^(?:point|line|lseg|box|"?path"?|polygon|circle)$/ + :string + # Bit strings + when /^bit(?: varying)?(?:\(\d+\))?$/ + :string + # XML type + when 'xml' + :xml + # tsvector type + when 'tsvector' + :tsvector + # Arrays + when /^\D+\[\]$/ + :string + # Object identifier types + when 'oid' + :integer + # UUID type + when 'uuid' + :uuid + # JSON type + when 'json' + :json + # Small and big integer types + when /^(?:small|big)int$/ + :integer + when /(num|date|tstz|ts|int4|int8)range$/ + field_type.to_sym + # Pass through all types that are not specific to PostgreSQL. + else + super + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index bcad9f30d7..c31fe96842 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -7,6 +7,7 @@ require 'active_record/connection_adapters/postgresql/quoting' require 'active_record/connection_adapters/postgresql/schema_statements' require 'active_record/connection_adapters/postgresql/database_statements' require 'active_record/connection_adapters/postgresql/referential_integrity' +require 'active_record/connection_adapters/postgresql/column' require 'arel/visitors/bind_visitor' # Make sure we're using pg high enough for PGResult#values @@ -43,224 +44,6 @@ module ActiveRecord end module ConnectionAdapters - # PostgreSQL-specific extensions to column definitions in a table. - class PostgreSQLColumn < Column #:nodoc: - attr_accessor :array - - def initialize(name, default, oid_type, sql_type = nil, null = true) - @oid_type = oid_type - default_value = self.class.extract_value_from_default(default) - - if sql_type =~ /\[\]$/ - @array = true - super(name, default_value, sql_type[0..sql_type.length - 3], null) - else - @array = false - super(name, default_value, sql_type, null) - end - - @default_function = default if has_default_function?(default_value, default) - end - - def number? - !array && super - end - - def text? - !array && super - end - - # :stopdoc: - class << self - include ConnectionAdapters::PostgreSQLColumn::Cast - include ConnectionAdapters::PostgreSQLColumn::ArrayParser - attr_accessor :money_precision - end - # :startdoc: - - # Extracts the value from a PostgreSQL column default definition. - def self.extract_value_from_default(default) - # This is a performance optimization for Ruby 1.9.2 in development. - # If the value is nil, we return nil straight away without checking - # the regular expressions. If we check each regular expression, - # Regexp#=== will call NilClass#to_str, which will trigger - # method_missing (defined by whiny nil in ActiveSupport) which - # makes this method very very slow. - return default unless default - - case default - when /\A'(.*)'::(num|date|tstz|ts|int4|int8)range\z/m - $1 - # Numeric types - when /\A\(?(-?\d+(\.\d*)?\)?(::bigint)?)\z/ - $1 - # Character types - when /\A\(?'(.*)'::.*\b(?:character varying|bpchar|text)\z/m - $1.gsub(/''/, "'") - # Binary data types - when /\A'(.*)'::bytea\z/m - $1 - # Date/time types - when /\A'(.+)'::(?:time(?:stamp)? with(?:out)? time zone|date)\z/ - $1 - when /\A'(.*)'::interval\z/ - $1 - # Boolean type - when 'true' - true - when 'false' - false - # Geometric types - when /\A'(.*)'::(?:point|line|lseg|box|"?path"?|polygon|circle)\z/ - $1 - # Network address types - when /\A'(.*)'::(?:cidr|inet|macaddr)\z/ - $1 - # Bit string types - when /\AB'(.*)'::"?bit(?: varying)?"?\z/ - $1 - # XML type - when /\A'(.*)'::xml\z/m - $1 - # Arrays - when /\A'(.*)'::"?\D+"?\[\]\z/ - $1 - # Hstore - when /\A'(.*)'::hstore\z/ - $1 - # JSON - when /\A'(.*)'::json\z/ - $1 - # Object identifier types - when /\A-?\d+\z/ - $1 - else - # Anything else is blank, some user type, or some function - # and we can't know the value of that, so return nil. - nil - end - end - - def type_cast_for_write(value) - if @oid_type.respond_to?(:type_cast_for_write) - @oid_type.type_cast_for_write(value) - else - super - end - end - - def type_cast(value) - return if value.nil? - return super if encoded? - - @oid_type.type_cast value - end - - def accessor - @oid_type.accessor - end - - private - - def has_default_function?(default_value, default) - !default_value && (%r{\w+\(.*\)} === default) - end - - def extract_limit(sql_type) - case sql_type - when /^bigint/i; 8 - when /^smallint/i; 2 - when /^timestamp/i; nil - else super - end - end - - # Extracts the scale from PostgreSQL-specific data types. - def extract_scale(sql_type) - # Money type has a fixed scale of 2. - sql_type =~ /^money/ ? 2 : super - end - - # Extracts the precision from PostgreSQL-specific data types. - def extract_precision(sql_type) - if sql_type == 'money' - self.class.money_precision - elsif sql_type =~ /timestamp/i - $1.to_i if sql_type =~ /\((\d+)\)/ - else - super - end - end - - # Maps PostgreSQL-specific data types to logical Rails types. - def simplified_type(field_type) - case field_type - # Numeric and monetary types - when /^(?:real|double precision)$/ - :float - # Monetary types - when 'money' - :decimal - when 'hstore' - :hstore - when 'ltree' - :ltree - # Network address types - when 'inet' - :inet - when 'cidr' - :cidr - when 'macaddr' - :macaddr - # Character types - when /^(?:character varying|bpchar)(?:\(\d+\))?$/ - :string - when /^citext(?:\(\d+\))?$/ - :citext - # Binary data types - when 'bytea' - :binary - # Date/time types - when /^timestamp with(?:out)? time zone$/ - :datetime - when /^interval(?:|\(\d+\))$/ - :string - # Geometric types - when /^(?:point|line|lseg|box|"?path"?|polygon|circle)$/ - :string - # Bit strings - when /^bit(?: varying)?(?:\(\d+\))?$/ - :string - # XML type - when 'xml' - :xml - # tsvector type - when 'tsvector' - :tsvector - # Arrays - when /^\D+\[\]$/ - :string - # Object identifier types - when 'oid' - :integer - # UUID type - when 'uuid' - :uuid - # JSON type - when 'json' - :json - # Small and big integer types - when /^(?:small|big)int$/ - :integer - when /(num|date|tstz|ts|int4|int8)range$/ - field_type.to_sym - # Pass through all types that are not specific to PostgreSQL. - else - super - end - end - end - # The PostgreSQL adapter works with the native C (https://bitbucket.org/ged/ruby-pg) driver. # # Options: diff --git a/activerecord/test/cases/adapters/postgresql/array_test.rb b/activerecord/test/cases/adapters/postgresql/array_test.rb index 714da83a54..36ded66998 100644 --- a/activerecord/test/cases/adapters/postgresql/array_test.rb +++ b/activerecord/test/cases/adapters/postgresql/array_test.rb @@ -48,6 +48,17 @@ class PostgresqlArrayTest < ActiveRecord::TestCase PgArray.reset_column_information end + def test_default_strings + @connection.add_column 'pg_arrays', 'names', :string, array: true, default: ["foo", "bar"] + PgArray.reset_column_information + column = PgArray.columns_hash["names"] + + assert_equal(["foo", "bar"], column.default) + assert_equal(["foo", "bar"], PgArray.new.names) + ensure + PgArray.reset_column_information + end + def test_change_column_with_array @connection.add_column :pg_arrays, :snippets, :string, array: true, default: [] @connection.change_column :pg_arrays, :snippets, :text, array: true, default: "{}" diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb index e59161fc5b..8c9797861c 100644 --- a/activerecord/test/cases/associations/eager_test.rb +++ b/activerecord/test/cases/associations/eager_test.rb @@ -235,6 +235,17 @@ class EagerAssociationTest < ActiveRecord::TestCase end end + def test_finding_with_includes_on_empty_polymorphic_type_column + sponsor = sponsors(:moustache_club_sponsor_for_groucho) + sponsor.update!(sponsorable_type: '', sponsorable_id: nil) # sponsorable_type column might be declared NOT NULL + sponsor = assert_queries(1) do + assert_nothing_raised { Sponsor.all.merge!(:includes => :sponsorable).find(sponsor.id) } + end + assert_no_queries do + assert_equal nil, sponsor.sponsorable + end + end + def test_loading_from_an_association posts = authors(:david).posts.merge(:includes => :comments, :order => "posts.id").to_a assert_equal 2, posts.first.comments.size @@ -709,16 +720,16 @@ class EagerAssociationTest < ActiveRecord::TestCase end def test_eager_with_invalid_association_reference - assert_raise(ActiveRecord::ConfigurationError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys") { + assert_raise(ActiveRecord::AssociationNotFoundError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys") { Post.all.merge!(:includes=> :monkeys ).find(6) } - assert_raise(ActiveRecord::ConfigurationError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys") { + assert_raise(ActiveRecord::AssociationNotFoundError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys") { Post.all.merge!(:includes=>[ :monkeys ]).find(6) } - assert_raise(ActiveRecord::ConfigurationError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys") { + assert_raise(ActiveRecord::AssociationNotFoundError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys") { Post.all.merge!(:includes=>[ 'monkeys' ]).find(6) } - assert_raise(ActiveRecord::ConfigurationError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys, :elephants") { + assert_raise(ActiveRecord::AssociationNotFoundError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys, :elephants") { Post.all.merge!(:includes=>[ :monkeys, :elephants ]).find(6) } end diff --git a/tasks/release.rb b/tasks/release.rb index 849117884c..767feaf236 100644 --- a/tasks/release.rb +++ b/tasks/release.rb @@ -67,7 +67,7 @@ end namespace :changelog do task :release_date do - FRAMEWORKS + ['guides'].each do |fw| + (FRAMEWORKS + ['guides']).each do |fw| require 'date' replace = '\1(' + Date.today.strftime('%B %d, %Y') + ')' fname = File.join fw, 'CHANGELOG.md' @@ -78,7 +78,7 @@ namespace :changelog do end task :release_summary do - FRAMEWORKS + ['guides'].each do |fw| + (FRAMEWORKS + ['guides']).each do |fw| puts "## #{fw}" fname = File.join fw, 'CHANGELOG.md' contents = File.readlines fname |