diff options
Diffstat (limited to 'activerecord')
165 files changed, 1799 insertions, 767 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index e1cace7d88..6cdf9ebd9b 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,103 @@ +* PostgreSQL, no longer disables user triggers if system triggers can't be + disabled. Disabling user triggers does not fulfill what the method promises. + Rails currently requires superuser privileges for this method. + + If you absolutely rely on this behavior, consider patching + `disable_referential_integrity`. + + *Yves Senn* + +* Restore aborted transaction state when `disable_referential_integrity` fails + due to missing permissions. + + *Toby Ovod-Everett*, *Yves Senn* + +* PostgreSQL, print warning message if `disable_referential_integrity` fails + due to missing permissions. + + *Andrey Nering*, *Yves Senn* + +* Allow `:limit` option for MySQL bigint primary key support. + + Example: + + create_table :foos, id: :primary_key, limit: 8 do |t| + end + + # or + + create_table :foos, id: false do |t| + t.primary_key :id, limit: 8 + end + + *Ryuta Kamizono* + +* `belongs_to` will now trigger a validation error by default if the association is not present. + You can turn this off on a per-association basis with `optional: true`. + (Note this new default only applies to new Rails apps that will be generated with + `config.active_record.belongs_to_required_by_default = true` in initializer.) + + *Josef Šimánek* + +* Fixed ActiveRecord::Relation#becomes! and changed_attributes issues for type + column. + + Fixes #17139. + + *Miklos Fazekas* + +* Format the time string according to the precision of the time column. + + *Ryuta Kamizono* + +* Allow `:precision` option for time type columns. + + *Ryuta Kamizono* + +* Add `ActiveRecord::Base.suppress` to prevent the receiver from being saved + during the given block. + + For example, here's a pattern of creating notifications when new comments + are posted. (The notification may in turn trigger an email, a push + notification, or just appear in the UI somewhere): + + class Comment < ActiveRecord::Base + belongs_to :commentable, polymorphic: true + after_create -> { Notification.create! comment: self, + recipients: commentable.recipients } + end + + That's what you want the bulk of the time. New comment creates a new + Notification. But there may well be off cases, like copying a commentable + and its comments, where you don't want that. So you'd have a concern + something like this: + + module Copyable + def copy_to(destination) + Notification.suppress do + # Copy logic that creates new comments that we do not want triggering + # notifications. + end + end + end + + *Michael Ryan* + +* `:time` option added for `#touch`. + + Fixes #18905. + + *Hyonjee Joo* + +* Deprecated passing of `start` value to `find_in_batches` and `find_each` + in favour of `begin_at` value. + + *Vipul A M* + +* Add `foreign_key_exists?` method. + + *Tõnis Simo* + * Use SQL COUNT and LIMIT 1 queries for `none?` and `one?` methods if no block or limit is given, instead of loading the entire collection to memory. This applies to relations (e.g. `User.all`) as well as associations (e.g. `account.users`) @@ -20,10 +120,6 @@ *Eugene Gilburg* -* Allow `:precision` option for time type columns. - - *Ryuta Kamizono* - * Have `enum` perform type casting consistently with the rest of Active Record, such as `where`. @@ -32,9 +128,9 @@ * `scoping` no longer pollutes the current scope of sibling classes when using STI. e.x. - StiOne.none.scoping do - StiTwo.all - end + StiOne.none.scoping do + StiTwo.all + end Fixes #18806. diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index d9d47c3d99..ef14e065ae 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -62,6 +62,7 @@ module ActiveRecord autoload :Serialization autoload :StatementCache autoload :Store + autoload :Suppressor autoload :TableMetadata autoload :Timestamp autoload :Transactions diff --git a/activerecord/lib/active_record/aggregations.rb b/activerecord/lib/active_record/aggregations.rb index 39077aea7e..3d497a30fb 100644 --- a/activerecord/lib/active_record/aggregations.rb +++ b/activerecord/lib/active_record/aggregations.rb @@ -3,10 +3,27 @@ module ActiveRecord module Aggregations # :nodoc: extend ActiveSupport::Concern - def clear_aggregation_cache #:nodoc: - @aggregation_cache.clear if persisted? + def initialize_dup(*) # :nodoc: + @aggregation_cache = {} + super end + def reload(*) # :nodoc: + clear_aggregation_cache + super + end + + private + + def clear_aggregation_cache # :nodoc: + @aggregation_cache.clear if persisted? + end + + def init_internals # :nodoc: + @aggregation_cache = {} + super + end + # Active Record implements aggregation through a macro-like class method called +composed_of+ # for representing attributes as value objects. It expresses relationships like "Account [is] # composed of Money [among other things]" or "Person [is] composed of [an] address". Each call @@ -87,11 +104,6 @@ module ActiveRecord # customer.address_city = "Copenhagen" # customer.address # => Address.new("Hyancintvej", "Copenhagen") # - # customer.address_street = "Vesterbrogade" - # customer.address # => Address.new("Hyancintvej", "Copenhagen") - # customer.clear_aggregation_cache - # customer.address # => Address.new("Vesterbrogade", "Copenhagen") - # # customer.address = Address.new("May Street", "Chicago") # customer.address_street # => "May Street" # customer.address_city # => "Chicago" diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 5a3b4f0c40..cecb2dbd0b 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -145,20 +145,14 @@ module ActiveRecord autoload :AliasTracker, 'active_record/associations/alias_tracker' end - # Clears out the association cache. - def clear_association_cache #:nodoc: - @association_cache.clear if persisted? - end - - # :nodoc: - attr_reader :association_cache - # Returns the association instance for the given name, instantiating it if it doesn't already exist def association(name) #:nodoc: association = association_instance_get(name) if association.nil? - raise AssociationNotFoundError.new(self, name) unless reflection = self.class._reflect_on_association(name) + unless reflection = self.class._reflect_on_association(name) + raise AssociationNotFoundError.new(self, name) + end association = reflection.association_class.new(self, reflection) association_instance_set(name, association) end @@ -166,7 +160,31 @@ module ActiveRecord association end + def association_cached?(name) # :nodoc + @association_cache.key?(name) + end + + def initialize_dup(*) # :nodoc: + @association_cache = {} + super + end + + def reload(*) # :nodoc: + clear_association_cache + super + end + private + # Clears out the association cache. + def clear_association_cache # :nodoc: + @association_cache.clear if persisted? + end + + def init_internals # :nodoc: + @association_cache = {} + super + end + # Returns the specified association instance if it responds to :loaded?, nil otherwise. def association_instance_get(name) @association_cache[name] @@ -1001,7 +1019,7 @@ module ActiveRecord # can affect what it does. # # Note that <tt>:dependent</tt> option is ignored for +has_one+ <tt>:through</tt> associations. - # + # # === Delete or destroy? # # +has_many+ and +has_and_belongs_to_many+ associations have the methods <tt>destroy</tt>, @@ -1386,7 +1404,7 @@ module ActiveRecord # has_one :last_comment, -> { order 'posted_on' }, class_name: "Comment" # has_one :project_manager, -> { where role: 'project_manager' }, class_name: "Person" # has_one :attachment, as: :attachable - # has_one :boss, readonly: :true + # has_one :boss, -> { readonly } # has_one :club, through: :membership # has_one :primary_address, -> { where primary: true }, through: :addressables, source: :addressable # has_one :credit_card, required: true @@ -1438,7 +1456,7 @@ module ActiveRecord # when you access the associated object. # # Scope examples: - # belongs_to :user, -> { where(id: 2) } + # belongs_to :firm, -> { where(id: 2) } # belongs_to :user, -> { joins(:friends) } # belongs_to :level, ->(level) { where("game_level > ?", level.current) } # @@ -1502,10 +1520,14 @@ module ActiveRecord # object that is the inverse of this <tt>belongs_to</tt> association. Does not work in # combination with the <tt>:polymorphic</tt> options. # See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail. + # [:optional] + # When set to +true+, the association will not have its presence validated. # [:required] # When set to +true+, the association will also have its presence validated. # This will validate the association itself, not the id. You can use # +:inverse_of+ to avoid an extra query during validation. + # NOTE: <tt>required</tt> is set to <tt>true</tt> by default and is deprecated. If + # you don't want to have association presence validated, use <tt>optional: true</tt>. # # Option examples: # belongs_to :firm, foreign_key: "client_of" @@ -1514,11 +1536,11 @@ module ActiveRecord # belongs_to :valid_coupon, ->(o) { where "discounts > ?", o.payments_count }, # class_name: "Coupon", foreign_key: "coupon_id" # belongs_to :attachable, polymorphic: true - # belongs_to :project, readonly: true + # belongs_to :project, -> { readonly } # belongs_to :post, counter_cache: true - # belongs_to :company, touch: true + # belongs_to :comment, touch: true # belongs_to :company, touch: :employees_last_updated_at - # belongs_to :company, required: true + # belongs_to :user, optional: true def belongs_to(name, scope = nil, options = {}) reflection = Builder::BelongsTo.build(self, name, scope, options) Reflection.add_reflection self, name, reflection diff --git a/activerecord/lib/active_record/associations/builder/belongs_to.rb b/activerecord/lib/active_record/associations/builder/belongs_to.rb index d0ad57f9c6..ec135d49b7 100644 --- a/activerecord/lib/active_record/associations/builder/belongs_to.rb +++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb @@ -5,7 +5,7 @@ module ActiveRecord::Associations::Builder end def self.valid_options(options) - super + [:foreign_type, :polymorphic, :touch, :counter_cache] + super + [:foreign_type, :polymorphic, :touch, :counter_cache, :optional] end def self.valid_dependent_options @@ -110,5 +110,23 @@ module ActiveRecord::Associations::Builder name = reflection.name model.after_destroy lambda { |o| o.association(name).handle_dependency } end + + def self.define_validations(model, reflection) + if reflection.options.key?(:required) + reflection.options[:optional] = !reflection.options.delete(:required) + end + + if reflection.options[:optional].nil? + required = model.belongs_to_required_by_default + else + required = !reflection.options[:optional] + end + + super + + if required + model.validates_presence_of reflection.name, message: :required + end + end end end diff --git a/activerecord/lib/active_record/associations/builder/has_one.rb b/activerecord/lib/active_record/associations/builder/has_one.rb index 64e9e6b334..a272d3c781 100644 --- a/activerecord/lib/active_record/associations/builder/has_one.rb +++ b/activerecord/lib/active_record/associations/builder/has_one.rb @@ -17,5 +17,12 @@ module ActiveRecord::Associations::Builder def self.add_destroy_callbacks(model, reflection) super unless reflection.options[:through] end + + def self.define_validations(model, reflection) + super + if reflection.options[:required] + model.validates_presence_of reflection.name, message: :required + end + end end end diff --git a/activerecord/lib/active_record/associations/builder/singular_association.rb b/activerecord/lib/active_record/associations/builder/singular_association.rb index f6274c027e..42542f188e 100644 --- a/activerecord/lib/active_record/associations/builder/singular_association.rb +++ b/activerecord/lib/active_record/associations/builder/singular_association.rb @@ -27,12 +27,5 @@ module ActiveRecord::Associations::Builder end CODE end - - def self.define_validations(model, reflection) - super - if reflection.options[:required] - model.validates_presence_of reflection.name, message: :required - end - end end end diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index 33e4516bd1..82a7c27799 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -63,7 +63,7 @@ module ActiveRecord def ids_writer(ids) pk_type = reflection.primary_key_type ids = Array(ids).reject(&:blank?) - ids.map! { |i| pk_type.type_cast_from_user(i) } + ids.map! { |i| pk_type.cast(i) } replace(klass.find(ids).index_by(&:id).values_at(*ids)) end @@ -129,6 +129,16 @@ module ActiveRecord first_nth_or_last(:last, *args) end + def take + if loaded? + target.first + else + scope.take.tap do |record| + set_inverse_instance record if record.is_a? ActiveRecord::Base + end + end + end + def build(attributes = {}, &block) if attributes.is_a?(Array) attributes.collect { |attr| build(attr, &block) } diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb index c22dc6e11e..87e80e88b2 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -227,6 +227,10 @@ module ActiveRecord @association.last(*args) end + def take + @association.take + end + # Returns a new object of the collection type that has been instantiated # with +attributes+ and linked to this object, but have not yet been saved. # You can pass an array of attributes hashes, this will return an array diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb index fcf06323e6..81eb5136a1 100644 --- a/activerecord/lib/active_record/associations/join_dependency.rb +++ b/activerecord/lib/active_record/associations/join_dependency.rb @@ -239,12 +239,10 @@ module ActiveRecord if node.reflection.collection? other = ar_parent.association(node.reflection.name) other.loaded! - else - if ar_parent.association_cache.key?(node.reflection.name) - model = ar_parent.association(node.reflection.name).target - construct(model, node, row, rs, seen, model_cache, aliases) - next - end + elsif ar_parent.association_cached?(node.reflection.name) + model = ar_parent.association(node.reflection.name).target + construct(model, node, row, rs, seen, model_cache, aliases) + next end key = aliases.column_alias(node, node.primary_key) diff --git a/activerecord/lib/active_record/attribute.rb b/activerecord/lib/active_record/attribute.rb index 91886f1324..73dd3fa041 100644 --- a/activerecord/lib/active_record/attribute.rb +++ b/activerecord/lib/active_record/attribute.rb @@ -43,7 +43,7 @@ module ActiveRecord end def value_for_database - type.type_cast_for_database(value) + type.serialize(value) end def changed_from?(old_value) @@ -108,13 +108,13 @@ module ActiveRecord class FromDatabase < Attribute # :nodoc: def type_cast(value) - type.type_cast_from_database(value) + type.deserialize(value) end end class FromUser < Attribute # :nodoc: def type_cast(value) - type.type_cast_from_user(value) + type.cast(value) end def came_from_user? 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 90c36e4b02..f9beb43e4b 100644 --- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb +++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb @@ -2,13 +2,13 @@ module ActiveRecord module AttributeMethods module TimeZoneConversion class TimeZoneConverter < DelegateClass(Type::Value) # :nodoc: - def type_cast_from_database(value) + def deserialize(value) convert_time_to_time_zone(super) end - def type_cast_from_user(value) + def cast(value) if value.is_a?(Array) - value.map { |v| type_cast_from_user(v) } + value.map { |v| cast(v) } elsif value.is_a?(Hash) set_time_zone_without_conversion(super) elsif value.respond_to?(:in_time_zone) diff --git a/activerecord/lib/active_record/attributes.rb b/activerecord/lib/active_record/attributes.rb index 2c475f3cda..c8979a60d7 100644 --- a/activerecord/lib/active_record/attributes.rb +++ b/activerecord/lib/active_record/attributes.rb @@ -102,14 +102,14 @@ module ActiveRecord # # Users may also define their own custom types, as long as they respond # to the methods defined on the value type. The method - # +type_cast_from_database+ or +type_cast_from_user+ will be called on + # +deserialize+ or +cast+ will be called on # your type object, with raw input from the database or from your # controllers. See ActiveRecord::Type::Value for the expected API. It is # recommended that your type objects inherit from an existing type, or # from ActiveRecord::Type::Value # # class MoneyType < ActiveRecord::Type::Integer - # def type_cast_from_user(value) + # def cast(value) # if value.include?('$') # price_in_dollars = value.gsub(/\$/, '').to_f # super(price_in_dollars * 100) @@ -119,21 +119,27 @@ module ActiveRecord # end # end # + # # config/initializers/types.rb + # ActiveRecord::Type.register(:money, MoneyType) + # + # # /app/models/store_listing.rb # class StoreListing < ActiveRecord::Base - # attribute :price_in_cents, MoneyType.new + # attribute :price_in_cents, :money # end # # store_listing = StoreListing.new(price_in_cents: '$10.00') # store_listing.price_in_cents # => 1000 # # For more details on creating custom types, see the documentation for - # ActiveRecord::Type::Value. + # ActiveRecord::Type::Value. For more details on registering your types + # to be referenced by a symbol, see ActiveRecord::Type.register. You can + # also pass a type object directly, in place of a symbol. # # ==== Querying # # When ActiveRecord::QueryMethods#where is called, it will # use the type defined by the model class to convert the value to SQL, - # calling +type_cast_for_database+ on your type object. For example: + # calling +serialize+ on your type object. For example: # # class Money < Struct.new(:amount, :currency) # end @@ -143,18 +149,20 @@ module ActiveRecord # @currency_converter = currency_converter # end # - # # value will be the result of +type_cast_from_database+ or - # # +type_cast_from_user+. Assumed to be in instance of +Money+ in + # # value will be the result of +deserialize+ or + # # +cast+. Assumed to be in instance of +Money+ in # # this case. - # def type_cast_for_database(value) + # def serialize(value) # value_in_bitcoins = @currency_converter.convert_to_bitcoins(value) # value_in_bitcoins.amount # end # end # + # ActiveRecord::Type.register(:money, MoneyType) + # # class Product < ActiveRecord::Base # currency_converter = ConversionRatesFromTheInternet.new - # attribute :price_in_bitcoins, MoneyType.new(currency_converter) + # attribute :price_in_bitcoins, :money, currency_converter # end # # Product.where(price_in_bitcoins: Money.new(5, "USD")) @@ -195,7 +203,7 @@ module ActiveRecord # Otherwise, the default will be +nil+. # # +user_provided_default+ Whether the default value should be cast using - # +type_cast_from_user+ or +type_cast_from_database+. + # +cast+ or +deserialize+. def define_attribute( name, cast_type, @@ -210,7 +218,7 @@ module ActiveRecord super attributes_to_define_after_schema_loads.each do |name, (type, options)| if type.is_a?(Symbol) - type = connection.type_for_attribute_options(type, **options.except(:default)) + type = ActiveRecord::Type.lookup(type, **options.except(:default)) end define_attribute(name, type, **options.slice(:default)) diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 100d3780f6..cc03e37a12 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -313,6 +313,7 @@ module ActiveRecord #:nodoc: include Serialization include Store include SecureToken + include Suppressor end ActiveSupport.run_load_hooks(:active_record, Base) diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb index 947e11c7bf..d2840b9498 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb @@ -52,7 +52,7 @@ module ActiveRecord def type_cast_from_column(column, value) # :nodoc: if column type = lookup_cast_type_from_column(column) - type.type_cast_for_database(value) + type.serialize(value) else value end @@ -103,7 +103,7 @@ module ActiveRecord end def quote_default_expression(value, column) #:nodoc: - value = lookup_cast_type(column.sql_type).type_cast_for_database(value) + value = lookup_cast_type(column.sql_type).serialize(value) quote(value) end @@ -144,29 +144,8 @@ module ActiveRecord binds.map(&:value_for_database) end - def type_for_attribute_options(type_name, **options) - klass = type_classes_with_standard_constructor.fetch(type_name, Type::Value) - klass.new(**options) - end - private - def type_classes_with_standard_constructor - { - big_integer: Type::BigInteger, - binary: Type::Binary, - boolean: Type::Boolean, - date: Type::Date, - date_time: Type::DateTime, - decimal: Type::Decimal, - float: Type::Float, - integer: Type::Integer, - string: Type::String, - text: Type::Text, - time: Type::Time, - } - end - def types_which_need_no_typecasting [nil, Numeric, String] end 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 bc8fa9b6cf..f754df93b6 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb @@ -18,6 +18,9 @@ module ActiveRecord "ADD #{accept(o)}" end + delegate :quote_column_name, :quote_table_name, :quote_default_expression, :type_to_sql, to: :@conn + private :quote_column_name, :quote_table_name, :quote_default_expression, :type_to_sql + private def visit_AlterTable(o) @@ -70,18 +73,6 @@ module ActiveRecord column_options end - def quote_column_name(name) - @conn.quote_column_name name - end - - def quote_table_name(name) - @conn.quote_table_name name - end - - def type_to_sql(type, limit, precision, scale) - @conn.type_to_sql type.to_sym, limit, precision, scale - end - def add_column_options!(sql, options) sql << " DEFAULT #{quote_default_expression(options[:default], options[:column])}" if options_include_default?(options) # must explicitly check for :null to allow change_column to work on migrations @@ -97,10 +88,6 @@ module ActiveRecord sql end - def quote_default_expression(value, column) - @conn.quote_default_expression(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 8cd4b8e5b2..0acc815d51 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -50,6 +50,14 @@ module ActiveRecord options[:primary_key] != default_primary_key end + def defined_for?(options_or_to_table = {}) + if options_or_to_table.is_a?(Hash) + options_or_to_table.all? {|key, value| options[key].to_s == value.to_s } + else + to_table == options_or_to_table.to_s + end + end + private def default_primary_key "id" @@ -134,6 +142,41 @@ module ActiveRecord end end + module ColumnMethods + # Appends a primary key definition to the table definition. + # Can be called multiple times, but this is probably not a good idea. + def primary_key(name, type = :primary_key, **options) + column(name, type, options.merge(primary_key: true)) + end + + # Appends a column or columns of a specified type. + # + # t.string(:goat) + # t.string(:goat, :sheep) + # + # See TableDefinition#column + [ + :bigint, + :binary, + :boolean, + :date, + :datetime, + :decimal, + :float, + :integer, + :string, + :text, + :time, + :timestamp, + ].each do |column_type| + module_eval <<-CODE, __FILE__, __LINE__ + 1 + def #{column_type}(*args, **options) + args.each { |name| column(name, :#{column_type}, options) } + end + CODE + end + end + # Represents the schema of an SQL table in an abstract way. This class # provides methods for manipulating the schema representation. # @@ -155,6 +198,8 @@ module ActiveRecord # The table definitions # The Columns are stored as a ColumnDefinition in the +columns+ attribute. class TableDefinition + include ColumnMethods + # An array of ColumnDefinition objects, representing the column changes # that have been defined. attr_accessor :indexes @@ -173,12 +218,6 @@ module ActiveRecord def columns; @columns_hash.values; end - # Appends a primary key definition to the table definition. - # Can be called multiple times, but this is probably not a good idea. - def primary_key(name, type = :primary_key, options = {}) - column(name, type, options.merge(:primary_key => true)) - end - # Returns a ColumnDefinition for the column with name +name+. def [](name) @columns_hash[name.to_s] @@ -335,14 +374,6 @@ module ActiveRecord @columns_hash.delete name.to_s end - [:string, :text, :integer, :bigint, :float, :decimal, :datetime, :timestamp, :time, :date, :binary, :boolean].each do |column_type| - define_method column_type do |*args| - options = args.extract_options! - column_names = args - column_names.each { |name| column(name, column_type, options) } - end - end - # Adds index options to the indexes hash, keyed by column name # This is primarily used to track indexes that need to be created after the table # @@ -454,6 +485,7 @@ module ActiveRecord # Available transformations are: # # change_table :table do |t| + # t.primary_key # t.column # t.index # t.rename_index @@ -466,6 +498,7 @@ module ActiveRecord # t.string # t.text # t.integer + # t.bigint # t.float # t.decimal # t.datetime @@ -482,6 +515,8 @@ module ActiveRecord # end # class Table + include ColumnMethods + attr_reader :name def initialize(table_name, base) @@ -632,25 +667,14 @@ module ActiveRecord end alias :remove_belongs_to :remove_references - # Adds a column or columns of a specified type. - # - # t.string(:goat) - # t.string(:goat, :sheep) - # - # See SchemaStatements#add_column - [:string, :text, :integer, :float, :decimal, :datetime, :timestamp, :time, :date, :binary, :boolean].each do |column_type| - define_method column_type do |*args| - options = args.extract_options! - args.each do |column_name| - @base.add_column(name, column_name, column_type, options) - end - end - end - def foreign_key(*args) # :nodoc: @base.add_foreign_key(name, *args) end + def foreign_key_exists?(*args) # :nodoc: + @base.foreign_key_exists?(name, *args) + end + private def native @base.native_database_types diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb index 932aaf7aa7..af7ef7cbaa 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb @@ -47,7 +47,7 @@ module ActiveRecord def schema_default(column) type = lookup_cast_type_from_column(column) - default = type.type_cast_from_database(column.default) + default = type.deserialize(column.default) unless default.nil? type.type_cast_for_schema(default) end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb index c9180f64db..d42f9a894b 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -667,7 +667,7 @@ module ActiveRecord # remove_reference(:products, :user, index: true, foreign_key: true) # def remove_reference(table_name, ref_name, options = {}) - remove_foreign_key table_name, ref_name if options[:foreign_key] + remove_foreign_key table_name, ref_name.to_s.pluralize if options[:foreign_key] remove_column(table_name, "#{ref_name}_id") remove_column(table_name, "#{ref_name}_type") if options[:polymorphic] @@ -757,21 +757,7 @@ module ActiveRecord def remove_foreign_key(from_table, options_or_to_table = {}) return unless supports_foreign_keys? - if options_or_to_table.is_a?(Hash) - options = options_or_to_table - else - options = { column: foreign_key_column_for(options_or_to_table) } - end - - fk_name_to_delete = options.fetch(:name) do - fk_to_delete = foreign_keys(from_table).detect {|fk| fk.column == options[:column].to_s } - - if fk_to_delete - fk_to_delete.name - else - raise ArgumentError, "Table '#{from_table}' has no foreign key on column '#{options[:column]}'" - end - end + fk_name_to_delete = foreign_key_for!(from_table, options_or_to_table).name at = create_alter_table from_table at.drop_foreign_key fk_name_to_delete @@ -779,6 +765,31 @@ module ActiveRecord execute schema_creation.accept(at) end + # Checks to see if a foreign key exists on a table for a given foreign key definition. + # + # # Check a foreign key exists + # foreign_key_exists?(:accounts, :branches) + # + # # Check a foreign key on a specified column exists + # foreign_key_exists?(:accounts, column: :owner_id) + # + # # Check a foreign key with a custom name exists + # foreign_key_exists?(:accounts, name: "special_fk_name") + # + def foreign_key_exists?(from_table, options_or_to_table = {}) + foreign_key_for(from_table, options_or_to_table).present? + end + + def foreign_key_for(from_table, options_or_to_table = {}) # :nodoc: + return unless supports_foreign_keys? + foreign_keys(from_table).detect {|fk| fk.defined_for? options_or_to_table } + end + + def foreign_key_for!(from_table, options_or_to_table = {}) # :nodoc: + foreign_key_for(from_table, options_or_to_table) or \ + raise ArgumentError, "Table '#{from_table}' has no foreign key for #{options_or_to_table}" + end + def foreign_key_column_for(table_name) # :nodoc: "#{table_name.to_s.singularize}_id" end 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 0a9e599c3c..98009da2bd 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -6,13 +6,31 @@ module ActiveRecord class AbstractMysqlAdapter < AbstractAdapter include Savepoints - class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition - def primary_key(name, type = :primary_key, options = {}) - options[:auto_increment] ||= type == :bigint + module ColumnMethods + def primary_key(name, type = :primary_key, **options) + options[:auto_increment] = true if type == :bigint super end end + class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition + include ColumnMethods + + def new_column_definition(name, type, options) # :nodoc: + column = super + case column.type + when :primary_key + column.type = :integer + column.auto_increment = true + end + column + end + end + + class Table < ActiveRecord::ConnectionAdapters::Table + include ColumnMethods + end + class SchemaCreation < AbstractAdapter::SchemaCreation def visit_AddColumn(o) add_column_position!(super, column_options(o)) @@ -57,6 +75,10 @@ module ActiveRecord end end + def update_table_definition(table_name, base) # :nodoc: + Table.new(table_name, base) + end + def schema_creation SchemaCreation.new self end @@ -75,7 +97,8 @@ module ActiveRecord def prepare_column_options(column) spec = super - spec.delete(:precision) if column.type == :datetime && column.precision == 0 + spec.delete(:precision) if /time/ === column.sql_type && column.precision == 0 + spec.delete(:limit) if :boolean === column.type spec end @@ -733,7 +756,7 @@ module ActiveRecord end def extract_precision(sql_type) - if /datetime/ === sql_type + if /time/ === sql_type super || 0 else super @@ -923,7 +946,7 @@ module ActiveRecord end class MysqlString < Type::String # :nodoc: - def type_cast_for_database(value) + def serialize(value) case value when true then "1" when false then "0" @@ -942,9 +965,8 @@ module ActiveRecord end end - def type_classes_with_standard_constructor - super.merge(string: MysqlString) - end + ActiveRecord::Type.register(:string, MysqlString, adapter: :mysql) + ActiveRecord::Type.register(:string, MysqlString, adapter: :mysql2) end end end diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index 16f50fc594..64985ee933 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -328,8 +328,8 @@ module ActiveRecord def initialize_type_map(m) # :nodoc: super - m.register_type %r(datetime)i, Fields::DateTime.new - m.register_type %r(time)i, Fields::Time.new + register_class_with_precision m, %r(datetime)i, Fields::DateTime + register_class_with_precision m, %r(time)i, Fields::Time end def exec_without_stmt(sql, name = 'SQL') # :nodoc: diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb index 2608a2abab..fb4e0de2a8 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb @@ -25,22 +25,22 @@ module ActiveRecord @delimiter = delimiter end - def type_cast_from_database(value) + def deserialize(value) if value.is_a?(::String) - type_cast_array(parse_pg_array(value), :type_cast_from_database) + type_cast_array(parse_pg_array(value), :deserialize) else super end end - def type_cast_from_user(value) + def cast(value) if value.is_a?(::String) value = parse_pg_array(value) end - type_cast_array(value, :type_cast_from_user) + type_cast_array(value, :cast) end - def type_cast_for_database(value) + def serialize(value) if value.is_a?(::Array) cast_value_for_database(value) else @@ -69,7 +69,7 @@ module ActiveRecord casted_values = value.map { |item| cast_value_for_database(item) } "{#{casted_values.join(delimiter)}}" else - quote_and_escape(subtype.type_cast_for_database(value)) + quote_and_escape(subtype.serialize(value)) end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb index 1dbb40ca1d..ea0fa2517f 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb @@ -7,7 +7,7 @@ module ActiveRecord :bit end - def type_cast(value) + def cast(value) if ::String === value case value when /^0x/i @@ -20,7 +20,7 @@ module ActiveRecord end end - def type_cast_for_database(value) + def serialize(value) Data.new(super) if value end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb index 6bd1b8ecae..8f9d6e7f9b 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb @@ -3,7 +3,7 @@ module ActiveRecord module PostgreSQL module OID # :nodoc: class Bytea < Type::Binary # :nodoc: - def type_cast_from_database(value) + def deserialize(value) return if value.nil? return value.to_s if value.is_a?(Type::Binary::Data) PGconn.unescape_bytea(super) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb index 222f10fa8f..eeccb09bdf 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb @@ -18,7 +18,7 @@ module ActiveRecord end end - def type_cast_for_database(value) + def serialize(value) if IPAddr === value "#{value}/#{value.instance_variable_get(:@mask_addr).to_s(2).count('1')}" else diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb index 77d5038efd..b3b610a5f6 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb @@ -7,7 +7,7 @@ module ActiveRecord :enum end - def type_cast(value) + def cast(value) value.to_s end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb index b46e50c865..9270fc9f21 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb @@ -9,7 +9,7 @@ module ActiveRecord :hstore end - def type_cast_from_database(value) + def deserialize(value) if value.is_a?(::String) ::Hash[value.scan(HstorePair).map { |k, v| v = v.upcase == 'NULL' ? nil : v.gsub(/\A"(.*)"\Z/m,'\1').gsub(/\\(.)/, '\1') @@ -21,7 +21,7 @@ module ActiveRecord end end - def type_cast_for_database(value) + def serialize(value) if value.is_a?(::Hash) value.map { |k, v| "#{escape_hstore(k)}=>#{escape_hstore(v)}" }.join(', ') else diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb index 13dd037314..8e1256baad 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb @@ -9,19 +9,19 @@ module ActiveRecord :json end - def type_cast_from_database(value) + def deserialize(value) if value.is_a?(::String) ::ActiveSupport::JSON.decode(value) rescue nil else - super + value end end - def type_cast_for_database(value) + def serialize(value) if value.is_a?(::Array) || value.is_a?(::Hash) ::ActiveSupport::JSON.encode(value) else - super + value end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb index 380c50fc14..afc9383f91 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb @@ -13,7 +13,7 @@ module ActiveRecord # the comparison here. Therefore, we need to parse and re-dump the # raw value here to ensure the insignificant whitespaces are # consistent with our encoder's output. - raw_old_value = type_cast_for_database(type_cast_from_database(raw_old_value)) + raw_old_value = serialize(deserialize(raw_old_value)) super(raw_old_value, new_value) end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb index 4084725ed7..bf565bcf47 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb @@ -9,13 +9,13 @@ module ActiveRecord :point end - def type_cast(value) + def cast(value) case value when ::String if value[0] == '(' && value[-1] == ')' value = value[1...-1] end - type_cast(value.split(',')) + cast(value.split(',')) when ::Array value.map { |v| Float(v) } else @@ -23,7 +23,7 @@ module ActiveRecord end end - def type_cast_for_database(value) + def serialize(value) if value.is_a?(::Array) "(#{number_for_point(value[0])},#{number_for_point(value[1])})" else diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb index 9d3633d109..fc201f8fb9 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb @@ -30,7 +30,7 @@ module ActiveRecord ::Range.new(from, to, extracted[:exclude_end]) end - def type_cast_for_database(value) + def serialize(value) if value.is_a?(::Range) from = type_cast_single_for_database(value.begin) to = type_cast_single_for_database(value.end) @@ -49,11 +49,11 @@ module ActiveRecord private def type_cast_single(value) - infinity?(value) ? value : @subtype.type_cast_from_database(value) + infinity?(value) ? value : @subtype.deserialize(value) end def type_cast_single_for_database(value) - infinity?(value) ? '' : @subtype.type_cast_for_database(value) + infinity?(value) ? '' : @subtype.serialize(value) end def extract_bounds(value) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb index 97b4fd3d08..5e839228e9 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb @@ -5,13 +5,13 @@ module ActiveRecord class Uuid < Type::Value # :nodoc: ACCEPTABLE_UUID = %r{\A\{?([a-fA-F0-9]{4}-?){8}\}?\z}x - alias_method :type_cast_for_database, :type_cast_from_database + alias_method :serialize, :deserialize def type :uuid end - def type_cast(value) + def cast(value) value.to_s[ACCEPTABLE_UUID, 0] end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/vector.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/vector.rb index de4187b028..b26e876b54 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/vector.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/vector.rb @@ -16,7 +16,7 @@ module ActiveRecord # FIXME: this should probably split on +delim+ and use +subtype+ # to cast the values. Unfortunately, the current Rails behavior # is to just return the string. - def type_cast(value) + def cast(value) value end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/xml.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/xml.rb index 334af7c598..d40d837cee 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/xml.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/xml.rb @@ -7,7 +7,7 @@ module ActiveRecord :xml end - def type_cast_for_database(value) + def serialize(value) return unless value Data.new(super) end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb index b8d0e26f85..b7755c4593 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb @@ -67,23 +67,6 @@ module ActiveRecord type_map.lookup(column.oid, column.fmod, column.sql_type) end - def type_for_attribute_options( - type_name, - array: false, - range: false, - **options - ) - if array - subtype = type_for_attribute_options(type_name, **options) - OID::Array.new(subtype) - elsif range - subtype = type_for_attribute_options(type_name, **options) - OID::Range.new(subtype) - else - super(type_name, **options) - end - end - private def _quote(value) @@ -122,27 +105,6 @@ module ActiveRecord super end end - - def type_classes_with_standard_constructor - super.merge( - bit: OID::Bit, - bit_varying: OID::BitVarying, - binary: OID::Bytea, - cidr: OID::Cidr, - date_time: OID::DateTime, - decimal: OID::Decimal, - enum: OID::Enum, - hstore: OID::Hstore, - inet: OID::Inet, - json: OID::Json, - jsonb: OID::Jsonb, - money: OID::Money, - point: OID::Point, - uuid: OID::Uuid, - vector: OID::Vector, - xml: OID::Xml, - ) - end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb b/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb index 52b307c432..c1835380f9 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb @@ -8,20 +8,39 @@ module ActiveRecord def disable_referential_integrity # :nodoc: if supports_disable_referential_integrity? + original_exception = nil + begin - execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} DISABLE TRIGGER ALL" }.join(";")) - rescue - execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} DISABLE TRIGGER USER" }.join(";")) + transaction(requires_new: true) do + execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} DISABLE TRIGGER ALL" }.join(";")) + end + rescue => e + original_exception = e end - end - yield - ensure - if supports_disable_referential_integrity? + + begin + yield + rescue ActiveRecord::InvalidForeignKey => e + warn <<-WARNING +WARNING: Rails was not able to disable referential integrity. + +This is most likely caused due to missing permissions. +Rails needs superuser privileges to disable referential integrity. + + cause: #{original_exception.try(:message)} + + WARNING + raise e + end + begin - execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} ENABLE TRIGGER ALL" }.join(";")) + transaction(require_new: true) do + execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} ENABLE TRIGGER ALL" }.join(";")) + end rescue - execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} ENABLE TRIGGER USER" }.join(";")) end + else + yield end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb index b9078d4c86..022dbdfa27 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb @@ -2,90 +2,129 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL module ColumnMethods - def xml(*args) - options = args.extract_options! - column(args[0], :xml, options) + # Defines the primary key field. + # Use of the native PostgreSQL UUID type is supported, and can be used + # by defining your tables as such: + # + # create_table :stuffs, id: :uuid do |t| + # t.string :content + # t.timestamps + # end + # + # By default, this will use the +uuid_generate_v4()+ function from the + # +uuid-ossp+ extension, which MUST be enabled on your database. To enable + # the +uuid-ossp+ extension, you can use the +enable_extension+ method in your + # migrations. To use a UUID primary key without +uuid-ossp+ enabled, you can + # set the +:default+ option to +nil+: + # + # create_table :stuffs, id: false do |t| + # t.primary_key :id, :uuid, default: nil + # t.uuid :foo_id + # t.timestamps + # end + # + # You may also pass a different UUID generation function from +uuid-ossp+ + # or another library. + # + # Note that setting the UUID primary key default value to +nil+ will + # require you to assure that you always provide a UUID value before saving + # a record (as primary keys cannot be +nil+). This might be done via the + # +SecureRandom.uuid+ method and a +before_save+ callback, for instance. + def primary_key(name, type = :primary_key, **options) + options[:default] = options.fetch(:default, 'uuid_generate_v4()') if type == :uuid + super + end + + def bigserial(*args, **options) + args.each { |name| column(name, :bigserial, options) } + end + + def bit(*args, **options) + args.each { |name| column(name, :bit, options) } end - def tsvector(*args) - options = args.extract_options! - column(args[0], :tsvector, options) + def bit_varying(*args, **options) + args.each { |name| column(name, :bit_varying, options) } end - def int4range(name, options = {}) - column(name, :int4range, options) + def cidr(*args, **options) + args.each { |name| column(name, :cidr, options) } end - def int8range(name, options = {}) - column(name, :int8range, options) + def citext(*args, **options) + args.each { |name| column(name, :citext, options) } end - def tsrange(name, options = {}) - column(name, :tsrange, options) + def daterange(*args, **options) + args.each { |name| column(name, :daterange, options) } end - def tstzrange(name, options = {}) - column(name, :tstzrange, options) + def hstore(*args, **options) + args.each { |name| column(name, :hstore, options) } end - def numrange(name, options = {}) - column(name, :numrange, options) + def inet(*args, **options) + args.each { |name| column(name, :inet, options) } end - def daterange(name, options = {}) - column(name, :daterange, options) + def int4range(*args, **options) + args.each { |name| column(name, :int4range, options) } end - def hstore(name, options = {}) - column(name, :hstore, options) + def int8range(*args, **options) + args.each { |name| column(name, :int8range, options) } end - def ltree(name, options = {}) - column(name, :ltree, options) + def json(*args, **options) + args.each { |name| column(name, :json, options) } end - def inet(name, options = {}) - column(name, :inet, options) + def jsonb(*args, **options) + args.each { |name| column(name, :jsonb, options) } end - def cidr(name, options = {}) - column(name, :cidr, options) + def ltree(*args, **options) + args.each { |name| column(name, :ltree, options) } end - def macaddr(name, options = {}) - column(name, :macaddr, options) + def macaddr(*args, **options) + args.each { |name| column(name, :macaddr, options) } end - def uuid(name, options = {}) - column(name, :uuid, options) + def money(*args, **options) + args.each { |name| column(name, :money, options) } end - def json(name, options = {}) - column(name, :json, options) + def numrange(*args, **options) + args.each { |name| column(name, :numrange, options) } end - def jsonb(name, options = {}) - column(name, :jsonb, options) + def point(*args, **options) + args.each { |name| column(name, :point, options) } end - def citext(name, options = {}) - column(name, :citext, options) + def serial(*args, **options) + args.each { |name| column(name, :serial, options) } end - def point(name, options = {}) - column(name, :point, options) + def tsrange(*args, **options) + args.each { |name| column(name, :tsrange, options) } end - def bit(name, options = {}) - column(name, :bit, options) + def tstzrange(*args, **options) + args.each { |name| column(name, :tstzrange, options) } end - def bit_varying(name, options = {}) - column(name, :bit_varying, options) + def tsvector(*args, **options) + args.each { |name| column(name, :tsvector, options) } end - def money(name, options = {}) - column(name, :money, options) + def uuid(*args, **options) + args.each { |name| column(name, :uuid, options) } + end + + def xml(*args, **options) + args.each { |name| column(name, :xml, options) } end end @@ -96,39 +135,6 @@ module ActiveRecord class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition include ColumnMethods - # Defines the primary key field. - # Use of the native PostgreSQL UUID type is supported, and can be used - # by defining your tables as such: - # - # create_table :stuffs, id: :uuid do |t| - # t.string :content - # t.timestamps - # end - # - # By default, this will use the +uuid_generate_v4()+ function from the - # +uuid-ossp+ extension, which MUST be enabled on your database. To enable - # the +uuid-ossp+ extension, you can use the +enable_extension+ method in your - # migrations. To use a UUID primary key without +uuid-ossp+ enabled, you can - # set the +:default+ option to +nil+: - # - # create_table :stuffs, id: false do |t| - # t.primary_key :id, :uuid, default: nil - # t.uuid :foo_id - # t.timestamps - # end - # - # You may also pass a different UUID generation function from +uuid-ossp+ - # or another library. - # - # Note that setting the UUID primary key default value to +nil+ will - # require you to assure that you always provide a UUID value before saving - # a record (as primary keys cannot be +nil+). This might be done via the - # +SecureRandom.uuid+ method and a +before_save+ callback, for instance. - def primary_key(name, type = :primary_key, options = {}) - options[:default] = options.fetch(:default, 'uuid_generate_v4()') if type == :uuid - super - end - def new_column_definition(name, type, options) # :nodoc: column = super column.array = options[:array] 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 503dda60f4..75b9d079bd 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -5,8 +5,7 @@ module ActiveRecord private def visit_ColumnDefinition(o) - o.sql_type = type_to_sql(o.type, o.limit, o.precision, o.scale) - o.sql_type << '[]' if o.array + o.sql_type = type_to_sql(o.type, o.limit, o.precision, o.scale, o.array) super end end @@ -407,12 +406,14 @@ module ActiveRecord def change_column(table_name, column_name, type, options = {}) clear_cache! quoted_table_name = quote_table_name(table_name) - sql_type = type_to_sql(type, options[:limit], options[:precision], options[:scale]) - sql_type << "[]" if options[:array] - sql = "ALTER TABLE #{quoted_table_name} ALTER COLUMN #{quote_column_name(column_name)} TYPE #{sql_type}" - sql << " USING #{options[:using]}" if options[:using] - if options[:cast_as] - sql << " USING CAST(#{quote_column_name(column_name)} AS #{type_to_sql(options[:cast_as], options[:limit], options[:precision], options[:scale])})" + quoted_column_name = quote_column_name(column_name) + sql_type = type_to_sql(type, options[:limit], options[:precision], options[:scale], options[:array]) + sql = "ALTER TABLE #{quoted_table_name} ALTER COLUMN #{quoted_column_name} TYPE #{sql_type}" + if options[:using] + sql << " USING #{options[:using]}" + elsif options[:cast_as] + cast_as_type = type_to_sql(options[:cast_as], options[:limit], options[:precision], options[:scale], options[:array]) + sql << " USING CAST(#{quoted_column_name} AS #{cast_as_type})" end execute sql @@ -509,8 +510,8 @@ module ActiveRecord end # Maps logical Rails types to PostgreSQL-specific data types. - def type_to_sql(type, limit = nil, precision = nil, scale = nil) - case type.to_s + def type_to_sql(type, limit = nil, precision = nil, scale = nil, array = nil) + sql = case type.to_s when 'binary' # PostgreSQL doesn't support limits on binary (bytea) columns. # The hard limit is 1Gb, because of a 32-bit size field, and TOAST. @@ -526,17 +527,18 @@ module ActiveRecord else raise(ActiveRecordError, "The limit on text can be at most 1GB - 1byte.") end when 'integer' - return 'integer' unless limit - case limit - when 1, 2; 'smallint' - when 3, 4; 'integer' - when 5..8; 'bigint' - else raise(ActiveRecordError, "No integer type has byte size #{limit}. Use a numeric with precision 0 instead.") + when 1, 2; 'smallint' + when nil, 3, 4; 'integer' + when 5..8; 'bigint' + else raise(ActiveRecordError, "No integer type has byte size #{limit}. Use a numeric with precision 0 instead.") end else - super + super(type, limit, precision, scale) end + + sql << '[]' if array && type != :primary_key + sql end # PostgreSQL requires the ORDER BY columns in the select list for distinct queries, and diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 6d7e1075d7..6d25b53b21 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -813,6 +813,25 @@ module ActiveRecord return unless coder_class coder_class.new(oid: row['oid'], name: row['typname']) end + + ActiveRecord::Type.add_modifier({ array: true }, OID::Array, adapter: :postgresql) + ActiveRecord::Type.add_modifier({ range: true }, OID::Range, adapter: :postgresql) + ActiveRecord::Type.register(:bit, OID::Bit, adapter: :postgresql) + ActiveRecord::Type.register(:bit_varying, OID::BitVarying, adapter: :postgresql) + ActiveRecord::Type.register(:binary, OID::Bytea, adapter: :postgresql) + ActiveRecord::Type.register(:cidr, OID::Cidr, adapter: :postgresql) + ActiveRecord::Type.register(:date_time, OID::DateTime, adapter: :postgresql) + ActiveRecord::Type.register(:decimal, OID::Decimal, adapter: :postgresql) + ActiveRecord::Type.register(:enum, OID::Enum, adapter: :postgresql) + ActiveRecord::Type.register(:hstore, OID::Hstore, adapter: :postgresql) + ActiveRecord::Type.register(:inet, OID::Inet, adapter: :postgresql) + ActiveRecord::Type.register(:json, OID::Json, adapter: :postgresql) + ActiveRecord::Type.register(:jsonb, OID::Jsonb, adapter: :postgresql) + ActiveRecord::Type.register(:money, OID::Money, adapter: :postgresql) + ActiveRecord::Type.register(:point, OID::Point, adapter: :postgresql) + ActiveRecord::Type.register(:uuid, OID::Uuid, adapter: :postgresql) + ActiveRecord::Type.register(:vector, OID::Vector, adapter: :postgresql) + ActiveRecord::Type.register(:xml, OID::Xml, adapter: :postgresql) end end end diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index a71f7a03ea..d41872d767 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -87,8 +87,9 @@ module ActiveRecord mattr_accessor :maintain_test_schema, instance_accessor: false + mattr_accessor :belongs_to_required_by_default, instance_accessor: false + class_attribute :default_connection_handler, instance_writer: false - class_attribute :find_by_statement_cache def self.connection_handler ActiveRecord::RuntimeRegistry.connection_handler || default_connection_handler @@ -108,10 +109,11 @@ module ActiveRecord end def initialize_find_by_cache # :nodoc: - self.find_by_statement_cache = {}.extend(Mutex_m) + @find_by_statement_cache = {}.extend(Mutex_m) end def inherited(child_class) # :nodoc: + # initialize cache at class definition for thread safety child_class.initialize_find_by_cache super end @@ -134,14 +136,13 @@ module ActiveRecord Please pass the id of the object by calling `.id` MSG end + key = primary_key - s = find_by_statement_cache[key] || find_by_statement_cache.synchronize { - find_by_statement_cache[key] ||= StatementCache.create(connection) { |params| - where(key => params.bind).limit(1) - } + statement = cached_find_by_statement(key) { |params| + where(key => params.bind).limit(1) } - record = s.execute([id], self, connection).first + record = statement.execute([id], self, connection).first unless record raise RecordNotFound, "Couldn't find #{name} with '#{primary_key}'=#{id}" end @@ -163,19 +164,16 @@ module ActiveRecord # We can't cache Post.find_by(author: david) ...yet return super unless hash.keys.all? { |k| columns_hash.has_key?(k.to_s) } - key = hash.keys + keys = hash.keys - klass = self - s = find_by_statement_cache[key] || find_by_statement_cache.synchronize { - find_by_statement_cache[key] ||= StatementCache.create(connection) { |params| - wheres = key.each_with_object({}) { |param,o| - o[param] = params.bind - } - klass.where(wheres).limit(1) + statement = cached_find_by_statement(keys) { |params| + wheres = keys.each_with_object({}) { |param, o| + o[param] = params.bind } + where(wheres).limit(1) } begin - s.execute(hash.values, self, connection).first + statement.execute(hash.values, self, connection).first rescue TypeError => e raise ActiveRecord::StatementInvalid.new(e.message, e) rescue RangeError @@ -249,6 +247,12 @@ module ActiveRecord private + def cached_find_by_statement(key, &block) # :nodoc: + @find_by_statement_cache[key] || @find_by_statement_cache.synchronize { + @find_by_statement_cache[key] ||= StatementCache.create(connection, &block) + } + end + def relation # :nodoc: relation = Relation.create(self, arel_table, predicate_builder) @@ -343,9 +347,6 @@ module ActiveRecord _run_initialize_callbacks - @aggregation_cache = {} - @association_cache = {} - @new_record = true @destroyed = false @@ -540,8 +541,6 @@ module ActiveRecord end def init_internals - @aggregation_cache = {} - @association_cache = {} @readonly = false @destroyed = false @marked_for_destruction = false diff --git a/activerecord/lib/active_record/enum.rb b/activerecord/lib/active_record/enum.rb index 3086f13f96..ea88983917 100644 --- a/activerecord/lib/active_record/enum.rb +++ b/activerecord/lib/active_record/enum.rb @@ -32,6 +32,12 @@ module ActiveRecord # Conversation.active # Conversation.archived # + # Of course, you can also query them directly if the scopes doesn't fit your + # needs: + # + # Conversation.where(status: [:active, :archived]) + # Conversation.where.not(status: :active) + # # You can set the default value from the database declaration, like: # # create_table :conversations do |t| @@ -59,15 +65,17 @@ module ActiveRecord # # In rare circumstances you might need to access the mapping directly. # The mappings are exposed through a class method with the pluralized attribute - # name: + # name, which return the mapping in a +HashWithIndifferentAccess+: # - # Conversation.statuses # => { "active" => 0, "archived" => 1 } + # Conversation.statuses[:active] # => 0 + # Conversation.statuses["archived"] # => 1 # - # Use that class method when you need to know the ordinal value of an enum: + # Use that class method when you need to know the ordinal value of an enum. + # For example, you can use that when manually building SQL strings: # # Conversation.where("status <> ?", Conversation.statuses[:archived]) # - # Where conditions on an enum attribute must use the ordinal value of an enum. + module Enum def self.extended(base) # :nodoc: base.class_attribute(:defined_enums) @@ -85,7 +93,7 @@ module ActiveRecord @mapping = mapping end - def type_cast_from_user(value) + def cast(value) return if value.blank? if mapping.has_key?(value) @@ -97,11 +105,12 @@ module ActiveRecord end end - def type_cast_from_database(value) + def deserialize(value) + return if value.nil? mapping.key(value.to_i) end - def type_cast_for_database(value) + def serialize(value) mapping.fetch(value, value) end diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index 739be524da..75b0e1e08d 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -708,6 +708,10 @@ module ActiveRecord def lhs_key @association.through_reflection.foreign_key end + + def join_table + @association.through_reflection.table_name + end end private diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb index c5b10fcddf..a09437b4b0 100644 --- a/activerecord/lib/active_record/locking/optimistic.rb +++ b/activerecord/lib/active_record/locking/optimistic.rb @@ -93,7 +93,7 @@ module ActiveRecord self.class.primary_key => id, lock_col => previous_lock_value, ).update_all( - attribute_names.map do |name| + attributes_for_update(attribute_names).map do |name| [name, _read_attribute(name)] end.to_h ) @@ -185,7 +185,7 @@ module ActiveRecord end class LockingType < DelegateClass(Type::Value) # :nodoc: - def type_cast_from_database(value) + def deserialize(value) # `nil` *should* be changed to 0 super.to_i end diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index 46f4794010..a83b90a95f 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -168,7 +168,7 @@ module ActiveRecord # This will generate the file <tt>timestamp_add_fieldname_to_tablename</tt>, which will look like this: # class AddFieldnameToTablename < ActiveRecord::Migration # def change - # add_column :tablenames, :field, :string + # add_column :tablenames, :fieldname, :string # end # end # @@ -395,7 +395,7 @@ module ActiveRecord def load_schema_if_pending! if ActiveRecord::Migrator.needs_migration? || !ActiveRecord::Migrator.any_migrations? - # Roundrip to Rake to allow plugins to hook into database initialization. + # Roundtrip to Rake to allow plugins to hook into database initialization. FileUtils.cd Rails.root do current_config = Base.connection_config Base.clear_all_connections! diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb index 117a128579..084ef397a8 100644 --- a/activerecord/lib/active_record/nested_attributes.rb +++ b/activerecord/lib/active_record/nested_attributes.rb @@ -521,7 +521,7 @@ module ActiveRecord # Determines if a hash contains a truthy _destroy key. def has_destroy_flag?(hash) - Type::Boolean.new.type_cast_from_user(hash['_destroy']) + Type::Boolean.new.cast(hash['_destroy']) end # Determines if a new record should be rejected by checking diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index af7aef6e43..7c076864a3 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -208,7 +208,8 @@ module ActiveRecord def becomes(klass) became = klass.new became.instance_variable_set("@attributes", @attributes) - became.instance_variable_set("@changed_attributes", @changed_attributes) if defined?(@changed_attributes) + changed_attributes = @changed_attributes if defined?(@changed_attributes) + became.instance_variable_set("@changed_attributes", changed_attributes || {}) became.instance_variable_set("@new_record", new_record?) became.instance_variable_set("@destroyed", destroyed?) became.instance_variable_set("@errors", errors) @@ -414,9 +415,6 @@ module ActiveRecord # end # def reload(options = nil) - clear_aggregation_cache - clear_association_cache - fresh_object = if options && options[:lock] self.class.unscoped { self.class.lock(options[:lock]).find(id) } @@ -429,14 +427,17 @@ module ActiveRecord self end - # Saves the record with the updated_at/on attributes set to the current time. + # Saves the record with the updated_at/on attributes set to the current time + # or the time specified. # Please note that no validation is performed and only the +after_touch+, # +after_commit+ and +after_rollback+ callbacks are executed. # + # This method can be passed attribute names and an optional time argument. # If attribute names are passed, they are updated along with updated_at/on - # attributes. + # attributes. If no time argument is passed, the current time is used as default. # - # product.touch # updates updated_at/on + # product.touch # updates updated_at/on with current time + # product.touch(time: Time.new(2015, 2, 16, 0, 0, 0)) # updates updated_at/on with specified time # product.touch(:designed_at) # updates the designed_at attribute and updated_at/on # product.touch(:started_at, :ended_at) # updates started_at, ended_at and updated_at/on attributes # @@ -460,19 +461,18 @@ module ActiveRecord # ball = Ball.new # ball.touch(:updated_at) # => raises ActiveRecordError # - def touch(*names) + def touch(*names, time: current_time_from_proper_timezone) raise ActiveRecordError, "cannot touch on a new record object" unless persisted? attributes = timestamp_attributes_for_update_in_model attributes.concat(names) unless attributes.empty? - current_time = current_time_from_proper_timezone changes = {} attributes.each do |column| column = column.to_s - changes[column] = write_attribute(column, current_time) + changes[column] = write_attribute(column, time) end changes[self.class.locking_column] = increment_lock if locking_enabled? diff --git a/activerecord/lib/active_record/relation/batches.rb b/activerecord/lib/active_record/relation/batches.rb index a543341149..e07580a563 100644 --- a/activerecord/lib/active_record/relation/batches.rb +++ b/activerecord/lib/active_record/relation/batches.rb @@ -27,15 +27,15 @@ module ActiveRecord # # ==== Options # * <tt>:batch_size</tt> - Specifies the size of the batch. Default to 1000. - # * <tt>:start</tt> - Specifies the primary key value to start from, inclusive of the value. + # * <tt>:begin_at</tt> - Specifies the primary key value to start from, inclusive of the value. # * <tt>:end_at</tt> - Specifies the primary key value to end at, inclusive of the value. # This is especially useful if you want multiple workers dealing with # the same processing queue. You can make worker 1 handle all the records # between id 0 and 10,000 and worker 2 handle from 10,000 and beyond - # (by setting the +:start+ and +:end_at+ option on each worker). + # (by setting the +:begin_at+ and +:end_at+ option on each worker). # # # Let's process for a batch of 2000 records, skipping the first 2000 rows - # Person.find_each(start: 2000, batch_size: 2000) do |person| + # Person.find_each(begin_at: 2000, batch_size: 2000) do |person| # person.party_all_night! # end # @@ -46,15 +46,22 @@ module ActiveRecord # # NOTE: You can't set the limit either, that's used to control # the batch sizes. - def find_each(start: nil, end_at: nil, batch_size: 1000) + def find_each(begin_at: nil, end_at: nil, batch_size: 1000, start: nil) + if start + begin_at = start + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Passing `start` value to find_each is deprecated, and will be removed in Rails 5.1. + Please pass `begin_at` instead. + MSG + end if block_given? - find_in_batches(start: start, end_at: end_at, batch_size: batch_size) do |records| + find_in_batches(begin_at: begin_at, end_at: end_at, batch_size: batch_size) do |records| records.each { |record| yield record } end else - enum_for(:find_each, start: start, end_at: end_at, batch_size: batch_size) do + enum_for(:find_each, begin_at: begin_at, end_at: end_at, batch_size: batch_size) do relation = self - apply_limits(relation, start, end_at).size + apply_limits(relation, begin_at, end_at).size end end end @@ -79,15 +86,15 @@ module ActiveRecord # # ==== Options # * <tt>:batch_size</tt> - Specifies the size of the batch. Default to 1000. - # * <tt>:start</tt> - Specifies the primary key value to start from, inclusive of the value. + # * <tt>:begin_at</tt> - Specifies the primary key value to start from, inclusive of the value. # * <tt>:end_at</tt> - Specifies the primary key value to end at, inclusive of the value. # This is especially useful if you want multiple workers dealing with # the same processing queue. You can make worker 1 handle all the records # between id 0 and 10,000 and worker 2 handle from 10,000 and beyond - # (by setting the +:start+ and +:end_at+ option on each worker). + # (by setting the +:begin_at+ and +:end_at+ option on each worker). # # # Let's process the next 2000 records - # Person.find_in_batches(start: 2000, batch_size: 2000) do |group| + # Person.find_in_batches(begin_at: 2000, batch_size: 2000) do |group| # group.each { |person| person.party_all_night! } # end # @@ -98,12 +105,19 @@ module ActiveRecord # # NOTE: You can't set the limit either, that's used to control # the batch sizes. - def find_in_batches(start: nil, end_at: nil, batch_size: 1000) - relation = self + def find_in_batches(begin_at: nil, end_at: nil, batch_size: 1000, start: nil) + if start + begin_at = start + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Passing `start` value to find_in_batches is deprecated, and will be removed in Rails 5.1. + Please pass `begin_at` instead. + MSG + end + relation = self unless block_given? - return to_enum(:find_in_batches, start: start, end_at: end_at, batch_size: batch_size) do - total = apply_limits(relation, start, end_at).size + return to_enum(:find_in_batches, begin_at: begin_at, end_at: end_at, batch_size: batch_size) do + total = apply_limits(relation, begin_at, end_at).size (total - 1).div(batch_size) + 1 end end @@ -113,7 +127,7 @@ module ActiveRecord end relation = relation.reorder(batch_order).limit(batch_size) - relation = apply_limits(relation, start, end_at) + relation = apply_limits(relation, begin_at, end_at) records = relation.to_a while records.any? @@ -131,8 +145,8 @@ module ActiveRecord private - def apply_limits(relation, start, end_at) - relation = relation.where(table[primary_key].gteq(start)) if start + def apply_limits(relation, begin_at, end_at) + relation = relation.where(table[primary_key].gteq(begin_at)) if begin_at relation = relation.where(table[primary_key].lteq(end_at)) if end_at relation end diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index 63e0d2fc21..4a4de86d48 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -353,9 +353,9 @@ module ActiveRecord def type_cast_calculated_value(value, type, operation = nil) case operation when 'count' then value.to_i - when 'sum' then type.type_cast_from_database(value || 0) + when 'sum' then type.deserialize(value || 0) when 'average' then value.respond_to?(:to_d) ? value.to_d : value - else type.type_cast_from_database(value) + else type.deserialize(value) end end diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 7514401072..69ce5cdc2a 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -999,11 +999,15 @@ module ActiveRecord end def arel_columns(columns) - columns.map do |field| - if (Symbol === field || String === field) && columns_hash.key?(field.to_s) - arel_table[field] - else - field + if from_clause.value + columns + else + columns.map do |field| + if (Symbol === field || String === field) && columns_hash.key?(field.to_s) + arel_table[field] + else + field + end end end end diff --git a/activerecord/lib/active_record/result.rb b/activerecord/lib/active_record/result.rb index 3a3e65ef32..500c478e65 100644 --- a/activerecord/lib/active_record/result.rb +++ b/activerecord/lib/active_record/result.rb @@ -81,7 +81,7 @@ module ActiveRecord def cast_values(type_overrides = {}) # :nodoc: types = columns.map { |name| column_type(name, type_overrides) } result = rows.map do |values| - types.zip(values).map { |type, value| type.type_cast_from_database(value) } + types.zip(values).map { |type, value| type.deserialize(value) } end columns.one? ? result.map!(&:first) : result diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb index 313e767dcb..c7f55ebaa1 100644 --- a/activerecord/lib/active_record/sanitization.rb +++ b/activerecord/lib/active_record/sanitization.rb @@ -75,7 +75,7 @@ module ActiveRecord def sanitize_sql_hash_for_assignment(attrs, table) c = connection attrs.map do |attr, value| - value = type_for_attribute(attr.to_s).type_cast_for_database(value) + value = type_for_attribute(attr.to_s).serialize(value) "#{c.quote_table_name_for_assignment(table, attr)} = #{c.quote(value)}" end.join(', ') end diff --git a/activerecord/lib/active_record/secure_token.rb b/activerecord/lib/active_record/secure_token.rb index 0990f815a7..a3023a0cb4 100644 --- a/activerecord/lib/active_record/secure_token.rb +++ b/activerecord/lib/active_record/secure_token.rb @@ -21,8 +21,8 @@ module ActiveRecord # SecureRandom::base58 is used to generate the 24-character unique token, so collisions are highly unlikely. # # Note that it's still possible to generate a race condition in the database in the same way that - # validates_presence_of can. You're encouraged to add a unique index in the database to deal with - # this even more unlikely scenario. + # <tt>validates_uniqueness_of</tt> can. You're encouraged to add a unique index in the database to deal + # with this even more unlikely scenario. def has_secure_token(attribute = :token) # Load securerandom only when has_secure_token is used. require 'active_support/core_ext/securerandom' diff --git a/activerecord/lib/active_record/suppressor.rb b/activerecord/lib/active_record/suppressor.rb new file mode 100644 index 0000000000..b0b86865fd --- /dev/null +++ b/activerecord/lib/active_record/suppressor.rb @@ -0,0 +1,55 @@ +module ActiveRecord + # ActiveRecord::Suppressor prevents the receiver from being saved during + # a given block. + # + # For example, here's a pattern of creating notifications when new comments + # are posted. (The notification may in turn trigger an email, a push + # notification, or just appear in the UI somewhere): + # + # class Comment < ActiveRecord::Base + # belongs_to :commentable, polymorphic: true + # after_create -> { Notification.create! comment: self, + # recipients: commentable.recipients } + # end + # + # That's what you want the bulk of the time. New comment creates a new + # Notification. But there may well be off cases, like copying a commentable + # and its comments, where you don't want that. So you'd have a concern + # something like this: + # + # module Copyable + # def copy_to(destination) + # Notification.suppress do + # # Copy logic that creates new comments that we do not want + # # triggering notifications. + # end + # end + # end + module Suppressor + extend ActiveSupport::Concern + + module ClassMethods + def suppress(&block) + SuppressorRegistry.suppressed[name] = true + yield + ensure + SuppressorRegistry.suppressed[name] = false + end + end + + # Ignore saving events if we're in suppression mode. + def save!(*args) # :nodoc: + SuppressorRegistry.suppressed[self.class.name] ? true : super + end + end + + class SuppressorRegistry # :nodoc: + extend ActiveSupport::PerThreadRegistry + + attr_reader :suppressed + + def initialize + @suppressed = {} + end + end +end diff --git a/activerecord/lib/active_record/type.rb b/activerecord/lib/active_record/type.rb index f18b076d58..2c0cda69d0 100644 --- a/activerecord/lib/active_record/type.rb +++ b/activerecord/lib/active_record/type.rb @@ -16,5 +16,51 @@ require 'active_record/type/text' require 'active_record/type/time' require 'active_record/type/unsigned_integer' +require 'active_record/type/adapter_specific_registry' require 'active_record/type/type_map' require 'active_record/type/hash_lookup_type_map' + +module ActiveRecord + module Type + @registry = AdapterSpecificRegistry.new + + class << self + attr_accessor :registry # :nodoc: + delegate :add_modifier, to: :registry + + # Add a new type to the registry, allowing it to be referenced as a + # symbol by ActiveRecord::Attributes::ClassMethods#attribute. If your + # type is only meant to be used with a specific database adapter, you can + # do so by passing +adapter: :postgresql+. If your type has the same + # name as a native type for the current adapter, an exception will be + # raised unless you specify an +:override+ option. +override: true+ will + # cause your type to be used instead of the native type. +override: + # false+ will cause the native type to be used over yours if one exists. + def register(type_name, klass = nil, **options, &block) + registry.register(type_name, klass, **options, &block) + end + + def lookup(*args, adapter: current_adapter_name, **kwargs) # :nodoc: + registry.lookup(*args, adapter: adapter, **kwargs) + end + + private + + def current_adapter_name + ActiveRecord::Base.connection.adapter_name.downcase.to_sym + end + end + + register(:big_integer, Type::BigInteger, override: false) + register(:binary, Type::Binary, override: false) + register(:boolean, Type::Boolean, override: false) + register(:date, Type::Date, override: false) + register(:date_time, Type::DateTime, override: false) + register(:decimal, Type::Decimal, override: false) + register(:float, Type::Float, override: false) + register(:integer, Type::Integer, override: false) + register(:string, Type::String, override: false) + register(:text, Type::Text, override: false) + register(:time, Type::Time, override: false) + end +end diff --git a/activerecord/lib/active_record/type/adapter_specific_registry.rb b/activerecord/lib/active_record/type/adapter_specific_registry.rb new file mode 100644 index 0000000000..5f71b3cb94 --- /dev/null +++ b/activerecord/lib/active_record/type/adapter_specific_registry.rb @@ -0,0 +1,142 @@ +module ActiveRecord + # :stopdoc: + module Type + class AdapterSpecificRegistry + def initialize + @registrations = [] + end + + def register(type_name, klass = nil, **options, &block) + block ||= proc { |_, *args| klass.new(*args) } + registrations << Registration.new(type_name, block, **options) + end + + def lookup(symbol, *args) + registration = registrations + .select { |r| r.matches?(symbol, *args) } + .max + + if registration + registration.call(self, symbol, *args) + else + raise ArgumentError, "Unknown type #{symbol.inspect}" + end + end + + def add_modifier(options, klass, **args) + registrations << DecorationRegistration.new(options, klass, **args) + end + + protected + + attr_reader :registrations + end + + class Registration + def initialize(name, block, adapter: nil, override: nil) + @name = name + @block = block + @adapter = adapter + @override = override + end + + def call(_registry, *args, adapter: nil, **kwargs) + if kwargs.any? # https://bugs.ruby-lang.org/issues/10856 + block.call(*args, **kwargs) + else + block.call(*args) + end + end + + def matches?(type_name, *args, **kwargs) + type_name == name && matches_adapter?(**kwargs) + end + + def <=>(other) + if conflicts_with?(other) + raise TypeConflictError.new("Type #{name} was registered for all + adapters, but shadows a native type with + the same name for #{other.adapter}".squish) + end + priority <=> other.priority + end + + protected + + attr_reader :name, :block, :adapter, :override + + def priority + result = 0 + if adapter + result |= 1 + end + if override + result |= 2 + end + result + end + + def priority_except_adapter + priority & 0b111111100 + end + + private + + def matches_adapter?(adapter: nil, **) + (self.adapter.nil? || adapter == self.adapter) + end + + def conflicts_with?(other) + same_priority_except_adapter?(other) && + has_adapter_conflict?(other) + end + + def same_priority_except_adapter?(other) + priority_except_adapter == other.priority_except_adapter + end + + def has_adapter_conflict?(other) + (override.nil? && other.adapter) || + (adapter && other.override.nil?) + end + end + + class DecorationRegistration < Registration + def initialize(options, klass, adapter: nil) + @options = options + @klass = klass + @adapter = adapter + end + + def call(registry, *args, **kwargs) + subtype = registry.lookup(*args, **kwargs.except(*options.keys)) + klass.new(subtype) + end + + def matches?(*args, **kwargs) + matches_adapter?(**kwargs) && matches_options?(**kwargs) + end + + def priority + super | 4 + end + + protected + + attr_reader :options, :klass + + private + + def matches_options?(**kwargs) + options.all? do |key, value| + kwargs[key] == value + end + end + end + end + + class TypeConflictError < StandardError + end + + # :startdoc: +end diff --git a/activerecord/lib/active_record/type/binary.rb b/activerecord/lib/active_record/type/binary.rb index 005a48ef0d..0baf8c63ad 100644 --- a/activerecord/lib/active_record/type/binary.rb +++ b/activerecord/lib/active_record/type/binary.rb @@ -9,7 +9,7 @@ module ActiveRecord true end - def type_cast(value) + def cast(value) if value.is_a?(Data) value.to_s else @@ -17,13 +17,13 @@ module ActiveRecord end end - def type_cast_for_database(value) + def serialize(value) return if value.nil? Data.new(super) end def changed_in_place?(raw_old_value, value) - old_value = type_cast_from_database(raw_old_value) + old_value = deserialize(raw_old_value) old_value != value end diff --git a/activerecord/lib/active_record/type/date_time.rb b/activerecord/lib/active_record/type/date_time.rb index a25f2521bb..a5199959b9 100644 --- a/activerecord/lib/active_record/type/date_time.rb +++ b/activerecord/lib/active_record/type/date_time.rb @@ -10,24 +10,6 @@ module ActiveRecord :datetime end - def type_cast_for_database(value) - if precision && value.respond_to?(:usec) - number_of_insignificant_digits = 6 - precision - round_power = 10 ** number_of_insignificant_digits - value = value.change(usec: value.usec / round_power * round_power) - end - - if value.acts_like?(:time) - zone_conversion_method = ActiveRecord::Base.default_timezone == :utc ? :getutc : :getlocal - - if value.respond_to?(zone_conversion_method) - value = value.send(zone_conversion_method) - end - end - - value - end - private def cast_value(string) diff --git a/activerecord/lib/active_record/type/float.rb b/activerecord/lib/active_record/type/float.rb index b3928242b7..d88482b85d 100644 --- a/activerecord/lib/active_record/type/float.rb +++ b/activerecord/lib/active_record/type/float.rb @@ -7,7 +7,7 @@ module ActiveRecord :float end - alias type_cast_for_database type_cast + alias serialize cast private diff --git a/activerecord/lib/active_record/type/helpers/accepts_multiparameter_time.rb b/activerecord/lib/active_record/type/helpers/accepts_multiparameter_time.rb index 640943c5e9..be571fc1c7 100644 --- a/activerecord/lib/active_record/type/helpers/accepts_multiparameter_time.rb +++ b/activerecord/lib/active_record/type/helpers/accepts_multiparameter_time.rb @@ -3,7 +3,7 @@ module ActiveRecord module Helpers class AcceptsMultiparameterTime < Module # :nodoc: def initialize(defaults: {}) - define_method(:type_cast_from_user) do |value| + define_method(:cast) do |value| if value.is_a?(Hash) value_from_multiparameter_assignment(value) else diff --git a/activerecord/lib/active_record/type/helpers/mutable.rb b/activerecord/lib/active_record/type/helpers/mutable.rb index dc37f4a885..88a9099277 100644 --- a/activerecord/lib/active_record/type/helpers/mutable.rb +++ b/activerecord/lib/active_record/type/helpers/mutable.rb @@ -2,15 +2,15 @@ module ActiveRecord module Type module Helpers module Mutable # :nodoc: - def type_cast_from_user(value) - type_cast_from_database(type_cast_for_database(value)) + def cast(value) + deserialize(serialize(value)) end # +raw_old_value+ will be the `_before_type_cast` version of the # value (likely a string). +new_value+ will be the current, type # cast value. def changed_in_place?(raw_old_value, new_value) - raw_old_value != type_cast_for_database(new_value) + raw_old_value != serialize(new_value) end end end diff --git a/activerecord/lib/active_record/type/helpers/numeric.rb b/activerecord/lib/active_record/type/helpers/numeric.rb index b0d4f03117..a755a02a59 100644 --- a/activerecord/lib/active_record/type/helpers/numeric.rb +++ b/activerecord/lib/active_record/type/helpers/numeric.rb @@ -2,7 +2,7 @@ module ActiveRecord module Type module Helpers module Numeric # :nodoc: - def type_cast(value) + def cast(value) value = case value when true then 1 when false then 0 diff --git a/activerecord/lib/active_record/type/helpers/time_value.rb b/activerecord/lib/active_record/type/helpers/time_value.rb index 6e14c3a9b5..7eb41557cb 100644 --- a/activerecord/lib/active_record/type/helpers/time_value.rb +++ b/activerecord/lib/active_record/type/helpers/time_value.rb @@ -2,6 +2,24 @@ module ActiveRecord module Type module Helpers module TimeValue # :nodoc: + def serialize(value) + if precision && value.respond_to?(:usec) + number_of_insignificant_digits = 6 - precision + round_power = 10 ** number_of_insignificant_digits + value = value.change(usec: value.usec / round_power * round_power) + end + + if value.acts_like?(:time) + zone_conversion_method = ActiveRecord::Base.default_timezone == :utc ? :getutc : :getlocal + + if value.respond_to?(zone_conversion_method) + value = value.send(zone_conversion_method) + end + end + + value + end + def type_cast_for_schema(value) "'#{value.to_s(:db)}'" end diff --git a/activerecord/lib/active_record/type/integer.rb b/activerecord/lib/active_record/type/integer.rb index 2ab2402dfd..2a1b04ac7f 100644 --- a/activerecord/lib/active_record/type/integer.rb +++ b/activerecord/lib/active_record/type/integer.rb @@ -16,13 +16,13 @@ module ActiveRecord :integer end - def type_cast_from_database(value) + def deserialize(value) return if value.nil? value.to_i end - def type_cast_for_database(value) - result = type_cast(value) + def serialize(value) + result = cast(value) if result ensure_in_range(result) end diff --git a/activerecord/lib/active_record/type/serialized.rb b/activerecord/lib/active_record/type/serialized.rb index 6c6c520048..732029c723 100644 --- a/activerecord/lib/active_record/type/serialized.rb +++ b/activerecord/lib/active_record/type/serialized.rb @@ -11,7 +11,7 @@ module ActiveRecord super(subtype) end - def type_cast_from_database(value) + def deserialize(value) if default_value?(value) value else @@ -19,7 +19,7 @@ module ActiveRecord end end - def type_cast_for_database(value) + def serialize(value) return if value.nil? unless default_value?(value) super coder.dump(value) @@ -28,7 +28,7 @@ module ActiveRecord def changed_in_place?(raw_old_value, value) return false if value.nil? - subtype.changed_in_place?(raw_old_value, type_cast_for_database(value)) + subtype.changed_in_place?(raw_old_value, serialize(value)) end def accessor diff --git a/activerecord/lib/active_record/type/string.rb b/activerecord/lib/active_record/type/string.rb index fbc0af2c5a..2662b7e874 100644 --- a/activerecord/lib/active_record/type/string.rb +++ b/activerecord/lib/active_record/type/string.rb @@ -11,7 +11,7 @@ module ActiveRecord end end - def type_cast_for_database(value) + def serialize(value) case value when ::Numeric, ActiveSupport::Duration then value.to_s when ::String then ::String.new(value) diff --git a/activerecord/lib/active_record/type/value.rb b/activerecord/lib/active_record/type/value.rb index 7338920f3b..fc3ef5e83b 100644 --- a/activerecord/lib/active_record/type/value.rb +++ b/activerecord/lib/active_record/type/value.rb @@ -14,12 +14,12 @@ module ActiveRecord # Convert a value from database input to the appropriate ruby type. The # return value of this method will be returned from - # ActiveRecord::AttributeMethods::Read#read_attribute. See also - # Value#type_cast and Value#cast_value. + # ActiveRecord::AttributeMethods::Read#read_attribute. The default + # implementation just calls Value#cast. # # +value+ The raw input, as provided from the database. - def type_cast_from_database(value) - type_cast(value) + def deserialize(value) + cast(value) end # Type casts a value from user input (e.g. from a setter). This value may @@ -29,18 +29,18 @@ module ActiveRecord # # The return value of this method will be returned from # ActiveRecord::AttributeMethods::Read#read_attribute. See also: - # Value#type_cast and Value#cast_value. + # Value#cast_value. # # +value+ The raw input, as provided to the attribute setter. - def type_cast_from_user(value) - type_cast(value) + def cast(value) + cast_value(value) unless value.nil? end # Cast a value from the ruby type to a type that the database knows how # to understand. The returned value from this method should be a # +String+, +Numeric+, +Date+, +Time+, +Symbol+, +true+, +false+, or # +nil+. - def type_cast_for_database(value) + def serialize(value) value end @@ -68,16 +68,16 @@ module ActiveRecord # which could be mutated, you should override this method. You will need # to either: # - # - pass +new_value+ to Value#type_cast_for_database and compare it to + # - pass +new_value+ to Value#serialize and compare it to # +raw_old_value+ # # or # - # - pass +raw_old_value+ to Value#type_cast_from_database and compare it to + # - pass +raw_old_value+ to Value#deserialize and compare it to # +new_value+ # # +raw_old_value+ The original value, before being passed to - # +type_cast_from_database+. + # +deserialize+. # # +new_value+ The current value, after type casting. def changed_in_place?(raw_old_value, new_value) @@ -93,16 +93,8 @@ module ActiveRecord private - # Convenience method. If you don't need separate behavior for - # Value#type_cast_from_database and Value#type_cast_from_user, you can override - # this method instead. The default behavior of both methods is to call - # this one. See also Value#cast_value. - def type_cast(value) # :doc: - cast_value(value) unless value.nil? - end - # Convenience method for types which do not need separate type casting - # behavior for user and database inputs. Called by Value#type_cast for + # behavior for user and database inputs. Called by Value#cast for # values except +nil+. def cast_value(value) # :doc: value diff --git a/activerecord/lib/active_record/type_caster/map.rb b/activerecord/lib/active_record/type_caster/map.rb index 03c9e8ff83..4b1941351c 100644 --- a/activerecord/lib/active_record/type_caster/map.rb +++ b/activerecord/lib/active_record/type_caster/map.rb @@ -8,7 +8,7 @@ module ActiveRecord def type_cast_for_database(attr_name, value) return value if value.is_a?(Arel::Nodes::BindParam) type = types.type_for_attribute(attr_name.to_s) - type.type_cast_for_database(value) + type.serialize(value) end protected diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb index f27adc9c40..e227212827 100644 --- a/activerecord/lib/active_record/validations.rb +++ b/activerecord/lib/active_record/validations.rb @@ -40,7 +40,7 @@ module ActiveRecord # Attempts to save the record just like Base#save but will raise a +RecordInvalid+ # exception instead of returning +false+ if the record is not valid. def save!(options={}) - perform_validations(options) ? super : raise_record_invalid + perform_validations(options) ? super : raise_validation_error end # Runs all the validations within the specified context. Returns +true+ if @@ -61,21 +61,9 @@ module ActiveRecord alias_method :validate, :valid? - # Runs all the validations within the specified context. Returns +true+ if - # no errors are found, raises +RecordInvalid+ otherwise. - # - # If the argument is +false+ (default is +nil+), the context is set to <tt>:create</tt> if - # <tt>new_record?</tt> is +true+, and to <tt>:update</tt> if it is not. - # - # Validations with no <tt>:on</tt> option will run no matter the context. Validations with - # some <tt>:on</tt> option will only run in the specified context. - def validate!(context = nil) - valid?(context) || raise_record_invalid - end - protected - def raise_record_invalid + def raise_validation_error raise(RecordInvalid.new(self)) end diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index a766d77e88..9be4b10a55 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -61,7 +61,7 @@ module ActiveRecord column = klass.columns_hash[attribute_name] cast_type = klass.type_for_attribute(attribute_name) - value = cast_type.type_cast_for_database(value) + value = cast_type.serialize(value) value = klass.connection.type_cast(value) if value.is_a?(String) && column.limit value = value.to_s[0, column.limit] diff --git a/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb b/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb index 7d0bd24ba7..48ceef365e 100644 --- a/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb +++ b/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb @@ -15,7 +15,7 @@ module ActiveRecord assert_raise ActiveRecord::NoDatabaseError do configuration = ActiveRecord::Base.configurations['arunit'].merge(database: 'inexistent_activerecord_unittest') connection = ActiveRecord::Base.mysql_connection(configuration) - connection.exec_query('drop table if exists ex') + connection.drop_table 'ex', if_exists: true end end @@ -110,7 +110,7 @@ module ActiveRecord result = @conn.exec_query('SELECT status FROM ex') - assert_equal 2, result.column_types['status'].type_cast_from_database(result.last['status']) + assert_equal 2, result.column_types['status'].deserialize(result.last['status']) end end diff --git a/activerecord/test/cases/adapters/mysql/reserved_word_test.rb b/activerecord/test/cases/adapters/mysql/reserved_word_test.rb index 403f7cbc74..2f9c070255 100644 --- a/activerecord/test/cases/adapters/mysql/reserved_word_test.rb +++ b/activerecord/test/cases/adapters/mysql/reserved_word_test.rb @@ -139,7 +139,7 @@ class MysqlReservedWordTest < ActiveRecord::TestCase # custom drop table, uses execute on connection to drop a table if it exists. note: escapes table_name def drop_tables_directly(table_names, connection = @connection) table_names.each do |name| - connection.execute("DROP TABLE IF EXISTS `#{name}`") + connection.drop_table name, if_exists: true end end diff --git a/activerecord/test/cases/adapters/mysql/schema_test.rb b/activerecord/test/cases/adapters/mysql/schema_test.rb index ab547747df..b7f9c2ce84 100644 --- a/activerecord/test/cases/adapters/mysql/schema_test.rb +++ b/activerecord/test/cases/adapters/mysql/schema_test.rb @@ -22,7 +22,7 @@ module ActiveRecord end teardown do - @connection.execute "drop table if exists mysql_doubles" + @connection.drop_table "mysql_doubles", if_exists: true end class MysqlDouble < ActiveRecord::Base diff --git a/activerecord/test/cases/adapters/mysql2/connection_test.rb b/activerecord/test/cases/adapters/mysql2/connection_test.rb index ff8ce93248..a8b39b21d4 100644 --- a/activerecord/test/cases/adapters/mysql2/connection_test.rb +++ b/activerecord/test/cases/adapters/mysql2/connection_test.rb @@ -22,7 +22,7 @@ class MysqlConnectionTest < ActiveRecord::TestCase assert_raise ActiveRecord::NoDatabaseError do configuration = ActiveRecord::Base.configurations['arunit'].merge(database: 'inexistent_activerecord_unittest') connection = ActiveRecord::Base.mysql2_connection(configuration) - connection.exec_query('drop table if exists ex') + connection.drop_table 'ex', if_exists: true end end diff --git a/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb b/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb index 7f97b454bb..beb829fc46 100644 --- a/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb +++ b/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb @@ -138,7 +138,7 @@ class MysqlReservedWordTest < ActiveRecord::TestCase # custom drop table, uses execute on connection to drop a table if it exists. note: escapes table_name def drop_tables_directly(table_names, connection = @connection) table_names.each do |name| - connection.execute("DROP TABLE IF EXISTS `#{name}`") + connection.drop_table name, if_exists: true end end diff --git a/activerecord/test/cases/adapters/postgresql/array_test.rb b/activerecord/test/cases/adapters/postgresql/array_test.rb index 3d5b7e5137..2163e35e70 100644 --- a/activerecord/test/cases/adapters/postgresql/array_test.rb +++ b/activerecord/test/cases/adapters/postgresql/array_test.rb @@ -28,7 +28,7 @@ class PostgresqlArrayTest < ActiveRecord::TestCase end teardown do - @connection.execute 'drop table if exists pg_arrays' + @connection.drop_table 'pg_arrays', if_exists: true disable_extension!('hstore', @connection) end @@ -92,9 +92,9 @@ class PostgresqlArrayTest < ActiveRecord::TestCase end def test_type_cast_array - assert_equal(['1', '2', '3'], @type.type_cast_from_database('{1,2,3}')) - assert_equal([], @type.type_cast_from_database('{}')) - assert_equal([nil], @type.type_cast_from_database('{NULL}')) + assert_equal(['1', '2', '3'], @type.deserialize('{1,2,3}')) + assert_equal([], @type.deserialize('{}')) + assert_equal([nil], @type.deserialize('{NULL}')) end def test_type_cast_integers @@ -206,7 +206,7 @@ class PostgresqlArrayTest < ActiveRecord::TestCase x = PgArray.create!(tags: tags) x.reload - assert_equal x.tags_before_type_cast, PgArray.type_for_attribute('tags').type_cast_for_database(tags) + assert_equal x.tags_before_type_cast, PgArray.type_for_attribute('tags').serialize(tags) end def test_quoting_non_standard_delimiters @@ -214,8 +214,8 @@ class PostgresqlArrayTest < ActiveRecord::TestCase comma_delim = OID::Array.new(ActiveRecord::Type::String.new, ',') semicolon_delim = OID::Array.new(ActiveRecord::Type::String.new, ';') - assert_equal %({"hello,",world;}), comma_delim.type_cast_for_database(strings) - assert_equal %({hello,;"world;"}), semicolon_delim.type_cast_for_database(strings) + assert_equal %({"hello,",world;}), comma_delim.serialize(strings) + assert_equal %({hello,;"world;"}), semicolon_delim.serialize(strings) end def test_mutate_array diff --git a/activerecord/test/cases/adapters/postgresql/bit_string_test.rb b/activerecord/test/cases/adapters/postgresql/bit_string_test.rb index 6c6b4dc22a..1a5ff4316c 100644 --- a/activerecord/test/cases/adapters/postgresql/bit_string_test.rb +++ b/activerecord/test/cases/adapters/postgresql/bit_string_test.rb @@ -20,7 +20,7 @@ class PostgresqlBitStringTest < ActiveRecord::TestCase def teardown return unless @connection - @connection.execute 'DROP TABLE IF EXISTS postgresql_bit_strings' + @connection.drop_table 'postgresql_bit_strings', if_exists: true end def test_bit_string_column diff --git a/activerecord/test/cases/adapters/postgresql/bytea_test.rb b/activerecord/test/cases/adapters/postgresql/bytea_test.rb index 678a476661..16db5ab83d 100644 --- a/activerecord/test/cases/adapters/postgresql/bytea_test.rb +++ b/activerecord/test/cases/adapters/postgresql/bytea_test.rb @@ -20,7 +20,7 @@ class PostgresqlByteaTest < ActiveRecord::TestCase end teardown do - @connection.execute 'drop table if exists bytea_data_type' + @connection.drop_table 'bytea_data_type', if_exists: true end def test_column @@ -40,16 +40,16 @@ class PostgresqlByteaTest < ActiveRecord::TestCase data = "\u001F\x8B" assert_equal('UTF-8', data.encoding.name) - assert_equal('ASCII-8BIT', @type.type_cast_from_database(data).encoding.name) + assert_equal('ASCII-8BIT', @type.deserialize(data).encoding.name) end def test_type_cast_binary_value data = "\u001F\x8B".force_encoding("BINARY") - assert_equal(data, @type.type_cast_from_database(data)) + assert_equal(data, @type.deserialize(data)) end def test_type_case_nil - assert_equal(nil, @type.type_cast_from_database(nil)) + assert_equal(nil, @type.deserialize(nil)) end def test_read_value diff --git a/activerecord/test/cases/adapters/postgresql/change_schema_test.rb b/activerecord/test/cases/adapters/postgresql/change_schema_test.rb index 6c1b29f7fe..5a9796887c 100644 --- a/activerecord/test/cases/adapters/postgresql/change_schema_test.rb +++ b/activerecord/test/cases/adapters/postgresql/change_schema_test.rb @@ -26,6 +26,13 @@ module ActiveRecord connection.change_column :strings, :somedate, :timestamp, cast_as: :timestamp assert_equal :datetime, connection.columns(:strings).find { |c| c.name == 'somedate' }.type end + + def test_change_type_with_array + connection.change_column :strings, :somedate, :timestamp, array: true, cast_as: :timestamp + column = connection.columns(:strings).find { |c| c.name == 'somedate' } + assert_equal :datetime, column.type + assert column.array? + end end end end diff --git a/activerecord/test/cases/adapters/postgresql/cidr_test.rb b/activerecord/test/cases/adapters/postgresql/cidr_test.rb index 54b679d3ab..6cb11d17b4 100644 --- a/activerecord/test/cases/adapters/postgresql/cidr_test.rb +++ b/activerecord/test/cases/adapters/postgresql/cidr_test.rb @@ -10,14 +10,14 @@ module ActiveRecord ip = IPAddr.new("255.0.0.0/8") ip2 = IPAddr.new("127.0.0.1") - assert_equal "255.0.0.0/8", type.type_cast_for_database(ip) - assert_equal "127.0.0.1/32", type.type_cast_for_database(ip2) + assert_equal "255.0.0.0/8", type.serialize(ip) + assert_equal "127.0.0.1/32", type.serialize(ip2) end test "casting does nothing with non-IPAddr objects" do type = OID::Cidr.new - assert_equal "foo", type.type_cast_for_database("foo") + assert_equal "foo", type.serialize("foo") end end end diff --git a/activerecord/test/cases/adapters/postgresql/citext_test.rb b/activerecord/test/cases/adapters/postgresql/citext_test.rb index 0ee2a21484..f706847890 100644 --- a/activerecord/test/cases/adapters/postgresql/citext_test.rb +++ b/activerecord/test/cases/adapters/postgresql/citext_test.rb @@ -19,7 +19,7 @@ if ActiveRecord::Base.connection.supports_extensions? end teardown do - @connection.execute 'DROP TABLE IF EXISTS citexts;' + @connection.drop_table 'citexts', if_exists: true disable_extension!('citext', @connection) end diff --git a/activerecord/test/cases/adapters/postgresql/composite_test.rb b/activerecord/test/cases/adapters/postgresql/composite_test.rb index 83dfb18e95..16e3f90a47 100644 --- a/activerecord/test/cases/adapters/postgresql/composite_test.rb +++ b/activerecord/test/cases/adapters/postgresql/composite_test.rb @@ -29,7 +29,7 @@ module PostgresqlCompositeBehavior def teardown super - @connection.execute 'DROP TABLE IF EXISTS postgresql_composites' + @connection.drop_table 'postgresql_composites', if_exists: true @connection.execute 'DROP TYPE IF EXISTS full_address' reset_connection PostgresqlComposite.reset_column_information @@ -83,17 +83,17 @@ class PostgresqlCompositeWithCustomOIDTest < ActiveRecord::TestCase class FullAddressType < ActiveRecord::Type::Value def type; :full_address end - def type_cast_from_database(value) + def deserialize(value) if value =~ /\("?([^",]*)"?,"?([^",]*)"?\)/ FullAddress.new($1, $2) end end - def type_cast_from_user(value) + def cast(value) value end - def type_cast_for_database(value) + def serialize(value) return if value.nil? "(#{value.city},#{value.street})" end diff --git a/activerecord/test/cases/adapters/postgresql/connection_test.rb b/activerecord/test/cases/adapters/postgresql/connection_test.rb index 7bf8d12eae..55ad76c8c0 100644 --- a/activerecord/test/cases/adapters/postgresql/connection_test.rb +++ b/activerecord/test/cases/adapters/postgresql/connection_test.rb @@ -131,7 +131,7 @@ module ActiveRecord name = @subscriber.payloads.last[:statement_name] assert name res = @connection.exec_query("EXPLAIN (FORMAT JSON) EXECUTE #{name}(1)") - plan = res.column_types['QUERY PLAN'].type_cast_from_database res.rows.first.first + plan = res.column_types['QUERY PLAN'].deserialize res.rows.first.first assert_operator plan.length, :>, 0 end diff --git a/activerecord/test/cases/adapters/postgresql/domain_test.rb b/activerecord/test/cases/adapters/postgresql/domain_test.rb index b7d776b40c..26e064c937 100644 --- a/activerecord/test/cases/adapters/postgresql/domain_test.rb +++ b/activerecord/test/cases/adapters/postgresql/domain_test.rb @@ -19,7 +19,7 @@ class PostgresqlDomainTest < ActiveRecord::TestCase end teardown do - @connection.execute 'DROP TABLE IF EXISTS postgresql_domains' + @connection.drop_table 'postgresql_domains', if_exists: true @connection.execute 'DROP DOMAIN IF EXISTS custom_money' reset_connection end diff --git a/activerecord/test/cases/adapters/postgresql/enum_test.rb b/activerecord/test/cases/adapters/postgresql/enum_test.rb index acb09b0607..7458de23d8 100644 --- a/activerecord/test/cases/adapters/postgresql/enum_test.rb +++ b/activerecord/test/cases/adapters/postgresql/enum_test.rb @@ -21,7 +21,7 @@ class PostgresqlEnumTest < ActiveRecord::TestCase end teardown do - @connection.execute 'DROP TABLE IF EXISTS postgresql_enums' + @connection.drop_table 'postgresql_enums', if_exists: true @connection.execute 'DROP TYPE IF EXISTS mood' reset_connection end diff --git a/activerecord/test/cases/adapters/postgresql/full_text_test.rb b/activerecord/test/cases/adapters/postgresql/full_text_test.rb index 81891a90fa..b83063c94e 100644 --- a/activerecord/test/cases/adapters/postgresql/full_text_test.rb +++ b/activerecord/test/cases/adapters/postgresql/full_text_test.rb @@ -13,7 +13,7 @@ class PostgresqlFullTextTest < ActiveRecord::TestCase end teardown do - @connection.execute 'DROP TABLE IF EXISTS tsvectors;' + @connection.drop_table 'tsvectors', if_exists: true end def test_tsvector_column diff --git a/activerecord/test/cases/adapters/postgresql/geometric_test.rb b/activerecord/test/cases/adapters/postgresql/geometric_test.rb index 4b25381a83..41e9572907 100644 --- a/activerecord/test/cases/adapters/postgresql/geometric_test.rb +++ b/activerecord/test/cases/adapters/postgresql/geometric_test.rb @@ -18,7 +18,7 @@ class PostgresqlPointTest < ActiveRecord::TestCase end teardown do - @connection.execute 'DROP TABLE IF EXISTS postgresql_points' + @connection.drop_table 'postgresql_points', if_exists: true end def test_column @@ -84,7 +84,7 @@ class PostgresqlGeometricTest < ActiveRecord::TestCase end teardown do - @connection.execute 'DROP TABLE IF EXISTS postgresql_geometrics' + @connection.drop_table 'postgresql_geometrics', if_exists: true end def test_geometric_types diff --git a/activerecord/test/cases/adapters/postgresql/hstore_test.rb b/activerecord/test/cases/adapters/postgresql/hstore_test.rb index 11053a6e38..e6835031c3 100644 --- a/activerecord/test/cases/adapters/postgresql/hstore_test.rb +++ b/activerecord/test/cases/adapters/postgresql/hstore_test.rb @@ -32,7 +32,7 @@ if ActiveRecord::Base.connection.supports_extensions? end teardown do - @connection.execute 'drop table if exists hstores' + @connection.drop_table 'hstores', if_exists: true end def test_hstore_included_in_extensions @@ -110,10 +110,10 @@ if ActiveRecord::Base.connection.supports_extensions? end def test_type_cast_hstore - assert_equal({'1' => '2'}, @type.type_cast_from_database("\"1\"=>\"2\"")) - assert_equal({}, @type.type_cast_from_database("")) - assert_equal({'key'=>nil}, @type.type_cast_from_database('key => NULL')) - assert_equal({'c'=>'}','"a"'=>'b "a b'}, @type.type_cast_from_database(%q(c=>"}", "\"a\""=>"b \"a b"))) + assert_equal({'1' => '2'}, @type.deserialize("\"1\"=>\"2\"")) + assert_equal({}, @type.deserialize("")) + assert_equal({'key'=>nil}, @type.deserialize('key => NULL')) + assert_equal({'c'=>'}','"a"'=>'b "a b'}, @type.deserialize(%q(c=>"}", "\"a\""=>"b \"a b"))) end def test_with_store_accessors @@ -165,47 +165,47 @@ if ActiveRecord::Base.connection.supports_extensions? end def test_gen1 - assert_equal(%q(" "=>""), @type.type_cast_for_database({' '=>''})) + assert_equal(%q(" "=>""), @type.serialize({' '=>''})) end def test_gen2 - assert_equal(%q(","=>""), @type.type_cast_for_database({','=>''})) + assert_equal(%q(","=>""), @type.serialize({','=>''})) end def test_gen3 - assert_equal(%q("="=>""), @type.type_cast_for_database({'='=>''})) + assert_equal(%q("="=>""), @type.serialize({'='=>''})) end def test_gen4 - assert_equal(%q(">"=>""), @type.type_cast_for_database({'>'=>''})) + assert_equal(%q(">"=>""), @type.serialize({'>'=>''})) end def test_parse1 - assert_equal({'a'=>nil,'b'=>nil,'c'=>'NuLl','null'=>'c'}, @type.type_cast_from_database('a=>null,b=>NuLl,c=>"NuLl",null=>c')) + assert_equal({'a'=>nil,'b'=>nil,'c'=>'NuLl','null'=>'c'}, @type.deserialize('a=>null,b=>NuLl,c=>"NuLl",null=>c')) end def test_parse2 - assert_equal({" " => " "}, @type.type_cast_from_database("\\ =>\\ ")) + assert_equal({" " => " "}, @type.deserialize("\\ =>\\ ")) end def test_parse3 - assert_equal({"=" => ">"}, @type.type_cast_from_database("==>>")) + assert_equal({"=" => ">"}, @type.deserialize("==>>")) end def test_parse4 - assert_equal({"=a"=>"q=w"}, @type.type_cast_from_database('\=a=>q=w')) + assert_equal({"=a"=>"q=w"}, @type.deserialize('\=a=>q=w')) end def test_parse5 - assert_equal({"=a"=>"q=w"}, @type.type_cast_from_database('"=a"=>q\=w')) + assert_equal({"=a"=>"q=w"}, @type.deserialize('"=a"=>q\=w')) end def test_parse6 - assert_equal({"\"a"=>"q>w"}, @type.type_cast_from_database('"\"a"=>q>w')) + assert_equal({"\"a"=>"q>w"}, @type.deserialize('"\"a"=>q>w')) end def test_parse7 - assert_equal({"\"a"=>"q\"w"}, @type.type_cast_from_database('\"a=>q"w')) + assert_equal({"\"a"=>"q\"w"}, @type.deserialize('\"a=>q"w')) end def test_rewrite diff --git a/activerecord/test/cases/adapters/postgresql/infinity_test.rb b/activerecord/test/cases/adapters/postgresql/infinity_test.rb index 74163ac712..24199c69b8 100644 --- a/activerecord/test/cases/adapters/postgresql/infinity_test.rb +++ b/activerecord/test/cases/adapters/postgresql/infinity_test.rb @@ -15,7 +15,7 @@ class PostgresqlInfinityTest < ActiveRecord::TestCase end teardown do - @connection.execute("DROP TABLE IF EXISTS postgresql_infinities") + @connection.drop_table 'postgresql_infinities', if_exists: true end test "type casting infinity on a float column" do diff --git a/activerecord/test/cases/adapters/postgresql/integer_test.rb b/activerecord/test/cases/adapters/postgresql/integer_test.rb index 7f8751281e..679a0fc7b3 100644 --- a/activerecord/test/cases/adapters/postgresql/integer_test.rb +++ b/activerecord/test/cases/adapters/postgresql/integer_test.rb @@ -16,7 +16,7 @@ class PostgresqlIntegerTest < ActiveRecord::TestCase end teardown do - @connection.execute "drop table if exists pg_integers" + @connection.drop_table "pg_integers", if_exists: true end test "schema properly respects bigint ranges" do diff --git a/activerecord/test/cases/adapters/postgresql/json_test.rb b/activerecord/test/cases/adapters/postgresql/json_test.rb index 3b123f979e..d8fded16b4 100644 --- a/activerecord/test/cases/adapters/postgresql/json_test.rb +++ b/activerecord/test/cases/adapters/postgresql/json_test.rb @@ -80,13 +80,13 @@ module PostgresqlJSONSharedTestCases type = JsonDataType.type_for_attribute("payload") data = "{\"a_key\":\"a_value\"}" - hash = type.type_cast_from_database(data) + hash = type.deserialize(data) assert_equal({'a_key' => 'a_value'}, hash) - assert_equal({'a_key' => 'a_value'}, type.type_cast_from_database(data)) + assert_equal({'a_key' => 'a_value'}, type.deserialize(data)) - assert_equal({}, type.type_cast_from_database("{}")) - assert_equal({'key'=>nil}, type.type_cast_from_database('{"key": null}')) - assert_equal({'c'=>'}','"a"'=>'b "a b'}, type.type_cast_from_database(%q({"c":"}", "\"a\"":"b \"a b"}))) + assert_equal({}, type.deserialize("{}")) + assert_equal({'key'=>nil}, type.deserialize('{"key": null}')) + assert_equal({'c'=>'}','"a"'=>'b "a b'}, type.deserialize(%q({"c":"}", "\"a\"":"b \"a b"}))) end def test_rewrite diff --git a/activerecord/test/cases/adapters/postgresql/ltree_test.rb b/activerecord/test/cases/adapters/postgresql/ltree_test.rb index 2b3823f9f1..ce0ad16557 100644 --- a/activerecord/test/cases/adapters/postgresql/ltree_test.rb +++ b/activerecord/test/cases/adapters/postgresql/ltree_test.rb @@ -22,7 +22,7 @@ class PostgresqlLtreeTest < ActiveRecord::TestCase end teardown do - @connection.execute 'drop table if exists ltrees' + @connection.drop_table 'ltrees', if_exists: true end def test_column diff --git a/activerecord/test/cases/adapters/postgresql/money_test.rb b/activerecord/test/cases/adapters/postgresql/money_test.rb index ba9af4be6f..cedd399380 100644 --- a/activerecord/test/cases/adapters/postgresql/money_test.rb +++ b/activerecord/test/cases/adapters/postgresql/money_test.rb @@ -16,7 +16,7 @@ class PostgresqlMoneyTest < ActiveRecord::TestCase end teardown do - @connection.execute 'DROP TABLE IF EXISTS postgresql_moneys' + @connection.drop_table 'postgresql_moneys', if_exists: true end def test_column @@ -47,10 +47,10 @@ class PostgresqlMoneyTest < ActiveRecord::TestCase def test_money_type_cast type = PostgresqlMoney.type_for_attribute('wealth') - assert_equal(12345678.12, type.type_cast_from_user("$12,345,678.12")) - assert_equal(12345678.12, type.type_cast_from_user("$12.345.678,12")) - assert_equal(-1.15, type.type_cast_from_user("-$1.15")) - assert_equal(-2.25, type.type_cast_from_user("($2.25)")) + assert_equal(12345678.12, type.cast("$12,345,678.12")) + assert_equal(12345678.12, type.cast("$12.345.678,12")) + assert_equal(-1.15, type.cast("-$1.15")) + assert_equal(-2.25, type.cast("($2.25)")) end def test_schema_dumping diff --git a/activerecord/test/cases/adapters/postgresql/network_test.rb b/activerecord/test/cases/adapters/postgresql/network_test.rb index 4cd2d4d5f3..033695518e 100644 --- a/activerecord/test/cases/adapters/postgresql/network_test.rb +++ b/activerecord/test/cases/adapters/postgresql/network_test.rb @@ -15,7 +15,7 @@ class PostgresqlNetworkTest < ActiveRecord::TestCase end teardown do - @connection.execute 'DROP TABLE IF EXISTS postgresql_network_addresses' + @connection.drop_table 'postgresql_network_addresses', if_exists: true end def test_cidr_column diff --git a/activerecord/test/cases/adapters/postgresql/numbers_test.rb b/activerecord/test/cases/adapters/postgresql/numbers_test.rb index 70aa898439..093b81fe8d 100644 --- a/activerecord/test/cases/adapters/postgresql/numbers_test.rb +++ b/activerecord/test/cases/adapters/postgresql/numbers_test.rb @@ -12,7 +12,7 @@ class PostgresqlNumberTest < ActiveRecord::TestCase end teardown do - @connection.execute 'DROP TABLE IF EXISTS postgresql_numbers' + @connection.drop_table 'postgresql_numbers', if_exists: true end def test_data_type diff --git a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb index 564712522d..a934180a43 100644 --- a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb +++ b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb @@ -227,8 +227,8 @@ module ActiveRecord "DELETE FROM pg_depend WHERE objid = 'ex2_id_seq'::regclass AND refobjid = 'ex'::regclass AND deptype = 'a'" ) ensure - @connection.exec_query('DROP TABLE IF EXISTS ex') - @connection.exec_query('DROP TABLE IF EXISTS ex2') + @connection.drop_table 'ex', if_exists: true + @connection.drop_table 'ex2', if_exists: true end def test_exec_insert_number diff --git a/activerecord/test/cases/adapters/postgresql/quoting_test.rb b/activerecord/test/cases/adapters/postgresql/quoting_test.rb index 60baacf67a..e4420d9d13 100644 --- a/activerecord/test/cases/adapters/postgresql/quoting_test.rb +++ b/activerecord/test/cases/adapters/postgresql/quoting_test.rb @@ -30,13 +30,13 @@ module ActiveRecord def test_quote_range range = "1,2]'; SELECT * FROM users; --".."a" type = OID::Range.new(Type::Integer.new, :int8range) - assert_equal "'[1,0]'", @conn.quote(type.type_cast_for_database(range)) + assert_equal "'[1,0]'", @conn.quote(type.serialize(range)) end def test_quote_bit_string value = "'); SELECT * FROM users; /*\n01\n*/--" type = OID::Bit.new - assert_equal nil, @conn.quote(type.type_cast_for_database(value)) + assert_equal nil, @conn.quote(type.serialize(value)) end end end diff --git a/activerecord/test/cases/adapters/postgresql/range_test.rb b/activerecord/test/cases/adapters/postgresql/range_test.rb index 70cf21100a..b6b451ca5c 100644 --- a/activerecord/test/cases/adapters/postgresql/range_test.rb +++ b/activerecord/test/cases/adapters/postgresql/range_test.rb @@ -91,7 +91,7 @@ _SQL end teardown do - @connection.execute 'DROP TABLE IF EXISTS postgresql_ranges' + @connection.drop_table 'postgresql_ranges', if_exists: true @connection.execute 'DROP TYPE IF EXISTS floatrange' reset_connection end diff --git a/activerecord/test/cases/adapters/postgresql/referential_integrity_test.rb b/activerecord/test/cases/adapters/postgresql/referential_integrity_test.rb new file mode 100644 index 0000000000..98291f1bbf --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/referential_integrity_test.rb @@ -0,0 +1,89 @@ +require 'cases/helper' +require 'support/connection_helper' + +class PostgreSQLReferentialIntegrityTest < ActiveRecord::TestCase + self.use_transactional_fixtures = false + + include ConnectionHelper + + module MissingSuperuserPrivileges + def execute(sql) + if sql.match(/DISABLE TRIGGER ALL/) || sql.match(/ENABLE TRIGGER ALL/) + super "BROKEN;" rescue nil # put transaction in broken state + raise ActiveRecord::StatementInvalid, 'PG::InsufficientPrivilege' + else + super + end + end + end + + def setup + @connection = ActiveRecord::Base.connection + end + + def teardown + reset_connection + if ActiveRecord::Base.connection.is_a?(MissingSuperuserPrivileges) + raise "MissingSuperuserPrivileges patch was not removed" + end + end + + def test_should_reraise_invalid_foreign_key_exception_and_show_warning + @connection.extend MissingSuperuserPrivileges + + warning = capture(:stderr) do + e = assert_raises(ActiveRecord::InvalidForeignKey) do + @connection.disable_referential_integrity do + raise ActiveRecord::InvalidForeignKey, 'Should be re-raised' + end + end + assert_equal 'Should be re-raised', e.message + end + assert_match (/WARNING: Rails was not able to disable referential integrity/), warning + assert_match (/cause: PG::InsufficientPrivilege/), warning + end + + def test_does_not_print_warning_if_no_invalid_foreign_key_exception_was_raised + @connection.extend MissingSuperuserPrivileges + + warning = capture(:stderr) do + e = assert_raises(ActiveRecord::StatementInvalid) do + @connection.disable_referential_integrity do + raise ActiveRecord::StatementInvalid, 'Should be re-raised' + end + end + assert_equal 'Should be re-raised', e.message + end + assert warning.blank?, "expected no warnings but got:\n#{warning}" + end + + def test_does_not_break_transactions + @connection.extend MissingSuperuserPrivileges + + @connection.transaction do + @connection.disable_referential_integrity do + assert_transaction_is_not_broken + end + assert_transaction_is_not_broken + end + end + + def test_does_not_break_nested_transactions + @connection.extend MissingSuperuserPrivileges + + @connection.transaction do + @connection.transaction(requires_new: true) do + @connection.disable_referential_integrity do + assert_transaction_is_not_broken + end + end + assert_transaction_is_not_broken + end + end + + private + + def assert_transaction_is_not_broken + assert_equal "1", @connection.select_value("SELECT 1") + end +end diff --git a/activerecord/test/cases/adapters/postgresql/rename_table_test.rb b/activerecord/test/cases/adapters/postgresql/rename_table_test.rb index 056a035622..f507328868 100644 --- a/activerecord/test/cases/adapters/postgresql/rename_table_test.rb +++ b/activerecord/test/cases/adapters/postgresql/rename_table_test.rb @@ -7,8 +7,8 @@ class PostgresqlRenameTableTest < ActiveRecord::TestCase end def teardown - @connection.execute 'DROP TABLE IF EXISTS "before_rename"' - @connection.execute 'DROP TABLE IF EXISTS "after_rename"' + @connection.drop_table "before_rename", if_exists: true + @connection.drop_table "after_rename", if_exists: true end test "renaming a table also renames the primary key index" do diff --git a/activerecord/test/cases/adapters/postgresql/schema_test.rb b/activerecord/test/cases/adapters/postgresql/schema_test.rb index 83e35ad1a1..77ff6d01bc 100644 --- a/activerecord/test/cases/adapters/postgresql/schema_test.rb +++ b/activerecord/test/cases/adapters/postgresql/schema_test.rb @@ -460,8 +460,8 @@ class SchemaForeignKeyTest < ActiveRecord::TestCase output = dump_table_schema "wagons" assert_match %r{\s+add_foreign_key "wagons", "my_schema\.trains", column: "train_id"$}, output ensure - @connection.execute "DROP TABLE IF EXISTS wagons" - @connection.execute "DROP TABLE IF EXISTS my_schema.trains" + @connection.drop_table "wagons", if_exists: true + @connection.drop_table "my_schema.trains", if_exists: true @connection.execute "DROP SCHEMA IF EXISTS my_schema" end end diff --git a/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb b/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb index 4506e874bc..c0907b8f21 100644 --- a/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb +++ b/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb @@ -18,8 +18,8 @@ class PostgresqlTypeLookupTest < ActiveRecord::TestCase bigint_array = @connection.type_map.lookup(1016, -1, "bigint[]") big_array = [123456789123456789] - assert_raises(RangeError) { int_array.type_cast_for_database(big_array) } - assert_equal "{123456789123456789}", bigint_array.type_cast_for_database(big_array) + assert_raises(RangeError) { int_array.serialize(big_array) } + assert_equal "{123456789123456789}", bigint_array.serialize(big_array) end test "range types correctly respect registration of subtypes" do @@ -27,7 +27,7 @@ class PostgresqlTypeLookupTest < ActiveRecord::TestCase bigint_range = @connection.type_map.lookup(3926, -1, "int8range") big_range = 0..123456789123456789 - assert_raises(RangeError) { int_range.type_cast_for_database(big_range) } - assert_equal "[0,123456789123456789]", bigint_range.type_cast_for_database(big_range) + assert_raises(RangeError) { int_range.serialize(big_range) } + assert_equal "[0,123456789123456789]", bigint_range.serialize(big_range) end end diff --git a/activerecord/test/cases/adapters/postgresql/uuid_test.rb b/activerecord/test/cases/adapters/postgresql/uuid_test.rb index 6693843497..1219e197ab 100644 --- a/activerecord/test/cases/adapters/postgresql/uuid_test.rb +++ b/activerecord/test/cases/adapters/postgresql/uuid_test.rb @@ -7,7 +7,7 @@ module PostgresqlUUIDHelper end def drop_table(name) - connection.execute "drop table if exists #{name}" + connection.drop_table name, if_exists: true end end diff --git a/activerecord/test/cases/adapters/postgresql/xml_test.rb b/activerecord/test/cases/adapters/postgresql/xml_test.rb index 05b34dcf7d..b097deb2f4 100644 --- a/activerecord/test/cases/adapters/postgresql/xml_test.rb +++ b/activerecord/test/cases/adapters/postgresql/xml_test.rb @@ -22,7 +22,7 @@ class PostgresqlXMLTest < ActiveRecord::TestCase end teardown do - @connection.execute 'drop table if exists xml_data_type' + @connection.drop_table 'xml_data_type', if_exists: true end def test_column diff --git a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb index c1d9b7c273..243f65df98 100644 --- a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb @@ -87,7 +87,7 @@ module ActiveRecord value = "hello".encode('ascii-8bit') type = Type::String.new - assert_equal "'hello'", @conn.quote(type.type_cast_for_database(value)) + assert_equal "'hello'", @conn.quote(type.serialize(value)) end end end diff --git a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb index f916d99bcf..5ca3c92027 100644 --- a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb @@ -22,7 +22,7 @@ module ActiveRecord def test_bad_connection assert_raise ActiveRecord::NoDatabaseError do connection = ActiveRecord::Base.sqlite3_connection(adapter: "sqlite3", database: "/tmp/should/_not/_exist/-cinco-dog.db") - connection.exec_query('drop table if exists ex') + connection.drop_table 'ex', if_exists: true end end @@ -191,7 +191,7 @@ module ActiveRecord binary.save! assert_equal str, binary.data ensure - DualEncoding.connection.execute('DROP TABLE IF EXISTS dual_encodings') + DualEncoding.connection.drop_table 'dual_encodings', if_exists: true end def test_type_cast_should_not_mutate_encoding diff --git a/activerecord/test/cases/associations/belongs_to_associations_test.rb b/activerecord/test/cases/associations/belongs_to_associations_test.rb index a425b3ed88..47fd7345c8 100644 --- a/activerecord/test/cases/associations/belongs_to_associations_test.rb +++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb @@ -58,6 +58,56 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase end end + def test_optional_relation + original_value = ActiveRecord::Base.belongs_to_required_by_default + ActiveRecord::Base.belongs_to_required_by_default = true + + model = Class.new(ActiveRecord::Base) do + self.table_name = "accounts" + def self.name; "Temp"; end + belongs_to :company, optional: true + end + + account = model.new + assert account.valid? + ensure + ActiveRecord::Base.belongs_to_required_by_default = original_value + end + + def test_not_optional_relation + original_value = ActiveRecord::Base.belongs_to_required_by_default + ActiveRecord::Base.belongs_to_required_by_default = true + + model = Class.new(ActiveRecord::Base) do + self.table_name = "accounts" + def self.name; "Temp"; end + belongs_to :company, optional: false + end + + account = model.new + refute account.valid? + assert_equal [{error: :blank}], account.errors.details[:company] + ensure + ActiveRecord::Base.belongs_to_required_by_default = original_value + end + + def test_required_belongs_to_config + original_value = ActiveRecord::Base.belongs_to_required_by_default + ActiveRecord::Base.belongs_to_required_by_default = true + + model = Class.new(ActiveRecord::Base) do + self.table_name = "accounts" + def self.name; "Temp"; end + belongs_to :company + end + + account = model.new + refute account.valid? + assert_equal [{error: :blank}], account.errors.details[:company] + ensure + ActiveRecord::Base.belongs_to_required_by_default = original_value + end + def test_default_scope_on_relations_is_not_cached counter = 0 diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index fde7b0ea3a..eebeaad7cf 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -33,6 +33,8 @@ require 'models/ship' require 'models/tyre' require 'models/subscriber' require 'models/subscription' +require 'models/zine' +require 'models/interest' class HasManyAssociationsTestForReorderWithJoinDependency < ActiveRecord::TestCase fixtures :authors, :posts, :comments @@ -97,7 +99,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase fixtures :accounts, :categories, :companies, :developers, :projects, :developers_projects, :topics, :authors, :comments, :posts, :readers, :taggings, :cars, :jobs, :tags, - :categorizations + :categorizations, :zines, :interests def setup Client.destroyed_client_ids.clear @@ -435,6 +437,32 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal companies(:another_first_firm_client), companies(:first_firm).clients_sorted_desc.find_by_type('Client') end + def test_taking + posts(:other_by_bob).destroy + assert_equal posts(:misc_by_bob), authors(:bob).posts.take + assert_equal posts(:misc_by_bob), authors(:bob).posts.take! + authors(:bob).posts.to_a + assert_equal posts(:misc_by_bob), authors(:bob).posts.take + assert_equal posts(:misc_by_bob), authors(:bob).posts.take! + end + + def test_taking_not_found + authors(:bob).posts.delete_all + assert_raise(ActiveRecord::RecordNotFound) { authors(:bob).posts.take! } + authors(:bob).posts.to_a + assert_raise(ActiveRecord::RecordNotFound) { authors(:bob).posts.take! } + end + + def test_taking_with_inverse_of + interests(:woodsmanship).destroy + interests(:survival).destroy + + zine = zines(:going_out) + interest = zine.interests.take + assert_equal interests(:hunting), interest + assert_same zine, interest.zine + end + def test_cant_save_has_many_readonly_association authors(:david).readonly_comments.each { |c| assert_raise(ActiveRecord::ReadOnlyRecord) { c.save! } } authors(:david).readonly_comments.each { |c| assert c.readonly? } diff --git a/activerecord/test/cases/associations_test.rb b/activerecord/test/cases/associations_test.rb index de358114ab..3d202a5527 100644 --- a/activerecord/test/cases/associations_test.rb +++ b/activerecord/test/cases/associations_test.rb @@ -43,28 +43,6 @@ class AssociationsTest < ActiveRecord::TestCase assert_equal favs, fav2 end - def test_clear_association_cache_stored - firm = Firm.find(1) - assert_kind_of Firm, firm - - firm.clear_association_cache - assert_equal Firm.find(1).clients.collect(&:name).sort, firm.clients.collect(&:name).sort - end - - def test_clear_association_cache_new_record - firm = Firm.new - client_stored = Client.find(3) - client_new = Client.new - client_new.name = "The Joneses" - clients = [ client_stored, client_new ] - - firm.clients << clients - assert_equal clients.map(&:name).to_set, firm.clients.map(&:name).to_set - - firm.clear_association_cache - assert_equal clients.map(&:name).to_set, firm.clients.map(&:name).to_set - end - def test_loading_the_association_target_should_keep_child_records_marked_for_destruction ship = Ship.create!(:name => "The good ship Dollypop") part = ship.parts.create!(:name => "Mast") diff --git a/activerecord/test/cases/attribute_decorators_test.rb b/activerecord/test/cases/attribute_decorators_test.rb index 0b96319cbd..2aeb2601c2 100644 --- a/activerecord/test/cases/attribute_decorators_test.rb +++ b/activerecord/test/cases/attribute_decorators_test.rb @@ -12,11 +12,11 @@ module ActiveRecord super(delegate) end - def type_cast_from_user(value) + def cast(value) "#{super} #{@decoration}" end - alias type_cast_from_database type_cast_from_user + alias deserialize cast end setup do @@ -102,11 +102,11 @@ module ActiveRecord end class Multiplier < SimpleDelegator - def type_cast_from_user(value) + def cast(value) return if value.nil? value * 2 end - alias type_cast_from_database type_cast_from_user + alias deserialize cast end test "decorating with a proc" do diff --git a/activerecord/test/cases/attribute_set_test.rb b/activerecord/test/cases/attribute_set_test.rb index 112cd2fb14..9d927481ec 100644 --- a/activerecord/test/cases/attribute_set_test.rb +++ b/activerecord/test/cases/attribute_set_test.rb @@ -151,12 +151,12 @@ module ActiveRecord end class MyType - def type_cast_from_user(value) + def cast(value) return if value.nil? value + " from user" end - def type_cast_from_database(value) + def deserialize(value) return if value.nil? value + " from database" end diff --git a/activerecord/test/cases/attribute_test.rb b/activerecord/test/cases/attribute_test.rb index eac73e11e8..aa419c7a67 100644 --- a/activerecord/test/cases/attribute_test.rb +++ b/activerecord/test/cases/attribute_test.rb @@ -13,7 +13,7 @@ module ActiveRecord end test "from_database + read type casts from database" do - @type.expect(:type_cast_from_database, 'type cast from database', ['a value']) + @type.expect(:deserialize, 'type cast from database', ['a value']) attribute = Attribute.from_database(nil, 'a value', @type) type_cast_value = attribute.value @@ -22,7 +22,7 @@ module ActiveRecord end test "from_user + read type casts from user" do - @type.expect(:type_cast_from_user, 'type cast from user', ['a value']) + @type.expect(:cast, 'type cast from user', ['a value']) attribute = Attribute.from_user(nil, 'a value', @type) type_cast_value = attribute.value @@ -31,7 +31,7 @@ module ActiveRecord end test "reading memoizes the value" do - @type.expect(:type_cast_from_database, 'from the database', ['whatever']) + @type.expect(:deserialize, 'from the database', ['whatever']) attribute = Attribute.from_database(nil, 'whatever', @type) type_cast_value = attribute.value @@ -42,7 +42,7 @@ module ActiveRecord end test "reading memoizes falsy values" do - @type.expect(:type_cast_from_database, false, ['whatever']) + @type.expect(:deserialize, false, ['whatever']) attribute = Attribute.from_database(nil, 'whatever', @type) attribute.value @@ -58,27 +58,27 @@ module ActiveRecord end test "from_database + read_for_database type casts to and from database" do - @type.expect(:type_cast_from_database, 'read from database', ['whatever']) - @type.expect(:type_cast_for_database, 'ready for database', ['read from database']) + @type.expect(:deserialize, 'read from database', ['whatever']) + @type.expect(:serialize, 'ready for database', ['read from database']) attribute = Attribute.from_database(nil, 'whatever', @type) - type_cast_for_database = attribute.value_for_database + serialize = attribute.value_for_database - assert_equal 'ready for database', type_cast_for_database + assert_equal 'ready for database', serialize end test "from_user + read_for_database type casts from the user to the database" do - @type.expect(:type_cast_from_user, 'read from user', ['whatever']) - @type.expect(:type_cast_for_database, 'ready for database', ['read from user']) + @type.expect(:cast, 'read from user', ['whatever']) + @type.expect(:serialize, 'ready for database', ['read from user']) attribute = Attribute.from_user(nil, 'whatever', @type) - type_cast_for_database = attribute.value_for_database + serialize = attribute.value_for_database - assert_equal 'ready for database', type_cast_for_database + assert_equal 'ready for database', serialize end test "duping dups the value" do - @type.expect(:type_cast_from_database, 'type cast', ['a value']) + @type.expect(:deserialize, 'type cast', ['a value']) attribute = Attribute.from_database(nil, 'a value', @type) value_from_orig = attribute.value @@ -90,7 +90,7 @@ module ActiveRecord end test "duping does not dup the value if it is not dupable" do - @type.expect(:type_cast_from_database, false, ['a value']) + @type.expect(:deserialize, false, ['a value']) attribute = Attribute.from_database(nil, 'a value', @type) assert_same attribute.value, attribute.dup.value @@ -102,11 +102,11 @@ module ActiveRecord end class MyType - def type_cast_from_user(value) + def cast(value) value + " from user" end - def type_cast_from_database(value) + def deserialize(value) value + " from database" end end diff --git a/activerecord/test/cases/attributes_test.rb b/activerecord/test/cases/attributes_test.rb index a753e8b74e..e7b76b1cf9 100644 --- a/activerecord/test/cases/attributes_test.rb +++ b/activerecord/test/cases/attributes_test.rb @@ -108,11 +108,11 @@ module ActiveRecord test "the given default value is cast from user" do custom_type = Class.new(Type::Value) do - def type_cast_from_user(*) + def cast(*) "from user" end - def type_cast_from_database(*) + def deserialize(*) "from database" end end diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index ef1173a2ba..993350ebd6 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -812,7 +812,6 @@ class BasicsTest < ActiveRecord::TestCase def test_dup_does_not_copy_associations author = authors(:david) assert_not_equal [], author.posts - author.send(:clear_association_cache) author_dup = author.dup assert_equal [], author_dup.posts @@ -1427,7 +1426,7 @@ class BasicsTest < ActiveRecord::TestCase attrs.delete 'id' typecast = Class.new(ActiveRecord::Type::Value) { - def type_cast value + def cast value "t.lo" end } diff --git a/activerecord/test/cases/batches_test.rb b/activerecord/test/cases/batches_test.rb index c05382598b..9e428098e4 100644 --- a/activerecord/test/cases/batches_test.rb +++ b/activerecord/test/cases/batches_test.rb @@ -37,9 +37,9 @@ class EachTest < ActiveRecord::TestCase if Enumerator.method_defined? :size def test_each_should_return_a_sized_enumerator - assert_equal 11, Post.find_each(:batch_size => 1).size - assert_equal 5, Post.find_each(:batch_size => 2, :start => 7).size - assert_equal 11, Post.find_each(:batch_size => 10_000).size + assert_equal 11, Post.find_each(batch_size: 1).size + assert_equal 5, Post.find_each(batch_size: 2, begin_at: 7).size + assert_equal 11, Post.find_each(batch_size: 10_000).size end end @@ -99,7 +99,7 @@ class EachTest < ActiveRecord::TestCase def test_find_in_batches_should_start_from_the_start_option assert_queries(@total) do - Post.find_in_batches(:batch_size => 1, :start => 2) do |batch| + Post.find_in_batches(batch_size: 1, begin_at: 2) do |batch| assert_kind_of Array, batch assert_kind_of Post, batch.first end @@ -172,7 +172,7 @@ class EachTest < ActiveRecord::TestCase def test_find_in_batches_should_not_modify_passed_options assert_nothing_raised do - Post.find_in_batches({ batch_size: 42, start: 1 }.freeze){} + Post.find_in_batches({ batch_size: 42, begin_at: 1 }.freeze){} end end @@ -181,7 +181,7 @@ class EachTest < ActiveRecord::TestCase start_nick = nick_order_subscribers.second.nick subscribers = [] - Subscriber.find_in_batches(:batch_size => 1, :start => start_nick) do |batch| + Subscriber.find_in_batches(batch_size: 1, begin_at: start_nick) do |batch| subscribers.concat(batch) end @@ -209,11 +209,32 @@ class EachTest < ActiveRecord::TestCase end end + def test_find_in_batches_start_deprecated + assert_deprecated do + assert_queries(@total) do + Post.find_in_batches(batch_size: 1, start: 2) do |batch| + assert_kind_of Array, batch + assert_kind_of Post, batch.first + end + end + end + end + + def test_find_each_start_deprecated + assert_deprecated do + assert_queries(@total) do + Post.find_each(batch_size: 1, start: 2) do |post| + assert_kind_of Post, post + end + end + end + end + if Enumerator.method_defined? :size def test_find_in_batches_should_return_a_sized_enumerator assert_equal 11, Post.find_in_batches(:batch_size => 1).size assert_equal 6, Post.find_in_batches(:batch_size => 2).size - assert_equal 4, Post.find_in_batches(:batch_size => 2, :start => 4).size + assert_equal 4, Post.find_in_batches(batch_size: 2, begin_at: 4).size assert_equal 4, Post.find_in_batches(:batch_size => 3).size assert_equal 1, Post.find_in_batches(:batch_size => 10_000).size end diff --git a/activerecord/test/cases/connection_adapters/type_lookup_test.rb b/activerecord/test/cases/connection_adapters/type_lookup_test.rb index ac2e0053b8..05c57985a1 100644 --- a/activerecord/test/cases/connection_adapters/type_lookup_test.rb +++ b/activerecord/test/cases/connection_adapters/type_lookup_test.rb @@ -90,7 +90,7 @@ module ActiveRecord cast_type = @connection.type_map.lookup(type) assert_equal :decimal, cast_type.type - assert_equal 2, cast_type.type_cast_from_user(2.1) + assert_equal 2, cast_type.cast(2.1) end end diff --git a/activerecord/test/cases/date_time_precision_test.rb b/activerecord/test/cases/date_time_precision_test.rb index 720f10a9d3..6a4e64b22c 100644 --- a/activerecord/test/cases/date_time_precision_test.rb +++ b/activerecord/test/cases/date_time_precision_test.rb @@ -71,9 +71,18 @@ class DateTimePrecisionTest < ActiveRecord::TestCase assert_equal 999900, foo.updated_at.usec end + def test_schema_dump_includes_datetime_precision + @connection.create_table(:foos, force: true) do |t| + t.timestamps precision: 6 + end + output = dump_table_schema("foos") + assert_match %r{t\.datetime\s+"created_at",\s+precision: 6,\s+null: false$}, output + assert_match %r{t\.datetime\s+"updated_at",\s+precision: 6,\s+null: false$}, output + end + if current_adapter?(:PostgreSQLAdapter) def test_datetime_precision_with_zero_should_be_dumped - @connection.create_table(:foos) do |t| + @connection.create_table(:foos, force: true) do |t| t.timestamps precision: 0 end output = dump_table_schema("foos") diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb index c2573ac72b..3a7cc572e6 100644 --- a/activerecord/test/cases/dirty_test.rb +++ b/activerecord/test/cases/dirty_test.rb @@ -623,13 +623,13 @@ class DirtyTest < ActiveRecord::TestCase end end - test "defaults with type that implements `type_cast_for_database`" do + test "defaults with type that implements `serialize`" do type = Class.new(ActiveRecord::Type::Value) do - def type_cast(value) + def cast(value) value.to_i end - def type_cast_for_database(value) + def serialize(value) value.to_s end end diff --git a/activerecord/test/cases/enum_test.rb b/activerecord/test/cases/enum_test.rb index ed568413a2..3b7bbcf47a 100644 --- a/activerecord/test/cases/enum_test.rb +++ b/activerecord/test/cases/enum_test.rb @@ -26,15 +26,47 @@ class EnumTest < ActiveRecord::TestCase assert_equal @book, Book.unread.first end - test "build from scope" do - assert Book.proposed.build.proposed? - refute Book.proposed.build.written? - assert Book.where(status: Book.statuses[:proposed]).build.proposed? + test "find via where with values" do + proposed, written = Book.statuses[:proposed], Book.statuses[:written] + + assert_equal @book, Book.where(status: proposed).first + refute_equal @book, Book.where(status: written).first + assert_equal @book, Book.where(status: [proposed]).first + refute_equal @book, Book.where(status: [written]).first + refute_equal @book, Book.where("status <> ?", proposed).first + assert_equal @book, Book.where("status <> ?", written).first + end + + test "find via where with symbols" do + assert_equal @book, Book.where(status: :proposed).first + refute_equal @book, Book.where(status: :written).first + assert_equal @book, Book.where(status: [:proposed]).first + refute_equal @book, Book.where(status: [:written]).first + refute_equal @book, Book.where.not(status: :proposed).first + assert_equal @book, Book.where.not(status: :written).first end - test "find via where" do + test "find via where with strings" do assert_equal @book, Book.where(status: "proposed").first refute_equal @book, Book.where(status: "written").first + assert_equal @book, Book.where(status: ["proposed"]).first + refute_equal @book, Book.where(status: ["written"]).first + refute_equal @book, Book.where.not(status: "proposed").first + assert_equal @book, Book.where.not(status: "written").first + end + + test "build from scope" do + assert Book.written.build.written? + refute Book.written.build.proposed? + end + + test "build from where" do + assert Book.where(status: Book.statuses[:written]).build.written? + refute Book.where(status: Book.statuses[:written]).build.proposed? + assert Book.where(status: :written).build.written? + refute Book.where(status: :written).build.proposed? + assert Book.where(status: "written").build.written? + refute Book.where(status: "written").build.proposed? end test "update by declaration" do @@ -140,19 +172,24 @@ class EnumTest < ActiveRecord::TestCase assert_equal "'unknown' is not a valid status", e.message end + test "NULL values from database should be casted to nil" do + Book.where(id: @book.id).update_all("status = NULL") + assert_nil @book.reload.status + end + test "assign nil value" do @book.status = nil - assert @book.status.nil? + assert_nil @book.status end test "assign empty string value" do @book.status = '' - assert @book.status.nil? + assert_nil @book.status end test "assign long empty string value" do @book.status = ' ' - assert @book.status.nil? + assert_nil @book.status end test "constant to access the mapping" do @@ -192,9 +229,10 @@ class EnumTest < ActiveRecord::TestCase ] conflicts.each_with_index do |name, i| - assert_raises(ArgumentError, "enum name `#{name}` should not be allowed") do + e = assert_raises(ArgumentError) do klass.class_eval { enum name => ["value_#{i}"] } end + assert_match(/You tried to define an enum named \"#{name}\" on the model/, e.message) end end @@ -214,9 +252,10 @@ class EnumTest < ActiveRecord::TestCase ] conflicts.each_with_index do |value, i| - assert_raises(ArgumentError, "enum value `#{value}` should not be allowed") do + e = assert_raises(ArgumentError, "enum value `#{value}` should not be allowed") do klass.class_eval { enum "status_#{i}" => [value] } end + assert_match(/You tried to define an enum named .* on the model/, e.message) end end diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb index f41245dfd2..7ef2ebc998 100644 --- a/activerecord/test/cases/fixtures_test.rb +++ b/activerecord/test/cases/fixtures_test.rb @@ -273,7 +273,7 @@ class HasManyThroughFixture < ActiveSupport::TestCase Class.new(ActiveRecord::Base) { define_singleton_method(:name) { name } } end - def test_has_many_through + def test_has_many_through_with_default_table_name pt = make_model "ParrotTreasure" parrot = make_model "Parrot" treasure = make_model "Treasure" @@ -292,6 +292,24 @@ class HasManyThroughFixture < ActiveSupport::TestCase assert_equal load_has_and_belongs_to_many['parrots_treasures'], rows['parrots_treasures'] end + def test_has_many_through_with_renamed_table + pt = make_model "ParrotTreasure" + parrot = make_model "Parrot" + treasure = make_model "Treasure" + + pt.belongs_to :parrot, :class => parrot + pt.belongs_to :treasure, :class => treasure + + parrot.has_many :parrot_treasures, :class => pt + parrot.has_many :treasures, :through => :parrot_treasures + + parrots = File.join FIXTURES_ROOT, 'parrots' + + fs = ActiveRecord::FixtureSet.new parrot.connection, "parrots", parrot, parrots + rows = fs.table_rows + assert_equal load_has_and_belongs_to_many['parrots_treasures'], rows['parrot_treasures'] + end + def load_has_and_belongs_to_many parrot = make_model "Parrot" parrot.has_and_belongs_to_many :treasures diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb index f1f927852c..f2ba28a32f 100644 --- a/activerecord/test/cases/helper.rb +++ b/activerecord/test/cases/helper.rb @@ -46,7 +46,7 @@ def in_memory_db? end def mysql_56? - current_adapter?(:Mysql2Adapter) && + current_adapter?(:MysqlAdapter, :Mysql2Adapter) && ActiveRecord::Base.connection.send(:version).join(".") >= "5.6.0" end diff --git a/activerecord/test/cases/inheritance_test.rb b/activerecord/test/cases/inheritance_test.rb index 6b18bfe84f..3268555cb8 100644 --- a/activerecord/test/cases/inheritance_test.rb +++ b/activerecord/test/cases/inheritance_test.rb @@ -121,6 +121,12 @@ class InheritanceTest < ActiveRecord::TestCase assert_kind_of Cabbage, cabbage end + def test_becomes_and_change_tracking_for_inheritance_columns + cucumber = Vegetable.find(1) + cabbage = cucumber.becomes!(Cabbage) + assert_equal ['Cucumber', 'Cabbage'], cabbage.custom_type_change + end + def test_alt_becomes_bang_resets_inheritance_type_column vegetable = Vegetable.create!(name: "Red Pepper") assert_nil vegetable.custom_type diff --git a/activerecord/test/cases/migration/change_schema_test.rb b/activerecord/test/cases/migration/change_schema_test.rb index 3cd4073a38..30c91dfdcb 100644 --- a/activerecord/test/cases/migration/change_schema_test.rb +++ b/activerecord/test/cases/migration/change_schema_test.rb @@ -68,8 +68,8 @@ module ActiveRecord five = columns.detect { |c| c.name == "five" } unless mysql assert_equal "hello", one.default - assert_equal true, connection.lookup_cast_type_from_column(two).type_cast_from_database(two.default) - assert_equal false, connection.lookup_cast_type_from_column(three).type_cast_from_database(three.default) + assert_equal true, connection.lookup_cast_type_from_column(two).deserialize(two.default) + assert_equal false, connection.lookup_cast_type_from_column(three).deserialize(three.default) assert_equal '1', four.default assert_equal "hello", five.default unless mysql end diff --git a/activerecord/test/cases/migration/change_table_test.rb b/activerecord/test/cases/migration/change_table_test.rb index 7010af5434..2ffe7a1b0d 100644 --- a/activerecord/test/cases/migration/change_table_test.rb +++ b/activerecord/test/cases/migration/change_table_test.rb @@ -13,7 +13,7 @@ module ActiveRecord end def with_change_table - yield ConnectionAdapters::Table.new(:delete_me, @connection) + yield ActiveRecord::Base.connection.update_table_definition(:delete_me, @connection) end def test_references_column_type_adds_id @@ -100,6 +100,13 @@ module ActiveRecord end end + def test_primary_key_creates_primary_key_column + with_change_table do |t| + @connection.expect :add_column, nil, [:delete_me, :id, :primary_key, primary_key: true, first: true] + t.primary_key :id, first: true + end + end + def test_integer_creates_integer_column with_change_table do |t| @connection.expect :add_column, nil, [:delete_me, :foo, :integer, {}] @@ -108,6 +115,14 @@ module ActiveRecord end end + def test_bigint_creates_bigint_column + with_change_table do |t| + @connection.expect :add_column, nil, [:delete_me, :foo, :bigint, {}] + @connection.expect :add_column, nil, [:delete_me, :bar, :bigint, {}] + t.bigint :foo, :bar + end + end + def test_string_creates_string_column with_change_table do |t| @connection.expect :add_column, nil, [:delete_me, :foo, :string, {}] @@ -116,6 +131,24 @@ module ActiveRecord end end + if current_adapter?(:PostgreSQLAdapter) + def test_json_creates_json_column + with_change_table do |t| + @connection.expect :add_column, nil, [:delete_me, :foo, :json, {}] + @connection.expect :add_column, nil, [:delete_me, :bar, :json, {}] + t.json :foo, :bar + end + end + + def test_xml_creates_xml_column + with_change_table do |t| + @connection.expect :add_column, nil, [:delete_me, :foo, :xml, {}] + @connection.expect :add_column, nil, [:delete_me, :bar, :xml, {}] + t.xml :foo, :bar + end + end + end + def test_column_creates_column with_change_table do |t| @connection.expect :add_column, nil, [:delete_me, :bar, :integer, {}] diff --git a/activerecord/test/cases/migration/columns_test.rb b/activerecord/test/cases/migration/columns_test.rb index 6f65288ac0..e5ccfe0f91 100644 --- a/activerecord/test/cases/migration/columns_test.rb +++ b/activerecord/test/cases/migration/columns_test.rb @@ -196,7 +196,7 @@ module ActiveRecord old_columns = connection.columns(TestModel.table_name) assert old_columns.find { |c| - default = connection.lookup_cast_type_from_column(c).type_cast_from_database(c.default) + default = connection.lookup_cast_type_from_column(c).deserialize(c.default) c.name == 'approved' && c.type == :boolean && default == true } @@ -204,11 +204,11 @@ module ActiveRecord new_columns = connection.columns(TestModel.table_name) assert_not new_columns.find { |c| - default = connection.lookup_cast_type_from_column(c).type_cast_from_database(c.default) + default = connection.lookup_cast_type_from_column(c).deserialize(c.default) c.name == 'approved' and c.type == :boolean and default == true } assert new_columns.find { |c| - default = connection.lookup_cast_type_from_column(c).type_cast_from_database(c.default) + default = connection.lookup_cast_type_from_column(c).deserialize(c.default) c.name == 'approved' and c.type == :boolean and default == false } change_column :test_models, :approved, :boolean, :default => true diff --git a/activerecord/test/cases/migration/foreign_key_test.rb b/activerecord/test/cases/migration/foreign_key_test.rb index b2f2d077eb..7f4790bf3e 100644 --- a/activerecord/test/cases/migration/foreign_key_test.rb +++ b/activerecord/test/cases/migration/foreign_key_test.rb @@ -147,6 +147,27 @@ module ActiveRecord assert_equal :nullify, fk.on_update end + def test_foreign_key_exists + @connection.add_foreign_key :astronauts, :rockets + + assert @connection.foreign_key_exists?(:astronauts, :rockets) + assert_not @connection.foreign_key_exists?(:astronauts, :stars) + end + + def test_foreign_key_exists_by_column + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id" + + assert @connection.foreign_key_exists?(:astronauts, column: "rocket_id") + assert_not @connection.foreign_key_exists?(:astronauts, column: "star_id") + end + + def test_foreign_key_exists_by_name + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", name: "fancy_named_fk" + + assert @connection.foreign_key_exists?(:astronauts, name: "fancy_named_fk") + assert_not @connection.foreign_key_exists?(:astronauts, name: "other_fancy_named_fk") + end + def test_remove_foreign_key_inferes_column @connection.add_foreign_key :astronauts, :rockets diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb index 5f9fd5d527..3b73685a2c 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -508,12 +508,14 @@ class MigrationTest < ActiveRecord::TestCase if current_adapter?(:MysqlAdapter, :Mysql2Adapter, :PostgreSQLAdapter) def test_out_of_range_limit_should_raise Person.connection.drop_table :test_limits rescue nil - assert_raise(ActiveRecord::ActiveRecordError, "integer limit didn't raise") do + e = assert_raise(ActiveRecord::ActiveRecordError, "integer limit didn't raise") do Person.connection.create_table :test_integer_limits, :force => true do |t| t.column :bigone, :integer, :limit => 10 end end + assert_match(/No integer type has byte size 10/, e.message) + unless current_adapter?(:PostgreSQLAdapter) assert_raise(ActiveRecord::ActiveRecordError, "text limit didn't raise") do Person.connection.create_table :test_text_limits, :force => true do |t| diff --git a/activerecord/test/cases/primary_keys_test.rb b/activerecord/test/cases/primary_keys_test.rb index 4b668f84dd..1ea1ef5e12 100644 --- a/activerecord/test/cases/primary_keys_test.rb +++ b/activerecord/test/cases/primary_keys_test.rb @@ -260,7 +260,7 @@ if current_adapter?(:PostgreSQLAdapter, :MysqlAdapter, :Mysql2Adapter) end teardown do - @connection.execute("DROP TABLE IF EXISTS widgets") + @connection.drop_table 'widgets', if_exists: true end test "primary key column type with bigserial" do @@ -282,5 +282,15 @@ if current_adapter?(:PostgreSQLAdapter, :MysqlAdapter, :Mysql2Adapter) assert_match %r{create_table "widgets", id: :bigint}, schema end end + + if current_adapter?(:MysqlAdapter, :Mysql2Adapter) + test "primary key column type with options" do + @connection.create_table(:widgets, id: :primary_key, limit: 8, force: true) + column = @connection.columns(:widgets).find { |c| c.name == 'id' } + assert column.auto_increment? + assert_equal :integer, column.type + assert_equal 8, column.limit + end + end end end diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb index b0e40c7145..67e9bef808 100644 --- a/activerecord/test/cases/reflection_test.rb +++ b/activerecord/test/cases/reflection_test.rb @@ -92,9 +92,9 @@ class ReflectionTest < ActiveRecord::TestCase type = @first.type_for_attribute("attribute_that_doesnt_exist") object = Object.new - assert_equal object, type.type_cast_from_database(object) - assert_equal object, type.type_cast_from_user(object) - assert_equal object, type.type_cast_for_database(object) + assert_equal object, type.deserialize(object) + assert_equal object, type.cast(object) + assert_equal object, type.serialize(object) end def test_reflection_klass_for_nested_class_name diff --git a/activerecord/test/cases/relation_test.rb b/activerecord/test/cases/relation_test.rb index c9c05b75a6..9353be1ba7 100644 --- a/activerecord/test/cases/relation_test.rb +++ b/activerecord/test/cases/relation_test.rb @@ -254,12 +254,12 @@ module ActiveRecord :string end - def type_cast_from_database(value) + def deserialize(value) raise value unless value == "type cast for database" "type cast from database" end - def type_cast_for_database(value) + def serialize(value) raise value unless value == "value from user" "type cast for database" end diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index dec3a37f42..0cf44388fa 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -157,6 +157,17 @@ class RelationTest < ActiveRecord::TestCase end end + def test_select_with_subquery_in_from_does_not_use_original_table_name + relation = Comment.group(:type).select('COUNT(post_id) AS post_count, type') + subquery = Comment.from(relation).select('type','post_count') + assert_equal(relation.map(&:post_count).sort,subquery.map(&:post_count).sort) + end + + def test_group_with_subquery_in_from_does_not_use_original_table_name + relation = Comment.group(:type).select('COUNT(post_id) AS post_count,type') + subquery = Comment.from(relation).group('type').average("post_count") + assert_equal(relation.map(&:post_count).sort,subquery.values.sort) + end def test_finding_with_conditions assert_equal ["David"], Author.where(:name => 'David').map(&:name) diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb index bafc9fa81b..513f65f707 100644 --- a/activerecord/test/cases/schema_dumper_test.rb +++ b/activerecord/test/cases/schema_dumper_test.rb @@ -225,17 +225,15 @@ class SchemaDumperTest < ActiveRecord::TestCase assert_match %r{t\.text\s+"long_text",\s+limit: 4294967295$}, output end - def test_schema_dumps_index_type + def test_schema_does_not_include_limit_for_emulated_mysql_boolean_fields output = standard_dump - assert_match %r{add_index "key_tests", \["awesome"\], name: "index_key_tests_on_awesome", type: :fulltext}, output - assert_match %r{add_index "key_tests", \["pizza"\], name: "index_key_tests_on_pizza", using: :btree}, output + assert_no_match %r{t\.boolean\s+"has_fun",.+limit: 1}, output end - end - if mysql_56? - def test_schema_dump_includes_datetime_precision + def test_schema_dumps_index_type output = standard_dump - assert_match %r{t\.datetime\s+"written_on",\s+precision: 6$}, output + assert_match %r{add_index "key_tests", \["awesome"\], name: "index_key_tests_on_awesome", type: :fulltext}, output + assert_match %r{add_index "key_tests", \["pizza"\], name: "index_key_tests_on_pizza", using: :btree}, output end end @@ -362,7 +360,7 @@ class SchemaDumperDefaultsTest < ActiveRecord::TestCase teardown do return unless @connection - @connection.execute 'DROP TABLE defaults' if @connection.table_exists? 'defaults' + @connection.drop_table 'defaults', if_exists: true end def test_schema_dump_defaults_with_universally_supported_types diff --git a/activerecord/test/cases/scoping/named_scoping_test.rb b/activerecord/test/cases/scoping/named_scoping_test.rb index 8cd94ebcc2..e4cc533517 100644 --- a/activerecord/test/cases/scoping/named_scoping_test.rb +++ b/activerecord/test/cases/scoping/named_scoping_test.rb @@ -317,13 +317,15 @@ class NamedScopingTest < ActiveRecord::TestCase ] conflicts.each do |name| - assert_raises(ArgumentError, "scope `#{name}` should not be allowed") do + e = assert_raises(ArgumentError, "scope `#{name}` should not be allowed") do klass.class_eval { scope name, ->{ where(approved: true) } } end + assert_match(/You tried to define a scope named \"#{name}\" on the model/, e.message) - assert_raises(ArgumentError, "scope `#{name}` should not be allowed") do + e = assert_raises(ArgumentError, "scope `#{name}` should not be allowed") do subklass.class_eval { scope name, ->{ where(approved: true) } } end + assert_match(/You tried to define a scope named \"#{name}\" on the model/, e.message) end non_conflicts.each do |name| diff --git a/activerecord/test/cases/suppressor_test.rb b/activerecord/test/cases/suppressor_test.rb new file mode 100644 index 0000000000..1c449d42fe --- /dev/null +++ b/activerecord/test/cases/suppressor_test.rb @@ -0,0 +1,21 @@ +require 'cases/helper' +require 'models/notification' +require 'models/user' + +class SuppressorTest < ActiveRecord::TestCase + def test_suppresses_creation_of_record_generated_by_callback + assert_difference -> { User.count } do + assert_no_difference -> { Notification.count } do + Notification.suppress { UserWithNotification.create! } + end + end + end + + def test_resumes_saving_after_suppression_complete + Notification.suppress { UserWithNotification.create! } + + assert_difference -> { Notification.count } do + Notification.create! + end + end +end diff --git a/activerecord/test/cases/time_precision_test.rb b/activerecord/test/cases/time_precision_test.rb index 23422fd50a..ff4e5ecec5 100644 --- a/activerecord/test/cases/time_precision_test.rb +++ b/activerecord/test/cases/time_precision_test.rb @@ -1,7 +1,13 @@ require 'cases/helper' +require 'support/schema_dumping_helper' if ActiveRecord::Base.connection.supports_datetime_with_precision? class TimePrecisionTest < ActiveRecord::TestCase + include SchemaDumpingHelper + self.use_transactional_fixtures = false + + class Foo < ActiveRecord::Base; end + setup do @connection = ActiveRecord::Base.connection end @@ -45,6 +51,43 @@ class TimePrecisionTest < ActiveRecord::TestCase assert_equal 4, database_datetime_precision('foos', 'finish') end + def test_formatting_time_according_to_precision + @connection.create_table(:foos, force: true) do |t| + t.time :start, precision: 0 + t.time :finish, precision: 4 + end + time = ::Time.utc(2000, 1, 1, 12, 30, 0, 999999) + Foo.create!(start: time, finish: time) + assert foo = Foo.find_by(start: time) + assert_equal 1, Foo.where(finish: time).count + assert_equal time.to_s, foo.start.to_s + assert_equal time.to_s, foo.finish.to_s + assert_equal 000000, foo.start.usec + assert_equal 999900, foo.finish.usec + end + + def test_schema_dump_includes_time_precision + @connection.create_table(:foos, force: true) do |t| + t.time :start, precision: 4 + t.time :finish, precision: 6 + end + output = dump_table_schema("foos") + assert_match %r{t\.time\s+"start",\s+precision: 4$}, output + assert_match %r{t\.time\s+"finish",\s+precision: 6$}, output + end + + if current_adapter?(:PostgreSQLAdapter) + def test_time_precision_with_zero_should_be_dumped + @connection.create_table(:foos, force: true) do |t| + t.time :start, precision: 0 + t.time :finish, precision: 0 + end + output = dump_table_schema("foos") + assert_match %r{t\.time\s+"start",\s+precision: 0$}, output + assert_match %r{t\.time\s+"finish",\s+precision: 0$}, output + end + end + private def database_datetime_precision(table_name, column_name) diff --git a/activerecord/test/cases/timestamp_test.rb b/activerecord/test/cases/timestamp_test.rb index db474c63a4..c0c62527df 100644 --- a/activerecord/test/cases/timestamp_test.rb +++ b/activerecord/test/cases/timestamp_test.rb @@ -73,6 +73,15 @@ class TimestampTest < ActiveRecord::TestCase assert_equal @previously_updated_at, @developer.updated_at end + def test_touching_updates_timestamp_with_given_time + previously_updated_at = @developer.updated_at + new_time = Time.utc(2015, 2, 16, 0, 0, 0) + @developer.touch(time: new_time) + + assert_not_equal previously_updated_at, @developer.updated_at + assert_equal new_time, @developer.updated_at + end + def test_touching_an_attribute_updates_timestamp previously_created_at = @developer.created_at @developer.touch(:created_at) @@ -91,6 +100,18 @@ class TimestampTest < ActiveRecord::TestCase assert_in_delta Time.now, task.ending, 1 end + def test_touching_an_attribute_updates_timestamp_with_given_time + previously_updated_at = @developer.updated_at + previously_created_at = @developer.created_at + new_time = Time.utc(2015, 2, 16, 4, 54, 0) + @developer.touch(:created_at, time: new_time) + + assert_not_equal previously_created_at, @developer.created_at + assert_not_equal previously_updated_at, @developer.updated_at + assert_equal new_time, @developer.created_at + assert_equal new_time, @developer.updated_at + end + def test_touching_many_attributes_updates_them task = Task.first previous_starting = task.starting diff --git a/activerecord/test/cases/transactions_test.rb b/activerecord/test/cases/transactions_test.rb index d1d8e71c34..88e595c39f 100644 --- a/activerecord/test/cases/transactions_test.rb +++ b/activerecord/test/cases/transactions_test.rb @@ -668,7 +668,7 @@ class TransactionTest < ActiveRecord::TestCase end end ensure - connection.execute("DROP TABLE IF EXISTS transaction_without_primary_keys") + connection.drop_table 'transaction_without_primary_keys', if_exists: true end private diff --git a/activerecord/test/cases/type/adapter_specific_registry_test.rb b/activerecord/test/cases/type/adapter_specific_registry_test.rb new file mode 100644 index 0000000000..8b836b4793 --- /dev/null +++ b/activerecord/test/cases/type/adapter_specific_registry_test.rb @@ -0,0 +1,133 @@ +require "cases/helper" + +module ActiveRecord + class AdapterSpecificRegistryTest < ActiveRecord::TestCase + test "a class can be registered for a symbol" do + registry = Type::AdapterSpecificRegistry.new + registry.register(:foo, ::String) + registry.register(:bar, ::Array) + + assert_equal "", registry.lookup(:foo) + assert_equal [], registry.lookup(:bar) + end + + test "a block can be registered" do + registry = Type::AdapterSpecificRegistry.new + registry.register(:foo) do |*args| + [*args, "block for foo"] + end + registry.register(:bar) do |*args| + [*args, "block for bar"] + end + + assert_equal [:foo, 1, "block for foo"], registry.lookup(:foo, 1) + assert_equal [:foo, 2, "block for foo"], registry.lookup(:foo, 2) + assert_equal [:bar, 1, 2, 3, "block for bar"], registry.lookup(:bar, 1, 2, 3) + end + + test "filtering by adapter" do + registry = Type::AdapterSpecificRegistry.new + registry.register(:foo, String, adapter: :sqlite3) + registry.register(:foo, Array, adapter: :postgresql) + + assert_equal "", registry.lookup(:foo, adapter: :sqlite3) + assert_equal [], registry.lookup(:foo, adapter: :postgresql) + end + + test "an error is raised if both a generic and adapter specific type match" do + registry = Type::AdapterSpecificRegistry.new + registry.register(:foo, String) + registry.register(:foo, Array, adapter: :postgresql) + + assert_raises TypeConflictError do + registry.lookup(:foo, adapter: :postgresql) + end + assert_equal "", registry.lookup(:foo, adapter: :sqlite3) + end + + test "a generic type can explicitly override an adapter specific type" do + registry = Type::AdapterSpecificRegistry.new + registry.register(:foo, String, override: true) + registry.register(:foo, Array, adapter: :postgresql) + + assert_equal "", registry.lookup(:foo, adapter: :postgresql) + assert_equal "", registry.lookup(:foo, adapter: :sqlite3) + end + + test "a generic type can explicitly allow an adapter type to be used instead" do + registry = Type::AdapterSpecificRegistry.new + registry.register(:foo, String, override: false) + registry.register(:foo, Array, adapter: :postgresql) + + assert_equal [], registry.lookup(:foo, adapter: :postgresql) + assert_equal "", registry.lookup(:foo, adapter: :sqlite3) + end + + test "a reasonable error is given when no type is found" do + registry = Type::AdapterSpecificRegistry.new + + e = assert_raises(ArgumentError) do + registry.lookup(:foo) + end + + assert_equal "Unknown type :foo", e.message + end + + test "construct args are passed to the type" do + type = Struct.new(:args) + registry = Type::AdapterSpecificRegistry.new + registry.register(:foo, type) + + assert_equal type.new, registry.lookup(:foo) + assert_equal type.new(:ordered_arg), registry.lookup(:foo, :ordered_arg) + assert_equal type.new(keyword: :arg), registry.lookup(:foo, keyword: :arg) + assert_equal type.new(keyword: :arg), registry.lookup(:foo, keyword: :arg, adapter: :postgresql) + end + + test "registering a modifier" do + decoration = Struct.new(:value) + registry = Type::AdapterSpecificRegistry.new + registry.register(:foo, String) + registry.register(:bar, Hash) + registry.add_modifier({ array: true }, decoration) + + assert_equal decoration.new(""), registry.lookup(:foo, array: true) + assert_equal decoration.new({}), registry.lookup(:bar, array: true) + assert_equal "", registry.lookup(:foo) + end + + test "registering multiple modifiers" do + decoration = Struct.new(:value) + other_decoration = Struct.new(:value) + registry = Type::AdapterSpecificRegistry.new + registry.register(:foo, String) + registry.add_modifier({ array: true }, decoration) + registry.add_modifier({ range: true }, other_decoration) + + assert_equal "", registry.lookup(:foo) + assert_equal decoration.new(""), registry.lookup(:foo, array: true) + assert_equal other_decoration.new(""), registry.lookup(:foo, range: true) + assert_equal( + decoration.new(other_decoration.new("")), + registry.lookup(:foo, array: true, range: true) + ) + end + + test "registering adapter specific modifiers" do + decoration = Struct.new(:value) + type = Struct.new(:args) + registry = Type::AdapterSpecificRegistry.new + registry.register(:foo, type) + registry.add_modifier({ array: true }, decoration, adapter: :postgresql) + + assert_equal( + decoration.new(type.new(keyword: :arg)), + registry.lookup(:foo, array: true, adapter: :postgresql, keyword: :arg) + ) + assert_equal( + type.new(array: true), + registry.lookup(:foo, array: true, adapter: :sqlite3) + ) + end + end +end diff --git a/activerecord/test/cases/type/decimal_test.rb b/activerecord/test/cases/type/decimal_test.rb index 34ed1d7b19..fe49d0e79a 100644 --- a/activerecord/test/cases/type/decimal_test.rb +++ b/activerecord/test/cases/type/decimal_test.rb @@ -5,29 +5,29 @@ module ActiveRecord class DecimalTest < ActiveRecord::TestCase def test_type_cast_decimal type = Decimal.new - assert_equal BigDecimal.new("0"), type.type_cast_from_user(BigDecimal.new("0")) - assert_equal BigDecimal.new("123"), type.type_cast_from_user(123.0) - assert_equal BigDecimal.new("1"), type.type_cast_from_user(:"1") + assert_equal BigDecimal.new("0"), type.cast(BigDecimal.new("0")) + assert_equal BigDecimal.new("123"), type.cast(123.0) + assert_equal BigDecimal.new("1"), type.cast(:"1") end def test_type_cast_decimal_from_float_with_large_precision type = Decimal.new(precision: ::Float::DIG + 2) - assert_equal BigDecimal.new("123.0"), type.type_cast_from_user(123.0) + assert_equal BigDecimal.new("123.0"), type.cast(123.0) end def test_type_cast_from_float_with_unspecified_precision type = Decimal.new - assert_equal 22.68.to_d, type.type_cast_from_user(22.68) + assert_equal 22.68.to_d, type.cast(22.68) end def test_type_cast_decimal_from_rational_with_precision type = Decimal.new(precision: 2) - assert_equal BigDecimal("0.33"), type.type_cast_from_user(Rational(1, 3)) + assert_equal BigDecimal("0.33"), type.cast(Rational(1, 3)) end def test_type_cast_decimal_from_rational_without_precision_defaults_to_18_36 type = Decimal.new - assert_equal BigDecimal("0.333333333333333333E0"), type.type_cast_from_user(Rational(1, 3)) + assert_equal BigDecimal("0.333333333333333333E0"), type.cast(Rational(1, 3)) end def test_type_cast_decimal_from_object_responding_to_d @@ -36,7 +36,7 @@ module ActiveRecord BigDecimal.new("1") end type = Decimal.new - assert_equal BigDecimal("1"), type.type_cast_from_user(value) + assert_equal BigDecimal("1"), type.cast(value) end def test_changed? diff --git a/activerecord/test/cases/type/integer_test.rb b/activerecord/test/cases/type/integer_test.rb index 1e836f2142..84fb05dd8e 100644 --- a/activerecord/test/cases/type/integer_test.rb +++ b/activerecord/test/cases/type/integer_test.rb @@ -6,45 +6,45 @@ module ActiveRecord class IntegerTest < ActiveRecord::TestCase test "simple values" do type = Type::Integer.new - assert_equal 1, type.type_cast_from_user(1) - assert_equal 1, type.type_cast_from_user('1') - assert_equal 1, type.type_cast_from_user('1ignore') - assert_equal 0, type.type_cast_from_user('bad1') - assert_equal 0, type.type_cast_from_user('bad') - assert_equal 1, type.type_cast_from_user(1.7) - assert_equal 0, type.type_cast_from_user(false) - assert_equal 1, type.type_cast_from_user(true) - assert_nil type.type_cast_from_user(nil) + assert_equal 1, type.cast(1) + assert_equal 1, type.cast('1') + assert_equal 1, type.cast('1ignore') + assert_equal 0, type.cast('bad1') + assert_equal 0, type.cast('bad') + assert_equal 1, type.cast(1.7) + assert_equal 0, type.cast(false) + assert_equal 1, type.cast(true) + assert_nil type.cast(nil) end test "random objects cast to nil" do type = Type::Integer.new - assert_nil type.type_cast_from_user([1,2]) - assert_nil type.type_cast_from_user({1 => 2}) - assert_nil type.type_cast_from_user((1..2)) + assert_nil type.cast([1,2]) + assert_nil type.cast({1 => 2}) + assert_nil type.cast((1..2)) end test "casting ActiveRecord models" do type = Type::Integer.new firm = Firm.create(:name => 'Apple') - assert_nil type.type_cast_from_user(firm) + assert_nil type.cast(firm) end test "casting objects without to_i" do type = Type::Integer.new - assert_nil type.type_cast_from_user(::Object.new) + assert_nil type.cast(::Object.new) end test "casting nan and infinity" do type = Type::Integer.new - assert_nil type.type_cast_from_user(::Float::NAN) - assert_nil type.type_cast_from_user(1.0/0.0) + assert_nil type.cast(::Float::NAN) + assert_nil type.cast(1.0/0.0) end test "casting booleans for database" do type = Type::Integer.new - assert_equal 1, type.type_cast_for_database(true) - assert_equal 0, type.type_cast_for_database(false) + assert_equal 1, type.serialize(true) + assert_equal 0, type.serialize(false) end test "changed?" do @@ -60,53 +60,53 @@ module ActiveRecord test "values below int min value are out of range" do assert_raises(::RangeError) do - Integer.new.type_cast_for_database(-2147483649) + Integer.new.serialize(-2147483649) end end test "values above int max value are out of range" do assert_raises(::RangeError) do - Integer.new.type_cast_for_database(2147483648) + Integer.new.serialize(2147483648) end end test "very small numbers are out of range" do assert_raises(::RangeError) do - Integer.new.type_cast_for_database(-9999999999999999999999999999999) + Integer.new.serialize(-9999999999999999999999999999999) end end test "very large numbers are out of range" do assert_raises(::RangeError) do - Integer.new.type_cast_for_database(9999999999999999999999999999999) + Integer.new.serialize(9999999999999999999999999999999) end end test "normal numbers are in range" do type = Integer.new - assert_equal(0, type.type_cast_for_database(0)) - assert_equal(-1, type.type_cast_for_database(-1)) - assert_equal(1, type.type_cast_for_database(1)) + assert_equal(0, type.serialize(0)) + assert_equal(-1, type.serialize(-1)) + assert_equal(1, type.serialize(1)) end test "int max value is in range" do - assert_equal(2147483647, Integer.new.type_cast_for_database(2147483647)) + assert_equal(2147483647, Integer.new.serialize(2147483647)) end test "int min value is in range" do - assert_equal(-2147483648, Integer.new.type_cast_for_database(-2147483648)) + assert_equal(-2147483648, Integer.new.serialize(-2147483648)) end test "columns with a larger limit have larger ranges" do type = Integer.new(limit: 8) - assert_equal(9223372036854775807, type.type_cast_for_database(9223372036854775807)) - assert_equal(-9223372036854775808, type.type_cast_for_database(-9223372036854775808)) + assert_equal(9223372036854775807, type.serialize(9223372036854775807)) + assert_equal(-9223372036854775808, type.serialize(-9223372036854775808)) assert_raises(::RangeError) do - type.type_cast_for_database(-9999999999999999999999999999999) + type.serialize(-9999999999999999999999999999999) end assert_raises(::RangeError) do - type.type_cast_for_database(9999999999999999999999999999999) + type.serialize(9999999999999999999999999999999) end end diff --git a/activerecord/test/cases/type/string_test.rb b/activerecord/test/cases/type/string_test.rb index 4d78f287f1..56e9bf434d 100644 --- a/activerecord/test/cases/type/string_test.rb +++ b/activerecord/test/cases/type/string_test.rb @@ -4,16 +4,16 @@ module ActiveRecord class StringTypeTest < ActiveRecord::TestCase test "type casting" do type = Type::String.new - assert_equal "t", type.type_cast_from_user(true) - assert_equal "f", type.type_cast_from_user(false) - assert_equal "123", type.type_cast_from_user(123) + assert_equal "t", type.cast(true) + assert_equal "f", type.cast(false) + assert_equal "123", type.cast(123) end test "values are duped coming out" do s = "foo" type = Type::String.new - assert_not_same s, type.type_cast_from_user(s) - assert_not_same s, type.type_cast_from_database(s) + assert_not_same s, type.cast(s) + assert_not_same s, type.deserialize(s) end test "string mutations are detected" do diff --git a/activerecord/test/cases/type/unsigned_integer_test.rb b/activerecord/test/cases/type/unsigned_integer_test.rb index 4b8e2fb518..f2c910eade 100644 --- a/activerecord/test/cases/type/unsigned_integer_test.rb +++ b/activerecord/test/cases/type/unsigned_integer_test.rb @@ -4,12 +4,12 @@ module ActiveRecord module Type class UnsignedIntegerTest < ActiveRecord::TestCase test "unsigned int max value is in range" do - assert_equal(4294967295, UnsignedInteger.new.type_cast_for_database(4294967295)) + assert_equal(4294967295, UnsignedInteger.new.serialize(4294967295)) end test "minus value is out of range" do assert_raises(::RangeError) do - UnsignedInteger.new.type_cast_for_database(-1) + UnsignedInteger.new.serialize(-1) end end end diff --git a/activerecord/test/cases/type_test.rb b/activerecord/test/cases/type_test.rb new file mode 100644 index 0000000000..d45a9b3141 --- /dev/null +++ b/activerecord/test/cases/type_test.rb @@ -0,0 +1,39 @@ +require "cases/helper" + +class TypeTest < ActiveRecord::TestCase + setup do + @old_registry = ActiveRecord::Type.registry + ActiveRecord::Type.registry = ActiveRecord::Type::AdapterSpecificRegistry.new + end + + teardown do + ActiveRecord::Type.registry = @old_registry + end + + test "registering a new type" do + type = Struct.new(:args) + ActiveRecord::Type.register(:foo, type) + + assert_equal type.new(:arg), ActiveRecord::Type.lookup(:foo, :arg) + end + + test "looking up a type for a specific adapter" do + type = Struct.new(:args) + pgtype = Struct.new(:args) + ActiveRecord::Type.register(:foo, type, override: false) + ActiveRecord::Type.register(:foo, pgtype, adapter: :postgresql) + + assert_equal type.new, ActiveRecord::Type.lookup(:foo, adapter: :sqlite) + assert_equal pgtype.new, ActiveRecord::Type.lookup(:foo, adapter: :postgresql) + end + + test "lookup defaults to the current adapter" do + current_adapter = ActiveRecord::Base.connection.adapter_name.downcase.to_sym + type = Struct.new(:args) + adapter_type = Struct.new(:args) + ActiveRecord::Type.register(:foo, type, override: false) + ActiveRecord::Type.register(:foo, adapter_type, adapter: current_adapter) + + assert_equal adapter_type.new, ActiveRecord::Type.lookup(:foo) + end +end diff --git a/activerecord/test/cases/types_test.rb b/activerecord/test/cases/types_test.rb index 34b6f2e8a5..9b1859c2ce 100644 --- a/activerecord/test/cases/types_test.rb +++ b/activerecord/test/cases/types_test.rb @@ -5,38 +5,38 @@ module ActiveRecord class TypesTest < ActiveRecord::TestCase def test_type_cast_boolean type = Type::Boolean.new - assert type.type_cast_from_user('').nil? - assert type.type_cast_from_user(nil).nil? - - assert type.type_cast_from_user(true) - assert type.type_cast_from_user(1) - assert type.type_cast_from_user('1') - assert type.type_cast_from_user('t') - assert type.type_cast_from_user('T') - assert type.type_cast_from_user('true') - assert type.type_cast_from_user('TRUE') - assert type.type_cast_from_user('on') - assert type.type_cast_from_user('ON') - assert type.type_cast_from_user(' ') - assert type.type_cast_from_user("\u3000\r\n") - assert type.type_cast_from_user("\u0000") - assert type.type_cast_from_user('SOMETHING RANDOM') + assert type.cast('').nil? + assert type.cast(nil).nil? + + assert type.cast(true) + assert type.cast(1) + assert type.cast('1') + assert type.cast('t') + assert type.cast('T') + assert type.cast('true') + assert type.cast('TRUE') + assert type.cast('on') + assert type.cast('ON') + assert type.cast(' ') + assert type.cast("\u3000\r\n") + assert type.cast("\u0000") + assert type.cast('SOMETHING RANDOM') # explicitly check for false vs nil - assert_equal false, type.type_cast_from_user(false) - assert_equal false, type.type_cast_from_user(0) - assert_equal false, type.type_cast_from_user('0') - assert_equal false, type.type_cast_from_user('f') - assert_equal false, type.type_cast_from_user('F') - assert_equal false, type.type_cast_from_user('false') - assert_equal false, type.type_cast_from_user('FALSE') - assert_equal false, type.type_cast_from_user('off') - assert_equal false, type.type_cast_from_user('OFF') + assert_equal false, type.cast(false) + assert_equal false, type.cast(0) + assert_equal false, type.cast('0') + assert_equal false, type.cast('f') + assert_equal false, type.cast('F') + assert_equal false, type.cast('false') + assert_equal false, type.cast('FALSE') + assert_equal false, type.cast('off') + assert_equal false, type.cast('OFF') end def test_type_cast_float type = Type::Float.new - assert_equal 1.0, type.type_cast_from_user("1") + assert_equal 1.0, type.cast("1") end def test_changing_float @@ -50,54 +50,54 @@ module ActiveRecord def test_type_cast_binary type = Type::Binary.new - assert_equal nil, type.type_cast_from_user(nil) - assert_equal "1", type.type_cast_from_user("1") - assert_equal 1, type.type_cast_from_user(1) + assert_equal nil, type.cast(nil) + assert_equal "1", type.cast("1") + assert_equal 1, type.cast(1) end def test_type_cast_time type = Type::Time.new - assert_equal nil, type.type_cast_from_user(nil) - assert_equal nil, type.type_cast_from_user('') - assert_equal nil, type.type_cast_from_user('ABC') + assert_equal nil, type.cast(nil) + assert_equal nil, type.cast('') + assert_equal nil, type.cast('ABC') time_string = Time.now.utc.strftime("%T") - assert_equal time_string, type.type_cast_from_user(time_string).strftime("%T") + assert_equal time_string, type.cast(time_string).strftime("%T") end def test_type_cast_datetime_and_timestamp type = Type::DateTime.new - assert_equal nil, type.type_cast_from_user(nil) - assert_equal nil, type.type_cast_from_user('') - assert_equal nil, type.type_cast_from_user(' ') - assert_equal nil, type.type_cast_from_user('ABC') + assert_equal nil, type.cast(nil) + assert_equal nil, type.cast('') + assert_equal nil, type.cast(' ') + assert_equal nil, type.cast('ABC') datetime_string = Time.now.utc.strftime("%FT%T") - assert_equal datetime_string, type.type_cast_from_user(datetime_string).strftime("%FT%T") + assert_equal datetime_string, type.cast(datetime_string).strftime("%FT%T") end def test_type_cast_date type = Type::Date.new - assert_equal nil, type.type_cast_from_user(nil) - assert_equal nil, type.type_cast_from_user('') - assert_equal nil, type.type_cast_from_user(' ') - assert_equal nil, type.type_cast_from_user('ABC') + assert_equal nil, type.cast(nil) + assert_equal nil, type.cast('') + assert_equal nil, type.cast(' ') + assert_equal nil, type.cast('ABC') date_string = Time.now.utc.strftime("%F") - assert_equal date_string, type.type_cast_from_user(date_string).strftime("%F") + assert_equal date_string, type.cast(date_string).strftime("%F") end def test_type_cast_duration_to_integer type = Type::Integer.new - assert_equal 1800, type.type_cast_from_user(30.minutes) - assert_equal 7200, type.type_cast_from_user(2.hours) + assert_equal 1800, type.cast(30.minutes) + assert_equal 7200, type.cast(2.hours) end def test_string_to_time_with_timezone [:utc, :local].each do |zone| with_timezone_config default: zone do type = Type::DateTime.new - assert_equal Time.utc(2013, 9, 4, 0, 0, 0), type.type_cast_from_user("Wed, 04 Sep 2013 03:00:00 EAT") + assert_equal Time.utc(2013, 9, 4, 0, 0, 0), type.cast("Wed, 04 Sep 2013 03:00:00 EAT") end end end @@ -110,7 +110,7 @@ module ActiveRecord def test_attributes_which_are_invalid_for_database_can_still_be_reassigned type_which_cannot_go_to_the_database = Type::Value.new - def type_which_cannot_go_to_the_database.type_cast_for_database(*) + def type_which_cannot_go_to_the_database.serialize(*) raise end klass = Class.new(ActiveRecord::Base) do diff --git a/activerecord/test/migrations/missing/1000_people_have_middle_names.rb b/activerecord/test/migrations/missing/1000_people_have_middle_names.rb index 9fd495b97c..4b83d61beb 100644 --- a/activerecord/test/migrations/missing/1000_people_have_middle_names.rb +++ b/activerecord/test/migrations/missing/1000_people_have_middle_names.rb @@ -6,4 +6,4 @@ class PeopleHaveMiddleNames < ActiveRecord::Migration def self.down remove_column "people", "middle_name" end -end
\ No newline at end of file +end diff --git a/activerecord/test/migrations/missing/1_people_have_last_names.rb b/activerecord/test/migrations/missing/1_people_have_last_names.rb index 81af5fef5e..68209f3ce9 100644 --- a/activerecord/test/migrations/missing/1_people_have_last_names.rb +++ b/activerecord/test/migrations/missing/1_people_have_last_names.rb @@ -6,4 +6,4 @@ class PeopleHaveLastNames < ActiveRecord::Migration def self.down remove_column "people", "last_name" end -end
\ No newline at end of file +end diff --git a/activerecord/test/migrations/missing/3_we_need_reminders.rb b/activerecord/test/migrations/missing/3_we_need_reminders.rb index d5e71ce8ef..25bb49cb32 100644 --- a/activerecord/test/migrations/missing/3_we_need_reminders.rb +++ b/activerecord/test/migrations/missing/3_we_need_reminders.rb @@ -9,4 +9,4 @@ class WeNeedReminders < ActiveRecord::Migration def self.down drop_table "reminders" end -end
\ No newline at end of file +end diff --git a/activerecord/test/migrations/missing/4_innocent_jointable.rb b/activerecord/test/migrations/missing/4_innocent_jointable.rb index 21c9ca5328..002a1bf2a6 100644 --- a/activerecord/test/migrations/missing/4_innocent_jointable.rb +++ b/activerecord/test/migrations/missing/4_innocent_jointable.rb @@ -9,4 +9,4 @@ class InnocentJointable < ActiveRecord::Migration def self.down drop_table "people_reminders" end -end
\ No newline at end of file +end diff --git a/activerecord/test/migrations/rename/1_we_need_things.rb b/activerecord/test/migrations/rename/1_we_need_things.rb index cdbe0b1679..f5484ac54f 100644 --- a/activerecord/test/migrations/rename/1_we_need_things.rb +++ b/activerecord/test/migrations/rename/1_we_need_things.rb @@ -8,4 +8,4 @@ class WeNeedThings < ActiveRecord::Migration def self.down drop_table "things" end -end
\ No newline at end of file +end diff --git a/activerecord/test/migrations/rename/2_rename_things.rb b/activerecord/test/migrations/rename/2_rename_things.rb index d441b71fc9..533a113ea8 100644 --- a/activerecord/test/migrations/rename/2_rename_things.rb +++ b/activerecord/test/migrations/rename/2_rename_things.rb @@ -6,4 +6,4 @@ class RenameThings < ActiveRecord::Migration def self.down rename_table "awesome_things", "things" end -end
\ No newline at end of file +end diff --git a/activerecord/test/migrations/valid/2_we_need_reminders.rb b/activerecord/test/migrations/valid/2_we_need_reminders.rb index d5e71ce8ef..25bb49cb32 100644 --- a/activerecord/test/migrations/valid/2_we_need_reminders.rb +++ b/activerecord/test/migrations/valid/2_we_need_reminders.rb @@ -9,4 +9,4 @@ class WeNeedReminders < ActiveRecord::Migration def self.down drop_table "reminders" end -end
\ No newline at end of file +end diff --git a/activerecord/test/migrations/valid/3_innocent_jointable.rb b/activerecord/test/migrations/valid/3_innocent_jointable.rb index 21c9ca5328..002a1bf2a6 100644 --- a/activerecord/test/migrations/valid/3_innocent_jointable.rb +++ b/activerecord/test/migrations/valid/3_innocent_jointable.rb @@ -9,4 +9,4 @@ class InnocentJointable < ActiveRecord::Migration def self.down drop_table "people_reminders" end -end
\ No newline at end of file +end diff --git a/activerecord/test/migrations/valid_with_subdirectories/sub/2_we_need_reminders.rb b/activerecord/test/migrations/valid_with_subdirectories/sub/2_we_need_reminders.rb index d5e71ce8ef..25bb49cb32 100644 --- a/activerecord/test/migrations/valid_with_subdirectories/sub/2_we_need_reminders.rb +++ b/activerecord/test/migrations/valid_with_subdirectories/sub/2_we_need_reminders.rb @@ -9,4 +9,4 @@ class WeNeedReminders < ActiveRecord::Migration def self.down drop_table "reminders" end -end
\ No newline at end of file +end diff --git a/activerecord/test/migrations/valid_with_subdirectories/sub1/3_innocent_jointable.rb b/activerecord/test/migrations/valid_with_subdirectories/sub1/3_innocent_jointable.rb index 21c9ca5328..002a1bf2a6 100644 --- a/activerecord/test/migrations/valid_with_subdirectories/sub1/3_innocent_jointable.rb +++ b/activerecord/test/migrations/valid_with_subdirectories/sub1/3_innocent_jointable.rb @@ -9,4 +9,4 @@ class InnocentJointable < ActiveRecord::Migration def self.down drop_table "people_reminders" end -end
\ No newline at end of file +end diff --git a/activerecord/test/models/admin.rb b/activerecord/test/models/admin.rb index 00e69fbed8..a38e3f4846 100644 --- a/activerecord/test/models/admin.rb +++ b/activerecord/test/models/admin.rb @@ -2,4 +2,4 @@ module Admin def self.table_name_prefix 'admin_' end -end
\ No newline at end of file +end diff --git a/activerecord/test/models/admin/account.rb b/activerecord/test/models/admin/account.rb index 46de28aae1..bd23192d20 100644 --- a/activerecord/test/models/admin/account.rb +++ b/activerecord/test/models/admin/account.rb @@ -1,3 +1,3 @@ class Admin::Account < ActiveRecord::Base has_many :users -end
\ No newline at end of file +end diff --git a/activerecord/test/models/binary.rb b/activerecord/test/models/binary.rb index 950c459199..39b2f5090a 100644 --- a/activerecord/test/models/binary.rb +++ b/activerecord/test/models/binary.rb @@ -1,2 +1,2 @@ class Binary < ActiveRecord::Base -end
\ No newline at end of file +end diff --git a/activerecord/test/models/company.rb b/activerecord/test/models/company.rb index 5a56616eb9..6961f8fd6f 100644 --- a/activerecord/test/models/company.rb +++ b/activerecord/test/models/company.rb @@ -214,7 +214,7 @@ class Account < ActiveRecord::Base protected def check_empty_credit_limit - errors.add_on_empty "credit_limit" + errors.add("credit_limit", :blank) if credit_limit.blank? end private diff --git a/activerecord/test/models/company_in_module.rb b/activerecord/test/models/company_in_module.rb index dae102d12b..bf0a0d1c3e 100644 --- a/activerecord/test/models/company_in_module.rb +++ b/activerecord/test/models/company_in_module.rb @@ -91,7 +91,7 @@ module MyApplication protected def check_empty_credit_limit - errors.add_on_empty "credit_limit" + errors.add("credit_card", :blank) if credit_card.blank? end end end diff --git a/activerecord/test/models/event.rb b/activerecord/test/models/event.rb index 99fa0feeb7..365ab32b0b 100644 --- a/activerecord/test/models/event.rb +++ b/activerecord/test/models/event.rb @@ -1,3 +1,3 @@ class Event < ActiveRecord::Base validates_uniqueness_of :title -end
\ No newline at end of file +end diff --git a/activerecord/test/models/guid.rb b/activerecord/test/models/guid.rb index 9208dc28fa..05653ba498 100644 --- a/activerecord/test/models/guid.rb +++ b/activerecord/test/models/guid.rb @@ -1,2 +1,2 @@ class Guid < ActiveRecord::Base -end
\ No newline at end of file +end diff --git a/activerecord/test/models/notification.rb b/activerecord/test/models/notification.rb new file mode 100644 index 0000000000..b4b4b8f1b6 --- /dev/null +++ b/activerecord/test/models/notification.rb @@ -0,0 +1,2 @@ +class Notification < ActiveRecord::Base +end diff --git a/activerecord/test/models/pirate.rb b/activerecord/test/models/pirate.rb index 366c70f902..30545bdcd7 100644 --- a/activerecord/test/models/pirate.rb +++ b/activerecord/test/models/pirate.rb @@ -89,4 +89,4 @@ class FamousPirate < ActiveRecord::Base self.table_name = 'pirates' has_many :famous_ships validates_presence_of :catchphrase, on: :conference -end
\ No newline at end of file +end diff --git a/activerecord/test/models/user.rb b/activerecord/test/models/user.rb index 23cd2e0e1c..f5dc93e994 100644 --- a/activerecord/test/models/user.rb +++ b/activerecord/test/models/user.rb @@ -2,3 +2,7 @@ class User < ActiveRecord::Base has_secure_token has_secure_token :auth_token end + +class UserWithNotification < User + after_create -> { Notification.create! message: "A new user has been created." } +end diff --git a/activerecord/test/schema/postgresql_specific_schema.rb b/activerecord/test/schema/postgresql_specific_schema.rb index 55360b9aa2..f84be0e7f4 100644 --- a/activerecord/test/schema/postgresql_specific_schema.rb +++ b/activerecord/test/schema/postgresql_specific_schema.rb @@ -2,7 +2,7 @@ ActiveRecord::Schema.define do %w(postgresql_times postgresql_oids defaults postgresql_timestamp_with_zones postgresql_partitioned_table postgresql_partitioned_table_parent).each do |table_name| - execute "DROP TABLE IF EXISTS #{quote_table_name table_name}" + drop_table table_name, if_exists: true end execute 'DROP SEQUENCE IF EXISTS companies_nonstd_seq CASCADE' @@ -88,16 +88,6 @@ _SQL end end - begin - execute <<_SQL - CREATE TABLE postgresql_xml_data_type ( - id SERIAL PRIMARY KEY, - data xml - ); -_SQL - rescue #This version of PostgreSQL either has no XML support or is was not compiled with XML support: skipping table - end - # This table is to verify if the :limit option is being ignored for text and binary columns create_table :limitless_fields, force: true do |t| t.binary :binary, limit: 100_000 diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index a7d90e3f89..08a16d5c9e 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -468,6 +468,10 @@ ActiveRecord::Schema.define do t.string :name end + create_table :notifications, force: true do |t| + t.string :message + end + create_table :numeric_data, force: true do |t| t.decimal :bank_balance, precision: 10, scale: 2 t.decimal :big_bank_balance, precision: 15, scale: 2 |