Skip to content

Instantly share code, notes, and snippets.

@sgoedecke
Created March 8, 2022 01:24
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/49eb8a5347ff2e6376eecbdb39c4866a to your computer and use it in GitHub Desktop.
Save sgoedecke/49eb8a5347ff2e6376eecbdb39c4866a 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
class CreateActiveStorageTables < ActiveRecord::Migration[5.2]
def change
# Use Active Record's configured type for primary and foreign keys
primary_key_type, foreign_key_type = primary_and_foreign_key_types
create_table :active_storage_blobs, id: primary_key_type do |t|
t.string :key, null: false
t.string :filename, null: false
t.string :content_type
t.text :metadata
t.string :service_name, null: false
t.bigint :byte_size, null: false
t.string :checksum
if connection.supports_datetime_with_precision?
t.datetime :created_at, precision: 6, null: false
else
t.datetime :created_at, null: false
end
t.index [ :key ], unique: true
end
create_table :active_storage_attachments, id: primary_key_type do |t|
t.string :name, null: false
t.references :record, null: false, polymorphic: true, index: false, type: foreign_key_type
t.references :blob, null: false, type: foreign_key_type
if connection.supports_datetime_with_precision?
t.datetime :created_at, precision: 6, null: false
else
t.datetime :created_at, null: false
end
t.index [ :record_type, :record_id, :name, :blob_id ], name: :index_active_storage_attachments_uniqueness, unique: true
t.foreign_key :active_storage_blobs, column: :blob_id
end
create_table :active_storage_variant_records, id: primary_key_type do |t|
t.belongs_to :blob, null: false, index: false, type: foreign_key_type
t.string :variation_digest, null: false
t.index [ :blob_id, :variation_digest ], name: :index_active_storage_variant_records_uniqueness, unique: true
t.foreign_key :active_storage_blobs, column: :blob_id
end
end
private
def primary_and_foreign_key_types
config = Rails.configuration.generators
setting = config.options[config.orm][:primary_key_type]
primary_key_type = setting || :primary_key
foreign_key_type = setting || :bigint
[primary_key_type, foreign_key_type]
end
end
class AddServiceNameToActiveStorageBlobs < ActiveRecord::Migration[6.0]
def up
unless column_exists?(:active_storage_blobs, :service_name)
add_column :active_storage_blobs, :service_name, :string
if configured_service = ActiveStorage::Blob.service.name
ActiveStorage::Blob.unscoped.update_all(service_name: configured_service)
end
change_column :active_storage_blobs, :service_name, :string, null: false
end
end
def down
remove_column :active_storage_blobs, :service_name
end
end
class CreateActiveStorageVariantRecords < ActiveRecord::Migration[6.0]
def change
# Use Active Record's configured type for primary key
create_table :active_storage_variant_records, id: primary_key_type, if_not_exists: true do |t|
t.belongs_to :blob, null: false, index: false, type: blobs_primary_key_type
t.string :variation_digest, null: false
t.index %i[ blob_id variation_digest ], name: "index_active_storage_variant_records_uniqueness", unique: true
t.foreign_key :active_storage_blobs, column: :blob_id
end
end
private
def primary_key_type
config = Rails.configuration.generators
config.options[config.orm][:primary_key_type] || :primary_key
end
def blobs_primary_key_type
pkey_name = connection.primary_key(:active_storage_blobs)
pkey_column = connection.columns(:active_storage_blobs).find { |c| c.name == pkey_name }
pkey_column.bigint? ? :bigint : pkey_column.type
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
class RemoveNotNullOnActiveStorageBlobsChecksum < ActiveRecord::Migration[6.0]
def change
change_column_null(:active_storage_blobs, :checksum, true)
end
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
class AbortBeforeEnqueueJob < ActiveJob::Base
MyError = Class.new(StandardError)
before_enqueue :throw_or_raise
after_enqueue { self.flag = "after_enqueue" }
before_perform { throw(:abort) }
after_perform { self.flag = "after_perform" }
attr_accessor :flag
def perform
raise "This should never be called"
end
def throw_or_raise
if (arguments.first || :abort) == :abort
throw(:abort)
else
raise(MyError)
end
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
ORIG_ARGV = ARGV.dup
require "bundler/setup"
require "active_support/core_ext/kernel/reporting"
silence_warnings do
Encoding.default_internal = Encoding::UTF_8
Encoding.default_external = Encoding::UTF_8
end
require "active_support/testing/autorun"
require "active_support/testing/method_call_assertions"
ENV["NO_RELOAD"] = "1"
require "active_support"
Thread.abort_on_exception = true
# Show backtraces for deprecated behavior for quicker cleanup.
ActiveSupport::Deprecation.debug = true
# Default to old to_time behavior but allow running tests with new behavior
ActiveSupport.to_time_preserves_timezone = ENV["PRESERVE_TIMEZONES"] == "1"
# Disable available locale checks to avoid warnings running the test suite.
I18n.enforce_available_locales = false
class ActiveSupport::TestCase
if Process.respond_to?(:fork) && !Gem.win_platform?
parallelize
else
parallelize(with: :threads)
end
include ActiveSupport::Testing::MethodCallAssertions
private
# Skips the current run on Rubinius using Minitest::Assertions#skip
def rubinius_skip(message = "")
skip message if RUBY_ENGINE == "rbx"
end
# Skips the current run on JRuby using Minitest::Assertions#skip
def jruby_skip(message = "")
skip message if defined?(JRUBY_VERSION)
end
end
require_relative "../../tools/test_common"
# frozen_string_literal: true
module ActiveModel
module Validations
class AcceptanceValidator < EachValidator # :nodoc:
def initialize(options)
super({ allow_nil: true, accept: ["1", true] }.merge!(options))
setup!(options[:class])
end
def validate_each(record, attribute, value)
unless acceptable_option?(value)
record.errors.add(attribute, :accepted, **options.except(:accept, :allow_nil))
end
end
private
def setup!(klass)
define_attributes = LazilyDefineAttributes.new(attributes)
klass.include(define_attributes) unless klass.included_modules.include?(define_attributes)
end
def acceptable_option?(value)
Array(options[:accept]).include?(value)
end
class LazilyDefineAttributes < Module
def initialize(attributes)
@attributes = attributes.map(&:to_s)
end
def included(klass)
@lock = Mutex.new
mod = self
define_method(:respond_to_missing?) do |method_name, include_private = false|
mod.define_on(klass)
super(method_name, include_private) || mod.matches?(method_name)
end
define_method(:method_missing) do |method_name, *args, &block|
mod.define_on(klass)
if mod.matches?(method_name)
send(method_name, *args, &block)
else
super(method_name, *args, &block)
end
end
end
def matches?(method_name)
attr_name = method_name.to_s.chomp("=")
attributes.any? { |name| name == attr_name }
end
def define_on(klass)
@lock&.synchronize do
return unless @lock
attr_readers = attributes.reject { |name| klass.attribute_method?(name) }
attr_writers = attributes.reject { |name| klass.attribute_method?("#{name}=") }
attr_reader(*attr_readers)
attr_writer(*attr_writers)
remove_method :respond_to_missing?
remove_method :method_missing
@lock = nil
end
end
def ==(other)
self.class == other.class && attributes == other.attributes
end
protected
attr_reader :attributes
end
end
module HelperMethods
# Encapsulates the pattern of wanting to validate the acceptance of a
# terms of service check box (or similar agreement).
#
# class Person < ActiveRecord::Base
# validates_acceptance_of :terms_of_service
# validates_acceptance_of :eula, message: 'must be abided'
# end
#
# If the database column does not exist, the +terms_of_service+ attribute
# is entirely virtual. This check is performed only if +terms_of_service+
# is not +nil+ and by default on save.
#
# Configuration options:
# * <tt>:message</tt> - A custom error message (default is: "must be
# accepted").
# * <tt>:accept</tt> - Specifies a value that is considered accepted.
# Also accepts an array of possible values. The default value is
# an array ["1", true], which makes it easy to relate to an HTML
# checkbox. This should be set to, or include, +true+ if you are validating
# a database column, since the attribute is typecast from "1" to +true+
# before validation.
#
# There is also a list of default options supported by every validator:
# +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+.
# See ActiveModel::Validations::ClassMethods#validates for more information.
def validates_acceptance_of(*attr_names)
validates_with AcceptanceValidator, _merge_attributes(attr_names)
end
end
end
end
# frozen_string_literal: true
require "cases/helper"
require "models/topic"
require "models/reply"
require "models/person"
class AcceptanceValidationTest < ActiveModel::TestCase
teardown do
self.class.send(:remove_const, :TestClass)
end
def test_terms_of_service_agreement_no_acceptance
klass = define_test_class(Topic)
klass.validates_acceptance_of(:terms_of_service)
t = klass.new("title" => "We should not be confirmed")
assert_predicate t, :valid?
end
def test_terms_of_service_agreement
klass = define_test_class(Topic)
klass.validates_acceptance_of(:terms_of_service)
t = klass.new("title" => "We should be confirmed", "terms_of_service" => "")
assert_predicate t, :invalid?
assert_equal ["must be accepted"], t.errors[:terms_of_service]
t.terms_of_service = "1"
assert_predicate t, :valid?
end
def test_eula
klass = define_test_class(Topic)
klass.validates_acceptance_of(:eula, message: "must be abided")
t = klass.new("title" => "We should be confirmed", "eula" => "")
assert_predicate t, :invalid?
assert_equal ["must be abided"], t.errors[:eula]
t.eula = "1"
assert_predicate t, :valid?
end
def test_terms_of_service_agreement_with_accept_value
klass = define_test_class(Topic)
klass.validates_acceptance_of(:terms_of_service, accept: "I agree.")
t = klass.new("title" => "We should be confirmed", "terms_of_service" => "")
assert_predicate t, :invalid?
assert_equal ["must be accepted"], t.errors[:terms_of_service]
t.terms_of_service = "I agree."
assert_predicate t, :valid?
end
def test_terms_of_service_agreement_with_multiple_accept_values
klass = define_test_class(Topic)
klass.validates_acceptance_of(:terms_of_service, accept: [1, "I concur."])
t = klass.new("title" => "We should be confirmed", "terms_of_service" => "")
assert_predicate t, :invalid?
assert_equal ["must be accepted"], t.errors[:terms_of_service]
t.terms_of_service = 1
assert_predicate t, :valid?
t.terms_of_service = "I concur."
assert_predicate t, :valid?
end
def test_validates_acceptance_of_for_ruby_class
klass = define_test_class(Person)
klass.validates_acceptance_of :karma
p = klass.new
p.karma = ""
assert_predicate p, :invalid?
assert_equal ["must be accepted"], p.errors[:karma]
p.karma = "1"
assert_predicate p, :valid?
end
def test_validates_acceptance_of_true
klass = define_test_class(Topic)
klass.validates_acceptance_of(:terms_of_service)
assert_predicate klass.new(terms_of_service: true), :valid?
end
def test_lazy_attribute_module_included_only_once
klass = define_test_class(Topic)
assert_difference -> { klass.ancestors.count }, 2 do
2.times do
klass.validates_acceptance_of(:something_to_accept)
assert klass.new.respond_to?(:something_to_accept)
end
2.times do
klass.validates_acceptance_of(:something_else_to_accept)
assert klass.new.respond_to?(:something_else_to_accept)
end
end
end
def test_lazy_attributes_module_included_again_if_needed
klass = define_test_class(Topic)
assert_difference -> { klass.ancestors.count }, 1 do
klass.validates_acceptance_of(:something_to_accept)
end
topic = klass.new
topic.something_to_accept
assert_difference -> { klass.ancestors.count }, 1 do
klass.validates_acceptance_of(:something_else_to_accept)
end
assert topic.respond_to?(:something_else_to_accept)
end
def test_lazy_attributes_respond_to?
klass = define_test_class(Topic)
klass.validates_acceptance_of(:terms_of_service)
topic = klass.new
threads = []
2.times do
threads << Thread.new do
assert topic.respond_to?(:terms_of_service)
end
end
threads.each(&:join)
end
private
def define_test_class(parent)
self.class.const_set(:TestClass, Class.new(parent))
end
end
# frozen_string_literal: true
module ActiveModel
module Type
module Helpers # :nodoc: all
class AcceptsMultiparameterTime < Module
module InstanceMethods
def serialize(value)
super(cast(value))
end
def cast(value)
if value.is_a?(Hash)
value_from_multiparameter_assignment(value)
else
super(value)
end
end
def assert_valid_value(value)
if value.is_a?(Hash)
value_from_multiparameter_assignment(value)
else
super(value)
end
end
def value_constructed_by_mass_assignment?(value)
value.is_a?(Hash)
end
end
def initialize(defaults: {})
include InstanceMethods
define_method(:value_from_multiparameter_assignment) do |values_hash|
defaults.each do |k, v|
values_hash[k] ||= v
end
return unless values_hash[1] && values_hash[2] && values_hash[3]
values = values_hash.sort.map!(&:last)
::Time.public_send(default_timezone, *values)
end
private :value_from_multiparameter_assignment
end
end
end
end
end
# frozen_string_literal: true
class String
# If you pass a single integer, returns a substring of one character at that
# position. The first character of the string is at position 0, the next at
# position 1, and so on. If a range is supplied, a substring containing
# characters at offsets given by the range is returned. In both cases, if an
# offset is negative, it is counted from the end of the string. Returns +nil+
# if the initial offset falls outside the string. Returns an empty string if
# the beginning of the range is greater than the end of the string.
#
# str = "hello"
# str.at(0) # => "h"
# str.at(1..3) # => "ell"
# str.at(-2) # => "l"
# str.at(-2..-1) # => "lo"
# str.at(5) # => nil
# str.at(5..-1) # => ""
#
# If a Regexp is given, the matching portion of the string is returned.
# If a String is given, that given string is returned if it occurs in
# the string. In both cases, +nil+ is returned if there is no match.
#
# str = "hello"
# str.at(/lo/) # => "lo"
# str.at(/ol/) # => nil
# str.at("lo") # => "lo"
# str.at("ol") # => nil
def at(position)
self[position]
end
# Returns a substring from the given position to the end of the string.
# If the position is negative, it is counted from the end of the string.
#
# str = "hello"
# str.from(0) # => "hello"
# str.from(3) # => "lo"
# str.from(-2) # => "lo"
#
# You can mix it with +to+ method and do fun things like:
#
# str = "hello"
# str.from(0).to(-1) # => "hello"
# str.from(1).to(-2) # => "ell"
def from(position)
self[position, length]
end
# Returns a substring from the beginning of the string to the given position.
# If the position is negative, it is counted from the end of the string.
#
# str = "hello"
# str.to(0) # => "h"
# str.to(3) # => "hell"
# str.to(-2) # => "hell"
#
# You can mix it with +from+ method and do fun things like:
#
# str = "hello"
# str.from(0).to(-1) # => "hello"
# str.from(1).to(-2) # => "ell"
def to(position)
position += size if position < 0
self[0, position + 1] || +""
end
# Returns the first character. If a limit is supplied, returns a substring
# from the beginning of the string until it reaches the limit value. If the
# given limit is greater than or equal to the string length, returns a copy of self.
#
# str = "hello"
# str.first # => "h"
# str.first(1) # => "h"
# str.first(2) # => "he"
# str.first(0) # => ""
# str.first(6) # => "hello"
def first(limit = 1)
self[0, limit] || raise(ArgumentError, "negative limit")
end
# Returns the last character of the string. If a limit is supplied, returns a substring
# from the end of the string until it reaches the limit value (counting backwards). If
# the given limit is greater than or equal to the string length, returns a copy of self.
#
# str = "hello"
# str.last # => "o"
# str.last(1) # => "o"
# str.last(2) # => "lo"
# str.last(0) # => ""
# str.last(6) # => "hello"
def last(limit = 1)
self[[length - limit, 0].max, limit] || raise(ArgumentError, "negative limit")
end
end
# frozen_string_literal: true
require_relative "../../abstract_unit"
require "active_support/core_ext/array"
class AccessTest < ActiveSupport::TestCase
def test_from
assert_equal %w( a b c d ), %w( a b c d ).from(0)
assert_equal %w( c d ), %w( a b c d ).from(2)
assert_equal %w(), %w( a b c d ).from(10)
assert_equal %w( d e ), %w( a b c d e ).from(-2)
assert_equal %w(), %w( a b c d e ).from(-10)
end
def test_to
assert_equal %w( a ), %w( a b c d ).to(0)
assert_equal %w( a b c ), %w( a b c d ).to(2)
assert_equal %w( a b c d ), %w( a b c d ).to(10)
assert_equal %w( a b c ), %w( a b c d ).to(-2)
assert_equal %w(), %w( a b c ).to(-10)
end
def test_specific_accessor
array = (1..42).to_a
assert_equal array[1], array.second
assert_equal array[2], array.third
assert_equal array[3], array.fourth
assert_equal array[4], array.fifth
assert_equal array[41], array.forty_two
assert_equal array[-3], array.third_to_last
assert_equal array[-2], array.second_to_last
end
def test_including
assert_equal [1, 2, 3, 4, 5], [1, 2, 4].including(3, 5).sort
assert_equal [1, 2, 3, 4, 5], [1, 2, 4].including([3, 5]).sort
assert_equal [[0, 1], [1, 0]], [[0, 1]].including([[1, 0]])
end
def test_excluding
assert_equal [1, 2, 4], [1, 2, 3, 4, 5].excluding(3, 5)
assert_equal [1, 2, 4], [1, 2, 3, 4, 5].excluding([3, 5])
assert_equal [[0, 1]], [[0, 1], [1, 0]].excluding([[1, 0]])
end
def test_without
assert_equal [1, 2, 4], [1, 2, 3, 4, 5].without(3, 5)
end
end
# frozen_string_literal: true
class Admin::Account < ActiveRecord::Base
has_many :users
end
# frozen_string_literal: true
module ActiveSupport
# Actionable errors lets you define actions to resolve an error.
#
# To make an error actionable, include the <tt>ActiveSupport::ActionableError</tt>
# module and invoke the +action+ class macro to define the action. An action
# needs a name and a block to execute.
module ActionableError
extend Concern
class NonActionable < StandardError; end
included do
class_attribute :_actions, default: {}
end
def self.actions(error) # :nodoc:
case error
when ActionableError, -> it { Class === it && it < ActionableError }
error._actions
else
{}
end
end
def self.dispatch(error, name) # :nodoc:
actions(error).fetch(name).call
rescue KeyError
raise NonActionable, "Cannot find action \"#{name}\""
end
module ClassMethods
# Defines an action that can resolve the error.
#
# class PendingMigrationError < MigrationError
# include ActiveSupport::ActionableError
#
# action "Run pending migrations" do
# ActiveRecord::Tasks::DatabaseTasks.migrate
# end
# end
def action(name, &block)
_actions[name] = block
end
end
end
end
# frozen_string_literal: true
require_relative "abstract_unit"
require "active_support/actionable_error"
class ActionableErrorTest < ActiveSupport::TestCase
NonActionableError = Class.new(StandardError)
class DispatchableError < StandardError
include ActiveSupport::ActionableError
class_attribute :flip1, default: false
class_attribute :flip2, default: false
action "Flip 1" do
self.flip1 = true
end
action "Flip 2" do
self.flip2 = true
end
end
test "returns all action of an actionable error" do
assert_equal ["Flip 1", "Flip 2"], ActiveSupport::ActionableError.actions(DispatchableError).keys
assert_equal ["Flip 1", "Flip 2"], ActiveSupport::ActionableError.actions(DispatchableError.new).keys
end
test "returns no actions for non-actionable errors" do
assert ActiveSupport::ActionableError.actions(Exception).empty?
assert ActiveSupport::ActionableError.actions(Exception.new).empty?
end
test "dispatches actions from error and name" do
assert_changes "DispatchableError.flip1", from: false, to: true do
ActiveSupport::ActionableError.dispatch DispatchableError, "Flip 1"
end
end
test "cannot dispatch missing actions" do
err = assert_raises ActiveSupport::ActionableError::NonActionable do
ActiveSupport::ActionableError.dispatch NonActionableError, "action"
end
assert_equal 'Cannot find action "action"', err.to_s
end
end
# frozen_string_literal: true
#--
# Copyright (c) 2014-2022 David Heinemeier Hansson
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#++
require "active_support"
require "active_support/rails"
require "active_job/version"
require "global_id"
module ActiveJob
extend ActiveSupport::Autoload
autoload :Base
autoload :QueueAdapters
eager_autoload do
autoload :Serializers
autoload :ConfiguredJob
end
autoload :TestCase
autoload :TestHelper
autoload :QueryTags
end
# frozen_string_literal: true
#--
# Copyright (c) 2004-2022 David Heinemeier Hansson
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#++
require "active_support"
require "active_support/rails"
require "active_model/version"
module ActiveModel
extend ActiveSupport::Autoload
autoload :Access
autoload :API
autoload :Attribute
autoload :Attributes
autoload :AttributeAssignment
autoload :AttributeMethods
autoload :BlockValidator, "active_model/validator"
autoload :Callbacks
autoload :Conversion
autoload :Dirty
autoload :EachValidator, "active_model/validator"
autoload :ForbiddenAttributesProtection
autoload :Lint
autoload :Model
autoload :Name, "active_model/naming"
autoload :Naming
autoload :SecurePassword
autoload :Serialization
autoload :Translation
autoload :Type
autoload :Validations
autoload :Validator
eager_autoload do
autoload :Errors
autoload :Error
autoload :RangeError, "active_model/errors"
autoload :StrictValidationFailed, "active_model/errors"
autoload :UnknownAttributeError, "active_model/errors"
end
module Serializers
extend ActiveSupport::Autoload
eager_autoload do
autoload :JSON
end
end
def self.eager_load!
super
ActiveModel::Serializers.eager_load!
end
end
ActiveSupport.on_load(:i18n) do
I18n.load_path << File.expand_path("active_model/locale/en.yml", __dir__)
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
#--
# Copyright (c) 2017-2022 David Heinemeier Hansson, Basecamp
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#++
require "active_record"
require "active_support"
require "active_support/rails"
require "active_support/core_ext/numeric/time"
require "active_storage/version"
require "active_storage/errors"
require "marcel"
module ActiveStorage
extend ActiveSupport::Autoload
autoload :Attached
autoload :FixtureSet
autoload :Service
autoload :Previewer
autoload :Analyzer
mattr_accessor :logger
mattr_accessor :verifier
mattr_accessor :variant_processor, default: :mini_magick
mattr_accessor :queues, default: {}
mattr_accessor :previewers, default: []
mattr_accessor :analyzers, default: []
mattr_accessor :paths, default: {}
mattr_accessor :variable_content_types, default: []
mattr_accessor :web_image_content_types, default: []
mattr_accessor :binary_content_type, default: "application/octet-stream"
mattr_accessor :content_types_to_serve_as_binary, default: []
mattr_accessor :content_types_allowed_inline, default: []
mattr_accessor :service_urls_expire_in, default: 5.minutes
mattr_accessor :urls_expire_in
mattr_accessor :routes_prefix, default: "/rails/active_storage"
mattr_accessor :draw_routes, default: true
mattr_accessor :resolve_model_to_route, default: :rails_storage_redirect
mattr_accessor :replace_on_assign_to_many, default: false
mattr_accessor :track_variants, default: false
mattr_accessor :video_preview_arguments, default: "-y -vframes 1 -f image2"
mattr_accessor :silence_invalid_content_types_warning, default: false
module Transformers
extend ActiveSupport::Autoload
autoload :Transformer
autoload :ImageProcessingTransformer
end
end
# frozen_string_literal: true
#--
# Copyright (c) 2005-2022 David Heinemeier Hansson
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#++
require "securerandom"
require "active_support/dependencies/autoload"
require "active_support/version"
require "active_support/logger"
require "active_support/lazy_load_hooks"
require "active_support/core_ext/date_and_time/compatibility"
module ActiveSupport
extend ActiveSupport::Autoload
autoload :Concern
autoload :CodeGenerator
autoload :ActionableError
autoload :ConfigurationFile
autoload :CurrentAttributes
autoload :Dependencies
autoload :DescendantsTracker
autoload :ExecutionContext
autoload :ExecutionWrapper
autoload :Executor
autoload :ErrorReporter
autoload :FileUpdateChecker
autoload :EventedFileUpdateChecker
autoload :ForkTracker
autoload :LogSubscriber
autoload :IsolatedExecutionState
autoload :Notifications
autoload :Reloader
autoload :PerThreadRegistry
autoload :SecureCompareRotator
eager_autoload do
autoload :BacktraceCleaner
autoload :ProxyObject
autoload :Benchmarkable
autoload :Cache
autoload :Callbacks
autoload :Configurable
autoload :Deprecation
autoload :Digest
autoload :Gzip
autoload :Inflector
autoload :JSON
autoload :JsonWithMarshalFallback
autoload :KeyGenerator
autoload :MessageEncryptor
autoload :MessageVerifier
autoload :Multibyte
autoload :NumberHelper
autoload :OptionMerger
autoload :OrderedHash
autoload :OrderedOptions
autoload :StringInquirer
autoload :EnvironmentInquirer
autoload :TaggedLogging
autoload :XmlMini
autoload :ArrayInquirer
end
autoload :Rescuable
autoload :SafeBuffer, "active_support/core_ext/string/output_safety"
autoload :TestCase
def self.eager_load!
super
NumberHelper.eager_load!
end
cattr_accessor :test_order # :nodoc:
cattr_accessor :test_parallelization_threshold, default: 50 # :nodoc:
singleton_class.attr_accessor :error_reporter # :nodoc:
def self.cache_format_version
Cache.format_version
end
def self.cache_format_version=(value)
Cache.format_version = value
end
def self.to_time_preserves_timezone
DateAndTime::Compatibility.preserve_timezone
end
def self.to_time_preserves_timezone=(value)
DateAndTime::Compatibility.preserve_timezone = value
end
def self.utc_to_local_returns_utc_offset_times
DateAndTime::Compatibility.utc_to_local_returns_utc_offset_times
end
def self.utc_to_local_returns_utc_offset_times=(value)
DateAndTime::Compatibility.utc_to_local_returns_utc_offset_times = value
end
end
autoload :I18n, "active_support/i18n"
# frozen_string_literal: true
require "active_support/core_ext/object/acts_like"
class Time
# Duck-types as a Time-like class. See Object#acts_like?.
def acts_like_time?
true
end
end
# frozen_string_literal: true
require_relative "../../abstract_unit"
require "active_support/core_ext/date/acts_like"
require "active_support/core_ext/time/acts_like"
require "active_support/core_ext/date_time/acts_like"
require "active_support/core_ext/object/acts_like"
class ObjectTests < ActiveSupport::TestCase
class DuckTime
def acts_like_time?
true
end
end
def test_duck_typing
object = Object.new
time = Time.now
date = Date.today
dt = DateTime.new
duck = DuckTime.new
assert_not object.acts_like?(:time)
assert_not object.acts_like?(:date)
assert time.acts_like?(:time)
assert_not time.acts_like?(:date)
assert_not date.acts_like?(:time)
assert date.acts_like?(:date)
assert dt.acts_like?(:time)
assert dt.acts_like?(:date)
assert duck.acts_like?(:time)
assert_not duck.acts_like?(:date)
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
require_relative "../abstract_unit"
module OtherAfterTeardown
def after_teardown
super
@witness = true
end
end
class AfterTeardownTest < ActiveSupport::TestCase
include OtherAfterTeardown
attr_writer :witness
MyError = Class.new(StandardError)
teardown do
raise MyError, "Test raises an error, all after_teardown should still get called"
end
def after_teardown
assert_changes -> { failures.count }, from: 0, to: 1 do
super
end
assert_equal true, @witness
failures.clear
end
def test_teardown_raise_but_all_after_teardown_method_are_called
assert true
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
class Module
# Allows you to make aliases for attributes, which includes
# getter, setter, and a predicate.
#
# class Content < ActiveRecord::Base
# # has a title attribute
# end
#
# class Email < Content
# alias_attribute :subject, :title
# end
#
# e = Email.find(1)
# e.title # => "Superstars"
# e.subject # => "Superstars"
# e.subject? # => true
# e.subject = "Megastars"
# e.title # => "Megastars"
def alias_attribute(new_name, old_name)
# The following reader methods use an explicit `self` receiver in order to
# support aliases that start with an uppercase letter. Otherwise, they would
# be resolved as constants instead.
module_eval <<-STR, __FILE__, __LINE__ + 1
def #{new_name}; self.#{old_name}; end # def subject; self.title; end
def #{new_name}?; self.#{old_name}?; end # def subject?; self.title?; end
def #{new_name}=(v); self.#{old_name} = v; end # def subject=(v); self.title = v; end
STR
end
end
# frozen_string_literal: true
require "active_support"
require "active_support/time"
require "active_support/core_ext"
# frozen_string_literal: true
require "active_storage/analyzer/null_analyzer"
module ActiveStorage::Blob::Analyzable
# Extracts and stores metadata from the file associated with this blob using a relevant analyzer. Active Storage comes
# with built-in analyzers for images and videos. See ActiveStorage::Analyzer::ImageAnalyzer and
# ActiveStorage::Analyzer::VideoAnalyzer for information about the specific attributes they extract and the third-party
# libraries they require.
#
# To choose the analyzer for a blob, Active Storage calls +accept?+ on each registered analyzer in order. It uses the
# first analyzer for which +accept?+ returns true when given the blob. If no registered analyzer accepts the blob, no
# metadata is extracted from it.
#
# In a Rails application, add or remove analyzers by manipulating +Rails.application.config.active_storage.analyzers+
# in an initializer:
#
# # Add a custom analyzer for Microsoft Office documents:
# Rails.application.config.active_storage.analyzers.append DOCXAnalyzer
#
# # Remove the built-in video analyzer:
# Rails.application.config.active_storage.analyzers.delete ActiveStorage::Analyzer::VideoAnalyzer
#
# Outside of a Rails application, manipulate +ActiveStorage.analyzers+ instead.
#
# You won't ordinarily need to call this method from a Rails application. New blobs are automatically and asynchronously
# analyzed via #analyze_later when they're attached for the first time.
def analyze
update! metadata: metadata.merge(extract_metadata_via_analyzer)
end
# Enqueues an ActiveStorage::AnalyzeJob which calls #analyze, or calls #analyze inline based on analyzer class configuration.
#
# This method is automatically called for a blob when it's attached for the first time. You can call it to analyze a blob
# again (e.g. if you add a new analyzer or modify an existing one).
def analyze_later
if analyzer_class.analyze_later?
ActiveStorage::AnalyzeJob.perform_later(self)
else
analyze
end
end
# Returns true if the blob has been analyzed.
def analyzed?
analyzed
end
private
def extract_metadata_via_analyzer
analyzer.metadata.merge(analyzed: true)
end
def analyzer
analyzer_class.new(self)
end
def analyzer_class
ActiveStorage.analyzers.detect { |klass| klass.accept?(self) } || ActiveStorage::Analyzer::NullAnalyzer
end
end
# frozen_string_literal: true
# Provides asynchronous analysis of ActiveStorage::Blob records via ActiveStorage::Blob#analyze_later.
class ActiveStorage::AnalyzeJob < ActiveStorage::BaseJob
queue_as { ActiveStorage.queues[:analysis] }
discard_on ActiveRecord::RecordNotFound
retry_on ActiveStorage::IntegrityError, attempts: 10, wait: :exponentially_longer
def perform(blob)
blob.analyze
end
end
# frozen_string_literal: true
require "test_helper"
require "database/setup"
class ActiveStorage::AnalyzeJobTest < ActiveJob::TestCase
setup { @blob = create_blob }
test "ignores missing blob" do
@blob.purge
perform_enqueued_jobs do
assert_nothing_raised do
ActiveStorage::AnalyzeJob.perform_later @blob
end
end
end
end
# frozen_string_literal: true
module ActiveStorage
# This is an abstract base class for analyzers, which extract metadata from blobs. See
# ActiveStorage::Analyzer::VideoAnalyzer for an example of a concrete subclass.
class Analyzer
attr_reader :blob
# Implement this method in a concrete subclass. Have it return true when given a blob from which
# the analyzer can extract metadata.
def self.accept?(blob)
false
end
# Implement this method in concrete subclasses. It will determine if blob analysis
# should be done in a job or performed inline. By default, analysis is enqueued in a job.
def self.analyze_later?
true
end
def initialize(blob)
@blob = blob
end
# Override this method in a concrete subclass. Have it return a Hash of metadata.
def metadata
raise NotImplementedError
end
private
# Downloads the blob to a tempfile on disk. Yields the tempfile.
def download_blob_to_tempfile(&block) # :doc:
blob.open tmpdir: tmpdir, &block
end
def logger # :doc:
ActiveStorage.logger
end
def tmpdir # :doc:
Dir.tmpdir
end
def instrument(analyzer, &block) # :doc:
ActiveSupport::Notifications.instrument("analyze.active_storage", analyzer: analyzer, &block)
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
class Module
# A module may or may not have a name.
#
# module M; end
# M.name # => "M"
#
# m = Module.new
# m.name # => nil
#
# +anonymous?+ method returns true if module does not have a name, false otherwise:
#
# Module.new.anonymous? # => true
#
# module M; end
# M.anonymous? # => false
#
# A module gets a name when it is first assigned to a constant. Either
# via the +module+ or +class+ keyword or by an explicit assignment:
#
# m = Module.new # creates an anonymous module
# m.anonymous? # => true
# M = m # m gets a name here as a side-effect
# m.name # => "M"
# m.anonymous? # => false
def anonymous?
name.nil?
end
end
# frozen_string_literal: true
require_relative "../../abstract_unit"
require "active_support/core_ext/module/anonymous"
class AnonymousTest < ActiveSupport::TestCase
test "an anonymous class or module are anonymous" do
assert_predicate Module.new, :anonymous?
assert_predicate Class.new, :anonymous?
end
test "a named class or module are not anonymous" do
assert_not_predicate Kernel, :anonymous?
assert_not_predicate Object, :anonymous?
end
end
# frozen_string_literal: true
class Fixtures::AnotherClass
end
# frozen_string_literal: true
module ActiveModel
# == Active \Model \API
#
# Includes the required interface for an object to interact with
# Action Pack and Action View, using different Active Model modules.
# It includes model name introspections, conversions, translations, and
# validations. Besides that, it allows you to initialize the object with a
# hash of attributes, pretty much like Active Record does.
#
# A minimal implementation could be:
#
# class Person
# include ActiveModel::API
# attr_accessor :name, :age
# end
#
# person = Person.new(name: 'bob', age: '18')
# person.name # => "bob"
# person.age # => "18"
#
# Note that, by default, <tt>ActiveModel::API</tt> implements <tt>persisted?</tt>
# to return +false+, which is the most common case. You may want to override
# it in your class to simulate a different scenario:
#
# class Person
# include ActiveModel::API
# attr_accessor :id, :name
#
# def persisted?
# self.id.present?
# end
# end
#
# person = Person.new(id: 1, name: 'bob')
# person.persisted? # => true
#
# Also, if for some reason you need to run code on <tt>initialize</tt>, make
# sure you call +super+ if you want the attributes hash initialization to
# happen.
#
# class Person
# include ActiveModel::API
# attr_accessor :id, :name, :omg
#
# def initialize(attributes={})
# super
# @omg ||= true
# end
# end
#
# person = Person.new(id: 1, name: 'bob')
# person.omg # => true
#
# For more detailed information on other functionalities available, please
# refer to the specific modules included in <tt>ActiveModel::API</tt>
# (see below).
module API
extend ActiveSupport::Concern
include ActiveModel::AttributeAssignment
include ActiveModel::Validations
include ActiveModel::Conversion
included do
extend ActiveModel::Naming
extend ActiveModel::Translation
end
# Initializes a new model with the given +params+.
#
# class Person
# include ActiveModel::API
# attr_accessor :name, :age
# end
#
# person = Person.new(name: 'bob', age: '18')
# person.name # => "bob"
# person.age # => "18"
def initialize(attributes = {})
assign_attributes(attributes) if attributes
super()
end
# Indicates if the model is persisted. Default is +false+.
#
# class Person
# include ActiveModel::API
# attr_accessor :id, :name
# end
#
# person = Person.new(id: 1, name: 'bob')
# person.persisted? # => false
def persisted?
false
end
end
end
# frozen_string_literal: true
require "cases/helper"
class APITest < ActiveModel::TestCase
include ActiveModel::Lint::Tests
module DefaultValue
def self.included(klass)
klass.class_eval { attr_accessor :hello }
end
def initialize(*args)
@attr ||= "default value"
super
end
end
class BasicModel
include DefaultValue
include ActiveModel::API
attr_accessor :attr
end
class BasicModelWithReversedMixins
include ActiveModel::API
include DefaultValue
attr_accessor :attr
end
class SimpleModel
include ActiveModel::API
attr_accessor :attr
end
def setup
@model = BasicModel.new
end
def test_initialize_with_params
object = BasicModel.new(attr: "value")
assert_equal "value", object.attr
end
def test_initialize_with_params_and_mixins_reversed
object = BasicModelWithReversedMixins.new(attr: "value")
assert_equal "value", object.attr
end
def test_initialize_with_nil_or_empty_hash_params_does_not_explode
assert_nothing_raised do
BasicModel.new()
BasicModel.new(nil)
BasicModel.new({})
SimpleModel.new(attr: "value")
end
end
def test_persisted_is_always_false
object = BasicModel.new(attr: "value")
assert_not object.persisted?
end
def test_mixin_inclusion_chain
object = BasicModel.new
assert_equal "default value", object.attr
end
def test_mixin_initializer_when_args_exist
object = BasicModel.new(hello: "world")
assert_equal "world", object.hello
end
def test_mixin_initializer_when_args_dont_exist
assert_raises(ActiveModel::UnknownAttributeError) do
SimpleModel.new(hello: "world")
end
end
end
# frozen_string_literal: true
require_relative "boot"
require "rails"
require "active_model/railtie"
require "active_job/railtie"
require "active_record/railtie"
require "action_controller/railtie"
require "action_view/railtie"
require "sprockets/railtie"
require "active_storage/engine"
Bundler.require(*Rails.groups)
module Dummy
class Application < Rails::Application
config.load_defaults Rails::VERSION::STRING.to_f
config.active_storage.service = :local
end
end
# frozen_string_literal: true
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
end
# frozen_string_literal: true
module ApplicationHelper
end
# frozen_string_literal: true
class ApplicationJob < ActiveJob::Base
end
# frozen_string_literal: true
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
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
require "helper"
require "active_job/arguments"
require "models/person"
require "active_support/core_ext/hash/indifferent_access"
require "jobs/kwargs_job"
require "support/stubs/strong_parameters"
class ArgumentSerializationTest < ActiveSupport::TestCase
module ModuleArgument
class ClassArgument; end
end
class ClassArgument; end
setup do
@person = Person.find("5")
end
[ nil, 1, 1.0, 1_000_000_000_000_000_000_000,
"a", true, false, BigDecimal(5),
:a,
1.day,
Date.new(2001, 2, 3),
Time.new(2002, 10, 31, 2, 2, 2.123456789r, "+02:00"),
DateTime.new(2001, 2, 3, 4, 5, 6.123456r, "+03:00"),
ActiveSupport::TimeWithZone.new(Time.utc(1999, 12, 31, 23, 59, "59.123456789".to_r), ActiveSupport::TimeZone["UTC"]),
[ 1, "a" ],
{ "a" => 1 },
ModuleArgument,
ModuleArgument::ClassArgument,
ClassArgument,
1..,
1...,
1..5,
1...5,
"a".."z",
"A".."Z",
Date.new(2001, 2, 3)..,
10.days.ago..Date.today,
Time.new(2002, 10, 31, 2, 2, 2.123456789r, "+02:00")..,
10.hours.ago..Time.current,
DateTime.new(2001, 2, 3, 4, 5, 6.123456r, "+03:00")..,
(DateTime.current - 4.weeks)..DateTime.current,
ActiveSupport::TimeWithZone.new(Time.utc(1999, 12, 31, 23, 59, "59.123456789".to_r), ActiveSupport::TimeZone["UTC"])..,
].each do |arg|
test "serializes #{arg.class} - #{arg.inspect} verbatim" do
assert_arguments_unchanged arg
end
end
[ Object.new, Person.find("5").to_gid, Class.new ].each do |arg|
test "does not serialize #{arg.class}" do
assert_raises ActiveJob::SerializationError do
ActiveJob::Arguments.serialize [ arg ]
end
assert_raises ActiveJob::DeserializationError do
ActiveJob::Arguments.deserialize [ arg ]
end
end
end
test "should convert records to Global IDs" do
assert_arguments_roundtrip [@person]
end
test "should keep Global IDs strings as they are" do
assert_arguments_roundtrip [@person.to_gid.to_s]
end
test "should dive deep into arrays and hashes" do
assert_arguments_roundtrip [3, [@person]]
assert_arguments_roundtrip [{ "a" => @person }]
end
test "should maintain string and symbol keys" do
assert_arguments_roundtrip([a: 1, "b" => 2])
end
test "serialize a ActionController::Parameters" do
parameters = Parameters.new(a: 1)
assert_equal(
{ "a" => 1, "_aj_hash_with_indifferent_access" => true },
ActiveJob::Arguments.serialize([parameters]).first
)
end
test "serialize a hash" do
symbol_key = { a: 1 }
string_key = { "a" => 1 }
indifferent_access = { a: 1 }.with_indifferent_access
assert_equal(
{ "a" => 1, "_aj_symbol_keys" => ["a"] },
ActiveJob::Arguments.serialize([symbol_key]).first
)
assert_equal(
{ "a" => 1, "_aj_symbol_keys" => [] },
ActiveJob::Arguments.serialize([string_key]).first
)
assert_equal(
{ "a" => 1, "_aj_hash_with_indifferent_access" => true },
ActiveJob::Arguments.serialize([indifferent_access]).first
)
end
test "deserialize a hash" do
symbol_key = { "a" => 1, "_aj_symbol_keys" => ["a"] }
string_key = { "a" => 1, "_aj_symbol_keys" => [] }
another_string_key = { "a" => 1 }
indifferent_access = { "a" => 1, "_aj_hash_with_indifferent_access" => true }
indifferent_access_symbol_key = symbol_key.with_indifferent_access
assert_equal(
{ a: 1 },
ActiveJob::Arguments.deserialize([symbol_key]).first
)
assert_equal(
{ "a" => 1 },
ActiveJob::Arguments.deserialize([string_key]).first
)
assert_equal(
{ "a" => 1 },
ActiveJob::Arguments.deserialize([another_string_key]).first
)
assert_equal(
{ "a" => 1 },
ActiveJob::Arguments.deserialize([indifferent_access]).first
)
assert_equal(
{ a: 1 },
ActiveJob::Arguments.deserialize([indifferent_access_symbol_key]).first
)
end
test "should maintain hash with indifferent access" do
symbol_key = { a: 1 }
string_key = { "a" => 1 }
indifferent_access = { a: 1 }.with_indifferent_access
assert_not_instance_of ActiveSupport::HashWithIndifferentAccess, perform_round_trip([symbol_key]).first
assert_not_instance_of ActiveSupport::HashWithIndifferentAccess, perform_round_trip([string_key]).first
assert_instance_of ActiveSupport::HashWithIndifferentAccess, perform_round_trip([indifferent_access]).first
end
test "should maintain time with zone" do
Time.use_zone "Alaska" do
time_with_zone = Time.new(2002, 10, 31, 2, 2, 2).in_time_zone
assert_instance_of ActiveSupport::TimeWithZone, perform_round_trip([time_with_zone]).first
assert_arguments_unchanged time_with_zone
end
end
test "should disallow non-string/symbol hash keys" do
assert_raises ActiveJob::SerializationError do
ActiveJob::Arguments.serialize [ { 1 => 2 } ]
end
assert_raises ActiveJob::SerializationError do
ActiveJob::Arguments.serialize [ { a: [{ 2 => 3 }] } ]
end
end
test "should not allow reserved hash keys" do
["_aj_globalid", :_aj_globalid,
"_aj_symbol_keys", :_aj_symbol_keys,
"_aj_hash_with_indifferent_access", :_aj_hash_with_indifferent_access,
"_aj_serialized", :_aj_serialized].each do |key|
assert_raises ActiveJob::SerializationError do
ActiveJob::Arguments.serialize [key => 1]
end
end
end
test "should not allow non-primitive objects" do
assert_raises ActiveJob::SerializationError do
ActiveJob::Arguments.serialize [Object.new]
end
assert_raises ActiveJob::SerializationError do
ActiveJob::Arguments.serialize [1, [Object.new]]
end
end
test "allows for keyword arguments" do
KwargsJob.perform_now(argument: 2)
assert_equal "Job with argument: 2", JobBuffer.last_value
end
test "raises a friendly SerializationError for records without ids" do
err = assert_raises ActiveJob::SerializationError do
ActiveJob::Arguments.serialize [Person.new(nil)]
end
assert_match "Unable to serialize Person without an id.", err.message
end
private
def assert_arguments_unchanged(*args)
assert_arguments_roundtrip args
end
def assert_arguments_roundtrip(args)
assert_equal args, perform_round_trip(args)
end
def perform_round_trip(args)
ActiveJob::Arguments.deserialize(ActiveJob::Arguments.serialize(args))
end
end
# frozen_string_literal: true
require "bigdecimal"
require "active_support/core_ext/hash"
module ActiveJob
# Raised when an exception is raised during job arguments deserialization.
#
# Wraps the original exception raised as +cause+.
class DeserializationError < StandardError
def initialize # :nodoc:
super("Error while trying to deserialize arguments: #{$!.message}")
set_backtrace $!.backtrace
end
end
# Raised when an unsupported argument type is set as a job argument. We
# currently support String, Integer, Float, NilClass, TrueClass, FalseClass,
# BigDecimal, Symbol, Date, Time, DateTime, ActiveSupport::TimeWithZone,
# ActiveSupport::Duration, Hash, ActiveSupport::HashWithIndifferentAccess,
# Array, Range, or GlobalID::Identification instances, although this can be
# extended by adding custom serializers.
# Raised if you set the key for a Hash something else than a string or
# a symbol. Also raised when trying to serialize an object which can't be
# identified with a GlobalID - such as an unpersisted Active Record model.
class SerializationError < ArgumentError; end
module Arguments
extend self
# Serializes a set of arguments. Intrinsic types that can safely be
# serialized without mutation are returned as-is. Arrays/Hashes are
# serialized element by element. All other types are serialized using
# GlobalID.
def serialize(arguments)
arguments.map { |argument| serialize_argument(argument) }
end
# Deserializes a set of arguments. Intrinsic types that can safely be
# deserialized without mutation are returned as-is. Arrays/Hashes are
# deserialized element by element. All other types are deserialized using
# GlobalID.
def deserialize(arguments)
arguments.map { |argument| deserialize_argument(argument) }
rescue
raise DeserializationError
end
private
# :nodoc:
PERMITTED_TYPES = [ NilClass, String, Integer, Float, BigDecimal, TrueClass, FalseClass ]
# :nodoc:
GLOBALID_KEY = "_aj_globalid"
# :nodoc:
SYMBOL_KEYS_KEY = "_aj_symbol_keys"
# :nodoc:
RUBY2_KEYWORDS_KEY = "_aj_ruby2_keywords"
# :nodoc:
WITH_INDIFFERENT_ACCESS_KEY = "_aj_hash_with_indifferent_access"
# :nodoc:
OBJECT_SERIALIZER_KEY = "_aj_serialized"
# :nodoc:
RESERVED_KEYS = [
GLOBALID_KEY, GLOBALID_KEY.to_sym,
SYMBOL_KEYS_KEY, SYMBOL_KEYS_KEY.to_sym,
RUBY2_KEYWORDS_KEY, RUBY2_KEYWORDS_KEY.to_sym,
OBJECT_SERIALIZER_KEY, OBJECT_SERIALIZER_KEY.to_sym,
WITH_INDIFFERENT_ACCESS_KEY, WITH_INDIFFERENT_ACCESS_KEY.to_sym,
]
private_constant :PERMITTED_TYPES, :RESERVED_KEYS, :GLOBALID_KEY,
:SYMBOL_KEYS_KEY, :RUBY2_KEYWORDS_KEY, :WITH_INDIFFERENT_ACCESS_KEY
unless Hash.respond_to?(:ruby2_keywords_hash?) && Hash.respond_to?(:ruby2_keywords_hash)
using Module.new {
refine Hash do
class << Hash
def ruby2_keywords_hash?(hash)
!new(*[hash]).default.equal?(hash)
end
def ruby2_keywords_hash(hash)
_ruby2_keywords_hash(**hash)
end
private
def _ruby2_keywords_hash(*args)
args.last
end
ruby2_keywords(:_ruby2_keywords_hash)
end
end
}
end
def serialize_argument(argument)
case argument
when *PERMITTED_TYPES
argument
when GlobalID::Identification
convert_to_global_id_hash(argument)
when Array
argument.map { |arg| serialize_argument(arg) }
when ActiveSupport::HashWithIndifferentAccess
serialize_indifferent_hash(argument)
when Hash
symbol_keys = argument.each_key.grep(Symbol).map!(&:to_s)
aj_hash_key = if Hash.ruby2_keywords_hash?(argument)
RUBY2_KEYWORDS_KEY
else
SYMBOL_KEYS_KEY
end
result = serialize_hash(argument)
result[aj_hash_key] = symbol_keys
result
when -> (arg) { arg.respond_to?(:permitted?) }
serialize_indifferent_hash(argument.to_h)
else
Serializers.serialize(argument)
end
end
def deserialize_argument(argument)
case argument
when String
argument
when *PERMITTED_TYPES
argument
when Array
argument.map { |arg| deserialize_argument(arg) }
when Hash
if serialized_global_id?(argument)
deserialize_global_id argument
elsif custom_serialized?(argument)
Serializers.deserialize(argument)
else
deserialize_hash(argument)
end
else
raise ArgumentError, "Can only deserialize primitive arguments: #{argument.inspect}"
end
end
def serialized_global_id?(hash)
hash.size == 1 && hash.include?(GLOBALID_KEY)
end
def deserialize_global_id(hash)
GlobalID::Locator.locate hash[GLOBALID_KEY]
end
def custom_serialized?(hash)
hash.key?(OBJECT_SERIALIZER_KEY)
end
def serialize_hash(argument)
argument.each_with_object({}) do |(key, value), hash|
hash[serialize_hash_key(key)] = serialize_argument(value)
end
end
def deserialize_hash(serialized_hash)
result = serialized_hash.transform_values { |v| deserialize_argument(v) }
if result.delete(WITH_INDIFFERENT_ACCESS_KEY)
result = result.with_indifferent_access
elsif symbol_keys = result.delete(SYMBOL_KEYS_KEY)
result = transform_symbol_keys(result, symbol_keys)
elsif symbol_keys = result.delete(RUBY2_KEYWORDS_KEY)
result = transform_symbol_keys(result, symbol_keys)
result = Hash.ruby2_keywords_hash(result)
end
result
end
def serialize_hash_key(key)
case key
when *RESERVED_KEYS
raise SerializationError.new("Can't serialize a Hash with reserved key #{key.inspect}")
when String, Symbol
key.to_s
else
raise SerializationError.new("Only string and symbol hash keys may be serialized as job arguments, but #{key.inspect} is a #{key.class}")
end
end
def serialize_indifferent_hash(indifferent_hash)
result = serialize_hash(indifferent_hash)
result[WITH_INDIFFERENT_ACCESS_KEY] = serialize_argument(true)
result
end
def transform_symbol_keys(hash, symbol_keys)
# NOTE: HashWithIndifferentAccess#transform_keys always
# returns stringified keys with indifferent access
# so we call #to_h here to ensure keys are symbolized.
hash.to_h.transform_keys do |key|
if symbol_keys.include?(key)
key.to_sym
else
key
end
end
end
def convert_to_global_id_hash(argument)
{ GLOBALID_KEY => argument.to_global_id.to_s }
rescue URI::GID::MissingModelIdError
raise SerializationError, "Unable to serialize #{argument.class} " \
"without an id. (Maybe you forgot to call save?)"
end
end
end
# frozen_string_literal: true
require "active_support/core_ext/array/wrap"
require "active_support/core_ext/array/access"
require "active_support/core_ext/array/conversions"
require "active_support/core_ext/array/deprecated_conversions" unless ENV["RAILS_DISABLE_DEPRECATED_TO_S_CONVERSION"]
require "active_support/core_ext/array/extract"
require "active_support/core_ext/array/extract_options"
require "active_support/core_ext/array/grouping"
require "active_support/core_ext/array/inquiry"
# 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
module ActiveSupport
# Wrapping an array in an +ArrayInquirer+ gives a friendlier way to check
# its string-like contents:
#
# variants = ActiveSupport::ArrayInquirer.new([:phone, :tablet])
#
# variants.phone? # => true
# variants.tablet? # => true
# variants.desktop? # => false
class ArrayInquirer < Array
# Passes each element of +candidates+ collection to ArrayInquirer collection.
# The method returns true if any element from the ArrayInquirer collection
# is equal to the stringified or symbolized form of any element in the +candidates+ collection.
#
# If +candidates+ collection is not given, method returns true.
#
# variants = ActiveSupport::ArrayInquirer.new([:phone, :tablet])
#
# variants.any? # => true
# variants.any?(:phone, :tablet) # => true
# variants.any?('phone', 'desktop') # => true
# variants.any?(:desktop, :watch) # => false
def any?(*candidates)
if candidates.none?
super
else
candidates.any? do |candidate|
include?(candidate.to_sym) || include?(candidate.to_s)
end
end
end
private
def respond_to_missing?(name, include_private = false)
name.end_with?("?") || super
end
def method_missing(name, *args)
if name.end_with?("?")
any?(name[0..-2])
else
super
end
end
end
end
# frozen_string_literal: true
require_relative "abstract_unit"
require "active_support/core_ext/array"
class ArrayInquirerTest < ActiveSupport::TestCase
def setup
@array_inquirer = ActiveSupport::ArrayInquirer.new([:mobile, :tablet, "api"])
end
def test_individual
assert_predicate @array_inquirer, :mobile?
assert_predicate @array_inquirer, :tablet?
assert_not_predicate @array_inquirer, :desktop?
end
def test_any
assert @array_inquirer.any?(:mobile, :desktop)
assert @array_inquirer.any?(:watch, :tablet)
assert_not @array_inquirer.any?(:desktop, :watch)
end
def test_any_string_symbol_mismatch
assert @array_inquirer.any?("mobile")
assert @array_inquirer.any?(:api)
end
def test_any_with_block
assert @array_inquirer.any? { |v| v == :mobile }
assert_not @array_inquirer.any? { |v| v == :desktop }
end
def test_respond_to
assert_respond_to @array_inquirer, :development?
end
def test_inquiry
result = [:mobile, :tablet, "api"].inquiry
assert_instance_of ActiveSupport::ArrayInquirer, result
assert_equal @array_inquirer, result
end
def test_respond_to_fallback_to_array_respond_to
Array.class_eval do
def respond_to_missing?(name, include_private = false)
(name == :foo) || super
end
end
arr = ActiveSupport::ArrayInquirer.new([:x])
assert_respond_to arr, :can_you_hear_me?
assert_respond_to arr, :foo
assert_not_respond_to arr, :nope
ensure
Array.class_eval do
undef_method :respond_to_missing?
def respond_to_missing?(name, include_private = false) # rubocop:disable Lint/DuplicateMethods
super
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
require "active_support/core_ext/enumerable"
module ActiveSupport
module Testing
module Assertions
UNTRACKED = Object.new # :nodoc:
# Asserts that an expression is not truthy. Passes if <tt>object</tt> is
# +nil+ or +false+. "Truthy" means "considered true in a conditional"
# like <tt>if foo</tt>.
#
# assert_not nil # => true
# assert_not false # => true
# assert_not 'foo' # => Expected "foo" to be nil or false
#
# An error message can be specified.
#
# assert_not foo, 'foo should be false'
def assert_not(object, message = nil)
message ||= "Expected #{mu_pp(object)} to be nil or false"
assert !object, message
end
# Assertion that the block should not raise an exception.
#
# Passes if evaluated code in the yielded block raises no exception.
#
# assert_nothing_raised do
# perform_service(param: 'no_exception')
# end
def assert_nothing_raised
yield.tap { assert(true) }
rescue => error
raise Minitest::UnexpectedError.new(error)
end
# Test numeric difference between the return value of an expression as a
# result of what is evaluated in the yielded block.
#
# assert_difference 'Article.count' do
# post :create, params: { article: {...} }
# end
#
# An arbitrary expression is passed in and evaluated.
#
# assert_difference 'Article.last.comments(:reload).size' do
# post :create, params: { comment: {...} }
# end
#
# An arbitrary positive or negative difference can be specified.
# The default is <tt>1</tt>.
#
# assert_difference 'Article.count', -1 do
# post :delete, params: { id: ... }
# end
#
# An array of expressions can also be passed in and evaluated.
#
# assert_difference [ 'Article.count', 'Post.count' ], 2 do
# post :create, params: { article: {...} }
# end
#
# A hash of expressions/numeric differences can also be passed in and evaluated.
#
# assert_difference ->{ Article.count } => 1, ->{ Notification.count } => 2 do
# post :create, params: { article: {...} }
# end
#
# A lambda or a list of lambdas can be passed in and evaluated:
#
# assert_difference ->{ Article.count }, 2 do
# post :create, params: { article: {...} }
# end
#
# assert_difference [->{ Article.count }, ->{ Post.count }], 2 do
# post :create, params: { article: {...} }
# end
#
# An error message can be specified.
#
# assert_difference 'Article.count', -1, 'An Article should be destroyed' do
# post :delete, params: { id: ... }
# end
def assert_difference(expression, *args, &block)
expressions =
if expression.is_a?(Hash)
message = args[0]
expression
else
difference = args[0] || 1
message = args[1]
Array(expression).index_with(difference)
end
exps = expressions.keys.map { |e|
e.respond_to?(:call) ? e : lambda { eval(e, block.binding) }
}
before = exps.map(&:call)
retval = _assert_nothing_raised_or_warn("assert_difference", &block)
expressions.zip(exps, before) do |(code, diff), exp, before_value|
error = "#{code.inspect} didn't change by #{diff}"
error = "#{message}.\n#{error}" if message
assert_equal(before_value + diff, exp.call, error)
end
retval
end
# Assertion that the numeric result of evaluating an expression is not
# changed before and after invoking the passed in block.
#
# assert_no_difference 'Article.count' do
# post :create, params: { article: invalid_attributes }
# end
#
# A lambda can be passed in and evaluated.
#
# assert_no_difference -> { Article.count } do
# post :create, params: { article: invalid_attributes }
# end
#
# An error message can be specified.
#
# assert_no_difference 'Article.count', 'An Article should not be created' do
# post :create, params: { article: invalid_attributes }
# end
#
# An array of expressions can also be passed in and evaluated.
#
# assert_no_difference [ 'Article.count', -> { Post.count } ] do
# post :create, params: { article: invalid_attributes }
# end
def assert_no_difference(expression, message = nil, &block)
assert_difference expression, 0, message, &block
end
# Assertion that the result of evaluating an expression is changed before
# and after invoking the passed in block.
#
# assert_changes 'Status.all_good?' do
# post :create, params: { status: { ok: false } }
# end
#
# You can pass the block as a string to be evaluated in the context of
# the block. A lambda can be passed for the block as well.
#
# assert_changes -> { Status.all_good? } do
# post :create, params: { status: { ok: false } }
# end
#
# The assertion is useful to test side effects. The passed block can be
# anything that can be converted to string with #to_s.
#
# assert_changes :@object do
# @object = 42
# end
#
# The keyword arguments +:from+ and +:to+ can be given to specify the
# expected initial value and the expected value after the block was
# executed.
#
# assert_changes :@object, from: nil, to: :foo do
# @object = :foo
# end
#
# An error message can be specified.
#
# assert_changes -> { Status.all_good? }, 'Expected the status to be bad' do
# post :create, params: { status: { incident: true } }
# end
def assert_changes(expression, message = nil, from: UNTRACKED, to: UNTRACKED, &block)
exp = expression.respond_to?(:call) ? expression : -> { eval(expression.to_s, block.binding) }
before = exp.call
retval = _assert_nothing_raised_or_warn("assert_changes", &block)
unless from == UNTRACKED
error = "Expected change from #{from.inspect}"
error = "#{message}.\n#{error}" if message
assert from === before, error
end
after = exp.call
error = "#{expression.inspect} didn't change"
error = "#{error}. It was already #{to}" if before == to
error = "#{message}.\n#{error}" if message
refute_equal before, after, error
unless to == UNTRACKED
error = "Expected change to #{to}\n"
error = "#{message}.\n#{error}" if message
assert to === after, error
end
retval
end
# Assertion that the result of evaluating an expression is not changed before
# and after invoking the passed in block.
#
# assert_no_changes 'Status.all_good?' do
# post :create, params: { status: { ok: true } }
# end
#
# Provide the optional keyword argument :from to specify the expected
# initial value.
#
# assert_no_changes -> { Status.all_good? }, from: true do
# post :create, params: { status: { ok: true } }
# end
#
# An error message can be specified.
#
# assert_no_changes -> { Status.all_good? }, 'Expected the status to be good' do
# post :create, params: { status: { ok: false } }
# end
def assert_no_changes(expression, message = nil, from: UNTRACKED, &block)
exp = expression.respond_to?(:call) ? expression : -> { eval(expression.to_s, block.binding) }
before = exp.call
retval = _assert_nothing_raised_or_warn("assert_no_changes", &block)
unless from == UNTRACKED
error = "Expected initial value of #{from.inspect}"
error = "#{message}.\n#{error}" if message
assert from === before, error
end
after = exp.call
error = "#{expression.inspect} changed"
error = "#{message}.\n#{error}" if message
if before.nil?
assert_nil after, error
else
assert_equal before, after, error
end
retval
end
private
def _assert_nothing_raised_or_warn(assertion, &block)
assert_nothing_raised(&block)
rescue Minitest::UnexpectedError => e
if tagged_logger && tagged_logger.warn?
warning = <<~MSG
#{self.class} - #{name}: #{e.error.class} raised.
If you expected this exception, use `assert_raises` as near to the code that raises as possible.
Other block based assertions (e.g. `#{assertion}`) can be used, as long as `assert_raises` is inside their block.
MSG
tagged_logger.warn warning
end
raise
end
end
end
end
# frozen_string_literal: true
# Be sure to restart your server when you modify this file.
# Version of your assets, change this if you want to expire all your assets.
Rails.application.config.assets.version = "1.0"
# Add additional assets to the asset load path.
# Rails.application.config.assets.paths << Emoji.images_path
# Add Yarn node_modules folder to the asset load path.
Rails.application.config.assets.paths << Rails.root.join("node_modules")
# Precompile additional assets.
# application.js, application.css, and all non-JS/CSS in the app/assets
# folder are already added.
# Rails.application.config.assets.precompile += %w( admin.js admin.css )
# 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
module AsyncJobsManager
def setup
ActiveJob::Base.queue_adapter = :async
ActiveJob::Base.queue_adapter.immediate = false
end
def clear_jobs
ActiveJob::Base.queue_adapter.shutdown
end
end
# frozen_string_literal: true
require "securerandom"
require "concurrent/scheduled_task"
require "concurrent/executor/thread_pool_executor"
require "concurrent/utility/processor_counter"
module ActiveJob
module QueueAdapters
# == Active Job Async adapter
#
# The Async adapter runs jobs with an in-process thread pool.
#
# This is the default queue adapter. It's well-suited for dev/test since
# it doesn't need an external infrastructure, but it's a poor fit for
# production since it drops pending jobs on restart.
#
# To use this adapter, set queue adapter to +:async+:
#
# config.active_job.queue_adapter = :async
#
# To configure the adapter's thread pool, instantiate the adapter and
# pass your own config:
#
# config.active_job.queue_adapter = ActiveJob::QueueAdapters::AsyncAdapter.new \
# min_threads: 1,
# max_threads: 2 * Concurrent.processor_count,
# idletime: 600.seconds
#
# The adapter uses a {Concurrent Ruby}[https://github.com/ruby-concurrency/concurrent-ruby] thread pool to schedule and execute
# jobs. Since jobs share a single thread pool, long-running jobs will block
# short-lived jobs. Fine for dev/test; bad for production.
class AsyncAdapter
# See {Concurrent::ThreadPoolExecutor}[https://ruby-concurrency.github.io/concurrent-ruby/master/Concurrent/ThreadPoolExecutor.html] for executor options.
def initialize(**executor_options)
@scheduler = Scheduler.new(**executor_options)
end
def enqueue(job) # :nodoc:
@scheduler.enqueue JobWrapper.new(job), queue_name: job.queue_name
end
def enqueue_at(job, timestamp) # :nodoc:
@scheduler.enqueue_at JobWrapper.new(job), timestamp, queue_name: job.queue_name
end
# Gracefully stop processing jobs. Finishes in-progress work and handles
# any new jobs following the executor's fallback policy (`caller_runs`).
# Waits for termination by default. Pass `wait: false` to continue.
def shutdown(wait: true) # :nodoc:
@scheduler.shutdown wait: wait
end
# Used for our test suite.
def immediate=(immediate) # :nodoc:
@scheduler.immediate = immediate
end
# Note that we don't actually need to serialize the jobs since we're
# performing them in-process, but we do so anyway for parity with other
# adapters and deployment environments. Otherwise, serialization bugs
# may creep in undetected.
class JobWrapper # :nodoc:
def initialize(job)
job.provider_job_id = SecureRandom.uuid
@job_data = job.serialize
end
def perform
Base.execute @job_data
end
end
class Scheduler # :nodoc:
DEFAULT_EXECUTOR_OPTIONS = {
min_threads: 0,
max_threads: Concurrent.processor_count,
auto_terminate: true,
idletime: 60, # 1 minute
max_queue: 0, # unlimited
fallback_policy: :caller_runs # shouldn't matter -- 0 max queue
}.freeze
attr_accessor :immediate
def initialize(**options)
self.immediate = false
@immediate_executor = Concurrent::ImmediateExecutor.new
@async_executor = Concurrent::ThreadPoolExecutor.new(DEFAULT_EXECUTOR_OPTIONS.merge(options))
end
def enqueue(job, queue_name:)
executor.post(job, &:perform)
end
def enqueue_at(job, timestamp, queue_name:)
delay = timestamp - Time.current.to_f
if delay > 0
Concurrent::ScheduledTask.execute(delay, args: [job], executor: executor, &:perform)
else
enqueue(job, queue_name: queue_name)
end
end
def shutdown(wait: true)
@async_executor.shutdown
@async_executor.wait_for_termination if wait
end
def executor
immediate ? @immediate_executor : @async_executor
end
end
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
require "fileutils"
class File
# Write to a file atomically. Useful for situations where you don't
# want other processes or threads to see half-written files.
#
# File.atomic_write('important.file') do |file|
# file.write('hello')
# end
#
# This method needs to create a temporary file. By default it will create it
# in the same directory as the destination file. If you don't like this
# behavior you can provide a different directory but it must be on the
# same physical filesystem as the file you're trying to write.
#
# File.atomic_write('/data/something.important', '/data/tmp') do |file|
# file.write('hello')
# end
def self.atomic_write(file_name, temp_dir = dirname(file_name))
require "tempfile" unless defined?(Tempfile)
Tempfile.open(".#{basename(file_name)}", temp_dir) do |temp_file|
temp_file.binmode
return_val = yield temp_file
temp_file.close
old_stat = if exist?(file_name)
# Get original file permissions
stat(file_name)
else
# If not possible, probe which are the default permissions in the
# destination directory.
probe_stat_in(dirname(file_name))
end
if old_stat
# Set correct permissions on new file
begin
chown(old_stat.uid, old_stat.gid, temp_file.path)
# This operation will affect filesystem ACL's
chmod(old_stat.mode, temp_file.path)
rescue Errno::EPERM, Errno::EACCES
# Changing file ownership failed, moving on.
end
end
# Overwrite original file with temp file
rename(temp_file.path, file_name)
return_val
end
end
# Private utility method.
def self.probe_stat_in(dir) # :nodoc:
basename = [
".permissions_check",
Thread.current.object_id,
Process.pid,
rand(1000000)
].join(".")
file_name = join(dir, basename)
FileUtils.touch(file_name)
stat(file_name)
rescue Errno::ENOENT
file_name = nil
ensure
FileUtils.rm_f(file_name) if file_name
end
end
# frozen_string_literal: true
require "active_support/core_ext/module/delegation"
module ActiveStorage
# Abstract base class for the concrete ActiveStorage::Attached::One and ActiveStorage::Attached::Many
# classes that both provide proxy access to the blob association for a record.
class Attached
attr_reader :name, :record
def initialize(name, record)
@name, @record = name, record
end
private
def change
record.attachment_changes[name]
end
end
end
require "active_storage/attached/model"
require "active_storage/attached/one"
require "active_storage/attached/many"
require "active_storage/attached/changes"
# frozen_string_literal: true
require "active_support/core_ext/module/delegation"
# Attachments associate records with blobs. Usually that's a one record-many blobs relationship,
# but it is possible to associate many different records with the same blob. A foreign-key constraint
# on the attachments table prevents blobs from being purged if they’re still attached to any records.
#
# Attachments also have access to all methods from {ActiveStorage::Blob}[rdoc-ref:ActiveStorage::Blob].
#
# If you wish to preload attachments or blobs, you can use these scopes:
#
# # preloads attachments, their corresponding blobs, and variant records (if using `ActiveStorage.track_variants`)
# User.all.with_attached_avatars
#
# # preloads blobs and variant records (if using `ActiveStorage.track_variants`)
# User.first.avatars.with_all_variant_records
class ActiveStorage::Attachment < ActiveStorage::Record
self.table_name = "active_storage_attachments"
belongs_to :record, polymorphic: true, touch: true
belongs_to :blob, class_name: "ActiveStorage::Blob", autosave: true
delegate_missing_to :blob
delegate :signed_id, to: :blob
after_create_commit :mirror_blob_later, :analyze_blob_later
after_destroy_commit :purge_dependent_blob_later
scope :with_all_variant_records, -> { includes(blob: :variant_records) }
# Synchronously deletes the attachment and {purges the blob}[rdoc-ref:ActiveStorage::Blob#purge].
def purge
transaction do
delete
record.touch if record&.persisted?
end
blob&.purge
end
# Deletes the attachment and {enqueues a background job}[rdoc-ref:ActiveStorage::Blob#purge_later] to purge the blob.
def purge_later
transaction do
delete
record.touch if record&.persisted?
end
blob&.purge_later
end
# Returns an ActiveStorage::Variant or ActiveStorage::VariantWithRecord
# instance for the attachment with the set of +transformations+ provided.
# See ActiveStorage::Blob::Representable#variant for more information.
#
# Raises an +ArgumentError+ if +transformations+ is a +Symbol+ which is an
# unknown pre-defined variant of the attachment.
def variant(transformations)
case transformations
when Symbol
variant_name = transformations
transformations = variants.fetch(variant_name) do
record_model_name = record.to_model.model_name.name
raise ArgumentError, "Cannot find variant :#{variant_name} for #{record_model_name}##{name}"
end
end
blob.variant(transformations)
end
private
def analyze_blob_later
blob.analyze_later unless blob.analyzed?
end
def mirror_blob_later
blob.mirror_later
end
def purge_dependent_blob_later
blob&.purge_later if dependent == :purge_later
end
def dependent
record.attachment_reflections[name]&.options&.fetch(:dependent, nil)
end
def variants
record.attachment_reflections[name]&.variants
end
end
ActiveSupport.run_load_hooks :active_storage_attachment, ActiveStorage::Attachment
# frozen_string_literal: true
require "test_helper"
require "database/setup"
require "active_support/testing/method_call_assertions"
class ActiveStorage::AttachmentTest < ActiveSupport::TestCase
include ActiveJob::TestHelper
setup do
@user = User.create!(name: "Josh")
end
teardown { ActiveStorage::Blob.all.each(&:delete) }
test "analyzing a directly-uploaded blob after attaching it" do
blob = directly_upload_file_blob(filename: "racecar.jpg")
assert_not blob.analyzed?
perform_enqueued_jobs do
@user.highlights.attach(blob)
end
assert blob.reload.analyzed?
assert_equal 4104, blob.metadata[:width]
assert_equal 2736, blob.metadata[:height]
end
test "attaching a un-analyzable blob" do
blob = create_blob(filename: "blank.txt")
assert_not_predicate blob, :analyzed?
assert_no_enqueued_jobs do
@user.highlights.attach(blob)
end
assert_predicate blob.reload, :analyzed?
end
test "mirroring a directly-uploaded blob after attaching it" do
with_service("mirror") do
blob = directly_upload_file_blob
assert_not ActiveStorage::Blob.service.mirrors.second.exist?(blob.key)
perform_enqueued_jobs do
@user.highlights.attach(blob)
end
assert ActiveStorage::Blob.service.mirrors.second.exist?(blob.key)
end
end
test "directly-uploaded blob identification for one attached occurs before validation" do
blob = directly_upload_file_blob(filename: "racecar.jpg", content_type: "application/octet-stream")
assert_blob_identified_before_owner_validated(@user, blob, "image/jpeg") do
@user.avatar.attach(blob)
end
end
test "directly-uploaded blob identification for many attached occurs before validation" do
blob = directly_upload_file_blob(filename: "racecar.jpg", content_type: "application/octet-stream")
assert_blob_identified_before_owner_validated(@user, blob, "image/jpeg") do
@user.highlights.attach(blob)
end
end
test "directly-uploaded blob identification for one attached occurs outside transaction" do
blob = directly_upload_file_blob(filename: "racecar.jpg")
assert_blob_identified_outside_transaction(blob) do
@user.avatar.attach(blob)
end
end
test "directly-uploaded blob identification for many attached occurs outside transaction" do
blob = directly_upload_file_blob(filename: "racecar.jpg")
assert_blob_identified_outside_transaction(blob) do
@user.highlights.attach(blob)
end
end
test "getting a signed blob ID from an attachment" do
blob = create_blob
@user.avatar.attach(blob)
signed_id = @user.avatar.signed_id
assert_equal blob, ActiveStorage::Blob.find_signed!(signed_id)
assert_equal blob, ActiveStorage::Blob.find_signed(signed_id)
end
test "getting a signed blob ID from an attachment with a custom purpose" do
blob = create_blob
@user.avatar.attach(blob)
signed_id = @user.avatar.signed_id(purpose: :custom_purpose)
assert_equal blob, ActiveStorage::Blob.find_signed!(signed_id, purpose: :custom_purpose)
end
test "getting a signed blob ID from an attachment with a expires_in" do
blob = create_blob
@user.avatar.attach(blob)
signed_id = @user.avatar.signed_id(expires_in: 1.minute)
assert_equal blob, ActiveStorage::Blob.find_signed!(signed_id)
end
test "fail to find blob within expiration date" do
blob = create_blob
@user.avatar.attach(blob)
signed_id = @user.avatar.signed_id(expires_in: 1.minute)
travel 2.minutes
assert_nil ActiveStorage::Blob.find_signed(signed_id)
end
test "signed blob ID backwards compatibility" do
blob = create_blob
@user.avatar.attach(blob)
signed_id_generated_old_way = ActiveStorage.verifier.generate(@user.avatar.blob.id, purpose: :blob_id)
assert_equal blob, ActiveStorage::Blob.find_signed!(signed_id_generated_old_way)
end
test "attaching with strict_loading and getting a signed blob ID from an attachment" do
blob = create_blob
@user.strict_loading!(true)
@user.avatar.attach(blob)
signed_id = @user.avatar.signed_id
assert_equal blob, ActiveStorage::Blob.find_signed(signed_id)
end
test "can destroy attachment without existing relation" do
blob = create_blob
@user.highlights.attach(blob)
attachment = @user.highlights.find_by(blob_id: blob.id)
attachment.update_attribute(:name, "old_highlights")
assert_nothing_raised { attachment.destroy }
end
private
def assert_blob_identified_before_owner_validated(owner, blob, content_type)
validated_content_type = nil
owner.class.validate do
validated_content_type ||= blob.content_type
end
yield
assert_equal content_type, validated_content_type
assert_equal content_type, blob.reload.content_type
end
def assert_blob_identified_outside_transaction(blob, &block)
baseline_transaction_depth = ActiveRecord::Base.connection.open_transactions
max_transaction_depth = -1
track_transaction_depth = ->(*) do
max_transaction_depth = [ActiveRecord::Base.connection.open_transactions, max_transaction_depth].max
end
blob.stub(:identify_without_saving, track_transaction_depth, &block)
assert_equal 0, (max_transaction_depth - baseline_transaction_depth)
end
end
# frozen_string_literal: true
class Module
# Declares an attribute reader backed by an internally-named instance variable.
def attr_internal_reader(*attrs)
attrs.each { |attr_name| attr_internal_define(attr_name, :reader) }
end
# Declares an attribute writer backed by an internally-named instance variable.
def attr_internal_writer(*attrs)
attrs.each { |attr_name| attr_internal_define(attr_name, :writer) }
end
# Declares an attribute reader and writer backed by an internally-named instance
# variable.
def attr_internal_accessor(*attrs)
attr_internal_reader(*attrs)
attr_internal_writer(*attrs)
end
alias_method :attr_internal, :attr_internal_accessor
class << self; attr_accessor :attr_internal_naming_format end
self.attr_internal_naming_format = "@_%s"
private
def attr_internal_ivar_name(attr)
Module.attr_internal_naming_format % attr
end
def attr_internal_define(attr_name, type)
internal_name = attr_internal_ivar_name(attr_name).delete_prefix("@")
# use native attr_* methods as they are faster on some Ruby implementations
public_send("attr_#{type}", internal_name)
attr_name, internal_name = "#{attr_name}=", "#{internal_name}=" if type == :writer
alias_method attr_name, internal_name
remove_method internal_name
end
end
# frozen_string_literal: true
require_relative "../../abstract_unit"
require "active_support/core_ext/module/attr_internal"
class AttrInternalTest < ActiveSupport::TestCase
def setup
@target = Class.new
@instance = @target.new
end
def test_reader
assert_nothing_raised { @target.attr_internal_reader :foo }
assert_not @instance.instance_variable_defined?("@_foo")
assert_raise(NoMethodError) { @instance.foo = 1 }
@instance.instance_variable_set("@_foo", 1)
assert_nothing_raised { assert_equal 1, @instance.foo }
end
def test_writer
assert_nothing_raised { @target.attr_internal_writer :foo }
assert_not @instance.instance_variable_defined?("@_foo")
assert_nothing_raised { assert_equal 1, @instance.foo = 1 }
assert_equal 1, @instance.instance_variable_get("@_foo")
assert_raise(NoMethodError) { @instance.foo }
end
def test_accessor
assert_nothing_raised { @target.attr_internal :foo }
assert_not @instance.instance_variable_defined?("@_foo")
assert_nothing_raised { assert_equal 1, @instance.foo = 1 }
assert_equal 1, @instance.instance_variable_get("@_foo")
assert_nothing_raised { assert_equal 1, @instance.foo }
end
def test_naming_format
assert_equal "@_%s", Module.attr_internal_naming_format
assert_nothing_raised { Module.attr_internal_naming_format = "@abc%sdef" }
@target.attr_internal :foo
assert_not @instance.instance_variable_defined?("@_foo")
assert_not @instance.instance_variable_defined?("@abcfoodef")
assert_nothing_raised { @instance.foo = 1 }
assert_not @instance.instance_variable_defined?("@_foo")
assert @instance.instance_variable_defined?("@abcfoodef")
ensure
Module.attr_internal_naming_format = "@_%s"
end
end
# frozen_string_literal: true
require "active_support/core_ext/module/redefine_method"
class Class
# Declare a class-level attribute whose value is inheritable by subclasses.
# Subclasses can change their own value and it will not impact parent class.
#
# ==== Options
#
# * <tt>:instance_reader</tt> - Sets the instance reader method (defaults to true).
# * <tt>:instance_writer</tt> - Sets the instance writer method (defaults to true).
# * <tt>:instance_accessor</tt> - Sets both instance methods (defaults to true).
# * <tt>:instance_predicate</tt> - Sets a predicate method (defaults to true).
# * <tt>:default</tt> - Sets a default value for the attribute (defaults to nil).
#
# ==== Examples
#
# class Base
# class_attribute :setting
# end
#
# class Subclass < Base
# end
#
# Base.setting = true
# Subclass.setting # => true
# Subclass.setting = false
# Subclass.setting # => false
# Base.setting # => true
#
# In the above case as long as Subclass does not assign a value to setting
# by performing <tt>Subclass.setting = _something_</tt>, <tt>Subclass.setting</tt>
# would read value assigned to parent class. Once Subclass assigns a value then
# the value assigned by Subclass would be returned.
#
# This matches normal Ruby method inheritance: think of writing an attribute
# on a subclass as overriding the reader method. However, you need to be aware
# when using +class_attribute+ with mutable structures as +Array+ or +Hash+.
# In such cases, you don't want to do changes in place. Instead use setters:
#
# Base.setting = []
# Base.setting # => []
# Subclass.setting # => []
#
# # Appending in child changes both parent and child because it is the same object:
# Subclass.setting << :foo
# Base.setting # => [:foo]
# Subclass.setting # => [:foo]
#
# # Use setters to not propagate changes:
# Base.setting = []
# Subclass.setting += [:foo]
# Base.setting # => []
# Subclass.setting # => [:foo]
#
# For convenience, an instance predicate method is defined as well.
# To skip it, pass <tt>instance_predicate: false</tt>.
#
# Subclass.setting? # => false
#
# Instances may overwrite the class value in the same way:
#
# Base.setting = true
# object = Base.new
# object.setting # => true
# object.setting = false
# object.setting # => false
# Base.setting # => true
#
# To opt out of the instance reader method, pass <tt>instance_reader: false</tt>.
#
# object.setting # => NoMethodError
# object.setting? # => NoMethodError
#
# To opt out of the instance writer method, pass <tt>instance_writer: false</tt>.
#
# object.setting = false # => NoMethodError
#
# To opt out of both instance methods, pass <tt>instance_accessor: false</tt>.
#
# To set a default value for the attribute, pass <tt>default:</tt>, like so:
#
# class_attribute :settings, default: {}
def class_attribute(*attrs, instance_accessor: true,
instance_reader: instance_accessor, instance_writer: instance_accessor, instance_predicate: true, default: nil)
class_methods, methods = [], []
attrs.each do |name|
unless name.is_a?(Symbol) || name.is_a?(String)
raise TypeError, "#{name.inspect} is not a symbol nor a string"
end
class_methods << <<~RUBY # In case the method exists and is not public
silence_redefinition_of_method def #{name}
end
RUBY
methods << <<~RUBY if instance_reader
silence_redefinition_of_method def #{name}
defined?(@#{name}) ? @#{name} : self.class.#{name}
end
RUBY
class_methods << <<~RUBY
silence_redefinition_of_method def #{name}=(value)
redefine_method(:#{name}) { value } if singleton_class?
redefine_singleton_method(:#{name}) { value }
value
end
RUBY
methods << <<~RUBY if instance_writer
silence_redefinition_of_method(:#{name}=)
attr_writer :#{name}
RUBY
if instance_predicate
class_methods << "silence_redefinition_of_method def #{name}?; !!self.#{name}; end"
if instance_reader
methods << "silence_redefinition_of_method def #{name}?; !!self.#{name}; end"
end
end
end
location = caller_locations(1, 1).first
class_eval(["class << self", *class_methods, "end", *methods].join(";").tr("\n", ";"), location.path, location.lineno)
attrs.each { |name| public_send("#{name}=", default) }
end
end
# frozen_string_literal: true
require_relative "../../abstract_unit"
require "active_support/core_ext/module/attribute_accessors_per_thread"
class ModuleAttributeAccessorPerThreadTest < ActiveSupport::TestCase
class MyClass
thread_mattr_accessor :foo
thread_mattr_accessor :bar, instance_writer: false
thread_mattr_reader :shaq, instance_reader: false
thread_mattr_accessor :camp, instance_accessor: false
end
class SubMyClass < MyClass
end
setup do
@class = MyClass
@subclass = SubMyClass
@object = @class.new
end
def test_is_shared_between_fibers
@class.foo = 42
enumerator = Enumerator.new do |yielder|
yielder.yield @class.foo
end
assert_equal 42, enumerator.next
end
def test_is_not_shared_between_fibers_if_isolation_level_is_fiber
previous_level = ActiveSupport::IsolatedExecutionState.isolation_level
ActiveSupport::IsolatedExecutionState.isolation_level = :fiber
@class.foo = 42
enumerator = Enumerator.new do |yielder|
yielder.yield @class.foo
end
assert_nil enumerator.next
ensure
ActiveSupport::IsolatedExecutionState.isolation_level = previous_level
end
def test_can_initialize_with_default_value
Thread.new do
@class.thread_mattr_accessor :baz, default: "default_value"
assert_equal "default_value", @class.baz
end.join
assert_nil @class.baz
end
def test_should_use_mattr_default
Thread.new do
assert_nil @class.foo
assert_nil @object.foo
end.join
end
def test_should_set_mattr_value
Thread.new do
@class.foo = :test
assert_equal :test, @class.foo
@class.foo = :test2
assert_equal :test2, @class.foo
end.join
end
def test_should_not_create_instance_writer
Thread.new do
assert_respond_to @class, :foo
assert_respond_to @class, :foo=
assert_respond_to @object, :bar
assert_not_respond_to @object, :bar=
end.join
end
def test_should_not_create_instance_reader
Thread.new do
assert_respond_to @class, :shaq
assert_not_respond_to @object, :shaq
end.join
end
def test_should_not_create_instance_accessors
Thread.new do
assert_respond_to @class, :camp
assert_not_respond_to @object, :camp
assert_not_respond_to @object, :camp=
end.join
end
def test_values_should_not_bleed_between_threads
threads = []
threads << Thread.new do
@class.foo = "things"
Thread.pass
assert_equal "things", @class.foo
end
threads << Thread.new do
@class.foo = "other things"
Thread.pass
assert_equal "other things", @class.foo
end
threads << Thread.new do
@class.foo = "really other things"
Thread.pass
assert_equal "really other things", @class.foo
end
threads.each(&:join)
end
def test_should_raise_name_error_if_attribute_name_is_invalid
exception = assert_raises NameError do
Class.new do
thread_cattr_reader "1nvalid"
end
end
assert_match "invalid attribute name: 1nvalid", exception.message
exception = assert_raises NameError do
Class.new do
thread_cattr_writer "1nvalid"
end
end
assert_match "invalid attribute name: 1nvalid", exception.message
exception = assert_raises NameError do
Class.new do
thread_mattr_reader "1valid_part"
end
end
assert_match "invalid attribute name: 1valid_part", exception.message
exception = assert_raises NameError do
Class.new do
thread_mattr_writer "2valid_part"
end
end
assert_match "invalid attribute name: 2valid_part", exception.message
end
def test_should_return_same_value_by_class_or_instance_accessor
@class.foo = "fries"
assert_equal @class.foo, @object.foo
end
def test_should_not_affect_superclass_if_subclass_set_value
@class.foo = "super"
assert_equal "super", @class.foo
assert_nil @subclass.foo
@subclass.foo = "sub"
assert_equal "super", @class.foo
assert_equal "sub", @subclass.foo
end
end
# frozen_string_literal: true
require_relative "../../abstract_unit"
require "active_support/core_ext/module/attribute_accessors"
class ModuleAttributeAccessorTest < ActiveSupport::TestCase
def setup
m = @module = Module.new do
mattr_accessor :foo
mattr_accessor :bar, instance_writer: false
mattr_reader :shaq, instance_reader: false
mattr_accessor :camp, instance_accessor: false
cattr_accessor(:defa) { "default_accessor_value" }
cattr_reader(:defr) { "default_reader_value" }
cattr_writer(:defw) { "default_writer_value" }
cattr_accessor(:deff) { false }
cattr_accessor(:quux) { :quux }
cattr_accessor :def_accessor, default: "default_accessor_value"
cattr_reader :def_reader, default: "default_reader_value"
cattr_writer :def_writer, default: "default_writer_value"
cattr_accessor :def_false, default: false
cattr_accessor(:def_priority, default: false) { :no_priority }
end
@class = Class.new
@class.instance_eval { include m }
@object = @class.new
end
def test_should_use_mattr_default
assert_nil @module.foo
assert_nil @object.foo
end
def test_mattr_default_keyword_arguments
assert_equal "default_accessor_value", @module.def_accessor
assert_equal "default_reader_value", @module.def_reader
assert_equal "default_writer_value", @module.class_variable_get(:@@def_writer)
end
def test_mattr_can_default_to_false
assert_equal false, @module.def_false
assert_equal false, @module.deff
end
def test_mattr_default_priority
assert_equal false, @module.def_priority
end
def test_should_set_mattr_value
@module.foo = :test
assert_equal :test, @object.foo
@object.foo = :test2
assert_equal :test2, @module.foo
end
def test_cattr_accessor_default_value
assert_equal :quux, @module.quux
assert_equal :quux, @object.quux
end
def test_should_not_create_instance_writer
assert_respond_to @module, :foo
assert_respond_to @module, :foo=
assert_respond_to @object, :bar
assert_not_respond_to @object, :bar=
end
def test_should_not_create_instance_reader
assert_respond_to @module, :shaq
assert_not_respond_to @object, :shaq
end
def test_should_not_create_instance_accessors
assert_respond_to @module, :camp
assert_not_respond_to @object, :camp
assert_not_respond_to @object, :camp=
end
def test_should_raise_name_error_if_attribute_name_is_invalid
exception = assert_raises NameError do
Class.new do
cattr_reader "1nvalid"
end
end
assert_match "invalid attribute name: 1nvalid", exception.message
exception = assert_raises NameError do
Class.new do
cattr_writer "1nvalid"
end
end
assert_match "invalid attribute name: 1nvalid", exception.message
exception = assert_raises NameError do
Class.new do
mattr_reader "valid_part\ninvalid_part"
end
end
assert_match "invalid attribute name: valid_part\ninvalid_part", exception.message
exception = assert_raises NameError do
Class.new do
mattr_writer "valid_part\ninvalid_part"
end
end
assert_match "invalid attribute name: valid_part\ninvalid_part", exception.message
end
def test_should_use_default_value_if_block_passed
assert_equal "default_accessor_value", @module.defa
assert_equal "default_reader_value", @module.defr
assert_equal "default_writer_value", @module.class_variable_get("@@defw")
end
def test_method_invocation_should_not_invoke_the_default_block
count = 0
@module.cattr_accessor(:defcount) { count += 1 }
assert_equal 1, count
assert_no_difference "count" do
@module.defcount
end
end
def test_declaring_multiple_attributes_at_once_invokes_the_block_multiple_times
count = 0
@module.cattr_accessor(:defn1, :defn2) { count += 1 }
assert_equal 1, @module.defn1
assert_equal 2, @module.defn2
end
def test_declaring_attributes_on_singleton_errors
klass = Class.new
ex = assert_raises TypeError do
class << klass
mattr_accessor :my_attr
end
end
assert_equal "module attributes should be defined directly on class, not singleton", ex.message
assert_not_includes Module.class_variables, :@@my_attr
end
end
# frozen_string_literal: true
# == Attribute Accessors
#
# Extends the module object with class/module and instance accessors for
# class/module attributes, just like the native attr* accessors for instance
# attributes.
class Module
# Defines a class attribute and creates a class and instance reader methods.
# The underlying class variable is set to +nil+, if it is not previously
# defined. All class and instance methods created will be public, even if
# this method is called with a private or protected access modifier.
#
# module HairColors
# mattr_reader :hair_colors
# end
#
# HairColors.hair_colors # => nil
# HairColors.class_variable_set("@@hair_colors", [:brown, :black])
# HairColors.hair_colors # => [:brown, :black]
#
# The attribute name must be a valid method name in Ruby.
#
# module Foo
# mattr_reader :"1_Badname"
# end
# # => NameError: invalid attribute name: 1_Badname
#
# To omit the instance reader method, pass
# <tt>instance_reader: false</tt> or <tt>instance_accessor: false</tt>.
#
# module HairColors
# mattr_reader :hair_colors, instance_reader: false
# end
#
# class Person
# include HairColors
# end
#
# Person.new.hair_colors # => NoMethodError
#
# You can set a default value for the attribute.
#
# module HairColors
# mattr_reader :hair_colors, default: [:brown, :black, :blonde, :red]
# end
#
# class Person
# include HairColors
# end
#
# Person.new.hair_colors # => [:brown, :black, :blonde, :red]
def mattr_reader(*syms, instance_reader: true, instance_accessor: true, default: nil, location: nil)
raise TypeError, "module attributes should be defined directly on class, not singleton" if singleton_class?
location ||= caller_locations(1, 1).first
definition = []
syms.each do |sym|
raise NameError.new("invalid attribute name: #{sym}") unless /\A[_A-Za-z]\w*\z/.match?(sym)
definition << "def self.#{sym}; @@#{sym}; end"
if instance_reader && instance_accessor
definition << "def #{sym}; @@#{sym}; end"
end
sym_default_value = (block_given? && default.nil?) ? yield : default
class_variable_set("@@#{sym}", sym_default_value) unless sym_default_value.nil? && class_variable_defined?("@@#{sym}")
end
module_eval(definition.join(";"), location.path, location.lineno)
end
alias :cattr_reader :mattr_reader
# Defines a class attribute and creates a class and instance writer methods to
# allow assignment to the attribute. All class and instance methods created
# will be public, even if this method is called with a private or protected
# access modifier.
#
# module HairColors
# mattr_writer :hair_colors
# end
#
# class Person
# include HairColors
# end
#
# HairColors.hair_colors = [:brown, :black]
# Person.class_variable_get("@@hair_colors") # => [:brown, :black]
# Person.new.hair_colors = [:blonde, :red]
# HairColors.class_variable_get("@@hair_colors") # => [:blonde, :red]
#
# To omit the instance writer method, pass
# <tt>instance_writer: false</tt> or <tt>instance_accessor: false</tt>.
#
# module HairColors
# mattr_writer :hair_colors, instance_writer: false
# end
#
# class Person
# include HairColors
# end
#
# Person.new.hair_colors = [:blonde, :red] # => NoMethodError
#
# You can set a default value for the attribute.
#
# module HairColors
# mattr_writer :hair_colors, default: [:brown, :black, :blonde, :red]
# end
#
# class Person
# include HairColors
# end
#
# Person.class_variable_get("@@hair_colors") # => [:brown, :black, :blonde, :red]
def mattr_writer(*syms, instance_writer: true, instance_accessor: true, default: nil, location: nil)
raise TypeError, "module attributes should be defined directly on class, not singleton" if singleton_class?
location ||= caller_locations(1, 1).first
definition = []
syms.each do |sym|
raise NameError.new("invalid attribute name: #{sym}") unless /\A[_A-Za-z]\w*\z/.match?(sym)
definition << "def self.#{sym}=(val); @@#{sym} = val; end"
if instance_writer && instance_accessor
definition << "def #{sym}=(val); @@#{sym} = val; end"
end
sym_default_value = (block_given? && default.nil?) ? yield : default
class_variable_set("@@#{sym}", sym_default_value) unless sym_default_value.nil? && class_variable_defined?("@@#{sym}")
end
module_eval(definition.join(";"), location.path, location.lineno)
end
alias :cattr_writer :mattr_writer
# Defines both class and instance accessors for class attributes.
# All class and instance methods created will be public, even if
# this method is called with a private or protected access modifier.
#
# module HairColors
# mattr_accessor :hair_colors
# end
#
# class Person
# include HairColors
# end
#
# HairColors.hair_colors = [:brown, :black, :blonde, :red]
# HairColors.hair_colors # => [:brown, :black, :blonde, :red]
# Person.new.hair_colors # => [:brown, :black, :blonde, :red]
#
# If a subclass changes the value then that would also change the value for
# parent class. Similarly if parent class changes the value then that would
# change the value of subclasses too.
#
# class Citizen < Person
# end
#
# Citizen.new.hair_colors << :blue
# Person.new.hair_colors # => [:brown, :black, :blonde, :red, :blue]
#
# To omit the instance writer method, pass <tt>instance_writer: false</tt>.
# To omit the instance reader method, pass <tt>instance_reader: false</tt>.
#
# module HairColors
# mattr_accessor :hair_colors, instance_writer: false, instance_reader: false
# end
#
# class Person
# include HairColors
# end
#
# Person.new.hair_colors = [:brown] # => NoMethodError
# Person.new.hair_colors # => NoMethodError
#
# Or pass <tt>instance_accessor: false</tt>, to omit both instance methods.
#
# module HairColors
# mattr_accessor :hair_colors, instance_accessor: false
# end
#
# class Person
# include HairColors
# end
#
# Person.new.hair_colors = [:brown] # => NoMethodError
# Person.new.hair_colors # => NoMethodError
#
# You can set a default value for the attribute.
#
# module HairColors
# mattr_accessor :hair_colors, default: [:brown, :black, :blonde, :red]
# end
#
# class Person
# include HairColors
# end
#
# Person.class_variable_get("@@hair_colors") # => [:brown, :black, :blonde, :red]
def mattr_accessor(*syms, instance_reader: true, instance_writer: true, instance_accessor: true, default: nil, &blk)
location = caller_locations(1, 1).first
mattr_reader(*syms, instance_reader: instance_reader, instance_accessor: instance_accessor, default: default, location: location, &blk)
mattr_writer(*syms, instance_writer: instance_writer, instance_accessor: instance_accessor, default: default, location: location)
end
alias :cattr_accessor :mattr_accessor
end
# frozen_string_literal: true
# == Attribute Accessors per Thread
#
# Extends the module object with class/module and instance accessors for
# class/module attributes, just like the native attr* accessors for instance
# attributes, but does so on a per-thread basis.
#
# So the values are scoped within the Thread.current space under the class name
# of the module.
#
# Note that it can also be scoped per-fiber if +Rails.application.config.active_support.isolation_level+
# is set to +:fiber+.
class Module
# Defines a per-thread class attribute and creates class and instance reader methods.
# The underlying per-thread class variable is set to +nil+, if it is not previously defined.
#
# module Current
# thread_mattr_reader :user
# end
#
# Current.user = "DHH"
# Current.user # => "DHH"
# Thread.new { Current.user }.value # => nil
#
# The attribute name must be a valid method name in Ruby.
#
# module Foo
# thread_mattr_reader :"1_Badname"
# end
# # => NameError: invalid attribute name: 1_Badname
#
# To omit the instance reader method, pass
# <tt>instance_reader: false</tt> or <tt>instance_accessor: false</tt>.
#
# class Current
# thread_mattr_reader :user, instance_reader: false
# end
#
# Current.new.user # => NoMethodError
def thread_mattr_reader(*syms, instance_reader: true, instance_accessor: true, default: nil) # :nodoc:
syms.each do |sym|
raise NameError.new("invalid attribute name: #{sym}") unless /^[_A-Za-z]\w*$/.match?(sym)
# The following generated method concatenates `name` because we want it
# to work with inheritance via polymorphism.
class_eval(<<-EOS, __FILE__, __LINE__ + 1)
def self.#{sym}
@__thread_mattr_#{sym} ||= "attr_\#{name}_#{sym}"
::ActiveSupport::IsolatedExecutionState[@__thread_mattr_#{sym}]
end
EOS
if instance_reader && instance_accessor
class_eval(<<-EOS, __FILE__, __LINE__ + 1)
def #{sym}
self.class.#{sym}
end
EOS
end
::ActiveSupport::IsolatedExecutionState["attr_#{name}_#{sym}"] = default unless default.nil?
end
end
alias :thread_cattr_reader :thread_mattr_reader
# Defines a per-thread class attribute and creates a class and instance writer methods to
# allow assignment to the attribute.
#
# module Current
# thread_mattr_writer :user
# end
#
# Current.user = "DHH"
# Thread.current[:attr_Current_user] # => "DHH"
#
# To omit the instance writer method, pass
# <tt>instance_writer: false</tt> or <tt>instance_accessor: false</tt>.
#
# class Current
# thread_mattr_writer :user, instance_writer: false
# end
#
# Current.new.user = "DHH" # => NoMethodError
def thread_mattr_writer(*syms, instance_writer: true, instance_accessor: true, default: nil) # :nodoc:
syms.each do |sym|
raise NameError.new("invalid attribute name: #{sym}") unless /^[_A-Za-z]\w*$/.match?(sym)
# The following generated method concatenates `name` because we want it
# to work with inheritance via polymorphism.
class_eval(<<-EOS, __FILE__, __LINE__ + 1)
def self.#{sym}=(obj)
@__thread_mattr_#{sym} ||= "attr_\#{name}_#{sym}"
::ActiveSupport::IsolatedExecutionState[@__thread_mattr_#{sym}] = obj
end
EOS
if instance_writer && instance_accessor
class_eval(<<-EOS, __FILE__, __LINE__ + 1)
def #{sym}=(obj)
self.class.#{sym} = obj
end
EOS
end
public_send("#{sym}=", default) unless default.nil?
end
end
alias :thread_cattr_writer :thread_mattr_writer
# Defines both class and instance accessors for class attributes.
#
# class Account
# thread_mattr_accessor :user
# end
#
# Account.user = "DHH"
# Account.user # => "DHH"
# Account.new.user # => "DHH"
#
# Unlike +mattr_accessor+, values are *not* shared with subclasses or parent classes.
# If a subclass changes the value, the parent class' value is not changed.
# If the parent class changes the value, the value of subclasses is not changed.
#
# class Customer < Account
# end
#
# Account.user # => "DHH"
# Customer.user # => nil
# Customer.user = "Rafael"
# Customer.user # => "Rafael"
# Account.user # => "DHH"
#
# To omit the instance writer method, pass <tt>instance_writer: false</tt>.
# To omit the instance reader method, pass <tt>instance_reader: false</tt>.
#
# class Current
# thread_mattr_accessor :user, instance_writer: false, instance_reader: false
# end
#
# Current.new.user = "DHH" # => NoMethodError
# Current.new.user # => NoMethodError
#
# Or pass <tt>instance_accessor: false</tt>, to omit both instance methods.
#
# class Current
# thread_mattr_accessor :user, instance_accessor: false
# end
#
# Current.new.user = "DHH" # => NoMethodError
# Current.new.user # => NoMethodError
def thread_mattr_accessor(*syms, instance_reader: true, instance_writer: true, instance_accessor: true, default: nil)
thread_mattr_reader(*syms, instance_reader: instance_reader, instance_accessor: instance_accessor, default: default)
thread_mattr_writer(*syms, instance_writer: instance_writer, instance_accessor: instance_accessor)
end
alias :thread_cattr_accessor :thread_mattr_accessor
end
# frozen_string_literal: true
require_relative "../../abstract_unit"
require "active_support/core_ext/module/aliasing"
module AttributeAliasing
class Content
attr_accessor :title, :Data
def initialize
@title, @Data = nil, nil
end
def title?
!title.nil?
end
def Data?
!self.Data.nil?
end
end
class Email < Content
alias_attribute :subject, :title
alias_attribute :body, :Data
end
end
class AttributeAliasingTest < ActiveSupport::TestCase
def test_attribute_alias
e = AttributeAliasing::Email.new
assert_not_predicate e, :subject?
e.title = "Upgrade computer"
assert_equal "Upgrade computer", e.subject
assert_predicate e, :subject?
e.subject = "We got a long way to go"
assert_equal "We got a long way to go", e.title
assert_predicate e, :title?
end
def test_aliasing_to_uppercase_attributes
# Although it's very un-Ruby, some people's AR-mapped tables have
# upper-case attributes, and when people want to alias those names
# to more sensible ones, everything goes *foof*.
e = AttributeAliasing::Email.new
assert_not_predicate e, :body?
assert_not_predicate e, :Data?
e.body = "No, really, this is not a joke."
assert_equal "No, really, this is not a joke.", e.Data
assert_predicate e, :Data?
e.Data = "Uppercased methods are the suck"
assert_equal "Uppercased methods are the suck", e.body
assert_predicate e, :body?
end
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 "cases/helper"
require "active_support/core_ext/hash/indifferent_access"
require "active_support/hash_with_indifferent_access"
class AttributeAssignmentTest < ActiveModel::TestCase
class Model
include ActiveModel::AttributeAssignment
attr_accessor :name, :description
def initialize(attributes = {})
assign_attributes(attributes)
end
def broken_attribute=(value)
raise ErrorFromAttributeWriter
end
private
attr_writer :metadata
end
class ErrorFromAttributeWriter < StandardError
end
class ProtectedParams
attr_accessor :permitted
alias :permitted? :permitted
delegate :keys, :key?, :has_key?, :empty?, to: :@parameters
def initialize(attributes)
@parameters = attributes.with_indifferent_access
@permitted = false
end
def permit!
@permitted = true
self
end
def [](key)
@parameters[key]
end
def to_h
@parameters
end
def each_pair(&block)
@parameters.each_pair(&block)
end
def dup
super.tap do |duplicate|
duplicate.instance_variable_set :@permitted, permitted?
end
end
end
test "simple assignment" do
model = Model.new
model.assign_attributes(name: "hello", description: "world")
assert_equal "hello", model.name
assert_equal "world", model.description
end
test "simple assignment alias" do
model = Model.new
model.attributes = { name: "hello", description: "world" }
assert_equal "hello", model.name
assert_equal "world", model.description
end
test "assign non-existing attribute" do
model = Model.new
error = assert_raises(ActiveModel::UnknownAttributeError) do
model.assign_attributes(hz: 1)
end
assert_equal model, error.record
assert_equal "hz", error.attribute
end
test "assign private attribute" do
model = Model.new
assert_raises(ActiveModel::UnknownAttributeError) do
model.assign_attributes(metadata: { a: 1 })
end
end
test "does not swallow errors raised in an attribute writer" do
assert_raises(ErrorFromAttributeWriter) do
Model.new(broken_attribute: 1)
end
end
test "an ArgumentError is raised if a non-hash-like object is passed" do
err = assert_raises(ArgumentError) do
Model.new(1)
end
assert_equal("When assigning attributes, you must pass a hash as an argument, Integer passed.", err.message)
end
test "forbidden attributes cannot be used for mass assignment" do
params = ProtectedParams.new(name: "Guille", description: "m")
assert_raises(ActiveModel::ForbiddenAttributesError) do
Model.new(params)
end
end
test "permitted attributes can be used for mass assignment" do
params = ProtectedParams.new(name: "Guille", description: "desc")
params.permit!
model = Model.new(params)
assert_equal "Guille", model.name
assert_equal "desc", model.description
end
test "regular hash should still be used for mass assignment" do
model = Model.new(name: "Guille", description: "m")
assert_equal "Guille", model.name
assert_equal "m", model.description
end
test "assigning no attributes should not raise, even if the hash is un-permitted" do
model = Model.new
assert_nil model.assign_attributes(ProtectedParams.new({}))
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 "active_support/core_ext/hash/indifferent_access"
require "active_support/core_ext/object/duplicable"
module ActiveModel
class AttributeMutationTracker # :nodoc:
OPTION_NOT_GIVEN = Object.new
def initialize(attributes)
@attributes = attributes
end
def changed_attribute_names
attr_names.select { |attr_name| changed?(attr_name) }
end
def changed_values
attr_names.each_with_object({}.with_indifferent_access) do |attr_name, result|
if changed?(attr_name)
result[attr_name] = original_value(attr_name)
end
end
end
def changes
attr_names.each_with_object({}.with_indifferent_access) do |attr_name, result|
if change = change_to_attribute(attr_name)
result.merge!(attr_name => change)
end
end
end
def change_to_attribute(attr_name)
if changed?(attr_name)
[original_value(attr_name), fetch_value(attr_name)]
end
end
def any_changes?
attr_names.any? { |attr| changed?(attr) }
end
def changed?(attr_name, from: OPTION_NOT_GIVEN, to: OPTION_NOT_GIVEN)
attribute_changed?(attr_name) &&
(OPTION_NOT_GIVEN == from || original_value(attr_name) == from) &&
(OPTION_NOT_GIVEN == to || fetch_value(attr_name) == to)
end
def changed_in_place?(attr_name)
attributes[attr_name].changed_in_place?
end
def forget_change(attr_name)
attributes[attr_name] = attributes[attr_name].forgetting_assignment
forced_changes.delete(attr_name)
end
def original_value(attr_name)
attributes[attr_name].original_value
end
def force_change(attr_name)
forced_changes[attr_name] = fetch_value(attr_name)
end
private
attr_reader :attributes
def forced_changes
@forced_changes ||= {}
end
def attr_names
attributes.keys
end
def attribute_changed?(attr_name)
forced_changes.include?(attr_name) || !!attributes[attr_name].changed?
end
def fetch_value(attr_name)
attributes.fetch_value(attr_name)
end
end
class ForcedMutationTracker < AttributeMutationTracker # :nodoc:
def initialize(attributes)
super
@finalized_changes = nil
end
def changed_in_place?(attr_name)
false
end
def change_to_attribute(attr_name)
if finalized_changes&.include?(attr_name)
finalized_changes[attr_name].dup
else
super
end
end
def forget_change(attr_name)
forced_changes.delete(attr_name)
end
def original_value(attr_name)
if changed?(attr_name)
forced_changes[attr_name]
else
fetch_value(attr_name)
end
end
def force_change(attr_name)
forced_changes[attr_name] = clone_value(attr_name) unless attribute_changed?(attr_name)
end
def finalize_changes
@finalized_changes = changes
end
private
attr_reader :finalized_changes
def attr_names
forced_changes.keys
end
def attribute_changed?(attr_name)
forced_changes.include?(attr_name)
end
def fetch_value(attr_name)
attributes.send(:_read_attribute, attr_name)
end
def clone_value(attr_name)
value = fetch_value(attr_name)
value.duplicable? ? value.clone : value
rescue TypeError, NoMethodError
value
end
end
class NullMutationTracker # :nodoc:
include Singleton
def changed_attribute_names
[]
end
def changed_values
{}
end
def changes
{}
end
def change_to_attribute(attr_name)
end
def any_changes?
false
end
def changed?(attr_name, **)
false
end
def changed_in_place?(attr_name)
false
end
def original_value(attr_name)
end
end
end
# frozen_string_literal: true
require "active_support/core_ext/enumerable"
require "active_support/core_ext/object/deep_dup"
require "active_model/attribute_set/builder"
require "active_model/attribute_set/yaml_encoder"
module ActiveModel
class AttributeSet # :nodoc:
delegate :each_value, :fetch, :except, to: :attributes
def initialize(attributes)
@attributes = attributes
end
def [](name)
@attributes[name] || default_attribute(name)
end
def []=(name, value)
@attributes[name] = value
end
def values_before_type_cast
attributes.transform_values(&:value_before_type_cast)
end
def values_for_database
attributes.transform_values(&:value_for_database)
end
def to_hash
keys.index_with { |name| self[name].value }
end
alias :to_h :to_hash
def key?(name)
attributes.key?(name) && self[name].initialized?
end
def keys
attributes.each_key.select { |name| self[name].initialized? }
end
def fetch_value(name, &block)
self[name].value(&block)
end
def write_from_database(name, value)
@attributes[name] = self[name].with_value_from_database(value)
end
def write_from_user(name, value)
raise FrozenError, "can't modify frozen attributes" if frozen?
@attributes[name] = self[name].with_value_from_user(value)
value
end
def write_cast_value(name, value)
@attributes[name] = self[name].with_cast_value(value)
end
def freeze
attributes.freeze
super
end
def deep_dup
AttributeSet.new(attributes.deep_dup)
end
def initialize_dup(_)
@attributes = @attributes.dup
super
end
def initialize_clone(_)
@attributes = @attributes.clone
super
end
def reset(key)
if key?(key)
write_from_database(key, nil)
end
end
def accessed
attributes.each_key.select { |name| self[name].has_been_read? }
end
def map(&block)
new_attributes = attributes.transform_values(&block)
AttributeSet.new(new_attributes)
end
def ==(other)
attributes == other.attributes
end
protected
attr_reader :attributes
private
def default_attribute(name)
Attribute.null(name)
end
end
end
# frozen_string_literal: true
require "cases/helper"
require "active_model/attribute_set"
module ActiveModel
class AttributeSetTest < ActiveModel::TestCase
test "building a new set from raw attributes" do
builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new)
attributes = builder.build_from_database(foo: "1.1", bar: "2.2")
assert_equal 1, attributes[:foo].value
assert_equal 2.2, attributes[:bar].value
assert_equal :foo, attributes[:foo].name
assert_equal :bar, attributes[:bar].name
end
test "building with custom types" do
builder = AttributeSet::Builder.new(foo: Type::Float.new)
attributes = builder.build_from_database({ foo: "3.3", bar: "4.4" }, { bar: Type::Integer.new })
assert_equal 3.3, attributes[:foo].value
assert_equal 4, attributes[:bar].value
end
test "[] returns a null object" do
builder = AttributeSet::Builder.new(foo: Type::Float.new)
attributes = builder.build_from_database(foo: "3.3")
assert_equal "3.3", attributes[:foo].value_before_type_cast
assert_nil attributes[:bar].value_before_type_cast
assert_equal :bar, attributes[:bar].name
end
test "duping creates a new hash, but does not dup the attributes" do
builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::String.new)
attributes = builder.build_from_database(foo: 1, bar: "foo")
# Ensure the type cast value is cached
attributes[:foo].value
attributes[:bar].value
duped = attributes.dup
duped.write_from_database(:foo, 2)
duped[:bar].value << "bar"
assert_equal 1, attributes[:foo].value
assert_equal 2, duped[:foo].value
assert_equal "foobar", attributes[:bar].value
assert_equal "foobar", duped[:bar].value
end
test "deep_duping creates a new hash and dups each attribute" do
builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::String.new)
attributes = builder.build_from_database(foo: 1, bar: "foo")
# Ensure the type cast value is cached
attributes[:foo].value
attributes[:bar].value
duped = attributes.deep_dup
duped.write_from_database(:foo, 2)
duped[:bar].value << "bar"
assert_equal 1, attributes[:foo].value
assert_equal 2, duped[:foo].value
assert_equal "foo", attributes[:bar].value
assert_equal "foobar", duped[:bar].value
end
test "freezing cloned set does not freeze original" do
attributes = AttributeSet.new({})
clone = attributes.clone
clone.freeze
assert_predicate clone, :frozen?
assert_not_predicate attributes, :frozen?
end
test "to_hash returns a hash of the type cast values" do
builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new)
attributes = builder.build_from_database(foo: "1.1", bar: "2.2")
assert_equal({ foo: 1, bar: 2.2 }, attributes.to_hash)
assert_equal({ foo: 1, bar: 2.2 }, attributes.to_h)
end
test "to_hash maintains order" do
builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new)
attributes = builder.build_from_database(foo: "2.2", bar: "3.3")
attributes[:bar]
hash = attributes.to_h
assert_equal [[:foo, 2], [:bar, 3.3]], hash.to_a
end
test "values_before_type_cast" do
builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Integer.new)
attributes = builder.build_from_database(foo: "1.1", bar: "2.2")
assert_equal({ foo: "1.1", bar: "2.2" }, attributes.values_before_type_cast)
end
test "known columns are built with uninitialized attributes" do
attributes = attributes_with_uninitialized_key
assert_predicate attributes[:foo], :initialized?
assert_not_predicate attributes[:bar], :initialized?
end
test "uninitialized attributes are not included in the attributes hash" do
attributes = attributes_with_uninitialized_key
assert_equal({ foo: 1 }, attributes.to_hash)
end
test "uninitialized attributes are not included in keys" do
attributes = attributes_with_uninitialized_key
assert_equal [:foo], attributes.keys
end
test "uninitialized attributes return false for key?" do
attributes = attributes_with_uninitialized_key
assert attributes.key?(:foo)
assert_not attributes.key?(:bar)
end
test "unknown attributes return false for key?" do
attributes = attributes_with_uninitialized_key
assert_not attributes.key?(:wibble)
end
test "fetch_value returns the value for the given initialized attribute" do
builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new)
attributes = builder.build_from_database(foo: "1.1", bar: "2.2")
assert_equal 1, attributes.fetch_value(:foo)
assert_equal 2.2, attributes.fetch_value(:bar)
end
test "fetch_value returns nil for unknown attributes" do
attributes = attributes_with_uninitialized_key
assert_nil attributes.fetch_value(:wibble) { "hello" }
end
test "fetch_value returns nil for unknown attributes when types has a default" do
types = Hash.new(Type::Value.new)
builder = AttributeSet::Builder.new(types)
attributes = builder.build_from_database
assert_nil attributes.fetch_value(:wibble) { "hello" }
end
test "fetch_value uses the given block for uninitialized attributes" do
attributes = attributes_with_uninitialized_key
value = attributes.fetch_value(:bar) { |n| n.to_s + "!" }
assert_equal "bar!", value
end
test "fetch_value returns nil for uninitialized attributes if no block is given" do
attributes = attributes_with_uninitialized_key
assert_nil attributes.fetch_value(:bar)
end
test "the primary_key is always initialized" do
defaults = { foo: Attribute.from_user(:foo, nil, nil) }
builder = AttributeSet::Builder.new({ foo: Type::Integer.new }, defaults)
attributes = builder.build_from_database
assert attributes.key?(:foo)
assert_equal [:foo], attributes.keys
assert_predicate attributes[:foo], :initialized?
end
class MyType
def cast(value)
return if value.nil?
value + " from user"
end
def deserialize(value)
return if value.nil?
value + " from database"
end
def assert_valid_value(*)
end
end
test "write_from_database sets the attribute with database typecasting" do
builder = AttributeSet::Builder.new(foo: MyType.new)
attributes = builder.build_from_database
assert_nil attributes.fetch_value(:foo)
attributes.write_from_database(:foo, "value")
assert_equal "value from database", attributes.fetch_value(:foo)
end
test "write_from_user sets the attribute with user typecasting" do
builder = AttributeSet::Builder.new(foo: MyType.new)
attributes = builder.build_from_database
assert_nil attributes.fetch_value(:foo)
attributes.write_from_user(:foo, "value")
assert_equal "value from user", attributes.fetch_value(:foo)
end
class MySerializedType < ::ActiveModel::Type::Value
def serialize(value)
value + " serialized"
end
end
test "values_for_database" do
builder = AttributeSet::Builder.new(foo: MySerializedType.new)
attributes = builder.build_from_database
attributes.write_from_user(:foo, "value")
assert_equal({ foo: "value serialized" }, attributes.values_for_database)
end
test "freezing doesn't prevent the set from materializing" do
builder = AttributeSet::Builder.new(foo: Type::String.new)
attributes = builder.build_from_database(foo: "1")
attributes.freeze
assert_equal({ foo: "1" }, attributes.to_hash)
end
test "marshalling dump/load materialized attribute hash" do
builder = AttributeSet::Builder.new(foo: Type::String.new)
def builder.build_from_database(values = {}, additional_types = {})
attributes = LazyAttributeHash.new(types, values, additional_types, default_attributes)
AttributeSet.new(attributes)
end
attributes = builder.build_from_database(foo: "1")
data = Marshal.dump(attributes)
attributes = Marshal.load(data)
assert_equal({ foo: "1" }, attributes.to_hash)
end
test "#accessed_attributes returns only attributes which have been read" do
builder = AttributeSet::Builder.new(foo: Type::Value.new, bar: Type::Value.new)
attributes = builder.build_from_database(foo: "1", bar: "2")
assert_equal [], attributes.accessed
attributes.fetch_value(:foo)
assert_equal [:foo], attributes.accessed
end
test "#map returns a new attribute set with the changes applied" do
builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Integer.new)
attributes = builder.build_from_database(foo: "1", bar: "2")
new_attributes = attributes.map do |attr|
attr.with_cast_value(attr.value + 1)
end
assert_equal 2, new_attributes.fetch_value(:foo)
assert_equal 3, new_attributes.fetch_value(:bar)
end
test "comparison for equality is correctly implemented" do
builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Integer.new)
attributes = builder.build_from_database(foo: "1", bar: "2")
attributes2 = builder.build_from_database(foo: "1", bar: "2")
attributes3 = builder.build_from_database(foo: "2", bar: "2")
assert_equal attributes, attributes2
assert_not_equal attributes2, attributes3
end
private
def attributes_with_uninitialized_key
builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new)
builder.build_from_database(foo: "1.1")
end
end
end
# frozen_string_literal: true
require_relative "../../abstract_unit"
require "active_support/core_ext/class/attribute"
class ClassAttributeTest < ActiveSupport::TestCase
def setup
@klass = Class.new do
class_attribute :setting
class_attribute :timeout, default: 5
end
@sub = Class.new(@klass)
end
test "defaults to nil" do
assert_nil @klass.setting
assert_nil @sub.setting
end
test "custom default" do
assert_equal 5, @klass.timeout
end
test "inheritable" do
@klass.setting = 1
assert_equal 1, @sub.setting
end
test "overridable" do
@sub.setting = 1
assert_nil @klass.setting
@klass.setting = 2
assert_equal 1, @sub.setting
assert_equal 1, Class.new(@sub).setting
end
test "predicate method" do
assert_equal false, @klass.setting?
@klass.setting = 1
assert_equal true, @klass.setting?
end
test "instance reader delegates to class" do
assert_nil @klass.new.setting
@klass.setting = 1
assert_equal 1, @klass.new.setting
end
test "instance override" do
object = @klass.new
object.setting = 1
assert_nil @klass.setting
@klass.setting = 2
assert_equal 1, object.setting
end
test "instance predicate" do
object = @klass.new
assert_equal false, object.setting?
object.setting = 1
assert_equal true, object.setting?
end
test "disabling instance writer" do
object = Class.new { class_attribute :setting, instance_writer: false }.new
assert_raise(NoMethodError) { object.setting = "boom" }
assert_not_respond_to object, :setting=
end
test "disabling instance reader" do
object = Class.new { class_attribute :setting, instance_reader: false }.new
assert_raise(NoMethodError) { object.setting }
assert_not_respond_to object, :setting
assert_raise(NoMethodError) { object.setting? }
assert_not_respond_to object, :setting?
end
test "disabling both instance writer and reader" do
object = Class.new { class_attribute :setting, instance_accessor: false }.new
assert_raise(NoMethodError) { object.setting }
assert_not_respond_to object, :setting
assert_raise(NoMethodError) { object.setting? }
assert_not_respond_to object, :setting?
assert_raise(NoMethodError) { object.setting = "boom" }
assert_not_respond_to object, :setting=
end
test "disabling instance predicate" do
object = Class.new { class_attribute :setting, instance_predicate: false }.new
assert_raise(NoMethodError) { object.setting? }
assert_not_respond_to object, :setting?
end
test "works well with singleton classes" do
object = @klass.new
object.singleton_class.setting = "foo"
assert_equal "foo", object.setting
end
test "works well with module singleton classes" do
@module = Module.new do
class << self
class_attribute :settings, default: 42
end
end
assert_equal 42, @module.settings
end
test "setter returns set value" do
val = @klass.public_send(:setting=, 1)
assert_equal 1, val
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 AttributesDirtyTest < ActiveModel::TestCase
class DirtyModel
include ActiveModel::Model
include ActiveModel::Attributes
include ActiveModel::Dirty
attribute :name, :string
attribute :color, :string
attribute :size, :integer
def save
changes_applied
end
end
setup do
@model = DirtyModel.new
end
test "setting attribute will result in change" do
assert_not_predicate @model, :changed?
assert_not_predicate @model, :name_changed?
@model.name = "Ringo"
assert_predicate @model, :changed?
assert_predicate @model, :name_changed?
end
test "list of changed attribute keys" do
assert_equal [], @model.changed
@model.name = "Paul"
assert_equal ["name"], @model.changed
end
test "changes to attribute values" do
assert_not @model.changes["name"]
@model.name = "John"
assert_equal [nil, "John"], @model.changes["name"]
end
test "checking if an attribute has changed to a particular value" do
@model.name = "Ringo"
assert @model.name_changed?(from: nil, to: "Ringo")
assert_not @model.name_changed?(from: "Pete", to: "Ringo")
assert @model.name_changed?(to: "Ringo")
assert_not @model.name_changed?(to: "Pete")
assert @model.name_changed?(from: nil)
assert_not @model.name_changed?(from: "Pete")
end
test "changes accessible through both strings and symbols" do
@model.name = "David"
assert_not_nil @model.changes[:name]
assert_not_nil @model.changes["name"]
end
test "be consistent with symbols arguments after the changes are applied" do
@model.name = "David"
assert @model.attribute_changed?(:name)
@model.save
@model.name = "Rafael"
assert @model.attribute_changed?(:name)
end
test "attribute mutation" do
@model.name = "Yam"
@model.save
assert_not_predicate @model, :name_changed?
@model.name.replace("Hadad")
assert_predicate @model, :name_changed?
end
test "resetting attribute" do
@model.name = "Bob"
@model.restore_name!
assert_nil @model.name
assert_not_predicate @model, :name_changed?
end
test "setting color to same value should not result in change being recorded" do
@model.color = "red"
assert_predicate @model, :color_changed?
@model.save
assert_not_predicate @model, :color_changed?
assert_not_predicate @model, :changed?
@model.color = "red"
assert_not_predicate @model, :color_changed?
assert_not_predicate @model, :changed?
end
test "saving should reset model's changed status" do
@model.name = "Alf"
assert_predicate @model, :changed?
@model.save
assert_not_predicate @model, :changed?
assert_not_predicate @model, :name_changed?
end
test "saving should preserve previous changes" do
@model.name = "Jericho Cane"
@model.save
assert_equal [nil, "Jericho Cane"], @model.previous_changes["name"]
end
test "setting new attributes should not affect previous changes" do
@model.name = "Jericho Cane"
@model.save
@model.name = "DudeFella ManGuy"
assert_equal [nil, "Jericho Cane"], @model.name_previous_change
end
test "saving should preserve model's previous changed status" do
@model.name = "Jericho Cane"
@model.save
assert_predicate @model, :name_previously_changed?
end
test "previous value is preserved when changed after save" do
assert_equal({}, @model.changed_attributes)
@model.name = "Paul"
assert_equal({ "name" => nil }, @model.changed_attributes)
@model.save
@model.name = "John"
assert_equal({ "name" => "Paul" }, @model.changed_attributes)
end
test "changing the same attribute multiple times retains the correct original value" do
@model.name = "Otto"
@model.save
@model.name = "DudeFella ManGuy"
@model.name = "Mr. Manfredgensonton"
assert_equal ["Otto", "Mr. Manfredgensonton"], @model.name_change
assert_equal @model.name_was, "Otto"
end
test "using attribute_will_change! with a symbol" do
@model.size = 1
assert_predicate @model, :size_changed?
end
test "clear_changes_information should reset all changes" do
@model.name = "Dmitry"
@model.name_changed?
@model.save
@model.name = "Bob"
assert_equal [nil, "Dmitry"], @model.previous_changes["name"]
assert_equal "Dmitry", @model.changed_attributes["name"]
@model.clear_changes_information
assert_equal ActiveSupport::HashWithIndifferentAccess.new, @model.previous_changes
assert_equal ActiveSupport::HashWithIndifferentAccess.new, @model.changed_attributes
end
test "restore_attributes should restore all previous data" do
@model.name = "Dmitry"
@model.color = "Red"
@model.save
@model.name = "Bob"
@model.color = "White"
@model.restore_attributes
assert_not_predicate @model, :changed?
assert_equal "Dmitry", @model.name
assert_equal "Red", @model.color
end
test "restore_attributes can restore only some attributes" do
@model.name = "Dmitry"
@model.color = "Red"
@model.save
@model.name = "Bob"
@model.color = "White"
@model.restore_attributes(["name"])
assert_predicate @model, :changed?
assert_equal "Dmitry", @model.name
assert_equal "White", @model.color
end
test "changing the attribute reports a change only when the cast value changes" do
@model.size = "2.3"
@model.save
@model.size = "2.1"
assert_equal false, @model.changed?
@model.size = "5.1"
assert_equal true, @model.changed?
assert_equal true, @model.size_changed?
assert_equal({ "size" => [2, 5] }, @model.changes)
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
module ActiveStorage
# Extracts duration (seconds) and bit_rate (bits/s) from an audio blob.
#
# Example:
#
# ActiveStorage::Analyzer::AudioAnalyzer.new(blob).metadata
# # => { duration: 5.0, bit_rate: 320340 }
#
# This analyzer requires the {FFmpeg}[https://www.ffmpeg.org] system library, which is not provided by Rails.
class Analyzer::AudioAnalyzer < Analyzer
def self.accept?(blob)
blob.audio?
end
def metadata
{ duration: duration, bit_rate: bit_rate }.compact
end
private
def duration
duration = audio_stream["duration"]
Float(duration) if duration
end
def bit_rate
bit_rate = audio_stream["bit_rate"]
Integer(bit_rate) if bit_rate
end
def audio_stream
@audio_stream ||= streams.detect { |stream| stream["codec_type"] == "audio" } || {}
end
def streams
probe["streams"] || []
end
def probe
@probe ||= download_blob_to_tempfile { |file| probe_from(file) }
end
def probe_from(file)
instrument(File.basename(ffprobe_path)) do
IO.popen([ ffprobe_path,
"-print_format", "json",
"-show_streams",
"-show_format",
"-v", "error",
file.path
]) do |output|
JSON.parse(output.read)
end
end
rescue Errno::ENOENT
logger.info "Skipping audio analysis because ffprobe isn't installed"
{}
end
def ffprobe_path
ActiveStorage.paths[:ffprobe] || "ffprobe"
end
end
end
# frozen_string_literal: true
require "test_helper"
require "database/setup"
require "active_storage/analyzer/audio_analyzer"
class ActiveStorage::Analyzer::AudioAnalyzerTest < ActiveSupport::TestCase
test "analyzing an audio" do
blob = create_file_blob(filename: "audio.mp3", content_type: "audio/mp3")
metadata = extract_metadata_from(blob)
assert_equal 0.914286, metadata[:duration]
assert_equal 128000, metadata[:bit_rate]
end
test "instrumenting analysis" do
events = subscribe_events_from("analyze.active_storage")
blob = create_file_blob(filename: "audio.mp3", content_type: "audio/mp3")
blob.analyze
assert_equal 1, events.size
assert_equal({ analyzer: "ffprobe" }, events.first.payload)
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
require "active_support/inflector/methods"
module ActiveSupport
# Autoload and eager load conveniences for your library.
#
# This module allows you to define autoloads based on
# Rails conventions (i.e. no need to define the path
# it is automatically guessed based on the filename)
# and also define a set of constants that needs to be
# eager loaded:
#
# module MyLib
# extend ActiveSupport::Autoload
#
# autoload :Model
#
# eager_autoload do
# autoload :Cache
# end
# end
#
# Then your library can be eager loaded by simply calling:
#
# MyLib.eager_load!
module Autoload
def self.extended(base) # :nodoc:
if RUBY_VERSION < "3"
base.class_eval do
@_autoloads = nil
@_under_path = nil
@_at_path = nil
@_eager_autoload = false
end
end
end
def autoload(const_name, path = @_at_path)
unless path
full = [name, @_under_path, const_name.to_s].compact.join("::")
path = Inflector.underscore(full)
end
if @_eager_autoload
@_eagerloaded_constants ||= []
@_eagerloaded_constants << const_name
end
super const_name, path
end
def autoload_under(path)
@_under_path, old_path = path, @_under_path
yield
ensure
@_under_path = old_path
end
def autoload_at(path)
@_at_path, old_path = path, @_at_path
yield
ensure
@_at_path = old_path
end
def eager_autoload
old_eager, @_eager_autoload = @_eager_autoload, true
yield
ensure
@_eager_autoload = old_eager
end
def eager_load!
if @_eagerloaded_constants
@_eagerloaded_constants.each { |const_name| const_get(const_name) }
@_eagerloaded_constants = nil
end
end
end
end
# frozen_string_literal: true
require_relative "abstract_unit"
class TestAutoloadModule < ActiveSupport::TestCase
include ActiveSupport::Testing::Isolation
module ::Fixtures
extend ActiveSupport::Autoload
module Autoload
extend ActiveSupport::Autoload
end
end
def setup
@some_class_path = File.expand_path("test/fixtures/autoload/some_class.rb")
@another_class_path = File.expand_path("test/fixtures/autoload/another_class.rb")
$LOAD_PATH << "test"
end
def teardown
$LOAD_PATH.pop
end
test "the autoload module works like normal autoload" do
module ::Fixtures::Autoload
autoload :SomeClass, "fixtures/autoload/some_class"
end
assert_nothing_raised { ::Fixtures::Autoload::SomeClass }
end
test "when specifying an :eager constant it still works like normal autoload by default" do
module ::Fixtures::Autoload
eager_autoload do
autoload :SomeClass, "fixtures/autoload/some_class"
end
end
assert_not_includes $LOADED_FEATURES, @some_class_path
assert_nothing_raised { ::Fixtures::Autoload::SomeClass }
end
test "the location of autoloaded constants defaults to :name.underscore" do
module ::Fixtures::Autoload
autoload :SomeClass
end
assert_not_includes $LOADED_FEATURES, @some_class_path
assert_nothing_raised { ::Fixtures::Autoload::SomeClass }
end
test "the location of :eager autoloaded constants defaults to :name.underscore" do
module ::Fixtures::Autoload
eager_autoload do
autoload :SomeClass
end
end
assert_not_includes $LOADED_FEATURES, @some_class_path
::Fixtures::Autoload.eager_load!
assert_includes $LOADED_FEATURES, @some_class_path
assert_nothing_raised { ::Fixtures::Autoload::SomeClass }
end
test "a directory for a block of autoloads can be specified" do
module ::Fixtures
autoload_under "autoload" do
autoload :AnotherClass
end
end
assert_not_includes $LOADED_FEATURES, @another_class_path
assert_nothing_raised { ::Fixtures::AnotherClass }
end
test "a path for a block of autoloads can be specified" do
module ::Fixtures
autoload_at "fixtures/autoload/another_class" do
autoload :AnotherClass
end
end
assert_not_includes $LOADED_FEATURES, @another_class_path
assert_nothing_raised { ::Fixtures::AnotherClass }
end
end
# frozen_string_literal: true
gem "minitest"
require "minitest"
Minitest.autorun
# 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 "service/shared_service_tests"
require "uri"
if SERVICE_CONFIGURATIONS[:azure_public]
class ActiveStorage::Service::AzureStoragePublicServiceTest < ActiveSupport::TestCase
SERVICE = ActiveStorage::Service.configure(:azure_public, SERVICE_CONFIGURATIONS)
include ActiveStorage::Service::SharedServiceTests
test "public URL generation" do
url = @service.url(@key, filename: ActiveStorage::Filename.new("avatar.png"))
assert_match(/.*\.blob\.core\.windows\.net\/.*\/#{@key}/, url)
response = Net::HTTP.get_response(URI(url))
assert_equal "200", response.code
end
test "direct upload" do
key = SecureRandom.base58(24)
data = "Something else entirely!"
checksum = OpenSSL::Digest::MD5.base64digest(data)
content_type = "text/xml"
url = @service.url_for_direct_upload(key, expires_in: 5.minutes, content_type: content_type, content_length: data.size, checksum: checksum)
uri = URI.parse url
request = Net::HTTP::Put.new uri.request_uri
request.body = data
@service.headers_for_direct_upload(key, checksum: checksum, content_type: content_type, filename: ActiveStorage::Filename.new("test.txt")).each do |k, v|
request.add_field k, v
end
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
http.request request
end
response = Net::HTTP.get_response(URI(@service.url(key)))
assert_equal "200", response.code
assert_equal data, response.body
ensure
@service.delete key
end
end
else
puts "Skipping Azure Storage Public Service tests because no Azure configuration was supplied"
end
# frozen_string_literal: true
gem "azure-storage-blob", ">= 2.0"
require "active_support/core_ext/numeric/bytes"
require "azure/storage/blob"
require "azure/storage/common/core/auth/shared_access_signature"
module ActiveStorage
# Wraps the Microsoft Azure Storage Blob Service as an Active Storage service.
# See ActiveStorage::Service for the generic API documentation that applies to all services.
class Service::AzureStorageService < Service
attr_reader :client, :container, :signer
def initialize(storage_account_name:, storage_access_key:, container:, public: false, **options)
@client = Azure::Storage::Blob::BlobService.create(storage_account_name: storage_account_name, storage_access_key: storage_access_key, **options)
@signer = Azure::Storage::Common::Core::Auth::SharedAccessSignature.new(storage_account_name, storage_access_key)
@container = container
@public = public
end
def upload(key, io, checksum: nil, filename: nil, content_type: nil, disposition: nil, custom_metadata: {}, **)
instrument :upload, key: key, checksum: checksum do
handle_errors do
content_disposition = content_disposition_with(filename: filename, type: disposition) if disposition && filename
client.create_block_blob(container, key, IO.try_convert(io) || io, content_md5: checksum, content_type: content_type, content_disposition: content_disposition, metadata: custom_metadata)
end
end
end
def download(key, &block)
if block_given?
instrument :streaming_download, key: key do
stream(key, &block)
end
else
instrument :download, key: key do
handle_errors do
_, io = client.get_blob(container, key)
io.force_encoding(Encoding::BINARY)
end
end
end
end
def download_chunk(key, range)
instrument :download_chunk, key: key, range: range do
handle_errors do
_, io = client.get_blob(container, key, start_range: range.begin, end_range: range.exclude_end? ? range.end - 1 : range.end)
io.force_encoding(Encoding::BINARY)
end
end
end
def delete(key)
instrument :delete, key: key do
client.delete_blob(container, key)
rescue Azure::Core::Http::HTTPError => e
raise unless e.type == "BlobNotFound"
# Ignore files already deleted
end
end
def delete_prefixed(prefix)
instrument :delete_prefixed, prefix: prefix do
marker = nil
loop do
results = client.list_blobs(container, prefix: prefix, marker: marker)
results.each do |blob|
client.delete_blob(container, blob.name)
end
break unless marker = results.continuation_token.presence
end
end
end
def exist?(key)
instrument :exist, key: key do |payload|
answer = blob_for(key).present?
payload[:exist] = answer
answer
end
end
def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:, custom_metadata: {})
instrument :url, key: key do |payload|
generated_url = signer.signed_uri(
uri_for(key), false,
service: "b",
permissions: "rw",
expiry: format_expiry(expires_in)
).to_s
payload[:url] = generated_url
generated_url
end
end
def headers_for_direct_upload(key, content_type:, checksum:, filename: nil, disposition: nil, custom_metadata: {}, **)
content_disposition = content_disposition_with(type: disposition, filename: filename) if filename
{ "Content-Type" => content_type, "Content-MD5" => checksum, "x-ms-blob-content-disposition" => content_disposition, "x-ms-blob-type" => "BlockBlob", **custom_metadata_headers(custom_metadata) }
end
def compose(source_keys, destination_key, filename: nil, content_type: nil, disposition: nil, custom_metadata: {})
content_disposition = content_disposition_with(type: disposition, filename: filename) if disposition && filename
client.create_append_blob(
container,
destination_key,
content_type: content_type,
content_disposition: content_disposition,
metadata: custom_metadata,
).tap do |blob|
source_keys.each do |source_key|
stream(source_key) do |chunk|
client.append_blob_block(container, blob.name, chunk)
end
end
end
end
private
def private_url(key, expires_in:, filename:, disposition:, content_type:, **)
signer.signed_uri(
uri_for(key), false,
service: "b",
permissions: "r",
expiry: format_expiry(expires_in),
content_disposition: content_disposition_with(type: disposition, filename: filename),
content_type: content_type
).to_s
end
def public_url(key, **)
uri_for(key).to_s
end
def uri_for(key)
client.generate_uri("#{container}/#{key}")
end
def blob_for(key)
client.get_blob_properties(container, key)
rescue Azure::Core::Http::HTTPError
false
end
def format_expiry(expires_in)
expires_in ? Time.now.utc.advance(seconds: expires_in).iso8601 : nil
end
# Reads the object for the given key in chunks, yielding each to the block.
def stream(key)
blob = blob_for(key)
chunk_size = 5.megabytes
offset = 0
raise ActiveStorage::FileNotFoundError unless blob.present?
while offset < blob.properties[:content_length]
_, chunk = client.get_blob(container, key, start_range: offset, end_range: offset + chunk_size - 1)
yield chunk.force_encoding(Encoding::BINARY)
offset += chunk_size
end
end
def handle_errors
yield
rescue Azure::Core::Http::HTTPError => e
case e.type
when "BlobNotFound"
raise ActiveStorage::FileNotFoundError
when "Md5Mismatch"
raise ActiveStorage::IntegrityError
else
raise
end
end
def custom_metadata_headers(metadata)
metadata.transform_keys { |key| "x-ms-meta-#{key}" }
end
end
end
# frozen_string_literal: true
require "service/shared_service_tests"
require "uri"
if SERVICE_CONFIGURATIONS[:azure]
class ActiveStorage::Service::AzureStorageServiceTest < ActiveSupport::TestCase
SERVICE = ActiveStorage::Service.configure(:azure, SERVICE_CONFIGURATIONS)
include ActiveStorage::Service::SharedServiceTests
test "direct upload with content type" do
key = SecureRandom.base58(24)
data = "Something else entirely!"
checksum = OpenSSL::Digest::MD5.base64digest(data)
content_type = "text/xml"
url = @service.url_for_direct_upload(key, expires_in: 5.minutes, content_type: content_type, content_length: data.size, checksum: checksum)
uri = URI.parse url
request = Net::HTTP::Put.new uri.request_uri
request.body = data
@service.headers_for_direct_upload(key, checksum: checksum, content_type: content_type, filename: ActiveStorage::Filename.new("test.txt")).each do |k, v|
request.add_field k, v
end
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
http.request request
end
assert_equal(content_type, @service.client.get_blob_properties(@service.container, key).properties[:content_type])
ensure
@service.delete key
end
test "direct upload with content disposition" do
key = SecureRandom.base58(24)
data = "Something else entirely!"
checksum = OpenSSL::Digest::MD5.base64digest(data)
url = @service.url_for_direct_upload(key, expires_in: 5.minutes, content_type: "text/plain", content_length: data.size, checksum: checksum)
uri = URI.parse url
request = Net::HTTP::Put.new uri.request_uri
request.body = data
@service.headers_for_direct_upload(key, checksum: checksum, content_type: "text/plain", filename: ActiveStorage::Filename.new("test.txt"), disposition: :attachment).each do |k, v|
request.add_field k, v
end
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
http.request request
end
assert_equal("attachment; filename=\"test.txt\"; filename*=UTF-8''test.txt", @service.client.get_blob_properties(@service.container, key).properties[:content_disposition])
ensure
@service.delete key
end
test "upload with content_type" do
key = SecureRandom.base58(24)
data = "Foobar"
@service.upload(key, StringIO.new(data), checksum: OpenSSL::Digest::MD5.base64digest(data), filename: ActiveStorage::Filename.new("test.txt"), content_type: "text/plain")
url = @service.url(key, expires_in: 2.minutes, disposition: :attachment, content_type: nil, filename: ActiveStorage::Filename.new("test.html"))
response = Net::HTTP.get_response(URI(url))
assert_equal "text/plain", response.content_type
assert_match(/attachment;.*test\.html/, response["Content-Disposition"])
ensure
@service.delete key
end
test "upload with content disposition" do
key = SecureRandom.base58(24)
data = "Foobar"
@service.upload(key, StringIO.new(data), checksum: OpenSSL::Digest::MD5.base64digest(data), filename: ActiveStorage::Filename.new("test.txt"), disposition: :inline)
assert_equal("inline; filename=\"test.txt\"; filename*=UTF-8''test.txt", @service.client.get_blob_properties(@service.container, key).properties[:content_disposition])
url = @service.url(key, expires_in: 2.minutes, disposition: :attachment, content_type: nil, filename: ActiveStorage::Filename.new("test.html"))
response = Net::HTTP.get_response(URI(url))
assert_match(/attachment;.*test\.html/, response["Content-Disposition"])
ensure
@service.delete key
end
test "upload with custom_metadata" do
key = SecureRandom.base58(24)
data = "Foobar"
@service.upload(key, StringIO.new(data), checksum: OpenSSL::Digest::MD5.base64digest(data), filename: ActiveStorage::Filename.new("test.txt"), custom_metadata: { "foo" => "baz" })
url = @service.url(key, expires_in: 2.minutes, disposition: :inline, content_type: "text/html", filename: ActiveStorage::Filename.new("test.html"))
response = Net::HTTP.get_response(URI(url))
assert_equal("baz", response["x-ms-meta-foo"])
ensure
@service.delete key
end
test "signed URL generation" do
url = @service.url(@key, expires_in: 5.minutes,
disposition: :inline, filename: ActiveStorage::Filename.new("avatar.png"), content_type: "image/png")
assert_match(/(\S+)&rscd=inline%3B\+filename%3D%22avatar\.png%22%3B\+filename\*%3DUTF-8%27%27avatar\.png&rsct=image%2Fpng/, url)
assert_match SERVICE_CONFIGURATIONS[:azure][:container], url
end
test "uploading a tempfile" do
key = SecureRandom.base58(24)
data = "Something else entirely!"
Tempfile.open do |file|
file.write(data)
file.rewind
@service.upload(key, file)
end
assert_equal data, @service.download(key)
ensure
@service.delete(key)
end
end
else
puts "Skipping Azure Storage Service tests because no Azure configuration was supplied"
end
# frozen_string_literal: true
module BackburnerJobsManager
def setup
ActiveJob::Base.queue_adapter = :backburner
Backburner.configure do |config|
config.beanstalk_url = ENV["BEANSTALK_URL"] if ENV["BEANSTALK_URL"]
config.logger = Rails.logger
end
unless can_run?
puts "Cannot run integration tests for backburner. To be able to run integration tests for backburner you need to install and start beanstalkd.\n"
status = ENV["CI"] ? false : true
exit status
end
end
def clear_jobs
tube.clear
end
def start_workers
@thread = Thread.new { Backburner.work "integration-tests" } # backburner dasherizes the queue name
end
def stop_workers
@thread.kill
end
def tube
@tube ||= Beaneater::Tube.new(@worker.connection, "backburner.worker.queue.integration-tests") # backburner dasherizes the queue name
end
def can_run?
begin
@worker = Backburner::Worker.new
rescue
return false
end
true
end
end
# frozen_string_literal: true
require "backburner"
module ActiveJob
module QueueAdapters
# == Backburner adapter for Active Job
#
# Backburner is a beanstalkd-powered job queue that can handle a very
# high volume of jobs. You create background jobs and place them on
# multiple work queues to be processed later. Read more about
# Backburner {here}[https://github.com/nesquena/backburner].
#
# To use Backburner set the queue_adapter config to +:backburner+.
#
# Rails.application.config.active_job.queue_adapter = :backburner
class BackburnerAdapter
def enqueue(job) # :nodoc:
Backburner::Worker.enqueue(JobWrapper, [job.serialize], queue: job.queue_name, pri: job.priority)
end
def enqueue_at(job, timestamp) # :nodoc:
delay = timestamp - Time.current.to_f
Backburner::Worker.enqueue(JobWrapper, [job.serialize], queue: job.queue_name, pri: job.priority, delay: delay)
end
class JobWrapper # :nodoc:
class << self
def perform(job_data)
Base.execute job_data
end
end
end
end
end
end
# frozen_string_literal: true
module ActiveSupport
# Backtraces often include many lines that are not relevant for the context
# under review. This makes it hard to find the signal amongst the backtrace
# noise, and adds debugging time. With a BacktraceCleaner, filters and
# silencers are used to remove the noisy lines, so that only the most relevant
# lines remain.
#
# Filters are used to modify lines of data, while silencers are used to remove
# lines entirely. The typical filter use case is to remove lengthy path
# information from the start of each line, and view file paths relevant to the
# app directory instead of the file system root. The typical silencer use case
# is to exclude the output of a noisy library from the backtrace, so that you
# can focus on the rest.
#
# bc = ActiveSupport::BacktraceCleaner.new
# bc.add_filter { |line| line.gsub(Rails.root.to_s, '') } # strip the Rails.root prefix
# bc.add_silencer { |line| /puma|rubygems/.match?(line) } # skip any lines from puma or rubygems
# bc.clean(exception.backtrace) # perform the cleanup
#
# To reconfigure an existing BacktraceCleaner (like the default one in Rails)
# and show as much data as possible, you can always call
# BacktraceCleaner#remove_silencers!, which will restore the
# backtrace to a pristine state. If you need to reconfigure an existing
# BacktraceCleaner so that it does not filter or modify the paths of any lines
# of the backtrace, you can call BacktraceCleaner#remove_filters!
# These two methods will give you a completely untouched backtrace.
#
# Inspired by the Quiet Backtrace gem by thoughtbot.
class BacktraceCleaner
def initialize
@filters, @silencers = [], []
add_gem_filter
add_gem_silencer
add_stdlib_silencer
end
# Returns the backtrace after all filters and silencers have been run
# against it. Filters run first, then silencers.
def clean(backtrace, kind = :silent)
filtered = filter_backtrace(backtrace)
case kind
when :silent
silence(filtered)
when :noise
noise(filtered)
else
filtered
end
end
alias :filter :clean
# Adds a filter from the block provided. Each line in the backtrace will be
# mapped against this filter.
#
# # Will turn "/my/rails/root/app/models/person.rb" into "/app/models/person.rb"
# backtrace_cleaner.add_filter { |line| line.gsub(Rails.root.to_s, '') }
def add_filter(&block)
@filters << block
end
# Adds a silencer from the block provided. If the silencer returns +true+
# for a given line, it will be excluded from the clean backtrace.
#
# # Will reject all lines that include the word "puma", like "/gems/puma/server.rb" or "/app/my_puma_server/rb"
# backtrace_cleaner.add_silencer { |line| /puma/.match?(line) }
def add_silencer(&block)
@silencers << block
end
# Removes all silencers, but leaves in the filters. Useful if your
# context of debugging suddenly expands as you suspect a bug in one of
# the libraries you use.
def remove_silencers!
@silencers = []
end
# Removes all filters, but leaves in the silencers. Useful if you suddenly
# need to see entire filepaths in the backtrace that you had already
# filtered out.
def remove_filters!
@filters = []
end
private
FORMATTED_GEMS_PATTERN = /\A[^\/]+ \([\w.]+\) /
def add_gem_filter
gems_paths = (Gem.path | [Gem.default_dir]).map { |p| Regexp.escape(p) }
return if gems_paths.empty?
gems_regexp = %r{\A(#{gems_paths.join('|')})/(bundler/)?gems/([^/]+)-([\w.]+)/(.*)}
gems_result = '\3 (\4) \5'
add_filter { |line| line.sub(gems_regexp, gems_result) }
end
def add_gem_silencer
add_silencer { |line| FORMATTED_GEMS_PATTERN.match?(line) }
end
def add_stdlib_silencer
add_silencer { |line| line.start_with?(RbConfig::CONFIG["rubylibdir"]) }
end
def filter_backtrace(backtrace)
@filters.each do |f|
backtrace = backtrace.map { |line| f.call(line) }
end
backtrace
end
def silence(backtrace)
@silencers.each do |s|
backtrace = backtrace.reject { |line| s.call(line) }
end
backtrace
end
def noise(backtrace)
backtrace.select do |line|
@silencers.any? do |s|
s.call(line)
end
end
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
class ActiveStorage::Representations::BaseController < ActiveStorage::BaseController # :nodoc:
include ActiveStorage::SetBlob
before_action :set_representation
private
def blob_scope
ActiveStorage::Blob.scope_for_strict_loading
end
def set_representation
@representation = @blob.representation(params[:variation_key]).processed
rescue ActiveSupport::MessageVerifier::InvalidSignature
head :not_found
end
end
# frozen_string_literal: true
class ActiveStorage::BaseJob < ActiveJob::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
class String
# Enables more predictable duck-typing on String-like classes. See <tt>Object#acts_like?</tt>.
def acts_like_string?
true
end
end
# frozen_string_literal: true
require_relative "behaviors/cache_delete_matched_behavior"
require_relative "behaviors/cache_increment_decrement_behavior"
require_relative "behaviors/cache_instrumentation_behavior"
require_relative "behaviors/cache_store_behavior"
require_relative "behaviors/cache_store_version_behavior"
require_relative "behaviors/cache_store_coder_behavior"
require_relative "behaviors/connection_pool_behavior"
require_relative "behaviors/encoded_key_cache_behavior"
require_relative "behaviors/failure_safety_behavior"
require_relative "behaviors/failure_raising_behavior"
require_relative "behaviors/local_cache_behavior"
# 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 "benchmark"
class << Benchmark
# Benchmark realtime in milliseconds.
#
# Benchmark.realtime { User.all }
# # => 8.0e-05
#
# Benchmark.ms { User.all }
# # => 0.074
def ms(&block)
1000 * realtime(&block)
end
end
# frozen_string_literal: true
require "active_support/core_ext/benchmark"
require "active_support/core_ext/hash/keys"
module ActiveSupport
module Benchmarkable
# Allows you to measure the execution time of a block in a template and
# records the result to the log. Wrap this block around expensive operations
# or possible bottlenecks to get a time reading for the operation. For
# example, let's say you thought your file processing method was taking too
# long; you could wrap it in a benchmark block.
#
# <% benchmark 'Process data files' do %>
# <%= expensive_files_operation %>
# <% end %>
#
# That would add something like "Process data files (345.2ms)" to the log,
# which you can then use to compare timings when optimizing your code.
#
# You may give an optional logger level (<tt>:debug</tt>, <tt>:info</tt>,
# <tt>:warn</tt>, <tt>:error</tt>) as the <tt>:level</tt> option. The
# default logger level value is <tt>:info</tt>.
#
# <% benchmark 'Low-level files', level: :debug do %>
# <%= lowlevel_files_operation %>
# <% end %>
#
# Finally, you can pass true as the third argument to silence all log
# activity (other than the timing information) from inside the block. This
# is great for boiling down a noisy block to just a single statement that
# produces one log line:
#
# <% benchmark 'Process data files', level: :info, silence: true do %>
# <%= expensive_and_chatty_files_operation %>
# <% end %>
def benchmark(message = "Benchmarking", options = {}, &block)
if logger
options.assert_valid_keys(:level, :silence)
options[:level] ||= :info
result = nil
ms = Benchmark.ms { result = options[:silence] ? logger.silence(&block) : yield }
logger.public_send(options[:level], "%s (%.1fms)" % [ message, ms ])
result
else
yield
end
end
end
end
# frozen_string_literal: true
require_relative "abstract_unit"
class BenchmarkableTest < ActiveSupport::TestCase
include ActiveSupport::Benchmarkable
attr_reader :buffer, :logger
class Buffer
include Enumerable
def initialize; @lines = []; end
def each(&block); @lines.each(&block); end
def write(x); @lines << x; end
def close; end
def last; @lines.last; end
def size; @lines.size; end
def empty?; @lines.empty?; end
end
def setup
@buffer = Buffer.new
@logger = ActiveSupport::Logger.new(@buffer)
end
def test_without_block
assert_raise(LocalJumpError) { benchmark }
assert_empty buffer
end
def test_defaults
i_was_run = false
benchmark { i_was_run = true }
assert i_was_run
assert_last_logged
end
def test_with_message
i_was_run = false
benchmark("test_run") { i_was_run = true }
assert i_was_run
assert_last_logged "test_run"
end
def test_with_silence
assert_difference "buffer.count", +2 do
benchmark("test_run") do
logger.info "SOMETHING"
end
end
assert_difference "buffer.count", +1 do
benchmark("test_run", silence: true) do
logger.info "NOTHING"
end
end
end
def test_within_level
logger.level = ActiveSupport::Logger::DEBUG
benchmark("included_debug_run", level: :debug) { }
assert_last_logged "included_debug_run"
end
def test_outside_level
logger.level = ActiveSupport::Logger::ERROR
benchmark("skipped_debug_run", level: :debug) { }
assert_no_match(/skipped_debug_run/, buffer.last)
ensure
logger.level = ActiveSupport::Logger::DEBUG
end
private
def assert_last_logged(message = "Benchmarking")
assert_match(/^#{message} \(.*\)$/, buffer.last)
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 "active_support/core_ext/big_decimal/conversions"
# frozen_string_literal: true
require "active_model/type/integer"
module ActiveModel
module Type
# Attribute type for integers that can be serialized to an unlimited number
# of bytes. This type is registered under the +:big_integer+ key.
#
# class Person
# include ActiveModel::Attributes
#
# attribute :id, :big_integer
# end
#
# person = Person.new
# person.id = "18_000_000_000"
#
# person.id # => 18000000000
#
# All casting and serialization are performed in the same way as the
# standard ActiveModel::Type::Integer type.
class BigInteger < Integer
private
def max_value
::Float::INFINITY
end
end
end
end
# frozen_string_literal: true
require "cases/helper"
module ActiveModel
module Type
class BigIntegerTest < ActiveModel::TestCase
def test_type_cast_big_integer
type = Type::BigInteger.new
assert_equal 1, type.cast(1)
assert_equal 1, type.cast("1")
end
def test_small_values
type = Type::BigInteger.new
assert_equal(-9999999999999999999999999999999, type.serialize(-9999999999999999999999999999999))
end
def test_large_values
type = Type::BigInteger.new
assert_equal 9999999999999999999999999999999, type.serialize(9999999999999999999999999999999)
end
end
end
end
# frozen_string_literal: true
require_relative "../abstract_unit"
require "active_support/core_ext/big_decimal"
class BigDecimalTest < ActiveSupport::TestCase
def test_to_s
bd = BigDecimal "0.01"
assert_equal "0.01", bd.to_s
assert_equal "+0.01", bd.to_s("+F")
assert_equal "+0.0 1", bd.to_s("+1F")
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
This file has been truncated, but you can view the full file.
# 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 (?, ?, ?)
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