diff options
Diffstat (limited to 'activerecord')
81 files changed, 1250 insertions, 436 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index d0d5481d81..b5dd19988d 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,112 @@ +* Dont enroll records in the transaction if they dont have commit callbacks. + That was causing a memory grow problem when creating a lot of records inside a transaction. + + Fixes #15549. + + *Will Bryant*, *Aaron Patterson* + +* Correctly create through records when created on a has many through + association when using `where`. + + Fixes #19073. + + *Sean Griffin* + +* Add `SchemaMigration.create_table` support any unicode charsets for MySQL. + + *Ryuta Kamizono* + +* 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. @@ -37,9 +146,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/association_relation.rb b/activerecord/lib/active_record/association_relation.rb index f2b44913db..ee0bb8fafe 100644 --- a/activerecord/lib/active_record/association_relation.rb +++ b/activerecord/lib/active_record/association_relation.rb @@ -13,6 +13,19 @@ module ActiveRecord other == to_a end + def build(*args, &block) + scoping { @association.build(*args, &block) } + end + alias new build + + def create(*args, &block) + scoping { @association.create(*args, &block) } + end + + def create!(*args, &block) + scoping { @association.create!(*args, &block) } + end + private def exec_queries diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index a146d78a5a..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>, @@ -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" @@ -1518,7 +1540,7 @@ module ActiveRecord # belongs_to :post, counter_cache: true # belongs_to :comment, touch: true # belongs_to :company, touch: :employees_last_updated_at - # belongs_to :user, 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 eea847cfa3..0ba03338f6 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -129,6 +129,16 @@ module ActiveRecord first_nth_or_last(:last, *args) end + def take(n = nil) + if loaded? + n ? target.take(n) : target.first + else + scope.take(n).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..e11c9490b7 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(n = nil) + @association.take(n) + 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_assignment.rb b/activerecord/lib/active_record/attribute_assignment.rb index 483d4200bd..cc265e2af6 100644 --- a/activerecord/lib/active_record/attribute_assignment.rb +++ b/activerecord/lib/active_record/attribute_assignment.rb @@ -29,6 +29,13 @@ module ActiveRecord assign_multiparameter_attributes(multi_parameter_attributes) unless multi_parameter_attributes.empty? end + # Re-raise with the ActiveRecord constant in case of an error + def _assign_attribute(k, v) # :nodoc: + super + rescue ActiveModel::UnknownAttributeError + raise UnknownAttributeError.new(self, k) + end + # Assign any deferred nested attributes after the base attributes have been set. def assign_nested_parameter_attributes(pairs) pairs.each { |k, v| _assign_attribute(k, v) } 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/database_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb index 42ad285340..ba38b73b60 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -232,10 +232,6 @@ module ActiveRecord current_transaction.add_record(record) end - def transaction_state - current_transaction.state - end - # Begins the transaction (and turns off auto-committing). def begin_db_transaction() 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 a2777fcd0a..8231f44b4e 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -142,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. # @@ -163,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 @@ -181,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] @@ -343,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 # @@ -378,7 +401,7 @@ module ActiveRecord # Adds a reference. Optionally adds a +type+ column, if the # +:polymorphic+ option is provided. +references+ and +belongs_to+ - # are acceptable. The reference column will be an +integer+ by default, + # are interchangeable. The reference column will be an +integer+ by default, # the +:type+ option can be used to specify a different type. A foreign # key will be created if the +:foreign_key+ option is passed. # @@ -462,6 +485,7 @@ module ActiveRecord # Available transformations are: # # change_table :table do |t| + # t.primary_key # t.column # t.index # t.rename_index @@ -474,6 +498,7 @@ module ActiveRecord # t.string # t.text # t.integer + # t.bigint # t.float # t.decimal # t.datetime @@ -490,6 +515,8 @@ module ActiveRecord # end # class Table + include ColumnMethods + attr_reader :name def initialize(table_name, base) @@ -640,21 +667,6 @@ 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 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 503b09d1fc..d42f9a894b 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -770,7 +770,7 @@ module ActiveRecord # # Check a foreign key exists # foreign_key_exists?(:accounts, :branches) # - # # Check a foreign key on specified column exists + # # 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 @@ -851,6 +851,12 @@ module ActiveRecord raise ArgumentError, "Error adding decimal column: precision cannot be empty if scale is specified" end + elsif [:datetime, :time].include?(type) && precision ||= native[:precision] + if (0..6) === precision + column_type_sql << "(#{precision})" + else + raise(ActiveRecordError, "No #{native[:name]} type has precision of #{precision}. The allowed range of precision is from 0 to 6") + end elsif (type != :primary_key) && (limit ||= native.is_a?(Hash) && native[:limit]) column_type_sql << "(#{limit})" end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb index 11440e30d4..c74f4e8fa5 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb @@ -1,13 +1,10 @@ module ActiveRecord module ConnectionAdapters class TransactionState - attr_reader :parent - VALID_STATES = Set.new([:committed, :rolledback, nil]) def initialize(state = nil) @state = state - @parent = nil end def finalized? @@ -27,7 +24,7 @@ module ActiveRecord end def set_state(state) - if !VALID_STATES.include?(state) + unless VALID_STATES.include?(state) raise ArgumentError, "Invalid transaction state: #{state}" end @state = state @@ -77,6 +74,10 @@ module ActiveRecord @state.set_state(:committed) end + def before_commit_records + records.uniq.each(&:before_committed!) + end + def commit_records ite = records.uniq while record = ite.shift @@ -159,13 +160,15 @@ module ActiveRecord def commit_transaction inner_transaction = @stack.pop - inner_transaction.commit if current_transaction.joinable? + inner_transaction.commit inner_transaction.records.each do |r| r.add_to_transaction end else + inner_transaction.before_commit_records + inner_transaction.commit inner_transaction.commit_records end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index ee11c0efa4..ae42e8ef8d 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -245,6 +245,11 @@ module ActiveRecord false end + # Does this adapter support datetime with precision? + def supports_datetime_with_precision? + false + end + # This is meant to be implemented by the adapters that support extensions def disable_extension(name) end @@ -473,7 +478,6 @@ module ActiveRecord message = "#{e.class.name}: #{e.message.force_encoding sql.encoding}: #{sql}" end - @logger.error message if @logger exception = translate_exception(e, message) exception.set_backtrace e.backtrace exception 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 0a1c66be1e..c084431588 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 @@ -212,6 +235,16 @@ module ActiveRecord end end + MAX_INDEX_LENGTH_FOR_CHARSETS_OF_4BYTES_MAXLEN = 191 + CHARSETS_OF_4BYTES_MAXLEN = ['utf8mb4', 'utf16', 'utf16le', 'utf32'] + def initialize_schema_migrations_table + if CHARSETS_OF_4BYTES_MAXLEN.include?(charset) + ActiveRecord::SchemaMigration.create_table(MAX_INDEX_LENGTH_FOR_CHARSETS_OF_4BYTES_MAXLEN) + else + ActiveRecord::SchemaMigration.create_table + end + end + # Returns true, since this connection adapter supports migrations. def supports_migrations? true @@ -251,6 +284,10 @@ module ActiveRecord version[0] >= 5 end + def supports_datetime_with_precision? + (version[0] == 5 && version[1] >= 6) || version[0] >= 6 + end + def native_database_types NATIVE_DATABASE_TYPES end @@ -623,13 +660,6 @@ module ActiveRecord when 0x1000000..0xffffffff; 'longtext' else raise(ActiveRecordError, "No text type has character length #{limit}") end - when 'datetime' - return super unless precision - - case precision - when 0..6; "datetime(#{precision})" - else raise(ActiveRecordError, "No datetime type has precision of #{precision}. The allowed range of precision is from 0 to 6.") - end else super end @@ -736,7 +766,7 @@ module ActiveRecord end def extract_precision(sql_type) - if /datetime/ === sql_type + if /time/ === sql_type super || 0 else super diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index fac6f81540..21631be25c 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -37,15 +37,6 @@ module ActiveRecord configure_connection end - MAX_INDEX_LENGTH_FOR_UTF8MB4 = 191 - def initialize_schema_migrations_table - if charset == 'utf8mb4' - ActiveRecord::SchemaMigration.create_table(MAX_INDEX_LENGTH_FOR_UTF8MB4) - else - ActiveRecord::SchemaMigration.create_table - end - end - def supports_explain? true 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/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 81fde18f64..eeb141dd1e 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,41 +510,35 @@ 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. + # The hard limit is 1GB, because of a 32-bit size field, and TOAST. case limit when nil, 0..0x3fffffff; super(type) else raise(ActiveRecordError, "No binary type has byte size #{limit}.") end when 'text' # PostgreSQL doesn't support limits on text columns. - # The hard limit is 1Gb, according to section 8.3 in the manual. + # The hard limit is 1GB, according to section 8.3 in the manual. case limit when nil, 0..0x3fffffff; super(type) 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.") - end - when 'datetime' - return super unless precision - - case precision - when 0..6; "timestamp(#{precision})" - else raise(ActiveRecordError, "No timestamp type has precision of #{precision}. The allowed range of precision is from 0 to 6") + 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 5a887ea529..6d25b53b21 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -180,6 +180,10 @@ module ActiveRecord true end + def supports_datetime_with_precision? + true + end + def index_algorithms { concurrently: 'CONCURRENTLY' } end @@ -477,7 +481,6 @@ module ActiveRecord register_class_with_limit m, 'varbit', OID::BitVarying m.alias_type 'timestamptz', 'timestamp' m.register_type 'date', Type::Date.new - m.register_type 'time', Type::Time.new m.register_type 'money', OID::Money.new m.register_type 'bytea', OID::Bytea.new @@ -503,10 +506,8 @@ module ActiveRecord m.alias_type 'lseg', 'varchar' m.alias_type 'box', 'varchar' - m.register_type 'timestamp' do |_, _, sql_type| - precision = extract_precision(sql_type) - OID::DateTime.new(precision: precision) - end + register_class_with_precision m, 'time', Type::Time + register_class_with_precision m, 'timestamp', OID::DateTime m.register_type 'numeric' do |_, fmod, sql_type| precision = extract_precision(sql_type) diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index 400b586c95..7e184dd510 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -121,6 +121,7 @@ module ActiveRecord @config = config @visitor = Arel::Visitors::SQLite.new self + @quoted_column_names = {} if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true }) @prepared_statements = true @@ -240,7 +241,7 @@ module ActiveRecord end def quote_column_name(name) #:nodoc: - %Q("#{name.to_s.gsub('"', '""')}") + @quoted_column_names[name] ||= %Q("#{name.to_s.gsub('"', '""')}") end #-- diff --git a/activerecord/lib/active_record/connection_handling.rb b/activerecord/lib/active_record/connection_handling.rb index 984af79642..24f5849e45 100644 --- a/activerecord/lib/active_record/connection_handling.rb +++ b/activerecord/lib/active_record/connection_handling.rb @@ -5,7 +5,7 @@ module ActiveRecord # Establishes the connection to the database. Accepts a hash as input where # the <tt>:adapter</tt> key must be specified with the name of a database adapter (in lower-case) - # example for regular databases (MySQL, Postgresql, etc): + # example for regular databases (MySQL, PostgreSQL, etc): # # ActiveRecord::Base.establish_connection( # adapter: "mysql", diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index 44d587206d..97ce4642aa 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 @@ -482,51 +483,6 @@ module ActiveRecord private - def set_transaction_state(state) # :nodoc: - @transaction_state = state - end - - def has_transactional_callbacks? # :nodoc: - !_rollback_callbacks.empty? || !_commit_callbacks.empty? - end - - # Updates the attributes on this particular ActiveRecord object so that - # if it is associated with a transaction, then the state of the AR object - # will be updated to reflect the current state of the transaction - # - # The @transaction_state variable stores the states of the associated - # transaction. This relies on the fact that a transaction can only be in - # one rollback or commit (otherwise a list of states would be required) - # Each AR object inside of a transaction carries that transaction's - # TransactionState. - # - # This method checks to see if the ActiveRecord object's state reflects - # the TransactionState, and rolls back or commits the ActiveRecord object - # as appropriate. - # - # Since ActiveRecord objects can be inside multiple transactions, this - # method recursively goes through the parent of the TransactionState and - # checks if the ActiveRecord object reflects the state of the object. - def sync_with_transaction_state - update_attributes_from_transaction_state(@transaction_state, 0) - end - - def update_attributes_from_transaction_state(transaction_state, depth) - @reflects_state = [false] if depth == 0 - - if transaction_state && transaction_state.finalized? && !has_transactional_callbacks? - unless @reflects_state[depth] - restore_transaction_record_state if transaction_state.rolledback? - clear_transaction_record_state - @reflects_state[depth] = true - end - - if transaction_state.parent && !@reflects_state[depth+1] - update_attributes_from_transaction_state(transaction_state.parent, depth+1) - end - end - end - # Under Ruby 1.9, Array#flatten will call #to_ary (recursively) on each of the elements # of the array, and then rescues from the possible NoMethodError. If those elements are # ActiveRecord::Base's, then this triggers the various method_missing's that we have, @@ -540,8 +496,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/errors.rb b/activerecord/lib/active_record/errors.rb index d710d96a9a..98aee77557 100644 --- a/activerecord/lib/active_record/errors.rb +++ b/activerecord/lib/active_record/errors.rb @@ -178,10 +178,8 @@ module ActiveRecord class DangerousAttributeError < ActiveRecordError end - UnknownAttributeError = ActiveSupport::Deprecation::DeprecatedConstantProxy.new( # :nodoc: - 'ActiveRecord::UnknownAttributeError', - 'ActiveModel::AttributeAssignment::UnknownAttributeError' - ) + # Raised when unknown attributes are supplied via mass assignment. + UnknownAttributeError = ActiveModel::UnknownAttributeError # Raised when an error occurred while doing a mass assignment to an attribute through the # +attributes=+ method. The exception has an +attribute+ property that is the name of the diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index 1bfdfb6608..18775caad2 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -706,6 +706,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 a58d8355aa..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 ) 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/persistence.rb b/activerecord/lib/active_record/persistence.rb index af7aef6e43..a6176dffdb 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -96,7 +96,8 @@ module ActiveRecord # Returns true if the record is persisted, i.e. it's not a new record and it was # not destroyed, otherwise returns false. def persisted? - !(new_record? || destroyed?) + sync_with_transaction_state + !(@new_record || @destroyed) end # Saves the model. @@ -208,7 +209,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 +416,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 +428,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 +462,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/railtie.rb b/activerecord/lib/active_record/railtie.rb index f1bdbc845c..b6de90e89d 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -58,6 +58,9 @@ module ActiveRecord require "active_record/railties/console_sandbox" if app.sandbox? require "active_record/base" console = ActiveSupport::Logger.new(STDERR) + console.formatter = Rails.logger.formatter + console.level = Rails.logger.level + Rails.logger.extend ActiveSupport::Logger.broadcast console end diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index 4a4de86d48..7f27e7b463 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -222,7 +222,7 @@ module ActiveRecord end def execute_simple_calculation(operation, column_name, distinct) #:nodoc: - # Postgresql doesn't like ORDER BY when there are no GROUP BY + # PostgreSQL doesn't like ORDER BY when there are no GROUP BY relation = unscope(:order) column_alias = column_name diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index fb47d915ff..7bd091b66c 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -1,4 +1,3 @@ -require 'active_support/deprecation' require 'active_support/core_ext/string/filters' module ActiveRecord 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/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/transactions.rb b/activerecord/lib/active_record/transactions.rb index dd405c7796..ffe7c4ae42 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -7,6 +7,10 @@ module ActiveRecord included do define_callbacks :commit, :rollback, + :before_commit, + :before_commit_without_transaction_enrollment, + :commit_without_transaction_enrollment, + :rollback_without_transaction_enrollment, scope: [:kind, :name] end @@ -206,6 +210,11 @@ module ActiveRecord connection.transaction(options, &block) end + def before_commit(*args, &block) # :nodoc: + set_options_for_callbacks!(args) + set_callback(:before_commit, :before, *args, &block) + end + # This callback is called after a record has been created, updated, or destroyed. # # You can specify that the callback should only be fired by a certain action with @@ -233,6 +242,21 @@ module ActiveRecord set_callback(:rollback, :after, *args, &block) end + def before_commit_without_transaction_enrollment(*args, &block) # :nodoc: + set_options_for_callbacks!(args) + set_callback(:before_commit_without_transaction_enrollment, :before, *args, &block) + end + + def after_commit_without_transaction_enrollment(*args, &block) # :nodoc: + set_options_for_callbacks!(args) + set_callback(:commit_without_transaction_enrollment, :after, *args, &block) + end + + def after_rollback_without_transaction_enrollment(*args, &block) # :nodoc: + set_options_for_callbacks!(args) + set_callback(:rollback_without_transaction_enrollment, :after, *args, &block) + end + def raise_in_transactional_callbacks ActiveSupport::Deprecation.warn('ActiveRecord::Base.raise_in_transactional_callbacks is deprecated and will be removed without replacement.') true @@ -296,12 +320,20 @@ module ActiveRecord clear_transaction_record_state end + def before_committed! # :nodoc: + _run_before_commit_without_transaction_enrollment_callbacks + _run_before_commit_callbacks + end + # Call the +after_commit+ callbacks. # # Ensure that it is not called if the object was never persisted (failed create), # but call it after the commit of a destroyed object. def committed!(should_run_callbacks: true) #:nodoc: - _run_commit_callbacks if should_run_callbacks && destroyed? || persisted? + if should_run_callbacks && destroyed? || persisted? + _run_commit_without_transaction_enrollment_callbacks + _run_commit_callbacks + end ensure force_clear_transaction_record_state end @@ -309,7 +341,10 @@ module ActiveRecord # Call the +after_rollback+ callbacks. The +force_restore_state+ argument indicates if the record # state should be rolled back to the beginning or just to the last savepoint. def rolledback!(force_restore_state: false, should_run_callbacks: true) #:nodoc: - _run_rollback_callbacks if should_run_callbacks + if should_run_callbacks + _run_rollback_without_transaction_enrollment_callbacks + _run_rollback_callbacks + end ensure restore_transaction_record_state(force_restore_state) clear_transaction_record_state @@ -407,5 +442,43 @@ module ActiveRecord end end end + + private + + def set_transaction_state(state) # :nodoc: + @transaction_state = state + end + + def has_transactional_callbacks? # :nodoc: + !_rollback_callbacks.empty? || !_commit_callbacks.empty? || !_before_commit_callbacks.empty? + end + + # Updates the attributes on this particular ActiveRecord object so that + # if it is associated with a transaction, then the state of the AR object + # will be updated to reflect the current state of the transaction + # + # The @transaction_state variable stores the states of the associated + # transaction. This relies on the fact that a transaction can only be in + # one rollback or commit (otherwise a list of states would be required) + # Each AR object inside of a transaction carries that transaction's + # TransactionState. + # + # This method checks to see if the ActiveRecord object's state reflects + # the TransactionState, and rolls back or commits the ActiveRecord object + # as appropriate. + # + # Since ActiveRecord objects can be inside multiple transactions, this + # method recursively goes through the parent of the TransactionState and + # checks if the ActiveRecord object reflects the state of the object. + def sync_with_transaction_state + update_attributes_from_transaction_state(@transaction_state) + end + + def update_attributes_from_transaction_state(transaction_state) + if transaction_state && transaction_state.finalized? + restore_transaction_record_state if transaction_state.rolledback? + clear_transaction_record_state + end + end end end diff --git a/activerecord/lib/active_record/type/date_time.rb b/activerecord/lib/active_record/type/date_time.rb index 05d2af3808..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 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 - private def cast_value(string) 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/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/test/cases/adapters/mysql2/schema_migrations_test.rb b/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb index 271b570eb5..417ccf6d11 100644 --- a/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb +++ b/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb @@ -2,7 +2,7 @@ require "cases/helper" module ActiveRecord module ConnectionAdapters - class Mysql2Adapter + class AbstractMysqlAdapter class SchemaMigrationsTest < ActiveRecord::TestCase def test_renaming_index_on_foreign_key connection.add_index "engines", "car_id" @@ -28,7 +28,7 @@ module ActiveRecord connection.initialize_schema_migrations_table - assert connection.column_exists?(smtn, :version, :string, limit: Mysql2Adapter::MAX_INDEX_LENGTH_FOR_UTF8MB4) + assert connection.column_exists?(smtn, :version, :string, limit: AbstractMysqlAdapter::MAX_INDEX_LENGTH_FOR_CHARSETS_OF_4BYTES_MAXLEN) ensure execute("ALTER DATABASE #{database_name} DEFAULT CHARACTER SET #{original_charset} COLLATE #{original_collation}") end 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/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/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..675bed9bfa 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -30,9 +30,12 @@ require 'models/college' require 'models/student' require 'models/pirate' require 'models/ship' +require 'models/ship_part' 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 +100,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 @@ -160,6 +163,30 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal college.students, Student.where(active: true, college_id: college.id) end + def test_add_record_to_collection_should_change_its_updated_at + ship = Ship.create(name: 'dauntless') + part = ShipPart.create(name: 'cockpit') + updated_at = part.updated_at + + ship.parts << part + + assert_equal part.ship, ship + assert_not_equal part.updated_at, updated_at + end + + def test_clear_collection_should_not_change_updated_at + # GH#17161: .clear calls delete_all (and returns the association), + # which is intended to not touch associated objects's updated_at field + ship = Ship.create(name: 'dauntless') + part = ShipPart.create(name: 'cockpit', ship_id: ship.id) + + ship.parts.clear + part.reload + + assert_equal nil, part.ship + assert !part.updated_at_changed? + end + def test_create_from_association_should_respect_default_scope car = Car.create(:name => 'honda') assert_equal 'honda', car.name @@ -435,6 +462,45 @@ 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_a_number + # taking from unloaded Relation + bob = Author.find(authors(:bob).id) + assert_equal [posts(:misc_by_bob)], bob.posts.take(1) + bob = Author.find(authors(:bob).id) + assert_equal [posts(:misc_by_bob), posts(:other_by_bob)], bob.posts.take(2) + + # taking from loaded Relation + bob.posts.to_a + assert_equal [posts(:misc_by_bob)], authors(:bob).posts.take(1) + assert_equal [posts(:misc_by_bob), posts(:other_by_bob)], authors(:bob).posts.take(2) + 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/has_many_through_associations_test.rb b/activerecord/test/cases/associations/has_many_through_associations_test.rb index 6729a5a9fc..5f52c65412 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -595,6 +595,12 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert posts(:thinking).reload.people(true).collect(&:first_name).include?("Jeb") end + def test_through_record_is_built_when_created_with_where + assert_difference("posts(:thinking).readers.count", 1) do + posts(:thinking).people.where(first_name: "Jeb").create + end + end + def test_associate_with_create_and_no_options peeps = posts(:thinking).people.count posts(:thinking).people.create(:first_name => 'foo') diff --git a/activerecord/test/cases/associations/has_one_associations_test.rb b/activerecord/test/cases/associations/has_one_associations_test.rb index 3b6a4038cd..7dc9266074 100644 --- a/activerecord/test/cases/associations/has_one_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_associations_test.rb @@ -276,7 +276,7 @@ class HasOneAssociationsTest < ActiveRecord::TestCase def test_create_with_inexistent_foreign_key_failing firm = Firm.create(name: 'GlobalMegaCorp') - assert_raises(ActiveModel::AttributeAssignment::UnknownAttributeError) do + assert_raises(ActiveRecord::UnknownAttributeError) do firm.create_account_with_inexistent_foreign_key end end 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_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb index 243c90e945..ea2b94cbf4 100644 --- a/activerecord/test/cases/attribute_methods_test.rb +++ b/activerecord/test/cases/attribute_methods_test.rb @@ -758,12 +758,12 @@ class AttributeMethodsTest < ActiveRecord::TestCase def test_bulk_update_respects_access_control privatize("title=(value)") - assert_raise(ActiveModel::AttributeAssignment::UnknownAttributeError) { @target.new(:title => "Rants about pants") } - assert_raise(ActiveModel::AttributeAssignment::UnknownAttributeError) { @target.new.attributes = { :title => "Ants in pants" } } + assert_raise(ActiveRecord::UnknownAttributeError) { @target.new(:title => "Rants about pants") } + assert_raise(ActiveRecord::UnknownAttributeError) { @target.new.attributes = { :title => "Ants in pants" } } end def test_bulk_update_raise_unknown_attribute_error - error = assert_raises(ActiveModel::AttributeAssignment::UnknownAttributeError) { + error = assert_raises(ActiveRecord::UnknownAttributeError) { Topic.new(hello: "world") } assert_instance_of Topic, error.record diff --git a/activerecord/test/cases/attributes_test.rb b/activerecord/test/cases/attributes_test.rb index e7b76b1cf9..927d7950a5 100644 --- a/activerecord/test/cases/attributes_test.rb +++ b/activerecord/test/cases/attributes_test.rb @@ -58,7 +58,7 @@ module ActiveRecord data = OverloadedType.new(non_existent_decimal: 1) assert_equal BigDecimal.new(1), data.non_existent_decimal - assert_raise ActiveModel::AttributeAssignment::UnknownAttributeError do + assert_raise ActiveRecord::UnknownAttributeError do UnoverloadedType.new(non_existent_decimal: 1) end end diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index 76907c832f..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 diff --git a/activerecord/test/cases/date_time_precision_test.rb b/activerecord/test/cases/date_time_precision_test.rb index 4602ba6d0d..6a4e64b22c 100644 --- a/activerecord/test/cases/date_time_precision_test.rb +++ b/activerecord/test/cases/date_time_precision_test.rb @@ -1,7 +1,7 @@ require 'cases/helper' require 'support/schema_dumping_helper' -if mysql_56? || current_adapter?(:PostgreSQLAdapter) +if ActiveRecord::Base.connection.supports_datetime_with_precision? class DateTimePrecisionTest < ActiveRecord::TestCase include SchemaDumpingHelper self.use_transactional_fixtures = false @@ -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/enum_test.rb b/activerecord/test/cases/enum_test.rb index e70d492efd..3b7bbcf47a 100644 --- a/activerecord/test/cases/enum_test.rb +++ b/activerecord/test/cases/enum_test.rb @@ -229,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 @@ -251,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 8b115f64a1..d1add21219 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_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/create_join_table_test.rb b/activerecord/test/cases/migration/create_join_table_test.rb index bea9d6b2c9..8fd08fe4ce 100644 --- a/activerecord/test/cases/migration/create_join_table_test.rb +++ b/activerecord/test/cases/migration/create_join_table_test.rb @@ -140,7 +140,7 @@ module ActiveRecord tables_after = connection.tables - tables_before tables_after.each do |table| - connection.execute "DROP TABLE #{table}" + connection.drop_table table end end end 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/migrator_test.rb b/activerecord/test/cases/migrator_test.rb index c0daa83e9c..1760314099 100644 --- a/activerecord/test/cases/migrator_test.rb +++ b/activerecord/test/cases/migrator_test.rb @@ -312,7 +312,7 @@ class MigratorTest < ActiveRecord::TestCase def test_migrator_db_has_no_schema_migrations_table _, migrator = migrator_class(3) - ActiveRecord::Base.connection.execute("DROP TABLE schema_migrations") + ActiveRecord::Base.connection.drop_table "schema_migrations", if_exists: true assert_not ActiveRecord::Base.connection.table_exists?('schema_migrations') migrator.migrate("valid", 1) assert ActiveRecord::Base.connection.table_exists?('schema_migrations') diff --git a/activerecord/test/cases/nested_attributes_test.rb b/activerecord/test/cases/nested_attributes_test.rb index 198cd6f341..c5f6589c22 100644 --- a/activerecord/test/cases/nested_attributes_test.rb +++ b/activerecord/test/cases/nested_attributes_test.rb @@ -672,7 +672,7 @@ module NestedAttributesOnACollectionAssociationTests end def test_should_not_assign_destroy_key_to_a_record - assert_nothing_raised ActiveModel::AttributeAssignment::UnknownAttributeError do + assert_nothing_raised ActiveRecord::UnknownAttributeError do @pirate.send(association_setter, { 'foo' => { '_destroy' => '0' }}) end end diff --git a/activerecord/test/cases/primary_keys_test.rb b/activerecord/test/cases/primary_keys_test.rb index b45fbf0143..1ea1ef5e12 100644 --- a/activerecord/test/cases/primary_keys_test.rb +++ b/activerecord/test/cases/primary_keys_test.rb @@ -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/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 0413984729..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 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 new file mode 100644 index 0000000000..ff4e5ecec5 --- /dev/null +++ b/activerecord/test/cases/time_precision_test.rb @@ -0,0 +1,108 @@ +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 + + teardown do + @connection.drop_table :foos, if_exists: true + end + + def test_time_data_type_with_precision + @connection.create_table(:foos, force: true) + @connection.add_column :foos, :start, :time, precision: 3 + @connection.add_column :foos, :finish, :time, precision: 6 + assert_equal 3, activerecord_column_option('foos', 'start', 'precision') + assert_equal 6, activerecord_column_option('foos', 'finish', 'precision') + end + + def test_passing_precision_to_time_does_not_set_limit + @connection.create_table(:foos, force: true) do |t| + t.time :start, precision: 3 + t.time :finish, precision: 6 + end + assert_nil activerecord_column_option('foos', 'start', 'limit') + assert_nil activerecord_column_option('foos', 'finish', 'limit') + end + + def test_invalid_time_precision_raises_error + assert_raises ActiveRecord::ActiveRecordError do + @connection.create_table(:foos, force: true) do |t| + t.time :start, precision: 7 + t.time :finish, precision: 7 + end + end + end + + def test_database_agrees_with_activerecord_about_precision + @connection.create_table(:foos, force: true) do |t| + t.time :start, precision: 2 + t.time :finish, precision: 4 + end + assert_equal 2, database_datetime_precision('foos', 'start') + 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) + results = @connection.exec_query("SELECT column_name, datetime_precision FROM information_schema.columns WHERE table_name = '#{table_name}'") + result = results.find do |result_hash| + result_hash["column_name"] == column_name + end + result && result["datetime_precision"].to_i + end + + def activerecord_column_option(tablename, column_name, option) + result = @connection.columns(tablename).find do |column| + column.name == column_name + end + result && result.send(option) + end +end +end 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/transaction_callbacks_test.rb b/activerecord/test/cases/transaction_callbacks_test.rb index f185cda263..e868022fed 100644 --- a/activerecord/test/cases/transaction_callbacks_test.rb +++ b/activerecord/test/cases/transaction_callbacks_test.rb @@ -400,3 +400,63 @@ class CallbacksOnMultipleActionsTest < ActiveRecord::TestCase assert_equal [:update_and_destroy, :create_and_destroy], topic.history end end + + +class TransactionEnrollmentCallbacksTest < ActiveRecord::TestCase + + class TopicWithoutTransactionalEnrollmentCallbacks < ActiveRecord::Base + self.table_name = :topics + + before_commit_without_transaction_enrollment { |r| r.history << :before_commit } + after_commit_without_transaction_enrollment { |r| r.history << :after_commit } + after_rollback_without_transaction_enrollment { |r| r.history << :rollback } + + def history + @history ||= [] + end + end + + def setup + @topic = TopicWithoutTransactionalEnrollmentCallbacks.create! + end + + def test_commit_does_not_run_transactions_callbacks_without_enrollment + @topic.transaction do + @topic.content = 'foo' + @topic.save! + end + assert @topic.history.empty? + end + + def test_commit_run_transactions_callbacks_with_explicit_enrollment + @topic.transaction do + 2.times do + @topic.content = 'foo' + @topic.save! + end + @topic.class.connection.add_transaction_record(@topic) + end + assert_equal [:before_commit, :after_commit], @topic.history + end + + def test_rollback_does_not_run_transactions_callbacks_without_enrollment + @topic.transaction do + @topic.content = 'foo' + @topic.save! + raise ActiveRecord::Rollback + end + assert @topic.history.empty? + end + + def test_rollback_run_transactions_callbacks_with_explicit_enrollment + @topic.transaction do + 2.times do + @topic.content = 'foo' + @topic.save! + end + @topic.class.connection.add_transaction_record(@topic) + raise ActiveRecord::Rollback + end + assert_equal [:rollback], @topic.history + end +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/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/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/mysql2_specific_schema.rb b/activerecord/test/schema/mysql2_specific_schema.rb index a9a6514c9d..d6fd0c4ab0 100644 --- a/activerecord/test/schema/mysql2_specific_schema.rb +++ b/activerecord/test/schema/mysql2_specific_schema.rb @@ -35,9 +35,7 @@ BEGIN END SQL - ActiveRecord::Base.connection.execute <<-SQL -DROP TABLE IF EXISTS collation_tests; -SQL + ActiveRecord::Base.connection.drop_table "collation_tests", if_exists: true ActiveRecord::Base.connection.execute <<-SQL CREATE TABLE collation_tests ( @@ -46,9 +44,7 @@ CREATE TABLE collation_tests ( ) CHARACTER SET utf8 COLLATE utf8_general_ci SQL - ActiveRecord::Base.connection.execute <<-SQL -DROP TABLE IF EXISTS enum_tests; -SQL + ActiveRecord::Base.connection.drop_table "enum_tests", if_exists: true ActiveRecord::Base.connection.execute <<-SQL CREATE TABLE enum_tests ( diff --git a/activerecord/test/schema/mysql_specific_schema.rb b/activerecord/test/schema/mysql_specific_schema.rb index f2cffca52c..b5378341b5 100644 --- a/activerecord/test/schema/mysql_specific_schema.rb +++ b/activerecord/test/schema/mysql_specific_schema.rb @@ -46,9 +46,7 @@ BEGIN END SQL - ActiveRecord::Base.connection.execute <<-SQL -DROP TABLE IF EXISTS collation_tests; -SQL + ActiveRecord::Base.connection.drop_table "collation_tests", if_exists: true ActiveRecord::Base.connection.execute <<-SQL CREATE TABLE collation_tests ( @@ -57,9 +55,7 @@ CREATE TABLE collation_tests ( ) CHARACTER SET utf8 COLLATE utf8_general_ci SQL - ActiveRecord::Base.connection.execute <<-SQL -DROP TABLE IF EXISTS enum_tests; -SQL + ActiveRecord::Base.connection.drop_table "enum_tests", if_exists: true ActiveRecord::Base.connection.execute <<-SQL CREATE TABLE enum_tests ( diff --git a/activerecord/test/schema/postgresql_specific_schema.rb b/activerecord/test/schema/postgresql_specific_schema.rb index 77d2f7fda2..f84be0e7f4 100644 --- a/activerecord/test/schema/postgresql_specific_schema.rb +++ b/activerecord/test/schema/postgresql_specific_schema.rb @@ -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..5e5f7a798e 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 @@ -676,6 +680,7 @@ ActiveRecord::Schema.define do create_table :ship_parts, force: true do |t| t.string :name t.integer :ship_id + t.datetime :updated_at end create_table :speedometers, force: true, id: false do |t| |