diff options
author | aditya-kapoor <aditya.kapoor@vinsol.com> | 2013-09-01 18:05:04 +0530 |
---|---|---|
committer | aditya-kapoor <aditya.kapoor@vinsol.com> | 2013-09-01 18:05:04 +0530 |
commit | 6b9b0767bf480d53029afeb3f8e86f66e933fb41 (patch) | |
tree | 62a76c76b8fce484d8a258aceb14ac475f2c94a8 /activerecord | |
parent | b27c40637aaf2549f91faec8e87f6d4afa9a7380 (diff) | |
parent | ab0cbff07ebb5df8c9354fa2b3fd9984e039d2c6 (diff) | |
download | rails-6b9b0767bf480d53029afeb3f8e86f66e933fb41.tar.gz rails-6b9b0767bf480d53029afeb3f8e86f66e933fb41.tar.bz2 rails-6b9b0767bf480d53029afeb3f8e86f66e933fb41.zip |
Merge branch 'master' of github.com:lifo/docrails
Diffstat (limited to 'activerecord')
32 files changed, 337 insertions, 167 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 904ed0e362..c7928e503b 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,9 +1,29 @@ +* Fix PredicateBuilder so polymorhic association keys in `where` clause can + also accept not only `ActiveRecord::Base` direct descendances (decorated + models, for example). + + *Mikhail Dieterle* + +* PostgreSQL adapter recognizes negative money values formatted with + parentheses (eg. `($1.25) # => -1.25`)). + Fixes #11899. + + *Yves Senn* + +* Stop interpreting SQL 'string' columns as :string type because there is no + common STRING datatype in SQL. + + *Ben Woosley* + +* `ActiveRecord::FinderMethods#exists?` returns `true`/`false` in all cases. + + *Xavier Noria* + * Assign inet/cidr attribute with `nil` value for invalid address. Example: record = User.new - record.logged_in_from_ip # is type of an inet or a cidr # Before: diff --git a/activerecord/RUNNING_UNIT_TESTS.rdoc b/activerecord/RUNNING_UNIT_TESTS.rdoc index c3ee34da55..ca1f2fd665 100644 --- a/activerecord/RUNNING_UNIT_TESTS.rdoc +++ b/activerecord/RUNNING_UNIT_TESTS.rdoc @@ -24,6 +24,7 @@ Simply executing <tt>bundle exec rake test</tt> is equivalent to the following: $ 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/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 5ceda933f2..33cbafc6aa 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -164,7 +164,7 @@ module ActiveRecord private # Returns the specified association instance if it responds to :loaded?, nil otherwise. def association_instance_get(name) - @association_cache[name.to_sym] + @association_cache[name] end # Set the specified association instance. diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb index 6dc2da56d1..ea7f768a68 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -848,8 +848,6 @@ module ActiveRecord def scope @association.scope end - - # :nodoc: alias spawn scope # Equivalent to <tt>Array#==</tt>. Returns +true+ if the two arrays diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb index 0cc836f991..928da71eed 100644 --- a/activerecord/lib/active_record/associations/preloader/association.rb +++ b/activerecord/lib/active_record/associations/preloader/association.rb @@ -29,6 +29,10 @@ module ActiveRecord end def records_for(ids) + query_scope(ids) + end + + def query_scope(ids) scope.where(association_key.in(ids)) end @@ -52,12 +56,9 @@ module ActiveRecord raise NotImplementedError end - # We're converting to a string here because postgres will return the aliased association - # key in a habtm as a string (for whatever reason) def owners_by_key @owners_by_key ||= owners.group_by do |owner| - key = owner[owner_key_name] - key && key.to_s + owner[owner_key_name] end end @@ -71,27 +72,34 @@ module ActiveRecord owners_map = owners_by_key owner_keys = owners_map.keys.compact - if klass.nil? || owner_keys.empty? - records = [] - else + # Each record may have multiple owners, and vice-versa + records_by_owner = Hash[owners.map { |owner| [owner, []] }] + + if klass && owner_keys.any? # Some databases impose a limit on the number of ids in a list (in Oracle it's 1000) # Make several smaller queries if necessary or make one query if the adapter supports it sliced = owner_keys.each_slice(klass.connection.in_clause_length || owner_keys.size) - records = sliced.flat_map { |slice| records_for(slice).to_a } + sliced.each { |slice| + records = records_for(slice) + caster = type_caster(records, association_key_name) + records.each do |record| + owner_key = caster.call record[association_key_name] + + owners_map[owner_key].each do |owner| + records_by_owner[owner] << record + end + end + } end - # Each record may have multiple owners, and vice-versa - records_by_owner = Hash[owners.map { |owner| [owner, []] }] - records.each do |record| - owner_key = record[association_key_name].to_s - - owners_map[owner_key].each do |owner| - records_by_owner[owner] << record - end - end records_by_owner end + IDENTITY = lambda { |value| value } + def type_caster(results, name) + IDENTITY + end + def reflection_scope @reflection_scope ||= reflection.scope ? klass.unscoped.instance_exec(nil, &reflection.scope) : klass.unscoped end diff --git a/activerecord/lib/active_record/associations/preloader/has_and_belongs_to_many.rb b/activerecord/lib/active_record/associations/preloader/has_and_belongs_to_many.rb index 9a3fada380..c042a44b21 100644 --- a/activerecord/lib/active_record/associations/preloader/has_and_belongs_to_many.rb +++ b/activerecord/lib/active_record/associations/preloader/has_and_belongs_to_many.rb @@ -12,7 +12,7 @@ module ActiveRecord # Unlike the other associations, we want to get a raw array of rows so that we can # access the aliased column on the join table def records_for(ids) - scope = super + scope = query_scope ids klass.connection.select_all(scope.arel, 'SQL', scope.bind_values) end @@ -40,6 +40,11 @@ module ActiveRecord end end + def type_caster(results, name) + caster = results.column_types.fetch(name, results.identity_type) + lambda { |value| caster.type_cast value } + end + def build_scope super.joins(join).select(join_select) end diff --git a/activerecord/lib/active_record/associations/preloader/through_association.rb b/activerecord/lib/active_record/associations/preloader/through_association.rb index de06931845..2c625cec04 100644 --- a/activerecord/lib/active_record/associations/preloader/through_association.rb +++ b/activerecord/lib/active_record/associations/preloader/through_association.rb @@ -27,17 +27,16 @@ module ActiveRecord def through_records_by_owner Preloader.new(owners, through_reflection.name, through_scope).run - Hash[owners.map do |owner| - through_records = Array.wrap(owner.send(through_reflection.name)) + should_reset = (through_scope != through_reflection.klass.unscoped) || + (reflection.options[:source_type] && through_reflection.collection?) - # Dont cache the association - we would only be caching a subset - if (through_scope != through_reflection.klass.unscoped) || - (reflection.options[:source_type] && through_reflection.collection?) - owner.association(through_reflection.name).reset - end + owners.each_with_object({}) do |owner, h| + association = owner.association through_reflection.name + h[owner] = Array(association.reader) - [owner, through_records] - end] + # Dont cache the association - we would only be caching a subset + association.reset if should_reset + end end def through_scope 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 41b5a6e926..f168282ea3 100644 --- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb +++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb @@ -51,7 +51,7 @@ module ActiveRecord def create_time_zone_conversion_attribute?(name, column) time_zone_aware_attributes && !self.skip_time_zone_conversion_for_attributes.include?(name.to_sym) && - [:datetime, :timestamp].include?(column.type) + (:datetime == column.type || :timestamp == column.type) end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb index c8fa4ef9af..552a22d28a 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb @@ -51,7 +51,6 @@ module ActiveRecord return value unless column case column.type - when :binary then value when :integer then value.to_i when :float then value.to_f else diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb index bccfa41ad1..fb53090edc 100644 --- a/activerecord/lib/active_record/connection_adapters/column.rb +++ b/activerecord/lib/active_record/connection_adapters/column.rb @@ -272,7 +272,7 @@ module ActiveRecord :text when /blob/i, /binary/i :binary - when /char/i, /string/i + when /char/i :string when /boolean/i :boolean diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb b/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb index ef7b976d4f..ea44e818e5 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb @@ -17,8 +17,8 @@ module ActiveRecord return string unless String === string case string - when 'infinity'; 1.0 / 0.0 - when '-infinity'; -1.0 / 0.0 + when 'infinity'; Float::INFINITY + when '-infinity'; -Float::INFINITY when / BC$/ super("-" + string.sub(/ BC$/, "")) else 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 751655e61c..527d13b9b0 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb @@ -139,7 +139,8 @@ module ActiveRecord exec_cache(sql, binds) types = {} - result.fields.each_with_index do |fname, i| + fields = result.fields + 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| @@ -148,7 +149,7 @@ module ActiveRecord } end - ret = ActiveRecord::Result.new(result.fields, result.values, types) + ret = ActiveRecord::Result.new(fields, result.values, types) result.clear return ret end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb index 58c6235059..dab876af14 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb @@ -34,12 +34,17 @@ module ActiveRecord class Money < Type def type_cast(value) return if value.nil? + return value unless String === value # Because money output is formatted according to the locale, there are two # cases to consider (note the decimal separators): # (1) $12,345,678.12 # (2) $12.345.678,12 + # Negative values are represented as follows: + # (3) -$2.55 + # (4) ($2.55) + value.sub!(/^\((.+)\)$/, '-\1') # (4) case value when /^-?\D+[\d,]+\.\d{2}$/ # (1) value.gsub!(/[^-\d.]/, '') diff --git a/activerecord/lib/active_record/dynamic_matchers.rb b/activerecord/lib/active_record/dynamic_matchers.rb index 3bac31c6aa..e650ebcf64 100644 --- a/activerecord/lib/active_record/dynamic_matchers.rb +++ b/activerecord/lib/active_record/dynamic_matchers.rb @@ -35,7 +35,7 @@ module ActiveRecord end def pattern - /^#{prefix}_([_a-zA-Z]\w*)#{suffix}$/ + @pattern ||= /\A#{prefix}_([_a-zA-Z]\w*)#{suffix}\Z/ end def prefix diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index b2a81a184a..40263b2a70 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -379,16 +379,16 @@ module ActiveRecord @@all_cached_fixtures = Hash.new { |h,k| h[k] = {} } - def self.default_fixture_model_name(fixture_set_name) # :nodoc: - ActiveRecord::Base.pluralize_table_names ? + def self.default_fixture_model_name(fixture_set_name, config = ActiveRecord::Base) # :nodoc: + config.pluralize_table_names ? fixture_set_name.singularize.camelize : fixture_set_name.camelize end - def self.default_fixture_table_name(fixture_set_name) # :nodoc: - "#{ ActiveRecord::Base.table_name_prefix }"\ + def self.default_fixture_table_name(fixture_set_name, config = ActiveRecord::Base) # :nodoc: + "#{ config.table_name_prefix }"\ "#{ fixture_set_name.tr('/', '_') }"\ - "#{ ActiveRecord::Base.table_name_suffix }".to_sym + "#{ config.table_name_suffix }".to_sym end def self.reset_cache @@ -436,7 +436,7 @@ module ActiveRecord cattr_accessor :all_loaded_fixtures self.all_loaded_fixtures = {} - def self.create_fixtures(fixtures_directory, fixture_set_names, class_names = {}) + def self.create_fixtures(fixtures_directory, fixture_set_names, class_names = {}, config = ActiveRecord::Base) fixture_set_names = Array(fixture_set_names).map(&:to_s) class_names = class_names.stringify_keys @@ -455,7 +455,7 @@ module ActiveRecord fixtures_map[fs_name] = new( # ActiveRecord::FixtureSet.new connection, fs_name, - class_names[fs_name] || default_fixture_model_name(fs_name), + class_names[fs_name] || (default_fixture_model_name(fs_name, config).safe_constantize), ::File.join(fixtures_directory, fs_name)) end @@ -497,17 +497,22 @@ module ActiveRecord Zlib.crc32(label.to_s) % MAX_ID end - attr_reader :table_name, :name, :fixtures, :model_class + attr_reader :table_name, :name, :fixtures, :model_class, :config - def initialize(connection, name, class_name, path) + def initialize(connection, name, class_name, path, config = ActiveRecord::Base) @fixtures = {} # Ordered hash @name = name @path = path + @config = config + + if class_name.is_a?(String) + ActiveSupport::Deprecation.warn("The ability to pass in strings as a class name will be removed in Rails 4.2, consider using the class itself instead.") + end if class_name.is_a?(Class) # TODO: Should be an AR::Base type class, or any? @model_class = class_name else - @model_class = class_name.constantize rescue nil + @model_class = class_name.safe_constantize if class_name end @connection = ( model_class.respond_to?(:connection) ? @@ -515,7 +520,7 @@ module ActiveRecord @table_name = ( model_class.respond_to?(:table_name) ? model_class.table_name : - self.class.default_fixture_table_name(name) ) + self.class.default_fixture_table_name(name, config) ) read_fixture_files end @@ -539,7 +544,7 @@ module ActiveRecord # Return a hash of rows to be inserted. The key is the table, the value is # a list of rows to insert to that table. def table_rows - now = ActiveRecord::Base.default_timezone == :utc ? Time.now.utc : Time.now + now = config.default_timezone == :utc ? Time.now.utc : Time.now now = now.to_s(:db) # allow a standard key to be used for doing defaults in YAML @@ -723,14 +728,16 @@ module ActiveRecord class_attribute :use_transactional_fixtures class_attribute :use_instantiated_fixtures # true, false, or :no_instances class_attribute :pre_loaded_fixtures + class_attribute :config self.fixture_table_names = [] self.use_transactional_fixtures = true self.use_instantiated_fixtures = false self.pre_loaded_fixtures = false + self.config = ActiveRecord::Base self.fixture_class_names = Hash.new do |h, fixture_set_name| - h[fixture_set_name] = ActiveRecord::FixtureSet.default_fixture_model_name(fixture_set_name) + h[fixture_set_name] = ActiveRecord::FixtureSet.default_fixture_model_name(fixture_set_name, self.config) end end @@ -743,13 +750,6 @@ module ActiveRecord # 'namespaced/fixture' => Another::Model # # The keys must be the fixture names, that coincide with the short paths to the fixture files. - #-- - # It is also possible to pass the class name instead of the class: - # set_fixture_class 'some_fixture' => 'SomeModel' - # I think this option is redundant, i propose to deprecate it. - # Isn't it easier to always pass the class itself? - # (2011-12-20 alexeymuranov) - #++ def set_fixture_class(class_names = {}) self.fixture_class_names = self.fixture_class_names.merge(class_names.stringify_keys) end @@ -763,7 +763,7 @@ module ActiveRecord end self.fixture_table_names |= fixture_set_names - require_fixture_classes(fixture_set_names) + require_fixture_classes(fixture_set_names, self.config) setup_fixture_accessors(fixture_set_names) end @@ -778,7 +778,7 @@ module ActiveRecord end end - def require_fixture_classes(fixture_set_names = nil) + def require_fixture_classes(fixture_set_names = nil, config = ActiveRecord::Base) if fixture_set_names fixture_set_names = fixture_set_names.map { |n| n.to_s } else @@ -786,7 +786,7 @@ module ActiveRecord end fixture_set_names.each do |file_name| - file_name = file_name.singularize if ActiveRecord::Base.pluralize_table_names + file_name = file_name.singularize if config.pluralize_table_names try_to_load_dependency(file_name) end end @@ -838,7 +838,7 @@ module ActiveRecord !self.class.uses_transaction?(method_name) end - def setup_fixtures + def setup_fixtures(config = ActiveRecord::Base) if pre_loaded_fixtures && !use_transactional_fixtures raise RuntimeError, 'pre_loaded_fixtures requires use_transactional_fixtures' end @@ -852,7 +852,7 @@ module ActiveRecord if @@already_loaded_fixtures[self.class] @loaded_fixtures = @@already_loaded_fixtures[self.class] else - @loaded_fixtures = load_fixtures + @loaded_fixtures = load_fixtures(config) @@already_loaded_fixtures[self.class] = @loaded_fixtures end @fixture_connections = enlist_fixture_connections @@ -863,11 +863,11 @@ module ActiveRecord else ActiveRecord::FixtureSet.reset_cache @@already_loaded_fixtures[self.class] = nil - @loaded_fixtures = load_fixtures + @loaded_fixtures = load_fixtures(config) end # Instantiate fixtures for every test if requested. - instantiate_fixtures if use_instantiated_fixtures + instantiate_fixtures(config) if use_instantiated_fixtures end def teardown_fixtures @@ -889,19 +889,19 @@ module ActiveRecord end private - def load_fixtures - fixtures = ActiveRecord::FixtureSet.create_fixtures(fixture_path, fixture_table_names, fixture_class_names) + def load_fixtures(config) + fixtures = ActiveRecord::FixtureSet.create_fixtures(fixture_path, fixture_table_names, fixture_class_names, config) Hash[fixtures.map { |f| [f.name, f] }] end # for pre_loaded_fixtures, only require the classes once. huge speed improvement @@required_fixture_classes = false - def instantiate_fixtures + def instantiate_fixtures(config) if pre_loaded_fixtures raise RuntimeError, 'Load fixtures before instantiating them.' if ActiveRecord::FixtureSet.all_loaded_fixtures.empty? unless @@required_fixture_classes - self.class.require_fixture_classes ActiveRecord::FixtureSet.all_loaded_fixtures.keys + self.class.require_fixture_classes ActiveRecord::FixtureSet.all_loaded_fixtures.keys, config @@required_fixture_classes = true end ActiveRecord::FixtureSet.instantiate_all_loaded_fixtures(self, load_instances?) diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index 19c6f8148b..a1ad4f6255 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -373,23 +373,23 @@ module ActiveRecord class << self attr_accessor :delegate # :nodoc: attr_accessor :disable_ddl_transaction # :nodoc: - end - def self.check_pending! - raise ActiveRecord::PendingMigrationError if ActiveRecord::Migrator.needs_migration? - end + def check_pending! + raise ActiveRecord::PendingMigrationError if ActiveRecord::Migrator.needs_migration? + end - def self.method_missing(name, *args, &block) # :nodoc: - (delegate || superclass.delegate).send(name, *args, &block) - end + def method_missing(name, *args, &block) # :nodoc: + (delegate || superclass.delegate).send(name, *args, &block) + end - def self.migrate(direction) - new.migrate direction - end + def migrate(direction) + new.migrate direction + end - # Disable DDL transactions for this migration. - def self.disable_ddl_transaction! - @disable_ddl_transaction = true + # Disable DDL transactions for this migration. + def disable_ddl_transaction! + @disable_ddl_transaction = true + end end def disable_ddl_transaction # :nodoc: @@ -617,8 +617,8 @@ module ActiveRecord say_with_time "#{method}(#{arg_list})" do unless @connection.respond_to? :revert unless arguments.empty? || method == :execute - arguments[0] = Migrator.proper_table_name(arguments.first) - arguments[1] = Migrator.proper_table_name(arguments.second) if method == :rename_table + arguments[0] = proper_table_name(arguments.first, table_name_options) + arguments[1] = proper_table_name(arguments.second, table_name_options) if method == :rename_table end end return super unless connection.respond_to?(method) @@ -671,6 +671,17 @@ module ActiveRecord copied end + # Finds the correct table name given an Active Record object. + # Uses the Active Record object's own table_name, or pre/suffix from the + # options passed in. + def proper_table_name(name, options = {}) + if name.respond_to? :table_name + name.table_name + else + "#{options[:table_name_prefix]}#{name}#{options[:table_name_suffix]}" + end + end + # Determines the version number of the next migration. def next_migration_number(number) if ActiveRecord::Base.timestamped_migrations @@ -680,6 +691,13 @@ module ActiveRecord end end + def table_name_options(config = ActiveRecord::Base) + { + table_name_prefix: config.table_name_prefix, + table_name_suffix: config.table_name_suffix + } + end + private def execute_block if connection.respond_to? :execute_block @@ -809,12 +827,16 @@ module ActiveRecord migrations(migrations_paths).last || NullMigration.new end - def proper_table_name(name) - # Use the Active Record objects own table_name, or pre/suffix from ActiveRecord::Base if name is a symbol/string + def proper_table_name(name, options = {}) + ActiveSupport::Deprecation.warn "ActiveRecord::Migrator.proper_table_name is deprecated and will be removed in Rails 4.2. Use the proper_table_name instance method on ActiveRecord::Migration instead" + options = { + table_name_prefix: ActiveRecord::Base.table_name_prefix, + table_name_suffix: ActiveRecord::Base.table_name_suffix + }.merge(options) if name.respond_to? :table_name name.table_name else - "#{ActiveRecord::Base.table_name_prefix}#{name}#{ActiveRecord::Base.table_name_suffix}" + "#{options[:table_name_prefix]}#{name}#{options[:table_name_suffix]}" end end diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb index 23541d1d27..75c0c1bda8 100644 --- a/activerecord/lib/active_record/model_schema.rb +++ b/activerecord/lib/active_record/model_schema.rb @@ -224,13 +224,20 @@ module ActiveRecord def decorate_columns(columns_hash) # :nodoc: return if columns_hash.empty? - columns_hash.each do |name, col| - if serialized_attributes.key?(name) - columns_hash[name] = AttributeMethods::Serialization::Type.new(col) - end - if create_time_zone_conversion_attribute?(name, col) - columns_hash[name] = AttributeMethods::TimeZoneConversion::Type.new(col) - end + @serialized_column_names ||= self.columns_hash.keys.find_all do |name| + serialized_attributes.key?(name) + end + + @serialized_column_names.each do |name| + columns_hash[name] = AttributeMethods::Serialization::Type.new(columns_hash[name]) + end + + @time_zone_column_names ||= self.columns_hash.find_all do |name, col| + create_time_zone_conversion_attribute?(name, col) + end.map!(&:first) + + @time_zone_column_names.each do |name| + columns_hash[name] = AttributeMethods::TimeZoneConversion::Type.new(columns_hash[name]) end columns_hash @@ -284,16 +291,19 @@ module ActiveRecord undefine_attribute_methods connection.schema_cache.clear_table_cache!(table_name) if table_exists? - @arel_engine = nil - @column_defaults = nil - @column_names = nil - @columns = nil - @columns_hash = nil - @column_types = nil - @content_columns = nil - @dynamic_methods_hash = nil - @inheritance_column = nil unless defined?(@explicit_inheritance_column) && @explicit_inheritance_column - @relation = nil + @arel_engine = nil + @column_defaults = nil + @column_names = nil + @columns = nil + @columns_hash = nil + @column_types = nil + @content_columns = nil + @dynamic_methods_hash = nil + @inheritance_column = nil unless defined?(@explicit_inheritance_column) && @explicit_inheritance_column + @relation = nil + @serialized_column_names = nil + @time_zone_column_names = nil + @cached_time_zone = nil end # This is a hook for use by modules that need to do extra stuff to diff --git a/activerecord/lib/active_record/railties/console_sandbox.rb b/activerecord/lib/active_record/railties/console_sandbox.rb index 604a220303..1a04950898 100644 --- a/activerecord/lib/active_record/railties/console_sandbox.rb +++ b/activerecord/lib/active_record/railties/console_sandbox.rb @@ -1,5 +1,7 @@ ActiveRecord::Base.connection.begin_transaction(joinable: false) at_exit do - ActiveRecord::Base.connection.rollback_transaction + if ActiveRecord::Base.connection.transaction_open? + ActiveRecord::Base.connection.rollback_transaction + end end diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index f428f160cf..5245fc130a 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -422,7 +422,6 @@ module ActiveRecord def valid_inverse_reflection?(reflection) reflection && klass.name == reflection.active_record.name && - klass.primary_key == reflection.active_record_primary_key && can_find_inverse_of_automatically?(reflection) end diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index 2d3bd563ac..0132a02f83 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -171,21 +171,21 @@ module ActiveRecord last or raise RecordNotFound end - # Returns truthy if a record exists in the table that matches the +id+ or - # conditions given, or falsy otherwise. The argument can take six forms: + # Returns +true+ if a record exists in the table that matches the +id+ or + # conditions given, or +false+ otherwise. The argument can take six forms: # # * Integer - Finds the record with this primary key. # * String - Finds the record with a primary key corresponding to this # string (such as <tt>'5'</tt>). # * Array - Finds the record that matches these +find+-style conditions - # (such as <tt>['color = ?', 'red']</tt>). + # (such as <tt>['name LIKE ?', "%#{query}%"]</tt>). # * Hash - Finds the record that matches these +find+-style conditions - # (such as <tt>{color: 'red'}</tt>). + # (such as <tt>{name: 'David'}</tt>). # * +false+ - Returns always +false+. # * No args - Returns +false+ if the table is empty, +true+ otherwise. # - # For more information about specifying conditions as a Hash or Array, - # see the Conditions section in the introduction to ActiveRecord::Base. + # For more information about specifying conditions as a hash or array, + # see the Conditions section in the introduction to <tt>ActiveRecord::Base</tt>. # # Note: You can't pass in a condition as a string (like <tt>name = # 'Jamie'</tt>), since it would be sanitized and then queried against @@ -213,7 +213,7 @@ module ActiveRecord relation = relation.where(table[primary_key].eq(conditions)) if conditions != :none end - connection.select_value(relation.arel, "#{name} Exists", relation.bind_values) + connection.select_value(relation, "#{name} Exists", relation.bind_values) ? true : false end # This method is called whenever no records are found with either a single diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb index 8948f2bba5..c60cd27a83 100644 --- a/activerecord/lib/active_record/relation/predicate_builder.rb +++ b/activerecord/lib/active_record/relation/predicate_builder.rb @@ -55,7 +55,7 @@ module ActiveRecord # # For polymorphic relationships, find the foreign key and type: # PriceEstimate.where(estimate_of: treasure) - if klass && value.class < Base && reflection = klass.reflect_on_association(column.to_sym) + if klass && value.is_a?(Base) && reflection = klass.reflect_on_association(column.to_sym) if reflection.polymorphic? queries << build(table[reflection.foreign_type], value.class.base_class) end diff --git a/activerecord/test/cases/adapters/postgresql/datatype_test.rb b/activerecord/test/cases/adapters/postgresql/datatype_test.rb index 75b6f4f8ce..3dbab08a99 100644 --- a/activerecord/test/cases/adapters/postgresql/datatype_test.rb +++ b/activerecord/test/cases/adapters/postgresql/datatype_test.rb @@ -298,6 +298,14 @@ _SQL assert_equal(-567.89, @second_money.wealth) end + def test_money_type_cast + column = PostgresqlMoney.columns.find { |c| c.name == 'wealth' } + assert_equal(12345678.12, column.type_cast("$12,345,678.12")) + assert_equal(12345678.12, column.type_cast("$12.345.678,12")) + assert_equal(-1.15, column.type_cast("-$1.15")) + assert_equal(-2.25, column.type_cast("($2.25)")) + end + def test_create_tstzrange skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges? tstzrange = Time.parse('2010-01-01 14:30:00 +0100')...Time.parse('2011-02-02 14:30:00 CDT') diff --git a/activerecord/test/cases/adapters/postgresql/quoting_test.rb b/activerecord/test/cases/adapters/postgresql/quoting_test.rb index b3429648ee..1122f8b9a1 100644 --- a/activerecord/test/cases/adapters/postgresql/quoting_test.rb +++ b/activerecord/test/cases/adapters/postgresql/quoting_test.rb @@ -47,11 +47,16 @@ module ActiveRecord def test_quote_cast_numeric fixnum = 666 - c = Column.new(nil, nil, 'string') + c = Column.new(nil, nil, 'varchar') assert_equal "'666'", @conn.quote(fixnum, c) c = Column.new(nil, nil, 'text') assert_equal "'666'", @conn.quote(fixnum, c) end + + def test_quote_time_usec + assert_equal "'1970-01-01 00:00:00.000000'", @conn.quote(Time.at(0)) + assert_equal "'1970-01-01 00:00:00.000000'", @conn.quote(Time.at(0).to_datetime) + end end end end diff --git a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb index e693d34f99..811d91f849 100644 --- a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb +++ b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb @@ -52,12 +52,10 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase def test_cascaded_eager_association_loading_with_join_for_count categories = Category.joins(:categorizations).includes([{:posts=>:comments}, :authors]) - assert_nothing_raised do - assert_equal 4, categories.count - assert_equal 4, categories.to_a.count - assert_equal 3, categories.distinct.count - assert_equal 3, categories.to_a.uniq.size # Must uniq since instantiating with inner joins will get dupes - end + assert_equal 4, categories.count + assert_equal 4, categories.to_a.count + assert_equal 3, categories.distinct.count + assert_equal 3, categories.to_a.uniq.size # Must uniq since instantiating with inner joins will get dupes end def test_cascaded_eager_association_loading_with_duplicated_includes 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 941e851aae..c00cae5750 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -28,7 +28,8 @@ require 'models/club' 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 + :subscribers, :books, :subscriptions, :developers, :categorizations, :essays, + :categories_posts # Dummies to force column loads so query counts are clean. def setup @@ -36,6 +37,14 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase Reader.create :person_id => 0, :post_id => 0 end + def test_pk_is_not_required_for_join + post = Post.includes(:scategories).first + post2 = Post.includes(:categories).first + + assert_operator post.categories.length, :>, 0 + assert_equal post2.categories, post.categories + end + def test_include? person = Person.new post = Post.new diff --git a/activerecord/test/cases/finder_respond_to_test.rb b/activerecord/test/cases/finder_respond_to_test.rb index 6101eb7b68..3ff22f222f 100644 --- a/activerecord/test/cases/finder_respond_to_test.rb +++ b/activerecord/test/cases/finder_respond_to_test.rb @@ -21,6 +21,11 @@ class FinderRespondToTest < ActiveRecord::TestCase assert_respond_to Topic, :find_by_title end + def test_should_respond_to_find_by_with_bang + ensure_topic_method_is_not_cached(:find_by_title!) + assert_respond_to Topic, :find_by_title! + end + def test_should_respond_to_find_by_two_attributes ensure_topic_method_is_not_cached(:find_by_title_and_author_name) assert_respond_to Topic, :find_by_title_and_author_name diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index 51a8a13d04..949e994f8d 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -45,17 +45,18 @@ class FinderTest < ActiveRecord::TestCase end def test_exists - assert Topic.exists?(1) - assert Topic.exists?("1") - assert Topic.exists?(title: "The First Topic") - assert Topic.exists?(heading: "The First Topic") - assert Topic.exists?(:author_name => "Mary", :approved => true) - assert Topic.exists?(["parent_id = ?", 1]) - assert !Topic.exists?(45) - assert !Topic.exists?(Topic.new) + assert_equal true, Topic.exists?(1) + assert_equal true, Topic.exists?("1") + assert_equal true, Topic.exists?(title: "The First Topic") + assert_equal true, Topic.exists?(heading: "The First Topic") + assert_equal true, Topic.exists?(:author_name => "Mary", :approved => true) + assert_equal true, Topic.exists?(["parent_id = ?", 1]) + + assert_equal false, Topic.exists?(45) + assert_equal false, Topic.exists?(Topic.new) begin - assert !Topic.exists?("foo") + assert_equal false, Topic.exists?("foo") rescue ActiveRecord::StatementInvalid # PostgreSQL complains about string comparison with integer field rescue Exception @@ -72,61 +73,62 @@ class FinderTest < ActiveRecord::TestCase end def test_exists_returns_true_with_one_record_and_no_args - assert Topic.exists? + assert_equal true, Topic.exists? end def test_exists_returns_false_with_false_arg - assert !Topic.exists?(false) + assert_equal false, Topic.exists?(false) end # exists? should handle nil for id's that come from URLs and always return false # (example: Topic.exists?(params[:id])) where params[:id] is nil def test_exists_with_nil_arg - assert !Topic.exists?(nil) - assert Topic.exists? - assert !Topic.first.replies.exists?(nil) - assert Topic.first.replies.exists? + assert_equal false, Topic.exists?(nil) + assert_equal true, Topic.exists? + + assert_equal false, Topic.first.replies.exists?(nil) + assert_equal true, Topic.first.replies.exists? end # ensures +exists?+ runs valid SQL by excluding order value def test_exists_with_order - assert Topic.order(:id).distinct.exists? + assert_equal true, Topic.order(:id).distinct.exists? end def test_exists_with_includes_limit_and_empty_result - assert !Topic.includes(:replies).limit(0).exists? - assert !Topic.includes(:replies).limit(1).where('0 = 1').exists? + assert_equal false, Topic.includes(:replies).limit(0).exists? + assert_equal false, Topic.includes(:replies).limit(1).where('0 = 1').exists? end def test_exists_with_distinct_association_includes_and_limit author = Author.first - assert !author.unique_categorized_posts.includes(:special_comments).limit(0).exists? - assert author.unique_categorized_posts.includes(:special_comments).limit(1).exists? + assert_equal false, author.unique_categorized_posts.includes(:special_comments).limit(0).exists? + assert_equal true, author.unique_categorized_posts.includes(:special_comments).limit(1).exists? end def test_exists_with_distinct_association_includes_limit_and_order author = Author.first - assert !author.unique_categorized_posts.includes(:special_comments).order('comments.taggings_count DESC').limit(0).exists? - assert author.unique_categorized_posts.includes(:special_comments).order('comments.taggings_count DESC').limit(1).exists? + assert_equal false, author.unique_categorized_posts.includes(:special_comments).order('comments.taggings_count DESC').limit(0).exists? + assert_equal true, author.unique_categorized_posts.includes(:special_comments).order('comments.taggings_count DESC').limit(1).exists? end def test_exists_with_empty_table_and_no_args_given Topic.delete_all - assert !Topic.exists? + assert_equal false, Topic.exists? end def test_exists_with_aggregate_having_three_mappings existing_address = customers(:david).address - assert Customer.exists?(:address => existing_address) + assert_equal true, Customer.exists?(:address => existing_address) end def test_exists_with_aggregate_having_three_mappings_with_one_difference existing_address = customers(:david).address - assert !Customer.exists?(:address => + assert_equal false, Customer.exists?(:address => Address.new(existing_address.street, existing_address.city, existing_address.country + "1")) - assert !Customer.exists?(:address => + assert_equal false, Customer.exists?(:address => Address.new(existing_address.street, existing_address.city + "1", existing_address.country)) - assert !Customer.exists?(:address => + assert_equal false, Customer.exists?(:address => Address.new(existing_address.street + "1", existing_address.city, existing_address.country)) end diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb index c07c9c3b74..4e877aed2e 100644 --- a/activerecord/test/cases/fixtures_test.rb +++ b/activerecord/test/cases/fixtures_test.rb @@ -190,11 +190,11 @@ class FixturesTest < ActiveRecord::TestCase end def test_empty_yaml_fixture - assert_not_nil ActiveRecord::FixtureSet.new( Account.connection, "accounts", 'Account', FIXTURES_ROOT + "/naked/yml/accounts") + assert_not_nil ActiveRecord::FixtureSet.new( Account.connection, "accounts", Account, FIXTURES_ROOT + "/naked/yml/accounts") end def test_empty_yaml_fixture_with_a_comment_in_it - assert_not_nil ActiveRecord::FixtureSet.new( Account.connection, "companies", 'Company', FIXTURES_ROOT + "/naked/yml/companies") + assert_not_nil ActiveRecord::FixtureSet.new( Account.connection, "companies", Company, FIXTURES_ROOT + "/naked/yml/companies") end def test_nonexistent_fixture_file @@ -204,19 +204,19 @@ class FixturesTest < ActiveRecord::TestCase assert Dir[nonexistent_fixture_path+"*"].empty? assert_raise(Errno::ENOENT) do - ActiveRecord::FixtureSet.new( Account.connection, "companies", 'Company', nonexistent_fixture_path) + ActiveRecord::FixtureSet.new( Account.connection, "companies", Company, nonexistent_fixture_path) end end def test_dirty_dirty_yaml_file assert_raise(ActiveRecord::Fixture::FormatError) do - ActiveRecord::FixtureSet.new( Account.connection, "courses", 'Course', FIXTURES_ROOT + "/naked/yml/courses") + ActiveRecord::FixtureSet.new( Account.connection, "courses", Course, FIXTURES_ROOT + "/naked/yml/courses") end end def test_omap_fixtures assert_nothing_raised do - fixtures = ActiveRecord::FixtureSet.new(Account.connection, 'categories', 'Category', FIXTURES_ROOT + "/categories_ordered") + fixtures = ActiveRecord::FixtureSet.new(Account.connection, 'categories', Category, FIXTURES_ROOT + "/categories_ordered") fixtures.each.with_index do |(name, fixture), i| assert_equal "fixture_no_#{i}", name @@ -449,7 +449,7 @@ class OverRideFixtureMethodTest < ActiveRecord::TestCase end class CheckSetTableNameFixturesTest < ActiveRecord::TestCase - set_fixture_class :funny_jokes => 'Joke' + set_fixture_class :funny_jokes => Joke fixtures :funny_jokes # Set to false to blow away fixtures cache and ensure our fixtures are loaded # and thus takes into account our set_fixture_class @@ -532,7 +532,7 @@ class InvalidTableNameFixturesTest < ActiveRecord::TestCase end class CheckEscapedYamlFixturesTest < ActiveRecord::TestCase - set_fixture_class :funny_jokes => 'Joke' + set_fixture_class :funny_jokes => Joke fixtures :funny_jokes # Set to false to blow away fixtures cache and ensure our fixtures are loaded # and thus takes into account our set_fixture_class @@ -574,7 +574,7 @@ class FixturesBrokenRollbackTest < ActiveRecord::TestCase end private - def load_fixtures + def load_fixtures(config) raise 'argh' end end diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb index ed080b2995..931caff727 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -321,12 +321,53 @@ class MigrationTest < ActiveRecord::TestCase assert_equal "schema_migrations", ActiveRecord::Migrator.schema_migrations_table_name end - def test_proper_table_name - assert_equal "table", ActiveRecord::Migrator.proper_table_name('table') - assert_equal "table", ActiveRecord::Migrator.proper_table_name(:table) - assert_equal "reminders", ActiveRecord::Migrator.proper_table_name(Reminder) + def test_proper_table_name_on_migrator + assert_deprecated do + assert_equal "table", ActiveRecord::Migrator.proper_table_name('table') + end + assert_deprecated do + assert_equal "table", ActiveRecord::Migrator.proper_table_name(:table) + end + assert_deprecated do + assert_equal "reminders", ActiveRecord::Migrator.proper_table_name(Reminder) + end + Reminder.reset_table_name + assert_deprecated do + assert_equal Reminder.table_name, ActiveRecord::Migrator.proper_table_name(Reminder) + end + + # Use the model's own prefix/suffix if a model is given + ActiveRecord::Base.table_name_prefix = "ARprefix_" + ActiveRecord::Base.table_name_suffix = "_ARsuffix" + Reminder.table_name_prefix = 'prefix_' + Reminder.table_name_suffix = '_suffix' + Reminder.reset_table_name + assert_deprecated do + assert_equal "prefix_reminders_suffix", ActiveRecord::Migrator.proper_table_name(Reminder) + end + Reminder.table_name_prefix = '' + Reminder.table_name_suffix = '' + Reminder.reset_table_name + + # Use AR::Base's prefix/suffix if string or symbol is given + ActiveRecord::Base.table_name_prefix = "prefix_" + ActiveRecord::Base.table_name_suffix = "_suffix" + Reminder.reset_table_name + assert_deprecated do + assert_equal "prefix_table_suffix", ActiveRecord::Migrator.proper_table_name('table') + end + assert_deprecated do + assert_equal "prefix_table_suffix", ActiveRecord::Migrator.proper_table_name(:table) + end + end + + def test_proper_table_name_on_migration + migration = ActiveRecord::Migration.new + assert_equal "table", migration.proper_table_name('table') + assert_equal "table", migration.proper_table_name(:table) + assert_equal "reminders", migration.proper_table_name(Reminder) Reminder.reset_table_name - assert_equal Reminder.table_name, ActiveRecord::Migrator.proper_table_name(Reminder) + assert_equal Reminder.table_name, migration.proper_table_name(Reminder) # Use the model's own prefix/suffix if a model is given ActiveRecord::Base.table_name_prefix = "ARprefix_" @@ -334,7 +375,7 @@ class MigrationTest < ActiveRecord::TestCase Reminder.table_name_prefix = 'prefix_' Reminder.table_name_suffix = '_suffix' Reminder.reset_table_name - assert_equal "prefix_reminders_suffix", ActiveRecord::Migrator.proper_table_name(Reminder) + assert_equal "prefix_reminders_suffix", migration.proper_table_name(Reminder) Reminder.table_name_prefix = '' Reminder.table_name_suffix = '' Reminder.reset_table_name @@ -343,8 +384,8 @@ class MigrationTest < ActiveRecord::TestCase ActiveRecord::Base.table_name_prefix = "prefix_" ActiveRecord::Base.table_name_suffix = "_suffix" Reminder.reset_table_name - assert_equal "prefix_table_suffix", ActiveRecord::Migrator.proper_table_name('table') - assert_equal "prefix_table_suffix", ActiveRecord::Migrator.proper_table_name(:table) + assert_equal "prefix_table_suffix", migration.proper_table_name('table', migration.table_name_options) + assert_equal "prefix_table_suffix", migration.proper_table_name(:table, migration.table_name_options) end def test_rename_table_with_prefix_and_suffix diff --git a/activerecord/test/cases/relation/where_test.rb b/activerecord/test/cases/relation/where_test.rb index d333be3560..3e460fa3d6 100644 --- a/activerecord/test/cases/relation/where_test.rb +++ b/activerecord/test/cases/relation/where_test.rb @@ -81,6 +81,31 @@ module ActiveRecord assert_equal expected.to_sql, actual.to_sql end + def test_decorated_polymorphic_where + treasure_decorator = Struct.new(:model) do + def self.method_missing(method, *args, &block) + Treasure.send(method, *args, &block) + end + + def is_a?(klass) + model.is_a?(klass) + end + + def method_missing(method, *args, &block) + model.send(method, *args, &block) + end + end + + treasure = Treasure.new + treasure.id = 1 + decorated_treasure = treasure_decorator.new(treasure) + + expected = PriceEstimate.where(estimate_of_type: 'Treasure', estimate_of_id: 1) + actual = PriceEstimate.where(estimate_of: decorated_treasure) + + assert_equal expected.to_sql, actual.to_sql + end + def test_aliased_attribute expected = Topic.where(heading: 'The First Topic') actual = Topic.where(title: 'The First Topic') diff --git a/activerecord/test/fixtures/tasks.yml b/activerecord/test/fixtures/tasks.yml index 402ca85faf..c38b32b0e5 100644 --- a/activerecord/test/fixtures/tasks.yml +++ b/activerecord/test/fixtures/tasks.yml @@ -1,4 +1,4 @@ -# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/Fixtures.html +# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html first_task: id: 1 starting: 2005-03-30t06:30:00.00+01:00 diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb index 93a7a2073c..25fb2cffe2 100644 --- a/activerecord/test/models/post.rb +++ b/activerecord/test/models/post.rb @@ -1,4 +1,10 @@ class Post < ActiveRecord::Base + class CategoryPost < ActiveRecord::Base + self.table_name = "categories_posts" + belongs_to :category + belongs_to :post + end + module NamedExtension def author 'lifo' @@ -72,6 +78,8 @@ class Post < ActiveRecord::Base has_many :special_comments_ratings, :through => :special_comments, :source => :ratings has_many :special_comments_ratings_taggings, :through => :special_comments_ratings, :source => :taggings + has_many :category_posts, :class_name => 'CategoryPost' + has_many :scategories, through: :category_posts, source: :category has_and_belongs_to_many :categories has_and_belongs_to_many :special_categories, :join_table => "categories_posts", :association_foreign_key => 'category_id' |