Skip to content

Instantly share code, notes, and snippets.

@sgoedecke
Created March 8, 2022 01:21
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sgoedecke/69bff5164b72a443f79ec07ed17f2542 to your computer and use it in GitHub Desktop.
Save sgoedecke/69bff5164b72a443f79ec07ed17f2542 to your computer and use it in GitHub Desktop.
This gist exceeds the recommended number of files (~10). To access all files, please clone this gist.
# frozen_string_literal: true
class PeopleHaveMiddleNames < ActiveRecord::Migration::Current
def self.up
add_column "people", "middle_name", :string
end
def self.down
remove_column "people", "middle_name"
end
end
# frozen_string_literal: true
class CreateArticles < ActiveRecord::Migration::Current
def self.up
end
def self.down
end
end
# frozen_string_literal: true
# coding: ISO-8859-15
class CurrenciesHaveSymbols < ActiveRecord::Migration::Current
def self.up
# We use € for default currency symbol
add_column "currencies", "symbol", :string, default: "€"
end
def self.down
remove_column "currencies", "symbol"
end
end
# frozen_string_literal: true
class GiveMeBigNumbers < ActiveRecord::Migration::Current
def self.up
create_table :big_numbers do |table|
table.column :bank_balance, :decimal, precision: 10, scale: 2
table.column :big_bank_balance, :decimal, precision: 15, scale: 2
table.column :world_population, :decimal, precision: 20
table.column :my_house_population, :decimal, precision: 2
table.column :value_of_e, :decimal
end
end
def self.down
drop_table :big_numbers
end
end
# frozen_string_literal: true
class PeopleHaveHobbies < ActiveRecord::Migration::Current
def self.up
add_column "people", "hobbies", :string
end
def self.down
remove_column "people", "hobbies"
end
end
# frozen_string_literal: true
class PeopleHaveLastNames < ActiveRecord::Migration::Current
def self.up
add_column "people", "last_name", :string
end
def self.down
remove_column "people", "last_name"
end
end
# frozen_string_literal: true
class Unscoped < ActiveRecord::Migration::Current
def self.change
create_table "unscoped"
end
end
# frozen_string_literal: true
class ValidPeopleHaveLastNames < ActiveRecord::Migration::Current
def self.up
add_column "people", "last_name", :string
end
def self.down
remove_column "people", "last_name"
end
end
# frozen_string_literal: true
class WeNeedThings < ActiveRecord::Migration::Current
def self.up
create_table("things") do |t|
t.column :content, :text
end
end
def self.down
drop_table "things"
end
end
# frozen_string_literal: true
class CreateArticles < ActiveRecord::Migration::Current
def self.up
end
def self.down
end
end
# frozen_string_literal: true
class PeopleHaveHobbies < ActiveRecord::Migration::Current
def self.up
add_column "people", "hobbies", :text
end
def self.down
remove_column "people", "hobbies"
end
end
# frozen_string_literal: true
class CreateComments < ActiveRecord::Migration::Current
def self.up
end
def self.down
end
end
# frozen_string_literal: true
class PeopleHaveDescriptions < ActiveRecord::Migration::Current
def self.up
add_column "people", "description", :text
end
def self.down
remove_column "people", "description"
end
end
# frozen_string_literal: true
class ValidWithTimestampsPeopleHaveLastNames < ActiveRecord::Migration::Current
def self.up
add_column "people", "last_name", :string
end
def self.down
remove_column "people", "last_name"
end
end
# frozen_string_literal: true
class ValidWithTimestampsWeNeedReminders < ActiveRecord::Migration::Current
def self.up
create_table("reminders") do |t|
t.column :content, :text
t.column :remind_at, :datetime
end
end
def self.down
drop_table "reminders"
end
end
# frozen_string_literal: true
class ValidWithTimestampsInnocentJointable < ActiveRecord::Migration::Current
def self.up
create_table("people_reminders", id: false) do |t|
t.column :reminder_id, :integer
t.column :person_id, :integer
end
end
def self.down
drop_table "people_reminders"
end
end
# frozen_string_literal: true
class MigrationVersionCheck < ActiveRecord::Migration::Current
def self.up
raise "incorrect migration version" unless version == 20131219224947
end
def self.down
end
end
# frozen_string_literal: true
class AddPeopleDescription < ActiveRecord::Migration::Current
add_column :people, :description, :string
end
# frozen_string_literal: true
class AddPeopleNumberOfLegs < ActiveRecord::Migration::Current
add_column :people, :number_of_legs, :integer
end
# frozen_string_literal: true
class AddPeopleHobby < ActiveRecord::Migration::Current
add_column :people, :hobby, :string
end
# frozen_string_literal: true
class AddPeopleLastName < ActiveRecord::Migration::Current
add_column :people, :last_name, :string
end
# frozen_string_literal: true
class CreateComments < ActiveRecord::Migration::Current
def self.up
end
def self.down
end
end
# frozen_string_literal: true
class MysqlOnly < ActiveRecord::Migration::Current
def self.change
create_table "mysql_only"
end
end
# frozen_string_literal: true
class PeopleHaveDescriptions < ActiveRecord::Migration::Current
def self.up
add_column "people", "description", :text
end
def self.down
remove_column "people", "description"
end
end
# frozen_string_literal: true
class RenameThings < ActiveRecord::Migration::Current
def self.up
rename_table "things", "awesome_things"
end
def self.down
rename_table "awesome_things", "things"
end
end
# frozen_string_literal: true
class WeNeedReminders < ActiveRecord::Migration::Current
def self.up
create_table("reminders") do |t|
t.column :content, :text
t.column :remind_at, :datetime
end
end
def self.down
drop_table "reminders"
end
end
# frozen_string_literal: true
class InnocentJointable < ActiveRecord::Migration::Current
def self.up
create_table("people_reminders", id: false) do |t|
t.column :reminder_id, :integer
t.column :person_id, :integer
end
end
def self.down
drop_table "people_reminders"
end
end
# frozen_string_literal: true
class WeNeedReminders < ActiveRecord::Migration::Current
def self.up
create_table("reminders") do |t|
t.column :content, :text
t.column :remind_at, :datetime
end
end
def self.down
drop_table "reminders"
end
end
# frozen_string_literal: true
class InnocentJointable < ActiveRecord::Migration::Current
def self.up
create_table("people_reminders", id: false) do |t|
t.column :reminder_id, :integer
t.column :person_id, :integer
end
end
def self.down
drop_table "people_reminders"
end
end
# frozen_string_literal: true
class AddExpressions < ActiveRecord::Migration::Current
def self.up
create_table("expressions") do |t|
t.column :expression, :string
end
end
def self.down
drop_table "expressions"
end
end
# frozen_string_literal: true
module ActiveRecord
module Validations
class AbsenceValidator < ActiveModel::Validations::AbsenceValidator # :nodoc:
def validate_each(record, attribute, association_or_value)
if record.class._reflect_on_association(attribute)
association_or_value = Array.wrap(association_or_value).reject(&:marked_for_destruction?)
end
super
end
end
module ClassMethods
# Validates that the specified attributes are not present (as defined by
# Object#present?). If the attribute is an association, the associated object
# is considered absent if it was marked for destruction.
#
# See ActiveModel::Validations::HelperMethods.validates_absence_of for more information.
def validates_absence_of(*attr_names)
validates_with AbsenceValidator, _merge_attributes(attr_names)
end
end
end
end
# frozen_string_literal: true
require "cases/helper"
require "models/face"
require "models/interest"
require "models/human"
require "models/topic"
class AbsenceValidationTest < ActiveRecord::TestCase
def test_non_association
boy_klass = Class.new(Human) do
def self.name; "Boy" end
validates_absence_of :name
end
assert_predicate boy_klass.new, :valid?
assert_not_predicate boy_klass.new(name: "Alex"), :valid?
end
def test_has_one_marked_for_destruction
boy_klass = Class.new(Human) do
def self.name; "Boy" end
validates_absence_of :face
end
boy = boy_klass.new(face: Face.new)
assert_not boy.valid?, "should not be valid if has_one association is present"
assert_equal 1, boy.errors[:face].size, "should only add one error"
boy.face.mark_for_destruction
assert boy.valid?, "should be valid if association is marked for destruction"
end
def test_has_many_marked_for_destruction
boy_klass = Class.new(Human) do
def self.name; "Boy" end
validates_absence_of :interests
end
boy = boy_klass.new
boy.interests << [i1 = Interest.new, i2 = Interest.new]
assert_not boy.valid?, "should not be valid if has_many association is present"
i1.mark_for_destruction
assert_not boy.valid?, "should not be valid if has_many association is present"
i2.mark_for_destruction
assert_predicate boy, :valid?
end
def test_does_not_call_to_a_on_associations
boy_klass = Class.new(Human) do
def self.name; "Boy" end
validates_absence_of :face
end
face_with_to_a = Face.new
def face_with_to_a.to_a; ["(/)", '(\)']; end
assert_nothing_raised { boy_klass.new(face: face_with_to_a).valid? }
end
def test_validates_absence_of_virtual_attribute_on_model
repair_validations(Interest) do
Interest.attr_accessor(:token)
Interest.validates_absence_of(:token)
interest = Interest.create!(topic: "Thought Leadering")
assert_predicate interest, :valid?
interest.token = "tl"
assert_predicate interest, :invalid?
end
end
end
# frozen_string_literal: true
require "set"
require "active_record/connection_adapters/sql_type_metadata"
require "active_record/connection_adapters/abstract/schema_dumper"
require "active_record/connection_adapters/abstract/schema_creation"
require "active_support/concurrency/load_interlock_aware_monitor"
require "arel/collectors/bind"
require "arel/collectors/composite"
require "arel/collectors/sql_string"
require "arel/collectors/substitute_binds"
module ActiveRecord
module ConnectionAdapters # :nodoc:
# Active Record supports multiple database systems. AbstractAdapter and
# related classes form the abstraction layer which makes this possible.
# An AbstractAdapter represents a connection to a database, and provides an
# abstract interface for database-specific functionality such as establishing
# a connection, escaping values, building the right SQL fragments for +:offset+
# and +:limit+ options, etc.
#
# All the concrete database adapters follow the interface laid down in this class.
# {ActiveRecord::Base.connection}[rdoc-ref:ConnectionHandling#connection] returns an AbstractAdapter object, which
# you can use.
#
# Most of the methods in the adapter are useful during migrations. Most
# notably, the instance methods provided by SchemaStatements are very useful.
class AbstractAdapter
ADAPTER_NAME = "Abstract"
include ActiveSupport::Callbacks
define_callbacks :checkout, :checkin
include Quoting, DatabaseStatements, SchemaStatements
include DatabaseLimits
include QueryCache
include Savepoints
SIMPLE_INT = /\A\d+\z/
COMMENT_REGEX = %r{(?:--.*\n)*|/\*(?:[^*]|\*[^/])*\*/}m
attr_accessor :pool
attr_reader :visitor, :owner, :logger, :lock
alias :in_use? :owner
set_callback :checkin, :after, :enable_lazy_transactions!
def self.type_cast_config_to_integer(config)
if config.is_a?(Integer)
config
elsif SIMPLE_INT.match?(config)
config.to_i
else
config
end
end
def self.type_cast_config_to_boolean(config)
if config == "false"
false
else
config
end
end
def self.validate_default_timezone(config)
case config
when nil
when "utc", "local"
config.to_sym
else
raise ArgumentError, "default_timezone must be either 'utc' or 'local'"
end
end
DEFAULT_READ_QUERY = [:begin, :commit, :explain, :release, :rollback, :savepoint, :select, :with] # :nodoc:
private_constant :DEFAULT_READ_QUERY
def self.build_read_query_regexp(*parts) # :nodoc:
parts += DEFAULT_READ_QUERY
parts = parts.map { |part| /#{part}/i }
/\A(?:[(\s]|#{COMMENT_REGEX})*#{Regexp.union(*parts)}/
end
def self.quoted_column_names # :nodoc:
@quoted_column_names ||= {}
end
def self.quoted_table_names # :nodoc:
@quoted_table_names ||= {}
end
def initialize(connection, logger = nil, config = {}) # :nodoc:
super()
@raw_connection = connection
@owner = nil
@instrumenter = ActiveSupport::Notifications.instrumenter
@logger = logger
@config = config
@pool = ActiveRecord::ConnectionAdapters::NullPool.new
@idle_since = Process.clock_gettime(Process::CLOCK_MONOTONIC)
@visitor = arel_visitor
@statements = build_statement_pool
@lock = ActiveSupport::Concurrency::LoadInterlockAwareMonitor.new
@prepared_statements = self.class.type_cast_config_to_boolean(
config.fetch(:prepared_statements, true)
)
@advisory_locks_enabled = self.class.type_cast_config_to_boolean(
config.fetch(:advisory_locks, true)
)
@default_timezone = self.class.validate_default_timezone(config[:default_timezone])
@raw_connection_dirty = false
configure_connection
end
EXCEPTION_NEVER = { Exception => :never }.freeze # :nodoc:
EXCEPTION_IMMEDIATE = { Exception => :immediate }.freeze # :nodoc:
private_constant :EXCEPTION_NEVER, :EXCEPTION_IMMEDIATE
def with_instrumenter(instrumenter, &block) # :nodoc:
Thread.handle_interrupt(EXCEPTION_NEVER) do
previous_instrumenter = @instrumenter
@instrumenter = instrumenter
Thread.handle_interrupt(EXCEPTION_IMMEDIATE, &block)
ensure
@instrumenter = previous_instrumenter
end
end
def check_if_write_query(sql) # :nodoc:
if preventing_writes? && write_query?(sql)
raise ActiveRecord::ReadOnlyError, "Write query attempted while in readonly mode: #{sql}"
end
end
def replica?
@config[:replica] || false
end
def use_metadata_table?
@config.fetch(:use_metadata_table, true)
end
def default_timezone
@default_timezone || ActiveRecord.default_timezone
end
# Determines whether writes are currently being prevented.
#
# Returns true if the connection is a replica.
#
# If the application is using legacy handling, returns
# true if +connection_handler.prevent_writes+ is set.
#
# If the application is using the new connection handling
# will return true based on +current_preventing_writes+.
def preventing_writes?
return true if replica?
return ActiveRecord::Base.connection_handler.prevent_writes if ActiveRecord.legacy_connection_handling
return false if connection_class.nil?
connection_class.current_preventing_writes
end
def migrations_paths # :nodoc:
@config[:migrations_paths] || Migrator.migrations_paths
end
def migration_context # :nodoc:
MigrationContext.new(migrations_paths, schema_migration)
end
def schema_migration # :nodoc:
@schema_migration ||= begin
conn = self
spec_name = conn.pool.pool_config.connection_specification_name
return ActiveRecord::SchemaMigration if spec_name == "ActiveRecord::Base"
schema_migration_name = "#{spec_name}::SchemaMigration"
Class.new(ActiveRecord::SchemaMigration) do
define_singleton_method(:name) { schema_migration_name }
define_singleton_method(:to_s) { schema_migration_name }
self.connection_specification_name = spec_name
end
end
end
def prepared_statements?
@prepared_statements && !prepared_statements_disabled_cache.include?(object_id)
end
alias :prepared_statements :prepared_statements?
def prepared_statements_disabled_cache # :nodoc:
ActiveSupport::IsolatedExecutionState[:active_record_prepared_statements_disabled_cache] ||= Set.new
end
class Version
include Comparable
attr_reader :full_version_string
def initialize(version_string, full_version_string = nil)
@version = version_string.split(".").map(&:to_i)
@full_version_string = full_version_string
end
def <=>(version_string)
@version <=> version_string.split(".").map(&:to_i)
end
def to_s
@version.join(".")
end
end
def valid_type?(type) # :nodoc:
!native_database_types[type].nil?
end
# this method must only be called while holding connection pool's mutex
def lease
if in_use?
msg = +"Cannot lease connection, "
if @owner == ActiveSupport::IsolatedExecutionState.context
msg << "it is already leased by the current thread."
else
msg << "it is already in use by a different thread: #{@owner}. " \
"Current thread: #{ActiveSupport::IsolatedExecutionState.context}."
end
raise ActiveRecordError, msg
end
@owner = ActiveSupport::IsolatedExecutionState.context
end
def connection_class # :nodoc:
@pool.connection_class
end
# The role (e.g. +:writing+) for the current connection. In a
# non-multi role application, +:writing+ is returned.
def role
@pool.role
end
# The shard (e.g. +:default+) for the current connection. In
# a non-sharded application, +:default+ is returned.
def shard
@pool.shard
end
def schema_cache
@pool.get_schema_cache(self)
end
def schema_cache=(cache)
cache.connection = self
@pool.set_schema_cache(cache)
end
# this method must only be called while holding connection pool's mutex
def expire
if in_use?
if @owner != ActiveSupport::IsolatedExecutionState.context
raise ActiveRecordError, "Cannot expire connection, " \
"it is owned by a different thread: #{@owner}. " \
"Current thread: #{ActiveSupport::IsolatedExecutionState.context}."
end
@idle_since = Process.clock_gettime(Process::CLOCK_MONOTONIC)
@owner = nil
else
raise ActiveRecordError, "Cannot expire connection, it is not currently leased."
end
end
# this method must only be called while holding connection pool's mutex (and a desire for segfaults)
def steal! # :nodoc:
if in_use?
if @owner != ActiveSupport::IsolatedExecutionState.context
pool.send :remove_connection_from_thread_cache, self, @owner
@owner = ActiveSupport::IsolatedExecutionState.context
end
else
raise ActiveRecordError, "Cannot steal connection, it is not currently leased."
end
end
# Seconds since this connection was returned to the pool
def seconds_idle # :nodoc:
return 0 if in_use?
Process.clock_gettime(Process::CLOCK_MONOTONIC) - @idle_since
end
def unprepared_statement
cache = prepared_statements_disabled_cache.add?(object_id) if @prepared_statements
yield
ensure
cache&.delete(object_id)
end
# Returns the human-readable name of the adapter. Use mixed case - one
# can always use downcase if needed.
def adapter_name
self.class::ADAPTER_NAME
end
# Does the database for this adapter exist?
def self.database_exists?(config)
raise NotImplementedError
end
# Does this adapter support DDL rollbacks in transactions? That is, would
# CREATE TABLE or ALTER TABLE get rolled back by a transaction?
def supports_ddl_transactions?
false
end
def supports_bulk_alter?
false
end
# Does this adapter support savepoints?
def supports_savepoints?
false
end
# Do TransactionRollbackErrors on savepoints affect the parent
# transaction?
def savepoint_errors_invalidate_transactions?
false
end
def supports_restart_db_transaction?
false
end
# Does this adapter support application-enforced advisory locking?
def supports_advisory_locks?
false
end
# Should primary key values be selected from their corresponding
# sequence before the insert statement? If true, next_sequence_value
# is called before each insert to set the record's primary key.
def prefetch_primary_key?(table_name = nil)
false
end
def supports_partitioned_indexes?
false
end
# Does this adapter support index sort order?
def supports_index_sort_order?
false
end
# Does this adapter support partial indices?
def supports_partial_index?
false
end
# Does this adapter support expression indices?
def supports_expression_index?
false
end
# Does this adapter support explain?
def supports_explain?
false
end
# Does this adapter support setting the isolation level for a transaction?
def supports_transaction_isolation?
false
end
# Does this adapter support database extensions?
def supports_extensions?
false
end
# Does this adapter support creating indexes in the same statement as
# creating the table?
def supports_indexes_in_create?
false
end
# Does this adapter support creating foreign key constraints?
def supports_foreign_keys?
false
end
# Does this adapter support creating invalid constraints?
def supports_validate_constraints?
false
end
# Does this adapter support creating deferrable constraints?
def supports_deferrable_constraints?
false
end
# Does this adapter support creating check constraints?
def supports_check_constraints?
false
end
# Does this adapter support views?
def supports_views?
false
end
# Does this adapter support materialized views?
def supports_materialized_views?
false
end
# Does this adapter support datetime with precision?
def supports_datetime_with_precision?
false
end
# Does this adapter support json data type?
def supports_json?
false
end
# Does this adapter support metadata comments on database objects (tables, columns, indexes)?
def supports_comments?
false
end
# Can comments for tables, columns, and indexes be specified in create/alter table statements?
def supports_comments_in_create?
false
end
# Does this adapter support virtual columns?
def supports_virtual_columns?
false
end
# Does this adapter support foreign/external tables?
def supports_foreign_tables?
false
end
# Does this adapter support optimizer hints?
def supports_optimizer_hints?
false
end
def supports_common_table_expressions?
false
end
def supports_lazy_transactions?
false
end
def supports_insert_returning?
false
end
def supports_insert_on_duplicate_skip?
false
end
def supports_insert_on_duplicate_update?
false
end
def supports_insert_conflict_target?
false
end
def supports_concurrent_connections?
true
end
def async_enabled? # :nodoc:
supports_concurrent_connections? &&
!ActiveRecord.async_query_executor.nil? && !pool.async_executor.nil?
end
# This is meant to be implemented by the adapters that support extensions
def disable_extension(name)
end
# This is meant to be implemented by the adapters that support extensions
def enable_extension(name)
end
# This is meant to be implemented by the adapters that support custom enum types
def create_enum(*) # :nodoc:
end
def advisory_locks_enabled? # :nodoc:
supports_advisory_locks? && @advisory_locks_enabled
end
# This is meant to be implemented by the adapters that support advisory
# locks
#
# Return true if we got the lock, otherwise false
def get_advisory_lock(lock_id) # :nodoc:
end
# This is meant to be implemented by the adapters that support advisory
# locks.
#
# Return true if we released the lock, otherwise false
def release_advisory_lock(lock_id) # :nodoc:
end
# A list of extensions, to be filled in by adapters that support them.
def extensions
[]
end
# A list of index algorithms, to be filled by adapters that support them.
def index_algorithms
{}
end
# REFERENTIAL INTEGRITY ====================================
# Override to turn off referential integrity while executing <tt>&block</tt>.
def disable_referential_integrity
yield
end
# Override to check all foreign key constraints in a database.
def all_foreign_keys_valid?
true
end
# CONNECTION MANAGEMENT ====================================
# Checks whether the connection to the database is still active. This includes
# checking whether the database is actually capable of responding, i.e. whether
# the connection isn't stale.
def active?
end
# Disconnects from the database if already connected, and establishes a
# new connection with the database. Implementors should call super
# immediately after establishing the new connection (and while still
# holding @lock).
def reconnect!(restore_transactions: false)
reset_transaction(restore: restore_transactions) do
clear_cache!(new_connection: true)
configure_connection
end
end
# Disconnects from the database if already connected. Otherwise, this
# method does nothing.
def disconnect!
clear_cache!(new_connection: true)
reset_transaction
end
# Immediately forget this connection ever existed. Unlike disconnect!,
# this will not communicate with the server.
#
# After calling this method, the behavior of all other methods becomes
# undefined. This is called internally just before a forked process gets
# rid of a connection that belonged to its parent.
def discard!
# This should be overridden by concrete adapters.
#
# Prevent @raw_connection's finalizer from touching the socket, or
# otherwise communicating with its server, when it is collected.
if schema_cache.connection == self
schema_cache.connection = nil
end
end
# Reset the state of this connection, directing the DBMS to clear
# transactions and other connection-related server-side state. Usually a
# database-dependent operation.
#
# If a database driver or protocol does not support such a feature,
# implementors may alias this to #reconnect!. Otherwise, implementors
# should call super immediately after resetting the connection (and while
# still holding @lock).
def reset!
clear_cache!(new_connection: true)
reset_transaction
configure_connection
end
# Removes the connection from the pool and disconnect it.
def throw_away!
pool.remove self
disconnect!
end
# Clear any caching the database adapter may be doing.
def clear_cache!(new_connection: false)
if @statements
@lock.synchronize do
if new_connection
@statements.reset
else
@statements.clear
end
end
end
end
# Returns true if its required to reload the connection between requests for development mode.
def requires_reloading?
false
end
# Checks whether the connection to the database is still active (i.e. not stale).
# This is done under the hood by calling #active?. If the connection
# is no longer active, then this method will reconnect to the database.
def verify!
reconnect! unless active?
end
# Provides access to the underlying database driver for this adapter. For
# example, this method returns a Mysql2::Client object in case of Mysql2Adapter,
# and a PG::Connection object in case of PostgreSQLAdapter.
#
# This is useful for when you need to call a proprietary method such as
# PostgreSQL's lo_* methods.
def raw_connection
disable_lazy_transactions!
@raw_connection_dirty = true
@raw_connection
end
def default_uniqueness_comparison(attribute, value) # :nodoc:
attribute.eq(value)
end
def case_sensitive_comparison(attribute, value) # :nodoc:
attribute.eq(value)
end
def case_insensitive_comparison(attribute, value) # :nodoc:
column = column_for_attribute(attribute)
if can_perform_case_insensitive_comparison_for?(column)
attribute.lower.eq(attribute.relation.lower(value))
else
attribute.eq(value)
end
end
def can_perform_case_insensitive_comparison_for?(column)
true
end
private :can_perform_case_insensitive_comparison_for?
# Check the connection back in to the connection pool
def close
pool.checkin self
end
def default_index_type?(index) # :nodoc:
index.using.nil?
end
# Called by ActiveRecord::InsertAll,
# Passed an instance of ActiveRecord::InsertAll::Builder,
# This method implements standard bulk inserts for all databases, but
# should be overridden by adapters to implement common features with
# non-standard syntax like handling duplicates or returning values.
def build_insert_sql(insert) # :nodoc:
if insert.skip_duplicates? || insert.update_duplicates?
raise NotImplementedError, "#{self.class} should define `build_insert_sql` to implement adapter-specific logic for handling duplicates during INSERT"
end
"INSERT #{insert.into} #{insert.values_list}"
end
def get_database_version # :nodoc:
end
def database_version # :nodoc:
schema_cache.database_version
end
def check_version # :nodoc:
end
# Returns the version identifier of the schema currently available in
# the database. This is generally equal to the number of the highest-
# numbered migration that has been executed, or 0 if no schema
# information is present / the database is empty.
def schema_version
migration_context.current_version
end
def field_ordered_value(column, values) # :nodoc:
node = Arel::Nodes::Case.new(column)
values.each.with_index(1) do |value, order|
node.when(value).then(order)
end
Arel::Nodes::Ascending.new(node.else(values.length + 1))
end
class << self
def register_class_with_precision(mapping, key, klass, **kwargs) # :nodoc:
mapping.register_type(key) do |*args|
precision = extract_precision(args.last)
klass.new(precision: precision, **kwargs)
end
end
def extended_type_map(default_timezone:) # :nodoc:
Type::TypeMap.new(self::TYPE_MAP).tap do |m|
register_class_with_precision m, %r(\A[^\(]*time)i, Type::Time, timezone: default_timezone
register_class_with_precision m, %r(\A[^\(]*datetime)i, Type::DateTime, timezone: default_timezone
m.alias_type %r(\A[^\(]*timestamp)i, "datetime"
end
end
private
def initialize_type_map(m)
register_class_with_limit m, %r(boolean)i, Type::Boolean
register_class_with_limit m, %r(char)i, Type::String
register_class_with_limit m, %r(binary)i, Type::Binary
register_class_with_limit m, %r(text)i, Type::Text
register_class_with_precision m, %r(date)i, Type::Date
register_class_with_precision m, %r(time)i, Type::Time
register_class_with_precision m, %r(datetime)i, Type::DateTime
register_class_with_limit m, %r(float)i, Type::Float
register_class_with_limit m, %r(int)i, Type::Integer
m.alias_type %r(blob)i, "binary"
m.alias_type %r(clob)i, "text"
m.alias_type %r(timestamp)i, "datetime"
m.alias_type %r(numeric)i, "decimal"
m.alias_type %r(number)i, "decimal"
m.alias_type %r(double)i, "float"
m.register_type %r(^json)i, Type::Json.new
m.register_type(%r(decimal)i) do |sql_type|
scale = extract_scale(sql_type)
precision = extract_precision(sql_type)
if scale == 0
# FIXME: Remove this class as well
Type::DecimalWithoutScale.new(precision: precision)
else
Type::Decimal.new(precision: precision, scale: scale)
end
end
end
def register_class_with_limit(mapping, key, klass)
mapping.register_type(key) do |*args|
limit = extract_limit(args.last)
klass.new(limit: limit)
end
end
def extract_scale(sql_type)
case sql_type
when /\((\d+)\)/ then 0
when /\((\d+)(,(\d+))\)/ then $3.to_i
end
end
def extract_precision(sql_type)
$1.to_i if sql_type =~ /\((\d+)(,\d+)?\)/
end
def extract_limit(sql_type)
$1.to_i if sql_type =~ /\((.*)\)/
end
end
TYPE_MAP = Type::TypeMap.new.tap { |m| initialize_type_map(m) }
EXTENDED_TYPE_MAPS = Concurrent::Map.new
private
def reconnect_can_restore_state?
transaction_manager.restorable? && !@raw_connection_dirty
end
def extended_type_map_key
if @default_timezone
{ default_timezone: @default_timezone }
end
end
def type_map
if key = extended_type_map_key
self.class::EXTENDED_TYPE_MAPS.compute_if_absent(key) do
self.class.extended_type_map(**key)
end
else
self.class::TYPE_MAP
end
end
def translate_exception_class(e, sql, binds)
message = "#{e.class.name}: #{e.message}"
exception = translate_exception(
e, message: message, sql: sql, binds: binds
)
exception.set_backtrace e.backtrace
exception
end
def log(sql, name = "SQL", binds = [], type_casted_binds = [], statement_name = nil, async: false, &block) # :doc:
@instrumenter.instrument(
"sql.active_record",
sql: sql,
name: name,
binds: binds,
type_casted_binds: type_casted_binds,
statement_name: statement_name,
async: async,
connection: self) do
@lock.synchronize(&block)
rescue => e
raise translate_exception_class(e, sql, binds)
end
end
def transform_query(sql)
ActiveRecord.query_transformers.each do |transformer|
sql = transformer.call(sql)
end
sql
end
def translate_exception(exception, message:, sql:, binds:)
# override in derived class
case exception
when RuntimeError
exception
else
ActiveRecord::StatementInvalid.new(message, sql: sql, binds: binds)
end
end
def without_prepared_statement?(binds)
!prepared_statements || binds.empty?
end
def column_for(table_name, column_name)
column_name = column_name.to_s
columns(table_name).detect { |c| c.name == column_name } ||
raise(ActiveRecordError, "No such column: #{table_name}.#{column_name}")
end
def column_for_attribute(attribute)
table_name = attribute.relation.name
schema_cache.columns_hash(table_name)[attribute.name.to_s]
end
def collector
if prepared_statements
Arel::Collectors::Composite.new(
Arel::Collectors::SQLString.new,
Arel::Collectors::Bind.new,
)
else
Arel::Collectors::SubstituteBinds.new(
self,
Arel::Collectors::SQLString.new,
)
end
end
def arel_visitor
Arel::Visitors::ToSql.new(self)
end
def build_statement_pool
end
# Builds the result object.
#
# This is an internal hook to make possible connection adapters to build
# custom result objects with connection-specific data.
def build_result(columns:, rows:, column_types: {})
ActiveRecord::Result.new(columns, rows, column_types)
end
# Perform any necessary initialization upon the newly-established
# @raw_connection -- this is the place to modify the adapter's
# connection settings, run queries to configure any application-global
# "session" variables, etc.
#
# Implementations may assume this method will only be called while
# holding @lock (or from #initialize).
def configure_connection
end
end
end
end
# frozen_string_literal: true
require "active_record/connection_adapters/abstract_adapter"
require "active_record/connection_adapters/statement_pool"
require "active_record/connection_adapters/mysql/column"
require "active_record/connection_adapters/mysql/explain_pretty_printer"
require "active_record/connection_adapters/mysql/quoting"
require "active_record/connection_adapters/mysql/schema_creation"
require "active_record/connection_adapters/mysql/schema_definitions"
require "active_record/connection_adapters/mysql/schema_dumper"
require "active_record/connection_adapters/mysql/schema_statements"
require "active_record/connection_adapters/mysql/type_metadata"
module ActiveRecord
module ConnectionAdapters
class AbstractMysqlAdapter < AbstractAdapter
include MySQL::Quoting
include MySQL::SchemaStatements
##
# :singleton-method:
# By default, the Mysql2Adapter will consider all columns of type <tt>tinyint(1)</tt>
# as boolean. If you wish to disable this emulation you can add the following line
# to your application.rb file:
#
# ActiveRecord::ConnectionAdapters::Mysql2Adapter.emulate_booleans = false
class_attribute :emulate_booleans, default: true
NATIVE_DATABASE_TYPES = {
primary_key: "bigint auto_increment PRIMARY KEY",
string: { name: "varchar", limit: 255 },
text: { name: "text" },
integer: { name: "int", limit: 4 },
bigint: { name: "bigint" },
float: { name: "float", limit: 24 },
decimal: { name: "decimal" },
datetime: { name: "datetime" },
timestamp: { name: "timestamp" },
time: { name: "time" },
date: { name: "date" },
binary: { name: "blob" },
blob: { name: "blob" },
boolean: { name: "tinyint", limit: 1 },
json: { name: "json" },
}
class StatementPool < ConnectionAdapters::StatementPool # :nodoc:
private
def dealloc(stmt)
stmt.close
end
end
def initialize(connection, logger, connection_options, config)
super(connection, logger, config)
end
def get_database_version # :nodoc:
full_version_string = get_full_version
version_string = version_string(full_version_string)
Version.new(version_string, full_version_string)
end
def mariadb? # :nodoc:
/mariadb/i.match?(full_version)
end
def supports_bulk_alter?
true
end
def supports_index_sort_order?
!mariadb? && database_version >= "8.0.1"
end
def supports_expression_index?
!mariadb? && database_version >= "8.0.13"
end
def supports_transaction_isolation?
true
end
def supports_restart_db_transaction?
true
end
def supports_explain?
true
end
def supports_indexes_in_create?
true
end
def supports_foreign_keys?
true
end
def supports_check_constraints?
if mariadb?
database_version >= "10.2.1"
else
database_version >= "8.0.16"
end
end
def supports_views?
true
end
def supports_datetime_with_precision?
mariadb? || database_version >= "5.6.4"
end
def supports_virtual_columns?
mariadb? || database_version >= "5.7.5"
end
# See https://dev.mysql.com/doc/refman/en/optimizer-hints.html for more details.
def supports_optimizer_hints?
!mariadb? && database_version >= "5.7.7"
end
def supports_common_table_expressions?
if mariadb?
database_version >= "10.2.1"
else
database_version >= "8.0.1"
end
end
def supports_advisory_locks?
true
end
def supports_insert_on_duplicate_skip?
true
end
def supports_insert_on_duplicate_update?
true
end
def field_ordered_value(column, values) # :nodoc:
field = Arel::Nodes::NamedFunction.new("FIELD", [column, values.reverse])
Arel::Nodes::Descending.new(field)
end
def get_advisory_lock(lock_name, timeout = 0) # :nodoc:
query_value("SELECT GET_LOCK(#{quote(lock_name.to_s)}, #{timeout})") == 1
end
def release_advisory_lock(lock_name) # :nodoc:
query_value("SELECT RELEASE_LOCK(#{quote(lock_name.to_s)})") == 1
end
def native_database_types
NATIVE_DATABASE_TYPES
end
def index_algorithms
{
default: "ALGORITHM = DEFAULT",
copy: "ALGORITHM = COPY",
inplace: "ALGORITHM = INPLACE",
instant: "ALGORITHM = INSTANT",
}
end
# HELPER METHODS ===========================================
# The two drivers have slightly different ways of yielding hashes of results, so
# this method must be implemented to provide a uniform interface.
def each_hash(result) # :nodoc:
raise NotImplementedError
end
# Must return the MySQL error number from the exception, if the exception has an
# error number.
def error_number(exception) # :nodoc:
raise NotImplementedError
end
# REFERENTIAL INTEGRITY ====================================
def disable_referential_integrity # :nodoc:
old = query_value("SELECT @@FOREIGN_KEY_CHECKS")
begin
update("SET FOREIGN_KEY_CHECKS = 0")
yield
ensure
update("SET FOREIGN_KEY_CHECKS = #{old}")
end
end
#--
# DATABASE STATEMENTS ======================================
#++
# Executes the SQL statement in the context of this connection.
def execute(sql, name = nil, async: false)
raw_execute(sql, name, async: async)
end
# Mysql2Adapter doesn't have to free a result after using it, but we use this method
# to write stuff in an abstract way without concerning ourselves about whether it
# needs to be explicitly freed or not.
def execute_and_free(sql, name = nil, async: false) # :nodoc:
yield execute(sql, name, async: async)
end
def begin_db_transaction # :nodoc:
execute("BEGIN", "TRANSACTION")
end
def begin_isolated_db_transaction(isolation) # :nodoc:
execute "SET TRANSACTION ISOLATION LEVEL #{transaction_isolation_levels.fetch(isolation)}"
begin_db_transaction
end
def commit_db_transaction # :nodoc:
execute("COMMIT", "TRANSACTION")
end
def exec_rollback_db_transaction # :nodoc:
execute("ROLLBACK", "TRANSACTION")
end
def exec_restart_db_transaction # :nodoc:
execute("ROLLBACK AND CHAIN", "TRANSACTION")
end
def empty_insert_statement_value(primary_key = nil) # :nodoc:
"VALUES ()"
end
# SCHEMA STATEMENTS ========================================
# Drops the database specified on the +name+ attribute
# and creates it again using the provided +options+.
def recreate_database(name, options = {})
drop_database(name)
sql = create_database(name, options)
reconnect!
sql
end
# Create a new MySQL database with optional <tt>:charset</tt> and <tt>:collation</tt>.
# Charset defaults to utf8mb4.
#
# Example:
# create_database 'charset_test', charset: 'latin1', collation: 'latin1_bin'
# create_database 'matt_development'
# create_database 'matt_development', charset: :big5
def create_database(name, options = {})
if options[:collation]
execute "CREATE DATABASE #{quote_table_name(name)} DEFAULT COLLATE #{quote_table_name(options[:collation])}"
elsif options[:charset]
execute "CREATE DATABASE #{quote_table_name(name)} DEFAULT CHARACTER SET #{quote_table_name(options[:charset])}"
elsif row_format_dynamic_by_default?
execute "CREATE DATABASE #{quote_table_name(name)} DEFAULT CHARACTER SET `utf8mb4`"
else
raise "Configure a supported :charset and ensure innodb_large_prefix is enabled to support indexes on varchar(255) string columns."
end
end
# Drops a MySQL database.
#
# Example:
# drop_database('sebastian_development')
def drop_database(name) # :nodoc:
execute "DROP DATABASE IF EXISTS #{quote_table_name(name)}"
end
def current_database
query_value("SELECT database()", "SCHEMA")
end
# Returns the database character set.
def charset
show_variable "character_set_database"
end
# Returns the database collation strategy.
def collation
show_variable "collation_database"
end
def table_comment(table_name) # :nodoc:
scope = quoted_scope(table_name)
query_value(<<~SQL, "SCHEMA").presence
SELECT table_comment
FROM information_schema.tables
WHERE table_schema = #{scope[:schema]}
AND table_name = #{scope[:name]}
SQL
end
def change_table_comment(table_name, comment_or_changes) # :nodoc:
comment = extract_new_comment_value(comment_or_changes)
comment = "" if comment.nil?
execute("ALTER TABLE #{quote_table_name(table_name)} COMMENT #{quote(comment)}")
end
# Renames a table.
#
# Example:
# rename_table('octopuses', 'octopi')
def rename_table(table_name, new_name)
schema_cache.clear_data_source_cache!(table_name.to_s)
schema_cache.clear_data_source_cache!(new_name.to_s)
execute "RENAME TABLE #{quote_table_name(table_name)} TO #{quote_table_name(new_name)}"
rename_table_indexes(table_name, new_name)
end
# Drops a table from the database.
#
# [<tt>:force</tt>]
# Set to +:cascade+ to drop dependent objects as well.
# Defaults to false.
# [<tt>:if_exists</tt>]
# Set to +true+ to only drop the table if it exists.
# Defaults to false.
# [<tt>:temporary</tt>]
# Set to +true+ to drop temporary table.
# Defaults to false.
#
# Although this command ignores most +options+ and the block if one is given,
# it can be helpful to provide these in a migration's +change+ method so it can be reverted.
# In that case, +options+ and the block will be used by create_table.
def drop_table(table_name, **options)
schema_cache.clear_data_source_cache!(table_name.to_s)
execute "DROP#{' TEMPORARY' if options[:temporary]} TABLE#{' IF EXISTS' if options[:if_exists]} #{quote_table_name(table_name)}#{' CASCADE' if options[:force] == :cascade}"
end
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
end
end
def change_column_default(table_name, column_name, default_or_changes) # :nodoc:
default = extract_new_default_value(default_or_changes)
change_column table_name, column_name, nil, default: default
end
def change_column_null(table_name, column_name, null, default = nil) # :nodoc:
unless null || default.nil?
execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL")
end
change_column table_name, column_name, nil, null: null
end
def change_column_comment(table_name, column_name, comment_or_changes) # :nodoc:
comment = extract_new_comment_value(comment_or_changes)
change_column table_name, column_name, nil, comment: comment
end
def change_column(table_name, column_name, type, **options) # :nodoc:
execute("ALTER TABLE #{quote_table_name(table_name)} #{change_column_for_alter(table_name, column_name, type, **options)}")
end
def rename_column(table_name, column_name, new_column_name) # :nodoc:
execute("ALTER TABLE #{quote_table_name(table_name)} #{rename_column_for_alter(table_name, column_name, new_column_name)}")
rename_column_indexes(table_name, column_name, new_column_name)
end
def add_index(table_name, column_name, **options) # :nodoc:
index, algorithm, if_not_exists = add_index_options(table_name, column_name, **options)
return if if_not_exists && index_exists?(table_name, column_name, name: index.name)
create_index = CreateIndexDefinition.new(index, algorithm)
execute schema_creation.accept(create_index)
end
def add_sql_comment!(sql, comment) # :nodoc:
sql << " COMMENT #{quote(comment)}" if comment.present?
sql
end
def foreign_keys(table_name)
raise ArgumentError unless table_name.present?
scope = quoted_scope(table_name)
fk_info = exec_query(<<~SQL, "SCHEMA")
SELECT fk.referenced_table_name AS 'to_table',
fk.referenced_column_name AS 'primary_key',
fk.column_name AS 'column',
fk.constraint_name AS 'name',
rc.update_rule AS 'on_update',
rc.delete_rule AS 'on_delete'
FROM information_schema.referential_constraints rc
JOIN information_schema.key_column_usage fk
USING (constraint_schema, constraint_name)
WHERE fk.referenced_column_name IS NOT NULL
AND fk.table_schema = #{scope[:schema]}
AND fk.table_name = #{scope[:name]}
AND rc.constraint_schema = #{scope[:schema]}
AND rc.table_name = #{scope[:name]}
SQL
fk_info.map do |row|
options = {
column: row["column"],
name: row["name"],
primary_key: row["primary_key"]
}
options[:on_update] = extract_foreign_key_action(row["on_update"])
options[:on_delete] = extract_foreign_key_action(row["on_delete"])
ForeignKeyDefinition.new(table_name, row["to_table"], options)
end
end
def check_constraints(table_name)
if supports_check_constraints?
scope = quoted_scope(table_name)
sql = <<~SQL
SELECT cc.constraint_name AS 'name',
cc.check_clause AS 'expression'
FROM information_schema.check_constraints cc
JOIN information_schema.table_constraints tc
USING (constraint_schema, constraint_name)
WHERE tc.table_schema = #{scope[:schema]}
AND tc.table_name = #{scope[:name]}
AND cc.constraint_schema = #{scope[:schema]}
SQL
sql += " AND cc.table_name = #{scope[:name]}" if mariadb?
chk_info = exec_query(sql, "SCHEMA")
chk_info.map do |row|
options = {
name: row["name"]
}
expression = row["expression"]
expression = expression[1..-2] unless mariadb? # remove parentheses added by mysql
CheckConstraintDefinition.new(table_name, expression, options)
end
else
raise NotImplementedError
end
end
def table_options(table_name) # :nodoc:
create_table_info = create_table_info(table_name)
# strip create_definitions and partition_options
# Be aware that `create_table_info` might not include any table options due to `NO_TABLE_OPTIONS` sql mode.
raw_table_options = create_table_info.sub(/\A.*\n\) ?/m, "").sub(/\n\/\*!.*\*\/\n\z/m, "").strip
return if raw_table_options.empty?
table_options = {}
if / DEFAULT CHARSET=(?<charset>\w+)(?: COLLATE=(?<collation>\w+))?/ =~ raw_table_options
raw_table_options = $` + $' # before part + after part
table_options[:charset] = charset
table_options[:collation] = collation if collation
end
# strip AUTO_INCREMENT
raw_table_options.sub!(/(ENGINE=\w+)(?: AUTO_INCREMENT=\d+)/, '\1')
# strip COMMENT
if raw_table_options.sub!(/ COMMENT='.+'/, "")
table_options[:comment] = table_comment(table_name)
end
table_options[:options] = raw_table_options unless raw_table_options == "ENGINE=InnoDB"
table_options
end
# SHOW VARIABLES LIKE 'name'
def show_variable(name)
query_value("SELECT @@#{name}", "SCHEMA")
rescue ActiveRecord::StatementInvalid
nil
end
def primary_keys(table_name) # :nodoc:
raise ArgumentError unless table_name.present?
scope = quoted_scope(table_name)
query_values(<<~SQL, "SCHEMA")
SELECT column_name
FROM information_schema.statistics
WHERE index_name = 'PRIMARY'
AND table_schema = #{scope[:schema]}
AND table_name = #{scope[:name]}
ORDER BY seq_in_index
SQL
end
def case_sensitive_comparison(attribute, value) # :nodoc:
column = column_for_attribute(attribute)
if column.collation && !column.case_sensitive?
attribute.eq(Arel::Nodes::Bin.new(value))
else
super
end
end
def can_perform_case_insensitive_comparison_for?(column)
column.case_sensitive?
end
private :can_perform_case_insensitive_comparison_for?
# In MySQL 5.7.5 and up, ONLY_FULL_GROUP_BY affects handling of queries that use
# DISTINCT and ORDER BY. It requires the ORDER BY columns in the select list for
# distinct queries, and requires that the ORDER BY include the distinct column.
# See https://dev.mysql.com/doc/refman/en/group-by-handling.html
def columns_for_distinct(columns, orders) # :nodoc:
order_columns = orders.compact_blank.map { |s|
# Convert Arel node to string
s = visitor.compile(s) unless s.is_a?(String)
# Remove any ASC/DESC modifiers
s.gsub(/\s+(?:ASC|DESC)\b/i, "")
}.compact_blank.map.with_index { |column, i| "#{column} AS alias_#{i}" }
(order_columns << super).join(", ")
end
def strict_mode?
self.class.type_cast_config_to_boolean(@config.fetch(:strict, true))
end
def default_index_type?(index) # :nodoc:
index.using == :btree || super
end
def build_insert_sql(insert) # :nodoc:
sql = +"INSERT #{insert.into} #{insert.values_list}"
if insert.skip_duplicates?
no_op_column = quote_column_name(insert.keys.first)
sql << " ON DUPLICATE KEY UPDATE #{no_op_column}=#{no_op_column}"
elsif insert.update_duplicates?
sql << " ON DUPLICATE KEY UPDATE "
if insert.raw_update_sql?
sql << insert.raw_update_sql
else
sql << insert.touch_model_timestamps_unless { |column| "#{column}<=>VALUES(#{column})" }
sql << insert.updatable_columns.map { |column| "#{column}=VALUES(#{column})" }.join(",")
end
end
sql
end
def check_version # :nodoc:
if database_version < "5.5.8"
raise "Your version of MySQL (#{database_version}) is too old. Active Record supports MySQL >= 5.5.8."
end
end
class << self
def extended_type_map(default_timezone: nil, emulate_booleans:) # :nodoc:
super(default_timezone: default_timezone).tap do |m|
if emulate_booleans
m.register_type %r(^tinyint\(1\))i, Type::Boolean.new
end
end
end
private
def initialize_type_map(m)
super
m.register_type(%r(char)i) do |sql_type|
limit = extract_limit(sql_type)
Type.lookup(:string, adapter: :mysql2, limit: limit)
end
m.register_type %r(tinytext)i, Type::Text.new(limit: 2**8 - 1)
m.register_type %r(tinyblob)i, Type::Binary.new(limit: 2**8 - 1)
m.register_type %r(text)i, Type::Text.new(limit: 2**16 - 1)
m.register_type %r(blob)i, Type::Binary.new(limit: 2**16 - 1)
m.register_type %r(mediumtext)i, Type::Text.new(limit: 2**24 - 1)
m.register_type %r(mediumblob)i, Type::Binary.new(limit: 2**24 - 1)
m.register_type %r(longtext)i, Type::Text.new(limit: 2**32 - 1)
m.register_type %r(longblob)i, Type::Binary.new(limit: 2**32 - 1)
m.register_type %r(^float)i, Type::Float.new(limit: 24)
m.register_type %r(^double)i, Type::Float.new(limit: 53)
register_integer_type m, %r(^bigint)i, limit: 8
register_integer_type m, %r(^int)i, limit: 4
register_integer_type m, %r(^mediumint)i, limit: 3
register_integer_type m, %r(^smallint)i, limit: 2
register_integer_type m, %r(^tinyint)i, limit: 1
m.alias_type %r(year)i, "integer"
m.alias_type %r(bit)i, "binary"
m.register_type %r(^enum)i, Type.lookup(:string, adapter: :mysql2)
m.register_type %r(^set)i, Type.lookup(:string, adapter: :mysql2)
end
def register_integer_type(mapping, key, **options)
mapping.register_type(key) do |sql_type|
if /\bunsigned\b/.match?(sql_type)
Type::UnsignedInteger.new(**options)
else
Type::Integer.new(**options)
end
end
end
def extract_precision(sql_type)
if /\A(?:date)?time(?:stamp)?\b/.match?(sql_type)
super || 0
else
super
end
end
end
TYPE_MAP = Type::TypeMap.new.tap { |m| initialize_type_map(m) }
EXTENDED_TYPE_MAPS = Concurrent::Map.new
EMULATE_BOOLEANS_TRUE = { emulate_booleans: true }.freeze
private
def extended_type_map_key
if @default_timezone
{ default_timezone: @default_timezone, emulate_booleans: emulate_booleans }
elsif emulate_booleans
EMULATE_BOOLEANS_TRUE
end
end
def raw_execute(sql, name, async: false)
materialize_transactions
mark_transaction_written_if_write(sql)
log(sql, name, async: async) do
ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
@raw_connection.query(sql)
end
end
end
# See https://dev.mysql.com/doc/mysql-errors/en/server-error-reference.html
ER_DB_CREATE_EXISTS = 1007
ER_FILSORT_ABORT = 1028
ER_DUP_ENTRY = 1062
ER_NOT_NULL_VIOLATION = 1048
ER_NO_REFERENCED_ROW = 1216
ER_ROW_IS_REFERENCED = 1217
ER_DO_NOT_HAVE_DEFAULT = 1364
ER_ROW_IS_REFERENCED_2 = 1451
ER_NO_REFERENCED_ROW_2 = 1452
ER_DATA_TOO_LONG = 1406
ER_OUT_OF_RANGE = 1264
ER_LOCK_DEADLOCK = 1213
ER_CANNOT_ADD_FOREIGN = 1215
ER_CANNOT_CREATE_TABLE = 1005
ER_LOCK_WAIT_TIMEOUT = 1205
ER_QUERY_INTERRUPTED = 1317
ER_QUERY_TIMEOUT = 3024
ER_FK_INCOMPATIBLE_COLUMNS = 3780
def translate_exception(exception, message:, sql:, binds:)
case error_number(exception)
when nil
if exception.message.match?(/MySQL client is not connected/i)
ConnectionNotEstablished.new(exception)
else
super
end
when ER_DB_CREATE_EXISTS
DatabaseAlreadyExists.new(message, sql: sql, binds: binds)
when ER_DUP_ENTRY
RecordNotUnique.new(message, sql: sql, binds: binds)
when ER_NO_REFERENCED_ROW, ER_ROW_IS_REFERENCED, ER_ROW_IS_REFERENCED_2, ER_NO_REFERENCED_ROW_2
InvalidForeignKey.new(message, sql: sql, binds: binds)
when ER_CANNOT_ADD_FOREIGN, ER_FK_INCOMPATIBLE_COLUMNS
mismatched_foreign_key(message, sql: sql, binds: binds)
when ER_CANNOT_CREATE_TABLE
if message.include?("errno: 150")
mismatched_foreign_key(message, sql: sql, binds: binds)
else
super
end
when ER_DATA_TOO_LONG
ValueTooLong.new(message, sql: sql, binds: binds)
when ER_OUT_OF_RANGE
RangeError.new(message, sql: sql, binds: binds)
when ER_NOT_NULL_VIOLATION, ER_DO_NOT_HAVE_DEFAULT
NotNullViolation.new(message, sql: sql, binds: binds)
when ER_LOCK_DEADLOCK
Deadlocked.new(message, sql: sql, binds: binds)
when ER_LOCK_WAIT_TIMEOUT
LockWaitTimeout.new(message, sql: sql, binds: binds)
when ER_QUERY_TIMEOUT, ER_FILSORT_ABORT
StatementTimeout.new(message, sql: sql, binds: binds)
when ER_QUERY_INTERRUPTED
QueryCanceled.new(message, sql: sql, binds: binds)
else
super
end
end
def change_column_for_alter(table_name, column_name, type, **options)
column = column_for(table_name, column_name)
type ||= column.sql_type
unless options.key?(:default)
options[:default] = column.default
end
unless options.key?(:null)
options[:null] = column.null
end
unless options.key?(:comment)
options[:comment] = column.comment
end
unless options.key?(:auto_increment)
options[:auto_increment] = column.auto_increment?
end
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_for_alter(table_name, column_name, new_column_name)
return rename_column_sql(table_name, column_name, new_column_name) if supports_rename_column?
column = column_for(table_name, column_name)
options = {
default: column.default,
null: column.null,
auto_increment: column.auto_increment?,
comment: column.comment
}
current_type = exec_query("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE #{quote(column_name)}", "SCHEMA").first["Type"]
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 add_index_for_alter(table_name, column_name, **options)
index, algorithm, _ = add_index_options(table_name, column_name, **options)
algorithm = ", #{algorithm}" if algorithm
"ADD #{schema_creation.accept(index)}#{algorithm}"
end
def remove_index_for_alter(table_name, column_name = nil, **options)
index_name = index_name_for_remove(table_name, column_name, options)
"DROP INDEX #{quote_column_name(index_name)}"
end
def supports_rename_index?
if mariadb?
database_version >= "10.5.2"
else
database_version >= "5.7.6"
end
end
def supports_rename_column?
if mariadb?
database_version >= "10.5.2"
else
database_version >= "8.0.3"
end
end
def configure_connection
variables = @config.fetch(:variables, {}).stringify_keys
# By default, MySQL 'where id is null' selects the last inserted id; Turn this off.
variables["sql_auto_is_null"] = 0
# Increase timeout so the server doesn't disconnect us.
wait_timeout = self.class.type_cast_config_to_integer(@config[:wait_timeout])
wait_timeout = 2147483 unless wait_timeout.is_a?(Integer)
variables["wait_timeout"] = wait_timeout
defaults = [":default", :default].to_set
# Make MySQL reject illegal values rather than truncating or blanking them, see
# https://dev.mysql.com/doc/refman/en/sql-mode.html#sqlmode_strict_all_tables
# If the user has provided another value for sql_mode, don't replace it.
if sql_mode = variables.delete("sql_mode")
sql_mode = quote(sql_mode)
elsif !defaults.include?(strict_mode?)
if strict_mode?
sql_mode = "CONCAT(@@sql_mode, ',STRICT_ALL_TABLES')"
else
sql_mode = "REPLACE(@@sql_mode, 'STRICT_TRANS_TABLES', '')"
sql_mode = "REPLACE(#{sql_mode}, 'STRICT_ALL_TABLES', '')"
sql_mode = "REPLACE(#{sql_mode}, 'TRADITIONAL', '')"
end
sql_mode = "CONCAT(#{sql_mode}, ',NO_AUTO_VALUE_ON_ZERO')"
end
sql_mode_assignment = "@@SESSION.sql_mode = #{sql_mode}, " if sql_mode
# NAMES does not have an equals sign, see
# https://dev.mysql.com/doc/refman/en/set-names.html
# (trailing comma because variable_assignments will always have content)
if @config[:encoding]
encoding = +"NAMES #{@config[:encoding]}"
encoding << " COLLATE #{@config[:collation]}" if @config[:collation]
encoding << ", "
end
# Gather up all of the SET variables...
variable_assignments = variables.filter_map do |k, v|
if defaults.include?(v)
"@@SESSION.#{k} = DEFAULT" # Sets the value to the global or compile default
elsif !v.nil?
"@@SESSION.#{k} = #{quote(v)}"
end
end.join(", ")
# ...and send them all in one query
execute("SET #{encoding} #{sql_mode_assignment} #{variable_assignments}", "SCHEMA")
end
def column_definitions(table_name) # :nodoc:
execute_and_free("SHOW FULL FIELDS FROM #{quote_table_name(table_name)}", "SCHEMA") do |result|
each_hash(result)
end
end
def create_table_info(table_name) # :nodoc:
exec_query("SHOW CREATE TABLE #{quote_table_name(table_name)}", "SCHEMA").first["Create Table"]
end
def arel_visitor
Arel::Visitors::MySQL.new(self)
end
def build_statement_pool
StatementPool.new(self.class.type_cast_config_to_integer(@config[:statement_limit]))
end
def mismatched_foreign_key(message, sql:, binds:)
match = %r/
(?:CREATE|ALTER)\s+TABLE\s*(?:`?\w+`?\.)?`?(?<table>\w+)`?.+?
FOREIGN\s+KEY\s*\(`?(?<foreign_key>\w+)`?\)\s*
REFERENCES\s*(`?(?<target_table>\w+)`?)\s*\(`?(?<primary_key>\w+)`?\)
/xmi.match(sql)
options = {
message: message,
sql: sql,
binds: binds,
}
if match
options[:table] = match[:table]
options[:foreign_key] = match[:foreign_key]
options[:target_table] = match[:target_table]
options[:primary_key] = match[:primary_key]
options[:primary_key_column] = column_for(match[:target_table], match[:primary_key])
end
MismatchedForeignKey.new(**options)
end
def version_string(full_version_string)
full_version_string.match(/^(?:5\.5\.5-)?(\d+\.\d+\.\d+)/)[1]
end
ActiveRecord::Type.register(:immutable_string, adapter: :mysql2) do |_, **args|
Type::ImmutableString.new(true: "1", false: "0", **args)
end
ActiveRecord::Type.register(:string, adapter: :mysql2) do |_, **args|
Type::String.new(true: "1", false: "0", **args)
end
ActiveRecord::Type.register(:unsigned_integer, Type::UnsignedInteger, adapter: :mysql2)
end
end
end
# frozen_string_literal: true
require "cases/helper"
class AbstractMysqlAdapterTest < ActiveRecord::Mysql2TestCase
if current_adapter?(:Mysql2Adapter)
class ExampleMysqlAdapter < ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter; end
def setup
@conn = ExampleMysqlAdapter.new(
ActiveRecord::ConnectionAdapters::Mysql2Adapter.new_client({}),
ActiveRecord::Base.logger,
nil,
{ socket: File::NULL }
)
end
def test_execute_not_raising_error
assert_nothing_raised do
@conn.execute("SELECT 1")
end
end
end
end
# frozen_string_literal: true
class Admin::Account < ActiveRecord::Base
has_many :users
end
# frozen_string_literal: true
require "rails/generators/named_base"
require "rails/generators/active_model"
require "rails/generators/active_record/migration"
require "active_record"
module ActiveRecord
module Generators # :nodoc:
class Base < Rails::Generators::NamedBase # :nodoc:
include ActiveRecord::Generators::Migration
# Set the current directory as base for the inherited generators.
def self.base_root
__dir__
end
end
end
end
# frozen_string_literal: true
require "cases/helper"
class ActiveRecordSchemaTest < ActiveRecord::TestCase
self.use_transactional_tests = false
setup do
@original_verbose = ActiveRecord::Migration.verbose
ActiveRecord::Migration.verbose = false
@connection = ActiveRecord::Base.connection
@schema_migration = @connection.schema_migration
@schema_migration.drop_table
end
teardown do
@connection.drop_table :fruits rescue nil
@connection.drop_table :nep_fruits rescue nil
@connection.drop_table :nep_schema_migrations rescue nil
@connection.drop_table :has_timestamps rescue nil
@connection.drop_table :multiple_indexes rescue nil
@schema_migration.delete_all rescue nil
ActiveRecord::Migration.verbose = @original_verbose
end
def test_has_primary_key
old_primary_key_prefix_type = ActiveRecord::Base.primary_key_prefix_type
ActiveRecord::Base.primary_key_prefix_type = :table_name_with_underscore
assert_equal "version", @schema_migration.primary_key
@schema_migration.create_table
assert_difference "@schema_migration.count", 1 do
@schema_migration.create version: 12
end
ensure
@schema_migration.drop_table
ActiveRecord::Base.primary_key_prefix_type = old_primary_key_prefix_type
end
def test_schema_without_version_is_the_current_version_schema
schema_class = ActiveRecord::Schema
assert schema_class < ActiveRecord::Migration[ActiveRecord::Migration.current_version]
assert_not schema_class < ActiveRecord::Migration[7.0]
assert schema_class < ActiveRecord::Schema::Definition
end
def test_schema_version_accessor
schema_class = ActiveRecord::Schema[6.1]
assert schema_class < ActiveRecord::Migration[6.1]
assert schema_class < ActiveRecord::Schema::Definition
end
def test_schema_define
ActiveRecord::Schema.define(version: 7) do
create_table :fruits do |t|
t.column :color, :string
t.column :fruit_size, :string # NOTE: "size" is reserved in Oracle
t.column :texture, :string
t.column :flavor, :string
end
end
assert_nothing_raised { @connection.select_all "SELECT * FROM fruits" }
assert_nothing_raised { @connection.select_all "SELECT * FROM schema_migrations" }
assert_equal 7, @connection.schema_version
end
def test_schema_define_with_table_name_prefix
old_table_name_prefix = ActiveRecord::Base.table_name_prefix
ActiveRecord::Base.table_name_prefix = "nep_"
@schema_migration.reset_table_name
ActiveRecord::InternalMetadata.reset_table_name
ActiveRecord::Schema.define(version: 7) do
create_table :fruits do |t|
t.column :color, :string
t.column :fruit_size, :string # NOTE: "size" is reserved in Oracle
t.column :texture, :string
t.column :flavor, :string
end
end
assert_equal 7, @connection.migration_context.current_version
ensure
ActiveRecord::Base.table_name_prefix = old_table_name_prefix
@schema_migration.reset_table_name
ActiveRecord::InternalMetadata.reset_table_name
end
def test_schema_raises_an_error_for_invalid_column_type
assert_raise NoMethodError do
ActiveRecord::Schema.define(version: 8) do
create_table :vegetables do |t|
t.unknown :color
end
end
end
end
def test_schema_subclass
Class.new(ActiveRecord::Schema).define(version: 9) do
create_table :fruits
end
assert_nothing_raised { @connection.select_all "SELECT * FROM fruits" }
end
def test_normalize_version
assert_equal "118", @schema_migration.normalize_migration_number("0000118")
assert_equal "002", @schema_migration.normalize_migration_number("2")
assert_equal "017", @schema_migration.normalize_migration_number("0017")
assert_equal "20131219224947", @schema_migration.normalize_migration_number("20131219224947")
end
def test_schema_load_with_multiple_indexes_for_column_of_different_names
ActiveRecord::Schema.define do
create_table :multiple_indexes do |t|
t.string "foo"
t.index ["foo"], name: "multiple_indexes_foo_1"
t.index ["foo"], name: "multiple_indexes_foo_2"
end
end
indexes = @connection.indexes("multiple_indexes")
assert_equal 2, indexes.length
assert_equal ["multiple_indexes_foo_1", "multiple_indexes_foo_2"], indexes.collect(&:name).sort
end
if current_adapter?(:PostgreSQLAdapter)
def test_timestamps_with_and_without_zones
ActiveRecord::Schema.define do
create_table :has_timestamps do |t|
t.datetime "default_format"
t.datetime "without_time_zone"
t.timestamp "also_without_time_zone"
t.timestamptz "with_time_zone"
end
end
assert @connection.column_exists?(:has_timestamps, :default_format, :datetime)
assert @connection.column_exists?(:has_timestamps, :without_time_zone, :datetime)
assert @connection.column_exists?(:has_timestamps, :also_without_time_zone, :datetime)
assert @connection.column_exists?(:has_timestamps, :with_time_zone, :timestamptz)
end
end
def test_timestamps_without_null_set_null_to_false_on_create_table
ActiveRecord::Schema.define do
create_table :has_timestamps do |t|
t.timestamps
end
end
assert @connection.column_exists?(:has_timestamps, :created_at, null: false)
assert @connection.column_exists?(:has_timestamps, :updated_at, null: false)
end
def test_timestamps_without_null_set_null_to_false_on_change_table
ActiveRecord::Schema.define do
create_table :has_timestamps
change_table :has_timestamps do |t|
t.timestamps default: Time.now
end
end
assert @connection.column_exists?(:has_timestamps, :created_at, null: false)
assert @connection.column_exists?(:has_timestamps, :updated_at, null: false)
end
if ActiveRecord::Base.connection.supports_bulk_alter?
def test_timestamps_without_null_set_null_to_false_on_change_table_with_bulk
ActiveRecord::Schema.define do
create_table :has_timestamps
change_table :has_timestamps, bulk: true do |t|
t.timestamps default: Time.now
end
end
assert @connection.column_exists?(:has_timestamps, :created_at, null: false)
assert @connection.column_exists?(:has_timestamps, :updated_at, null: false)
end
end
def test_timestamps_without_null_set_null_to_false_on_add_timestamps
ActiveRecord::Schema.define do
create_table :has_timestamps
add_timestamps :has_timestamps, default: Time.now
end
assert @connection.column_exists?(:has_timestamps, :created_at, null: false)
assert @connection.column_exists?(:has_timestamps, :updated_at, null: false)
end
if supports_datetime_with_precision?
def test_timestamps_sets_precision_on_create_table
ActiveRecord::Schema.define do
create_table :has_timestamps do |t|
t.timestamps
end
end
assert @connection.column_exists?(:has_timestamps, :created_at, precision: 6, null: false)
assert @connection.column_exists?(:has_timestamps, :updated_at, precision: 6, null: false)
end
def test_timestamps_sets_precision_on_change_table
ActiveRecord::Schema.define do
create_table :has_timestamps
change_table :has_timestamps do |t|
t.timestamps default: Time.now
end
end
assert @connection.column_exists?(:has_timestamps, :created_at, precision: 6, null: false)
assert @connection.column_exists?(:has_timestamps, :updated_at, precision: 6, null: false)
end
if ActiveRecord::Base.connection.supports_bulk_alter?
def test_timestamps_sets_precision_on_change_table_with_bulk
ActiveRecord::Schema.define do
create_table :has_timestamps
change_table :has_timestamps, bulk: true do |t|
t.timestamps default: Time.now
end
end
assert @connection.column_exists?(:has_timestamps, :created_at, precision: 6, null: false)
assert @connection.column_exists?(:has_timestamps, :updated_at, precision: 6, null: false)
end
end
def test_timestamps_sets_precision_on_add_timestamps
ActiveRecord::Schema.define do
create_table :has_timestamps
add_timestamps :has_timestamps, default: Time.now
end
assert @connection.column_exists?(:has_timestamps, :created_at, precision: 6, null: false)
assert @connection.column_exists?(:has_timestamps, :updated_at, precision: 6, null: false)
end
end
end
# frozen_string_literal: true
require "cases/helper"
class PostgresqlActiveSchemaTest < ActiveRecord::PostgreSQLTestCase
def setup
ActiveRecord::Base.connection.materialize_transactions
ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.class_eval do
def execute(sql, name = nil) sql end
end
end
teardown do
ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.class_eval do
remove_method :execute
end
end
def test_create_database_with_encoding
assert_equal %(CREATE DATABASE "matt" ENCODING = 'utf8'), create_database(:matt)
assert_equal %(CREATE DATABASE "aimonetti" ENCODING = 'latin1'), create_database(:aimonetti, encoding: :latin1)
assert_equal %(CREATE DATABASE "aimonetti" ENCODING = 'latin1'), create_database(:aimonetti, "encoding" => :latin1)
end
def test_create_database_with_collation_and_ctype
assert_equal %(CREATE DATABASE "aimonetti" ENCODING = 'UTF8' LC_COLLATE = 'ja_JP.UTF8' LC_CTYPE = 'ja_JP.UTF8'), create_database(:aimonetti, encoding: :"UTF8", collation: :"ja_JP.UTF8", ctype: :"ja_JP.UTF8")
end
def test_add_index
expected = %(CREATE UNIQUE INDEX "index_people_on_last_name" ON "people" ("last_name") WHERE state = 'active')
assert_equal expected, add_index(:people, :last_name, unique: true, where: "state = 'active'")
expected = %(CREATE UNIQUE INDEX "index_people_on_lower_last_name" ON "people" (lower(last_name)))
assert_equal expected, add_index(:people, "lower(last_name)", unique: true)
expected = %(CREATE UNIQUE INDEX "index_people_on_last_name_varchar_pattern_ops" ON "people" (last_name varchar_pattern_ops))
assert_equal expected, add_index(:people, "last_name varchar_pattern_ops", unique: true)
expected = %(CREATE INDEX CONCURRENTLY "index_people_on_last_name" ON "people" ("last_name"))
assert_equal expected, add_index(:people, :last_name, algorithm: :concurrently)
expected = %(CREATE INDEX CONCURRENTLY IF NOT EXISTS "index_people_on_last_name" ON "people" ("last_name"))
assert_equal expected, add_index(:people, :last_name, if_not_exists: true, algorithm: :concurrently)
expected = %(CREATE INDEX "index_people_on_last_name_and_first_name" ON "people" ("last_name" DESC, "first_name" ASC))
assert_equal expected, add_index(:people, [:last_name, :first_name], order: { last_name: :desc, first_name: :asc })
assert_equal expected, add_index(:people, ["last_name", :first_name], order: { last_name: :desc, "first_name" => :asc })
%w(gin gist hash btree).each do |type|
expected = %(CREATE INDEX "index_people_on_last_name" ON "people" USING #{type} ("last_name"))
assert_equal expected, add_index(:people, :last_name, using: type)
expected = %(CREATE INDEX CONCURRENTLY "index_people_on_last_name" ON "people" USING #{type} ("last_name"))
assert_equal expected, add_index(:people, :last_name, using: type, algorithm: :concurrently)
expected = %(CREATE UNIQUE INDEX "index_people_on_last_name" ON "people" USING #{type} ("last_name") WHERE state = 'active')
assert_equal expected, add_index(:people, :last_name, using: type, unique: true, where: "state = 'active'")
expected = %(CREATE UNIQUE INDEX "index_people_on_lower_last_name" ON "people" USING #{type} (lower(last_name)))
assert_equal expected, add_index(:people, "lower(last_name)", using: type, unique: true)
end
expected = %(CREATE INDEX "index_people_on_last_name" ON "people" USING gist ("last_name" bpchar_pattern_ops))
assert_equal expected, add_index(:people, :last_name, using: :gist, opclass: { last_name: :bpchar_pattern_ops })
expected = %(CREATE INDEX "index_people_on_last_name_and_first_name" ON "people" ("last_name" DESC NULLS LAST, "first_name" ASC))
assert_equal expected, add_index(:people, [:last_name, :first_name], order: { last_name: "DESC NULLS LAST", first_name: :asc })
expected = %(CREATE INDEX "index_people_on_last_name" ON "people" ("last_name" NULLS FIRST))
assert_equal expected, add_index(:people, :last_name, order: "NULLS FIRST")
expected = %(CREATE INDEX IF NOT EXISTS "index_people_on_last_name" ON "people" ("last_name"))
assert_equal expected, add_index(:people, :last_name, if_not_exists: true)
assert_raise ArgumentError do
add_index(:people, :last_name, algorithm: :copy)
end
end
def test_remove_index
# remove_index calls index_name_for_remove which can't work since execute is stubbed
ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.define_method(:index_name_for_remove) do |*|
"index_people_on_last_name"
end
expected = %(DROP INDEX CONCURRENTLY "index_people_on_last_name")
assert_equal expected, remove_index(:people, name: "index_people_on_last_name", algorithm: :concurrently)
assert_raise ArgumentError do
add_index(:people, :last_name, algorithm: :copy)
end
ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.remove_method :index_name_for_remove
end
def test_remove_index_when_name_is_specified
expected = %(DROP INDEX CONCURRENTLY "index_people_on_last_name")
assert_equal expected, remove_index(:people, name: "index_people_on_last_name", algorithm: :concurrently)
end
def test_remove_index_with_wrong_option
assert_raises ArgumentError do
remove_index(:people, coulmn: :last_name)
end
end
private
def method_missing(...)
ActiveRecord::Base.connection.public_send(...)
end
end
# frozen_string_literal: true
require "cases/helper"
module ActiveRecord
module ConnectionAdapters
class AdapterLeasingTest < ActiveRecord::TestCase
class Pool < ConnectionPool
def insert_connection_for_test!(c)
synchronize do
adopt_connection(c)
@available.add c
end
end
end
def setup
@adapter = AbstractAdapter.new nil, nil
end
def test_in_use?
assert_not @adapter.in_use?, "adapter is not in use"
assert @adapter.lease, "lease adapter"
assert @adapter.in_use?, "adapter is in use"
end
def test_lease_twice
assert @adapter.lease, "should lease adapter"
assert_raises(ActiveRecordError) do
@adapter.lease
end
end
def test_expire_mutates_in_use
assert @adapter.lease, "lease adapter"
assert @adapter.in_use?, "adapter is in use"
@adapter.expire
assert_not @adapter.in_use?, "adapter is in use"
end
def test_close
db_config = ActiveRecord::DatabaseConfigurations::HashConfig.new("test", "primary", {})
pool_config = ActiveRecord::ConnectionAdapters::PoolConfig.new(ActiveRecord::Base, db_config, :writing, :default)
pool = Pool.new(pool_config)
pool.insert_connection_for_test! @adapter
@adapter.pool = pool
# Make sure the pool marks the connection in use
assert_equal @adapter, pool.connection
assert_predicate @adapter, :in_use?
# Close should put the adapter back in the pool
@adapter.close
assert_not_predicate @adapter, :in_use?
assert_equal @adapter, pool.connection
end
end
end
end
# frozen_string_literal: true
require "cases/helper"
require "support/connection_helper"
require "models/book"
require "models/post"
require "models/author"
require "models/event"
module ActiveRecord
class AdapterPreventWritesTest < ActiveRecord::TestCase
def setup
@connection = ActiveRecord::Base.connection
end
def test_preventing_writes_predicate
assert_not_predicate @connection, :preventing_writes?
ActiveRecord::Base.while_preventing_writes do
assert_predicate @connection, :preventing_writes?
end
assert_not_predicate @connection, :preventing_writes?
end
def test_errors_when_an_insert_query_is_called_while_preventing_writes
ActiveRecord::Base.while_preventing_writes do
assert_raises(ActiveRecord::ReadOnlyError) do
@connection.insert("INSERT INTO subscribers(nick) VALUES ('138853948594')", nil, false)
end
end
end
def test_errors_when_an_update_query_is_called_while_preventing_writes
@connection.insert("INSERT INTO subscribers(nick) VALUES ('138853948594')")
ActiveRecord::Base.while_preventing_writes do
assert_raises(ActiveRecord::ReadOnlyError) do
@connection.update("UPDATE subscribers SET nick = '9989' WHERE nick = '138853948594'")
end
end
end
def test_errors_when_a_delete_query_is_called_while_preventing_writes
@connection.insert("INSERT INTO subscribers(nick) VALUES ('138853948594')")
ActiveRecord::Base.while_preventing_writes do
assert_raises(ActiveRecord::ReadOnlyError) do
@connection.delete("DELETE FROM subscribers WHERE nick = '138853948594'")
end
end
end
if current_adapter?(:PostgreSQLAdapter)
def test_doesnt_error_when_a_select_query_has_encoding_errors
ActiveRecord::Base.while_preventing_writes do
# Contrary to other adapters, Postgres will eagerly fail on encoding errors.
# But at least we can assert it fails in the client and not before when trying to
# match the query.
assert_raises ActiveRecord::StatementInvalid do
@connection.select_all("SELECT '\xC8'")
end
end
end
else
def test_doesnt_error_when_a_select_query_has_encoding_errors
ActiveRecord::Base.while_preventing_writes do
@connection.select_all("SELECT '\xC8'")
end
end
end
def test_doesnt_error_when_a_select_query_is_called_while_preventing_writes
@connection.insert("INSERT INTO subscribers(nick) VALUES ('138853948594')")
ActiveRecord::Base.while_preventing_writes do
result = @connection.select_all("SELECT subscribers.* FROM subscribers WHERE nick = '138853948594'")
assert_equal 1, result.length
end
end
if ActiveRecord::Base.connection.supports_common_table_expressions?
def test_doesnt_error_when_a_read_query_with_a_cte_is_called_while_preventing_writes
@connection.insert("INSERT INTO subscribers(nick) VALUES ('138853948594')")
ActiveRecord::Base.while_preventing_writes do
result = @connection.select_all(<<~SQL)
WITH matching_subscribers AS (SELECT subscribers.* FROM subscribers WHERE nick = '138853948594')
SELECT * FROM matching_subscribers
SQL
assert_equal 1, result.length
end
end
end
def test_doesnt_error_when_a_select_query_starting_with_a_slash_star_comment_is_called_while_preventing_writes
@connection.insert("INSERT INTO subscribers(nick) VALUES ('138853948594')")
ActiveRecord::Base.while_preventing_writes do
result = @connection.select_all("/* some comment */ SELECT subscribers.* FROM subscribers WHERE nick = '138853948594'")
assert_equal 1, result.length
end
end
def test_errors_when_an_insert_query_prefixed_by_a_slash_star_comment_is_called_while_preventing_writes
ActiveRecord::Base.while_preventing_writes do
assert_raises(ActiveRecord::ReadOnlyError) do
@connection.insert("/* some comment */ INSERT INTO subscribers(nick) VALUES ('138853948594')", nil, false)
end
end
end
def test_doesnt_error_when_a_select_query_starting_with_double_dash_comments_is_called_while_preventing_writes
@connection.insert("INSERT INTO subscribers(nick) VALUES ('138853948594')")
ActiveRecord::Base.while_preventing_writes do
result = @connection.select_all("-- some comment\n-- comment about INSERT\nSELECT subscribers.* FROM subscribers WHERE nick = '138853948594'")
assert_equal 1, result.length
end
end
def test_errors_when_an_insert_query_prefixed_by_a_double_dash_comment_is_called_while_preventing_writes
ActiveRecord::Base.while_preventing_writes do
assert_raises(ActiveRecord::ReadOnlyError) do
@connection.insert("-- some comment\nINSERT INTO subscribers(nick) VALUES ('138853948594')", nil, false)
end
end
end
def test_errors_when_an_insert_query_prefixed_by_a_slash_star_comment_containing_read_command_is_called_while_preventing_writes
ActiveRecord::Base.while_preventing_writes do
assert_raises(ActiveRecord::ReadOnlyError) do
@connection.insert("/* SELECT */ INSERT INTO subscribers(nick) VALUES ('138853948594')", nil, false)
end
end
end
def test_errors_when_an_insert_query_prefixed_by_a_double_dash_comment_containing_read_command_is_called_while_preventing_writes
ActiveRecord::Base.while_preventing_writes do
assert_raises(ActiveRecord::ReadOnlyError) do
@connection.insert("-- SELECT\nINSERT INTO subscribers(nick) VALUES ('138853948594')", nil, false)
end
end
end
end
class AdapterPreventWritesLegacyTest < ActiveRecord::TestCase
def setup
@old_value = ActiveRecord.legacy_connection_handling
ActiveRecord.legacy_connection_handling = true
@connection = ActiveRecord::Base.connection
@connection_handler = ActiveRecord::Base.connection_handler
end
def teardown
clean_up_legacy_connection_handlers
ActiveRecord.legacy_connection_handling = @old_value
end
def test_preventing_writes_predicate_legacy
assert_not_predicate @connection, :preventing_writes?
@connection_handler.while_preventing_writes do
assert_predicate @connection, :preventing_writes?
end
assert_not_predicate @connection, :preventing_writes?
end
def test_errors_when_an_insert_query_is_called_while_preventing_writes
@connection_handler.while_preventing_writes do
assert_raises(ActiveRecord::ReadOnlyError) do
@connection.insert("INSERT INTO subscribers(nick) VALUES ('138853948594')", nil, false)
end
end
end
def test_errors_when_an_update_query_is_called_while_preventing_writes
@connection.insert("INSERT INTO subscribers(nick) VALUES ('138853948594')")
@connection_handler.while_preventing_writes do
assert_raises(ActiveRecord::ReadOnlyError) do
@connection.update("UPDATE subscribers SET nick = '9989' WHERE nick = '138853948594'")
end
end
end
def test_errors_when_a_delete_query_is_called_while_preventing_writes
@connection.insert("INSERT INTO subscribers(nick) VALUES ('138853948594')")
@connection_handler.while_preventing_writes do
assert_raises(ActiveRecord::ReadOnlyError) do
@connection.delete("DELETE FROM subscribers WHERE nick = '138853948594'")
end
end
end
def test_doesnt_error_when_a_select_query_is_called_while_preventing_writes
@connection.insert("INSERT INTO subscribers(nick) VALUES ('138853948594')")
@connection_handler.while_preventing_writes do
result = @connection.select_all("SELECT subscribers.* FROM subscribers WHERE nick = '138853948594'")
assert_equal 1, result.length
end
end
if ActiveRecord::Base.connection.supports_common_table_expressions?
def test_doesnt_error_when_a_read_query_with_a_cte_is_called_while_preventing_writes
@connection.insert("INSERT INTO subscribers(nick) VALUES ('138853948594')")
@connection_handler.while_preventing_writes do
result = @connection.select_all(<<~SQL)
WITH matching_subscribers AS (SELECT subscribers.* FROM subscribers WHERE nick = '138853948594')
SELECT * FROM matching_subscribers
SQL
assert_equal 1, result.length
end
end
end
def test_doesnt_error_when_a_select_query_starting_with_a_slash_star_comment_is_called_while_preventing_writes
@connection.insert("INSERT INTO subscribers(nick) VALUES ('138853948594')")
@connection_handler.while_preventing_writes do
result = @connection.select_all("/* some comment */ SELECT subscribers.* FROM subscribers WHERE nick = '138853948594'")
assert_equal 1, result.length
end
end
def test_errors_when_an_insert_query_prefixed_by_a_slash_star_comment_is_called_while_preventing_writes
@connection_handler.while_preventing_writes do
assert_raises(ActiveRecord::ReadOnlyError) do
@connection.insert("/* some comment */ INSERT INTO subscribers(nick) VALUES ('138853948594')", nil, false)
end
end
end
def test_doesnt_error_when_a_select_query_starting_with_double_dash_comments_is_called_while_preventing_writes
@connection.insert("INSERT INTO subscribers(nick) VALUES ('138853948594')")
@connection_handler.while_preventing_writes do
result = @connection.select_all("-- some comment\n-- comment about INSERT\nSELECT subscribers.* FROM subscribers WHERE nick = '138853948594'")
assert_equal 1, result.length
end
end
def test_errors_when_an_insert_query_prefixed_by_a_double_dash_comment_is_called_while_preventing_writes
@connection_handler.while_preventing_writes do
assert_raises(ActiveRecord::ReadOnlyError) do
@connection.insert("-- some comment\nINSERT INTO subscribers(nick) VALUES ('138853948594')", nil, false)
end
end
end
def test_errors_when_an_insert_query_prefixed_by_a_slash_star_comment_containing_read_command_is_called_while_preventing_writes
@connection_handler.while_preventing_writes do
assert_raises(ActiveRecord::ReadOnlyError) do
@connection.insert("/* SELECT */ INSERT INTO subscribers(nick) VALUES ('138853948594')", nil, false)
end
end
end
def test_errors_when_an_insert_query_prefixed_by_a_double_dash_comment_containing_read_command_is_called_while_preventing_writes
@connection_handler.while_preventing_writes do
assert_raises(ActiveRecord::ReadOnlyError) do
@connection.insert("-- SELECT\nINSERT INTO subscribers(nick) VALUES ('138853948594')", nil, false)
end
end
end
end
end
# frozen_string_literal: true
module ActiveRecord
# :stopdoc:
module Type
class AdapterSpecificRegistry # :nodoc:
def initialize
@registrations = []
end
def initialize_copy(other)
@registrations = @registrations.dup
end
def add_modifier(options, klass, **args)
registrations << DecorationRegistration.new(options, klass, **args)
end
def register(type_name, klass = nil, **options, &block)
unless block_given?
block = proc { |_, *args| klass.new(*args) }
block.ruby2_keywords if block.respond_to?(:ruby2_keywords)
end
registrations << Registration.new(type_name, block, **options)
end
def lookup(symbol, *args, **kwargs)
registration = find_registration(symbol, *args, **kwargs)
if registration
registration.call(self, symbol, *args, **kwargs)
else
raise ArgumentError, "Unknown type #{symbol.inspect}"
end
end
private
attr_reader :registrations
def find_registration(symbol, *args, **kwargs)
registrations
.select { |registration| registration.matches?(symbol, *args, **kwargs) }
.max
end
end
class Registration # :nodoc:
def initialize(name, block, adapter: nil, override: nil)
@name = name
@block = block
@adapter = adapter
@override = override
end
def call(_registry, *args, adapter: nil, **kwargs)
block.call(*args, **kwargs)
end
def matches?(type_name, *args, **kwargs)
type_name == name && matches_adapter?(**kwargs)
end
def <=>(other)
if conflicts_with?(other)
raise TypeConflictError.new("Type #{name} was registered for all
adapters, but shadows a native type with
the same name for #{other.adapter}".squish)
end
priority <=> other.priority
end
protected
attr_reader :name, :block, :adapter, :override
def priority
result = 0
if adapter
result |= 1
end
if override
result |= 2
end
result
end
def priority_except_adapter
priority & 0b111111100
end
private
def matches_adapter?(adapter: nil, **)
(self.adapter.nil? || adapter == self.adapter)
end
def conflicts_with?(other)
same_priority_except_adapter?(other) &&
has_adapter_conflict?(other)
end
def same_priority_except_adapter?(other)
priority_except_adapter == other.priority_except_adapter
end
def has_adapter_conflict?(other)
(override.nil? && other.adapter) ||
(adapter && other.override.nil?)
end
end
class DecorationRegistration < Registration # :nodoc:
def initialize(options, klass, adapter: nil)
@options = options
@klass = klass
@adapter = adapter
end
def call(registry, *args, **kwargs)
subtype = registry.lookup(*args, **kwargs.except(*options.keys))
klass.new(subtype)
end
def matches?(*args, **kwargs)
matches_adapter?(**kwargs) && matches_options?(**kwargs)
end
def priority
super | 4
end
private
attr_reader :options, :klass
def matches_options?(**kwargs)
options.all? do |key, value|
kwargs[key] == value
end
end
end
end
class TypeConflictError < StandardError # :nodoc:
end
# :startdoc:
end
# frozen_string_literal: true
require "cases/helper"
module ActiveRecord
class AdapterSpecificRegistryTest < ActiveRecord::TestCase
test "a class can be registered for a symbol" do
registry = Type::AdapterSpecificRegistry.new
registry.register(:foo, ::String)
registry.register(:bar, ::Array)
assert_equal "", registry.lookup(:foo)
assert_equal [], registry.lookup(:bar)
end
test "a block can be registered" do
registry = Type::AdapterSpecificRegistry.new
registry.register(:foo) do |*args|
[*args, "block for foo"]
end
registry.register(:bar) do |*args|
[*args, "block for bar"]
end
assert_equal [:foo, 1, "block for foo"], registry.lookup(:foo, 1)
assert_equal [:foo, 2, "block for foo"], registry.lookup(:foo, 2)
assert_equal [:bar, 1, 2, 3, "block for bar"], registry.lookup(:bar, 1, 2, 3)
end
test "filtering by adapter" do
registry = Type::AdapterSpecificRegistry.new
registry.register(:foo, String, adapter: :sqlite3)
registry.register(:foo, Array, adapter: :postgresql)
assert_equal "", registry.lookup(:foo, adapter: :sqlite3)
assert_equal [], registry.lookup(:foo, adapter: :postgresql)
end
test "an error is raised if both a generic and adapter specific type match" do
registry = Type::AdapterSpecificRegistry.new
registry.register(:foo, String)
registry.register(:foo, Array, adapter: :postgresql)
assert_raises TypeConflictError do
registry.lookup(:foo, adapter: :postgresql)
end
assert_equal "", registry.lookup(:foo, adapter: :sqlite3)
end
test "a generic type can explicitly override an adapter specific type" do
registry = Type::AdapterSpecificRegistry.new
registry.register(:foo, String, override: true)
registry.register(:foo, Array, adapter: :postgresql)
assert_equal "", registry.lookup(:foo, adapter: :postgresql)
assert_equal "", registry.lookup(:foo, adapter: :sqlite3)
end
test "a generic type can explicitly allow an adapter type to be used instead" do
registry = Type::AdapterSpecificRegistry.new
registry.register(:foo, String, override: false)
registry.register(:foo, Array, adapter: :postgresql)
assert_equal [], registry.lookup(:foo, adapter: :postgresql)
assert_equal "", registry.lookup(:foo, adapter: :sqlite3)
end
test "a reasonable error is given when no type is found" do
registry = Type::AdapterSpecificRegistry.new
e = assert_raises(ArgumentError) do
registry.lookup(:foo)
end
assert_equal "Unknown type :foo", e.message
end
test "construct args are passed to the type" do
registry = Type::AdapterSpecificRegistry.new
registry.register(:foo, type)
assert_equal type.new, registry.lookup(:foo)
assert_equal type.new(:ordered_arg), registry.lookup(:foo, :ordered_arg)
assert_equal type.new(keyword: :arg), registry.lookup(:foo, keyword: :arg)
assert_equal type.new(keyword: :arg), registry.lookup(:foo, keyword: :arg, adapter: :postgresql)
end
test "registering a modifier" do
decoration = Struct.new(:value)
registry = Type::AdapterSpecificRegistry.new
registry.register(:foo, String)
registry.register(:bar, Hash)
registry.add_modifier({ array: true }, decoration)
assert_equal decoration.new(""), registry.lookup(:foo, array: true)
assert_equal decoration.new({}), registry.lookup(:bar, array: true)
assert_equal "", registry.lookup(:foo)
end
test "registering multiple modifiers" do
decoration = Struct.new(:value)
other_decoration = Struct.new(:value)
registry = Type::AdapterSpecificRegistry.new
registry.register(:foo, String)
registry.add_modifier({ array: true }, decoration)
registry.add_modifier({ range: true }, other_decoration)
assert_equal "", registry.lookup(:foo)
assert_equal decoration.new(""), registry.lookup(:foo, array: true)
assert_equal other_decoration.new(""), registry.lookup(:foo, range: true)
assert_equal(
decoration.new(other_decoration.new("")),
registry.lookup(:foo, array: true, range: true)
)
end
test "registering adapter specific modifiers" do
decoration = Struct.new(:value)
registry = Type::AdapterSpecificRegistry.new
registry.register(:foo, type)
registry.add_modifier({ array: true }, decoration, adapter: :postgresql)
assert_equal(
decoration.new(type.new(keyword: :arg)),
registry.lookup(:foo, array: true, adapter: :postgresql, keyword: :arg)
)
assert_equal(
type.new(array: true),
registry.lookup(:foo, array: true, adapter: :sqlite3)
)
end
TYPE = Class.new do
attr_reader :args
def initialize(args = nil)
@args = args
end
def ==(other) self.args == other.args end
end
private def type; TYPE end
end
end
# frozen_string_literal: true
require "cases/helper"
require "support/connection_helper"
require "models/book"
require "models/post"
require "models/author"
require "models/event"
module ActiveRecord
class AdapterTest < ActiveRecord::TestCase
def setup
@connection = ActiveRecord::Base.connection
@connection.materialize_transactions
end
##
# PostgreSQL does not support null bytes in strings
unless current_adapter?(:PostgreSQLAdapter) ||
(current_adapter?(:SQLite3Adapter) && !ActiveRecord::Base.connection.prepared_statements)
def test_update_prepared_statement
b = Book.create(name: "my \x00 book")
b.reload
assert_equal "my \x00 book", b.name
b.update(name: "my other \x00 book")
b.reload
assert_equal "my other \x00 book", b.name
end
end
def test_create_record_with_pk_as_zero
Book.create(id: 0)
assert_equal 0, Book.find(0).id
assert_nothing_raised { Book.destroy(0) }
end
def test_valid_column
@connection.native_database_types.each_key do |type|
assert @connection.valid_type?(type)
end
end
def test_invalid_column
assert_not @connection.valid_type?(:foobar)
end
def test_tables
tables = @connection.tables
assert_includes tables, "accounts"
assert_includes tables, "authors"
assert_includes tables, "tasks"
assert_includes tables, "topics"
end
def test_table_exists?
assert @connection.table_exists?("accounts")
assert @connection.table_exists?(:accounts)
assert_not @connection.table_exists?("nonexistingtable")
assert_not @connection.table_exists?("'")
assert_not @connection.table_exists?(nil)
end
def test_data_sources
data_sources = @connection.data_sources
assert_includes data_sources, "accounts"
assert_includes data_sources, "authors"
assert_includes data_sources, "tasks"
assert_includes data_sources, "topics"
end
def test_data_source_exists?
assert @connection.data_source_exists?("accounts")
assert @connection.data_source_exists?(:accounts)
assert_not @connection.data_source_exists?("nonexistingtable")
assert_not @connection.data_source_exists?("'")
assert_not @connection.data_source_exists?(nil)
end
def test_indexes
idx_name = "accounts_idx"
indexes = @connection.indexes("accounts")
assert_empty indexes
@connection.add_index :accounts, :firm_id, name: idx_name
indexes = @connection.indexes("accounts")
assert_equal "accounts", indexes.first.table
assert_equal idx_name, indexes.first.name
assert_not indexes.first.unique
assert_equal ["firm_id"], indexes.first.columns
ensure
@connection.remove_index(:accounts, name: idx_name) rescue nil
end
def test_remove_index_when_name_and_wrong_column_name_specified
index_name = "accounts_idx"
@connection.add_index :accounts, :firm_id, name: index_name
assert_raises ArgumentError do
@connection.remove_index :accounts, name: index_name, column: :wrong_column_name
end
ensure
@connection.remove_index(:accounts, name: index_name)
end
def test_remove_index_when_name_and_wrong_column_name_specified_positional_argument
index_name = "accounts_idx"
@connection.add_index :accounts, :firm_id, name: index_name
assert_raises ArgumentError do
@connection.remove_index :accounts, :wrong_column_name, name: index_name
end
ensure
@connection.remove_index(:accounts, name: index_name)
end
def test_current_database
if @connection.respond_to?(:current_database)
assert_equal ARTest.test_configuration_hashes["arunit"]["database"], @connection.current_database
end
end
def test_exec_query_returns_an_empty_result
result = @connection.exec_query "INSERT INTO subscribers(nick) VALUES('me')"
assert_instance_of(ActiveRecord::Result, result)
end
if current_adapter?(:Mysql2Adapter)
def test_charset
assert_not_nil @connection.charset
assert_not_equal "character_set_database", @connection.charset
assert_equal @connection.show_variable("character_set_database"), @connection.charset
end
def test_collation
assert_not_nil @connection.collation
assert_not_equal "collation_database", @connection.collation
assert_equal @connection.show_variable("collation_database"), @connection.collation
end
def test_show_nonexistent_variable_returns_nil
assert_nil @connection.show_variable("foo_bar_baz")
end
def test_not_specifying_database_name_for_cross_database_selects
assert_nothing_raised do
db_config = ActiveRecord::Base.configurations.configs_for(env_name: "arunit", name: "primary")
ActiveRecord::Base.establish_connection(db_config.configuration_hash.except(:database))
config = ARTest.test_configuration_hashes
ActiveRecord::Base.connection.execute(
"SELECT #{config['arunit']['database']}.pirates.*, #{config['arunit2']['database']}.courses.* " \
"FROM #{config['arunit']['database']}.pirates, #{config['arunit2']['database']}.courses"
)
end
ensure
ActiveRecord::Base.establish_connection :arunit
end
end
def test_table_alias
def @connection.test_table_alias_length() 10; end
class << @connection
alias_method :old_table_alias_length, :table_alias_length
alias_method :table_alias_length, :test_table_alias_length
end
assert_equal "posts", @connection.table_alias_for("posts")
assert_equal "posts_comm", @connection.table_alias_for("posts_comments")
assert_equal "dbo_posts", @connection.table_alias_for("dbo.posts")
class << @connection
remove_method :table_alias_length
alias_method :table_alias_length, :old_table_alias_length
end
end
def test_uniqueness_violations_are_translated_to_specific_exception
@connection.execute "INSERT INTO subscribers(nick) VALUES('me')"
error = assert_raises(ActiveRecord::RecordNotUnique) do
@connection.execute "INSERT INTO subscribers(nick) VALUES('me')"
end
assert_not_nil error.cause
end
def test_not_null_violations_are_translated_to_specific_exception
error = assert_raises(ActiveRecord::NotNullViolation) do
Post.create
end
assert_not_nil error.cause
end
unless current_adapter?(:SQLite3Adapter)
def test_value_limit_violations_are_translated_to_specific_exception
error = assert_raises(ActiveRecord::ValueTooLong) do
Event.create(title: "abcdefgh")
end
assert_not_nil error.cause
end
def test_numeric_value_out_of_ranges_are_translated_to_specific_exception
error = assert_raises(ActiveRecord::RangeError) do
Book.connection.create("INSERT INTO books(author_id) VALUES (9223372036854775808)")
end
assert_not_nil error.cause
end
end
def test_exceptions_from_notifications_are_not_translated
original_error = StandardError.new("This StandardError shouldn't get translated")
subscriber = ActiveSupport::Notifications.subscribe("sql.active_record") { raise original_error }
actual_error = assert_raises(StandardError) do
@connection.execute("SELECT * FROM posts")
end
assert_equal original_error, actual_error
ensure
ActiveSupport::Notifications.unsubscribe(subscriber) if subscriber
end
def test_database_related_exceptions_are_translated_to_statement_invalid
error = assert_raises(ActiveRecord::StatementInvalid) do
@connection.execute("This is a syntax error")
end
assert_instance_of ActiveRecord::StatementInvalid, error
assert_kind_of Exception, error.cause
end
def test_select_all_always_return_activerecord_result
result = @connection.select_all "SELECT * FROM posts"
assert result.is_a?(ActiveRecord::Result)
end
if ActiveRecord::Base.connection.prepared_statements
def test_select_all_insert_update_delete_with_casted_binds
binds = [Event.type_for_attribute("id").serialize(1)]
bind_param = Arel::Nodes::BindParam.new(nil)
id = @connection.insert("INSERT INTO events(id) VALUES (#{bind_param.to_sql})", nil, nil, nil, nil, binds)
assert_equal 1, id
updated = @connection.update("UPDATE events SET title = 'foo' WHERE id = #{bind_param.to_sql}", nil, binds)
assert_equal 1, updated
result = @connection.select_all("SELECT * FROM events WHERE id = #{bind_param.to_sql}", nil, binds)
assert_equal({ "id" => 1, "title" => "foo" }, result.first)
deleted = @connection.delete("DELETE FROM events WHERE id = #{bind_param.to_sql}", nil, binds)
assert_equal 1, deleted
result = @connection.select_all("SELECT * FROM events WHERE id = #{bind_param.to_sql}", nil, binds)
assert_nil result.first
end
def test_select_all_insert_update_delete_with_binds
binds = [Relation::QueryAttribute.new("id", 1, Event.type_for_attribute("id"))]
bind_param = Arel::Nodes::BindParam.new(nil)
id = @connection.insert("INSERT INTO events(id) VALUES (#{bind_param.to_sql})", nil, nil, nil, nil, binds)
assert_equal 1, id
updated = @connection.update("UPDATE events SET title = 'foo' WHERE id = #{bind_param.to_sql}", nil, binds)
assert_equal 1, updated
result = @connection.select_all("SELECT * FROM events WHERE id = #{bind_param.to_sql}", nil, binds)
assert_equal({ "id" => 1, "title" => "foo" }, result.first)
deleted = @connection.delete("DELETE FROM events WHERE id = #{bind_param.to_sql}", nil, binds)
assert_equal 1, deleted
result = @connection.select_all("SELECT * FROM events WHERE id = #{bind_param.to_sql}", nil, binds)
assert_nil result.first
end
end
def test_select_methods_passing_a_association_relation
author = Author.create!(name: "john")
Post.create!(author: author, title: "foo", body: "bar")
query = author.posts.where(title: "foo").select(:title)
assert_equal({ "title" => "foo" }, @connection.select_one(query))
assert @connection.select_all(query).is_a?(ActiveRecord::Result)
assert_equal "foo", @connection.select_value(query)
assert_equal ["foo"], @connection.select_values(query)
end
def test_select_methods_passing_a_relation
Post.create!(title: "foo", body: "bar")
query = Post.where(title: "foo").select(:title)
assert_equal({ "title" => "foo" }, @connection.select_one(query))
assert @connection.select_all(query).is_a?(ActiveRecord::Result)
assert_equal "foo", @connection.select_value(query)
assert_equal ["foo"], @connection.select_values(query)
end
test "type_to_sql returns a String for unmapped types" do
assert_equal "special_db_type", @connection.type_to_sql(:special_db_type)
end
end
class AdapterForeignKeyTest < ActiveRecord::TestCase
self.use_transactional_tests = false
fixtures :fk_test_has_pk
def setup
@connection = ActiveRecord::Base.connection
end
def test_foreign_key_violations_are_translated_to_specific_exception_with_validate_false
klass_has_fk = Class.new(ActiveRecord::Base) do
self.table_name = "fk_test_has_fk"
end
error = assert_raises(ActiveRecord::InvalidForeignKey) do
has_fk = klass_has_fk.new
has_fk.fk_id = 1231231231
has_fk.save(validate: false)
end
assert_not_nil error.cause
end
def test_foreign_key_violations_on_insert_are_translated_to_specific_exception
error = assert_raises(ActiveRecord::InvalidForeignKey) do
insert_into_fk_test_has_fk
end
assert_not_nil error.cause
end
def test_foreign_key_violations_on_delete_are_translated_to_specific_exception
insert_into_fk_test_has_fk fk_id: 1
error = assert_raises(ActiveRecord::InvalidForeignKey) do
@connection.execute "DELETE FROM fk_test_has_pk WHERE pk_id = 1"
end
assert_not_nil error.cause
end
def test_disable_referential_integrity
assert_nothing_raised do
@connection.disable_referential_integrity do
insert_into_fk_test_has_fk
# should delete created record as otherwise disable_referential_integrity will try to enable constraints
# after executed block and will fail (at least on Oracle)
@connection.execute "DELETE FROM fk_test_has_fk"
end
end
end
private
def insert_into_fk_test_has_fk(fk_id: 0)
# Oracle adapter uses prefetched primary key values from sequence and passes them to connection adapter insert method
if @connection.prefetch_primary_key?
id_value = @connection.next_sequence_value(@connection.default_sequence_name("fk_test_has_fk", "id"))
@connection.execute "INSERT INTO fk_test_has_fk (id,fk_id) VALUES (#{id_value},#{fk_id})"
else
@connection.execute "INSERT INTO fk_test_has_fk (fk_id) VALUES (#{fk_id})"
end
end
end
class AdapterTestWithoutTransaction < ActiveRecord::TestCase
self.use_transactional_tests = false
fixtures :posts, :authors, :author_addresses
def setup
@connection = ActiveRecord::Base.connection
end
unless in_memory_db?
test "reconnect after a disconnect" do
assert_predicate @connection, :active?
@connection.disconnect!
assert_not_predicate @connection, :active?
@connection.reconnect!
assert_predicate @connection, :active?
end
test "materialized transaction state is reset after a reconnect" do
@connection.begin_transaction
assert_predicate @connection, :transaction_open?
@connection.materialize_transactions
assert raw_transaction_open?(@connection)
@connection.reconnect!
assert_not_predicate @connection, :transaction_open?
assert_not raw_transaction_open?(@connection)
end
test "materialized transaction state can be restored after a reconnect" do
@connection.begin_transaction
assert_predicate @connection, :transaction_open?
# +materialize_transactions+ currently automatically dirties the
# connection, which would make it unrestorable
@connection.transaction_manager.stub(:dirty_current_transaction, nil) do
@connection.materialize_transactions
end
assert raw_transaction_open?(@connection)
@connection.reconnect!(restore_transactions: true)
assert_predicate @connection, :transaction_open?
assert_not raw_transaction_open?(@connection)
ensure
@connection.reconnect!
assert_not_predicate @connection, :transaction_open?
end
test "materialized transaction state is reset after a disconnect" do
@connection.begin_transaction
assert_predicate @connection, :transaction_open?
@connection.materialize_transactions
assert raw_transaction_open?(@connection)
@connection.disconnect!
assert_not_predicate @connection, :transaction_open?
ensure
@connection.reconnect!
assert_not raw_transaction_open?(@connection)
end
test "unmaterialized transaction state is reset after a reconnect" do
@connection.begin_transaction
assert_predicate @connection, :transaction_open?
assert_not raw_transaction_open?(@connection)
@connection.reconnect!
assert_not_predicate @connection, :transaction_open?
assert_not raw_transaction_open?(@connection)
@connection.materialize_transactions
assert_not raw_transaction_open?(@connection)
end
test "unmaterialized transaction state can be restored after a reconnect" do
@connection.begin_transaction
assert_predicate @connection, :transaction_open?
assert_not raw_transaction_open?(@connection)
@connection.reconnect!(restore_transactions: true)
assert_predicate @connection, :transaction_open?
assert_not raw_transaction_open?(@connection)
@connection.materialize_transactions
assert raw_transaction_open?(@connection)
ensure
@connection.reconnect!
assert_not_predicate @connection, :transaction_open?
assert_not raw_transaction_open?(@connection)
end
test "unmaterialized transaction state is reset after a disconnect" do
@connection.begin_transaction
assert_predicate @connection, :transaction_open?
assert_not raw_transaction_open?(@connection)
@connection.disconnect!
assert_not_predicate @connection, :transaction_open?
ensure
@connection.reconnect!
assert_not raw_transaction_open?(@connection)
@connection.materialize_transactions
assert_not raw_transaction_open?(@connection)
end
end
def test_create_with_query_cache
@connection.enable_query_cache!
count = Post.count
@connection.create("INSERT INTO posts(title, body) VALUES ('', '')")
assert_equal count + 1, Post.count
ensure
reset_fixtures("posts")
@connection.disable_query_cache!
end
def test_truncate
assert_operator Post.count, :>, 0
@connection.truncate("posts")
assert_equal 0, Post.count
ensure
reset_fixtures("posts")
end
def test_truncate_with_query_cache
@connection.enable_query_cache!
assert_operator Post.count, :>, 0
@connection.truncate("posts")
assert_equal 0, Post.count
ensure
reset_fixtures("posts")
@connection.disable_query_cache!
end
def test_truncate_tables
assert_operator Post.count, :>, 0
assert_operator Author.count, :>, 0
assert_operator AuthorAddress.count, :>, 0
@connection.truncate_tables("author_addresses", "authors", "posts")
assert_equal 0, Post.count
assert_equal 0, Author.count
assert_equal 0, AuthorAddress.count
ensure
reset_fixtures("posts", "authors", "author_addresses")
end
def test_truncate_tables_with_query_cache
@connection.enable_query_cache!
assert_operator Post.count, :>, 0
assert_operator Author.count, :>, 0
assert_operator AuthorAddress.count, :>, 0
@connection.truncate_tables("author_addresses", "authors", "posts")
assert_equal 0, Post.count
assert_equal 0, Author.count
assert_equal 0, AuthorAddress.count
ensure
reset_fixtures("posts", "authors", "author_addresses")
@connection.disable_query_cache!
end
# test resetting sequences in odd tables in PostgreSQL
if ActiveRecord::Base.connection.respond_to?(:reset_pk_sequence!)
require "models/movie"
require "models/subscriber"
def test_reset_empty_table_with_custom_pk
Movie.delete_all
Movie.connection.reset_pk_sequence! "movies"
assert_equal 1, Movie.create(name: "fight club").id
end
def test_reset_table_with_non_integer_pk
Subscriber.delete_all
Subscriber.connection.reset_pk_sequence! "subscribers"
sub = Subscriber.new(name: "robert drake")
sub.id = "bob drake"
assert_nothing_raised { sub.save! }
end
end
private
def raw_transaction_open?(connection)
case connection.class::ADAPTER_NAME
when "PostgreSQL"
connection.instance_variable_get(:@raw_connection).transaction_status == ::PG::PQTRANS_INTRANS
when "Mysql2"
begin
connection.instance_variable_get(:@raw_connection).query("SAVEPOINT transaction_test")
connection.instance_variable_get(:@raw_connection).query("RELEASE SAVEPOINT transaction_test")
true
rescue
false
end
when "SQLite"
begin
connection.instance_variable_get(:@raw_connection).transaction { nil }
false
rescue
true
end
else
skip
end
end
def reset_fixtures(*fixture_names)
ActiveRecord::FixtureSet.reset_cache
fixture_names.each do |fixture_name|
ActiveRecord::FixtureSet.create_fixtures(FIXTURES_ROOT, fixture_name)
end
end
end
end
if ActiveRecord::Base.connection.supports_advisory_locks?
class AdvisoryLocksEnabledTest < ActiveRecord::TestCase
include ConnectionHelper
def test_advisory_locks_enabled?
assert ActiveRecord::Base.connection.advisory_locks_enabled?
run_without_connection do |orig_connection|
ActiveRecord::Base.establish_connection(
orig_connection.merge(advisory_locks: false)
)
assert_not ActiveRecord::Base.connection.advisory_locks_enabled?
ActiveRecord::Base.establish_connection(
orig_connection.merge(advisory_locks: true)
)
assert ActiveRecord::Base.connection.advisory_locks_enabled?
end
end
end
end
# frozen_string_literal: true
module Admin
def self.table_name_prefix
"admin_"
end
end
# frozen_string_literal: true
require "openssl"
module ActiveRecord
module Encryption
class Cipher
# A 256-GCM cipher.
#
# By default it will use random initialization vectors. For deterministic encryption, it will use a SHA-256 hash of
# the text to encrypt and the secret.
#
# See +Encryptor+
class Aes256Gcm
CIPHER_TYPE = "aes-256-gcm"
class << self
def key_length
OpenSSL::Cipher.new(CIPHER_TYPE).key_len
end
def iv_length
OpenSSL::Cipher.new(CIPHER_TYPE).iv_len
end
end
# When iv not provided, it will generate a random iv on each encryption operation (default and
# recommended operation)
def initialize(secret, deterministic: false)
@secret = secret
@deterministic = deterministic
end
def encrypt(clear_text)
# This code is extracted from +ActiveSupport::MessageEncryptor+. Not using it directly because we want to control
# the message format and only serialize things once at the +ActiveRecord::Encryption::Message+ level. Also, this
# cipher is prepared to deal with deterministic/non deterministic encryption modes.
cipher = OpenSSL::Cipher.new(CIPHER_TYPE)
cipher.encrypt
cipher.key = @secret
iv = generate_iv(cipher, clear_text)
cipher.iv = iv
encrypted_data = clear_text.empty? ? clear_text.dup : cipher.update(clear_text)
encrypted_data << cipher.final
ActiveRecord::Encryption::Message.new(payload: encrypted_data).tap do |message|
message.headers.iv = iv
message.headers.auth_tag = cipher.auth_tag
end
end
def decrypt(encrypted_message)
encrypted_data = encrypted_message.payload
iv = encrypted_message.headers.iv
auth_tag = encrypted_message.headers.auth_tag
# Currently the OpenSSL bindings do not raise an error if auth_tag is
# truncated, which would allow an attacker to easily forge it. See
# https://github.com/ruby/openssl/issues/63
raise ActiveRecord::Encryption::Errors::EncryptedContentIntegrity if auth_tag.nil? || auth_tag.bytes.length != 16
cipher = OpenSSL::Cipher.new(CIPHER_TYPE)
cipher.decrypt
cipher.key = @secret
cipher.iv = iv
cipher.auth_tag = auth_tag
cipher.auth_data = ""
decrypted_data = encrypted_data.empty? ? encrypted_data : cipher.update(encrypted_data)
decrypted_data << cipher.final
decrypted_data
rescue OpenSSL::Cipher::CipherError, TypeError, ArgumentError
raise ActiveRecord::Encryption::Errors::Decryption
end
private
def generate_iv(cipher, clear_text)
if @deterministic
generate_deterministic_iv(clear_text)
else
cipher.random_iv
end
end
def generate_deterministic_iv(clear_text)
OpenSSL::HMAC.digest(OpenSSL::Digest::SHA256.new, @secret, clear_text)[0, ActiveRecord::Encryption.cipher.iv_length]
end
end
end
end
end
# frozen_string_literal: true
require "cases/encryption/helper"
class ActiveRecord::Encryption::Aes256GcmTest < ActiveRecord::EncryptionTestCase
setup do
@key = ActiveRecord::Encryption.key_generator.generate_random_key length: ActiveRecord::Encryption::Cipher::Aes256Gcm.key_length
@cipher = ActiveRecord::Encryption::Cipher::Aes256Gcm.new(@key)
end
test "encrypts strings" do
assert_cipher_encrypts(@cipher, "Some clear text")
end
test "works with empty strings" do
assert_cipher_encrypts(@cipher, "")
end
test "uses non-deterministic encryption by default" do
assert_not_equal @cipher.encrypt("Some text").payload, @cipher.encrypt("Some text").payload
end
test "in deterministic mode, it generates the same ciphertext for the same inputs" do
cipher = ActiveRecord::Encryption::Cipher::Aes256Gcm.new(@key, deterministic: true)
assert_cipher_encrypts(cipher, "Some clear text")
assert_equal cipher.encrypt("Some text").payload, cipher.encrypt("Some text").payload
assert_not_equal cipher.encrypt("Some text").payload, cipher.encrypt("Some other text").payload
end
test "it generates different ivs for different ciphertexts" do
cipher = ActiveRecord::Encryption::Cipher::Aes256Gcm.new(@key, deterministic: true)
assert_equal cipher.encrypt("Some text").headers.iv, cipher.encrypt("Some text").headers.iv
assert_not_equal cipher.encrypt("Some text").headers.iv, cipher.encrypt("Some other text").headers.iv
end
private
def assert_cipher_encrypts(cipher, content_to_encrypt)
encrypted_content = cipher.encrypt(content_to_encrypt)
assert_not_equal content_to_encrypt, encrypted_content
assert_equal content_to_encrypt, cipher.decrypt(encrypted_content)
end
end
# frozen_string_literal: true
module ActiveRecord
# See ActiveRecord::Aggregations::ClassMethods for documentation
module Aggregations
def initialize_dup(*) # :nodoc:
@aggregation_cache = {}
super
end
def reload(*) # :nodoc:
clear_aggregation_cache
super
end
private
def clear_aggregation_cache
@aggregation_cache.clear if persisted?
end
def init_internals
@aggregation_cache = {}
super
end
# Active Record implements aggregation through a macro-like class method called #composed_of
# for representing attributes as value objects. It expresses relationships like "Account [is]
# composed of Money [among other things]" or "Person [is] composed of [an] address". Each call
# to the macro adds a description of how the value objects are created from the attributes of
# the entity object (when the entity is initialized either as a new object or from finding an
# existing object) and how it can be turned back into attributes (when the entity is saved to
# the database).
#
# class Customer < ActiveRecord::Base
# composed_of :balance, class_name: "Money", mapping: %w(balance amount)
# composed_of :address, mapping: [ %w(address_street street), %w(address_city city) ]
# end
#
# The customer class now has the following methods to manipulate the value objects:
# * <tt>Customer#balance, Customer#balance=(money)</tt>
# * <tt>Customer#address, Customer#address=(address)</tt>
#
# These methods will operate with value objects like the ones described below:
#
# class Money
# include Comparable
# attr_reader :amount, :currency
# EXCHANGE_RATES = { "USD_TO_DKK" => 6 }
#
# def initialize(amount, currency = "USD")
# @amount, @currency = amount, currency
# end
#
# def exchange_to(other_currency)
# exchanged_amount = (amount * EXCHANGE_RATES["#{currency}_TO_#{other_currency}"]).floor
# Money.new(exchanged_amount, other_currency)
# end
#
# def ==(other_money)
# amount == other_money.amount && currency == other_money.currency
# end
#
# def <=>(other_money)
# if currency == other_money.currency
# amount <=> other_money.amount
# else
# amount <=> other_money.exchange_to(currency).amount
# end
# end
# end
#
# class Address
# attr_reader :street, :city
# def initialize(street, city)
# @street, @city = street, city
# end
#
# def close_to?(other_address)
# city == other_address.city
# end
#
# def ==(other_address)
# city == other_address.city && street == other_address.street
# end
# end
#
# Now it's possible to access attributes from the database through the value objects instead. If
# you choose to name the composition the same as the attribute's name, it will be the only way to
# access that attribute. That's the case with our +balance+ attribute. You interact with the value
# objects just like you would with any other attribute:
#
# customer.balance = Money.new(20) # sets the Money value object and the attribute
# customer.balance # => Money value object
# customer.balance.exchange_to("DKK") # => Money.new(120, "DKK")
# customer.balance > Money.new(10) # => true
# customer.balance == Money.new(20) # => true
# customer.balance < Money.new(5) # => false
#
# Value objects can also be composed of multiple attributes, such as the case of Address. The order
# of the mappings will determine the order of the parameters.
#
# customer.address_street = "Hyancintvej"
# customer.address_city = "Copenhagen"
# customer.address # => Address.new("Hyancintvej", "Copenhagen")
#
# customer.address = Address.new("May Street", "Chicago")
# customer.address_street # => "May Street"
# customer.address_city # => "Chicago"
#
# == Writing value objects
#
# Value objects are immutable and interchangeable objects that represent a given value, such as
# a Money object representing $5. Two Money objects both representing $5 should be equal (through
# methods such as <tt>==</tt> and <tt><=></tt> from Comparable if ranking makes sense). This is
# unlike entity objects where equality is determined by identity. An entity class such as Customer can
# easily have two different objects that both have an address on Hyancintvej. Entity identity is
# determined by object or relational unique identifiers (such as primary keys). Normal
# ActiveRecord::Base classes are entity objects.
#
# It's also important to treat the value objects as immutable. Don't allow the Money object to have
# its amount changed after creation. Create a new Money object with the new value instead. The
# <tt>Money#exchange_to</tt> method is an example of this. It returns a new value object instead of changing
# its own values. Active Record won't persist value objects that have been changed through means
# other than the writer method.
#
# The immutable requirement is enforced by Active Record by freezing any object assigned as a value
# object. Attempting to change it afterwards will result in a +RuntimeError+.
#
# Read more about value objects on http://c2.com/cgi/wiki?ValueObject and on the dangers of not
# keeping value objects immutable on http://c2.com/cgi/wiki?ValueObjectsShouldBeImmutable
#
# == Custom constructors and converters
#
# By default value objects are initialized by calling the <tt>new</tt> constructor of the value
# class passing each of the mapped attributes, in the order specified by the <tt>:mapping</tt>
# option, as arguments. If the value class doesn't support this convention then #composed_of allows
# a custom constructor to be specified.
#
# When a new value is assigned to the value object, the default assumption is that the new value
# is an instance of the value class. Specifying a custom converter allows the new value to be automatically
# converted to an instance of value class if necessary.
#
# For example, the +NetworkResource+ model has +network_address+ and +cidr_range+ attributes that should be
# aggregated using the +NetAddr::CIDR+ value class (https://www.rubydoc.info/gems/netaddr/1.5.0/NetAddr/CIDR).
# The constructor for the value class is called +create+ and it expects a CIDR address string as a parameter.
# New values can be assigned to the value object using either another +NetAddr::CIDR+ object, a string
# or an array. The <tt>:constructor</tt> and <tt>:converter</tt> options can be used to meet
# these requirements:
#
# class NetworkResource < ActiveRecord::Base
# composed_of :cidr,
# class_name: 'NetAddr::CIDR',
# mapping: [ %w(network_address network), %w(cidr_range bits) ],
# allow_nil: true,
# constructor: Proc.new { |network_address, cidr_range| NetAddr::CIDR.create("#{network_address}/#{cidr_range}") },
# converter: Proc.new { |value| NetAddr::CIDR.create(value.is_a?(Array) ? value.join('/') : value) }
# end
#
# # This calls the :constructor
# network_resource = NetworkResource.new(network_address: '192.168.0.1', cidr_range: 24)
#
# # These assignments will both use the :converter
# network_resource.cidr = [ '192.168.2.1', 8 ]
# network_resource.cidr = '192.168.0.1/24'
#
# # This assignment won't use the :converter as the value is already an instance of the value class
# network_resource.cidr = NetAddr::CIDR.create('192.168.2.1/8')
#
# # Saving and then reloading will use the :constructor on reload
# network_resource.save
# network_resource.reload
#
# == Finding records by a value object
#
# Once a #composed_of relationship is specified for a model, records can be loaded from the database
# by specifying an instance of the value object in the conditions hash. The following example
# finds all customers with +address_street+ equal to "May Street" and +address_city+ equal to "Chicago":
#
# Customer.where(address: Address.new("May Street", "Chicago"))
#
module ClassMethods
# Adds reader and writer methods for manipulating a value object:
# <tt>composed_of :address</tt> adds <tt>address</tt> and <tt>address=(new_address)</tt> methods.
#
# Options are:
# * <tt>:class_name</tt> - Specifies the class name of the association. Use it only if that name
# can't be inferred from the part id. So <tt>composed_of :address</tt> will by default be linked
# to the Address class, but if the real class name is +CompanyAddress+, you'll have to specify it
# with this option.
# * <tt>:mapping</tt> - Specifies the mapping of entity attributes to attributes of the value
# object. Each mapping is represented as an array where the first item is the name of the
# entity attribute and the second item is the name of the attribute in the value object. The
# order in which mappings are defined determines the order in which attributes are sent to the
# value class constructor.
# * <tt>:allow_nil</tt> - Specifies that the value object will not be instantiated when all mapped
# attributes are +nil+. Setting the value object to +nil+ has the effect of writing +nil+ to all
# mapped attributes.
# This defaults to +false+.
# * <tt>:constructor</tt> - A symbol specifying the name of the constructor method or a Proc that
# is called to initialize the value object. The constructor is passed all of the mapped attributes,
# in the order that they are defined in the <tt>:mapping option</tt>, as arguments and uses them
# to instantiate a <tt>:class_name</tt> object.
# The default is <tt>:new</tt>.
# * <tt>:converter</tt> - A symbol specifying the name of a class method of <tt>:class_name</tt>
# or a Proc that is called when a new value is assigned to the value object. The converter is
# passed the single value that is used in the assignment and is only called if the new value is
# not an instance of <tt>:class_name</tt>. If <tt>:allow_nil</tt> is set to true, the converter
# can return +nil+ to skip the assignment.
#
# Option examples:
# composed_of :temperature, mapping: %w(reading celsius)
# composed_of :balance, class_name: "Money", mapping: %w(balance amount)
# composed_of :address, mapping: [ %w(address_street street), %w(address_city city) ]
# composed_of :gps_location
# composed_of :gps_location, allow_nil: true
# composed_of :ip_address,
# class_name: 'IPAddr',
# mapping: %w(ip to_i),
# constructor: Proc.new { |ip| IPAddr.new(ip, Socket::AF_INET) },
# converter: Proc.new { |ip| ip.is_a?(Integer) ? IPAddr.new(ip, Socket::AF_INET) : IPAddr.new(ip.to_s) }
#
def composed_of(part_id, options = {})
options.assert_valid_keys(:class_name, :mapping, :allow_nil, :constructor, :converter)
unless self < Aggregations
include Aggregations
end
name = part_id.id2name
class_name = options[:class_name] || name.camelize
mapping = options[:mapping] || [ name, name ]
mapping = [ mapping ] unless mapping.first.is_a?(Array)
allow_nil = options[:allow_nil] || false
constructor = options[:constructor] || :new
converter = options[:converter]
reader_method(name, class_name, mapping, allow_nil, constructor)
writer_method(name, class_name, mapping, allow_nil, converter)
reflection = ActiveRecord::Reflection.create(:composed_of, part_id, nil, options, self)
Reflection.add_aggregate_reflection self, part_id, reflection
end
private
def reader_method(name, class_name, mapping, allow_nil, constructor)
define_method(name) do
if @aggregation_cache[name].nil? && (!allow_nil || mapping.any? { |key, _| !read_attribute(key).nil? })
attrs = mapping.collect { |key, _| read_attribute(key) }
object = constructor.respond_to?(:call) ?
constructor.call(*attrs) :
class_name.constantize.send(constructor, *attrs)
@aggregation_cache[name] = object
end
@aggregation_cache[name]
end
end
def writer_method(name, class_name, mapping, allow_nil, converter)
define_method("#{name}=") do |part|
klass = class_name.constantize
unless part.is_a?(klass) || converter.nil? || part.nil?
part = converter.respond_to?(:call) ? converter.call(part) : klass.send(converter, part)
end
hash_from_multiparameter_assignment = part.is_a?(Hash) &&
part.keys.all?(Integer)
if hash_from_multiparameter_assignment
raise ArgumentError unless part.size == part.each_key.max
part = klass.new(*part.sort.map(&:last))
end
if part.nil? && allow_nil
mapping.each { |key, _| write_attribute(key, nil) }
@aggregation_cache[name] = nil
else
mapping.each { |key, value| write_attribute(key, part.send(value)) }
@aggregation_cache[name] = part.freeze
end
end
end
end
end
end
# frozen_string_literal: true
require "cases/helper"
require "models/customer"
class AggregationsTest < ActiveRecord::TestCase
fixtures :customers
def test_find_single_value_object
assert_equal 50, customers(:david).balance.amount
assert_kind_of Money, customers(:david).balance
assert_equal 300, customers(:david).balance.exchange_to("DKK").amount
end
def test_find_multiple_value_object
assert_equal customers(:david).address_street, customers(:david).address.street
assert(
customers(:david).address.close_to?(Address.new("Different Street", customers(:david).address_city, customers(:david).address_country))
)
end
def test_change_single_value_object
customers(:david).balance = Money.new(100)
customers(:david).save
assert_equal 100, customers(:david).reload.balance.amount
end
def test_immutable_value_objects
customers(:david).balance = Money.new(100)
assert_raise(FrozenError) { customers(:david).balance.instance_eval { @amount = 20 } }
end
def test_inferred_mapping
assert_equal "35.544623640962634", customers(:david).gps_location.latitude
assert_equal "-105.9309951055148", customers(:david).gps_location.longitude
customers(:david).gps_location = GpsLocation.new("39x-110")
assert_equal "39", customers(:david).gps_location.latitude
assert_equal "-110", customers(:david).gps_location.longitude
customers(:david).save
customers(:david).reload
assert_equal "39", customers(:david).gps_location.latitude
assert_equal "-110", customers(:david).gps_location.longitude
end
def test_reloaded_instance_refreshes_aggregations
assert_equal "35.544623640962634", customers(:david).gps_location.latitude
assert_equal "-105.9309951055148", customers(:david).gps_location.longitude
Customer.update_all("gps_location = '24x113'")
customers(:david).reload
assert_equal "24x113", customers(:david)["gps_location"]
assert_equal GpsLocation.new("24x113"), customers(:david).gps_location
end
def test_gps_equality
assert_equal GpsLocation.new("39x110"), GpsLocation.new("39x110")
end
def test_gps_inequality
assert_not_equal GpsLocation.new("39x110"), GpsLocation.new("39x111")
end
def test_allow_nil_gps_is_nil
assert_nil customers(:zaphod).gps_location
end
def test_allow_nil_gps_set_to_nil
customers(:david).gps_location = nil
customers(:david).save
customers(:david).reload
assert_nil customers(:david).gps_location
end
def test_allow_nil_set_address_attributes_to_nil
customers(:zaphod).address = nil
assert_nil customers(:zaphod).attributes[:address_street]
assert_nil customers(:zaphod).attributes[:address_city]
assert_nil customers(:zaphod).attributes[:address_country]
end
def test_allow_nil_address_set_to_nil
customers(:zaphod).address = nil
customers(:zaphod).save
customers(:zaphod).reload
assert_nil customers(:zaphod).address
end
def test_nil_raises_error_when_allow_nil_is_false
assert_raise(NoMethodError) { customers(:david).balance = nil }
end
def test_allow_nil_address_loaded_when_only_some_attributes_are_nil
customers(:zaphod).address_street = nil
customers(:zaphod).save
customers(:zaphod).reload
assert_kind_of Address, customers(:zaphod).address
assert_nil customers(:zaphod).address.street
end
def test_nil_assignment_results_in_nil
customers(:david).gps_location = GpsLocation.new("39x111")
assert_not_nil customers(:david).gps_location
customers(:david).gps_location = nil
assert_nil customers(:david).gps_location
end
def test_nil_return_from_converter_is_respected_when_allow_nil_is_true
customers(:david).non_blank_gps_location = ""
customers(:david).save
customers(:david).reload
assert_nil customers(:david).non_blank_gps_location
ensure
Customer.gps_conversion_was_run = nil
end
def test_nil_return_from_converter_results_in_failure_when_allow_nil_is_false
assert_raises(NoMethodError) do
customers(:barney).gps_location = ""
end
end
def test_do_not_run_the_converter_when_nil_was_set
customers(:david).non_blank_gps_location = nil
assert_nil Customer.gps_conversion_was_run
end
def test_custom_constructor
assert_equal "Barney GUMBLE", customers(:barney).fullname.to_s
assert_kind_of Fullname, customers(:barney).fullname
end
def test_custom_converter
customers(:barney).fullname = "Barnoit Gumbleau"
assert_equal "Barnoit GUMBLEAU", customers(:barney).fullname.to_s
assert_kind_of Fullname, customers(:barney).fullname
end
def test_assigning_hash_to_custom_converter
customers(:barney).fullname = { first: "Barney", last: "Stinson" }
assert_equal "Barney STINSON", customers(:barney).name
end
def test_assigning_hash_without_custom_converter
customers(:barney).fullname_no_converter = { first: "Barney", last: "Stinson" }
assert_equal({ first: "Barney", last: "Stinson" }.to_s, customers(:barney).name)
end
end
class OverridingAggregationsTest < ActiveRecord::TestCase
class DifferentName; end
class Person < ActiveRecord::Base
composed_of :composed_of, mapping: %w(person_first_name first_name)
end
class DifferentPerson < Person
composed_of :composed_of, class_name: "DifferentName", mapping: %w(different_person_first_name first_name)
end
def test_composed_of_aggregation_redefinition_reflections_should_differ_and_not_inherited
assert_not_equal Person.reflect_on_aggregation(:composed_of),
DifferentPerson.reflect_on_aggregation(:composed_of)
end
end
# frozen_string_literal: true
class Aircraft < ActiveRecord::Base
self.pluralize_table_names = false
has_many :engines, foreign_key: "car_id"
has_many :wheels, as: :wheelable
end
# frozen_string_literal: true
module Arel # :nodoc: all
module AliasPredication
def as(other)
Nodes::As.new self, Nodes::SqlLiteral.new(other)
end
end
end
# frozen_string_literal: true
require "active_support/core_ext/string/conversions"
module ActiveRecord
module Associations
# Keeps track of table aliases for ActiveRecord::Associations::JoinDependency
class AliasTracker # :nodoc:
def self.create(connection, initial_table, joins, aliases = nil)
if joins.empty?
aliases ||= Hash.new(0)
elsif aliases
default_proc = aliases.default_proc || proc { 0 }
aliases.default_proc = proc { |h, k|
h[k] = initial_count_for(connection, k, joins) + default_proc.call(h, k)
}
else
aliases = Hash.new { |h, k|
h[k] = initial_count_for(connection, k, joins)
}
end
aliases[initial_table] = 1
new(connection, aliases)
end
def self.initial_count_for(connection, name, table_joins)
quoted_name = nil
counts = table_joins.map do |join|
if join.is_a?(Arel::Nodes::StringJoin)
# quoted_name should be case ignored as some database adapters (Oracle) return quoted name in uppercase
quoted_name ||= connection.quote_table_name(name)
# Table names + table aliases
join.left.scan(
/JOIN(?:\s+\w+)?\s+(?:\S+\s+)?(?:#{quoted_name}|#{name})\sON/i
).size
elsif join.is_a?(Arel::Nodes::Join)
join.left.name == name ? 1 : 0
else
raise ArgumentError, "joins list should be initialized by list of Arel::Nodes::Join"
end
end
counts.sum
end
# table_joins is an array of arel joins which might conflict with the aliases we assign here
def initialize(connection, aliases)
@aliases = aliases
@connection = connection
end
def aliased_table_for(arel_table, table_name = nil)
table_name ||= arel_table.name
if aliases[table_name] == 0
# If it's zero, we can have our table_name
aliases[table_name] = 1
arel_table = arel_table.alias(table_name) if arel_table.name != table_name
else
# Otherwise, we need to use an alias
aliased_name = @connection.table_alias_for(yield)
# Update the count
count = aliases[aliased_name] += 1
aliased_name = "#{truncate(aliased_name)}_#{count}" if count > 1
arel_table = arel_table.alias(aliased_name)
end
arel_table
end
attr_reader :aliases
private
def truncate(name)
name.slice(0, @connection.table_alias_length - 2)
end
end
end
end
# frozen_string_literal: true
module Arel # :nodoc: all
module Nodes
class And < Arel::Nodes::NodeExpression
attr_reader :children
def initialize(children)
super()
@children = children
end
def left
children.first
end
def right
children[1]
end
def hash
children.hash
end
def eql?(other)
self.class == other.class &&
self.children == other.children
end
alias :== :eql?
end
end
end
# frozen_string_literal: true
require "cases/helper"
require "models/author"
module ActiveRecord
class AndTest < ActiveRecord::TestCase
fixtures :authors, :author_addresses
def test_and
david, mary, bob = authors(:david, :mary, :bob)
david_and_mary = Author.where(id: [david, mary]).order(:id)
mary_and_bob = Author.where(id: [mary, bob]).order(:id)
assert_equal [mary], david_and_mary.and(mary_and_bob)
end
def test_and_with_non_relation_attribute
hash = { "id" => 123 }
error = assert_raises(ArgumentError) do
Author.and(hash)
end
assert_equal(
"You have passed Hash object to #and. Pass an ActiveRecord::Relation object instead.",
error.message
)
end
def test_and_with_structurally_incompatible_scope
posts_scope = Author.unscope(:order).limit(10).offset(10).select(:id).order(:id)
error = assert_raises(ArgumentError) do
Author.limit(10).select(:id).order(:name).and(posts_scope)
end
assert_equal(
"Relation passed to #and must be structurally compatible. Incompatible values: [:order, :offset]",
error.message
)
end
end
end
# frozen_string_literal: true
require "cases/helper"
require "models/post"
class AnnotateTest < ActiveRecord::TestCase
fixtures :posts
def test_annotate_wraps_content_in_an_inline_comment
quoted_posts_id, quoted_posts = regexp_escape_table_name("posts.id"), regexp_escape_table_name("posts")
assert_sql(%r{SELECT #{quoted_posts_id} FROM #{quoted_posts} /\* foo \*/}i) do
posts = Post.select(:id).annotate("foo")
assert posts.first
end
end
def test_annotate_is_sanitized
quoted_posts_id, quoted_posts = regexp_escape_table_name("posts.id"), regexp_escape_table_name("posts")
assert_sql(%r{SELECT #{quoted_posts_id} FROM #{quoted_posts} /\* foo \*/}i) do
posts = Post.select(:id).annotate("*/foo/*")
assert posts.first
end
assert_sql(%r{SELECT #{quoted_posts_id} FROM #{quoted_posts} /\* foo \*/}i) do
posts = Post.select(:id).annotate("**//foo//**")
assert posts.first
end
assert_sql(%r{SELECT #{quoted_posts_id} FROM #{quoted_posts} /\* foo \*/ /\* bar \*/}i) do
posts = Post.select(:id).annotate("*/foo/*").annotate("*/bar")
assert posts.first
end
assert_sql(%r{SELECT #{quoted_posts_id} FROM #{quoted_posts} /\* \+ MAX_EXECUTION_TIME\(1\) \*/}i) do
posts = Post.select(:id).annotate("+ MAX_EXECUTION_TIME(1)")
assert posts.first
end
end
private
def regexp_escape_table_name(name)
Regexp.escape(Post.connection.quote_table_name(name))
end
end
# frozen_string_literal: true
require "rails/generators/active_record"
module ActiveRecord
module Generators # :nodoc:
class ApplicationRecordGenerator < ::Rails::Generators::Base # :nodoc:
source_root File.expand_path("templates", __dir__)
# FIXME: Change this file to a symlink once RubyGems 2.5.0 is required.
def create_application_record
template "application_record.rb", application_record_file_name
end
private
def application_record_file_name
@application_record_file_name ||=
if namespaced?
"app/models/#{namespaced_path}/application_record.rb"
else
"app/models/application_record.rb"
end
end
end
end
end
# frozen_string_literal: true
require "arel/errors"
require "arel/crud"
require "arel/factory_methods"
require "arel/expressions"
require "arel/predications"
require "arel/filter_predications"
require "arel/window_predications"
require "arel/math"
require "arel/alias_predication"
require "arel/order_predications"
require "arel/table"
require "arel/attributes/attribute"
require "arel/visitors"
require "arel/collectors/sql_string"
require "arel/tree_manager"
require "arel/insert_manager"
require "arel/select_manager"
require "arel/update_manager"
require "arel/delete_manager"
require "arel/nodes"
module Arel
VERSION = "10.0.0"
# Wrap a known-safe SQL string for passing to query methods, e.g.
#
# Post.order(Arel.sql("REPLACE(title, 'misc', 'zzzz') asc")).pluck(:id)
#
# Great caution should be taken to avoid SQL injection vulnerabilities.
# This method should not be used with unsafe values such as request
# parameters or model attributes.
def self.sql(raw_sql)
Arel::Nodes::SqlLiteral.new raw_sql
end
def self.star # :nodoc:
sql "*"
end
def self.arel_node?(value) # :nodoc:
value.is_a?(Arel::Nodes::Node) || value.is_a?(Arel::Attribute) || value.is_a?(Arel::Nodes::SqlLiteral)
end
def self.fetch_attribute(value, &block) # :nodoc:
unless String === value
value.fetch_attribute(&block)
end
end
end
# frozen_string_literal: true
module ActiveRecord
module ConnectionAdapters
module PostgreSQL
module OID # :nodoc:
class Array < Type::Value # :nodoc:
include ActiveModel::Type::Helpers::Mutable
Data = Struct.new(:encoder, :values) # :nodoc:
attr_reader :subtype, :delimiter
delegate :type, :user_input_in_time_zone, :limit, :precision, :scale, to: :subtype
def initialize(subtype, delimiter = ",")
@subtype = subtype
@delimiter = delimiter
@pg_encoder = PG::TextEncoder::Array.new name: "#{type}[]", delimiter: delimiter
@pg_decoder = PG::TextDecoder::Array.new name: "#{type}[]", delimiter: delimiter
end
def deserialize(value)
case value
when ::String
type_cast_array(@pg_decoder.decode(value), :deserialize)
when Data
type_cast_array(value.values, :deserialize)
else
super
end
end
def cast(value)
if value.is_a?(::String)
value = begin
@pg_decoder.decode(value)
rescue TypeError
# malformed array string is treated as [], will raise in PG 2.0 gem
# this keeps a consistent implementation
[]
end
end
type_cast_array(value, :cast)
end
def serialize(value)
if value.is_a?(::Array)
casted_values = type_cast_array(value, :serialize)
Data.new(@pg_encoder, casted_values)
else
super
end
end
def ==(other)
other.is_a?(Array) &&
subtype == other.subtype &&
delimiter == other.delimiter
end
def type_cast_for_schema(value)
return super unless value.is_a?(::Array)
"[" + value.map { |v| subtype.type_cast_for_schema(v) }.join(", ") + "]"
end
def map(value, &block)
value.map(&block)
end
def changed_in_place?(raw_old_value, new_value)
deserialize(raw_old_value) != new_value
end
def force_equality?(value)
value.is_a?(::Array)
end
private
def type_cast_array(value, method)
if value.is_a?(::Array)
value.map { |item| type_cast_array(item, method) }
else
@subtype.public_send(method, value)
end
end
end
end
end
end
end
# frozen_string_literal: true
require "active_support/core_ext/array/extract"
module ActiveRecord
class PredicateBuilder
class ArrayHandler # :nodoc:
def initialize(predicate_builder)
@predicate_builder = predicate_builder
end
def call(attribute, value)
return attribute.in([]) if value.empty?
values = value.map { |x| x.is_a?(Base) ? x.id : x }
nils = values.extract!(&:nil?)
ranges = values.extract! { |v| v.is_a?(Range) }
values_predicate =
case values.length
when 0 then NullPredicate
when 1 then predicate_builder.build(attribute, values.first)
else Arel::Nodes::HomogeneousIn.new(values, attribute, :in)
end
unless nils.empty?
values_predicate = values_predicate.or(attribute.eq(nil))
end
if ranges.empty?
values_predicate
else
array_predicates = ranges.map! { |range| predicate_builder.build(attribute, range) }
array_predicates.inject(values_predicate, &:or)
end
end
private
attr_reader :predicate_builder
module NullPredicate # :nodoc:
def self.or(other)
other
end
end
end
end
end
# frozen_string_literal: true
require "cases/helper"
require "support/schema_dumping_helper"
class PostgresqlArrayTest < ActiveRecord::PostgreSQLTestCase
include SchemaDumpingHelper
include InTimeZone
class PgArray < ActiveRecord::Base
self.table_name = "pg_arrays"
end
def setup
@connection = ActiveRecord::Base.connection
enable_extension!("hstore", @connection)
@connection.transaction do
@connection.create_table "pg_arrays", force: true do |t|
t.string "tags", array: true, limit: 255
t.integer "ratings", array: true
t.datetime :datetimes, array: true
t.hstore :hstores, array: true
t.decimal :decimals, array: true, default: [], precision: 10, scale: 2
t.timestamp :timestamps, array: true, default: [], precision: 6
end
end
PgArray.reset_column_information
@column = PgArray.columns_hash["tags"]
@type = PgArray.type_for_attribute("tags")
end
teardown do
@connection.drop_table "pg_arrays", if_exists: true
disable_extension!("hstore", @connection)
end
def test_column
assert_equal :string, @column.type
assert_equal "character varying(255)", @column.sql_type
assert_predicate @column, :array?
assert_not_predicate @type, :binary?
ratings_column = PgArray.columns_hash["ratings"]
assert_equal :integer, ratings_column.type
assert_predicate ratings_column, :array?
end
def test_not_compatible_with_serialize_array
new_klass = Class.new(PgArray) do
serialize :tags, Array
end
assert_raises(ActiveRecord::AttributeMethods::Serialization::ColumnNotSerializableError) do
new_klass.new
end
end
class MyTags
def initialize(tags); @tags = tags end
def to_a; @tags end
def self.load(tags); new(tags) end
def self.dump(object); object.to_a end
end
def test_array_with_serialized_attributes
new_klass = Class.new(PgArray) do
serialize :tags, MyTags
end
new_klass.create!(tags: MyTags.new(["one", "two"]))
record = new_klass.first
assert_instance_of MyTags, record.tags
assert_equal ["one", "two"], record.tags.to_a
record.tags = MyTags.new(["three", "four"])
record.save!
assert_equal ["three", "four"], record.reload.tags.to_a
end
def test_default
@connection.add_column "pg_arrays", "score", :integer, array: true, default: [4, 4, 2]
PgArray.reset_column_information
assert_equal([4, 4, 2], PgArray.column_defaults["score"])
assert_equal([4, 4, 2], PgArray.new.score)
ensure
PgArray.reset_column_information
end
def test_default_strings
@connection.add_column "pg_arrays", "names", :string, array: true, default: ["foo", "bar"]
PgArray.reset_column_information
assert_equal(["foo", "bar"], PgArray.column_defaults["names"])
assert_equal(["foo", "bar"], PgArray.new.names)
ensure
PgArray.reset_column_information
end
def test_change_column_with_array
@connection.add_column :pg_arrays, :snippets, :string, array: true, default: []
@connection.change_column :pg_arrays, :snippets, :text, array: true, default: []
PgArray.reset_column_information
column = PgArray.columns_hash["snippets"]
assert_equal :text, column.type
assert_equal [], PgArray.column_defaults["snippets"]
assert_predicate column, :array?
end
def test_change_column_from_non_array_to_array
@connection.add_column :pg_arrays, :snippets, :string
@connection.change_column :pg_arrays, :snippets, :text, array: true, default: [], using: "string_to_array(\"snippets\", ',')"
PgArray.reset_column_information
column = PgArray.columns_hash["snippets"]
assert_equal :text, column.type
assert_equal [], PgArray.column_defaults["snippets"]
assert_predicate column, :array?
end
def test_change_column_cant_make_non_array_column_to_array
@connection.add_column :pg_arrays, :a_string, :string
assert_raises ActiveRecord::StatementInvalid do
@connection.transaction do
@connection.change_column :pg_arrays, :a_string, :string, array: true
end
end
end
def test_change_column_default_with_array
@connection.change_column_default :pg_arrays, :tags, []
PgArray.reset_column_information
assert_equal [], PgArray.column_defaults["tags"]
end
def test_type_cast_array
assert_equal(["1", "2", "3"], @type.deserialize("{1,2,3}"))
assert_equal([], @type.deserialize("{}"))
assert_equal([nil], @type.deserialize("{NULL}"))
end
def test_type_cast_integers
x = PgArray.new(ratings: ["1", "2"])
assert_equal([1, 2], x.ratings)
x.save!
x.reload
assert_equal([1, 2], x.ratings)
end
def test_schema_dump_with_shorthand
output = dump_table_schema "pg_arrays"
assert_match %r[t\.string\s+"tags",\s+limit: 255,\s+array: true], output
assert_match %r[t\.integer\s+"ratings",\s+array: true], output
assert_match %r[t\.decimal\s+"decimals",\s+precision: 10,\s+scale: 2,\s+default: \[\],\s+array: true], output
end
def test_select_with_strings
@connection.execute "insert into pg_arrays (tags) VALUES ('{1,2,3}')"
x = PgArray.first
assert_equal(["1", "2", "3"], x.tags)
end
def test_rewrite_with_strings
@connection.execute "insert into pg_arrays (tags) VALUES ('{1,2,3}')"
x = PgArray.first
x.tags = ["1", "2", "3", "4"]
x.save!
assert_equal ["1", "2", "3", "4"], x.reload.tags
end
def test_select_with_integers
@connection.execute "insert into pg_arrays (ratings) VALUES ('{1,2,3}')"
x = PgArray.first
assert_equal([1, 2, 3], x.ratings)
end
def test_rewrite_with_integers
@connection.execute "insert into pg_arrays (ratings) VALUES ('{1,2,3}')"
x = PgArray.first
x.ratings = [2, "3", 4]
x.save!
assert_equal [2, 3, 4], x.reload.ratings
end
def test_multi_dimensional_with_strings
assert_cycle(:tags, [[["1"], ["2"]], [["2"], ["3"]]])
end
def test_with_empty_strings
assert_cycle(:tags, [ "1", "2", "", "4", "", "5" ])
end
def test_with_multi_dimensional_empty_strings
assert_cycle(:tags, [[["1", "2"], ["", "4"], ["", "5"]]])
end
def test_with_arbitrary_whitespace
assert_cycle(:tags, [[["1", "2"], [" ", "4"], [" ", "5"]]])
end
def test_multi_dimensional_with_integers
assert_cycle(:ratings, [[[1], [7]], [[8], [10]]])
end
def test_strings_with_quotes
assert_cycle(:tags, ["this has", 'some "s that need to be escaped"'])
end
def test_strings_with_commas
assert_cycle(:tags, ["this,has", "many,values"])
end
def test_strings_with_array_delimiters
assert_cycle(:tags, ["{", "}"])
end
def test_strings_with_null_strings
assert_cycle(:tags, ["NULL", "NULL"])
end
def test_contains_nils
assert_cycle(:tags, ["1", nil, nil])
end
def test_insert_fixture
tag_values = ["val1", "val2", "val3_with_'_multiple_quote_'_chars"]
@connection.insert_fixture({ "tags" => tag_values }, "pg_arrays")
assert_equal(PgArray.last.tags, tag_values)
end
def test_attribute_for_inspect_for_array_field
record = PgArray.new { |a| a.ratings = (1..10).to_a }
assert_equal("[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]", record.attribute_for_inspect(:ratings))
end
def test_attribute_for_inspect_for_array_field_for_large_array
record = PgArray.new { |a| a.ratings = (1..11).to_a }
assert_equal("[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]", record.attribute_for_inspect(:ratings))
end
def test_escaping
unknown = 'foo\\",bar,baz,\\'
tags = ["hello_#{unknown}"]
ar = PgArray.create!(tags: tags)
ar.reload
assert_equal tags, ar.tags
end
def test_string_quoting_rules_match_pg_behavior
tags = ["", "one{", "two}", %(three"), "four\\", "five ", "six\t", "seven\n", "eight,", "nine", "ten\r", "NULL"]
x = PgArray.create!(tags: tags)
x.reload
assert_not_predicate x, :changed?
end
def test_quoting_non_standard_delimiters
strings = ["hello,", "world;"]
oid = ActiveRecord::ConnectionAdapters::PostgreSQL::OID
comma_delim = oid::Array.new(ActiveRecord::Type::String.new, ",")
semicolon_delim = oid::Array.new(ActiveRecord::Type::String.new, ";")
conn = PgArray.connection
assert_equal %({"hello,",world;}), conn.type_cast(comma_delim.serialize(strings))
assert_equal %({hello,;"world;"}), conn.type_cast(semicolon_delim.serialize(strings))
end
def test_mutate_array
x = PgArray.create!(tags: %w(one two))
x.tags << "three"
x.save!
x.reload
assert_equal %w(one two three), x.tags
assert_not_predicate x, :changed?
end
def test_mutate_value_in_array
x = PgArray.create!(hstores: [{ a: "a" }, { b: "b" }])
x.hstores.first["a"] = "c"
x.save!
x.reload
assert_equal [{ "a" => "c" }, { "b" => "b" }], x.hstores
assert_not_predicate x, :changed?
end
def test_datetime_with_timezone_awareness
tz = "Pacific Time (US & Canada)"
in_time_zone tz do
PgArray.reset_column_information
time_string = Time.current.to_s
time = Time.zone.parse(time_string)
record = PgArray.new(datetimes: [time_string])
assert_equal [time], record.datetimes
assert_equal ActiveSupport::TimeZone[tz], record.datetimes.first.time_zone
record.save!
record.reload
assert_equal [time], record.datetimes
assert_equal ActiveSupport::TimeZone[tz], record.datetimes.first.time_zone
end
end
def test_assigning_non_array_value
record = PgArray.new(tags: "not-an-array")
assert_equal [], record.tags
assert_equal "not-an-array", record.tags_before_type_cast
assert record.save
assert_equal record.tags, record.reload.tags
end
def test_assigning_empty_string
record = PgArray.new(tags: "")
assert_equal [], record.tags
assert_equal "", record.tags_before_type_cast
assert record.save
assert_equal record.tags, record.reload.tags
end
def test_assigning_valid_pg_array_literal
record = PgArray.new(tags: "{1,2,3}")
assert_equal ["1", "2", "3"], record.tags
assert_equal "{1,2,3}", record.tags_before_type_cast
assert record.save
assert_equal record.tags, record.reload.tags
end
def test_where_by_attribute_with_array
tags = ["black", "blue"]
record = PgArray.create!(tags: tags)
assert_equal record, PgArray.where(tags: tags).take
end
def test_uniqueness_validation
klass = Class.new(PgArray) do
validates_uniqueness_of :tags
def self.model_name; ActiveModel::Name.new(PgArray) end
end
e1 = klass.create("tags" => ["black", "blue"])
assert e1.persisted?, "Saving e1"
e2 = klass.create("tags" => ["black", "blue"])
assert_not e2.persisted?, "e2 shouldn't be valid"
assert e2.errors[:tags].any?, "Should have errors for tags"
assert_equal ["has already been taken"], e2.errors[:tags], "Should have uniqueness message for tags"
end
def test_encoding_arrays_of_utf8_strings
arrays_of_utf8_strings = %w(nový ファイル)
assert_equal arrays_of_utf8_strings, @type.deserialize(@type.serialize(arrays_of_utf8_strings))
assert_equal [arrays_of_utf8_strings], @type.deserialize(@type.serialize([arrays_of_utf8_strings]))
end
def test_precision_is_respected_on_timestamp_columns
time = Time.now.change(usec: 123)
record = PgArray.create!(timestamps: [time])
assert_equal 123, record.timestamps.first.usec
record.reload
assert_equal 123, record.timestamps.first.usec
end
private
def assert_cycle(field, array)
# test creation
x = PgArray.create!(field => array)
x.reload
assert_equal(array, x.public_send(field))
# test updating
x = PgArray.create!(field => [])
x.public_send("#{field}=", array)
x.save!
x.reload
assert_equal(array, x.public_send(field))
end
end
# frozen_string_literal: true
class Publisher::Article < ActiveRecord::Base
has_and_belongs_to_many :magazines
has_and_belongs_to_many :tags
end
# frozen_string_literal: true
class ARUnit2Model < ActiveRecord::Base
self.abstract_class = true
end
# frozen_string_literal: true
require_relative "../helper"
module Arel
module Nodes
describe "As" do
describe "#as" do
it "makes an AS node" do
attr = Table.new(:users)[:id]
as = attr.as(Arel.sql("foo"))
assert_equal attr, as.left
assert_equal "foo", as.right
end
it "converts right to SqlLiteral if a string" do
attr = Table.new(:users)[:id]
as = attr.as("foo")
assert_kind_of Arel::Nodes::SqlLiteral, as.right
end
end
describe "equality" do
it "is equal with equal ivars" do
array = [As.new("foo", "bar"), As.new("foo", "bar")]
assert_equal 1, array.uniq.size
end
it "is not equal with different ivars" do
array = [As.new("foo", "bar"), As.new("foo", "baz")]
assert_equal 2, array.uniq.size
end
end
end
end
end
# frozen_string_literal: true
module Arel # :nodoc: all
module Nodes
class Ascending < Ordering
def reverse
Descending.new(expr)
end
def direction
:asc
end
def ascending?
true
end
def descending?
false
end
end
end
end
# frozen_string_literal: true
require_relative "../helper"
module Arel
module Nodes
class TestAscending < Arel::Test
def test_construct
ascending = Ascending.new "zomg"
assert_equal "zomg", ascending.expr
end
def test_reverse
ascending = Ascending.new "zomg"
descending = ascending.reverse
assert_kind_of Descending, descending
assert_equal ascending.expr, descending.expr
end
def test_direction
ascending = Ascending.new "zomg"
assert_equal :asc, ascending.direction
end
def test_ascending?
ascending = Ascending.new "zomg"
assert ascending.ascending?
end
def test_descending?
ascending = Ascending.new "zomg"
assert_not ascending.descending?
end
def test_equality_with_same_ivars
array = [Ascending.new("zomg"), Ascending.new("zomg")]
assert_equal 1, array.uniq.size
end
def test_inequality_with_different_ivars
array = [Ascending.new("zomg"), Ascending.new("zomg!")]
assert_equal 2, array.uniq.size
end
end
end
end
# frozen_string_literal: true
module ActiveRecord
module Validations
class AssociatedValidator < ActiveModel::EachValidator # :nodoc:
def validate_each(record, attribute, value)
if Array(value).reject { |r| valid_object?(r) }.any?
record.errors.add(attribute, :invalid, **options.merge(value: value))
end
end
private
def valid_object?(record)
(record.respond_to?(:marked_for_destruction?) && record.marked_for_destruction?) || record.valid?
end
end
module ClassMethods
# Validates whether the associated object or objects are all valid.
# Works with any kind of association.
#
# class Book < ActiveRecord::Base
# has_many :pages
# belongs_to :library
#
# validates_associated :pages, :library
# end
#
# WARNING: This validation must not be used on both ends of an association.
# Doing so will lead to a circular dependency and cause infinite recursion.
#
# NOTE: This validation will not fail if the association hasn't been
# assigned. If you want to ensure that the association is both present and
# guaranteed to be valid, you also need to use
# {validates_presence_of}[rdoc-ref:Validations::ClassMethods#validates_presence_of].
#
# Configuration options:
#
# * <tt>:message</tt> - A custom error message (default is: "is invalid").
# * <tt>:on</tt> - Specifies the contexts where this validation is active.
# Runs in all validation contexts by default +nil+. You can pass a symbol
# or an array of symbols. (e.g. <tt>on: :create</tt> or
# <tt>on: :custom_validation_context</tt> or
# <tt>on: [:create, :custom_validation_context]</tt>)
# * <tt>:if</tt> - Specifies a method, proc, or string to call to determine
# if the validation should occur (e.g. <tt>if: :allow_validation</tt>,
# or <tt>if: Proc.new { |user| user.signup_step > 2 }</tt>). The method,
# proc or string should return or evaluate to a +true+ or +false+ value.
# * <tt>:unless</tt> - Specifies a method, proc, or string to call to
# determine if the validation should not occur (e.g. <tt>unless: :skip_validation</tt>,
# or <tt>unless: Proc.new { |user| user.signup_step <= 2 }</tt>). The
# method, proc, or string should return or evaluate to a +true+ or +false+
# value.
def validates_associated(*attr_names)
validates_with AssociatedValidator, _merge_attributes(attr_names)
end
end
end
end
# frozen_string_literal: true
module ActiveRecord
module Associations
class Preloader
class Association # :nodoc:
class LoaderQuery
attr_reader :scope, :association_key_name
def initialize(scope, association_key_name)
@scope = scope
@association_key_name = association_key_name
end
def eql?(other)
association_key_name == other.association_key_name &&
scope.table_name == other.scope.table_name &&
scope.values_for_queries == other.scope.values_for_queries
end
def hash
[association_key_name, scope.table_name, scope.values_for_queries].hash
end
def records_for(loaders)
LoaderRecords.new(loaders, self).records
end
def load_records_in_batch(loaders)
raw_records = records_for(loaders)
loaders.each do |loader|
loader.load_records(raw_records)
loader.run
end
end
def load_records_for_keys(keys, &block)
scope.where(association_key_name => keys).load(&block)
end
end
class LoaderRecords
def initialize(loaders, loader_query)
@loader_query = loader_query
@loaders = loaders
@keys_to_load = Set.new
@already_loaded_records_by_key = {}
populate_keys_to_load_and_already_loaded_records
end
def records
load_records + already_loaded_records
end
private
attr_reader :loader_query, :loaders, :keys_to_load, :already_loaded_records_by_key
def populate_keys_to_load_and_already_loaded_records
loaders.each do |loader|
loader.owners_by_key.each do |key, owners|
if loaded_owner = owners.find { |owner| loader.loaded?(owner) }
already_loaded_records_by_key[key] = loader.target_for(loaded_owner)
else
keys_to_load << key
end
end
end
@keys_to_load.subtract(already_loaded_records_by_key.keys)
end
def load_records
loader_query.load_records_for_keys(keys_to_load) do |record|
loaders.each { |l| l.set_inverse(record) }
end
end
def already_loaded_records
already_loaded_records_by_key.values.flatten
end
end
attr_reader :klass
def initialize(klass, owners, reflection, preload_scope, reflection_scope, associate_by_default)
@klass = klass
@owners = owners.uniq(&:__id__)
@reflection = reflection
@preload_scope = preload_scope
@reflection_scope = reflection_scope
@associate = associate_by_default || !preload_scope || preload_scope.empty_scope?
@model = owners.first && owners.first.class
@run = false
end
def table_name
@klass.table_name
end
def future_classes
if run?
[]
else
[@klass]
end
end
def runnable_loaders
[self]
end
def run?
@run
end
def run
return self if run?
@run = true
records = records_by_owner
owners.each do |owner|
associate_records_to_owner(owner, records[owner] || [])
end if @associate
self
end
def records_by_owner
load_records unless defined?(@records_by_owner)
@records_by_owner
end
def preloaded_records
load_records unless defined?(@preloaded_records)
@preloaded_records
end
# The name of the key on the associated records
def association_key_name
reflection.join_primary_key(klass)
end
def loader_query
LoaderQuery.new(scope, association_key_name)
end
def owners_by_key
@owners_by_key ||= owners.each_with_object({}) do |owner, result|
key = convert_key(owner[owner_key_name])
(result[key] ||= []) << owner if key
end
end
def loaded?(owner)
owner.association(reflection.name).loaded?
end
def target_for(owner)
Array.wrap(owner.association(reflection.name).target)
end
def scope
@scope ||= build_scope
end
def set_inverse(record)
if owners = owners_by_key[convert_key(record[association_key_name])]
# Processing only the first owner
# because the record is modified but not an owner
association = owners.first.association(reflection.name)
association.set_inverse_instance(record)
end
end
def load_records(raw_records = nil)
# owners can be duplicated when a relation has a collection association join
# #compare_by_identity makes such owners different hash keys
@records_by_owner = {}.compare_by_identity
raw_records ||= loader_query.records_for([self])
@preloaded_records = raw_records.select do |record|
assignments = false
owners_by_key[convert_key(record[association_key_name])]&.each do |owner|
entries = (@records_by_owner[owner] ||= [])
if reflection.collection? || entries.empty?
entries << record
assignments = true
end
end
assignments
end
end
def associate_records_from_unscoped(unscoped_records)
return if unscoped_records.nil? || unscoped_records.empty?
return if !reflection_scope.empty_scope?
return if preload_scope && !preload_scope.empty_scope?
return if reflection.collection?
unscoped_records.select { |r| r[association_key_name].present? }.each do |record|
owners = owners_by_key[convert_key(record[association_key_name])]
owners&.each_with_index do |owner, i|
association = owner.association(reflection.name)
association.target = record
if i == 0 # Set inverse on first owner
association.set_inverse_instance(record)
end
end
end
end
private
attr_reader :owners, :reflection, :preload_scope, :model
# The name of the key on the model which declares the association
def owner_key_name
reflection.join_foreign_key
end
def associate_records_to_owner(owner, records)
return if loaded?(owner)
association = owner.association(reflection.name)
if reflection.collection?
association.target = records
else
association.target = records.first
end
end
def key_conversion_required?
unless defined?(@key_conversion_required)
@key_conversion_required = (association_key_type != owner_key_type)
end
@key_conversion_required
end
def convert_key(key)
if key_conversion_required?
key.to_s
else
key
end
end
def association_key_type
@klass.type_for_attribute(association_key_name).type
end
def owner_key_type
@model.type_for_attribute(owner_key_name).type
end
def reflection_scope
@reflection_scope ||= reflection.join_scopes(klass.arel_table, klass.predicate_builder, klass).inject(klass.unscoped, &:merge!)
end
def build_scope
scope = klass.scope_for_association
if reflection.type && !reflection.through_reflection?
scope.where!(reflection.type => model.polymorphic_name)
end
scope.merge!(reflection_scope) unless reflection_scope.empty_scope?
if preload_scope && !preload_scope.empty_scope?
scope.merge!(preload_scope)
end
cascade_strict_loading(scope)
end
def cascade_strict_loading(scope)
preload_scope&.strict_loading_value ? scope.strict_loading : scope
end
end
end
end
end
# frozen_string_literal: true
module ActiveRecord
class PredicateBuilder
class AssociationQueryValue # :nodoc:
def initialize(associated_table, value)
@associated_table = associated_table
@value = value
end
def queries
[ associated_table.join_foreign_key => ids ]
end
private
attr_reader :associated_table, :value
def ids
case value
when Relation
value.select_values.empty? ? value.select(primary_key) : value
when Array
value.map { |v| convert_to_id(v) }
else
convert_to_id(value)
end
end
def primary_key
associated_table.join_primary_key
end
def convert_to_id(value)
if value.respond_to?(primary_key)
value.public_send(primary_key)
else
value
end
end
end
end
end
# frozen_string_literal: true
module ActiveRecord
class AssociationRelation < Relation # :nodoc:
def initialize(klass, association, **)
super(klass)
@association = association
end
def proxy_association
@association
end
def ==(other)
other == records
end
%w(insert insert_all insert! insert_all! upsert upsert_all).each do |method|
class_eval <<~RUBY
def #{method}(attributes, **kwargs)
if @association.reflection.through_reflection?
raise ArgumentError, "Bulk insert or upsert is currently not supported for has_many through association"
end
scoping { klass.#{method}(attributes, **kwargs) }
end
RUBY
end
private
def _new(attributes, &block)
@association.build(attributes, &block)
end
def _create(attributes, &block)
@association.create(attributes, &block)
end
def _create!(attributes, &block)
@association.create!(attributes, &block)
end
def exec_queries
super do |record|
@association.set_inverse_instance_from_queries(record)
yield record if block_given?
end
end
end
end
# frozen_string_literal: true
module ActiveRecord
module Associations
class AssociationScope # :nodoc:
def self.scope(association)
INSTANCE.scope(association)
end
def self.create(&block)
block ||= lambda { |val| val }
new(block)
end
def initialize(value_transformation)
@value_transformation = value_transformation
end
INSTANCE = create
def scope(association)
klass = association.klass
reflection = association.reflection
scope = klass.unscoped
owner = association.owner
chain = get_chain(reflection, association, scope.alias_tracker)
scope.extending! reflection.extensions
scope = add_constraints(scope, owner, chain)
scope.limit!(1) unless reflection.collection?
scope
end
def self.get_bind_values(owner, chain)
binds = []
last_reflection = chain.last
binds << last_reflection.join_id_for(owner)
if last_reflection.type
binds << owner.class.polymorphic_name
end
chain.each_cons(2).each do |reflection, next_reflection|
if reflection.type
binds << next_reflection.klass.polymorphic_name
end
end
binds
end
private
attr_reader :value_transformation
def join(table, constraint)
Arel::Nodes::LeadingJoin.new(table, Arel::Nodes::On.new(constraint))
end
def last_chain_scope(scope, reflection, owner)
primary_key = reflection.join_primary_key
foreign_key = reflection.join_foreign_key
table = reflection.aliased_table
value = transform_value(owner[foreign_key])
scope = apply_scope(scope, table, primary_key, value)
if reflection.type
polymorphic_type = transform_value(owner.class.polymorphic_name)
scope = apply_scope(scope, table, reflection.type, polymorphic_type)
end
scope
end
def transform_value(value)
value_transformation.call(value)
end
def next_chain_scope(scope, reflection, next_reflection)
primary_key = reflection.join_primary_key
foreign_key = reflection.join_foreign_key
table = reflection.aliased_table
foreign_table = next_reflection.aliased_table
constraint = table[primary_key].eq(foreign_table[foreign_key])
if reflection.type
value = transform_value(next_reflection.klass.polymorphic_name)
scope = apply_scope(scope, table, reflection.type, value)
end
scope.joins!(join(foreign_table, constraint))
end
class ReflectionProxy < SimpleDelegator # :nodoc:
attr_reader :aliased_table
def initialize(reflection, aliased_table)
super(reflection)
@aliased_table = aliased_table
end
def all_includes; nil; end
end
def get_chain(reflection, association, tracker)
name = reflection.name
chain = [Reflection::RuntimeReflection.new(reflection, association)]
reflection.chain.drop(1).each do |refl|
aliased_table = tracker.aliased_table_for(refl.klass.arel_table) do
refl.alias_candidate(name)
end
chain << ReflectionProxy.new(refl, aliased_table)
end
chain
end
def add_constraints(scope, owner, chain)
scope = last_chain_scope(scope, chain.last, owner)
chain.each_cons(2) do |reflection, next_reflection|
scope = next_chain_scope(scope, reflection, next_reflection)
end
chain_head = chain.first
chain.reverse_each do |reflection|
reflection.constraints.each do |scope_chain_item|
item = eval_scope(reflection, scope_chain_item, owner)
if scope_chain_item == chain_head.scope
scope.merge! item.except(:where, :includes, :unscope, :order)
elsif !item.references_values.empty?
scope.merge! item.only(:joins, :left_outer_joins)
associations = item.eager_load_values | item.includes_values
unless associations.empty?
scope.joins! item.construct_join_dependency(associations, Arel::Nodes::OuterJoin)
end
end
reflection.all_includes do
scope.includes_values |= item.includes_values
end
scope.unscope!(*item.unscope_values)
scope.where_clause += item.where_clause
scope.order_values = item.order_values | scope.order_values
end
end
scope
end
def apply_scope(scope, table, key, value)
if scope.table == table
scope.where!(key => value)
else
scope.where!(table.name => { key => value })
end
end
def eval_scope(reflection, scope, owner)
relation = reflection.build_scope(reflection.aliased_table)
relation.instance_exec(owner, &scope) || relation
end
end
end
end
# frozen_string_literal: true
require "cases/helper"
require "models/topic"
require "models/reply"
require "models/human"
require "models/interest"
class AssociationValidationTest < ActiveRecord::TestCase
fixtures :topics
repair_validations(Topic, Reply)
def test_validates_associated_many
Topic.validates_associated(:replies)
Reply.validates_presence_of(:content)
t = Topic.create("title" => "uhohuhoh", "content" => "whatever")
t.replies << [r = Reply.new("title" => "A reply"), r2 = Reply.new("title" => "Another reply", "content" => "non-empty"), r3 = Reply.new("title" => "Yet another reply"), r4 = Reply.new("title" => "The last reply", "content" => "non-empty")]
assert_not_predicate t, :valid?
assert_predicate t.errors[:replies], :any?
assert_equal 1, r.errors.count # make sure all associated objects have been validated
assert_equal 0, r2.errors.count
assert_equal 1, r3.errors.count
assert_equal 0, r4.errors.count
r.content = r3.content = "non-empty"
assert_predicate t, :valid?
end
def test_validates_associated_one
Reply.validates :topic, associated: true
Topic.validates_presence_of(:content)
r = Reply.new("title" => "A reply", "content" => "with content!")
r.topic = Topic.create("title" => "uhohuhoh")
assert_not_predicate r, :valid?
assert_predicate r.errors[:topic], :any?
r.topic.content = "non-empty"
assert_predicate r, :valid?
end
def test_validates_associated_marked_for_destruction
Topic.validates_associated(:replies)
Reply.validates_presence_of(:content)
t = Topic.new
t.replies << Reply.new
assert_predicate t, :invalid?
t.replies.first.mark_for_destruction
assert_predicate t, :valid?
end
def test_validates_associated_without_marked_for_destruction
reply = Class.new do
def valid?
true
end
end
Topic.validates_associated(:replies)
t = Topic.new
t.define_singleton_method(:replies) { [reply.new] }
assert_predicate t, :valid?
end
def test_validates_associated_with_custom_message_using_quotes
Reply.validates_associated :topic, message: "This string contains 'single' and \"double\" quotes"
Topic.validates_presence_of :content
r = Reply.create("title" => "A reply", "content" => "with content!")
r.topic = Topic.create("title" => "uhohuhoh")
assert_not_operator r, :valid?
assert_equal ["This string contains 'single' and \"double\" quotes"], r.errors[:topic]
end
def test_validates_associated_missing
Reply.validates_presence_of(:topic)
r = Reply.create("title" => "A reply", "content" => "with content!")
assert_not_predicate r, :valid?
assert_predicate r.errors[:topic], :any?
r.topic = Topic.first
assert_predicate r, :valid?
end
def test_validates_presence_of_belongs_to_association__parent_is_new_record
repair_validations(Interest) do
# Note that Interest and Human have the :inverse_of option set
Interest.validates_presence_of(:human)
human = Human.new(name: "John")
interest = human.interests.build(topic: "Airplanes")
assert interest.valid?, "Expected interest to be valid, but was not. Interest should have a human object associated"
end
end
def test_validates_presence_of_belongs_to_association__existing_parent
repair_validations(Interest) do
Interest.validates_presence_of(:human)
human = Human.create!(name: "John")
interest = human.interests.build(topic: "Airplanes")
assert interest.valid?, "Expected interest to be valid, but was not. Interest should have a human object associated"
end
end
end
# frozen_string_literal: true
module ActiveRecord
class AssociationNotFoundError < ConfigurationError # :nodoc:
attr_reader :record, :association_name
def initialize(record = nil, association_name = nil)
@record = record
@association_name = association_name
if record && association_name
super("Association named '#{association_name}' was not found on #{record.class.name}; perhaps you misspelled it?")
else
super("Association was not found.")
end
end
if defined?(DidYouMean::Correctable) && defined?(DidYouMean::SpellChecker)
include DidYouMean::Correctable
def corrections
if record && association_name
@corrections ||= begin
maybe_these = record.class.reflections.keys
DidYouMean::SpellChecker.new(dictionary: maybe_these).correct(association_name)
end
else
[]
end
end
end
end
class InverseOfAssociationNotFoundError < ActiveRecordError # :nodoc:
attr_reader :reflection, :associated_class
def initialize(reflection = nil, associated_class = nil)
if reflection
@reflection = reflection
@associated_class = associated_class.nil? ? reflection.klass : associated_class
super("Could not find the inverse association for #{reflection.name} (#{reflection.options[:inverse_of].inspect} in #{associated_class.nil? ? reflection.class_name : associated_class.name})")
else
super("Could not find the inverse association.")
end
end
if defined?(DidYouMean::Correctable) && defined?(DidYouMean::SpellChecker)
include DidYouMean::Correctable
def corrections
if reflection && associated_class
@corrections ||= begin
maybe_these = associated_class.reflections.keys
DidYouMean::SpellChecker.new(dictionary: maybe_these).correct(reflection.options[:inverse_of].to_s)
end
else
[]
end
end
end
end
class InverseOfAssociationRecursiveError < ActiveRecordError # :nodoc:
attr_reader :reflection
def initialize(reflection = nil)
if reflection
@reflection = reflection
super("Inverse association #{reflection.name} (#{reflection.options[:inverse_of].inspect} in #{reflection.class_name}) is recursive.")
else
super("Inverse association is recursive.")
end
end
end
class HasManyThroughAssociationNotFoundError < ActiveRecordError # :nodoc:
attr_reader :owner_class, :reflection
def initialize(owner_class = nil, reflection = nil)
if owner_class && reflection
@owner_class = owner_class
@reflection = reflection
super("Could not find the association #{reflection.options[:through].inspect} in model #{owner_class.name}")
else
super("Could not find the association.")
end
end
if defined?(DidYouMean::Correctable) && defined?(DidYouMean::SpellChecker)
include DidYouMean::Correctable
def corrections
if owner_class && reflection
@corrections ||= begin
maybe_these = owner_class.reflections.keys
maybe_these -= [reflection.name.to_s] # remove failing reflection
DidYouMean::SpellChecker.new(dictionary: maybe_these).correct(reflection.options[:through].to_s)
end
else
[]
end
end
end
end
class HasManyThroughAssociationPolymorphicSourceError < ActiveRecordError # :nodoc:
def initialize(owner_class_name = nil, reflection = nil, source_reflection = nil)
if owner_class_name && reflection && source_reflection
super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' on the polymorphic object '#{source_reflection.class_name}##{source_reflection.name}' without 'source_type'. Try adding 'source_type: \"#{reflection.name.to_s.classify}\"' to 'has_many :through' definition.")
else
super("Cannot have a has_many :through association.")
end
end
end
class HasManyThroughAssociationPolymorphicThroughError < ActiveRecordError # :nodoc:
def initialize(owner_class_name = nil, reflection = nil)
if owner_class_name && reflection
super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' which goes through the polymorphic association '#{owner_class_name}##{reflection.through_reflection.name}'.")
else
super("Cannot have a has_many :through association.")
end
end
end
class HasManyThroughAssociationPointlessSourceTypeError < ActiveRecordError # :nodoc:
def initialize(owner_class_name = nil, reflection = nil, source_reflection = nil)
if owner_class_name && reflection && source_reflection
super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' with a :source_type option if the '#{reflection.through_reflection.class_name}##{source_reflection.name}' is not polymorphic. Try removing :source_type on your association.")
else
super("Cannot have a has_many :through association.")
end
end
end
class HasOneThroughCantAssociateThroughCollection < ActiveRecordError # :nodoc:
def initialize(owner_class_name = nil, reflection = nil, through_reflection = nil)
if owner_class_name && reflection && through_reflection
super("Cannot have a has_one :through association '#{owner_class_name}##{reflection.name}' where the :through association '#{owner_class_name}##{through_reflection.name}' is a collection. Specify a has_one or belongs_to association in the :through option instead.")
else
super("Cannot have a has_one :through association.")
end
end
end
class HasOneAssociationPolymorphicThroughError < ActiveRecordError # :nodoc:
def initialize(owner_class_name = nil, reflection = nil)
if owner_class_name && reflection
super("Cannot have a has_one :through association '#{owner_class_name}##{reflection.name}' which goes through the polymorphic association '#{owner_class_name}##{reflection.through_reflection.name}'.")
else
super("Cannot have a has_one :through association.")
end
end
end
class HasManyThroughSourceAssociationNotFoundError < ActiveRecordError # :nodoc:
def initialize(reflection = nil)
if reflection
through_reflection = reflection.through_reflection
source_reflection_names = reflection.source_reflection_names
source_associations = reflection.through_reflection.klass._reflections.keys
super("Could not find the source association(s) #{source_reflection_names.collect(&:inspect).to_sentence(two_words_connector: ' or ', last_word_connector: ', or ')} in model #{through_reflection.klass}. Try 'has_many #{reflection.name.inspect}, :through => #{through_reflection.name.inspect}, :source => <name>'. Is it one of #{source_associations.to_sentence(two_words_connector: ' or ', last_word_connector: ', or ')}?")
else
super("Could not find the source association(s).")
end
end
end
class HasManyThroughOrderError < ActiveRecordError # :nodoc:
def initialize(owner_class_name = nil, reflection = nil, through_reflection = nil)
if owner_class_name && reflection && through_reflection
super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' which goes through '#{owner_class_name}##{through_reflection.name}' before the through association is defined.")
else
super("Cannot have a has_many :through association before the through association is defined.")
end
end
end
class ThroughCantAssociateThroughHasOneOrManyReflection < ActiveRecordError # :nodoc:
def initialize(owner = nil, reflection = nil)
if owner && reflection
super("Cannot modify association '#{owner.class.name}##{reflection.name}' because the source reflection class '#{reflection.source_reflection.class_name}' is associated to '#{reflection.through_reflection.class_name}' via :#{reflection.source_reflection.macro}.")
else
super("Cannot modify association.")
end
end
end
class AmbiguousSourceReflectionForThroughAssociation < ActiveRecordError # :nodoc:
def initialize(klass, macro, association_name, options, possible_sources)
example_options = options.dup
example_options[:source] = possible_sources.first
super("Ambiguous source reflection for through association. Please " \
"specify a :source directive on your declaration like:\n" \
"\n" \
" class #{klass} < ActiveRecord::Base\n" \
" #{macro} :#{association_name}, #{example_options}\n" \
" end"
)
end
end
class HasManyThroughCantAssociateThroughHasOneOrManyReflection < ThroughCantAssociateThroughHasOneOrManyReflection # :nodoc:
end
class HasOneThroughCantAssociateThroughHasOneOrManyReflection < ThroughCantAssociateThroughHasOneOrManyReflection # :nodoc:
end
class ThroughNestedAssociationsAreReadonly < ActiveRecordError # :nodoc:
def initialize(owner = nil, reflection = nil)
if owner && reflection
super("Cannot modify association '#{owner.class.name}##{reflection.name}' because it goes through more than one other association.")
else
super("Through nested associations are read-only.")
end
end
end
class HasManyThroughNestedAssociationsAreReadonly < ThroughNestedAssociationsAreReadonly # :nodoc:
end
class HasOneThroughNestedAssociationsAreReadonly < ThroughNestedAssociationsAreReadonly # :nodoc:
end
# This error is raised when trying to eager load a polymorphic association using a JOIN.
# Eager loading polymorphic associations is only possible with
# {ActiveRecord::Relation#preload}[rdoc-ref:QueryMethods#preload].
class EagerLoadPolymorphicError < ActiveRecordError
def initialize(reflection = nil)
if reflection
super("Cannot eagerly load the polymorphic association #{reflection.name.inspect}")
else
super("Eager load polymorphic error.")
end
end
end
# This error is raised when trying to destroy a parent instance in N:1 or 1:1 associations
# (has_many, has_one) when there is at least 1 child associated instance.
# ex: if @project.tasks.size > 0, DeleteRestrictionError will be raised when trying to destroy @project
class DeleteRestrictionError < ActiveRecordError # :nodoc:
def initialize(name = nil)
if name
super("Cannot delete record because of dependent #{name}")
else
super("Delete restriction error.")
end
end
end
# See ActiveRecord::Associations::ClassMethods for documentation.
module Associations # :nodoc:
extend ActiveSupport::Autoload
extend ActiveSupport::Concern
# These classes will be loaded when associations are created.
# So there is no need to eager load them.
autoload :Association
autoload :SingularAssociation
autoload :CollectionAssociation
autoload :ForeignAssociation
autoload :CollectionProxy
autoload :ThroughAssociation
module Builder # :nodoc:
autoload :Association, "active_record/associations/builder/association"
autoload :SingularAssociation, "active_record/associations/builder/singular_association"
autoload :CollectionAssociation, "active_record/associations/builder/collection_association"
autoload :BelongsTo, "active_record/associations/builder/belongs_to"
autoload :HasOne, "active_record/associations/builder/has_one"
autoload :HasMany, "active_record/associations/builder/has_many"
autoload :HasAndBelongsToMany, "active_record/associations/builder/has_and_belongs_to_many"
end
eager_autoload do
autoload :BelongsToAssociation
autoload :BelongsToPolymorphicAssociation
autoload :HasManyAssociation
autoload :HasManyThroughAssociation
autoload :HasOneAssociation
autoload :HasOneThroughAssociation
autoload :Preloader
autoload :JoinDependency
autoload :AssociationScope
autoload :DisableJoinsAssociationScope
autoload :AliasTracker
end
def self.eager_load!
super
Preloader.eager_load!
JoinDependency.eager_load!
end
# Returns the association instance for the given name, instantiating it if it doesn't already exist
def association(name) # :nodoc:
association = association_instance_get(name)
if association.nil?
unless reflection = self.class._reflect_on_association(name)
raise AssociationNotFoundError.new(self, name)
end
association = reflection.association_class.new(self, reflection)
association_instance_set(name, association)
end
association
end
def association_cached?(name) # :nodoc:
@association_cache.key?(name)
end
def initialize_dup(*) # :nodoc:
@association_cache = {}
super
end
private
def init_internals
@association_cache = {}
super
end
# Returns the specified association instance if it exists, +nil+ otherwise.
def association_instance_get(name)
@association_cache[name]
end
# Set the specified association instance.
def association_instance_set(name, association)
@association_cache[name] = association
end
# \Associations are a set of macro-like class methods for tying objects together through
# foreign keys. They express relationships like "Project has one Project Manager"
# or "Project belongs to a Portfolio". Each macro adds a number of methods to the
# class which are specialized according to the collection or association symbol and the
# options hash. It works much the same way as Ruby's own <tt>attr*</tt>
# methods.
#
# class Project < ActiveRecord::Base
# belongs_to :portfolio
# has_one :project_manager
# has_many :milestones
# has_and_belongs_to_many :categories
# end
#
# The project class now has the following methods (and more) to ease the traversal and
# manipulation of its relationships:
# * <tt>Project#portfolio</tt>, <tt>Project#portfolio=(portfolio)</tt>, <tt>Project#reload_portfolio</tt>
# * <tt>Project#project_manager</tt>, <tt>Project#project_manager=(project_manager)</tt>, <tt>Project#reload_project_manager</tt>
# * <tt>Project#milestones.empty?</tt>, <tt>Project#milestones.size</tt>, <tt>Project#milestones</tt>, <tt>Project#milestones<<(milestone)</tt>,
# <tt>Project#milestones.delete(milestone)</tt>, <tt>Project#milestones.destroy(milestone)</tt>, <tt>Project#milestones.find(milestone_id)</tt>,
# <tt>Project#milestones.build</tt>, <tt>Project#milestones.create</tt>
# * <tt>Project#categories.empty?</tt>, <tt>Project#categories.size</tt>, <tt>Project#categories</tt>, <tt>Project#categories<<(category1)</tt>,
# <tt>Project#categories.delete(category1)</tt>, <tt>Project#categories.destroy(category1)</tt>
#
# === A word of warning
#
# Don't create associations that have the same name as {instance methods}[rdoc-ref:ActiveRecord::Core] of
# <tt>ActiveRecord::Base</tt>. Since the association adds a method with that name to
# its model, using an association with the same name as one provided by <tt>ActiveRecord::Base</tt> will override the method inherited through <tt>ActiveRecord::Base</tt> and will break things.
# For instance, +attributes+ and +connection+ would be bad choices for association names, because those names already exist in the list of <tt>ActiveRecord::Base</tt> instance methods.
#
# == Auto-generated methods
# See also Instance Public methods below for more details.
#
# === Singular associations (one-to-one)
# | | belongs_to |
# generated methods | belongs_to | :polymorphic | has_one
# ----------------------------------+------------+--------------+---------
# other | X | X | X
# other=(other) | X | X | X
# build_other(attributes={}) | X | | X
# create_other(attributes={}) | X | | X
# create_other!(attributes={}) | X | | X
# reload_other | X | X | X
# other_changed? | X | X |
# other_previously_changed? | X | X |
#
# === Collection associations (one-to-many / many-to-many)
# | | | has_many
# generated methods | habtm | has_many | :through
# ----------------------------------+-------+----------+----------
# others | X | X | X
# others=(other,other,...) | X | X | X
# other_ids | X | X | X
# other_ids=(id,id,...) | X | X | X
# others<< | X | X | X
# others.push | X | X | X
# others.concat | X | X | X
# others.build(attributes={}) | X | X | X
# others.create(attributes={}) | X | X | X
# others.create!(attributes={}) | X | X | X
# others.size | X | X | X
# others.length | X | X | X
# others.count | X | X | X
# others.sum(*args) | X | X | X
# others.empty? | X | X | X
# others.clear | X | X | X
# others.delete(other,other,...) | X | X | X
# others.delete_all | X | X | X
# others.destroy(other,other,...) | X | X | X
# others.destroy_all | X | X | X
# others.find(*args) | X | X | X
# others.exists? | X | X | X
# others.distinct | X | X | X
# others.reset | X | X | X
# others.reload | X | X | X
#
# === Overriding generated methods
#
# Association methods are generated in a module included into the model
# class, making overrides easy. The original generated method can thus be
# called with +super+:
#
# class Car < ActiveRecord::Base
# belongs_to :owner
# belongs_to :old_owner
#
# def owner=(new_owner)
# self.old_owner = self.owner
# super
# end
# end
#
# The association methods module is included immediately after the
# generated attributes methods module, meaning an association will
# override the methods for an attribute with the same name.
#
# == Cardinality and associations
#
# Active Record associations can be used to describe one-to-one, one-to-many, and many-to-many
# relationships between models. Each model uses an association to describe its role in
# the relation. The #belongs_to association is always used in the model that has
# the foreign key.
#
# === One-to-one
#
# Use #has_one in the base, and #belongs_to in the associated model.
#
# class Employee < ActiveRecord::Base
# has_one :office
# end
# class Office < ActiveRecord::Base
# belongs_to :employee # foreign key - employee_id
# end
#
# === One-to-many
#
# Use #has_many in the base, and #belongs_to in the associated model.
#
# class Manager < ActiveRecord::Base
# has_many :employees
# end
# class Employee < ActiveRecord::Base
# belongs_to :manager # foreign key - manager_id
# end
#
# === Many-to-many
#
# There are two ways to build a many-to-many relationship.
#
# The first way uses a #has_many association with the <tt>:through</tt> option and a join model, so
# there are two stages of associations.
#
# class Assignment < ActiveRecord::Base
# belongs_to :programmer # foreign key - programmer_id
# belongs_to :project # foreign key - project_id
# end
# class Programmer < ActiveRecord::Base
# has_many :assignments
# has_many :projects, through: :assignments
# end
# class Project < ActiveRecord::Base
# has_many :assignments
# has_many :programmers, through: :assignments
# end
#
# For the second way, use #has_and_belongs_to_many in both models. This requires a join table
# that has no corresponding model or primary key.
#
# class Programmer < ActiveRecord::Base
# has_and_belongs_to_many :projects # foreign keys in the join table
# end
# class Project < ActiveRecord::Base
# has_and_belongs_to_many :programmers # foreign keys in the join table
# end
#
# Choosing which way to build a many-to-many relationship is not always simple.
# If you need to work with the relationship model as its own entity,
# use #has_many <tt>:through</tt>. Use #has_and_belongs_to_many when working with legacy schemas or when
# you never work directly with the relationship itself.
#
# == Is it a #belongs_to or #has_one association?
#
# Both express a 1-1 relationship. The difference is mostly where to place the foreign
# key, which goes on the table for the class declaring the #belongs_to relationship.
#
# class User < ActiveRecord::Base
# # I reference an account.
# belongs_to :account
# end
#
# class Account < ActiveRecord::Base
# # One user references me.
# has_one :user
# end
#
# The tables for these classes could look something like:
#
# CREATE TABLE users (
# id bigint NOT NULL auto_increment,
# account_id bigint default NULL,
# name varchar default NULL,
# PRIMARY KEY (id)
# )
#
# CREATE TABLE accounts (
# id bigint NOT NULL auto_increment,
# name varchar default NULL,
# PRIMARY KEY (id)
# )
#
# == Unsaved objects and associations
#
# You can manipulate objects and associations before they are saved to the database, but
# there is some special behavior you should be aware of, mostly involving the saving of
# associated objects.
#
# You can set the <tt>:autosave</tt> option on a #has_one, #belongs_to,
# #has_many, or #has_and_belongs_to_many association. Setting it
# to +true+ will _always_ save the members, whereas setting it to +false+ will
# _never_ save the members. More details about <tt>:autosave</tt> option is available at
# AutosaveAssociation.
#
# === One-to-one associations
#
# * Assigning an object to a #has_one association automatically saves that object and
# the object being replaced (if there is one), in order to update their foreign
# keys - except if the parent object is unsaved (<tt>new_record? == true</tt>).
# * If either of these saves fail (due to one of the objects being invalid), an
# ActiveRecord::RecordNotSaved exception is raised and the assignment is
# cancelled.
# * If you wish to assign an object to a #has_one association without saving it,
# use the <tt>#build_association</tt> method (documented below). The object being
# replaced will still be saved to update its foreign key.
# * Assigning an object to a #belongs_to association does not save the object, since
# the foreign key field belongs on the parent. It does not save the parent either.
#
# === Collections
#
# * Adding an object to a collection (#has_many or #has_and_belongs_to_many) automatically
# saves that object, except if the parent object (the owner of the collection) is not yet
# stored in the database.
# * If saving any of the objects being added to a collection (via <tt>push</tt> or similar)
# fails, then <tt>push</tt> returns +false+.
# * If saving fails while replacing the collection (via <tt>association=</tt>), an
# ActiveRecord::RecordNotSaved exception is raised and the assignment is
# cancelled.
# * You can add an object to a collection without automatically saving it by using the
# <tt>collection.build</tt> method (documented below).
# * All unsaved (<tt>new_record? == true</tt>) members of the collection are automatically
# saved when the parent is saved.
#
# == Customizing the query
#
# \Associations are built from <tt>Relation</tt> objects, and you can use the Relation syntax
# to customize them. For example, to add a condition:
#
# class Blog < ActiveRecord::Base
# has_many :published_posts, -> { where(published: true) }, class_name: 'Post'
# end
#
# Inside the <tt>-> { ... }</tt> block you can use all of the usual Relation methods.
#
# === Accessing the owner object
#
# Sometimes it is useful to have access to the owner object when building the query. The owner
# is passed as a parameter to the block. For example, the following association would find all
# events that occur on the user's birthday:
#
# class User < ActiveRecord::Base
# has_many :birthday_events, ->(user) { where(starts_on: user.birthday) }, class_name: 'Event'
# end
#
# Note: Joining, eager loading, and preloading of these associations is not possible.
# These operations happen before instance creation and the scope will be called with a +nil+ argument.
#
# == Association callbacks
#
# Similar to the normal callbacks that hook into the life cycle of an Active Record object,
# you can also define callbacks that get triggered when you add an object to or remove an
# object from an association collection.
#
# class Firm < ActiveRecord::Base
# has_many :clients,
# dependent: :destroy,
# after_add: :congratulate_client,
# after_remove: :log_after_remove
#
# def congratulate_client(record)
# # ...
# end
#
# def log_after_remove(record)
# # ...
# end
#
# It's possible to stack callbacks by passing them as an array. Example:
#
# class Firm < ActiveRecord::Base
# has_many :clients,
# dependent: :destroy,
# after_add: [:congratulate_client, -> (firm, record) { firm.log << "after_adding#{record.id}" }],
# after_remove: :log_after_remove
# end
#
# Possible callbacks are: +before_add+, +after_add+, +before_remove+, and +after_remove+.
#
# If any of the +before_add+ callbacks throw an exception, the object will not be
# added to the collection.
#
# Similarly, if any of the +before_remove+ callbacks throw an exception, the object
# will not be removed from the collection.
#
# Note: To trigger remove callbacks, you must use +destroy+ / +destroy_all+ methods. For example:
#
# * <tt>firm.clients.destroy(client)</tt>
# * <tt>firm.clients.destroy(*clients)</tt>
# * <tt>firm.clients.destroy_all</tt>
#
# +delete+ / +delete_all+ methods like the following do *not* trigger remove callbacks:
#
# * <tt>firm.clients.delete(client)</tt>
# * <tt>firm.clients.delete(*clients)</tt>
# * <tt>firm.clients.delete_all</tt>
#
# == Association extensions
#
# The proxy objects that control the access to associations can be extended through anonymous
# modules. This is especially beneficial for adding new finders, creators, and other
# factory-type methods that are only used as part of this association.
#
# class Account < ActiveRecord::Base
# has_many :people do
# def find_or_create_by_name(name)
# first_name, last_name = name.split(" ", 2)
# find_or_create_by(first_name: first_name, last_name: last_name)
# end
# end
# end
#
# person = Account.first.people.find_or_create_by_name("David Heinemeier Hansson")
# person.first_name # => "David"
# person.last_name # => "Heinemeier Hansson"
#
# If you need to share the same extensions between many associations, you can use a named
# extension module.
#
# module FindOrCreateByNameExtension
# def find_or_create_by_name(name)
# first_name, last_name = name.split(" ", 2)
# find_or_create_by(first_name: first_name, last_name: last_name)
# end
# end
#
# class Account < ActiveRecord::Base
# has_many :people, -> { extending FindOrCreateByNameExtension }
# end
#
# class Company < ActiveRecord::Base
# has_many :people, -> { extending FindOrCreateByNameExtension }
# end
#
# Some extensions can only be made to work with knowledge of the association's internals.
# Extensions can access relevant state using the following methods (where +items+ is the
# name of the association):
#
# * <tt>record.association(:items).owner</tt> - Returns the object the association is part of.
# * <tt>record.association(:items).reflection</tt> - Returns the reflection object that describes the association.
# * <tt>record.association(:items).target</tt> - Returns the associated object for #belongs_to and #has_one, or
# the collection of associated objects for #has_many and #has_and_belongs_to_many.
#
# However, inside the actual extension code, you will not have access to the <tt>record</tt> as
# above. In this case, you can access <tt>proxy_association</tt>. For example,
# <tt>record.association(:items)</tt> and <tt>record.items.proxy_association</tt> will return
# the same object, allowing you to make calls like <tt>proxy_association.owner</tt> inside
# association extensions.
#
# == Association Join Models
#
# Has Many associations can be configured with the <tt>:through</tt> option to use an
# explicit join model to retrieve the data. This operates similarly to a
# #has_and_belongs_to_many association. The advantage is that you're able to add validations,
# callbacks, and extra attributes on the join model. Consider the following schema:
#
# class Author < ActiveRecord::Base
# has_many :authorships
# has_many :books, through: :authorships
# end
#
# class Authorship < ActiveRecord::Base
# belongs_to :author
# belongs_to :book
# end
#
# @author = Author.first
# @author.authorships.collect { |a| a.book } # selects all books that the author's authorships belong to
# @author.books # selects all books by using the Authorship join model
#
# You can also go through a #has_many association on the join model:
#
# class Firm < ActiveRecord::Base
# has_many :clients
# has_many :invoices, through: :clients
# end
#
# class Client < ActiveRecord::Base
# belongs_to :firm
# has_many :invoices
# end
#
# class Invoice < ActiveRecord::Base
# belongs_to :client
# end
#
# @firm = Firm.first
# @firm.clients.flat_map { |c| c.invoices } # select all invoices for all clients of the firm
# @firm.invoices # selects all invoices by going through the Client join model
#
# Similarly you can go through a #has_one association on the join model:
#
# class Group < ActiveRecord::Base
# has_many :users
# has_many :avatars, through: :users
# end
#
# class User < ActiveRecord::Base
# belongs_to :group
# has_one :avatar
# end
#
# class Avatar < ActiveRecord::Base
# belongs_to :user
# end
#
# @group = Group.first
# @group.users.collect { |u| u.avatar }.compact # select all avatars for all users in the group
# @group.avatars # selects all avatars by going through the User join model.
#
# An important caveat with going through #has_one or #has_many associations on the
# join model is that these associations are *read-only*. For example, the following
# would not work following the previous example:
#
# @group.avatars << Avatar.new # this would work if User belonged_to Avatar rather than the other way around
# @group.avatars.delete(@group.avatars.last) # so would this
#
# == Setting Inverses
#
# If you are using a #belongs_to on the join model, it is a good idea to set the
# <tt>:inverse_of</tt> option on the #belongs_to, which will mean that the following example
# works correctly (where <tt>tags</tt> is a #has_many <tt>:through</tt> association):
#
# @post = Post.first
# @tag = @post.tags.build name: "ruby"
# @tag.save
#
# The last line ought to save the through record (a <tt>Tagging</tt>). This will only work if the
# <tt>:inverse_of</tt> is set:
#
# class Tagging < ActiveRecord::Base
# belongs_to :post
# belongs_to :tag, inverse_of: :taggings
# end
#
# If you do not set the <tt>:inverse_of</tt> record, the association will
# do its best to match itself up with the correct inverse. Automatic
# inverse detection only works on #has_many, #has_one, and
# #belongs_to associations.
#
# <tt>:foreign_key</tt> and <tt>:through</tt> options on the associations
# will also prevent the association's inverse from being found automatically,
# as will a custom scopes in some cases. See further details in the
# {Active Record Associations guide}[https://guides.rubyonrails.org/association_basics.html#bi-directional-associations].
#
# The automatic guessing of the inverse association uses a heuristic based
# on the name of the class, so it may not work for all associations,
# especially the ones with non-standard names.
#
# You can turn off the automatic detection of inverse associations by setting
# the <tt>:inverse_of</tt> option to <tt>false</tt> like so:
#
# class Tagging < ActiveRecord::Base
# belongs_to :tag, inverse_of: false
# end
#
# == Nested \Associations
#
# You can actually specify *any* association with the <tt>:through</tt> option, including an
# association which has a <tt>:through</tt> option itself. For example:
#
# class Author < ActiveRecord::Base
# has_many :posts
# has_many :comments, through: :posts
# has_many :commenters, through: :comments
# end
#
# class Post < ActiveRecord::Base
# has_many :comments
# end
#
# class Comment < ActiveRecord::Base
# belongs_to :commenter
# end
#
# @author = Author.first
# @author.commenters # => People who commented on posts written by the author
#
# An equivalent way of setting up this association this would be:
#
# class Author < ActiveRecord::Base
# has_many :posts
# has_many :commenters, through: :posts
# end
#
# class Post < ActiveRecord::Base
# has_many :comments
# has_many :commenters, through: :comments
# end
#
# class Comment < ActiveRecord::Base
# belongs_to :commenter
# end
#
# When using a nested association, you will not be able to modify the association because there
# is not enough information to know what modification to make. For example, if you tried to
# add a <tt>Commenter</tt> in the example above, there would be no way to tell how to set up the
# intermediate <tt>Post</tt> and <tt>Comment</tt> objects.
#
# == Polymorphic \Associations
#
# Polymorphic associations on models are not restricted on what types of models they
# can be associated with. Rather, they specify an interface that a #has_many association
# must adhere to.
#
# class Asset < ActiveRecord::Base
# belongs_to :attachable, polymorphic: true
# end
#
# class Post < ActiveRecord::Base
# has_many :assets, as: :attachable # The :as option specifies the polymorphic interface to use.
# end
#
# @asset.attachable = @post
#
# This works by using a type column in addition to a foreign key to specify the associated
# record. In the Asset example, you'd need an +attachable_id+ integer column and an
# +attachable_type+ string column.
#
# Using polymorphic associations in combination with single table inheritance (STI) is
# a little tricky. In order for the associations to work as expected, ensure that you
# store the base model for the STI models in the type column of the polymorphic
# association. To continue with the asset example above, suppose there are guest posts
# and member posts that use the posts table for STI. In this case, there must be a +type+
# column in the posts table.
#
# Note: The <tt>attachable_type=</tt> method is being called when assigning an +attachable+.
# The +class_name+ of the +attachable+ is passed as a String.
#
# class Asset < ActiveRecord::Base
# belongs_to :attachable, polymorphic: true
#
# def attachable_type=(class_name)
# super(class_name.constantize.base_class.to_s)
# end
# end
#
# class Post < ActiveRecord::Base
# # because we store "Post" in attachable_type now dependent: :destroy will work
# has_many :assets, as: :attachable, dependent: :destroy
# end
#
# class GuestPost < Post
# end
#
# class MemberPost < Post
# end
#
# == Caching
#
# All of the methods are built on a simple caching principle that will keep the result
# of the last query around unless specifically instructed not to. The cache is even
# shared across methods to make it even cheaper to use the macro-added methods without
# worrying too much about performance at the first go.
#
# project.milestones # fetches milestones from the database
# project.milestones.size # uses the milestone cache
# project.milestones.empty? # uses the milestone cache
# project.milestones.reload.size # fetches milestones from the database
# project.milestones # uses the milestone cache
#
# == Eager loading of associations
#
# Eager loading is a way to find objects of a certain class and a number of named associations.
# It is one of the easiest ways to prevent the dreaded N+1 problem in which fetching 100
# posts that each need to display their author triggers 101 database queries. Through the
# use of eager loading, the number of queries will be reduced from 101 to 2.
#
# class Post < ActiveRecord::Base
# belongs_to :author
# has_many :comments
# end
#
# Consider the following loop using the class above:
#
# Post.all.each do |post|
# puts "Post: " + post.title
# puts "Written by: " + post.author.name
# puts "Last comment on: " + post.comments.first.created_on
# end
#
# To iterate over these one hundred posts, we'll generate 201 database queries. Let's
# first just optimize it for retrieving the author:
#
# Post.includes(:author).each do |post|
#
# This references the name of the #belongs_to association that also used the <tt>:author</tt>
# symbol. After loading the posts, +find+ will collect the +author_id+ from each one and load
# all of the referenced authors with one query. Doing so will cut down the number of queries
# from 201 to 102.
#
# We can improve upon the situation further by referencing both associations in the finder with:
#
# Post.includes(:author, :comments).each do |post|
#
# This will load all comments with a single query. This reduces the total number of queries
# to 3. In general, the number of queries will be 1 plus the number of associations
# named (except if some of the associations are polymorphic #belongs_to - see below).
#
# To include a deep hierarchy of associations, use a hash:
#
# Post.includes(:author, { comments: { author: :gravatar } }).each do |post|
#
# The above code will load all the comments and all of their associated
# authors and gravatars. You can mix and match any combination of symbols,
# arrays, and hashes to retrieve the associations you want to load.
#
# All of this power shouldn't fool you into thinking that you can pull out huge amounts
# of data with no performance penalty just because you've reduced the number of queries.
# The database still needs to send all the data to Active Record and it still needs to
# be processed. So it's no catch-all for performance problems, but it's a great way to
# cut down on the number of queries in a situation as the one described above.
#
# Since only one table is loaded at a time, conditions or orders cannot reference tables
# other than the main one. If this is the case, Active Record falls back to the previously
# used <tt>LEFT OUTER JOIN</tt> based strategy. For example:
#
# Post.includes([:author, :comments]).where(['comments.approved = ?', true])
#
# This will result in a single SQL query with joins along the lines of:
# <tt>LEFT OUTER JOIN comments ON comments.post_id = posts.id</tt> and
# <tt>LEFT OUTER JOIN authors ON authors.id = posts.author_id</tt>. Note that using conditions
# like this can have unintended consequences.
# In the above example, posts with no approved comments are not returned at all because
# the conditions apply to the SQL statement as a whole and not just to the association.
#
# You must disambiguate column references for this fallback to happen, for example
# <tt>order: "author.name DESC"</tt> will work but <tt>order: "name DESC"</tt> will not.
#
# If you want to load all posts (including posts with no approved comments), then write
# your own <tt>LEFT OUTER JOIN</tt> query using <tt>ON</tt>:
#
# Post.joins("LEFT OUTER JOIN comments ON comments.post_id = posts.id AND comments.approved = '1'")
#
# In this case, it is usually more natural to include an association which has conditions defined on it:
#
# class Post < ActiveRecord::Base
# has_many :approved_comments, -> { where(approved: true) }, class_name: 'Comment'
# end
#
# Post.includes(:approved_comments)
#
# This will load posts and eager load the +approved_comments+ association, which contains
# only those comments that have been approved.
#
# If you eager load an association with a specified <tt>:limit</tt> option, it will be ignored,
# returning all the associated objects:
#
# class Picture < ActiveRecord::Base
# has_many :most_recent_comments, -> { order('id DESC').limit(10) }, class_name: 'Comment'
# end
#
# Picture.includes(:most_recent_comments).first.most_recent_comments # => returns all associated comments.
#
# Eager loading is supported with polymorphic associations.
#
# class Address < ActiveRecord::Base
# belongs_to :addressable, polymorphic: true
# end
#
# A call that tries to eager load the addressable model
#
# Address.includes(:addressable)
#
# This will execute one query to load the addresses and load the addressables with one
# query per addressable type.
# For example, if all the addressables are either of class Person or Company, then a total
# of 3 queries will be executed. The list of addressable types to load is determined on
# the back of the addresses loaded. This is not supported if Active Record has to fallback
# to the previous implementation of eager loading and will raise ActiveRecord::EagerLoadPolymorphicError.
# The reason is that the parent model's type is a column value so its corresponding table
# name cannot be put in the +FROM+/+JOIN+ clauses of that query.
#
# == Table Aliasing
#
# Active Record uses table aliasing in the case that a table is referenced multiple times
# in a join. If a table is referenced only once, the standard table name is used. The
# second time, the table is aliased as <tt>#{reflection_name}_#{parent_table_name}</tt>.
# Indexes are appended for any more successive uses of the table name.
#
# Post.joins(:comments)
# # => SELECT ... FROM posts INNER JOIN comments ON ...
# Post.joins(:special_comments) # STI
# # => SELECT ... FROM posts INNER JOIN comments ON ... AND comments.type = 'SpecialComment'
# Post.joins(:comments, :special_comments) # special_comments is the reflection name, posts is the parent table name
# # => SELECT ... FROM posts INNER JOIN comments ON ... INNER JOIN comments special_comments_posts
#
# Acts as tree example:
#
# TreeMixin.joins(:children)
# # => SELECT ... FROM mixins INNER JOIN mixins childrens_mixins ...
# TreeMixin.joins(children: :parent)
# # => SELECT ... FROM mixins INNER JOIN mixins childrens_mixins ...
# INNER JOIN parents_mixins ...
# TreeMixin.joins(children: {parent: :children})
# # => SELECT ... FROM mixins INNER JOIN mixins childrens_mixins ...
# INNER JOIN parents_mixins ...
# INNER JOIN mixins childrens_mixins_2
#
# Has and Belongs to Many join tables use the same idea, but add a <tt>_join</tt> suffix:
#
# Post.joins(:categories)
# # => SELECT ... FROM posts INNER JOIN categories_posts ... INNER JOIN categories ...
# Post.joins(categories: :posts)
# # => SELECT ... FROM posts INNER JOIN categories_posts ... INNER JOIN categories ...
# INNER JOIN categories_posts posts_categories_join INNER JOIN posts posts_categories
# Post.joins(categories: {posts: :categories})
# # => SELECT ... FROM posts INNER JOIN categories_posts ... INNER JOIN categories ...
# INNER JOIN categories_posts posts_categories_join INNER JOIN posts posts_categories
# INNER JOIN categories_posts categories_posts_join INNER JOIN categories categories_posts_2
#
# If you wish to specify your own custom joins using ActiveRecord::QueryMethods#joins method, those table
# names will take precedence over the eager associations:
#
# Post.joins(:comments).joins("inner join comments ...")
# # => SELECT ... FROM posts INNER JOIN comments_posts ON ... INNER JOIN comments ...
# Post.joins(:comments, :special_comments).joins("inner join comments ...")
# # => SELECT ... FROM posts INNER JOIN comments comments_posts ON ...
# INNER JOIN comments special_comments_posts ...
# INNER JOIN comments ...
#
# Table aliases are automatically truncated according to the maximum length of table identifiers
# according to the specific database.
#
# == Modules
#
# By default, associations will look for objects within the current module scope. Consider:
#
# module MyApplication
# module Business
# class Firm < ActiveRecord::Base
# has_many :clients
# end
#
# class Client < ActiveRecord::Base; end
# end
# end
#
# When <tt>Firm#clients</tt> is called, it will in turn call
# <tt>MyApplication::Business::Client.find_all_by_firm_id(firm.id)</tt>.
# If you want to associate with a class in another module scope, this can be done by
# specifying the complete class name.
#
# module MyApplication
# module Business
# class Firm < ActiveRecord::Base; end
# end
#
# module Billing
# class Account < ActiveRecord::Base
# belongs_to :firm, class_name: "MyApplication::Business::Firm"
# end
# end
# end
#
# == Bi-directional associations
#
# When you specify an association, there is usually an association on the associated model
# that specifies the same relationship in reverse. For example, with the following models:
#
# class Dungeon < ActiveRecord::Base
# has_many :traps
# has_one :evil_wizard
# end
#
# class Trap < ActiveRecord::Base
# belongs_to :dungeon
# end
#
# class EvilWizard < ActiveRecord::Base
# belongs_to :dungeon
# end
#
# The +traps+ association on +Dungeon+ and the +dungeon+ association on +Trap+ are
# the inverse of each other, and the inverse of the +dungeon+ association on +EvilWizard+
# is the +evil_wizard+ association on +Dungeon+ (and vice-versa). By default,
# Active Record can guess the inverse of the association based on the name
# of the class. The result is the following:
#
# d = Dungeon.first
# t = d.traps.first
# d.object_id == t.dungeon.object_id # => true
#
# The +Dungeon+ instances +d+ and <tt>t.dungeon</tt> in the above example refer to
# the same in-memory instance since the association matches the name of the class.
# The result would be the same if we added +:inverse_of+ to our model definitions:
#
# class Dungeon < ActiveRecord::Base
# has_many :traps, inverse_of: :dungeon
# has_one :evil_wizard, inverse_of: :dungeon
# end
#
# class Trap < ActiveRecord::Base
# belongs_to :dungeon, inverse_of: :traps
# end
#
# class EvilWizard < ActiveRecord::Base
# belongs_to :dungeon, inverse_of: :evil_wizard
# end
#
# For more information, see the documentation for the +:inverse_of+ option and the
# {Active Record Associations guide}[https://guides.rubyonrails.org/association_basics.html#bi-directional-associations].
#
# == Deleting from associations
#
# === Dependent associations
#
# #has_many, #has_one, and #belongs_to associations support the <tt>:dependent</tt> option.
# This allows you to specify that associated records should be deleted when the owner is
# deleted.
#
# For example:
#
# class Author
# has_many :posts, dependent: :destroy
# end
# Author.find(1).destroy # => Will destroy all of the author's posts, too
#
# The <tt>:dependent</tt> option can have different values which specify how the deletion
# is done. For more information, see the documentation for this option on the different
# specific association types. When no option is given, the behavior is to do nothing
# with the associated records when destroying a record.
#
# Note that <tt>:dependent</tt> is implemented using Rails' callback
# system, which works by processing callbacks in order. Therefore, other
# callbacks declared either before or after the <tt>:dependent</tt> option
# can affect what it does.
#
# Note that <tt>:dependent</tt> option is ignored for #has_one <tt>:through</tt> associations.
#
# === Delete or destroy?
#
# #has_many and #has_and_belongs_to_many associations have the methods <tt>destroy</tt>,
# <tt>delete</tt>, <tt>destroy_all</tt> and <tt>delete_all</tt>.
#
# For #has_and_belongs_to_many, <tt>delete</tt> and <tt>destroy</tt> are the same: they
# cause the records in the join table to be removed.
#
# For #has_many, <tt>destroy</tt> and <tt>destroy_all</tt> will always call the <tt>destroy</tt> method of the
# record(s) being removed so that callbacks are run. However <tt>delete</tt> and <tt>delete_all</tt> will either
# do the deletion according to the strategy specified by the <tt>:dependent</tt> option, or
# if no <tt>:dependent</tt> option is given, then it will follow the default strategy.
# The default strategy is to do nothing (leave the foreign keys with the parent ids set), except for
# #has_many <tt>:through</tt>, where the default strategy is <tt>delete_all</tt> (delete
# the join records, without running their callbacks).
#
# There is also a <tt>clear</tt> method which is the same as <tt>delete_all</tt>, except that
# it returns the association rather than the records which have been deleted.
#
# === What gets deleted?
#
# There is a potential pitfall here: #has_and_belongs_to_many and #has_many <tt>:through</tt>
# associations have records in join tables, as well as the associated records. So when we
# call one of these deletion methods, what exactly should be deleted?
#
# The answer is that it is assumed that deletion on an association is about removing the
# <i>link</i> between the owner and the associated object(s), rather than necessarily the
# associated objects themselves. So with #has_and_belongs_to_many and #has_many
# <tt>:through</tt>, the join records will be deleted, but the associated records won't.
#
# This makes sense if you think about it: if you were to call <tt>post.tags.delete(Tag.find_by(name: 'food'))</tt>
# you would want the 'food' tag to be unlinked from the post, rather than for the tag itself
# to be removed from the database.
#
# However, there are examples where this strategy doesn't make sense. For example, suppose
# a person has many projects, and each project has many tasks. If we deleted one of a person's
# tasks, we would probably not want the project to be deleted. In this scenario, the delete method
# won't actually work: it can only be used if the association on the join model is a
# #belongs_to. In other situations you are expected to perform operations directly on
# either the associated records or the <tt>:through</tt> association.
#
# With a regular #has_many there is no distinction between the "associated records"
# and the "link", so there is only one choice for what gets deleted.
#
# With #has_and_belongs_to_many and #has_many <tt>:through</tt>, if you want to delete the
# associated records themselves, you can always do something along the lines of
# <tt>person.tasks.each(&:destroy)</tt>.
#
# == Type safety with ActiveRecord::AssociationTypeMismatch
#
# If you attempt to assign an object to an association that doesn't match the inferred
# or specified <tt>:class_name</tt>, you'll get an ActiveRecord::AssociationTypeMismatch.
#
# == Options
#
# All of the association macros can be specialized through options. This makes cases
# more complex than the simple and guessable ones possible.
module ClassMethods
# Specifies a one-to-many association. The following methods for retrieval and query of
# collections of associated objects will be added:
#
# +collection+ is a placeholder for the symbol passed as the +name+ argument, so
# <tt>has_many :clients</tt> would add among others <tt>clients.empty?</tt>.
#
# [collection]
# Returns a Relation of all the associated objects.
# An empty Relation is returned if none are found.
# [collection<<(object, ...)]
# Adds one or more objects to the collection by setting their foreign keys to the collection's primary key.
# Note that this operation instantly fires update SQL without waiting for the save or update call on the
# parent object, unless the parent object is a new record.
# This will also run validations and callbacks of associated object(s).
# [collection.delete(object, ...)]
# Removes one or more objects from the collection by setting their foreign keys to +NULL+.
# Objects will be in addition destroyed if they're associated with <tt>dependent: :destroy</tt>,
# and deleted if they're associated with <tt>dependent: :delete_all</tt>.
#
# If the <tt>:through</tt> option is used, then the join records are deleted (rather than
# nullified) by default, but you can specify <tt>dependent: :destroy</tt> or
# <tt>dependent: :nullify</tt> to override this.
# [collection.destroy(object, ...)]
# Removes one or more objects from the collection by running <tt>destroy</tt> on
# each record, regardless of any dependent option, ensuring callbacks are run.
#
# If the <tt>:through</tt> option is used, then the join records are destroyed
# instead, not the objects themselves.
# [collection=objects]
# Replaces the collections content by deleting and adding objects as appropriate. If the <tt>:through</tt>
# option is true callbacks in the join models are triggered except destroy callbacks, since deletion is
# direct by default. You can specify <tt>dependent: :destroy</tt> or
# <tt>dependent: :nullify</tt> to override this.
# [collection_singular_ids]
# Returns an array of the associated objects' ids
# [collection_singular_ids=ids]
# Replace the collection with the objects identified by the primary keys in +ids+. This
# method loads the models and calls <tt>collection=</tt>. See above.
# [collection.clear]
# Removes every object from the collection. This destroys the associated objects if they
# are associated with <tt>dependent: :destroy</tt>, deletes them directly from the
# database if <tt>dependent: :delete_all</tt>, otherwise sets their foreign keys to +NULL+.
# If the <tt>:through</tt> option is true no destroy callbacks are invoked on the join models.
# Join models are directly deleted.
# [collection.empty?]
# Returns +true+ if there are no associated objects.
# [collection.size]
# Returns the number of associated objects.
# [collection.find(...)]
# Finds an associated object according to the same rules as ActiveRecord::FinderMethods#find.
# [collection.exists?(...)]
# Checks whether an associated object with the given conditions exists.
# Uses the same rules as ActiveRecord::FinderMethods#exists?.
# [collection.build(attributes = {}, ...)]
# Returns one or more new objects of the collection type that have been instantiated
# with +attributes+ and linked to this object through a foreign key, but have not yet
# been saved.
# [collection.create(attributes = {})]
# Returns a new object of the collection type that has been instantiated
# with +attributes+, linked to this object through a foreign key, and that has already
# been saved (if it passed the validation). *Note*: This only works if the base model
# already exists in the DB, not if it is a new (unsaved) record!
# [collection.create!(attributes = {})]
# Does the same as <tt>collection.create</tt>, but raises ActiveRecord::RecordInvalid
# if the record is invalid.
# [collection.reload]
# Returns a Relation of all of the associated objects, forcing a database read.
# An empty Relation is returned if none are found.
#
# === Example
#
# A <tt>Firm</tt> class declares <tt>has_many :clients</tt>, which will add:
# * <tt>Firm#clients</tt> (similar to <tt>Client.where(firm_id: id)</tt>)
# * <tt>Firm#clients<<</tt>
# * <tt>Firm#clients.delete</tt>
# * <tt>Firm#clients.destroy</tt>
# * <tt>Firm#clients=</tt>
# * <tt>Firm#client_ids</tt>
# * <tt>Firm#client_ids=</tt>
# * <tt>Firm#clients.clear</tt>
# * <tt>Firm#clients.empty?</tt> (similar to <tt>firm.clients.size == 0</tt>)
# * <tt>Firm#clients.size</tt> (similar to <tt>Client.count "firm_id = #{id}"</tt>)
# * <tt>Firm#clients.find</tt> (similar to <tt>Client.where(firm_id: id).find(id)</tt>)
# * <tt>Firm#clients.exists?(name: 'ACME')</tt> (similar to <tt>Client.exists?(name: 'ACME', firm_id: firm.id)</tt>)
# * <tt>Firm#clients.build</tt> (similar to <tt>Client.new(firm_id: id)</tt>)
# * <tt>Firm#clients.create</tt> (similar to <tt>c = Client.new(firm_id: id); c.save; c</tt>)
# * <tt>Firm#clients.create!</tt> (similar to <tt>c = Client.new(firm_id: id); c.save!</tt>)
# * <tt>Firm#clients.reload</tt>
# The declaration can also include an +options+ hash to specialize the behavior of the association.
#
# === Scopes
#
# You can pass a second argument +scope+ as a callable (i.e. proc or
# lambda) to retrieve a specific set of records or customize the generated
# query when you access the associated collection.
#
# Scope examples:
# has_many :comments, -> { where(author_id: 1) }
# has_many :employees, -> { joins(:address) }
# has_many :posts, ->(blog) { where("max_post_length > ?", blog.max_post_length) }
#
# === Extensions
#
# The +extension+ argument allows you to pass a block into a has_many
# association. This is useful for adding new finders, creators, and other
# factory-type methods to be used as part of the association.
#
# Extension examples:
# has_many :employees do
# def find_or_create_by_name(name)
# first_name, last_name = name.split(" ", 2)
# find_or_create_by(first_name: first_name, last_name: last_name)
# end
# end
#
# === Options
# [:class_name]
# Specify the class name of the association. Use it only if that name can't be inferred
# from the association name. So <tt>has_many :products</tt> will by default be linked
# to the +Product+ class, but if the real class name is +SpecialProduct+, you'll have to
# specify it with this option.
# [:foreign_key]
# Specify the foreign key used for the association. By default this is guessed to be the name
# of this class in lower-case and "_id" suffixed. So a Person class that makes a #has_many
# association will use "person_id" as the default <tt>:foreign_key</tt>.
#
# Setting the <tt>:foreign_key</tt> option prevents automatic detection of the association's
# inverse, so it is generally a good idea to set the <tt>:inverse_of</tt> option as well.
# [:foreign_type]
# Specify the column used to store the associated object's type, if this is a polymorphic
# association. By default this is guessed to be the name of the polymorphic association
# specified on "as" option with a "_type" suffix. So a class that defines a
# <tt>has_many :tags, as: :taggable</tt> association will use "taggable_type" as the
# default <tt>:foreign_type</tt>.
# [:primary_key]
# Specify the name of the column to use as the primary key for the association. By default this is +id+.
# [:dependent]
# Controls what happens to the associated objects when
# their owner is destroyed. Note that these are implemented as
# callbacks, and Rails executes callbacks in order. Therefore, other
# similar callbacks may affect the <tt>:dependent</tt> behavior, and the
# <tt>:dependent</tt> behavior may affect other callbacks.
#
# * <tt>nil</tt> do nothing (default).
# * <tt>:destroy</tt> causes all the associated objects to also be destroyed.
# * <tt>:destroy_async</tt> destroys all the associated objects in a background job. <b>WARNING:</b> Do not use
# this option if the association is backed by foreign key constraints in your database. The foreign key
# constraint actions will occur inside the same transaction that deletes its owner.
# * <tt>:delete_all</tt> causes all the associated objects to be deleted directly from the database (so callbacks will not be executed).
# * <tt>:nullify</tt> causes the foreign keys to be set to +NULL+. Polymorphic type will also be nullified
# on polymorphic associations. Callbacks are not executed.
# * <tt>:restrict_with_exception</tt> causes an <tt>ActiveRecord::DeleteRestrictionError</tt> exception to be raised if there are any associated records.
# * <tt>:restrict_with_error</tt> causes an error to be added to the owner if there are any associated objects.
#
# If using with the <tt>:through</tt> option, the association on the join model must be
# a #belongs_to, and the records which get deleted are the join records, rather than
# the associated records.
#
# If using <tt>dependent: :destroy</tt> on a scoped association, only the scoped objects are destroyed.
# For example, if a Post model defines
# <tt>has_many :comments, -> { where published: true }, dependent: :destroy</tt> and <tt>destroy</tt> is
# called on a post, only published comments are destroyed. This means that any unpublished comments in the
# database would still contain a foreign key pointing to the now deleted post.
# [:counter_cache]
# This option can be used to configure a custom named <tt>:counter_cache.</tt> You only need this option,
# when you customized the name of your <tt>:counter_cache</tt> on the #belongs_to association.
# [:as]
# Specifies a polymorphic interface (See #belongs_to).
# [:through]
# Specifies an association through which to perform the query. This can be any other type
# of association, including other <tt>:through</tt> associations. Options for <tt>:class_name</tt>,
# <tt>:primary_key</tt> and <tt>:foreign_key</tt> are ignored, as the association uses the
# source reflection.
#
# If the association on the join model is a #belongs_to, the collection can be modified
# and the records on the <tt>:through</tt> model will be automatically created and removed
# as appropriate. Otherwise, the collection is read-only, so you should manipulate the
# <tt>:through</tt> association directly.
#
# If you are going to modify the association (rather than just read from it), then it is
# a good idea to set the <tt>:inverse_of</tt> option on the source association on the
# join model. This allows associated records to be built which will automatically create
# the appropriate join model records when they are saved. (See the 'Association Join Models'
# and 'Setting Inverses' sections above.)
# [:disable_joins]
# Specifies whether joins should be skipped for an association. If set to true, two or more queries
# will be generated. Note that in some cases, if order or limit is applied, it will be done in-memory
# due to database limitations. This option is only applicable on <tt>has_many :through</tt> associations as
# +has_many+ alone do not perform a join.
# [:source]
# Specifies the source association name used by #has_many <tt>:through</tt> queries.
# Only use it if the name cannot be inferred from the association.
# <tt>has_many :subscribers, through: :subscriptions</tt> will look for either <tt>:subscribers</tt> or
# <tt>:subscriber</tt> on Subscription, unless a <tt>:source</tt> is given.
# [:source_type]
# Specifies type of the source association used by #has_many <tt>:through</tt> queries where the source
# association is a polymorphic #belongs_to.
# [:validate]
# When set to +true+, validates new objects added to association when saving the parent object. +true+ by default.
# If you want to ensure associated objects are revalidated on every update, use +validates_associated+.
# [:autosave]
# If true, always save the associated objects or destroy them if marked for destruction,
# when saving the parent object. If false, never save or destroy the associated objects.
# By default, only save associated objects that are new records. This option is implemented as a
# +before_save+ callback. Because callbacks are run in the order they are defined, associated objects
# may need to be explicitly saved in any user-defined +before_save+ callbacks.
#
# Note that NestedAttributes::ClassMethods#accepts_nested_attributes_for sets
# <tt>:autosave</tt> to <tt>true</tt>.
# [:inverse_of]
# Specifies the name of the #belongs_to association on the associated object
# that is the inverse of this #has_many association.
# See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail.
# [:extend]
# Specifies a module or array of modules that will be extended into the association object returned.
# Useful for defining methods on associations, especially when they should be shared between multiple
# association objects.
# [:strict_loading]
# When set to +true+, enforces strict loading every time the associated record is loaded through this
# association.
# [:ensuring_owner_was]
# Specifies an instance method to be called on the owner. The method must return true in order for the
# associated records to be deleted in a background job.
#
# Option examples:
# has_many :comments, -> { order("posted_on") }
# has_many :comments, -> { includes(:author) }
# has_many :people, -> { where(deleted: false).order("name") }, class_name: "Person"
# has_many :tracks, -> { order("position") }, dependent: :destroy
# has_many :comments, dependent: :nullify
# has_many :tags, as: :taggable
# has_many :reports, -> { readonly }
# has_many :subscribers, through: :subscriptions, source: :user
# has_many :subscribers, through: :subscriptions, disable_joins: true
# has_many :comments, strict_loading: true
def has_many(name, scope = nil, **options, &extension)
reflection = Builder::HasMany.build(self, name, scope, options, &extension)
Reflection.add_reflection self, name, reflection
end
# Specifies a one-to-one association with another class. This method should only be used
# if the other class contains the foreign key. If the current class contains the foreign key,
# then you should use #belongs_to instead. See also ActiveRecord::Associations::ClassMethods's overview
# on when to use #has_one and when to use #belongs_to.
#
# The following methods for retrieval and query of a single associated object will be added:
#
# +association+ is a placeholder for the symbol passed as the +name+ argument, so
# <tt>has_one :manager</tt> would add among others <tt>manager.nil?</tt>.
#
# [association]
# Returns the associated object. +nil+ is returned if none is found.
# [association=(associate)]
# Assigns the associate object, extracts the primary key, sets it as the foreign key,
# and saves the associate object. To avoid database inconsistencies, permanently deletes an existing
# associated object when assigning a new one, even if the new one isn't saved to database.
# [build_association(attributes = {})]
# Returns a new object of the associated type that has been instantiated
# with +attributes+ and linked to this object through a foreign key, but has not
# yet been saved.
# [create_association(attributes = {})]
# Returns a new object of the associated type that has been instantiated
# with +attributes+, linked to this object through a foreign key, and that
# has already been saved (if it passed the validation).
# [create_association!(attributes = {})]
# Does the same as <tt>create_association</tt>, but raises ActiveRecord::RecordInvalid
# if the record is invalid.
# [reload_association]
# Returns the associated object, forcing a database read.
#
# === Example
#
# An Account class declares <tt>has_one :beneficiary</tt>, which will add:
# * <tt>Account#beneficiary</tt> (similar to <tt>Beneficiary.where(account_id: id).first</tt>)
# * <tt>Account#beneficiary=(beneficiary)</tt> (similar to <tt>beneficiary.account_id = account.id; beneficiary.save</tt>)
# * <tt>Account#build_beneficiary</tt> (similar to <tt>Beneficiary.new(account_id: id)</tt>)
# * <tt>Account#create_beneficiary</tt> (similar to <tt>b = Beneficiary.new(account_id: id); b.save; b</tt>)
# * <tt>Account#create_beneficiary!</tt> (similar to <tt>b = Beneficiary.new(account_id: id); b.save!; b</tt>)
# * <tt>Account#reload_beneficiary</tt>
#
# === Scopes
#
# You can pass a second argument +scope+ as a callable (i.e. proc or
# lambda) to retrieve a specific record or customize the generated query
# when you access the associated object.
#
# Scope examples:
# has_one :author, -> { where(comment_id: 1) }
# has_one :employer, -> { joins(:company) }
# has_one :latest_post, ->(blog) { where("created_at > ?", blog.enabled_at) }
#
# === Options
#
# The declaration can also include an +options+ hash to specialize the behavior of the association.
#
# Options are:
# [:class_name]
# Specify the class name of the association. Use it only if that name can't be inferred
# from the association name. So <tt>has_one :manager</tt> will by default be linked to the Manager class, but
# if the real class name is Person, you'll have to specify it with this option.
# [:dependent]
# Controls what happens to the associated object when
# its owner is destroyed:
#
# * <tt>nil</tt> do nothing (default).
# * <tt>:destroy</tt> causes the associated object to also be destroyed
# * <tt>:destroy_async</tt> causes the associated object to be destroyed in a background job. <b>WARNING:</b> Do not use
# this option if the association is backed by foreign key constraints in your database. The foreign key
# constraint actions will occur inside the same transaction that deletes its owner.
# * <tt>:delete</tt> causes the associated object to be deleted directly from the database (so callbacks will not execute)
# * <tt>:nullify</tt> causes the foreign key to be set to +NULL+. Polymorphic type column is also nullified
# on polymorphic associations. Callbacks are not executed.
# * <tt>:restrict_with_exception</tt> causes an <tt>ActiveRecord::DeleteRestrictionError</tt> exception to be raised if there is an associated record
# * <tt>:restrict_with_error</tt> causes an error to be added to the owner if there is an associated object
#
# Note that <tt>:dependent</tt> option is ignored when using <tt>:through</tt> option.
# [:foreign_key]
# Specify the foreign key used for the association. By default this is guessed to be the name
# of this class in lower-case and "_id" suffixed. So a Person class that makes a #has_one association
# will use "person_id" as the default <tt>:foreign_key</tt>.
#
# Setting the <tt>:foreign_key</tt> option prevents automatic detection of the association's
# inverse, so it is generally a good idea to set the <tt>:inverse_of</tt> option as well.
# [:foreign_type]
# Specify the column used to store the associated object's type, if this is a polymorphic
# association. By default this is guessed to be the name of the polymorphic association
# specified on "as" option with a "_type" suffix. So a class that defines a
# <tt>has_one :tag, as: :taggable</tt> association will use "taggable_type" as the
# default <tt>:foreign_type</tt>.
# [:primary_key]
# Specify the method that returns the primary key used for the association. By default this is +id+.
# [:as]
# Specifies a polymorphic interface (See #belongs_to).
# [:through]
# Specifies a Join Model through which to perform the query. Options for <tt>:class_name</tt>,
# <tt>:primary_key</tt>, and <tt>:foreign_key</tt> are ignored, as the association uses the
# source reflection. You can only use a <tt>:through</tt> query through a #has_one
# or #belongs_to association on the join model.
#
# If the association on the join model is a #belongs_to, the collection can be modified
# and the records on the <tt>:through</tt> model will be automatically created and removed
# as appropriate. Otherwise, the collection is read-only, so you should manipulate the
# <tt>:through</tt> association directly.
#
# If you are going to modify the association (rather than just read from it), then it is
# a good idea to set the <tt>:inverse_of</tt> option on the source association on the
# join model. This allows associated records to be built which will automatically create
# the appropriate join model records when they are saved. (See the 'Association Join Models'
# and 'Setting Inverses' sections above.)
# [:disable_joins]
# Specifies whether joins should be skipped for an association. If set to true, two or more queries
# will be generated. Note that in some cases, if order or limit is applied, it will be done in-memory
# due to database limitations. This option is only applicable on <tt>has_one :through</tt> associations as
# +has_one+ alone does not perform a join.
# [:source]
# Specifies the source association name used by #has_one <tt>:through</tt> queries.
# Only use it if the name cannot be inferred from the association.
# <tt>has_one :favorite, through: :favorites</tt> will look for a
# <tt>:favorite</tt> on Favorite, unless a <tt>:source</tt> is given.
# [:source_type]
# Specifies type of the source association used by #has_one <tt>:through</tt> queries where the source
# association is a polymorphic #belongs_to.
# [:validate]
# When set to +true+, validates new objects added to association when saving the parent object. +false+ by default.
# If you want to ensure associated objects are revalidated on every update, use +validates_associated+.
# [:autosave]
# If true, always save the associated object or destroy it if marked for destruction,
# when saving the parent object. If false, never save or destroy the associated object.
# By default, only save the associated object if it's a new record.
#
# Note that NestedAttributes::ClassMethods#accepts_nested_attributes_for sets
# <tt>:autosave</tt> to <tt>true</tt>.
# [:inverse_of]
# Specifies the name of the #belongs_to association on the associated object
# that is the inverse of this #has_one association.
# See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail.
# [:required]
# When set to +true+, the association will also have its presence validated.
# This will validate the association itself, not the id. You can use
# +:inverse_of+ to avoid an extra query during validation.
# [:strict_loading]
# Enforces strict loading every time the associated record is loaded through this association.
# [:ensuring_owner_was]
# Specifies an instance method to be called on the owner. The method must return true in order for the
# associated records to be deleted in a background job.
#
# Option examples:
# has_one :credit_card, dependent: :destroy # destroys the associated credit card
# has_one :credit_card, dependent: :nullify # updates the associated records foreign
# # key value to NULL rather than destroying it
# has_one :last_comment, -> { order('posted_on') }, class_name: "Comment"
# has_one :project_manager, -> { where(role: 'project_manager') }, class_name: "Person"
# has_one :attachment, as: :attachable
# has_one :boss, -> { readonly }
# has_one :club, through: :membership
# has_one :club, through: :membership, disable_joins: true
# has_one :primary_address, -> { where(primary: true) }, through: :addressables, source: :addressable
# has_one :credit_card, required: true
# has_one :credit_card, strict_loading: true
def has_one(name, scope = nil, **options)
reflection = Builder::HasOne.build(self, name, scope, options)
Reflection.add_reflection self, name, reflection
end
# Specifies a one-to-one association with another class. This method should only be used
# if this class contains the foreign key. If the other class contains the foreign key,
# then you should use #has_one instead. See also ActiveRecord::Associations::ClassMethods's overview
# on when to use #has_one and when to use #belongs_to.
#
# Methods will be added for retrieval and query for a single associated object, for which
# this object holds an id:
#
# +association+ is a placeholder for the symbol passed as the +name+ argument, so
# <tt>belongs_to :author</tt> would add among others <tt>author.nil?</tt>.
#
# [association]
# Returns the associated object. +nil+ is returned if none is found.
# [association=(associate)]
# Assigns the associate object, extracts the primary key, and sets it as the foreign key.
# No modification or deletion of existing records takes place.
# [build_association(attributes = {})]
# Returns a new object of the associated type that has been instantiated
# with +attributes+ and linked to this object through a foreign key, but has not yet been saved.
# [create_association(attributes = {})]
# Returns a new object of the associated type that has been instantiated
# with +attributes+, linked to this object through a foreign key, and that
# has already been saved (if it passed the validation).
# [create_association!(attributes = {})]
# Does the same as <tt>create_association</tt>, but raises ActiveRecord::RecordInvalid
# if the record is invalid.
# [reload_association]
# Returns the associated object, forcing a database read.
# [association_changed?]
# Returns true if a new associate object has been assigned and the next save will update the foreign key.
# [association_previously_changed?]
# Returns true if the previous save updated the association to reference a new associate object.
#
# === Example
#
# A Post class declares <tt>belongs_to :author</tt>, which will add:
# * <tt>Post#author</tt> (similar to <tt>Author.find(author_id)</tt>)
# * <tt>Post#author=(author)</tt> (similar to <tt>post.author_id = author.id</tt>)
# * <tt>Post#build_author</tt> (similar to <tt>post.author = Author.new</tt>)
# * <tt>Post#create_author</tt> (similar to <tt>post.author = Author.new; post.author.save; post.author</tt>)
# * <tt>Post#create_author!</tt> (similar to <tt>post.author = Author.new; post.author.save!; post.author</tt>)
# * <tt>Post#reload_author</tt>
# * <tt>Post#author_changed?</tt>
# * <tt>Post#author_previously_changed?</tt>
# The declaration can also include an +options+ hash to specialize the behavior of the association.
#
# === Scopes
#
# You can pass a second argument +scope+ as a callable (i.e. proc or
# lambda) to retrieve a specific record or customize the generated query
# when you access the associated object.
#
# Scope examples:
# belongs_to :firm, -> { where(id: 2) }
# belongs_to :user, -> { joins(:friends) }
# belongs_to :level, ->(game) { where("game_level > ?", game.current_level) }
#
# === Options
#
# [:class_name]
# Specify the class name of the association. Use it only if that name can't be inferred
# from the association name. So <tt>belongs_to :author</tt> will by default be linked to the Author class, but
# if the real class name is Person, you'll have to specify it with this option.
# [:foreign_key]
# Specify the foreign key used for the association. By default this is guessed to be the name
# of the association with an "_id" suffix. So a class that defines a <tt>belongs_to :person</tt>
# association will use "person_id" as the default <tt>:foreign_key</tt>. Similarly,
# <tt>belongs_to :favorite_person, class_name: "Person"</tt> will use a foreign key
# of "favorite_person_id".
#
# Setting the <tt>:foreign_key</tt> option prevents automatic detection of the association's
# inverse, so it is generally a good idea to set the <tt>:inverse_of</tt> option as well.
# [:foreign_type]
# Specify the column used to store the associated object's type, if this is a polymorphic
# association. By default this is guessed to be the name of the association with a "_type"
# suffix. So a class that defines a <tt>belongs_to :taggable, polymorphic: true</tt>
# association will use "taggable_type" as the default <tt>:foreign_type</tt>.
# [:primary_key]
# Specify the method that returns the primary key of associated object used for the association.
# By default this is +id+.
# [:dependent]
# If set to <tt>:destroy</tt>, the associated object is destroyed when this object is. If set to
# <tt>:delete</tt>, the associated object is deleted *without* calling its destroy method. If set to
# <tt>:destroy_async</tt>, the associated object is scheduled to be destroyed in a background job.
# This option should not be specified when #belongs_to is used in conjunction with
# a #has_many relationship on another class because of the potential to leave
# orphaned records behind.
# [:counter_cache]
# Caches the number of belonging objects on the associate class through the use of CounterCache::ClassMethods#increment_counter
# and CounterCache::ClassMethods#decrement_counter. The counter cache is incremented when an object of this
# class is created and decremented when it's destroyed. This requires that a column
# named <tt>#{table_name}_count</tt> (such as +comments_count+ for a belonging Comment class)
# is used on the associate class (such as a Post class) - that is the migration for
# <tt>#{table_name}_count</tt> is created on the associate class (such that <tt>Post.comments_count</tt> will
# return the count cached, see note below). You can also specify a custom counter
# cache column by providing a column name instead of a +true+/+false+ value to this
# option (e.g., <tt>counter_cache: :my_custom_counter</tt>.)
# Note: Specifying a counter cache will add it to that model's list of readonly attributes
# using +attr_readonly+.
# [:polymorphic]
# Specify this association is a polymorphic association by passing +true+.
# Note: If you've enabled the counter cache, then you may want to add the counter cache attribute
# to the +attr_readonly+ list in the associated classes (e.g. <tt>class Post; attr_readonly :comments_count; end</tt>).
# [:validate]
# When set to +true+, validates new objects added to association when saving the parent object. +false+ by default.
# If you want to ensure associated objects are revalidated on every update, use +validates_associated+.
# [:autosave]
# If true, always save the associated object or destroy it if marked for destruction, when
# saving the parent object.
# If false, never save or destroy the associated object.
# By default, only save the associated object if it's a new record.
#
# Note that NestedAttributes::ClassMethods#accepts_nested_attributes_for
# sets <tt>:autosave</tt> to <tt>true</tt>.
# [:touch]
# If true, the associated object will be touched (the updated_at/on attributes set to current time)
# when this record is either saved or destroyed. If you specify a symbol, that attribute
# will be updated with the current time in addition to the updated_at/on attribute.
# Please note that with touching no validation is performed and only the +after_touch+,
# +after_commit+ and +after_rollback+ callbacks are executed.
# [:inverse_of]
# Specifies the name of the #has_one or #has_many association on the associated
# object that is the inverse of this #belongs_to association.
# See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail.
# [:optional]
# When set to +true+, the association will not have its presence validated.
# [:required]
# When set to +true+, the association will also have its presence validated.
# This will validate the association itself, not the id. You can use
# +:inverse_of+ to avoid an extra query during validation.
# NOTE: <tt>required</tt> is set to <tt>true</tt> by default and is deprecated. If
# you don't want to have association presence validated, use <tt>optional: true</tt>.
# [:default]
# Provide a callable (i.e. proc or lambda) to specify that the association should
# be initialized with a particular record before validation.
# [:strict_loading]
# Enforces strict loading every time the associated record is loaded through this association.
# [:ensuring_owner_was]
# Specifies an instance method to be called on the owner. The method must return true in order for the
# associated records to be deleted in a background job.
#
# Option examples:
# belongs_to :firm, foreign_key: "client_of"
# belongs_to :person, primary_key: "name", foreign_key: "person_name"
# belongs_to :author, class_name: "Person", foreign_key: "author_id"
# belongs_to :valid_coupon, ->(o) { where "discounts > ?", o.payments_count },
# class_name: "Coupon", foreign_key: "coupon_id"
# belongs_to :attachable, polymorphic: true
# belongs_to :project, -> { readonly }
# belongs_to :post, counter_cache: true
# belongs_to :comment, touch: true
# belongs_to :company, touch: :employees_last_updated_at
# belongs_to :user, optional: true
# belongs_to :account, default: -> { company.account }
# belongs_to :account, strict_loading: true
def belongs_to(name, scope = nil, **options)
reflection = Builder::BelongsTo.build(self, name, scope, options)
Reflection.add_reflection self, name, reflection
end
# Specifies a many-to-many relationship with another class. This associates two classes via an
# intermediate join table. Unless the join table is explicitly specified as an option, it is
# guessed using the lexical order of the class names. So a join between Developer and Project
# will give the default join table name of "developers_projects" because "D" precedes "P" alphabetically.
# Note that this precedence is calculated using the <tt><</tt> operator for String. This
# means that if the strings are of different lengths, and the strings are equal when compared
# up to the shortest length, then the longer string is considered of higher
# lexical precedence than the shorter one. For example, one would expect the tables "paper_boxes" and "papers"
# to generate a join table name of "papers_paper_boxes" because of the length of the name "paper_boxes",
# but it in fact generates a join table name of "paper_boxes_papers". Be aware of this caveat, and use the
# custom <tt>:join_table</tt> option if you need to.
# If your tables share a common prefix, it will only appear once at the beginning. For example,
# the tables "catalog_categories" and "catalog_products" generate a join table name of "catalog_categories_products".
#
# The join table should not have a primary key or a model associated with it. You must manually generate the
# join table with a migration such as this:
#
# class CreateDevelopersProjectsJoinTable < ActiveRecord::Migration[7.1]
# def change
# create_join_table :developers, :projects
# end
# end
#
# It's also a good idea to add indexes to each of those columns to speed up the joins process.
# However, in MySQL it is advised to add a compound index for both of the columns as MySQL only
# uses one index per table during the lookup.
#
# Adds the following methods for retrieval and query:
#
# +collection+ is a placeholder for the symbol passed as the +name+ argument, so
# <tt>has_and_belongs_to_many :categories</tt> would add among others <tt>categories.empty?</tt>.
#
# [collection]
# Returns a Relation of all the associated objects.
# An empty Relation is returned if none are found.
# [collection<<(object, ...)]
# Adds one or more objects to the collection by creating associations in the join table
# (<tt>collection.push</tt> and <tt>collection.concat</tt> are aliases to this method).
# Note that this operation instantly fires update SQL without waiting for the save or update call on the
# parent object, unless the parent object is a new record.
# [collection.delete(object, ...)]
# Removes one or more objects from the collection by removing their associations from the join table.
# This does not destroy the objects.
# [collection.destroy(object, ...)]
# Removes one or more objects from the collection by running destroy on each association in the join table, overriding any dependent option.
# This does not destroy the objects.
# [collection=objects]
# Replaces the collection's content by deleting and adding objects as appropriate.
# [collection_singular_ids]
# Returns an array of the associated objects' ids.
# [collection_singular_ids=ids]
# Replace the collection by the objects identified by the primary keys in +ids+.
# [collection.clear]
# Removes every object from the collection. This does not destroy the objects.
# [collection.empty?]
# Returns +true+ if there are no associated objects.
# [collection.size]
# Returns the number of associated objects.
# [collection.find(id)]
# Finds an associated object responding to the +id+ and that
# meets the condition that it has to be associated with this object.
# Uses the same rules as ActiveRecord::FinderMethods#find.
# [collection.exists?(...)]
# Checks whether an associated object with the given conditions exists.
# Uses the same rules as ActiveRecord::FinderMethods#exists?.
# [collection.build(attributes = {})]
# Returns a new object of the collection type that has been instantiated
# with +attributes+ and linked to this object through the join table, but has not yet been saved.
# [collection.create(attributes = {})]
# Returns a new object of the collection type that has been instantiated
# with +attributes+, linked to this object through the join table, and that has already been
# saved (if it passed the validation).
# [collection.reload]
# Returns a Relation of all of the associated objects, forcing a database read.
# An empty Relation is returned if none are found.
#
# === Example
#
# A Developer class declares <tt>has_and_belongs_to_many :projects</tt>, which will add:
# * <tt>Developer#projects</tt>
# * <tt>Developer#projects<<</tt>
# * <tt>Developer#projects.delete</tt>
# * <tt>Developer#projects.destroy</tt>
# * <tt>Developer#projects=</tt>
# * <tt>Developer#project_ids</tt>
# * <tt>Developer#project_ids=</tt>
# * <tt>Developer#projects.clear</tt>
# * <tt>Developer#projects.empty?</tt>
# * <tt>Developer#projects.size</tt>
# * <tt>Developer#projects.find(id)</tt>
# * <tt>Developer#projects.exists?(...)</tt>
# * <tt>Developer#projects.build</tt> (similar to <tt>Project.new(developer_id: id)</tt>)
# * <tt>Developer#projects.create</tt> (similar to <tt>c = Project.new(developer_id: id); c.save; c</tt>)
# * <tt>Developer#projects.reload</tt>
# The declaration may include an +options+ hash to specialize the behavior of the association.
#
# === Scopes
#
# You can pass a second argument +scope+ as a callable (i.e. proc or
# lambda) to retrieve a specific set of records or customize the generated
# query when you access the associated collection.
#
# Scope examples:
# has_and_belongs_to_many :projects, -> { includes(:milestones, :manager) }
# has_and_belongs_to_many :categories, ->(post) {
# where("default_category = ?", post.default_category)
# }
#
# === Extensions
#
# The +extension+ argument allows you to pass a block into a
# has_and_belongs_to_many association. This is useful for adding new
# finders, creators, and other factory-type methods to be used as part of
# the association.
#
# Extension examples:
# has_and_belongs_to_many :contractors do
# def find_or_create_by_name(name)
# first_name, last_name = name.split(" ", 2)
# find_or_create_by(first_name: first_name, last_name: last_name)
# end
# end
#
# === Options
#
# [:class_name]
# Specify the class name of the association. Use it only if that name can't be inferred
# from the association name. So <tt>has_and_belongs_to_many :projects</tt> will by default be linked to the
# Project class, but if the real class name is SuperProject, you'll have to specify it with this option.
# [:join_table]
# Specify the name of the join table if the default based on lexical order isn't what you want.
# <b>WARNING:</b> If you're overwriting the table name of either class, the +table_name+ method
# MUST be declared underneath any #has_and_belongs_to_many declaration in order to work.
# [:foreign_key]
# Specify the foreign key used for the association. By default this is guessed to be the name
# of this class in lower-case and "_id" suffixed. So a Person class that makes
# a #has_and_belongs_to_many association to Project will use "person_id" as the
# default <tt>:foreign_key</tt>.
#
# Setting the <tt>:foreign_key</tt> option prevents automatic detection of the association's
# inverse, so it is generally a good idea to set the <tt>:inverse_of</tt> option as well.
# [:association_foreign_key]
# Specify the foreign key used for the association on the receiving side of the association.
# By default this is guessed to be the name of the associated class in lower-case and "_id" suffixed.
# So if a Person class makes a #has_and_belongs_to_many association to Project,
# the association will use "project_id" as the default <tt>:association_foreign_key</tt>.
# [:validate]
# When set to +true+, validates new objects added to association when saving the parent object. +true+ by default.
# If you want to ensure associated objects are revalidated on every update, use +validates_associated+.
# [:autosave]
# If true, always save the associated objects or destroy them if marked for destruction, when
# saving the parent object.
# If false, never save or destroy the associated objects.
# By default, only save associated objects that are new records.
#
# Note that NestedAttributes::ClassMethods#accepts_nested_attributes_for sets
# <tt>:autosave</tt> to <tt>true</tt>.
# [:strict_loading]
# Enforces strict loading every time an associated record is loaded through this association.
#
# Option examples:
# has_and_belongs_to_many :projects
# has_and_belongs_to_many :projects, -> { includes(:milestones, :manager) }
# has_and_belongs_to_many :nations, class_name: "Country"
# has_and_belongs_to_many :categories, join_table: "prods_cats"
# has_and_belongs_to_many :categories, -> { readonly }
# has_and_belongs_to_many :categories, strict_loading: true
def has_and_belongs_to_many(name, scope = nil, **options, &extension)
habtm_reflection = ActiveRecord::Reflection::HasAndBelongsToManyReflection.new(name, scope, options, self)
builder = Builder::HasAndBelongsToMany.new name, self, options
join_model = builder.through_model
const_set join_model.name, join_model
private_constant join_model.name
middle_reflection = builder.middle_reflection join_model
Builder::HasMany.define_callbacks self, middle_reflection
Reflection.add_reflection self, middle_reflection.name, middle_reflection
middle_reflection.parent_reflection = habtm_reflection
include Module.new {
class_eval <<-RUBY, __FILE__, __LINE__ + 1
def destroy_associations
association(:#{middle_reflection.name}).delete_all(:delete_all)
association(:#{name}).reset
super
end
RUBY
}
hm_options = {}
hm_options[:through] = middle_reflection.name
hm_options[:source] = join_model.right_reflection.name
[:before_add, :after_add, :before_remove, :after_remove, :autosave, :validate, :join_table, :class_name, :extend, :strict_loading].each do |k|
hm_options[k] = options[k] if options.key? k
end
has_many name, scope, **hm_options, &extension
_reflections[name.to_s].parent_reflection = habtm_reflection
end
end
end
end
# frozen_string_literal: true
require "cases/helper"
require "models/computer"
require "models/developer"
require "models/project"
require "models/company"
require "models/categorization"
require "models/category"
require "models/post"
require "models/author"
require "models/book"
require "models/comment"
require "models/tag"
require "models/tagging"
require "models/person"
require "models/reader"
require "models/ship_part"
require "models/ship"
require "models/liquid"
require "models/molecule"
require "models/electron"
require "models/human"
require "models/interest"
require "models/pirate"
require "models/parrot"
require "models/bird"
require "models/treasure"
require "models/price_estimate"
require "models/invoice"
require "models/discount"
require "models/line_item"
require "models/shipping_line"
require "models/essay"
class AssociationsTest < ActiveRecord::TestCase
fixtures :accounts, :companies, :developers, :projects, :developers_projects,
:computers, :people, :readers, :authors, :author_addresses, :author_favorites,
:comments, :posts
def test_eager_loading_should_not_change_count_of_children
liquid = Liquid.create(name: "salty")
molecule = liquid.molecules.create(name: "molecule_1")
molecule.electrons.create(name: "electron_1")
molecule.electrons.create(name: "electron_2")
liquids = Liquid.includes(molecules: :electrons).references(:molecules).where("molecules.id is not null")
assert_equal 1, liquids[0].molecules.length
end
def test_subselect
author = authors :david
favs = author.author_favorites
fav2 = author.author_favorites.where(author: Author.where(id: author.id)).to_a
assert_equal favs, fav2
end
def test_loading_the_association_target_should_keep_child_records_marked_for_destruction
ship = Ship.create!(name: "The good ship Dollypop")
part = ship.parts.create!(name: "Mast")
part.mark_for_destruction
assert_predicate ship.parts[0], :marked_for_destruction?
end
def test_loading_the_association_target_should_load_most_recent_attributes_for_child_records_marked_for_destruction
ship = Ship.create!(name: "The good ship Dollypop")
part = ship.parts.create!(name: "Mast")
part.mark_for_destruction
ShipPart.find(part.id).update_columns(name: "Deck")
assert_equal "Deck", ship.parts[0].name
end
def test_include_with_order_works
assert_nothing_raised { Account.all.merge!(order: "id", includes: :firm).first }
assert_nothing_raised { Account.all.merge!(order: :id, includes: :firm).first }
end
def test_bad_collection_keys
assert_raise(ArgumentError, "ActiveRecord should have barked on bad collection keys") do
Class.new(ActiveRecord::Base).has_many(:wheels, name: "wheels")
end
end
def test_should_construct_new_finder_sql_after_create
person = Person.new first_name: "clark"
assert_equal [], person.readers.to_a
person.save!
reader = Reader.create! person: person, post: Post.new(title: "foo", body: "bar")
assert person.readers.find(reader.id)
end
def test_force_reload
firm = Firm.new("name" => "A New Firm, Inc")
firm.save
firm.clients.each { } # forcing to load all clients
assert firm.clients.empty?, "New firm shouldn't have client objects"
assert_equal 0, firm.clients.size, "New firm should have 0 clients"
client = Client.new("name" => "TheClient.com", "firm_id" => firm.id)
client.save
assert firm.clients.empty?, "New firm should have cached no client objects"
assert_equal 0, firm.clients.size, "New firm should have cached 0 clients count"
firm.clients.reload
assert_not firm.clients.empty?, "New firm should have reloaded client objects"
assert_equal 1, firm.clients.size, "New firm should have reloaded clients count"
end
def test_using_limitable_reflections_helper
using_limitable_reflections = lambda { |reflections| Tagging.all.send :using_limitable_reflections?, reflections }
belongs_to_reflections = [Tagging.reflect_on_association(:tag), Tagging.reflect_on_association(:super_tag)]
has_many_reflections = [Tag.reflect_on_association(:taggings), Developer.reflect_on_association(:projects)]
mixed_reflections = (belongs_to_reflections + has_many_reflections).uniq
assert using_limitable_reflections.call(belongs_to_reflections), "Belong to associations are limitable"
assert_not using_limitable_reflections.call(has_many_reflections), "All has many style associations are not limitable"
assert_not using_limitable_reflections.call(mixed_reflections), "No collection associations (has many style) should pass"
end
def test_association_with_references
firm = companies(:first_firm)
assert_equal [:foo], firm.association_with_references.references_values
end
end
class AssociationProxyTest < ActiveRecord::TestCase
fixtures :authors, :author_addresses, :posts, :categorizations, :categories, :developers, :projects, :developers_projects
def test_push_does_not_load_target
david = authors(:david)
david.posts << (post = Post.new(title: "New on Edge", body: "More cool stuff!"))
assert_not_predicate david.posts, :loaded?
assert_includes david.posts, post
end
def test_push_has_many_through_does_not_load_target
david = authors(:david)
david.categories << categories(:technology)
assert_not_predicate david.categories, :loaded?
assert_includes david.categories, categories(:technology)
end
def test_push_followed_by_save_does_not_load_target
david = authors(:david)
david.posts << (post = Post.new(title: "New on Edge", body: "More cool stuff!"))
assert_not_predicate david.posts, :loaded?
david.save
assert_not_predicate david.posts, :loaded?
assert_includes david.posts, post
end
def test_push_does_not_lose_additions_to_new_record
josh = Author.new(name: "Josh")
josh.posts << Post.new(title: "New on Edge", body: "More cool stuff!")
assert_predicate josh.posts, :loaded?
assert_equal 1, josh.posts.size
end
def test_append_behaves_like_push
josh = Author.new(name: "Josh")
josh.posts.append Post.new(title: "New on Edge", body: "More cool stuff!")
assert_predicate josh.posts, :loaded?
assert_equal 1, josh.posts.size
end
def test_prepend_is_not_defined
josh = Author.new(name: "Josh")
assert_raises(NoMethodError) { josh.posts.prepend Post.new }
end
def test_save_on_parent_does_not_load_target
david = developers(:david)
assert_not_predicate david.projects, :loaded?
david.update_columns(created_at: Time.now)
assert_not_predicate david.projects, :loaded?
end
def test_load_does_load_target
david = developers(:david)
assert_not_predicate david.projects, :loaded?
david.projects.load
assert_predicate david.projects, :loaded?
end
def test_inspect_does_not_reload_a_not_yet_loaded_target
andreas = Developer.new name: "Andreas", log: "new developer added"
assert_not_predicate andreas.audit_logs, :loaded?
assert_match(/message: "new developer added"/, andreas.audit_logs.inspect)
assert_predicate andreas.audit_logs, :loaded?
end
def test_save_on_parent_saves_children
developer = Developer.create name: "Bryan", salary: 50_000
assert_equal 1, developer.reload.audit_logs.size
end
def test_create_via_association_with_block
post = authors(:david).posts.create(title: "New on Edge") { |p| p.body = "More cool stuff!" }
assert_equal post.title, "New on Edge"
assert_equal post.body, "More cool stuff!"
end
def test_create_with_bang_via_association_with_block
post = authors(:david).posts.create!(title: "New on Edge") { |p| p.body = "More cool stuff!" }
assert_equal post.title, "New on Edge"
assert_equal post.body, "More cool stuff!"
end
def test_reload_returns_association
david = developers(:david)
assert_nothing_raised do
assert_equal david.projects, david.projects.reload.reload
end
end
def test_proxy_association_accessor
david = developers(:david)
assert_equal david.association(:projects), david.projects.proxy_association
end
def test_scoped_allows_conditions
assert developers(:david).projects.merge(where: "foo").to_sql.include?("foo")
end
test "getting a scope from an association" do
david = developers(:david)
assert david.projects.scope.is_a?(ActiveRecord::Relation)
assert_equal david.projects, david.projects.scope
end
test "proxy object is cached" do
david = developers(:david)
assert_same david.projects, david.projects
end
test "proxy object can be stubbed" do
david = developers(:david)
david.projects.define_singleton_method(:extra_method) { 42 }
assert_equal 42, david.projects.extra_method
end
test "inverses get set of subsets of the association" do
human = Human.create
human.interests.create
human = Human.find(human.id)
assert_queries(1) do
assert_equal human, human.interests.where("1=1").first.human
end
end
test "first! works on loaded associations" do
david = authors(:david)
assert_equal david.first_posts.first, david.first_posts.reload.first!
assert_predicate david.first_posts, :loaded?
assert_no_queries { david.first_posts.first! }
end
def test_pluck_uses_loaded_target
david = authors(:david)
assert_equal david.first_posts.pluck(:title), david.first_posts.load.pluck(:title)
assert_predicate david.first_posts, :loaded?
assert_no_queries { david.first_posts.pluck(:title) }
end
def test_pick_uses_loaded_target
david = authors(:david)
assert_equal david.first_posts.pick(:title), david.first_posts.load.pick(:title)
assert_predicate david.first_posts, :loaded?
assert_no_queries { david.first_posts.pick(:title) }
end
def test_reset_unloads_target
david = authors(:david)
david.posts.reload
assert_predicate david.posts, :loaded?
assert_predicate david.posts, :loaded
david.posts.reset
assert_not_predicate david.posts, :loaded?
assert_not_predicate david.posts, :loaded
end
def test_target_merging_ignores_persisted_in_memory_records
david = authors(:david)
assert david.thinking_posts.include?(posts(:thinking))
david.thinking_posts.create!(title: "Something else entirely", body: "Does not matter.")
assert_equal 1, david.thinking_posts.size
assert_equal 1, david.thinking_posts.to_a.size
end
end
class OverridingAssociationsTest < ActiveRecord::TestCase
class DifferentPerson < ActiveRecord::Base; end
class PeopleList < ActiveRecord::Base
has_and_belongs_to_many :has_and_belongs_to_many, before_add: :enlist
has_many :has_many, before_add: :enlist
belongs_to :belongs_to
has_one :has_one
end
class DifferentPeopleList < PeopleList
# Different association with the same name, callbacks should be omitted here.
has_and_belongs_to_many :has_and_belongs_to_many, class_name: "DifferentPerson"
has_many :has_many, class_name: "DifferentPerson"
belongs_to :belongs_to, class_name: "DifferentPerson"
has_one :has_one, class_name: "DifferentPerson"
end
def test_habtm_association_redefinition_callbacks_should_differ_and_not_inherited
# redeclared association on AR descendant should not inherit callbacks from superclass
callbacks = PeopleList.before_add_for_has_and_belongs_to_many
assert_equal(1, callbacks.length)
callbacks = DifferentPeopleList.before_add_for_has_and_belongs_to_many
assert_equal([], callbacks)
end
def test_has_many_association_redefinition_callbacks_should_differ_and_not_inherited
# redeclared association on AR descendant should not inherit callbacks from superclass
callbacks = PeopleList.before_add_for_has_many
assert_equal(1, callbacks.length)
callbacks = DifferentPeopleList.before_add_for_has_many
assert_equal([], callbacks)
end
def test_habtm_association_redefinition_reflections_should_differ_and_not_inherited
assert_not_equal(
PeopleList.reflect_on_association(:has_and_belongs_to_many),
DifferentPeopleList.reflect_on_association(:has_and_belongs_to_many)
)
end
def test_has_many_association_redefinition_reflections_should_differ_and_not_inherited
assert_not_equal(
PeopleList.reflect_on_association(:has_many),
DifferentPeopleList.reflect_on_association(:has_many)
)
end
def test_belongs_to_association_redefinition_reflections_should_differ_and_not_inherited
assert_not_equal(
PeopleList.reflect_on_association(:belongs_to),
DifferentPeopleList.reflect_on_association(:belongs_to)
)
end
def test_has_one_association_redefinition_reflections_should_differ_and_not_inherited
assert_not_equal(
PeopleList.reflect_on_association(:has_one),
DifferentPeopleList.reflect_on_association(:has_one)
)
end
def test_requires_symbol_argument
assert_raises ArgumentError do
Class.new(Post) do
belongs_to "author"
end
end
end
class ModelAssociatedToClassesThatDoNotExist < ActiveRecord::Base
self.table_name = "accounts" # this is just to avoid adding a new model just for this test
has_one :non_existent_has_one_class
belongs_to :non_existent_belongs_to_class
has_many :non_existent_has_many_classes
end
def test_associations_raise_with_name_error_if_associated_to_classes_that_do_not_exist
assert_raises NameError do
ModelAssociatedToClassesThatDoNotExist.new.non_existent_has_one_class
end
assert_raises NameError do
ModelAssociatedToClassesThatDoNotExist.new.non_existent_belongs_to_class
end
assert_raises NameError do
ModelAssociatedToClassesThatDoNotExist.new.non_existent_has_many_classes
end
end
end
class PreloaderTest < ActiveRecord::TestCase
fixtures :posts, :comments, :books, :authors, :tags, :taggings, :essays, :categories, :author_addresses
def test_preload_with_scope
post = posts(:welcome)
preloader = ActiveRecord::Associations::Preloader.new(records: [post], associations: :comments, scope: Comment.where(body: "Thank you for the welcome"))
preloader.call
assert_predicate post.comments, :loaded?
assert_equal [comments(:greetings)], post.comments
end
def test_preload_makes_correct_number_of_queries_on_array
post = posts(:welcome)
assert_queries(1) do
preloader = ActiveRecord::Associations::Preloader.new(records: [post], associations: :comments)
preloader.call
end
end
def test_preload_makes_correct_number_of_queries_on_relation
post = posts(:welcome)
relation = Post.where(id: post.id)
assert_queries(2) do
preloader = ActiveRecord::Associations::Preloader.new(records: relation, associations: :comments)
preloader.call
end
end
def test_preload_for_hmt_with_conditions
post = posts(:welcome)
_normal_category = post.categories.create!(name: "Normal")
special_category = post.special_categories.create!(name: "Special")
preloader = ActiveRecord::Associations::Preloader.new(records: [post], associations: :hmt_special_categories)
preloader.call
assert_equal 1, post.hmt_special_categories.length
assert_equal [special_category], post.hmt_special_categories
end
def test_preload_groups_queries_with_same_scope
book = books(:awdr)
post = posts(:welcome)
assert_queries(1) do
preloader = ActiveRecord::Associations::Preloader.new(records: [book, post], associations: :author)
preloader.call
end
assert_no_queries do
book.author
post.author
end
end
def test_preload_grouped_queries_with_already_loaded_records
book = books(:awdr)
post = posts(:welcome)
book.author
assert_no_queries do
ActiveRecord::Associations::Preloader.new(records: [book, post], associations: :author).call
book.author
post.author
end
end
def test_preload_grouped_queries_of_middle_records
comments = [
comments(:eager_sti_on_associations_s_comment1),
comments(:eager_sti_on_associations_s_comment2),
]
assert_queries(2) do
ActiveRecord::Associations::Preloader.new(records: comments, associations: [:author, :ordinary_post]).call
end
end
def test_preload_grouped_queries_of_through_records
author = authors(:david)
assert_queries(3) do
ActiveRecord::Associations::Preloader.new(records: [author], associations: [:hello_post_comments, :comments]).call
end
end
def test_preload_with_instance_dependent_scope
david = authors(:david)
david2 = Author.create!(name: "David")
bob = authors(:bob)
post = Post.create!(
author: david,
title: "test post",
body: "this post is about David"
)
post2 = Post.create!(
author: david,
title: "test post 2",
body: "this post is also about David"
)
assert_queries(2) do
preloader = ActiveRecord::Associations::Preloader.new(records: [david, david2, bob], associations: :posts_mentioning_author)
preloader.call
end
assert_predicate david.posts_mentioning_author, :loaded?
assert_predicate david2.posts_mentioning_author, :loaded?
assert_predicate bob.posts_mentioning_author, :loaded?
assert_equal [post, post2].sort, david.posts_mentioning_author.sort
assert_equal [], david2.posts_mentioning_author
assert_equal [], bob.posts_mentioning_author
end
def test_preload_with_instance_dependent_through_scope
david = authors(:david)
david2 = Author.create!(name: "David")
bob = authors(:bob)
comment1 = david.posts.first.comments.create!(body: "Hi David!")
comment2 = david.posts.first.comments.create!(body: "This comment mentions david")
assert_queries(2) do
preloader = ActiveRecord::Associations::Preloader.new(records: [david, david2, bob], associations: :comments_mentioning_author)
preloader.call
end
assert_predicate david.comments_mentioning_author, :loaded?
assert_predicate david2.comments_mentioning_author, :loaded?
assert_predicate bob.comments_mentioning_author, :loaded?
assert_equal [comment1, comment2].sort, david.comments_mentioning_author.sort
assert_equal [], david2.comments_mentioning_author
assert_equal [], bob.comments_mentioning_author
end
def test_preload_with_through_instance_dependent_scope
david = authors(:david)
david2 = Author.create!(name: "David")
bob = authors(:bob)
post = Post.create!(
author: david,
title: "test post",
body: "this post is about David"
)
Post.create!(
author: david,
title: "test post 2",
body: "this post is also about David"
)
post3 = Post.create!(
author: bob,
title: "test post 3",
body: "this post is about Bob"
)
comment1 = post.comments.create!(body: "hi!")
comment2 = post.comments.create!(body: "hello!")
comment3 = post3.comments.create!(body: "HI BOB!")
assert_queries(3) do
preloader = ActiveRecord::Associations::Preloader.new(records: [david, david2, bob], associations: :comments_on_posts_mentioning_author)
preloader.call
end
assert_predicate david.comments_on_posts_mentioning_author, :loaded?
assert_predicate david2.comments_on_posts_mentioning_author, :loaded?
assert_predicate bob.comments_on_posts_mentioning_author, :loaded?
assert_equal [comment1, comment2].sort, david.comments_on_posts_mentioning_author.sort
assert_equal [], david2.comments_on_posts_mentioning_author
assert_equal [comment3], bob.comments_on_posts_mentioning_author
end
def test_some_already_loaded_associations
item_discount = Discount.create(amount: 5)
shipping_discount = Discount.create(amount: 20)
invoice = Invoice.new
line_item = LineItem.new(amount: 20)
line_item.discount_applications << LineItemDiscountApplication.new(discount: item_discount)
invoice.line_items << line_item
shipping_line = ShippingLine.new(amount: 50)
shipping_line.discount_applications << ShippingLineDiscountApplication.new(discount: shipping_discount)
invoice.shipping_lines << shipping_line
invoice.save!
invoice.reload
# SELECT "line_items".* FROM "line_items" WHERE "line_items"."invoice_id" = ?
# SELECT "shipping_lines".* FROM shipping_lines WHERE "shipping_lines"."invoice_id" = ?
# SELECT "line_item_discount_applications".* FROM "line_item_discount_applications" WHERE "line_item_discount_applications"."line_item_id" = ?
# SELECT "shipping_line_discount_applications".* FROM "shipping_line_discount_applications" WHERE "shipping_line_discount_applications"."shipping_line_id" = ?
# SELECT "discounts".* FROM "discounts" WHERE "discounts"."id" IN (?, ?).
assert_queries(5) do
preloader = ActiveRecord::Associations::Preloader.new(records: [invoice], associations: [
line_items: { discount_applications: :discount },
shipping_lines: { discount_applications: :discount },
])
preloader.call
end
assert_no_queries do
assert_not_nil invoice.line_items.first.discount_applications.first.discount
assert_not_nil invoice.shipping_lines.first.discount_applications.first.discount
end
invoice.reload
invoice.line_items.map { |i| i.discount_applications.to_a }
# `line_items` and `line_item_discount_applications` are already preloaded, so we expect:
# SELECT "shipping_lines".* FROM shipping_lines WHERE "shipping_lines"."invoice_id" = ?
# SELECT "shipping_line_discount_applications".* FROM "shipping_line_discount_applications" WHERE "shipping_line_discount_applications"."shipping_line_id" = ?
# SELECT "discounts".* FROM "discounts" WHERE "discounts"."id" = ?.
assert_queries(3) do
preloader = ActiveRecord::Associations::Preloader.new(records: [invoice], associations: [
line_items: { discount_applications: :discount },
shipping_lines: { discount_applications: :discount },
])
preloader.call
end
assert_no_queries do
assert_not_nil invoice.line_items.first.discount_applications.first.discount
assert_not_nil invoice.shipping_lines.first.discount_applications.first.discount
end
end
def test_preload_through
comments = [
comments(:eager_sti_on_associations_s_comment1),
comments(:eager_sti_on_associations_s_comment2),
]
assert_queries(2) do
preloader = ActiveRecord::Associations::Preloader.new(records: comments, associations: [:author, :post])
preloader.call
end
assert_no_queries do
comments.each(&:author)
end
end
def test_preload_groups_queries_with_same_scope_at_second_level
author = nil
# Expected
# SELECT FROM authors ...
# SELECT FROM posts ... (thinking)
# SELECT FROM posts ... (welcome)
# SELECT FROM comments ... (comments for both welcome and thinking)
assert_queries(4) do
author = Author
.where(name: "David")
.includes(thinking_posts: :comments, welcome_posts: :comments)
.first
end
assert_no_queries do
author.thinking_posts.map(&:comments)
author.welcome_posts.map(&:comments)
end
end
def test_preload_groups_queries_with_same_sql_at_second_level
author = nil
# Expected
# SELECT FROM authors ...
# SELECT FROM posts ... (thinking)
# SELECT FROM posts ... (welcome)
# SELECT FROM comments ... (comments for both welcome and thinking)
assert_queries(4) do
author = Author
.where(name: "David")
.includes(thinking_posts: :comments, welcome_posts: :comments_with_extending)
.first
end
assert_no_queries do
author.thinking_posts.map(&:comments)
author.welcome_posts.map(&:comments_with_extending)
end
end
def test_preload_with_grouping_sets_inverse_association
mary = authors(:mary)
bob = authors(:bob)
AuthorFavorite.create!(author: mary, favorite_author: bob)
favorites = AuthorFavorite.all.load
assert_queries(1) do
preloader = ActiveRecord::Associations::Preloader.new(records: favorites, associations: [:author, :favorite_author])
preloader.call
end
assert_no_queries do
favorites.first.author
favorites.first.favorite_author
end
end
def test_preload_can_group_separate_levels
mary = authors(:mary)
bob = authors(:bob)
AuthorFavorite.create!(author: mary, favorite_author: bob)
assert_queries(3) do
preloader = ActiveRecord::Associations::Preloader.new(records: [mary], associations: [:posts, favorite_authors: :posts])
preloader.call
end
assert_no_queries do
mary.posts
mary.favorite_authors.map(&:posts)
end
end
def test_preload_can_group_multi_level_ping_pong_through
mary = authors(:mary)
bob = authors(:bob)
AuthorFavorite.create!(author: mary, favorite_author: bob)
associations = { similar_posts: :comments, favorite_authors: { similar_posts: :comments } }
assert_queries(9) do
preloader = ActiveRecord::Associations::Preloader.new(records: [mary], associations: associations)
preloader.call
end
assert_no_queries do
mary.similar_posts.map(&:comments).each(&:to_a)
mary.favorite_authors.flat_map(&:similar_posts).map(&:comments).each(&:to_a)
end
# Preloading with automatic scope inversing reduces the number of queries
tag_reflection = Tagging.reflect_on_association(:tag)
taggings_reflection = Tag.reflect_on_association(:taggings)
assert tag_reflection.scope
assert_not taggings_reflection.scope
with_automatic_scope_inversing(tag_reflection, taggings_reflection) do
mary.reload
assert_queries(8) do
preloader = ActiveRecord::Associations::Preloader.new(records: [mary], associations: associations)
preloader.call
end
end
end
def test_preload_does_not_group_same_class_different_scope
post = posts(:welcome)
postesque = Postesque.create(author: Author.last)
postesque.reload
# When the scopes differ in the generated SQL:
# SELECT "authors".* FROM "authors" WHERE (name LIKE '%a%') AND "authors"."id" = ?
# SELECT "authors".* FROM "authors" WHERE "authors"."id" = ?.
assert_queries(2) do
preloader = ActiveRecord::Associations::Preloader.new(records: [post, postesque], associations: :author_with_the_letter_a)
preloader.call
end
assert_no_queries do
post.author_with_the_letter_a
postesque.author_with_the_letter_a
end
post.reload
postesque.reload
# When the generated SQL is identical, but one scope has preload values.
assert_queries(3) do
preloader = ActiveRecord::Associations::Preloader.new(records: [post, postesque], associations: :author_with_address)
preloader.call
end
assert_no_queries do
post.author_with_address
postesque.author_with_address
end
end
def test_preload_does_not_group_same_scope_different_key_name
post = posts(:welcome)
postesque = Postesque.create(author: Author.last)
postesque.reload
assert_queries(2) do
preloader = ActiveRecord::Associations::Preloader.new(records: [post, postesque], associations: :author)
preloader.call
end
assert_no_queries do
post.author
postesque.author
end
end
def test_preload_with_available_records
post = posts(:welcome)
david = authors(:david)
assert_no_queries do
ActiveRecord::Associations::Preloader.new(records: [post], associations: :author, available_records: [[david]]).call
assert_predicate post.association(:author), :loaded?
assert_same david, post.author
end
end
def test_preload_with_available_records_sti
book = Book.create!
essay_special = EssaySpecial.create!
book.essay = essay_special
book.save!
book.reload
assert_not_predicate book.association(:essay), :loaded?
assert_no_queries do
ActiveRecord::Associations::Preloader.new(records: [book], associations: :essay, available_records: [[essay_special]]).call
end
assert_predicate book.association(:essay), :loaded?
assert_same essay_special, book.essay
end
def test_preload_with_only_some_records_available
bob_post = posts(:misc_by_bob)
mary_post = posts(:misc_by_mary)
bob = authors(:bob)
mary = authors(:mary)
assert_queries(1) do
ActiveRecord::Associations::Preloader.new(records: [bob_post, mary_post], associations: :author, available_records: [bob]).call
end
assert_no_queries do
assert_same bob, bob_post.author
assert_equal mary, mary_post.author
end
end
def test_preload_with_some_records_already_loaded
bob_post = posts(:misc_by_bob)
mary_post = posts(:misc_by_mary)
bob = bob_post.author
mary = authors(:mary)
assert bob_post.association(:author).loaded?
assert_not mary_post.association(:author).loaded?
assert_queries(1) do
ActiveRecord::Associations::Preloader.new(records: [bob_post, mary_post], associations: :author).call
end
assert_no_queries do
assert_same bob, bob_post.author
assert_equal mary, mary_post.author
end
end
def test_preload_with_available_records_with_through_association
author = authors(:david)
categories = Category.all.to_a
assert_queries(1) do
# One query to get the middle records (i.e. essays)
ActiveRecord::Associations::Preloader.new(records: [author], associations: :essay_category, available_records: categories).call
end
assert_predicate author.association(:essay_category), :loaded?
assert categories.map(&:object_id).include?(author.essay_category.object_id)
end
def test_preload_with_only_some_records_available_with_through_associations
mary = authors(:mary)
mary_essay = essays(:mary_stay_home)
mary_category = categories(:technology)
mary_essay.update!(category: mary_category)
dave = authors(:david)
dave_category = categories(:general)
assert_queries(2) do
ActiveRecord::Associations::Preloader.new(records: [mary, dave], associations: :essay_category, available_records: [mary_category]).call
end
assert_no_queries do
assert_same mary_category, mary.essay_category
assert_equal dave_category, dave.essay_category
end
end
def test_preload_with_available_records_with_multiple_classes
essay = essays(:david_modest_proposal)
general = categories(:general)
david = authors(:david)
assert_no_queries do
ActiveRecord::Associations::Preloader.new(records: [essay], associations: [:category, :author], available_records: [general, david]).call
assert_predicate essay.association(:category), :loaded?
assert_predicate essay.association(:author), :loaded?
assert_same general, essay.category
assert_same david, essay.author
end
end
def test_preload_with_available_records_queries_when_scoped
post = posts(:welcome)
david = authors(:david)
assert_queries(1) do
ActiveRecord::Associations::Preloader.new(records: [post], associations: :author, scope: Author.where(name: "David"), available_records: [david]).call
end
assert_predicate post.association(:author), :loaded?
assert_not_equal david.object_id, post.author.object_id
end
def test_preload_with_available_records_queries_when_collection
post = posts(:welcome)
comments = Comment.all.to_a
assert_queries(1) do
ActiveRecord::Associations::Preloader.new(records: [post], associations: :comments, available_records: comments).call
end
assert_predicate post.association(:comments), :loaded?
assert_empty post.comments.map(&:object_id) & comments.map(&:object_id)
end
def test_preload_with_available_records_queries_when_incomplete
post = posts(:welcome)
bob = authors(:bob)
david = authors(:david)
assert_queries(1) do
ActiveRecord::Associations::Preloader.new(records: [post], associations: :author, available_records: [bob]).call
end
assert_no_queries do
assert_predicate post.association(:author), :loaded?
assert_equal david, post.author
end
end
def test_preload_with_unpersisted_records_no_ops
author = Author.new
new_post_with_author = Post.new(author: author)
new_post_without_author = Post.new
posts = [new_post_with_author, new_post_without_author]
assert_no_queries do
ActiveRecord::Associations::Preloader.new(records: posts, associations: :author).call
assert_same author, new_post_with_author.author
assert_nil new_post_without_author.author
end
end
def test_preload_wont_set_the_wrong_target
post = posts(:welcome)
post.update!(author_id: 54321)
some_other_record = categories(:general)
some_other_record.update!(id: 54321)
assert_raises do
some_other_record.association(:author)
end
assert_nothing_raised do
ActiveRecord::Associations::Preloader.new(records: [post], associations: :author, available_records: [[some_other_record]]).call
assert post.association(:author).loaded?
assert_not_equal some_other_record, post.author
end
end
end
class GeneratedMethodsTest < ActiveRecord::TestCase
fixtures :developers, :computers, :posts, :comments
def test_association_methods_override_attribute_methods_of_same_name
assert_equal(developers(:david), computers(:workstation).developer)
# this next line will fail if the attribute methods module is generated lazily
# after the association methods module is generated
assert_equal(developers(:david), computers(:workstation).developer)
assert_equal(developers(:david).id, computers(:workstation)[:developer])
end
def test_model_method_overrides_association_method
assert_equal(comments(:greetings).body, posts(:welcome).first_comment)
end
module MyModule
def comments; :none end
end
class MyArticle < ActiveRecord::Base
self.table_name = "articles"
include MyModule
has_many :comments, inverse_of: false
end
def test_included_module_overwrites_association_methods
assert_equal :none, MyArticle.new.comments
end
end
class WithAnnotationsTest < ActiveRecord::TestCase
fixtures :pirates, :parrots
def test_belongs_to_with_annotation_includes_a_query_comment
pirate = SpacePirate.where.not(parrot_id: nil).first
assert pirate, "should have a Pirate record"
log = capture_sql do
pirate.parrot
end
assert_not_predicate log, :empty?
assert_predicate log.select { |query| query.match?(%r{/\*}) }, :empty?
assert_sql(%r{/\* that tells jokes \*/}) do
pirate.parrot_with_annotation
end
end
def test_has_and_belongs_to_many_with_annotation_includes_a_query_comment
pirate = SpacePirate.first
assert pirate, "should have a Pirate record"
log = capture_sql do
pirate.parrots.first
end
assert_not_predicate log, :empty?
assert_predicate log.select { |query| query.match?(%r{/\*}) }, :empty?
assert_sql(%r{/\* that are very colorful \*/}) do
pirate.parrots_with_annotation.first
end
end
def test_has_one_with_annotation_includes_a_query_comment
pirate = SpacePirate.first
assert pirate, "should have a Pirate record"
log = capture_sql do
pirate.ship
end
assert_not_predicate log, :empty?
assert_predicate log.select { |query| query.match?(%r{/\*}) }, :empty?
assert_sql(%r{/\* that is a rocket \*/}) do
pirate.ship_with_annotation
end
end
def test_has_many_with_annotation_includes_a_query_comment
pirate = SpacePirate.first
assert pirate, "should have a Pirate record"
log = capture_sql do
pirate.birds.first
end
assert_not_predicate log, :empty?
assert_predicate log.select { |query| query.match?(%r{/\*}) }, :empty?
assert_sql(%r{/\* that are also parrots \*/}) do
pirate.birds_with_annotation.first
end
end
def test_has_many_through_with_annotation_includes_a_query_comment
pirate = SpacePirate.first
assert pirate, "should have a Pirate record"
log = capture_sql do
pirate.treasure_estimates.first
end
assert_not_predicate log, :empty?
assert_predicate log.select { |query| query.match?(%r{/\*}) }, :empty?
assert_sql(%r{/\* yarrr \*/}) do
pirate.treasure_estimates_with_annotation.first
end
end
def test_has_many_through_with_annotation_includes_a_query_comment_when_eager_loading
pirate = SpacePirate.first
assert pirate, "should have a Pirate record"
log = capture_sql do
pirate.treasure_estimates.first
end
assert_not_predicate log, :empty?
assert_predicate log.select { |query| query.match?(%r{/\*}) }, :empty?
assert_sql(%r{/\* yarrr \*/}) do
SpacePirate.includes(:treasure_estimates_with_annotation, :treasures).first
end
end
end
# frozen_string_literal: true
require "cases/helper"
require "support/connection_helper"
require "models/post"
module AsynchronousQueriesSharedTests
def test_async_select_failure
ActiveRecord::Base.asynchronous_queries_tracker.start_session
if in_memory_db?
assert_raises ActiveRecord::StatementInvalid do
@connection.select_all "SELECT * FROM does_not_exists", async: true
end
else
future_result = @connection.select_all "SELECT * FROM does_not_exists", async: true
assert_kind_of ActiveRecord::FutureResult, future_result
assert_raises ActiveRecord::StatementInvalid do
future_result.result
end
end
ensure
ActiveRecord::Base.asynchronous_queries_tracker.finalize_session
end
def test_async_query_from_transaction
ActiveRecord::Base.asynchronous_queries_tracker.start_session
assert_nothing_raised do
@connection.select_all "SELECT * FROM posts", async: true
end
unless in_memory_db?
@connection.transaction do
assert_raises ActiveRecord::AsynchronousQueryInsideTransactionError do
@connection.select_all "SELECT * FROM posts", async: true
end
end
end
ensure
ActiveRecord::Base.asynchronous_queries_tracker.finalize_session
end
def test_async_query_cache
ActiveRecord::Base.asynchronous_queries_tracker.start_session
@connection.enable_query_cache!
@connection.select_all "SELECT * FROM posts"
result = @connection.select_all "SELECT * FROM posts", async: true
assert_equal ActiveRecord::Result, result.class
ensure
ActiveRecord::Base.asynchronous_queries_tracker.finalize_session
@connection.disable_query_cache!
end
def test_async_query_foreground_fallback
status = {}
subscriber = ActiveSupport::Notifications.subscribe("sql.active_record") do |event|
if event.payload[:sql] == "SELECT * FROM does_not_exists"
status[:executed] = true
status[:async] = event.payload[:async]
end
end
@connection.pool.stub(:schedule_query, proc { }) do
if in_memory_db?
assert_raises ActiveRecord::StatementInvalid do
@connection.select_all "SELECT * FROM does_not_exists", async: true
end
else
future_result = @connection.select_all "SELECT * FROM does_not_exists", async: true
assert_kind_of ActiveRecord::FutureResult, future_result
assert_raises ActiveRecord::StatementInvalid do
future_result.result
end
end
end
assert_equal true, status[:executed]
assert_equal false, status[:async]
ensure
ActiveSupport::Notifications.unsubscribe(subscriber) if subscriber
end
private
def wait_for_future_result(result)
100.times do
break unless result.pending?
sleep 0.01
end
end
end
class AsynchronousQueriesTest < ActiveRecord::TestCase
self.use_transactional_tests = false
include AsynchronousQueriesSharedTests
def setup
@connection = ActiveRecord::Base.connection
end
def test_async_select_all
ActiveRecord::Base.asynchronous_queries_tracker.start_session
status = {}
subscriber = ActiveSupport::Notifications.subscribe("sql.active_record") do |event|
if event.payload[:sql] == "SELECT * FROM posts"
status[:executed] = true
status[:async] = event.payload[:async]
end
end
future_result = @connection.select_all "SELECT * FROM posts", async: true
if in_memory_db?
assert_kind_of ActiveRecord::Result, future_result
else
assert_kind_of ActiveRecord::FutureResult, future_result
wait_for_future_result(future_result)
end
assert_kind_of ActiveRecord::Result, future_result.result
assert_equal @connection.supports_concurrent_connections?, status[:async]
ensure
ActiveRecord::Base.asynchronous_queries_tracker.finalize_session
ActiveSupport::Notifications.unsubscribe(subscriber) if subscriber
end
end
class AsynchronousQueriesWithTransactionalTest < ActiveRecord::TestCase
self.use_transactional_tests = true
include AsynchronousQueriesSharedTests
def setup
@connection = ActiveRecord::Base.connection
@connection.materialize_transactions
end
end
class AsynchronousExecutorTypeTest < ActiveRecord::TestCase
def test_null_configuration_uses_a_single_null_executor_by_default
old_value = ActiveRecord.async_query_executor
ActiveRecord.async_query_executor = nil
handler = ActiveRecord::ConnectionAdapters::ConnectionHandler.new
db_config = ActiveRecord::Base.configurations.configs_for(env_name: "arunit", name: "primary")
db_config2 = ActiveRecord::Base.configurations.configs_for(env_name: "arunit2", name: "primary")
pool1 = handler.establish_connection(db_config)
pool2 = handler.establish_connection(db_config2, owner_name: ARUnit2Model)
async_pool1 = pool1.instance_variable_get(:@async_executor)
async_pool2 = pool2.instance_variable_get(:@async_executor)
assert_nil async_pool1
assert_nil async_pool2
assert_equal 2, handler.all_connection_pools.count
ensure
clean_up_connection_handler
ActiveRecord.async_query_executor = old_value
end
def test_one_global_thread_pool_is_used_when_set_with_default_concurrency
old_value = ActiveRecord.async_query_executor
ActiveRecord.async_query_executor = :global_thread_pool
handler = ActiveRecord::ConnectionAdapters::ConnectionHandler.new
db_config = ActiveRecord::Base.configurations.configs_for(env_name: "arunit", name: "primary")
db_config2 = ActiveRecord::Base.configurations.configs_for(env_name: "arunit2", name: "primary")
pool1 = handler.establish_connection(db_config)
pool2 = handler.establish_connection(db_config2, owner_name: ARUnit2Model)
async_pool1 = pool1.instance_variable_get(:@async_executor)
async_pool2 = pool2.instance_variable_get(:@async_executor)
assert async_pool1.is_a?(Concurrent::ThreadPoolExecutor)
assert async_pool2.is_a?(Concurrent::ThreadPoolExecutor)
assert_equal 0, async_pool1.min_length
assert_equal 4, async_pool1.max_length
assert_equal 16, async_pool1.max_queue
assert_equal :caller_runs, async_pool1.fallback_policy
assert_equal 0, async_pool2.min_length
assert_equal 4, async_pool2.max_length
assert_equal 16, async_pool2.max_queue
assert_equal :caller_runs, async_pool2.fallback_policy
assert_equal 2, handler.all_connection_pools.count
assert_equal async_pool1, async_pool2
ensure
clean_up_connection_handler
ActiveRecord.async_query_executor = old_value
end
def test_concurrency_can_be_set_on_global_thread_pool
old_value = ActiveRecord.async_query_executor
ActiveRecord.async_query_executor = :global_thread_pool
old_concurrency = ActiveRecord.global_executor_concurrency
old_global_thread_pool_async_query_executor = ActiveRecord.instance_variable_get(:@global_thread_pool_async_query_executor)
ActiveRecord.instance_variable_set(:@global_thread_pool_async_query_executor, nil)
ActiveRecord.global_executor_concurrency = 8
handler = ActiveRecord::ConnectionAdapters::ConnectionHandler.new
db_config = ActiveRecord::Base.configurations.configs_for(env_name: "arunit", name: "primary")
db_config2 = ActiveRecord::Base.configurations.configs_for(env_name: "arunit2", name: "primary")
pool1 = handler.establish_connection(db_config)
pool2 = handler.establish_connection(db_config2, owner_name: ARUnit2Model)
async_pool1 = pool1.instance_variable_get(:@async_executor)
async_pool2 = pool2.instance_variable_get(:@async_executor)
assert async_pool1.is_a?(Concurrent::ThreadPoolExecutor)
assert async_pool2.is_a?(Concurrent::ThreadPoolExecutor)
assert_equal 0, async_pool1.min_length
assert_equal 8, async_pool1.max_length
assert_equal 32, async_pool1.max_queue
assert_equal :caller_runs, async_pool1.fallback_policy
assert_equal 0, async_pool2.min_length
assert_equal 8, async_pool2.max_length
assert_equal 32, async_pool2.max_queue
assert_equal :caller_runs, async_pool2.fallback_policy
assert_equal 2, handler.all_connection_pools.count
assert_equal async_pool1, async_pool2
ensure
clean_up_connection_handler
ActiveRecord.global_executor_concurrency = old_concurrency
ActiveRecord.async_query_executor = old_value
ActiveRecord.instance_variable_set(:@global_thread_pool_async_query_executor, old_global_thread_pool_async_query_executor)
end
def test_concurrency_cannot_be_set_with_null_executor_or_multi_thread_pool
old_value = ActiveRecord.async_query_executor
ActiveRecord.async_query_executor = nil
assert_raises ArgumentError do
ActiveRecord.global_executor_concurrency = 8
end
ActiveRecord.async_query_executor = :multi_thread_pool
assert_raises ArgumentError do
ActiveRecord.global_executor_concurrency = 8
end
ensure
ActiveRecord.async_query_executor = old_value
end
def test_multi_thread_pool_executor_configuration
old_value = ActiveRecord.async_query_executor
ActiveRecord.async_query_executor = :multi_thread_pool
handler = ActiveRecord::ConnectionAdapters::ConnectionHandler.new
config_hash = ActiveRecord::Base.configurations.configs_for(env_name: "arunit", name: "primary").configuration_hash
new_config_hash = config_hash.merge(min_threads: 0, max_threads: 10)
db_config = ActiveRecord::DatabaseConfigurations::HashConfig.new("arunit", "primary", new_config_hash)
db_config2 = ActiveRecord::Base.configurations.configs_for(env_name: "arunit2", name: "primary")
pool1 = handler.establish_connection(db_config)
pool2 = handler.establish_connection(db_config2, owner_name: ARUnit2Model)
async_pool1 = pool1.instance_variable_get(:@async_executor)
async_pool2 = pool2.instance_variable_get(:@async_executor)
assert async_pool1.is_a?(Concurrent::ThreadPoolExecutor)
assert async_pool2.is_a?(Concurrent::ThreadPoolExecutor)
assert_equal 0, async_pool1.min_length
assert_equal 10, async_pool1.max_length
assert_equal 40, async_pool1.max_queue
assert_equal :caller_runs, async_pool1.fallback_policy
assert_equal 0, async_pool2.min_length
assert_equal 5, async_pool2.max_length
assert_equal 20, async_pool2.max_queue
assert_equal :caller_runs, async_pool2.fallback_policy
assert_equal 2, handler.all_connection_pools.count
assert_not_equal async_pool1, async_pool2
ensure
clean_up_connection_handler
ActiveRecord.async_query_executor = old_value
end
def test_multi_thread_pool_is_used_only_by_configurations_that_enable_it
old_value = ActiveRecord.async_query_executor
ActiveRecord.async_query_executor = :multi_thread_pool
handler = ActiveRecord::ConnectionAdapters::ConnectionHandler.new
config_hash1 = ActiveRecord::Base.configurations.configs_for(env_name: "arunit", name: "primary").configuration_hash
new_config1 = config_hash1.merge(min_threads: 0, max_threads: 10)
db_config1 = ActiveRecord::DatabaseConfigurations::HashConfig.new("arunit", "primary", new_config1)
config_hash2 = ActiveRecord::Base.configurations.configs_for(env_name: "arunit2", name: "primary").configuration_hash
new_config2 = config_hash2.merge(min_threads: 0, max_threads: 0)
db_config2 = ActiveRecord::DatabaseConfigurations::HashConfig.new("arunit2", "primary", new_config2)
pool1 = handler.establish_connection(db_config1)
pool2 = handler.establish_connection(db_config2, owner_name: ARUnit2Model)
async_pool1 = pool1.instance_variable_get(:@async_executor)
async_pool2 = pool2.instance_variable_get(:@async_executor)
assert async_pool1.is_a?(Concurrent::ThreadPoolExecutor)
assert_nil async_pool2
assert_equal 0, async_pool1.min_length
assert_equal 10, async_pool1.max_length
assert_equal 40, async_pool1.max_queue
assert_equal :caller_runs, async_pool1.fallback_policy
assert_equal 2, handler.all_connection_pools.count
assert_not_equal async_pool1, async_pool2
ensure
clean_up_connection_handler
ActiveRecord.async_query_executor = old_value
end
end
# frozen_string_literal: true
module ActiveRecord
class AsynchronousQueriesTracker # :nodoc:
module NullSession # :nodoc:
class << self
def active?
true
end
def finalize
end
end
end
class Session # :nodoc:
def initialize
@active = true
end
def active?
@active
end
def finalize
@active = false
end
end
class << self
def install_executor_hooks(executor = ActiveSupport::Executor)
executor.register_hook(self)
end
def run
ActiveRecord::Base.asynchronous_queries_tracker.start_session
end
def complete(asynchronous_queries_tracker)
asynchronous_queries_tracker.finalize_session
end
end
attr_reader :current_session
def initialize
@current_session = NullSession
end
def start_session
@current_session = Session.new
self
end
def finalize_session
@current_session.finalize
@current_session = NullSession
end
end
end
# frozen_string_literal: true
class Attachment < ActiveRecord::Base
belongs_to :record, polymorphic: true
has_one :translation
end
# frozen_string_literal: true
module Arel # :nodoc: all
module Attributes
class Attribute < Struct.new :relation, :name
include Arel::Expressions
include Arel::Predications
include Arel::AliasPredication
include Arel::OrderPredications
include Arel::Math
def type_caster
relation.type_for_attribute(name)
end
###
# Create a node for lowering this attribute
def lower
relation.lower self
end
def type_cast_for_database(value)
relation.type_cast_for_database(name, value)
end
def able_to_type_cast?
relation.able_to_type_cast?
end
end
end
Attribute = Attributes::Attribute
end
# frozen_string_literal: true
require "active_model/forbidden_attributes_protection"
module ActiveRecord
module AttributeAssignment
include ActiveModel::AttributeAssignment
private
def _assign_attributes(attributes)
multi_parameter_attributes = nested_parameter_attributes = nil
attributes.each do |k, v|
key = k.to_s
if key.include?("(")
(multi_parameter_attributes ||= {})[key] = v
elsif v.is_a?(Hash)
(nested_parameter_attributes ||= {})[key] = v
else
_assign_attribute(key, v)
end
end
assign_nested_parameter_attributes(nested_parameter_attributes) if nested_parameter_attributes
assign_multiparameter_attributes(multi_parameter_attributes) if multi_parameter_attributes
end
# Assign any deferred nested attributes after the base attributes have been set.
def assign_nested_parameter_attributes(pairs)
pairs.each { |k, v| _assign_attribute(k, v) }
end
# Instantiates objects for all attribute classes that needs more than one constructor parameter. This is done
# by calling new on the column type or aggregation type (through composed_of) object with these parameters.
# So having the pairs written_on(1) = "2004", written_on(2) = "6", written_on(3) = "24", will instantiate
# written_on (a date type) with Date.new("2004", "6", "24"). You can also specify a typecast character in the
# parentheses to have the parameters typecasted before they're used in the constructor. Use i for Integer and
# f for Float. If all the values for a given attribute are empty, the attribute will be set to +nil+.
def assign_multiparameter_attributes(pairs)
execute_callstack_for_multiparameter_attributes(
extract_callstack_for_multiparameter_attributes(pairs)
)
end
def execute_callstack_for_multiparameter_attributes(callstack)
errors = []
callstack.each do |name, values_with_empty_parameters|
if values_with_empty_parameters.each_value.all?(NilClass)
values = nil
else
values = values_with_empty_parameters
end
send("#{name}=", values)
rescue => ex
errors << AttributeAssignmentError.new("error on assignment #{values_with_empty_parameters.values.inspect} to #{name} (#{ex.message})", ex, name)
end
unless errors.empty?
error_descriptions = errors.map(&:message).join(",")
raise MultiparameterAssignmentErrors.new(errors), "#{errors.size} error(s) on assignment of multiparameter attributes [#{error_descriptions}]"
end
end
def extract_callstack_for_multiparameter_attributes(pairs)
attributes = {}
pairs.each do |(multiparameter_name, value)|
attribute_name = multiparameter_name.split("(").first
attributes[attribute_name] ||= {}
parameter_value = value.empty? ? nil : type_cast_attribute_value(multiparameter_name, value)
attributes[attribute_name][find_parameter_position(multiparameter_name)] ||= parameter_value
end
attributes
end
def type_cast_attribute_value(multiparameter_name, value)
multiparameter_name =~ /\([0-9]*([if])\)/ ? value.send("to_" + $1) : value
end
def find_parameter_position(multiparameter_name)
multiparameter_name.scan(/\(([0-9]*).*\)/).first.first.to_i
end
end
end
# frozen_string_literal: true
require "mutex_m"
require "active_support/core_ext/enumerable"
module ActiveRecord
# = Active Record Attribute Methods
module AttributeMethods
extend ActiveSupport::Concern
include ActiveModel::AttributeMethods
included do
initialize_generated_modules
include Read
include Write
include BeforeTypeCast
include Query
include PrimaryKey
include TimeZoneConversion
include Dirty
include Serialization
end
RESTRICTED_CLASS_METHODS = %w(private public protected allocate new name parent superclass)
class GeneratedAttributeMethods < Module # :nodoc:
include Mutex_m
end
class << self
def dangerous_attribute_methods # :nodoc:
@dangerous_attribute_methods ||= (
Base.instance_methods +
Base.private_instance_methods -
Base.superclass.instance_methods -
Base.superclass.private_instance_methods
).map { |m| -m.to_s }.to_set.freeze
end
end
module ClassMethods
def inherited(child_class) # :nodoc:
child_class.initialize_generated_modules
super
end
def initialize_generated_modules # :nodoc:
@generated_attribute_methods = const_set(:GeneratedAttributeMethods, GeneratedAttributeMethods.new)
private_constant :GeneratedAttributeMethods
@attribute_methods_generated = false
include @generated_attribute_methods
super
end
# Generates all the attribute related methods for columns in the database
# accessors, mutators and query methods.
def define_attribute_methods # :nodoc:
return false if @attribute_methods_generated
# Use a mutex; we don't want two threads simultaneously trying to define
# attribute methods.
generated_attribute_methods.synchronize do
return false if @attribute_methods_generated
superclass.define_attribute_methods unless base_class?
super(attribute_names)
@attribute_methods_generated = true
end
end
def undefine_attribute_methods # :nodoc:
generated_attribute_methods.synchronize do
super if defined?(@attribute_methods_generated) && @attribute_methods_generated
@attribute_methods_generated = false
end
end
# Raises an ActiveRecord::DangerousAttributeError exception when an
# \Active \Record method is defined in the model, otherwise +false+.
#
# class Person < ActiveRecord::Base
# def save
# 'already defined by Active Record'
# end
# end
#
# Person.instance_method_already_implemented?(:save)
# # => ActiveRecord::DangerousAttributeError: save is defined by Active Record. Check to make sure that you don't have an attribute or method with the same name.
#
# Person.instance_method_already_implemented?(:name)
# # => false
def instance_method_already_implemented?(method_name)
if dangerous_attribute_method?(method_name)
raise DangerousAttributeError, "#{method_name} is defined by Active Record. Check to make sure that you don't have an attribute or method with the same name."
end
if superclass == Base
super
else
# If ThisClass < ... < SomeSuperClass < ... < Base and SomeSuperClass
# defines its own attribute method, then we don't want to override that.
defined = method_defined_within?(method_name, superclass, Base) &&
! superclass.instance_method(method_name).owner.is_a?(GeneratedAttributeMethods)
defined || super
end
end
# A method name is 'dangerous' if it is already (re)defined by Active Record, but
# not by any ancestors. (So 'puts' is not dangerous but 'save' is.)
def dangerous_attribute_method?(name) # :nodoc:
::ActiveRecord::AttributeMethods.dangerous_attribute_methods.include?(name.to_s)
end
def method_defined_within?(name, klass, superklass = klass.superclass) # :nodoc:
if klass.method_defined?(name) || klass.private_method_defined?(name)
if superklass.method_defined?(name) || superklass.private_method_defined?(name)
klass.instance_method(name).owner != superklass.instance_method(name).owner
else
true
end
else
false
end
end
# A class method is 'dangerous' if it is already (re)defined by Active Record, but
# not by any ancestors. (So 'puts' is not dangerous but 'new' is.)
def dangerous_class_method?(method_name)
return true if RESTRICTED_CLASS_METHODS.include?(method_name.to_s)
if Base.respond_to?(method_name, true)
if Object.respond_to?(method_name, true)
Base.method(method_name).owner != Object.method(method_name).owner
else
true
end
else
false
end
end
# Returns +true+ if +attribute+ is an attribute method and table exists,
# +false+ otherwise.
#
# class Person < ActiveRecord::Base
# end
#
# Person.attribute_method?('name') # => true
# Person.attribute_method?(:age=) # => true
# Person.attribute_method?(:nothing) # => false
def attribute_method?(attribute)
super || (table_exists? && column_names.include?(attribute.to_s.delete_suffix("=")))
end
# Returns an array of column names as strings if it's not an abstract class and
# table exists. Otherwise it returns an empty array.
#
# class Person < ActiveRecord::Base
# end
#
# Person.attribute_names
# # => ["id", "created_at", "updated_at", "name", "age"]
def attribute_names
@attribute_names ||= if !abstract_class? && table_exists?
attribute_types.keys
else
[]
end.freeze
end
# Returns true if the given attribute exists, otherwise false.
#
# class Person < ActiveRecord::Base
# alias_attribute :new_name, :name
# end
#
# Person.has_attribute?('name') # => true
# Person.has_attribute?('new_name') # => true
# Person.has_attribute?(:age) # => true
# Person.has_attribute?(:nothing) # => false
def has_attribute?(attr_name)
attr_name = attr_name.to_s
attr_name = attribute_aliases[attr_name] || attr_name
attribute_types.key?(attr_name)
end
def _has_attribute?(attr_name) # :nodoc:
attribute_types.key?(attr_name)
end
end
# A Person object with a name attribute can ask <tt>person.respond_to?(:name)</tt>,
# <tt>person.respond_to?(:name=)</tt>, and <tt>person.respond_to?(:name?)</tt>
# which will all return +true+. It also defines the attribute methods if they have
# not been generated.
#
# class Person < ActiveRecord::Base
# end
#
# person = Person.new
# person.respond_to?(:name) # => true
# person.respond_to?(:name=) # => true
# person.respond_to?(:name?) # => true
# person.respond_to?('age') # => true
# person.respond_to?('age=') # => true
# person.respond_to?('age?') # => true
# person.respond_to?(:nothing) # => false
def respond_to?(name, include_private = false)
return false unless super
# If the result is true then check for the select case.
# For queries selecting a subset of columns, return false for unselected columns.
# We check defined?(@attributes) not to issue warnings if called on objects that
# have been allocated but not yet initialized.
if defined?(@attributes)
if name = self.class.symbol_column_to_string(name.to_sym)
return _has_attribute?(name)
end
end
true
end
# Returns +true+ if the given attribute is in the attributes hash, otherwise +false+.
#
# class Person < ActiveRecord::Base
# alias_attribute :new_name, :name
# end
#
# person = Person.new
# person.has_attribute?(:name) # => true
# person.has_attribute?(:new_name) # => true
# person.has_attribute?('age') # => true
# person.has_attribute?(:nothing) # => false
def has_attribute?(attr_name)
attr_name = attr_name.to_s
attr_name = self.class.attribute_aliases[attr_name] || attr_name
@attributes.key?(attr_name)
end
def _has_attribute?(attr_name) # :nodoc:
@attributes.key?(attr_name)
end
# Returns an array of names for the attributes available on this object.
#
# class Person < ActiveRecord::Base
# end
#
# person = Person.new
# person.attribute_names
# # => ["id", "created_at", "updated_at", "name", "age"]
def attribute_names
@attributes.keys
end
# Returns a hash of all the attributes with their names as keys and the values of the attributes as values.
#
# class Person < ActiveRecord::Base
# end
#
# person = Person.create(name: 'Francesco', age: 22)
# person.attributes
# # => {"id"=>3, "created_at"=>Sun, 21 Oct 2012 04:53:04, "updated_at"=>Sun, 21 Oct 2012 04:53:04, "name"=>"Francesco", "age"=>22}
def attributes
@attributes.to_hash
end
# Returns an <tt>#inspect</tt>-like string for the value of the
# attribute +attr_name+. String attributes are truncated up to 50
# characters. Other attributes return the value of <tt>#inspect</tt>
# without modification.
#
# person = Person.create!(name: 'David Heinemeier Hansson ' * 3)
#
# person.attribute_for_inspect(:name)
# # => "\"David Heinemeier Hansson David Heinemeier Hansson ...\""
#
# person.attribute_for_inspect(:created_at)
# # => "\"2012-10-22 00:15:07.000000000 +0000\""
#
# person.attribute_for_inspect(:tag_ids)
# # => "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]"
def attribute_for_inspect(attr_name)
attr_name = attr_name.to_s
attr_name = self.class.attribute_aliases[attr_name] || attr_name
value = _read_attribute(attr_name)
format_for_inspect(attr_name, value)
end
# Returns +true+ if the specified +attribute+ has been set by the user or by a
# database load and is neither +nil+ nor <tt>empty?</tt> (the latter only applies
# to objects that respond to <tt>empty?</tt>, most notably Strings). Otherwise, +false+.
# Note that it always returns +true+ with boolean attributes.
#
# class Task < ActiveRecord::Base
# end
#
# task = Task.new(title: '', is_done: false)
# task.attribute_present?(:title) # => false
# task.attribute_present?(:is_done) # => true
# task.title = 'Buy milk'
# task.is_done = true
# task.attribute_present?(:title) # => true
# task.attribute_present?(:is_done) # => true
def attribute_present?(attr_name)
attr_name = attr_name.to_s
attr_name = self.class.attribute_aliases[attr_name] || attr_name
value = _read_attribute(attr_name)
!value.nil? && !(value.respond_to?(:empty?) && value.empty?)
end
# Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (for example,
# "2004-12-12" in a date column is cast to a date object, like Date.new(2004, 12, 12)). It raises
# <tt>ActiveModel::MissingAttributeError</tt> if the identified attribute is missing.
#
# Note: +:id+ is always present.
#
# class Person < ActiveRecord::Base
# belongs_to :organization
# end
#
# person = Person.new(name: 'Francesco', age: '22')
# person[:name] # => "Francesco"
# person[:age] # => 22
#
# person = Person.select('id').first
# person[:name] # => ActiveModel::MissingAttributeError: missing attribute: name
# person[:organization_id] # => ActiveModel::MissingAttributeError: missing attribute: organization_id
def [](attr_name)
read_attribute(attr_name) { |n| missing_attribute(n, caller) }
end
# Updates the attribute identified by <tt>attr_name</tt> with the specified +value+.
# (Alias for the protected #write_attribute method).
#
# class Person < ActiveRecord::Base
# end
#
# person = Person.new
# person[:age] = '22'
# person[:age] # => 22
# person[:age].class # => Integer
def []=(attr_name, value)
write_attribute(attr_name, value)
end
# Returns the name of all database fields which have been read from this
# model. This can be useful in development mode to determine which fields
# need to be selected. For performance critical pages, selecting only the
# required fields can be an easy performance win (assuming you aren't using
# all of the fields on the model).
#
# For example:
#
# class PostsController < ActionController::Base
# after_action :print_accessed_fields, only: :index
#
# def index
# @posts = Post.all
# end
#
# private
#
# def print_accessed_fields
# p @posts.first.accessed_fields
# end
# end
#
# Which allows you to quickly change your code to:
#
# class PostsController < ActionController::Base
# def index
# @posts = Post.select(:id, :title, :author_id, :updated_at)
# end
# end
def accessed_fields
@attributes.accessed
end
private
def attribute_method?(attr_name)
# We check defined? because Syck calls respond_to? before actually calling initialize.
defined?(@attributes) && @attributes.key?(attr_name)
end
def attributes_with_values(attribute_names)
attribute_names.index_with { |name| @attributes[name] }
end
# Filters the primary keys, readonly attributes and virtual columns from the attribute names.
def attributes_for_update(attribute_names)
attribute_names &= self.class.column_names
attribute_names.delete_if do |name|
self.class.readonly_attribute?(name) ||
column_for_attribute(name).virtual?
end
end
# Filters out the virtual columns and also primary keys, from the attribute names, when the primary
# key is to be generated (e.g. the id attribute has no value).
def attributes_for_create(attribute_names)
attribute_names &= self.class.column_names
attribute_names.delete_if do |name|
(pk_attribute?(name) && id.nil?) ||
column_for_attribute(name).virtual?
end
end
def format_for_inspect(name, value)
if value.nil?
value.inspect
else
inspected_value = if value.is_a?(String) && value.length > 50
"#{value[0, 50]}...".inspect
elsif value.is_a?(Date) || value.is_a?(Time)
%("#{value.to_fs(:inspect)}")
else
value.inspect
end
inspection_filter.filter_param(name, inspected_value)
end
end
def pk_attribute?(name)
name == @primary_key
end
end
end
# frozen_string_literal: true
require "cases/helper"
require "models/minimalistic"
require "models/developer"
require "models/auto_id"
require "models/boolean"
require "models/computer"
require "models/topic"
require "models/company"
require "models/category"
require "models/reply"
require "models/contact"
require "models/keyboard"
require "models/numeric_data"
class AttributeMethodsTest < ActiveRecord::TestCase
include InTimeZone
fixtures :topics, :developers, :companies, :computers
def setup
@old_matchers = ActiveRecord::Base.send(:attribute_method_patterns).dup
@target = Class.new(ActiveRecord::Base)
@target.table_name = "topics"
end
teardown do
ActiveRecord::Base.send(:attribute_method_patterns).clear
ActiveRecord::Base.send(:attribute_method_patterns).concat(@old_matchers)
end
test "attribute_for_inspect with a string" do
t = topics(:first)
t.title = "The First Topic Now Has A Title With\nNewlines And More Than 50 Characters"
assert_equal '"The First Topic Now Has A Title With\nNewlines And ..."', t.attribute_for_inspect(:title)
assert_equal '"The First Topic Now Has A Title With\nNewlines And ..."', t.attribute_for_inspect(:heading)
end
test "attribute_for_inspect with a date" do
t = topics(:first)
assert_equal %("#{t.written_on.to_fs(:inspect)}"), t.attribute_for_inspect(:written_on)
end
test "attribute_for_inspect with an array" do
t = topics(:first)
t.content = [Object.new]
assert_match %r(\[#<Object:0x[0-9a-f]+>\]), t.attribute_for_inspect(:content)
end
test "attribute_for_inspect with a long array" do
t = topics(:first)
t.content = (1..11).to_a
assert_equal "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]", t.attribute_for_inspect(:content)
end
test "attribute_for_inspect with a non-primary key id attribute" do
t = topics(:first).becomes(TitlePrimaryKeyTopic)
t.title = "The First Topic Now Has A Title With\nNewlines And More Than 50 Characters"
assert_equal "1", t.attribute_for_inspect(:id)
end
test "attribute_present" do
t = Topic.new
t.title = "hello there!"
t.written_on = Time.now
t.author_name = ""
assert t.attribute_present?("title")
assert t.attribute_present?("heading")
assert t.attribute_present?("written_on")
assert_not t.attribute_present?("content")
assert_not t.attribute_present?("author_name")
end
test "attribute_present with booleans" do
b1 = Boolean.new
b1.value = false
assert b1.attribute_present?(:value)
b2 = Boolean.new
b2.value = true
assert b2.attribute_present?(:value)
b3 = Boolean.new
assert_not b3.attribute_present?(:value)
b4 = Boolean.new
b4.value = false
b4.save!
assert Boolean.find(b4.id).attribute_present?(:value)
end
test "caching a nil primary key" do
klass = Class.new(Minimalistic)
assert_called(klass, :reset_primary_key, returns: nil) do
2.times { klass.primary_key }
end
end
test "attribute keys on a new instance" do
t = Topic.new
assert_nil t.title, "The topics table has a title column, so it should be nil"
assert_raise(NoMethodError) { t.title2 }
end
test "boolean attributes" do
assert_not_predicate Topic.find(1), :approved?
assert_predicate Topic.find(2), :approved?
end
test "set attributes" do
topic = Topic.find(1)
topic.attributes = { title: "Budget", author_name: "Jason" }
topic.save
assert_equal("Budget", topic.title)
assert_equal("Jason", topic.author_name)
assert_equal(topics(:first).author_email_address, Topic.find(1).author_email_address)
end
test "set attributes without a hash" do
topic = Topic.new
assert_raise(ArgumentError) { topic.attributes = "" }
end
test "integers as nil" do
test = AutoId.create(value: "")
assert_nil AutoId.find(test.id).value
end
test "set attributes with a block" do
topic = Topic.new do |t|
t.title = "Budget"
t.author_name = "Jason"
end
assert_equal("Budget", topic.title)
assert_equal("Jason", topic.author_name)
end
test "respond_to?" do
topic = Topic.find(1)
assert_respond_to topic, "title"
assert_respond_to topic, "title?"
assert_respond_to topic, "title="
assert_respond_to topic, :title
assert_respond_to topic, :title?
assert_respond_to topic, :title=
assert_respond_to topic, "author_name"
assert_respond_to topic, "attribute_names"
assert_not_respond_to topic, "nothingness"
assert_not_respond_to topic, :nothingness
end
test "respond_to? with a custom primary key" do
keyboard = Keyboard.create
assert_not_nil keyboard.key_number
assert_equal keyboard.key_number, keyboard.id
assert_respond_to keyboard, "key_number"
assert_respond_to keyboard, "id"
end
test "id_before_type_cast with a custom primary key" do
keyboard = Keyboard.create
keyboard.key_number = "10"
assert_equal "10", keyboard.id_before_type_cast
assert_nil keyboard.read_attribute_before_type_cast("id")
assert_equal "10", keyboard.read_attribute_before_type_cast("key_number")
assert_equal "10", keyboard.read_attribute_before_type_cast(:key_number)
end
# IRB inspects the return value of MyModel.allocate.
test "allocated objects can be inspected" do
topic = Topic.allocate
assert_equal "#<Topic not initialized>", topic.inspect
end
test "array content" do
content = %w( one two three )
topic = Topic.new
topic.content = content
topic.save
assert_equal content, Topic.find(topic.id).content
end
test "read attributes_before_type_cast" do
category = Category.new(name: "Test category", type: nil)
category_attrs = { "name" => "Test category", "id" => nil, "type" => nil, "categorizations_count" => nil }
assert_equal category_attrs, category.attributes_before_type_cast
end
if current_adapter?(:Mysql2Adapter)
test "read attributes_before_type_cast on a boolean" do
bool = Boolean.create!("value" => false)
assert_equal 0, bool.reload.attributes_before_type_cast["value"]
end
end
test "read attributes_before_type_cast on a datetime" do
in_time_zone "Pacific Time (US & Canada)" do
record = @target.new
record.written_on = "345643456"
assert_equal "345643456", record.written_on_before_type_cast
assert_nil record.written_on
record.written_on = "2009-10-11 12:13:14"
assert_equal "2009-10-11 12:13:14", record.written_on_before_type_cast
assert_equal Time.zone.parse("2009-10-11 12:13:14"), record.written_on
assert_equal ActiveSupport::TimeZone["Pacific Time (US & Canada)"], record.written_on.time_zone
end
end
test "read attributes_for_database" do
topic = Topic.new
topic.content = { one: 1, two: 2 }
db_attributes = Topic.instantiate(topic.attributes_for_database).attributes
before_type_cast_attributes = Topic.instantiate(topic.attributes_before_type_cast).attributes
assert_equal topic.attributes, db_attributes
assert_not_equal topic.attributes, before_type_cast_attributes
end
test "read attributes_after_type_cast on a date" do
tz = "Pacific Time (US & Canada)"
in_time_zone tz do
record = @target.new
date_string = "2011-03-24"
time = Time.zone.parse date_string
record.written_on = date_string
assert_equal date_string, record.written_on_before_type_cast
assert_equal time, record.written_on
assert_equal ActiveSupport::TimeZone[tz], record.written_on.time_zone
record.save
record.reload
assert_equal time, record.written_on
end
end
test "hash content" do
topic = Topic.new
topic.content = { "one" => 1, "two" => 2 }
topic.save
assert_equal 2, Topic.find(topic.id).content["two"]
topic.content_will_change!
topic.content["three"] = 3
topic.save
assert_equal 3, Topic.find(topic.id).content["three"]
end
test "update array content" do
topic = Topic.new
topic.content = %w( one two three )
topic.content.push "four"
assert_equal(%w( one two three four ), topic.content)
topic.save
topic = Topic.find(topic.id)
topic.content << "five"
assert_equal(%w( one two three four five ), topic.content)
end
test "case-sensitive attributes hash" do
expected = ["created_at", "developer", "extendedWarranty", "id", "system", "timezone", "updated_at"]
assert_equal expected, Computer.first.attributes.keys.sort
end
test "attributes without primary key" do
klass = Class.new(ActiveRecord::Base) do
self.table_name = "developers_projects"
end
assert_equal klass.column_names, klass.new.attributes.keys
assert_not klass.new.has_attribute?("id")
end
test "hashes are not mangled" do
new_topic = { title: "New Topic", content: { key: "First value" } }
new_topic_values = { title: "AnotherTopic", content: { key: "Second value" } }
topic = Topic.new(new_topic)
assert_equal new_topic[:title], topic.title
assert_equal new_topic[:content], topic.content
topic.attributes = new_topic_values
assert_equal new_topic_values[:title], topic.title
assert_equal new_topic_values[:content], topic.content
end
test "create through factory" do
topic = Topic.create(title: "New Topic")
topicReloaded = Topic.find(topic.id)
assert_equal(topic, topicReloaded)
end
test "write_attribute" do
topic = Topic.new
topic.write_attribute :title, "Still another topic"
assert_equal "Still another topic", topic.title
topic[:title] = "Still another topic: part 2"
assert_equal "Still another topic: part 2", topic.title
topic.write_attribute "title", "Still another topic: part 3"
assert_equal "Still another topic: part 3", topic.title
topic["title"] = "Still another topic: part 4"
assert_equal "Still another topic: part 4", topic.title
end
test "write_attribute can write aliased attributes as well" do
topic = Topic.new(title: "Don't change the topic")
topic.write_attribute :heading, "New topic"
assert_equal "New topic", topic.title
end
test "write_attribute raises ActiveModel::MissingAttributeError when the attribute does not exist" do
topic = Topic.first
assert_raises(ActiveModel::MissingAttributeError) { topic.update_columns(no_column_exists: "Hello!") }
assert_raises(ActiveModel::UnknownAttributeError) { topic.update(no_column_exists: "Hello!") }
end
test "write_attribute allows writing to aliased attributes" do
topic = Topic.first
assert_nothing_raised { topic.update_columns(heading: "Hello!") }
assert_nothing_raised { topic.update(heading: "Hello!") }
end
test "read_attribute" do
topic = Topic.new
topic.title = "Don't change the topic"
assert_equal "Don't change the topic", topic.read_attribute("title")
assert_equal "Don't change the topic", topic["title"]
assert_equal "Don't change the topic", topic.read_attribute(:title)
assert_equal "Don't change the topic", topic[:title]
end
test "read_attribute can read aliased attributes as well" do
topic = Topic.new(title: "Don't change the topic")
assert_equal "Don't change the topic", topic.read_attribute("heading")
assert_equal "Don't change the topic", topic["heading"]
assert_equal "Don't change the topic", topic.read_attribute(:heading)
assert_equal "Don't change the topic", topic[:heading]
end
test "read_attribute raises ActiveModel::MissingAttributeError when the attribute does not exist" do
computer = Computer.select("id").first
assert_raises(ActiveModel::MissingAttributeError) { computer[:developer] }
assert_raises(ActiveModel::MissingAttributeError) { computer[:extendedWarranty] }
assert_raises(ActiveModel::MissingAttributeError) { computer[:no_column_exists] = "Hello!" }
assert_nothing_raised { computer[:developer] = "Hello!" }
end
test "read_attribute when false" do
topic = topics(:first)
topic.approved = false
assert_not topic.approved?, "approved should be false"
topic.approved = "false"
assert_not topic.approved?, "approved should be false"
end
test "read_attribute when true" do
topic = topics(:first)
topic.approved = true
assert topic.approved?, "approved should be true"
topic.approved = "true"
assert topic.approved?, "approved should be true"
end
test "boolean attributes writing and reading" do
topic = Topic.new
topic.approved = "false"
assert_not topic.approved?, "approved should be false"
topic.approved = "false"
assert_not topic.approved?, "approved should be false"
topic.approved = "true"
assert topic.approved?, "approved should be true"
topic.approved = "true"
assert topic.approved?, "approved should be true"
end
test "overridden write_attribute" do
topic = Topic.new
def topic.write_attribute(attr_name, value)
super(attr_name, value.downcase)
end
topic.write_attribute :title, "Yet another topic"
assert_equal "yet another topic", topic.title
topic[:title] = "Yet another topic: part 2"
assert_equal "yet another topic: part 2", topic.title
topic.write_attribute "title", "Yet another topic: part 3"
assert_equal "yet another topic: part 3", topic.title
topic["title"] = "Yet another topic: part 4"
assert_equal "yet another topic: part 4", topic.title
end
test "overridden read_attribute" do
topic = Topic.new
topic.title = "Stop changing the topic"
def topic.read_attribute(attr_name)
super(attr_name).upcase
end
assert_equal "STOP CHANGING THE TOPIC", topic.read_attribute("title")
assert_equal "STOP CHANGING THE TOPIC", topic["title"]
assert_equal "STOP CHANGING THE TOPIC", topic.read_attribute(:title)
assert_equal "STOP CHANGING THE TOPIC", topic[:title]
end
test "read overridden attribute" do
topic = Topic.new(title: "a")
def topic.title() "b" end
assert_equal "a", topic[:title]
end
test "read overridden attribute with predicate respects override" do
topic = Topic.new
topic.approved = true
def topic.approved; false; end
assert_not topic.approved?, "overridden approved should be false"
end
test "string attribute predicate" do
[nil, "", " "].each do |value|
assert_equal false, Topic.new(author_name: value).author_name?
end
assert_equal true, Topic.new(author_name: "Name").author_name?
ActiveModel::Type::Boolean::FALSE_VALUES.each do |value|
assert_predicate Topic.new(author_name: value), :author_name?
end
end
test "number attribute predicate" do
[nil, 0, "0"].each do |value|
assert_equal false, Developer.new(salary: value).salary?
end
assert_equal true, Developer.new(salary: 1).salary?
assert_equal true, Developer.new(salary: "1").salary?
end
test "boolean attribute predicate" do
[nil, "", false, "false", "f", 0].each do |value|
assert_equal false, Topic.new(approved: value).approved?
end
[true, "true", "1", 1].each do |value|
assert_equal true, Topic.new(approved: value).approved?
end
end
test "user-defined text attribute predicate" do
klass = Class.new(ActiveRecord::Base) do
self.table_name = Topic.table_name
attribute :user_defined_text, :text
end
topic = klass.new(user_defined_text: "text")
assert_predicate topic, :user_defined_text?
ActiveModel::Type::Boolean::FALSE_VALUES.each do |value|
topic = klass.new(user_defined_text: value)
assert_predicate topic, :user_defined_text?
end
end
test "user-defined date attribute predicate" do
klass = Class.new(ActiveRecord::Base) do
self.table_name = Topic.table_name
attribute :user_defined_date, :date
end
topic = klass.new(user_defined_date: Date.current)
assert_predicate topic, :user_defined_date?
end
test "user-defined datetime attribute predicate" do
klass = Class.new(ActiveRecord::Base) do
self.table_name = Topic.table_name
attribute :user_defined_datetime, :datetime
end
topic = klass.new(user_defined_datetime: Time.current)
assert_predicate topic, :user_defined_datetime?
end
test "user-defined time attribute predicate" do
klass = Class.new(ActiveRecord::Base) do
self.table_name = Topic.table_name
attribute :user_defined_time, :time
end
topic = klass.new(user_defined_time: Time.current)
assert_predicate topic, :user_defined_time?
end
test "user-defined json attribute predicate" do
klass = Class.new(ActiveRecord::Base) do
self.table_name = Topic.table_name
attribute :user_defined_json, :json
end
topic = klass.new(user_defined_json: { key: "value" })
assert_predicate topic, :user_defined_json?
topic = klass.new(user_defined_json: {})
assert_not_predicate topic, :user_defined_json?
end
test "custom field attribute predicate" do
object = Company.find_by_sql(<<~SQL).first
SELECT c1.*, c2.type as string_value, c2.rating as int_value
FROM companies c1, companies c2
WHERE c1.firm_id = c2.id
AND c1.id = 2
SQL
assert_equal "Firm", object.string_value
assert_predicate object, :string_value?
object.string_value = " "
assert_not_predicate object, :string_value?
assert_equal 1, object.int_value.to_i
assert_predicate object, :int_value?
object.int_value = "0"
assert_not_predicate object, :int_value?
end
test "non-attribute read and write" do
topic = Topic.new
assert_not_respond_to topic, "mumbo"
assert_raise(NoMethodError) { topic.mumbo }
assert_raise(NoMethodError) { topic.mumbo = 5 }
end
test "undeclared attribute method does not affect respond_to? and method_missing" do
topic = @target.new(title: "Budget")
assert_respond_to topic, "title"
assert_equal "Budget", topic.title
assert_not_respond_to topic, "title_hello_world"
assert_raise(NoMethodError) { topic.title_hello_world }
end
test "declared prefixed attribute method affects respond_to? and method_missing" do
topic = @target.new(title: "Budget")
%w(default_ title_).each do |prefix|
@target.class_eval "def #{prefix}attribute(*args) args end"
@target.attribute_method_prefix prefix
meth = "#{prefix}title"
assert_respond_to topic, meth
assert_equal ["title"], topic.public_send(meth)
assert_equal ["title", "a"], topic.public_send(meth, "a")
assert_equal ["title", 1, 2, 3], topic.public_send(meth, 1, 2, 3)
end
end
test "declared suffixed attribute method affects respond_to? and method_missing" do
%w(_default _title_default _it! _candidate= able?).each do |suffix|
@target.class_eval "def attribute#{suffix}(*args) args end"
@target.attribute_method_suffix suffix
topic = @target.new(title: "Budget")
meth = "title#{suffix}"
assert_respond_to topic, meth
assert_equal ["title"], topic.public_send(meth)
assert_equal ["title", "a"], topic.public_send(meth, "a")
assert_equal ["title", 1, 2, 3], topic.public_send(meth, 1, 2, 3)
end
end
test "declared affixed attribute method affects respond_to? and method_missing" do
[["mark_", "_for_update"], ["reset_", "!"], ["default_", "_value?"]].each do |prefix, suffix|
@target.class_eval "def #{prefix}attribute#{suffix}(*args) args end"
@target.attribute_method_affix(prefix: prefix, suffix: suffix)
topic = @target.new(title: "Budget")
meth = "#{prefix}title#{suffix}"
assert_respond_to topic, meth
assert_equal ["title"], topic.public_send(meth)
assert_equal ["title", "a"], topic.public_send(meth, "a")
assert_equal ["title", 1, 2, 3], topic.public_send(meth, 1, 2, 3)
end
end
test "should unserialize attributes for frozen records" do
myobj = { value1: :value2 }
topic = Topic.create(content: myobj)
topic.freeze
assert_equal myobj, topic.content
end
test "typecast attribute from select to false" do
Topic.create(title: "Budget")
# Oracle does not support boolean expressions in SELECT.
if current_adapter?(:OracleAdapter)
topic = Topic.all.merge!(select: "topics.*, 0 as is_test").first
else
topic = Topic.all.merge!(select: "topics.*, 1=2 as is_test").first
end
assert_not_predicate topic, :is_test?
end
test "typecast attribute from select to true" do
Topic.create(title: "Budget")
# Oracle does not support boolean expressions in SELECT.
if current_adapter?(:OracleAdapter)
topic = Topic.all.merge!(select: "topics.*, 1 as is_test").first
else
topic = Topic.all.merge!(select: "topics.*, 2=2 as is_test").first
end
assert_predicate topic, :is_test?
end
test "raises ActiveRecord::DangerousAttributeError when defining an AR method in a model" do
%w(save create_or_update).each do |method|
klass = Class.new(ActiveRecord::Base)
klass.class_eval "def #{method}() 'defined #{method}' end"
assert_raise ActiveRecord::DangerousAttributeError do
klass.instance_method_already_implemented?(method)
end
end
end
test "converted values are returned after assignment" do
developer = Developer.new(name: 1337, salary: "50000")
assert_equal "50000", developer.salary_before_type_cast
assert_equal 1337, developer.name_before_type_cast
assert_equal 50000, developer.salary
assert_equal "1337", developer.name
developer.save!
assert_equal 50000, developer.salary
assert_equal "1337", developer.name
end
test "write nil to time attribute" do
in_time_zone "Pacific Time (US & Canada)" do
record = @target.new
record.written_on = nil
assert_nil record.written_on
end
end
test "write time to date attribute" do
in_time_zone "Pacific Time (US & Canada)" do
record = @target.new
record.last_read = Time.utc(2010, 1, 1, 10)
assert_equal Date.civil(2010, 1, 1), record.last_read
end
end
test "time attributes are retrieved in the current time zone" do
in_time_zone "Pacific Time (US & Canada)" do
utc_time = Time.utc(2008, 1, 1)
record = @target.new
record[:written_on] = utc_time
assert_equal utc_time, record.written_on # record.written on is equal to (i.e., simultaneous with) utc_time
assert_kind_of ActiveSupport::TimeWithZone, record.written_on # but is a TimeWithZone
assert_equal ActiveSupport::TimeZone["Pacific Time (US & Canada)"], record.written_on.time_zone # and is in the current Time.zone
assert_equal Time.utc(2007, 12, 31, 16), record.written_on.time # and represents time values adjusted accordingly
end
end
test "setting a time zone-aware attribute to UTC" do
in_time_zone "Pacific Time (US & Canada)" do
utc_time = Time.utc(2008, 1, 1)
record = @target.new
record.written_on = utc_time
assert_equal utc_time, record.written_on
assert_equal ActiveSupport::TimeZone["Pacific Time (US & Canada)"], record.written_on.time_zone
assert_equal Time.utc(2007, 12, 31, 16), record.written_on.time
end
end
test "setting time zone-aware attribute in other time zone" do
utc_time = Time.utc(2008, 1, 1)
cst_time = utc_time.in_time_zone("Central Time (US & Canada)")
in_time_zone "Pacific Time (US & Canada)" do
record = @target.new
record.written_on = cst_time
assert_equal utc_time, record.written_on
assert_equal ActiveSupport::TimeZone["Pacific Time (US & Canada)"], record.written_on.time_zone
assert_equal Time.utc(2007, 12, 31, 16), record.written_on.time
end
end
test "setting time zone-aware read attribute" do
utc_time = Time.utc(2008, 1, 1)
cst_time = utc_time.in_time_zone("Central Time (US & Canada)")
in_time_zone "Pacific Time (US & Canada)" do
record = @target.create(written_on: cst_time).reload
assert_equal utc_time, record[:written_on]
assert_equal ActiveSupport::TimeZone["Pacific Time (US & Canada)"], record[:written_on].time_zone
assert_equal Time.utc(2007, 12, 31, 16), record[:written_on].time
end
end
test "setting time zone-aware attribute with a string" do
utc_time = Time.utc(2008, 1, 1)
(-11..13).each do |timezone_offset|
time_string = utc_time.in_time_zone(timezone_offset).to_s
in_time_zone "Pacific Time (US & Canada)" do
record = @target.new
record.written_on = time_string
assert_equal Time.zone.parse(time_string), record.written_on
assert_equal ActiveSupport::TimeZone["Pacific Time (US & Canada)"], record.written_on.time_zone
assert_equal Time.utc(2007, 12, 31, 16), record.written_on.time
end
end
end
test "time zone-aware attribute saved" do
in_time_zone 1 do
record = @target.create(written_on: "2012-02-20 10:00")
record.written_on = "2012-02-20 09:00"
record.save
assert_equal Time.zone.local(2012, 02, 20, 9), record.reload.written_on
end
end
test "setting a time zone-aware attribute to a blank string returns nil" do
in_time_zone "Pacific Time (US & Canada)" do
record = @target.new
record.written_on = " "
assert_nil record.written_on
assert_nil record[:written_on]
end
end
test "setting a time zone-aware attribute interprets time zone-unaware string in time zone" do
time_string = "Tue Jan 01 00:00:00 2008"
(-11..13).each do |timezone_offset|
in_time_zone timezone_offset do
record = @target.new
record.written_on = time_string
assert_equal Time.zone.parse(time_string), record.written_on
assert_equal ActiveSupport::TimeZone[timezone_offset], record.written_on.time_zone
assert_equal Time.utc(2008, 1, 1), record.written_on.time
end
end
end
test "setting a time zone-aware datetime in the current time zone" do
utc_time = Time.utc(2008, 1, 1)
in_time_zone "Pacific Time (US & Canada)" do
record = @target.new
record.written_on = utc_time.in_time_zone
assert_equal utc_time, record.written_on
assert_equal ActiveSupport::TimeZone["Pacific Time (US & Canada)"], record.written_on.time_zone
assert_equal Time.utc(2007, 12, 31, 16), record.written_on.time
end
end
test "YAML dumping a record with time zone-aware attribute" do
in_time_zone "Pacific Time (US & Canada)" do
record = Topic.new(id: 1)
record.written_on = "Jan 01 00:00:00 2014"
payload = YAML.dump(record)
assert_equal record, YAML.respond_to?(:unsafe_load) ? YAML.unsafe_load(payload) : YAML.load(payload)
end
ensure
# NOTE: Reset column info because global topics
# don't have tz-aware attributes by default.
Topic.reset_column_information
end
test "setting a time zone-aware time in the current time zone" do
in_time_zone "Pacific Time (US & Canada)" do
record = @target.new
time_string = "10:00:00"
expected_time = Time.zone.parse("2000-01-01 #{time_string}")
record.bonus_time = time_string
assert_equal expected_time, record.bonus_time
assert_equal ActiveSupport::TimeZone["Pacific Time (US & Canada)"], record.bonus_time.time_zone
record.bonus_time = ""
assert_nil record.bonus_time
end
end
test "setting a time zone-aware time with DST" do
in_time_zone "Pacific Time (US & Canada)" do
current_time = Time.zone.local(2014, 06, 15, 10)
record = @target.new(bonus_time: current_time)
time_before_save = record.bonus_time
record.save
record.reload
assert_equal time_before_save, record.bonus_time
assert_equal ActiveSupport::TimeZone["Pacific Time (US & Canada)"], record.bonus_time.time_zone
end
end
test "setting invalid string to a zone-aware time attribute" do
in_time_zone "Pacific Time (US & Canada)" do
record = @target.new
time_string = "ABC"
record.bonus_time = time_string
assert_nil record.bonus_time
end
end
test "removing time zone-aware types" do
with_time_zone_aware_types(:datetime) do
in_time_zone "Pacific Time (US & Canada)" do
record = @target.new(bonus_time: "10:00:00")
expected_time = Time.utc(2000, 01, 01, 10)
assert_equal expected_time, record.bonus_time
assert_predicate record.bonus_time, :utc?
end
end
end
test "time zone-aware attributes do not recurse infinitely on invalid values" do
in_time_zone "Pacific Time (US & Canada)" do
record = @target.new(bonus_time: [])
assert_nil record.bonus_time
end
end
test "setting a time_zone_conversion_for_attributes should write the value on a class variable" do
Topic.skip_time_zone_conversion_for_attributes = [:field_a]
Minimalistic.skip_time_zone_conversion_for_attributes = [:field_b]
assert_equal [:field_a], Topic.skip_time_zone_conversion_for_attributes
assert_equal [:field_b], Minimalistic.skip_time_zone_conversion_for_attributes
end
test "attribute readers respect access control" do
privatize("title")
topic = @target.new(title: "The pros and cons of programming naked.")
assert_not_respond_to topic, :title
exception = assert_raise(NoMethodError) { topic.title }
assert_includes exception.message, "private method"
assert_equal "I'm private", topic.send(:title)
end
test "attribute writers respect access control" do
privatize("title=(value)")
topic = @target.new
assert_not_respond_to topic, :title=
exception = assert_raise(NoMethodError) { topic.title = "Pants" }
assert_includes exception.message, "private method"
topic.send(:title=, "Very large pants")
end
test "attribute predicates respect access control" do
privatize("title?")
topic = @target.new(title: "Isaac Newton's pants")
assert_not_respond_to topic, :title?
exception = assert_raise(NoMethodError) { topic.title? }
assert_includes exception.message, "private method"
assert topic.send(:title?)
end
test "bulk updates respect access control" do
privatize("title=(value)")
assert_raise(ActiveRecord::UnknownAttributeError) { @target.new(title: "Rants about pants") }
assert_raise(ActiveRecord::UnknownAttributeError) { @target.new.attributes = { title: "Ants in pants" } }
end
test "bulk update raises ActiveRecord::UnknownAttributeError" do
error = assert_raises(ActiveRecord::UnknownAttributeError) {
Topic.new(hello: "world")
}
assert_instance_of Topic, error.record
assert_equal "hello", error.attribute
assert_match "unknown attribute 'hello' for Topic.", error.message
end
test "method overrides in multi-level subclasses" do
klass = Class.new(Developer) do
def name
"dev:#{read_attribute(:name)}"
end
end
2.times { klass = Class.new(klass) }
dev = klass.new(name: "arthurnn")
dev.save!
assert_equal "dev:arthurnn", dev.reload.name
end
test "global methods are overwritten" do
klass = Class.new(ActiveRecord::Base) do
self.table_name = "computers"
end
assert_not klass.instance_method_already_implemented?(:system)
computer = klass.new
assert_nil computer.system
end
test "global methods are overwritten when subclassing" do
klass = Class.new(ActiveRecord::Base) do
self.abstract_class = true
end
subklass = Class.new(klass) do
self.table_name = "computers"
end
assert_not klass.instance_method_already_implemented?(:system)
assert_not subklass.instance_method_already_implemented?(:system)
computer = subklass.new
assert_nil computer.system
end
test "instance methods should be defined on the base class" do
subklass = Class.new(Topic)
Topic.define_attribute_methods
instance = subklass.new
instance.id = 5
assert_equal 5, instance.id
assert subklass.method_defined?(:id), "subklass is missing id method"
Topic.undefine_attribute_methods
assert_equal 5, instance.id
assert subklass.method_defined?(:id), "subklass is missing id method"
end
test "define_attribute_method works with both symbol and string" do
klass = Class.new(ActiveRecord::Base)
klass.table_name = "foo"
assert_nothing_raised { klass.define_attribute_method(:foo) }
assert_nothing_raised { klass.define_attribute_method("bar") }
end
test "read_attribute with nil should not asplode" do
assert_nil Topic.new.read_attribute(nil)
end
# If B < A, and A defines an accessor for 'foo', we don't want to override
# that by defining a 'foo' method in the generated methods module for B.
# (That module will be inserted between the two, e.g. [B, <GeneratedAttributes>, A].)
test "inherited custom accessors" do
klass = new_topic_like_ar_class do
self.abstract_class = true
def title; "omg"; end
def title=(val); self.author_name = val; end
end
subklass = Class.new(klass)
[klass, subklass].each(&:define_attribute_methods)
topic = subklass.find(1)
assert_equal "omg", topic.title
topic.title = "lol"
assert_equal "lol", topic.author_name
end
test "inherited custom accessors with reserved names" do
klass = Class.new(ActiveRecord::Base) do
self.table_name = "computers"
self.abstract_class = true
def system; "omg"; end
def system=(val); self.developer = val; end
end
subklass = Class.new(klass)
[klass, subklass].each(&:define_attribute_methods)
computer = subklass.find(1)
assert_equal "omg", computer.system
computer.developer = 99
assert_equal 99, computer.developer
end
test "on_the_fly_super_invokable_generated_attribute_methods_via_method_missing" do
klass = new_topic_like_ar_class do
def title
super + "!"
end
end
real_topic = topics(:first)
assert_equal real_topic.title + "!", klass.find(real_topic.id).title
end
test "on-the-fly super-invokable generated attribute predicates via method_missing" do
klass = new_topic_like_ar_class do
def title?
!super
end
end
real_topic = topics(:first)
assert_equal !real_topic.title?, klass.find(real_topic.id).title?
end
test "calling super when the parent does not define method raises NoMethodError" do
klass = new_topic_like_ar_class do
def some_method_that_is_not_on_super
super
end
end
assert_raise(NoMethodError) do
klass.new.some_method_that_is_not_on_super
end
end
test "attribute_method?" do
assert @target.attribute_method?(:title)
assert @target.attribute_method?(:title=)
assert_not @target.attribute_method?(:wibble)
end
test "attribute_method? returns false if the table does not exist" do
@target.table_name = "wibble"
assert_not @target.attribute_method?(:title)
end
test "attribute_names on a new record" do
model = @target.new
assert_equal @target.column_names, model.attribute_names
end
test "attribute_names on a queried record" do
model = @target.last!
assert_equal @target.column_names, model.attribute_names
end
test "attribute_names with a custom select" do
model = @target.select("id").last!
assert_equal ["id"], model.attribute_names
# Ensure other columns exist.
assert_not_equal ["id"], @target.column_names
end
test "came_from_user?" do
model = @target.first
assert_not_predicate model, :id_came_from_user?
model.id = "omg"
assert_predicate model, :id_came_from_user?
end
test "accessed_fields" do
model = @target.first
assert_equal [], model.accessed_fields
model.title
assert_equal ["title"], model.accessed_fields
end
test "generated attribute methods ancestors have correct module" do
mod = Topic.send(:generated_attribute_methods)
assert_equal "Topic::GeneratedAttributeMethods", mod.inspect
end
test "read_attribute_before_type_cast with aliased attribute" do
model = NumericData.new(new_bank_balance: "abcd")
assert_equal "abcd", model.read_attribute_before_type_cast("new_bank_balance")
end
private
def new_topic_like_ar_class(&block)
klass = Class.new(ActiveRecord::Base) do
self.table_name = "topics"
class_eval(&block)
end
assert_empty klass.send(:generated_attribute_methods).instance_methods(false)
klass
end
def with_time_zone_aware_types(*types)
old_types = ActiveRecord::Base.time_zone_aware_types
ActiveRecord::Base.time_zone_aware_types = types
yield
ensure
ActiveRecord::Base.time_zone_aware_types = old_types
end
def privatize(method_signature)
@target.class_eval(<<-private_method, __FILE__, __LINE__ + 1)
private
def #{method_signature}
"I'm private"
end
private_method
end
end
# frozen_string_literal: true
require_relative "../helper"
require "ostruct"
module Arel
module Attributes
class AttributeTest < Arel::Spec
describe "#not_eq" do
it "should create a NotEqual node" do
relation = Table.new(:users)
_(relation[:id].not_eq(10)).must_be_kind_of Nodes::NotEqual
end
it "should generate != in sql" do
relation = Table.new(:users)
mgr = relation.project relation[:id]
mgr.where relation[:id].not_eq(10)
_(mgr.to_sql).must_be_like %{
SELECT "users"."id" FROM "users" WHERE "users"."id" != 10
}
end
it "should handle nil" do
relation = Table.new(:users)
mgr = relation.project relation[:id]
mgr.where relation[:id].not_eq(nil)
_(mgr.to_sql).must_be_like %{
SELECT "users"."id" FROM "users" WHERE "users"."id" IS NOT NULL
}
end
end
describe "#not_eq_any" do
it "should create a Grouping node" do
relation = Table.new(:users)
_(relation[:id].not_eq_any([1, 2])).must_be_kind_of Nodes::Grouping
end
it "should generate ORs in sql" do
relation = Table.new(:users)
mgr = relation.project relation[:id]
mgr.where relation[:id].not_eq_any([1, 2])
_(mgr.to_sql).must_be_like %{
SELECT "users"."id" FROM "users" WHERE ("users"."id" != 1 OR "users"."id" != 2)
}
end
end
describe "#not_eq_all" do
it "should create a Grouping node" do
relation = Table.new(:users)
_(relation[:id].not_eq_all([1, 2])).must_be_kind_of Nodes::Grouping
end
it "should generate ANDs in sql" do
relation = Table.new(:users)
mgr = relation.project relation[:id]
mgr.where relation[:id].not_eq_all([1, 2])
_(mgr.to_sql).must_be_like %{
SELECT "users"."id" FROM "users" WHERE ("users"."id" != 1 AND "users"."id" != 2)
}
end
end
describe "#gt" do
it "should create a GreaterThan node" do
relation = Table.new(:users)
_(relation[:id].gt(10)).must_be_kind_of Nodes::GreaterThan
end
it "should generate > in sql" do
relation = Table.new(:users)
mgr = relation.project relation[:id]
mgr.where relation[:id].gt(10)
_(mgr.to_sql).must_be_like %{
SELECT "users"."id" FROM "users" WHERE "users"."id" > 10
}
end
it "should handle comparing with a subquery" do
users = Table.new(:users)
avg = users.project(users[:karma].average)
mgr = users.project(Arel.star).where(users[:karma].gt(avg))
_(mgr.to_sql).must_be_like %{
SELECT * FROM "users" WHERE "users"."karma" > (SELECT AVG("users"."karma") FROM "users")
}
end
it "should accept various data types." do
relation = Table.new(:users)
mgr = relation.project relation[:id]
mgr.where relation[:name].gt("fake_name")
_(mgr.to_sql).must_match %{"users"."name" > 'fake_name'}
current_time = ::Time.now
mgr.where relation[:created_at].gt(current_time)
_(mgr.to_sql).must_match %{"users"."created_at" > '#{current_time}'}
end
end
describe "#gt_any" do
it "should create a Grouping node" do
relation = Table.new(:users)
_(relation[:id].gt_any([1, 2])).must_be_kind_of Nodes::Grouping
end
it "should generate ORs in sql" do
relation = Table.new(:users)
mgr = relation.project relation[:id]
mgr.where relation[:id].gt_any([1, 2])
_(mgr.to_sql).must_be_like %{
SELECT "users"."id" FROM "users" WHERE ("users"."id" > 1 OR "users"."id" > 2)
}
end
end
describe "#gt_all" do
it "should create a Grouping node" do
relation = Table.new(:users)
_(relation[:id].gt_all([1, 2])).must_be_kind_of Nodes::Grouping
end
it "should generate ANDs in sql" do
relation = Table.new(:users)
mgr = relation.project relation[:id]
mgr.where relation[:id].gt_all([1, 2])
_(mgr.to_sql).must_be_like %{
SELECT "users"."id" FROM "users" WHERE ("users"."id" > 1 AND "users"."id" > 2)
}
end
end
describe "#gteq" do
it "should create a GreaterThanOrEqual node" do
relation = Table.new(:users)
_(relation[:id].gteq(10)).must_be_kind_of Nodes::GreaterThanOrEqual
end
it "should generate >= in sql" do
relation = Table.new(:users)
mgr = relation.project relation[:id]
mgr.where relation[:id].gteq(10)
_(mgr.to_sql).must_be_like %{
SELECT "users"."id" FROM "users" WHERE "users"."id" >= 10
}
end
it "should accept various data types." do
relation = Table.new(:users)
mgr = relation.project relation[:id]
mgr.where relation[:name].gteq("fake_name")
_(mgr.to_sql).must_match %{"users"."name" >= 'fake_name'}
current_time = ::Time.now
mgr.where relation[:created_at].gteq(current_time)
_(mgr.to_sql).must_match %{"users"."created_at" >= '#{current_time}'}
end
end
describe "#gteq_any" do
it "should create a Grouping node" do
relation = Table.new(:users)
_(relation[:id].gteq_any([1, 2])).must_be_kind_of Nodes::Grouping
end
it "should generate ORs in sql" do
relation = Table.new(:users)
mgr = relation.project relation[:id]
mgr.where relation[:id].gteq_any([1, 2])
_(mgr.to_sql).must_be_like %{
SELECT "users"."id" FROM "users" WHERE ("users"."id" >= 1 OR "users"."id" >= 2)
}
end
end
describe "#gteq_all" do
it "should create a Grouping node" do
relation = Table.new(:users)
_(relation[:id].gteq_all([1, 2])).must_be_kind_of Nodes::Grouping
end
it "should generate ANDs in sql" do
relation = Table.new(:users)
mgr = relation.project relation[:id]
mgr.where relation[:id].gteq_all([1, 2])
_(mgr.to_sql).must_be_like %{
SELECT "users"."id" FROM "users" WHERE ("users"."id" >= 1 AND "users"."id" >= 2)
}
end
end
describe "#lt" do
it "should create a LessThan node" do
relation = Table.new(:users)
_(relation[:id].lt(10)).must_be_kind_of Nodes::LessThan
end
it "should generate < in sql" do
relation = Table.new(:users)
mgr = relation.project relation[:id]
mgr.where relation[:id].lt(10)
_(mgr.to_sql).must_be_like %{
SELECT "users"."id" FROM "users" WHERE "users"."id" < 10
}
end
it "should accept various data types." do
relation = Table.new(:users)
mgr = relation.project relation[:id]
mgr.where relation[:name].lt("fake_name")
_(mgr.to_sql).must_match %{"users"."name" < 'fake_name'}
current_time = ::Time.now
mgr.where relation[:created_at].lt(current_time)
_(mgr.to_sql).must_match %{"users"."created_at" < '#{current_time}'}
end
end
describe "#lt_any" do
it "should create a Grouping node" do
relation = Table.new(:users)
_(relation[:id].lt_any([1, 2])).must_be_kind_of Nodes::Grouping
end
it "should generate ORs in sql" do
relation = Table.new(:users)
mgr = relation.project relation[:id]
mgr.where relation[:id].lt_any([1, 2])
_(mgr.to_sql).must_be_like %{
SELECT "users"."id" FROM "users" WHERE ("users"."id" < 1 OR "users"."id" < 2)
}
end
end
describe "#lt_all" do
it "should create a Grouping node" do
relation = Table.new(:users)
_(relation[:id].lt_all([1, 2])).must_be_kind_of Nodes::Grouping
end
it "should generate ANDs in sql" do
relation = Table.new(:users)
mgr = relation.project relation[:id]
mgr.where relation[:id].lt_all([1, 2])
_(mgr.to_sql).must_be_like %{
SELECT "users"."id" FROM "users" WHERE ("users"."id" < 1 AND "users"."id" < 2)
}
end
end
describe "#lteq" do
it "should create a LessThanOrEqual node" do
relation = Table.new(:users)
_(relation[:id].lteq(10)).must_be_kind_of Nodes::LessThanOrEqual
end
it "should generate <= in sql" do
relation = Table.new(:users)
mgr = relation.project relation[:id]
mgr.where relation[:id].lteq(10)
_(mgr.to_sql).must_be_like %{
SELECT "users"."id" FROM "users" WHERE "users"."id" <= 10
}
end
it "should accept various data types." do
relation = Table.new(:users)
mgr = relation.project relation[:id]
mgr.where relation[:name].lteq("fake_name")
_(mgr.to_sql).must_match %{"users"."name" <= 'fake_name'}
current_time = ::Time.now
mgr.where relation[:created_at].lteq(current_time)
_(mgr.to_sql).must_match %{"users"."created_at" <= '#{current_time}'}
end
end
describe "#lteq_any" do
it "should create a Grouping node" do
relation = Table.new(:users)
_(relation[:id].lteq_any([1, 2])).must_be_kind_of Nodes::Grouping
end
it "should generate ORs in sql" do
relation = Table.new(:users)
mgr = relation.project relation[:id]
mgr.where relation[:id].lteq_any([1, 2])
_(mgr.to_sql).must_be_like %{
SELECT "users"."id" FROM "users" WHERE ("users"."id" <= 1 OR "users"."id" <= 2)
}
end
end
describe "#lteq_all" do
it "should create a Grouping node" do
relation = Table.new(:users)
_(relation[:id].lteq_all([1, 2])).must_be_kind_of Nodes::Grouping
end
it "should generate ANDs in sql" do
relation = Table.new(:users)
mgr = relation.project relation[:id]
mgr.where relation[:id].lteq_all([1, 2])
_(mgr.to_sql).must_be_like %{
SELECT "users"."id" FROM "users" WHERE ("users"."id" <= 1 AND "users"."id" <= 2)
}
end
end
describe "#average" do
it "should create a AVG node" do
relation = Table.new(:users)
_(relation[:id].average).must_be_kind_of Nodes::Avg
end
it "should generate the proper SQL" do
relation = Table.new(:users)
mgr = relation.project relation[:id].average
_(mgr.to_sql).must_be_like %{
SELECT AVG("users"."id")
FROM "users"
}
end
end
describe "#maximum" do
it "should create a MAX node" do
relation = Table.new(:users)
_(relation[:id].maximum).must_be_kind_of Nodes::Max
end
it "should generate proper SQL" do
relation = Table.new(:users)
mgr = relation.project relation[:id].maximum
_(mgr.to_sql).must_be_like %{
SELECT MAX("users"."id")
FROM "users"
}
end
end
describe "#minimum" do
it "should create a Min node" do
relation = Table.new(:users)
_(relation[:id].minimum).must_be_kind_of Nodes::Min
end
it "should generate proper SQL" do
relation = Table.new(:users)
mgr = relation.project relation[:id].minimum
_(mgr.to_sql).must_be_like %{
SELECT MIN("users"."id")
FROM "users"
}
end
end
describe "#sum" do
it "should create a SUM node" do
relation = Table.new(:users)
_(relation[:id].sum).must_be_kind_of Nodes::Sum
end
it "should generate the proper SQL" do
relation = Table.new(:users)
mgr = relation.project relation[:id].sum
_(mgr.to_sql).must_be_like %{
SELECT SUM("users"."id")
FROM "users"
}
end
end
describe "#count" do
it "should return a count node" do
relation = Table.new(:users)
_(relation[:id].count).must_be_kind_of Nodes::Count
end
it "should take a distinct param" do
relation = Table.new(:users)
count = relation[:id].count(nil)
_(count).must_be_kind_of Nodes::Count
_(count.distinct).must_be_nil
end
end
describe "#eq" do
it "should return an equality node" do
attribute = Attribute.new nil, nil
equality = attribute.eq 1
_(equality.left).must_equal attribute
_(equality.right.value).must_equal 1
_(equality).must_be_kind_of Nodes::Equality
end
it "should generate = in sql" do
relation = Table.new(:users)
mgr = relation.project relation[:id]
mgr.where relation[:id].eq(10)
_(mgr.to_sql).must_be_like %{
SELECT "users"."id" FROM "users" WHERE "users"."id" = 10
}
end
it "should handle nil" do
relation = Table.new(:users)
mgr = relation.project relation[:id]
mgr.where relation[:id].eq(nil)
_(mgr.to_sql).must_be_like %{
SELECT "users"."id" FROM "users" WHERE "users"."id" IS NULL
}
end
end
describe "#eq_any" do
it "should create a Grouping node" do
relation = Table.new(:users)
_(relation[:id].eq_any([1, 2])).must_be_kind_of Nodes::Grouping
end
it "should generate ORs in sql" do
relation = Table.new(:users)
mgr = relation.project relation[:id]
mgr.where relation[:id].eq_any([1, 2])
_(mgr.to_sql).must_be_like %{
SELECT "users"."id" FROM "users" WHERE ("users"."id" = 1 OR "users"."id" = 2)
}
end
it "should not eat input" do
relation = Table.new(:users)
mgr = relation.project relation[:id]
values = [1, 2]
mgr.where relation[:id].eq_any(values)
_(values).must_equal [1, 2]
end
end
describe "#eq_all" do
it "should create a Grouping node" do
relation = Table.new(:users)
_(relation[:id].eq_all([1, 2])).must_be_kind_of Nodes::Grouping
end
it "should generate ANDs in sql" do
relation = Table.new(:users)
mgr = relation.project relation[:id]
mgr.where relation[:id].eq_all([1, 2])
_(mgr.to_sql).must_be_like %{
SELECT "users"."id" FROM "users" WHERE ("users"."id" = 1 AND "users"."id" = 2)
}
end
it "should not eat input" do
relation = Table.new(:users)
mgr = relation.project relation[:id]
values = [1, 2]
mgr.where relation[:id].eq_all(values)
_(values).must_equal [1, 2]
end
end
describe "#matches" do
it "should create a Matches node" do
relation = Table.new(:users)
_(relation[:name].matches("%bacon%")).must_be_kind_of Nodes::Matches
end
it "should generate LIKE in sql" do
relation = Table.new(:users)
mgr = relation.project relation[:id]
mgr.where relation[:name].matches("%bacon%")
_(mgr.to_sql).must_be_like %{
SELECT "users"."id" FROM "users" WHERE "users"."name" LIKE '%bacon%'
}
end
end
describe "#matches_any" do
it "should create a Grouping node" do
relation = Table.new(:users)
_(relation[:name].matches_any(["%chunky%", "%bacon%"])).must_be_kind_of Nodes::Grouping
end
it "should generate ORs in sql" do
relation = Table.new(:users)
mgr = relation.project relation[:id]
mgr.where relation[:name].matches_any(["%chunky%", "%bacon%"])
_(mgr.to_sql).must_be_like %{
SELECT "users"."id" FROM "users" WHERE ("users"."name" LIKE '%chunky%' OR "users"."name" LIKE '%bacon%')
}
end
end
describe "#matches_all" do
it "should create a Grouping node" do
relation = Table.new(:users)
_(relation[:name].matches_all(["%chunky%", "%bacon%"])).must_be_kind_of Nodes::Grouping
end
it "should generate ANDs in sql" do
relation = Table.new(:users)
mgr = relation.project relation[:id]
mgr.where relation[:name].matches_all(["%chunky%", "%bacon%"])
_(mgr.to_sql).must_be_like %{
SELECT "users"."id" FROM "users" WHERE ("users"."name" LIKE '%chunky%' AND "users"."name" LIKE '%bacon%')
}
end
end
describe "#does_not_match" do
it "should create a DoesNotMatch node" do
relation = Table.new(:users)
_(relation[:name].does_not_match("%bacon%")).must_be_kind_of Nodes::DoesNotMatch
end
it "should generate NOT LIKE in sql" do
relation = Table.new(:users)
mgr = relation.project relation[:id]
mgr.where relation[:name].does_not_match("%bacon%")
_(mgr.to_sql).must_be_like %{
SELECT "users"."id" FROM "users" WHERE "users"."name" NOT LIKE '%bacon%'
}
end
end
describe "#does_not_match_any" do
it "should create a Grouping node" do
relation = Table.new(:users)
_(relation[:name].does_not_match_any(["%chunky%", "%bacon%"])).must_be_kind_of Nodes::Grouping
end
it "should generate ORs in sql" do
relation = Table.new(:users)
mgr = relation.project relation[:id]
mgr.where relation[:name].does_not_match_any(["%chunky%", "%bacon%"])
_(mgr.to_sql).must_be_like %{
SELECT "users"."id" FROM "users" WHERE ("users"."name" NOT LIKE '%chunky%' OR "users"."name" NOT LIKE '%bacon%')
}
end
end
describe "#does_not_match_all" do
it "should create a Grouping node" do
relation = Table.new(:users)
_(relation[:name].does_not_match_all(["%chunky%", "%bacon%"])).must_be_kind_of Nodes::Grouping
end
it "should generate ANDs in sql" do
relation = Table.new(:users)
mgr = relation.project relation[:id]
mgr.where relation[:name].does_not_match_all(["%chunky%", "%bacon%"])
_(mgr.to_sql).must_be_like %{
SELECT "users"."id" FROM "users" WHERE ("users"."name" NOT LIKE '%chunky%' AND "users"."name" NOT LIKE '%bacon%')
}
end
end
describe "#between" do
it "can be constructed with a standard range" do
attribute = Attribute.new nil, nil
node = attribute.between(1..3)
_(node).must_equal Nodes::Between.new(
attribute,
Nodes::And.new([
Nodes::Casted.new(1, attribute),
Nodes::Casted.new(3, attribute)
])
)
end
it "can be constructed with a range starting from -Infinity" do
attribute = Attribute.new nil, nil
node = attribute.between(-::Float::INFINITY..3)
_(node).must_equal Nodes::LessThanOrEqual.new(
attribute,
Nodes::Casted.new(3, attribute)
)
end
it "can be constructed with a quoted range starting from -Infinity" do
attribute = Attribute.new nil, nil
node = attribute.between(quoted_range(-::Float::INFINITY, 3, false))
_(node).must_equal Nodes::LessThanOrEqual.new(
attribute,
Nodes::Quoted.new(3)
)
end
it "can be constructed with an exclusive range starting from -Infinity" do
attribute = Attribute.new nil, nil
node = attribute.between(-::Float::INFINITY...3)
_(node).must_equal Nodes::LessThan.new(
attribute,
Nodes::Casted.new(3, attribute)
)
end
it "can be constructed with a quoted exclusive range starting from -Infinity" do
attribute = Attribute.new nil, nil
node = attribute.between(quoted_range(-::Float::INFINITY, 3, true))
_(node).must_equal Nodes::LessThan.new(
attribute,
Nodes::Quoted.new(3)
)
end
it "can be constructed with an infinite range" do
attribute = Attribute.new nil, nil
node = attribute.between(-::Float::INFINITY..::Float::INFINITY)
_(node).must_equal Nodes::NotIn.new(attribute, [])
end
it "can be constructed with a quoted infinite range" do
attribute = Attribute.new nil, nil
node = attribute.between(quoted_range(-::Float::INFINITY, ::Float::INFINITY, false))
_(node).must_equal Nodes::NotIn.new(attribute, [])
end
it "can be constructed with a range ending at Infinity" do
attribute = Attribute.new nil, nil
node = attribute.between(0..::Float::INFINITY)
_(node).must_equal Nodes::GreaterThanOrEqual.new(
attribute,
Nodes::Casted.new(0, attribute)
)
end
it "can be constructed with a range implicitly starting at Infinity" do
attribute = Attribute.new nil, nil
node = attribute.between(..0)
_(node).must_equal Nodes::LessThanOrEqual.new(
attribute,
Nodes::Casted.new(0, attribute)
)
end
it "can be constructed with a range implicitly ending at Infinity" do
attribute = Attribute.new nil, nil
node = attribute.between(0..)
_(node).must_equal Nodes::GreaterThanOrEqual.new(
attribute,
Nodes::Casted.new(0, attribute)
)
end
it "can be constructed with a quoted range ending at Infinity" do
attribute = Attribute.new nil, nil
node = attribute.between(quoted_range(0, ::Float::INFINITY, false))
_(node).must_equal Nodes::GreaterThanOrEqual.new(
attribute,
Nodes::Quoted.new(0)
)
end
it "can be constructed with an endless range starting from Infinity" do
attribute = Attribute.new nil, nil
node = attribute.between(::Float::INFINITY..)
_(node).must_equal Nodes::In.new(attribute, [])
end
it "can be constructed with a beginless range ending in -Infinity" do
attribute = Attribute.new nil, nil
node = attribute.between(..-::Float::INFINITY)
_(node).must_equal Nodes::In.new(attribute, [])
end
it "can be constructed with an exclusive range" do
attribute = Attribute.new nil, nil
node = attribute.between(0...3)
_(node).must_equal Nodes::And.new([
Nodes::GreaterThanOrEqual.new(
attribute,
Nodes::Casted.new(0, attribute)
),
Nodes::LessThan.new(
attribute,
Nodes::Casted.new(3, attribute)
)
])
end
end
describe "#in" do
it "can be constructed with a subquery" do
relation = Table.new(:users)
mgr = relation.project relation[:id]
mgr.where relation[:name].does_not_match_all(["%chunky%", "%bacon%"])
attribute = Attribute.new nil, nil
node = attribute.in(mgr)
_(node).must_equal Nodes::In.new(attribute, mgr.ast)
end
it "can be constructed with a list" do
attribute = Attribute.new nil, nil
node = attribute.in([1, 2, 3])
_(node).must_equal Nodes::In.new(
attribute,
[
Nodes::Casted.new(1, attribute),
Nodes::Casted.new(2, attribute),
Nodes::Casted.new(3, attribute),
]
)
end
it "can be constructed with a random object" do
attribute = Attribute.new nil, nil
random_object = Object.new
node = attribute.in(random_object)
_(node).must_equal Nodes::In.new(
attribute,
Nodes::Casted.new(random_object, attribute)
)
end
it "should generate IN in sql" do
relation = Table.new(:users)
mgr = relation.project relation[:id]
mgr.where relation[:id].in([1, 2, 3])
_(mgr.to_sql).must_be_like %{
SELECT "users"."id" FROM "users" WHERE "users"."id" IN (1, 2, 3)
}
end
end
describe "#in_any" do
it "should create a Grouping node" do
relation = Table.new(:users)
_(relation[:id].in_any([1, 2])).must_be_kind_of Nodes::Grouping
end
it "should generate ORs in sql" do
relation = Table.new(:users)
mgr = relation.project relation[:id]
mgr.where relation[:id].in_any([[1, 2], [3, 4]])
_(mgr.to_sql).must_be_like %{
SELECT "users"."id" FROM "users" WHERE ("users"."id" IN (1, 2) OR "users"."id" IN (3, 4))
}
end
end
describe "#in_all" do
it "should create a Grouping node" do
relation = Table.new(:users)
_(relation[:id].in_all([1, 2])).must_be_kind_of Nodes::Grouping
end
it "should generate ANDs in sql" do
relation = Table.new(:users)
mgr = relation.project relation[:id]
mgr.where relation[:id].in_all([[1, 2], [3, 4]])
_(mgr.to_sql).must_be_like %{
SELECT "users"."id" FROM "users" WHERE ("users"."id" IN (1, 2) AND "users"."id" IN (3, 4))
}
end
end
describe "#not_between" do
it "can be constructed with a standard range" do
attribute = Attribute.new nil, nil
node = attribute.not_between(1..3)
_(node).must_equal Nodes::Grouping.new(
Nodes::Or.new(
Nodes::LessThan.new(
attribute,
Nodes::Casted.new(1, attribute)
),
Nodes::GreaterThan.new(
attribute,
Nodes::Casted.new(3, attribute)
)
)
)
end
it "can be constructed with a range starting from -Infinity" do
attribute = Attribute.new nil, nil
node = attribute.not_between(-::Float::INFINITY..3)
_(node).must_equal Nodes::GreaterThan.new(
attribute,
Nodes::Casted.new(3, attribute)
)
end
it "can be constructed with a quoted range starting from -Infinity" do
attribute = Attribute.new nil, nil
node = attribute.not_between(quoted_range(-::Float::INFINITY, 3, false))
_(node).must_equal Nodes::GreaterThan.new(
attribute,
Nodes::Quoted.new(3)
)
end
it "can be constructed with an exclusive range starting from -Infinity" do
attribute = Attribute.new nil, nil
node = attribute.not_between(-::Float::INFINITY...3)
_(node).must_equal Nodes::GreaterThanOrEqual.new(
attribute,
Nodes::Casted.new(3, attribute)
)
end
it "can be constructed with a quoted exclusive range starting from -Infinity" do
attribute = Attribute.new nil, nil
node = attribute.not_between(quoted_range(-::Float::INFINITY, 3, true))
_(node).must_equal Nodes::GreaterThanOrEqual.new(
attribute,
Nodes::Quoted.new(3)
)
end
it "can be constructed with an infinite range" do
attribute = Attribute.new nil, nil
node = attribute.not_between(-::Float::INFINITY..::Float::INFINITY)
_(node).must_equal Nodes::In.new(attribute, [])
end
it "can be constructed with a quoted infinite range" do
attribute = Attribute.new nil, nil
node = attribute.not_between(quoted_range(-::Float::INFINITY, ::Float::INFINITY, false))
_(node).must_equal Nodes::In.new(attribute, [])
end
it "can be constructed with a range ending at Infinity" do
attribute = Attribute.new nil, nil
node = attribute.not_between(0..::Float::INFINITY)
_(node).must_equal Nodes::LessThan.new(
attribute,
Nodes::Casted.new(0, attribute)
)
end
it "can be constructed with a range implicitly starting at Infinity" do
attribute = Attribute.new nil, nil
node = attribute.not_between(..0)
_(node).must_equal Nodes::GreaterThan.new(
attribute,
Nodes::Casted.new(0, attribute)
)
end
it "can be constructed with a range implicitly ending at Infinity" do
attribute = Attribute.new nil, nil
node = attribute.not_between(0..)
_(node).must_equal Nodes::LessThan.new(
attribute,
Nodes::Casted.new(0, attribute)
)
end
it "can be constructed with a quoted range ending at Infinity" do
attribute = Attribute.new nil, nil
node = attribute.not_between(quoted_range(0, ::Float::INFINITY, false))
_(node).must_equal Nodes::LessThan.new(
attribute,
Nodes::Quoted.new(0)
)
end
it "can be constructed with an endless range starting from Infinity" do
attribute = Attribute.new nil, nil
node = attribute.not_between(::Float::INFINITY..)
_(node).must_equal Nodes::NotIn.new(attribute, [])
end
it "can be constructed with a beginless range ending in -Infinity" do
attribute = Attribute.new nil, nil
node = attribute.not_between(..-::Float::INFINITY)
_(node).must_equal Nodes::NotIn.new(attribute, [])
end
it "can be constructed with an exclusive range" do
attribute = Attribute.new nil, nil
node = attribute.not_between(0...3)
_(node).must_equal Nodes::Grouping.new(
Nodes::Or.new(
Nodes::LessThan.new(
attribute,
Nodes::Casted.new(0, attribute)
),
Nodes::GreaterThanOrEqual.new(
attribute,
Nodes::Casted.new(3, attribute)
)
)
)
end
end
describe "#not_in" do
it "can be constructed with a subquery" do
relation = Table.new(:users)
mgr = relation.project relation[:id]
mgr.where relation[:name].does_not_match_all(["%chunky%", "%bacon%"])
attribute = Attribute.new nil, nil
node = attribute.not_in(mgr)
_(node).must_equal Nodes::NotIn.new(attribute, mgr.ast)
end
it "can be constructed with a Union" do
relation = Table.new(:users)
mgr1 = relation.project(relation[:id])
mgr2 = relation.project(relation[:id])
union = mgr1.union(mgr2)
node = relation[:id].in(union)
_(node.to_sql).must_be_like %{
"users"."id" IN (( SELECT "users"."id" FROM "users" UNION SELECT "users"."id" FROM "users" ))
}
end
it "can be constructed with a list" do
attribute = Attribute.new nil, nil
node = attribute.not_in([1, 2, 3])
_(node).must_equal Nodes::NotIn.new(
attribute,
[
Nodes::Casted.new(1, attribute),
Nodes::Casted.new(2, attribute),
Nodes::Casted.new(3, attribute),
]
)
end
it "can be constructed with a random object" do
attribute = Attribute.new nil, nil
random_object = Object.new
node = attribute.not_in(random_object)
_(node).must_equal Nodes::NotIn.new(
attribute,
Nodes::Casted.new(random_object, attribute)
)
end
it "should generate NOT IN in sql" do
relation = Table.new(:users)
mgr = relation.project relation[:id]
mgr.where relation[:id].not_in([1, 2, 3])
_(mgr.to_sql).must_be_like %{
SELECT "users"."id" FROM "users" WHERE "users"."id" NOT IN (1, 2, 3)
}
end
end
describe "#not_in_any" do
it "should create a Grouping node" do
relation = Table.new(:users)
_(relation[:id].not_in_any([1, 2])).must_be_kind_of Nodes::Grouping
end
it "should generate ORs in sql" do
relation = Table.new(:users)
mgr = relation.project relation[:id]
mgr.where relation[:id].not_in_any([[1, 2], [3, 4]])
_(mgr.to_sql).must_be_like %{
SELECT "users"."id" FROM "users" WHERE ("users"."id" NOT IN (1, 2) OR "users"."id" NOT IN (3, 4))
}
end
end
describe "#not_in_all" do
it "should create a Grouping node" do
relation = Table.new(:users)
_(relation[:id].not_in_all([1, 2])).must_be_kind_of Nodes::Grouping
end
it "should generate ANDs in sql" do
relation = Table.new(:users)
mgr = relation.project relation[:id]
mgr.where relation[:id].not_in_all([[1, 2], [3, 4]])
_(mgr.to_sql).must_be_like %{
SELECT "users"."id" FROM "users" WHERE ("users"."id" NOT IN (1, 2) AND "users"."id" NOT IN (3, 4))
}
end
end
describe "#eq_all" do
it "should create a Grouping node" do
relation = Table.new(:users)
_(relation[:id].eq_all([1, 2])).must_be_kind_of Nodes::Grouping
end
it "should generate ANDs in sql" do
relation = Table.new(:users)
mgr = relation.project relation[:id]
mgr.where relation[:id].eq_all([1, 2])
_(mgr.to_sql).must_be_like %{
SELECT "users"."id" FROM "users" WHERE ("users"."id" = 1 AND "users"."id" = 2)
}
end
end
describe "#asc" do
it "should create an Ascending node" do
relation = Table.new(:users)
_(relation[:id].asc).must_be_kind_of Nodes::Ascending
end
it "should generate ASC in sql" do
relation = Table.new(:users)
mgr = relation.project relation[:id]
mgr.order relation[:id].asc
_(mgr.to_sql).must_be_like %{
SELECT "users"."id" FROM "users" ORDER BY "users"."id" ASC
}
end
end
describe "#desc" do
it "should create a Descending node" do
relation = Table.new(:users)
_(relation[:id].desc).must_be_kind_of Nodes::Descending
end
it "should generate DESC in sql" do
relation = Table.new(:users)
mgr = relation.project relation[:id]
mgr.order relation[:id].desc
_(mgr.to_sql).must_be_like %{
SELECT "users"."id" FROM "users" ORDER BY "users"."id" DESC
}
end
end
describe "#contains" do
it "should create a Contains node" do
relation = Table.new(:products)
_(relation[:tags].contains(["foo", "bar"])).must_be_kind_of Nodes::Contains
end
it "should generate @> in sql" do
relation = Table.new(:products, type_caster: fake_pg_caster)
mgr = relation.project relation[:id]
mgr.where relation[:tags].contains(["foo", "bar"])
_(mgr.to_sql).must_be_like %{ SELECT "products"."id" FROM "products" WHERE "products"."tags" @> '{foo,bar}' }
end
end
describe "#overlaps" do
it "should create an Overlaps node" do
relation = Table.new(:products)
_(relation[:tags].overlaps(["foo", "bar"])).must_be_kind_of Nodes::Overlaps
end
it "should generate && in sql" do
relation = Table.new(:products, type_caster: fake_pg_caster)
mgr = relation.project relation[:id]
mgr.where relation[:tags].overlaps(["foo", "bar"])
_(mgr.to_sql).must_be_like %{ SELECT "products"."id" FROM "products" WHERE "products"."tags" && '{foo,bar}' }
end
end
describe "equality" do
describe "#to_sql" do
it "should produce sql" do
table = Table.new :users
condition = table["id"].eq 1
_(condition.to_sql).must_equal '"users"."id" = 1'
end
end
end
describe "type casting" do
it "does not type cast by default" do
table = Table.new(:foo)
condition = table["id"].eq("1")
assert_not table.able_to_type_cast?
_(condition.to_sql).must_equal %("foo"."id" = '1')
end
it "type casts when given an explicit caster" do
fake_caster = Object.new
def fake_caster.type_cast_for_database(attr_name, value)
if attr_name == "id"
value.to_i
else
value
end
end
table = Table.new(:foo, type_caster: fake_caster)
condition = table["id"].eq("1").and(table["other_id"].eq("2"))
assert table.able_to_type_cast?
_(condition.to_sql).must_equal %("foo"."id" = 1 AND "foo"."other_id" = '2')
end
it "does not type cast SqlLiteral nodes" do
fake_caster = Object.new
def fake_caster.type_cast_for_database(attr_name, value)
value.to_i
end
table = Table.new(:foo, type_caster: fake_caster)
condition = table["id"].eq(Arel.sql("(select 1)"))
assert table.able_to_type_cast?
_(condition.to_sql).must_equal %("foo"."id" = (select 1))
end
end
private
def quoted_range(begin_val, end_val, exclude)
OpenStruct.new(
begin: Nodes::Quoted.new(begin_val),
end: Nodes::Quoted.new(end_val),
exclude_end?: exclude,
)
end
# Mimic PG::TextDecoder::Array casting
def fake_pg_caster
Object.new.tap do |caster|
def caster.type_cast_for_database(attr_name, value)
if attr_name == "tags"
"{#{value.join(",")}}"
else
value
end
end
end
end
end
end
end
# frozen_string_literal: true
require "active_model/attribute/user_provided_default"
module ActiveRecord
# See ActiveRecord::Attributes::ClassMethods for documentation
module Attributes
extend ActiveSupport::Concern
included do
class_attribute :attributes_to_define_after_schema_loads, instance_accessor: false, default: {} # :internal:
end
module ClassMethods
# Defines an attribute with a type on this model. It will override the
# type of existing attributes if needed. This allows control over how
# values are converted to and from SQL when assigned to a model. It also
# changes the behavior of values passed to
# {ActiveRecord::Base.where}[rdoc-ref:QueryMethods#where]. This will let you use
# your domain objects across much of Active Record, without having to
# rely on implementation details or monkey patching.
#
# +name+ The name of the methods to define attribute methods for, and the
# column which this will persist to.
#
# +cast_type+ A symbol such as +:string+ or +:integer+, or a type object
# to be used for this attribute. See the examples below for more
# information about providing custom type objects.
#
# ==== Options
#
# The following options are accepted:
#
# +default+ The default value to use when no value is provided. If this option
# is not passed, the previous default value (if any) will be used.
# Otherwise, the default will be +nil+.
#
# +array+ (PostgreSQL only) specifies that the type should be an array (see the
# examples below).
#
# +range+ (PostgreSQL only) specifies that the type should be a range (see the
# examples below).
#
# When using a symbol for +cast_type+, extra options are forwarded to the
# constructor of the type object.
#
# ==== Examples
#
# The type detected by Active Record can be overridden.
#
# # db/schema.rb
# create_table :store_listings, force: true do |t|
# t.decimal :price_in_cents
# end
#
# # app/models/store_listing.rb
# class StoreListing < ActiveRecord::Base
# end
#
# store_listing = StoreListing.new(price_in_cents: '10.1')
#
# # before
# store_listing.price_in_cents # => BigDecimal(10.1)
#
# class StoreListing < ActiveRecord::Base
# attribute :price_in_cents, :integer
# end
#
# # after
# store_listing.price_in_cents # => 10
#
# A default can also be provided.
#
# # db/schema.rb
# create_table :store_listings, force: true do |t|
# t.string :my_string, default: "original default"
# end
#
# StoreListing.new.my_string # => "original default"
#
# # app/models/store_listing.rb
# class StoreListing < ActiveRecord::Base
# attribute :my_string, :string, default: "new default"
# end
#
# StoreListing.new.my_string # => "new default"
#
# class Product < ActiveRecord::Base
# attribute :my_default_proc, :datetime, default: -> { Time.now }
# end
#
# Product.new.my_default_proc # => 2015-05-30 11:04:48 -0600
# sleep 1
# Product.new.my_default_proc # => 2015-05-30 11:04:49 -0600
#
# \Attributes do not need to be backed by a database column.
#
# # app/models/my_model.rb
# class MyModel < ActiveRecord::Base
# attribute :my_string, :string
# attribute :my_int_array, :integer, array: true
# attribute :my_float_range, :float, range: true
# end
#
# model = MyModel.new(
# my_string: "string",
# my_int_array: ["1", "2", "3"],
# my_float_range: "[1,3.5]",
# )
# model.attributes
# # =>
# {
# my_string: "string",
# my_int_array: [1, 2, 3],
# my_float_range: 1.0..3.5
# }
#
# Passing options to the type constructor
#
# # app/models/my_model.rb
# class MyModel < ActiveRecord::Base
# attribute :small_int, :integer, limit: 2
# end
#
# MyModel.create(small_int: 65537)
# # => Error: 65537 is out of range for the limit of two bytes
#
# ==== Creating Custom Types
#
# Users may also define their own custom types, as long as they respond
# to the methods defined on the value type. The method +deserialize+ or
# +cast+ will be called on your type object, with raw input from the
# database or from your controllers. See ActiveModel::Type::Value for the
# expected API. It is recommended that your type objects inherit from an
# existing type, or from ActiveRecord::Type::Value
#
# class MoneyType < ActiveRecord::Type::Integer
# def cast(value)
# if !value.kind_of?(Numeric) && value.include?('$')
# price_in_dollars = value.gsub(/\$/, '').to_f
# super(price_in_dollars * 100)
# else
# super
# end
# end
# end
#
# # config/initializers/types.rb
# ActiveRecord::Type.register(:money, MoneyType)
#
# # app/models/store_listing.rb
# class StoreListing < ActiveRecord::Base
# attribute :price_in_cents, :money
# end
#
# store_listing = StoreListing.new(price_in_cents: '$10.00')
# store_listing.price_in_cents # => 1000
#
# For more details on creating custom types, see the documentation for
# ActiveModel::Type::Value. For more details on registering your types
# to be referenced by a symbol, see ActiveRecord::Type.register. You can
# also pass a type object directly, in place of a symbol.
#
# ==== \Querying
#
# When {ActiveRecord::Base.where}[rdoc-ref:QueryMethods#where] is called, it will
# use the type defined by the model class to convert the value to SQL,
# calling +serialize+ on your type object. For example:
#
# class Money < Struct.new(:amount, :currency)
# end
#
# class MoneyType < ActiveRecord::Type::Value
# def initialize(currency_converter:)
# @currency_converter = currency_converter
# end
#
# # value will be the result of +deserialize+ or
# # +cast+. Assumed to be an instance of +Money+ in
# # this case.
# def serialize(value)
# value_in_bitcoins = @currency_converter.convert_to_bitcoins(value)
# value_in_bitcoins.amount
# end
# end
#
# # config/initializers/types.rb
# ActiveRecord::Type.register(:money, MoneyType)
#
# # app/models/product.rb
# class Product < ActiveRecord::Base
# currency_converter = ConversionRatesFromTheInternet.new
# attribute :price_in_bitcoins, :money, currency_converter: currency_converter
# end
#
# Product.where(price_in_bitcoins: Money.new(5, "USD"))
# # => SELECT * FROM products WHERE price_in_bitcoins = 0.02230
#
# Product.where(price_in_bitcoins: Money.new(5, "GBP"))
# # => SELECT * FROM products WHERE price_in_bitcoins = 0.03412
#
# ==== Dirty Tracking
#
# The type of an attribute is given the opportunity to change how dirty
# tracking is performed. The methods +changed?+ and +changed_in_place?+
# will be called from ActiveModel::Dirty. See the documentation for those
# methods in ActiveModel::Type::Value for more details.
def attribute(name, cast_type = nil, default: NO_DEFAULT_PROVIDED, **options)
name = name.to_s
name = attribute_aliases[name] || name
reload_schema_from_cache
case cast_type
when Symbol
cast_type = Type.lookup(cast_type, **options, adapter: Type.adapter_name_from(self))
when nil
if (prev_cast_type, prev_default = attributes_to_define_after_schema_loads[name])
default = prev_default if default == NO_DEFAULT_PROVIDED
else
prev_cast_type = -> subtype { subtype }
end
cast_type = if block_given?
-> subtype { yield Proc === prev_cast_type ? prev_cast_type[subtype] : prev_cast_type }
else
prev_cast_type
end
end
self.attributes_to_define_after_schema_loads =
attributes_to_define_after_schema_loads.merge(name => [cast_type, default])
end
# This is the low level API which sits beneath +attribute+. It only
# accepts type objects, and will do its work immediately instead of
# waiting for the schema to load. Automatic schema detection and
# ClassMethods#attribute both call this under the hood. While this method
# is provided so it can be used by plugin authors, application code
# should probably use ClassMethods#attribute.
#
# +name+ The name of the attribute being defined. Expected to be a +String+.
#
# +cast_type+ The type object to use for this attribute.
#
# +default+ The default value to use when no value is provided. If this option
# is not passed, the previous default value (if any) will be used.
# Otherwise, the default will be +nil+. A proc can also be passed, and
# will be called once each time a new value is needed.
#
# +user_provided_default+ Whether the default value should be cast using
# +cast+ or +deserialize+.
def define_attribute(
name,
cast_type,
default: NO_DEFAULT_PROVIDED,
user_provided_default: true
)
attribute_types[name] = cast_type
define_default_attribute(name, default, cast_type, from_user: user_provided_default)
end
def load_schema! # :nodoc:
super
attributes_to_define_after_schema_loads.each do |name, (cast_type, default)|
cast_type = cast_type[type_for_attribute(name)] if Proc === cast_type
define_attribute(name, cast_type, default: default)
end
end
private
NO_DEFAULT_PROVIDED = Object.new # :nodoc:
private_constant :NO_DEFAULT_PROVIDED
def define_default_attribute(name, value, type, from_user:)
if value == NO_DEFAULT_PROVIDED
default_attribute = _default_attributes[name].with_type(type)
elsif from_user
default_attribute = ActiveModel::Attribute::UserProvidedDefault.new(
name,
value,
type,
_default_attributes.fetch(name.to_s) { nil },
)
else
default_attribute = ActiveModel::Attribute.from_database(name, value, type)
end
_default_attributes[name] = default_attribute
end
end
end
end
# frozen_string_literal: true
require "cases/helper"
class OverloadedType < ActiveRecord::Base
attribute :overloaded_float, :integer
attribute :overloaded_string_with_limit, :string, limit: 50
attribute :non_existent_decimal, :decimal
attribute :string_with_default, :string, default: "the overloaded default"
end
class ChildOfOverloadedType < OverloadedType
end
class GrandchildOfOverloadedType < ChildOfOverloadedType
attribute :overloaded_float, :float
end
class UnoverloadedType < ActiveRecord::Base
self.table_name = "overloaded_types"
end
module ActiveRecord
class CustomPropertiesTest < ActiveRecord::TestCase
test "overloading types" do
data = OverloadedType.new
data.overloaded_float = "1.1"
data.unoverloaded_float = "1.1"
assert_equal 1, data.overloaded_float
assert_equal 1.1, data.unoverloaded_float
end
test "overloaded properties save" do
data = OverloadedType.new
data.overloaded_float = "2.2"
data.save!
data.reload
assert_equal 2, data.overloaded_float
assert_kind_of Integer, OverloadedType.last.overloaded_float
assert_equal 2.0, UnoverloadedType.last.overloaded_float
assert_kind_of Float, UnoverloadedType.last.overloaded_float
end
test "properties assigned in constructor" do
data = OverloadedType.new(overloaded_float: "3.3")
assert_equal 3, data.overloaded_float
end
test "overloaded properties with limit" do
assert_equal 50, OverloadedType.type_for_attribute("overloaded_string_with_limit").limit
assert_equal 255, UnoverloadedType.type_for_attribute("overloaded_string_with_limit").limit
end
test "overloaded default but keeping its own type" do
klass = Class.new(UnoverloadedType) do
attribute :overloaded_string_with_limit, default: "the overloaded default"
end
assert_equal 255, UnoverloadedType.type_for_attribute("overloaded_string_with_limit").limit
assert_equal 255, klass.type_for_attribute("overloaded_string_with_limit").limit
assert_nil UnoverloadedType.new.overloaded_string_with_limit
assert_equal "the overloaded default", klass.new.overloaded_string_with_limit
end
test "attributes with overridden types keep their type when a default value is configured separately" do
child = Class.new(OverloadedType) do
attribute :overloaded_float, default: "123"
end
assert_equal OverloadedType.type_for_attribute("overloaded_float"), child.type_for_attribute("overloaded_float")
assert_equal 123, child.new.overloaded_float
end
test "extra options are forwarded to the type caster constructor" do
klass = Class.new(OverloadedType) do
attribute :starts_at, :datetime, precision: 3, limit: 2, scale: 1, default: -> { Time.now.utc }
end
starts_at_type = klass.type_for_attribute(:starts_at)
assert_equal 3, starts_at_type.precision
assert_equal 2, starts_at_type.limit
assert_equal 1, starts_at_type.scale
assert_kind_of Type::DateTime, starts_at_type
assert_instance_of Time, klass.new.starts_at
end
test "time zone aware attribute" do
with_timezone_config aware_attributes: true, zone: "Pacific Time (US & Canada)" do
klass = Class.new(OverloadedType) do
attribute :starts_at, :datetime, precision: 3, default: -> { Time.now.utc }
attribute :ends_at, default: -> { Time.now.utc }
end
starts_at_type = klass.type_for_attribute(:starts_at)
ends_at_type = klass.type_for_attribute(:ends_at)
assert_instance_of AttributeMethods::TimeZoneConversion::TimeZoneConverter, starts_at_type
assert_instance_of AttributeMethods::TimeZoneConversion::TimeZoneConverter, ends_at_type
assert_kind_of Type::DateTime, starts_at_type.__getobj__
assert_kind_of Type::DateTime, ends_at_type.__getobj__
assert_instance_of ActiveSupport::TimeWithZone, klass.new.starts_at
assert_instance_of ActiveSupport::TimeWithZone, klass.new.ends_at
end
end
test "nonexistent attribute" do
data = OverloadedType.new(non_existent_decimal: 1)
assert_equal BigDecimal(1), data.non_existent_decimal
assert_raise ActiveRecord::UnknownAttributeError do
UnoverloadedType.new(non_existent_decimal: 1)
end
end
test "model with nonexistent attribute with default value can be saved" do
klass = Class.new(OverloadedType) do
attribute :non_existent_string_with_default, :string, default: "nonexistent"
end
model = klass.new
assert model.save
end
test "changing defaults" do
data = OverloadedType.new
unoverloaded_data = UnoverloadedType.new
assert_equal "the overloaded default", data.string_with_default
assert_equal "the original default", unoverloaded_data.string_with_default
end
test "defaults are not touched on the columns" do
assert_equal "the original default", OverloadedType.columns_hash["string_with_default"].default
end
test "children inherit custom properties" do
data = ChildOfOverloadedType.new(overloaded_float: "4.4")
assert_equal 4, data.overloaded_float
end
test "children can override parents" do
data = GrandchildOfOverloadedType.new(overloaded_float: "4.4")
assert_equal 4.4, data.overloaded_float
end
test "overloading properties does not attribute method order" do
attribute_names = OverloadedType.attribute_names
expected = OverloadedType.column_names + ["non_existent_decimal"]
assert_equal expected, attribute_names
end
test "caches are cleared" do
klass = Class.new(OverloadedType)
column_count = klass.columns.length
assert_equal column_count + 1, klass.attribute_types.length
assert_equal column_count + 1, klass.column_defaults.length
assert_equal column_count + 1, klass.attribute_names.length
assert_not klass.attribute_types.include?("wibble")
klass.attribute :wibble, Type::Value.new
assert_equal column_count + 2, klass.attribute_types.length
assert_equal column_count + 2, klass.column_defaults.length
assert_equal column_count + 2, klass.attribute_names.length
assert_includes klass.attribute_types, "wibble"
end
test "the given default value is cast from user" do
custom_type = Class.new(Type::Value) do
def cast(*)
"from user"
end
def deserialize(*)
"from database"
end
end
klass = Class.new(OverloadedType) do
attribute :wibble, custom_type.new, default: "default"
end
model = klass.new
assert_equal "from user", model.wibble
end
test "procs for default values" do
klass = Class.new(OverloadedType) do
@@counter = 0
attribute :counter, :integer, default: -> { @@counter += 1 }
end
assert_equal 1, klass.new.counter
assert_equal 2, klass.new.counter
end
test "procs for default values are evaluated even after column_defaults is called" do
klass = Class.new(OverloadedType) do
@@counter = 0
attribute :counter, :integer, default: -> { @@counter += 1 }
end
assert_equal 1, klass.new.counter
# column_defaults will increment the counter since the proc is called
klass.column_defaults
assert_equal 3, klass.new.counter
end
test "procs are memoized before type casting" do
klass = Class.new(OverloadedType) do
@@counter = 0
attribute :counter, :integer, default: -> { @@counter += 1 }
end
model = klass.new
assert_equal 1, model.counter_before_type_cast
assert_equal 1, model.counter_before_type_cast
end
test "user provided defaults are persisted even if unchanged" do
model = OverloadedType.create!
assert_equal "the overloaded default", model.reload.string_with_default
end
if current_adapter?(:PostgreSQLAdapter)
test "array types can be specified" do
klass = Class.new(OverloadedType) do
attribute :my_array, :string, limit: 50, array: true
attribute :my_int_array, :integer, array: true
end
string_array = ConnectionAdapters::PostgreSQL::OID::Array.new(
Type::String.new(limit: 50))
int_array = ConnectionAdapters::PostgreSQL::OID::Array.new(
Type::Integer.new)
assert_not_equal string_array, int_array
assert_equal string_array, klass.type_for_attribute("my_array")
assert_equal int_array, klass.type_for_attribute("my_int_array")
end
test "range types can be specified" do
klass = Class.new(OverloadedType) do
attribute :my_range, :string, limit: 50, range: true
attribute :my_int_range, :integer, range: true
end
string_range = ConnectionAdapters::PostgreSQL::OID::Range.new(
Type::String.new(limit: 50))
int_range = ConnectionAdapters::PostgreSQL::OID::Range.new(
Type::Integer.new)
assert_not_equal string_range, int_range
assert_equal string_range, klass.type_for_attribute("my_range")
assert_equal int_range, klass.type_for_attribute("my_int_range")
end
end
test "attributes added after subclasses load are inherited" do
parent = Class.new(ActiveRecord::Base) do
self.table_name = "topics"
end
child = Class.new(parent)
child.new # => force a schema load
parent.attribute(:foo, Type::Value.new)
assert_equal(:bar, child.new(foo: :bar).foo)
end
test "attributes not backed by database columns are not dirty when unchanged" do
assert_not_predicate OverloadedType.new, :non_existent_decimal_changed?
end
test "attributes not backed by database columns are always initialized" do
OverloadedType.create!
model = OverloadedType.last
assert_nil model.non_existent_decimal
model.non_existent_decimal = "123"
assert_equal 123, model.non_existent_decimal
end
test "attributes not backed by database columns return the default on models loaded from database" do
child = Class.new(OverloadedType) do
attribute :non_existent_decimal, :decimal, default: 123
end
child.create!
model = child.last
assert_equal 123, model.non_existent_decimal
end
test "attributes not backed by database columns keep their type when a default value is configured separately" do
child = Class.new(OverloadedType) do
attribute :non_existent_decimal, default: "123"
end
assert_equal OverloadedType.type_for_attribute("non_existent_decimal"), child.type_for_attribute("non_existent_decimal")
assert_equal 123, child.new.non_existent_decimal
end
test "attributes not backed by database columns properly interact with mutation and dirty" do
child = Class.new(ActiveRecord::Base) do
self.table_name = "topics"
attribute :foo, :string, default: "lol"
end
child.create!
model = child.last
assert_equal "lol", model.foo
model.foo << "asdf"
assert_equal "lolasdf", model.foo
assert_predicate model, :foo_changed?
model.reload
assert_equal "lol", model.foo
model.foo = "lol"
assert_not_predicate model, :changed?
end
test "attributes not backed by database columns appear in inspect" do
inspection = OverloadedType.new.inspect
assert_includes inspection, "non_existent_decimal"
end
test "attributes do not require a type" do
klass = Class.new(OverloadedType) do
attribute :no_type
end
assert_equal 1, klass.new(no_type: 1).no_type
assert_equal "foo", klass.new(no_type: "foo").no_type
end
test "attributes do not require a connection is established" do
assert_not_called(ActiveRecord::Base, :connection) do
Class.new(OverloadedType) do
attribute :foo, :string
end
end
end
test "unknown type error is raised" do
assert_raise(ArgumentError) do
OverloadedType.attribute :foo, :unknown
end
end
test "immutable_strings_by_default changes schema inference for string columns" do
with_immutable_strings do
OverloadedType.reset_column_information
immutable_string_type = Type.lookup(:immutable_string).class
assert_instance_of immutable_string_type, OverloadedType.type_for_attribute("inferred_string")
end
end
test "immutable_strings_by_default retains limit information" do
with_immutable_strings do
OverloadedType.reset_column_information
assert_equal 255, OverloadedType.type_for_attribute("inferred_string").limit
end
end
test "immutable_strings_by_default does not affect `attribute :foo, :string`" do
with_immutable_strings do
OverloadedType.reset_column_information
default_string_type = Type.lookup(:string).class
assert_instance_of default_string_type, OverloadedType.type_for_attribute("string_with_default")
end
end
test "serialize boolean for both string types" do
default_string_type = Type.lookup(:string)
immutable_string_type = Type.lookup(:immutable_string)
assert_equal default_string_type.serialize(true), immutable_string_type.serialize(true)
assert_equal default_string_type.serialize(false), immutable_string_type.serialize(false)
end
private
def with_immutable_strings
old_value = ActiveRecord::Base.immutable_strings_by_default
ActiveRecord::Base.immutable_strings_by_default = true
yield
ensure
ActiveRecord::Base.immutable_strings_by_default = old_value
end
end
end
# frozen_string_literal: true
class Author < ActiveRecord::Base
has_many :posts
has_many :serialized_posts
has_one :post
has_many :very_special_comments, through: :posts
has_many :posts_with_comments, -> { includes(:comments) }, class_name: "Post"
has_many :popular_grouped_posts, -> { includes(:comments).group("type").having("SUM(legacy_comments_count) > 1").select("type") }, class_name: "Post"
has_many :posts_with_comments_sorted_by_comment_id, -> { includes(:comments).order("comments.id") }, class_name: "Post"
has_many :posts_sorted_by_id, -> { order(:id) }, class_name: "Post"
has_many :posts_sorted_by_id_limited, -> { order("posts.id").limit(1) }, class_name: "Post"
has_many :posts_with_categories, -> { includes(:categories) }, class_name: "Post"
has_many :posts_with_comments_and_categories, -> { includes(:comments, :categories).order("posts.id") }, class_name: "Post"
has_many :posts_with_special_categorizations, class_name: "PostWithSpecialCategorization"
has_one :post_about_thinking, -> { where("posts.title like '%thinking%'") }, class_name: "Post"
has_one :post_about_thinking_with_last_comment, -> { where("posts.title like '%thinking%'").includes(:last_comment) }, class_name: "Post"
has_many :comments, through: :posts do
def ratings
Rating.joins(:comment).merge(self)
end
end
has_many :comments_with_order, -> { ordered_by_post_id }, through: :posts, source: :comments
has_many :no_joins_comments, through: :posts, disable_joins: :true, source: :comments
has_many :comments_with_foreign_key, through: :posts, source: :comments, foreign_key: :post_id
has_many :no_joins_comments_with_foreign_key, through: :posts, disable_joins: :true, source: :comments, foreign_key: :post_id
has_many :members,
through: :comments_with_order,
source: :origin,
source_type: "Member"
has_many :no_joins_members,
through: :comments_with_order,
source: :origin,
source_type: "Member",
disable_joins: true
has_many :ordered_members,
-> { order(id: :desc) },
through: :comments_with_order,
source: :origin,
source_type: "Member"
has_many :no_joins_ordered_members,
-> { order(id: :desc) },
through: :comments_with_order,
source: :origin,
source_type: "Member",
disable_joins: true
has_many :ratings, through: :comments
has_many :good_ratings,
-> { where("ratings.value > 5").order(:id) },
through: :comments,
source: :ratings
has_many :no_joins_ratings, through: :no_joins_comments, disable_joins: :true, source: :ratings
has_many :no_joins_good_ratings,
-> { where("ratings.value > 5").order(:id) },
through: :comments,
source: :ratings,
disable_joins: true
has_many :comments_containing_the_letter_e, through: :posts, source: :comments
has_many :comments_with_order_and_conditions, -> { order("comments.body").where("comments.body like 'Thank%'") }, through: :posts, source: :comments
has_many :comments_with_include, -> { includes(:post).where(posts: { type: "Post" }) }, through: :posts, source: :comments
has_many :comments_for_first_author, -> { for_first_author }, through: :posts, source: :comments
has_many :first_posts
has_many :comments_on_first_posts, -> { order("posts.id desc, comments.id asc") }, through: :first_posts, source: :comments
has_one :first_post
has_one :comment_on_first_post, -> { order("posts.id desc, comments.id asc") }, through: :first_post, source: :comments
has_many :thinking_posts, -> { where(title: "So I was thinking") }, dependent: :delete_all, class_name: "Post"
has_many :welcome_posts, -> { where(title: "Welcome to the weblog") }, class_name: "Post"
has_many :welcome_posts_with_one_comment,
-> { where(title: "Welcome to the weblog").where(comments_count: 1) },
class_name: "Post"
has_many :welcome_posts_with_comments,
-> { where(title: "Welcome to the weblog").where("legacy_comments_count > 0") },
class_name: "Post"
has_many :comments_desc, -> { order("comments.id DESC") }, through: :posts_sorted_by_id, source: :comments
has_many :unordered_comments, -> { unscope(:order).distinct }, through: :posts_sorted_by_id_limited, source: :comments
has_many :funky_comments, through: :posts, source: :comments
has_many :ordered_uniq_comments, -> { distinct.order("comments.id") }, through: :posts, source: :comments
has_many :ordered_uniq_comments_desc, -> { distinct.order("comments.id DESC") }, through: :posts, source: :comments
has_many :readonly_comments, -> { readonly }, through: :posts, source: :comments
has_many :special_posts
has_many :special_post_comments, through: :special_posts, source: :comments
has_many :special_posts_with_default_scope, class_name: "SpecialPostWithDefaultScope"
has_many :sti_posts, class_name: "StiPost"
has_many :sti_post_comments, through: :sti_posts, source: :comments
has_many :special_nonexistent_posts, -> { where("posts.body = 'nonexistent'") }, class_name: "SpecialPost"
has_many :special_nonexistent_post_comments, -> { where("comments.post_id" => 0) }, through: :special_nonexistent_posts, source: :comments
has_many :nonexistent_comments, through: :posts
has_many :hello_posts, -> { where "posts.body = 'hello'" }, class_name: "Post"
has_many :hello_post_comments, through: :hello_posts, source: :comments
has_many :posts_with_no_comments, -> { where("comments.id" => nil).includes(:comments) }, class_name: "Post"
has_many :posts_with_no_comments_2, -> { left_joins(:comments).where("comments.id": nil) }, class_name: "Post"
has_many :hello_posts_with_hash_conditions, -> { where(body: "hello") }, class_name: "Post"
has_many :hello_post_comments_with_hash_conditions, through: :hello_posts_with_hash_conditions, source: :comments
has_many :other_posts, class_name: "Post"
has_many :posts_with_callbacks, class_name: "Post", before_add: :log_before_adding,
after_add: :log_after_adding,
before_remove: :log_before_removing,
after_remove: :log_after_removing
has_many :posts_with_thrown_callbacks, class_name: "Post", before_add: :throw_abort,
after_add: :ensure_not_called,
before_remove: :throw_abort,
after_remove: :ensure_not_called
has_many :posts_with_proc_callbacks, class_name: "Post",
before_add: Proc.new { |o, r| o.post_log << "before_adding#{r.id || '<new>'}" },
after_add: Proc.new { |o, r| o.post_log << "after_adding#{r.id || '<new>'}" },
before_remove: Proc.new { |o, r| o.post_log << "before_removing#{r.id}" },
after_remove: Proc.new { |o, r| o.post_log << "after_removing#{r.id}" }
has_many :posts_with_multiple_callbacks, class_name: "Post",
before_add: [:log_before_adding, Proc.new { |o, r| o.post_log << "before_adding_proc#{r.id || '<new>'}" }],
after_add: [:log_after_adding, Proc.new { |o, r| o.post_log << "after_adding_proc#{r.id || '<new>'}" }]
has_many :unchangeable_posts, class_name: "Post", before_add: :raise_exception, after_add: :log_after_adding
has_many :categorizations, -> { }
has_many :categories, through: :categorizations
has_many :named_categories, through: :categorizations
has_many :special_categorizations
has_many :special_categories, through: :special_categorizations, source: :category
has_one :special_category, through: :special_categorizations, source: :category
has_many :general_categorizations, -> { joins(:category).where("categories.name": "General") }, class_name: "Categorization"
has_many :general_posts, through: :general_categorizations, source: :post
has_many :special_categories_with_conditions, -> { where(categorizations: { special: true }) }, through: :categorizations, source: :category
has_many :nonspecial_categories_with_conditions, -> { where(categorizations: { special: false }) }, through: :categorizations, source: :category
has_many :categories_like_general, -> { where(name: "General") }, through: :categorizations, source: :category, class_name: "Category"
has_many :categorized_posts, through: :categorizations, source: :post
has_many :unique_categorized_posts, -> { distinct }, through: :categorizations, source: :post
has_many :nothings, through: :kateggorizatons, class_name: "Category"
has_many :author_favorites
has_many :favorite_authors, -> { order("name") }, through: :author_favorites
has_many :taggings, through: :posts, source: :taggings
has_many :taggings_2, through: :posts, source: :tagging
has_many :tags, through: :posts
has_many :ordered_tags, through: :posts
has_many :post_categories, through: :posts, source: :categories
has_many :tagging_tags, through: :taggings, source: :tag
has_many :similar_posts, -> { distinct }, through: :tags, source: :tagged_posts
has_many :ordered_posts, -> { distinct }, through: :ordered_tags, source: :tagged_posts
has_many :distinct_tags, -> { select("DISTINCT tags.*").order("tags.name") }, through: :posts, source: :tags
has_many :tags_with_primary_key, through: :posts
has_many :books
has_many :published_books, class_name: "PublishedBook"
has_many :unpublished_books, -> { where(status: [:proposed, :written]) }, class_name: "Book"
has_many :subscriptions, through: :books
has_many :subscribers, -> { order("subscribers.nick") }, through: :subscriptions
has_many :distinct_subscribers, -> { select("DISTINCT subscribers.*").order("subscribers.nick") }, through: :subscriptions, source: :subscriber
has_one :essay, primary_key: :name, as: :writer
has_one :essay_category, through: :essay, source: :category
has_one :essay_owner, through: :essay, source: :owner
has_one :essay_2, primary_key: :name, class_name: "Essay", foreign_key: :author_id
has_one :essay_category_2, through: :essay_2, source: :category
has_many :essays, primary_key: :name, as: :writer
has_many :essay_categories, through: :essays, source: :category
has_many :essay_owners, through: :essays, source: :owner
has_many :essays_2, primary_key: :name, class_name: "Essay", foreign_key: :author_id
has_many :essay_categories_2, through: :essays_2, source: :category
belongs_to :owned_essay, primary_key: :name, class_name: "Essay"
has_one :owned_essay_category, through: :owned_essay, source: :category
belongs_to :author_address, dependent: :destroy
belongs_to :author_address_extra, dependent: :delete, class_name: "AuthorAddress"
has_many :category_post_comments, through: :categories, source: :post_comments
has_many :misc_posts, -> { where(posts: { title: ["misc post by bob", "misc post by mary"] }) }, class_name: "Post"
has_many :misc_post_first_blue_tags, through: :misc_posts, source: :first_blue_tags
has_many :misc_post_first_blue_tags_2, -> { where(posts: { title: ["misc post by bob", "misc post by mary"] }) },
through: :posts, source: :first_blue_tags_2
has_many :posts_with_default_include, class_name: "PostWithDefaultInclude"
has_many :comments_on_posts_with_default_include, through: :posts_with_default_include, source: :comments
has_many :posts_with_signature, ->(record) { where(arel_table[:title].matches("%by #{record.name.downcase}%")) }, class_name: "Post"
has_many :posts_mentioning_author, ->(record = nil) { where(arel_table[:body].matches("%#{record&.name&.downcase}%")) }, class_name: "Post"
has_many :comments_on_posts_mentioning_author, through: :posts_mentioning_author, source: :comments
has_many :comments_mentioning_author, ->(record) { where(arel_table[:body].matches("%#{record.name.downcase}%")) }, through: :posts, source: :comments
has_one :recent_post, -> { order(id: :desc) }, class_name: "Post"
has_one :recent_response, through: :recent_post, source: :comments
has_many :posts_with_extension, -> { order(:title) }, class_name: "Post" do
def extension_method; end
end
has_many :posts_with_extension_and_instance, ->(record) { order(:title) }, class_name: "Post" do
def extension_method; end
end
has_many :top_posts, -> { order(id: :asc) }, class_name: "Post"
has_many :other_top_posts, -> { order(id: :asc) }, class_name: "Post"
has_many :topics, primary_key: "name", foreign_key: "author_name"
has_many :topics_without_type, -> { select(:id, :title, :author_name) },
class_name: "Topic", primary_key: "name", foreign_key: "author_name"
has_many :lazy_readers_skimmers_or_not, through: :posts
has_many :lazy_readers_skimmers_or_not_2, through: :posts_with_no_comments, source: :lazy_readers_skimmers_or_not
has_many :lazy_readers_skimmers_or_not_3, through: :posts_with_no_comments_2, source: :lazy_readers_skimmers_or_not
attr_accessor :post_log
after_initialize :set_post_log
def set_post_log
@post_log = []
end
def label
"#{id}-#{name}"
end
def social
%w(twitter github)
end
validates_presence_of :name
private
def throw_abort(_)
throw(:abort)
end
def ensure_not_called(_)
raise
end
def log_before_adding(object)
@post_log << "before_adding#{object.id || '<new>'}"
end
def log_after_adding(object)
@post_log << "after_adding#{object.id}"
end
def log_before_removing(object)
@post_log << "before_removing#{object.id}"
end
def log_after_removing(object)
@post_log << "after_removing#{object.id}"
end
def raise_exception(object)
raise Exception.new("You can't add a post")
end
end
class AuthorAddress < ActiveRecord::Base
has_one :author
def self.destroyed_author_address_ids
@destroyed_author_address_ids ||= []
end
before_destroy do |author_address|
AuthorAddress.destroyed_author_address_ids << author_address.id
end
end
class AuthorFavorite < ActiveRecord::Base
belongs_to :author
belongs_to :favorite_author, class_name: "Author"
end
class AuthorFavoriteWithScope < ActiveRecord::Base
self.table_name = "author_favorites"
default_scope { order(id: :asc) }
belongs_to :author
belongs_to :favorite_author, class_name: "Author"
end
# frozen_string_literal: true
require "models/author"
class EncryptedAuthor < Author
self.table_name = "authors"
validates :name, uniqueness: true
encrypts :name, previous: { deterministic: true }
end
class EncryptedAuthorWithKey < Author
self.table_name = "authors"
encrypts :name, key: "some secret key!"
end
# frozen_string_literal: true
class AutoId < ActiveRecord::Base
self.table_name = "auto_id_tests"
self.primary_key = "auto_id"
end
# frozen_string_literal: true
require "cases/helper"
require "support/schema_dumping_helper"
class Mysql2AutoIncrementTest < ActiveRecord::Mysql2TestCase
include SchemaDumpingHelper
def setup
@connection = ActiveRecord::Base.connection
end
def teardown
@connection.drop_table :auto_increments, if_exists: true
end
def test_auto_increment_without_primary_key
@connection.create_table :auto_increments, id: false, force: true do |t|
t.integer :id, null: false, auto_increment: true
t.index :id
end
output = dump_table_schema("auto_increments")
assert_match(/t\.integer\s+"id",\s+null: false,\s+auto_increment: true$/, output)
end
def test_auto_increment_with_composite_primary_key
@connection.create_table :auto_increments, primary_key: [:id, :created_at], force: true do |t|
t.integer :id, null: false, auto_increment: true
t.datetime :created_at, null: false
end
output = dump_table_schema("auto_increments")
assert_match(/t\.integer\s+"id",\s+null: false,\s+auto_increment: true$/, output)
end
end
# frozen_string_literal: true
module ActiveRecord
# = Active Record Autosave Association
#
# AutosaveAssociation is a module that takes care of automatically saving
# associated records when their parent is saved. In addition to saving, it
# also destroys any associated records that were marked for destruction.
# (See #mark_for_destruction and #marked_for_destruction?).
#
# Saving of the parent, its associations, and the destruction of marked
# associations, all happen inside a transaction. This should never leave the
# database in an inconsistent state.
#
# If validations for any of the associations fail, their error messages will
# be applied to the parent.
#
# Note that it also means that associations marked for destruction won't
# be destroyed directly. They will however still be marked for destruction.
#
# Note that <tt>autosave: false</tt> is not same as not declaring <tt>:autosave</tt>.
# When the <tt>:autosave</tt> option is not present then new association records are
# saved but the updated association records are not saved.
#
# == Validation
#
# Child records are validated unless <tt>:validate</tt> is +false+.
#
# == Callbacks
#
# Association with autosave option defines several callbacks on your
# model (around_save, before_save, after_create, after_update). Please note that
# callbacks are executed in the order they were defined in
# model. You should avoid modifying the association content before
# autosave callbacks are executed. Placing your callbacks after
# associations is usually a good practice.
#
# === One-to-one Example
#
# class Post < ActiveRecord::Base
# has_one :author, autosave: true
# end
#
# Saving changes to the parent and its associated model can now be performed
# automatically _and_ atomically:
#
# post = Post.find(1)
# post.title # => "The current global position of migrating ducks"
# post.author.name # => "alloy"
#
# post.title = "On the migration of ducks"
# post.author.name = "Eloy Duran"
#
# post.save
# post.reload
# post.title # => "On the migration of ducks"
# post.author.name # => "Eloy Duran"
#
# Destroying an associated model, as part of the parent's save action, is as
# simple as marking it for destruction:
#
# post.author.mark_for_destruction
# post.author.marked_for_destruction? # => true
#
# Note that the model is _not_ yet removed from the database:
#
# id = post.author.id
# Author.find_by(id: id).nil? # => false
#
# post.save
# post.reload.author # => nil
#
# Now it _is_ removed from the database:
#
# Author.find_by(id: id).nil? # => true
#
# === One-to-many Example
#
# When <tt>:autosave</tt> is not declared new children are saved when their parent is saved:
#
# class Post < ActiveRecord::Base
# has_many :comments # :autosave option is not declared
# end
#
# post = Post.new(title: 'ruby rocks')
# post.comments.build(body: 'hello world')
# post.save # => saves both post and comment
#
# post = Post.create(title: 'ruby rocks')
# post.comments.build(body: 'hello world')
# post.save # => saves both post and comment
#
# post = Post.create(title: 'ruby rocks')
# comment = post.comments.create(body: 'hello world')
# comment.body = 'hi everyone'
# post.save # => saves post, but not comment
#
# When <tt>:autosave</tt> is true all children are saved, no matter whether they
# are new records or not:
#
# class Post < ActiveRecord::Base
# has_many :comments, autosave: true
# end
#
# post = Post.create(title: 'ruby rocks')
# comment = post.comments.create(body: 'hello world')
# comment.body = 'hi everyone'
# post.comments.build(body: "good morning.")
# post.save # => saves post and both comments.
#
# Destroying one of the associated models as part of the parent's save action
# is as simple as marking it for destruction:
#
# post.comments # => [#<Comment id: 1, ...>, #<Comment id: 2, ...]>
# post.comments[1].mark_for_destruction
# post.comments[1].marked_for_destruction? # => true
# post.comments.length # => 2
#
# Note that the model is _not_ yet removed from the database:
#
# id = post.comments.last.id
# Comment.find_by(id: id).nil? # => false
#
# post.save
# post.reload.comments.length # => 1
#
# Now it _is_ removed from the database:
#
# Comment.find_by(id: id).nil? # => true
#
# === Caveats
#
# Note that autosave will only trigger for already-persisted association records
# if the records themselves have been changed. This is to protect against
# <tt>SystemStackError</tt> caused by circular association validations. The one
# exception is if a custom validation context is used, in which case the validations
# will always fire on the associated records.
module AutosaveAssociation
extend ActiveSupport::Concern
module AssociationBuilderExtension # :nodoc:
def self.build(model, reflection)
model.send(:add_autosave_association_callbacks, reflection)
end
def self.valid_options
[ :autosave ]
end
end
included do
Associations::Builder::Association.extensions << AssociationBuilderExtension
end
module ClassMethods # :nodoc:
private
def define_non_cyclic_method(name, &block)
return if method_defined?(name, false)
define_method(name) do |*args|
result = true; @_already_called ||= {}
# Loop prevention for validation of associations
unless @_already_called[name]
begin
@_already_called[name] = true
result = instance_eval(&block)
ensure
@_already_called[name] = false
end
end
result
end
end
# Adds validation and save callbacks for the association as specified by
# the +reflection+.
#
# For performance reasons, we don't check whether to validate at runtime.
# However the validation and callback methods are lazy and those methods
# get created when they are invoked for the very first time. However,
# this can change, for instance, when using nested attributes, which is
# called _after_ the association has been defined. Since we don't want
# the callbacks to get defined multiple times, there are guards that
# check if the save or validation methods have already been defined
# before actually defining them.
def add_autosave_association_callbacks(reflection)
save_method = :"autosave_associated_records_for_#{reflection.name}"
if reflection.collection?
around_save :around_save_collection_association
define_non_cyclic_method(save_method) { save_collection_association(reflection) }
# Doesn't use after_save as that would save associations added in after_create/after_update twice
after_create save_method
after_update save_method
elsif reflection.has_one?
define_non_cyclic_method(save_method) { save_has_one_association(reflection) }
# Configures two callbacks instead of a single after_save so that
# the model may rely on their execution order relative to its
# own callbacks.
#
# For example, given that after_creates run before after_saves, if
# we configured instead an after_save there would be no way to fire
# a custom after_create callback after the child association gets
# created.
after_create save_method
after_update save_method
else
define_non_cyclic_method(save_method) { throw(:abort) if save_belongs_to_association(reflection) == false }
before_save save_method
end
define_autosave_validation_callbacks(reflection)
end
def define_autosave_validation_callbacks(reflection)
validation_method = :"validate_associated_records_for_#{reflection.name}"
if reflection.validate? && !method_defined?(validation_method)
if reflection.collection?
method = :validate_collection_association
else
method = :validate_single_association
end
define_non_cyclic_method(validation_method) { send(method, reflection) }
validate validation_method
after_validation :_ensure_no_duplicate_errors
end
end
end
# Reloads the attributes of the object as usual and clears <tt>marked_for_destruction</tt> flag.
def reload(options = nil)
@marked_for_destruction = false
@destroyed_by_association = nil
super
end
# Marks this record to be destroyed as part of the parent's save transaction.
# This does _not_ actually destroy the record instantly, rather child record will be destroyed
# when <tt>parent.save</tt> is called.
#
# Only useful if the <tt>:autosave</tt> option on the parent is enabled for this associated model.
def mark_for_destruction
@marked_for_destruction = true
end
# Returns whether or not this record will be destroyed as part of the parent's save transaction.
#
# Only useful if the <tt>:autosave</tt> option on the parent is enabled for this associated model.
def marked_for_destruction?
@marked_for_destruction
end
# Records the association that is being destroyed and destroying this
# record in the process.
def destroyed_by_association=(reflection)
@destroyed_by_association = reflection
end
# Returns the association for the parent being destroyed.
#
# Used to avoid updating the counter cache unnecessarily.
def destroyed_by_association
@destroyed_by_association
end
# Returns whether or not this record has been changed in any way (including whether
# any of its nested autosave associations are likewise changed)
def changed_for_autosave?
new_record? || has_changes_to_save? || marked_for_destruction? || nested_records_changed_for_autosave?
end
private
# Returns the record for an association collection that should be validated
# or saved. If +autosave+ is +false+ only new records will be returned,
# unless the parent is/was a new record itself.
def associated_records_to_validate_or_save(association, new_record, autosave)
if new_record || custom_validation_context?
association && association.target
elsif autosave
association.target.find_all(&:changed_for_autosave?)
else
association.target.find_all(&:new_record?)
end
end
# Go through nested autosave associations that are loaded in memory (without loading
# any new ones), and return true if any are changed for autosave.
# Returns false if already called to prevent an infinite loop.
def nested_records_changed_for_autosave?
@_nested_records_changed_for_autosave_already_called ||= false
return false if @_nested_records_changed_for_autosave_already_called
begin
@_nested_records_changed_for_autosave_already_called = true
self.class._reflections.values.any? do |reflection|
if reflection.options[:autosave]
association = association_instance_get(reflection.name)
association && Array.wrap(association.target).any?(&:changed_for_autosave?)
end
end
ensure
@_nested_records_changed_for_autosave_already_called = false
end
end
# Validate the association if <tt>:validate</tt> or <tt>:autosave</tt> is
# turned on for the association.
def validate_single_association(reflection)
association = association_instance_get(reflection.name)
record = association && association.reader
association_valid?(reflection, record) if record && (record.changed_for_autosave? || custom_validation_context?)
end
# Validate the associated records if <tt>:validate</tt> or
# <tt>:autosave</tt> is turned on for the association specified by
# +reflection+.
def validate_collection_association(reflection)
if association = association_instance_get(reflection.name)
if records = associated_records_to_validate_or_save(association, new_record?, reflection.options[:autosave])
records.each_with_index { |record, index| association_valid?(reflection, record, index) }
end
end
end
# Returns whether or not the association is valid and applies any errors to
# the parent, <tt>self</tt>, if it wasn't. Skips any <tt>:autosave</tt>
# enabled records if they're marked_for_destruction? or destroyed.
def association_valid?(reflection, record, index = nil)
return true if record.destroyed? || (reflection.options[:autosave] && record.marked_for_destruction?)
context = validation_context if custom_validation_context?
unless valid = record.valid?(context)
if reflection.options[:autosave]
indexed_attribute = !index.nil? && (reflection.options[:index_errors] || ActiveRecord.index_nested_attribute_errors)
record.errors.group_by_attribute.each { |attribute, errors|
attribute = normalize_reflection_attribute(indexed_attribute, reflection, index, attribute)
errors.each { |error|
self.errors.import(
error,
attribute: attribute
)
}
}
else
errors.add(reflection.name)
end
end
valid
end
def normalize_reflection_attribute(indexed_attribute, reflection, index, attribute)
if indexed_attribute
"#{reflection.name}[#{index}].#{attribute}"
else
"#{reflection.name}.#{attribute}"
end
end
# Is used as an around_save callback to check while saving a collection
# association whether or not the parent was a new record before saving.
def around_save_collection_association
previously_new_record_before_save = (@new_record_before_save ||= false)
@new_record_before_save = !previously_new_record_before_save && new_record?
yield
ensure
@new_record_before_save = previously_new_record_before_save
end
# Saves any new associated records, or all loaded autosave associations if
# <tt>:autosave</tt> is enabled on the association.
#
# In addition, it destroys all children that were marked for destruction
# with #mark_for_destruction.
#
# This all happens inside a transaction, _if_ the Transactions module is included into
# ActiveRecord::Base after the AutosaveAssociation module, which it does by default.
def save_collection_association(reflection)
if association = association_instance_get(reflection.name)
autosave = reflection.options[:autosave]
# By saving the instance variable in a local variable,
# we make the whole callback re-entrant.
new_record_before_save = @new_record_before_save
# reconstruct the scope now that we know the owner's id
association.reset_scope
if records = associated_records_to_validate_or_save(association, new_record_before_save, autosave)
if autosave
records_to_destroy = records.select(&:marked_for_destruction?)
records_to_destroy.each { |record| association.destroy(record) }
records -= records_to_destroy
end
records.each do |record|
next if record.destroyed?
saved = true
if autosave != false && (new_record_before_save || record.new_record?)
association.set_inverse_instance(record)
if autosave
saved = association.insert_record(record, false)
elsif !reflection.nested?
association_saved = association.insert_record(record)
if reflection.validate?
errors.add(reflection.name) unless association_saved
saved = association_saved
end
end
elsif autosave
saved = record.save(validate: false)
end
raise(RecordInvalid.new(association.owner)) unless saved
end
end
end
end
# Saves the associated record if it's new or <tt>:autosave</tt> is enabled
# on the association.
#
# In addition, it will destroy the association if it was marked for
# destruction with #mark_for_destruction.
#
# This all happens inside a transaction, _if_ the Transactions module is included into
# ActiveRecord::Base after the AutosaveAssociation module, which it does by default.
def save_has_one_association(reflection)
association = association_instance_get(reflection.name)
record = association && association.load_target
if record && !record.destroyed?
autosave = reflection.options[:autosave]
if autosave && record.marked_for_destruction?
record.destroy
elsif autosave != false
key = reflection.options[:primary_key] ? public_send(reflection.options[:primary_key]) : id
if (autosave && record.changed_for_autosave?) || _record_changed?(reflection, record, key)
unless reflection.through_reflection
record[reflection.foreign_key] = key
association.set_inverse_instance(record)
end
saved = record.save(validate: !autosave)
raise ActiveRecord::Rollback if !saved && autosave
saved
end
end
end
end
# If the record is new or it has changed, returns true.
def _record_changed?(reflection, record, key)
record.new_record? ||
association_foreign_key_changed?(reflection, record, key) ||
record.will_save_change_to_attribute?(reflection.foreign_key)
end
def association_foreign_key_changed?(reflection, record, key)
return false if reflection.through_reflection?
record._has_attribute?(reflection.foreign_key) && record._read_attribute(reflection.foreign_key) != key
end
# Saves the associated record if it's new or <tt>:autosave</tt> is enabled.
#
# In addition, it will destroy the association if it was marked for destruction.
def save_belongs_to_association(reflection)
association = association_instance_get(reflection.name)
return unless association && association.loaded? && !association.stale_target?
record = association.load_target
if record && !record.destroyed?
autosave = reflection.options[:autosave]
if autosave && record.marked_for_destruction?
self[reflection.foreign_key] = nil
record.destroy
elsif autosave != false
saved = record.save(validate: !autosave) if record.new_record? || (autosave && record.changed_for_autosave?)
if association.updated?
association_id = record.public_send(reflection.options[:primary_key] || :id)
self[reflection.foreign_key] = association_id
association.loaded!
end
saved if autosave
end
end
end
def custom_validation_context?
validation_context && [:create, :update].exclude?(validation_context)
end
def _ensure_no_duplicate_errors
errors.uniq!
end
end
end
# frozen_string_literal: true
require "cases/helper"
require "models/author"
require "models/book"
require "models/bird"
require "models/post"
require "models/comment"
require "models/category"
require "models/company"
require "models/contract"
require "models/customer"
require "models/developer"
require "models/computer"
require "models/invoice"
require "models/line_item"
require "models/mouse"
require "models/order"
require "models/parrot"
require "models/pirate"
require "models/project"
require "models/ship"
require "models/ship_part"
require "models/squeak"
require "models/tag"
require "models/tagging"
require "models/treasure"
require "models/eye"
require "models/electron"
require "models/molecule"
require "models/member"
require "models/member_detail"
require "models/organization"
require "models/guitar"
require "models/tuning_peg"
require "models/reply"
require "models/attachment"
require "models/translation"
class TestAutosaveAssociationsInGeneral < ActiveRecord::TestCase
def test_autosave_works_even_when_other_callbacks_update_the_parent_model
reference = Class.new(ActiveRecord::Base) do
self.table_name = "references"
def self.name; "Reference"; end
end
person = Class.new(ActiveRecord::Base) do
self.table_name = "people"
def self.name; "Person"; end
# It is necessary that the after_create is before the has_many _and_ that it updates the model.
# This replicates a bug found in https://github.com/rails/rails/issues/38120
after_create { update(first_name: "first name") }
has_many :references, autosave: true, anonymous_class: reference
end
reference_instance = reference.create!
person_instance = person.create!(first_name: "foo", references: [reference_instance])
reference_instance.reload
assert_equal person_instance.id, reference_instance.person_id
assert_equal "first name", person_instance.first_name # Make sure the after_create is actually called
end
def test_autosave_does_not_pass_through_non_custom_validation_contexts
person = Class.new(ActiveRecord::Base) {
self.table_name = "people"
validate :should_be_cool, on: :create
def self.name; "Person"; end
private
def should_be_cool
unless first_name == "cool"
errors.add :first_name, "not cool"
end
end
}
reference = Class.new(ActiveRecord::Base) {
self.table_name = "references"
def self.name; "Reference"; end
belongs_to :person, autosave: true, anonymous_class: person
}
u = person.create!(first_name: "cool")
u.first_name = "nah"
assert_predicate u, :valid?
r = reference.new(person: u)
assert_predicate r, :valid?
end
def test_autosave_collection_association_callbacks_get_called_once
ship_with_saving_stack = Class.new(Ship) do
def save_collection_association(reflection)
@count ||= 0
@count += 1 if reflection.name == :parts
super
end
end
ship = ship_with_saving_stack.new(name: "Nights Dirty Lightning")
ship.parts.build(name: "part")
ship.save!
assert_equal 1, ship.instance_variable_get(:@count)
end
def test_autosave_has_one_association_callbacks_get_called_once
# a bidirectional autosave is required to trigger multiple calls to
# save_has_one_association
assert Ship.reflect_on_association(:pirate).options[:autosave]
assert Pirate.reflect_on_association(:ship).options[:autosave]
pirate_with_saving_stack = Class.new(Pirate) do
def save_has_one_association(reflection)
@count ||= 0
@count += 1 if reflection.name == :ship
super
end
end
pirate = pirate_with_saving_stack.new(catchphrase: "Aye")
pirate.build_ship(name: "Nights Dirty Lightning")
pirate.save!
assert_equal 1, pirate.instance_variable_get(:@count)
end
def test_autosave_belongs_to_association_callbacks_get_called_once
ship_with_saving_stack = Class.new(Ship) do
def save_belongs_to_association(reflection)
@count ||= 0
@count += 1 if reflection.name == :pirate
super
end
end
ship = ship_with_saving_stack.new(name: "Nights Dirty Lightning")
ship.build_pirate(catchphrase: "Aye")
ship.save!
assert_equal 1, ship.instance_variable_get(:@count)
end
def test_should_not_add_the_same_callbacks_multiple_times_for_has_one
assert_no_difference_when_adding_callbacks_twice_for Pirate, :ship
end
def test_should_not_add_the_same_callbacks_multiple_times_for_belongs_to
assert_no_difference_when_adding_callbacks_twice_for Ship, :pirate
end
def test_should_not_add_the_same_callbacks_multiple_times_for_has_many
assert_no_difference_when_adding_callbacks_twice_for Pirate, :birds
end
def test_should_not_add_the_same_callbacks_multiple_times_for_has_and_belongs_to_many
assert_no_difference_when_adding_callbacks_twice_for Pirate, :parrots
end
def test_cyclic_autosaves_do_not_add_multiple_validations
ship = ShipWithoutNestedAttributes.new
ship.prisoners.build
assert_not_predicate ship, :valid?
assert_equal 1, ship.errors[:name].length
end
private
def assert_no_difference_when_adding_callbacks_twice_for(model, association_name)
reflection = model.reflect_on_association(association_name)
assert_no_difference "callbacks_for_model(#{model.name}).length" do
model.send(:add_autosave_association_callbacks, reflection)
end
end
def callbacks_for_model(model)
model.instance_variables.grep(/_callbacks$/).flat_map do |ivar|
model.instance_variable_get(ivar)
end
end
end
class TestDefaultAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCase
fixtures :companies, :accounts
def test_should_save_parent_but_not_invalid_child
firm = Firm.new(name: "GlobalMegaCorp")
assert_predicate firm, :valid?
firm.build_account_using_primary_key
assert_not_predicate firm.build_account_using_primary_key, :valid?
assert firm.save
assert_not_predicate firm.account_using_primary_key, :persisted?
end
def test_save_fails_for_invalid_has_one
firm = Firm.first
assert_predicate firm, :valid?
firm.build_account
assert_not_predicate firm.account, :valid?
assert_not_predicate firm, :valid?
assert_not firm.save
assert_equal ["is invalid"], firm.errors["account"]
end
def test_save_succeeds_for_invalid_has_one_with_validate_false
firm = Firm.first
assert_predicate firm, :valid?
firm.build_unvalidated_account
assert_not_predicate firm.unvalidated_account, :valid?
assert_predicate firm, :valid?
assert firm.save
end
def test_build_before_child_saved
firm = Firm.find(1)
account = firm.build_account("credit_limit" => 1000)
assert_equal account, firm.account
assert_not_predicate account, :persisted?
assert firm.save
assert_equal account, firm.account
assert_predicate account, :persisted?
end
def test_build_before_either_saved
firm = Firm.new("name" => "GlobalMegaCorp")
firm.account = account = Account.new("credit_limit" => 1000)
assert_equal account, firm.account
assert_not_predicate account, :persisted?
assert firm.save
assert_equal account, firm.account
assert_predicate account, :persisted?
end
def test_assignment_before_parent_saved
firm = Firm.new("name" => "GlobalMegaCorp")
firm.account = a = Account.find(1)
assert_not_predicate firm, :persisted?
assert_equal a, firm.account
assert firm.save
assert_equal a, firm.account
firm.association(:account).reload
assert_equal a, firm.account
end
def test_assignment_before_either_saved
firm = Firm.new("name" => "GlobalMegaCorp")
firm.account = a = Account.new("credit_limit" => 1000)
assert_not_predicate firm, :persisted?
assert_not_predicate a, :persisted?
assert_equal a, firm.account
assert firm.save
assert_predicate firm, :persisted?
assert_predicate a, :persisted?
assert_equal a, firm.account
firm.association(:account).reload
assert_equal a, firm.account
end
def test_not_resaved_when_unchanged
firm = Firm.all.merge!(includes: :account).first
firm.name += "-changed"
assert_queries(1) { firm.save! }
firm = Firm.first
firm.account = Account.first
assert_queries(Firm.partial_updates? ? 0 : 1) { firm.save! }
firm = Firm.first.dup
firm.account = Account.first
assert_queries(2) { firm.save! }
firm = Firm.first.dup
firm.account = Account.first.dup
assert_queries(2) { firm.save! }
end
def test_callbacks_firing_order_on_create
eye = Eye.create(iris_attributes: { color: "honey" })
assert_equal [true, false], eye.after_create_callbacks_stack
end
def test_callbacks_firing_order_on_update
eye = Eye.create(iris_attributes: { color: "honey" })
eye.update(iris_attributes: { color: "green" })
assert_equal [true, false], eye.after_update_callbacks_stack
end
def test_callbacks_firing_order_on_save
eye = Eye.create(iris_attributes: { color: "honey" })
assert_equal [false, false], eye.after_save_callbacks_stack
eye.update(iris_attributes: { color: "blue" })
assert_equal [false, false, false, false], eye.after_save_callbacks_stack
end
end
class TestDefaultAutosaveAssociationOnABelongsToAssociation < ActiveRecord::TestCase
fixtures :companies, :posts, :tags, :taggings
def test_should_save_parent_but_not_invalid_child
client = Client.new(name: "Joe (the Plumber)")
assert_predicate client, :valid?
client.build_firm
assert_not_predicate client.firm, :valid?
assert client.save
assert_not_predicate client.firm, :persisted?
end
def test_save_fails_for_invalid_belongs_to
# Oracle saves empty string as NULL therefore :message changed to one space
assert log = AuditLog.create(developer_id: 0, message: " ")
log.developer = Developer.new
assert_not_predicate log.developer, :valid?
assert_not_predicate log, :valid?
assert_not log.save
assert_equal ["is invalid"], log.errors["developer"]
end
def test_save_succeeds_for_invalid_belongs_to_with_validate_false
# Oracle saves empty string as NULL therefore :message changed to one space
assert log = AuditLog.create(developer_id: 0, message: " ")
log.unvalidated_developer = Developer.new
assert_not_predicate log.unvalidated_developer, :valid?
assert_predicate log, :valid?
assert log.save
end
def test_assignment_before_parent_saved
client = Client.first
apple = Firm.new("name" => "Apple")
client.firm = apple
assert_equal apple, client.firm
assert_not_predicate apple, :persisted?
assert client.save
assert apple.save
assert_predicate apple, :persisted?
assert_equal apple, client.firm
client.association(:firm).reload
assert_equal apple, client.firm
end
def test_assignment_before_either_saved
final_cut = Client.new("name" => "Final Cut")
apple = Firm.new("name" => "Apple")
final_cut.firm = apple
assert_not_predicate final_cut, :persisted?
assert_not_predicate apple, :persisted?
assert final_cut.save
assert_predicate final_cut, :persisted?
assert_predicate apple, :persisted?
assert_equal apple, final_cut.firm
final_cut.association(:firm).reload
assert_equal apple, final_cut.firm
end
def test_store_two_association_with_one_save
num_orders = Order.count
num_customers = Customer.count
order = Order.new
customer1 = order.billing = Customer.new
customer2 = order.shipping = Customer.new
assert order.save
assert_equal customer1, order.billing
assert_equal customer2, order.shipping
order.reload
assert_equal customer1, order.billing
assert_equal customer2, order.shipping
assert_equal num_orders + 1, Order.count
assert_equal num_customers + 2, Customer.count
end
def test_store_association_in_two_relations_with_one_save
num_orders = Order.count
num_customers = Customer.count
order = Order.new
customer = order.billing = order.shipping = Customer.new
assert order.save
assert_equal customer, order.billing
assert_equal customer, order.shipping
order.reload
assert_equal customer, order.billing
assert_equal customer, order.shipping
assert_equal num_orders + 1, Order.count
assert_equal num_customers + 1, Customer.count
end
def test_store_association_in_two_relations_with_one_save_in_existing_object
num_orders = Order.count
num_customers = Customer.count
order = Order.create
customer = order.billing = order.shipping = Customer.new
assert order.save
assert_equal customer, order.billing
assert_equal customer, order.shipping
order.reload
assert_equal customer, order.billing
assert_equal customer, order.shipping
assert_equal num_orders + 1, Order.count
assert_equal num_customers + 1, Customer.count
end
def test_store_association_in_two_relations_with_one_save_in_existing_object_with_values
num_orders = Order.count
num_customers = Customer.count
order = Order.create
customer = order.billing = order.shipping = Customer.new
assert order.save
assert_equal customer, order.billing
assert_equal customer, order.shipping
order.reload
customer = order.billing = order.shipping = Customer.new
assert order.save
order.reload
assert_equal customer, order.billing
assert_equal customer, order.shipping
assert_equal num_orders + 1, Order.count
assert_equal num_customers + 2, Customer.count
end
def test_store_association_with_a_polymorphic_relationship
num_tagging = Tagging.count
tags(:misc).create_tagging(taggable: posts(:thinking))
assert_equal num_tagging + 1, Tagging.count
end
def test_build_and_then_save_parent_should_not_reload_target
client = Client.first
apple = client.build_firm(name: "Apple")
client.save!
assert_no_queries { assert_equal apple, client.firm }
end
def test_validation_does_not_validate_stale_association_target
valid_developer = Developer.create!(name: "Dude", salary: 50_000)
invalid_developer = Developer.new()
auditlog = AuditLog.new(message: "foo")
auditlog.developer = invalid_developer
auditlog.developer_id = valid_developer.id
assert_predicate auditlog, :valid?
end
def test_validation_does_not_validate_non_dirty_association_target
mouse = Mouse.create!(name: "Will")
Squeak.create!(mouse: mouse)
mouse.name = nil
mouse.save! validate: false
squeak = Squeak.last
assert_equal true, squeak.valid?
assert_equal true, squeak.mouse.present?
assert_equal true, squeak.valid?
end
end
class TestDefaultAutosaveAssociationOnAHasManyAssociationWithAcceptsNestedAttributes < ActiveRecord::TestCase
def test_invalid_adding_with_nested_attributes
molecule = Molecule.new
valid_electron = Electron.new(name: "electron")
invalid_electron = Electron.new
molecule.electrons = [valid_electron, invalid_electron]
molecule.save
assert_not_predicate invalid_electron, :valid?
assert_predicate valid_electron, :valid?
assert_not molecule.persisted?, "Molecule should not be persisted when its electrons are invalid"
end
def test_errors_should_be_indexed_when_passed_as_array
guitar = Guitar.new
tuning_peg_valid = TuningPeg.new
tuning_peg_valid.pitch = 440.0
tuning_peg_invalid = TuningPeg.new
guitar.tuning_pegs = [tuning_peg_valid, tuning_peg_invalid]
assert_not_predicate tuning_peg_invalid, :valid?
assert_predicate tuning_peg_valid, :valid?
assert_not_predicate guitar, :valid?
assert_equal ["is not a number"], guitar.errors["tuning_pegs[1].pitch"]
assert_not_equal ["is not a number"], guitar.errors["tuning_pegs.pitch"]
end
def test_errors_should_be_indexed_when_global_flag_is_set
old_attribute_config = ActiveRecord.index_nested_attribute_errors
ActiveRecord.index_nested_attribute_errors = true
molecule = Molecule.new
valid_electron = Electron.new(name: "electron")
invalid_electron = Electron.new
molecule.electrons = [valid_electron, invalid_electron]
assert_not_predicate invalid_electron, :valid?
assert_predicate valid_electron, :valid?
assert_not_predicate molecule, :valid?
assert_equal ["can't be blank"], molecule.errors["electrons[1].name"]
assert_not_equal ["can't be blank"], molecule.errors["electrons.name"]
ensure
ActiveRecord.index_nested_attribute_errors = old_attribute_config
end
def test_errors_details_should_be_set
molecule = Molecule.new
valid_electron = Electron.new(name: "electron")
invalid_electron = Electron.new
molecule.electrons = [valid_electron, invalid_electron]
assert_not_predicate invalid_electron, :valid?
assert_predicate valid_electron, :valid?
assert_not_predicate molecule, :valid?
assert_equal [{ error: :blank }], molecule.errors.details[:"electrons.name"]
end
def test_errors_details_should_be_indexed_when_passed_as_array
guitar = Guitar.new
tuning_peg_valid = TuningPeg.new
tuning_peg_valid.pitch = 440.0
tuning_peg_invalid = TuningPeg.new
guitar.tuning_pegs = [tuning_peg_valid, tuning_peg_invalid]
assert_not_predicate tuning_peg_invalid, :valid?
assert_predicate tuning_peg_valid, :valid?
assert_not_predicate guitar, :valid?
assert_equal [{ error: :not_a_number, value: nil }], guitar.errors.details[:"tuning_pegs[1].pitch"]
assert_equal [], guitar.errors.details[:"tuning_pegs.pitch"]
end
def test_errors_details_should_be_indexed_when_global_flag_is_set
old_attribute_config = ActiveRecord.index_nested_attribute_errors
ActiveRecord.index_nested_attribute_errors = true
molecule = Molecule.new
valid_electron = Electron.new(name: "electron")
invalid_electron = Electron.new
molecule.electrons = [valid_electron, invalid_electron]
assert_not_predicate invalid_electron, :valid?
assert_predicate valid_electron, :valid?
assert_not_predicate molecule, :valid?
assert_equal [{ error: :blank }], molecule.errors.details[:"electrons[1].name"]
assert_equal [], molecule.errors.details[:"electrons.name"]
ensure
ActiveRecord.index_nested_attribute_errors = old_attribute_config
end
def test_valid_adding_with_nested_attributes
molecule = Molecule.new
valid_electron = Electron.new(name: "electron")
molecule.electrons = [valid_electron]
molecule.save
assert_predicate valid_electron, :valid?
assert_predicate molecule, :persisted?
assert_equal 1, molecule.electrons.count
end
end
class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCase
fixtures :companies, :developers
def test_invalid_adding
firm = Firm.find(1)
assert_not (firm.clients_of_firm << c = Client.new)
assert_not_predicate c, :persisted?
assert_not_predicate firm, :valid?
assert_not firm.save
assert_not_predicate c, :persisted?
end
def test_invalid_adding_before_save
new_firm = Firm.new("name" => "A New Firm, Inc")
new_firm.clients_of_firm.concat([c = Client.new, Client.new("name" => "Apple")])
assert_not_predicate c, :persisted?
assert_not_predicate c, :valid?
assert_not_predicate new_firm, :valid?
assert_not new_firm.save
assert_not_predicate c, :persisted?
assert_not_predicate new_firm, :persisted?
end
def test_adding_unsavable_association
new_firm = Firm.new("name" => "A New Firm, Inc")
client = new_firm.clients.new("name" => "Apple")
client.throw_on_save = true
assert_predicate client, :valid?
assert_predicate new_firm, :valid?
assert_not new_firm.save
assert_not_predicate new_firm, :persisted?
assert_not_predicate client, :persisted?
end
def test_invalid_adding_with_validate_false
firm = Firm.first
client = Client.new
firm.unvalidated_clients_of_firm << client
assert_predicate firm, :valid?
assert_not_predicate client, :valid?
assert firm.save
assert_not_predicate client, :persisted?
end
def test_valid_adding_with_validate_false
no_of_clients = Client.count
firm = Firm.first
client = Client.new("name" => "Apple")
assert_predicate firm, :valid?
assert_predicate client, :valid?
assert_not_predicate client, :persisted?
firm.unvalidated_clients_of_firm << client
assert firm.save
assert_predicate client, :persisted?
assert_equal no_of_clients + 1, Client.count
end
def test_parent_should_save_children_record_with_foreign_key_validation_set_in_before_save_callback
company = NewlyContractedCompany.new(name: "test")
assert company.save
assert_not_empty company.reload.new_contracts
end
def test_parent_should_not_get_saved_with_duplicate_children_records
assert_no_difference "Reply.count" do
assert_no_difference "SillyUniqueReply.count" do
reply = Reply.new
reply.silly_unique_replies.build([
{ content: "Best content" },
{ content: "Best content" }
])
assert_not reply.save
assert_equal ["is invalid"], reply.errors[:silly_unique_replies]
assert_empty reply.silly_unique_replies.first.errors
assert_equal(
["has already been taken"],
reply.silly_unique_replies.last.errors[:content]
)
end
end
end
def test_invalid_build
new_client = companies(:first_firm).clients_of_firm.build
assert_not_predicate new_client, :persisted?
assert_not_predicate new_client, :valid?
assert_equal new_client, companies(:first_firm).clients_of_firm.last
assert_not companies(:first_firm).save
assert_not_predicate new_client, :persisted?
assert_equal 2, companies(:first_firm).clients_of_firm.reload.size
end
def test_adding_before_save
no_of_firms = Firm.count
no_of_clients = Client.count
new_firm = Firm.new("name" => "A New Firm, Inc")
c = Client.new("name" => "Apple")
new_firm.clients_of_firm.push Client.new("name" => "Natural Company")
assert_equal 1, new_firm.clients_of_firm.size
new_firm.clients_of_firm << c
assert_equal 2, new_firm.clients_of_firm.size
assert_equal no_of_firms, Firm.count # Firm was not saved to database.
assert_equal no_of_clients, Client.count # Clients were not saved to database.
assert new_firm.save
assert_predicate new_firm, :persisted?
assert_predicate c, :persisted?
assert_equal new_firm, c.firm
assert_equal no_of_firms + 1, Firm.count # Firm was saved to database.
assert_equal no_of_clients + 2, Client.count # Clients were saved to database.
assert_equal 2, new_firm.clients_of_firm.size
assert_equal 2, new_firm.clients_of_firm.reload.size
end
def test_assign_ids
firm = Firm.new("name" => "Apple")
firm.client_ids = [companies(:first_client).id, companies(:second_client).id]
firm.save
firm.reload
assert_equal 2, firm.clients.length
assert_includes firm.clients, companies(:second_client)
end
def test_assign_ids_for_through_a_belongs_to
firm = Firm.new("name" => "Apple")
firm.developer_ids = [developers(:david).id, developers(:jamis).id]
firm.save
firm.reload
assert_equal 2, firm.developers.length
assert_includes firm.developers, developers(:david)
end
def test_build_before_save
company = companies(:first_firm)
new_client = assert_queries(0) { company.clients_of_firm.build("name" => "Another Client") }
assert_not_predicate company.clients_of_firm, :loaded?
company.name += "-changed"
assert_queries(2) { assert company.save }
assert_predicate new_client, :persisted?
assert_equal 3, company.clients_of_firm.reload.size
end
def test_build_many_before_save
company = companies(:first_firm)
assert_queries(0) { company.clients_of_firm.build([{ "name" => "Another Client" }, { "name" => "Another Client II" }]) }
company.name += "-changed"
assert_queries(3) { assert company.save }
assert_equal 4, company.clients_of_firm.reload.size
end
def test_build_via_block_before_save
company = companies(:first_firm)
new_client = assert_queries(0) { company.clients_of_firm.build { |client| client.name = "Another Client" } }
assert_not_predicate company.clients_of_firm, :loaded?
company.name += "-changed"
assert_queries(2) { assert company.save }
assert_predicate new_client, :persisted?
assert_equal 3, company.clients_of_firm.reload.size
end
def test_build_many_via_block_before_save
company = companies(:first_firm)
assert_queries(0) do
company.clients_of_firm.build([{ "name" => "Another Client" }, { "name" => "Another Client II" }]) do |client|
client.name = "changed"
end
end
company.name += "-changed"
assert_queries(3) { assert company.save }
assert_equal 4, company.clients_of_firm.reload.size
end
def test_replace_on_new_object
firm = Firm.new("name" => "New Firm")
firm.clients = [companies(:second_client), Client.new("name" => "New Client")]
assert firm.save
firm.reload
assert_equal 2, firm.clients.length
assert_includes firm.clients, Client.find_by_name("New Client")
end
def test_replace_on_duplicated_object
firm = Firm.create!("name" => "New Firm").dup
firm.clients = [companies(:second_client), Client.new("name" => "New Client")]
assert firm.save
firm.reload
assert_equal 2, firm.clients.length
assert_includes firm.clients, Client.find_by_name("New Client")
end
end
class TestDefaultAutosaveAssociationOnNewRecord < ActiveRecord::TestCase
def test_autosave_new_record_on_belongs_to_can_be_disabled_per_relationship
new_account = Account.new("credit_limit" => 1000)
new_firm = Firm.new("name" => "some firm")
assert_not_predicate new_firm, :persisted?
new_account.firm = new_firm
new_account.save!
assert_predicate new_firm, :persisted?
new_account = Account.new("credit_limit" => 1000)
new_autosaved_firm = Firm.new("name" => "some firm")
assert_not_predicate new_autosaved_firm, :persisted?
new_account.unautosaved_firm = new_autosaved_firm
new_account.save!
assert_not_predicate new_autosaved_firm, :persisted?
end
def test_autosave_new_record_on_has_one_can_be_disabled_per_relationship
firm = Firm.new("name" => "some firm")
account = Account.new("credit_limit" => 1000)
assert_not_predicate account, :persisted?
firm.account = account
firm.save!
assert_predicate account, :persisted?
firm = Firm.new("name" => "some firm")
account = Account.new("credit_limit" => 1000)
firm.unautosaved_account = account
assert_not_predicate account, :persisted?
firm.unautosaved_account = account
firm.save!
assert_not_predicate account, :persisted?
end
def test_autosave_new_record_on_has_many_can_be_disabled_per_relationship
firm = Firm.new("name" => "some firm")
account = Account.new("credit_limit" => 1000)
assert_not_predicate account, :persisted?
firm.accounts << account
firm.save!
assert_predicate account, :persisted?
firm = Firm.new("name" => "some firm")
account = Account.new("credit_limit" => 1000)
assert_not_predicate account, :persisted?
firm.unautosaved_accounts << account
firm.save!
assert_not_predicate account, :persisted?
end
def test_autosave_new_record_with_after_create_callback
post = PostWithAfterCreateCallback.new(title: "Captain Murphy", body: "is back")
post.comments.build(body: "foo")
post.save!
assert_not_nil post.author_id
end
def test_autosave_new_record_with_after_create_callback_and_habtm_association
post = PostWithAfterCreateCallback.new(title: "Captain Murphy", body: "is back")
post.comments.build(body: "foo")
post.categories.build(name: "bar")
post.save!
assert_equal 1, post.categories.reload.length
end
end
class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase
self.use_transactional_tests = false
setup do
@pirate = Pirate.create(catchphrase: "Don' botharrr talkin' like one, savvy?")
@ship = @pirate.create_ship(name: "Nights Dirty Lightning")
end
teardown do
# We are running without transactional tests and need to cleanup.
Bird.delete_all
Parrot.delete_all
@ship.delete
@pirate.delete
end
# reload
def test_a_marked_for_destruction_record_should_not_be_be_marked_after_reload
@pirate.mark_for_destruction
@pirate.ship.mark_for_destruction
assert_not_predicate @pirate.reload, :marked_for_destruction?
assert_not_predicate @pirate.ship.reload, :marked_for_destruction?
end
# has_one
def test_should_destroy_a_child_association_as_part_of_the_save_transaction_if_it_was_marked_for_destruction
assert_not_predicate @pirate.ship, :marked_for_destruction?
@pirate.ship.mark_for_destruction
id = @pirate.ship.id
assert_predicate @pirate.ship, :marked_for_destruction?
assert Ship.find_by_id(id)
@pirate.save
assert_nil @pirate.reload.ship
assert_nil Ship.find_by_id(id)
end
def test_should_skip_validation_on_a_child_association_if_marked_for_destruction
@pirate.ship.name = ""
assert_not_predicate @pirate, :valid?
@pirate.ship.mark_for_destruction
assert_not_called(@pirate.ship, :valid?) do
assert_difference("Ship.count", -1) { @pirate.save! }
end
end
def test_a_child_marked_for_destruction_should_not_be_destroyed_twice
@pirate.ship.mark_for_destruction
assert @pirate.save
class << @pirate.ship
def destroy; raise "Should not be called" end
end
assert @pirate.save
end
def test_should_rollback_destructions_if_an_exception_occurred_while_saving_a_child
# Stub the save method of the @pirate.ship instance to destroy and then raise an exception
class << @pirate.ship
def save(**)
super
destroy
raise "Oh noes!"
end
end
@ship.pirate.catchphrase = "Changed Catchphrase"
@ship.name_will_change!
assert_raise(RuntimeError) { assert_not @pirate.save }
assert_not_nil @pirate.reload.ship
end
def test_should_save_changed_has_one_changed_object_if_child_is_saved
@pirate.ship.name = "NewName"
assert @pirate.save
assert_equal "NewName", @pirate.ship.reload.name
end
def test_should_not_save_changed_has_one_unchanged_object_if_child_is_saved
assert_not_called(@pirate.ship, :save) do
assert @pirate.save
end
end
# belongs_to
def test_should_destroy_a_parent_association_as_part_of_the_save_transaction_if_it_was_marked_for_destruction
assert_not_predicate @ship.pirate, :marked_for_destruction?
@ship.pirate.mark_for_destruction
id = @ship.pirate.id
assert_predicate @ship.pirate, :marked_for_destruction?
assert Pirate.find_by_id(id)
@ship.save
assert_nil @ship.reload.pirate
assert_nil Pirate.find_by_id(id)
end
def test_should_skip_validation_on_a_parent_association_if_marked_for_destruction
@ship.pirate.catchphrase = ""
assert_not_predicate @ship, :valid?
@ship.pirate.mark_for_destruction
assert_not_called(@ship.pirate, :valid?) do
assert_difference("Pirate.count", -1) { @ship.save! }
end
end
def test_a_parent_marked_for_destruction_should_not_be_destroyed_twice
@ship.pirate.mark_for_destruction
assert @ship.save
class << @ship.pirate
def destroy; raise "Should not be called" end
end
assert @ship.save
end
def test_should_rollback_destructions_if_an_exception_occurred_while_saving_a_parent
# Stub the save method of the @ship.pirate instance to destroy and then raise an exception
class << @ship.pirate
def save(**)
super
destroy
raise "Oh noes!"
end
end
@ship.pirate.catchphrase = "Changed Catchphrase"
assert_raise(RuntimeError) { assert_not @ship.save }
assert_not_nil @ship.reload.pirate
end
def test_should_save_changed_child_objects_if_parent_is_saved
@pirate = @ship.create_pirate(catchphrase: "Don' botharrr talkin' like one, savvy?")
@parrot = @pirate.parrots.create!(name: "Posideons Killer")
@parrot.name = "NewName"
@ship.save
assert_equal "NewName", @parrot.reload.name
end
def test_should_destroy_has_many_as_part_of_the_save_transaction_if_they_were_marked_for_destruction
2.times { |i| @pirate.birds.create!(name: "birds_#{i}") }
assert_not @pirate.birds.any?(&:marked_for_destruction?)
@pirate.birds.each(&:mark_for_destruction)
klass = @pirate.birds.first.class
ids = @pirate.birds.map(&:id)
assert @pirate.birds.all?(&:marked_for_destruction?)
ids.each { |id| assert klass.find_by_id(id) }
@pirate.save
assert_empty @pirate.reload.birds
ids.each { |id| assert_nil klass.find_by_id(id) }
end
def test_should_not_resave_destroyed_association
@pirate.birds.create!(name: :parrot)
@pirate.birds.first.destroy
@pirate.save!
assert_empty @pirate.reload.birds
end
def test_should_skip_validation_on_has_many_if_marked_for_destruction
2.times { |i| @pirate.birds.create!(name: "birds_#{i}") }
@pirate.birds.each { |bird| bird.name = "" }
assert_not_predicate @pirate, :valid?
@pirate.birds.each(&:mark_for_destruction)
assert_not_called(@pirate.birds.first, :valid?) do
assert_not_called(@pirate.birds.last, :valid?) do
assert_difference("Bird.count", -2) { @pirate.save! }
end
end
end
def test_should_skip_validation_on_has_many_if_destroyed
@pirate.birds.create!(name: "birds_1")
@pirate.birds.each { |bird| bird.name = "" }
assert_not_predicate @pirate, :valid?
@pirate.birds.each(&:destroy)
assert_predicate @pirate, :valid?
end
def test_a_child_marked_for_destruction_should_not_be_destroyed_twice_while_saving_has_many
@pirate.birds.create!(name: "birds_1")
@pirate.birds.each(&:mark_for_destruction)
assert @pirate.save
@pirate.birds.each do |bird|
assert_not_called(bird, :destroy) do
assert @pirate.save
end
end
end
def test_should_rollback_destructions_if_an_exception_occurred_while_saving_has_many
2.times { |i| @pirate.birds.create!(name: "birds_#{i}") }
before = @pirate.birds.map { |c| c.mark_for_destruction ; c }
# Stub the destroy method of the second child to raise an exception
class << before.last
def destroy(*args)
super
raise "Oh noes!"
end
end
assert_raise(RuntimeError) { assert_not @pirate.save }
assert_equal before, @pirate.reload.birds
end
def test_when_new_record_a_child_marked_for_destruction_should_not_affect_other_records_from_saving
@pirate = @ship.build_pirate(catchphrase: "Arr' now I shall keep me eye on you matey!") # new record
3.times { |i| @pirate.birds.build(name: "birds_#{i}") }
@pirate.birds[1].mark_for_destruction
@pirate.save!
assert_equal 2, @pirate.birds.reload.length
end
def test_should_save_new_record_that_has_same_value_as_existing_record_marked_for_destruction_on_field_that_has_unique_index
Bird.connection.add_index :birds, :name, unique: true
3.times { |i| @pirate.birds.create(name: "unique_birds_#{i}") }
@pirate.birds[0].mark_for_destruction
@pirate.birds.build(name: @pirate.birds[0].name)
@pirate.save!
assert_equal 3, @pirate.birds.reload.length
ensure
Bird.connection.remove_index :birds, column: :name
end
# Add and remove callbacks tests for association collections.
%w{ method proc }.each do |callback_type|
define_method("test_should_run_add_callback_#{callback_type}s_for_has_many") do
association_name_with_callbacks = "birds_with_#{callback_type}_callbacks"
pirate = Pirate.new(catchphrase: "Arr")
pirate.public_send(association_name_with_callbacks).build(name: "Crowe the One-Eyed")
expected = [
"before_adding_#{callback_type}_bird_<new>",
"after_adding_#{callback_type}_bird_<new>"
]
assert_equal expected, pirate.ship_log
end
define_method("test_should_run_remove_callback_#{callback_type}s_for_has_many") do
association_name_with_callbacks = "birds_with_#{callback_type}_callbacks"
@pirate.public_send(association_name_with_callbacks).create!(name: "Crowe the One-Eyed")
@pirate.public_send(association_name_with_callbacks).each(&:mark_for_destruction)
child_id = @pirate.public_send(association_name_with_callbacks).first.id
@pirate.ship_log.clear
@pirate.save
expected = [
"before_removing_#{callback_type}_bird_#{child_id}",
"after_removing_#{callback_type}_bird_#{child_id}"
]
assert_equal expected, @pirate.ship_log
end
end
def test_should_destroy_habtm_as_part_of_the_save_transaction_if_they_were_marked_for_destruction
2.times { |i| @pirate.parrots.create!(name: "parrots_#{i}") }
assert_not @pirate.parrots.any?(&:marked_for_destruction?)
@pirate.parrots.each(&:mark_for_destruction)
assert_no_difference "Parrot.count" do
@pirate.save
end
assert_empty @pirate.reload.parrots
join_records = Pirate.connection.select_all("SELECT * FROM parrots_pirates WHERE pirate_id = #{@pirate.id}")
assert_empty join_records
end
def test_should_skip_validation_on_habtm_if_marked_for_destruction
2.times { |i| @pirate.parrots.create!(name: "parrots_#{i}") }
@pirate.parrots.each { |parrot| parrot.name = "" }
assert_not_predicate @pirate, :valid?
@pirate.parrots.each { |parrot| parrot.mark_for_destruction }
assert_not_called(@pirate.parrots.first, :valid?) do
assert_not_called(@pirate.parrots.last, :valid?) do
@pirate.save!
end
end
assert_empty @pirate.reload.parrots
end
def test_should_skip_validation_on_habtm_if_destroyed
@pirate.parrots.create!(name: "parrots_1")
@pirate.parrots.each { |parrot| parrot.name = "" }
assert_not_predicate @pirate, :valid?
@pirate.parrots.each(&:destroy)
assert_predicate @pirate, :valid?
end
def test_a_child_marked_for_destruction_should_not_be_destroyed_twice_while_saving_habtm
@pirate.parrots.create!(name: "parrots_1")
@pirate.parrots.each(&:mark_for_destruction)
assert @pirate.save
Pirate.transaction do
assert_no_queries do
assert @pirate.save
end
end
end
def test_should_rollback_destructions_if_an_exception_occurred_while_saving_habtm
2.times { |i| @pirate.parrots.create!(name: "parrots_#{i}") }
before = @pirate.parrots.map { |c| c.mark_for_destruction ; c }
class << @pirate.association(:parrots)
def destroy(*args)
super
raise "Oh noes!"
end
end
assert_raise(RuntimeError) { assert_not @pirate.save }
assert_equal before, @pirate.reload.parrots
end
# Add and remove callbacks tests for association collections.
%w{ method proc }.each do |callback_type|
define_method("test_should_run_add_callback_#{callback_type}s_for_habtm") do
association_name_with_callbacks = "parrots_with_#{callback_type}_callbacks"
pirate = Pirate.new(catchphrase: "Arr")
pirate.public_send(association_name_with_callbacks).build(name: "Crowe the One-Eyed")
expected = [
"before_adding_#{callback_type}_parrot_<new>",
"after_adding_#{callback_type}_parrot_<new>"
]
assert_equal expected, pirate.ship_log
end
define_method("test_should_run_remove_callback_#{callback_type}s_for_habtm") do
association_name_with_callbacks = "parrots_with_#{callback_type}_callbacks"
@pirate.public_send(association_name_with_callbacks).create!(name: "Crowe the One-Eyed")
@pirate.public_send(association_name_with_callbacks).each(&:mark_for_destruction)
child_id = @pirate.public_send(association_name_with_callbacks).first.id
@pirate.ship_log.clear
@pirate.save
expected = [
"before_removing_#{callback_type}_parrot_#{child_id}",
"after_removing_#{callback_type}_parrot_#{child_id}"
]
assert_equal expected, @pirate.ship_log
end
end
end
class TestAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCase
self.use_transactional_tests = false unless supports_savepoints?
def setup
super
@pirate = Pirate.create(catchphrase: "Don' botharrr talkin' like one, savvy?")
@ship = @pirate.create_ship(name: "Nights Dirty Lightning")
end
def test_should_still_work_without_an_associated_model
@ship.destroy
@pirate.reload.catchphrase = "Arr"
@pirate.save
assert_equal "Arr", @pirate.reload.catchphrase
end
def test_should_automatically_save_the_associated_model
@pirate.ship.name = "The Vile Serpent"
@pirate.save
assert_equal "The Vile Serpent", @pirate.reload.ship.name
end
def test_changed_for_autosave_should_handle_cycles
@ship.pirate = @pirate
assert_no_queries { @ship.save! }
@parrot = @pirate.parrots.create(name: "some_name")
@parrot.name = "changed_name"
assert_queries(1) { @ship.save! }
assert_no_queries { @ship.save! }
end
def test_should_automatically_save_bang_the_associated_model
@pirate.ship.name = "The Vile Serpent"
@pirate.save!
assert_equal "The Vile Serpent", @pirate.reload.ship.name
end
def test_should_automatically_validate_the_associated_model
@pirate.ship.name = ""
assert_predicate @pirate, :invalid?
assert_predicate @pirate.errors[:"ship.name"], :any?
end
def test_should_merge_errors_on_the_associated_models_onto_the_parent_even_if_it_is_not_valid
@pirate.ship.name = nil
@pirate.catchphrase = nil
assert_predicate @pirate, :invalid?
assert_predicate @pirate.errors[:"ship.name"], :any?
assert_predicate @pirate.errors[:catchphrase], :any?
end
def test_should_not_ignore_different_error_messages_on_the_same_attribute
old_validators = Ship._validators.deep_dup
old_callbacks = Ship._validate_callbacks.deep_dup
Ship.validates_format_of :name, with: /\w/
@pirate.ship.name = ""
@pirate.catchphrase = nil
assert_predicate @pirate, :invalid?
assert_equal ["can't be blank", "is invalid"], @pirate.errors[:"ship.name"]
ensure
Ship._validators = old_validators if old_validators
Ship._validate_callbacks = old_callbacks if old_callbacks
end
def test_should_still_allow_to_bypass_validations_on_the_associated_model
@pirate.catchphrase = ""
@pirate.ship.name = ""
@pirate.save(validate: false)
# Oracle saves empty string as NULL
if current_adapter?(:OracleAdapter)
assert_equal [nil, nil], [@pirate.reload.catchphrase, @pirate.ship.name]
else
assert_equal ["", ""], [@pirate.reload.catchphrase, @pirate.ship.name]
end
end
def test_should_allow_to_bypass_validations_on_associated_models_at_any_depth
2.times { |i| @pirate.ship.parts.create!(name: "part #{i}") }
@pirate.catchphrase = ""
@pirate.ship.name = ""
@pirate.ship.parts.each { |part| part.name = "" }
@pirate.save(validate: false)
values = [@pirate.reload.catchphrase, @pirate.ship.name, *@pirate.ship.parts.map(&:name)]
# Oracle saves empty string as NULL
if current_adapter?(:OracleAdapter)
assert_equal [nil, nil, nil, nil], values
else
assert_equal ["", "", "", ""], values
end
end
def test_should_still_raise_an_ActiveRecordRecord_Invalid_exception_if_we_want_that
@pirate.ship.name = ""
assert_raise(ActiveRecord::RecordInvalid) do
@pirate.save!
end
end
def test_should_not_save_and_return_false_if_a_callback_cancelled_saving
pirate = Pirate.new(catchphrase: "Arr")
ship = pirate.build_ship(name: "The Vile Serpent")
ship.cancel_save_from_callback = true
assert_no_difference "Pirate.count" do
assert_no_difference "Ship.count" do
assert_not pirate.save
end
end
end
def test_should_rollback_any_changes_if_an_exception_occurred_while_saving
before = [@pirate.catchphrase, @pirate.ship.name]
@pirate.catchphrase = "Arr"
@pirate.ship.name = "The Vile Serpent"
# Stub the save method of the @pirate.ship instance to raise an exception
class << @pirate.ship
def save(**)
super
raise "Oh noes!"
end
end
assert_raise(RuntimeError) { assert_not @pirate.save }
assert_equal before, [@pirate.reload.catchphrase, @pirate.ship.name]
end
def test_should_not_load_the_associated_model
assert_queries(1) { @pirate.catchphrase = "Arr"; @pirate.save! }
end
def test_mark_for_destruction_is_ignored_without_autosave_true
ship = ShipWithoutNestedAttributes.new(name: "The Black Flag")
ship.parts.build.mark_for_destruction
assert_not_predicate ship, :valid?
end
end
class TestAutosaveAssociationOnAHasOneThroughAssociation < ActiveRecord::TestCase
self.use_transactional_tests = false unless supports_savepoints?
def create_member_with_organization
organization = Organization.create
member = Member.create
MemberDetail.create(organization: organization, member: member)
member
end
def test_should_not_has_one_through_model
member = create_member_with_organization
class << member.organization
def save(**)
super
raise "Oh noes!"
end
end
assert_nothing_raised { member.save }
end
def create_author_with_post_with_comment
Author.create! name: "David" # make comment_id not match author_id
author = Author.create! name: "Sergiy"
post = Post.create! author: author, title: "foo", body: "bar"
Comment.create! post: post, body: "cool comment"
author
end
def test_should_not_reversed_has_one_through_model
author = create_author_with_post_with_comment
class << author.comment_on_first_post
def save(**)
super
raise "Oh noes!"
end
end
assert_nothing_raised { author.save }
end
end
class TestAutosaveAssociationOnABelongsToAssociation < ActiveRecord::TestCase
self.use_transactional_tests = false unless supports_savepoints?
def setup
super
@ship = Ship.create(name: "Nights Dirty Lightning")
@pirate = @ship.create_pirate(catchphrase: "Don' botharrr talkin' like one, savvy?")
end
def test_should_still_work_without_an_associated_model
@pirate.destroy
@ship.reload.name = "The Vile Serpent"
@ship.save
assert_equal "The Vile Serpent", @ship.reload.name
end
def test_should_automatically_save_the_associated_model
@ship.pirate.catchphrase = "Arr"
@ship.save
assert_equal "Arr", @ship.reload.pirate.catchphrase
end
def test_should_automatically_save_bang_the_associated_model
@ship.pirate.catchphrase = "Arr"
@ship.save!
assert_equal "Arr", @ship.reload.pirate.catchphrase
end
def test_should_automatically_validate_the_associated_model
@ship.pirate.catchphrase = ""
assert_predicate @ship, :invalid?
assert_predicate @ship.errors[:"pirate.catchphrase"], :any?
end
def test_should_merge_errors_on_the_associated_model_onto_the_parent_even_if_it_is_not_valid
@ship.name = nil
@ship.pirate.catchphrase = nil
assert_predicate @ship, :invalid?
assert_predicate @ship.errors[:name], :any?
assert_predicate @ship.errors[:"pirate.catchphrase"], :any?
end
def test_should_still_allow_to_bypass_validations_on_the_associated_model
@ship.pirate.catchphrase = ""
@ship.name = ""
@ship.save(validate: false)
# Oracle saves empty string as NULL
if current_adapter?(:OracleAdapter)
assert_equal [nil, nil], [@ship.reload.name, @ship.pirate.catchphrase]
else
assert_equal ["", ""], [@ship.reload.name, @ship.pirate.catchphrase]
end
end
def test_should_still_raise_an_ActiveRecordRecord_Invalid_exception_if_we_want_that
@ship.pirate.catchphrase = ""
assert_raise(ActiveRecord::RecordInvalid) do
@ship.save!
end
end
def test_should_not_save_and_return_false_if_a_callback_cancelled_saving
ship = Ship.new(name: "The Vile Serpent")
pirate = ship.build_pirate(catchphrase: "Arr")
pirate.cancel_save_from_callback = true
assert_no_difference "Ship.count" do
assert_no_difference "Pirate.count" do
assert_not ship.save
end
end
end
def test_should_rollback_any_changes_if_an_exception_occurred_while_saving
before = [@ship.pirate.catchphrase, @ship.name]
@ship.pirate.catchphrase = "Arr"
@ship.name = "The Vile Serpent"
# Stub the save method of the @ship.pirate instance to raise an exception
class << @ship.pirate
def save(**)
super
raise "Oh noes!"
end
end
assert_raise(RuntimeError) { assert_not @ship.save }
assert_equal before, [@ship.pirate.reload.catchphrase, @ship.reload.name]
end
def test_should_not_load_the_associated_model
assert_queries(1) { @ship.name = "The Vile Serpent"; @ship.save! }
end
def test_should_save_with_non_nullable_foreign_keys
parent = Post.new title: "foo", body: "..."
child = parent.comments.build body: "..."
child.save!
assert_equal child.reload.post, parent.reload
end
end
module AutosaveAssociationOnACollectionAssociationTests
def test_should_automatically_save_the_associated_models
new_names = ["Grace OMalley", "Privateers Greed"]
@pirate.public_send(@association_name).each_with_index { |child, i| child.name = new_names[i] }
@pirate.save
assert_equal new_names.sort, @pirate.reload.public_send(@association_name).map(&:name).sort
end
def test_should_automatically_save_bang_the_associated_models
new_names = ["Grace OMalley", "Privateers Greed"]
@pirate.public_send(@association_name).each_with_index { |child, i| child.name = new_names[i] }
@pirate.save!
assert_equal new_names.sort, @pirate.reload.public_send(@association_name).map(&:name).sort
end
def test_should_update_children_when_autosave_is_true_and_parent_is_new_but_child_is_not
parrot = Parrot.create!(name: "Polly")
parrot.name = "Squawky"
pirate = Pirate.new(parrots: [parrot], catchphrase: "Arrrr")
pirate.save!
assert_equal "Squawky", parrot.reload.name
end
def test_should_not_update_children_when_parent_creation_with_no_reason
parrot = Parrot.create!(name: "Polly")
assert_equal 0, parrot.updated_count
Pirate.create!(parrot_ids: [parrot.id], catchphrase: "Arrrr")
assert_equal 0, parrot.reload.updated_count
end
def test_should_automatically_validate_the_associated_models
@pirate.public_send(@association_name).each { |child| child.name = "" }
assert_not_predicate @pirate, :valid?
assert_equal ["can't be blank"], @pirate.errors["#{@association_name}.name"]
assert_empty @pirate.errors[@association_name]
end
def test_should_not_use_default_invalid_error_on_associated_models
@pirate.public_send(@association_name).build(name: "")
assert_not_predicate @pirate, :valid?
assert_equal ["can't be blank"], @pirate.errors["#{@association_name}.name"]
assert_empty @pirate.errors[@association_name]
end
def test_should_default_invalid_error_from_i18n
I18n.backend.store_translations(:en, activerecord: { errors: { models:
{ @associated_model_name.to_s.to_sym => { blank: "cannot be blank" } }
} })
@pirate.public_send(@association_name).build(name: "")
assert_not_predicate @pirate, :valid?
assert_equal ["cannot be blank"], @pirate.errors["#{@association_name}.name"]
assert_equal ["#{@association_name.to_s.humanize} name cannot be blank"], @pirate.errors.full_messages
assert_empty @pirate.errors[@association_name]
ensure
I18n.backend = I18n::Backend::Simple.new
end
def test_should_merge_errors_on_the_associated_models_onto_the_parent_even_if_it_is_not_valid
@pirate.public_send(@association_name).each { |child| child.name = "" }
@pirate.catchphrase = nil
assert_not_predicate @pirate, :valid?
assert_equal ["can't be blank"], @pirate.errors["#{@association_name}.name"]
assert_predicate @pirate.errors[:catchphrase], :any?
end
def test_should_allow_to_bypass_validations_on_the_associated_models_on_update
@pirate.catchphrase = ""
@pirate.public_send(@association_name).each { |child| child.name = "" }
assert @pirate.save(validate: false)
# Oracle saves empty string as NULL
if current_adapter?(:OracleAdapter)
assert_equal [nil, nil, nil], [
@pirate.reload.catchphrase,
@pirate.public_send(@association_name).first.name,
@pirate.public_send(@association_name).last.name
]
else
assert_equal ["", "", ""], [
@pirate.reload.catchphrase,
@pirate.public_send(@association_name).first.name,
@pirate.public_send(@association_name).last.name
]
end
end
def test_should_validation_the_associated_models_on_create
assert_no_difference("#{ @association_name == :birds ? 'Bird' : 'Parrot' }.count") do
2.times { @pirate.public_send(@association_name).build }
@pirate.save
end
end
def test_should_allow_to_bypass_validations_on_the_associated_models_on_create
assert_difference("#{ @association_name == :birds ? 'Bird' : 'Parrot' }.count", 2) do
2.times { @pirate.public_send(@association_name).build }
@pirate.save(validate: false)
end
end
def test_should_not_save_and_return_false_if_a_callback_cancelled_saving_in_either_create_or_update
@pirate.catchphrase = "Changed"
@child_1.name = "Changed"
@child_1.cancel_save_from_callback = true
assert_not @pirate.save
assert_equal "Don' botharrr talkin' like one, savvy?", @pirate.reload.catchphrase
assert_equal "Posideons Killer", @child_1.reload.name
new_pirate = Pirate.new(catchphrase: "Arr")
new_child = new_pirate.public_send(@association_name).build(name: "Grace OMalley")
new_child.cancel_save_from_callback = true
assert_no_difference "Pirate.count" do
assert_no_difference "#{new_child.class.name}.count" do
assert_not new_pirate.save
end
end
end
def test_should_rollback_any_changes_if_an_exception_occurred_while_saving
before = [@pirate.catchphrase, *@pirate.public_send(@association_name).map(&:name)]
new_names = ["Grace OMalley", "Privateers Greed"]
@pirate.catchphrase = "Arr"
@pirate.public_send(@association_name).each_with_index { |child, i| child.name = new_names[i] }
# Stub the save method of the first child instance to raise an exception
class << @pirate.public_send(@association_name).first
def save(**)
super
raise "Oh noes!"
end
end
assert_raise(RuntimeError) { assert_not @pirate.save }
assert_equal before, [@pirate.reload.catchphrase, *@pirate.send(@association_name).map(&:name)]
end
def test_should_still_raise_an_ActiveRecordRecord_Invalid_exception_if_we_want_that
@pirate.public_send(@association_name).each { |child| child.name = "" }
assert_raise(ActiveRecord::RecordInvalid) do
@pirate.save!
end
end
def test_should_not_load_the_associated_models_if_they_were_not_loaded_yet
assert_queries(1) { @pirate.catchphrase = "Arr"; @pirate.save! }
@pirate.public_send(@association_name).load_target
assert_queries(3) do
@pirate.catchphrase = "Yarr"
new_names = ["Grace OMalley", "Privateers Greed"]
@pirate.public_send(@association_name).each_with_index { |child, i| child.name = new_names[i] }
@pirate.save!
end
end
end
class TestAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCase
self.use_transactional_tests = false unless supports_savepoints?
def setup
super
@association_name = :birds
@associated_model_name = :bird
@pirate = Pirate.create(catchphrase: "Don' botharrr talkin' like one, savvy?")
@child_1 = @pirate.birds.create(name: "Posideons Killer")
@child_2 = @pirate.birds.create(name: "Killer bandita Dionne")
end
include AutosaveAssociationOnACollectionAssociationTests
end
class TestAutosaveAssociationOnAHasAndBelongsToManyAssociation < ActiveRecord::TestCase
self.use_transactional_tests = false unless supports_savepoints?
def setup
super
@association_name = :autosaved_parrots
@associated_model_name = :parrot
@habtm = true
@pirate = Pirate.create(catchphrase: "Don' botharrr talkin' like one, savvy?")
@child_1 = @pirate.parrots.create(name: "Posideons Killer")
@child_2 = @pirate.parrots.create(name: "Killer bandita Dionne")
end
include AutosaveAssociationOnACollectionAssociationTests
end
class TestAutosaveAssociationOnAHasAndBelongsToManyAssociationWithAcceptsNestedAttributes < ActiveRecord::TestCase
self.use_transactional_tests = false unless supports_savepoints?
def setup
super
@association_name = :parrots
@associated_model_name = :parrot
@habtm = true
@pirate = Pirate.create(catchphrase: "Don' botharrr talkin' like one, savvy?")
@child_1 = @pirate.parrots.create(name: "Posideons Killer")
@child_2 = @pirate.parrots.create(name: "Killer bandita Dionne")
end
include AutosaveAssociationOnACollectionAssociationTests
end
class TestAutosaveAssociationValidationsOnAHasManyAssociation < ActiveRecord::TestCase
self.use_transactional_tests = false unless supports_savepoints?
def setup
super
@pirate = Pirate.create(catchphrase: "Don' botharrr talkin' like one, savvy?")
@pirate.birds.create(name: "cookoo")
@author = Author.new(name: "DHH")
@author.published_books.build(name: "Rework", isbn: "1234")
@author.published_books.build(name: "Remote", isbn: "1234")
end
test "should automatically validate associations" do
assert_predicate @pirate, :valid?
@pirate.birds.each { |bird| bird.name = "" }
assert_not_predicate @pirate, :valid?
end
test "rollbacks whole transaction and raises ActiveRecord::RecordInvalid when associations fail to #save! due to uniqueness validation failure" do
author_count_before_save = Author.count
book_count_before_save = Book.count
assert_no_difference "Author.count" do
assert_no_difference "Book.count" do
exception = assert_raises(ActiveRecord::RecordInvalid) do
@author.save!
end
assert_equal("Validation failed: Published books is invalid", exception.message)
end
end
assert_equal(author_count_before_save, Author.count)
assert_equal(book_count_before_save, Book.count)
end
test "rollbacks whole transaction when associations fail to #save due to uniqueness validation failure" do
author_count_before_save = Author.count
book_count_before_save = Book.count
assert_no_difference "Author.count" do
assert_no_difference "Book.count" do
assert_nothing_raised do
result = @author.save
assert_not(result)
end
end
end
assert_equal(author_count_before_save, Author.count)
assert_equal(book_count_before_save, Book.count)
end
def test_validations_still_fire_on_unchanged_association_with_custom_validation_context
pirate = FamousPirate.create!(catchphrase: "Avast Ye!")
pirate.famous_ships.create!
assert pirate.valid?
assert_not pirate.valid?(:conference)
end
end
class TestAutosaveAssociationValidationsOnAHasOneAssociation < ActiveRecord::TestCase
self.use_transactional_tests = false unless supports_savepoints?
def setup
super
@pirate = Pirate.create(catchphrase: "Don' botharrr talkin' like one, savvy?")
@pirate.create_ship(name: "titanic")
super
end
test "should automatically validate associations with :validate => true" do
assert_predicate @pirate, :valid?
@pirate.ship.name = ""
assert_not_predicate @pirate, :valid?
end
test "should not automatically add validate associations without :validate => true" do
assert_predicate @pirate, :valid?
@pirate.non_validated_ship.name = ""
assert_predicate @pirate, :valid?
end
end
class TestAutosaveAssociationValidationsOnABelongsToAssociation < ActiveRecord::TestCase
self.use_transactional_tests = false unless supports_savepoints?
def setup
super
@pirate = Pirate.create(catchphrase: "Don' botharrr talkin' like one, savvy?")
end
test "should automatically validate associations with :validate => true" do
assert_predicate @pirate, :valid?
@pirate.parrot = Parrot.new(name: "")
assert_not_predicate @pirate, :valid?
end
test "should not automatically validate associations without :validate => true" do
assert_predicate @pirate, :valid?
@pirate.non_validated_parrot = Parrot.new(name: "")
assert_predicate @pirate, :valid?
end
def test_validations_still_fire_on_unchanged_association_with_custom_validation_context
firm_with_low_credit = Firm.create!(name: "Something", account: Account.new(credit_limit: 50))
assert firm_with_low_credit.valid?
assert_not firm_with_low_credit.valid?(:bank_loan)
end
end
class TestAutosaveAssociationValidationsOnAHABTMAssociation < ActiveRecord::TestCase
self.use_transactional_tests = false unless supports_savepoints?
def setup
super
@pirate = Pirate.create(catchphrase: "Don' botharrr talkin' like one, savvy?")
end
test "should automatically validate associations with :validate => true" do
assert_predicate @pirate, :valid?
@pirate.parrots = [ Parrot.new(name: "popuga") ]
@pirate.parrots.each { |parrot| parrot.name = "" }
assert_not_predicate @pirate, :valid?
end
test "should not automatically validate associations without :validate => true" do
assert_predicate @pirate, :valid?
@pirate.non_validated_parrots = [ Parrot.new(name: "popuga") ]
@pirate.non_validated_parrots.each { |parrot| parrot.name = "" }
assert_predicate @pirate, :valid?
end
end
class TestAutosaveAssociationValidationMethodsGeneration < ActiveRecord::TestCase
self.use_transactional_tests = false unless supports_savepoints?
def setup
super
@pirate = Pirate.new
end
test "should generate validation methods for has_many associations" do
assert_respond_to @pirate, :validate_associated_records_for_birds
end
test "should generate validation methods for has_one associations with :validate => true" do
assert_respond_to @pirate, :validate_associated_records_for_ship
end
test "should not generate validation methods for has_one associations without :validate => true" do
assert_not_respond_to @pirate, :validate_associated_records_for_non_validated_ship
end
test "should generate validation methods for belongs_to associations with :validate => true" do
assert_respond_to @pirate, :validate_associated_records_for_parrot
end
test "should not generate validation methods for belongs_to associations without :validate => true" do
assert_not_respond_to @pirate, :validate_associated_records_for_non_validated_parrot
end
test "should generate validation methods for HABTM associations with :validate => true" do
assert_respond_to @pirate, :validate_associated_records_for_parrots
end
end
class TestAutosaveAssociationWithTouch < ActiveRecord::TestCase
def test_autosave_with_touch_should_not_raise_system_stack_error
invoice = Invoice.create
assert_nothing_raised { invoice.line_items.create(amount: 10) }
end
end
class TestAutosaveAssociationOnAHasManyAssociationWithInverse < ActiveRecord::TestCase
class Post < ActiveRecord::Base
has_many :comments, inverse_of: :post
end
class Comment < ActiveRecord::Base
belongs_to :post, inverse_of: :comments
attr_accessor :post_comments_count
after_save do
self.post_comments_count = post.comments.count
end
end
def setup
Comment.delete_all
end
def test_after_save_callback_with_autosave
post = Post.new(title: "Test", body: "...")
comment = post.comments.build(body: "...")
post.save!
assert_equal 1, post.comments.count
assert_equal 1, comment.post_comments_count
end
end
class TestAutosaveAssociationOnAHasManyAssociationDefinedInSubclassWithAcceptsNestedAttributes < ActiveRecord::TestCase
def test_should_update_children_when_association_redefined_in_subclass
agency = Agency.create!(name: "Agency")
valid_project = Project.create!(firm: agency, name: "Initial")
agency.update!(
"projects_attributes" => {
"0" => {
"name" => "Updated",
"id" => valid_project.id
}
}
)
valid_project.reload
assert_equal "Updated", valid_project.name
end
end
class TestAutosaveAssociationOnABelongsToAssociationDefinedAsRecord < ActiveRecord::TestCase
def test_should_not_raise_error
translation = Translation.create(locale: "fr", key: "bread", value: "Baguette 🥖")
author = Author.create(name: "Dorian Marié")
translation.build_attachment(record: author)
assert_nothing_raised do
translation.save!
end
end
end
# frozen_string_literal: true
require "active_support/benchmarkable"
require "active_support/dependencies"
require "active_support/descendants_tracker"
require "active_support/time"
require "active_support/core_ext/class/subclasses"
require "active_record/log_subscriber"
require "active_record/explain_subscriber"
require "active_record/relation/delegation"
require "active_record/attributes"
require "active_record/type_caster"
require "active_record/database_configurations"
module ActiveRecord # :nodoc:
# = Active Record
#
# Active Record objects don't specify their attributes directly, but rather infer them from
# the table definition with which they're linked. Adding, removing, and changing attributes
# and their type is done directly in the database. Any change is instantly reflected in the
# Active Record objects. The mapping that binds a given Active Record class to a certain
# database table will happen automatically in most common cases, but can be overwritten for the uncommon ones.
#
# See the mapping rules in table_name and the full example in link:files/activerecord/README_rdoc.html for more insight.
#
# == Creation
#
# Active Records accept constructor parameters either in a hash or as a block. The hash
# method is especially useful when you're receiving the data from somewhere else, like an
# HTTP request. It works like this:
#
# user = User.new(name: "David", occupation: "Code Artist")
# user.name # => "David"
#
# You can also use block initialization:
#
# user = User.new do |u|
# u.name = "David"
# u.occupation = "Code Artist"
# end
#
# And of course you can just create a bare object and specify the attributes after the fact:
#
# user = User.new
# user.name = "David"
# user.occupation = "Code Artist"
#
# == Conditions
#
# Conditions can either be specified as a string, array, or hash representing the WHERE-part of an SQL statement.
# The array form is to be used when the condition input is tainted and requires sanitization. The string form can
# be used for statements that don't involve tainted data. The hash form works much like the array form, except
# only equality and range is possible. Examples:
#
# class User < ActiveRecord::Base
# def self.authenticate_unsafely(user_name, password)
# where("user_name = '#{user_name}' AND password = '#{password}'").first
# end
#
# def self.authenticate_safely(user_name, password)
# where("user_name = ? AND password = ?", user_name, password).first
# end
#
# def self.authenticate_safely_simply(user_name, password)
# where(user_name: user_name, password: password).first
# end
# end
#
# The <tt>authenticate_unsafely</tt> method inserts the parameters directly into the query
# and is thus susceptible to SQL-injection attacks if the <tt>user_name</tt> and +password+
# parameters come directly from an HTTP request. The <tt>authenticate_safely</tt> and
# <tt>authenticate_safely_simply</tt> both will sanitize the <tt>user_name</tt> and +password+
# before inserting them in the query, which will ensure that an attacker can't escape the
# query and fake the login (or worse).
#
# When using multiple parameters in the conditions, it can easily become hard to read exactly
# what the fourth or fifth question mark is supposed to represent. In those cases, you can
# resort to named bind variables instead. That's done by replacing the question marks with
# symbols and supplying a hash with values for the matching symbol keys:
#
# Company.where(
# "id = :id AND name = :name AND division = :division AND created_at > :accounting_date",
# { id: 3, name: "37signals", division: "First", accounting_date: '2005-01-01' }
# ).first
#
# Similarly, a simple hash without a statement will generate conditions based on equality with the SQL AND
# operator. For instance:
#
# Student.where(first_name: "Harvey", status: 1)
# Student.where(params[:student])
#
# A range may be used in the hash to use the SQL BETWEEN operator:
#
# Student.where(grade: 9..12)
#
# An array may be used in the hash to use the SQL IN operator:
#
# Student.where(grade: [9,11,12])
#
# When joining tables, nested hashes or keys written in the form 'table_name.column_name'
# can be used to qualify the table name of a particular condition. For instance:
#
# Student.joins(:schools).where(schools: { category: 'public' })
# Student.joins(:schools).where('schools.category' => 'public' )
#
# == Overwriting default accessors
#
# All column values are automatically available through basic accessors on the Active Record
# object, but sometimes you want to specialize this behavior. This can be done by overwriting
# the default accessors (using the same name as the attribute) and calling
# +super+ to actually change things.
#
# class Song < ActiveRecord::Base
# # Uses an integer of seconds to hold the length of the song
#
# def length=(minutes)
# super(minutes.to_i * 60)
# end
#
# def length
# super / 60
# end
# end
#
# == Attribute query methods
#
# 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.
# 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:
#
# user = User.new(name: "David")
# user.name? # => true
#
# anonymous = User.new(name: "")
# anonymous.name? # => false
#
# Query methods will also respect any overrides of default accessors:
#
# class User
# # Has admin boolean column
# def admin
# false
# end
# end
#
# user.update(admin: true)
#
# user.read_attribute(:admin) # => true, gets the column value
# user[:admin] # => true, also gets the column value
#
# user.admin # => false, due to the getter override
# user.admin? # => false, due to the getter override
#
# == Accessing attributes before they have been typecasted
#
# Sometimes you want to be able to read the raw attribute data without having the column-determined
# typecast run its course first. That can be done by using the <tt><attribute>_before_type_cast</tt>
# accessors that all attributes have. For example, if your Account model has a <tt>balance</tt> attribute,
# you can call <tt>account.balance_before_type_cast</tt> or <tt>account.id_before_type_cast</tt>.
#
# This is especially useful in validation situations where the user might supply a string for an
# integer field and you want to display the original string back in an error message. Accessing the
# attribute normally would typecast the string to 0, which isn't what you want.
#
# == Dynamic attribute-based finders
#
# Dynamic attribute-based finders are a mildly deprecated way of getting (and/or creating) objects
# by simple queries without turning to SQL. They work by appending the name of an attribute
# to <tt>find_by_</tt> like <tt>Person.find_by_user_name</tt>.
# Instead of writing <tt>Person.find_by(user_name: user_name)</tt>, you can use
# <tt>Person.find_by_user_name(user_name)</tt>.
#
# It's possible to add an exclamation point (!) on the end of the dynamic finders to get them to raise an
# ActiveRecord::RecordNotFound error if they do not return any records,
# like <tt>Person.find_by_last_name!</tt>.
#
# It's also possible to use multiple attributes in the same <tt>find_by_</tt> by separating them with
# "_and_".
#
# Person.find_by(user_name: user_name, password: password)
# Person.find_by_user_name_and_password(user_name, password) # with dynamic finder
#
# It's even possible to call these dynamic finder methods on relations and named scopes.
#
# Payment.order("created_on").find_by_amount(50)
#
# == Saving arrays, hashes, and other non-mappable objects in text columns
#
# Active Record can serialize any object in text columns using YAML. To do so, you must
# specify this with a call to the class method
# {serialize}[rdoc-ref:AttributeMethods::Serialization::ClassMethods#serialize].
# This makes it possible to store arrays, hashes, and other non-mappable objects without doing
# any additional work.
#
# class User < ActiveRecord::Base
# serialize :preferences
# end
#
# user = User.create(preferences: { "background" => "black", "display" => large })
# User.find(user.id).preferences # => { "background" => "black", "display" => large }
#
# You can also specify a class option as the second parameter that'll raise an exception
# if a serialized object is retrieved as a descendant of a class not in the hierarchy.
#
# class User < ActiveRecord::Base
# serialize :preferences, Hash
# end
#
# user = User.create(preferences: %w( one two three ))
# User.find(user.id).preferences # raises SerializationTypeMismatch
#
# When you specify a class option, the default value for that attribute will be a new
# instance of that class.
#
# class User < ActiveRecord::Base
# serialize :preferences, OpenStruct
# end
#
# user = User.new
# user.preferences.theme_color = "red"
#
#
# == Single table inheritance
#
# Active Record allows inheritance by storing the name of the class in a
# column that is named "type" by default. See ActiveRecord::Inheritance for
# more details.
#
# == Connection to multiple databases in different models
#
# Connections are usually created through
# {ActiveRecord::Base.establish_connection}[rdoc-ref:ConnectionHandling#establish_connection] and retrieved
# by ActiveRecord::Base.connection. All classes inheriting from ActiveRecord::Base will use this
# connection. But you can also set a class-specific connection. For example, if Course is an
# ActiveRecord::Base, but resides in a different database, you can just say <tt>Course.establish_connection</tt>
# and Course and all of its subclasses will use this connection instead.
#
# This feature is implemented by keeping a connection pool in ActiveRecord::Base that is
# a hash indexed by the class. If a connection is requested, the
# {ActiveRecord::Base.retrieve_connection}[rdoc-ref:ConnectionHandling#retrieve_connection] method
# will go up the class-hierarchy until a connection is found in the connection pool.
#
# == Exceptions
#
# * ActiveRecordError - Generic error class and superclass of all other errors raised by Active Record.
# * AdapterNotSpecified - The configuration hash used in
# {ActiveRecord::Base.establish_connection}[rdoc-ref:ConnectionHandling#establish_connection]
# didn't include an <tt>:adapter</tt> key.
# * AdapterNotFound - The <tt>:adapter</tt> key used in
# {ActiveRecord::Base.establish_connection}[rdoc-ref:ConnectionHandling#establish_connection]
# specified a non-existent adapter
# (or a bad spelling of an existing one).
# * AssociationTypeMismatch - The object assigned to the association wasn't of the type
# specified in the association definition.
# * AttributeAssignmentError - An error occurred while doing a mass assignment through the
# {ActiveRecord::Base#attributes=}[rdoc-ref:AttributeAssignment#attributes=] method.
# You can inspect the +attribute+ property of the exception object to determine which attribute
# triggered the error.
# * ConnectionNotEstablished - No connection has been established.
# Use {ActiveRecord::Base.establish_connection}[rdoc-ref:ConnectionHandling#establish_connection] before querying.
# * MultiparameterAssignmentErrors - Collection of errors that occurred during a mass assignment using the
# {ActiveRecord::Base#attributes=}[rdoc-ref:AttributeAssignment#attributes=] method.
# The +errors+ property of this exception contains an array of
# AttributeAssignmentError
# objects that should be inspected to determine which attributes triggered the errors.
# * RecordInvalid - raised by {ActiveRecord::Base#save!}[rdoc-ref:Persistence#save!] and
# {ActiveRecord::Base.create!}[rdoc-ref:Persistence::ClassMethods#create!]
# when the record is invalid.
# * RecordNotFound - No record responded to the {ActiveRecord::Base.find}[rdoc-ref:FinderMethods#find] method.
# Either the row with the given ID doesn't exist or the row didn't meet the additional restrictions.
# Some {ActiveRecord::Base.find}[rdoc-ref:FinderMethods#find] calls do not raise this exception to signal
# nothing was found, please check its documentation for further details.
# * SerializationTypeMismatch - The serialized object wasn't of the class specified as the second parameter.
# * StatementInvalid - The database server rejected the SQL statement. The precise error is added in the message.
#
# *Note*: The attributes listed are class-level attributes (accessible from both the class and instance level).
# So it's possible to assign a logger to the class through <tt>Base.logger=</tt> which will then be used by all
# instances in the current object space.
class Base
extend ActiveModel::Naming
extend ActiveSupport::Benchmarkable
extend ActiveSupport::DescendantsTracker
extend ConnectionHandling
extend QueryCache::ClassMethods
extend Querying
extend Translation
extend DynamicMatchers
extend DelegatedType
extend Explain
extend Enum
extend Delegation::DelegateCache
extend Aggregations::ClassMethods
include Core
include Persistence
include ReadonlyAttributes
include ModelSchema
include Inheritance
include Scoping
include Sanitization
include AttributeAssignment
include ActiveModel::Conversion
include Integration
include Validations
include CounterCache
include Attributes
include Locking::Optimistic
include Locking::Pessimistic
include AttributeMethods
include Callbacks
include Timestamp
include Associations
include SecurePassword
include AutosaveAssociation
include NestedAttributes
include Transactions
include TouchLater
include NoTouching
include Reflection
include Serialization
include Store
include SecureToken
include SignedId
include Suppressor
include Encryption::EncryptableRecord
end
ActiveSupport.run_load_hooks(:active_record, Base)
end
# frozen_string_literal: true
require "cases/helper"
require "models/bird"
class BasePreventWritesTest < ActiveRecord::TestCase
if !in_memory_db?
test "creating a record raises if preventing writes" do
ActiveRecord::Base.while_preventing_writes do
error = assert_raises ActiveRecord::ReadOnlyError do
Bird.create! name: "Bluejay"
end
assert_match %r/\AWrite query attempted while in readonly mode: INSERT /, error.message
end
end
test "updating a record raises if preventing writes" do
bird = Bird.create! name: "Bluejay"
ActiveRecord::Base.while_preventing_writes do
error = assert_raises ActiveRecord::ReadOnlyError do
bird.update! name: "Robin"
end
assert_match %r/\AWrite query attempted while in readonly mode: UPDATE /, error.message
end
end
test "deleting a record raises if preventing writes" do
bird = Bird.create! name: "Bluejay"
ActiveRecord::Base.while_preventing_writes do
error = assert_raises ActiveRecord::ReadOnlyError do
bird.destroy!
end
assert_match %r/\AWrite query attempted while in readonly mode: DELETE /, error.message
end
end
test "selecting a record does not raise if preventing writes" do
bird = Bird.create! name: "Bluejay"
ActiveRecord::Base.while_preventing_writes do
assert_equal bird, Bird.where(name: "Bluejay").last
end
end
test "an explain query does not raise if preventing writes" do
Bird.create!(name: "Bluejay")
ActiveRecord::Base.while_preventing_writes do
assert_queries(2) { Bird.where(name: "Bluejay").explain }
end
end
test "an empty transaction does not raise if preventing writes" do
ActiveRecord::Base.while_preventing_writes do
assert_queries(2, ignore_none: true) do
Bird.transaction do
ActiveRecord::Base.connection.materialize_transactions
end
end
end
end
test "preventing writes applies to all connections in block" do
ActiveRecord::Base.while_preventing_writes do
conn1_error = assert_raises ActiveRecord::ReadOnlyError do
assert_equal ActiveRecord::Base.connection, Bird.connection
assert_not_equal ARUnit2Model.connection, Bird.connection
Bird.create!(name: "Bluejay")
end
assert_match %r/\AWrite query attempted while in readonly mode: INSERT /, conn1_error.message
end
ActiveRecord::Base.while_preventing_writes do
conn2_error = assert_raises ActiveRecord::ReadOnlyError do
assert_not_equal ActiveRecord::Base.connection, Professor.connection
assert_equal ARUnit2Model.connection, Professor.connection
Professor.create!(name: "Professor Bluejay")
end
assert_match %r/\AWrite query attempted while in readonly mode: INSERT /, conn2_error.message
end
end
test "current_preventing_writes" do
ActiveRecord::Base.while_preventing_writes do
assert ActiveRecord::Base.current_preventing_writes, "expected connection current_preventing_writes to return true"
end
end
end
class BasePreventWritesLegacyTest < ActiveRecord::TestCase
def setup
@old_value = ActiveRecord.legacy_connection_handling
ActiveRecord.legacy_connection_handling = true
ActiveRecord::Base.establish_connection :arunit
ARUnit2Model.establish_connection :arunit2
end
def teardown
clean_up_legacy_connection_handlers
ActiveRecord.legacy_connection_handling = @old_value
end
if !in_memory_db?
test "creating a record raises if preventing writes" do
ActiveRecord::Base.connection_handler.while_preventing_writes do
error = assert_raises ActiveRecord::ReadOnlyError do
Bird.create! name: "Bluejay"
end
assert_match %r/\AWrite query attempted while in readonly mode: INSERT /, error.message
end
end
test "updating a record raises if preventing writes" do
bird = Bird.create! name: "Bluejay"
ActiveRecord::Base.connection_handler.while_preventing_writes do
error = assert_raises ActiveRecord::ReadOnlyError do
bird.update! name: "Robin"
end
assert_match %r/\AWrite query attempted while in readonly mode: UPDATE /, error.message
end
end
test "deleting a record raises if preventing writes" do
bird = Bird.create! name: "Bluejay"
ActiveRecord::Base.connection_handler.while_preventing_writes do
error = assert_raises ActiveRecord::ReadOnlyError do
bird.destroy!
end
assert_match %r/\AWrite query attempted while in readonly mode: DELETE /, error.message
end
end
test "selecting a record does not raise if preventing writes" do
bird = Bird.create! name: "Bluejay"
ActiveRecord::Base.connection_handler.while_preventing_writes do
assert_equal bird, Bird.where(name: "Bluejay").last
end
end
test "an explain query does not raise if preventing writes" do
Bird.create!(name: "Bluejay")
ActiveRecord::Base.connection_handler.while_preventing_writes do
assert_queries(2) { Bird.where(name: "Bluejay").explain }
end
end
test "an empty transaction does not raise if preventing writes" do
ActiveRecord::Base.connection_handler.while_preventing_writes do
assert_queries(2, ignore_none: true) do
Bird.transaction do
ActiveRecord::Base.connection.materialize_transactions
end
end
end
end
test "preventing writes applies to all connections on a handler" do
ActiveRecord::Base.connection_handler.while_preventing_writes do
conn1_error = assert_raises ActiveRecord::ReadOnlyError do
assert_equal ActiveRecord::Base.connection, Bird.connection
assert_not_equal ARUnit2Model.connection, Bird.connection
Bird.create!(name: "Bluejay")
end
assert_match %r/\AWrite query attempted while in readonly mode: INSERT /, conn1_error.message
end
ActiveRecord::Base.connection_handler.while_preventing_writes do
conn2_error = assert_raises ActiveRecord::ReadOnlyError do
assert_not_equal ActiveRecord::Base.connection, Professor.connection
assert_equal ARUnit2Model.connection, Professor.connection
Professor.create!(name: "Professor Bluejay")
end
assert_match %r/\AWrite query attempted while in readonly mode: INSERT /, conn2_error.message
end
end
test "preventing writes with multiple handlers" do
ActiveRecord::Base.connects_to(database: { writing: :arunit, reading: :arunit })
ActiveRecord::Base.connected_to(role: :writing) do
conn1_error = assert_raises ActiveRecord::ReadOnlyError do
assert_equal :writing, ActiveRecord::Base.current_role
ActiveRecord::Base.connection_handler.while_preventing_writes do
Bird.create!(name: "Bluejay")
end
end
assert_match %r/\AWrite query attempted while in readonly mode: INSERT /, conn1_error.message
end
ActiveRecord::Base.connected_to(role: :reading) do
conn2_error = assert_raises ActiveRecord::ReadOnlyError do
assert_equal :reading, ActiveRecord::Base.current_role
ActiveRecord::Base.connection_handler.while_preventing_writes do
Bird.create!(name: "Bluejay")
end
end
assert_match %r/\AWrite query attempted while in readonly mode: INSERT /, conn2_error.message
end
end
test "current_preventing_writes" do
ActiveRecord::Base.connection_handler.while_preventing_writes do
assert ActiveRecord::Base.current_preventing_writes, "expected connection current_preventing_writes to return true"
end
end
end
end
end
# frozen_string_literal: true
require "cases/helper"
require "models/post"
require "models/author"
require "models/topic"
require "models/reply"
require "models/category"
require "models/categorization"
require "models/company"
require "models/customer"
require "models/developer"
require "models/computer"
require "models/project"
require "models/default"
require "models/auto_id"
require "models/column_name"
require "models/subscriber"
require "models/comment"
require "models/minimalistic"
require "models/warehouse_thing"
require "models/parrot"
require "models/person"
require "models/edge"
require "models/joke"
require "models/bird"
require "models/car"
require "models/bulb"
require "models/pet"
require "models/owner"
require "concurrent/atomic/count_down_latch"
require "active_support/core_ext/enumerable"
class FirstAbstractClass < ActiveRecord::Base
self.abstract_class = true
connects_to database: { writing: :arunit, reading: :arunit }
end
class SecondAbstractClass < FirstAbstractClass
self.abstract_class = true
connects_to database: { writing: :arunit, reading: :arunit }
end
class ThirdAbstractClass < SecondAbstractClass
self.abstract_class = true
end
class Photo < SecondAbstractClass; end
class Smarts < ActiveRecord::Base; end
class CreditCard < ActiveRecord::Base
class PinNumber < ActiveRecord::Base
class CvvCode < ActiveRecord::Base; end
class SubCvvCode < CvvCode; end
end
class SubPinNumber < PinNumber; end
class Brand < Category; end
end
class MasterCreditCard < ActiveRecord::Base; end
class NonExistentTable < ActiveRecord::Base; end
class TestOracleDefault < ActiveRecord::Base; end
class ReadonlyTitlePost < Post
attr_readonly :title
end
class Weird < ActiveRecord::Base; end
class LintTest < ActiveRecord::TestCase
include ActiveModel::Lint::Tests
class LintModel < ActiveRecord::Base; end
def setup
@model = LintModel.new
end
end
class BasicsTest < ActiveRecord::TestCase
fixtures :topics, :companies, :developers, :projects, :computers, :accounts, :minimalistics, "warehouse-things", :authors, :author_addresses, :categorizations, :categories, :posts
def test_generated_association_methods_module_name
mod = Post.send(:generated_association_methods)
assert_equal "Post::GeneratedAssociationMethods", mod.inspect
end
def test_generated_relation_methods_module_name
mod = Post.send(:generated_relation_methods)
assert_equal "Post::GeneratedRelationMethods", mod.inspect
end
def test_arel_attribute_normalization
assert_equal Post.arel_table["body"], Post.arel_table[:body]
assert_equal Post.arel_table["body"], Post.arel_table[:text]
end
def test_incomplete_schema_loading
topic = Topic.first
payload = { foo: 42 }
topic.update!(content: payload)
Topic.reset_column_information
Topic.connection.stub(:lookup_cast_type_from_column, ->(_) { raise "Some Error" }) do
assert_raises RuntimeError do
Topic.columns_hash
end
end
assert_equal payload, Topic.first.content
end
def test_column_names_are_escaped
conn = ActiveRecord::Base.connection
classname = conn.class.name[/[^:]*$/]
badchar = {
"SQLite3Adapter" => '"',
"Mysql2Adapter" => "`",
"PostgreSQLAdapter" => '"',
"OracleAdapter" => '"',
}.fetch(classname) {
raise "need a bad char for #{classname}"
}
quoted = conn.quote_column_name "foo#{badchar}bar"
if current_adapter?(:OracleAdapter)
# Oracle does not allow double quotes in table and column names at all
# therefore quoting removes them
assert_equal("#{badchar}foobar#{badchar}", quoted)
else
assert_equal("#{badchar}foo#{badchar * 2}bar#{badchar}", quoted)
end
end
def test_columns_should_obey_set_primary_key
pk = Subscriber.columns_hash[Subscriber.primary_key]
assert_equal "nick", pk.name, "nick should be primary key"
end
def test_primary_key_with_no_id
assert_nil Edge.primary_key
end
def test_primary_key_and_references_columns_should_be_identical_type
pk = Author.columns_hash["id"]
ref = Post.columns_hash["author_id"]
assert_equal pk.sql_type, ref.sql_type
end
def test_many_mutations
car = Car.new name: "<3<3<3"
car.engines_count = 0
20_000.times { car.engines_count += 1 }
assert car.save
end
def test_limit_without_comma
assert_equal 1, Topic.limit("1").to_a.length
assert_equal 1, Topic.limit(1).to_a.length
end
def test_limit_should_take_value_from_latest_limit
assert_equal 1, Topic.limit(2).limit(1).to_a.length
end
def test_invalid_limit
assert_raises(ArgumentError) do
Topic.limit("asdfadf").to_a
end
end
def test_limit_should_sanitize_sql_injection_for_limit_without_commas
assert_raises(ArgumentError) do
Topic.limit("1 select * from schema").to_a
end
end
def test_limit_should_sanitize_sql_injection_for_limit_with_commas
assert_raises(ArgumentError) do
Topic.limit("1, 7 procedure help()").to_a
end
end
def test_select_symbol
topic_ids = Topic.select(:id).map(&:id).sort
assert_equal Topic.pluck(:id).sort, topic_ids
end
def test_table_exists
assert_not_predicate NonExistentTable, :table_exists?
assert_predicate Topic, :table_exists?
end
def test_preserving_date_objects
# Oracle enhanced adapter allows to define Date attributes in model class (see topic.rb)
assert_kind_of(
Date, Topic.find(1).last_read,
"The last_read attribute should be of the Date class"
)
end
def test_previously_changed
topic = Topic.first
topic.title = "<3<3<3"
assert_equal({}, topic.previous_changes)
topic.save!
expected = ["The First Topic", "<3<3<3"]
assert_equal(expected, topic.previous_changes["title"])
end
def test_previously_changed_dup
topic = Topic.first
topic.title = "<3<3<3"
topic.save!
t2 = topic.dup
assert_equal(topic.previous_changes, t2.previous_changes)
topic.title = "lolwut"
topic.save!
assert_not_equal(topic.previous_changes, t2.previous_changes)
end
def test_preserving_time_objects
assert_kind_of(
Time, Topic.find(1).bonus_time,
"The bonus_time attribute should be of the Time class"
)
assert_kind_of(
Time, Topic.find(1).written_on,
"The written_on attribute should be of the Time class"
)
# For adapters which support microsecond resolution.
if supports_datetime_with_precision?
assert_equal 11, Topic.find(1).written_on.sec
assert_equal 223300, Topic.find(1).written_on.usec
assert_equal 9900, Topic.find(2).written_on.usec
assert_equal 129346, Topic.find(3).written_on.usec
end
end
def test_preserving_time_objects_with_local_time_conversion_to_default_timezone_utc
with_env_tz eastern_time_zone do
with_timezone_config default: :utc do
time = Time.local(2000)
topic = Topic.create("written_on" => time)
saved_time = Topic.find(topic.id).reload.written_on
assert_equal time, saved_time
assert_equal [0, 0, 0, 1, 1, 2000, 6, 1, false, "EST"], time.to_a
assert_equal [0, 0, 5, 1, 1, 2000, 6, 1, false, "UTC"], saved_time.to_a
end
end
end
def test_preserving_time_objects_with_time_with_zone_conversion_to_default_timezone_utc
with_env_tz eastern_time_zone do
with_timezone_config default: :utc do
Time.use_zone "Central Time (US & Canada)" do
time = Time.zone.local(2000)
topic = Topic.create("written_on" => time)
saved_time = Topic.find(topic.id).reload.written_on
assert_equal time, saved_time
assert_equal [0, 0, 0, 1, 1, 2000, 6, 1, false, "CST"], time.to_a
assert_equal [0, 0, 6, 1, 1, 2000, 6, 1, false, "UTC"], saved_time.to_a
end
end
end
end
def test_preserving_time_objects_with_utc_time_conversion_to_default_timezone_local
with_env_tz eastern_time_zone do
with_timezone_config default: :local do
time = Time.utc(2000)
topic = Topic.create("written_on" => time)
saved_time = Topic.find(topic.id).reload.written_on
assert_equal time, saved_time
assert_equal [0, 0, 0, 1, 1, 2000, 6, 1, false, "UTC"], time.to_a
assert_equal [0, 0, 19, 31, 12, 1999, 5, 365, false, "EST"], saved_time.to_a
end
end
end
def test_preserving_time_objects_with_time_with_zone_conversion_to_default_timezone_local
with_env_tz eastern_time_zone do
with_timezone_config default: :local do
Time.use_zone "Central Time (US & Canada)" do
time = Time.zone.local(2000)
topic = Topic.create("written_on" => time)
saved_time = Topic.find(topic.id).reload.written_on
assert_equal time, saved_time
assert_equal [0, 0, 0, 1, 1, 2000, 6, 1, false, "CST"], time.to_a
assert_equal [0, 0, 1, 1, 1, 2000, 6, 1, false, "EST"], saved_time.to_a
end
end
end
end
def test_time_zone_aware_attribute_with_default_timezone_utc_on_utc_can_be_created
with_env_tz eastern_time_zone do
with_timezone_config aware_attributes: true, default: :utc, zone: "UTC" do
pet = Pet.create(name: "Bidu")
assert_predicate pet, :persisted?
saved_pet = Pet.find(pet.id)
assert_not_nil saved_pet.created_at
assert_not_nil saved_pet.updated_at
end
end
end
def eastern_time_zone
if Gem.win_platform?
"EST5EDT"
else
"America/New_York"
end
end
def test_custom_mutator
topic = Topic.find(1)
# This mutator is protected in the class definition
topic.send(:approved=, true)
assert topic.instance_variable_get("@custom_approved")
end
def test_initialize_with_attributes
topic = Topic.new(
"title" => "initialized from attributes", "written_on" => "2003-12-12 23:23")
assert_equal("initialized from attributes", topic.title)
end
def test_initialize_with_invalid_attribute
ex = assert_raise(ActiveRecord::MultiparameterAssignmentErrors) do
Topic.new("title" => "test",
"written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "00")
end
assert_equal(1, ex.errors.size)
assert_equal("written_on", ex.errors[0].attribute)
end
def test_create_after_initialize_without_block
cb = CustomBulb.create(name: "Dude")
assert_equal("Dude", cb.name)
assert_equal(true, cb.frickinawesome)
end
def test_create_after_initialize_with_block
cb = CustomBulb.create { |c| c.name = "Dude" }
assert_equal("Dude", cb.name)
assert_equal(true, cb.frickinawesome)
end
def test_create_after_initialize_with_array_param
cbs = CustomBulb.create([{ name: "Dude" }, { name: "Bob" }])
assert_equal "Dude", cbs[0].name
assert_equal "Bob", cbs[1].name
assert cbs[0].frickinawesome
assert_not cbs[1].frickinawesome
end
def test_load
topics = Topic.all.merge!(order: "id").to_a
assert_equal(5, topics.size)
assert_equal(topics(:first).title, topics.first.title)
end
def test_load_with_condition
topics = Topic.all.merge!(where: "author_name = 'Mary'").to_a
assert_equal(1, topics.size)
assert_equal(topics(:second).title, topics.first.title)
end
GUESSED_CLASSES = [Category, Smarts, CreditCard, CreditCard::PinNumber, CreditCard::PinNumber::CvvCode, CreditCard::SubPinNumber, CreditCard::Brand, MasterCreditCard]
def test_table_name_guesses
assert_equal "topics", Topic.table_name
assert_equal "categories", Category.table_name
assert_equal "smarts", Smarts.table_name
assert_equal "credit_cards", CreditCard.table_name
assert_equal "credit_card_pin_numbers", CreditCard::PinNumber.table_name
assert_equal "credit_card_pin_number_cvv_codes", CreditCard::PinNumber::CvvCode.table_name
assert_equal "credit_card_pin_numbers", CreditCard::SubPinNumber.table_name
assert_equal "categories", CreditCard::Brand.table_name
assert_equal "master_credit_cards", MasterCreditCard.table_name
ensure
GUESSED_CLASSES.each(&:reset_table_name)
end
def test_singular_table_name_guesses
ActiveRecord::Base.pluralize_table_names = false
GUESSED_CLASSES.each(&:reset_table_name)
assert_equal "category", Category.table_name
assert_equal "smarts", Smarts.table_name
assert_equal "credit_card", CreditCard.table_name
assert_equal "credit_card_pin_number", CreditCard::PinNumber.table_name
assert_equal "credit_card_pin_number_cvv_code", CreditCard::PinNumber::CvvCode.table_name
assert_equal "credit_card_pin_number", CreditCard::SubPinNumber.table_name
assert_equal "category", CreditCard::Brand.table_name
assert_equal "master_credit_card", MasterCreditCard.table_name
ensure
ActiveRecord::Base.pluralize_table_names = true
GUESSED_CLASSES.each(&:reset_table_name)
end
def test_table_name_guesses_with_prefixes_and_suffixes
ActiveRecord::Base.table_name_prefix = "test_"
Category.reset_table_name
assert_equal "test_categories", Category.table_name
ActiveRecord::Base.table_name_suffix = "_test"
Category.reset_table_name
assert_equal "test_categories_test", Category.table_name
ActiveRecord::Base.table_name_prefix = ""
Category.reset_table_name
assert_equal "categories_test", Category.table_name
ActiveRecord::Base.table_name_suffix = ""
Category.reset_table_name
assert_equal "categories", Category.table_name
ensure
ActiveRecord::Base.table_name_prefix = ""
ActiveRecord::Base.table_name_suffix = ""
GUESSED_CLASSES.each(&:reset_table_name)
end
def test_singular_table_name_guesses_with_prefixes_and_suffixes
ActiveRecord::Base.pluralize_table_names = false
ActiveRecord::Base.table_name_prefix = "test_"
Category.reset_table_name
assert_equal "test_category", Category.table_name
ActiveRecord::Base.table_name_suffix = "_test"
Category.reset_table_name
assert_equal "test_category_test", Category.table_name
ActiveRecord::Base.table_name_prefix = ""
Category.reset_table_name
assert_equal "category_test", Category.table_name
ActiveRecord::Base.table_name_suffix = ""
Category.reset_table_name
assert_equal "category", Category.table_name
ensure
ActiveRecord::Base.pluralize_table_names = true
ActiveRecord::Base.table_name_prefix = ""
ActiveRecord::Base.table_name_suffix = ""
GUESSED_CLASSES.each(&:reset_table_name)
end
def test_table_name_guesses_with_inherited_prefixes_and_suffixes
GUESSED_CLASSES.each(&:reset_table_name)
CreditCard.table_name_prefix = "test_"
CreditCard.reset_table_name
Category.reset_table_name
assert_equal "test_credit_cards", CreditCard.table_name
assert_equal "categories", Category.table_name
CreditCard.table_name_suffix = "_test"
CreditCard.reset_table_name
Category.reset_table_name
assert_equal "test_credit_cards_test", CreditCard.table_name
assert_equal "categories", Category.table_name
CreditCard.table_name_prefix = ""
CreditCard.reset_table_name
Category.reset_table_name
assert_equal "credit_cards_test", CreditCard.table_name
assert_equal "categories", Category.table_name
CreditCard.table_name_suffix = ""
CreditCard.reset_table_name
Category.reset_table_name
assert_equal "credit_cards", CreditCard.table_name
assert_equal "categories", Category.table_name
ensure
CreditCard.table_name_prefix = ""
CreditCard.table_name_suffix = ""
GUESSED_CLASSES.each(&:reset_table_name)
end
def test_singular_table_name_guesses_for_individual_table
Post.pluralize_table_names = false
Post.reset_table_name
assert_equal "post", Post.table_name
assert_equal "categories", Category.table_name
ensure
Post.pluralize_table_names = true
Post.reset_table_name
end
def test_table_name_based_on_model_name
assert_equal "posts", PostRecord.table_name
end
def test_null_fields
assert_nil Topic.find(1).parent_id
assert_nil Topic.create("title" => "Hey you").parent_id
end
def test_default_values
topic = Topic.new
assert_predicate topic, :approved?
assert_nil topic.written_on
assert_nil topic.bonus_time
assert_nil topic.last_read
topic.save
topic = Topic.find(topic.id)
assert_predicate topic, :approved?
assert_nil topic.last_read
# Oracle has some funky default handling, so it requires a bit of
# extra testing. See ticket #2788.
if current_adapter?(:OracleAdapter)
test = TestOracleDefault.new
assert_equal "X", test.test_char
assert_equal "hello", test.test_string
assert_equal 3, test.test_int
end
end
# Oracle does not have a TIME datatype.
unless current_adapter?(:OracleAdapter)
def test_utc_as_time_zone
with_timezone_config default: :utc do
attributes = { "bonus_time" => "5:42:00AM" }
topic = Topic.find(1)
topic.attributes = attributes
assert_equal Time.utc(2000, 1, 1, 5, 42, 0), topic.bonus_time
end
end
def test_utc_as_time_zone_and_new
with_timezone_config default: :utc do
attributes = { "bonus_time(1i)" => "2000",
"bonus_time(2i)" => "1",
"bonus_time(3i)" => "1",
"bonus_time(4i)" => "10",
"bonus_time(5i)" => "35",
"bonus_time(6i)" => "50" }
topic = Topic.new(attributes)
assert_equal Time.utc(2000, 1, 1, 10, 35, 50), topic.bonus_time
end
end
end
def test_default_values_on_empty_strings
topic = Topic.new
topic.approved = nil
topic.last_read = nil
topic.save
topic = Topic.find(topic.id)
assert_nil topic.last_read
assert_nil topic.approved
end
def test_equality
assert_equal Topic.find(1), Topic.find(2).topic
end
def test_find_by_slug
assert_equal Topic.find("1-meowmeow"), Topic.find(1)
end
def test_out_of_range_slugs
assert_equal [Topic.find(1)], Topic.where(id: ["1-meowmeow", "9223372036854775808-hello"])
end
def test_find_by_slug_with_array
assert_equal Topic.find([1, 2]), Topic.find(["1-meowmeow", "2-hello"])
assert_equal "The Second Topic of the day", Topic.find(["2-hello", "1-meowmeow"]).first.title
end
def test_find_by_slug_with_range
assert_equal Topic.where(id: "1-meowmeow".."2-hello"), Topic.where(id: 1..2)
end
def test_equality_of_new_records
assert_not_equal Topic.new, Topic.new
assert_equal false, Topic.new == Topic.new
end
def test_equality_of_destroyed_records
topic_1 = Topic.new(title: "test_1")
topic_1.save
topic_2 = Topic.find(topic_1.id)
topic_1.destroy
assert_equal topic_1, topic_2
assert_equal topic_2, topic_1
end
def test_equality_with_blank_ids
one = Subscriber.new(id: "")
two = Subscriber.new(id: "")
assert_equal one, two
end
def test_equality_of_relation_and_collection_proxy
car = Car.create!
car.bulbs.build
car.save
bulbs_of_car = Bulb.where(car_id: car.id)
assert_equal car.bulbs, bulbs_of_car, "CollectionProxy should be comparable with Relation"
assert_equal bulbs_of_car, car.bulbs, "Relation should be comparable with CollectionProxy"
end
def test_equality_of_relation_and_array
car = Car.create!
car.bulbs.build
car.save
bulbs_of_car = Bulb.where(car_id: car.id)
assert_equal bulbs_of_car, car.bulbs.to_a, "Relation should be comparable with Array"
end
def test_equality_of_relation_and_association_relation
car = Car.create!
car.bulbs.build
car.save
bulbs_of_car = Bulb.where(car_id: car.id)
assert_equal bulbs_of_car, car.bulbs.includes(:car), "Relation should be comparable with AssociationRelation"
assert_equal car.bulbs.includes(:car), bulbs_of_car, "AssociationRelation should be comparable with Relation"
end
def test_equality_of_collection_proxy_and_association_relation
car = Car.create!
car.bulbs.build
car.save
assert_equal car.bulbs, car.bulbs.includes(:car), "CollectionProxy should be comparable with AssociationRelation"
assert_equal car.bulbs.includes(:car), car.bulbs, "AssociationRelation should be comparable with CollectionProxy"
end
def test_hashing
assert_equal [ Topic.find(1) ], [ Topic.find(2).topic ] & [ Topic.find(1) ]
end
def test_successful_comparison_of_like_class_records
topic_1 = Topic.create!
topic_2 = Topic.create!
assert_equal [topic_2, topic_1].sort, [topic_1, topic_2]
end
def test_failed_comparison_of_unlike_class_records
assert_raises ArgumentError do
[ topics(:first), posts(:welcome) ].sort
end
end
def test_create_without_prepared_statement
topic = Topic.connection.unprepared_statement do
Topic.create(title: "foo")
end
assert_equal topic, Topic.find(topic.id)
end
def test_destroy_without_prepared_statement
topic = Topic.create(title: "foo")
Topic.connection.unprepared_statement do
Topic.find(topic.id).destroy
end
assert_nil Topic.find_by_id(topic.id)
end
def test_comparison_with_different_objects
topic = Topic.create
category = Category.create(name: "comparison")
assert_nil topic <=> category
end
def test_comparison_with_different_objects_in_array
topic = Topic.create
assert_raises(ArgumentError) do
[1, topic].sort
end
end
def test_readonly_attributes
assert_equal Set.new([ "title", "comments_count" ]), ReadonlyTitlePost.readonly_attributes
post = ReadonlyTitlePost.create(title: "cannot change this", body: "changeable")
post.reload
assert_equal "cannot change this", post.title
post.update(title: "try to change", body: "changed")
post.reload
assert_equal "cannot change this", post.title
assert_equal "changed", post.body
end
def test_unicode_column_name
Weird.reset_column_information
weird = Weird.create(なまえ: "たこ焼き仮面")
assert_equal "たこ焼き仮面", weird.なまえ
end
unless current_adapter?(:PostgreSQLAdapter)
def test_respect_internal_encoding
old_default_internal = Encoding.default_internal
silence_warnings { Encoding.default_internal = "EUC-JP" }
Weird.reset_column_information
assert_equal ["EUC-JP"], Weird.columns.map { |c| c.name.encoding.name }.uniq
ensure
silence_warnings { Encoding.default_internal = old_default_internal }
Weird.reset_column_information
end
end
def test_non_valid_identifier_column_name
weird = Weird.create("a$b" => "value")
weird.reload
assert_equal "value", weird.public_send("a$b")
assert_equal "value", weird.read_attribute("a$b")
weird.update_columns("a$b" => "value2")
weird.reload
assert_equal "value2", weird.public_send("a$b")
assert_equal "value2", weird.read_attribute("a$b")
end
def test_group_weirds_by_from
Weird.create("a$b" => "value", :from => "aaron")
count = Weird.group(Weird.arel_table[:from]).count
assert_equal 1, count["aaron"]
end
def test_attributes_on_dummy_time
# Oracle does not have a TIME datatype.
return true if current_adapter?(:OracleAdapter)
with_timezone_config default: :local do
attributes = {
"bonus_time" => "5:42:00AM"
}
topic = Topic.find(1)
topic.attributes = attributes
assert_equal Time.local(2000, 1, 1, 5, 42, 0), topic.bonus_time
topic.save!
assert_equal topic, Topic.find_by(attributes)
end
end
def test_attributes_on_dummy_time_with_invalid_time
# Oracle does not have a TIME datatype.
return true if current_adapter?(:OracleAdapter)
attributes = {
"bonus_time" => "not a time"
}
topic = Topic.find(1)
topic.attributes = attributes
assert_nil topic.bonus_time
end
def test_attributes
category = Category.new(name: "Ruby")
expected_attributes = category.attribute_names.index_with do |attribute_name|
category.public_send(attribute_name)
end
assert_instance_of Hash, category.attributes
assert_equal expected_attributes, category.attributes
end
def test_new_record_returns_boolean
assert_equal false, Topic.new.persisted?
assert_equal true, Topic.find(1).persisted?
end
def test_previously_new_record_returns_boolean
assert_equal false, Topic.new.previously_new_record?
assert_equal true, Topic.create.previously_new_record?
assert_equal false, Topic.find(1).previously_new_record?
end
def test_previously_persisted_returns_boolean
assert_equal false, Topic.new.previously_persisted?
assert_equal false, Topic.new.destroy.previously_persisted?
assert_equal false, Topic.first.previously_persisted?
assert_equal true, Topic.first.destroy.previously_persisted?
assert_equal true, Topic.first.delete.previously_persisted?
end
def test_dup
topic = Topic.find(1)
duped_topic = nil
assert_nothing_raised { duped_topic = topic.dup }
assert_equal topic.title, duped_topic.title
assert_not_predicate duped_topic, :persisted?
# test if the attributes have been duped
topic.title = "a"
duped_topic.title = "b"
assert_equal "a", topic.title
assert_equal "b", duped_topic.title
# test if the attribute values have been duped
duped_topic = topic.dup
duped_topic.title.replace "c"
assert_equal "a", topic.title
# test if attributes set as part of after_initialize are duped correctly
assert_equal topic.author_email_address, duped_topic.author_email_address
# test if saved clone object differs from original
duped_topic.save
assert_predicate duped_topic, :persisted?
assert_not_equal duped_topic.id, topic.id
duped_topic.reload
assert_equal("c", duped_topic.title)
end
DeveloperSalary = Struct.new(:amount)
def test_dup_with_aggregate_of_same_name_as_attribute
developer_with_aggregate = Class.new(ActiveRecord::Base) do
self.table_name = "developers"
composed_of :salary, class_name: "BasicsTest::DeveloperSalary", mapping: [%w(salary amount)]
end
dev = developer_with_aggregate.find(1)
assert_kind_of DeveloperSalary, dev.salary
dup = nil
assert_nothing_raised { dup = dev.dup }
assert_kind_of DeveloperSalary, dup.salary
assert_equal dev.salary.amount, dup.salary.amount
assert_not_predicate dup, :persisted?
# test if the attributes have been duped
original_amount = dup.salary.amount
dev.salary.amount = 1
assert_equal original_amount, dup.salary.amount
assert dup.save
assert_predicate dup, :persisted?
assert_not_equal dup.id, dev.id
end
def test_dup_does_not_copy_associations
author = authors(:david)
assert_not_equal [], author.posts
author_dup = author.dup
assert_equal [], author_dup.posts
end
def test_clone_preserves_subtype
clone = nil
assert_nothing_raised { clone = Company.find(3).clone }
assert_kind_of Client, clone
end
def test_clone_of_new_object_with_defaults
developer = Developer.new
assert_not_predicate developer, :name_changed?
assert_not_predicate developer, :salary_changed?
cloned_developer = developer.clone
assert_not_predicate cloned_developer, :name_changed?
assert_not_predicate cloned_developer, :salary_changed?
end
def test_clone_of_new_object_marks_attributes_as_dirty
developer = Developer.new name: "Bjorn", salary: 100000
assert_predicate developer, :name_changed?
assert_predicate developer, :salary_changed?
cloned_developer = developer.clone
assert_predicate cloned_developer, :name_changed?
assert_predicate cloned_developer, :salary_changed?
end
def test_clone_of_new_object_marks_as_dirty_only_changed_attributes
developer = Developer.new name: "Bjorn"
assert developer.name_changed? # obviously
assert_not developer.salary_changed? # attribute has non-nil default value, so treated as not changed
cloned_developer = developer.clone
assert_predicate cloned_developer, :name_changed?
assert_not cloned_developer.salary_changed? # ... and cloned instance should behave same
end
def test_dup_of_saved_object_marks_attributes_as_dirty
developer = Developer.create! name: "Bjorn", salary: 100000
assert_not_predicate developer, :name_changed?
assert_not_predicate developer, :salary_changed?
cloned_developer = developer.dup
assert cloned_developer.name_changed? # both attributes differ from defaults
assert_predicate cloned_developer, :salary_changed?
end
def test_dup_of_saved_object_marks_as_dirty_only_changed_attributes
developer = Developer.create! name: "Bjorn"
assert_not developer.name_changed? # both attributes of saved object should be treated as not changed
assert_not_predicate developer, :salary_changed?
cloned_developer = developer.dup
assert cloned_developer.name_changed? # ... but on cloned object should be
assert_not cloned_developer.salary_changed? # ... BUT salary has non-nil default which should be treated as not changed on cloned instance
end
def test_bignum
company = Company.find(1)
company.rating = 2147483648
company.save
company = Company.find(1)
assert_equal 2147483648, company.rating
end
def test_bignum_pk
company = Company.create!(id: 2147483648, name: "foo")
assert_equal company, Company.find(company.id)
end
if current_adapter?(:PostgreSQLAdapter, :Mysql2Adapter, :SQLite3Adapter)
def test_default_char_types
default = Default.new
assert_equal "Y", default.char1
assert_equal "a varchar field", default.char2
# Mysql text type can't have default value
unless current_adapter?(:Mysql2Adapter)
assert_equal "a text field", default.char3
end
end
def test_default_in_local_time
with_timezone_config default: :local do
default = Default.new
assert_equal Date.new(2004, 1, 1), default.fixed_date
assert_equal Time.local(2004, 1, 1, 0, 0, 0, 0), default.fixed_time
if current_adapter?(:PostgreSQLAdapter)
assert_equal Time.utc(2004, 1, 1, 0, 0, 0, 0), default.fixed_time_with_time_zone
end
end
end
def test_default_in_utc
with_timezone_config default: :utc do
default = Default.new
assert_equal Date.new(2004, 1, 1), default.fixed_date
assert_equal Time.utc(2004, 1, 1, 0, 0, 0, 0), default.fixed_time
if current_adapter?(:PostgreSQLAdapter)
assert_equal Time.utc(2004, 1, 1, 0, 0, 0, 0), default.fixed_time_with_time_zone
end
end
end
def test_default_in_utc_with_time_zone
with_timezone_config default: :utc do
Time.use_zone "Central Time (US & Canada)" do
default = Default.new
assert_equal Date.new(2004, 1, 1), default.fixed_date
assert_equal Time.utc(2004, 1, 1, 0, 0, 0, 0), default.fixed_time
if current_adapter?(:PostgreSQLAdapter)
assert_equal Time.utc(2004, 1, 1, 0, 0, 0, 0), default.fixed_time_with_time_zone
end
end
end
end
unless in_memory_db?
def test_connection_in_local_time
with_timezone_config default: :utc do
new_config = ActiveRecord::Base.connection_db_config.configuration_hash.merge(default_timezone: "local")
ActiveRecord::Base.establish_connection(new_config)
Default.reset_column_information
default = Default.new
assert_equal Date.new(2004, 1, 1), default.fixed_date
assert_equal Time.local(2004, 1, 1, 0, 0, 0, 0), default.fixed_time
if current_adapter?(:PostgreSQLAdapter)
assert_equal Time.utc(2004, 1, 1, 0, 0, 0, 0), default.fixed_time_with_time_zone
end
end
ensure
ActiveRecord::Base.establish_connection :arunit
Default.reset_column_information
end
def test_connection_in_utc_time
with_timezone_config default: :local do
new_config = ActiveRecord::Base.connection_db_config.configuration_hash.merge(default_timezone: "utc")
ActiveRecord::Base.establish_connection(new_config)
Default.reset_column_information
default = Default.new
assert_equal Date.new(2004, 1, 1), default.fixed_date
assert_equal Time.utc(2004, 1, 1, 0, 0, 0, 0), default.fixed_time
if current_adapter?(:PostgreSQLAdapter)
assert_equal Time.utc(2004, 1, 1, 0, 0, 0, 0), default.fixed_time_with_time_zone
end
end
ensure
ActiveRecord::Base.establish_connection :arunit
Default.reset_column_information
end
end
end
def test_auto_id
auto = AutoId.new
auto.save
assert(auto.id > 0)
end
def test_sql_injection_via_find
assert_raise(ActiveRecord::RecordNotFound, ActiveRecord::StatementInvalid) do
Topic.find("123456 OR id > 0")
end
end
def test_column_name_properly_quoted
col_record = ColumnName.new
col_record.references = 40
assert col_record.save
col_record.references = 41
assert col_record.save
assert_not_nil c2 = ColumnName.find(col_record.id)
assert_equal(41, c2.references)
end
def test_quoting_arrays
replies = Reply.all.merge!(where: [ "id IN (?)", topics(:first).replies.collect(&:id) ]).to_a
assert_equal topics(:first).replies.size, replies.size
replies = Reply.all.merge!(where: [ "id IN (?)", [] ]).to_a
assert_equal 0, replies.size
end
def test_quote
author_name = "\\ \001 ' \n \\n \""
topic = Topic.create("author_name" => author_name)
assert_equal author_name, Topic.find(topic.id).author_name
end
def test_toggle_attribute
assert_not_predicate topics(:first), :approved?
topics(:first).toggle!(:approved)
assert_predicate topics(:first), :approved?
topic = topics(:first)
topic.toggle(:approved)
assert_not_predicate topic, :approved?
topic.reload
assert_predicate topic, :approved?
end
def test_reload
t1 = Topic.find(1)
t2 = Topic.find(1)
t1.title = "something else"
t1.save
t2.reload
assert_equal t1.title, t2.title
end
def test_switching_between_table_name
k = Class.new(Joke)
assert_difference("GoodJoke.count") do
k.table_name = "cold_jokes"
k.create
k.table_name = "funny_jokes"
k.create
end
end
def test_clear_cache_when_setting_table_name
original_table_name = Joke.table_name
Joke.table_name = "funny_jokes"
before_columns = Joke.columns
before_seq = Joke.sequence_name
Joke.table_name = "cold_jokes"
after_columns = Joke.columns
after_seq = Joke.sequence_name
assert_not_equal before_columns, after_columns
assert_not_equal before_seq, after_seq unless before_seq.nil? && after_seq.nil?
ensure
Joke.table_name = original_table_name
end
def test_dont_clear_sequence_name_when_setting_explicitly
k = Class.new(Joke)
k.sequence_name = "black_jokes_seq"
k.table_name = "cold_jokes"
before_seq = k.sequence_name
k.table_name = "funny_jokes"
after_seq = k.sequence_name
assert_equal before_seq, after_seq unless before_seq.nil? && after_seq.nil?
end
def test_dont_clear_inheritance_column_when_setting_explicitly
k = Class.new(Joke)
k.inheritance_column = "my_type"
before_inherit = k.inheritance_column
k.reset_column_information
after_inherit = k.inheritance_column
assert_equal before_inherit, after_inherit unless before_inherit.blank? && after_inherit.blank?
end
def test_set_table_name_symbol_converted_to_string
k = Class.new(Joke)
k.table_name = :cold_jokes
assert_equal "cold_jokes", k.table_name
end
def test_quoted_table_name_after_set_table_name
klass = Class.new(ActiveRecord::Base)
klass.table_name = "foo"
assert_equal "foo", klass.table_name
assert_equal klass.connection.quote_table_name("foo"), klass.quoted_table_name
klass.table_name = "bar"
assert_equal "bar", klass.table_name
assert_equal klass.connection.quote_table_name("bar"), klass.quoted_table_name
end
def test_set_table_name_with_inheritance
k = Class.new(ActiveRecord::Base)
def k.name; "Foo"; end
def k.table_name; super + "ks"; end
assert_equal "foosks", k.table_name
end
def test_sequence_name_with_abstract_class
ak = Class.new(ActiveRecord::Base)
ak.abstract_class = true
k = Class.new(ak)
k.table_name = "projects"
orig_name = k.sequence_name
skip "sequences not supported by db" unless orig_name
assert_equal k.reset_sequence_name, orig_name
end
def test_count_with_join
res = Post.count_by_sql "SELECT COUNT(*) FROM posts LEFT JOIN comments ON posts.id=comments.post_id WHERE posts.#{QUOTED_TYPE} = 'Post'"
res2 = Post.where("posts.#{QUOTED_TYPE} = 'Post'").joins("LEFT JOIN comments ON posts.id=comments.post_id").count
assert_equal res, res2
res4 = Post.count_by_sql "SELECT COUNT(p.id) FROM posts p, comments co WHERE p.#{QUOTED_TYPE} = 'Post' AND p.id=co.post_id"
res5 = Post.where("p.#{QUOTED_TYPE} = 'Post' AND p.id=co.post_id").joins("p, comments co").select("p.id").count
assert_equal res4, res5
res6 = Post.count_by_sql "SELECT COUNT(DISTINCT p.id) FROM posts p, comments co WHERE p.#{QUOTED_TYPE} = 'Post' AND p.id=co.post_id"
res7 = Post.where("p.#{QUOTED_TYPE} = 'Post' AND p.id=co.post_id").joins("p, comments co").select("p.id").distinct.count
assert_equal res6, res7
end
def test_no_limit_offset
assert_nothing_raised do
Developer.all.merge!(offset: 2).to_a
end
end
def test_last
assert_equal Developer.all.merge!(order: "id desc").first, Developer.last
end
def test_all
developers = Developer.all
assert_kind_of ActiveRecord::Relation, developers
assert_equal Developer.all, developers
end
def test_all_with_conditions
assert_equal Developer.all.merge!(order: "id desc").to_a, Developer.order("id desc").to_a
end
def test_find_ordered_last
last = Developer.order("developers.salary ASC").last
assert_equal last, Developer.order("developers.salary": "ASC").to_a.last
end
def test_find_reverse_ordered_last
last = Developer.order("developers.salary DESC").last
assert_equal last, Developer.order("developers.salary": "DESC").to_a.last
end
def test_find_multiple_ordered_last
last = Developer.order("developers.name, developers.salary DESC").last
assert_equal last, Developer.order(:"developers.name", "developers.salary": "DESC").to_a.last
end
def test_find_keeps_multiple_order_values
combined = Developer.order("developers.name, developers.salary").to_a
assert_equal combined, Developer.order(:"developers.name", :"developers.salary").to_a
end
def test_find_keeps_multiple_group_values
combined = Developer.merge(group: "developers.name, developers.salary, developers.id, developers.legacy_created_at, developers.legacy_updated_at, developers.legacy_created_on, developers.legacy_updated_on").to_a
assert_equal combined, Developer.merge(group: ["developers.name", "developers.salary", "developers.id", "developers.created_at", "developers.updated_at", "developers.created_on", "developers.updated_on"]).to_a
end
def test_find_symbol_ordered_last
last = Developer.all.merge!(order: :salary).last
assert_equal last, Developer.all.merge!(order: :salary).to_a.last
end
def test_abstract_class_table_name
assert_nil AbstractCompany.table_name
end
def test_find_on_abstract_base_class_doesnt_use_type_condition
old_class = LooseDescendant
Object.send :remove_const, :LooseDescendant
descendant = old_class.create! first_name: "bob"
assert_not_nil LoosePerson.find(descendant.id), "Should have found instance of LooseDescendant when finding abstract LoosePerson: #{descendant.inspect}"
ensure
unless Object.const_defined?(:LooseDescendant)
Object.const_set :LooseDescendant, old_class
end
end
def test_assert_queries
query = lambda { ActiveRecord::Base.connection.execute "select count(*) from developers" }
assert_queries(2) { 2.times { query.call } }
assert_queries 1, &query
assert_no_queries { assert true }
end
def test_benchmark_with_log_level
original_logger = ActiveRecord::Base.logger
log = StringIO.new
ActiveRecord::Base.logger = ActiveSupport::Logger.new(log)
ActiveRecord::Base.logger.level = Logger::WARN
ActiveRecord::Base.benchmark("Debug Topic Count", level: :debug) { Topic.count }
ActiveRecord::Base.benchmark("Warn Topic Count", level: :warn) { Topic.count }
ActiveRecord::Base.benchmark("Error Topic Count", level: :error) { Topic.count }
assert_no_match(/Debug Topic Count/, log.string)
assert_match(/Warn Topic Count/, log.string)
assert_match(/Error Topic Count/, log.string)
ensure
ActiveRecord::Base.logger = original_logger
end
def test_benchmark_with_use_silence
original_logger = ActiveRecord::Base.logger
log = StringIO.new
ActiveRecord::Base.logger = ActiveSupport::Logger.new(log)
ActiveRecord::Base.logger.level = Logger::DEBUG
ActiveRecord::Base.benchmark("Logging", level: :debug, silence: false) { ActiveRecord::Base.logger.debug "Quiet" }
assert_match(/Quiet/, log.string)
ensure
ActiveRecord::Base.logger = original_logger
end
def test_clear_cache!
# preheat cache
c1 = Post.connection.schema_cache.columns("posts")
assert_not_equal 0, Post.connection.schema_cache.size
ActiveRecord::Base.clear_cache!
assert_equal 0, Post.connection.schema_cache.size
c2 = Post.connection.schema_cache.columns("posts")
assert_not_equal 0, Post.connection.schema_cache.size
assert_equal c1, c2
end
def test_before_remove_const_resets_the_current_scope
# Done this way because a class cannot be defined in a method using the
# class keyword.
Object.const_set(:ReloadableModel, Class.new(ActiveRecord::Base))
ReloadableModel.current_scope = ReloadableModel.all
assert_not_nil ActiveRecord::Scoping::ScopeRegistry.current_scope(ReloadableModel) # precondition
ReloadableModel.before_remove_const
assert_nil ActiveRecord::Scoping::ScopeRegistry.current_scope(ReloadableModel)
ensure
Object.send(:remove_const, :ReloadableModel)
end
def test_marshal_round_trip
expected = posts(:welcome)
marshalled = Marshal.dump(expected)
actual = Marshal.load(marshalled)
assert_equal expected.attributes, actual.attributes
end
def test_marshal_inspected_round_trip
expected = posts(:welcome)
expected.inspect
marshalled = Marshal.dump(expected)
actual = Marshal.load(marshalled)
assert_equal expected.attributes, actual.attributes
end
def test_marshal_new_record_round_trip
marshalled = Marshal.dump(Post.new)
post = Marshal.load(marshalled)
assert post.new_record?, "should be a new record"
end
def test_marshalling_with_associations
post = Post.new
post.comments.build
marshalled = Marshal.dump(post)
post = Marshal.load(marshalled)
assert_equal 1, post.comments.length
end
if Process.respond_to?(:fork) && !in_memory_db?
def test_marshal_between_processes
# Define a new model to ensure there are no caches
if self.class.const_defined?("Post", false)
flunk "there should be no post constant"
end
self.class.const_set("Post", Class.new(ActiveRecord::Base) {
has_many :comments
})
rd, wr = IO.pipe
rd.binmode
wr.binmode
ActiveRecord::Base.connection_handler.clear_all_connections!
fork do
rd.close
post = Post.new
post.comments.build
wr.write Marshal.dump(post)
wr.close
end
wr.close
assert Marshal.load rd.read
rd.close
ensure
self.class.send(:remove_const, "Post") if self.class.const_defined?("Post", false)
end
end
def test_marshalling_new_record_round_trip_with_associations
post = Post.new
post.comments.build
post = Marshal.load(Marshal.dump(post))
assert post.new_record?, "should be a new record"
end
def test_attribute_names
expected = ["id", "type", "firm_id", "firm_name", "name", "client_of", "rating", "account_id", "description", "metadata"]
assert_equal expected, Company.attribute_names
end
def test_has_attribute
assert Company.has_attribute?("id")
assert Company.has_attribute?("type")
assert Company.has_attribute?("name")
assert Company.has_attribute?("new_name")
assert Company.has_attribute?("metadata")
assert_not Company.has_attribute?("lastname")
assert_not Company.has_attribute?("age")
company = Company.new
assert company.has_attribute?("id")
assert company.has_attribute?("type")
assert company.has_attribute?("name")
assert company.has_attribute?("new_name")
assert company.has_attribute?("metadata")
assert_not company.has_attribute?("lastname")
assert_not company.has_attribute?("age")
end
def test_has_attribute_with_symbol
assert Company.has_attribute?(:id)
assert Company.has_attribute?(:type)
assert Company.has_attribute?(:name)
assert Company.has_attribute?(:new_name)
assert Company.has_attribute?(:metadata)
assert_not Company.has_attribute?(:lastname)
assert_not Company.has_attribute?(:age)
company = Company.new
assert company.has_attribute?(:id)
assert company.has_attribute?(:type)
assert company.has_attribute?(:name)
assert company.has_attribute?(:new_name)
assert company.has_attribute?(:metadata)
assert_not company.has_attribute?(:lastname)
assert_not company.has_attribute?(:age)
end
def test_attribute_names_on_table_not_exists
assert_equal [], NonExistentTable.attribute_names
end
def test_attribute_names_on_abstract_class
assert_equal [], AbstractCompany.attribute_names
end
def test_touch_should_raise_error_on_a_new_object
company = Company.new(rating: 1, name: "37signals", firm_name: "37signals")
assert_raises(ActiveRecord::ActiveRecordError) do
company.touch :updated_at
end
end
def test_distinct_delegates_to_scoped
assert_equal Bird.all.distinct, Bird.distinct
end
def test_table_name_with_2_abstract_subclasses
assert_equal "photos", Photo.table_name
end
def test_column_types_typecast
topic = Topic.first
assert_not_equal "t.lo", topic.author_name
attrs = topic.attributes.dup
attrs.delete "id"
typecast = Class.new(ActiveRecord::Type::Value) {
def cast(value)
"t.lo"
end
}
types = { "author_name" => typecast.new }
topic = Topic.instantiate(attrs, types)
assert_equal "t.lo", topic.author_name
end
def test_typecasting_aliases
assert_equal 10, Topic.select("10 as tenderlove").first.tenderlove
end
def test_default_values_are_deeply_dupped
company = Company.new
company.description << "foo"
assert_equal "", Company.new.description
end
test "scoped can take a values hash" do
klass = Class.new(ActiveRecord::Base)
klass.table_name = "bar"
assert_equal ["foo"], klass.all.merge!(select: "foo").select_values
end
test "connection_handler can be overridden" do
klass = Class.new(ActiveRecord::Base)
orig_handler = klass.connection_handler
new_handler = ActiveRecord::ConnectionAdapters::ConnectionHandler.new
thread_connection_handler = nil
t = Thread.new do
klass.connection_handler = new_handler
thread_connection_handler = klass.connection_handler
end
t.join
assert_equal klass.connection_handler, orig_handler
assert_equal thread_connection_handler, new_handler
end
test "new threads get default the default connection handler" do
klass = Class.new(ActiveRecord::Base)
orig_handler = klass.connection_handler
handler = nil
t = Thread.new do
handler = klass.connection_handler
end
t.join
assert_equal handler, orig_handler
assert_equal klass.connection_handler, orig_handler
assert_equal klass.default_connection_handler, orig_handler
end
test "changing a connection handler in a main thread does not poison the other threads" do
klass = Class.new(ActiveRecord::Base)
orig_handler = klass.connection_handler
new_handler = ActiveRecord::ConnectionAdapters::ConnectionHandler.new
after_handler = nil
latch1 = Concurrent::CountDownLatch.new
latch2 = Concurrent::CountDownLatch.new
t = Thread.new do
klass.connection_handler = new_handler
latch1.count_down
latch2.wait
after_handler = klass.connection_handler
end
latch1.wait
klass.connection_handler = orig_handler
latch2.count_down
t.join
assert_equal after_handler, new_handler
assert_equal orig_handler, klass.connection_handler
end
# Note: This is a performance optimization for Array#uniq and Hash#[] with
# AR::Base objects. If the future has made this irrelevant, feel free to
# delete this.
test "records without an id have unique hashes" do
assert_not_equal Post.new.hash, Post.new.hash
end
test "records of different classes have different hashes" do
assert_not_equal Post.new(id: 1).hash, Developer.new(id: 1).hash
end
test "resetting column information doesn't remove attribute methods" do
topic = topics(:first)
assert_not_predicate topic, :id_changed?
Topic.reset_column_information
assert_not_predicate topic, :id_changed?
end
test "ignored columns are not present in columns_hash" do
cache_columns = Developer.connection.schema_cache.columns_hash(Developer.table_name)
assert_includes cache_columns.keys, "first_name"
assert_not_includes Developer.columns_hash.keys, "first_name"
assert_not_includes SubDeveloper.columns_hash.keys, "first_name"
assert_not_includes SymbolIgnoredDeveloper.columns_hash.keys, "first_name"
end
test ".columns_hash raises an error if the record has an empty table name" do
expected_message = "FirstAbstractClass has no table configured. Set one with FirstAbstractClass.table_name="
exception = assert_raises(ActiveRecord::TableNotSpecified) do
FirstAbstractClass.columns_hash
end
assert_equal expected_message, exception.message
end
test "ignored columns have no attribute methods" do
assert_not_respond_to Developer.new, :first_name
assert_not_respond_to Developer.new, :first_name=
assert_not_respond_to Developer.new, :first_name?
assert_not_respond_to SubDeveloper.new, :first_name
assert_not_respond_to SubDeveloper.new, :first_name=
assert_not_respond_to SubDeveloper.new, :first_name?
assert_not_respond_to SymbolIgnoredDeveloper.new, :first_name
assert_not_respond_to SymbolIgnoredDeveloper.new, :first_name=
assert_not_respond_to SymbolIgnoredDeveloper.new, :first_name?
end
test "ignored columns don't prevent explicit declaration of attribute methods" do
assert_respond_to Developer.new, :last_name
assert_respond_to Developer.new, :last_name=
assert_respond_to Developer.new, :last_name?
assert_respond_to SubDeveloper.new, :last_name
assert_respond_to SubDeveloper.new, :last_name=
assert_respond_to SubDeveloper.new, :last_name?
assert_respond_to SymbolIgnoredDeveloper.new, :last_name
assert_respond_to SymbolIgnoredDeveloper.new, :last_name=
assert_respond_to SymbolIgnoredDeveloper.new, :last_name?
end
test "ignored columns are stored as an array of string" do
assert_equal(%w(first_name last_name), Developer.ignored_columns)
assert_equal(%w(first_name last_name), SymbolIgnoredDeveloper.ignored_columns)
end
test "when #reload called, ignored columns' attribute methods are not defined" do
developer = Developer.create!(name: "Developer")
assert_not_respond_to developer, :first_name
assert_not_respond_to developer, :first_name=
developer.reload
assert_not_respond_to developer, :first_name
assert_not_respond_to developer, :first_name=
end
test "when ignored attribute is loaded, cast type should be preferred over DB type" do
developer = AttributedDeveloper.create
developer.update_column :name, "name"
loaded_developer = AttributedDeveloper.where(id: developer.id).select("*").first
assert_equal "Developer: name", loaded_developer.name
end
test "when assigning new ignored columns it invalidates cache for column names" do
assert_not_includes ColumnNamesCachedDeveloper.column_names, "name"
end
test "ignored columns not included in SELECT" do
query = Developer.all.to_sql.downcase
# ignored column
assert_not query.include?("first_name")
# regular column
assert query.include?("name")
end
test "column names are quoted when using #from clause and model has ignored columns" do
assert_not_empty Developer.ignored_columns
query = Developer.from("developers").to_sql
quoted_id = "#{Developer.quoted_table_name}.#{Developer.quoted_primary_key}"
assert_match(/SELECT #{Regexp.escape(quoted_id)}.* FROM developers/, query)
end
test "using table name qualified column names unless having SELECT list explicitly" do
assert_equal developers(:david), Developer.from("developers").joins(:shared_computers).take
end
test "protected environments by default is an array with production" do
assert_equal ["production"], ActiveRecord::Base.protected_environments
end
def test_protected_environments_are_stored_as_an_array_of_string
previous_protected_environments = ActiveRecord::Base.protected_environments
ActiveRecord::Base.protected_environments = [:staging, "production"]
assert_equal ["staging", "production"], ActiveRecord::Base.protected_environments
ensure
ActiveRecord::Base.protected_environments = previous_protected_environments
end
test "cannot call connects_to on non-abstract or non-ActiveRecord::Base classes" do
error = assert_raises(NotImplementedError) do
Bird.connects_to(database: { writing: :arunit })
end
assert_equal "`connects_to` can only be called on ActiveRecord::Base or abstract classes", error.message
end
test "cannot call connected_to on subclasses of ActiveRecord::Base with legacy connection handling" do
old_value = ActiveRecord.legacy_connection_handling
ActiveRecord.legacy_connection_handling = true
error = assert_raises(NotImplementedError) do
Bird.connected_to(role: :reading) { }
end
assert_equal "`connected_to` can only be called on ActiveRecord::Base with legacy connection handling.", error.message
ensure
clean_up_legacy_connection_handlers
ActiveRecord.legacy_connection_handling = old_value
end
test "cannot call connected_to with role and shard on non-abstract classes" do
error = assert_raises(NotImplementedError) do
Bird.connected_to(role: :reading, shard: :default) { }
end
assert_equal "calling `connected_to` is only allowed on ActiveRecord::Base or abstract classes.", error.message
end
test "can call connected_to with role and shard on abstract classes" do
SecondAbstractClass.connected_to(role: :reading, shard: :default) do
assert SecondAbstractClass.connected_to?(role: :reading, shard: :default)
end
end
test "cannot call connected_to on the abstract class that did not establish the connection" do
error = assert_raises(NotImplementedError) do
ThirdAbstractClass.connected_to(role: :reading) { }
end
assert_equal "calling `connected_to` is only allowed on the abstract class that established the connection.", error.message
end
test "#connecting_to with role" do
SecondAbstractClass.connecting_to(role: :reading)
assert SecondAbstractClass.connected_to?(role: :reading)
assert SecondAbstractClass.current_preventing_writes
ensure
ActiveRecord::Base.connected_to_stack.pop
end
test "#connecting_to with role and shard" do
SecondAbstractClass.connecting_to(role: :reading, shard: :default)
assert SecondAbstractClass.connected_to?(role: :reading, shard: :default)
ensure
ActiveRecord::Base.connected_to_stack.pop
end
test "#connecting_to with prevent_writes" do
SecondAbstractClass.connecting_to(role: :writing, prevent_writes: true)
assert SecondAbstractClass.connected_to?(role: :writing)
assert SecondAbstractClass.current_preventing_writes
ensure
ActiveRecord::Base.connected_to_stack.pop
end
test "#connecting_to doesn't work with legacy connection handling" do
old_value = ActiveRecord.legacy_connection_handling
ActiveRecord.legacy_connection_handling = true
assert_raises NotImplementedError do
SecondAbstractClass.connecting_to(role: :writing, prevent_writes: true)
end
ensure
ActiveRecord.legacy_connection_handling = old_value
end
test "#connected_to_many doesn't work with legacy connection handling" do
old_value = ActiveRecord.legacy_connection_handling
ActiveRecord.legacy_connection_handling = true
assert_raises NotImplementedError do
ActiveRecord::Base.connected_to_many([SecondAbstractClass], role: :writing)
end
ensure
ActiveRecord.legacy_connection_handling = old_value
end
test "#connected_to_many cannot be called on anything but ActiveRecord::Base" do
assert_raises NotImplementedError do
SecondAbstractClass.connected_to_many([SecondAbstractClass], role: :writing)
end
end
test "#connected_to_many cannot be called with classes that include ActiveRecord::Base" do
assert_raises NotImplementedError do
ActiveRecord::Base.connected_to_many([ActiveRecord::Base], role: :writing)
end
end
test "#connected_to_many sets prevent_writes if role is reading" do
ActiveRecord::Base.connected_to_many([SecondAbstractClass], role: :reading) do
assert SecondAbstractClass.current_preventing_writes
assert_not ActiveRecord::Base.current_preventing_writes
end
end
test "#connected_to_many with a single argument for classes" do
ActiveRecord::Base.connected_to_many(SecondAbstractClass, role: :reading) do
assert SecondAbstractClass.current_preventing_writes
assert_not ActiveRecord::Base.current_preventing_writes
end
end
test "#connected_to_many with a multiple classes without brackets works" do
ActiveRecord::Base.connected_to_many(FirstAbstractClass, SecondAbstractClass, role: :reading) do
assert FirstAbstractClass.current_preventing_writes
assert SecondAbstractClass.current_preventing_writes
assert_not ActiveRecord::Base.current_preventing_writes
end
end
end
# frozen_string_literal: true
module ActiveRecord
class PredicateBuilder
class BasicObjectHandler # :nodoc:
def initialize(predicate_builder)
@predicate_builder = predicate_builder
end
def call(attribute, value)
bind = predicate_builder.build_bind_attribute(attribute.name, value)
attribute.eq(bind)
end
private
attr_reader :predicate_builder
end
end
end
# frozen_string_literal: true
module ActiveRecord
module Associations
class Preloader
class Batch # :nodoc:
def initialize(preloaders, available_records:)
@preloaders = preloaders.reject(&:empty?)
@available_records = available_records.flatten.group_by { |r| r.class.base_class }
end
def call
branches = @preloaders.flat_map(&:branches)
until branches.empty?
loaders = branches.flat_map(&:runnable_loaders)
loaders.each { |loader| loader.associate_records_from_unscoped(@available_records[loader.klass.base_class]) }
if loaders.any?
future_tables = branches.flat_map do |branch|
branch.future_classes - branch.runnable_loaders.map(&:klass)
end.map(&:table_name).uniq
target_loaders = loaders.reject { |l| future_tables.include?(l.table_name) }
target_loaders = loaders if target_loaders.empty?
group_and_load_similar(target_loaders)
target_loaders.each(&:run)
end
finished, in_progress = branches.partition(&:done?)
branches = in_progress + finished.flat_map(&:children)
end
end
private
attr_reader :loaders
def group_and_load_similar(loaders)
loaders.grep_v(ThroughAssociation).group_by(&:loader_query).each_pair do |query, similar_loaders|
query.load_records_in_batch(similar_loaders)
end
end
end
end
end
end
# frozen_string_literal: true
module ActiveRecord
module Batches
class BatchEnumerator
include Enumerable
def initialize(of: 1000, start: nil, finish: nil, relation:) # :nodoc:
@of = of
@relation = relation
@start = start
@finish = finish
end
# The primary key value from which the BatchEnumerator starts, inclusive of the value.
attr_reader :start
# The primary key value at which the BatchEnumerator ends, inclusive of the value.
attr_reader :finish
# The relation from which the BatchEnumerator yields batches.
attr_reader :relation
# The size of the batches yielded by the BatchEnumerator.
def batch_size
@of
end
# Looping through a collection of records from the database (using the
# +all+ method, for example) is very inefficient since it will try to
# instantiate all the objects at once.
#
# In that case, batch processing methods allow you to work with the
# records in batches, thereby greatly reducing memory consumption.
#
# Person.in_batches.each_record do |person|
# person.do_awesome_stuff
# end
#
# Person.where("age > 21").in_batches(of: 10).each_record do |person|
# person.party_all_night!
# end
#
# If you do not provide a block to #each_record, it will return an Enumerator
# for chaining with other methods:
#
# Person.in_batches.each_record.with_index do |person, index|
# person.award_trophy(index + 1)
# end
def each_record(&block)
return to_enum(:each_record) unless block_given?
@relation.to_enum(:in_batches, of: @of, start: @start, finish: @finish, load: true).each do |relation|
relation.records.each(&block)
end
end
# Deletes records in batches. Returns the total number of rows affected.
#
# Person.in_batches.delete_all
#
# See Relation#delete_all for details of how each batch is deleted.
def delete_all
sum(&:delete_all)
end
# Updates records in batches. Returns the total number of rows affected.
#
# Person.in_batches.update_all("age = age + 1")
#
# See Relation#update_all for details of how each batch is updated.
def update_all(updates)
sum do |relation|
relation.update_all(updates)
end
end
# Destroys records in batches.
#
# Person.where("age < 10").in_batches.destroy_all
#
# See Relation#destroy_all for details of how each batch is destroyed.
def destroy_all
each(&:destroy_all)
end
# Yields an ActiveRecord::Relation object for each batch of records.
#
# Person.in_batches.each do |relation|
# relation.update_all(awesome: true)
# end
def each(&block)
enum = @relation.to_enum(:in_batches, of: @of, start: @start, finish: @finish, load: false)
return enum.each(&block) if block_given?
enum
end
end
end
end
# frozen_string_literal: true
require "active_record/relation/batches/batch_enumerator"
module ActiveRecord
module Batches
ORDER_IGNORE_MESSAGE = "Scoped order is ignored, it's forced to be batch order."
# Looping through a collection of records from the database
# (using the Scoping::Named::ClassMethods.all method, for example)
# is very inefficient since it will try to instantiate all the objects at once.
#
# In that case, batch processing methods allow you to work
# with the records in batches, thereby greatly reducing memory consumption.
#
# The #find_each method uses #find_in_batches with a batch size of 1000 (or as
# specified by the +:batch_size+ option).
#
# Person.find_each do |person|
# person.do_awesome_stuff
# end
#
# Person.where("age > 21").find_each do |person|
# person.party_all_night!
# end
#
# If you do not provide a block to #find_each, it will return an Enumerator
# for chaining with other methods:
#
# Person.find_each.with_index do |person, index|
# person.award_trophy(index + 1)
# end
#
# ==== Options
# * <tt>:batch_size</tt> - Specifies the size of the batch. Defaults to 1000.
# * <tt>:start</tt> - Specifies the primary key value to start from, inclusive of the value.
# * <tt>:finish</tt> - Specifies the primary key value to end at, inclusive of the value.
# * <tt>:error_on_ignore</tt> - Overrides the application config to specify if an error should be raised when
# an order is present in the relation.
# * <tt>:order</tt> - Specifies the primary key order (can be +:asc+ or +:desc+). Defaults to +:asc+.
#
# Limits are honored, and if present there is no requirement for the batch
# size: it can be less than, equal to, or greater than the limit.
#
# The options +start+ and +finish+ are especially useful if you want
# multiple workers dealing with the same processing queue. You can make
# worker 1 handle all the records between id 1 and 9999 and worker 2
# handle from 10000 and beyond by setting the +:start+ and +:finish+
# option on each worker.
#
# # In worker 1, let's process until 9999 records.
# Person.find_each(finish: 9_999) do |person|
# person.party_all_night!
# end
#
# # In worker 2, let's process from record 10_000 and onwards.
# Person.find_each(start: 10_000) do |person|
# person.party_all_night!
# end
#
# NOTE: Order can be ascending (:asc) or descending (:desc). It is automatically set to
# ascending on the primary key ("id ASC").
# This also means that this method only works when the primary key is
# orderable (e.g. an integer or string).
#
# NOTE: By its nature, batch processing is subject to race conditions if
# other processes are modifying the database.
def find_each(start: nil, finish: nil, batch_size: 1000, error_on_ignore: nil, order: :asc, &block)
if block_given?
find_in_batches(start: start, finish: finish, batch_size: batch_size, error_on_ignore: error_on_ignore, order: order) do |records|
records.each(&block)
end
else
enum_for(:find_each, start: start, finish: finish, batch_size: batch_size, error_on_ignore: error_on_ignore, order: order) do
relation = self
apply_limits(relation, start, finish, order).size
end
end
end
# Yields each batch of records that was found by the find options as
# an array.
#
# Person.where("age > 21").find_in_batches do |group|
# sleep(50) # Make sure it doesn't get too crowded in there!
# group.each { |person| person.party_all_night! }
# end
#
# If you do not provide a block to #find_in_batches, it will return an Enumerator
# for chaining with other methods:
#
# Person.find_in_batches.with_index do |group, batch|
# puts "Processing group ##{batch}"
# group.each(&:recover_from_last_night!)
# end
#
# To be yielded each record one by one, use #find_each instead.
#
# ==== Options
# * <tt>:batch_size</tt> - Specifies the size of the batch. Defaults to 1000.
# * <tt>:start</tt> - Specifies the primary key value to start from, inclusive of the value.
# * <tt>:finish</tt> - Specifies the primary key value to end at, inclusive of the value.
# * <tt>:error_on_ignore</tt> - Overrides the application config to specify if an error should be raised when
# an order is present in the relation.
# * <tt>:order</tt> - Specifies the primary key order (can be +:asc+ or +:desc+). Defaults to +:asc+.
#
# Limits are honored, and if present there is no requirement for the batch
# size: it can be less than, equal to, or greater than the limit.
#
# The options +start+ and +finish+ are especially useful if you want
# multiple workers dealing with the same processing queue. You can make
# worker 1 handle all the records between id 1 and 9999 and worker 2
# handle from 10000 and beyond by setting the +:start+ and +:finish+
# option on each worker.
#
# # Let's process from record 10_000 on.
# Person.find_in_batches(start: 10_000) do |group|
# group.each { |person| person.party_all_night! }
# end
#
# NOTE: Order can be ascending (:asc) or descending (:desc). It is automatically set to
# ascending on the primary key ("id ASC").
# This also means that this method only works when the primary key is
# orderable (e.g. an integer or string).
#
# NOTE: By its nature, batch processing is subject to race conditions if
# other processes are modifying the database.
def find_in_batches(start: nil, finish: nil, batch_size: 1000, error_on_ignore: nil, order: :asc)
relation = self
unless block_given?
return to_enum(:find_in_batches, start: start, finish: finish, batch_size: batch_size, error_on_ignore: error_on_ignore, order: order) do
total = apply_limits(relation, start, finish, order).size
(total - 1).div(batch_size) + 1
end
end
in_batches(of: batch_size, start: start, finish: finish, load: true, error_on_ignore: error_on_ignore, order: order) do |batch|
yield batch.to_a
end
end
# Yields ActiveRecord::Relation objects to work with a batch of records.
#
# Person.where("age > 21").in_batches do |relation|
# relation.delete_all
# sleep(10) # Throttle the delete queries
# end
#
# If you do not provide a block to #in_batches, it will return a
# BatchEnumerator which is enumerable.
#
# Person.in_batches.each_with_index do |relation, batch_index|
# puts "Processing relation ##{batch_index}"
# relation.delete_all
# end
#
# Examples of calling methods on the returned BatchEnumerator object:
#
# Person.in_batches.delete_all
# Person.in_batches.update_all(awesome: true)
# Person.in_batches.each_record(&:party_all_night!)
#
# ==== Options
# * <tt>:of</tt> - Specifies the size of the batch. Defaults to 1000.
# * <tt>:load</tt> - Specifies if the relation should be loaded. Defaults to false.
# * <tt>:start</tt> - Specifies the primary key value to start from, inclusive of the value.
# * <tt>:finish</tt> - Specifies the primary key value to end at, inclusive of the value.
# * <tt>:error_on_ignore</tt> - Overrides the application config to specify if an error should be raised when
# an order is present in the relation.
# * <tt>:order</tt> - Specifies the primary key order (can be +:asc+ or +:desc+). Defaults to +:asc+.
#
# Limits are honored, and if present there is no requirement for the batch
# size, it can be less than, equal, or greater than the limit.
#
# The options +start+ and +finish+ are especially useful if you want
# multiple workers dealing with the same processing queue. You can make
# worker 1 handle all the records between id 1 and 9999 and worker 2
# handle from 10000 and beyond by setting the +:start+ and +:finish+
# option on each worker.
#
# # Let's process from record 10_000 on.
# Person.in_batches(start: 10_000).update_all(awesome: true)
#
# An example of calling where query method on the relation:
#
# Person.in_batches.each do |relation|
# relation.update_all('age = age + 1')
# relation.where('age > 21').update_all(should_party: true)
# relation.where('age <= 21').delete_all
# end
#
# NOTE: If you are going to iterate through each record, you should call
# #each_record on the yielded BatchEnumerator:
#
# Person.in_batches.each_record(&:party_all_night!)
#
# NOTE: Order can be ascending (:asc) or descending (:desc). It is automatically set to
# ascending on the primary key ("id ASC").
# This also means that this method only works when the primary key is
# orderable (e.g. an integer or string).
#
# NOTE: By its nature, batch processing is subject to race conditions if
# other processes are modifying the database.
def in_batches(of: 1000, start: nil, finish: nil, load: false, error_on_ignore: nil, order: :asc)
relation = self
unless block_given?
return BatchEnumerator.new(of: of, start: start, finish: finish, relation: self)
end
unless [:asc, :desc].include?(order)
raise ArgumentError, ":order must be :asc or :desc, got #{order.inspect}"
end
if arel.orders.present?
act_on_ignored_order(error_on_ignore)
end
batch_limit = of
if limit_value
remaining = limit_value
batch_limit = remaining if remaining < batch_limit
end
relation = relation.reorder(batch_order(order)).limit(batch_limit)
relation = apply_limits(relation, start, finish, order)
relation.skip_query_cache! # Retaining the results in the query cache would undermine the point of batching
batch_relation = relation
loop do
if load
records = batch_relation.records
ids = records.map(&:id)
yielded_relation = where(primary_key => ids)
yielded_relation.load_records(records)
else
ids = batch_relation.pluck(primary_key)
yielded_relation = where(primary_key => ids)
end
break if ids.empty?
primary_key_offset = ids.last
raise ArgumentError.new("Primary key not included in the custom select clause") unless primary_key_offset
yield yielded_relation
break if ids.length < batch_limit
if limit_value
remaining -= ids.length
if remaining == 0
# Saves a useless iteration when the limit is a multiple of the
# batch size.
break
elsif remaining < batch_limit
relation = relation.limit(remaining)
end
end
batch_relation = relation.where(
predicate_builder[primary_key, primary_key_offset, order == :desc ? :lt : :gt]
)
end
end
private
def apply_limits(relation, start, finish, order)
relation = apply_start_limit(relation, start, order) if start
relation = apply_finish_limit(relation, finish, order) if finish
relation
end
def apply_start_limit(relation, start, order)
relation.where(predicate_builder[primary_key, start, order == :desc ? :lteq : :gteq])
end
def apply_finish_limit(relation, finish, order)
relation.where(predicate_builder[primary_key, finish, order == :desc ? :gteq : :lteq])
end
def batch_order(order)
table[primary_key].public_send(order)
end
def act_on_ignored_order(error_on_ignore)
raise_error = (error_on_ignore.nil? ? ActiveRecord.error_on_ignored_order : error_on_ignore)
if raise_error
raise ArgumentError.new(ORDER_IGNORE_MESSAGE)
elsif logger
logger.warn(ORDER_IGNORE_MESSAGE)
end
end
end
end
# frozen_string_literal: true
require "cases/helper"
require "models/comment"
require "models/post"
require "models/subscriber"
class EachTest < ActiveRecord::TestCase
fixtures :posts, :subscribers
def setup
@posts = Post.order("id asc")
@total = Post.count
Post.count("id") # preheat arel's table cache
end
def test_each_should_execute_one_query_per_batch
assert_queries(@total + 1) do
Post.find_each(batch_size: 1) do |post|
assert_kind_of Post, post
end
end
end
def test_each_should_not_return_query_chain_and_execute_only_one_query
assert_queries(1) do
result = Post.find_each(batch_size: 100000) { }
assert_nil result
end
end
def test_each_should_return_an_enumerator_if_no_block_is_present
assert_queries(1) do
Post.find_each(batch_size: 100000).with_index do |post, index|
assert_kind_of Post, post
assert_kind_of Integer, index
end
end
end
def test_each_should_return_a_sized_enumerator
assert_equal 11, Post.find_each(batch_size: 1).size
assert_equal 5, Post.find_each(batch_size: 2, start: 7).size
assert_equal 11, Post.find_each(batch_size: 10_000).size
end
def test_each_enumerator_should_execute_one_query_per_batch
assert_queries(@total + 1) do
Post.find_each(batch_size: 1).with_index do |post, index|
assert_kind_of Post, post
assert_kind_of Integer, index
end
end
end
def test_each_should_raise_if_select_is_set_without_id
assert_raise(ArgumentError) do
Post.select(:title).find_each(batch_size: 1) { |post|
flunk "should not call this block"
}
end
end
def test_each_should_execute_if_id_is_in_select
assert_queries(6) do
Post.select("id, title, type").find_each(batch_size: 2) do |post|
assert_kind_of Post, post
end
end
end
test "find_each should honor limit if passed a block" do
limit = @total - 1
total = 0
Post.limit(limit).find_each do |post|
total += 1
end
assert_equal limit, total
end
test "find_each should honor limit if no block is passed" do
limit = @total - 1
total = 0
Post.limit(limit).find_each.each do |post|
total += 1
end
assert_equal limit, total
end
def test_warn_if_order_scope_is_set
assert_called(ActiveRecord::Base.logger, :warn) do
Post.order("title").find_each { |post| post }
end
end
def test_logger_not_required
previous_logger = ActiveRecord::Base.logger
ActiveRecord::Base.logger = nil
assert_nothing_raised do
Post.order("comments_count DESC").find_each { |post| post }
end
ensure
ActiveRecord::Base.logger = previous_logger
end
def test_find_in_batches_should_return_batches
assert_queries(@total + 1) do
Post.find_in_batches(batch_size: 1) do |batch|
assert_kind_of Array, batch
assert_kind_of Post, batch.first
end
end
end
def test_find_in_batches_should_start_from_the_start_option
assert_queries(@total) do
Post.find_in_batches(batch_size: 1, start: 2) do |batch|
assert_kind_of Array, batch
assert_kind_of Post, batch.first
end
end
end
def test_find_in_batches_should_end_at_the_finish_option
assert_queries(6) do
Post.find_in_batches(batch_size: 1, finish: 5) do |batch|
assert_kind_of Array, batch
assert_kind_of Post, batch.first
end
end
end
def test_find_in_batches_shouldnt_execute_query_unless_needed
assert_queries(2) do
Post.find_in_batches(batch_size: @total) { |batch| assert_kind_of Array, batch }
end
assert_queries(1) do
Post.find_in_batches(batch_size: @total + 1) { |batch| assert_kind_of Array, batch }
end
end
def test_find_in_batches_should_quote_batch_order
c = Post.connection
assert_sql(/ORDER BY #{Regexp.escape(c.quote_table_name("posts.id"))}/i) do
Post.find_in_batches(batch_size: 1) do |batch|
assert_kind_of Array, batch
assert_kind_of Post, batch.first
end
end
end
def test_find_in_batches_should_quote_batch_order_with_desc_order
c = Post.connection
assert_sql(/ORDER BY #{Regexp.escape(c.quote_table_name("posts.id"))} DESC/) do
Post.find_in_batches(batch_size: 1, order: :desc) do |batch|
assert_kind_of Array, batch
assert_kind_of Post, batch.first
end
end
end
def test_each_should_raise_if_order_is_invalid
assert_raise(ArgumentError) do
Post.select(:title).find_each(batch_size: 1, order: :invalid) { |post|
flunk "should not call this block"
}
end
end
def test_find_in_batches_should_not_use_records_after_yielding_them_in_case_original_array_is_modified
not_a_post = +"not a post"
def not_a_post.id; end
not_a_post.stub(:id, -> { raise StandardError.new("not_a_post had #id called on it") }) do
assert_nothing_raised do
Post.find_in_batches(batch_size: 1) do |batch|
assert_kind_of Array, batch
assert_kind_of Post, batch.first
batch.map! { not_a_post }
end
end
end
end
def test_find_in_batches_should_ignore_the_order_default_scope
# First post is with title scope
first_post = PostWithDefaultScope.first
posts = []
PostWithDefaultScope.find_in_batches do |batch|
posts.concat(batch)
end
# posts.first will be ordered using id only. Title order scope should not apply here
assert_not_equal first_post, posts.first
assert_equal posts(:welcome).id, posts.first.id
end
def test_find_in_batches_should_error_on_ignore_the_order
assert_raise(ArgumentError) do
PostWithDefaultScope.find_in_batches(error_on_ignore: true) { }
end
end
def test_find_in_batches_should_not_error_if_config_overridden
# Set the config option which will be overridden
prev = ActiveRecord.error_on_ignored_order
ActiveRecord.error_on_ignored_order = true
assert_nothing_raised do
PostWithDefaultScope.find_in_batches(error_on_ignore: false) { }
end
ensure
# Set back to default
ActiveRecord.error_on_ignored_order = prev
end
def test_find_in_batches_should_error_on_config_specified_to_error
# Set the config option
prev = ActiveRecord.error_on_ignored_order
ActiveRecord.error_on_ignored_order = true
assert_raise(ArgumentError) do
PostWithDefaultScope.find_in_batches() { }
end
ensure
# Set back to default
ActiveRecord.error_on_ignored_order = prev
end
def test_find_in_batches_should_not_error_by_default
assert_nothing_raised do
PostWithDefaultScope.find_in_batches() { }
end
end
def test_find_in_batches_should_not_ignore_the_default_scope_if_it_is_other_then_order
default_scope = SpecialPostWithDefaultScope.all
posts = []
SpecialPostWithDefaultScope.find_in_batches do |batch|
posts.concat(batch)
end
assert_equal default_scope.pluck(:id).sort, posts.map(&:id).sort
end
def test_find_in_batches_should_use_any_column_as_primary_key
nick_order_subscribers = Subscriber.order("nick asc")
start_nick = nick_order_subscribers.second.nick
subscribers = []
Subscriber.find_in_batches(batch_size: 1, start: start_nick) do |batch|
subscribers.concat(batch)
end
assert_equal nick_order_subscribers[1..-1].map(&:id), subscribers.map(&:id)
end
def test_find_in_batches_should_use_any_column_as_primary_key_when_start_is_not_specified
assert_queries(Subscriber.count + 1) do
Subscriber.find_in_batches(batch_size: 1) do |batch|
assert_kind_of Array, batch
assert_kind_of Subscriber, batch.first
end
end
end
def test_find_in_batches_should_return_an_enumerator
enum = nil
assert_no_queries do
enum = Post.find_in_batches(batch_size: 1)
end
assert_queries(4) do
enum.first(4) do |batch|
assert_kind_of Array, batch
assert_kind_of Post, batch.first
end
end
end
test "find_in_batches should honor limit if passed a block" do
limit = @total - 1
total = 0
Post.limit(limit).find_in_batches do |batch|
total += batch.size
end
assert_equal limit, total
end
test "find_in_batches should honor limit if no block is passed" do
limit = @total - 1
total = 0
Post.limit(limit).find_in_batches.each do |batch|
total += batch.size
end
assert_equal limit, total
end
def test_in_batches_should_not_execute_any_query
assert_no_queries do
assert_kind_of ActiveRecord::Batches::BatchEnumerator, Post.in_batches(of: 2)
end
end
def test_in_batches_has_attribute_readers
enumerator = Post.no_comments.in_batches(of: 2, start: 42, finish: 84)
assert_equal Post.no_comments, enumerator.relation
assert_equal 2, enumerator.batch_size
assert_equal 42, enumerator.start
assert_equal 84, enumerator.finish
end
def test_in_batches_should_yield_relation_if_block_given
assert_queries(6) do
Post.in_batches(of: 2) do |relation|
assert_kind_of ActiveRecord::Relation, relation
end
end
end
def test_in_batches_should_be_enumerable_if_no_block_given
assert_queries(6) do
Post.in_batches(of: 2).each do |relation|
assert_kind_of ActiveRecord::Relation, relation
end
end
end
def test_in_batches_each_record_should_yield_record_if_block_is_given
assert_queries(6) do
Post.in_batches(of: 2).each_record do |post|
assert_predicate post.title, :present?
assert_kind_of Post, post
end
end
end
def test_in_batches_each_record_should_return_enumerator_if_no_block_given
assert_queries(6) do
Post.in_batches(of: 2).each_record.with_index do |post, i|
assert_predicate post.title, :present?
assert_kind_of Post, post
end
end
end
def test_in_batches_each_record_should_be_ordered_by_id
ids = Post.order("id ASC").pluck(:id)
assert_queries(6) do
Post.in_batches(of: 2).each_record.with_index do |post, i|
assert_equal ids[i], post.id
end
end
end
def test_in_batches_update_all_affect_all_records
assert_queries(6 + 6) do # 6 selects, 6 updates
Post.in_batches(of: 2).update_all(title: "updated-title")
end
assert_equal Post.all.pluck(:title), ["updated-title"] * Post.count
end
def test_in_batches_update_all_returns_rows_affected
assert_equal 11, Post.in_batches(of: 2).update_all(title: "updated-title")
end
def test_in_batches_update_all_returns_zero_when_no_batches
assert_equal 0, Post.where("1=0").in_batches(of: 2).update_all(title: "updated-title")
end
def test_in_batches_delete_all_should_not_delete_records_in_other_batches
not_deleted_count = Post.where("id <= 2").count
Post.where("id > 2").in_batches(of: 2).delete_all
assert_equal 0, Post.where("id > 2").count
assert_equal not_deleted_count, Post.count
end
def test_in_batches_delete_all_returns_rows_affected
assert_equal 11, Post.in_batches(of: 2).delete_all
end
def test_in_batches_delete_all_returns_zero_when_no_batches
assert_equal 0, Post.where("1=0").in_batches(of: 2).delete_all
end
def test_in_batches_should_not_be_loaded
Post.in_batches(of: 1) do |relation|
assert_not_predicate relation, :loaded?
end
Post.in_batches(of: 1, load: false) do |relation|
assert_not_predicate relation, :loaded?
end
end
def test_in_batches_should_be_loaded
Post.in_batches(of: 1, load: true) do |relation|
assert_predicate relation, :loaded?
end
end
def test_in_batches_if_not_loaded_executes_more_queries
assert_queries(@total + 1) do
Post.in_batches(of: 1, load: false) do |relation|
assert_not_predicate relation, :loaded?
end
end
end
def test_in_batches_should_return_relations
assert_queries(@total + 1) do
Post.in_batches(of: 1) do |relation|
assert_kind_of ActiveRecord::Relation, relation
end
end
end
def test_in_batches_should_start_from_the_start_option
post = Post.order("id ASC").where("id >= ?", 2).first
assert_queries(2) do
relation = Post.in_batches(of: 1, start: 2).first
assert_equal post, relation.first
end
end
def test_in_batches_should_end_at_the_finish_option
post = Post.order("id DESC").where("id <= ?", 5).first
assert_queries(7) do
relation = Post.in_batches(of: 1, finish: 5, load: true).reverse_each.first
assert_equal post, relation.last
end
end
def test_in_batches_shouldnt_execute_query_unless_needed
assert_queries(2) do
Post.in_batches(of: @total) { |relation| assert_kind_of ActiveRecord::Relation, relation }
end
assert_queries(1) do
Post.in_batches(of: @total + 1) { |relation| assert_kind_of ActiveRecord::Relation, relation }
end
end
def test_in_batches_should_quote_batch_order
c = Post.connection
assert_sql(/ORDER BY #{c.quote_table_name('posts')}\.#{c.quote_column_name('id')}/) do
Post.in_batches(of: 1) do |relation|
assert_kind_of ActiveRecord::Relation, relation
assert_kind_of Post, relation.first
end
end
end
def test_in_batches_should_quote_batch_order_with_desc_order
c = Post.connection
assert_sql(/ORDER BY #{Regexp.escape(c.quote_table_name("posts.id"))} DESC/) do
Post.in_batches(of: 1, order: :desc) do |relation|
assert_kind_of ActiveRecord::Relation, relation
assert_kind_of Post, relation.first
end
end
end
def test_in_batches_should_not_use_records_after_yielding_them_in_case_original_array_is_modified
not_a_post = +"not a post"
def not_a_post.id
raise StandardError.new("not_a_post had #id called on it")
end
assert_nothing_raised do
Post.in_batches(of: 1) do |relation|
assert_kind_of ActiveRecord::Relation, relation
assert_kind_of Post, relation.first
[not_a_post] * relation.count
end
end
end
def test_in_batches_should_not_ignore_default_scope_without_order_statements
default_scope = SpecialPostWithDefaultScope.all
posts = []
SpecialPostWithDefaultScope.in_batches do |relation|
posts.concat(relation)
end
assert_equal default_scope.pluck(:id).sort, posts.map(&:id).sort
end
def test_in_batches_should_use_any_column_as_primary_key
nick_order_subscribers = Subscriber.order("nick asc")
start_nick = nick_order_subscribers.second.nick
subscribers = []
Subscriber.in_batches(of: 1, start: start_nick) do |relation|
subscribers.concat(relation)
end
assert_equal nick_order_subscribers[1..-1].map(&:id), subscribers.map(&:id)
end
def test_in_batches_should_use_any_column_as_primary_key_when_start_is_not_specified
assert_queries(Subscriber.count + 1) do
Subscriber.in_batches(of: 1, load: true) do |relation|
assert_kind_of ActiveRecord::Relation, relation
assert_kind_of Subscriber, relation.first
end
end
end
def test_in_batches_should_return_an_enumerator
enum = nil
assert_no_queries do
enum = Post.in_batches(of: 1)
end
assert_queries(4) do
enum.first(4) do |relation|
assert_kind_of ActiveRecord::Relation, relation
assert_kind_of Post, relation.first
end
end
end
def test_in_batches_relations_should_not_overlap_with_each_other
seen_posts = []
Post.in_batches(of: 2, load: true) do |relation|
relation.to_a.each do |post|
assert_not seen_posts.include?(post)
seen_posts << post
end
end
end
def test_in_batches_relations_with_condition_should_not_overlap_with_each_other
seen_posts = []
author_id = Post.first.author_id
posts_by_author = Post.where(author_id: author_id)
Post.in_batches(of: 2) do |batch|
seen_posts += batch.where(author_id: author_id)
end
assert_equal posts_by_author.pluck(:id).sort, seen_posts.map(&:id).sort
end
def test_in_batches_relations_update_all_should_not_affect_matching_records_in_other_batches
Post.update_all(author_id: 0)
person = Post.last
person.update(author_id: 1)
Post.in_batches(of: 2) do |batch|
batch.where("author_id >= 1").update_all("author_id = author_id + 1")
end
assert_equal 2, person.reload.author_id # incremented only once
end
def test_find_in_batches_should_return_a_sized_enumerator
assert_equal 11, Post.find_in_batches(batch_size: 1).size
assert_equal 6, Post.find_in_batches(batch_size: 2).size
assert_equal 4, Post.find_in_batches(batch_size: 2, start: 4).size
assert_equal 4, Post.find_in_batches(batch_size: 3).size
assert_equal 1, Post.find_in_batches(batch_size: 10_000).size
end
[true, false].each do |load|
test "in_batches should return limit records when limit is less than batch size and load is #{load}" do
limit = 3
batch_size = 5
total = 0
Post.limit(limit).in_batches(of: batch_size, load: load) do |batch|
total += batch.count
end
assert_equal limit, total
end
test "in_batches should return limit records when limit is greater than batch size and load is #{load}" do
limit = 5
batch_size = 3
total = 0
Post.limit(limit).in_batches(of: batch_size, load: load) do |batch|
total += batch.count
end
assert_equal limit, total
end
test "in_batches should return limit records when limit is a multiple of the batch size and load is #{load}" do
limit = 6
batch_size = 3
total = 0
Post.limit(limit).in_batches(of: batch_size, load: load) do |batch|
total += batch.count
end
assert_equal limit, total
end
test "in_batches should return no records if the limit is 0 and load is #{load}" do
limit = 0
batch_size = 1
total = 0
Post.limit(limit).in_batches(of: batch_size, load: load) do |batch|
total += batch.count
end
assert_equal limit, total
end
test "in_batches should return all if the limit is greater than the number of records when load is #{load}" do
limit = @total + 1
batch_size = 1
total = 0
Post.limit(limit).in_batches(of: batch_size, load: load) do |batch|
total += batch.count
end
assert_equal @total, total
end
end
test ".find_each respects table alias" do
assert_queries(1) do
table_alias = Post.arel_table.alias("omg_posts")
table_metadata = ActiveRecord::TableMetadata.new(Post, table_alias)
predicate_builder = ActiveRecord::PredicateBuilder.new(table_metadata)
posts = ActiveRecord::Relation.create(
Post,
table: table_alias,
predicate_builder: predicate_builder
)
posts.find_each { }
end
end
test ".find_each bypasses the query cache for its own queries" do
Post.cache do
assert_queries(2) do
Post.find_each { }
Post.find_each { }
end
end
end
test ".find_each does not disable the query cache inside the given block" do
Post.cache do
Post.find_each(start: 1, finish: 1) do |post|
assert_queries(1) do
post.comments.count
post.comments.count
end
end
end
end
test ".find_in_batches bypasses the query cache for its own queries" do
Post.cache do
assert_queries(2) do
Post.find_in_batches { }
Post.find_in_batches { }
end
end
end
test ".find_in_batches does not disable the query cache inside the given block" do
Post.cache do
Post.find_in_batches(start: 1, finish: 1) do |batch|
assert_queries(1) do
batch.first.comments.count
batch.first.comments.count
end
end
end
end
test ".in_batches bypasses the query cache for its own queries" do
Post.cache do
assert_queries(2) do
Post.in_batches { }
Post.in_batches { }
end
end
end
test ".in_batches does not disable the query cache inside the given block" do
Post.cache do
Post.in_batches(start: 1, finish: 1) do |relation|
assert_queries(1) do
relation.count
relation.count
end
end
end
end
end
# frozen_string_literal: true
module ActiveRecord
module AttributeMethods
# = Active Record Attribute Methods Before Type Cast
#
# ActiveRecord::AttributeMethods::BeforeTypeCast provides a way to
# read the value of the attributes before typecasting and deserialization.
#
# class Task < ActiveRecord::Base
# end
#
# task = Task.new(id: '1', completed_on: '2012-10-21')
# task.id # => 1
# task.completed_on # => Sun, 21 Oct 2012
#
# task.attributes_before_type_cast
# # => {"id"=>"1", "completed_on"=>"2012-10-21", ... }
# task.read_attribute_before_type_cast('id') # => "1"
# task.read_attribute_before_type_cast('completed_on') # => "2012-10-21"
#
# In addition to #read_attribute_before_type_cast and #attributes_before_type_cast,
# it declares a method for all attributes with the <tt>*_before_type_cast</tt>
# suffix.
#
# task.id_before_type_cast # => "1"
# task.completed_on_before_type_cast # => "2012-10-21"
module BeforeTypeCast
extend ActiveSupport::Concern
included do
attribute_method_suffix "_before_type_cast", "_for_database", parameters: false
attribute_method_suffix "_came_from_user?", parameters: false
end
# Returns the value of the attribute identified by +attr_name+ before
# typecasting and deserialization.
#
# class Task < ActiveRecord::Base
# end
#
# task = Task.new(id: '1', completed_on: '2012-10-21')
# task.read_attribute('id') # => 1
# task.read_attribute_before_type_cast('id') # => '1'
# task.read_attribute('completed_on') # => Sun, 21 Oct 2012
# task.read_attribute_before_type_cast('completed_on') # => "2012-10-21"
# task.read_attribute_before_type_cast(:completed_on) # => "2012-10-21"
def read_attribute_before_type_cast(attr_name)
name = attr_name.to_s
name = self.class.attribute_aliases[name] || name
attribute_before_type_cast(name)
end
# Returns a hash of attributes before typecasting and deserialization.
#
# class Task < ActiveRecord::Base
# end
#
# task = Task.new(title: nil, is_done: true, completed_on: '2012-10-21')
# task.attributes
# # => {"id"=>nil, "title"=>nil, "is_done"=>true, "completed_on"=>Sun, 21 Oct 2012, "created_at"=>nil, "updated_at"=>nil}
# task.attributes_before_type_cast
# # => {"id"=>nil, "title"=>nil, "is_done"=>true, "completed_on"=>"2012-10-21", "created_at"=>nil, "updated_at"=>nil}
def attributes_before_type_cast
@attributes.values_before_type_cast
end
# Returns a hash of attributes for assignment to the database.
def attributes_for_database
@attributes.values_for_database
end
private
# Dispatch target for <tt>*_before_type_cast</tt> attribute methods.
def attribute_before_type_cast(attr_name)
@attributes[attr_name].value_before_type_cast
end
def attribute_for_database(attr_name)
@attributes[attr_name].value_for_database
end
def attribute_came_from_user?(attr_name)
@attributes[attr_name].came_from_user?
end
end
end
end
# frozen_string_literal: true
module ActiveRecord::Associations::Builder # :nodoc:
class BelongsTo < SingularAssociation # :nodoc:
def self.macro
:belongs_to
end
def self.valid_options(options)
valid = super + [:polymorphic, :counter_cache, :optional, :default]
valid += [:foreign_type] if options[:polymorphic]
valid += [:ensuring_owner_was] if options[:dependent] == :destroy_async
valid
end
def self.valid_dependent_options
[:destroy, :delete, :destroy_async]
end
def self.define_callbacks(model, reflection)
super
add_counter_cache_callbacks(model, reflection) if reflection.options[:counter_cache]
add_touch_callbacks(model, reflection) if reflection.options[:touch]
add_default_callbacks(model, reflection) if reflection.options[:default]
end
def self.add_counter_cache_callbacks(model, reflection)
cache_column = reflection.counter_cache_column
model.after_update lambda { |record|
association = association(reflection.name)
if association.saved_change_to_target?
association.increment_counters
association.decrement_counters_before_last_save
end
}
klass = reflection.class_name.safe_constantize
klass.attr_readonly cache_column if klass && klass.respond_to?(:attr_readonly)
end
def self.touch_record(o, changes, foreign_key, name, touch, touch_method) # :nodoc:
old_foreign_id = changes[foreign_key] && changes[foreign_key].first
if old_foreign_id
association = o.association(name)
reflection = association.reflection
if reflection.polymorphic?
foreign_type = reflection.foreign_type
klass = changes[foreign_type] && changes[foreign_type].first || o.public_send(foreign_type)
klass = o.class.polymorphic_class_for(klass)
else
klass = association.klass
end
primary_key = reflection.association_primary_key(klass)
old_record = klass.find_by(primary_key => old_foreign_id)
if old_record
if touch != true
old_record.public_send(touch_method, touch)
else
old_record.public_send(touch_method)
end
end
end
record = o.public_send name
if record && record.persisted?
if touch != true
record.public_send(touch_method, touch)
else
record.public_send(touch_method)
end
end
end
def self.add_touch_callbacks(model, reflection)
foreign_key = reflection.foreign_key
name = reflection.name
touch = reflection.options[:touch]
callback = lambda { |changes_method| lambda { |record|
BelongsTo.touch_record(record, record.send(changes_method), foreign_key, name, touch, belongs_to_touch_method)
}}
if reflection.counter_cache_column
touch_callback = callback.(:saved_changes)
update_callback = lambda { |record|
instance_exec(record, &touch_callback) unless association(reflection.name).saved_change_to_target?
}
model.after_update update_callback, if: :saved_changes?
else
model.after_create callback.(:saved_changes), if: :saved_changes?
model.after_update callback.(:saved_changes), if: :saved_changes?
model.after_destroy callback.(:changes_to_save)
end
model.after_touch callback.(:changes_to_save)
end
def self.add_default_callbacks(model, reflection)
model.before_validation lambda { |o|
o.association(reflection.name).default(&reflection.options[:default])
}
end
def self.add_destroy_callbacks(model, reflection)
model.after_destroy lambda { |o| o.association(reflection.name).handle_dependency }
end
def self.define_validations(model, reflection)
if reflection.options.key?(:required)
reflection.options[:optional] = !reflection.options.delete(:required)
end
if reflection.options[:optional].nil?
required = model.belongs_to_required_by_default
else
required = !reflection.options[:optional]
end
super
if required
model.validates_presence_of reflection.name, message: :required
end
end
def self.define_change_tracking_methods(model, reflection)
model.generated_association_methods.class_eval <<-CODE, __FILE__, __LINE__ + 1
def #{reflection.name}_changed?
association(:#{reflection.name}).target_changed?
end
def #{reflection.name}_previously_changed?
association(:#{reflection.name}).target_previously_changed?
end
CODE
end
private_class_method :macro, :valid_options, :valid_dependent_options, :define_callbacks,
:define_validations, :define_change_tracking_methods, :add_counter_cache_callbacks,
:add_touch_callbacks, :add_default_callbacks, :add_destroy_callbacks
end
end
# frozen_string_literal: true
module ActiveRecord
module Associations
# = Active Record Belongs To Association
class BelongsToAssociation < SingularAssociation # :nodoc:
def handle_dependency
return unless load_target
case options[:dependent]
when :destroy
raise ActiveRecord::Rollback unless target.destroy
when :destroy_async
id = owner.public_send(reflection.foreign_key.to_sym)
primary_key_column = reflection.active_record_primary_key.to_sym
enqueue_destroy_association(
owner_model_name: owner.class.to_s,
owner_id: owner.id,
association_class: reflection.klass.to_s,
association_ids: [id],
association_primary_key_column: primary_key_column,
ensuring_owner_was_method: options.fetch(:ensuring_owner_was, nil)
)
else
target.public_send(options[:dependent])
end
end
def inversed_from(record)
replace_keys(record)
super
end
def default(&block)
writer(owner.instance_exec(&block)) if reader.nil?
end
def reset
super
@updated = false
end
def updated?
@updated
end
def decrement_counters
update_counters(-1)
end
def increment_counters
update_counters(1)
end
def decrement_counters_before_last_save
if reflection.polymorphic?
model_type_was = owner.attribute_before_last_save(reflection.foreign_type)
model_was = owner.class.polymorphic_class_for(model_type_was) if model_type_was
else
model_was = klass
end
foreign_key_was = owner.attribute_before_last_save(reflection.foreign_key)
if foreign_key_was && model_was < ActiveRecord::Base
update_counters_via_scope(model_was, foreign_key_was, -1)
end
end
def target_changed?
owner.attribute_changed?(reflection.foreign_key) || (!foreign_key_present? && target&.new_record?)
end
def target_previously_changed?
owner.attribute_previously_changed?(reflection.foreign_key)
end
def saved_change_to_target?
owner.saved_change_to_attribute?(reflection.foreign_key)
end
private
def replace(record)
if record
raise_on_type_mismatch!(record)
set_inverse_instance(record)
@updated = true
elsif target
remove_inverse_instance(target)
end
replace_keys(record, force: true)
self.target = record
end
def update_counters(by)
if require_counter_update? && foreign_key_present?
if target && !stale_target?
target.increment!(reflection.counter_cache_column, by, touch: reflection.options[:touch])
else
update_counters_via_scope(klass, owner._read_attribute(reflection.foreign_key), by)
end
end
end
def update_counters_via_scope(klass, foreign_key, by)
scope = klass.unscoped.where!(primary_key(klass) => foreign_key)
scope.update_counters(reflection.counter_cache_column => by, touch: reflection.options[:touch])
end
def find_target?
!loaded? && foreign_key_present? && klass
end
def require_counter_update?
reflection.counter_cache_column && owner.persisted?
end
def replace_keys(record, force: false)
target_key = record ? record._read_attribute(primary_key(record.class)) : nil
if force || owner._read_attribute(reflection.foreign_key) != target_key
owner[reflection.foreign_key] = target_key
end
end
def primary_key(klass)
reflection.association_primary_key(klass)
end
def foreign_key_present?
owner._read_attribute(reflection.foreign_key)
end
def invertible_for?(record)
inverse = inverse_reflection_for(record)
inverse && (inverse.has_one? || inverse.klass.has_many_inversing)
end
def stale_state
result = owner._read_attribute(reflection.foreign_key) { |n| owner.send(:missing_attribute, n, caller) }
result && result.to_s
end
end
end
end
# frozen_string_literal: true
require "cases/helper"
require "models/developer"
require "models/project"
require "models/company"
require "models/topic"
require "models/reply"
require "models/computer"
require "models/post"
require "models/author"
require "models/tag"
require "models/tagging"
require "models/comment"
require "models/sponsor"
require "models/member"
require "models/essay"
require "models/toy"
require "models/invoice"
require "models/line_item"
require "models/column"
require "models/record"
require "models/admin"
require "models/admin/user"
require "models/ship"
require "models/treasure"
require "models/parrot"
require "models/book"
require "models/citation"
require "models/tree"
require "models/node"
require "models/club"
class BelongsToAssociationsTest < ActiveRecord::TestCase
fixtures :accounts, :companies, :developers, :projects, :topics,
:developers_projects, :computers, :authors, :author_addresses,
:essays, :posts, :tags, :taggings, :comments, :sponsors, :members, :nodes
def test_belongs_to
client = Client.find(3)
first_firm = companies(:first_firm)
assert_sql(/LIMIT|ROWNUM <=|FETCH FIRST/) do
assert_equal first_firm, client.firm
assert_equal first_firm.name, client.firm.name
end
end
def test_where_with_custom_primary_key
assert_equal [authors(:david)], Author.where(owned_essay: essays(:david_modest_proposal))
end
def test_find_by_with_custom_primary_key
assert_equal authors(:david), Author.find_by(owned_essay: essays(:david_modest_proposal))
end
def test_where_on_polymorphic_association_with_nil
assert_equal comments(:greetings), Comment.where(author: nil).first
assert_equal comments(:greetings), Comment.where(author: [nil]).first
end
def test_where_on_polymorphic_association_with_empty_array
assert_empty Comment.where(author: [])
end
def test_assigning_belongs_to_on_destroyed_object
client = Client.create!(name: "Client")
client.destroy!
assert_raise(FrozenError) { client.firm = nil }
assert_raise(FrozenError) { client.firm = Firm.new(name: "Firm") }
end
def test_eager_loading_wont_mutate_owner_record
client = Client.eager_load(:firm_with_basic_id).first
assert_not_predicate client, :firm_id_came_from_user?
client = Client.preload(:firm_with_basic_id).first
assert_not_predicate client, :firm_id_came_from_user?
end
def test_missing_attribute_error_is_raised_when_no_foreign_key_attribute
assert_raises(ActiveModel::MissingAttributeError) { Client.select(:id).first.firm }
end
def test_belongs_to_does_not_use_order_by
sql_log = capture_sql { Client.find(3).firm }
assert sql_log.all? { |sql| !/order by/i.match?(sql) }, "ORDER BY was used in the query: #{sql_log}"
end
def test_belongs_to_with_primary_key
client = Client.create(name: "Primary key client", firm_name: companies(:first_firm).name)
assert_equal companies(:first_firm).name, client.firm_with_primary_key.name
end
def test_belongs_to_with_primary_key_joins_on_correct_column
sql = Client.joins(:firm_with_primary_key).to_sql
if current_adapter?(:Mysql2Adapter)
assert_no_match(/`firm_with_primary_keys_companies`\.`id`/, sql)
assert_match(/`firm_with_primary_keys_companies`\.`name`/, sql)
elsif current_adapter?(:OracleAdapter)
# on Oracle aliases are truncated to 30 characters and are quoted in uppercase
assert_no_match(/"firm_with_primary_keys_compani"\."id"/i, sql)
assert_match(/"firm_with_primary_keys_compani"\."name"/i, sql)
else
assert_no_match(/"firm_with_primary_keys_companies"\."id"/, sql)
assert_match(/"firm_with_primary_keys_companies"\."name"/, sql)
end
end
def test_optional_relation_can_be_set_per_model
model1 = Class.new(ActiveRecord::Base) do
self.table_name = "accounts"
self.belongs_to_required_by_default = false
belongs_to :company
def self.name
"FirstModel"
end
end.new
model2 = Class.new(ActiveRecord::Base) do
self.table_name = "accounts"
self.belongs_to_required_by_default = true
belongs_to :company
def self.name
"SecondModel"
end
end.new
assert_predicate model1, :valid?
assert_not_predicate model2, :valid?
end
def test_optional_relation
original_value = ActiveRecord::Base.belongs_to_required_by_default
ActiveRecord::Base.belongs_to_required_by_default = true
model = Class.new(ActiveRecord::Base) do
self.table_name = "accounts"
def self.name; "Temp"; end
belongs_to :company, optional: true
end
account = model.new
assert_predicate account, :valid?
ensure
ActiveRecord::Base.belongs_to_required_by_default = original_value
end
def test_not_optional_relation
original_value = ActiveRecord::Base.belongs_to_required_by_default
ActiveRecord::Base.belongs_to_required_by_default = true
model = Class.new(ActiveRecord::Base) do
self.table_name = "accounts"
def self.name; "Temp"; end
belongs_to :company, optional: false
end
account = model.new
assert_not_predicate account, :valid?
assert_equal [{ error: :blank }], account.errors.details[:company]
ensure
ActiveRecord::Base.belongs_to_required_by_default = original_value
end
def test_required_belongs_to_config
original_value = ActiveRecord::Base.belongs_to_required_by_default
ActiveRecord::Base.belongs_to_required_by_default = true
model = Class.new(ActiveRecord::Base) do
self.table_name = "accounts"
def self.name; "Temp"; end
belongs_to :company
end
account = model.new
assert_not_predicate account, :valid?
assert_equal [{ error: :blank }], account.errors.details[:company]
ensure
ActiveRecord::Base.belongs_to_required_by_default = original_value
end
def test_default
david = developers(:david)
jamis = developers(:jamis)
model = Class.new(ActiveRecord::Base) do
self.table_name = "ships"
def self.name; "Temp"; end
belongs_to :developer, default: -> { david }
end
ship = model.create!
assert_equal david, ship.developer
ship = model.create!(developer: jamis)
assert_equal jamis, ship.developer
ship.update!(developer: nil)
assert_equal david, ship.developer
end
def test_default_with_lambda
model = Class.new(ActiveRecord::Base) do
self.table_name = "ships"
def self.name; "Temp"; end
belongs_to :developer, default: -> { default_developer }
def default_developer
Developer.first
end
end
ship = model.create!
assert_equal developers(:david), ship.developer
ship = model.create!(developer: developers(:jamis))
assert_equal developers(:jamis), ship.developer
end
def test_default_scope_on_relations_is_not_cached
counter = 0
comments = Class.new(ActiveRecord::Base) {
self.table_name = "comments"
self.inheritance_column = "not_there"
posts = Class.new(ActiveRecord::Base) {
self.table_name = "posts"
self.inheritance_column = "not_there"
default_scope -> {
counter += 1
where("id = :inc", inc: counter)
}
has_many :comments, anonymous_class: comments
}
belongs_to :post, anonymous_class: posts, inverse_of: false
}
assert_equal 0, counter
comment = comments.first
assert_equal 0, counter
sql = capture_sql { comment.post }
comment.reload
assert_not_equal sql, capture_sql { comment.post }
end
def test_proxy_assignment
account = Account.find(1)
assert_nothing_raised { account.firm = account.firm }
end
def test_type_mismatch
assert_raise(ActiveRecord::AssociationTypeMismatch) { Account.find(1).firm = 1 }
assert_raise(ActiveRecord::AssociationTypeMismatch) { Account.find(1).firm = Project.find(1) }
end
def test_raises_type_mismatch_with_namespaced_class
assert_nil defined?(Region), "This test requires that there is no top-level Region class"
ActiveRecord::Base.connection.instance_eval do
create_table(:admin_regions, force: true) { |t| t.string :name }
add_column :admin_users, :region_id, :integer
end
Admin.const_set "RegionalUser", Class.new(Admin::User) { belongs_to(:region) }
Admin.const_set "Region", Class.new(ActiveRecord::Base)
e = assert_raise(ActiveRecord::AssociationTypeMismatch) {
Admin::RegionalUser.new(region: "wrong value")
}
assert_match(/^Region\([^)]+\) expected, got "wrong value" which is an instance of String\([^)]+\)$/, e.message)
ensure
Admin.send :remove_const, "Region" if Admin.const_defined?("Region")
Admin.send :remove_const, "RegionalUser" if Admin.const_defined?("RegionalUser")
ActiveRecord::Base.connection.instance_eval do
remove_column :admin_users, :region_id if column_exists?(:admin_users, :region_id)
drop_table :admin_regions, if_exists: true
end
Admin::User.reset_column_information
end
def test_natural_assignment
apple = Firm.create("name" => "Apple")
citibank = Account.create("credit_limit" => 10)
citibank.firm = apple
assert_equal apple.id, citibank.firm_id
end
def test_id_assignment
apple = Firm.create("name" => "Apple")
citibank = Account.create("credit_limit" => 10)
citibank.firm_id = apple
assert_nil citibank.firm_id
end
def test_natural_assignment_with_primary_key
apple = Firm.create("name" => "Apple")
citibank = Client.create("name" => "Primary key client")
citibank.firm_with_primary_key = apple
assert_equal apple.name, citibank.firm_name
end
def test_eager_loading_with_primary_key
Firm.create("name" => "Apple")
Client.create("name" => "Citibank", :firm_name => "Apple")
citibank_result = Client.all.merge!(where: { name: "Citibank" }, includes: :firm_with_primary_key).first
assert_predicate citibank_result.association(:firm_with_primary_key), :loaded?
end
def test_eager_loading_with_primary_key_as_symbol
Firm.create("name" => "Apple")
Client.create("name" => "Citibank", :firm_name => "Apple")
citibank_result = Client.all.merge!(where: { name: "Citibank" }, includes: :firm_with_primary_key_symbols).first
assert_predicate citibank_result.association(:firm_with_primary_key_symbols), :loaded?
end
def test_creating_the_belonging_object
citibank = Account.create("credit_limit" => 10)
apple = citibank.create_firm("name" => "Apple")
assert_equal apple, citibank.firm
citibank.save
citibank.reload
assert_equal apple, citibank.firm
end
def test_creating_the_belonging_object_from_new_record
citibank = Account.new("credit_limit" => 10)
apple = citibank.create_firm("name" => "Apple")
assert_equal apple, citibank.firm
citibank.save
citibank.reload
assert_equal apple, citibank.firm
end
def test_creating_the_belonging_object_with_primary_key
client = Client.create(name: "Primary key client")
apple = client.create_firm_with_primary_key("name" => "Apple")
assert_equal apple, client.firm_with_primary_key
client.save
client.reload
assert_equal apple, client.firm_with_primary_key
end
def test_building_the_belonging_object
citibank = Account.create("credit_limit" => 10)
apple = citibank.build_firm("name" => "Apple")
citibank.save
assert_equal apple.id, citibank.firm_id
end
def test_building_the_belonging_object_with_implicit_sti_base_class
account = Account.new
company = account.build_firm
assert_kind_of Company, company, "Expected #{company.class} to be a Company"
end
def test_building_the_belonging_object_with_explicit_sti_base_class
account = Account.new
company = account.build_firm(type: "Company")
assert_kind_of Company, company, "Expected #{company.class} to be a Company"
end
def test_building_the_belonging_object_with_sti_subclass
account = Account.new
company = account.build_firm(type: "Firm")
assert_kind_of Firm, company, "Expected #{company.class} to be a Firm"
end
def test_building_the_belonging_object_with_an_invalid_type
account = Account.new
assert_raise(ActiveRecord::SubclassNotFound) { account.build_firm(type: "InvalidType") }
end
def test_building_the_belonging_object_with_an_unrelated_type
account = Account.new
assert_raise(ActiveRecord::SubclassNotFound) { account.build_firm(type: "Account") }
end
def test_building_the_belonging_object_with_primary_key
client = Client.create(name: "Primary key client")
apple = client.build_firm_with_primary_key("name" => "Apple")
client.save
assert_equal apple.name, client.firm_name
end
def test_create!
client = Client.create!(name: "Jimmy")
account = client.create_account!(credit_limit: 10)
assert_equal account, client.account
assert_predicate account, :persisted?
client.save
client.reload
assert_equal account, client.account
end
def test_failing_create!
client = Client.create!(name: "Jimmy")
assert_raise(ActiveRecord::RecordInvalid) { client.create_account! }
assert_not_nil client.account
assert_predicate client.account, :new_record?
end
def test_reloading_the_belonging_object
odegy_account = accounts(:odegy_account)
assert_equal "Odegy", odegy_account.firm.name
Company.where(id: odegy_account.firm_id).update_all(name: "ODEGY")
assert_equal "Odegy", odegy_account.firm.name
assert_equal "ODEGY", odegy_account.reload_firm.name
end
def test_reload_the_belonging_object_with_query_cache
odegy_account_id = accounts(:odegy_account).id
connection = ActiveRecord::Base.connection
connection.enable_query_cache!
connection.clear_query_cache
# Populate the cache with a query
odegy_account = Account.find(odegy_account_id)
# Populate the cache with a second query
odegy_account.firm
assert_equal 2, connection.query_cache.size
# Clear the cache and fetch the firm again, populating the cache with a query
assert_queries(1) { odegy_account.reload_firm }
# This query is not cached anymore, so it should make a real SQL query
assert_queries(1) { Account.find(odegy_account_id) }
ensure
ActiveRecord::Base.connection.disable_query_cache!
end
def test_natural_assignment_to_nil
client = Client.find(3)
client.firm = nil
client.save
client.association(:firm).reload
assert_nil client.firm
assert_nil client.client_of
end
def test_natural_assignment_to_nil_with_primary_key
client = Client.create(name: "Primary key client", firm_name: companies(:first_firm).name)
client.firm_with_primary_key = nil
client.save
client.association(:firm_with_primary_key).reload
assert_nil client.firm_with_primary_key
assert_nil client.client_of
end
def test_with_different_class_name
assert_equal Company.find(1).name, Company.find(3).firm_with_other_name.name
assert_not_nil Company.find(3).firm_with_other_name, "Microsoft should have a firm"
end
def test_with_condition
assert_equal Company.find(1).name, Company.find(3).firm_with_condition.name
assert_not_nil Company.find(3).firm_with_condition, "Microsoft should have a firm"
end
def test_polymorphic_association_class
sponsor = Sponsor.new
assert_nil sponsor.association(:sponsorable).klass
sponsor.association(:sponsorable).reload
assert_nil sponsor.sponsorable
sponsor.sponsorable_type = "" # the column doesn't have to be declared NOT NULL
assert_nil sponsor.association(:sponsorable).klass
sponsor.association(:sponsorable).reload
assert_nil sponsor.sponsorable
sponsor.sponsorable = Member.new name: "Bert"
assert_equal Member, sponsor.association(:sponsorable).klass
end
def test_with_polymorphic_and_condition
sponsor = Sponsor.create
member = Member.create name: "Bert"
sponsor.sponsorable = member
sponsor.save!
assert_equal member, sponsor.sponsorable
assert_nil sponsor.sponsorable_with_conditions
sponsor = Sponsor.preload(:sponsorable, :sponsorable_with_conditions).last
assert_equal member, sponsor.sponsorable
assert_nil sponsor.sponsorable_with_conditions
end
def test_with_select
assert_equal 1, Post.find(2).author_with_select.attributes.size
assert_equal 1, Post.includes(:author_with_select).find(2).author_with_select.attributes.size
end
def test_custom_attribute_with_select
assert_equal 2, Company.find(2).firm_with_select.attributes.size
assert_equal 2, Company.includes(:firm_with_select).find(2).firm_with_select.attributes.size
end
def test_belongs_to_without_counter_cache_option
# Ship has a conventionally named `treasures_count` column, but the counter_cache
# option is not given on the association.
ship = Ship.create(name: "Countless")
assert_no_difference lambda { ship.reload.treasures_count }, "treasures_count should not be changed unless counter_cache is given on the relation" do
treasure = Treasure.new(name: "Gold", ship: ship)
treasure.save
end
assert_no_difference lambda { ship.reload.treasures_count }, "treasures_count should not be changed unless counter_cache is given on the relation" do
treasure = ship.treasures.first
treasure.destroy
end
end
def test_belongs_to_counter
debate = Topic.create("title" => "debate")
assert_equal 0, debate.read_attribute("replies_count"), "No replies yet"
trash = debate.replies.create("title" => "blah!", "content" => "world around!")
assert_equal 1, Topic.find(debate.id).read_attribute("replies_count"), "First reply created"
trash.destroy
assert_equal 0, Topic.find(debate.id).read_attribute("replies_count"), "First reply deleted"
end
def test_belongs_to_counter_with_assigning_nil
topic = Topic.create!(title: "debate")
reply = Reply.create!(title: "blah!", content: "world around!", topic: topic)
assert_equal topic.id, reply.parent_id
assert_equal 1, topic.reload.replies.size
reply.topic = nil
reply.reload
assert_equal topic.id, reply.parent_id
assert_equal 1, topic.reload.replies.size
reply.topic = nil
reply.save!
assert_equal 0, topic.reload.replies.size
end
def test_belongs_to_counter_with_assigning_new_object
topic = Topic.create!(title: "debate")
reply = Reply.create!(title: "blah!", content: "world around!", topic: topic)
assert_equal topic.id, reply.parent_id
assert_equal 1, topic.reload.replies_count
topic2 = reply.build_topic(title: "debate2")
reply.save!
assert_not_equal topic.id, reply.parent_id
assert_equal topic2.id, reply.parent_id
assert_equal 0, topic.reload.replies_count
assert_equal 1, topic2.reload.replies_count
end
def test_belongs_to_with_primary_key_counter
debate = Topic.create("title" => "debate")
debate2 = Topic.create("title" => "debate2")
reply = Reply.create("title" => "blah!", "content" => "world around!", "parent_title" => "debate2")
assert_equal 0, debate.reload.replies_count
assert_equal 1, debate2.reload.replies_count
reply.parent_title = "debate"
reply.save!
assert_equal 1, debate.reload.replies_count
assert_equal 0, debate2.reload.replies_count
assert_no_queries do
reply.topic_with_primary_key = debate
end
assert_equal 1, debate.reload.replies_count
assert_equal 0, debate2.reload.replies_count
reply.topic_with_primary_key = debate2
reply.save!
assert_equal 0, debate.reload.replies_count
assert_equal 1, debate2.reload.replies_count
reply.topic_with_primary_key = nil
reply.save!
assert_equal 0, debate.reload.replies_count
assert_equal 0, debate2.reload.replies_count
end
def test_belongs_to_counter_with_reassigning
topic1 = Topic.create("title" => "t1")
topic2 = Topic.create("title" => "t2")
reply1 = Reply.new("title" => "r1", "content" => "r1")
reply1.topic = topic1
assert reply1.save
assert_equal 1, Topic.find(topic1.id).replies.size
assert_equal 0, Topic.find(topic2.id).replies.size
reply1.topic = Topic.find(topic2.id)
assert_no_queries do
reply1.topic = topic2
end
assert reply1.save
assert_equal 0, Topic.find(topic1.id).replies.size
assert_equal 1, Topic.find(topic2.id).replies.size
reply1.topic = nil
reply1.save!
assert_equal 0, Topic.find(topic1.id).replies.size
assert_equal 0, Topic.find(topic2.id).replies.size
reply1.topic = topic1
reply1.save!
assert_equal 1, Topic.find(topic1.id).replies.size
assert_equal 0, Topic.find(topic2.id).replies.size
reply1.destroy
assert_equal 0, Topic.find(topic1.id).replies.size
assert_equal 0, Topic.find(topic2.id).replies.size
end
def test_belongs_to_reassign_with_namespaced_models_and_counters
topic1 = Web::Topic.create("title" => "t1")
topic2 = Web::Topic.create("title" => "t2")
reply1 = Web::Reply.new("title" => "r1", "content" => "r1")
reply1.topic = topic1
assert reply1.save
assert_equal 1, Web::Topic.find(topic1.id).replies.size
assert_equal 0, Web::Topic.find(topic2.id).replies.size
reply1.topic = Web::Topic.find(topic2.id)
assert reply1.save
assert_equal 0, Web::Topic.find(topic1.id).replies.size
assert_equal 1, Web::Topic.find(topic2.id).replies.size
end
def test_belongs_to_counter_after_save
topic = Topic.create!(title: "monday night")
assert_queries(2) do
topic.replies.create!(title: "re: monday night", content: "football")
end
assert_equal 1, Topic.find(topic.id)[:replies_count]
topic.save!
assert_equal 1, Topic.find(topic.id)[:replies_count]
end
def test_belongs_to_counter_after_touch
topic = Topic.create!(title: "topic")
assert_equal 0, topic.replies_count
assert_equal 0, topic.after_touch_called
reply = Reply.create!(title: "blah!", content: "world around!", topic_with_primary_key: topic)
assert_equal 1, topic.replies_count
assert_equal 1, topic.after_touch_called
reply.destroy!
assert_equal 0, topic.replies_count
assert_equal 2, topic.after_touch_called
end
def test_belongs_to_touch_with_reassigning
debate = Topic.create!(title: "debate")
debate2 = Topic.create!(title: "debate2")
reply = Reply.create!(title: "blah!", content: "world around!", parent_title: "debate2")
time = 1.day.ago
debate.touch(time: time)
debate2.touch(time: time)
assert_queries(3) do
reply.parent_title = "debate"
reply.save!
end
assert_operator debate.reload.updated_at, :>, time
assert_operator debate2.reload.updated_at, :>, time
debate.touch(time: time)
debate2.touch(time: time)
assert_queries(3) do
reply.topic_with_primary_key = debate2
reply.save!
end
assert_operator debate.reload.updated_at, :>, time
assert_operator debate2.reload.updated_at, :>, time
end
def test_belongs_to_with_touch_option_on_touch
line_item = LineItem.create!
Invoice.create!(line_items: [line_item])
assert_queries(1) { line_item.touch }
end
def test_belongs_to_with_touch_on_multiple_records
line_item = LineItem.create!(amount: 1)
line_item2 = LineItem.create!(amount: 2)
Invoice.create!(line_items: [line_item, line_item2])
assert_queries(1) do
LineItem.transaction do
line_item.touch
line_item2.touch
end
end
assert_queries(2) do
line_item.touch
line_item2.touch
end
end
def test_belongs_to_with_touch_option_on_touch_without_updated_at_attributes
assert_not LineItem.column_names.include?("updated_at")
line_item = LineItem.create!
invoice = Invoice.create!(line_items: [line_item])
initial = invoice.updated_at
travel(1.second) do
line_item.touch
end
assert_not_equal initial, invoice.reload.updated_at
end
def test_belongs_to_with_touch_option_on_touch_and_removed_parent
line_item = LineItem.create!
Invoice.create!(line_items: [line_item])
line_item.invoice = nil
assert_queries(2) { line_item.touch }
end
def test_belongs_to_with_touch_option_on_update
line_item = LineItem.create!
Invoice.create!(line_items: [line_item])
assert_queries(2) { line_item.update amount: 10 }
end
def test_belongs_to_with_touch_option_on_empty_update
line_item = LineItem.create!
Invoice.create!(line_items: [line_item])
assert_no_queries { line_item.save }
end
def test_belongs_to_with_touch_option_on_destroy
line_item = LineItem.create!
Invoice.create!(line_items: [line_item])
assert_queries(2) { line_item.destroy }
end
def test_belongs_to_with_touch_option_on_destroy_with_destroyed_parent
line_item = LineItem.create!
invoice = Invoice.create!(line_items: [line_item])
invoice.destroy
assert_queries(1) { line_item.destroy }
end
def test_belongs_to_with_touch_option_on_touch_and_reassigned_parent
line_item = LineItem.create!
Invoice.create!(line_items: [line_item])
line_item.invoice = Invoice.create!
assert_queries(3) { line_item.touch }
end
def test_belongs_to_counter_after_update
topic = Topic.create!(title: "37s")
topic.replies.create!(title: "re: 37s", content: "rails")
assert_equal 1, Topic.find(topic.id)[:replies_count]
topic.update(title: "37signals")
assert_equal 1, Topic.find(topic.id)[:replies_count]
end
def test_belongs_to_counter_when_update_columns
topic = Topic.create!(title: "37s")
topic.replies.create!(title: "re: 37s", content: "rails")
assert_equal 1, Topic.find(topic.id)[:replies_count]
topic.update_columns(content: "rails is wonderful")
assert_equal 1, Topic.find(topic.id)[:replies_count]
end
def test_assignment_before_child_saved
final_cut = Client.new("name" => "Final Cut")
firm = Firm.find(1)
final_cut.firm = firm
assert_not_predicate final_cut, :persisted?
assert final_cut.save
assert_predicate final_cut, :persisted?
assert_predicate firm, :persisted?
assert_equal firm, final_cut.firm
final_cut.association(:firm).reload
assert_equal firm, final_cut.firm
end
def test_assignment_before_child_saved_with_primary_key
final_cut = Client.new("name" => "Final Cut")
firm = Firm.find(1)
final_cut.firm_with_primary_key = firm
assert_not_predicate final_cut, :persisted?
assert final_cut.save
assert_predicate final_cut, :persisted?
assert_predicate firm, :persisted?
assert_equal firm, final_cut.firm_with_primary_key
final_cut.association(:firm_with_primary_key).reload
assert_equal firm, final_cut.firm_with_primary_key
end
def test_new_record_with_foreign_key_but_no_object
client = Client.new("firm_id" => 1)
assert_equal Firm.first, client.firm_with_basic_id
end
def test_setting_foreign_key_after_nil_target_loaded
client = Client.new
client.firm_with_basic_id
client.firm_id = 1
assert_equal companies(:first_firm), client.firm_with_basic_id
end
def test_polymorphic_setting_foreign_key_after_nil_target_loaded
sponsor = Sponsor.new
sponsor.sponsorable
sponsor.sponsorable_id = 1
sponsor.sponsorable_type = "Member"
assert_equal members(:groucho), sponsor.sponsorable
end
def test_dont_find_target_when_foreign_key_is_null
tagging = taggings(:thinking_general)
assert_no_queries { tagging.super_tag }
end
def test_dont_find_target_when_saving_foreign_key_after_stale_association_loaded
client = Client.create!(name: "Test client", firm_with_basic_id: Firm.find(1))
client.firm_id = Firm.create!(name: "Test firm").id
assert_queries(1) { client.save! }
end
def test_field_name_same_as_foreign_key
computer = Computer.find(1)
assert_not_nil computer.developer, ":foreign key == attribute didn't lock up" # '
end
def test_counter_cache
topic = Topic.create title: "Zoom-zoom-zoom"
assert_equal 0, topic[:replies_count]
reply = Reply.create(title: "re: zoom", content: "speedy quick!")
reply.topic = topic
reply.save!
assert_equal 1, topic.reload[:replies_count]
assert_equal 1, topic.replies.size
topic[:replies_count] = 15
assert_equal 15, topic.replies.size
end
def test_counter_cache_double_destroy
topic = Topic.create title: "Zoom-zoom-zoom"
5.times do
topic.replies.create(title: "re: zoom", content: "speedy quick!")
end
assert_equal 5, topic.reload[:replies_count]
assert_equal 5, topic.replies.size
reply = topic.replies.first
reply.destroy
assert_equal 4, topic.reload[:replies_count]
reply.destroy
assert_equal 4, topic.reload[:replies_count]
assert_equal 4, topic.replies.size
end
def test_concurrent_counter_cache_double_destroy
topic = Topic.create title: "Zoom-zoom-zoom"
5.times do
topic.replies.create(title: "re: zoom", content: "speedy quick!")
end
assert_equal 5, topic.reload[:replies_count]
assert_equal 5, topic.replies.size
reply = topic.replies.first
reply_clone = Reply.find(reply.id)
reply.destroy
assert_equal 4, topic.reload[:replies_count]
reply_clone.destroy
assert_equal 4, topic.reload[:replies_count]
assert_equal 4, topic.replies.size
end
def test_custom_counter_cache
reply = Reply.create(title: "re: zoom", content: "speedy quick!")
assert_equal 0, reply[:replies_count]
silly = SillyReply.create(title: "gaga", content: "boo-boo")
silly.reply = reply
silly.save!
assert_equal 1, reply.reload[:replies_count]
assert_equal 1, reply.replies.size
reply[:replies_count] = 17
assert_equal 17, reply.replies.size
end
def test_replace_counter_cache
topic = Topic.create(title: "Zoom-zoom-zoom")
reply = Reply.create(title: "re: zoom", content: "speedy quick!")
reply.topic = topic
reply.save
topic.reload
assert_equal 1, topic.replies_count
end
def test_association_assignment_sticks
post = Post.first
author1, author2 = Author.all.merge!(limit: 2).to_a
assert_not_nil author1
assert_not_nil author2
# make sure the association is loaded
post.author
# set the association by id, directly
post.author_id = author2.id
# save and reload
post.save!
post.reload
# the author id of the post should be the id we set
assert_equal post.author_id, author2.id
end
def test_cant_save_readonly_association
assert_raise(ActiveRecord::ReadOnlyRecord) { companies(:first_client).readonly_firm.save! }
assert_predicate companies(:first_client).readonly_firm, :readonly?
end
def test_polymorphic_assignment_foreign_key_type_string
comment = Comment.first
comment.author = authors(:david)
comment.resource = members(:groucho)
comment.save
assert_equal 1, authors(:david).id
assert_equal 1, comment.author_id
assert_equal authors(:david), Comment.includes(:author).first.author
assert_equal 1, members(:groucho).id
assert_equal "1", comment.resource_id
assert_equal members(:groucho), Comment.includes(:resource).first.resource
end
def test_polymorphic_assignment_foreign_type_field_updating
# should update when assigning a saved record
sponsor = Sponsor.new
member = Member.create
sponsor.sponsorable = member
assert_equal "Member", sponsor.sponsorable_type
# should update when assigning a new record
sponsor = Sponsor.new
member = Member.new
sponsor.sponsorable = member
assert_equal "Member", sponsor.sponsorable_type
end
def test_polymorphic_assignment_with_primary_key_foreign_type_field_updating
# should update when assigning a saved record
essay = Essay.new
writer = Author.create(name: "David")
essay.writer = writer
assert_equal "Author", essay.writer_type
# should update when assigning a new record
essay = Essay.new
writer = Author.new
essay.writer = writer
assert_equal "Author", essay.writer_type
end
def test_polymorphic_assignment_updates_foreign_id_field_for_new_and_saved_records
sponsor = Sponsor.new
saved_member = Member.create
new_member = Member.new
sponsor.sponsorable = saved_member
assert_equal saved_member.id, sponsor.sponsorable_id
sponsor.sponsorable = new_member
assert_nil sponsor.sponsorable_id
end
def test_assignment_updates_foreign_id_field_for_new_and_saved_records
client = Client.new
saved_firm = Firm.create name: "Saved"
new_firm = Firm.new
client.firm = saved_firm
assert_equal saved_firm.id, client.client_of
client.firm = new_firm
assert_nil client.client_of
end
def test_polymorphic_assignment_with_primary_key_updates_foreign_id_field_for_new_and_saved_records
essay = Essay.new
saved_writer = Author.create(name: "David")
new_writer = Author.new
essay.writer = saved_writer
assert_equal saved_writer.name, essay.writer_id
essay.writer = new_writer
assert_nil essay.writer_id
end
def test_polymorphic_assignment_with_nil
essay = Essay.new
assert_nil essay.writer_id
assert_nil essay.writer_type
essay.writer_id = 1
essay.writer_type = "Author"
essay.writer = nil
assert_nil essay.writer_id
assert_nil essay.writer_type
end
def test_belongs_to_proxy_should_not_respond_to_private_methods
assert_raise(NoMethodError) { companies(:first_firm).private_method }
assert_raise(NoMethodError) { companies(:second_client).firm.private_method }
end
def test_belongs_to_proxy_should_respond_to_private_methods_via_send
companies(:first_firm).send(:private_method)
companies(:second_client).firm.send(:private_method)
end
def test_save_of_record_with_loaded_belongs_to
@account = companies(:first_firm).account
assert_nothing_raised do
Account.find(@account.id).save!
Account.all.merge!(includes: :firm).find(@account.id).save!
end
@account.firm.delete
assert_nothing_raised do
Account.find(@account.id).save!
Account.all.merge!(includes: :firm).find(@account.id).save!
end
end
def test_dependent_delete_and_destroy_with_belongs_to
AuthorAddress.destroyed_author_address_ids.clear
author_address = author_addresses(:david_address)
author_address_extra = author_addresses(:david_address_extra)
assert_equal [], AuthorAddress.destroyed_author_address_ids
assert_difference "AuthorAddress.count", -2 do
authors(:david).destroy
end
assert_equal [], AuthorAddress.where(id: [author_address.id, author_address_extra.id])
assert_equal [author_address.id], AuthorAddress.destroyed_author_address_ids
end
def test_belongs_to_invalid_dependent_option_raises_exception
error = assert_raise ArgumentError do
Class.new(Author).belongs_to :special_author_address, dependent: :nullify
end
assert_equal error.message, "The :dependent option must be one of [:destroy, :delete, :destroy_async], but is :nullify"
end
class EssayDestroy < ActiveRecord::Base
self.table_name = "essays"
belongs_to :book, dependent: :destroy, class_name: "DestroyableBook"
end
class DestroyableBook < ActiveRecord::Base
self.table_name = "books"
belongs_to :author, class_name: "UndestroyableAuthor", dependent: :destroy
end
class UndestroyableAuthor < ActiveRecord::Base
self.table_name = "authors"
has_one :book, class_name: "DestroyableBook", foreign_key: "author_id"
before_destroy :dont
def dont
throw(:abort)
end
end
def test_dependency_should_halt_parent_destruction
author = UndestroyableAuthor.create!(name: "Test")
book = DestroyableBook.create!(author: author)
assert_no_difference ["UndestroyableAuthor.count", "DestroyableBook.count"] do
assert_not book.destroy
end
end
def test_dependency_should_halt_parent_destruction_with_cascaded_three_levels
author = UndestroyableAuthor.create!(name: "Test")
book = DestroyableBook.create!(author: author)
essay = EssayDestroy.create!(book: book)
assert_no_difference ["UndestroyableAuthor.count", "DestroyableBook.count", "EssayDestroy.count"] do
assert_not essay.destroy
assert_not essay.destroyed?
end
end
def test_attributes_are_being_set_when_initialized_from_belongs_to_association_with_where_clause
new_firm = accounts(:signals37).build_firm(name: "Apple")
assert_equal new_firm.name, "Apple"
end
def test_attributes_are_set_without_error_when_initialized_from_belongs_to_association_with_array_in_where_clause
new_account = Account.where(credit_limit: [ 50, 60 ]).new
assert_nil new_account.credit_limit
end
def test_reassigning_the_parent_id_updates_the_object
client = companies(:second_client)
client.firm
client.firm_with_condition
firm_proxy = client.send(:association_instance_get, :firm)
firm_with_condition_proxy = client.send(:association_instance_get, :firm_with_condition)
assert_not_predicate firm_proxy, :stale_target?
assert_not_predicate firm_with_condition_proxy, :stale_target?
assert_equal companies(:first_firm), client.firm
assert_equal companies(:first_firm), client.firm_with_condition
client.client_of = companies(:another_firm).id
assert_predicate firm_proxy, :stale_target?
assert_predicate firm_with_condition_proxy, :stale_target?
assert_equal companies(:another_firm), client.firm
assert_equal companies(:another_firm), client.firm_with_condition
end
def test_assigning_nil_on_an_association_clears_the_associations_inverse
with_has_many_inversing do
book = Book.create!
citation = book.citations.create!
assert_same book, citation.book
assert_nothing_raised do
citation.book = nil
citation.save!
end
end
end
def test_clearing_an_association_clears_the_associations_inverse
author = Author.create(name: "Jimmy Tolkien")
post = author.create_post(title: "The silly medallion", body: "")
assert_equal post, author.post
assert_equal author, post.author
author.update!(post: nil)
assert_nil author.post
post.update!(title: "The Silmarillion")
assert_nil author.post
end
def test_destroying_child_with_unloaded_parent_and_foreign_key_and_touch_is_possible_with_has_many_inversing
with_has_many_inversing do
book = Book.create!
citation = book.citations.create!
assert_difference "Citation.count", -1 do
Citation.find(citation.id).destroy
end
end
end
def test_polymorphic_reassignment_of_associated_id_updates_the_object
sponsor = sponsors(:moustache_club_sponsor_for_groucho)
sponsor.sponsorable
proxy = sponsor.send(:association_instance_get, :sponsorable)
assert_not_predicate proxy, :stale_target?
assert_equal members(:groucho), sponsor.sponsorable
sponsor.sponsorable_id = members(:some_other_guy).id
assert_predicate proxy, :stale_target?
assert_equal members(:some_other_guy), sponsor.sponsorable
end
def test_polymorphic_reassignment_of_associated_type_updates_the_object
sponsor = sponsors(:moustache_club_sponsor_for_groucho)
sponsor.sponsorable
proxy = sponsor.send(:association_instance_get, :sponsorable)
assert_not_predicate proxy, :stale_target?
assert_equal members(:groucho), sponsor.sponsorable
sponsor.sponsorable_type = "Firm"
assert_predicate proxy, :stale_target?
assert_equal companies(:first_firm), sponsor.sponsorable
end
def test_reloading_association_with_key_change
client = companies(:second_client)
firm = client.association(:firm)
client.firm = companies(:another_firm)
firm.reload
assert_equal companies(:another_firm), firm.target
client.client_of = companies(:first_firm).id
firm.reload
assert_equal companies(:first_firm), firm.target
end
def test_polymorphic_counter_cache
tagging = taggings(:welcome_general)
post = posts(:welcome)
comment = comments(:greetings)
assert_equal post.id, comment.id
assert_difference "post.reload.tags_count", -1 do
assert_difference "comment.reload.tags_count", +1 do
tagging.taggable = comment
tagging.save!
end
end
assert_difference "comment.reload.tags_count", -1 do
assert_difference "post.reload.tags_count", +1 do
tagging.taggable_type = post.class.polymorphic_name
tagging.taggable_id = post.id
tagging.save!
end
end
end
def test_polymorphic_with_custom_foreign_type
sponsor = sponsors(:moustache_club_sponsor_for_groucho)
groucho = members(:groucho)
other = members(:some_other_guy)
assert_equal groucho, sponsor.sponsorable
assert_equal groucho, sponsor.thing
sponsor.thing = other
assert_equal other, sponsor.sponsorable
assert_equal other, sponsor.thing
sponsor.sponsorable = groucho
assert_equal groucho, sponsor.sponsorable
assert_equal groucho, sponsor.thing
end
class WheelPolymorphicName < ActiveRecord::Base
self.table_name = "wheels"
belongs_to :wheelable, polymorphic: true, counter_cache: :wheels_count, touch: :wheels_owned_at
def self.polymorphic_class_for(name)
raise "Unexpected name: #{name}" unless name == "polymorphic_car"
CarPolymorphicName
end
end
class CarPolymorphicName < ActiveRecord::Base
self.table_name = "cars"
has_many :wheels, as: :wheelable
def self.polymorphic_name
"polymorphic_car"
end
end
def test_polymorphic_with_custom_name_counter_cache
car = CarPolymorphicName.create!
wheel = WheelPolymorphicName.create!(wheelable_type: "polymorphic_car", wheelable_id: car.id)
assert_equal 1, car.reload.wheels_count
wheel.update! wheelable: nil
assert_equal 0, car.reload.wheels_count
end
def test_polymorphic_with_custom_name_touch_old_belongs_to_model
car = CarPolymorphicName.create!
wheel = WheelPolymorphicName.create!(wheelable: car)
touch_time = 1.day.ago.round
travel_to(touch_time) do
wheel.update!(wheelable: nil)
end
assert_equal touch_time, car.reload.wheels_owned_at
end
def test_build_with_conditions
client = companies(:second_client)
firm = client.build_bob_firm
assert_equal "Bob", firm.name
end
def test_create_with_conditions
client = companies(:second_client)
firm = client.create_bob_firm
assert_equal "Bob", firm.name
end
def test_create_bang_with_conditions
client = companies(:second_client)
firm = client.create_bob_firm!
assert_equal "Bob", firm.name
end
def test_build_with_block
client = Client.create(name: "Client Company")
firm = client.build_firm { |f| f.name = "Agency Company" }
assert_equal "Agency Company", firm.name
end
def test_create_with_block
client = Client.create(name: "Client Company")
firm = client.create_firm { |f| f.name = "Agency Company" }
assert_equal "Agency Company", firm.name
end
def test_create_bang_with_block
client = Client.create(name: "Client Company")
firm = client.create_firm! { |f| f.name = "Agency Company" }
assert_equal "Agency Company", firm.name
end
def test_should_set_foreign_key_on_create_association
client = Client.create! name: "fuu"
firm = client.create_firm name: "baa"
assert_equal firm.id, client.client_of
end
def test_should_set_foreign_key_on_create_association!
client = Client.create! name: "fuu"
firm = client.create_firm! name: "baa"
assert_equal firm.id, client.client_of
end
def test_self_referential_belongs_to_with_counter_cache_assigning_nil
comment = Comment.create! post: posts(:thinking), body: "fuu"
comment.parent = nil
comment.save!
assert_nil comment.reload.parent
assert_equal 0, comments(:greetings).reload.children_count
end
def test_belongs_to_with_id_assigning
post = posts(:welcome)
comment = Comment.create! body: "foo", post: post
parent = comments(:greetings)
assert_equal 0, parent.reload.children_count
comment.parent_id = parent.id
comment.save!
assert_equal 1, parent.reload.children_count
end
def test_belongs_to_with_out_of_range_value_assigning
model = Class.new(Author) do
def self.name; "Temp"; end
validates :author_address, presence: true
end
author = model.new
author.author_address_id = 9223372036854775808 # out of range in the bigint
assert_nil author.author_address
assert_not_predicate author, :valid?
assert_equal [{ error: :blank }], author.errors.details[:author_address]
end
def test_polymorphic_with_custom_primary_key
toy = Toy.create!
sponsor = Sponsor.create!(sponsorable: toy)
assert_equal toy, sponsor.reload.sponsorable
end
class SponsorWithTouchInverse < Sponsor
belongs_to :sponsorable, polymorphic: true, inverse_of: :sponsors, touch: true
end
def test_destroying_polymorphic_child_with_unloaded_parent_and_touch_is_possible_with_has_many_inversing
with_has_many_inversing do
toy = Toy.create!
sponsor = toy.sponsors.create!
assert_difference "Sponsor.count", -1 do
SponsorWithTouchInverse.find(sponsor.id).destroy
end
end
end
def test_polymorphic_with_false
assert_nothing_raised do
Class.new(ActiveRecord::Base) do
def self.name; "Post"; end
belongs_to :category, polymorphic: false
end
end
end
test "stale tracking doesn't care about the type" do
apple = Firm.create("name" => "Apple")
citibank = Account.create("credit_limit" => 10)
citibank.firm_id = apple.id
citibank.firm # load it
citibank.firm_id = apple.id.to_s
assert_not_predicate citibank.association(:firm), :stale_target?
end
def test_reflect_the_most_recent_change
author1, author2 = Author.limit(2)
post = Post.new(title: "foo", body: "bar")
post.author = author1
post.author_id = author2.id
assert post.save
assert_equal post.author_id, author2.id
end
test "dangerous association name raises ArgumentError" do
[:errors, "errors", :save, "save"].each do |name|
assert_raises(ArgumentError, "Association #{name} should not be allowed") do
Class.new(ActiveRecord::Base) do
belongs_to name
end
end
end
end
test "belongs_to works with model called Record" do
record = Record.create!
Column.create! record: record
assert_equal 1, Column.count
end
def test_multiple_counter_cache_with_after_create_update
post = posts(:welcome)
parent = comments(:greetings)
assert_difference "parent.reload.children_count", +1 do
assert_difference "post.reload.comments_count", +1 do
CommentWithAfterCreateUpdate.create(body: "foo", post: post, parent: parent)
end
end
end
test "assigning an association doesn't result in duplicate objects" do
post = Post.create!(title: "title", body: "body")
post.comments = [post.comments.build(body: "body")]
post.save!
assert_equal 1, post.comments.size
assert_equal 1, Comment.where(post_id: post.id).count
assert_equal post.id, Comment.last.post.id
end
test "tracking change from one persisted record to another" do
node = nodes(:child_one_of_a)
assert_not_nil node.parent
assert_not node.parent_changed?
assert_not node.parent_previously_changed?
node.parent = nodes(:grandparent)
assert node.parent_changed?
assert_not node.parent_previously_changed?
node.save!
assert_not node.parent_changed?
assert node.parent_previously_changed?
end
test "tracking change from persisted record to new record" do
node = nodes(:child_one_of_a)
assert_not_nil node.parent
assert_not node.parent_changed?
assert_not node.parent_previously_changed?
node.parent = Node.new(tree: node.tree, parent: nodes(:parent_a), name: "Child three")
assert node.parent_changed?
assert_not node.parent_previously_changed?
node.save!
assert_not node.parent_changed?
assert node.parent_previously_changed?
end
test "tracking change from persisted record to nil" do
node = nodes(:child_one_of_a)
assert_not_nil node.parent
assert_not node.parent_changed?
assert_not node.parent_previously_changed?
node.parent = nil
assert node.parent_changed?
assert_not node.parent_previously_changed?
node.save!
assert_not node.parent_changed?
assert node.parent_previously_changed?
end
test "tracking change from nil to persisted record" do
node = nodes(:grandparent)
assert_nil node.parent
assert_not node.parent_changed?
assert_not node.parent_previously_changed?
node.parent = Node.create!(tree: node.tree, name: "Great-grandparent")
assert node.parent_changed?
assert_not node.parent_previously_changed?
node.save!
assert_not node.parent_changed?
assert node.parent_previously_changed?
end
test "tracking change from nil to new record" do
node = nodes(:grandparent)
assert_nil node.parent
assert_not node.parent_changed?
assert_not node.parent_previously_changed?
node.parent = Node.new(tree: node.tree, name: "Great-grandparent")
assert node.parent_changed?
assert_not node.parent_previously_changed?
node.save!
assert_not node.parent_changed?
assert node.parent_previously_changed?
end
test "tracking polymorphic changes" do
comment = comments(:greetings)
assert_nil comment.author
assert_not comment.author_changed?
assert_not comment.author_previously_changed?
comment.author = authors(:david)
assert comment.author_changed?
comment.save!
assert_not comment.author_changed?
assert comment.author_previously_changed?
assert_equal authors(:david).id, companies(:first_firm).id
comment.author = companies(:first_firm)
assert comment.author_changed?
comment.save!
assert_not comment.author_changed?
assert comment.author_previously_changed?
end
end
class BelongsToWithForeignKeyTest < ActiveRecord::TestCase
fixtures :authors, :author_addresses
def test_destroy_linked_models
address = AuthorAddress.create!
author = Author.create! name: "Author", author_address_id: address.id
author.destroy!
end
end
# frozen_string_literal: true
module ActiveRecord
module Associations
# = Active Record Belongs To Polymorphic Association
class BelongsToPolymorphicAssociation < BelongsToAssociation # :nodoc:
def klass
type = owner[reflection.foreign_type]
type.presence && owner.class.polymorphic_class_for(type)
end
def target_changed?
super || owner.attribute_changed?(reflection.foreign_type)
end
def target_previously_changed?
super || owner.attribute_previously_changed?(reflection.foreign_type)
end
def saved_change_to_target?
super || owner.saved_change_to_attribute?(reflection.foreign_type)
end
private
def replace_keys(record, force: false)
super
target_type = record ? record.class.polymorphic_name : nil
if force || owner._read_attribute(reflection.foreign_type) != target_type
owner[reflection.foreign_type] = target_type
end
end
def inverse_reflection_for(record)
reflection.polymorphic_inverse_of(record.class)
end
def raise_on_type_mismatch!(record)
# A polymorphic association cannot have a type mismatch, by definition
end
def stale_state
foreign_key = super
foreign_key && [foreign_key.to_s, owner[reflection.foreign_type].to_s]
end
end
end
end
# frozen_string_literal: true
require "cases/helper"
require "models/content"
class BidirectionalDestroyDependenciesTest < ActiveRecord::TestCase
fixtures :content, :content_positions
def setup
Content.destroyed_ids.clear
ContentPosition.destroyed_ids.clear
end
def test_bidirectional_dependence_when_destroying_item_with_belongs_to_association
content_position = ContentPosition.find(1)
content = content_position.content
assert_not_nil content
content_position.destroy
assert_equal [content_position.id], ContentPosition.destroyed_ids
assert_equal [content.id], Content.destroyed_ids
end
def test_bidirectional_dependence_when_destroying_item_with_has_one_association
content = Content.find(1)
content_position = content.content_position
assert_not_nil content_position
content.destroy
assert_equal [content.id], Content.destroyed_ids
assert_equal [content_position.id], ContentPosition.destroyed_ids
end
def test_bidirectional_dependence_when_destroying_item_with_has_one_association_fails_first_time
content = ContentWhichRequiresTwoDestroyCalls.find(1)
2.times { content.destroy }
assert_equal content.destroyed?, true
end
end
# frozen_string_literal: true
require_relative "../helper"
module Arel
module Nodes
class TestBin < Arel::Test
def test_new
assert Arel::Nodes::Bin.new("zomg")
end
def test_default_to_sql
viz = Arel::Visitors::ToSql.new Table.engine.connection_pool
node = Arel::Nodes::Bin.new(Arel.sql("zomg"))
assert_equal "zomg", viz.accept(node, Collectors::SQLString.new).value
end
def test_mysql_to_sql
viz = Arel::Visitors::MySQL.new Table.engine.connection_pool
node = Arel::Nodes::Bin.new(Arel.sql("zomg"))
assert_equal "BINARY zomg", viz.accept(node, Collectors::SQLString.new).value
end
def test_equality_with_same_ivars
array = [Bin.new("zomg"), Bin.new("zomg")]
assert_equal 1, array.uniq.size
end
def test_inequality_with_different_ivars
array = [Bin.new("zomg"), Bin.new("zomg!")]
assert_equal 2, array.uniq.size
end
end
end
end
# frozen_string_literal: true
class Binary < ActiveRecord::Base
end
# frozen_string_literal: true
class BinaryField < ActiveRecord::Base
serialize :normal_blob
serialize :normal_text
end
# frozen_string_literal: true
require "cases/helper"
require "models/binary"
class BinaryTest < ActiveRecord::TestCase
FIXTURES = %w(flowers.jpg example.log test.txt)
def test_mixed_encoding
str = +"\x80"
str.force_encoding("ASCII-8BIT")
binary = Binary.new name: "いただきます!", data: str
binary.save!
binary.reload
assert_equal str, binary.data
name = binary.name
assert_equal "いただきます!", name
end
def test_load_save
Binary.delete_all
FIXTURES.each do |filename|
data = File.read(ASSETS_ROOT + "/#{filename}")
data.force_encoding("ASCII-8BIT")
data.freeze
bin = Binary.new(data: data)
assert_equal data, bin.data, "Newly assigned data differs from original"
bin.save!
assert_equal data, bin.data, "Data differs from original after save"
assert_equal data, bin.reload.data, "Reloaded data differs from original"
end
end
end
# frozen_string_literal: true
module Arel # :nodoc: all
module Collectors
class Bind
def initialize
@binds = []
end
def <<(str)
self
end
def add_bind(bind)
@binds << bind
self
end
def add_binds(binds, proc_for_binds = nil)
@binds.concat proc_for_binds ? binds.map(&proc_for_binds) : binds
self
end
def value
@binds
end
end
end
end
# frozen_string_literal: true
module Arel # :nodoc: all
module Nodes
class BindParam < Node
attr_reader :value
def initialize(value)
@value = value
super()
end
def hash
[self.class, self.value].hash
end
def eql?(other)
other.is_a?(BindParam) &&
value == other.value
end
alias :== :eql?
def nil?
value.nil?
end
def value_before_type_cast
if value.respond_to?(:value_before_type_cast)
value.value_before_type_cast
else
value
end
end
def infinite?
value.respond_to?(:infinite?) && value.infinite?
end
def unboundable?
value.respond_to?(:unboundable?) && value.unboundable?
end
end
end
end
# frozen_string_literal: true
require_relative "../helper"
module Arel
module Nodes
describe "BindParam" do
it "is equal to other bind params with the same value" do
_(BindParam.new(1)).must_equal(BindParam.new(1))
_(BindParam.new("foo")).must_equal(BindParam.new("foo"))
end
it "is not equal to other nodes" do
_(BindParam.new(nil)).wont_equal(Node.new)
end
it "is not equal to bind params with different values" do
_(BindParam.new(1)).wont_equal(BindParam.new(2))
end
end
end
end
# frozen_string_literal: true
require "cases/helper"
require "models/topic"
require "models/reply"
require "models/author"
require "models/post"
if ActiveRecord::Base.connection.prepared_statements
module ActiveRecord
class BindParameterTest < ActiveRecord::TestCase
fixtures :topics, :authors, :author_addresses, :posts
class LogListener
attr_accessor :calls
def initialize
@calls = []
end
def call(*args)
calls << args
end
end
def setup
super
@connection = ActiveRecord::Base.connection
@subscriber = LogListener.new
@subscription = ActiveSupport::Notifications.subscribe("sql.active_record", @subscriber)
end
def teardown
ActiveSupport::Notifications.unsubscribe(@subscription)
end
def test_statement_cache
@connection.clear_cache!
topics = Topic.where(id: 1)
assert_equal [1], topics.map(&:id)
assert_includes statement_cache, to_sql_key(topics.arel)
@connection.clear_cache!
assert_not_includes statement_cache, to_sql_key(topics.arel)
end
def test_statement_cache_with_query_cache
@connection.enable_query_cache!
@connection.clear_cache!
topics = Topic.where(id: 1)
assert_equal [1], topics.map(&:id)
assert_includes statement_cache, to_sql_key(topics.arel)
ensure
@connection.disable_query_cache!
end
def test_statement_cache_with_find
@connection.clear_cache!
assert_equal 1, Topic.find(1).id
assert_raises(RecordNotFound) { SillyReply.find(2) }
topic_sql = cached_statement(Topic, [Topic.primary_key])
assert_includes statement_cache, to_sql_key(topic_sql)
reply_sql = cached_statement(SillyReply, [SillyReply.primary_key])
assert_includes statement_cache, to_sql_key(reply_sql)
replies = SillyReply.where(id: 2).limit(1)
assert_includes statement_cache, to_sql_key(replies.arel)
end
def test_statement_cache_with_find_by
@connection.clear_cache!
assert_equal 1, Topic.find_by!(id: 1).id
assert_raises(RecordNotFound) { SillyReply.find_by!(id: 2) }
topic_sql = cached_statement(Topic, ["id"])
assert_includes statement_cache, to_sql_key(topic_sql)
reply_sql = cached_statement(SillyReply, ["id"])
assert_includes statement_cache, to_sql_key(reply_sql)
replies = SillyReply.where(id: 2).limit(1)
assert_includes statement_cache, to_sql_key(replies.arel)
end
def test_statement_cache_with_in_clause
@connection.clear_cache!
topics = Topic.where(id: [1, 3]).order(:id)
assert_equal [1, 3], topics.map(&:id)
assert_not_includes statement_cache, to_sql_key(topics.arel)
end
def test_statement_cache_with_sql_string_literal
@connection.clear_cache!
topics = Topic.where("topics.id = ?", 1)
assert_equal [1], topics.map(&:id)
assert_not_includes statement_cache, to_sql_key(topics.arel)
end
def test_too_many_binds
bind_params_length = @connection.send(:bind_params_length)
topics = Topic.where(id: (1 .. bind_params_length).to_a << 2**63)
assert_equal Topic.count, topics.count
topics = Topic.where.not(id: (1 .. bind_params_length).to_a << 2**63)
assert_equal 0, topics.count
end
def test_too_many_binds_with_query_cache
@connection.enable_query_cache!
bind_params_length = @connection.send(:bind_params_length)
topics = Topic.where(id: (1 .. bind_params_length + 1).to_a)
assert_equal Topic.count, topics.count
topics = Topic.where.not(id: (1 .. bind_params_length + 1).to_a)
assert_equal 0, topics.count
ensure
@connection.disable_query_cache!
end
def test_bind_from_join_in_subquery
subquery = Author.joins(:thinking_posts).where(name: "David")
scope = Author.from(subquery, "authors").where(id: 1)
assert_equal 1, scope.count
end
def test_binds_are_logged
sub = Arel::Nodes::BindParam.new(1)
binds = [Relation::QueryAttribute.new("id", 1, Type::Value.new)]
sql = "select * from topics where id = #{sub.to_sql}"
@connection.exec_query(sql, "SQL", binds)
message = @subscriber.calls.find { |args| args[4][:sql] == sql }
assert_equal binds, message[4][:binds]
end
def test_find_one_uses_binds
Topic.find(1)
message = @subscriber.calls.find { |args| args[4][:binds].any? { |attr| attr.value == 1 } }
assert message, "expected a message with binds"
end
def test_logs_binds_after_type_cast
binds = [Relation::QueryAttribute.new("id", "10", Type::Integer.new)]
assert_logs_binds(binds)
end
def test_bind_params_to_sql_with_prepared_statements
assert_bind_params_to_sql
end
def test_bind_params_to_sql_with_unprepared_statements
@connection.unprepared_statement do
assert_bind_params_to_sql
end
end
def test_nested_unprepared_statements
assert_predicate @connection, :prepared_statements?
@connection.unprepared_statement do
assert_not_predicate @connection, :prepared_statements?
@connection.unprepared_statement do
assert_not_predicate @connection, :prepared_statements?
end
assert_not_predicate @connection, :prepared_statements?
end
assert_predicate @connection, :prepared_statements?
end
def test_binds_with_filtered_attributes
ActiveRecord::Base.filter_attributes = [:auth]
binds = [Relation::QueryAttribute.new("auth_token", "abcd", Type::String.new)]
assert_filtered_log_binds(binds)
ActiveRecord::Base.filter_attributes = []
end
private
def assert_bind_params_to_sql
table = Author.quoted_table_name
pk = "#{table}.#{Author.quoted_primary_key}"
# prepared_statements: true
#
# SELECT `authors`.* FROM `authors` WHERE (`authors`.`id` IN (?, ?, ?) OR `authors`.`id` IS NULL)
#
# prepared_statements: false
#
# SELECT `authors`.* FROM `authors` WHERE (`authors`.`id` IN (1, 2, 3) OR `authors`.`id` IS NULL)
#
sql = "SELECT #{table}.* FROM #{table} WHERE (#{pk} IN (#{bind_params(1..3)}) OR #{pk} IS NULL)"
authors = Author.where(id: [1, 2, 3, nil])
assert_equal sql, @connection.to_sql(authors.arel)
assert_sql(sql) { assert_equal 3, authors.length }
# prepared_statements: true
#
# SELECT `authors`.* FROM `authors` WHERE `authors`.`id` IN (?, ?, ?)
#
# prepared_statements: false
#
# SELECT `authors`.* FROM `authors` WHERE `authors`.`id` IN (1, 2, 3)
#
sql = "SELECT #{table}.* FROM #{table} WHERE #{pk} IN (#{bind_params(1..3)})"
authors = Author.where(id: [1, 2, 3, 9223372036854775808])
assert_equal sql, @connection.to_sql(authors.arel)
assert_sql(sql) { assert_equal 3, authors.length }
end
def bind_params(ids)
collector = @connection.send(:collector)
bind_params = ids.map { |i| Arel::Nodes::BindParam.new(i) }
sql, _ = @connection.visitor.compile(bind_params, collector)
sql
end
def to_sql_key(arel)
sql = @connection.to_sql(arel)
@connection.respond_to?(:sql_key, true) ? @connection.send(:sql_key, sql) : sql
end
def cached_statement(klass, key)
cache = klass.send(:cached_find_by_statement, key) do
raise "#{klass} has no cached statement by #{key.inspect}"
end
cache.send(:query_builder).instance_variable_get(:@sql)
end
def statement_cache
@connection.instance_variable_get(:@statements).send(:cache)
end
def assert_logs_binds(binds)
payload = {
name: "SQL",
sql: "select * from topics where id = ?",
binds: binds,
type_casted_binds: @connection.send(:type_casted_binds, binds)
}
event = ActiveSupport::Notifications::Event.new(
"foo",
Time.now,
Time.now,
123,
payload)
logger = Class.new(ActiveRecord::LogSubscriber) {
attr_reader :debugs
def initialize
super
@debugs = []
end
def debug(str)
@debugs << str
end
}.new
logger.sql(event)
assert_match %r(\[\["id", 10\]\]\z), logger.debugs.first
end
def assert_filtered_log_binds(binds)
payload = {
name: "SQL",
sql: "select * from users where auth_token = ?",
binds: binds,
type_casted_binds: @connection.send(:type_casted_binds, binds)
}
event = ActiveSupport::Notifications::Event.new(
"foo",
Time.now,
Time.now,
123,
payload)
logger = Class.new(ActiveRecord::LogSubscriber) {
attr_reader :debugs
def initialize
super
@debugs = []
end
def debug(str)
@debugs << str
end
}.new
logger.sql(event)
assert_match %r/#{Regexp.escape '[["auth_token", "[FILTERED]"]]'}/, logger.debugs.first
end
end
end
end
# frozen_string_literal: true
require_relative "../helper"
require "arel/collectors/bind"
module Arel
module Collectors
class TestBind < Arel::Test
def setup
@conn = FakeRecord::Base.new
@visitor = Visitors::ToSql.new @conn.connection
super
end
def collect(node)
@visitor.accept(node, Collectors::Bind.new)
end
def compile(node)
collect(node).value
end
def ast_with_binds(bvs)
table = Table.new(:users)
manager = Arel::SelectManager.new table
manager.where(table[:age].eq(Nodes::BindParam.new(bvs.shift)))
manager.where(table[:name].eq(Nodes::BindParam.new(bvs.shift)))
manager.ast
end
def test_compile_gathers_all_bind_params
binds = compile(ast_with_binds(["hello", "world"]))
assert_equal ["hello", "world"], binds
binds = compile(ast_with_binds(["hello2", "world3"]))
assert_equal ["hello2", "world3"], binds
end
end
end
end
# frozen_string_literal: true
class Bird < ActiveRecord::Base
belongs_to :pirate
validates_presence_of :name
accepts_nested_attributes_for :pirate
before_save do
# force materialize_transactions
self.class.connection.materialize_transactions
end
attr_accessor :cancel_save_from_callback
before_save :cancel_save_callback_method, if: :cancel_save_from_callback
def cancel_save_callback_method
throw(:abort)
end
attr_accessor :total_count, :enable_count
after_initialize do
self.total_count = Bird.count if enable_count
end
end
# frozen_string_literal: true
module ActiveRecord
module ConnectionAdapters
module PostgreSQL
module OID # :nodoc:
class Bit < Type::Value # :nodoc:
def type
:bit
end
def cast_value(value)
if ::String === value
case value
when /^0x/i
value[2..-1].hex.to_s(2) # Hexadecimal notation
else
value # Bit-string notation
end
else
value.to_s
end
end
def serialize(value)
Data.new(super) if value
end
class Data
def initialize(value)
@value = value
end
def to_s
value
end
def binary?
/\A[01]*\Z/.match?(value)
end
def hex?
/\A[0-9A-F]*\Z/i.match?(value)
end
private
attr_reader :value
end
end
end
end
end
end
# frozen_string_literal: true
require "cases/helper"
require "support/connection_helper"
require "support/schema_dumping_helper"
class PostgresqlBitStringTest < ActiveRecord::PostgreSQLTestCase
include ConnectionHelper
include SchemaDumpingHelper
class PostgresqlBitString < ActiveRecord::Base; end
def setup
@connection = ActiveRecord::Base.connection
@connection.create_table("postgresql_bit_strings", force: true) do |t|
t.bit :a_bit, default: "00000011", limit: 8
t.bit_varying :a_bit_varying, default: "0011", limit: 4
t.bit :another_bit
t.bit_varying :another_bit_varying
end
end
def teardown
return unless @connection
@connection.drop_table "postgresql_bit_strings", if_exists: true
end
def test_bit_string_column
column = PostgresqlBitString.columns_hash["a_bit"]
assert_equal :bit, column.type
assert_equal "bit(8)", column.sql_type
assert_not_predicate column, :array?
type = PostgresqlBitString.type_for_attribute("a_bit")
assert_not_predicate type, :binary?
end
def test_bit_string_varying_column
column = PostgresqlBitString.columns_hash["a_bit_varying"]
assert_equal :bit_varying, column.type
assert_equal "bit varying(4)", column.sql_type
assert_not_predicate column, :array?
type = PostgresqlBitString.type_for_attribute("a_bit_varying")
assert_not_predicate type, :binary?
end
def test_default
assert_equal "00000011", PostgresqlBitString.column_defaults["a_bit"]
assert_equal "00000011", PostgresqlBitString.new.a_bit
assert_equal "0011", PostgresqlBitString.column_defaults["a_bit_varying"]
assert_equal "0011", PostgresqlBitString.new.a_bit_varying
end
def test_schema_dumping
output = dump_table_schema("postgresql_bit_strings")
assert_match %r{t\.bit\s+"a_bit",\s+limit: 8,\s+default: "00000011"$}, output
assert_match %r{t\.bit_varying\s+"a_bit_varying",\s+limit: 4,\s+default: "0011"$}, output
end
if ActiveRecord::Base.connection.prepared_statements
def test_assigning_invalid_hex_string_raises_exception
assert_raises(ActiveRecord::StatementInvalid) { PostgresqlBitString.create! a_bit: "FF" }
assert_raises(ActiveRecord::StatementInvalid) { PostgresqlBitString.create! a_bit_varying: "F" }
end
end
def test_roundtrip
record = PostgresqlBitString.create!(a_bit: "00001010", a_bit_varying: "0101")
assert_equal "00001010", record.a_bit
assert_equal "0101", record.a_bit_varying
assert_nil record.another_bit
assert_nil record.another_bit_varying
record.a_bit = "11111111"
record.a_bit_varying = "0xF"
record.save!
assert record.reload
assert_equal "11111111", record.a_bit
assert_equal "1111", record.a_bit_varying
end
end
# frozen_string_literal: true
module ActiveRecord
module ConnectionAdapters
module PostgreSQL
module OID # :nodoc:
class BitVarying < OID::Bit # :nodoc:
def type
:bit_varying
end
end
end
end
end
end
# frozen_string_literal: true
class Book < ActiveRecord::Base
belongs_to :author
has_many :citations, foreign_key: "book1_id", inverse_of: :book
has_many :references, -> { distinct }, through: :citations, source: :reference_of
has_many :subscriptions
has_many :subscribers, through: :subscriptions
has_one :essay
enum status: [:proposed, :written, :published]
enum last_read: { unread: 0, reading: 2, read: 3, forgotten: nil }
enum nullable_status: [:single, :married]
enum language: [:english, :spanish, :french], _prefix: :in
enum author_visibility: [:visible, :invisible], _prefix: true
enum illustrator_visibility: [:visible, :invisible], _prefix: true
enum font_size: [:small, :medium, :large], _prefix: :with, _suffix: true
enum difficulty: [:easy, :medium, :hard], _suffix: :to_read
enum cover: { hard: "hard", soft: "soft" }
enum boolean_status: { enabled: true, disabled: false }
def published!
super
"do publish work..."
end
end
class PublishedBook < ActiveRecord::Base
self.table_name = "books"
enum :cover, { hard: "0", soft: "1" }, default: :hard
validates_uniqueness_of :isbn
end
# frozen_string_literal: true
class BookDestroyAsync < ActiveRecord::Base
self.table_name = "books"
has_many :taggings, as: :taggable, class_name: "Tagging"
has_many :tags, through: :taggings, dependent: :destroy_async
has_many :essays, dependent: :destroy_async, class_name: "EssayDestroyAsync", foreign_key: "book_id"
has_one :content, dependent: :destroy_async
enum status: [:proposed, :written, :published]
def published!
super
"do publish work..."
end
end
class BookDestroyAsyncWithScopedTags < ActiveRecord::Base
self.table_name = "books"
has_many :taggings, as: :taggable, class_name: "Tagging"
has_many :tags, -> { where name: "Der be rum" }, through: :taggings, dependent: :destroy_async
end
# frozen_string_literal: true
class UnencryptedBook < ActiveRecord::Base
self.table_name = "encrypted_books"
end
class EncryptedBook < ActiveRecord::Base
self.table_name = "encrypted_books"
encrypts :name, deterministic: true
end
class EncryptedBookWithDowncaseName < ActiveRecord::Base
self.table_name = "encrypted_books"
validates :name, uniqueness: true
encrypts :name, deterministic: true, downcase: true
end
class EncryptedBookThatIgnoresCase < ActiveRecord::Base
self.table_name = "encrypted_books"
encrypts :name, deterministic: true, ignore_case: true
end
# frozen_string_literal: true
class Boolean < ActiveRecord::Base
def has_fun
super
end
end
# frozen_string_literal: true
require "cases/helper"
require "models/boolean"
class BooleanTest < ActiveRecord::TestCase
def test_boolean
b_nil = Boolean.create!(value: nil)
b_false = Boolean.create!(value: false)
b_true = Boolean.create!(value: true)
assert_nil Boolean.find(b_nil.id).value
assert_not_predicate Boolean.find(b_false.id), :value?
assert_predicate Boolean.find(b_true.id), :value?
end
def test_boolean_without_questionmark
b_true = Boolean.create!(value: true)
subclass = Class.new(Boolean).find(b_true.id)
superclass = Boolean.find(b_true.id)
assert_equal superclass.read_attribute(:has_fun), subclass.read_attribute(:has_fun)
end
def test_boolean_cast_from_string
b_blank = Boolean.create!(value: "")
b_false = Boolean.create!(value: "0")
b_true = Boolean.create!(value: "1")
assert_nil Boolean.find(b_blank.id).value
assert_not_predicate Boolean.find(b_false.id), :value?
assert_predicate Boolean.find(b_true.id), :value?
end
def test_find_by_boolean_string
b_false = Boolean.create!(value: "false")
b_true = Boolean.create!(value: "true")
assert_equal b_false, Boolean.find_by(value: "false")
assert_equal b_true, Boolean.find_by(value: "true")
end
def test_find_by_falsy_boolean_symbol
ActiveModel::Type::Boolean::FALSE_VALUES.each do |value|
b_false = Boolean.create!(value: value)
assert_not_predicate b_false, :value?
assert_equal b_false, Boolean.find_by(id: b_false.id, value: value.to_s.to_sym)
end
end
end
# frozen_string_literal: true
class Branch < ActiveRecord::Base
has_many :branches
belongs_to :branch, optional: true
end
class BrokenBranch < Branch
has_many :branches, class_name: "BrokenBranch", foreign_key: :branch_id
belongs_to :branch, optional: true, inverse_of: :branch, class_name: "BrokenBranch"
end
# frozen_string_literal: true
class Bulb < ActiveRecord::Base
default_scope { where(name: "defaulty") }
belongs_to :car, touch: true
scope :awesome, -> { where(frickinawesome: true) }
attr_reader :scope_after_initialize, :attributes_after_initialize, :count_after_create
after_initialize :record_scope_after_initialize
def record_scope_after_initialize
@scope_after_initialize = self.class.all
end
after_initialize :record_attributes_after_initialize
def record_attributes_after_initialize
@attributes_after_initialize = attributes.dup
end
after_create :record_count_after_create
def record_count_after_create
@count_after_create = Bulb.unscoped do
car&.bulbs&.count
end
end
def color=(color)
self[:color] = color.upcase + "!"
end
def self.new(attributes = {}, &block)
bulb_type = (attributes || {}).delete(:bulb_type)
if bulb_type.present?
bulb_class = "#{bulb_type.to_s.camelize}Bulb".constantize
bulb_class.new(attributes, &block)
else
super
end
end
end
class CustomBulb < Bulb
after_initialize :set_awesomeness
def set_awesomeness
self.frickinawesome = true if name == "Dude"
end
end
class FunkyBulb < Bulb
before_destroy do
raise "before_destroy was called"
end
end
class FailedBulb < Bulb
before_destroy do
throw(:abort)
end
end
# frozen_string_literal: true
module ActiveRecord
module ConnectionAdapters
module PostgreSQL
module OID # :nodoc:
class Bytea < Type::Binary # :nodoc:
def deserialize(value)
return if value.nil?
return value.to_s if value.is_a?(Type::Binary::Data)
PG::Connection.unescape_bytea(super)
end
end
end
end
end
end
# frozen_string_literal: true
require "cases/helper"
require "support/schema_dumping_helper"
class PostgresqlByteaTest < ActiveRecord::PostgreSQLTestCase
include SchemaDumpingHelper
class ByteaDataType < ActiveRecord::Base
self.table_name = "bytea_data_type"
end
def setup
@connection = ActiveRecord::Base.connection
@connection.transaction do
@connection.create_table("bytea_data_type") do |t|
t.binary "payload"
t.binary "serialized"
end
end
@column = ByteaDataType.columns_hash["payload"]
@type = ByteaDataType.type_for_attribute("payload")
end
teardown do
@connection.drop_table "bytea_data_type", if_exists: true
end
def test_column
assert @column.is_a?(ActiveRecord::ConnectionAdapters::PostgreSQLColumn)
assert_equal :binary, @column.type
end
def test_binary_columns_are_limitless_the_upper_limit_is_one_GB
assert_equal "bytea", @connection.type_to_sql(:binary, limit: 100_000)
assert_raise ArgumentError do
@connection.type_to_sql(:binary, limit: 4294967295)
end
end
def test_type_cast_binary_converts_the_encoding
assert @column
data = "\u001F\x8B"
assert_equal("UTF-8", data.encoding.name)
assert_equal("ASCII-8BIT", @type.deserialize(data).encoding.name)
end
def test_type_cast_binary_value
data = (+"\u001F\x8B").force_encoding("BINARY")
assert_equal(data, @type.deserialize(data))
end
def test_type_case_nil
assert_nil(@type.deserialize(nil))
end
def test_read_value
data = "\u001F"
@connection.execute "insert into bytea_data_type (payload) VALUES ('#{data}')"
record = ByteaDataType.first
assert_equal(data, record.payload)
record.delete
end
def test_read_nil_value
@connection.execute "insert into bytea_data_type (payload) VALUES (null)"
record = ByteaDataType.first
assert_nil(record.payload)
record.delete
end
def test_write_value
data = "\u001F"
record = ByteaDataType.create(payload: data)
assert_not_predicate record, :new_record?
assert_equal(data, record.payload)
end
def test_via_to_sql
data = "'\u001F\\"
ByteaDataType.create(payload: data)
sql = ByteaDataType.where(payload: data).select(:payload).to_sql
result = @connection.query(sql)
assert_equal([[data]], result)
end
def test_via_to_sql_with_complicating_connection
Thread.new do
other_conn = ActiveRecord::Base.connection
other_conn.execute("SET standard_conforming_strings = off")
other_conn.execute("SET escape_string_warning = off")
end.join
test_via_to_sql
end
def test_write_binary
data = File.read(File.join(__dir__, "..", "..", "..", "assets", "example.log"))
assert(data.size > 1)
record = ByteaDataType.create(payload: data)
assert_not_predicate record, :new_record?
assert_equal(data, record.payload)
assert_equal(data, ByteaDataType.where(id: record.id).first.payload)
end
def test_write_nil
record = ByteaDataType.create(payload: nil)
assert_not_predicate record, :new_record?
assert_nil(record.payload)
assert_nil(ByteaDataType.where(id: record.id).first.payload)
end
class Serializer
def load(str); str; end
def dump(str); str; end
end
def test_serialize
klass = Class.new(ByteaDataType) {
serialize :serialized, Serializer.new
}
obj = klass.new
obj.serialized = "hello world"
obj.save!
obj.reload
assert_equal "hello world", obj.serialized
end
def test_schema_dumping
output = dump_table_schema("bytea_data_type")
assert_match %r{t\.binary\s+"payload"$}, output
assert_match %r{t\.binary\s+"serialized"$}, output
end
end
# frozen_string_literal: true
require "cases/helper"
module ActiveRecord
class CacheKeyTest < ActiveRecord::TestCase
self.use_transactional_tests = false
class CacheMe < ActiveRecord::Base
self.cache_versioning = false
end
class CacheMeWithVersion < ActiveRecord::Base
self.cache_versioning = true
end
setup do
@connection = ActiveRecord::Base.connection
@connection.create_table(:cache_mes, force: true) { |t| t.timestamps }
@connection.create_table(:cache_me_with_versions, force: true) { |t| t.timestamps }
end
teardown do
@connection.drop_table :cache_mes, if_exists: true
@connection.drop_table :cache_me_with_versions, if_exists: true
end
test "cache_key format is not too precise" do
record = CacheMe.create
key = record.cache_key
assert_equal key, record.reload.cache_key
end
test "cache_key has no version when versioning is on" do
record = CacheMeWithVersion.create
assert_equal "active_record/cache_key_test/cache_me_with_versions/#{record.id}", record.cache_key
end
test "cache_version is only there when versioning is on" do
assert_predicate CacheMeWithVersion.create.cache_version, :present?
assert_not_predicate CacheMe.create.cache_version, :present?
end
test "cache_key_with_version always has both key and version" do
r1 = CacheMeWithVersion.create
assert_equal "active_record/cache_key_test/cache_me_with_versions/#{r1.id}-#{r1.updated_at.utc.to_fs(:usec)}", r1.cache_key_with_version
r2 = CacheMe.create
assert_equal "active_record/cache_key_test/cache_mes/#{r2.id}-#{r2.updated_at.utc.to_fs(:usec)}", r2.cache_key_with_version
end
test "cache_version is the same when it comes from the DB or from the user" do
skip("Mysql2 and PostgreSQL don't return a string value for updated_at") if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter)
record = CacheMeWithVersion.create
record_from_db = CacheMeWithVersion.find(record.id)
assert_not_called(record_from_db, :updated_at) do
record_from_db.cache_version
end
assert_equal record.cache_version, record_from_db.cache_version
end
test "cache_version does not truncate zeros when timestamp ends in zeros" do
skip("Mysql2 and PostgreSQL don't return a string value for updated_at") if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter)
travel_to Time.now.beginning_of_day do
record = CacheMeWithVersion.create
record_from_db = CacheMeWithVersion.find(record.id)
assert_not_called(record_from_db, :updated_at) do
record_from_db.cache_version
end
assert_equal record.cache_version, record_from_db.cache_version
end
end
test "cache_version calls updated_at when the value is generated at create time" do
record = CacheMeWithVersion.create
assert_called(record, :updated_at) do
record.cache_version
end
end
test "cache_version does NOT call updated_at when value is from the database" do
skip("Mysql2 and PostgreSQL don't return a string value for updated_at") if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter)
record = CacheMeWithVersion.create
record_from_db = CacheMeWithVersion.find(record.id)
assert_not_called(record_from_db, :updated_at) do
record_from_db.cache_version
end
end
test "cache_version does call updated_at when it is assigned via a Time object" do
record = CacheMeWithVersion.create
record_from_db = CacheMeWithVersion.find(record.id)
assert_called(record_from_db, :updated_at) do
record_from_db.updated_at = Time.now
record_from_db.cache_version
end
end
test "cache_version does call updated_at when it is assigned via a string" do
record = CacheMeWithVersion.create
record_from_db = CacheMeWithVersion.find(record.id)
assert_called(record_from_db, :updated_at) do
record_from_db.updated_at = Time.now.to_s
record_from_db.cache_version
end
end
test "cache_version does call updated_at when it is assigned via a hash" do
record = CacheMeWithVersion.create
record_from_db = CacheMeWithVersion.find(record.id)
assert_called(record_from_db, :updated_at) do
record_from_db.updated_at = { 1 => 2016, 2 => 11, 3 => 12, 4 => 1, 5 => 2, 6 => 3, 7 => 22 }
record_from_db.cache_version
end
end
test "updated_at on class but not on instance raises an error" do
record = CacheMeWithVersion.create
record_from_db = CacheMeWithVersion.where(id: record.id).select(:id).first
assert_raises(ActiveModel::MissingAttributeError) do
record_from_db.cache_version
end
end
end
end
# frozen_string_literal: true
class CakeDesigner < ActiveRecord::Base
has_one :chef, as: :employable
end
# frozen_string_literal: true
require "active_support/core_ext/enumerable"
module ActiveRecord
module Calculations
# Count the records.
#
# Person.count
# # => the total count of all people
#
# Person.count(:age)
# # => returns the total count of all people whose age is present in database
#
# Person.count(:all)
# # => performs a COUNT(*) (:all is an alias for '*')
#
# Person.distinct.count(:age)
# # => counts the number of different age values
#
# If #count is used with {Relation#group}[rdoc-ref:QueryMethods#group],
# it returns a Hash whose keys represent the aggregated column,
# and the values are the respective amounts:
#
# Person.group(:city).count
# # => { 'Rome' => 5, 'Paris' => 3 }
#
# If #count is used with {Relation#group}[rdoc-ref:QueryMethods#group] for multiple columns, it returns a Hash whose
# keys are an array containing the individual values of each column and the value
# of each key would be the #count.
#
# Article.group(:status, :category).count
# # => {["draft", "business"]=>10, ["draft", "technology"]=>4,
# # ["published", "business"]=>0, ["published", "technology"]=>2}
#
# If #count is used with {Relation#select}[rdoc-ref:QueryMethods#select], it will count the selected columns:
#
# Person.select(:age).count
# # => counts the number of different age values
#
# Note: not all valid {Relation#select}[rdoc-ref:QueryMethods#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)
if block_given?
unless column_name.nil?
raise ArgumentError, "Column name argument is not supported when a block is passed."
end
super()
else
calculate(:count, column_name)
end
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)
calculate(:average, column_name)
end
# Calculates the minimum value on a given column. The value is returned
# with the same data type of the column, or +nil+ if there's no row. See
# #calculate for examples with options.
#
# Person.minimum(:age) # => 7
def minimum(column_name)
calculate(:minimum, column_name)
end
# Calculates the maximum value on a given column. The value is returned
# with the same data type of the column, or +nil+ if there's no row. See
# #calculate for examples with options.
#
# Person.maximum(:age) # => 93
def maximum(column_name)
calculate(:maximum, column_name)
end
# Calculates the sum of values on a given column. The value is returned
# with the same data type of the column, +0+ if there's no row. See
# #calculate for examples with options.
#
# Person.sum(:age) # => 4562
def sum(identity_or_column = nil, &block)
if block_given?
values = map(&block)
if identity_or_column.nil? && (values.first.is_a?(Numeric) || values.first(1) == [])
identity_or_column = 0
end
if identity_or_column.nil?
ActiveSupport::Deprecation.warn(<<-MSG.squish)
Rails 7.0 has deprecated Enumerable.sum in favor of Ruby's native implementation available since 2.4.
Sum of non-numeric elements requires an initial argument.
MSG
values.inject(:+) || 0
else
values.sum(identity_or_column)
end
else
calculate(:sum, identity_or_column)
end
end
# This calculates aggregate values in the given column. Methods for #count, #sum, #average,
# #minimum, and #maximum have been added as shortcuts.
#
# Person.calculate(:count, :all) # The same as Person.count
# Person.average(:age) # SELECT AVG(age) FROM people...
#
# # Selects the minimum age for any family without any minors
# Person.group(:last_name).having("min(age) > 17").minimum(:age)
#
# Person.sum("2 * age")
#
# There are two basic forms of output:
#
# * Single aggregate value: The single value is type cast to Integer for COUNT, Float
# for AVG, and the given column's type for everything else.
#
# * Grouped values: This returns an ordered hash of the values and groups them. It
# takes either a column name, or the name of a belongs_to association.
#
# values = Person.group('last_name').maximum(:age)
# puts values["Drake"]
# # => 43
#
# drake = Family.find_by(last_name: 'Drake')
# values = Person.group(:family).maximum(:age) # Person belongs_to :family
# puts values[drake]
# # => 43
#
# values.each do |family, max_age|
# ...
# end
def calculate(operation, column_name)
if has_include?(column_name)
relation = apply_join_dependency
if operation.to_s.downcase == "count"
unless distinct_value || distinct_select?(column_name || select_for_count)
relation.distinct!
relation.select_values = [ klass.primary_key || table[Arel.star] ]
end
# PostgreSQL: ORDER BY expressions must appear in SELECT list when using DISTINCT
relation.order_values = [] if group_values.empty?
end
relation.calculate(operation, column_name)
else
perform_calculation(operation, column_name)
end
end
# Use #pluck as a shortcut to select one or more attributes without
# loading an entire record object per row.
#
# Person.pluck(:name)
#
# instead of
#
# Person.all.map(&:name)
#
# Pluck returns an Array of attribute values type-casted to match
# the plucked column names, if they can be deduced. Plucking an SQL fragment
# returns String values by default.
#
# Person.pluck(:name)
# # SELECT people.name FROM people
# # => ['David', 'Jeremy', 'Jose']
#
# Person.pluck(:id, :name)
# # SELECT people.id, people.name FROM people
# # => [[1, 'David'], [2, 'Jeremy'], [3, 'Jose']]
#
# Person.distinct.pluck(:role)
# # SELECT DISTINCT role FROM people
# # => ['admin', 'member', 'guest']
#
# Person.where(age: 21).limit(5).pluck(:id)
# # SELECT people.id FROM people WHERE people.age = 21 LIMIT 5
# # => [2, 3]
#
# Person.pluck(Arel.sql('DATEDIFF(updated_at, created_at)'))
# # SELECT DATEDIFF(updated_at, created_at) FROM people
# # => ['0', '27761', '173']
#
# See also #ids.
#
def pluck(*column_names)
if loaded? && all_attributes?(column_names)
return records.pluck(*column_names)
end
if has_include?(column_names.first)
relation = apply_join_dependency
relation.pluck(*column_names)
else
klass.disallow_raw_sql!(column_names)
columns = arel_columns(column_names)
relation = spawn
relation.select_values = columns
result = skip_query_cache_if_necessary do
if where_clause.contradiction?
ActiveRecord::Result.empty
else
klass.connection.select_all(relation.arel, "#{klass.name} Pluck")
end
end
type_cast_pluck_values(result, columns)
end
end
# Pick the value(s) from the named column(s) in the current relation.
# This is short-hand for <tt>relation.limit(1).pluck(*column_names).first</tt>, and is primarily useful
# when you have a relation that's already narrowed down to a single row.
#
# Just like #pluck, #pick will only load the actual value, not the entire record object, so it's also
# more efficient. The value is, again like with pluck, typecast by the column type.
#
# Person.where(id: 1).pick(:name)
# # SELECT people.name FROM people WHERE id = 1 LIMIT 1
# # => 'David'
#
# Person.where(id: 1).pick(:name, :email_address)
# # SELECT people.name, people.email_address FROM people WHERE id = 1 LIMIT 1
# # => [ 'David', 'david@loudthinking.com' ]
def pick(*column_names)
if loaded? && all_attributes?(column_names)
return records.pick(*column_names)
end
limit(1).pluck(*column_names).first
end
# Pluck all the ID's for the relation using the table's primary key
#
# Person.ids # SELECT people.id FROM people
# Person.joins(:companies).ids # SELECT people.id FROM people INNER JOIN companies ON companies.person_id = people.id
def ids
pluck primary_key
end
private
def all_attributes?(column_names)
(column_names.map(&:to_s) - @klass.attribute_names - @klass.attribute_aliases.keys).empty?
end
def has_include?(column_name)
eager_loading? || (includes_values.present? && column_name && column_name != :all)
end
def perform_calculation(operation, column_name)
operation = operation.to_s.downcase
# If #count is used with #distinct (i.e. `relation.distinct.count`) it is
# considered distinct.
distinct = distinct_value
if operation == "count"
column_name ||= select_for_count
if column_name == :all
if !distinct
distinct = distinct_select?(select_for_count) if group_values.empty?
elsif group_values.any? || select_values.empty? && order_values.empty?
column_name = primary_key
end
elsif distinct_select?(column_name)
distinct = nil
end
end
if group_values.any?
execute_grouped_calculation(operation, column_name, distinct)
else
execute_simple_calculation(operation, column_name, distinct)
end
end
def distinct_select?(column_name)
column_name.is_a?(::String) && /\bDISTINCT[\s(]/i.match?(column_name)
end
def aggregate_column(column_name)
return column_name if Arel::Expressions === column_name
arel_column(column_name.to_s) do |name|
Arel.sql(column_name == :all ? "*" : name)
end
end
def operation_over_aggregate_column(column, operation, distinct)
operation == "count" ? column.count(distinct) : column.public_send(operation)
end
def execute_simple_calculation(operation, column_name, distinct) # :nodoc:
if operation == "count" && (column_name == :all && distinct || has_limit_or_offset?)
# Shortcut when limit is zero.
return 0 if limit_value == 0
query_builder = build_count_subquery(spawn, column_name, distinct)
else
# PostgreSQL doesn't like ORDER BY when there are no GROUP BY
relation = unscope(:order).distinct!(false)
column = aggregate_column(column_name)
select_value = operation_over_aggregate_column(column, operation, distinct)
select_value.distinct = true if operation == "sum" && distinct
relation.select_values = [select_value]
query_builder = relation.arel
end
result = skip_query_cache_if_necessary { @klass.connection.select_all(query_builder, "#{@klass.name} #{operation.capitalize}") }
if operation != "count"
type = column.try(:type_caster) ||
lookup_cast_type_from_join_dependencies(column_name.to_s) || Type.default_value
type = type.subtype if Enum::EnumType === type
end
type_cast_calculated_value(result.cast_values.first, operation, type)
end
def execute_grouped_calculation(operation, column_name, distinct) # :nodoc:
group_fields = group_values
group_fields = group_fields.uniq if group_fields.size > 1
if group_fields.size == 1 && group_fields.first.respond_to?(:to_sym)
association = klass._reflect_on_association(group_fields.first)
associated = association && association.belongs_to? # only count belongs_to associations
group_fields = Array(association.foreign_key) if associated
end
group_fields = arel_columns(group_fields)
group_aliases = group_fields.map { |field|
field = connection.visitor.compile(field) if Arel.arel_node?(field)
column_alias_for(field.to_s.downcase)
}
group_columns = group_aliases.zip(group_fields)
column = aggregate_column(column_name)
column_alias = column_alias_for("#{operation} #{column_name.to_s.downcase}")
select_value = operation_over_aggregate_column(column, operation, distinct)
select_value.as(connection.quote_column_name(column_alias))
select_values = [select_value]
select_values += self.select_values unless having_clause.empty?
select_values.concat group_columns.map { |aliaz, field|
aliaz = connection.quote_column_name(aliaz)
if field.respond_to?(:as)
field.as(aliaz)
else
"#{field} AS #{aliaz}"
end
}
relation = except(:group).distinct!(false)
relation.group_values = group_fields
relation.select_values = select_values
calculated_data = skip_query_cache_if_necessary { @klass.connection.select_all(relation.arel, "#{@klass.name} #{operation.capitalize}") }
if association
key_ids = calculated_data.collect { |row| row[group_aliases.first] }
key_records = association.klass.base_class.where(association.klass.base_class.primary_key => key_ids)
key_records = key_records.index_by(&:id)
end
key_types = group_columns.each_with_object({}) do |(aliaz, col_name), types|
types[aliaz] = type_for(col_name) do
calculated_data.column_types.fetch(aliaz, Type.default_value)
end
end
hash_rows = calculated_data.cast_values(key_types).map! do |row|
calculated_data.columns.each_with_object({}).with_index do |(col_name, hash), i|
hash[col_name] = row[i]
end
end
if operation != "count"
type = column.try(:type_caster) ||
lookup_cast_type_from_join_dependencies(column_name.to_s) || Type.default_value
type = type.subtype if Enum::EnumType === type
end
hash_rows.each_with_object({}) do |row, result|
key = group_aliases.map { |aliaz| row[aliaz] }
key = key.first if key.size == 1
key = key_records[key] if associated
result[key] = type_cast_calculated_value(row[column_alias], operation, type)
end
end
# Converts the given field to the value that the database adapter returns as
# a usable column name:
#
# column_alias_for("users.id") # => "users_id"
# column_alias_for("sum(id)") # => "sum_id"
# column_alias_for("count(distinct users.id)") # => "count_distinct_users_id"
# column_alias_for("count(*)") # => "count_all"
def column_alias_for(field)
column_alias = +field
column_alias.gsub!(/\*/, "all")
column_alias.gsub!(/\W+/, " ")
column_alias.strip!
column_alias.gsub!(/ +/, "_")
connection.table_alias_for(column_alias)
end
def type_for(field, &block)
field_name = field.respond_to?(:name) ? field.name.to_s : field.to_s.split(".").last
@klass.type_for_attribute(field_name, &block)
end
def lookup_cast_type_from_join_dependencies(name, join_dependencies = build_join_dependencies)
each_join_dependencies(join_dependencies) do |join|
type = join.base_klass.attribute_types.fetch(name, nil)
return type if type
end
nil
end
def type_cast_pluck_values(result, columns)
cast_types = if result.columns.size != columns.size
klass.attribute_types
else
join_dependencies = nil
columns.map.with_index do |column, i|
column.try(:type_caster) ||
klass.attribute_types.fetch(name = result.columns[i]) do
join_dependencies ||= build_join_dependencies
lookup_cast_type_from_join_dependencies(name, join_dependencies) ||
result.column_types[name] || Type.default_value
end
end
end
result.cast_values(cast_types)
end
def type_cast_calculated_value(value, operation, type)
case operation
when "count"
value.to_i
when "sum"
type.deserialize(value || 0)
when "average"
case type.type
when :integer, :decimal
value&.to_d
else
type.deserialize(value)
end
else # "minimum", "maximum"
type.deserialize(value)
end
end
def select_for_count
if select_values.present?
return select_values.first if select_values.one?
select_values.join(", ")
else
:all
end
end
def build_count_subquery(relation, column_name, distinct)
if column_name == :all
column_alias = Arel.star
relation.select_values = [ Arel.sql(FinderMethods::ONE_AS_ONE) ] unless distinct
else
column_alias = Arel.sql("count_column")
relation.select_values = [ aggregate_column(column_name).as(column_alias) ]
end
subquery_alias = Arel.sql("subquery_for_count")
select_value = operation_over_aggregate_column(column_alias, "count", false)
relation.build_subquery(subquery_alias, select_value)
end
end
end
# frozen_string_literal: true
require "cases/helper"
require "models/book"
require "models/club"
require "models/company"
require "models/contract"
require "models/edge"
require "models/organization"
require "models/possession"
require "models/author"
require "models/topic"
require "models/reply"
require "models/numeric_data"
require "models/need_quoting"
require "models/minivan"
require "models/speedometer"
require "models/ship_part"
require "models/treasure"
require "models/developer"
require "models/post"
require "models/comment"
require "models/rating"
require "support/stubs/strong_parameters"
class CalculationsTest < ActiveRecord::TestCase
fixtures :companies, :accounts, :authors, :author_addresses, :topics, :speedometers, :minivans, :books, :posts, :comments
def test_should_sum_field
assert_equal 318, Account.sum(:credit_limit)
end
def test_should_sum_arel_attribute
assert_equal 318, Account.sum(Account.arel_table[:credit_limit])
end
def test_should_average_field
value = Account.average(:credit_limit)
assert_equal 53.0, value
end
def test_should_average_arel_attribute
value = Account.average(Account.arel_table[:credit_limit])
assert_equal 53.0, value
end
def test_should_resolve_aliased_attributes
assert_equal 318, Account.sum(:available_credit)
end
def test_should_return_decimal_average_of_integer_field
value = Account.average(:id)
assert_equal 3.5, value
assert_instance_of BigDecimal, value
end
def test_should_return_integer_average_if_db_returns_such
value = Book.average(:status)
assert_equal 1.0, value
assert_instance_of BigDecimal, value
end
def test_should_return_float_average_if_db_returns_such
NumericData.create!(temperature: 37.5)
value = NumericData.average(:temperature)
assert_equal 37.5, value
assert_instance_of Float, value
if current_adapter?(:PostgreSQLAdapter, :SQLite3Adapter)
NumericData.create!(temperature: "Infinity")
value = NumericData.average(:temperature)
assert_equal Float::INFINITY, value
assert_instance_of Float, value
end
end
def test_should_return_decimal_average_if_db_returns_such
NumericData.create!([{ bank_balance: 37.50 }, { bank_balance: 37.45 }])
value = NumericData.average(:bank_balance)
assert_equal 37.475, value
assert_instance_of BigDecimal, value
end
def test_should_return_nil_as_average
assert_nil NumericData.average(:bank_balance)
end
def test_should_get_maximum_of_field
assert_equal 60, Account.maximum(:credit_limit)
end
def test_should_get_maximum_of_arel_attribute
assert_equal 60, Account.maximum(Account.arel_table[:credit_limit])
end
def test_should_get_maximum_of_field_with_include
assert_equal 55, Account.where("companies.name != 'Summit'").references(:companies).includes(:firm).maximum(:credit_limit)
end
def test_should_get_maximum_of_arel_attribute_with_include
assert_equal 55, Account.where("companies.name != 'Summit'").references(:companies).includes(:firm).maximum(Account.arel_table[:credit_limit])
end
def test_should_get_minimum_of_field
assert_equal 50, Account.minimum(:credit_limit)
end
def test_should_get_minimum_of_arel_attribute
assert_equal 50, Account.minimum(Account.arel_table[:credit_limit])
end
def test_should_group_by_field
c = Account.group(:firm_id).sum(:credit_limit)
[1, 6, 2].each do |firm_id|
assert_includes c.keys, firm_id, "Group #{c.inspect} does not contain firm_id #{firm_id}"
end
end
def test_should_group_by_arel_attribute
c = Account.group(Account.arel_table[:firm_id]).sum(:credit_limit)
[1, 6, 2].each do |firm_id|
assert_includes c.keys, firm_id, "Group #{c.inspect} does not contain firm_id #{firm_id}"
end
end
def test_should_group_by_multiple_fields
c = Account.group("firm_id", :credit_limit).count(:all)
[ [nil, 50], [1, 50], [6, 50], [6, 55], [9, 53], [2, 60] ].each { |firm_and_limit| assert_includes c.keys, firm_and_limit }
end
def test_should_group_by_multiple_fields_having_functions
c = Topic.group(:author_name, "COALESCE(type, title)").count(:all)
assert_equal 1, c[["Carl", "The Third Topic of the day"]]
assert_equal 1, c[["Mary", "Reply"]]
assert_equal 1, c[["David", "The First Topic"]]
assert_equal 1, c[["Carl", "Reply"]]
end
def test_should_group_by_summed_field
expected = { nil => 50, 1 => 50, 2 => 60, 6 => 105, 9 => 53 }
assert_equal expected, Account.group(:firm_id).sum(:credit_limit)
end
def test_group_by_multiple_same_field
accounts = Account.group(:firm_id)
expected = {
nil => 50,
1 => 50,
2 => 60,
6 => 105,
9 => 53
}
assert_equal expected, accounts.sum(:credit_limit)
assert_equal expected, accounts.merge!(accounts).uniq!(:group).sum(:credit_limit)
expected = {
nil => 50,
1 => 50,
2 => 60,
6 => 55,
9 => 53
}
assert_equal expected, accounts.merge!(accounts).maximum(:credit_limit)
expected = {
nil => 50,
1 => 50,
2 => 60,
6 => 50,
9 => 53
}
assert_equal expected, accounts.merge!(accounts).minimum(:credit_limit)
end
def test_should_generate_valid_sql_with_joins_and_group
assert_nothing_raised do
AuditLog.joins(:developer).group(:id).count
end
end
def test_should_calculate_against_given_relation
developer = Developer.create!(name: "developer")
developer.audit_logs.create!(message: "first log")
developer.audit_logs.create!(message: "second log")
c = developer.audit_logs.joins(:developer).group(:id).count
assert_equal developer.audit_logs.count, c.size
developer.audit_logs.each do |log|
assert_equal 1, c[log.id]
end
end
def test_should_not_use_alias_for_grouped_field
assert_sql(/GROUP BY #{Regexp.escape(Account.connection.quote_table_name("accounts.firm_id"))}/i) do
c = Account.group(:firm_id).order("accounts_firm_id").sum(:credit_limit)
assert_equal [1, 2, 6, 9], c.keys.compact
end
end
def test_should_order_by_grouped_field
c = Account.group(:firm_id).order("firm_id").sum(:credit_limit)
assert_equal [1, 2, 6, 9], c.keys.compact
end
def test_should_order_by_calculation
c = Account.group(:firm_id).order("sum_credit_limit desc, firm_id").sum(:credit_limit)
assert_equal [105, 60, 53, 50, 50], c.keys.collect { |k| c[k] }
assert_equal [6, 2, 9, 1], c.keys.compact
end
def test_should_limit_calculation
c = Account.where("firm_id IS NOT NULL").group(:firm_id).order("firm_id").limit(2).sum(:credit_limit)
assert_equal [1, 2], c.keys.compact
end
def test_should_limit_calculation_with_offset
c = Account.where("firm_id IS NOT NULL").group(:firm_id).order("firm_id").
limit(2).offset(1).sum(:credit_limit)
assert_equal [2, 6], c.keys.compact
end
def test_limit_should_apply_before_count
accounts = Account.order(:id).limit(4)
assert_equal 3, accounts.count(:firm_id)
assert_equal 3, accounts.select(:firm_id).count
end
def test_limit_should_apply_before_count_arel_attribute
accounts = Account.order(:id).limit(4)
firm_id_attribute = Account.arel_table[:firm_id]
assert_equal 3, accounts.count(firm_id_attribute)
assert_equal 3, accounts.select(firm_id_attribute).count
end
def test_count_should_shortcut_with_limit_zero
accounts = Account.limit(0)
assert_no_queries { assert_equal 0, accounts.count }
end
def test_limit_is_kept
return if current_adapter?(:OracleAdapter)
queries = capture_sql { Account.limit(1).count }
assert_equal 1, queries.length
assert_match(/LIMIT/, queries.first)
end
def test_offset_is_kept
return if current_adapter?(:OracleAdapter)
queries = capture_sql { Account.offset(1).count }
assert_equal 1, queries.length
assert_match(/OFFSET/, queries.first)
end
def test_limit_with_offset_is_kept
return if current_adapter?(:OracleAdapter)
queries = capture_sql { Account.limit(1).offset(1).count }
assert_equal 1, queries.length
assert_match(/LIMIT/, queries.first)
assert_match(/OFFSET/, queries.first)
end
def test_no_limit_no_offset
queries = capture_sql { Account.count }
assert_equal 1, queries.length
assert_no_match(/LIMIT/, queries.first)
assert_no_match(/OFFSET/, queries.first)
end
def test_count_on_invalid_columns_raises
e = assert_raises(ActiveRecord::StatementInvalid) {
Account.select("credit_limit, firm_name").count
}
assert_match %r{accounts}i, e.sql
assert_match "credit_limit, firm_name", e.sql
end
def test_apply_distinct_in_count
queries = capture_sql do
Account.distinct.count
Account.group(:firm_id).distinct.count
end
queries.each do |query|
assert_match %r{\ASELECT(?! DISTINCT) COUNT\(DISTINCT\b}, query
end
end
def test_count_with_eager_loading_and_custom_order
posts = Post.includes(:comments).order("comments.id")
assert_queries(1) { assert_equal 11, posts.count }
assert_queries(1) { assert_equal 11, posts.count(:all) }
end
def test_count_with_eager_loading_and_custom_select_and_order
posts = Post.includes(:comments).order("comments.id").select(:type)
assert_queries(1) { assert_equal 11, posts.count }
assert_queries(1) { assert_equal 11, posts.count(:all) }
end
def test_count_with_eager_loading_and_custom_order_and_distinct
posts = Post.includes(:comments).order("comments.id").distinct
assert_queries(1) { assert_equal 11, posts.count }
assert_queries(1) { assert_equal 11, posts.count(:all) }
end
def test_distinct_count_all_with_custom_select_and_order
accounts = Account.distinct.select("credit_limit % 10").order(Arel.sql("credit_limit % 10"))
assert_queries(1) { assert_equal 3, accounts.count(:all) }
assert_queries(1) { assert_equal 3, accounts.load.size }
end
def test_distinct_count_with_order_and_limit
assert_equal 4, Account.distinct.order(:firm_id).limit(4).count
end
def test_distinct_count_with_order_and_offset
assert_equal 4, Account.distinct.order(:firm_id).offset(2).count
end
def test_distinct_count_with_order_and_limit_and_offset
assert_equal 4, Account.distinct.order(:firm_id).limit(4).offset(2).count
end
def test_distinct_joins_count_with_order_and_limit
assert_equal 3, Account.joins(:firm).distinct.order(:firm_id).limit(3).count
end
def test_distinct_joins_count_with_order_and_offset
assert_equal 3, Account.joins(:firm).distinct.order(:firm_id).offset(2).count
end
def test_distinct_joins_count_with_order_and_limit_and_offset
assert_equal 3, Account.joins(:firm).distinct.order(:firm_id).limit(3).offset(2).count
end
def test_distinct_joins_count_with_group_by
expected = { nil => 4, 1 => 1, 2 => 1, 4 => 1, 5 => 1, 7 => 1 }
assert_equal expected, Post.left_joins(:comments).group(:post_id).distinct.count(:author_id)
assert_equal expected, Post.left_joins(:comments).group(:post_id).distinct.select(:author_id).count
assert_equal expected, Post.left_joins(:comments).group(:post_id).count("DISTINCT posts.author_id")
assert_equal expected, Post.left_joins(:comments).group(:post_id).select("DISTINCT posts.author_id").count
expected = { nil => 6, 1 => 1, 2 => 1, 4 => 1, 5 => 1, 7 => 1 }
assert_equal expected, Post.left_joins(:comments).group(:post_id).distinct.count(:all)
assert_equal expected, Post.left_joins(:comments).group(:post_id).distinct.select(:author_id).count(:all)
end
def test_distinct_count_with_group_by_and_order_and_limit
assert_equal({ 6 => 2 }, Account.group(:firm_id).distinct.order("1 DESC").limit(1).count)
end
def test_should_group_by_summed_field_having_condition
c = Account.group(:firm_id).having("sum(credit_limit) > 50").sum(:credit_limit)
assert_nil c[1]
assert_equal 105, c[6]
assert_equal 60, c[2]
end
def test_should_group_by_summed_field_having_condition_from_select
skip unless current_adapter?(:Mysql2Adapter, :SQLite3Adapter)
c = Account.select("MIN(credit_limit) AS min_credit_limit").group(:firm_id).having("min_credit_limit > 50").sum(:credit_limit)
assert_nil c[1]
assert_equal 60, c[2]
assert_equal 53, c[9]
end
def test_should_group_by_summed_association
c = Account.group(:firm).sum(:credit_limit)
assert_equal 50, c[companies(:first_firm)]
assert_equal 105, c[companies(:rails_core)]
assert_equal 60, c[companies(:first_client)]
end
def test_should_sum_field_with_conditions
assert_equal 105, Account.where("firm_id = 6").sum(:credit_limit)
end
def test_should_return_zero_if_sum_conditions_return_nothing
assert_equal 0, Account.where("1 = 2").sum(:credit_limit)
assert_equal 0, companies(:rails_core).companies.where("1 = 2").sum(:id)
end
def test_sum_should_return_valid_values_for_decimals
NumericData.create(bank_balance: 19.83)
assert_equal 19.83, NumericData.sum(:bank_balance)
end
def test_should_return_type_casted_values_with_group_and_expression
assert_equal 0.5, Account.group(:firm_name).sum("0.01 * credit_limit")["37signals"]
end
def test_should_group_by_summed_field_with_conditions
c = Account.where("firm_id > 1").group(:firm_id).sum(:credit_limit)
assert_nil c[1]
assert_equal 105, c[6]
assert_equal 60, c[2]
end
def test_should_group_by_summed_field_with_conditions_and_having
c = Account.where("firm_id > 1").group(:firm_id).
having("sum(credit_limit) > 60").sum(:credit_limit)
assert_nil c[1]
assert_equal 105, c[6]
assert_nil c[2]
end
def test_should_group_by_fields_with_table_alias
c = Account.group("accounts.firm_id").sum(:credit_limit)
assert_equal 50, c[1]
assert_equal 105, c[6]
assert_equal 60, c[2]
end
def test_should_calculate_grouped_with_longer_field
field = "a" * Account.connection.max_identifier_length
Account.update_all("#{field} = credit_limit")
c = Account.group(:firm_id).sum(field)
assert_equal 50, c[1]
assert_equal 105, c[6]
assert_equal 60, c[2]
end
def test_should_calculate_with_invalid_field
assert_equal 6, Account.calculate(:count, "*")
assert_equal 6, Account.calculate(:count, :all)
end
def test_should_calculate_grouped_with_invalid_field
c = Account.group("accounts.firm_id").count(:all)
assert_equal 1, c[1]
assert_equal 2, c[6]
assert_equal 1, c[2]
end
def test_should_calculate_grouped_association_with_invalid_field
c = Account.group(:firm).count(:all)
assert_equal 1, c[companies(:first_firm)]
assert_equal 2, c[companies(:rails_core)]
assert_equal 1, c[companies(:first_client)]
end
def test_should_group_by_association_with_non_numeric_foreign_key
Speedometer.create! id: "ABC"
Minivan.create! id: "OMG", speedometer_id: "ABC"
c = Minivan.group(:speedometer).count(:all)
first_key = c.keys.first
assert_equal Speedometer, first_key.class
assert_equal 1, c[first_key]
end
def test_should_calculate_grouped_association_with_foreign_key_option
Account.belongs_to :another_firm, class_name: "Firm", foreign_key: "firm_id"
c = Account.group(:another_firm).count(:all)
assert_equal 1, c[companies(:first_firm)]
assert_equal 2, c[companies(:rails_core)]
assert_equal 1, c[companies(:first_client)]
end
def test_should_calculate_grouped_by_function
c = Company.group("UPPER(#{QUOTED_TYPE})").count(:all)
assert_equal 2, c[nil]
assert_equal 1, c["DEPENDENTFIRM"]
assert_equal 5, c["CLIENT"]
assert_equal 3, c["FIRM"]
end
def test_should_calculate_grouped_by_function_with_table_alias
c = Company.group("UPPER(companies.#{QUOTED_TYPE})").count(:all)
assert_equal 2, c[nil]
assert_equal 1, c["DEPENDENTFIRM"]
assert_equal 5, c["CLIENT"]
assert_equal 3, c["FIRM"]
end
def test_should_not_overshadow_enumerable_sum
some_companies = companies(:rails_core).companies.order(:id)
assert_equal 6, [1, 2, 3].sum(&:abs)
assert_equal 15, some_companies.sum(&:id)
assert_equal 25, some_companies.sum(10, &:id)
assert_deprecated do
assert_equal "LeetsoftJadedpixel", some_companies.sum(&:name)
end
assert_equal "companies: LeetsoftJadedpixel", some_companies.sum("companies: ", &:name)
end
def test_should_sum_scoped_field
assert_equal 15, companies(:rails_core).companies.sum(:id)
end
def test_should_sum_scoped_field_with_from
assert_equal Club.count, Organization.clubs.count
end
def test_should_sum_scoped_field_with_conditions
assert_equal 8, companies(:rails_core).companies.where("id > 7").sum(:id)
end
def test_should_group_by_scoped_field
c = companies(:rails_core).companies.group(:name).sum(:id)
assert_equal 7, c["Leetsoft"]
assert_equal 8, c["Jadedpixel"]
end
def test_should_group_by_summed_field_through_association_and_having
c = companies(:rails_core).companies.group(:name).having("sum(id) > 7").sum(:id)
assert_nil c["Leetsoft"]
assert_equal 8, c["Jadedpixel"]
end
def test_should_count_selected_field_with_include
assert_equal 6, Account.includes(:firm).distinct.count
assert_equal 4, Account.includes(:firm).distinct.select(:credit_limit).count
assert_equal 4, Account.includes(:firm).distinct.count("DISTINCT credit_limit")
assert_equal 4, Account.includes(:firm).distinct.count("DISTINCT(credit_limit)")
end
def test_should_not_perform_joined_include_by_default
assert_equal Account.count, Account.includes(:firm).count
queries = capture_sql { Account.includes(:firm).count }
assert_no_match(/join/i, queries.last)
end
def test_should_perform_joined_include_when_referencing_included_tables
joined_count = Account.includes(:firm).where(companies: { name: "37signals" }).count
assert_equal 1, joined_count
end
def test_should_count_scoped_select
Account.update_all("credit_limit = NULL")
assert_equal 0, Account.select("credit_limit").count
end
def test_should_count_scoped_select_with_options
Account.update_all("credit_limit = NULL")
Account.last.update_columns("credit_limit" => 49)
Account.first.update_columns("credit_limit" => 51)
assert_equal 1, Account.select("credit_limit").where("credit_limit >= 50").count
end
def test_should_count_manual_select_with_include
assert_equal 6, Account.select("DISTINCT accounts.id").includes(:firm).count
end
def test_should_count_manual_select_with_count_all
assert_equal 5, Account.select("DISTINCT accounts.firm_id").count(:all)
end
def test_should_count_with_manual_distinct_select_and_distinct
assert_equal 4, Account.select("DISTINCT accounts.firm_id").distinct(true).count
end
def test_should_count_manual_select_with_group_with_count_all
expected = { nil => 1, 1 => 1, 2 => 1, 6 => 2, 9 => 1 }
actual = Account.select("DISTINCT accounts.firm_id").group("accounts.firm_id").count(:all)
assert_equal expected, actual
end
def test_should_count_manual_with_count_all
assert_equal 6, Account.count(:all)
end
def test_count_selected_arel_attribute
assert_equal 5, Account.select(Account.arel_table[:firm_id]).count
assert_equal 4, Account.distinct.select(Account.arel_table[:firm_id]).count
end
def test_count_with_column_parameter
assert_equal 5, Account.count(:firm_id)
end
def test_count_with_arel_attribute
assert_equal 5, Account.count(Account.arel_table[:firm_id])
end
def test_count_with_arel_star
assert_equal 6, Account.count(Arel.star)
end
def test_count_with_distinct
assert_equal 4, Account.select(:credit_limit).distinct.count
end
def test_count_with_aliased_attribute
assert_equal 6, Account.count(:available_credit)
end
def test_count_with_column_and_options_parameter
assert_equal 2, Account.where("credit_limit = 50 AND firm_id IS NOT NULL").count(:firm_id)
end
def test_should_count_field_in_joined_table
assert_equal 5, Account.joins(:firm).count("companies.id")
assert_equal 4, Account.joins(:firm).distinct.count("companies.id")
end
def test_count_arel_attribute_in_joined_table_with
assert_equal 5, Account.joins(:firm).count(Company.arel_table[:id])
assert_equal 4, Account.joins(:firm).distinct.count(Company.arel_table[:id])
end
def test_count_selected_arel_attribute_in_joined_table
assert_equal 5, Account.joins(:firm).select(Company.arel_table[:id]).count
assert_equal 4, Account.joins(:firm).distinct.select(Company.arel_table[:id]).count
end
def test_should_count_field_in_joined_table_with_group_by
c = Account.group("accounts.firm_id").joins(:firm).count("companies.id")
[1, 6, 2, 9].each { |firm_id| assert_includes c.keys, firm_id }
end
def test_should_count_field_of_root_table_with_conflicting_group_by_column
expected = { 1 => 2, 2 => 1, 4 => 5, 5 => 3, 7 => 1 }
assert_equal expected, Post.joins(:comments).group(:post_id).count
assert_equal expected, Post.joins(:comments).group("comments.post_id").count
assert_equal expected, Post.joins(:comments).group(:post_id).select("DISTINCT posts.author_id").count(:all)
end
def test_count_with_no_parameters_isnt_deprecated
assert_not_deprecated { Account.count }
end
def test_count_with_too_many_parameters_raises
assert_raise(ArgumentError) { Account.count(1, 2, 3) }
end
def test_count_with_order
assert_equal 6, Account.order(:credit_limit).count
end
def test_count_with_reverse_order
assert_equal 6, Account.order(:credit_limit).reverse_order.count
end
def test_count_with_where_and_order
assert_equal 1, Account.where(firm_name: "37signals").count
assert_equal 1, Account.where(firm_name: "37signals").order(:firm_name).count
assert_equal 1, Account.where(firm_name: "37signals").order(:firm_name).reverse_order.count
end
def test_count_with_block
assert_equal 4, Account.count { |account| account.credit_limit.modulo(10).zero? }
end
def test_should_sum_expression
assert_equal 636, Account.sum("2 * credit_limit")
end
def test_sum_expression_returns_zero_when_no_records_to_sum
assert_equal 0, Account.where("1 = 2").sum("2 * credit_limit")
end
def test_count_with_from_option
assert_equal Company.count(:all), Company.from("companies").count(:all)
assert_equal Account.where("credit_limit = 50").count(:all),
Account.from("accounts").where("credit_limit = 50").count(:all)
assert_equal Company.where(type: "Firm").count(:type),
Company.where(type: "Firm").from("companies").count(:type)
end
def test_sum_with_from_option
assert_equal Account.sum(:credit_limit), Account.from("accounts").sum(:credit_limit)
assert_equal Account.where("credit_limit > 50").sum(:credit_limit),
Account.where("credit_limit > 50").from("accounts").sum(:credit_limit)
end
def test_average_with_from_option
assert_equal Account.average(:credit_limit), Account.from("accounts").average(:credit_limit)
assert_equal Account.where("credit_limit > 50").average(:credit_limit),
Account.where("credit_limit > 50").from("accounts").average(:credit_limit)
end
def test_minimum_with_from_option
assert_equal Account.minimum(:credit_limit), Account.from("accounts").minimum(:credit_limit)
assert_equal Account.where("credit_limit > 50").minimum(:credit_limit),
Account.where("credit_limit > 50").from("accounts").minimum(:credit_limit)
end
def test_maximum_with_from_option
assert_equal Account.maximum(:credit_limit), Account.from("accounts").maximum(:credit_limit)
assert_equal Account.where("credit_limit > 50").maximum(:credit_limit),
Account.where("credit_limit > 50").from("accounts").maximum(:credit_limit)
end
def test_maximum_with_not_auto_table_name_prefix_if_column_included
Company.create!(name: "test", contracts: [Contract.new(developer_id: 7)])
assert_equal 7, Company.includes(:contracts).maximum(:developer_id)
end
def test_minimum_with_not_auto_table_name_prefix_if_column_included
Company.create!(name: "test", contracts: [Contract.new(developer_id: 7)])
assert_equal 7, Company.includes(:contracts).minimum(:developer_id)
end
def test_sum_with_not_auto_table_name_prefix_if_column_included
Company.create!(name: "test", contracts: [Contract.new(developer_id: 7)])
assert_equal 7, Company.includes(:contracts).sum(:developer_id)
end
def test_from_option_with_specified_index
edges = Edge.from("edges /*! USE INDEX(unique_edge_index) */")
assert_equal Edge.count(:all), edges.count(:all)
assert_equal Edge.where("sink_id < 5").count(:all), edges.where("sink_id < 5").count(:all)
end
def test_from_option_with_table_different_than_class
assert_equal Account.count(:all), Company.from("accounts").count(:all)
end
def test_distinct_is_honored_when_used_with_count_operation_after_group
# Count the number of authors for approved topics
approved_topics_count = Topic.group(:approved).count(:author_name)[true]
assert_equal approved_topics_count, 4
# Count the number of distinct authors for approved Topics
distinct_authors_for_approved_count = Topic.group(:approved).distinct.count(:author_name)[true]
assert_equal distinct_authors_for_approved_count, 3
end
def test_pluck
assert_equal [1, 2, 3, 4, 5], Topic.order(:id).pluck(:id)
end
def test_pluck_with_empty_in
assert_queries(0) do
assert_equal [], Topic.where(id: []).pluck(:id)
end
end
def test_pluck_without_column_names
if current_adapter?(:OracleAdapter)
assert_equal [[1, "Firm", 1, nil, "37signals", nil, 1, nil, nil]], Company.order(:id).limit(1).pluck
else
assert_equal [[1, "Firm", 1, nil, "37signals", nil, 1, nil, ""]], Company.order(:id).limit(1).pluck
end
end
def test_pluck_type_cast
topic = topics(:first)
relation = Topic.where(id: topic.id)
assert_equal [ topic.approved ], relation.pluck(:approved)
assert_equal [ topic.last_read ], relation.pluck(:last_read)
assert_equal [ topic.written_on ], relation.pluck(:written_on)
end
def test_pluck_type_cast_with_conflict_column_names
expected = [
[Date.new(2004, 4, 15), "unread"],
[Date.new(2004, 4, 15), "reading"],
[Date.new(2004, 4, 15), "read"],
]
actual = AuthorAddress.joins(author: [:topics, :books]).order(:"books.last_read")
.where("books.last_read": [:unread, :reading, :read])
.pluck(:"topics.last_read", :"books.last_read")
assert_equal expected, actual
end
def test_pluck_type_cast_with_joins_without_table_name_qualified_column
assert_pluck_type_cast_without_table_name_qualified_column(AuthorAddress.joins(author: :books))
end
def test_pluck_type_cast_with_left_joins_without_table_name_qualified_column
assert_pluck_type_cast_without_table_name_qualified_column(AuthorAddress.left_joins(author: :books))
end
def test_pluck_type_cast_with_eager_load_without_table_name_qualified_column
assert_pluck_type_cast_without_table_name_qualified_column(AuthorAddress.eager_load(author: :books))
end
def assert_pluck_type_cast_without_table_name_qualified_column(author_addresses)
expected = [
[nil, "unread"],
["ebook", "reading"],
["paperback", "read"],
]
actual = author_addresses.order(:last_read)
.where("books.last_read": [:unread, :reading, :read])
.pluck(:format, :last_read)
assert_equal expected, actual
end
private :assert_pluck_type_cast_without_table_name_qualified_column
def test_pluck_with_type_cast_does_not_corrupt_the_query_cache
topic = topics(:first)
relation = Topic.where(id: topic.id)
assert_queries 1 do
Topic.cache do
kind = relation.select(:written_on).load.first.read_attribute_before_type_cast(:written_on).class
relation.pluck(:written_on)
assert_kind_of kind, relation.select(:written_on).load.first.read_attribute_before_type_cast(:written_on)
end
end
end
def test_pluck_and_distinct
assert_equal [50, 53, 55, 60], Account.order(:credit_limit).distinct.pluck(:credit_limit)
end
def test_pluck_in_relation
company = Company.first
contract = company.contracts.create!
assert_equal [contract.id], company.contracts.pluck(:id)
end
def test_pluck_on_aliased_attribute
assert_equal "The First Topic", Topic.order(:id).pluck(:heading).first
end
def test_pluck_with_serialization
t = Topic.create!(content: { foo: :bar })
assert_equal [{ foo: :bar }], Topic.where(id: t.id).pluck(:content)
end
def test_pluck_with_qualified_column_name
assert_equal [1, 2, 3, 4, 5], Topic.order(:id).pluck("topics.id")
end
def test_pluck_auto_table_name_prefix
c = Company.create!(name: "test", contracts: [Contract.new])
assert_equal [c.id], Company.joins(:contracts).pluck(:id)
end
def test_pluck_if_table_included
c = Company.create!(name: "test", contracts: [Contract.new(developer_id: 7)])
assert_equal [c.id], Company.includes(:contracts).where("contracts.id" => c.contracts.first).pluck(:id)
end
def test_pluck_not_auto_table_name_prefix_if_column_joined
company = Company.create!(name: "test", contracts: [Contract.new(developer_id: 7)])
metadata = company.contracts.first.metadata
assert_equal [metadata], Company.joins(:contracts).pluck(:metadata)
end
def test_pluck_with_selection_clause
assert_equal [50, 53, 55, 60], Account.pluck(Arel.sql("DISTINCT credit_limit")).sort
assert_equal [50, 53, 55, 60], Account.pluck(Arel.sql("DISTINCT accounts.credit_limit")).sort
assert_equal [50, 53, 55, 60], Account.pluck(Arel.sql("DISTINCT(credit_limit)")).sort
assert_equal [50 + 53 + 55 + 60], Account.pluck(Arel.sql("SUM(DISTINCT(credit_limit))"))
end
def test_plucks_with_ids
assert_equal Company.all.map(&:id).sort, Company.ids.sort
end
def test_pluck_with_includes_limit_and_empty_result
assert_equal [], Topic.includes(:replies).limit(0).pluck(:id)
assert_equal [], Topic.includes(:replies).limit(1).where("0 = 1").pluck(:id)
end
def test_pluck_with_includes_offset
assert_equal [5], Topic.includes(:replies).order(:id).offset(4).pluck(:id)
assert_equal [], Topic.includes(:replies).order(:id).offset(5).pluck(:id)
end
def test_pluck_with_join
assert_equal [[2, 2], [4, 4]], Reply.includes(:topic).order(:id).pluck(:id, :"topics.id")
end
def test_group_by_with_order_by_virtual_count_attribute
expected = { "SpecialPost" => 1, "StiPost" => 2 }
actual = Post.group(:type).order(:count).limit(2).maximum(:comments_count)
assert_equal expected, actual
end if current_adapter?(:PostgreSQLAdapter)
def test_group_by_with_limit
expected = { "StiPost" => 3, "SpecialPost" => 1 }
actual = Post.includes(:comments).group(:type).order(type: :desc).limit(2).count("comments.id")
assert_equal expected, actual
end
def test_group_by_with_offset
expected = { "SpecialPost" => 1, "Post" => 8 }
actual = Post.includes(:comments).group(:type).order(type: :desc).offset(1).count("comments.id")
assert_equal expected, actual
end
def test_group_by_with_limit_and_offset
expected = { "SpecialPost" => 1 }
actual = Post.includes(:comments).group(:type).order(type: :desc).offset(1).limit(1).count("comments.id")
assert_equal expected, actual
end
def test_group_by_with_quoted_count_and_order_by_alias
quoted_posts_id = Post.connection.quote_table_name("posts.id")
expected = { "SpecialPost" => 1, "StiPost" => 1, "Post" => 9 }
actual = Post.group(:type).order("count_posts_id").count(quoted_posts_id)
assert_equal expected, actual
end
def test_pluck_not_auto_table_name_prefix_if_column_included
Company.create!(name: "test", contracts: [Contract.new(developer_id: 7)])
ids = Company.includes(:contracts).pluck(:developer_id)
assert_equal Company.count, ids.length
assert_equal [7], ids.compact
end
def test_pluck_multiple_columns
assert_equal [
[1, "The First Topic"], [2, "The Second Topic of the day"],
[3, "The Third Topic of the day"], [4, "The Fourth Topic of the day"],
[5, "The Fifth Topic of the day"]
], Topic.order(:id).pluck(:id, :title)
assert_equal [
[1, "The First Topic", "David"], [2, "The Second Topic of the day", "Mary"],
[3, "The Third Topic of the day", "Carl"], [4, "The Fourth Topic of the day", "Carl"],
[5, "The Fifth Topic of the day", "Jason"]
], Topic.order(:id).pluck(:id, :title, :author_name)
end
def test_pluck_with_multiple_columns_and_selection_clause
assert_equal [[1, 50], [2, 50], [3, 50], [4, 60], [5, 55], [6, 53]],
Account.order(:id).pluck("id, credit_limit")
end
def test_pluck_with_line_endings
assert_equal [[1, 50], [2, 50], [3, 50], [4, 60], [5, 55], [6, 53]],
Account.order(:id).pluck("id, credit_limit\n")
end
def test_pluck_with_multiple_columns_and_includes
Company.create!(name: "test", contracts: [Contract.new(developer_id: 7)])
companies_and_developers = Company.order("companies.id").includes(:contracts).pluck(:name, :developer_id)
assert_equal Company.count, companies_and_developers.length
assert_equal ["37signals", nil], companies_and_developers.first
assert_equal ["test", 7], companies_and_developers.last
end
def test_pluck_with_reserved_words
Possession.create!(where: "Over There")
assert_equal ["Over There"], Possession.pluck(:where)
end
def test_pluck_replaces_select_clause
takes_relation = Topic.select(:approved, :id).order(:id)
assert_equal [1, 2, 3, 4, 5], takes_relation.pluck(:id)
assert_equal [false, true, true, true, true], takes_relation.pluck(:approved)
end
def test_pluck_columns_with_same_name
expected = [["The First Topic", "The Second Topic of the day"], ["The Third Topic of the day", "The Fourth Topic of the day"]]
actual = Topic.joins(:replies).order(:id)
.pluck("topics.title", "replies_topics.title")
assert_equal expected, actual
end
def test_pluck_functions_with_alias
assert_equal [
[1, "The First Topic"], [2, "The Second Topic of the day"],
[3, "The Third Topic of the day"], [4, "The Fourth Topic of the day"],
[5, "The Fifth Topic of the day"]
], Topic.order(:id).pluck(
Arel.sql("COALESCE(id, 0) id"),
Arel.sql("COALESCE(title, 'untitled') title")
)
end
def test_pluck_functions_without_alias
assert_equal [
[1, "The First Topic"], [2, "The Second Topic of the day"],
[3, "The Third Topic of the day"], [4, "The Fourth Topic of the day"],
[5, "The Fifth Topic of the day"]
], Topic.order(:id).pluck(
Arel.sql("COALESCE(id, 0)"),
Arel.sql("COALESCE(title, 'untitled')")
)
end
def test_calculation_with_polymorphic_relation
part = ShipPart.create!(name: "has trinket")
part.trinkets.create!
assert_equal part.id, ShipPart.joins(:trinkets).sum(:id)
end
def test_pluck_joined_with_polymorphic_relation
part = ShipPart.create!(name: "has trinket")
part.trinkets.create!
assert_equal [part.id], ShipPart.joins(:trinkets).pluck(:id)
end
def test_pluck_loaded_relation
companies = Company.order(:id).limit(3).load
assert_queries(0) do
assert_equal ["37signals", "Summit", "Microsoft"], companies.pluck(:name)
end
end
def test_pluck_loaded_relation_multiple_columns
companies = Company.order(:id).limit(3).load
assert_queries(0) do
assert_equal [[1, "37signals"], [2, "Summit"], [3, "Microsoft"]], companies.pluck(:id, :name)
end
end
def test_pluck_loaded_relation_sql_fragment
companies = Company.order(:name).limit(3).load
assert_queries(1) do
assert_equal ["37signals", "Apex", "Ex Nihilo"], companies.pluck(Arel.sql("DISTINCT name"))
end
end
def test_pluck_loaded_relation_aliased_attribute
companies = Company.order(:id).limit(3).load
assert_queries(0) do
assert_equal ["37signals", "Summit", "Microsoft"], companies.pluck(:new_name)
end
end
def test_pick_one
assert_equal "The First Topic", Topic.order(:id).pick(:heading)
assert_no_queries do
assert_nil Topic.none.pick(:heading)
assert_nil Topic.where(id: 9999999999999999999).pick(:heading)
end
end
def test_pick_two
assert_equal ["David", "david@loudthinking.com"], Topic.order(:id).pick(:author_name, :author_email_address)
assert_no_queries do
assert_nil Topic.none.pick(:author_name, :author_email_address)
assert_nil Topic.where(id: 9999999999999999999).pick(:author_name, :author_email_address)
end
end
def test_pick_delegate_to_all
cool_first = minivans(:cool_first)
assert_equal cool_first.color, Minivan.pick(:color)
end
def test_pick_loaded_relation
companies = Company.order(:id).limit(3).load
assert_no_queries do
assert_equal "37signals", companies.pick(:name)
end
end
def test_pick_loaded_relation_multiple_columns
companies = Company.order(:id).limit(3).load
assert_no_queries do
assert_equal [1, "37signals"], companies.pick(:id, :name)
end
end
def test_pick_loaded_relation_sql_fragment
companies = Company.order(:name).limit(3).load
assert_queries 1 do
assert_equal "37signals", companies.pick(Arel.sql("DISTINCT name"))
end
end
def test_pick_loaded_relation_aliased_attribute
companies = Company.order(:id).limit(3).load
assert_no_queries do
assert_equal "37signals", companies.pick(:new_name)
end
end
def test_grouped_calculation_with_polymorphic_relation
part = ShipPart.create!(name: "has trinket")
part.trinkets.create!
assert_equal({ "has trinket" => part.id }, ShipPart.joins(:trinkets).group("ship_parts.name").sum(:id))
end
def test_calculation_grouped_by_association_doesnt_error_when_no_records_have_association
Client.update_all(client_of: nil)
assert_equal({ nil => Client.count }, Client.group(:firm).count)
end
def test_should_reference_correct_aliases_while_joining_tables_of_has_many_through_association
assert_nothing_raised do
developer = Developer.create!(name: "developer")
developer.ratings.includes(comment: :post).where(posts: { id: 1 }).count
end
end
def test_sum_uses_enumerable_version_when_block_is_given
block_called = false
relation = Client.all.load
assert_no_queries do
assert_equal 0, relation.sum { block_called = true; 0 }
end
assert block_called
end
def test_having_with_strong_parameters
params = ProtectedParams.new(credit_limit: "50")
assert_raises(ActiveModel::ForbiddenAttributesError) do
Account.group(:id).having(params)
end
result = Account.group(:id).having(params.permit!)
assert_equal 50, result[0].credit_limit
assert_equal 50, result[1].credit_limit
assert_equal 50, result[2].credit_limit
end
def test_count_takes_attribute_type_precedence_over_database_type
assert_called(
Account.connection, :select_all,
returns: ActiveRecord::Result.new(["count"], [["10"]])
) do
result = Account.count
assert_equal 10, result
assert_instance_of Integer, result
end
end
def test_sum_takes_attribute_type_precedence_over_database_type
assert_called(
Account.connection, :select_all,
returns: ActiveRecord::Result.new(["sum"], [[10.to_d]])
) do
result = Account.sum(:credit_limit)
assert_equal 10, result
assert_instance_of Integer, result
end
end
def test_group_by_attribute_with_custom_type
assert_equal({ "proposed" => 2, "published" => 2 }, Book.group(:status).count)
end
def test_aggregate_attribute_on_enum_type
assert_equal 4, Book.sum(:status)
assert_equal 1, Book.sum(:difficulty)
assert_equal 0, Book.minimum(:difficulty)
assert_equal 1, Book.maximum(:difficulty)
assert_equal({ "proposed" => 0, "published" => 4 }, Book.group(:status).sum(:status))
assert_equal({ "proposed" => 0, "published" => 1 }, Book.group(:status).sum(:difficulty))
assert_equal({ "proposed" => 0, "published" => 0 }, Book.group(:status).minimum(:difficulty))
assert_equal({ "proposed" => 0, "published" => 1 }, Book.group(:status).maximum(:difficulty))
end
def test_minimum_and_maximum_on_non_numeric_type
assert_equal Date.new(2004, 4, 15), Topic.minimum(:last_read)
assert_equal Date.new(2004, 4, 15), Topic.maximum(:last_read)
assert_equal({ false => Date.new(2004, 4, 15), true => nil }, Topic.group(:approved).minimum(:last_read))
assert_equal({ false => Date.new(2004, 4, 15), true => nil }, Topic.group(:approved).maximum(:last_read))
end
def test_minimum_and_maximum_on_time_attributes
assert_minimum_and_maximum_on_time_attributes(Time)
end
def test_minimum_and_maximum_on_tz_aware_attributes
with_timezone_config aware_attributes: true, zone: "Pacific Time (US & Canada)" do
Topic.reset_column_information
assert_minimum_and_maximum_on_time_attributes(ActiveSupport::TimeWithZone)
end
ensure
Topic.reset_column_information
end
def assert_minimum_and_maximum_on_time_attributes(time_class)
skip unless supports_datetime_with_precision? # Remove once MySQL 5.5 support is dropped.
actual = Topic.minimum(:written_on)
assert_equal Time.utc(2003, 7, 16, 14, 28, 11, 223300), actual
assert_instance_of time_class, actual
actual = Topic.maximum(:written_on)
assert_equal Time.utc(2013, 7, 13, 11, 11, 0, 9900), actual
assert_instance_of time_class, actual
expected = {
false => Time.utc(2003, 7, 16, 14, 28, 11, 223300),
true => Time.utc(2004, 7, 15, 14, 28, 0, 9900),
}
actual = Topic.group(:approved).minimum(:written_on)
assert_equal expected, actual
assert_instance_of time_class, actual[true]
assert_instance_of time_class, actual[true]
expected = {
false => Time.utc(2003, 7, 16, 14, 28, 11, 223300),
true => Time.utc(2013, 7, 13, 11, 11, 0, 9900),
}
actual = Topic.group(:approved).maximum(:written_on)
assert_equal expected, actual
assert_instance_of time_class, actual[true]
assert_instance_of time_class, actual[true]
assert_minimum_and_maximum_on_time_attributes_joins_with_column(time_class, :"topics.written_on")
assert_minimum_and_maximum_on_time_attributes_joins_with_column(time_class, :written_on)
end
private :assert_minimum_and_maximum_on_time_attributes
def assert_minimum_and_maximum_on_time_attributes_joins_with_column(time_class, column)
actual = Author.joins(:topics).maximum(column)
assert_equal Time.utc(2004, 7, 15, 14, 28, 0, 9900), actual
assert_instance_of time_class, actual
actual = Author.joins(:topics).minimum(column)
assert_equal Time.utc(2003, 7, 16, 14, 28, 11, 223300), actual
assert_instance_of time_class, actual
expected = {
1 => Time.utc(2003, 7, 16, 14, 28, 11, 223300),
2 => Time.utc(2004, 7, 15, 14, 28, 0, 9900),
}
actual = Author.joins(:topics).group(:id).maximum(column)
assert_equal expected, actual
assert_instance_of time_class, actual[1]
assert_instance_of time_class, actual[2]
actual = Author.joins(:topics).group(:id).minimum(column)
assert_equal expected, actual
assert_instance_of time_class, actual[1]
assert_instance_of time_class, actual[2]
end
private :assert_minimum_and_maximum_on_time_attributes_joins_with_column
def test_select_avg_with_group_by_as_virtual_attribute_with_sql
rails_core = companies(:rails_core)
sql = <<~SQL
SELECT firm_id, AVG(credit_limit) AS avg_credit_limit
FROM accounts
WHERE firm_id = ?
GROUP BY firm_id
LIMIT 1
SQL
account = Account.find_by_sql([sql, rails_core]).first
# id was not selected, so it should be nil
# (cannot select id because it wasn't used in the GROUP BY clause)
assert_nil account.id
# firm_id was explicitly selected, so it should be present
assert_equal(rails_core, account.firm)
# avg_credit_limit should be present as a virtual attribute
assert_equal(52.5, account.avg_credit_limit)
end
def test_select_avg_with_group_by_as_virtual_attribute_with_ar
rails_core = companies(:rails_core)
account = Account
.select(:firm_id, "AVG(credit_limit) AS avg_credit_limit")
.where(firm: rails_core)
.group(:firm_id)
.take!
# id was not selected, so it should be nil
# (cannot select id because it wasn't used in the GROUP BY clause)
assert_nil account.id
# firm_id was explicitly selected, so it should be present
assert_equal(rails_core, account.firm)
# avg_credit_limit should be present as a virtual attribute
assert_equal(52.5, account.avg_credit_limit)
end
def test_select_avg_with_joins_and_group_by_as_virtual_attribute_with_sql
rails_core = companies(:rails_core)
sql = <<~SQL
SELECT companies.*, AVG(accounts.credit_limit) AS avg_credit_limit
FROM companies
INNER JOIN accounts ON companies.id = accounts.firm_id
WHERE companies.id = ?
GROUP BY companies.id
LIMIT 1
SQL
firm = DependentFirm.find_by_sql([sql, rails_core]).first
# all the DependentFirm attributes should be present
assert_equal rails_core, firm
assert_equal rails_core.name, firm.name
# avg_credit_limit should be present as a virtual attribute
assert_equal(52.5, firm.avg_credit_limit)
end
def test_select_avg_with_joins_and_group_by_as_virtual_attribute_with_ar
rails_core = companies(:rails_core)
firm = DependentFirm
.select("companies.*", "AVG(accounts.credit_limit) AS avg_credit_limit")
.where(id: rails_core)
.joins(:account)
.group(:id)
.take!
# all the DependentFirm attributes should be present
assert_equal rails_core, firm
assert_equal rails_core.name, firm.name
# avg_credit_limit should be present as a virtual attribute
assert_equal(52.5, firm.avg_credit_limit)
end
def test_count_with_block_and_column_name_raises_an_error
assert_raises(ArgumentError) do
Account.count(:firm_id) { true }
end
end
test "#skip_query_cache! for #pluck" do
Account.cache do
assert_queries(1) do
Account.pluck(:credit_limit)
Account.pluck(:credit_limit)
end
assert_queries(2) do
Account.all.skip_query_cache!.pluck(:credit_limit)
Account.all.skip_query_cache!.pluck(:credit_limit)
end
end
end
test "#skip_query_cache! for a simple calculation" do
Account.cache do
assert_queries(1) do
Account.calculate(:sum, :credit_limit)
Account.calculate(:sum, :credit_limit)
end
assert_queries(2) do
Account.all.skip_query_cache!.calculate(:sum, :credit_limit)
Account.all.skip_query_cache!.calculate(:sum, :credit_limit)
end
end
end
test "#skip_query_cache! for a grouped calculation" do
Account.cache do
assert_queries(1) do
Account.group(:firm_id).calculate(:sum, :credit_limit)
Account.group(:firm_id).calculate(:sum, :credit_limit)
end
assert_queries(2) do
Account.all.skip_query_cache!.group(:firm_id).calculate(:sum, :credit_limit)
Account.all.skip_query_cache!.group(:firm_id).calculate(:sum, :credit_limit)
end
end
end
test "group alias is properly quoted" do
assert_nothing_raised do
NeedQuoting.group(:name).count
end
end
end
# frozen_string_literal: true
module ActiveRecord
# = Active Record \Callbacks
#
# \Callbacks are hooks into the life cycle of an Active Record object that allow you to trigger logic
# before or after a change in the object state. This can be used to make sure that associated and
# dependent objects are deleted when {ActiveRecord::Base#destroy}[rdoc-ref:Persistence#destroy] is called (by overwriting +before_destroy+) or
# to massage attributes before they're validated (by overwriting +before_validation+).
# As an example of the callbacks initiated, consider the {ActiveRecord::Base#save}[rdoc-ref:Persistence#save] call for a new record:
#
# * (-) <tt>save</tt>
# * (-) <tt>valid</tt>
# * (1) <tt>before_validation</tt>
# * (-) <tt>validate</tt>
# * (2) <tt>after_validation</tt>
# * (3) <tt>before_save</tt>
# * (4) <tt>before_create</tt>
# * (-) <tt>create</tt>
# * (5) <tt>after_create</tt>
# * (6) <tt>after_save</tt>
# * (7) <tt>after_commit</tt>
#
# Also, an <tt>after_rollback</tt> callback can be configured to be triggered whenever a rollback is issued.
# Check out ActiveRecord::Transactions for more details about <tt>after_commit</tt> and
# <tt>after_rollback</tt>.
#
# Additionally, an <tt>after_touch</tt> callback is triggered whenever an
# object is touched.
#
# Lastly an <tt>after_find</tt> and <tt>after_initialize</tt> callback is triggered for each object that
# is found and instantiated by a finder, with <tt>after_initialize</tt> being triggered after new objects
# are instantiated as well.
#
# There are nineteen callbacks in total, which give a lot of control over how to react and prepare for each state in the
# Active Record life cycle. The sequence for calling {ActiveRecord::Base#save}[rdoc-ref:Persistence#save] for an existing record is similar,
# except that each <tt>_create</tt> callback is replaced by the corresponding <tt>_update</tt> callback.
#
# Examples:
# class CreditCard < ActiveRecord::Base
# # Strip everything but digits, so the user can specify "555 234 34" or
# # "5552-3434" and both will mean "55523434"
# before_validation(on: :create) do
# self.number = number.gsub(/[^0-9]/, "") if attribute_present?("number")
# end
# end
#
# class Subscription < ActiveRecord::Base
# before_create :record_signup
#
# private
# def record_signup
# self.signed_up_on = Date.today
# end
# end
#
# class Firm < ActiveRecord::Base
# # Disables access to the system, for associated clients and people when the firm is destroyed
# before_destroy { |record| Person.where(firm_id: record.id).update_all(access: 'disabled') }
# before_destroy { |record| Client.where(client_of: record.id).update_all(access: 'disabled') }
# end
#
# == Inheritable callback queues
#
# Besides the overwritable callback methods, it's also possible to register callbacks through the
# use of the callback macros. Their main advantage is that the macros add behavior into a callback
# queue that is kept intact through an inheritance hierarchy.
#
# class Topic < ActiveRecord::Base
# before_destroy :destroy_author
# end
#
# class Reply < Topic
# before_destroy :destroy_readers
# end
#
# When <tt>Topic#destroy</tt> is run only +destroy_author+ is called. When <tt>Reply#destroy</tt> is
# run, both +destroy_author+ and +destroy_readers+ are called.
#
# *IMPORTANT:* In order for inheritance to work for the callback queues, you must specify the
# callbacks before specifying the associations. Otherwise, you might trigger the loading of a
# child before the parent has registered the callbacks and they won't be inherited.
#
# == Types of callbacks
#
# There are three types of callbacks accepted by the callback macros: method references (symbol), callback objects,
# inline methods (using a proc). Method references and callback objects are the recommended approaches,
# inline methods using a proc are sometimes appropriate (such as for creating mix-ins).
#
# The method reference callbacks work by specifying a protected or private method available in the object, like this:
#
# class Topic < ActiveRecord::Base
# before_destroy :delete_parents
#
# private
# def delete_parents
# self.class.delete_by(parent_id: id)
# end
# end
#
# The callback objects have methods named after the callback called with the record as the only parameter, such as:
#
# class BankAccount < ActiveRecord::Base
# before_save EncryptionWrapper.new
# after_save EncryptionWrapper.new
# after_initialize EncryptionWrapper.new
# end
#
# class EncryptionWrapper
# def before_save(record)
# record.credit_card_number = encrypt(record.credit_card_number)
# end
#
# def after_save(record)
# record.credit_card_number = decrypt(record.credit_card_number)
# end
#
# alias_method :after_initialize, :after_save
#
# private
# def encrypt(value)
# # Secrecy is committed
# end
#
# def decrypt(value)
# # Secrecy is unveiled
# end
# end
#
# So you specify the object you want to be messaged on a given callback. When that callback is triggered, the object has
# a method by the name of the callback messaged. You can make these callbacks more flexible by passing in other
# initialization data such as the name of the attribute to work with:
#
# class BankAccount < ActiveRecord::Base
# before_save EncryptionWrapper.new("credit_card_number")
# after_save EncryptionWrapper.new("credit_card_number")
# after_initialize EncryptionWrapper.new("credit_card_number")
# end
#
# class EncryptionWrapper
# def initialize(attribute)
# @attribute = attribute
# end
#
# def before_save(record)
# record.send("#{@attribute}=", encrypt(record.send("#{@attribute}")))
# end
#
# def after_save(record)
# record.send("#{@attribute}=", decrypt(record.send("#{@attribute}")))
# end
#
# alias_method :after_initialize, :after_save
#
# private
# def encrypt(value)
# # Secrecy is committed
# end
#
# def decrypt(value)
# # Secrecy is unveiled
# end
# end
#
# == <tt>before_validation*</tt> returning statements
#
# If the +before_validation+ callback throws +:abort+, the process will be
# aborted and {ActiveRecord::Base#save}[rdoc-ref:Persistence#save] will return +false+.
# If {ActiveRecord::Base#save!}[rdoc-ref:Persistence#save!] is called it will raise an ActiveRecord::RecordInvalid exception.
# Nothing will be appended to the errors object.
#
# == Canceling callbacks
#
# If a <tt>before_*</tt> callback throws +:abort+, all the later callbacks and
# the associated action are cancelled.
# Callbacks are generally run in the order they are defined, with the exception of callbacks defined as
# methods on the model, which are called last.
#
# == Ordering callbacks
#
# Sometimes application code requires that callbacks execute in a specific order. For example, a +before_destroy+
# callback (+log_children+ in this case) should be executed before records in the +children+ association are destroyed by the
# <tt>dependent: :destroy</tt> option.
#
# Let's look at the code below:
#
# class Topic < ActiveRecord::Base
# has_many :children, dependent: :destroy
#
# before_destroy :log_children
#
# private
# def log_children
# # Child processing
# end
# end
#
# In this case, the problem is that when the +before_destroy+ callback is executed, records in the +children+ association no
# longer exist because the {ActiveRecord::Base#destroy}[rdoc-ref:Persistence#destroy] callback was executed first.
# You can use the +prepend+ option on the +before_destroy+ callback to avoid this.
#
# class Topic < ActiveRecord::Base
# has_many :children, dependent: :destroy
#
# before_destroy :log_children, prepend: true
#
# private
# def log_children
# # Child processing
# end
# end
#
# This way, the +before_destroy+ is executed before the <tt>dependent: :destroy</tt> is called, and the data is still available.
#
# Also, there are cases when you want several callbacks of the same type to
# be executed in order.
#
# For example:
#
# class Topic < ActiveRecord::Base
# has_many :children
#
# after_save :log_children
# after_save :do_something_else
#
# private
#
# def log_children
# # Child processing
# end
#
# def do_something_else
# # Something else
# end
# end
#
# In this case the +log_children+ is executed before +do_something_else+.
# The same applies to all non-transactional callbacks.
#
# As seen below, in case there are multiple transactional callbacks the order
# is reversed.
#
# For example:
#
# class Topic < ActiveRecord::Base
# has_many :children
#
# after_commit :log_children
# after_commit :do_something_else
#
# private
#
# def log_children
# # Child processing
# end
#
# def do_something_else
# # Something else
# end
# end
#
# In this case the +do_something_else+ is executed before +log_children+.
#
# == \Transactions
#
# The entire callback chain of a {#save}[rdoc-ref:Persistence#save], {#save!}[rdoc-ref:Persistence#save!],
# or {#destroy}[rdoc-ref:Persistence#destroy] call runs within a transaction. That includes <tt>after_*</tt> hooks.
# If everything goes fine a +COMMIT+ is executed once the chain has been completed.
#
# If a <tt>before_*</tt> callback cancels the action a +ROLLBACK+ is issued. You
# can also trigger a +ROLLBACK+ raising an exception in any of the callbacks,
# including <tt>after_*</tt> hooks. Note, however, that in that case the client
# needs to be aware of it because an ordinary {#save}[rdoc-ref:Persistence#save] will raise such exception
# instead of quietly returning +false+.
#
# == Debugging callbacks
#
# The callback chain is accessible via the <tt>_*_callbacks</tt> method on an object. Active Model \Callbacks support
# <tt>:before</tt>, <tt>:after</tt> and <tt>:around</tt> as values for the <tt>kind</tt> property. The <tt>kind</tt> property
# defines what part of the chain the callback runs in.
#
# To find all callbacks in the +before_save+ callback chain:
#
# Topic._save_callbacks.select { |cb| cb.kind.eql?(:before) }
#
# Returns an array of callback objects that form the +before_save+ chain.
#
# To further check if the before_save chain contains a proc defined as <tt>rest_when_dead</tt> use the <tt>filter</tt> property of the callback object:
#
# Topic._save_callbacks.select { |cb| cb.kind.eql?(:before) }.collect(&:filter).include?(:rest_when_dead)
#
# Returns true or false depending on whether the proc is contained in the +before_save+ callback chain on a Topic model.
#
module Callbacks
extend ActiveSupport::Concern
CALLBACKS = [
:after_initialize, :after_find, :after_touch, :before_validation, :after_validation,
:before_save, :around_save, :after_save, :before_create, :around_create,
:after_create, :before_update, :around_update, :after_update,
:before_destroy, :around_destroy, :after_destroy, :after_commit, :after_rollback
]
module ClassMethods
include ActiveModel::Callbacks
##
# :method: after_initialize
#
# :call-seq: after_initialize(*args, &block)
#
# Registers a callback to be called after a record is instantiated. See
# ActiveRecord::Callbacks for more information.
##
# :method: after_find
#
# :call-seq: after_find(*args, &block)
#
# Registers a callback to be called after a record is instantiated
# via a finder. See ActiveRecord::Callbacks for more information.
##
# :method: after_touch
#
# :call-seq: after_touch(*args, &block)
#
# Registers a callback to be called after a record is touched. See
# ActiveRecord::Callbacks for more information.
##
# :method: before_save
#
# :call-seq: before_save(*args, &block)
#
# Registers a callback to be called before a record is saved. See
# ActiveRecord::Callbacks for more information.
##
# :method: around_save
#
# :call-seq: around_save(*args, &block)
#
# Registers a callback to be called around the save of a record. See
# ActiveRecord::Callbacks for more information.
##
# :method: after_save
#
# :call-seq: after_save(*args, &block)
#
# Registers a callback to be called after a record is saved. See
# ActiveRecord::Callbacks for more information.
##
# :method: before_create
#
# :call-seq: before_create(*args, &block)
#
# Registers a callback to be called before a record is created. See
# ActiveRecord::Callbacks for more information.
##
# :method: around_create
#
# :call-seq: around_create(*args, &block)
#
# Registers a callback to be called around the creation of a record. See
# ActiveRecord::Callbacks for more information.
##
# :method: after_create
#
# :call-seq: after_create(*args, &block)
#
# Registers a callback to be called after a record is created. See
# ActiveRecord::Callbacks for more information.
##
# :method: before_update
#
# :call-seq: before_update(*args, &block)
#
# Registers a callback to be called before a record is updated. See
# ActiveRecord::Callbacks for more information.
##
# :method: around_update
#
# :call-seq: around_update(*args, &block)
#
# Registers a callback to be called around the update of a record. See
# ActiveRecord::Callbacks for more information.
##
# :method: after_update
#
# :call-seq: after_update(*args, &block)
#
# Registers a callback to be called after a record is updated. See
# ActiveRecord::Callbacks for more information.
##
# :method: before_destroy
#
# :call-seq: before_destroy(*args, &block)
#
# Registers a callback to be called before a record is destroyed. See
# ActiveRecord::Callbacks for more information.
##
# :method: around_destroy
#
# :call-seq: around_destroy(*args, &block)
#
# Registers a callback to be called around the destruction of a record.
# See ActiveRecord::Callbacks for more information.
##
# :method: after_destroy
#
# :call-seq: after_destroy(*args, &block)
#
# Registers a callback to be called after a record is destroyed. See
# ActiveRecord::Callbacks for more information.
end
included do
include ActiveModel::Validations::Callbacks
define_model_callbacks :initialize, :find, :touch, only: :after
define_model_callbacks :save, :create, :update, :destroy
end
def destroy # :nodoc:
@_destroy_callback_already_called ||= false
return if @_destroy_callback_already_called
@_destroy_callback_already_called = true
_run_destroy_callbacks { super }
rescue RecordNotDestroyed => e
@_association_destroy_exception = e
false
ensure
@_destroy_callback_already_called = false
end
def touch(*, **) # :nodoc:
_run_touch_callbacks { super }
end
def increment!(attribute, by = 1, touch: nil) # :nodoc:
touch ? _run_touch_callbacks { super } : super
end
private
def create_or_update(**)
_run_save_callbacks { super }
end
def _create_record
_run_create_callbacks { super }
end
def _update_record
_run_update_callbacks { super }
end
end
end
# frozen_string_literal: true
require "cases/helper"
require "models/developer"
require "models/computer"
class CallbackDeveloper < ActiveRecord::Base
self.table_name = "developers"
class << self
def callback_proc(callback_method)
Proc.new { |model| model.history << [callback_method, :proc] }
end
def define_callback_method(callback_method)
define_method(callback_method) do
history << [callback_method, :method]
end
send(callback_method, :"#{callback_method}")
end
def callback_object(callback_method)
klass = Class.new
klass.define_method(callback_method) do |model|
model.history << [callback_method, :object]
end
klass.new
end
end
ActiveRecord::Callbacks::CALLBACKS.each do |callback_method|
next if callback_method.start_with?("around_")
define_callback_method(callback_method)
send(callback_method, callback_proc(callback_method))
send(callback_method, callback_object(callback_method))
send(callback_method) { |model| model.history << [callback_method, :block] }
end
def history
@history ||= []
end
end
class CallbackDeveloperWithHaltedValidation < CallbackDeveloper
before_validation proc { |model| model.history << [:before_validation, :throwing_abort]; throw(:abort) }
before_validation proc { |model| model.history << [:before_validation, :should_never_get_here] }
end
class ParentDeveloper < ActiveRecord::Base
self.table_name = "developers"
attr_accessor :after_save_called
before_validation { |record| record.after_save_called = true }
end
class ChildDeveloper < ParentDeveloper
end
class ImmutableDeveloper < ActiveRecord::Base
self.table_name = "developers"
validates_inclusion_of :salary, in: 50000..200000
before_save :cancel
before_destroy :cancel
private
def cancel
false
end
end
class DeveloperWithCanceledCallbacks < ActiveRecord::Base
self.table_name = "developers"
validates_inclusion_of :salary, in: 50000..200000
before_save :cancel
before_destroy :cancel
private
def cancel
throw(:abort)
end
end
class OnCallbacksDeveloper < ActiveRecord::Base
self.table_name = "developers"
before_validation { history << :before_validation }
before_validation(on: :create) { history << :before_validation_on_create }
before_validation(on: :update) { history << :before_validation_on_update }
validate do
history << :validate
end
after_validation { history << :after_validation }
after_validation(on: :create) { history << :after_validation_on_create }
after_validation(on: :update) { history << :after_validation_on_update }
def history
@history ||= []
end
end
class ContextualCallbacksDeveloper < ActiveRecord::Base
self.table_name = "developers"
before_validation { history << :before_validation }
before_validation :before_validation_on_create_and_update, on: [ :create, :update ]
validate do
history << :validate
end
after_validation { history << :after_validation }
after_validation :after_validation_on_create_and_update, on: [ :create, :update ]
def before_validation_on_create_and_update
history << "before_validation_on_#{validation_context}".to_sym
end
def after_validation_on_create_and_update
history << "after_validation_on_#{validation_context}".to_sym
end
def history
@history ||= []
end
end
class CallbackHaltedDeveloper < ActiveRecord::Base
self.table_name = "developers"
attr_reader :after_save_called, :after_create_called, :after_update_called, :after_destroy_called
attr_accessor :cancel_before_save, :cancel_before_create, :cancel_before_update, :cancel_before_destroy
before_save { throw(:abort) if defined?(@cancel_before_save) }
before_create { throw(:abort) if @cancel_before_create }
before_update { throw(:abort) if @cancel_before_update }
before_destroy { throw(:abort) if @cancel_before_destroy }
after_save { @after_save_called = true }
after_update { @after_update_called = true }
after_create { @after_create_called = true }
after_destroy { @after_destroy_called = true }
end
class CallbacksTest < ActiveRecord::TestCase
fixtures :developers
def test_initialize
david = CallbackDeveloper.new
assert_equal [
[ :after_initialize, :method ],
[ :after_initialize, :proc ],
[ :after_initialize, :object ],
[ :after_initialize, :block ],
], david.history
end
def test_find
david = CallbackDeveloper.find(1)
assert_equal [
[ :after_find, :method ],
[ :after_find, :proc ],
[ :after_find, :object ],
[ :after_find, :block ],
[ :after_initialize, :method ],
[ :after_initialize, :proc ],
[ :after_initialize, :object ],
[ :after_initialize, :block ],
], david.history
end
def test_new_valid?
david = CallbackDeveloper.new
david.valid?
assert_equal [
[ :after_initialize, :method ],
[ :after_initialize, :proc ],
[ :after_initialize, :object ],
[ :after_initialize, :block ],
[ :before_validation, :method ],
[ :before_validation, :proc ],
[ :before_validation, :object ],
[ :before_validation, :block ],
[ :after_validation, :method ],
[ :after_validation, :proc ],
[ :after_validation, :object ],
[ :after_validation, :block ],
], david.history
end
def test_existing_valid?
david = CallbackDeveloper.find(1)
david.valid?
assert_equal [
[ :after_find, :method ],
[ :after_find, :proc ],
[ :after_find, :object ],
[ :after_find, :block ],
[ :after_initialize, :method ],
[ :after_initialize, :proc ],
[ :after_initialize, :object ],
[ :after_initialize, :block ],
[ :before_validation, :method ],
[ :before_validation, :proc ],
[ :before_validation, :object ],
[ :before_validation, :block ],
[ :after_validation, :method ],
[ :after_validation, :proc ],
[ :after_validation, :object ],
[ :after_validation, :block ],
], david.history
end
def test_create
david = CallbackDeveloper.create("name" => "David", "salary" => 1000000)
assert_equal [
[ :after_initialize, :method ],
[ :after_initialize, :proc ],
[ :after_initialize, :object ],
[ :after_initialize, :block ],
[ :before_validation, :method ],
[ :before_validation, :proc ],
[ :before_validation, :object ],
[ :before_validation, :block ],
[ :after_validation, :method ],
[ :after_validation, :proc ],
[ :after_validation, :object ],
[ :after_validation, :block ],
[ :before_save, :method ],
[ :before_save, :proc ],
[ :before_save, :object ],
[ :before_save, :block ],
[ :before_create, :method ],
[ :before_create, :proc ],
[ :before_create, :object ],
[ :before_create, :block ],
[ :after_create, :method ],
[ :after_create, :proc ],
[ :after_create, :object ],
[ :after_create, :block ],
[ :after_save, :method ],
[ :after_save, :proc ],
[ :after_save, :object ],
[ :after_save, :block ],
[ :after_commit, :block ],
[ :after_commit, :object ],
[ :after_commit, :proc ],
[ :after_commit, :method ]
], david.history
end
def test_validate_on_create
david = OnCallbacksDeveloper.create("name" => "David", "salary" => 1000000)
assert_equal [
:before_validation,
:before_validation_on_create,
:validate,
:after_validation,
:after_validation_on_create
], david.history
end
def test_validate_on_contextual_create
david = ContextualCallbacksDeveloper.create("name" => "David", "salary" => 1000000)
assert_equal [
:before_validation,
:before_validation_on_create,
:validate,
:after_validation,
:after_validation_on_create
], david.history
end
def test_update
david = CallbackDeveloper.find(1)
david.save
assert_equal [
[ :after_find, :method ],
[ :after_find, :proc ],
[ :after_find, :object ],
[ :after_find, :block ],
[ :after_initialize, :method ],
[ :after_initialize, :proc ],
[ :after_initialize, :object ],
[ :after_initialize, :block ],
[ :before_validation, :method ],
[ :before_validation, :proc ],
[ :before_validation, :object ],
[ :before_validation, :block ],
[ :after_validation, :method ],
[ :after_validation, :proc ],
[ :after_validation, :object ],
[ :after_validation, :block ],
[ :before_save, :method ],
[ :before_save, :proc ],
[ :before_save, :object ],
[ :before_save, :block ],
[ :before_update, :method ],
[ :before_update, :proc ],
[ :before_update, :object ],
[ :before_update, :block ],
[ :after_update, :method ],
[ :after_update, :proc ],
[ :after_update, :object ],
[ :after_update, :block ],
[ :after_save, :method ],
[ :after_save, :proc ],
[ :after_save, :object ],
[ :after_save, :block ],
[ :after_commit, :block ],
[ :after_commit, :object ],
[ :after_commit, :proc ],
[ :after_commit, :method ]
], david.history
end
def test_validate_on_update
david = OnCallbacksDeveloper.find(1)
david.save
assert_equal [
:before_validation,
:before_validation_on_update,
:validate,
:after_validation,
:after_validation_on_update
], david.history
end
def test_validate_on_contextual_update
david = ContextualCallbacksDeveloper.find(1)
david.save
assert_equal [
:before_validation,
:before_validation_on_update,
:validate,
:after_validation,
:after_validation_on_update
], david.history
end
def test_destroy
david = CallbackDeveloper.find(1)
david.destroy
assert_equal [
[ :after_find, :method ],
[ :after_find, :proc ],
[ :after_find, :object ],
[ :after_find, :block ],
[ :after_initialize, :method ],
[ :after_initialize, :proc ],
[ :after_initialize, :object ],
[ :after_initialize, :block ],
[ :before_destroy, :method ],
[ :before_destroy, :proc ],
[ :before_destroy, :object ],
[ :before_destroy, :block ],
[ :after_destroy, :method ],
[ :after_destroy, :proc ],
[ :after_destroy, :object ],
[ :after_destroy, :block ],
[ :after_commit, :block ],
[ :after_commit, :object ],
[ :after_commit, :proc ],
[ :after_commit, :method ]
], david.history
end
def test_delete
david = CallbackDeveloper.find(1)
CallbackDeveloper.delete(david.id)
assert_equal [
[ :after_find, :method ],
[ :after_find, :proc ],
[ :after_find, :object ],
[ :after_find, :block ],
[ :after_initialize, :method ],
[ :after_initialize, :proc ],
[ :after_initialize, :object ],
[ :after_initialize, :block ],
], david.history
end
def assert_save_callbacks_not_called(someone)
assert_not someone.after_save_called
assert_not someone.after_create_called
assert_not someone.after_update_called
end
private :assert_save_callbacks_not_called
def test_before_create_throwing_abort
someone = CallbackHaltedDeveloper.new
someone.cancel_before_create = true
assert_predicate someone, :valid?
assert_not someone.save
assert_save_callbacks_not_called(someone)
end
def test_before_save_throwing_abort
david = DeveloperWithCanceledCallbacks.find(1)
assert_predicate david, :valid?
assert_not david.save
exc = assert_raise(ActiveRecord::RecordNotSaved) { david.save! }
assert_equal david, exc.record
david = DeveloperWithCanceledCallbacks.find(1)
david.salary = 10_000_000
assert_not_predicate david, :valid?
assert_not david.save
assert_raise(ActiveRecord::RecordInvalid) { david.save! }
someone = CallbackHaltedDeveloper.find(1)
someone.cancel_before_save = true
assert_predicate someone, :valid?
assert_not someone.save
assert_save_callbacks_not_called(someone)
end
def test_before_update_throwing_abort
someone = CallbackHaltedDeveloper.find(1)
someone.cancel_before_update = true
assert_predicate someone, :valid?
assert_not someone.save
assert_save_callbacks_not_called(someone)
end
def test_before_destroy_throwing_abort
david = DeveloperWithCanceledCallbacks.find(1)
assert_not david.destroy
exc = assert_raise(ActiveRecord::RecordNotDestroyed) { david.destroy! }
assert_equal david, exc.record
assert_not_nil ImmutableDeveloper.find_by_id(1)
someone = CallbackHaltedDeveloper.find(1)
someone.cancel_before_destroy = true
assert_not someone.destroy
assert_raise(ActiveRecord::RecordNotDestroyed) { someone.destroy! }
assert_not someone.after_destroy_called
end
def test_callback_throwing_abort
david = CallbackDeveloperWithHaltedValidation.find(1)
david.save
assert_equal [
[ :after_find, :method ],
[ :after_find, :proc ],
[ :after_find, :object ],
[ :after_find, :block ],
[ :after_initialize, :method ],
[ :after_initialize, :proc ],
[ :after_initialize, :object ],
[ :after_initialize, :block ],
[ :before_validation, :method ],
[ :before_validation, :proc ],
[ :before_validation, :object ],
[ :before_validation, :block ],
[ :before_validation, :throwing_abort ],
], david.history
end
def test_inheritance_of_callbacks
parent = ParentDeveloper.new
assert_not parent.after_save_called
parent.save
assert parent.after_save_called
child = ChildDeveloper.new
assert_not child.after_save_called
child.save
assert child.after_save_called
end
def test_before_save_doesnt_allow_on_option
exception = assert_raises ArgumentError do
Class.new(ActiveRecord::Base) do
before_save(on: :create) { }
end
end
assert_equal "Unknown key: :on. Valid keys are: :if, :unless, :prepend", exception.message
end
def test_around_save_doesnt_allow_on_option
exception = assert_raises ArgumentError do
Class.new(ActiveRecord::Base) do
around_save(on: :create) { }
end
end
assert_equal "Unknown key: :on. Valid keys are: :if, :unless, :prepend", exception.message
end
def test_after_save_doesnt_allow_on_option
exception = assert_raises ArgumentError do
Class.new(ActiveRecord::Base) do
after_save(on: :create) { }
end
end
assert_equal "Unknown key: :on. Valid keys are: :if, :unless, :prepend", exception.message
end
end
# frozen_string_literal: true
class Car < ActiveRecord::Base
has_many :bulbs
has_many :all_bulbs, -> { unscope(where: :name) }, class_name: "Bulb"
has_many :all_bulbs2, -> { unscope(:where) }, class_name: "Bulb"
has_many :other_bulbs, -> { unscope(where: :name).where(name: "other") }, class_name: "Bulb"
has_many :old_bulbs, -> { rewhere(name: "old") }, class_name: "Bulb"
has_many :funky_bulbs, class_name: "FunkyBulb", dependent: :destroy
has_many :failed_bulbs, class_name: "FailedBulb", dependent: :destroy
has_many :foo_bulbs, -> { where(name: "foo") }, class_name: "Bulb"
has_many :awesome_bulbs, -> { awesome }, class_name: "Bulb"
has_one :bulb
has_many :tyres
has_many :engines, dependent: :destroy, inverse_of: :my_car
has_many :wheels, as: :wheelable, dependent: :destroy
has_many :price_estimates, as: :estimate_of
scope :incl_tyres, -> { includes(:tyres) }
scope :incl_engines, -> { includes(:engines) }
scope :order_using_new_style, -> { order("name asc") }
attribute :wheels_owned_at, :datetime, default: -> { Time.now }
end
class CoolCar < Car
default_scope { order("name desc") }
end
class FastCar < Car
default_scope { order("name desc") }
end
# frozen_string_literal: true
class Carrier < ActiveRecord::Base
end
# frozen_string_literal: true
class Cart < ActiveRecord::Base
self.primary_key = :id
end
# frozen_string_literal: true
require "cases/helper"
require "models/post"
require "models/comment"
require "models/author"
require "models/categorization"
require "models/category"
require "models/company"
require "models/topic"
require "models/reply"
require "models/person"
require "models/vertex"
require "models/edge"
class CascadedEagerLoadingTest < ActiveRecord::TestCase
fixtures :authors, :author_addresses, :mixins, :companies, :posts, :topics, :accounts, :comments,
:categorizations, :people, :categories, :edges, :vertices
def test_eager_association_loading_with_cascaded_two_levels
authors = Author.includes(posts: :comments).order(:id).to_a
assert_equal 3, authors.size
assert_equal 5, authors[0].posts.size
assert_equal 3, authors[1].posts.size
assert_equal 11, authors[0].posts.collect { |post| post.comments.size }.inject(0) { |sum, i| sum + i }
end
def test_eager_association_loading_with_cascaded_two_levels_and_one_level
authors = Author.includes({ posts: :comments }, :categorizations).order(:id).to_a
assert_equal 3, authors.size
assert_equal 5, authors[0].posts.size
assert_equal 3, authors[1].posts.size
assert_equal 11, authors[0].posts.collect { |post| post.comments.size }.inject(0) { |sum, i| sum + i }
assert_equal 1, authors[0].categorizations.size
assert_equal 2, authors[1].categorizations.size
end
def test_eager_association_loading_with_hmt_does_not_table_name_collide_when_joining_associations
authors = Author.joins(:posts).eager_load(:comments).where(posts: { tags_count: 1 }).order(:id).to_a
assert_equal 3, assert_queries(0) { authors.size }
assert_equal 11, assert_queries(0) { authors[0].comments.size }
end
def test_eager_association_loading_grafts_stashed_associations_to_correct_parent
assert_equal people(:michael), Person.eager_load(primary_contact: :primary_contact).where("primary_contacts_people_2.first_name = ?", "Susan").order("people.id").first
end
def test_cascaded_eager_association_loading_with_join_for_count
categories = Category.joins(:categorizations).includes([{ posts: :comments }, :authors])
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
categories = Category.includes(:categorizations).includes(categorizations: :author).where("categorizations.id is not null").references(:categorizations)
assert_nothing_raised do
assert_equal 3, categories.count
assert_equal 3, categories.to_a.size
end
end
def test_cascaded_eager_association_loading_with_twice_includes_edge_cases
categories = Category.includes(categorizations: :author).includes(categorizations: :post).where("posts.id is not null").references(:posts)
assert_nothing_raised do
assert_equal 3, categories.count
assert_equal 3, categories.to_a.size
end
end
def test_eager_association_loading_with_join_for_count
authors = Author.joins(:special_posts).includes([:posts, :categorizations])
assert_nothing_raised { authors.count }
assert_queries(3) { authors.to_a }
end
def test_eager_association_loading_with_cascaded_two_levels_with_two_has_many_associations
authors = Author.all.merge!(includes: { posts: [:comments, :categorizations] }, order: "authors.id").to_a
assert_equal 3, authors.size
assert_equal 5, authors[0].posts.size
assert_equal 3, authors[1].posts.size
assert_equal 11, authors[0].posts.collect { |post| post.comments.size }.inject(0) { |sum, i| sum + i }
end
def test_eager_association_loading_with_cascaded_two_levels_and_self_table_reference
authors = Author.all.merge!(includes: { posts: [:comments, :author] }, order: "authors.id").to_a
assert_equal 3, authors.size
assert_equal 5, authors[0].posts.size
assert_equal authors(:david).name, authors[0].name
assert_equal [authors(:david).name], authors[0].posts.collect { |post| post.author.name }.uniq
end
def test_eager_association_loading_with_cascaded_two_levels_with_condition
authors = Author.all.merge!(includes: { posts: :comments }, where: "authors.id=1", order: "authors.id").to_a
assert_equal 1, authors.size
assert_equal 5, authors[0].posts.size
end
def test_eager_association_loading_with_cascaded_three_levels_by_ping_pong
firms = Firm.all.merge!(includes: { account: { firm: :account } }, order: "companies.id").to_a
assert_equal 3, firms.size
assert_equal firms.first.account, firms.first.account.firm.account
assert_equal companies(:first_firm).account, assert_queries(0) { firms.first.account.firm.account }
assert_equal companies(:first_firm).account.firm.account, assert_queries(0) { firms.first.account.firm.account }
end
def test_eager_association_loading_with_has_many_sti
topics = Topic.all.merge!(includes: :replies, order: "topics.id").to_a
first, second, = topics(:first).replies.size, topics(:second).replies.size
assert_queries(0) do
assert_equal first, topics[0].replies.size
assert_equal second, topics[1].replies.size
end
end
def test_eager_association_loading_with_has_many_sti_and_subclasses
reply = Reply.new(title: "gaga", content: "boo-boo", parent_id: 1)
assert reply.save
topics = Topic.all.merge!(includes: :replies, order: ["topics.id", "replies_topics.id"]).to_a
assert_queries(0) do
assert_equal 2, topics[0].replies.size
assert_equal 0, topics[1].replies.size
end
end
def test_eager_association_loading_with_belongs_to_sti
replies = Reply.all.merge!(includes: :topic, order: "topics.id").to_a
assert_includes replies, topics(:second)
assert_not_includes replies, topics(:first)
assert_equal topics(:first), assert_queries(0) { replies.first.topic }
end
def test_eager_association_loading_with_multiple_stis_and_order
author = Author.all.merge!(includes: { posts: [ :special_comments, :very_special_comment ] }, order: ["authors.name", "comments.body", "very_special_comments_posts.body"], where: "posts.id = 4").first
assert_equal authors(:david), author
assert_queries(0) do
author.posts.first.special_comments
author.posts.first.very_special_comment
end
end
def test_eager_association_loading_of_stis_with_multiple_references
authors = Author.all.merge!(includes: { posts: { special_comments: { post: [ :special_comments, :very_special_comment ] } } }, order: "comments.body, very_special_comments_posts.body", where: "posts.id = 4").to_a
assert_equal [authors(:david)], authors
assert_queries(0) do
authors.first.posts.first.special_comments.first.post.special_comments
authors.first.posts.first.special_comments.first.post.very_special_comment
end
end
def test_eager_association_loading_where_first_level_returns_nil
authors = Author.all.merge!(includes: { post_about_thinking: :comments }, order: "authors.id DESC").to_a
assert_equal [authors(:bob), authors(:mary), authors(:david)], authors
assert_queries(0) do
authors[2].post_about_thinking.comments.first
end
end
def test_preload_through_missing_records
post = Post.where.not(author_id: Author.select(:id)).preload(author: { comments: :post }).first!
assert_queries(0) { assert_nil post.author }
end
def test_eager_association_loading_with_missing_first_record
posts = Post.where(id: 3).preload(author: { comments: :post }).to_a
assert_equal posts.size, 1
end
def test_eager_association_loading_with_recursive_cascading_four_levels_has_many_through
source = Vertex.all.merge!(includes: { sinks: { sinks: { sinks: :sinks } } }, order: "vertices.id").first
assert_equal vertices(:vertex_4), assert_queries(0) { source.sinks.first.sinks.first.sinks.first }
end
def test_eager_association_loading_with_recursive_cascading_four_levels_has_and_belongs_to_many
sink = Vertex.all.merge!(includes: { sources: { sources: { sources: :sources } } }, order: "vertices.id DESC").first
assert_equal vertices(:vertex_1), assert_queries(0) { sink.sources.first.sources.first.sources.first.sources.first }
end
def test_eager_association_loading_with_cascaded_interdependent_one_level_and_two_levels
authors_relation = Author.all.merge!(includes: [:comments, { posts: :categorizations }], order: "authors.id")
authors = authors_relation.to_a
assert_equal 3, authors.size
assert_equal 11, authors[0].comments.size
assert_equal 1, authors[1].comments.size
assert_equal 5, authors[0].posts.size
assert_equal 3, authors[1].posts.size
assert_equal 3, authors[0].posts.collect { |post| post.categorizations.size }.inject(0) { |sum, i| sum + i }
end
def test_preloaded_records_are_not_duplicated
author = Author.first
expected = Post.where(author: author)
.includes(author: :first_posts).map { |post| post.author.first_posts.size }
actual = author.posts
.includes(author: :first_posts).map { |post| post.author.first_posts.size }
assert_equal expected, actual
end
def test_preloading_across_has_one_constrains_loaded_records
author = authors(:david)
old_post = author.posts.create!(title: "first post", body: "test")
old_post.comments.create!(author: authors(:mary), body: "a response")
recent_post = author.posts.create!(title: "first post", body: "test")
last_comment = recent_post.comments.create!(author: authors(:bob), body: "a response")
authors = Author.where(id: author.id)
retrieved_comments = []
reset_callbacks(Comment, :initialize) do
Comment.after_initialize { |record| retrieved_comments << record }
authors.preload(recent_post: :comments).load
end
assert_equal 1, retrieved_comments.size
assert_equal [last_comment], retrieved_comments
end
def test_preloading_across_has_one_through_constrains_loaded_records
author = authors(:david)
old_post = author.posts.create!(title: "first post", body: "test")
old_post.comments.create!(author: authors(:mary), body: "a response")
recent_post = author.posts.create!(title: "first post", body: "test")
recent_post.comments.create!(author: authors(:bob), body: "a response")
authors = Author.where(id: author.id)
retrieved_authors = []
reset_callbacks(Author, :initialize) do
Author.after_initialize { |record| retrieved_authors << record }
authors.preload(recent_response: :author).load
end
assert_equal 2, retrieved_authors.size
assert_equal [author, authors(:bob)], retrieved_authors
end
end
# frozen_string_literal: true
module Arel # :nodoc: all
module Nodes
class Case < Arel::Nodes::NodeExpression
attr_accessor :case, :conditions, :default
def initialize(expression = nil, default = nil)
@case = expression
@conditions = []
@default = default
end
def when(condition, expression = nil)
@conditions << When.new(Nodes.build_quoted(condition), expression)
self
end
def then(expression)
@conditions.last.right = Nodes.build_quoted(expression)
self
end
def else(expression)
@default = Else.new Nodes.build_quoted(expression)
self
end
def initialize_copy(other)
super
@case = @case.clone if @case
@conditions = @conditions.map { |x| x.clone }
@default = @default.clone if @default
end
def hash
[@case, @conditions, @default].hash
end
def eql?(other)
self.class == other.class &&
self.case == other.case &&
self.conditions == other.conditions &&
self.default == other.default
end
alias :== :eql?
end
class When < Binary # :nodoc:
end
class Else < Unary # :nodoc:
end
end
end
# frozen_string_literal: true
require "cases/helper"
class PostgresqlCaseInsensitiveTest < ActiveRecord::PostgreSQLTestCase
class Default < ActiveRecord::Base; end
def test_case_insensitiveness
connection = ActiveRecord::Base.connection
attr = Default.arel_table[:char1]
comparison = connection.case_insensitive_comparison(attr, nil)
assert_match(/lower/i, comparison.to_sql)
attr = Default.arel_table[:char2]
comparison = connection.case_insensitive_comparison(attr, nil)
assert_match(/lower/i, comparison.to_sql)
attr = Default.arel_table[:char3]
comparison = connection.case_insensitive_comparison(attr, nil)
assert_match(/lower/i, comparison.to_sql)
attr = Default.arel_table[:multiline_default]
comparison = connection.case_insensitive_comparison(attr, nil)
assert_match(/lower/i, comparison.to_sql)
end
end
# frozen_string_literal: true
require "cases/helper"
class Mysql2CaseSensitivityTest < ActiveRecord::Mysql2TestCase
class CollationTest < ActiveRecord::Base
end
repair_validations(CollationTest)
def test_columns_include_collation_different_from_table
assert_equal "utf8mb4_bin", CollationTest.columns_hash["string_cs_column"].collation
assert_equal "utf8mb4_general_ci", CollationTest.columns_hash["string_ci_column"].collation
end
def test_case_sensitive
assert_not_predicate CollationTest.columns_hash["string_ci_column"], :case_sensitive?
assert_predicate CollationTest.columns_hash["string_cs_column"], :case_sensitive?
end
def test_case_insensitive_comparison_for_ci_column
CollationTest.validates_uniqueness_of(:string_ci_column, case_sensitive: false)
CollationTest.create!(string_ci_column: "A")
invalid = CollationTest.new(string_ci_column: "a")
queries = capture_sql { invalid.save }
ci_uniqueness_query = queries.detect { |q| q.match(/string_ci_column/) }
assert_no_match(/lower/i, ci_uniqueness_query)
end
def test_case_insensitive_comparison_for_cs_column
CollationTest.validates_uniqueness_of(:string_cs_column, case_sensitive: false)
CollationTest.create!(string_cs_column: "A")
invalid = CollationTest.new(string_cs_column: "a")
queries = capture_sql { invalid.save }
cs_uniqueness_query = queries.detect { |q| q.match(/string_cs_column/) }
assert_match(/lower/i, cs_uniqueness_query)
end
def test_case_sensitive_comparison_for_ci_column
CollationTest.validates_uniqueness_of(:string_ci_column, case_sensitive: true)
CollationTest.create!(string_ci_column: "A")
invalid = CollationTest.new(string_ci_column: "A")
queries = capture_sql { invalid.save }
ci_uniqueness_query = queries.detect { |q| q.match(/string_ci_column/) }
assert_match(/binary/i, ci_uniqueness_query)
end
def test_case_sensitive_comparison_for_cs_column
CollationTest.validates_uniqueness_of(:string_cs_column, case_sensitive: true)
CollationTest.create!(string_cs_column: "A")
invalid = CollationTest.new(string_cs_column: "A")
queries = capture_sql { invalid.save }
cs_uniqueness_query = queries.detect { |q| q.match(/string_cs_column/) }
assert_no_match(/binary/i, cs_uniqueness_query)
end
def test_case_sensitive_comparison_for_binary_column
CollationTest.validates_uniqueness_of(:binary_column, case_sensitive: true)
CollationTest.create!(binary_column: "A")
invalid = CollationTest.new(binary_column: "A")
queries = capture_sql { invalid.save }
bin_uniqueness_query = queries.detect { |q| q.match(/binary_column/) }
assert_no_match(/\bBINARY\b/, bin_uniqueness_query)
end
end
This file has been truncated, but you can view the full file.
# frozen_string_literal: true
require_relative "../helper"
module Arel
module Nodes
class NodesTest < Arel::Spec
describe "Case" do
describe "#initialize" do
it "sets case expression from first argument" do
node = Case.new "foo"
assert_equal "foo", node.case
end
it "sets default case from second argument" do
node = Case.new nil, "bar"
assert_equal "bar", node.default
end
end
describe "#clone" do
it "clones case, conditions and default" do
foo = Nodes.build_quoted "foo"
node = Case.new
node.case = foo
node.conditions = [When.new(foo, foo)]
node.default = foo
dolly = node.clone
assert_equal dolly.case, node.case
assert_not_same dolly.case, node.case
assert_equal dolly.conditions, node.conditions
assert_not_same doll
View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment