diff options
29 files changed, 230 insertions, 43 deletions
diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb index 652ce0b3d8..5852aeaec2 100644 --- a/actionmailer/lib/action_mailer/base.rb +++ b/actionmailer/lib/action_mailer/base.rb @@ -301,12 +301,13 @@ module ActionMailer # end # end # - # Callbacks in ActionMailer are implemented using AbstractController::Callbacks, so you - # can define and configure callbacks in the same manner that you would use callbacks in - # classes that inherit from ActionController::Base. + # Callbacks in Action Mailer are implemented using + # <tt>AbstractController::Callbacks</tt>, so you can define and configure + # callbacks in the same manner that you would use callbacks in classes that + # inherit from <tt>ActionController::Base</tt>. # # Note that unless you have a specific reason to do so, you should prefer using before_action - # rather than after_action in your ActionMailer classes so that headers are parsed properly. + # rather than after_action in your Action Mailer classes so that headers are parsed properly. # # = Previewing emails # diff --git a/actionmailer/lib/action_mailer/test_helper.rb b/actionmailer/lib/action_mailer/test_helper.rb index 6452bf616c..06da0dd27e 100644 --- a/actionmailer/lib/action_mailer/test_helper.rb +++ b/actionmailer/lib/action_mailer/test_helper.rb @@ -1,4 +1,6 @@ module ActionMailer + # Provides helper methods for testing Action Mailer, including #assert_emails + # and #assert_no_emails module TestHelper # Asserts that the number of emails sent matches the given number. # diff --git a/actionmailer/lib/action_mailer/version.rb b/actionmailer/lib/action_mailer/version.rb index a98aec913f..06f80a8fdc 100644 --- a/actionmailer/lib/action_mailer/version.rb +++ b/actionmailer/lib/action_mailer/version.rb @@ -1,7 +1,8 @@ require_relative 'gem_version' module ActionMailer - # Returns the version of the currently loaded ActionMailer as a <tt>Gem::Version</tt> + # Returns the version of the currently loaded Action Mailer as a + # <tt>Gem::Version</tt>. def self.version gem_version end diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index bf34507903..d8157d02ab 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,9 @@ +* Add a properties API to allow custom types and type casting behavior + to be specified. Will enable many edge cases to be deprecated, and + allow for additional interesting features in the future. + + *Sean Griffin* + * Fix has_and_belongs_to_many public reflection. When defining a has_and_belongs_to_many, internally we convert that to two has_many. But as `reflections` is a public API, people expect to see the right macro. @@ -30,7 +36,7 @@ * Change belongs_to touch to be consistent with timestamp updates - If a model is set up with a belongs_to: touch relatinoship the parent + If a model is set up with a belongs_to: touch relationship the parent record will only be touched if the record was modified. This makes it consistent with timestamp updating on the record itself. diff --git a/activerecord/lib/active_record/associations/preloader.rb b/activerecord/lib/active_record/associations/preloader.rb index 42571d6af0..7519fec10a 100644 --- a/activerecord/lib/active_record/associations/preloader.rb +++ b/activerecord/lib/active_record/associations/preloader.rb @@ -143,6 +143,7 @@ module ActiveRecord def grouped_records(association, records) h = {} records.each do |record| + next unless record assoc = record.association(association) klasses = h[assoc.reflection] ||= {} (klasses[assoc.klass] ||= []) << record diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index db4d5f0129..8b0fffcf06 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -19,6 +19,7 @@ require 'active_record/errors' require 'active_record/log_subscriber' require 'active_record/explain_subscriber' require 'active_record/relation/delegation' +require 'active_record/properties' module ActiveRecord #:nodoc: # = Active Record @@ -321,6 +322,7 @@ module ActiveRecord #:nodoc: include Reflection include Serialization include Store + include Properties end ActiveSupport.run_load_hooks(:active_record, Base) 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 8efb288c95..82e62786ca 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -223,8 +223,6 @@ module ActiveRecord if value.kind_of?(String) && column && column.type == :binary s = value.unpack("H*")[0] "x'#{s}'" - elsif value.kind_of?(BigDecimal) - value.to_s("F") else super end diff --git a/activerecord/lib/active_record/connection_adapters/type/binary.rb b/activerecord/lib/active_record/connection_adapters/type/binary.rb index 4b2d1a66e0..60afe44de1 100644 --- a/activerecord/lib/active_record/connection_adapters/type/binary.rb +++ b/activerecord/lib/active_record/connection_adapters/type/binary.rb @@ -1,7 +1,7 @@ module ActiveRecord module ConnectionAdapters module Type - class Binary < Value # :nodoc: + class Binary < Value def type :binary end diff --git a/activerecord/lib/active_record/connection_adapters/type/boolean.rb b/activerecord/lib/active_record/connection_adapters/type/boolean.rb index 2337bdd563..0d97379189 100644 --- a/activerecord/lib/active_record/connection_adapters/type/boolean.rb +++ b/activerecord/lib/active_record/connection_adapters/type/boolean.rb @@ -1,7 +1,7 @@ module ActiveRecord module ConnectionAdapters module Type - class Boolean < Value # :nodoc: + class Boolean < Value def type :boolean end diff --git a/activerecord/lib/active_record/connection_adapters/type/date.rb b/activerecord/lib/active_record/connection_adapters/type/date.rb index 1e7205fd0b..e8becbe1f4 100644 --- a/activerecord/lib/active_record/connection_adapters/type/date.rb +++ b/activerecord/lib/active_record/connection_adapters/type/date.rb @@ -1,7 +1,7 @@ module ActiveRecord module ConnectionAdapters module Type - class Date < Value # :nodoc: + class Date < Value def type :date end diff --git a/activerecord/lib/active_record/connection_adapters/type/date_time.rb b/activerecord/lib/active_record/connection_adapters/type/date_time.rb index c34f4c5a53..64f5d05301 100644 --- a/activerecord/lib/active_record/connection_adapters/type/date_time.rb +++ b/activerecord/lib/active_record/connection_adapters/type/date_time.rb @@ -1,7 +1,7 @@ module ActiveRecord module ConnectionAdapters module Type - class DateTime < Value # :nodoc: + class DateTime < Value include TimeValue def type diff --git a/activerecord/lib/active_record/connection_adapters/type/decimal.rb b/activerecord/lib/active_record/connection_adapters/type/decimal.rb index ac5af4b963..e93906ba19 100644 --- a/activerecord/lib/active_record/connection_adapters/type/decimal.rb +++ b/activerecord/lib/active_record/connection_adapters/type/decimal.rb @@ -1,7 +1,7 @@ module ActiveRecord module ConnectionAdapters module Type - class Decimal < Value # :nodoc: + class Decimal < Value include Numeric def type diff --git a/activerecord/lib/active_record/connection_adapters/type/float.rb b/activerecord/lib/active_record/connection_adapters/type/float.rb index 28111e9d8e..f2427d2dfa 100644 --- a/activerecord/lib/active_record/connection_adapters/type/float.rb +++ b/activerecord/lib/active_record/connection_adapters/type/float.rb @@ -1,7 +1,7 @@ module ActiveRecord module ConnectionAdapters module Type - class Float < Value # :nodoc: + class Float < Value include Numeric def type diff --git a/activerecord/lib/active_record/connection_adapters/type/integer.rb b/activerecord/lib/active_record/connection_adapters/type/integer.rb index 8e6a509b5b..596f4de2a8 100644 --- a/activerecord/lib/active_record/connection_adapters/type/integer.rb +++ b/activerecord/lib/active_record/connection_adapters/type/integer.rb @@ -1,7 +1,7 @@ module ActiveRecord module ConnectionAdapters module Type - class Integer < Value # :nodoc: + class Integer < Value include Numeric def type diff --git a/activerecord/lib/active_record/connection_adapters/type/string.rb b/activerecord/lib/active_record/connection_adapters/type/string.rb index 55f0e1ee1c..471f949e09 100644 --- a/activerecord/lib/active_record/connection_adapters/type/string.rb +++ b/activerecord/lib/active_record/connection_adapters/type/string.rb @@ -1,7 +1,7 @@ module ActiveRecord module ConnectionAdapters module Type - class String < Value # :nodoc: + class String < Value def type :string end diff --git a/activerecord/lib/active_record/connection_adapters/type/text.rb b/activerecord/lib/active_record/connection_adapters/type/text.rb index ee5842a3fc..61095ebb38 100644 --- a/activerecord/lib/active_record/connection_adapters/type/text.rb +++ b/activerecord/lib/active_record/connection_adapters/type/text.rb @@ -3,7 +3,7 @@ require 'active_record/connection_adapters/type/string' module ActiveRecord module ConnectionAdapters module Type - class Text < String # :nodoc: + class Text < String def type :text end diff --git a/activerecord/lib/active_record/connection_adapters/type/time.rb b/activerecord/lib/active_record/connection_adapters/type/time.rb index 4dd201e3fe..bc331b0fa7 100644 --- a/activerecord/lib/active_record/connection_adapters/type/time.rb +++ b/activerecord/lib/active_record/connection_adapters/type/time.rb @@ -1,7 +1,7 @@ module ActiveRecord module ConnectionAdapters module Type - class Time < Value # :nodoc: + class Time < Value include TimeValue def type diff --git a/activerecord/lib/active_record/connection_adapters/type/value.rb b/activerecord/lib/active_record/connection_adapters/type/value.rb index 9bbc249c2b..60b443004c 100644 --- a/activerecord/lib/active_record/connection_adapters/type/value.rb +++ b/activerecord/lib/active_record/connection_adapters/type/value.rb @@ -1,9 +1,11 @@ module ActiveRecord module ConnectionAdapters module Type - class Value # :nodoc: + class Value attr_reader :precision, :scale, :limit + # Valid options are +precision+, +scale+, and +limit+. + # They are only used when dumping schema. def initialize(options = {}) options.assert_valid_keys(:precision, :scale, :limit) @precision = options[:precision] @@ -11,8 +13,13 @@ module ActiveRecord @limit = options[:limit] end + # The simplified that this object represents. Subclasses + # should override this method. def type; end + # Takes an input from the database, or from attribute setters, + # and casts it to a type appropriate for this object. This method + # should not be overriden by subclasses. Instead, override `cast_value`. def type_cast(value) cast_value(value) unless value.nil? end @@ -43,7 +50,9 @@ module ActiveRecord private - def cast_value(value) + # Responsible for casting values from external sources to the appropriate + # type. Called by `type_cast` for all values except `nil`. + def cast_value(value) # :api: public value end end diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb index 8449fb1266..a4e10ed2e7 100644 --- a/activerecord/lib/active_record/model_schema.rb +++ b/activerecord/lib/active_record/model_schema.rb @@ -217,16 +217,6 @@ module ActiveRecord connection.schema_cache.table_exists?(table_name) end - # Returns an array of column objects for the table associated with this class. - def columns - connection.schema_cache.columns(table_name) - end - - # Returns a hash of column objects for the table associated with this class. - def columns_hash - connection.schema_cache.columns_hash(table_name) - end - def column_types # :nodoc: @column_types ||= decorate_columns(columns_hash.dup) end diff --git a/activerecord/lib/active_record/properties.rb b/activerecord/lib/active_record/properties.rb new file mode 100644 index 0000000000..a5d724de0e --- /dev/null +++ b/activerecord/lib/active_record/properties.rb @@ -0,0 +1,95 @@ +module ActiveRecord + module Properties + extend ActiveSupport::Concern + + Type = ConnectionAdapters::Type + + module ClassMethods + # Defines or overrides a property on this model. This allows customization of + # Active Record's type casting behavior, as well as adding support for user defined + # types. + # + # ==== Examples + # + # The type detected by Active Record can be overriden. + # + # # db/schema.rb + # create_table :store_listings, force: true do |t| + # t.decimal :price_in_cents + # end + # + # # app/models/store_listing.rb + # class StoreListing < ActiveRecord::Base + # end + # + # store_listing = StoreListing.new(price_in_cents: '10.1') + # + # # before + # store_listing.price_in_cents # => BigDecimal.new(10.1) + # + # class StoreListing < ActiveRecord::Base + # property :price_in_cents, Type::Integer.new + # end + # + # # after + # store_listing.price_in_cents # => 10 + # + # Users may also define their own custom types, as long as they respond to the methods + # defined on the value type. The `type_cast` method on your type object will be called + # with values both from the database, and from your controllers. See + # `ActiveRecord::Properties::Type::Value` for the expected API. It is recommended that your + # type objects inherit from an existing type, or the base value type. + # + # class MoneyType < ActiveRecord::Type::Integer + # def type_cast(value) + # if value.include?('$') + # price_in_dollars = value.gsub(/\$/, '').to_f + # price_in_dollars * 100 + # else + # value.to_i + # end + # end + # end + # + # class StoreListing < ActiveRecord::Base + # property :price_in_cents, MoneyType.new + # end + # + # store_listing = StoreListing.new(price_in_cents: '$10.00') + # store_listing.price_in_cents # => 1000 + def property(name, cast_type) + name = name.to_s + user_provided_columns[name] = ConnectionAdapters::Column.new(name, nil, cast_type) + end + + # Returns an array of column objects for the table associated with this class. + def columns + @columns ||= add_user_provided_columns(connection.schema_cache.columns(table_name)) + end + + # Returns a hash of column objects for the table associated with this class. + def columns_hash + @columns_hash ||= Hash[columns.map { |c| [c.name, c] }] + end + + def reset_column_information # :nodoc: + super + + @columns = nil + @columns_hash = nil + end + + private + + def user_provided_columns + @user_provided_columns ||= {} + end + + def add_user_provided_columns(schema_columns) + schema_columns.reject { |column| + user_provided_columns.key? column.name + } + user_provided_columns.values + end + end + end +end diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb index ebcf453305..4bd4486b41 100644 --- a/activerecord/test/cases/associations/eager_test.rb +++ b/activerecord/test/cases/associations/eager_test.rb @@ -1167,6 +1167,13 @@ class EagerAssociationTest < ActiveRecord::TestCase ) end + test "deep preload" do + post = Post.preload(author: :posts, comments: :post).first + + assert_predicate post.author.association(:posts), :loaded? + assert_predicate post.comments.first.association(:post), :loaded? + end + test "preloading does not cache has many association subset when preloaded with a through association" do author = Author.includes(:comments_with_order_and_conditions, :posts).first assert_no_queries { assert_equal 2, author.comments_with_order_and_conditions.size } @@ -1196,6 +1203,15 @@ class EagerAssociationTest < ActiveRecord::TestCase end end + test "preloading the same association twice works" do + Member.create! + members = Member.preload(:current_membership).includes(current_membership: :club).all.to_a + assert_no_queries { + members_with_membership = members.select(&:current_membership) + assert_equal 3, members_with_membership.map(&:current_membership).map(&:club).size + } + end + test "preloading with a polymorphic association and using the existential predicate" do assert_equal authors(:david), authors(:david).essays.includes(:writer).first.writer diff --git a/activerecord/test/cases/custom_properties_test.rb b/activerecord/test/cases/custom_properties_test.rb new file mode 100644 index 0000000000..9598f0299c --- /dev/null +++ b/activerecord/test/cases/custom_properties_test.rb @@ -0,0 +1,64 @@ +require 'cases/helper' + +class OverloadedType < ActiveRecord::Base + property :overloaded_float, Type::Integer.new + property :overloaded_string_with_limit, Type::String.new(limit: 50) + property :non_existent_decimal, Type::Decimal.new +end + +class UnoverloadedType < ActiveRecord::Base + self.table_name = 'overloaded_types' +end + +module ActiveRecord + class CustomPropertiesTest < ActiveRecord::TestCase + def test_overloading_types + data = OverloadedType.new + + data.overloaded_float = "1.1" + data.unoverloaded_float = "1.1" + + assert_equal 1, data.overloaded_float + assert_equal 1.1, data.unoverloaded_float + end + + def test_overloaded_properties_save + data = OverloadedType.new + + data.overloaded_float = "2.2" + data.save! + data.reload + + assert_equal 2, data.overloaded_float + assert_equal 2.0, UnoverloadedType.last.overloaded_float + end + + def test_properties_assigned_in_constructor + data = OverloadedType.new(overloaded_float: '3.3') + + assert_equal 3, data.overloaded_float + end + + def test_overloaded_properties_with_limit + assert_equal 50, OverloadedType.columns_hash['overloaded_string_with_limit'].limit + assert_equal 255, UnoverloadedType.columns_hash['overloaded_string_with_limit'].limit + end + + def test_nonexistent_property + data = OverloadedType.new(non_existent_decimal: 1) + + assert_equal BigDecimal.new(1), data.non_existent_decimal + assert_raise ActiveRecord::UnknownAttributeError do + UnoverloadedType.new(non_existent_decimal: 1) + end + end + + def test_overloaded_properties_have_no_default + data = OverloadedType.new + unoverloaded_data = UnoverloadedType.new + + assert_nil data.overloaded_float + assert unoverloaded_data.overloaded_float + end + end +end diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index 61111b254a..88df997a2f 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -552,13 +552,6 @@ class RelationTest < ActiveRecord::TestCase end end - def test_deep_preload - post = Post.preload(author: :posts, comments: :post).first - - assert_predicate post.author.association(:posts), :loaded? - assert_predicate post.comments.first.association(:post), :loaded? - end - def test_preload_applies_to_all_chained_preloaded_scopes assert_queries(3) do post = Post.with_comments.with_tags.first diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index c15ee5022e..4cce58f4f4 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -855,6 +855,12 @@ ActiveRecord::Schema.define do execute "ALTER TABLE lessons_students ADD CONSTRAINT student_id_fk FOREIGN KEY (#{quote_column_name 'student_id'}) REFERENCES #{quote_table_name 'students'} (#{quote_column_name 'id'})" end + + create_table :overloaded_types, force: true do |t| + t.float :overloaded_float, default: 500 + t.float :unoverloaded_float + t.string :overloaded_string_with_limit, limit: 255 + end end Course.connection.create_table :courses, force: true do |t| diff --git a/activesupport/lib/active_support/i18n_railtie.rb b/activesupport/lib/active_support/i18n_railtie.rb index 23cd6716e3..a45f009859 100644 --- a/activesupport/lib/active_support/i18n_railtie.rb +++ b/activesupport/lib/active_support/i18n_railtie.rb @@ -1,4 +1,3 @@ -require "active_support" require "active_support/file_update_checker" require "active_support/core_ext/array/wrap" diff --git a/activesupport/lib/active_support/railtie.rb b/activesupport/lib/active_support/railtie.rb index 133aa6a054..6d09ad4bd4 100644 --- a/activesupport/lib/active_support/railtie.rb +++ b/activesupport/lib/active_support/railtie.rb @@ -1,4 +1,3 @@ -require "active_support" require "active_support/i18n_railtie" module ActiveSupport diff --git a/activesupport/lib/active_support/time.rb b/activesupport/lib/active_support/time.rb index 92a593965e..ea2d3391bd 100644 --- a/activesupport/lib/active_support/time.rb +++ b/activesupport/lib/active_support/time.rb @@ -1,5 +1,3 @@ -require 'active_support' - module ActiveSupport autoload :Duration, 'active_support/duration' autoload :TimeWithZone, 'active_support/time_with_zone' diff --git a/railties/lib/rails/generators/actions.rb b/railties/lib/rails/generators/actions.rb index abcbe7068d..a239874df0 100644 --- a/railties/lib/rails/generators/actions.rb +++ b/railties/lib/rails/generators/actions.rb @@ -258,13 +258,12 @@ module Rails # Surround string with single quotes if there is no quotes. # Otherwise fall back to double quotes def quote(str) - if str.class == String && str.scan("'").size > 0 + if str.include?("'") str.inspect else "'#{str}'" end end - end end end diff --git a/railties/test/generators/actions_test.rb b/railties/test/generators/actions_test.rb index d7be17ed36..6d6de0fb52 100644 --- a/railties/test/generators/actions_test.rb +++ b/railties/test/generators/actions_test.rb @@ -78,6 +78,14 @@ class ActionsTest < Rails::Generators::TestCase assert_file 'Gemfile', /gem 'rspec', github: 'dchelimsky\/rspec', tag: '1\.2\.9\.rc1'/ end + def test_gem_falls_back_to_inspect_if_string_contains_single_quote + run_generator + + action :gem, 'rspec', ">=2.0'0" + + assert_file 'Gemfile', /^gem 'rspec', ">=2\.0'0"$/ + end + def test_gem_group_should_wrap_gems_in_a_group run_generator |