diff options
Diffstat (limited to 'activerecord')
151 files changed, 2127 insertions, 736 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 6494c7374e..8980a1d874 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,158 @@ +* Clear query cache on rollback. + + *Florian Weingarten* + +* Fixed setting of foreign_key for through associations while building of new record. + + Fixes #12698. + + *Ivan Antropov* + +* Improve a dump of the primary key support. If it is not a default primary key, + correctly dump the type and options. + + Fixes #14169, #16599. + + *Ryuta Kamizono* + +* Format the datetime string according to the precision of the datetime field. + + Incompatible to rounding behavior between MySQL 5.6 and earlier. + + In 5.5, when you insert `2014-08-17 12:30:00.999999` the fractional part + is ignored. In 5.6, it's rounded to `2014-08-17 12:30:01`: + + http://bugs.mysql.com/bug.php?id=68760 + + *Ryuta Kamizono* + +* Allow precision option for MySQL datetimes. + + *Ryuta Kamizono* + +* Fixed automatic inverse_of for models nested in module. + + *Andrew McCloud* + +* Change `ActiveRecord::Relation#update` behavior so that it can + be called without passing ids of the records to be updated. + + This change allows to update multiple records returned by + `ActiveRecord::Relation` with callbacks and validations. + + # Before + # ArgumentError: wrong number of arguments (1 for 2) + Comment.where(group: 'expert').update(body: "Group of Rails Experts") + + # After + # Comments with group expert updated with body "Group of Rails Experts" + Comment.where(group: 'expert').update(body: "Group of Rails Experts") + + *Prathamesh Sonpatki* + +* Fix `reaping_frequency` option when the value is a string. + + This usually happens when it is configured using `DATABASE_URL`. + + *korbin* + +* Fix error message when trying to create an associated record and the foreign + key is missing. + + Before this fix the following exception was being raised: + + NoMethodError: undefined method `val' for #<Arel::Nodes::BindParam:0x007fc64d19c218> + + Now the message is: + + ActiveRecord::UnknownAttributeError: unknown attribute 'foreign_key' for Model. + + *Rafael Mendonça França* + +* When a table has a composite primary key, the `primary_key` method for + SQLite3 and PostgreSQL adapters was only returning the first field of the key. + Ensures that it will return nil instead, as Active Record doesn't support + composite primary keys. + + Fixes #18070. + + *arthurnn* + +* `validates_size_of` / `validates_length_of` do not count records, + which are `marked_for_destruction?`. + + Fixes #7247. + + *Yves Senn* + +* Ensure `first!` and friends work on loaded associations. + + Fixes #18237. + + *Sean Griffin* + +* `eager_load` preserves readonly flag for associations. + + Closes #15853. + + *Takashi Kokubun* + +* Provide `:touch` option to `save()` to accommodate saving without updating + timestamps. + + Fixes #18202. + + *Dan Olson* + +* Provide a more helpful error message when an unsupported class is passed to + `serialize`. + + Fixes #18224. + + *Sean Griffin* + +* Add bigint primary key support for MySQL. + + Example: + + create_table :foos, id: :bigint do |t| + end + + *Ryuta Kamizono* + +* Support for any type primary key. + + Fixes #14194. + + *Ryuta Kamizono* + +* Dump the default `nil` for PostgreSQL UUID primary key. + + *Ryuta Kamizono* + +* Add a `:foreign_key` option to `references` and associated migration + methods. The model and migration generators now use this option, rather than + the `add_foreign_key` form. + + *Sean Griffin* + +* Don't raise when writing an attribute with an out-of-range datetime passed + by the user. + + *Grey Baker* + +* Replace deprecated `ActiveRecord::Tasks::DatabaseTasks#load_schema` with + `ActiveRecord::Tasks::DatabaseTasks#load_schema_for`. + + *Yves Senn* + +* Fixes bug with 'ActiveRecord::Type::Numeric' that causes negative values to + be marked as having changed when set to the same negative value. + + Closes #18161. + + *Daniel Fox* + * Introduce `force: :cascade` option for `create_table`. Using this option will recreate tables even if they have dependent objects (like foreign keys). `db/schema.rb` now uses `force: :cascade`. This makes it possible to @@ -13,7 +168,7 @@ *Yves Senn* -* Fix undesirable RangeError by Type::Integer. Add Type::UnsignedInteger. +* Fix undesirable RangeError by `Type::Integer`. Add `Type::UnsignedInteger`. *Ryuta Kamizono* diff --git a/activerecord/MIT-LICENSE b/activerecord/MIT-LICENSE index 2950f05b11..7c2197229d 100644 --- a/activerecord/MIT-LICENSE +++ b/activerecord/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2014 David Heinemeier Hansson +Copyright (c) 2004-2015 David Heinemeier Hansson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/activerecord/RUNNING_UNIT_TESTS.rdoc b/activerecord/RUNNING_UNIT_TESTS.rdoc index 569685bd45..7e3460365b 100644 --- a/activerecord/RUNNING_UNIT_TESTS.rdoc +++ b/activerecord/RUNNING_UNIT_TESTS.rdoc @@ -20,11 +20,11 @@ example: Simply executing <tt>bundle exec rake test</tt> is equivalent to the following: - $ bundle exec rake test_mysql - $ bundle exec rake test_mysql2 - $ bundle exec rake test_postgresql - $ bundle exec rake test_sqlite3 - $ bundle exec rake test_sqlite3_mem + $ bundle exec rake test:mysql + $ bundle exec rake test:mysql2 + $ bundle exec rake test:postgresql + $ bundle exec rake test:sqlite3 + $ bundle exec rake test:sqlite3_mem There should be tests available for each database backend listed in the {Config File}[rdoc-label:label-Config+File]. (the exact set of available tests is diff --git a/activerecord/activerecord.gemspec b/activerecord/activerecord.gemspec index 471769a962..c5cd0c89f7 100644 --- a/activerecord/activerecord.gemspec +++ b/activerecord/activerecord.gemspec @@ -7,7 +7,7 @@ Gem::Specification.new do |s| s.summary = 'Object-relational mapper framework (part of Rails).' s.description = 'Databases on Rails. Build a persistent domain model by mapping database tables to Ruby classes. Strong conventions for associations, validations, aggregations, migrations, and testing come baked-in.' - s.required_ruby_version = '>= 2.1.0' + s.required_ruby_version = '>= 2.2.0' s.license = 'MIT' @@ -24,5 +24,5 @@ Gem::Specification.new do |s| s.add_dependency 'activesupport', version s.add_dependency 'activemodel', version - s.add_dependency 'arel', '~> 6.0' + s.add_dependency 'arel', '7.0.0.alpha' end diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index 9028970a3d..2eec62846b 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -1,5 +1,5 @@ #-- -# Copyright (c) 2004-2014 David Heinemeier Hansson +# Copyright (c) 2004-2015 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -62,6 +62,7 @@ module ActiveRecord autoload :Serialization autoload :StatementCache autoload :Store + autoload :TableMetadata autoload :Timestamp autoload :Transactions autoload :Translation diff --git a/activerecord/lib/active_record/association_relation.rb b/activerecord/lib/active_record/association_relation.rb index 5a84792f45..f2b44913db 100644 --- a/activerecord/lib/active_record/association_relation.rb +++ b/activerecord/lib/active_record/association_relation.rb @@ -1,7 +1,7 @@ module ActiveRecord class AssociationRelation < Relation - def initialize(klass, table, association) - super(klass, table) + def initialize(klass, table, predicate_builder, association) + super(klass, table, predicate_builder) @association = association end diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index cd5fdd5964..14af55f327 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -116,6 +116,7 @@ module ActiveRecord autoload :Association, 'active_record/associations/association' autoload :SingularAssociation, 'active_record/associations/singular_association' autoload :CollectionAssociation, 'active_record/associations/collection_association' + autoload :ForeignAssociation, 'active_record/associations/foreign_association' autoload :CollectionProxy, 'active_record/associations/collection_proxy' autoload :BelongsToAssociation, 'active_record/associations/belongs_to_association' diff --git a/activerecord/lib/active_record/associations/alias_tracker.rb b/activerecord/lib/active_record/associations/alias_tracker.rb index 0c3234ed24..2b7e4f28c5 100644 --- a/activerecord/lib/active_record/associations/alias_tracker.rb +++ b/activerecord/lib/active_record/associations/alias_tracker.rb @@ -5,20 +5,23 @@ module ActiveRecord # Keeps track of table aliases for ActiveRecord::Associations::ClassMethods::JoinDependency and # ActiveRecord::Associations::ThroughAssociationScope class AliasTracker # :nodoc: - attr_reader :aliases, :connection + attr_reader :aliases - def self.empty(connection) - new connection, Hash.new(0) + def self.create(connection, initial_table, type_caster) + aliases = Hash.new(0) + aliases[initial_table] = 1 + new connection, aliases, type_caster end - def self.create(connection, table_joins) - if table_joins.empty? - empty connection + def self.create_with_joins(connection, initial_table, joins, type_caster) + if joins.empty? + create(connection, initial_table, type_caster) else - aliases = Hash.new { |h,k| - h[k] = initial_count_for(connection, k, table_joins) + aliases = Hash.new { |h, k| + h[k] = initial_count_for(connection, k, joins) } - new connection, aliases + aliases[initial_table] = 1 + new connection, aliases, type_caster end end @@ -51,19 +54,20 @@ module ActiveRecord end # table_joins is an array of arel joins which might conflict with the aliases we assign here - def initialize(connection, aliases) + def initialize(connection, aliases, type_caster) @aliases = aliases @connection = connection + @type_caster = type_caster end def aliased_table_for(table_name, aliased_name) if aliases[table_name].zero? # If it's zero, we can have our table_name aliases[table_name] = 1 - Arel::Table.new(table_name) + Arel::Table.new(table_name, type_caster: @type_caster) else # Otherwise, we need to use an alias - aliased_name = connection.table_alias_for(aliased_name) + aliased_name = @connection.table_alias_for(aliased_name) # Update the count aliases[aliased_name] += 1 @@ -73,14 +77,14 @@ module ActiveRecord else aliased_name end - Arel::Table.new(table_name).alias(table_alias) + Arel::Table.new(table_name, type_caster: @type_caster).alias(table_alias) end end private def truncate(name) - name.slice(0, connection.table_alias_length - 2) + name.slice(0, @connection.table_alias_length - 2) end end end diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb index f1c36cd047..0d8e4ba870 100644 --- a/activerecord/lib/active_record/associations/association.rb +++ b/activerecord/lib/active_record/associations/association.rb @@ -121,7 +121,7 @@ module ActiveRecord # Can be overridden (i.e. in ThroughAssociation) to merge in other scopes (i.e. the # through association's scope) def target_scope - AssociationRelation.create(klass, klass.arel_table, self).merge!(klass.all) + AssociationRelation.create(klass, klass.arel_table, klass.predicate_builder, self).merge!(klass.all) end # Loads the \target if needed and returns it. diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb index 0ac10531e5..d06b7b3508 100644 --- a/activerecord/lib/active_record/associations/association_scope.rb +++ b/activerecord/lib/active_record/associations/association_scope.rb @@ -33,10 +33,11 @@ module ActiveRecord reflection = association.reflection scope = klass.unscoped owner = association.owner - alias_tracker = AliasTracker.empty connection + alias_tracker = AliasTracker.create connection, association.klass.table_name, klass.type_caster + chain_head, chain_tail = get_chain(reflection, association, alias_tracker) scope.extending! Array(reflection.options[:extend]) - add_constraints(scope, owner, klass, reflection, alias_tracker) + add_constraints(scope, owner, klass, reflection, connection, chain_head, chain_tail) end def join_type @@ -61,22 +62,6 @@ module ActiveRecord end private - - def construct_tables(chain, klass, refl, alias_tracker) - chain.map do |reflection| - alias_tracker.aliased_table_for( - table_name_for(reflection, klass, refl), - table_alias_for(reflection, refl, reflection != refl) - ) - end - end - - def table_alias_for(reflection, refl, join = false) - name = "#{reflection.plural_name}_#{alias_suffix(refl)}" - name << "_join" if join - name - end - def join(table, constraint) table.create_join(table, table.create_on(constraint), join_type) end @@ -95,8 +80,8 @@ module ActiveRecord bind_value scope, column, value, connection end - def last_chain_scope(scope, table, reflection, owner, connection, assoc_klass) - join_keys = reflection.join_keys(assoc_klass) + def last_chain_scope(scope, table, reflection, owner, connection, association_klass) + join_keys = reflection.join_keys(association_klass) key = join_keys.key foreign_key = join_keys.foreign_key @@ -112,8 +97,8 @@ module ActiveRecord end end - def next_chain_scope(scope, table, reflection, connection, assoc_klass, foreign_table, next_reflection) - join_keys = reflection.join_keys(assoc_klass) + def next_chain_scope(scope, table, reflection, connection, association_klass, foreign_table, next_reflection) + join_keys = reflection.join_keys(association_klass) key = join_keys.key foreign_key = join_keys.foreign_key @@ -128,38 +113,57 @@ module ActiveRecord scope = scope.joins(join(foreign_table, constraint)) end - def add_constraints(scope, owner, assoc_klass, refl, tracker) - chain = refl.chain - scope_chain = refl.scope_chain - connection = tracker.connection + class ReflectionProxy < SimpleDelegator # :nodoc: + attr_accessor :next + attr_reader :alias_name + + def initialize(reflection, alias_name) + super(reflection) + @alias_name = alias_name + end + + def all_includes; nil; end + end - tables = construct_tables(chain, assoc_klass, refl, tracker) + def get_chain(reflection, association, tracker) + name = reflection.name + runtime_reflection = Reflection::RuntimeReflection.new(reflection, association) + previous_reflection = runtime_reflection + reflection.chain.drop(1).each do |refl| + alias_name = tracker.aliased_table_for(refl.table_name, refl.alias_candidate(name)) + proxy = ReflectionProxy.new(refl, alias_name) + previous_reflection.next = proxy + previous_reflection = proxy + end + [runtime_reflection, previous_reflection] + end - owner_reflection = chain.last - table = tables.last - scope = last_chain_scope(scope, table, owner_reflection, owner, connection, assoc_klass) + def add_constraints(scope, owner, association_klass, refl, connection, chain_head, chain_tail) + owner_reflection = chain_tail + table = owner_reflection.alias_name + scope = last_chain_scope(scope, table, owner_reflection, owner, connection, association_klass) - chain.each_with_index do |reflection, i| - table, foreign_table = tables.shift, tables.first + reflection = chain_head + loop do + break unless reflection + table = reflection.alias_name - unless reflection == chain.last - next_reflection = chain[i + 1] - scope = next_chain_scope(scope, table, reflection, connection, assoc_klass, foreign_table, next_reflection) + unless reflection == chain_tail + next_reflection = reflection.next + foreign_table = next_reflection.alias_name + scope = next_chain_scope(scope, table, reflection, connection, association_klass, foreign_table, next_reflection) end - is_first_chain = i == 0 - klass = is_first_chain ? assoc_klass : reflection.klass - # Exclude the scope of the association itself, because that # was already merged in the #scope method. - scope_chain[i].each do |scope_chain_item| - item = eval_scope(klass, scope_chain_item, owner) + reflection.constraints.each do |scope_chain_item| + item = eval_scope(reflection.klass, scope_chain_item, owner) if scope_chain_item == refl.scope scope.merge! item.except(:where, :includes, :bind) end - if is_first_chain + reflection.all_includes do scope.includes! item.includes_values end @@ -167,26 +171,13 @@ module ActiveRecord scope.bind_values += item.bind_values scope.order_values |= item.order_values end + + reflection = reflection.next end scope end - def alias_suffix(refl) - refl.name - end - - def table_name_for(reflection, klass, refl) - if reflection == refl - # If this is a polymorphic belongs_to, we want to get the klass from the - # association because it depends on the polymorphic_type attribute of - # the owner - klass.table_name - else - reflection.table_name - end - end - def eval_scope(klass, scope, owner) klass.unscoped.instance_exec(owner, &scope) end diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb index 81fdd681de..c63b42e2a0 100644 --- a/activerecord/lib/active_record/associations/belongs_to_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_association.rb @@ -73,11 +73,11 @@ module ActiveRecord # Checks whether record is different to the current target, without loading it def different_target?(record) - record.id != owner[reflection.foreign_key] + record.id != owner._read_attribute(reflection.foreign_key) end def replace_keys(record) - owner[reflection.foreign_key] = record[reflection.association_primary_key(record.class)] + owner[reflection.foreign_key] = record._read_attribute(reflection.association_primary_key(record.class)) end def remove_keys @@ -85,7 +85,7 @@ module ActiveRecord end def foreign_key_present? - owner[reflection.foreign_key] + owner._read_attribute(reflection.foreign_key) end # NOTE - for now, we're only supporting inverse setting from belongs_to back onto @@ -99,12 +99,13 @@ module ActiveRecord if options[:primary_key] owner.send(reflection.name).try(:id) else - owner[reflection.foreign_key] + owner._read_attribute(reflection.foreign_key) end end def stale_state - owner[reflection.foreign_key] && owner[reflection.foreign_key].to_s + result = owner._read_attribute(reflection.foreign_key) + result && result.to_s end end end diff --git a/activerecord/lib/active_record/associations/builder/association.rb b/activerecord/lib/active_record/associations/builder/association.rb index 947d61ee7b..88406740d8 100644 --- a/activerecord/lib/active_record/associations/builder/association.rb +++ b/activerecord/lib/active_record/associations/builder/association.rb @@ -1,5 +1,3 @@ -require 'active_support/core_ext/module/attribute_accessors' - # This is the parent Association class which defines the variables # used by all associations. # @@ -15,15 +13,10 @@ module ActiveRecord::Associations::Builder class Association #:nodoc: class << self attr_accessor :extensions - # TODO: This class accessor is needed to make activerecord-deprecated_finders work. - # We can move it to a constant in 5.0. - attr_accessor :valid_options end self.extensions = [] - self.valid_options = [:class_name, :class, :foreign_key, :validate] - - attr_reader :name, :scope, :options + VALID_OPTIONS = [:class_name, :class, :foreign_key, :validate] # :nodoc: def self.build(model, name, scope, options, &block) if model.dangerous_attribute_method?(name) @@ -32,57 +25,60 @@ module ActiveRecord::Associations::Builder "Please choose a different association name." end - builder = create_builder model, name, scope, options, &block - reflection = builder.build(model) + extension = define_extensions model, name, &block + reflection = create_reflection model, name, scope, options, extension define_accessors model, reflection define_callbacks model, reflection define_validations model, reflection - builder.define_extensions model reflection end - def self.create_builder(model, name, scope, options, &block) + def self.create_reflection(model, name, scope, options, extension = nil) raise ArgumentError, "association names must be a Symbol" unless name.kind_of?(Symbol) - new(model, name, scope, options, &block) - end - - def initialize(model, name, scope, options) - # TODO: Move this to create_builder as soon we drop support to activerecord-deprecated_finders. if scope.is_a?(Hash) options = scope scope = nil end - # TODO: Remove this model argument as soon we drop support to activerecord-deprecated_finders. - @name = name - @scope = scope - @options = options + validate_options(options) - validate_options + scope = build_scope(scope, extension) + + ActiveRecord::Reflection.create(macro, name, scope, options, model) + end + + def self.build_scope(scope, extension) + new_scope = scope if scope && scope.arity == 0 - @scope = proc { instance_exec(&scope) } + new_scope = proc { instance_exec(&scope) } + end + + if extension + new_scope = wrap_scope new_scope, extension end + + new_scope end - def build(model) - ActiveRecord::Reflection.create(macro, name, scope, options, model) + def self.wrap_scope(scope, extension) + scope end - def macro + def self.macro raise NotImplementedError end - def valid_options - Association.valid_options + Association.extensions.flat_map(&:valid_options) + def self.valid_options(options) + VALID_OPTIONS + Association.extensions.flat_map(&:valid_options) end - def validate_options - options.assert_valid_keys(valid_options) + def self.validate_options(options) + options.assert_valid_keys(valid_options(options)) end - def define_extensions(model) + def self.define_extensions(model, name) end def self.define_callbacks(model, reflection) @@ -133,8 +129,6 @@ module ActiveRecord::Associations::Builder raise NotImplementedError end - private - def self.check_dependent_options(dependent) unless valid_dependent_options.include? dependent raise ArgumentError, "The :dependent option must be one of #{valid_dependent_options}, but is :#{dependent}" diff --git a/activerecord/lib/active_record/associations/builder/belongs_to.rb b/activerecord/lib/active_record/associations/builder/belongs_to.rb index 954ea3878a..d0ad57f9c6 100644 --- a/activerecord/lib/active_record/associations/builder/belongs_to.rb +++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb @@ -1,10 +1,10 @@ module ActiveRecord::Associations::Builder class BelongsTo < SingularAssociation #:nodoc: - def macro + def self.macro :belongs_to end - def valid_options + def self.valid_options(options) super + [:foreign_type, :polymorphic, :touch, :counter_cache] end @@ -23,8 +23,6 @@ module ActiveRecord::Associations::Builder add_counter_cache_methods mixin end - private - def self.add_counter_cache_methods(mixin) return if mixin.method_defined? :belongs_to_counter_cache_after_update diff --git a/activerecord/lib/active_record/associations/builder/collection_association.rb b/activerecord/lib/active_record/associations/builder/collection_association.rb index bc15a49996..2ff67f904d 100644 --- a/activerecord/lib/active_record/associations/builder/collection_association.rb +++ b/activerecord/lib/active_record/associations/builder/collection_association.rb @@ -7,22 +7,11 @@ module ActiveRecord::Associations::Builder CALLBACKS = [:before_add, :after_add, :before_remove, :after_remove] - def valid_options + def self.valid_options(options) super + [:table_name, :before_add, :after_add, :before_remove, :after_remove, :extend] end - attr_reader :block_extension - - def initialize(model, name, scope, options) - super - @mod = nil - if block_given? - @mod = Module.new(&Proc.new) - @scope = wrap_scope @scope, @mod - end - end - def self.define_callbacks(model, reflection) super name = reflection.name @@ -32,10 +21,11 @@ module ActiveRecord::Associations::Builder } end - def define_extensions(model) - if @mod + def self.define_extensions(model, name) + if block_given? extension_module_name = "#{model.name.demodulize}#{name.to_s.camelize}AssociationExtension" - model.parent.const_set(extension_module_name, @mod) + extension = Module.new(&Proc.new) + model.parent.const_set(extension_module_name, extension) end end @@ -78,9 +68,7 @@ module ActiveRecord::Associations::Builder CODE end - private - - def wrap_scope(scope, mod) + def self.wrap_scope(scope, mod) if scope proc { |owner| instance_exec(owner, &scope).extending(mod) } else diff --git a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb index 092b4ebd2f..93dc4ae118 100644 --- a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb +++ b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb @@ -87,11 +87,11 @@ module ActiveRecord::Associations::Builder middle_name = [lhs_model.name.downcase.pluralize, association_name].join('_').gsub(/::/, '_').to_sym middle_options = middle_options join_model - hm_builder = HasMany.create_builder(lhs_model, - middle_name, - nil, - middle_options) - hm_builder.build lhs_model + + HasMany.create_reflection(lhs_model, + middle_name, + nil, + middle_options) end private diff --git a/activerecord/lib/active_record/associations/builder/has_many.rb b/activerecord/lib/active_record/associations/builder/has_many.rb index 1b87f92170..1c1b47bd56 100644 --- a/activerecord/lib/active_record/associations/builder/has_many.rb +++ b/activerecord/lib/active_record/associations/builder/has_many.rb @@ -1,10 +1,10 @@ module ActiveRecord::Associations::Builder class HasMany < CollectionAssociation #:nodoc: - def macro + def self.macro :has_many end - def valid_options + def self.valid_options(options) super + [:primary_key, :dependent, :as, :through, :source, :source_type, :inverse_of, :counter_cache, :join_table, :foreign_type] end diff --git a/activerecord/lib/active_record/associations/builder/has_one.rb b/activerecord/lib/active_record/associations/builder/has_one.rb index 1387717396..64e9e6b334 100644 --- a/activerecord/lib/active_record/associations/builder/has_one.rb +++ b/activerecord/lib/active_record/associations/builder/has_one.rb @@ -1,10 +1,10 @@ module ActiveRecord::Associations::Builder class HasOne < SingularAssociation #:nodoc: - def macro + def self.macro :has_one end - def valid_options + def self.valid_options(options) valid = super + [:as, :foreign_type] valid += [:through, :source, :source_type] if options[:through] valid @@ -14,8 +14,6 @@ module ActiveRecord::Associations::Builder [:destroy, :delete, :nullify, :restrict_with_error, :restrict_with_exception] end - private - def self.add_destroy_callbacks(model, reflection) super unless reflection.options[:through] end diff --git a/activerecord/lib/active_record/associations/builder/singular_association.rb b/activerecord/lib/active_record/associations/builder/singular_association.rb index 6e6dd7204c..1369212837 100644 --- a/activerecord/lib/active_record/associations/builder/singular_association.rb +++ b/activerecord/lib/active_record/associations/builder/singular_association.rb @@ -2,7 +2,7 @@ module ActiveRecord::Associations::Builder class SingularAssociation < Association #:nodoc: - def valid_options + def self.valid_options(options) super + [:dependent, :primary_key, :inverse_of, :required] end diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index 7b6aefe345..f2c96e9a2a 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -218,11 +218,7 @@ module ActiveRecord # Count all records using SQL. Construct options and pass them with # scope to the target class's +count+. - def count(column_name = nil, count_options = {}) - # TODO: Remove count_options argument as soon we remove support to - # activerecord-deprecated_finders. - column_name, count_options = nil, column_name if column_name.is_a?(Hash) - + def count(column_name = nil) relation = scope if association_scope.distinct_value # This is needed because 'SELECT count(DISTINCT *)..' is not valid SQL. @@ -597,8 +593,8 @@ module ActiveRecord if reflection.is_a?(ActiveRecord::Reflection::ThroughReflection) assoc = owner.association(reflection.through_reflection.name) assoc.reader.any? { |source| - target = source.send(reflection.source_reflection.name) - target.respond_to?(:include?) ? target.include?(record) : target == record + target_reflection = source.send(reflection.source_reflection.name) + target_reflection.respond_to?(:include?) ? target_reflection.include?(record) : target_reflection == record } || target.include?(record) else target.include?(record) diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb index 060b2278d9..c22dc6e11e 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -29,10 +29,11 @@ module ActiveRecord # instantiation of the actual post records. class CollectionProxy < Relation delegate(*(ActiveRecord::Calculations.public_instance_methods - [:count]), to: :scope) + delegate :find_nth, to: :scope def initialize(klass, association) #:nodoc: @association = association - super klass, klass.arel_table + super klass, klass.arel_table, klass.predicate_builder merge! association.scope(nullify: false) end @@ -687,10 +688,8 @@ module ActiveRecord # # #<Pet id: 2, name: "Spook", person_id: 1>, # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> # # ] - def count(column_name = nil, options = {}) - # TODO: Remove options argument as soon we remove support to - # activerecord-deprecated_finders. - @association.count(column_name, options) + def count(column_name = nil) + @association.count(column_name) end # Returns the size of the collection. If the collection hasn't been loaded, diff --git a/activerecord/lib/active_record/associations/foreign_association.rb b/activerecord/lib/active_record/associations/foreign_association.rb new file mode 100644 index 0000000000..fe48ecec29 --- /dev/null +++ b/activerecord/lib/active_record/associations/foreign_association.rb @@ -0,0 +1,11 @@ +module ActiveRecord::Associations + module ForeignAssociation + def foreign_key_present? + if reflection.klass.primary_key + owner.attribute_present?(reflection.active_record_primary_key) + else + false + end + end + end +end diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index 93084e0dcf..d7f655d00c 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -6,6 +6,7 @@ module ActiveRecord # If the association has a <tt>:through</tt> option further specialization # is provided by its child HasManyThroughAssociation. class HasManyAssociation < CollectionAssociation #:nodoc: + include ForeignAssociation def handle_dependency case options[:dependent] @@ -153,14 +154,6 @@ module ActiveRecord end end - def foreign_key_present? - if reflection.klass.primary_key - owner.attribute_present?(reflection.association_primary_key) - else - false - end - end - def concat_records(records, *) update_counter_if_success(super, records.length) end diff --git a/activerecord/lib/active_record/associations/has_many_through_association.rb b/activerecord/lib/active_record/associations/has_many_through_association.rb index 3f4d3bfc08..7a050ca224 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -13,21 +13,6 @@ module ActiveRecord @through_association = nil end - # Returns the size of the collection by executing a SELECT COUNT(*) query - # if the collection hasn't been loaded, and by calling collection.size if - # it has. If the collection will likely have a size greater than zero, - # and if fetching the collection will be needed afterwards, one less - # SELECT query will be generated by using #length instead. - def size - if has_cached_counter? - owner._read_attribute cached_counter_attribute_name(reflection) - elsif loaded? - target.size - else - super - end - end - def concat(*records) unless owner.new_record? records.flatten.each do |record| diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index e6095d84dc..74b8c53758 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -2,6 +2,7 @@ module ActiveRecord # = Active Record Belongs To Has One Association module Associations class HasOneAssociation < SingularAssociation #:nodoc: + include ForeignAssociation def handle_dependency case options[:dependent] diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb index cf63430a97..4b75370171 100644 --- a/activerecord/lib/active_record/associations/join_dependency.rb +++ b/activerecord/lib/active_record/associations/join_dependency.rb @@ -93,8 +93,7 @@ module ActiveRecord # joins # => [] # def initialize(base, associations, joins) - @alias_tracker = AliasTracker.create(base.connection, joins) - @alias_tracker.aliased_table_for(base.table_name, base.table_name) # Updates the count for base.table_name to 1 + @alias_tracker = AliasTracker.create_with_joins(base.connection, base.table_name, joins, base.type_caster) tree = self.class.make_tree associations @join_root = JoinBase.new base, build(tree, base) @join_root.children.each { |child| construct_tables! @join_root, child } @@ -257,6 +256,7 @@ module ActiveRecord construct(model, node, row, rs, seen, model_cache, aliases) else model = construct_model(ar_parent, node, row, model_cache, id, aliases) + model.readonly! seen[parent.base_klass][primary_id][node.base_klass][id] = model construct(model, node, row, rs, seen, model_cache, aliases) end diff --git a/activerecord/lib/active_record/associations/join_dependency/join_association.rb b/activerecord/lib/active_record/associations/join_dependency/join_association.rb index 5dede5527d..c1ef86a95b 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_association.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb @@ -43,16 +43,23 @@ module ActiveRecord constraint = build_constraint(klass, table, key, foreign_table, foreign_key) + predicate_builder = PredicateBuilder.new(TableMetadata.new(klass, table)) scope_chain_items = scope_chain[scope_chain_index].map do |item| if item.is_a?(Relation) item else - ActiveRecord::Relation.create(klass, table).instance_exec(node, &item) + ActiveRecord::Relation.create(klass, table, predicate_builder) + .instance_exec(node, &item) end end scope_chain_index += 1 - scope_chain_items.concat [klass.send(:build_default_scope, ActiveRecord::Relation.create(klass, table))].compact + relation = ActiveRecord::Relation.create( + klass, + table, + predicate_builder, + ) + scope_chain_items.concat [klass.send(:build_default_scope, relation)].compact rel = scope_chain_items.inject(scope_chain_items.shift) do |left, right| left.merge right diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb index 7d6523dbc4..a6e1a24360 100644 --- a/activerecord/lib/active_record/associations/preloader/association.rb +++ b/activerecord/lib/active_record/associations/preloader/association.rb @@ -33,7 +33,7 @@ module ActiveRecord end def query_scope(ids) - scope.where(association_key.in(ids)) + scope.where(association_key_name => ids) end def table diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb index e47e81aa0f..09828dbd9b 100644 --- a/activerecord/lib/active_record/associations/through_association.rb +++ b/activerecord/lib/active_record/associations/through_association.rb @@ -91,6 +91,17 @@ module ActiveRecord raise HasManyThroughNestedAssociationsAreReadonly.new(owner, reflection) end end + + def build_record(attributes) + inverse = source_reflection.inverse_of + target = through_association.target + + if inverse && target && !target.is_a?(Array) + attributes[inverse.foreign_key] = target.id + end + + super(attributes) + end end end end diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index 83fcefa64d..b7edac791e 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -150,7 +150,7 @@ module ActiveRecord BLACKLISTED_CLASS_METHODS.include?(method_name.to_s) || class_method_defined_within?(method_name, Base) end - def class_method_defined_within?(name, klass, superklass = klass.superclass) # :nodoc + def class_method_defined_within?(name, klass, superklass = klass.superclass) # :nodoc: if klass.respond_to?(name, true) if superklass.respond_to?(name, true) klass.method(name).owner != superklass.method(name).owner diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb index 033e71f7b9..d5702accaf 100644 --- a/activerecord/lib/active_record/attribute_methods/dirty.rb +++ b/activerecord/lib/active_record/attribute_methods/dirty.rb @@ -76,6 +76,10 @@ module ActiveRecord private + def changes_include?(attr_name) + super || attribute_changed_in_place?(attr_name) + end + def calculate_changes_from_defaults @changed_attributes = nil self.class.column_defaults.each do |attr, orig_value| diff --git a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb index 87274dd4e1..777f7ab4d7 100644 --- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb +++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb @@ -12,7 +12,11 @@ module ActiveRecord if value.is_a?(Array) value.map { |v| type_cast_from_user(v) } elsif value.respond_to?(:in_time_zone) - value.in_time_zone || super + begin + value.in_time_zone || super + rescue ArgumentError + nil + end end end diff --git a/activerecord/lib/active_record/attributes.rb b/activerecord/lib/active_record/attributes.rb index 3288108a6a..aafb990bc1 100644 --- a/activerecord/lib/active_record/attributes.rb +++ b/activerecord/lib/active_record/attributes.rb @@ -53,9 +53,9 @@ module ActiveRecord # store_listing.price_in_cents # => 10 # # Users may also define their own custom types, as long as they respond to the methods - # defined on the value type. The `type_cast` method on your type object will be called + # defined on the value type. The +type_cast+ method on your type object will be called # with values both from the database, and from your controllers. See - # `ActiveRecord::Attributes::Type::Value` for the expected API. It is recommended that your + # +ActiveRecord::Attributes::Type::Value+ for the expected API. It is recommended that your # type objects inherit from an existing type, or the base value type. # # class MoneyType < ActiveRecord::Type::Integer @@ -122,6 +122,7 @@ module ActiveRecord end def clear_caches_calculated_from_columns + @arel_table = nil @attributes_builder = nil @column_names = nil @column_types = nil diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index f978fbd0a4..bb01231bca 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -22,6 +22,7 @@ require 'active_record/log_subscriber' require 'active_record/explain_subscriber' require 'active_record/relation/delegation' require 'active_record/attributes' +require 'active_record/type_caster' module ActiveRecord #:nodoc: # = Active Record @@ -141,7 +142,7 @@ module ActiveRecord #:nodoc: # # In addition to the basic accessors, query methods are also automatically available on the Active Record object. # Query methods allow you to test whether an attribute value is present. - # For numeric values, present is defined as non-zero. + # Additionally, when dealing with numeric values, a query method will return false if the value is zero. # # For example, an Active Record User with the <tt>name</tt> attribute has a <tt>name?</tt> method that you can call # to determine whether the user has a name: diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb index 523d492a48..497ce8c15c 100644 --- a/activerecord/lib/active_record/callbacks.rb +++ b/activerecord/lib/active_record/callbacks.rb @@ -298,7 +298,7 @@ module ActiveRecord private - def create_or_update #:nodoc: + def create_or_update(*) #:nodoc: _run_save_callbacks { super } end diff --git a/activerecord/lib/active_record/coders/yaml_column.rb b/activerecord/lib/active_record/coders/yaml_column.rb index d3d7396c91..9ea22ed798 100644 --- a/activerecord/lib/active_record/coders/yaml_column.rb +++ b/activerecord/lib/active_record/coders/yaml_column.rb @@ -8,6 +8,7 @@ module ActiveRecord def initialize(object_class = Object) @object_class = object_class + check_arity_of_constructor end def dump(obj) @@ -33,6 +34,16 @@ module ActiveRecord obj end + + private + + def check_arity_of_constructor + begin + load(nil) + rescue ArgumentError + raise ArgumentError, "Cannot serialize #{object_class}. Classes passed to `serialize` must have a 0 argument constructor." + end + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb index 0fa00d03a3..6235745fb2 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -236,7 +236,7 @@ module ActiveRecord @spec = spec @checkout_timeout = (spec.config[:checkout_timeout] && spec.config[:checkout_timeout].to_f) || 5 - @reaper = Reaper.new self, spec.config[:reaping_frequency] + @reaper = Reaper.new(self, (spec.config[:reaping_frequency] && spec.config[:reaping_frequency].to_f)) @reaper.run # default max pool size to 5 @@ -363,7 +363,7 @@ module ActiveRecord conn.expire end - release owner + release conn, owner @available.add conn end @@ -376,7 +376,7 @@ module ActiveRecord @connections.delete conn @available.delete conn - release conn.owner + release conn, conn.owner @available.add checkout_new_connection if @available.any_waiting? end @@ -424,10 +424,12 @@ module ActiveRecord end end - def release(owner) + def release(conn, owner) thread_id = owner.object_id - @reserved_connections.delete thread_id + if @reserved_connections[thread_id] == conn + @reserved_connections.delete thread_id + end end def new_connection 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 12b16b2473..59cdd8e98c 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -258,7 +258,18 @@ module ActiveRecord # Rolls back the transaction (and turns on auto-committing). Must be # done if the transaction block raises an exception or returns false. - def rollback_db_transaction() end + def rollback_db_transaction + exec_rollback_db_transaction + end + + def exec_rollback_db_transaction() end #:nodoc: + + def rollback_to_savepoint(name = nil) + exec_rollback_to_savepoint(name) + end + + def exec_rollback_to_savepoint(name = nil) #:nodoc: + end def default_sequence_name(table, column) nil diff --git a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb index 4a4506c7f5..5e27cfe507 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb @@ -3,7 +3,7 @@ module ActiveRecord module QueryCache class << self def included(base) #:nodoc: - dirties_query_cache base, :insert, :update, :delete + dirties_query_cache base, :insert, :update, :delete, :rollback_to_savepoint, :rollback_db_transaction end def dirties_query_cache(base, *method_names) diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb index 679878d860..143d7d9574 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb @@ -19,7 +19,7 @@ module ActiveRecord # Cast a +value+ to a type that the database understands. For example, # SQLite does not understand dates, so this method will convert a Date # to a String. - def type_cast(value, column) + def type_cast(value, column = nil) if value.respond_to?(:quoted_id) && value.respond_to?(:id) return value.id end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/savepoints.rb b/activerecord/lib/active_record/connection_adapters/abstract/savepoints.rb index 25c17ce971..c0662f8473 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/savepoints.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/savepoints.rb @@ -9,7 +9,7 @@ module ActiveRecord execute("SAVEPOINT #{name}") end - def rollback_to_savepoint(name = current_savepoint_name) + def exec_rollback_to_savepoint(name = current_savepoint_name) execute("ROLLBACK TO SAVEPOINT #{name}") 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 5c95b95184..18ff869ea6 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb @@ -30,7 +30,7 @@ module ActiveRecord def visit_ColumnDefinition(o) sql_type = type_to_sql(o.type, o.limit, o.precision, o.scale) column_sql = "#{quote_column_name(o.name)} #{sql_type}" - add_column_options!(column_sql, column_options(o)) unless o.primary_key? + add_column_options!(column_sql, column_options(o)) unless o.type == :primary_key column_sql end @@ -65,6 +65,8 @@ module ActiveRecord column_options[:column] = o column_options[:first] = o.first column_options[:after] = o.after + column_options[:auto_increment] = o.auto_increment + column_options[:primary_key] = o.primary_key column_options end @@ -89,14 +91,17 @@ module ActiveRecord if options[:auto_increment] == true sql << " AUTO_INCREMENT" end + if options[:primary_key] == true + sql << " PRIMARY KEY" + end sql end def quote_default_expression(value, column) column.sql_type ||= type_to_sql(column.type, column.limit, column.precision, column.scale) - column.cast_type ||= type_for_column(column) + value = type_for_column(column).type_cast_for_database(value) - @conn.quote(value, column) + @conn.quote(value) end def options_include_default?(options) 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 537e21029e..68d8a2cd6a 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -15,14 +15,14 @@ module ActiveRecord # are typically created by methods in TableDefinition, and added to the # +columns+ attribute of said TableDefinition object, in order to be used # for generating a number of table creation or table changing SQL statements. - class ColumnDefinition < Struct.new(:name, :type, :limit, :precision, :scale, :default, :null, :first, :after, :primary_key, :sql_type, :cast_type) #:nodoc: + class ColumnDefinition < Struct.new(:name, :type, :limit, :precision, :scale, :default, :null, :first, :after, :auto_increment, :primary_key, :sql_type, :cast_type) #:nodoc: def primary_key? primary_key || type.to_sym == :primary_key end end - class ChangeColumnDefinition < Struct.new(:column, :type, :options) #:nodoc: + class ChangeColumnDefinition < Struct.new(:column, :name) #:nodoc: end class ForeignKeyDefinition < Struct.new(:from_table, :to_table, :options) #:nodoc: @@ -68,6 +68,84 @@ module ActiveRecord end end + class ReferenceDefinition # :nodoc: + def initialize( + name, + polymorphic: false, + index: false, + foreign_key: false, + type: :integer, + **options + ) + @name = name + @polymorphic = polymorphic + @index = index + @foreign_key = foreign_key + @type = type + @options = options + + if polymorphic && foreign_key + raise ArgumentError, "Cannot add a foreign key to a polymorphic relation" + end + end + + def add_to(table) + columns.each do |column_options| + table.column(*column_options) + end + + if index + table.index(column_names, index_options) + end + + if foreign_key + table.foreign_key(foreign_table_name, foreign_key_options) + end + end + + protected + + attr_reader :name, :polymorphic, :index, :foreign_key, :type, :options + + private + + def as_options(value, default = {}) + if value.is_a?(Hash) + value + else + default + end + end + + def polymorphic_options + as_options(polymorphic, options) + end + + def index_options + as_options(index) + end + + def foreign_key_options + as_options(foreign_key) + end + + def columns + result = [["#{name}_id", type, options]] + if polymorphic + result.unshift(["#{name}_type", :string, polymorphic_options]) + end + result + end + + def column_names + columns.map(&:first) + end + + def foreign_table_name + name.to_s.pluralize + end + end + # Represents the schema of an SQL table in an abstract way. This class # provides methods for manipulating the schema representation. # @@ -94,11 +172,12 @@ module ActiveRecord # An array of ColumnDefinition objects, representing the column changes # that have been defined. attr_accessor :indexes - attr_reader :name, :temporary, :options, :as + attr_reader :name, :temporary, :options, :as, :foreign_keys def initialize(types, name, temporary, options, as = nil) @columns_hash = {} @indexes = {} + @foreign_keys = {} @native = types @temporary = temporary @options = options @@ -286,6 +365,10 @@ module ActiveRecord indexes[column_name] = options end + def foreign_key(table_name, options = {}) # :nodoc: + foreign_keys[table_name] = options + end + # Appends <tt>:datetime</tt> columns <tt>:created_at</tt> and # <tt>:updated_at</tt> to the table. See SchemaStatements#add_timestamps # @@ -297,24 +380,20 @@ module ActiveRecord column(:updated_at, :datetime, options) end - # Adds a reference. Optionally adds a +type+ column, if <tt>:polymorphic</tt> option is provided. - # <tt>references</tt> and <tt>belongs_to</tt> are acceptable. The reference column will be an +integer+ - # by default, the <tt>:type</tt> option can be used to specify a different type. + # 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, + # the +:type+ option can be used to specify a different type. A foreign + # key will be created if the +:foreign_key+ option is passed. # # t.references(:user) # t.references(:user, type: "string") # t.belongs_to(:supplier, polymorphic: true) # # See SchemaStatements#add_reference - def references(*args) - options = args.extract_options! - polymorphic = options.delete(:polymorphic) - index_options = options.delete(:index) - type = options.delete(:type) || :integer + def references(*args, **options) args.each do |col| - column("#{col}_id", type, options) - column("#{col}_type", :string, polymorphic.is_a?(Hash) ? polymorphic : options) if polymorphic - index(polymorphic ? %w(type id).map { |t| "#{col}_#{t}" } : "#{col}_id", index_options.is_a?(Hash) ? index_options : {}) if index_options + ReferenceDefinition.new(col, **options).add_to(self) end end alias :belongs_to :references @@ -333,6 +412,7 @@ module ActiveRecord column.null = options[:null] column.first = options[:first] column.after = options[:after] + column.auto_increment = options[:auto_increment] column.primary_key = type == :primary_key || options[:primary_key] column end @@ -422,33 +502,36 @@ module ActiveRecord end # Adds a new column to the named table. - # See TableDefinition#column for details of the options you can use. # - # ====== Creating a simple column # t.column(:name, :string) + # + # See TableDefinition#column for details of the options you can use. def column(column_name, type, options = {}) @base.add_column(name, column_name, type, options) end - # Checks to see if a column exists. See SchemaStatements#column_exists? + # Checks to see if a column exists. + # + # See SchemaStatements#column_exists? def column_exists?(column_name, type = nil, options = {}) @base.column_exists?(name, column_name, type, options) end # Adds a new index to the table. +column_name+ can be a single Symbol, or - # an Array of Symbols. See SchemaStatements#add_index + # an Array of Symbols. # - # ====== Creating a simple index # t.index(:name) - # ====== Creating a unique index # t.index([:branch_id, :party_id], unique: true) - # ====== Creating a named index # t.index([:branch_id, :party_id], unique: true, name: 'by_branch_party') + # + # See SchemaStatements#add_index for details of the options you can use. def index(column_name, options = {}) @base.add_index(name, column_name, options) end - # Checks to see if an index exists. See SchemaStatements#index_exists? + # Checks to see if an index exists. + # + # See SchemaStatements#index_exists? def index_exists?(column_name, options = {}) @base.index_exists?(name, column_name, options) end @@ -456,30 +539,37 @@ module ActiveRecord # Renames the given index on the table. # # t.rename_index(:user_id, :account_id) + # + # See SchemaStatements#rename_index def rename_index(index_name, new_index_name) @base.rename_index(name, index_name, new_index_name) end - # Adds timestamps (+created_at+ and +updated_at+) columns to the table. See SchemaStatements#add_timestamps + # Adds timestamps (+created_at+ and +updated_at+) columns to the table. # - # t.timestamps null: false + # t.timestamps(null: false) + # + # See SchemaStatements#add_timestamps def timestamps(options = {}) @base.add_timestamps(name, options) end # Changes the column's definition according to the new options. - # See TableDefinition#column for details of the options you can use. # # t.change(:name, :string, limit: 80) # t.change(:description, :text) + # + # See TableDefinition#column for details of the options you can use. def change(column_name, type, options = {}) @base.change_column(name, column_name, type, options) end - # Sets a new default value for a column. See SchemaStatements#change_column_default + # Sets a new default value for a column. # # t.change_default(:qualification, 'new') # t.change_default(:authorized, 1) + # + # See SchemaStatements#change_column_default def change_default(column_name, default) @base.change_column_default(name, column_name, default) end @@ -488,20 +578,19 @@ module ActiveRecord # # t.remove(:qualification) # t.remove(:qualification, :experience) + # + # See SchemaStatements#remove_columns def remove(*column_names) @base.remove_columns(name, *column_names) end # Removes the given index from the table. # - # ====== Remove the index_table_name_on_column in the table_name table - # t.remove_index :column - # ====== Remove the index named index_table_name_on_branch_id in the table_name table - # t.remove_index column: :branch_id - # ====== Remove the index named index_table_name_on_branch_id_and_party_id in the table_name table - # t.remove_index column: [:branch_id, :party_id] - # ====== Remove the index named by_branch_party in the table_name table - # t.remove_index name: :by_branch_party + # t.remove_index(:branch_id) + # t.remove_index(column: [:branch_id, :party_id]) + # t.remove_index(name: :by_branch_party) + # + # See SchemaStatements#remove_index def remove_index(options = {}) @base.remove_index(name, options) end @@ -509,6 +598,8 @@ module ActiveRecord # Removes the timestamp columns (+created_at+ and +updated_at+) from the table. # # t.remove_timestamps + # + # See SchemaStatements#remove_timestamps def remove_timestamps(options = {}) @base.remove_timestamps(name, options) end @@ -516,17 +607,19 @@ module ActiveRecord # Renames a column. # # t.rename(:description, :name) + # + # See SchemaStatements#rename_column def rename(column_name, new_column_name) @base.rename_column(name, column_name, new_column_name) end - # Adds a reference. Optionally adds a +type+ column, if <tt>:polymorphic</tt> option is provided. - # <tt>references</tt> and <tt>belongs_to</tt> are acceptable. The reference column will be an +integer+ - # by default, the <tt>:type</tt> option can be used to specify a different type. + # Adds a reference. Optionally adds a +type+ column, if + # <tt>:polymorphic</tt> option is provided. # # t.references(:user) # t.references(:user, type: "string") # t.belongs_to(:supplier, polymorphic: true) + # t.belongs_to(:supplier, foreign_key: true) # # See SchemaStatements#add_reference def references(*args) @@ -538,7 +631,6 @@ module ActiveRecord alias :belongs_to :references # Removes a reference. Optionally removes a +type+ column. - # <tt>remove_references</tt> and <tt>remove_belongs_to</tt> are acceptable. # # t.remove_references(:user) # t.remove_belongs_to(:supplier, polymorphic: true) @@ -552,10 +644,12 @@ module ActiveRecord end alias :remove_belongs_to :remove_references - # Adds a column or columns of a specified type + # 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! @@ -565,6 +659,10 @@ module ActiveRecord end end + def foreign_key(*args) # :nodoc: + @base.add_foreign_key(name, *args) + end + private def native @base.native_database_types diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb index 0834105079..42ea599a74 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb @@ -12,6 +12,12 @@ module ActiveRecord spec end + def column_spec_for_primary_key(column) + return if column.type == :integer + spec = { id: column.type.inspect } + spec.merge!(prepare_column_options(column).delete_if { |key, _| [:name, :type].include?(key) }) + end + # This can be overridden on a Adapter level basis to support other # extended datatypes (Example: Adding an array option in the # PostgreSQLAdapter) 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 b340e8334b..34d60493ea 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -204,7 +204,17 @@ module ActiveRecord end result = execute schema_creation.accept td - td.indexes.each_pair { |c, o| add_index(table_name, c, o) } unless supports_indexes_in_create? + + unless supports_indexes_in_create? + td.indexes.each_pair do |column_name, index_options| + add_index(table_name, column_name, index_options) + end + end + + td.foreign_keys.each_pair do |other_table_name, foreign_key_options| + add_foreign_key(table_name, other_table_name, foreign_key_options) + end + result end @@ -576,9 +586,8 @@ module ActiveRecord # rename_index :people, 'index_people_on_last_name', 'index_users_on_last_name' # def rename_index(table_name, old_name, new_name) - if new_name.length > allowed_index_name_length - raise ArgumentError, "Index name '#{new_name}' on table '#{table_name}' is too long; the limit is #{allowed_index_name_length} characters" - end + validate_index_length!(table_name, new_name) + # this is a naive implementation; some DBs may support this more efficiently (Postgres, for instance) old_index_def = indexes(table_name).detect { |i| i.name == old_name } return unless old_index_def @@ -627,17 +636,16 @@ module ActiveRecord # # add_belongs_to(:products, :supplier, polymorphic: true) # - # ====== Create a supplier_id, supplier_type columns and appropriate index + # ====== Create supplier_id, supplier_type columns and appropriate index # # add_reference(:products, :supplier, polymorphic: true, index: true) # - def add_reference(table_name, ref_name, options = {}) - polymorphic = options.delete(:polymorphic) - index_options = options.delete(:index) - type = options.delete(:type) || :integer - add_column(table_name, "#{ref_name}_id", type, options) - add_column(table_name, "#{ref_name}_type", :string, polymorphic.is_a?(Hash) ? polymorphic : options) if polymorphic - add_index(table_name, polymorphic ? %w[type id].map{ |t| "#{ref_name}_#{t}" } : "#{ref_name}_id", index_options.is_a?(Hash) ? index_options : {}) if index_options + # ====== Create a supplier_id column and appropriate foreign key + # + # add_reference(:products, :supplier, foreign_key: true) + # + def add_reference(table_name, *args) + ReferenceDefinition.new(*args).add_to(update_table_definition(table_name, self)) end alias :add_belongs_to :add_reference @@ -973,12 +981,12 @@ module ActiveRecord end private - def create_table_definition(name, temporary, options, as = nil) + def create_table_definition(name, temporary = false, options = nil, as = nil) TableDefinition.new native_database_types, name, temporary, options, as end def create_alter_table(name) - AlterTable.new create_table_definition(name, false, {}) + AlterTable.new create_table_definition(name) end def foreign_key_name(table_name, options) # :nodoc: @@ -986,6 +994,12 @@ module ActiveRecord "fk_rails_#{SecureRandom.hex(5)}" end end + + def validate_index_length!(table_name, new_name) + if new_name.length > allowed_index_name_length + raise ArgumentError, "Index name '#{new_name}' on table '#{table_name}' is too long; the limit is #{allowed_index_name_length} characters" + end + end end 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 c4506885ed..fa24d9b43f 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -21,6 +21,7 @@ module ActiveRecord autoload :IndexDefinition autoload :ColumnDefinition autoload :ChangeColumnDefinition + autoload :ForeignKeyDefinition autoload :TableDefinition autoload :Table autoload :AlterTable @@ -351,9 +352,6 @@ module ActiveRecord def create_savepoint(name = nil) end - def rollback_to_savepoint(name = nil) - end - def release_savepoint(name = nil) end @@ -459,7 +457,12 @@ module ActiveRecord end def translate_exception_class(e, sql) - message = "#{e.class.name}: #{e.message}: #{sql}" + begin + message = "#{e.class.name}: #{e.message}: #{sql}" + rescue Encoding::CompatibilityError + 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 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 ced80bacc8..e9a3c26c32 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -6,6 +6,13 @@ 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 + super + end + end + class SchemaCreation < AbstractAdapter::SchemaCreation def visit_AddColumn(o) add_column_position!(super, column_options(o)) @@ -31,12 +38,8 @@ module ActiveRecord end def visit_ChangeColumnDefinition(o) - column = o.column - options = o.options - sql_type = type_to_sql(o.type, options[:limit], options[:precision], options[:scale]) - change_column_sql = "CHANGE #{quote_column_name(column.name)} #{quote_column_name(options[:name])} #{sql_type}" - add_column_options!(change_column_sql, options.merge(column: column)) - add_column_position!(change_column_sql, options) + change_column_sql = "CHANGE #{quote_column_name(o.name)} #{accept(o.column)}" + add_column_position!(change_column_sql, column_options(o.column)) end def add_column_position!(sql, options) @@ -58,6 +61,18 @@ module ActiveRecord SchemaCreation.new self end + def column_spec_for_primary_key(column) + spec = {} + if column.extra == 'auto_increment' + return unless column.limit == 8 + spec[:id] = ':bigint' + else + spec[:id] = column.type.inspect + spec.merge!(prepare_column_options(column).delete_if { |key, _| [:name, :type, :null].include?(key) }) + end + spec + end + class Column < ConnectionAdapters::Column # :nodoc: attr_reader :collation, :strict, :extra @@ -324,7 +339,7 @@ module ActiveRecord execute "COMMIT" end - def rollback_db_transaction #:nodoc: + def exec_rollback_db_transaction #:nodoc: execute "ROLLBACK" end @@ -492,6 +507,8 @@ module ActiveRecord def rename_index(table_name, old_name, new_name) if supports_rename_index? + validate_index_length!(table_name, new_name) + execute "ALTER TABLE #{quote_table_name(table_name)} RENAME INDEX #{quote_table_name(old_name)} TO #{quote_table_name(new_name)}" else super @@ -582,6 +599,13 @@ 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 @@ -670,6 +694,11 @@ module ActiveRecord m.alias_type %r(year)i, 'integer' m.alias_type %r(bit)i, 'binary' + m.register_type(%r(datetime)i) do |sql_type| + precision = extract_precision(sql_type) + MysqlDateTime.new(precision: precision) + end + m.register_type(%r(enum)i) do |sql_type| limit = sql_type[/^enum\((.+)\)/i, 1] .split(',').map{|enum| enum.strip.length - 2}.max @@ -735,7 +764,7 @@ module ActiveRecord end def add_column_sql(table_name, column_name, type, options = {}) - td = create_table_definition table_name, options[:temporary], options[:options] + td = create_table_definition(table_name) cd = td.new_column_definition(column_name, type, options) schema_creation.visit_AddColumn cd end @@ -751,21 +780,23 @@ module ActiveRecord options[:null] = column.null end - options[:name] = column.name - schema_creation.accept ChangeColumnDefinition.new column, type, options + td = create_table_definition(table_name) + cd = td.new_column_definition(column.name, type, options) + schema_creation.accept(ChangeColumnDefinition.new(cd, column.name)) end def rename_column_sql(table_name, column_name, new_column_name) column = column_for(table_name, column_name) options = { - name: new_column_name, default: column.default, null: column.null, auto_increment: column.extra == "auto_increment" } current_type = select_one("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE '#{column_name}'", 'SCHEMA')["Type"] - schema_creation.accept ChangeColumnDefinition.new column, current_type, options + td = create_table_definition(table_name) + cd = td.new_column_definition(new_column_name, current_type, options) + schema_creation.accept(ChangeColumnDefinition.new(cd, column.name)) end def remove_column_sql(table_name, column_name, type = nil, options = {}) @@ -859,6 +890,26 @@ module ActiveRecord end end + def create_table_definition(name, temporary = false, options = nil, as = nil) # :nodoc: + TableDefinition.new(native_database_types, name, temporary, options, as) + end + + class MysqlDateTime < Type::DateTime # :nodoc: + def type_cast_for_database(value) + if value.acts_like?(:time) && value.respond_to?(:usec) + result = super.to_s(:db) + case precision + when 1..6 + "#{result}.#{sprintf("%0#{precision}d", value.usec / 10 ** (6 - precision))}" + else + result + end + else + super + end + end + end + class MysqlString < Type::String # :nodoc: def type_cast_for_database(value) case value diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb index dd303c73d5..af307b57a4 100644 --- a/activerecord/lib/active_record/connection_adapters/column.rb +++ b/activerecord/lib/active_record/connection_adapters/column.rb @@ -16,7 +16,7 @@ module ActiveRecord attr_reader :name, :cast_type, :null, :sql_type, :default, :default_function delegate :type, :precision, :scale, :limit, :klass, :accessor, - :number?, :binary?, :changed?, + :text?, :number?, :binary?, :changed?, :type_cast_from_user, :type_cast_from_database, :type_cast_for_database, :type_cast_for_schema, to: :cast_type diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/column.rb b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb index 37e5c3859c..1458fbf496 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/column.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb @@ -15,6 +15,10 @@ module ActiveRecord @default_function = default_function end + + def serial? + default_function && default_function =~ /\Anextval\(.*\)\z/ + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb index d09468329a..11d3f5301a 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb @@ -223,7 +223,7 @@ module ActiveRecord end # Aborts a transaction. - def rollback_db_transaction + def exec_rollback_db_transaction execute "ROLLBACK" end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb index 997613d7be..6bd1b8ecae 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb @@ -5,6 +5,7 @@ module ActiveRecord class Bytea < Type::Binary # :nodoc: def type_cast_from_database(value) return if value.nil? + return value.to_s if value.is_a?(Type::Binary::Data) PGconn.unescape_bytea(super) end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb index 2d2fede4e8..b2a42e9ebb 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb @@ -8,6 +8,10 @@ module ActiveRecord def initialize(type) @type = type end + + def text? + false + end 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 b37630a04c..a9522e152f 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb @@ -125,10 +125,8 @@ module ActiveRecord # 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 = {}) - return super unless type == :uuid - options[:default] = options.fetch(:default, 'uuid_generate_v4()') - options[:primary_key] = true - column name, type, options + options[:default] = options.fetch(:default, 'uuid_generate_v4()') if type == :uuid + super end def new_column_definition(name, type, options) # :nodoc: 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 7ba5437474..a90adcf4aa 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -4,15 +4,6 @@ module ActiveRecord class SchemaCreation < AbstractAdapter::SchemaCreation private - def visit_ColumnDefinition(o) - sql = super - if o.primary_key? && o.type != :primary_key - sql << " PRIMARY KEY " - add_column_options!(sql, column_options(o)) - end - sql - end - def column_options(o) column_options = super column_options[:array] = o.array @@ -394,15 +385,15 @@ module ActiveRecord # Returns just a table's primary key def primary_key(table) - row = exec_query(<<-end_sql, 'SCHEMA').rows.first + pks = exec_query(<<-end_sql, 'SCHEMA').rows SELECT attr.attname FROM pg_attribute attr - INNER JOIN pg_constraint cons ON attr.attrelid = cons.conrelid AND attr.attnum = cons.conkey[1] + INNER JOIN pg_constraint cons ON attr.attrelid = cons.conrelid AND attr.attnum = any(cons.conkey) WHERE cons.contype = 'p' AND cons.conrelid = '#{quote_table_name(table)}'::regclass end_sql - - row && row.first + return nil unless pks.count == 1 + pks[0][0] end # Renames a table. @@ -492,9 +483,8 @@ module ActiveRecord end def rename_index(table_name, old_name, new_name) - if new_name.length > allowed_index_name_length - raise ArgumentError, "Index name '#{new_name}' on table '#{table_name}' is too long; the limit is #{allowed_index_name_length} characters" - end + validate_index_length!(table_name, new_name) + execute "ALTER INDEX #{quote_column_name(old_name)} RENAME TO #{quote_table_name(new_name)}" end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 6ef47d8a11..13bb5c187e 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -125,6 +125,21 @@ module ActiveRecord PostgreSQL::SchemaCreation.new self end + def column_spec_for_primary_key(column) + spec = {} + if column.serial? + return unless column.sql_type == 'bigint' + spec[:id] = ':bigserial' + elsif column.type == :uuid + spec[:id] = ':uuid' + spec[:default] = column.default_function.inspect + else + spec[:id] = column.type.inspect + spec.merge!(prepare_column_options(column).delete_if { |key, _| [:name, :type, :null].include?(key) }) + end + spec + end + # Adds +:array+ option to the default set provided by the # AbstractAdapter def prepare_column_options(column) # :nodoc: @@ -445,8 +460,8 @@ module ActiveRecord def initialize_type_map(m) # :nodoc: register_class_with_limit m, 'int2', OID::Integer - m.alias_type 'int4', 'int2' - m.alias_type 'int8', 'int2' + register_class_with_limit m, 'int4', OID::Integer + register_class_with_limit m, 'int8', OID::Integer m.alias_type 'oid', 'int2' m.register_type 'float4', OID::Float.new m.alias_type 'float8', 'float4' @@ -746,7 +761,7 @@ module ActiveRecord $1.strip if $1 end - def create_table_definition(name, temporary, options, as = nil) # :nodoc: + def create_table_definition(name, temporary = false, options = nil, as = nil) # :nodoc: PostgreSQL::TableDefinition.new native_database_types, name, temporary, options, as end end diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index 0f7e0fac01..03dfd29a0a 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -351,7 +351,7 @@ module ActiveRecord log('commit transaction',nil) { @connection.commit } end - def rollback_db_transaction #:nodoc: + def exec_rollback_db_transaction #:nodoc: log('rollback transaction',nil) { @connection.rollback } end @@ -418,10 +418,9 @@ module ActiveRecord end def primary_key(table_name) #:nodoc: - column = table_structure(table_name).find { |field| - field['pk'] == 1 - } - column && column['name'] + pks = table_structure(table_name).select { |f| f['pk'] > 0 } + return nil unless pks.count == 1 + pks[0]['name'] end def remove_index!(table_name, index_name) #:nodoc: @@ -578,23 +577,12 @@ module ActiveRecord rename.each { |a| column_mappings[a.last] = a.first } from_columns = columns(from).collect(&:name) columns = columns.find_all{|col| from_columns.include?(column_mappings[col])} + from_columns_to_copy = columns.map { |col| column_mappings[col] } quoted_columns = columns.map { |col| quote_column_name(col) } * ',' + quoted_from_columns = from_columns_to_copy.map { |col| quote_column_name(col) } * ',' - quoted_to = quote_table_name(to) - - raw_column_mappings = Hash[columns(from).map { |c| [c.name, c] }] - - exec_query("SELECT * FROM #{quote_table_name(from)}").each do |row| - sql = "INSERT INTO #{quoted_to} (#{quoted_columns}) VALUES (" - - column_values = columns.map do |col| - quote(row[column_mappings[col]], raw_column_mappings[col]) - end - - sql << column_values * ', ' - sql << ')' - exec_query sql - end + exec_query("INSERT INTO #{quote_table_name(to)} (#{quoted_columns}) + SELECT #{quoted_from_columns} FROM #{quote_table_name(from)}") end def sqlite_version diff --git a/activerecord/lib/active_record/connection_handling.rb b/activerecord/lib/active_record/connection_handling.rb index 8f51590c99..984af79642 100644 --- a/activerecord/lib/active_record/connection_handling.rb +++ b/activerecord/lib/active_record/connection_handling.rb @@ -1,6 +1,6 @@ module ActiveRecord module ConnectionHandling - RAILS_ENV = -> { (Rails.env if defined?(Rails)) || ENV["RAILS_ENV"] || ENV["RACK_ENV"] } + RAILS_ENV = -> { (Rails.env if defined?(Rails.env)) || ENV["RAILS_ENV"] || ENV["RACK_ENV"] } DEFAULT_ENV = -> { RAILS_ENV.call || "default_env" } # Establishes the connection to the database. Accepts a hash as input where diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index c2d5582f02..38b2d632d2 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -131,6 +131,7 @@ module ActiveRecord return super if block_given? || primary_key.nil? || default_scopes.any? || + current_scope || columns_hash.include?(inheritance_column) || ids.first.kind_of?(Array) @@ -234,7 +235,7 @@ module ActiveRecord # scope :published_and_commented, -> { published.and(self.arel_table[:comments_count].gt(0)) } # end def arel_table # :nodoc: - @arel_table ||= Arel::Table.new(table_name) + @arel_table ||= Arel::Table.new(table_name, type_caster: type_caster) end # Returns the Arel engine. @@ -247,10 +248,18 @@ module ActiveRecord end end + def predicate_builder # :nodoc: + @predicate_builder ||= PredicateBuilder.new(table_metadata) + end + + def type_caster # :nodoc: + TypeCaster::Map.new(self) + end + private - def relation #:nodoc: - relation = Relation.create(self, arel_table) + def relation # :nodoc: + relation = Relation.create(self, arel_table, predicate_builder) if finder_needs_type_condition? relation.where(type_condition).create_with(inheritance_column.to_sym => sti_name) @@ -258,6 +267,10 @@ module ActiveRecord relation end end + + def table_metadata # :nodoc: + TableMetadata.new(self, arel_table) + end end # New objects can be instantiated as either empty (pass no construction parameter) or pre-set with diff --git a/activerecord/lib/active_record/counter_cache.rb b/activerecord/lib/active_record/counter_cache.rb index 101889638d..7d8e0a2063 100644 --- a/activerecord/lib/active_record/counter_cache.rb +++ b/activerecord/lib/active_record/counter_cache.rb @@ -37,10 +37,9 @@ module ActiveRecord reflection = child_class._reflections.values.find { |e| e.belongs_to? && e.foreign_key.to_s == foreign_key && e.options[:counter_cache].present? } counter_name = reflection.counter_cache_column - stmt = unscoped.where(arel_table[primary_key].eq(object.id)).arel.compile_update({ - arel_table[counter_name] => object.send(counter_association).count(:all) - }, primary_key) - connection.update stmt + unscoped.where(primary_key => object.id).update_all( + counter_name => object.send(counter_association).count(:all) + ) end return true end diff --git a/activerecord/lib/active_record/dynamic_matchers.rb b/activerecord/lib/active_record/dynamic_matchers.rb index e94b74063e..b6dd6814db 100644 --- a/activerecord/lib/active_record/dynamic_matchers.rb +++ b/activerecord/lib/active_record/dynamic_matchers.rb @@ -1,10 +1,5 @@ module ActiveRecord module DynamicMatchers #:nodoc: - # This code in this file seems to have a lot of indirection, but the indirection - # is there to provide extension points for the activerecord-deprecated_finders - # gem. When we stop supporting activerecord-deprecated_finders (from Rails 5), - # then we can remove the indirection. - def respond_to?(name, include_private = false) if self == Base super @@ -72,26 +67,14 @@ module ActiveRecord CODE end - def body - raise NotImplementedError - end - end + private - module Finder - # Extended in activerecord-deprecated_finders def body - result - end - - # Extended in activerecord-deprecated_finders - def result "#{finder}(#{attributes_hash})" end # The parameters in the signature may have reserved Ruby words, in order # to prevent errors, we start each param name with `_`. - # - # Extended in activerecord-deprecated_finders def signature attribute_names.map { |name| "_#{name}" }.join(', ') end @@ -109,7 +92,6 @@ module ActiveRecord class FindBy < Method Method.matchers << self - include Finder def self.prefix "find_by" @@ -122,7 +104,6 @@ module ActiveRecord class FindByBang < Method Method.matchers << self - include Finder def self.prefix "find_by" diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb index ced694ba9a..9f053453bd 100644 --- a/activerecord/lib/active_record/locking/optimistic.rb +++ b/activerecord/lib/active_record/locking/optimistic.rb @@ -11,7 +11,7 @@ module ActiveRecord # # == Usage # - # Active Records support optimistic locking if the field +lock_version+ is present. Each update to the + # Active Record supports optimistic locking if the +lock_version+ field is present. Each update to the # record increments the +lock_version+ column and the locking facilities ensure that records instantiated twice # will let the last one saved raise a +StaleObjectError+ if the first was also updated. Example: # @@ -80,17 +80,15 @@ module ActiveRecord begin relation = self.class.unscoped - stmt = relation.where( - relation.table[self.class.primary_key].eq(id).and( - relation.table[lock_col].eq(self.class.quote_value(previous_lock_value, column_for_attribute(lock_col))) - ) - ).arel.compile_update( - arel_attributes_with_values_for_update(attribute_names), - self.class.primary_key + affected_rows = relation.where( + self.class.primary_key => id, + lock_col => previous_lock_value, + ).update_all( + attribute_names.map do |name| + [name, _read_attribute(name)] + end.to_h ) - affected_rows = self.class.connection.update stmt - unless affected_rows == 1 raise ActiveRecord::StaleObjectError.new(self, "update") end diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index 3cac465440..46f4794010 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -39,7 +39,7 @@ module ActiveRecord class PendingMigrationError < MigrationError#:nodoc: def initialize - if defined?(Rails) + if defined?(Rails.env) super("Migrations are pending. To resolve this issue, run:\n\n\tbin/rake db:migrate RAILS_ENV=#{::Rails.env}") else super("Migrations are pending. To resolve this issue, run:\n\n\tbin/rake db:migrate") diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb index 92ad9c9101..641512d323 100644 --- a/activerecord/lib/active_record/model_schema.rb +++ b/activerecord/lib/active_record/model_schema.rb @@ -147,7 +147,7 @@ module ActiveRecord @quoted_table_name = nil @arel_table = nil @sequence_name = nil unless defined?(@explicit_sequence_name) && @explicit_sequence_name - @relation = Relation.create(self, arel_table) + @predicate_builder = nil end # Returns a quoted version of the table name, used to construct SQL statements. @@ -298,12 +298,12 @@ module ActiveRecord connection.schema_cache.clear_table_cache!(table_name) if table_exists? @arel_engine = nil + @arel_table = nil @column_names = nil @column_types = nil @content_columns = nil @default_attributes = nil @inheritance_column = nil unless defined?(@explicit_inheritance_column) && @explicit_inheritance_column - @relation = nil end private diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb index 8a2a06f2ca..846e1162a9 100644 --- a/activerecord/lib/active_record/nested_attributes.rb +++ b/activerecord/lib/active_record/nested_attributes.rb @@ -81,6 +81,9 @@ module ActiveRecord # # Note that the model will _not_ be destroyed until the parent is saved. # + # Also note that the model will not be destroyed unless you also specify + # its id in the updated hash. + # # === One-to-many # # Consider a member that has a number of posts: @@ -111,7 +114,7 @@ module ActiveRecord # member.posts.first.title # => 'Kari, the awesome Ruby documentation browser!' # member.posts.second.title # => 'The egalitarian assumption of the modern citizen' # - # You may also set a :reject_if proc to silently ignore any new record + # You may also set a +:reject_if+ proc to silently ignore any new record # hashes if they fail to pass your criteria. For example, the previous # example could be rewritten as: # @@ -133,7 +136,7 @@ module ActiveRecord # member.posts.first.title # => 'Kari, the awesome Ruby documentation browser!' # member.posts.second.title # => 'The egalitarian assumption of the modern citizen' # - # Alternatively, :reject_if also accepts a symbol for using methods: + # Alternatively, +:reject_if+ also accepts a symbol for using methods: # # class Member < ActiveRecord::Base # has_many :posts @@ -212,13 +215,13 @@ module ActiveRecord # All changes to models, including the destruction of those marked for # destruction, are saved and destroyed automatically and atomically when # the parent model is saved. This happens inside the transaction initiated - # by the parents save method. See ActiveRecord::AutosaveAssociation. + # by the parent's save method. See ActiveRecord::AutosaveAssociation. # # === Validating the presence of a parent model # # If you want to validate that a child record is associated with a parent - # record, you can use <tt>validates_presence_of</tt> and - # <tt>inverse_of</tt> as this example illustrates: + # record, you can use the +validates_presence_of+ method and the +:inverse_of+ + # key as this example illustrates: # # class Member < ActiveRecord::Base # has_many :posts, inverse_of: :member @@ -230,7 +233,7 @@ module ActiveRecord # validates_presence_of :member # end # - # Note that if you do not specify the <tt>inverse_of</tt> option, then + # Note that if you do not specify the +:inverse_of+ option, then # Active Record will try to automatically guess the inverse association # based on heuristics. # @@ -264,29 +267,31 @@ module ActiveRecord # Allows you to specify a Proc or a Symbol pointing to a method # that checks whether a record should be built for a certain attribute # hash. The hash is passed to the supplied Proc or the method - # and it should return either +true+ or +false+. When no :reject_if + # and it should return either +true+ or +false+. When no +:reject_if+ # is specified, a record will be built for all attribute hashes that # do not have a <tt>_destroy</tt> value that evaluates to true. # Passing <tt>:all_blank</tt> instead of a Proc will create a proc # that will reject a record where all the attributes are blank excluding - # any value for _destroy. + # any value for +_destroy+. # [:limit] - # Allows you to specify the maximum number of the associated records that - # can be processed with the nested attributes. Limit also can be specified as a - # Proc or a Symbol pointing to a method that should return number. If the size of the - # nested attributes array exceeds the specified limit, NestedAttributes::TooManyRecords - # exception is raised. If omitted, any number associations can be processed. - # Note that the :limit option is only applicable to one-to-many associations. + # Allows you to specify the maximum number of associated records that + # can be processed with the nested attributes. Limit also can be specified + # as a Proc or a Symbol pointing to a method that should return a number. + # If the size of the nested attributes array exceeds the specified limit, + # NestedAttributes::TooManyRecords exception is raised. If omitted, any + # number of associations can be processed. + # Note that the +:limit+ option is only applicable to one-to-many + # associations. # [:update_only] # For a one-to-one association, this option allows you to specify how - # nested attributes are to be used when an associated record already + # nested attributes are going to be used when an associated record already # exists. In general, an existing record may either be updated with the # new set of attribute values or be replaced by a wholly new record - # containing those values. By default the :update_only option is +false+ + # containing those values. By default the +:update_only+ option is +false+ # and the nested attributes are used to update the existing record only # if they include the record's <tt>:id</tt> value. Otherwise a new # record will be instantiated and used to replace the existing one. - # However if the :update_only option is +true+, the nested attributes + # However if the +:update_only+ option is +true+, the nested attributes # are used to update the record's attributes always, regardless of # whether the <tt>:id</tt> is present. The option is ignored for collection # associations. diff --git a/activerecord/lib/active_record/no_touching.rb b/activerecord/lib/active_record/no_touching.rb index dbf4564ae5..edb5066fa0 100644 --- a/activerecord/lib/active_record/no_touching.rb +++ b/activerecord/lib/active_record/no_touching.rb @@ -45,7 +45,7 @@ module ActiveRecord NoTouching.applied_to?(self.class) end - def touch(*) + def touch(*) # :nodoc: super unless no_touching? end end diff --git a/activerecord/lib/active_record/null_relation.rb b/activerecord/lib/active_record/null_relation.rb index 807c301596..b406da14dc 100644 --- a/activerecord/lib/active_record/null_relation.rb +++ b/activerecord/lib/active_record/null_relation.rb @@ -62,9 +62,7 @@ module ActiveRecord calculate :maximum, nil end - def calculate(operation, _column_name, _options = {}) - # TODO: Remove _options argument as soon we remove support to - # activerecord-deprecated_finders. + def calculate(operation, _column_name) if [:count, :sum, :size].include? operation group_values.any? ? Hash.new : 0 elsif [:average, :minimum, :maximum].include?(operation) && group_values.any? diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index 1fc82f05d4..f53c5f17ef 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -109,6 +109,10 @@ module ActiveRecord # validate: false, validations are bypassed altogether. See # ActiveRecord::Validations for more information. # + # By default, #save also sets the +updated_at+/+updated_on+ attributes to + # the current time. However, if you supply <tt>touch: false</tt>, these + # timestamps will not be updated. + # # There's a series of callbacks associated with +save+. If any of the # <tt>before_*</tt> callbacks return +false+ the action is cancelled and # +save+ returns +false+. See ActiveRecord::Callbacks for further @@ -116,21 +120,25 @@ module ActiveRecord # # Attributes marked as readonly are silently ignored if the record is # being updated. - def save(*) - create_or_update + def save(*args) + create_or_update(*args) rescue ActiveRecord::RecordInvalid false end # Saves the model. # - # If the model is new a record gets created in the database, otherwise + # If the model is new, a record gets created in the database, otherwise # the existing record gets updated. # # With <tt>save!</tt> validations always run. If any of them fail # ActiveRecord::RecordInvalid gets raised. See ActiveRecord::Validations # for more information. # + # By default, #save! also sets the +updated_at+/+updated_on+ attributes to + # the current time. However, if you supply <tt>touch: false</tt>, these + # timestamps will not be updated. + # # There's a series of callbacks associated with <tt>save!</tt>. If any of # the <tt>before_*</tt> callbacks return +false+ the action is cancelled # and <tt>save!</tt> raises ActiveRecord::RecordNotSaved. See @@ -138,8 +146,8 @@ module ActiveRecord # # Attributes marked as readonly are silently ignored if the record is # being updated. - def save!(*) - create_or_update || raise(RecordNotSaved.new(nil, self)) + def save!(*args) + create_or_update(*args) || raise(RecordNotSaved.new(nil, self)) end # Deletes the record in the database and freezes this instance to @@ -498,9 +506,9 @@ module ActiveRecord relation end - def create_or_update + def create_or_update(*args) raise ReadOnlyRecord, "#{self.class} is marked as readonly" if readonly? - result = new_record? ? _create_record : _update_record + result = new_record? ? _create_record : _update_record(*args) result != false end diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index 4daf2a0e2b..04c2be045d 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -319,7 +319,7 @@ db_namespace = namespace :db do begin should_reconnect = ActiveRecord::Base.connection_pool.active_connection? ActiveRecord::Schema.verbose = false - ActiveRecord::Tasks::DatabaseTasks.load_schema_for ActiveRecord::Base.configurations['test'], :ruby, ENV['SCHEMA'] + ActiveRecord::Tasks::DatabaseTasks.load_schema ActiveRecord::Base.configurations['test'], :ruby, ENV['SCHEMA'] ensure if should_reconnect ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations[ActiveRecord::Tasks::DatabaseTasks.env]) @@ -329,7 +329,7 @@ db_namespace = namespace :db do # desc "Recreate the test database from an existent structure.sql file" task :load_structure => %w(db:test:purge) do - ActiveRecord::Tasks::DatabaseTasks.load_schema_for ActiveRecord::Base.configurations['test'], :sql, ENV['SCHEMA'] + ActiveRecord::Tasks::DatabaseTasks.load_schema ActiveRecord::Base.configurations['test'], :sql, ENV['SCHEMA'] end # desc "Recreate the test database from a fresh schema" diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 9849e03036..7696ef13c7 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -149,7 +149,7 @@ module ActiveRecord JoinKeys = Struct.new(:key, :foreign_key) # :nodoc: - def join_keys(assoc_klass) + def join_keys(association_klass) JoinKeys.new(foreign_key, active_record_primary_key) end @@ -161,7 +161,16 @@ module ActiveRecord macro end + + def constraints + scope_chain.flatten + end + + def alias_candidate(name) + "#{plural_name}_#{name}" + end end + # Base class for AggregateReflection and AssociationReflection. Objects of # AggregateReflection and AssociationReflection are returned by the Reflection::ClassMethods. # @@ -499,7 +508,7 @@ module ActiveRecord # returns either nil or the inverse association name that it finds. def automatic_inverse_of if can_find_inverse_of_automatically?(self) - inverse_name = ActiveSupport::Inflector.underscore(options[:as] || active_record.name).to_sym + inverse_name = ActiveSupport::Inflector.underscore(options[:as] || active_record.name.demodulize).to_sym begin reflection = klass._reflect_on_association(inverse_name) @@ -601,8 +610,8 @@ module ActiveRecord def belongs_to?; true; end - def join_keys(assoc_klass) - key = polymorphic? ? association_primary_key(assoc_klass) : association_primary_key + def join_keys(association_klass) + key = polymorphic? ? association_primary_key(association_klass) : association_primary_key JoinKeys.new(key, foreign_key) end @@ -698,6 +707,11 @@ module ActiveRecord @chain ||= begin a = source_reflection.chain b = through_reflection.chain + + if options[:source_type] + b[0] = PolymorphicReflection.new(b[0], self) + end + chain = a + b chain[0] = self # Use self so we don't lose the information from :source_type chain @@ -745,8 +759,8 @@ module ActiveRecord end end - def join_keys(assoc_klass) - source_reflection.join_keys(assoc_klass) + def join_keys(association_klass) + source_reflection.join_keys(association_klass) end # The macro used by the source association @@ -855,6 +869,12 @@ module ActiveRecord check_validity_of_inverse! end + def constraints + scope_chain = source_reflection.constraints + scope_chain << scope if scope + scope_chain + end + protected def actual_source_reflection # FIXME: this is a horrible name @@ -877,5 +897,81 @@ module ActiveRecord delegate(*delegate_methods, to: :delegate_reflection) end + + class PolymorphicReflection < ThroughReflection # :nodoc: + def initialize(reflection, previous_reflection) + @reflection = reflection + @previous_reflection = previous_reflection + end + + def klass + @reflection.klass + end + + def scope + @reflection.scope + end + + def table_name + @reflection.table_name + end + + def plural_name + @reflection.plural_name + end + + def join_keys(association_klass) + @reflection.join_keys(association_klass) + end + + def type + @reflection.type + end + + def constraints + [source_type_info] + end + + def source_type_info + type = @previous_reflection.foreign_type + source_type = @previous_reflection.options[:source_type] + lambda { |object| where(type => source_type) } + end + end + + class RuntimeReflection < PolymorphicReflection # :nodoc: + attr_accessor :next + + def initialize(reflection, association) + @reflection = reflection + @association = association + end + + def klass + @association.klass + end + + def table_name + klass.table_name + end + + def constraints + @reflection.constraints + end + + def source_type_info + @reflection.source_type_info + end + + def alias_candidate(name) + "#{plural_name}_#{name}_join" + end + + def alias_name + Arel::Table.new(table_name) + end + + def all_includes; yield; end + end end end diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index daafb0b645..ab3debc03b 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -16,17 +16,17 @@ module ActiveRecord include FinderMethods, Calculations, SpawnMethods, QueryMethods, Batches, Explain, Delegation - attr_reader :table, :klass, :loaded + attr_reader :table, :klass, :loaded, :predicate_builder alias :model :klass alias :loaded? :loaded - def initialize(klass, table, values = {}) + def initialize(klass, table, predicate_builder, values = {}) @klass = klass @table = table @values = values @offsets = {} @loaded = false - @predicate_builder = PredicateBuilder.new(klass, table) + @predicate_builder = predicate_builder end def initialize_copy(other) @@ -362,9 +362,21 @@ module ActiveRecord # # Updates multiple records # people = { 1 => { "first_name" => "David" }, 2 => { "first_name" => "Jeremy" } } # Person.update(people.keys, people.values) - def update(id, attributes) + # + # # Updates multiple records from the result of a relation + # people = Person.where(group: 'expert') + # people.update(group: 'masters') + # + # Note: Updating a large number of records will run a + # UPDATE query for each record, which may cause a performance + # issue. So if it is not needed to run callbacks for each update, it is + # preferred to use <tt>update_all</tt> for updating all records using + # a single query. + def update(id = :all, attributes) if id.is_a?(Array) id.map.with_index { |one_id, idx| update(one_id, attributes[idx]) } + elsif id == :all + to_a.each { |record| record.update(attributes) } else object = find(id) object.update(attributes) @@ -569,7 +581,7 @@ module ActiveRecord [name, binds.fetch(name.to_s) { case where.right when Array then where.right.map(&:val) - else + when Arel::Nodes::Casted, Arel::Nodes::Quoted where.right.val end }] @@ -633,10 +645,6 @@ module ActiveRecord "#<#{self.class.name} [#{entries.join(', ')}]>" end - protected - - attr_reader :predicate_builder - private def exec_queries diff --git a/activerecord/lib/active_record/relation/batches.rb b/activerecord/lib/active_record/relation/batches.rb index 20d24b409b..4f0502ae75 100644 --- a/activerecord/lib/active_record/relation/batches.rb +++ b/activerecord/lib/active_record/relation/batches.rb @@ -27,7 +27,7 @@ module ActiveRecord # # ==== Options # * <tt>:batch_size</tt> - Specifies the size of the batch. Default to 1000. - # * <tt>:start</tt> - Specifies the starting point for the batch processing. + # * <tt>:start</tt> - Specifies the primary key value to start from. # This is especially useful if you want multiple workers dealing with # the same processing queue. You can make worker 1 handle all the records # between id 0 and 10,000 and worker 2 handle from 10,000 and beyond @@ -77,7 +77,7 @@ module ActiveRecord # # ==== Options # * <tt>:batch_size</tt> - Specifies the size of the batch. Default to 1000. - # * <tt>:start</tt> - Specifies the starting point for the batch processing. + # * <tt>:start</tt> - Specifies the primary key value to start from. # This is especially useful if you want multiple workers dealing with # the same processing queue. You can make worker 1 handle all the records # between id 0 and 10,000 and worker 2 handle from 10,000 and beyond diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index 71673324eb..1d4cb1a83b 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -35,21 +35,16 @@ module ActiveRecord # # Note: not all valid +select+ expressions are valid +count+ expressions. The specifics differ # between databases. In invalid cases, an error from the database is thrown. - def count(column_name = nil, options = {}) - # TODO: Remove options argument as soon we remove support to - # activerecord-deprecated_finders. - column_name, options = nil, column_name if column_name.is_a?(Hash) - calculate(:count, column_name, options) + def count(column_name = nil) + calculate(:count, column_name) end # Calculates the average value on a given column. Returns +nil+ if there's # no row. See +calculate+ for examples with options. # # Person.average(:age) # => 35.8 - def average(column_name, options = {}) - # TODO: Remove options argument as soon we remove support to - # activerecord-deprecated_finders. - calculate(:average, column_name, options) + def average(column_name) + calculate(:average, column_name) end # Calculates the minimum value on a given column. The value is returned @@ -57,10 +52,8 @@ module ActiveRecord # +calculate+ for examples with options. # # Person.minimum(:age) # => 7 - def minimum(column_name, options = {}) - # TODO: Remove options argument as soon we remove support to - # activerecord-deprecated_finders. - calculate(:minimum, column_name, options) + def minimum(column_name) + calculate(:minimum, column_name) end # Calculates the maximum value on a given column. The value is returned @@ -68,10 +61,8 @@ module ActiveRecord # +calculate+ for examples with options. # # Person.maximum(:age) # => 93 - def maximum(column_name, options = {}) - # TODO: Remove options argument as soon we remove support to - # activerecord-deprecated_finders. - calculate(:maximum, column_name, options) + def maximum(column_name) + calculate(:maximum, column_name) end # Calculates the sum of values on a given column. The value is returned @@ -114,17 +105,15 @@ module ActiveRecord # Person.group(:last_name).having("min(age) > 17").minimum(:age) # # Person.sum("2 * age") - def calculate(operation, column_name, options = {}) - # TODO: Remove options argument as soon we remove support to - # activerecord-deprecated_finders. + def calculate(operation, column_name) if column_name.is_a?(Symbol) && attribute_alias?(column_name) column_name = attribute_alias(column_name) end if has_include?(column_name) - construct_relation_for_association_calculations.calculate(operation, column_name, options) + construct_relation_for_association_calculations.calculate(operation, column_name) else - perform_calculation(operation, column_name, options) + perform_calculation(operation, column_name) end end @@ -196,9 +185,7 @@ module ActiveRecord eager_loading? || (includes_values.present? && ((column_name && column_name != :all) || references_eager_loaded_tables?)) end - def perform_calculation(operation, column_name, options = {}) - # TODO: Remove options argument as soon we remove support to - # activerecord-deprecated_finders. + def perform_calculation(operation, column_name) operation = operation.to_s.downcase # If #count is used with #distinct / #uniq it is considered distinct. (eg. relation.distinct.count) diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index 357861caaa..088bc203b7 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -397,7 +397,7 @@ module ActiveRecord else if relation.limit_value limited_ids = limited_ids_for(relation) - limited_ids.empty? ? relation.none! : relation.where!(table[primary_key].in(limited_ids)) + limited_ids.empty? ? relation.none! : relation.where!(primary_key => limited_ids) end relation.except(:limit, :offset) end diff --git a/activerecord/lib/active_record/relation/merger.rb b/activerecord/lib/active_record/relation/merger.rb index a27f990f74..afb0b208c3 100644 --- a/activerecord/lib/active_record/relation/merger.rb +++ b/activerecord/lib/active_record/relation/merger.rb @@ -22,7 +22,7 @@ module ActiveRecord # build a relation to merge in rather than directly merging # the values. def other - other = Relation.create(relation.klass, relation.table) + other = Relation.create(relation.klass, relation.table, relation.predicate_builder) hash.each { |k, v| if k == :joins if Hash === v diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb index 67e646bf18..567efce8ae 100644 --- a/activerecord/lib/active_record/relation/predicate_builder.rb +++ b/activerecord/lib/active_record/relation/predicate_builder.rb @@ -1,23 +1,26 @@ module ActiveRecord class PredicateBuilder # :nodoc: - @handlers = [] + require 'active_record/relation/predicate_builder/array_handler' + require 'active_record/relation/predicate_builder/association_query_handler' + require 'active_record/relation/predicate_builder/base_handler' + require 'active_record/relation/predicate_builder/basic_object_handler' + require 'active_record/relation/predicate_builder/class_handler' + require 'active_record/relation/predicate_builder/range_handler' + require 'active_record/relation/predicate_builder/relation_handler' - autoload :RelationHandler, 'active_record/relation/predicate_builder/relation_handler' - autoload :ArrayHandler, 'active_record/relation/predicate_builder/array_handler' + delegate :resolve_column_aliases, to: :table - def initialize(klass, table) - @klass = klass + def initialize(table) @table = table - end - - def resolve_column_aliases(hash) - hash = hash.dup - hash.keys.grep(Symbol) do |key| - if klass.attribute_alias? key - hash[klass.attribute_alias(key)] = hash.delete key - end - end - hash + @handlers = [] + + register_handler(BasicObject, BasicObjectHandler.new(self)) + register_handler(Class, ClassHandler.new(self)) + register_handler(Base, BaseHandler.new(self)) + register_handler(Range, RangeHandler.new(self)) + register_handler(Relation, RelationHandler.new) + register_handler(Array, ArrayHandler.new(self)) + register_handler(AssociationQueryValue, AssociationQueryHandler.new(self)) end def build_from_hash(attributes) @@ -26,35 +29,16 @@ module ActiveRecord end def expand(column, value) - queries = [] - # Find the foreign key when using queries such as: # Post.where(author: author) # # For polymorphic relationships, find the foreign key and type: # PriceEstimate.where(estimate_of: treasure) - if klass && reflection = klass._reflect_on_association(column) - if reflection.polymorphic? && base_class = polymorphic_base_class_from_value(value) - queries << self.class.build(table[reflection.foreign_type], base_class.name) - end - - column = reflection.foreign_key + if table.associated_with?(column) + value = AssociationQueryValue.new(table.associated_table(column), value) end - queries << self.class.build(table[column], value) - queries - end - - def polymorphic_base_class_from_value(value) - case value - when Relation - value.klass.base_class - when Array - val = value.compact.first - val.class.base_class if val.is_a?(Base) - when Base - value.class.base_class - end + build(table.arel_attribute(column), value) end def self.references(attributes) @@ -79,46 +63,24 @@ module ActiveRecord # ) # end # ActiveRecord::PredicateBuilder.register_handler(MyCustomDateRange, handler) - def self.register_handler(klass, handler) + def register_handler(klass, handler) @handlers.unshift([klass, handler]) end - register_handler(BasicObject, ->(attribute, value) { attribute.eq(value) }) - register_handler(Class, ->(attribute, value) { deprecate_class_handler; attribute.eq(value.name) }) - register_handler(Base, ->(attribute, value) { attribute.eq(value.id) }) - register_handler(Range, ->(attribute, value) { attribute.between(value) }) - register_handler(Relation, RelationHandler.new) - register_handler(Array, ArrayHandler.new) - - def self.build(attribute, value) + def build(attribute, value) handler_for(value).call(attribute, value) end - def self.handler_for(object) - @handlers.detect { |klass, _| klass === object }.last - end - private_class_method :handler_for - - def self.deprecate_class_handler - ActiveSupport::Deprecation.warn(<<-MSG.squish) - Passing a class as a value in an Active Record query is deprecated and - will be removed. Pass a string instead. - MSG - end - protected - attr_reader :klass, :table + attr_reader :table def expand_from_hash(attributes) return ["1=0"] if attributes.empty? attributes.flat_map do |key, value| if value.is_a?(Hash) - arel_table = Arel::Table.new(key) - association = klass._reflect_on_association(key) - builder = self.class.new(association && association.klass, arel_table) - + builder = self.class.new(table.associated_table(key)) builder.expand_from_hash(value) else expand(key, value) @@ -141,5 +103,9 @@ module ActiveRecord attributes end + + def handler_for(object) + @handlers.detect { |klass, _| klass === object }.last + end end end diff --git a/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb index 4cba297be5..4b5f5773a0 100644 --- a/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb +++ b/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb @@ -3,6 +3,10 @@ require 'active_support/core_ext/string/filters' module ActiveRecord class PredicateBuilder class ArrayHandler # :nodoc: + def initialize(predicate_builder) + @predicate_builder = predicate_builder + end + def call(attribute, value) values = value.map { |x| x.is_a?(Base) ? x.id : x } nils, values = values.partition(&:nil?) @@ -14,20 +18,24 @@ module ActiveRecord values_predicate = case values.length when 0 then NullPredicate - when 1 then attribute.eq(values.first) + when 1 then predicate_builder.build(attribute, values.first) else attribute.in(values) end unless nils.empty? - values_predicate = values_predicate.or(attribute.eq(nil)) + values_predicate = values_predicate.or(predicate_builder.build(attribute, nil)) end - array_predicates = ranges.map { |range| attribute.between(range) } + array_predicates = ranges.map { |range| predicate_builder.build(attribute, range) } array_predicates.unshift(values_predicate) array_predicates.inject { |composite, predicate| composite.or(predicate) } end - module NullPredicate + protected + + attr_reader :predicate_builder + + module NullPredicate # :nodoc: def self.or(other) other end diff --git a/activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb new file mode 100644 index 0000000000..aabcf20c1d --- /dev/null +++ b/activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb @@ -0,0 +1,58 @@ +module ActiveRecord + class PredicateBuilder + class AssociationQueryHandler # :nodoc: + def initialize(predicate_builder) + @predicate_builder = predicate_builder + end + + def call(attribute, value) + queries = {} + + table = value.associated_table + if value.base_class + queries[table.association_foreign_type] = value.base_class.name + end + + queries[table.association_foreign_key] = value.ids + predicate_builder.build_from_hash(queries) + end + + protected + + attr_reader :predicate_builder + end + + class AssociationQueryValue # :nodoc: + attr_reader :associated_table, :value + + def initialize(associated_table, value) + @associated_table = associated_table + @value = value + end + + def ids + value + end + + def base_class + if associated_table.polymorphic_association? + @base_class ||= polymorphic_base_class_from_value + end + end + + private + + def polymorphic_base_class_from_value + case value + when Relation + value.klass.base_class + when Array + val = value.compact.first + val.class.base_class if val.is_a?(Base) + when Base + value.class.base_class + end + end + end + end +end diff --git a/activerecord/lib/active_record/relation/predicate_builder/base_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/base_handler.rb new file mode 100644 index 0000000000..6fa5b16f73 --- /dev/null +++ b/activerecord/lib/active_record/relation/predicate_builder/base_handler.rb @@ -0,0 +1,17 @@ +module ActiveRecord + class PredicateBuilder + class BaseHandler # :nodoc: + def initialize(predicate_builder) + @predicate_builder = predicate_builder + end + + def call(attribute, value) + predicate_builder.build(attribute, value.id) + end + + protected + + attr_reader :predicate_builder + end + end +end diff --git a/activerecord/lib/active_record/relation/predicate_builder/basic_object_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/basic_object_handler.rb new file mode 100644 index 0000000000..6cec75dc0a --- /dev/null +++ b/activerecord/lib/active_record/relation/predicate_builder/basic_object_handler.rb @@ -0,0 +1,17 @@ +module ActiveRecord + class PredicateBuilder + class BasicObjectHandler # :nodoc: + def initialize(predicate_builder) + @predicate_builder = predicate_builder + end + + def call(attribute, value) + attribute.eq(value) + end + + protected + + attr_reader :predicate_builder + end + end +end diff --git a/activerecord/lib/active_record/relation/predicate_builder/class_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/class_handler.rb new file mode 100644 index 0000000000..ed313fc9d4 --- /dev/null +++ b/activerecord/lib/active_record/relation/predicate_builder/class_handler.rb @@ -0,0 +1,27 @@ +module ActiveRecord + class PredicateBuilder + class ClassHandler # :nodoc: + def initialize(predicate_builder) + @predicate_builder = predicate_builder + end + + def call(attribute, value) + print_deprecation_warning + predicate_builder.build(attribute, value.name) + end + + protected + + attr_reader :predicate_builder + + private + + def print_deprecation_warning + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Passing a class as a value in an Active Record query is deprecated and + will be removed. Pass a string instead. + MSG + end + end + end +end diff --git a/activerecord/lib/active_record/relation/predicate_builder/range_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/range_handler.rb new file mode 100644 index 0000000000..1b3849e3ad --- /dev/null +++ b/activerecord/lib/active_record/relation/predicate_builder/range_handler.rb @@ -0,0 +1,17 @@ +module ActiveRecord + class PredicateBuilder + class RangeHandler # :nodoc: + def initialize(predicate_builder) + @predicate_builder = predicate_builder + end + + def call(attribute, value) + attribute.between(value) + end + + protected + + attr_reader :predicate_builder + end + end +end diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb index 57d66bce4b..01bddea6c9 100644 --- a/activerecord/lib/active_record/relation/spawn_methods.rb +++ b/activerecord/lib/active_record/relation/spawn_methods.rb @@ -67,7 +67,7 @@ module ActiveRecord private def relation_with(values) # :nodoc: - result = Relation.create(klass, table, values) + result = Relation.create(klass, table, predicate_builder, values) result.extend(*extending_values) if extending_values.any? result end diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb index 6c103e331f..f5aa60a69a 100644 --- a/activerecord/lib/active_record/sanitization.rb +++ b/activerecord/lib/active_record/sanitization.rb @@ -88,7 +88,7 @@ module ActiveRecord # # => "address_street='123 abc st.' and address_city='chicago'" def sanitize_sql_hash_for_conditions(attrs, default_table_name = self.table_name) table = Arel::Table.new(table_name).alias(default_table_name) - predicate_builder = PredicateBuilder.new(self, table) + predicate_builder = PredicateBuilder.new(TableMetadata.new(self, table)) ActiveSupport::Deprecation.warn(<<-EOWARN) sanitize_sql_hash_for_conditions is deprecated, and will be removed in Rails 5.0 EOWARN @@ -107,7 +107,8 @@ sanitize_sql_hash_for_conditions is deprecated, and will be removed in Rails 5.0 def sanitize_sql_hash_for_assignment(attrs, table) c = connection attrs.map do |attr, value| - "#{c.quote_table_name_for_assignment(table, attr)} = #{quote_bound_value(value, c, columns_hash[attr.to_s])}" + value = type_for_attribute(attr.to_s).type_cast_for_database(value) + "#{c.quote_table_name_for_assignment(table, attr)} = #{c.quote(value)}" end.join(', ') end @@ -163,10 +164,8 @@ sanitize_sql_hash_for_conditions is deprecated, and will be removed in Rails 5.0 end end - def quote_bound_value(value, c = connection, column = nil) #:nodoc: - if column - c.quote(value, column) - elsif value.respond_to?(:map) && !value.acts_like?(:string) + def quote_bound_value(value, c = connection) #:nodoc: + if value.respond_to?(:map) && !value.acts_like?(:string) if value.respond_to?(:empty?) && value.empty? c.quote(nil) else diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb index 3c44a625cc..da95920571 100644 --- a/activerecord/lib/active_record/schema_dumper.rb +++ b/activerecord/lib/active_record/schema_dumper.rb @@ -117,11 +117,12 @@ HEADER if pkcol if pk != 'id' tbl.print %Q(, primary_key: "#{pk}") - elsif pkcol.sql_type == 'bigint' - tbl.print ", id: :bigserial" - elsif pkcol.sql_type == 'uuid' - tbl.print ", id: :uuid" - tbl.print %Q(, default: "#{pkcol.default_function}") if pkcol.default_function + end + pkcolspec = @connection.column_spec_for_primary_key(pkcol) + if pkcolspec + pkcolspec.each do |key, value| + tbl.print ", #{key}: #{value}" + end end else tbl.print ", id: false" diff --git a/activerecord/lib/active_record/table_metadata.rb b/activerecord/lib/active_record/table_metadata.rb new file mode 100644 index 0000000000..11e33e8dfe --- /dev/null +++ b/activerecord/lib/active_record/table_metadata.rb @@ -0,0 +1,53 @@ +module ActiveRecord + class TableMetadata # :nodoc: + delegate :foreign_type, :foreign_key, to: :association, prefix: true + + def initialize(klass, arel_table, association = nil) + @klass = klass + @arel_table = arel_table + @association = association + end + + def resolve_column_aliases(hash) + hash = hash.dup + hash.keys.grep(Symbol) do |key| + if klass.attribute_alias? key + hash[klass.attribute_alias(key)] = hash.delete key + end + end + hash + end + + def arel_attribute(column_name) + arel_table[column_name] + end + + def associated_with?(association_name) + klass && klass._reflect_on_association(association_name) + end + + def associated_table(table_name) + return self if table_name == arel_table.name + + association = klass._reflect_on_association(table_name) + if association && !association.polymorphic? + association_klass = association.klass + arel_table = association_klass.arel_table + else + type_caster = TypeCaster::Connection.new(klass.connection, table_name) + association_klass = nil + arel_table = Arel::Table.new(table_name, type_caster: type_caster) + end + + TableMetadata.new(association_klass, arel_table, association) + end + + def polymorphic_association? + association && association.polymorphic? + end + + protected + + attr_reader :klass, :arel_table, :association + end +end diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb index 6ed610e5f0..69aceb66b1 100644 --- a/activerecord/lib/active_record/tasks/database_tasks.rb +++ b/activerecord/lib/active_record/tasks/database_tasks.rb @@ -188,27 +188,7 @@ module ActiveRecord class_for_adapter(configuration['adapter']).new(*arguments).structure_load(filename) end - def load_schema(format = ActiveRecord::Base.schema_format, file = nil) - ActiveSupport::Deprecation.warn(<<-MSG.squish) - This method will act on a specific connection in the future. - To act on the current connection, use `load_schema_current` instead. - MSG - - load_schema_current(format, file) - end - - def schema_file(format = ActiveSupport::Base.schema_format) - case format - when :ruby - File.join(db_dir, "schema.rb") - when :sql - File.join(db_dir, "structure.sql") - end - end - - # This method is the successor of +load_schema+. We should rename it - # after +load_schema+ went through a deprecation cycle. (Rails > 4.2) - def load_schema_for(configuration, format = ActiveRecord::Base.schema_format, file = nil) # :nodoc: + def load_schema(configuration, format = ActiveRecord::Base.schema_format, file = nil) # :nodoc: file ||= schema_file(format) case format @@ -224,6 +204,23 @@ module ActiveRecord end end + def load_schema_for(*args) + ActiveSupport::Deprecation.warn(<<-MSG.squish) + This method was renamed to `#load_schema` and will be removed in the future. + Use `#load_schema` instead. + MSG + load_schema(*args) + end + + def schema_file(format = ActiveSupport::Base.schema_format) + case format + when :ruby + File.join(db_dir, "schema.rb") + when :sql + File.join(db_dir, "structure.sql") + end + end + def load_schema_current_if_exists(format = ActiveRecord::Base.schema_format, file = nil, environment = env) if File.exist?(file || schema_file(format)) load_schema_current(format, file, environment) @@ -232,7 +229,7 @@ module ActiveRecord def load_schema_current(format = ActiveRecord::Base.schema_format, file = nil, environment = env) each_current_configuration(environment) { |configuration| - load_schema_for configuration, format, file + load_schema configuration, format, file } ActiveRecord::Base.establish_connection(environment.to_sym) end diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb index 936a18d99a..20e4235788 100644 --- a/activerecord/lib/active_record/timestamp.rb +++ b/activerecord/lib/active_record/timestamp.rb @@ -57,8 +57,8 @@ module ActiveRecord super end - def _update_record(*args) - if should_record_timestamps? + def _update_record(*args, touch: true, **options) + if touch && should_record_timestamps? current_time = current_time_from_proper_timezone timestamp_attributes_for_update_in_model.each do |column| @@ -67,7 +67,7 @@ module ActiveRecord write_attribute(column, current_time) end end - super + super(*args) end def should_record_timestamps? diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index c4a97db582..de701edca0 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -360,14 +360,12 @@ module ActiveRecord # Save the new record state and id of a record so it can be restored later if a transaction fails. def remember_transaction_record_state #:nodoc: @_start_transaction_state[:id] = id - unless @_start_transaction_state.include?(:new_record) - @_start_transaction_state[:new_record] = @new_record - end - unless @_start_transaction_state.include?(:destroyed) - @_start_transaction_state[:destroyed] = @destroyed - end + @_start_transaction_state.reverse_merge!( + new_record: @new_record, + destroyed: @destroyed, + frozen?: frozen?, + ) @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) + 1 - @_start_transaction_state[:frozen?] = frozen? end # Clear the new record state and id of a record. diff --git a/activerecord/lib/active_record/type/date_time.rb b/activerecord/lib/active_record/type/date_time.rb index 5f19608a33..0a737815bc 100644 --- a/activerecord/lib/active_record/type/date_time.rb +++ b/activerecord/lib/active_record/type/date_time.rb @@ -11,7 +11,11 @@ module ActiveRecord zone_conversion_method = ActiveRecord::Base.default_timezone == :utc ? :getutc : :getlocal if value.acts_like?(:time) - value.send(zone_conversion_method) + if value.respond_to?(zone_conversion_method) + value.send(zone_conversion_method) + else + value + end else super end diff --git a/activerecord/lib/active_record/type/decimal.rb b/activerecord/lib/active_record/type/decimal.rb index d10778eeb6..7b2bee2c42 100644 --- a/activerecord/lib/active_record/type/decimal.rb +++ b/activerecord/lib/active_record/type/decimal.rb @@ -16,7 +16,7 @@ module ActiveRecord def cast_value(value) case value when ::Float - BigDecimal(value, float_precision) + convert_float_to_big_decimal(value) when ::Numeric, ::String BigDecimal(value, precision.to_i) else @@ -28,6 +28,14 @@ module ActiveRecord end end + def convert_float_to_big_decimal(value) + if precision + BigDecimal(value, float_precision) + else + value.to_d + end + end + def float_precision if precision.to_i > ::Float::DIG + 1 ::Float::DIG + 1 diff --git a/activerecord/lib/active_record/type/numeric.rb b/activerecord/lib/active_record/type/numeric.rb index fa43266504..674f996f38 100644 --- a/activerecord/lib/active_record/type/numeric.rb +++ b/activerecord/lib/active_record/type/numeric.rb @@ -29,7 +29,7 @@ module ActiveRecord # 'wibble'.to_i will give zero, we want to make sure # that we aren't marking int zero to string zero as # changed. - value.to_s !~ /\A\d+\.?\d*\z/ + value.to_s !~ /\A-?\d+\.?\d*\z/ end end end diff --git a/activerecord/lib/active_record/type/serialized.rb b/activerecord/lib/active_record/type/serialized.rb index 3191a868ef..3cac03464e 100644 --- a/activerecord/lib/active_record/type/serialized.rb +++ b/activerecord/lib/active_record/type/serialized.rb @@ -29,7 +29,7 @@ module ActiveRecord def changed_in_place?(raw_old_value, value) return false if value.nil? - subtype.changed_in_place?(raw_old_value, coder.dump(value)) + subtype.changed_in_place?(raw_old_value, type_cast_for_database(value)) end def accessor diff --git a/activerecord/lib/active_record/type/string.rb b/activerecord/lib/active_record/type/string.rb index fbc0af2c5a..cf95e25be0 100644 --- a/activerecord/lib/active_record/type/string.rb +++ b/activerecord/lib/active_record/type/string.rb @@ -21,6 +21,10 @@ module ActiveRecord end end + def text? + true + end + private def cast_value(value) diff --git a/activerecord/lib/active_record/type/value.rb b/activerecord/lib/active_record/type/value.rb index 9456a4a56c..60ae47db3d 100644 --- a/activerecord/lib/active_record/type/value.rb +++ b/activerecord/lib/active_record/type/value.rb @@ -50,6 +50,10 @@ module ActiveRecord # These predicates are not documented, as I need to look further into # their use, and see if they can be removed entirely. + def text? # :nodoc: + false + end + def number? # :nodoc: false end @@ -91,8 +95,8 @@ module ActiveRecord # Convenience method for types which do not need separate type casting # behavior for user and database inputs. Called by - # `type_cast_from_database` and `type_cast_from_user` for all values - # except `nil`. + # +type_cast_from_database+ and +type_cast_from_user+ for all values + # except +nil+. def cast_value(value) # :doc: value end diff --git a/activerecord/lib/active_record/type_caster.rb b/activerecord/lib/active_record/type_caster.rb new file mode 100644 index 0000000000..63ba10c289 --- /dev/null +++ b/activerecord/lib/active_record/type_caster.rb @@ -0,0 +1,7 @@ +require 'active_record/type_caster/map' +require 'active_record/type_caster/connection' + +module ActiveRecord + module TypeCaster + end +end diff --git a/activerecord/lib/active_record/type_caster/connection.rb b/activerecord/lib/active_record/type_caster/connection.rb new file mode 100644 index 0000000000..9e4a130b40 --- /dev/null +++ b/activerecord/lib/active_record/type_caster/connection.rb @@ -0,0 +1,34 @@ +module ActiveRecord + module TypeCaster + class Connection + def initialize(connection, table_name) + @connection = connection + @table_name = table_name + end + + def type_cast_for_database(attribute_name, value) + return value if value.is_a?(Arel::Nodes::BindParam) + type = type_for(attribute_name) + type.type_cast_for_database(value) + end + + protected + + attr_reader :connection, :table_name + + private + + def type_for(attribute_name) + if connection.schema_cache.table_exists?(table_name) + column_for(attribute_name).cast_type + else + Type::Value.new + end + end + + def column_for(attribute_name) + connection.schema_cache.columns_hash(table_name)[attribute_name.to_s] + end + end + end +end diff --git a/activerecord/lib/active_record/type_caster/map.rb b/activerecord/lib/active_record/type_caster/map.rb new file mode 100644 index 0000000000..03c9e8ff83 --- /dev/null +++ b/activerecord/lib/active_record/type_caster/map.rb @@ -0,0 +1,19 @@ +module ActiveRecord + module TypeCaster + class Map + def initialize(types) + @types = types + end + + def type_cast_for_database(attr_name, value) + return value if value.is_a?(Arel::Nodes::BindParam) + type = types.type_for_attribute(attr_name.to_s) + type.type_cast_for_database(value) + end + + protected + + attr_reader :types + end + end +end diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb index a6c8ff7f3a..f27adc9c40 100644 --- a/activerecord/lib/active_record/validations.rb +++ b/activerecord/lib/active_record/validations.rb @@ -88,3 +88,4 @@ end require "active_record/validations/associated" require "active_record/validations/uniqueness" require "active_record/validations/presence" +require "active_record/validations/length" diff --git a/activerecord/lib/active_record/validations/length.rb b/activerecord/lib/active_record/validations/length.rb new file mode 100644 index 0000000000..ef5a6cbbe7 --- /dev/null +++ b/activerecord/lib/active_record/validations/length.rb @@ -0,0 +1,21 @@ +module ActiveRecord + module Validations + class LengthValidator < ActiveModel::Validations::LengthValidator # :nodoc: + def validate_each(record, attribute, association_or_value) + if association_or_value.respond_to?(:loaded?) && association_or_value.loaded? + association_or_value = association_or_value.target.reject(&:marked_for_destruction?) + end + super + end + end + + module ClassMethods + # See <tt>ActiveModel::Validation::LengthValidator</tt> for more information. + def validates_length_of(*attr_names) + validates_with LengthValidator, _merge_attributes(attr_names) + end + + alias_method :validates_size_of, :validates_length_of + end + end +end diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index 3e8afe37a8..f52f91e89c 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -16,9 +16,8 @@ module ActiveRecord value = map_enum_attribute(finder_class, attribute, value) relation = build_relation(finder_class, table, attribute, value) - relation = relation.and(table[finder_class.primary_key.to_sym].not_eq(record.id)) if record.persisted? + relation = relation.where.not(finder_class.primary_key => record.id) if record.persisted? relation = scope_relation(record, table, relation) - relation = finder_class.unscoped.where(relation) relation = relation.merge(options[:conditions]) if options[:conditions] if relation.exists? @@ -60,17 +59,21 @@ module ActiveRecord end column = klass.columns_hash[attribute_name] - value = klass.connection.type_cast(value, column) + value = klass.type_for_attribute(attribute_name).type_cast_for_database(value) + value = klass.connection.type_cast(value) if value.is_a?(String) && column.limit value = value.to_s[0, column.limit] end - if !options[:case_sensitive] && value.is_a?(String) + value = Arel::Nodes::Quoted.new(value) + + comparison = if !options[:case_sensitive] && value && column.text? # will use SQL LOWER function before comparison, unless it detects a case insensitive collation klass.connection.case_insensitive_comparison(table, attribute, column, value) else klass.connection.case_sensitive_comparison(table, attribute, column, value) end + klass.unscoped.where(comparison) end def scope_relation(record, table, relation) @@ -81,7 +84,7 @@ module ActiveRecord else scope_value = record._read_attribute(scope_item) end - relation = relation.and(table[scope_item].eq(scope_value)) + relation = relation.where(scope_item => scope_value) end relation diff --git a/activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb b/activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb index fb0fbb4759..f7bf6987c4 100644 --- a/activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb +++ b/activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb @@ -15,8 +15,5 @@ class <%= migration_class_name %> < ActiveRecord::Migration <% attributes_with_index.each do |attribute| -%> add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %> <% end -%> -<% attributes.select(&:reference?).reject(&:polymorphic?).each do |attribute| -%> - add_foreign_key :<%= table_name %>, :<%= attribute.name.pluralize %> -<% end -%> end end diff --git a/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb b/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb index 7df9bcad25..ae9c74fd05 100644 --- a/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb +++ b/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb @@ -4,9 +4,6 @@ class <%= migration_class_name %> < ActiveRecord::Migration <% attributes.each do |attribute| -%> <%- if attribute.reference? -%> add_reference :<%= table_name %>, :<%= attribute.name %><%= attribute.inject_options %> - <%- unless attribute.polymorphic? -%> - add_foreign_key :<%= table_name %>, :<%= attribute.name.pluralize %> - <%- end -%> <%- else -%> add_column :<%= table_name %>, :<%= attribute.name %>, :<%= attribute.type %><%= attribute.inject_options %> <%- if attribute.has_index? -%> @@ -29,9 +26,6 @@ class <%= migration_class_name %> < ActiveRecord::Migration <%- if migration_action -%> <%- if attribute.reference? -%> remove_reference :<%= table_name %>, :<%= attribute.name %><%= attribute.inject_options %> - <%- unless attribute.polymorphic? -%> - remove_foreign_key :<%= table_name %>, :<%= attribute.name.pluralize %> - <%- end -%> <%- else -%> <%- if attribute.has_index? -%> remove_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %> diff --git a/activerecord/test/cases/adapter_test.rb b/activerecord/test/cases/adapter_test.rb index 6f84bae432..99e3d7021d 100644 --- a/activerecord/test/cases/adapter_test.rb +++ b/activerecord/test/cases/adapter_test.rb @@ -213,6 +213,16 @@ module ActiveRecord test "type_to_sql returns a String for unmapped types" do assert_equal "special_db_type", @connection.type_to_sql(:special_db_type) end + + unless current_adapter?(:PostgreSQLAdapter) + def test_log_invalid_encoding + assert_raise ActiveRecord::StatementInvalid do + @connection.send :log, "SELECT 'ы' FROM DUAL" do + raise 'ы'.force_encoding(Encoding::ASCII_8BIT) + end + end + end + end end class AdapterTestWithoutTransaction < ActiveRecord::TestCase diff --git a/activerecord/test/cases/adapters/mysql/datetime_test.rb b/activerecord/test/cases/adapters/mysql/datetime_test.rb new file mode 100644 index 0000000000..ae00f4e131 --- /dev/null +++ b/activerecord/test/cases/adapters/mysql/datetime_test.rb @@ -0,0 +1,87 @@ +require 'cases/helper' + +if mysql_56? + class DateTimeTest < ActiveRecord::TestCase + self.use_transactional_fixtures = false + + class Foo < ActiveRecord::Base; end + + def test_default_datetime_precision + ActiveRecord::Base.connection.create_table(:foos, force: true) + ActiveRecord::Base.connection.add_column :foos, :created_at, :datetime + ActiveRecord::Base.connection.add_column :foos, :updated_at, :datetime + assert_nil activerecord_column_option('foos', 'created_at', 'precision') + end + + def test_datetime_data_type_with_precision + ActiveRecord::Base.connection.create_table(:foos, force: true) + ActiveRecord::Base.connection.add_column :foos, :created_at, :datetime, precision: 1 + ActiveRecord::Base.connection.add_column :foos, :updated_at, :datetime, precision: 5 + assert_equal 1, activerecord_column_option('foos', 'created_at', 'precision') + assert_equal 5, activerecord_column_option('foos', 'updated_at', 'precision') + end + + def test_timestamps_helper_with_custom_precision + ActiveRecord::Base.connection.create_table(:foos, force: true) do |t| + t.timestamps null: true, precision: 4 + end + assert_equal 4, activerecord_column_option('foos', 'created_at', 'precision') + assert_equal 4, activerecord_column_option('foos', 'updated_at', 'precision') + end + + def test_passing_precision_to_datetime_does_not_set_limit + ActiveRecord::Base.connection.create_table(:foos, force: true) do |t| + t.timestamps null: true, precision: 4 + end + assert_nil activerecord_column_option('foos', 'created_at', 'limit') + assert_nil activerecord_column_option('foos', 'updated_at', 'limit') + end + + def test_invalid_datetime_precision_raises_error + assert_raises ActiveRecord::ActiveRecordError do + ActiveRecord::Base.connection.create_table(:foos, force: true) do |t| + t.timestamps null: true, precision: 7 + end + end + end + + def test_mysql_agrees_with_activerecord_about_precision + ActiveRecord::Base.connection.create_table(:foos, force: true) do |t| + t.timestamps null: true, precision: 4 + end + assert_equal 4, mysql_datetime_precision('foos', 'created_at') + assert_equal 4, mysql_datetime_precision('foos', 'updated_at') + end + + def test_formatting_datetime_according_to_precision + ActiveRecord::Base.connection.create_table(:foos, force: true) do |t| + t.datetime :created_at, precision: 0 + t.datetime :updated_at, precision: 4 + end + date = ::Time.utc(2014, 8, 17, 12, 30, 0, 999999) + Foo.create!(created_at: date, updated_at: date) + assert foo = Foo.find_by(created_at: date) + assert_equal date.to_s, foo.created_at.to_s + assert_equal date.to_s, foo.updated_at.to_s + assert_equal 000000, foo.created_at.usec + assert_equal 999900, foo.updated_at.usec + end + + private + + def mysql_datetime_precision(table_name, column_name) + results = ActiveRecord::Base.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"] + end + + def activerecord_column_option(tablename, column_name, option) + result = ActiveRecord::Base.connection.columns(tablename).find do |column| + column.name == column_name + end + result && result.send(option) + end + end +end diff --git a/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb b/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb index 28106d3772..85db8f4614 100644 --- a/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb +++ b/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb @@ -99,6 +99,12 @@ module ActiveRecord end end + def test_composite_primary_key + with_example_table '`id` INT(11), `number` INT(11), foo INT(11), PRIMARY KEY (`id`, `number`)' do + assert_nil @conn.primary_key('ex') + end + end + def test_tinyint_integer_typecasting with_example_table '`status` TINYINT(4)' do insert(@conn, { 'status' => 2 }, 'ex') diff --git a/activerecord/test/cases/adapters/mysql/quoting_test.rb b/activerecord/test/cases/adapters/mysql/quoting_test.rb index d8a954efa8..a2206153e9 100644 --- a/activerecord/test/cases/adapters/mysql/quoting_test.rb +++ b/activerecord/test/cases/adapters/mysql/quoting_test.rb @@ -9,15 +9,11 @@ module ActiveRecord end def test_type_cast_true - c = Column.new(nil, 1, Type::Boolean.new) - assert_equal 1, @conn.type_cast(true, nil) - assert_equal 1, @conn.type_cast(true, c) + assert_equal 1, @conn.type_cast(true) end def test_type_cast_false - c = Column.new(nil, 1, Type::Boolean.new) - assert_equal 0, @conn.type_cast(false, nil) - assert_equal 0, @conn.type_cast(false, c) + assert_equal 0, @conn.type_cast(false) end end end diff --git a/activerecord/test/cases/adapters/mysql2/boolean_test.rb b/activerecord/test/cases/adapters/mysql2/boolean_test.rb index 03627135b2..0e641ba3bf 100644 --- a/activerecord/test/cases/adapters/mysql2/boolean_test.rb +++ b/activerecord/test/cases/adapters/mysql2/boolean_test.rb @@ -47,8 +47,7 @@ class Mysql2BooleanTest < ActiveRecord::TestCase assert_equal 1, attributes["archived"] assert_equal "1", attributes["published"] - assert_equal 1, @connection.type_cast(true, boolean_column) - assert_equal "1", @connection.type_cast(true, string_column) + assert_equal 1, @connection.type_cast(true) end test "test type casting without emulated booleans" do @@ -60,8 +59,7 @@ class Mysql2BooleanTest < ActiveRecord::TestCase assert_equal 1, attributes["archived"] assert_equal "1", attributes["published"] - assert_equal 1, @connection.type_cast(true, boolean_column) - assert_equal "1", @connection.type_cast(true, string_column) + assert_equal 1, @connection.type_cast(true) end test "with booleans stored as 1 and 0" do diff --git a/activerecord/test/cases/adapters/mysql2/datetime_test.rb b/activerecord/test/cases/adapters/mysql2/datetime_test.rb new file mode 100644 index 0000000000..ae00f4e131 --- /dev/null +++ b/activerecord/test/cases/adapters/mysql2/datetime_test.rb @@ -0,0 +1,87 @@ +require 'cases/helper' + +if mysql_56? + class DateTimeTest < ActiveRecord::TestCase + self.use_transactional_fixtures = false + + class Foo < ActiveRecord::Base; end + + def test_default_datetime_precision + ActiveRecord::Base.connection.create_table(:foos, force: true) + ActiveRecord::Base.connection.add_column :foos, :created_at, :datetime + ActiveRecord::Base.connection.add_column :foos, :updated_at, :datetime + assert_nil activerecord_column_option('foos', 'created_at', 'precision') + end + + def test_datetime_data_type_with_precision + ActiveRecord::Base.connection.create_table(:foos, force: true) + ActiveRecord::Base.connection.add_column :foos, :created_at, :datetime, precision: 1 + ActiveRecord::Base.connection.add_column :foos, :updated_at, :datetime, precision: 5 + assert_equal 1, activerecord_column_option('foos', 'created_at', 'precision') + assert_equal 5, activerecord_column_option('foos', 'updated_at', 'precision') + end + + def test_timestamps_helper_with_custom_precision + ActiveRecord::Base.connection.create_table(:foos, force: true) do |t| + t.timestamps null: true, precision: 4 + end + assert_equal 4, activerecord_column_option('foos', 'created_at', 'precision') + assert_equal 4, activerecord_column_option('foos', 'updated_at', 'precision') + end + + def test_passing_precision_to_datetime_does_not_set_limit + ActiveRecord::Base.connection.create_table(:foos, force: true) do |t| + t.timestamps null: true, precision: 4 + end + assert_nil activerecord_column_option('foos', 'created_at', 'limit') + assert_nil activerecord_column_option('foos', 'updated_at', 'limit') + end + + def test_invalid_datetime_precision_raises_error + assert_raises ActiveRecord::ActiveRecordError do + ActiveRecord::Base.connection.create_table(:foos, force: true) do |t| + t.timestamps null: true, precision: 7 + end + end + end + + def test_mysql_agrees_with_activerecord_about_precision + ActiveRecord::Base.connection.create_table(:foos, force: true) do |t| + t.timestamps null: true, precision: 4 + end + assert_equal 4, mysql_datetime_precision('foos', 'created_at') + assert_equal 4, mysql_datetime_precision('foos', 'updated_at') + end + + def test_formatting_datetime_according_to_precision + ActiveRecord::Base.connection.create_table(:foos, force: true) do |t| + t.datetime :created_at, precision: 0 + t.datetime :updated_at, precision: 4 + end + date = ::Time.utc(2014, 8, 17, 12, 30, 0, 999999) + Foo.create!(created_at: date, updated_at: date) + assert foo = Foo.find_by(created_at: date) + assert_equal date.to_s, foo.created_at.to_s + assert_equal date.to_s, foo.updated_at.to_s + assert_equal 000000, foo.created_at.usec + assert_equal 999900, foo.updated_at.usec + end + + private + + def mysql_datetime_precision(table_name, column_name) + results = ActiveRecord::Base.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"] + end + + def activerecord_column_option(tablename, column_name, option) + result = ActiveRecord::Base.connection.columns(tablename).find do |column| + column.name == column_name + end + result && result.send(option) + end + end +end diff --git a/activerecord/test/cases/adapters/mysql2/explain_test.rb b/activerecord/test/cases/adapters/mysql2/explain_test.rb index f67f21fab1..2b01d941b8 100644 --- a/activerecord/test/cases/adapters/mysql2/explain_test.rb +++ b/activerecord/test/cases/adapters/mysql2/explain_test.rb @@ -18,7 +18,7 @@ module ActiveRecord explain = Developer.where(:id => 1).includes(:audit_logs).explain assert_match %(EXPLAIN for: SELECT `developers`.* FROM `developers` WHERE `developers`.`id` = 1), explain assert_match %r(developers |.* const), explain - assert_match %(EXPLAIN for: SELECT `audit_logs`.* FROM `audit_logs` WHERE `audit_logs`.`developer_id` IN (1)), explain + assert_match %(EXPLAIN for: SELECT `audit_logs`.* FROM `audit_logs` WHERE `audit_logs`.`developer_id` = 1), explain assert_match %r(audit_logs |.* ALL), explain end end diff --git a/activerecord/test/cases/adapters/postgresql/cidr_test.rb b/activerecord/test/cases/adapters/postgresql/cidr_test.rb new file mode 100644 index 0000000000..54b679d3ab --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/cidr_test.rb @@ -0,0 +1,25 @@ +require "cases/helper" +require "ipaddr" + +module ActiveRecord + module ConnectionAdapters + class PostgreSQLAdapter + class CidrTest < ActiveRecord::TestCase + test "type casting IPAddr for database" do + type = OID::Cidr.new + ip = IPAddr.new("255.0.0.0/8") + ip2 = IPAddr.new("127.0.0.1") + + assert_equal "255.0.0.0/8", type.type_cast_for_database(ip) + assert_equal "127.0.0.1/32", type.type_cast_for_database(ip2) + end + + test "casting does nothing with non-IPAddr objects" do + type = OID::Cidr.new + + assert_equal "foo", type.type_cast_for_database("foo") + end + end + end + end +end diff --git a/activerecord/test/cases/adapters/postgresql/explain_test.rb b/activerecord/test/cases/adapters/postgresql/explain_test.rb index d2dd04b84b..6ffb4c9f33 100644 --- a/activerecord/test/cases/adapters/postgresql/explain_test.rb +++ b/activerecord/test/cases/adapters/postgresql/explain_test.rb @@ -18,7 +18,7 @@ module ActiveRecord explain = Developer.where(:id => 1).includes(:audit_logs).explain assert_match %(QUERY PLAN), explain assert_match %(EXPLAIN for: SELECT "developers".* FROM "developers" WHERE "developers"."id" = $1), explain - assert_match %(EXPLAIN for: SELECT "audit_logs".* FROM "audit_logs" WHERE "audit_logs"."developer_id" IN (1)), explain + assert_match %(EXPLAIN for: SELECT "audit_logs".* FROM "audit_logs" WHERE "audit_logs"."developer_id" = 1), explain end end end diff --git a/activerecord/test/cases/adapters/postgresql/integer_test.rb b/activerecord/test/cases/adapters/postgresql/integer_test.rb new file mode 100644 index 0000000000..7f8751281e --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/integer_test.rb @@ -0,0 +1,25 @@ +require "cases/helper" +require "active_support/core_ext/numeric/bytes" + +class PostgresqlIntegerTest < ActiveRecord::TestCase + class PgInteger < ActiveRecord::Base + end + + def setup + @connection = ActiveRecord::Base.connection + + @connection.transaction do + @connection.create_table "pg_integers", force: true do |t| + t.integer :quota, limit: 8, default: 2.gigabytes + end + end + end + + teardown do + @connection.execute "drop table if exists pg_integers" + end + + test "schema properly respects bigint ranges" do + assert_equal 2.gigabytes, PgInteger.new.quota + end +end diff --git a/activerecord/test/cases/adapters/postgresql/money_test.rb b/activerecord/test/cases/adapters/postgresql/money_test.rb index 87183174f2..54cff192c1 100644 --- a/activerecord/test/cases/adapters/postgresql/money_test.rb +++ b/activerecord/test/cases/adapters/postgresql/money_test.rb @@ -10,7 +10,7 @@ class PostgresqlMoneyTest < ActiveRecord::TestCase setup do @connection = ActiveRecord::Base.connection @connection.execute("set lc_monetary = 'C'") - @connection.create_table('postgresql_moneys') do |t| + @connection.create_table('postgresql_moneys', force: true) do |t| t.column "wealth", "money" t.column "depth", "money", default: "150.55" end diff --git a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb index c3c696b871..6bb2b26cd5 100644 --- a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb +++ b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb @@ -54,6 +54,12 @@ module ActiveRecord end end + def test_composite_primary_key + with_example_table 'id serial, number serial, PRIMARY KEY (id, number)' do + assert_nil @connection.primary_key('ex') + end + end + def test_primary_key_raises_error_if_table_not_found assert_raises(ActiveRecord::StatementInvalid) do @connection.primary_key('unobtainium') diff --git a/activerecord/test/cases/adapters/postgresql/quoting_test.rb b/activerecord/test/cases/adapters/postgresql/quoting_test.rb index 11d5173d37..9ac0036d66 100644 --- a/activerecord/test/cases/adapters/postgresql/quoting_test.rb +++ b/activerecord/test/cases/adapters/postgresql/quoting_test.rb @@ -10,47 +10,21 @@ module ActiveRecord end def test_type_cast_true - c = PostgreSQLColumn.new(nil, 1, Type::Boolean.new, 'boolean') - assert_equal 't', @conn.type_cast(true, nil) - assert_equal 't', @conn.type_cast(true, c) + assert_equal 't', @conn.type_cast(true) end def test_type_cast_false - c = PostgreSQLColumn.new(nil, 1, Type::Boolean.new, 'boolean') - assert_equal 'f', @conn.type_cast(false, nil) - assert_equal 'f', @conn.type_cast(false, c) - end - - def test_type_cast_cidr - ip = IPAddr.new('255.0.0.0/8') - c = PostgreSQLColumn.new(nil, ip, OID::Cidr.new, 'cidr') - assert_equal ip, @conn.type_cast(ip, c) - end - - def test_type_cast_inet - ip = IPAddr.new('255.1.0.0/8') - c = PostgreSQLColumn.new(nil, ip, OID::Cidr.new, 'inet') - assert_equal ip, @conn.type_cast(ip, c) + assert_equal 'f', @conn.type_cast(false) end def test_quote_float_nan nan = 0.0/0 - c = PostgreSQLColumn.new(nil, 1, OID::Float.new, 'float') - assert_equal "'NaN'", @conn.quote(nan, c) + assert_equal "'NaN'", @conn.quote(nan) end def test_quote_float_infinity infinity = 1.0/0 - c = PostgreSQLColumn.new(nil, 1, OID::Float.new, 'float') - assert_equal "'Infinity'", @conn.quote(infinity, c) - end - - def test_quote_cast_numeric - fixnum = 666 - c = PostgreSQLColumn.new(nil, nil, Type::String.new, 'varchar') - assert_equal "'666'", @conn.quote(fixnum, c) - c = PostgreSQLColumn.new(nil, nil, Type::Text.new, 'text') - assert_equal "'666'", @conn.quote(fixnum, c) + assert_equal "'Infinity'", @conn.quote(infinity) end def test_quote_time_usec diff --git a/activerecord/test/cases/adapters/postgresql/uuid_test.rb b/activerecord/test/cases/adapters/postgresql/uuid_test.rb index d6deb6fb1f..d5d2dd16e2 100644 --- a/activerecord/test/cases/adapters/postgresql/uuid_test.rb +++ b/activerecord/test/cases/adapters/postgresql/uuid_test.rb @@ -116,6 +116,23 @@ class PostgresqlUUIDTest < ActiveRecord::TestCase output = dump_table_schema "uuid_data_type" assert_match %r{t.uuid "guid"}, output end + + def test_uniqueness_validation_ignores_uuid + klass = Class.new(ActiveRecord::Base) do + self.table_name = "uuid_data_type" + validates :guid, uniqueness: { case_sensitive: false } + + def self.name + "UUIDType" + end + end + + record = klass.create!(guid: "a0ee-bc99-9c0b-4ef8-bb6d-6bb9-bd38-0a11") + duplicate = klass.new(guid: record.guid) + + assert record.guid.present? # Ensure we actually are testing a UUID + assert_not duplicate.valid? + end end class PostgresqlLargeKeysTest < ActiveRecord::TestCase @@ -215,6 +232,7 @@ end class PostgresqlUUIDTestNilDefault < ActiveRecord::TestCase include PostgresqlUUIDHelper + include SchemaDumpingHelper setup do enable_extension!('uuid-ossp', connection) @@ -238,6 +256,11 @@ class PostgresqlUUIDTestNilDefault < ActiveRecord::TestCase WHERE a.attname='id' AND a.attrelid = 'pg_uuids'::regclass").first assert_nil col_desc["default"] end + + def test_schema_dumper_for_uuid_primary_key_with_default_override_via_nil + schema = dump_table_schema "pg_uuids" + assert_match(/\bcreate_table "pg_uuids", id: :uuid, default: nil/, schema) + end end end diff --git a/activerecord/test/cases/adapters/sqlite3/explain_test.rb b/activerecord/test/cases/adapters/sqlite3/explain_test.rb index de6e35ef57..7d66c44798 100644 --- a/activerecord/test/cases/adapters/sqlite3/explain_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/explain_test.rb @@ -18,7 +18,7 @@ module ActiveRecord explain = Developer.where(:id => 1).includes(:audit_logs).explain assert_match %(EXPLAIN for: SELECT "developers".* FROM "developers" WHERE "developers"."id" = ?), explain assert_match(/(SEARCH )?TABLE developers USING (INTEGER )?PRIMARY KEY/, explain) - assert_match %(EXPLAIN for: SELECT "audit_logs".* FROM "audit_logs" WHERE "audit_logs"."developer_id" IN (1)), explain + assert_match %(EXPLAIN for: SELECT "audit_logs".* FROM "audit_logs" WHERE "audit_logs"."developer_id" = 1), explain assert_match(/(SCAN )?TABLE audit_logs/, explain) end end diff --git a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb index ac8332e2fa..df497e761c 100644 --- a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb @@ -15,73 +15,52 @@ module ActiveRecord def test_type_cast_binary_encoding_without_logger @conn.extend(Module.new { def logger; end }) - column = Column.new(nil, nil, Type::String.new) binary = SecureRandom.hex expected = binary.dup.encode!(Encoding::UTF_8) - assert_equal expected, @conn.type_cast(binary, column) + assert_equal expected, @conn.type_cast(binary) end def test_type_cast_symbol - assert_equal 'foo', @conn.type_cast(:foo, nil) + assert_equal 'foo', @conn.type_cast(:foo) end def test_type_cast_date date = Date.today expected = @conn.quoted_date(date) - assert_equal expected, @conn.type_cast(date, nil) + assert_equal expected, @conn.type_cast(date) end def test_type_cast_time time = Time.now expected = @conn.quoted_date(time) - assert_equal expected, @conn.type_cast(time, nil) + assert_equal expected, @conn.type_cast(time) end def test_type_cast_numeric - assert_equal 10, @conn.type_cast(10, nil) - assert_equal 2.2, @conn.type_cast(2.2, nil) + assert_equal 10, @conn.type_cast(10) + assert_equal 2.2, @conn.type_cast(2.2) end def test_type_cast_nil - assert_equal nil, @conn.type_cast(nil, nil) + assert_equal nil, @conn.type_cast(nil) end def test_type_cast_true - c = Column.new(nil, 1, Type::Integer.new) - assert_equal 't', @conn.type_cast(true, nil) - assert_equal 1, @conn.type_cast(true, c) + assert_equal 't', @conn.type_cast(true) end def test_type_cast_false - c = Column.new(nil, 1, Type::Integer.new) - assert_equal 'f', @conn.type_cast(false, nil) - assert_equal 0, @conn.type_cast(false, c) - end - - def test_type_cast_string - assert_equal '10', @conn.type_cast('10', nil) - - c = Column.new(nil, 1, Type::Integer.new) - assert_equal 10, @conn.type_cast('10', c) - - c = Column.new(nil, 1, Type::Float.new) - assert_equal 10.1, @conn.type_cast('10.1', c) - - c = Column.new(nil, 1, Type::Binary.new) - assert_equal '10.1', @conn.type_cast('10.1', c) - - c = Column.new(nil, 1, Type::Date.new) - assert_equal '10.1', @conn.type_cast('10.1', c) + assert_equal 'f', @conn.type_cast(false) end def test_type_cast_bigdecimal bd = BigDecimal.new '10.0' - assert_equal bd.to_f, @conn.type_cast(bd, nil) + assert_equal bd.to_f, @conn.type_cast(bd) end def test_type_cast_unknown_should_raise_error obj = Class.new.new - assert_raise(TypeError) { @conn.type_cast(obj, nil) } + assert_raise(TypeError) { @conn.type_cast(obj) } end def test_type_cast_object_which_responds_to_quoted_id @@ -94,14 +73,14 @@ module ActiveRecord 10 end }.new - assert_equal 10, @conn.type_cast(quoted_id_obj, nil) + assert_equal 10, @conn.type_cast(quoted_id_obj) quoted_id_obj = Class.new { def quoted_id "'zomg'" end }.new - assert_raise(TypeError) { @conn.type_cast(quoted_id_obj, nil) } + assert_raise(TypeError) { @conn.type_cast(quoted_id_obj) } end def test_quoting_binary_strings diff --git a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb index 9d09ff49c7..029663e7f4 100644 --- a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb @@ -405,6 +405,12 @@ module ActiveRecord end end + def test_composite_primary_key + with_example_table 'id integer, number integer, foo integer, PRIMARY KEY (id, number)' do + assert_nil @conn.primary_key('ex') + end + end + def test_supports_extensions assert_not @conn.supports_extensions?, 'does not support extensions' end diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb index db8fd92c1f..fdb437d11d 100644 --- a/activerecord/test/cases/associations/eager_test.rb +++ b/activerecord/test/cases/associations/eager_test.rb @@ -1328,7 +1328,6 @@ class EagerAssociationTest < ActiveRecord::TestCase end test "eager-loading readonly association" do - skip "eager_load does not yet preserve readonly associations" # has-one firm = Firm.where(id: "1").eager_load(:readonly_account).first! assert firm.readonly_account.readonly? @@ -1340,6 +1339,10 @@ class EagerAssociationTest < ActiveRecord::TestCase # has-many :through david = Author.where(id: "1").eager_load(:readonly_comments).first! assert david.readonly_comments.first.readonly? + + # belongs_to + post = Post.where(id: "1").eager_load(:author).first! + assert post.author.readonly? end test "preloading a polymorphic association with references to the associated table" do diff --git a/activerecord/test/cases/associations/extension_test.rb b/activerecord/test/cases/associations/extension_test.rb index 9d373cd73b..b161cde335 100644 --- a/activerecord/test/cases/associations/extension_test.rb +++ b/activerecord/test/cases/associations/extension_test.rb @@ -76,7 +76,6 @@ class AssociationsExtensionsTest < ActiveRecord::TestCase private def extend!(model) - builder = ActiveRecord::Associations::Builder::HasMany.new(model, :association_name, nil, {}) { } - builder.define_extensions(model) + ActiveRecord::Associations::Builder::HasMany.define_extensions(model, :association_name) { } end end diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index d3b74aa616..21a45042fa 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -31,6 +31,8 @@ require 'models/student' require 'models/pirate' require 'models/ship' require 'models/tyre' +require 'models/subscriber' +require 'models/subscription' class HasManyAssociationsTestForReorderWithJoinDependency < ActiveRecord::TestCase fixtures :authors, :posts, :comments @@ -43,12 +45,59 @@ class HasManyAssociationsTestForReorderWithJoinDependency < ActiveRecord::TestCa end end +class HasManyAssociationsTestPrimaryKeys < ActiveRecord::TestCase + fixtures :authors, :essays, :subscribers, :subscriptions, :people + + def test_custom_primary_key_on_new_record_should_fetch_with_query + subscriber = Subscriber.new(nick: 'webster132') + assert !subscriber.subscriptions.loaded? + + assert_queries 1 do + assert_equal 2, subscriber.subscriptions.size + end + + assert_equal subscriber.subscriptions, Subscription.where(subscriber_id: 'webster132') + end + + def test_association_primary_key_on_new_record_should_fetch_with_query + author = Author.new(:name => "David") + assert !author.essays.loaded? + + assert_queries 1 do + assert_equal 1, author.essays.size + end + + assert_equal author.essays, Essay.where(writer_id: "David") + end + + def test_has_many_custom_primary_key + david = authors(:david) + assert_equal david.essays, Essay.where(writer_id: "David") + end + + def test_has_many_assignment_with_custom_primary_key + david = people(:david) + + assert_equal ["A Modest Proposal"], david.essays.map(&:name) + david.essays = [Essay.create!(name: "Remote Work" )] + assert_equal ["Remote Work"], david.essays.map(&:name) + end + + def test_blank_custom_primary_key_on_new_record_should_not_run_queries + author = Author.new + assert !author.essays.loaded? + + assert_queries 0 do + assert_equal 0, author.essays.size + end + end +end class HasManyAssociationsTest < ActiveRecord::TestCase fixtures :accounts, :categories, :companies, :developers, :projects, :developers_projects, :topics, :authors, :comments, - :people, :posts, :readers, :taggings, :cars, :essays, - :categorizations, :jobs, :tags + :posts, :readers, :taggings, :cars, :jobs, :tags, + :categorizations def setup Client.destroyed_client_ids.clear @@ -1578,39 +1627,6 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end end - def test_custom_primary_key_on_new_record_should_fetch_with_query - author = Author.new(:name => "David") - assert !author.essays.loaded? - - assert_queries 1 do - assert_equal 1, author.essays.size - end - - assert_equal author.essays, Essay.where(writer_id: "David") - end - - def test_has_many_custom_primary_key - david = authors(:david) - assert_equal david.essays, Essay.where(writer_id: "David") - end - - def test_has_many_assignment_with_custom_primary_key - david = people(:david) - - assert_equal ["A Modest Proposal"], david.essays.map(&:name) - david.essays = [Essay.create!(name: "Remote Work" )] - assert_equal ["Remote Work"], david.essays.map(&:name) - end - - def test_blank_custom_primary_key_on_new_record_should_not_run_queries - author = Author.new - assert !author.essays.loaded? - - assert_queries 0 do - assert_equal 0, author.essays.size - end - end - def test_calling_first_or_last_with_integer_on_association_should_not_load_association firm = companies(:first_firm) firm.clients.create(:name => 'Foo') 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 589a232bdb..6729a5a9fc 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -25,12 +25,13 @@ require 'models/categorization' require 'models/member' require 'models/membership' require 'models/club' +require 'models/organization' class HasManyThroughAssociationsTest < ActiveRecord::TestCase fixtures :posts, :readers, :people, :comments, :authors, :categories, :taggings, :tags, :owners, :pets, :toys, :jobs, :references, :companies, :members, :author_addresses, :subscribers, :books, :subscriptions, :developers, :categorizations, :essays, - :categories_posts, :clubs, :memberships + :categories_posts, :clubs, :memberships, :organizations # Dummies to force column loads so query counts are clean. def setup @@ -1151,4 +1152,12 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase club.members << member assert_equal 1, SuperMembership.where(member_id: member.id, club_id: club.id).count end + + def test_build_for_has_many_through_association + organization = organizations(:nsa) + author = organization.author + post_direct = author.posts.build + post_through = organization.posts.build + assert_equal post_direct.author_id, post_through.author_id + end end diff --git a/activerecord/test/cases/associations/has_one_associations_test.rb b/activerecord/test/cases/associations/has_one_associations_test.rb index a69f7a5262..9b6757e256 100644 --- a/activerecord/test/cases/associations/has_one_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_associations_test.rb @@ -273,6 +273,14 @@ class HasOneAssociationsTest < ActiveRecord::TestCase assert_equal account, firm.reload.account end + def test_create_with_inexistent_foreign_key_failing + firm = Firm.create(name: 'GlobalMegaCorp') + + assert_raises(ActiveRecord::UnknownAttributeError) do + firm.create_account_with_inexistent_foreign_key + end + end + def test_build firm = Firm.new("name" => "GlobalMegaCorp") firm.save @@ -566,6 +574,12 @@ class HasOneAssociationsTest < ActiveRecord::TestCase assert_equal author.post, post end + def test_has_one_loading_for_new_record + post = Post.create!(author_id: 42, title: 'foo', body: 'bar') + author = Author.new(id: 42) + assert_equal post, author.post + end + def test_has_one_relationship_cannot_have_a_counter_cache assert_raise(ArgumentError) do Class.new(ActiveRecord::Base) do diff --git a/activerecord/test/cases/associations/inverse_associations_test.rb b/activerecord/test/cases/associations/inverse_associations_test.rb index 60df4e14dd..423b8238b1 100644 --- a/activerecord/test/cases/associations/inverse_associations_test.rb +++ b/activerecord/test/cases/associations/inverse_associations_test.rb @@ -10,6 +10,9 @@ require 'models/comment' require 'models/car' require 'models/bulb' require 'models/mixed_case_monkey' +require 'models/admin' +require 'models/admin/account' +require 'models/admin/user' class AutomaticInverseFindingTests < ActiveRecord::TestCase fixtures :ratings, :comments, :cars @@ -27,6 +30,15 @@ class AutomaticInverseFindingTests < ActiveRecord::TestCase assert_equal monkey_reflection, man_reflection.inverse_of, "The man reflection's inverse should be the monkey reflection" end + def test_has_many_and_belongs_to_should_find_inverse_automatically_for_model_in_module + account_reflection = Admin::Account.reflect_on_association(:users) + user_reflection = Admin::User.reflect_on_association(:account) + + assert_respond_to account_reflection, :has_inverse? + assert account_reflection.has_inverse?, "The Admin::Account reflection should have an inverse" + assert_equal user_reflection, account_reflection.inverse_of, "The Admin::Account reflection's inverse should be the Admin::User reflection" + end + def test_has_one_and_belongs_to_should_find_inverse_automatically car_reflection = Car.reflect_on_association(:bulb) bulb_reflection = Bulb.reflect_on_association(:car) diff --git a/activerecord/test/cases/associations_test.rb b/activerecord/test/cases/associations_test.rb index c6769edcbf..72963fd56c 100644 --- a/activerecord/test/cases/associations_test.rb +++ b/activerecord/test/cases/associations_test.rb @@ -264,6 +264,11 @@ class AssociationProxyTest < ActiveRecord::TestCase end end + test "first! works on loaded associations" do + david = authors(:david) + assert_equal david.posts.first, david.posts.reload.first! + end + def test_reset_unloads_target david = authors(:david) david.posts.reload diff --git a/activerecord/test/cases/date_time_test.rb b/activerecord/test/cases/date_time_test.rb index c0491bbee5..330232cee2 100644 --- a/activerecord/test/cases/date_time_test.rb +++ b/activerecord/test/cases/date_time_test.rb @@ -3,6 +3,8 @@ require 'models/topic' require 'models/task' class DateTimeTest < ActiveRecord::TestCase + include InTimeZone + def test_saves_both_date_and_time with_env_tz 'America/New_York' do with_timezone_config default: :utc do @@ -29,6 +31,14 @@ class DateTimeTest < ActiveRecord::TestCase assert_nil task.ending end + def test_assign_bad_date_time_with_timezone + in_time_zone "Pacific Time (US & Canada)" do + task = Task.new + task.starting = '2014-07-01T24:59:59GMT' + assert_nil task.starting + end + end + def test_assign_empty_date topic = Topic.new topic.last_read = '' @@ -40,4 +50,12 @@ class DateTimeTest < ActiveRecord::TestCase topic.bonus_time = '' assert_nil topic.bonus_time end + + def test_assign_in_local_timezone + now = DateTime.now + with_timezone_config default: :local do + task = Task.new starting: now + assert now, task.starting + end + end end diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb index eb9b1a2d74..98cf60a8c4 100644 --- a/activerecord/test/cases/dirty_test.rb +++ b/activerecord/test/cases/dirty_test.rb @@ -688,7 +688,14 @@ class DirtyTest < ActiveRecord::TestCase serialize :data end - klass.create!(data: "foo") + binary = klass.create!(data: "\\\\foo") + + assert_not binary.changed? + + binary.data = binary.data.dup + + assert_not binary.changed? + binary = klass.last assert_not binary.changed? @@ -698,6 +705,21 @@ class DirtyTest < ActiveRecord::TestCase assert binary.changed? end + test "attribute_changed? doesn't compute in-place changes for unrelated attributes" do + test_type_class = Class.new(ActiveRecord::Type::Value) do + define_method(:changed_in_place?) do |*| + raise + end + end + klass = Class.new(ActiveRecord::Base) do + self.table_name = 'people' + attribute :foo, test_type_class.new + end + + model = klass.new(first_name: "Jim") + assert model.first_name_changed? + end + private def with_partial_writes(klass, on = true) old = klass.partial_writes? diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index 5c98be342f..02dc5d3ad3 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -15,9 +15,11 @@ require 'models/customer' require 'models/toy' require 'models/matey' require 'models/dog' +require 'models/car' +require 'models/tyre' class FinderTest < ActiveRecord::TestCase - fixtures :companies, :topics, :entrants, :developers, :developers_projects, :posts, :comments, :accounts, :authors, :customers, :categories, :categorizations + fixtures :companies, :topics, :entrants, :developers, :developers_projects, :posts, :comments, :accounts, :authors, :customers, :categories, :categorizations, :cars def test_find_by_id_with_hash assert_raises(ActiveRecord::StatementInvalid) do @@ -1101,6 +1103,26 @@ class FinderTest < ActiveRecord::TestCase end end + test "find on a scope does not perform statement caching" do + honda = cars(:honda) + zyke = cars(:zyke) + tyre = honda.tyres.create! + tyre2 = zyke.tyres.create! + + assert_equal tyre, honda.tyres.custom_find(tyre.id) + assert_equal tyre2, zyke.tyres.custom_find(tyre2.id) + end + + test "find_by on a scope does not perform statement caching" do + honda = cars(:honda) + zyke = cars(:zyke) + tyre = honda.tyres.create! + tyre2 = zyke.tyres.create! + + assert_equal tyre, honda.tyres.custom_find_by(id: tyre.id) + assert_equal tyre2, zyke.tyres.custom_find_by(id: tyre2.id) + end + protected def bind(statement, *vars) if vars.first.is_a?(Hash) diff --git a/activerecord/test/cases/inheritance_test.rb b/activerecord/test/cases/inheritance_test.rb index 0338669016..fe6323ab02 100644 --- a/activerecord/test/cases/inheritance_test.rb +++ b/activerecord/test/cases/inheritance_test.rb @@ -304,7 +304,7 @@ class InheritanceTest < ActiveRecord::TestCase def test_eager_load_belongs_to_primary_key_quoting con = Account.connection - assert_sql(/#{con.quote_table_name('companies')}.#{con.quote_column_name('id')} IN \(1\)/) do + assert_sql(/#{con.quote_table_name('companies')}.#{con.quote_column_name('id')} = 1/) do Account.all.merge!(:includes => :firm).find(1) end end diff --git a/activerecord/test/cases/locking_test.rb b/activerecord/test/cases/locking_test.rb index 5a4b1fb919..ee43f07dd7 100644 --- a/activerecord/test/cases/locking_test.rb +++ b/activerecord/test/cases/locking_test.rb @@ -33,8 +33,6 @@ class OptimisticLockingTest < ActiveRecord::TestCase p1 = Person.find(1) assert_equal 0, p1.lock_version - Person.expects(:quote_value).with(0, Person.columns_hash[Person.locking_column]).returns('0').once - p1.first_name = 'anika2' p1.save! diff --git a/activerecord/test/cases/migration/references_foreign_key_test.rb b/activerecord/test/cases/migration/references_foreign_key_test.rb new file mode 100644 index 0000000000..bba8897a0d --- /dev/null +++ b/activerecord/test/cases/migration/references_foreign_key_test.rb @@ -0,0 +1,101 @@ +require 'cases/helper' + +if ActiveRecord::Base.connection.supports_foreign_keys? +module ActiveRecord + class Migration + class ReferencesForeignKeyTest < ActiveRecord::TestCase + setup do + @connection = ActiveRecord::Base.connection + @connection.create_table(:testing_parents, force: true) + end + + teardown do + @connection.execute("drop table if exists testings") + @connection.execute("drop table if exists testing_parents") + end + + test "foreign keys can be created with the table" do + @connection.create_table :testings do |t| + t.references :testing_parent, foreign_key: true + end + + fk = @connection.foreign_keys("testings").first + assert_equal "testings", fk.from_table + assert_equal "testing_parents", fk.to_table + end + + test "no foreign key is created by default" do + @connection.create_table :testings do |t| + t.references :testing_parent + end + + assert_equal [], @connection.foreign_keys("testings") + end + + test "options hash can be passed" do + @connection.change_table :testing_parents do |t| + t.integer :other_id + t.index :other_id, unique: true + end + @connection.create_table :testings do |t| + t.references :testing_parent, foreign_key: { primary_key: :other_id } + end + + fk = @connection.foreign_keys("testings").find { |k| k.to_table == "testing_parents" } + assert_equal "other_id", fk.primary_key + end + + test "foreign keys cannot be added to polymorphic relations when creating the table" do + @connection.create_table :testings do |t| + assert_raises(ArgumentError) do + t.references :testing_parent, polymorphic: true, foreign_key: true + end + end + end + + test "foreign keys can be created while changing the table" do + @connection.create_table :testings + @connection.change_table :testings do |t| + t.references :testing_parent, foreign_key: true + end + + fk = @connection.foreign_keys("testings").first + assert_equal "testings", fk.from_table + assert_equal "testing_parents", fk.to_table + end + + test "foreign keys are not added by default when changing the table" do + @connection.create_table :testings + @connection.change_table :testings do |t| + t.references :testing_parent + end + + assert_equal [], @connection.foreign_keys("testings") + end + + test "foreign keys accept options when changing the table" do + @connection.change_table :testing_parents do |t| + t.integer :other_id + t.index :other_id, unique: true + end + @connection.create_table :testings + @connection.change_table :testings do |t| + t.references :testing_parent, foreign_key: { primary_key: :other_id } + end + + fk = @connection.foreign_keys("testings").find { |k| k.to_table == "testing_parents" } + assert_equal "other_id", fk.primary_key + end + + test "foreign keys cannot be added to polymorphic relations when changing the table" do + @connection.create_table :testings + @connection.change_table :testings do |t| + assert_raises(ArgumentError) do + t.references :testing_parent, polymorphic: true, foreign_key: true + end + end + end + end + end +end +end diff --git a/activerecord/test/cases/persistence_test.rb b/activerecord/test/cases/persistence_test.rb index 6fc4731f01..d6816041bc 100644 --- a/activerecord/test/cases/persistence_test.rb +++ b/activerecord/test/cases/persistence_test.rb @@ -252,8 +252,10 @@ class PersistenceTest < ActiveRecord::TestCase def test_create_columns_not_equal_attributes topic = Topic.instantiate( - 'title' => 'Another New Topic', - 'does_not_exist' => 'test' + 'attributes' => { + 'title' => 'Another New Topic', + 'does_not_exist' => 'test' + } ) assert_nothing_raised { topic.save } end @@ -878,4 +880,35 @@ class PersistenceTest < ActiveRecord::TestCase assert_equal "Welcome to the weblog", post.title assert_not post.new_record? end + + class SaveTest < ActiveRecord::TestCase + self.use_transactional_fixtures = false + + def test_save_touch_false + widget = Class.new(ActiveRecord::Base) do + connection.create_table :widgets, force: true do |t| + t.string :name + t.timestamps null: false + end + + self.table_name = :widgets + end + + instance = widget.create!({ + name: 'Bob', + created_at: 1.day.ago, + updated_at: 1.day.ago + }) + + created_at = instance.created_at + updated_at = instance.updated_at + + instance.name = 'Barb' + instance.save!(touch: false) + assert_equal instance.created_at, created_at + assert_equal instance.updated_at, updated_at + ensure + ActiveRecord::Base.connection.drop_table :widgets + end + end end diff --git a/activerecord/test/cases/pooled_connections_test.rb b/activerecord/test/cases/pooled_connections_test.rb index 98888150a8..287a3f33ea 100644 --- a/activerecord/test/cases/pooled_connections_test.rb +++ b/activerecord/test/cases/pooled_connections_test.rb @@ -35,6 +35,22 @@ class PooledConnectionsTest < ActiveRecord::TestCase end end + def checkout_checkin_connections_loop(pool_size, loops) + ActiveRecord::Base.establish_connection(@connection.merge({:pool => pool_size, :checkout_timeout => 0.5})) + @connection_count = 0 + @timed_out = 0 + loops.times do + begin + conn = ActiveRecord::Base.connection_pool.checkout + ActiveRecord::Base.connection_pool.checkin conn + @connection_count += 1 + ActiveRecord::Base.connection.tables + rescue ActiveRecord::ConnectionTimeoutError + @timed_out += 1 + end + end + end + def test_pooled_connection_checkin_one checkout_checkin_connections 1, 2 assert_equal 2, @connection_count @@ -42,6 +58,20 @@ class PooledConnectionsTest < ActiveRecord::TestCase assert_equal 1, ActiveRecord::Base.connection_pool.connections.size end + def test_pooled_connection_checkin_two + checkout_checkin_connections_loop 2, 3 + assert_equal 3, @connection_count + assert_equal 0, @timed_out + assert_equal 2, ActiveRecord::Base.connection_pool.connections.size + end + + def test_pooled_connection_remove + ActiveRecord::Base.establish_connection(@connection.merge({:pool => 2, :checkout_timeout => 0.5})) + old_connection = ActiveRecord::Base.connection + extra_connection = ActiveRecord::Base.connection_pool.checkout + ActiveRecord::Base.connection_pool.remove(extra_connection) + assert_equal ActiveRecord::Base.connection, old_connection + end private diff --git a/activerecord/test/cases/primary_keys_test.rb b/activerecord/test/cases/primary_keys_test.rb index f19a6ea5e3..751eccc015 100644 --- a/activerecord/test/cases/primary_keys_test.rb +++ b/activerecord/test/cases/primary_keys_test.rb @@ -1,4 +1,5 @@ require "cases/helper" +require 'support/schema_dumping_helper' require 'models/topic' require 'models/reply' require 'models/subscriber' @@ -195,6 +196,37 @@ class PrimaryKeyWithNoConnectionTest < ActiveRecord::TestCase end end +class PrimaryKeyAnyTypeTest < ActiveRecord::TestCase + include SchemaDumpingHelper + + self.use_transactional_fixtures = false + + class Barcode < ActiveRecord::Base + end + + setup do + @connection = ActiveRecord::Base.connection + @connection.create_table(:barcodes, primary_key: "code", id: :string, limit: 42, force: true) + end + + teardown do + @connection.execute("DROP TABLE IF EXISTS barcodes") + end + + def test_any_type_primary_key + assert_equal "code", Barcode.primary_key + + column_type = Barcode.type_for_attribute(Barcode.primary_key) + assert_equal :string, column_type.type + assert_equal 42, column_type.limit + end + + test "schema dump primary key includes type and options" do + schema = dump_table_schema "barcodes" + assert_match %r{create_table "barcodes", primary_key: "code", id: :string, limit: 42}, schema + end +end + if current_adapter?(:MysqlAdapter, :Mysql2Adapter) class PrimaryKeyWithAnsiQuotesTest < ActiveRecord::TestCase self.use_transactional_fixtures = false @@ -209,8 +241,10 @@ if current_adapter?(:MysqlAdapter, :Mysql2Adapter) end end -if current_adapter?(:PostgreSQLAdapter) +if current_adapter?(:PostgreSQLAdapter, :MysqlAdapter, :Mysql2Adapter) class PrimaryKeyBigSerialTest < ActiveRecord::TestCase + include SchemaDumpingHelper + self.use_transactional_fixtures = false class Widget < ActiveRecord::Base @@ -218,19 +252,35 @@ if current_adapter?(:PostgreSQLAdapter) setup do @connection = ActiveRecord::Base.connection - @connection.create_table(:widgets, id: :bigserial) { |t| } + if current_adapter?(:PostgreSQLAdapter) + @connection.create_table(:widgets, id: :bigserial, force: true) + else + @connection.create_table(:widgets, id: :bigint, force: true) + end end teardown do - @connection.drop_table :widgets + @connection.execute("DROP TABLE IF EXISTS widgets") end - def test_bigserial_primary_key - assert_equal "id", Widget.primary_key - assert_equal :integer, Widget.columns_hash[Widget.primary_key].type + test "primary key column type with bigserial" do + column_type = Widget.type_for_attribute(Widget.primary_key) + assert_equal :integer, column_type.type + assert_equal 8, column_type.limit + end + test "primary key with bigserial are automatically numbered" do widget = Widget.create! assert_not_nil widget.id end + + test "schema dump primary key with bigserial" do + schema = dump_table_schema "widgets" + if current_adapter?(:PostgreSQLAdapter) + assert_match %r{create_table "widgets", id: :bigserial}, schema + else + assert_match %r{create_table "widgets", id: :bigint}, schema + end + end end end diff --git a/activerecord/test/cases/query_cache_test.rb b/activerecord/test/cases/query_cache_test.rb index 9d89d6a1e8..744f9edc47 100644 --- a/activerecord/test/cases/query_cache_test.rb +++ b/activerecord/test/cases/query_cache_test.rb @@ -212,6 +212,38 @@ class QueryCacheTest < ActiveRecord::TestCase ensure ActiveRecord::Base.configurations = conf end + + def test_query_cache_doesnt_leak_cached_results_of_rolled_back_queries + ActiveRecord::Base.connection.enable_query_cache! + post = Post.first + + Post.transaction do + post.update_attributes(title: 'rollback') + assert_equal 1, Post.where(title: 'rollback').to_a.count + raise ActiveRecord::Rollback + end + + assert_equal 0, Post.where(title: 'rollback').to_a.count + + ActiveRecord::Base.connection.uncached do + assert_equal 0, Post.where(title: 'rollback').to_a.count + end + + begin + Post.transaction do + post.update_attributes(title: 'rollback') + assert_equal 1, Post.where(title: 'rollback').to_a.count + raise 'broken' + end + rescue Exception + end + + assert_equal 0, Post.where(title: 'rollback').to_a.count + + ActiveRecord::Base.connection.uncached do + assert_equal 0, Post.where(title: 'rollback').to_a.count + end + end end class QueryCacheExpiryTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/reaper_test.rb b/activerecord/test/cases/reaper_test.rb index f52fd22489..cccfc6774e 100644 --- a/activerecord/test/cases/reaper_test.rb +++ b/activerecord/test/cases/reaper_test.rb @@ -60,7 +60,7 @@ module ActiveRecord def test_connection_pool_starts_reaper spec = ActiveRecord::Base.connection_pool.spec.dup - spec.config[:reaping_frequency] = 0.0001 + spec.config[:reaping_frequency] = '0.0001' pool = ConnectionPool.new spec diff --git a/activerecord/test/cases/relation/mutation_test.rb b/activerecord/test/cases/relation/mutation_test.rb index 4c94c2fd0d..2443f10269 100644 --- a/activerecord/test/cases/relation/mutation_test.rb +++ b/activerecord/test/cases/relation/mutation_test.rb @@ -25,7 +25,7 @@ module ActiveRecord end def relation - @relation ||= Relation.new FakeKlass.new('posts'), Post.arel_table + @relation ||= Relation.new FakeKlass.new('posts'), Post.arel_table, Post.predicate_builder end (Relation::MULTI_VALUE_METHODS - [:references, :extending, :order, :unscope, :select]).each do |method| @@ -99,7 +99,7 @@ module ActiveRecord end test '#reorder!' do - relation = self.relation.order('foo') + @relation = self.relation.order('foo') assert relation.reorder!('bar').equal?(relation) assert_equal ['bar'], relation.order_values @@ -116,7 +116,7 @@ module ActiveRecord end test 'reverse_order!' do - relation = Post.order('title ASC, comments_count DESC') + @relation = Post.order('title ASC, comments_count DESC') relation.reverse_order! diff --git a/activerecord/test/cases/relation/predicate_builder_test.rb b/activerecord/test/cases/relation/predicate_builder_test.rb index 4057835688..0cc081fced 100644 --- a/activerecord/test/cases/relation/predicate_builder_test.rb +++ b/activerecord/test/cases/relation/predicate_builder_test.rb @@ -4,11 +4,13 @@ require 'models/topic' module ActiveRecord class PredicateBuilderTest < ActiveRecord::TestCase def test_registering_new_handlers - PredicateBuilder.register_handler(Regexp, proc do |column, value| + Topic.predicate_builder.register_handler(Regexp, proc do |column, value| Arel::Nodes::InfixOperation.new('~', column, Arel.sql(value.source)) end) assert_match %r{["`]topics["`].["`]title["`] ~ rails}i, Topic.where(title: /rails/).to_sql + ensure + Topic.reset_column_information end end end diff --git a/activerecord/test/cases/relation_test.rb b/activerecord/test/cases/relation_test.rb index 3280945d09..f7cb471984 100644 --- a/activerecord/test/cases/relation_test.rb +++ b/activerecord/test/cases/relation_test.rb @@ -23,19 +23,19 @@ module ActiveRecord end def test_construction - relation = Relation.new FakeKlass, :b + relation = Relation.new(FakeKlass, :b, nil) assert_equal FakeKlass, relation.klass assert_equal :b, relation.table assert !relation.loaded, 'relation is not loaded' end def test_responds_to_model_and_returns_klass - relation = Relation.new FakeKlass, :b + relation = Relation.new(FakeKlass, :b, nil) assert_equal FakeKlass, relation.model end def test_initialize_single_values - relation = Relation.new FakeKlass, :b + relation = Relation.new(FakeKlass, :b, nil) (Relation::SINGLE_VALUE_METHODS - [:create_with]).each do |method| assert_nil relation.send("#{method}_value"), method.to_s end @@ -43,19 +43,19 @@ module ActiveRecord end def test_multi_value_initialize - relation = Relation.new FakeKlass, :b + relation = Relation.new(FakeKlass, :b, nil) Relation::MULTI_VALUE_METHODS.each do |method| assert_equal [], relation.send("#{method}_values"), method.to_s end end def test_extensions - relation = Relation.new FakeKlass, :b + relation = Relation.new(FakeKlass, :b, nil) assert_equal [], relation.extensions end def test_empty_where_values_hash - relation = Relation.new FakeKlass, :b + relation = Relation.new(FakeKlass, :b, nil) assert_equal({}, relation.where_values_hash) relation.where! :hello @@ -63,19 +63,20 @@ module ActiveRecord end def test_has_values - relation = Relation.new Post, Post.arel_table + relation = Relation.new(Post, Post.arel_table, Post.predicate_builder) relation.where! relation.table[:id].eq(10) assert_equal({:id => 10}, relation.where_values_hash) end def test_values_wrong_table - relation = Relation.new Post, Post.arel_table + relation = Relation.new(Post, Post.arel_table, Post.predicate_builder) relation.where! Comment.arel_table[:id].eq(10) assert_equal({}, relation.where_values_hash) end def test_tree_is_not_traversed - relation = Relation.new Post, Post.arel_table + relation = Relation.new(Post, Post.arel_table, Post.predicate_builder) + # FIXME: Remove the Arel::Nodes::Quoted in Rails 5.1 left = relation.table[:id].eq(10) right = relation.table[:id].eq(10) combine = left.and right @@ -84,24 +85,25 @@ module ActiveRecord end def test_table_name_delegates_to_klass - relation = Relation.new FakeKlass.new('posts'), :b + relation = Relation.new(FakeKlass.new('posts'), :b, Post.predicate_builder) assert_equal 'posts', relation.table_name end def test_scope_for_create - relation = Relation.new FakeKlass, :b + relation = Relation.new(FakeKlass, :b, nil) assert_equal({}, relation.scope_for_create) end def test_create_with_value - relation = Relation.new Post, Post.arel_table + relation = Relation.new(Post, Post.arel_table, Post.predicate_builder) hash = { :hello => 'world' } relation.create_with_value = hash assert_equal hash, relation.scope_for_create end def test_create_with_value_with_wheres - relation = Relation.new Post, Post.arel_table + relation = Relation.new(Post, Post.arel_table, Post.predicate_builder) + # FIXME: Remove the Arel::Nodes::Quoted in Rails 5.1 relation.where! relation.table[:id].eq(10) relation.create_with_value = {:hello => 'world'} assert_equal({:hello => 'world', :id => 10}, relation.scope_for_create) @@ -109,9 +111,10 @@ module ActiveRecord # FIXME: is this really wanted or expected behavior? def test_scope_for_create_is_cached - relation = Relation.new Post, Post.arel_table + relation = Relation.new(Post, Post.arel_table, Post.predicate_builder) assert_equal({}, relation.scope_for_create) + # FIXME: Remove the Arel::Nodes::Quoted in Rails 5.1 relation.where! relation.table[:id].eq(10) assert_equal({}, relation.scope_for_create) @@ -126,31 +129,31 @@ module ActiveRecord end def test_empty_eager_loading? - relation = Relation.new FakeKlass, :b + relation = Relation.new(FakeKlass, :b, nil) assert !relation.eager_loading? end def test_eager_load_values - relation = Relation.new FakeKlass, :b + relation = Relation.new(FakeKlass, :b, nil) relation.eager_load! :b assert relation.eager_loading? end def test_references_values - relation = Relation.new FakeKlass, :b + relation = Relation.new(FakeKlass, :b, nil) assert_equal [], relation.references_values relation = relation.references(:foo).references(:omg, :lol) assert_equal ['foo', 'omg', 'lol'], relation.references_values end def test_references_values_dont_duplicate - relation = Relation.new FakeKlass, :b + relation = Relation.new(FakeKlass, :b, nil) relation = relation.references(:foo).references(:foo) assert_equal ['foo'], relation.references_values end test 'merging a hash into a relation' do - relation = Relation.new FakeKlass, :b + relation = Relation.new(FakeKlass, :b, nil) relation = relation.merge where: :lol, readonly: true assert_equal [:lol], relation.where_values @@ -158,7 +161,7 @@ module ActiveRecord end test 'merging an empty hash into a relation' do - assert_equal [], Relation.new(FakeKlass, :b).merge({}).where_values + assert_equal [], Relation.new(FakeKlass, :b, nil).merge({}).where_values end test 'merging a hash with unknown keys raises' do @@ -166,7 +169,7 @@ module ActiveRecord end test '#values returns a dup of the values' do - relation = Relation.new(FakeKlass, :b).where! :foo + relation = Relation.new(FakeKlass, :b, nil).where! :foo values = relation.values values[:where] = nil @@ -174,12 +177,12 @@ module ActiveRecord end test 'relations can be created with a values hash' do - relation = Relation.new(FakeKlass, :b, where: [:foo]) + relation = Relation.new(FakeKlass, :b, nil, where: [:foo]) assert_equal [:foo], relation.where_values end test 'merging a single where value' do - relation = Relation.new(FakeKlass, :b) + relation = Relation.new(FakeKlass, :b, nil) relation.merge!(where: :foo) assert_equal [:foo], relation.where_values end @@ -192,13 +195,13 @@ module ActiveRecord end end - relation = Relation.new(klass, :b) + relation = Relation.new(klass, :b, nil) relation.merge!(where: ['foo = ?', 'bar']) assert_equal ['foo = bar'], relation.where_values end def test_merging_readonly_false - relation = Relation.new FakeKlass, :b + relation = Relation.new(FakeKlass, :b, nil) readonly_false_relation = relation.readonly(false) # test merging in both directions assert_equal false, relation.merge(readonly_false_relation).readonly_value diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index 3a0398d08d..fb9258c038 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -22,6 +22,11 @@ class RelationTest < ActiveRecord::TestCase fixtures :authors, :topics, :entrants, :developers, :companies, :developers_projects, :accounts, :categories, :categorizations, :posts, :comments, :tags, :taggings, :cars, :minivans + class TopicWithCallbacks < ActiveRecord::Base + self.table_name = :topics + before_update { |topic| topic.author_name = 'David' if topic.author_name.blank? } + end + def test_do_not_double_quote_string_id van = Minivan.last assert van @@ -353,7 +358,7 @@ class RelationTest < ActiveRecord::TestCase def test_null_relation_calculations_methods assert_no_queries(ignore_none: false) do assert_equal 0, Developer.none.count - assert_equal 0, Developer.none.calculate(:count, nil, {}) + assert_equal 0, Developer.none.calculate(:count, nil) assert_equal nil, Developer.none.calculate(:average, 'salary') end end @@ -1429,6 +1434,19 @@ class RelationTest < ActiveRecord::TestCase assert_equal posts(:welcome), comments(:greetings).post end + def test_update_on_relation + topic1 = TopicWithCallbacks.create! title: 'arel', author_name: nil + topic2 = TopicWithCallbacks.create! title: 'activerecord', author_name: nil + topics = TopicWithCallbacks.where(id: [topic1.id, topic2.id]) + topics.update(title: 'adequaterecord') + + assert_equal 'adequaterecord', topic1.reload.title + assert_equal 'adequaterecord', topic2.reload.title + # Testing that the before_update callbacks have run + assert_equal 'David', topic1.reload.author_name + assert_equal 'David', topic2.reload.author_name + end + def test_distinct tag1 = Tag.create(:name => 'Foo') tag2 = Tag.create(:name => 'Foo') @@ -1663,7 +1681,9 @@ class RelationTest < ActiveRecord::TestCase test 'using a custom table affects the wheres' do table_alias = Post.arel_table.alias('omg_posts') - relation = ActiveRecord::Relation.new Post, table_alias + table_metadata = ActiveRecord::TableMetadata.new(Post, table_alias) + predicate_builder = ActiveRecord::PredicateBuilder.new(table_metadata) + relation = ActiveRecord::Relation.new(Post, table_alias, predicate_builder) relation.where!(:foo => "bar") node = relation.arel.constraints.first.grep(Arel::Attributes::Attribute).first diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb index a094136766..57b7346503 100644 --- a/activerecord/test/cases/schema_dumper_test.rb +++ b/activerecord/test/cases/schema_dumper_test.rb @@ -232,6 +232,13 @@ class SchemaDumperTest < ActiveRecord::TestCase end end + if mysql_56? + def test_schema_dump_includes_datetime_precision + output = standard_dump + assert_match %r{t.datetime\s+"written_on",\s+precision: 6$}, output + end + end + def test_schema_dump_includes_decimal_options output = dump_all_table_schema([/^[^n]/]) assert_match %r{precision: 3,[[:space:]]+scale: 2,[[:space:]]+default: 2.78}, output diff --git a/activerecord/test/cases/serialized_attribute_test.rb b/activerecord/test/cases/serialized_attribute_test.rb index 56a0e92e1d..c261a5b1c0 100644 --- a/activerecord/test/cases/serialized_attribute_test.rb +++ b/activerecord/test/cases/serialized_attribute_test.rb @@ -256,4 +256,18 @@ class SerializedAttributeTest < ActiveRecord::TestCase assert_equal("second", t.content) assert_equal("second", t.reload.content) end + + def test_nil_is_not_changed_when_serialized_with_a_class + Topic.serialize(:content, Array) + + topic = Topic.new(content: nil) + + assert_not topic.content_changed? + end + + def test_classes_without_no_arg_constructors_are_not_supported + assert_raises(ArgumentError) do + Topic.serialize(:content, Regexp) + end + end end diff --git a/activerecord/test/cases/transactions_test.rb b/activerecord/test/cases/transactions_test.rb index cf50bd4ddb..0bbb4bb79e 100644 --- a/activerecord/test/cases/transactions_test.rb +++ b/activerecord/test/cases/transactions_test.rb @@ -562,6 +562,21 @@ class TransactionTest < ActiveRecord::TestCase assert !@second.destroyed?, 'not destroyed' end + def test_restore_frozen_state_after_double_destroy + topic = Topic.create + reply = topic.replies.create + + Topic.transaction do + topic.destroy # calls #destroy on reply (since dependent: destroy) + reply.destroy + + raise ActiveRecord::Rollback + end + + assert_not reply.frozen? + assert_not topic.frozen? + end + def test_sqlite_add_column_in_transaction return true unless current_adapter?(:SQLite3Adapter) diff --git a/activerecord/test/cases/type/decimal_test.rb b/activerecord/test/cases/type/decimal_test.rb index da30de373e..34ed1d7b19 100644 --- a/activerecord/test/cases/type/decimal_test.rb +++ b/activerecord/test/cases/type/decimal_test.rb @@ -15,6 +15,11 @@ module ActiveRecord assert_equal BigDecimal.new("123.0"), type.type_cast_from_user(123.0) end + def test_type_cast_from_float_with_unspecified_precision + type = Decimal.new + assert_equal 22.68.to_d, type.type_cast_from_user(22.68) + end + def test_type_cast_decimal_from_rational_with_precision type = Decimal.new(precision: 2) assert_equal BigDecimal("0.33"), type.type_cast_from_user(Rational(1, 3)) @@ -33,6 +38,14 @@ module ActiveRecord type = Decimal.new assert_equal BigDecimal("1"), type.type_cast_from_user(value) end + + def test_changed? + type = Decimal.new + + assert type.changed?(5.0, 5.0, '5.0wibble') + assert_not type.changed?(5.0, 5.0, '5.0') + assert_not type.changed?(-5.0, -5.0, '-5.0') + end end end end diff --git a/activerecord/test/cases/type/integer_test.rb b/activerecord/test/cases/type/integer_test.rb index 5942f77e18..ff956b7680 100644 --- a/activerecord/test/cases/type/integer_test.rb +++ b/activerecord/test/cases/type/integer_test.rb @@ -41,12 +41,20 @@ module ActiveRecord assert_nil type.type_cast_from_user(1.0/0.0) end + test "casting booleans for database" do + type = Type::Integer.new + assert_equal 1, type.type_cast_for_database(true) + assert_equal 0, type.type_cast_for_database(false) + end + test "changed?" do type = Type::Integer.new assert type.changed?(5, 5, '5wibble') assert_not type.changed?(5, 5, '5') assert_not type.changed?(5, 5, '5.0') + assert_not type.changed?(-5, -5, '-5') + assert_not type.changed?(-5, -5, '-5.0') assert_not type.changed?(nil, nil, nil) end diff --git a/activerecord/test/cases/validations/length_validation_test.rb b/activerecord/test/cases/validations/length_validation_test.rb index 4a92da38ce..2c0e282761 100644 --- a/activerecord/test/cases/validations/length_validation_test.rb +++ b/activerecord/test/cases/validations/length_validation_test.rb @@ -2,6 +2,7 @@ require "cases/helper" require 'models/owner' require 'models/pet' +require 'models/person' class LengthValidationTest < ActiveRecord::TestCase fixtures :owners @@ -44,4 +45,21 @@ class LengthValidationTest < ActiveRecord::TestCase assert o.valid? end end + + def test_validates_size_of_reprects_records_marked_for_destruction + assert_nothing_raised { Owner.validates_size_of :pets, minimum: 1 } + owner = Owner.new + assert_not owner.save + assert owner.errors[:pets].any? + pet = owner.pets.build + assert owner.valid? + assert owner.save + + pet_count = Pet.count + assert_not owner.update_attributes pets_attributes: [ {_destroy: 1, id: pet.id} ] + assert_not owner.valid? + assert owner.errors[:pets].any? + assert_equal pet_count, Pet.count + end + end diff --git a/activerecord/test/models/company.rb b/activerecord/test/models/company.rb index 42f7fb4680..5a56616eb9 100644 --- a/activerecord/test/models/company.rb +++ b/activerecord/test/models/company.rb @@ -72,6 +72,7 @@ class Firm < Company # Oracle tests were failing because of that as the second fixture was selected has_one :account_using_primary_key, -> { order('id') }, :primary_key => "firm_id", :class_name => "Account" has_one :account_using_foreign_and_primary_keys, :foreign_key => "firm_name", :primary_key => "name", :class_name => "Account" + has_one :account_with_inexistent_foreign_key, class_name: 'Account', foreign_key: "inexistent" has_one :deletable_account, :foreign_key => "firm_id", :class_name => "Account", :dependent => :delete has_one :account_limit_500_with_hash_conditions, -> { where :credit_limit => 500 }, :foreign_key => "firm_id", :class_name => "Account" diff --git a/activerecord/test/models/organization.rb b/activerecord/test/models/organization.rb index 72e7bade68..f3e92f3067 100644 --- a/activerecord/test/models/organization.rb +++ b/activerecord/test/models/organization.rb @@ -8,5 +8,7 @@ class Organization < ActiveRecord::Base has_one :author, :primary_key => :name has_one :author_owned_essay_category, :through => :author, :source => :owned_essay_category + has_many :posts, :through => :author, :source => :posts + scope :clubs, -> { from('clubs') } end diff --git a/activerecord/test/models/owner.rb b/activerecord/test/models/owner.rb index 2e3a9a3681..cedb774b10 100644 --- a/activerecord/test/models/owner.rb +++ b/activerecord/test/models/owner.rb @@ -17,6 +17,8 @@ class Owner < ActiveRecord::Base after_commit :execute_blocks + accepts_nested_attributes_for :pets, allow_destroy: true + def blocks @blocks ||= [] end diff --git a/activerecord/test/models/tyre.rb b/activerecord/test/models/tyre.rb index bc3444aa7d..e50a21ca68 100644 --- a/activerecord/test/models/tyre.rb +++ b/activerecord/test/models/tyre.rb @@ -1,3 +1,11 @@ class Tyre < ActiveRecord::Base belongs_to :car + + def self.custom_find(id) + find(id) + end + + def self.custom_find_by(*args) + find_by(*args) + end end diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index a9c2b1d112..5907d6ef97 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -726,7 +726,7 @@ ActiveRecord::Schema.define do t.string :author_name t.string :author_email_address if mysql_56? - t.datetime :written_on, limit: 6 + t.datetime :written_on, precision: 6 else t.datetime :written_on end |