diff options
Diffstat (limited to 'activerecord/lib')
21 files changed, 173 insertions, 68 deletions
diff --git a/activerecord/lib/active_record/associations/builder/belongs_to.rb b/activerecord/lib/active_record/associations/builder/belongs_to.rb index aa43c34d86..e8e36e7cd0 100644 --- a/activerecord/lib/active_record/associations/builder/belongs_to.rb +++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb @@ -5,7 +5,7 @@ module ActiveRecord::Associations::Builder end def self.valid_options(options) - super + [:foreign_type, :polymorphic, :touch] + super + [:foreign_type, :polymorphic, :touch, :counter_cache] end def self.valid_dependent_options @@ -91,7 +91,13 @@ module ActiveRecord::Associations::Builder old_foreign_id = o.changed_attributes[foreign_key] if old_foreign_id - klass = o.association(name).klass + association = o.association(name) + reflection = association.reflection + if reflection.polymorphic? + klass = o.public_send("#{reflection.foreign_type}_was").constantize + else + klass = association.klass + end old_record = klass.find_by(klass.primary_key => old_foreign_id) if old_record diff --git a/activerecord/lib/active_record/associations/builder/singular_association.rb b/activerecord/lib/active_record/associations/builder/singular_association.rb index 66b03c0301..a6e8300642 100644 --- a/activerecord/lib/active_record/associations/builder/singular_association.rb +++ b/activerecord/lib/active_record/associations/builder/singular_association.rb @@ -3,7 +3,7 @@ module ActiveRecord::Associations::Builder class SingularAssociation < Association #:nodoc: def self.valid_options(options) - super + [:remote, :dependent, :counter_cache, :primary_key, :inverse_of] + super + [:remote, :dependent, :primary_key, :inverse_of] end def self.define_accessors(model, reflection) diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index 3924eec872..217fc52dd5 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -128,6 +128,16 @@ module ActiveRecord end end + def find_generated_attribute_method(method_name) # :nodoc: + klass = self + until klass == Base + gen_methods = klass.generated_attribute_methods + return gen_methods.instance_method(method_name) if method_defined_within?(method_name, gen_methods, Object) + klass = klass.superclass + end + nil + end + # Returns +true+ if +attribute+ is an attribute method and table exists, # +false+ otherwise. # @@ -163,7 +173,14 @@ module ActiveRecord def method_missing(method, *args, &block) # :nodoc: self.class.define_attribute_methods if respond_to_without_attributes?(method) - send(method, *args, &block) + # make sure to invoke the correct attribute method, as we might have gotten here via a `super` + # call in a overwritten attribute method + if attribute_method = self.class.find_generated_attribute_method(method) + # this is probably horribly slow, but should only happen at most once for a given AR class + attribute_method.bind(self).call(*args, &block) + else + send(method, *args, &block) + end else super end diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index e05e22ebb0..1d3ec75aa1 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -4,7 +4,7 @@ require 'active_support/benchmarkable' require 'active_support/dependencies' require 'active_support/descendants_tracker' require 'active_support/time' -require 'active_support/core_ext/class/attribute_accessors' +require 'active_support/core_ext/module/attribute_accessors' require 'active_support/core_ext/class/delegating_attributes' require 'active_support/core_ext/array/extract_options' require 'active_support/core_ext/hash/deep_merge' diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb index 128a9377c1..35f19f0bc0 100644 --- a/activerecord/lib/active_record/callbacks.rb +++ b/activerecord/lib/active_record/callbacks.rb @@ -128,7 +128,7 @@ module ActiveRecord # record.credit_card_number = decrypt(record.credit_card_number) # end # - # alias_method :after_find, :after_save + # alias_method :after_initialize, :after_save # # private # def encrypt(value) @@ -163,7 +163,7 @@ module ActiveRecord # record.send("#{@attribute}=", decrypt(record.send("#{@attribute}"))) # end # - # alias_method :after_find, :after_save + # alias_method :after_initialize, :after_save # # private # def encrypt(value) 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 06a2ddb8b7..1a323b6a9c 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -305,10 +305,6 @@ module ActiveRecord "DEFAULT VALUES" end - def case_sensitive_equality_operator - "=" - end - def limited_update_conditions(where_sql, quoted_table_name, quoted_primary_key) "WHERE #{quoted_primary_key} IN (SELECT #{quoted_primary_key} FROM #{quoted_table_name} #{where_sql})" end 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 8399232d73..adc23a6674 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb @@ -31,8 +31,8 @@ module ActiveRecord old, @query_cache_enabled = @query_cache_enabled, true yield ensure - clear_query_cache @query_cache_enabled = old + clear_query_cache unless @query_cache_enabled end def enable_query_cache! 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 6268ae4875..51a6929dab 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -690,7 +690,7 @@ module ActiveRecord column_type_sql else - type + type.to_s end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb index dcbc3466b2..e3270e734f 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -207,9 +207,14 @@ module ActiveRecord end def type_cast(value, column) - return super unless value == true || value == false - - value ? 1 : 0 + case value + when TrueClass + 1 + when FalseClass + 0 + else + super + end end # MySQL 4 technically support transaction isolation, but it is affected by a bug @@ -278,7 +283,7 @@ module ActiveRecord # REFERENTIAL INTEGRITY ==================================== - def disable_referential_integrity(&block) #:nodoc: + def disable_referential_integrity #:nodoc: old = select_value("SELECT @@FOREIGN_KEY_CHECKS") begin @@ -487,6 +492,14 @@ module ActiveRecord rename_table_indexes(table_name, new_name) end + def rename_index(table_name, old_name, new_name) + if (version[0] == 5 && version[1] >= 7) || version[0] >= 6 + execute "ALTER TABLE #{quote_table_name(table_name)} RENAME INDEX #{quote_table_name(old_name)} TO #{quote_table_name(new_name)}" + else + super + end + end + def change_column_default(table_name, column_name, default) column = column_for(table_name, column_name) change_column table_name, column_name, column.sql_type, :default => default 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 fa173d13a2..f349c37724 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb @@ -142,7 +142,7 @@ module ActiveRecord fields.each_with_index do |fname, i| ftype = result.ftype i fmod = result.fmod i - types[fname] = OID::TYPE_MAP.fetch(ftype, fmod) { |oid, mod| + types[fname] = type_map.fetch(ftype, fmod) { |oid, mod| warn "unknown OID: #{fname}(#{oid}) (#{sql})" OID::Identity.new } diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb index 6c5792954f..fae260a921 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb @@ -301,17 +301,15 @@ module ActiveRecord end end - TYPE_MAP = TypeMap.new # :nodoc: - - # When the PG adapter connects, the pg_type table is queried. The + # When the PG adapter connects, the pg_type table is queried. The # key of this hash maps to the `typname` column from the table. - # TYPE_MAP is then dynamically built with oids as the key and type + # type_map is then dynamically built with oids as the key and type # objects as values. NAMES = Hash.new { |h,k| # :nodoc: h[k] = OID::Identity.new } - # Register an OID type named +name+ with a typcasting object in + # Register an OID type named +name+ with a typecasting object in # +type+. +name+ should correspond to the `typname` column in # the `pg_type` table. def self.register_type(name, type) 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 5dc70a5ad1..571257f6dd 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -172,7 +172,7 @@ module ActiveRecord def columns(table_name) # Limit, precision, and scale are all handled by the superclass. column_definitions(table_name).map do |column_name, type, default, notnull, oid, fmod| - oid = OID::TYPE_MAP.fetch(oid.to_i, fmod.to_i) { + oid = type_map.fetch(oid.to_i, fmod.to_i) { OID::Identity.new } PostgreSQLColumn.new(column_name, default, oid, type, notnull == 'f') diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index adeb57d913..d23a24589c 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -565,7 +565,8 @@ module ActiveRecord raise "Your version of PostgreSQL (#{postgresql_version}) is too old, please upgrade!" end - initialize_type_map + @type_map = OID::TypeMap.new + initialize_type_map(type_map) @local_tz = execute('SHOW TIME ZONE', 'SCHEMA').first["TimeZone"] @use_insert_returning = @config.key?(:insert_returning) ? self.class.type_cast_config_to_boolean(@config[:insert_returning]) : true end @@ -738,39 +739,56 @@ module ActiveRecord private + def type_map + @type_map + end + def reload_type_map - OID::TYPE_MAP.clear - initialize_type_map + type_map.clear + initialize_type_map(type_map) end - def initialize_type_map + def add_oid(row, records_by_oid, type_map) + return type_map if type_map.key? row['type_elem'].to_i + + if OID.registered_type? row['typname'] + # this composite type is explicitly registered + vector = OID::NAMES[row['typname']] + else + # use the default for composite types + unless type_map.key? row['typelem'].to_i + add_oid records_by_oid[row['typelem']], records_by_oid, type_map + end + + vector = OID::Vector.new row['typdelim'], type_map[row['typelem'].to_i] + end + + type_map[row['oid'].to_i] = vector + type_map + end + + def initialize_type_map(type_map) result = execute('SELECT oid, typname, typelem, typdelim, typinput FROM pg_type', 'SCHEMA') leaves, nodes = result.partition { |row| row['typelem'] == '0' } # populate the leaf nodes leaves.find_all { |row| OID.registered_type? row['typname'] }.each do |row| - OID::TYPE_MAP[row['oid'].to_i] = OID::NAMES[row['typname']] + type_map[row['oid'].to_i] = OID::NAMES[row['typname']] end + records_by_oid = result.group_by { |row| row['oid'] } + arrays, nodes = nodes.partition { |row| row['typinput'] == 'array_in' } # populate composite types - nodes.find_all { |row| OID::TYPE_MAP.key? row['typelem'].to_i }.each do |row| - if OID.registered_type? row['typname'] - # this composite type is explicitly registered - vector = OID::NAMES[row['typname']] - else - # use the default for composite types - vector = OID::Vector.new row['typdelim'], OID::TYPE_MAP[row['typelem'].to_i] - end - - OID::TYPE_MAP[row['oid'].to_i] = vector + nodes.each do |row| + add_oid row, records_by_oid, type_map end # populate array types - arrays.find_all { |row| OID::TYPE_MAP.key? row['typelem'].to_i }.each do |row| - array = OID::Array.new OID::TYPE_MAP[row['typelem'].to_i] - OID::TYPE_MAP[row['oid'].to_i] = array + arrays.find_all { |row| type_map.key? row['typelem'].to_i }.each do |row| + array = OID::Array.new type_map[row['typelem'].to_i] + type_map[row['oid'].to_i] = array end end diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index 96b5686ae0..8808ad5a4c 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -126,7 +126,7 @@ module ActiveRecord # Returns an instance of <tt>Arel::Table</tt> loaded with the current table name. # # class Post < ActiveRecord::Base - # scope :published_and_commented, published.and(self.arel_table[:comments_count].gt(0)) + # scope :published_and_commented, -> { published.and(self.arel_table[:comments_count].gt(0)) } # end def arel_table @arel_table ||= Arel::Table.new(table_name, arel_engine) diff --git a/activerecord/lib/active_record/enum.rb b/activerecord/lib/active_record/enum.rb index 5fcc0382d8..b42039e722 100644 --- a/activerecord/lib/active_record/enum.rb +++ b/activerecord/lib/active_record/enum.rb @@ -26,12 +26,23 @@ module ActiveRecord # # Good practice is to let the first declared status be the default. # - # Finally, it's also possible to explicitly map the relation between attribute and database integer: + # Finally, it's also possible to explicitly map the relation between attribute and + # database integer with a +Hash+: # # class Conversation < ActiveRecord::Base # enum status: { active: 0, archived: 1 } # end # + # Note that when an +Array+ is used, the implicit mapping from the values to database + # integers is derived from the order the values appear in the array. In the example, + # <tt>:active</tt> is mapped to +0+ as it's the first elemet, and <tt>:archived</tt> + # is mapped to +1+. In general, the +i+-th element is mapped to <tt>i-1</tt> in the + # database. + # + # Therefore, once a value is added to the enum array, its position in the array must + # be maintained, and new values should only be added to the end of the array. To + # remove unused values, the explicit +Hash+ syntax should be used. + # # In rare circumstances you might need to access the mapping directly. # The mappings are exposed through a constant with the attributes name: # @@ -77,12 +88,13 @@ module ActiveRecord end end - def _enum_methods_module - @_enum_methods_module ||= begin - mod = Module.new - include mod - mod + private + def _enum_methods_module + @_enum_methods_module ||= begin + mod = Module.new + include mod + mod + end end - end end end diff --git a/activerecord/lib/active_record/fixture_set/file.rb b/activerecord/lib/active_record/fixture_set/file.rb index fbd7a4d891..8132310c91 100644 --- a/activerecord/lib/active_record/fixture_set/file.rb +++ b/activerecord/lib/active_record/fixture_set/file.rb @@ -38,7 +38,8 @@ module ActiveRecord end def render(content) - ERB.new(content).result + context = ActiveRecord::FixtureSet::RenderContext.create_subclass.new + ERB.new(content).result(context.get_binding) end # Validate our unmarshalled data. diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index 8601414209..d241788a9b 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -119,6 +119,23 @@ module ActiveRecord # perhaps you should reexamine whether your application is properly testable. Hence, dynamic values # in fixtures are to be considered a code smell. # + # Helper methods defined in a fixture will not be available in other fixtures, to prevent against + # unwanted inter-test dependencies. Methods used by multiple fixtures should be defined in a module + # that is included in <tt>ActiveRecord::FixtureSet.context_class</tt>. + # + # - define a helper method in `test_helper.rb` + # class FixtureFileHelpers + # def file_sha(path) + # Digest::SHA2.hexdigest(File.read(Rails.root.join('test/fixtures', path))) + # end + # end + # ActiveRecord::FixtureSet.context_class.send :include, FixtureFileHelpers + # + # - use the helper method in a fixture + # photo: + # name: kitten.png + # sha: <%= file_sha 'files/kitten.png' %> + # # = Transactional Fixtures # # Test cases can use begin+rollback to isolate their changes to the database instead of having to @@ -529,6 +546,11 @@ module ActiveRecord Zlib.crc32(label.to_s) % MAX_ID end + # Superclass for the evaluation contexts used by ERB fixtures. + def self.context_class + @context_class ||= Class.new + end + attr_reader :table_name, :name, :fixtures, :model_class, :config def initialize(connection, name, class_name, path, config = ActiveRecord::Base) @@ -989,3 +1011,13 @@ module ActiveRecord end end end + +class ActiveRecord::FixtureSet::RenderContext # :nodoc: + def self.create_subclass + Class.new ActiveRecord::FixtureSet.context_class do + def get_binding + binding() + end + end + end +end diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index a4247fb6f4..7d7e97e6c9 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -1,41 +1,48 @@ -require "active_support/core_ext/class/attribute_accessors" +require "active_support/core_ext/module/attribute_accessors" require 'set' module ActiveRecord + class MigrationError < ActiveRecordError#:nodoc: + def initialize(message = nil) + message = "\n\n#{message}\n\n" if message + super + end + end + # Exception that can be raised to stop migrations from going backwards. - class IrreversibleMigration < ActiveRecordError + class IrreversibleMigration < MigrationError end - class DuplicateMigrationVersionError < ActiveRecordError#:nodoc: + class DuplicateMigrationVersionError < MigrationError#:nodoc: def initialize(version) super("Multiple migrations have the version number #{version}") end end - class DuplicateMigrationNameError < ActiveRecordError#:nodoc: + class DuplicateMigrationNameError < MigrationError#:nodoc: def initialize(name) super("Multiple migrations have the name #{name}") end end - class UnknownMigrationVersionError < ActiveRecordError #:nodoc: + class UnknownMigrationVersionError < MigrationError #:nodoc: def initialize(version) super("No migration with version number #{version}") end end - class IllegalMigrationNameError < ActiveRecordError#:nodoc: + class IllegalMigrationNameError < MigrationError#:nodoc: def initialize(name) super("Illegal name for migration file: #{name}\n\t(only lower case letters, numbers, and '_' allowed)") end end - class PendingMigrationError < ActiveRecordError#:nodoc: + class PendingMigrationError < MigrationError#:nodoc: def initialize if defined?(Rails) - super("Migrations are pending; run 'bin/rake db:migrate RAILS_ENV=#{::Rails.env}' to resolve this issue.") + super("Migrations are pending. To resolve this issue, run:\n\n\tbin/rake db:migrate RAILS_ENV=#{::Rails.env}") else - super("Migrations are pending; run 'bin/rake db:migrate' to resolve this issue.") + super("Migrations are pending. To resolve this issue, run:\n\n\tbin/rake db:migrate") end end end diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index a73a140ef1..35fbad466e 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -10,9 +10,6 @@ module ActiveRecord # The +attributes+ parameter can be either a Hash or an Array of Hashes. These Hashes describe the # attributes on the objects that are to be created. # - # +create+ respects mass-assignment security and accepts either +:as+ or +:without_protection+ options - # in the +options+ parameter. - # # ==== Examples # # Create a single new object # User.create(first_name: 'Jamie') diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index d91d6367a3..3963f2b3e0 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -384,7 +384,7 @@ module ActiveRecord @records.last else @last ||= - if offset_value || limit_value + if limit_value to_a.last else reverse_order.limit(1).to_a.first diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index 38f37f5c8a..7ebe9dfec0 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -51,7 +51,15 @@ module ActiveRecord value = value.attributes[reflection.primary_key_column.name] unless value.nil? end - column = klass.columns_hash[attribute.to_s] + attribute_name = attribute.to_s + + # the attribute may be an aliased attribute + if klass.attribute_aliases[attribute_name] + attribute = klass.attribute_aliases[attribute_name] + attribute_name = attribute.to_s + end + + column = klass.columns_hash[attribute_name] value = klass.connection.type_cast(value, column) value = value.to_s[0, column.limit] if value && column.limit && column.text? @@ -166,11 +174,11 @@ module ActiveRecord # WHERE title = 'My Post' | # | # | # User 2 does the same thing and also - # | # infers that his title is unique. + # | # infers that their title is unique. # | SELECT * FROM comments # | WHERE title = 'My Post' # | - # # User 1 inserts his comment. | + # # User 1 inserts their comment. | # INSERT INTO comments | # (title, content) VALUES | # ('My Post', 'hi!') | @@ -196,7 +204,7 @@ module ActiveRecord # exception. You can either choose to let this error propagate (which # will result in the default Rails exception page being shown), or you # can catch it and restart the transaction (e.g. by telling the user - # that the title already exists, and asking him to re-enter the title). + # that the title already exists, and asking them to re-enter the title). # This technique is also known as # {optimistic concurrency control}[http://en.wikipedia.org/wiki/Optimistic_concurrency_control]. # |