aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib
diff options
context:
space:
mode:
Diffstat (limited to 'activerecord/lib')
-rw-r--r--activerecord/lib/active_record/associations.rb5
-rw-r--r--activerecord/lib/active_record/associations/builder/collection_association.rb4
-rw-r--r--activerecord/lib/active_record/associations/preloader/through_association.rb6
-rw-r--r--activerecord/lib/active_record/attribute_methods/query.rb3
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb164
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb5
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/quoting.rb14
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb10
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb22
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_adapter.rb50
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb139
-rw-r--r--activerecord/lib/active_record/connection_adapters/determine_if_preparable_visitor.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb51
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb10
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb58
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb4
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb8
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb66
-rw-r--r--activerecord/lib/active_record/connection_adapters/schema_cache.rb27
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3/database_statements.rb38
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb58
-rw-r--r--activerecord/lib/active_record/core.rb5
-rw-r--r--activerecord/lib/active_record/database_configurations.rb2
-rw-r--r--activerecord/lib/active_record/enum.rb6
-rw-r--r--activerecord/lib/active_record/errors.rb2
-rw-r--r--activerecord/lib/active_record/gem_version.rb2
-rw-r--r--activerecord/lib/active_record/insert_all.rb167
-rw-r--r--activerecord/lib/active_record/migration.rb2
-rw-r--r--activerecord/lib/active_record/migration/command_recorder.rb17
-rw-r--r--activerecord/lib/active_record/persistence.rb203
-rw-r--r--activerecord/lib/active_record/querying.rb29
-rw-r--r--activerecord/lib/active_record/railties/databases.rake10
-rw-r--r--activerecord/lib/active_record/relation.rb37
-rw-r--r--activerecord/lib/active_record/relation/calculations.rb11
-rw-r--r--activerecord/lib/active_record/relation/delegation.rb51
-rw-r--r--activerecord/lib/active_record/relation/finder_methods.rb4
-rw-r--r--activerecord/lib/active_record/relation/query_methods.rb56
-rw-r--r--activerecord/lib/active_record/sanitization.rb5
-rw-r--r--activerecord/lib/active_record/scoping/default.rb13
-rw-r--r--activerecord/lib/active_record/statement_cache.rb4
-rw-r--r--activerecord/lib/active_record/tasks/database_tasks.rb21
-rw-r--r--activerecord/lib/active_record/validations/uniqueness.rb8
-rw-r--r--activerecord/lib/arel/insert_manager.rb6
-rw-r--r--activerecord/lib/arel/nodes.rb1
-rw-r--r--activerecord/lib/arel/nodes/select_core.rb6
-rw-r--r--activerecord/lib/arel/nodes/unary.rb1
-rw-r--r--activerecord/lib/arel/nodes/values.rb16
-rw-r--r--activerecord/lib/arel/nodes/values_list.rb19
-rw-r--r--activerecord/lib/arel/select_manager.rb19
-rw-r--r--activerecord/lib/arel/visitors/depth_first.rb3
-rw-r--r--activerecord/lib/arel/visitors/dot.rb5
-rw-r--r--activerecord/lib/arel/visitors/ibm_db.rb12
-rw-r--r--activerecord/lib/arel/visitors/informix.rb5
-rw-r--r--activerecord/lib/arel/visitors/mssql.rb13
-rw-r--r--activerecord/lib/arel/visitors/to_sql.rb36
56 files changed, 1099 insertions, 444 deletions
diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb
index 7bdbd8ce69..64c20adc87 100644
--- a/activerecord/lib/active_record/associations.rb
+++ b/activerecord/lib/active_record/associations.rb
@@ -703,8 +703,9 @@ module ActiveRecord
# #belongs_to associations.
#
# Extra options on the associations, as defined in the
- # <tt>AssociationReflection::INVALID_AUTOMATIC_INVERSE_OPTIONS</tt> constant, will
- # also prevent the association's inverse from being found automatically.
+ # <tt>AssociationReflection::INVALID_AUTOMATIC_INVERSE_OPTIONS</tt>
+ # constant, or a custom scope, will also prevent the association's inverse
+ # from being found automatically.
#
# 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,
diff --git a/activerecord/lib/active_record/associations/builder/collection_association.rb b/activerecord/lib/active_record/associations/builder/collection_association.rb
index ff57c40121..5848cd9112 100644
--- a/activerecord/lib/active_record/associations/builder/collection_association.rb
+++ b/activerecord/lib/active_record/associations/builder/collection_association.rb
@@ -20,10 +20,10 @@ module ActiveRecord::Associations::Builder # :nodoc:
}
end
- def self.define_extensions(model, name)
+ def self.define_extensions(model, name, &block)
if block_given?
extension_module_name = "#{model.name.demodulize}#{name.to_s.camelize}AssociationExtension"
- extension = Module.new(&Proc.new)
+ extension = Module.new(&block)
model.module_parent.const_set(extension_module_name, extension)
end
end
diff --git a/activerecord/lib/active_record/associations/preloader/through_association.rb b/activerecord/lib/active_record/associations/preloader/through_association.rb
index 25254e652a..32653956b2 100644
--- a/activerecord/lib/active_record/associations/preloader/through_association.rb
+++ b/activerecord/lib/active_record/associations/preloader/through_association.rb
@@ -14,6 +14,7 @@ module ActiveRecord
owners.each do |owner|
through_records = Array(owner.association(through_reflection.name).target)
+
if already_loaded
if source_type = reflection.options[:source_type]
through_records = through_records.select do |record|
@@ -23,17 +24,20 @@ module ActiveRecord
else
owner.association(through_reflection.name).reset if through_scope
end
+
result = through_records.flat_map do |record|
record.association(source_reflection.name).target
end
+
result.compact!
result.sort_by! { |rhs| preload_index[rhs] } if scope.order_values.any?
result.uniq! if scope.distinct_value
associate_records_to_owner(owner, result)
end
+
unless scope.empty_scope?
middle_records.each do |owner|
- owner.association(source_reflection.name).reset
+ owner.association(source_reflection.name).reset if owner
end
end
end
diff --git a/activerecord/lib/active_record/attribute_methods/query.rb b/activerecord/lib/active_record/attribute_methods/query.rb
index 6757e9b66a..6811f54b10 100644
--- a/activerecord/lib/active_record/attribute_methods/query.rb
+++ b/activerecord/lib/active_record/attribute_methods/query.rb
@@ -16,8 +16,7 @@ module ActiveRecord
when true then true
when false, nil then false
else
- column = self.class.columns_hash[attr_name]
- if column.nil?
+ if !type_for_attribute(attr_name) { false }
if Numeric === value || value !~ /[^0-9]/
!value.to_i.zero?
else
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
index aa2ecee74a..6aacbe5f88 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
@@ -20,9 +20,22 @@ module ActiveRecord
raise "Passing bind parameters with an arel AST is forbidden. " \
"The values must be stored on the AST directly"
end
- sql, binds = visitor.compile(arel_or_sql_string.ast, collector)
- [sql.freeze, binds || []]
+
+ if prepared_statements
+ sql, binds = visitor.compile(arel_or_sql_string.ast, collector)
+
+ if binds.length > bind_params_length
+ unprepared_statement do
+ sql, binds = to_sql_and_binds(arel_or_sql_string)
+ visitor.preparable = false
+ end
+ end
+ else
+ sql = visitor.compile(arel_or_sql_string.ast, collector)
+ end
+ [sql.freeze, binds]
else
+ visitor.preparable = false if prepared_statements
[arel_or_sql_string.dup.freeze, binds]
end
end
@@ -47,13 +60,8 @@ module ActiveRecord
arel = arel_from_relation(arel)
sql, binds = to_sql_and_binds(arel, binds)
- if !prepared_statements || (arel.is_a?(String) && preparable.nil?)
- preparable = false
- elsif binds.length > bind_params_length
- sql, binds = unprepared_statement { to_sql_and_binds(arel) }
- preparable = false
- else
- preparable = visitor.preparable
+ if preparable.nil?
+ preparable = prepared_statements ? visitor.preparable : false
end
if prepared_statements && preparable
@@ -134,11 +142,6 @@ module ActiveRecord
exec_query(sql, name, binds)
end
- # Executes the truncate statement.
- def truncate(table_name, name = nil)
- raise NotImplementedError
- end
-
# Executes update +sql+ statement in the context of this connection using
# +binds+ as the bind substitutes. +name+ is logged along with
# the executed +sql+ statement.
@@ -173,6 +176,23 @@ module ActiveRecord
exec_delete(sql, name, binds)
end
+ # Executes the truncate statement.
+ def truncate(table_name, name = nil)
+ execute(build_truncate_statements(table_name), name)
+ end
+
+ def truncate_tables(*table_names) # :nodoc:
+ return if table_names.empty?
+
+ with_multi_statements do
+ disable_referential_integrity do
+ Array(build_truncate_statements(*table_names)).each do |sql|
+ execute_batch(sql, "Truncate Tables")
+ end
+ end
+ end
+ end
+
# Runs the given block in a database transaction, and returns the result
# of the block.
#
@@ -333,46 +353,20 @@ module ActiveRecord
# We keep this method to provide fallback
# for databases like sqlite that do not support bulk inserts.
def insert_fixture(fixture, table_name)
- fixture = fixture.stringify_keys
-
- columns = schema_cache.columns_hash(table_name)
- binds = fixture.map do |name, value|
- if column = columns[name]
- type = lookup_cast_type_from_column(column)
- Relation::QueryAttribute.new(name, value, type)
- else
- raise Fixture::FixtureError, %(table "#{table_name}" has no column named #{name.inspect}.)
- end
- end
-
- table = Arel::Table.new(table_name)
-
- values = binds.map do |bind|
- value = with_yaml_fallback(bind.value_for_database)
- [table[bind.name], value]
- end
-
- manager = Arel::InsertManager.new
- manager.into(table)
- manager.insert(values)
- execute manager.to_sql, "Fixture Insert"
+ execute(build_fixture_sql(Array.wrap(fixture), table_name), "Fixture Insert")
end
def insert_fixtures_set(fixture_set, tables_to_delete = [])
- fixture_inserts = fixture_set.map do |table_name, fixtures|
- next if fixtures.empty?
-
- build_fixture_sql(fixtures, table_name)
- end.compact
-
- table_deletes = tables_to_delete.map { |table| +"DELETE FROM #{quote_table_name table}" }
- total_sql = Array.wrap(combine_multi_statements(table_deletes + fixture_inserts))
-
- disable_referential_integrity do
- transaction(requires_new: true) do
- total_sql.each do |sql|
- execute sql, "Fixtures Load"
- yield if block_given?
+ fixture_inserts = build_fixture_statements(fixture_set)
+ table_deletes = tables_to_delete.map { |table| "DELETE FROM #{quote_table_name(table)}" }
+ total_sql = Array(combine_multi_statements(table_deletes + fixture_inserts))
+
+ with_multi_statements do
+ disable_referential_integrity do
+ transaction(requires_new: true) do
+ total_sql.each do |sql|
+ execute_batch(sql, "Fixtures Load")
+ end
end
end
end
@@ -396,15 +390,33 @@ module ActiveRecord
end
end
+ # Fixture value is quoted by Arel, however scalar values
+ # are not quotable. In this case we want to convert
+ # the column value to YAML.
+ def with_yaml_fallback(value) # :nodoc:
+ if value.is_a?(Hash) || value.is_a?(Array)
+ YAML.dump(value)
+ else
+ value
+ end
+ end
+
private
+ def execute_batch(sql, name = nil)
+ execute(sql, name)
+ end
+
+ DEFAULT_INSERT_VALUE = Arel.sql("DEFAULT").freeze
+ private_constant :DEFAULT_INSERT_VALUE
+
def default_insert_value(column)
- Arel.sql("DEFAULT")
+ DEFAULT_INSERT_VALUE
end
def build_fixture_sql(fixtures, table_name)
columns = schema_cache.columns_hash(table_name)
- values = fixtures.map do |fixture|
+ values_list = fixtures.map do |fixture|
fixture = fixture.stringify_keys
unknown_columns = fixture.keys - columns.keys
@@ -426,12 +438,43 @@ module ActiveRecord
table = Arel::Table.new(table_name)
manager = Arel::InsertManager.new
manager.into(table)
- columns.each_key { |column| manager.columns << table[column] }
- manager.values = manager.create_values_list(values)
+ if values_list.size == 1
+ values = values_list.shift
+ new_values = []
+ columns.each_key.with_index { |column, i|
+ unless values[i].equal?(DEFAULT_INSERT_VALUE)
+ new_values << values[i]
+ manager.columns << table[column]
+ end
+ }
+ values_list << new_values
+ else
+ columns.each_key { |column| manager.columns << table[column] }
+ end
+
+ manager.values = manager.create_values_list(values_list)
manager.to_sql
end
+ def build_fixture_statements(fixture_set)
+ fixture_set.map do |table_name, fixtures|
+ next if fixtures.empty?
+ build_fixture_sql(fixtures, table_name)
+ end.compact
+ end
+
+ def build_truncate_statements(*table_names)
+ truncate_tables = table_names.map do |table_name|
+ "TRUNCATE TABLE #{quote_table_name(table_name)}"
+ end
+ combine_multi_statements(truncate_tables)
+ end
+
+ def with_multi_statements
+ yield
+ end
+
def combine_multi_statements(total_sql)
total_sql.join(";\n")
end
@@ -465,17 +508,6 @@ module ActiveRecord
relation
end
end
-
- # Fixture value is quoted by Arel, however scalar values
- # are not quotable. In this case we want to convert
- # the column value to YAML.
- def with_yaml_fallback(value)
- if value.is_a?(Hash) || value.is_a?(Array)
- YAML.dump(value)
- else
- value
- end
- end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb
index d950099bab..93b1c4e632 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb
@@ -97,9 +97,8 @@ module ActiveRecord
arel = arel_from_relation(arel)
sql, binds = to_sql_and_binds(arel, binds)
- if binds.length > bind_params_length
- sql, binds = unprepared_statement { to_sql_and_binds(arel) }
- preparable = false
+ if preparable.nil?
+ preparable = prepared_statements ? visitor.preparable : false
end
cache_sql(sql, name, binds) { super(sql, name, binds, preparable: preparable) }
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
index eefe621feb..9d24d839c1 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
@@ -138,15 +138,15 @@ module ActiveRecord
"'#{quote_string(value.to_s)}'"
end
- def type_casted_binds(binds) # :nodoc:
- if binds.first.is_a?(Array)
- binds.map { |column, value| type_cast(value, column) }
- else
- binds.map { |attr| type_cast(attr.value_for_database) }
+ private
+ def type_casted_binds(binds)
+ if binds.first.is_a?(Array)
+ binds.map { |column, value| type_cast(value, column) }
+ else
+ binds.map { |attr| type_cast(attr.value_for_database) }
+ end
end
- end
- private
def lookup_cast_type(sql_type)
type_map.lookup(sql_type)
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
index 11d4b4a503..4861872129 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
@@ -105,13 +105,9 @@ module ActiveRecord
!ActiveRecord::SchemaDumper.fk_ignore_pattern.match?(name) if name
end
- def defined_for?(to_table_ord = nil, to_table: nil, **options)
- if to_table_ord
- self.to_table == to_table_ord.to_s
- else
- (to_table.nil? || to_table.to_s == self.to_table) &&
- options.all? { |k, v| self.options[k].to_s == v.to_s }
- end
+ def defined_for?(to_table: nil, **options)
+ (to_table.nil? || to_table.to_s == self.to_table) &&
+ options.all? { |k, v| self.options[k].to_s == v.to_s }
end
private
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
index b202a82f55..6981ea6ecd 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
@@ -852,7 +852,7 @@ module ActiveRecord
# [<tt>:null</tt>]
# Whether the column allows nulls. Defaults to true.
#
- # ====== Create a user_id bigint column without a index
+ # ====== Create a user_id bigint column without an index
#
# add_reference(:products, :user, index: false)
#
@@ -1002,10 +1002,10 @@ module ActiveRecord
# with an addition of
# [<tt>:to_table</tt>]
# The name of the table that contains the referenced primary key.
- def remove_foreign_key(from_table, options_or_to_table = {})
+ def remove_foreign_key(from_table, to_table = nil, **options)
return unless supports_foreign_keys?
- fk_name_to_delete = foreign_key_for!(from_table, options_or_to_table).name
+ fk_name_to_delete = foreign_key_for!(from_table, to_table: to_table, **options).name
at = create_alter_table from_table
at.drop_foreign_key fk_name_to_delete
@@ -1024,8 +1024,8 @@ module ActiveRecord
# # Checks to see if a foreign key with a custom name exists.
# foreign_key_exists?(:accounts, name: "special_fk_name")
#
- def foreign_key_exists?(from_table, options_or_to_table = {})
- foreign_key_for(from_table, options_or_to_table).present?
+ def foreign_key_exists?(from_table, to_table = nil, **options)
+ foreign_key_for(from_table, to_table: to_table, **options).present?
end
def foreign_key_column_for(table_name) # :nodoc:
@@ -1051,7 +1051,7 @@ module ActiveRecord
def assume_migrated_upto_version(version, migrations_paths = nil)
unless migrations_paths.nil?
- ActiveSupport::Deprecation.warn(<<~MSG)
+ ActiveSupport::Deprecation.warn(<<~MSG.squish)
Passing migrations_paths to #assume_migrated_upto_version is deprecated and will be removed in Rails 6.1.
MSG
end
@@ -1341,14 +1341,14 @@ module ActiveRecord
end
end
- def foreign_key_for(from_table, options_or_to_table = {})
+ def foreign_key_for(from_table, **options)
return unless supports_foreign_keys?
- foreign_keys(from_table).detect { |fk| fk.defined_for? options_or_to_table }
+ foreign_keys(from_table).detect { |fk| fk.defined_for?(options) }
end
- def foreign_key_for!(from_table, options_or_to_table = {})
- foreign_key_for(from_table, options_or_to_table) || \
- raise(ArgumentError, "Table '#{from_table}' has no foreign key for #{options_or_to_table}")
+ def foreign_key_for!(from_table, to_table: nil, **options)
+ foreign_key_for(from_table, to_table: to_table, **options) ||
+ raise(ArgumentError, "Table '#{from_table}' has no foreign key for #{to_table || options}")
end
def extract_foreign_key_action(specifier)
diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
index c1d846f58d..7aad306d50 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
@@ -102,7 +102,7 @@ module ActiveRecord
end
def self.build_read_query_regexp(*parts) # :nodoc:
- parts = parts.map { |part| /\A\s*#{part}/i }
+ parts = parts.map { |part| /\A[\(\s]*#{part}/i }
Regexp.union(*parts)
end
@@ -120,6 +120,7 @@ module ActiveRecord
@quoted_column_names, @quoted_table_names = {}, {}
@prevent_writes = false
@visitor = arel_visitor
+ @statements = build_statement_pool
@lock = ActiveSupport::Concurrency::LoadInterlockAwareMonitor.new
if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true })
@@ -383,10 +384,31 @@ module ActiveRecord
false
end
+ # Does this adapter support optimizer hints?
+ def supports_optimizer_hints?
+ 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
+
# This is meant to be implemented by the adapters that support extensions
def disable_extension(name)
end
@@ -476,11 +498,9 @@ module ActiveRecord
# this should be overridden by concrete adapters
end
- ###
- # Clear any caching the database adapter may be doing, for example
- # clearing the prepared statement cache. This is database specific.
+ # Clear any caching the database adapter may be doing.
def clear_cache!
- # this should be overridden by concrete adapters
+ @lock.synchronize { @statements.clear } if @statements
end
# Returns true if its required to reload the connection between requests for development mode.
@@ -506,8 +526,8 @@ module ActiveRecord
@connection
end
- def default_uniqueness_comparison(attribute, value) # :nodoc:
- case_sensitive_comparison(attribute, value)
+ def default_uniqueness_comparison(attribute, value, klass) # :nodoc:
+ attribute.eq(value)
end
def case_sensitive_comparison(attribute, value) # :nodoc:
@@ -542,6 +562,19 @@ module ActiveRecord
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
+
private
def check_version
end
@@ -689,6 +722,9 @@ module ActiveRecord
def arel_visitor
Arel::Visitors::ToSql.new(self)
end
+
+ def build_statement_pool
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
index 246c6b5123..8ca2cfa9ed 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
@@ -53,8 +53,6 @@ module ActiveRecord
def initialize(connection, logger, connection_options, config)
super(connection, logger, config)
-
- @statements = StatementPool.new(self.class.type_cast_config_to_integer(config[:statement_limit]))
end
def version #:nodoc:
@@ -105,10 +103,23 @@ module ActiveRecord
mariadb? || version >= "5.7.5"
end
+ # See https://dev.mysql.com/doc/refman/8.0/en/optimizer-hints.html for more details.
+ def supports_optimizer_hints?
+ !mariadb? && version >= "5.7.7"
+ end
+
def supports_advisory_locks?
true
end
+ def supports_insert_on_duplicate_skip?
+ true
+ end
+
+ def supports_insert_on_duplicate_update?
+ true
+ end
+
def get_advisory_lock(lock_name, timeout = 0) # :nodoc:
query_value("SELECT GET_LOCK(#{quote(lock_name.to_s)}, #{timeout})") == 1
end
@@ -154,10 +165,9 @@ module ActiveRecord
# CONNECTION MANAGEMENT ====================================
- # Clears the prepared statements cache.
- def clear_cache!
+ def clear_cache! # :nodoc:
reload_type_map
- @statements.clear
+ super
end
#--
@@ -264,10 +274,6 @@ module ActiveRecord
show_variable "collation_database"
end
- def truncate(table_name, name = nil)
- execute "TRUNCATE TABLE #{quote_table_name(table_name)}", name
- end
-
def table_comment(table_name) # :nodoc:
scope = quoted_scope(table_name)
@@ -431,30 +437,6 @@ module ActiveRecord
table_options
end
- # Maps logical Rails types to MySQL-specific data types.
- def type_to_sql(type, limit: nil, precision: nil, scale: nil, unsigned: nil, **) # :nodoc:
- sql = \
- case type.to_s
- when "integer"
- integer_to_sql(limit)
- when "text"
- text_to_sql(limit)
- when "blob"
- binary_to_sql(limit)
- when "binary"
- if (0..0xfff) === limit
- "varbinary(#{limit})"
- else
- binary_to_sql(limit)
- end
- else
- super
- end
-
- sql = "#{sql} unsigned" if unsigned && type != :primary_key
- sql
- end
-
# SHOW VARIABLES LIKE 'name'
def show_variable(name)
query_value("SELECT @@#{name}", "SCHEMA")
@@ -477,6 +459,21 @@ module ActiveRecord
SQL
end
+ def default_uniqueness_comparison(attribute, value, klass) # :nodoc:
+ column = column_for_attribute(attribute)
+
+ if column.collation && !column.case_sensitive? && !value.nil?
+ ActiveSupport::Deprecation.warn(<<~MSG.squish)
+ Uniqueness validator will no longer enforce case sensitive comparison in Rails 6.1.
+ To continue case sensitive comparison on the :#{attribute.name} attribute in #{klass} model,
+ pass `case_sensitive: true` option explicitly to the uniqueness validator.
+ MSG
+ attribute.eq(Arel::Nodes::Bin.new(value))
+ else
+ super
+ end
+ end
+
def case_sensitive_comparison(attribute, value) # :nodoc:
column = column_for_attribute(attribute)
@@ -515,10 +512,18 @@ module ActiveRecord
index.using == :btree || super
end
- def insert_fixtures_set(fixture_set, tables_to_delete = [])
- with_multi_statements do
- super { discard_remaining_results }
+ def build_insert_sql(insert) # :nodoc:
+ sql = +"INSERT #{insert.into} #{insert.values_list}"
+
+ if insert.skip_duplicates?
+ any_column = quote_column_name(insert.model.columns.first.name)
+ sql << " ON DUPLICATE KEY UPDATE #{any_column}=#{any_column}"
+ elsif insert.update_duplicates?
+ sql << " ON DUPLICATE KEY UPDATE "
+ sql << insert.updatable_columns.map { |column| "#{column}=VALUES(#{column})" }.join(",")
end
+
+ sql
end
private
@@ -528,33 +533,6 @@ module ActiveRecord
end
end
- def combine_multi_statements(total_sql)
- total_sql.each_with_object([]) do |sql, total_sql_chunks|
- previous_packet = total_sql_chunks.last
- sql << ";\n"
- if max_allowed_packet_reached?(sql, previous_packet) || total_sql_chunks.empty?
- total_sql_chunks << sql
- else
- previous_packet << sql
- end
- end
- end
-
- def max_allowed_packet_reached?(current_packet, previous_packet)
- if current_packet.bytesize > max_allowed_packet
- raise ActiveRecordError, "Fixtures set is too large #{current_packet.bytesize}. Consider increasing the max_allowed_packet variable."
- elsif previous_packet.nil?
- false
- else
- (current_packet.bytesize + previous_packet.bytesize) > max_allowed_packet
- end
- end
-
- def max_allowed_packet
- bytes_margin = 2
- @max_allowed_packet ||= (show_variable("max_allowed_packet") - bytes_margin)
- end
-
def initialize_type_map(m = type_map)
super
@@ -794,6 +772,10 @@ module ActiveRecord
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+)`?.+?
@@ -818,37 +800,6 @@ module ActiveRecord
MismatchedForeignKey.new(options)
end
- def integer_to_sql(limit) # :nodoc:
- case limit
- when 1; "tinyint"
- when 2; "smallint"
- when 3; "mediumint"
- when nil, 4; "int"
- when 5..8; "bigint"
- else raise(ActiveRecordError, "No integer type has byte size #{limit}. Use a decimal with scale 0 instead.")
- end
- end
-
- def text_to_sql(limit) # :nodoc:
- case limit
- when 0..0xff; "tinytext"
- when nil, 0x100..0xffff; "text"
- when 0x10000..0xffffff; "mediumtext"
- when 0x1000000..0xffffffff; "longtext"
- else raise(ActiveRecordError, "No text type has byte length #{limit}")
- end
- end
-
- def binary_to_sql(limit) # :nodoc:
- case limit
- when 0..0xff; "tinyblob"
- when nil, 0x100..0xffff; "blob"
- when 0x10000..0xffffff; "mediumblob"
- when 0x1000000..0xffffffff; "longblob"
- else raise(ActiveRecordError, "No binary type has byte length #{limit}")
- end
- end
-
def version_string
full_version.match(/^(?:5\.5\.5-)?(\d+\.\d+\.\d+)/)[1]
end
diff --git a/activerecord/lib/active_record/connection_adapters/determine_if_preparable_visitor.rb b/activerecord/lib/active_record/connection_adapters/determine_if_preparable_visitor.rb
index 883747b84b..1df4dea2d8 100644
--- a/activerecord/lib/active_record/connection_adapters/determine_if_preparable_visitor.rb
+++ b/activerecord/lib/active_record/connection_adapters/determine_if_preparable_visitor.rb
@@ -3,7 +3,7 @@
module ActiveRecord
module ConnectionAdapters
module DetermineIfPreparableVisitor
- attr_reader :preparable
+ attr_accessor :preparable
def accept(*)
@preparable = true
diff --git a/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb b/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb
index 6adcc14545..1199c0ad1b 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb
@@ -11,7 +11,7 @@ module ActiveRecord
else
super
end
- discard_remaining_results
+ @connection.abandon_results!
result
end
@@ -69,22 +69,31 @@ module ActiveRecord
alias :exec_update :exec_delete
private
+ def execute_batch(sql, name = nil)
+ super
+ @connection.abandon_results!
+ end
+
def default_insert_value(column)
- Arel.sql("DEFAULT") unless column.auto_increment?
+ super unless column.auto_increment?
end
def last_inserted_id(result)
@connection.last_id
end
- def discard_remaining_results
- @connection.abandon_results!
- end
-
def supports_set_server_option?
@connection.respond_to?(:set_server_option)
end
+ def build_truncate_statements(*table_names)
+ if table_names.size == 1
+ super.first
+ else
+ super
+ end
+ end
+
def multi_statements_enabled?(flags)
if flags.is_a?(Array)
flags.include?("MULTI_STATEMENTS")
@@ -117,6 +126,36 @@ module ActiveRecord
end
end
+ def combine_multi_statements(total_sql)
+ total_sql.each_with_object([]) do |sql, total_sql_chunks|
+ previous_packet = total_sql_chunks.last
+ if max_allowed_packet_reached?(sql, previous_packet)
+ total_sql_chunks << +sql
+ else
+ previous_packet << ";\n"
+ previous_packet << sql
+ end
+ end
+ end
+
+ def max_allowed_packet_reached?(current_packet, previous_packet)
+ if current_packet.bytesize > max_allowed_packet
+ raise ActiveRecordError,
+ "Fixtures set is too large #{current_packet.bytesize}. Consider increasing the max_allowed_packet variable."
+ elsif previous_packet.nil?
+ true
+ else
+ (current_packet.bytesize + previous_packet.bytesize) > max_allowed_packet
+ end
+ end
+
+ def max_allowed_packet
+ @max_allowed_packet ||= begin
+ bytes_margin = 2
+ show_variable("max_allowed_packet") - bytes_margin
+ end
+ end
+
def exec_stmt_and_free(sql, name, binds, cache_stmt: false)
if preventing_writes? && write_query?(sql)
raise ActiveRecord::ReadOnlyError, "Write query attempted while in readonly mode: #{sql}"
diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb
index 8cb6b64fec..d21535a709 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb
@@ -64,16 +64,6 @@ module ActiveRecord
case type
when :virtual
type = options[:type]
- when :text, :blob, :binary
- case (size = options[:size])&.to_s
- when "tiny", "medium", "long"
- sql_type = @conn.native_database_types[type][:name]
- type = "#{size}#{sql_type}"
- else
- raise ArgumentError, <<~MSG unless size.nil?
- #{size.inspect} is invalid :size value. Only :tiny, :medium, and :long are allowed.
- MSG
- end
when :primary_key
type = :integer
options[:limit] ||= 8
diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb
index e9484a08de..4018f0815c 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb
@@ -97,6 +97,30 @@ module ActiveRecord
MySQL::SchemaDumper.create(self, options)
end
+ # Maps logical Rails types to MySQL-specific data types.
+ def type_to_sql(type, limit: nil, precision: nil, scale: nil, size: limit_to_size(limit, type), unsigned: nil, **)
+ sql =
+ case type.to_s
+ when "integer"
+ integer_to_sql(limit)
+ when "text"
+ type_with_size_to_sql("text", size)
+ when "blob"
+ type_with_size_to_sql("blob", size)
+ when "binary"
+ if (0..0xfff) === limit
+ "varbinary(#{limit})"
+ else
+ type_with_size_to_sql("blob", size)
+ end
+ else
+ super
+ end
+
+ sql = "#{sql} unsigned" if unsigned && type != :primary_key
+ sql
+ end
+
private
CHARSETS_OF_4BYTES_MAXLEN = ["utf8mb4", "utf16", "utf16le", "utf32"]
@@ -197,6 +221,40 @@ module ActiveRecord
schema, name = nil, schema unless name
[schema, name]
end
+
+ def type_with_size_to_sql(type, size)
+ case size&.to_s
+ when nil, "tiny", "medium", "long"
+ "#{size}#{type}"
+ else
+ raise ArgumentError,
+ "#{size.inspect} is invalid :size value. Only :tiny, :medium, and :long are allowed."
+ end
+ end
+
+ def limit_to_size(limit, type)
+ case type.to_s
+ when "text", "blob", "binary"
+ case limit
+ when 0..0xff; "tiny"
+ when nil, 0x100..0xffff; nil
+ when 0x10000..0xffffff; "medium"
+ when 0x1000000..0xffffffff; "long"
+ else raise ActiveRecordError, "No #{type} type has byte size #{limit}"
+ end
+ end
+ end
+
+ def integer_to_sql(limit)
+ case limit
+ when 1; "tinyint"
+ when 2; "smallint"
+ when 3; "mediumint"
+ when nil, 4; "int"
+ when 5..8; "bigint"
+ else raise ActiveRecordError, "No integer type has byte size #{limit}. Use a decimal with scale 0 instead."
+ end
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb
index 41633872e2..ae7dbd2868 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb
@@ -164,6 +164,10 @@ module ActiveRecord
end
private
+ def build_truncate_statements(*table_names)
+ "TRUNCATE TABLE #{table_names.map(&method(:quote_table_name)).join(", ")}"
+ end
+
# Returns the current ID of a table's sequence.
def last_insert_id_result(sequence_name)
exec_query("SELECT currval(#{quote(sequence_name)})", "SQL")
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
index 946436f7f9..a38c1325c0 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
@@ -548,14 +548,14 @@ module ActiveRecord
# The hard limit is 1GB, because of a 32-bit size field, and TOAST.
case limit
when nil, 0..0x3fffffff; super(type)
- else raise(ActiveRecordError, "No binary type has byte size #{limit}.")
+ else raise ActiveRecordError, "No binary type has byte size #{limit}. The limit on binary can be at most 1GB - 1byte."
end
when "text"
# PostgreSQL doesn't support limits on text columns.
# The hard limit is 1GB, according to section 8.3 in the manual.
case limit
when nil, 0..0x3fffffff; super(type)
- else raise(ActiveRecordError, "The limit on text can be at most 1GB - 1byte.")
+ else raise ActiveRecordError, "No text type has byte size #{limit}. The limit on text can be at most 1GB - 1byte."
end
when "integer"
case limit
@@ -623,10 +623,10 @@ module ActiveRecord
# validate_foreign_key :accounts, name: :special_fk_name
#
# The +options+ hash accepts the same keys as SchemaStatements#add_foreign_key.
- def validate_foreign_key(from_table, options_or_to_table = {})
+ def validate_foreign_key(from_table, to_table = nil, **options)
return unless supports_validate_constraints?
- fk_name_to_validate = foreign_key_for!(from_table, options_or_to_table).name
+ fk_name_to_validate = foreign_key_for!(from_table, to_table: to_table, **options).name
validate_constraint from_table, fk_name_to_validate
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
index 95beeb4cae..29f764e8f4 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
@@ -196,6 +196,17 @@ module ActiveRecord
true
end
+ def supports_insert_returning?
+ true
+ end
+
+ def supports_insert_on_conflict?
+ postgresql_version >= 90500
+ end
+ alias supports_insert_on_duplicate_skip? supports_insert_on_conflict?
+ alias supports_insert_on_duplicate_update? supports_insert_on_conflict?
+ alias supports_insert_conflict_target? supports_insert_on_conflict?
+
def index_algorithms
{ concurrently: "CONCURRENTLY" }
end
@@ -236,15 +247,10 @@ module ActiveRecord
# @local_tz is initialized as nil to avoid warnings when connect tries to use it
@local_tz = nil
- @default_timezone = nil
- @timestamp_decoder = nil
@max_identifier_length = nil
configure_connection
add_pg_encoders
- @statements = StatementPool.new @connection,
- self.class.type_cast_config_to_integer(config[:statement_limit])
-
add_pg_decoders
@type_map = Type::HashLookupTypeMap.new
@@ -253,17 +259,6 @@ module ActiveRecord
@use_insert_returning = @config.key?(:insert_returning) ? self.class.type_cast_config_to_boolean(@config[:insert_returning]) : true
end
- # Clears the prepared statements cache.
- def clear_cache!
- @lock.synchronize do
- @statements.clear
- end
- end
-
- def truncate(table_name, name = nil)
- exec_query "TRUNCATE TABLE #{quote_table_name(table_name)}", name, []
- end
-
# Is this connection alive and ready for queries?
def active?
@lock.synchronize do
@@ -280,6 +275,8 @@ module ActiveRecord
super
@connection.reset
configure_connection
+ rescue PG::ConnectionBad
+ connect
end
end
@@ -350,6 +347,13 @@ module ActiveRecord
postgresql_version >= 90400
end
+ def supports_optimizer_hints?
+ unless defined?(@has_pg_hint_plan)
+ @has_pg_hint_plan = extension_available?("pg_hint_plan")
+ end
+ @has_pg_hint_plan
+ end
+
def supports_lazy_transactions?
true
end
@@ -380,9 +384,12 @@ module ActiveRecord
}
end
+ def extension_available?(name)
+ query_value("SELECT true FROM pg_available_extensions WHERE name = #{quote(name)}", "SCHEMA")
+ end
+
def extension_enabled?(name)
- res = exec_query("SELECT EXISTS(SELECT * FROM pg_available_extensions WHERE name = '#{name}' AND installed_version IS NOT NULL) as enabled", "SCHEMA")
- res.cast_values.first
+ query_value("SELECT installed_version IS NOT NULL FROM pg_available_extensions WHERE name = #{quote(name)}", "SCHEMA")
end
def extensions
@@ -425,6 +432,20 @@ module ActiveRecord
index.using == :btree || super
end
+ def build_insert_sql(insert) # :nodoc:
+ sql = +"INSERT #{insert.into} #{insert.values_list}"
+
+ if insert.skip_duplicates?
+ sql << " ON CONFLICT #{insert.conflict_target} DO NOTHING"
+ elsif insert.update_duplicates?
+ sql << " ON CONFLICT #{insert.conflict_target} DO UPDATE SET "
+ sql << insert.updatable_columns.map { |column| "#{column}=excluded.#{column}" }.join(",")
+ end
+
+ sql << " RETURNING #{insert.returning}" if insert.returning
+ sql
+ end
+
private
def check_version
if postgresql_version < 90300
@@ -723,6 +744,8 @@ module ActiveRecord
def connect
@connection = PG.connect(@connection_parameters)
configure_connection
+ add_pg_encoders
+ add_pg_decoders
end
# Configures the encoding, verbosity, schema search path, and time zone of the connection.
@@ -803,6 +826,10 @@ module ActiveRecord
Arel::Visitors::PostgreSQL.new(self)
end
+ def build_statement_pool
+ StatementPool.new(@connection, self.class.type_cast_config_to_integer(@config[:statement_limit]))
+ end
+
def can_perform_case_insensitive_comparison_for?(column)
@case_insensitive_cache ||= {}
@case_insensitive_cache[column.sql_type] ||= begin
@@ -846,6 +873,9 @@ module ActiveRecord
end
def add_pg_decoders
+ @default_timezone = nil
+ @timestamp_decoder = nil
+
coders_by_name = {
"int2" => PG::TextDecoder::Integer,
"int4" => PG::TextDecoder::Integer,
diff --git a/activerecord/lib/active_record/connection_adapters/schema_cache.rb b/activerecord/lib/active_record/connection_adapters/schema_cache.rb
index c10765f42d..07453b4403 100644
--- a/activerecord/lib/active_record/connection_adapters/schema_cache.rb
+++ b/activerecord/lib/active_record/connection_adapters/schema_cache.rb
@@ -13,6 +13,7 @@ module ActiveRecord
@columns_hash = {}
@primary_keys = {}
@data_sources = {}
+ @indexes = {}
end
def initialize_dup(other)
@@ -21,22 +22,25 @@ module ActiveRecord
@columns_hash = @columns_hash.dup
@primary_keys = @primary_keys.dup
@data_sources = @data_sources.dup
+ @indexes = @indexes.dup
end
def encode_with(coder)
- coder["columns"] = @columns
+ coder["columns"] = @columns
coder["columns_hash"] = @columns_hash
coder["primary_keys"] = @primary_keys
coder["data_sources"] = @data_sources
- coder["version"] = connection.migration_context.current_version
+ coder["indexes"] = @indexes
+ coder["version"] = connection.migration_context.current_version
end
def init_with(coder)
- @columns = coder["columns"]
+ @columns = coder["columns"]
@columns_hash = coder["columns_hash"]
@primary_keys = coder["primary_keys"]
@data_sources = coder["data_sources"]
- @version = coder["version"]
+ @indexes = coder["indexes"] || {}
+ @version = coder["version"]
end
def primary_keys(table_name)
@@ -57,6 +61,7 @@ module ActiveRecord
primary_keys(table_name)
columns(table_name)
columns_hash(table_name)
+ indexes(table_name)
end
end
@@ -82,17 +87,22 @@ module ActiveRecord
@columns_hash.key?(table_name)
end
+ def indexes(table_name)
+ @indexes[table_name] ||= connection.indexes(table_name)
+ end
+
# Clears out internal caches
def clear!
@columns.clear
@columns_hash.clear
@primary_keys.clear
@data_sources.clear
+ @indexes.clear
@version = nil
end
def size
- [@columns, @columns_hash, @primary_keys, @data_sources].map(&:size).inject :+
+ [@columns, @columns_hash, @primary_keys, @data_sources].sum(&:size)
end
# Clear out internal caches for the data source +name+.
@@ -101,20 +111,21 @@ module ActiveRecord
@columns_hash.delete name
@primary_keys.delete name
@data_sources.delete name
+ @indexes.delete name
end
def marshal_dump
# if we get current version during initialization, it happens stack over flow.
@version = connection.migration_context.current_version
- [@version, @columns, @columns_hash, @primary_keys, @data_sources]
+ [@version, @columns, @columns_hash, @primary_keys, @data_sources, @indexes]
end
def marshal_load(array)
- @version, @columns, @columns_hash, @primary_keys, @data_sources = array
+ @version, @columns, @columns_hash, @primary_keys, @data_sources, @indexes = array
+ @indexes = @indexes || {}
end
private
-
def prepare_data_sources
connection.data_sources.each { |source| @data_sources[source] = true }
end
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/database_statements.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/database_statements.rb
new file mode 100644
index 0000000000..84dcae49b9
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3/database_statements.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+module ActiveRecord
+ module ConnectionAdapters
+ module SQLite3
+ module DatabaseStatements
+ private
+ def execute_batch(sql, name = nil)
+ if preventing_writes? && write_query?(sql)
+ raise ActiveRecord::ReadOnlyError, "Write query attempted while in readonly mode: #{sql}"
+ end
+
+ materialize_transactions
+
+ log(sql, name) do
+ ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
+ @connection.execute_batch(sql)
+ end
+ end
+ end
+
+ def build_fixture_statements(fixture_set)
+ fixture_set.flat_map do |table_name, fixtures|
+ next if fixtures.empty?
+ fixtures.map { |fixture| build_fixture_sql([fixture], table_name) }
+ end.compact
+ end
+
+ def build_truncate_statements(*table_names)
+ truncate_tables = table_names.map do |table_name|
+ "DELETE FROM #{quote_table_name(table_name)}"
+ end
+ combine_multi_statements(truncate_tables)
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb
index bd649451d6..e64e995e1a 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb
@@ -72,7 +72,7 @@ module ActiveRecord
table = strip_table_name_prefix_and_suffix(table)
fk_to_table = strip_table_name_prefix_and_suffix(fk.to_table)
fk_to_table == table && options.all? { |k, v| fk.options[k].to_s == v.to_s }
- end || raise(ArgumentError, "Table '#{from_table}' has no foreign key for #{to_table}")
+ end || raise(ArgumentError, "Table '#{from_table}' has no foreign key for #{to_table || options}")
foreign_keys.delete(fkey)
alter_table(from_table, foreign_keys)
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
index 9f28fdf749..ff23a525b9 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
@@ -4,6 +4,7 @@ require "active_record/connection_adapters/abstract_adapter"
require "active_record/connection_adapters/statement_pool"
require "active_record/connection_adapters/sqlite3/explain_pretty_printer"
require "active_record/connection_adapters/sqlite3/quoting"
+require "active_record/connection_adapters/sqlite3/database_statements"
require "active_record/connection_adapters/sqlite3/schema_creation"
require "active_record/connection_adapters/sqlite3/schema_definitions"
require "active_record/connection_adapters/sqlite3/schema_dumper"
@@ -36,8 +37,6 @@ module ActiveRecord
config.merge(results_as_hash: true)
)
- db.busy_timeout(ConnectionAdapters::SQLite3Adapter.type_cast_config_to_integer(config[:timeout])) if config[:timeout]
-
ConnectionAdapters::SQLite3Adapter.new(db, logger, nil, config)
rescue Errno::ENOENT => error
if error.message.include?("No such file or directory")
@@ -60,6 +59,7 @@ module ActiveRecord
include SQLite3::Quoting
include SQLite3::SchemaStatements
+ include SQLite3::DatabaseStatements
NATIVE_DATABASE_TYPES = {
primary_key: "integer PRIMARY KEY AUTOINCREMENT NOT NULL",
@@ -95,9 +95,6 @@ module ActiveRecord
def initialize(connection, logger, connection_options, config)
super(connection, logger, config)
-
- @active = true
- @statements = StatementPool.new(self.class.type_cast_config_to_integer(config[:statement_limit]))
configure_connection
end
@@ -137,23 +134,29 @@ module ActiveRecord
true
end
+ def supports_insert_on_conflict?
+ sqlite_version >= "3.24.0"
+ end
+ alias supports_insert_on_duplicate_skip? supports_insert_on_conflict?
+ alias supports_insert_on_duplicate_update? supports_insert_on_conflict?
+ alias supports_insert_conflict_target? supports_insert_on_conflict?
+
def active?
- @active
+ !@connection.closed?
+ end
+
+ def reconnect!
+ super
+ connect if @connection.closed?
end
# Disconnects from the database if already connected. Otherwise, this
# method does nothing.
def disconnect!
super
- @active = false
@connection.close rescue nil
end
- # Clears the prepared statements cache.
- def clear_cache!
- @statements.clear
- end
-
def supports_index_sort_order?
true
end
@@ -381,16 +384,17 @@ module ActiveRecord
end
end
- def insert_fixtures_set(fixture_set, tables_to_delete = [])
- disable_referential_integrity do
- transaction(requires_new: true) do
- tables_to_delete.each { |table| delete "DELETE FROM #{quote_table_name(table)}", "Fixture Delete" }
+ def build_insert_sql(insert) # :nodoc:
+ sql = +"INSERT #{insert.into} #{insert.values_list}"
- fixture_set.each do |table_name, rows|
- rows.each { |row| insert_fixture(row, table_name) }
- end
- end
+ if insert.skip_duplicates?
+ sql << " ON CONFLICT #{insert.conflict_target} DO NOTHING"
+ elsif insert.update_duplicates?
+ sql << " ON CONFLICT #{insert.conflict_target} DO UPDATE SET "
+ sql << insert.updatable_columns.map { |column| "#{column}=excluded.#{column}" }.join(",")
end
+
+ sql
end
private
@@ -589,7 +593,21 @@ module ActiveRecord
Arel::Visitors::SQLite.new(self)
end
+ def build_statement_pool
+ StatementPool.new(self.class.type_cast_config_to_integer(@config[:statement_limit]))
+ end
+
+ def connect
+ @connection = ::SQLite3::Database.new(
+ @config[:database].to_s,
+ @config.merge(results_as_hash: true)
+ )
+ configure_connection
+ end
+
def configure_connection
+ @connection.busy_timeout(self.class.type_cast_config_to_integer(@config[:timeout])) if @config[:timeout]
+
execute("PRAGMA foreign_keys = ON", "SCHEMA")
end
diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb
index c67980173f..eb4b48bc37 100644
--- a/activerecord/lib/active_record/core.rb
+++ b/activerecord/lib/active_record/core.rb
@@ -161,7 +161,7 @@ module ActiveRecord
return super if block_given? ||
primary_key.nil? ||
scope_attributes? ||
- columns_hash.include?(inheritance_column)
+ columns_hash.key?(inheritance_column) && !base_class?
id = ids.first
@@ -182,7 +182,8 @@ module ActiveRecord
end
def find_by(*args) # :nodoc:
- return super if scope_attributes? || reflect_on_all_aggregations.any?
+ return super if scope_attributes? || reflect_on_all_aggregations.any? ||
+ columns_hash.key?(inheritance_column) && !base_class?
hash = args.first
diff --git a/activerecord/lib/active_record/database_configurations.rb b/activerecord/lib/active_record/database_configurations.rb
index a6c702cbbc..4656045fe5 100644
--- a/activerecord/lib/active_record/database_configurations.rb
+++ b/activerecord/lib/active_record/database_configurations.rb
@@ -106,7 +106,7 @@ module ActiveRecord
build_db_config = configs.each_pair.flat_map do |env_name, config|
walk_configs(env_name.to_s, "primary", config)
- end.compact
+ end.flatten.compact
if url = ENV["DATABASE_URL"]
build_url_config(url, build_db_config)
diff --git a/activerecord/lib/active_record/enum.rb b/activerecord/lib/active_record/enum.rb
index e6dba66a08..8077630aeb 100644
--- a/activerecord/lib/active_record/enum.rb
+++ b/activerecord/lib/active_record/enum.rb
@@ -31,7 +31,9 @@ module ActiveRecord
# as well. With the above example:
#
# Conversation.active
+ # Conversation.not_active
# Conversation.archived
+ # Conversation.not_archived
#
# Of course, you can also query them directly if the scopes don't fit your
# needs:
@@ -196,9 +198,13 @@ module ActiveRecord
define_method("#{value_method_name}!") { update!(attr => value) }
# scope :active, -> { where(status: 0) }
+ # scope :not_active, -> { where.not(status: 0) }
if enum_scopes != false
klass.send(:detect_enum_conflict!, name, value_method_name, true)
klass.scope value_method_name, -> { where(attr => value) }
+
+ klass.send(:detect_enum_conflict!, name, "not_#{value_method_name}", true)
+ klass.scope "not_#{value_method_name}", -> { where.not(attr => value) }
end
end
end
diff --git a/activerecord/lib/active_record/errors.rb b/activerecord/lib/active_record/errors.rb
index 48c0e8bbcc..60cf9818c1 100644
--- a/activerecord/lib/active_record/errors.rb
+++ b/activerecord/lib/active_record/errors.rb
@@ -68,7 +68,7 @@ module ActiveRecord
# Raised by {ActiveRecord::Base#save!}[rdoc-ref:Persistence#save!] and
# {ActiveRecord::Base.create!}[rdoc-ref:Persistence::ClassMethods#create!]
- # methods when a record is invalid and can not be saved.
+ # methods when a record is invalid and cannot be saved.
class RecordNotSaved < ActiveRecordError
attr_reader :record
diff --git a/activerecord/lib/active_record/gem_version.rb b/activerecord/lib/active_record/gem_version.rb
index 16dff9c2b0..f77bc2e3c1 100644
--- a/activerecord/lib/active_record/gem_version.rb
+++ b/activerecord/lib/active_record/gem_version.rb
@@ -10,7 +10,7 @@ module ActiveRecord
MAJOR = 6
MINOR = 0
TINY = 0
- PRE = "beta1"
+ PRE = "beta3"
STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
end
diff --git a/activerecord/lib/active_record/insert_all.rb b/activerecord/lib/active_record/insert_all.rb
new file mode 100644
index 0000000000..98c98d61cd
--- /dev/null
+++ b/activerecord/lib/active_record/insert_all.rb
@@ -0,0 +1,167 @@
+# frozen_string_literal: true
+
+module ActiveRecord
+ class InsertAll
+ attr_reader :model, :connection, :inserts, :keys
+ attr_reader :on_duplicate, :returning, :unique_by
+
+ def initialize(model, inserts, on_duplicate:, returning: nil, unique_by: nil)
+ raise ArgumentError, "Empty list of attributes passed" if inserts.blank?
+
+ @model, @connection, @inserts, @keys = model, model.connection, inserts, inserts.first.keys.map(&:to_s).to_set
+ @on_duplicate, @returning, @unique_by = on_duplicate, returning, unique_by
+
+ @returning = (connection.supports_insert_returning? ? primary_keys : false) if @returning.nil?
+ @returning = false if @returning == []
+
+ @on_duplicate = :skip if @on_duplicate == :update && updatable_columns.empty?
+
+ ensure_valid_options_for_connection!
+ end
+
+ def execute
+ connection.exec_query to_sql, "Bulk Insert"
+ end
+
+ def updatable_columns
+ keys - readonly_columns - unique_by_columns
+ end
+
+ def skip_duplicates?
+ on_duplicate == :skip
+ end
+
+ def update_duplicates?
+ on_duplicate == :update
+ end
+
+ def map_key_with_value
+ inserts.map do |attributes|
+ attributes = attributes.stringify_keys
+ verify_attributes(attributes)
+
+ keys.map do |key|
+ yield key, attributes[key]
+ end
+ end
+ end
+
+ private
+ def ensure_valid_options_for_connection!
+ if returning && !connection.supports_insert_returning?
+ raise ArgumentError, "#{connection.class} does not support :returning"
+ end
+
+ unless %i{ raise skip update }.member?(on_duplicate)
+ raise NotImplementedError, "#{on_duplicate.inspect} is an unknown value for :on_duplicate. Valid values are :raise, :skip, and :update"
+ end
+
+ if on_duplicate == :skip && !connection.supports_insert_on_duplicate_skip?
+ raise ArgumentError, "#{connection.class} does not support skipping duplicates"
+ end
+
+ if on_duplicate == :update && !connection.supports_insert_on_duplicate_update?
+ raise ArgumentError, "#{connection.class} does not support upsert"
+ end
+
+ if unique_by && !connection.supports_insert_conflict_target?
+ raise ArgumentError, "#{connection.class} does not support :unique_by"
+ end
+ end
+
+ def to_sql
+ connection.build_insert_sql(ActiveRecord::InsertAll::Builder.new(self))
+ end
+
+ def readonly_columns
+ primary_keys + model.readonly_attributes.to_a
+ end
+
+ def unique_by_columns
+ unique_by ? unique_by.fetch(:columns).map(&:to_s) : []
+ end
+
+ def primary_keys
+ Array.wrap(model.primary_key)
+ end
+
+ def verify_attributes(attributes)
+ if keys != attributes.keys.to_set
+ raise ArgumentError, "All objects being inserted must have the same keys"
+ end
+ end
+
+
+ class Builder
+ attr_reader :model
+
+ delegate :skip_duplicates?, :update_duplicates?, to: :insert_all
+
+ def initialize(insert_all)
+ @insert_all, @model, @connection = insert_all, insert_all.model, insert_all.connection
+ end
+
+ def into
+ "INTO #{model.quoted_table_name}(#{columns_list})"
+ end
+
+ def values_list
+ types = extract_types_from_columns_on(model.table_name, keys: insert_all.keys)
+
+ values_list = insert_all.map_key_with_value do |key, value|
+ bind = Relation::QueryAttribute.new(key, value, types[key])
+ connection.with_yaml_fallback(bind.value_for_database)
+ end
+
+ Arel::InsertManager.new.create_values_list(values_list).to_sql
+ end
+
+ def returning
+ quote_columns(insert_all.returning).join(",") if insert_all.returning
+ end
+
+ def conflict_target
+ return unless conflict_columns
+ sql = +"(#{quote_columns(conflict_columns).join(',')})"
+ sql << " WHERE #{where}" if where
+ sql
+ end
+
+ def updatable_columns
+ quote_columns(insert_all.updatable_columns)
+ end
+
+ private
+ attr_reader :connection, :insert_all
+
+ def columns_list
+ quote_columns(insert_all.keys).join(",")
+ end
+
+ def extract_types_from_columns_on(table_name, keys:)
+ columns = connection.schema_cache.columns_hash(table_name)
+
+ unknown_column = (keys - columns.keys).first
+ raise UnknownAttributeError.new(model.new, unknown_column) if unknown_column
+
+ keys.map { |key| [ key, connection.lookup_cast_type_from_column(columns[key]) ] }.to_h
+ end
+
+ def quote_columns(columns)
+ columns.map(&connection.method(:quote_column_name))
+ end
+
+ def conflict_columns
+ @conflict_columns ||= begin
+ conflict_columns = insert_all.unique_by.fetch(:columns) if insert_all.unique_by
+ conflict_columns ||= Array.wrap(model.primary_key) if update_duplicates?
+ conflict_columns
+ end
+ end
+
+ def where
+ insert_all.unique_by && insert_all.unique_by[:where]
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb
index c20274420f..997b7f763a 100644
--- a/activerecord/lib/active_record/migration.rb
+++ b/activerecord/lib/active_record/migration.rb
@@ -308,7 +308,7 @@ module ActiveRecord
# named +column_name+ from the table called +table_name+.
# * <tt>remove_columns(table_name, *column_names)</tt>: Removes the given
# columns from the table definition.
- # * <tt>remove_foreign_key(from_table, options_or_to_table)</tt>: Removes the
+ # * <tt>remove_foreign_key(from_table, to_table = nil, **options)</tt>: Removes the
# given foreign key from the table called +table_name+.
# * <tt>remove_index(table_name, column: column_names)</tt>: Removes the index
# specified by +column_names+.
diff --git a/activerecord/lib/active_record/migration/command_recorder.rb b/activerecord/lib/active_record/migration/command_recorder.rb
index 82f5121d94..8e7f596076 100644
--- a/activerecord/lib/active_record/migration/command_recorder.rb
+++ b/activerecord/lib/active_record/migration/command_recorder.rb
@@ -231,24 +231,15 @@ module ActiveRecord
end
def invert_remove_foreign_key(args)
- from_table, options_or_to_table, options_or_nil = args
+ options = args.extract_options!
+ from_table, to_table = args
- to_table = if options_or_to_table.is_a?(Hash)
- options_or_to_table[:to_table]
- else
- options_or_to_table
- end
-
- remove_options = if options_or_to_table.is_a?(Hash)
- options_or_to_table.except(:to_table)
- else
- options_or_nil
- end
+ to_table ||= options.delete(:to_table)
raise ActiveRecord::IrreversibleMigration, "remove_foreign_key is only reversible if given a second table" if to_table.nil?
reversed_args = [from_table, to_table]
- reversed_args << remove_options if remove_options.present?
+ reversed_args << options unless options.empty?
[:add_foreign_key, reversed_args]
end
diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb
index 10148d0dca..0c31f0f57e 100644
--- a/activerecord/lib/active_record/persistence.rb
+++ b/activerecord/lib/active_record/persistence.rb
@@ -1,5 +1,7 @@
# frozen_string_literal: true
+require "active_record/insert_all"
+
module ActiveRecord
# = Active Record \Persistence
module Persistence
@@ -55,6 +57,207 @@ module ActiveRecord
end
end
+ # Inserts a single record into the database. This method constructs a single SQL INSERT
+ # statement and sends it straight to the database. It does not instantiate the involved
+ # models and it does not trigger Active Record callbacks or validations. However, values
+ # passed to #insert will still go through Active Record's normal type casting and
+ # serialization.
+ #
+ # See <tt>ActiveRecord::Persistence#insert_all</tt> for documentation.
+ def insert(attributes, returning: nil, unique_by: nil)
+ insert_all([ attributes ], returning: returning, unique_by: unique_by)
+ end
+
+ # Inserts multiple records into the database. This method constructs a single SQL INSERT
+ # statement and sends it straight to the database. It does not instantiate the involved
+ # models and it does not trigger Active Record callbacks or validations. However, values
+ # passed to #insert_all will still go through Active Record's normal type casting and
+ # serialization.
+ #
+ # The +attributes+ parameter is an Array of Hashes. These Hashes describe the
+ # attributes on the objects that are to be created. All of the Hashes must have
+ # same keys.
+ #
+ # Records that would violate a unique constraint on the table are skipped.
+ #
+ # Returns an <tt>ActiveRecord::Result</tt>. The contents of the result depend on the
+ # value of <tt>:returning</tt> (see below).
+ #
+ # ==== Options
+ #
+ # [:returning]
+ # (Postgres-only) An array of attributes that should be returned for all successfully
+ # inserted records. For databases that support <tt>INSERT ... RETURNING</tt>, this will default
+ # to returning the primary keys of the successfully inserted records. Pass
+ # <tt>returning: %w[ id name ]</tt> to return the id and name of every successfully inserted
+ # record or pass <tt>returning: false</tt> to omit the clause.
+ #
+ # [:unique_by]
+ # (Postgres and SQLite only) In a table with more than one unique constraint or index,
+ # new records may be considered duplicates according to different criteria. By default,
+ # new rows will be skipped if they violate _any_ unique constraint or index. By defining
+ # <tt>:unique_by</tt>, you can skip rows that would create duplicates according to the given
+ # constraint but raise <tt>ActiveRecord::RecordNotUnique</tt> if rows violate other constraints.
+ #
+ # (For example, maybe you assume a client will try to import the same ISBNs more than
+ # once and want to silently ignore the duplicate records, but you don't except any of
+ # your code to attempt to create two rows with the same primary key and would appreciate
+ # an exception report in that scenario.)
+ #
+ # Indexes can be identified by an array of columns:
+ #
+ # unique_by: { columns: %w[ isbn ] }
+ #
+ # Partial indexes can be identified by an array of columns and a <tt>:where</tt> condition:
+ #
+ # unique_by: { columns: %w[ isbn ], where: "published_on IS NOT NULL" }
+ #
+ # ==== Example
+ #
+ # # Insert multiple records and skip duplicates
+ # # ('Eloquent Ruby' will be skipped because its id is duplicate)
+ # Book.insert_all([
+ # { id: 1, title: 'Rework', author: 'David' },
+ # { id: 1, title: 'Eloquent Ruby', author: 'Russ' }
+ # ])
+ #
+ def insert_all(attributes, returning: nil, unique_by: nil)
+ InsertAll.new(self, attributes, on_duplicate: :skip, returning: returning, unique_by: unique_by).execute
+ end
+
+ # Inserts a single record into the database. This method constructs a single SQL INSERT
+ # statement and sends it straight to the database. It does not instantiate the involved
+ # models and it does not trigger Active Record callbacks or validations. However, values
+ # passed to #insert! will still go through Active Record's normal type casting and
+ # serialization.
+ #
+ # See <tt>ActiveRecord::Persistence#insert_all!</tt> for documentation.
+ def insert!(attributes, returning: nil)
+ insert_all!([ attributes ], returning: returning)
+ end
+
+ # Inserts multiple records into the database. This method constructs a single SQL INSERT
+ # statement and sends it straight to the database. It does not instantiate the involved
+ # models and it does not trigger Active Record callbacks or validations. However, values
+ # passed to #insert_all! will still go through Active Record's normal type casting and
+ # serialization.
+ #
+ # The +attributes+ parameter is an Array of Hashes. These Hashes describe the
+ # attributes on the objects that are to be created. All of the Hashes must have
+ # same keys.
+ #
+ # #insert_all! will raise <tt>ActiveRecord::RecordNotUnique</tt> if any of the records being
+ # inserts would violate a unique constraint on the table. In that case, no records
+ # would be inserted.
+ #
+ # To skip duplicate records, see <tt>ActiveRecord::Persistence#insert_all</tt>.
+ # To replace them, see <tt>ActiveRecord::Persistence#upsert_all</tt>.
+ #
+ # Returns an <tt>ActiveRecord::Result</tt>. The contents of the result depend on the
+ # value of <tt>:returning</tt> (see below).
+ #
+ # ==== Options
+ #
+ # [:returning]
+ # (Postgres-only) An array of attributes that should be returned for all successfully
+ # inserted records. For databases that support <tt>INSERT ... RETURNING</tt>, this will default
+ # to returning the primary keys of the successfully inserted records. Pass
+ # <tt>returning: %w[ id name ]</tt> to return the id and name of every successfully inserted
+ # record or pass <tt>returning: false</tt> to omit the clause.
+ #
+ # ==== Examples
+ #
+ # # Insert multiple records
+ # Book.insert_all!([
+ # { title: 'Rework', author: 'David' },
+ # { title: 'Eloquent Ruby', author: 'Russ' }
+ # ])
+ #
+ # # Raises ActiveRecord::RecordNotUnique because 'Eloquent Ruby'
+ # # does not have a unique ID
+ # Book.insert_all!([
+ # { id: 1, title: 'Rework', author: 'David' },
+ # { id: 1, title: 'Eloquent Ruby', author: 'Russ' }
+ # ])
+ #
+ def insert_all!(attributes, returning: nil)
+ InsertAll.new(self, attributes, on_duplicate: :raise, returning: returning).execute
+ end
+
+ # Upserts (updates or inserts) a single record into the database. This method constructs
+ # a single SQL INSERT statement and sends it straight to the database. It does not
+ # instantiate the involved models and it does not trigger Active Record callbacks or
+ # validations. However, values passed to #upsert will still go through Active Record's
+ # normal type casting and serialization.
+ #
+ # See <tt>ActiveRecord::Persistence#upsert_all</tt> for documentation.
+ def upsert(attributes, returning: nil, unique_by: nil)
+ upsert_all([ attributes ], returning: returning, unique_by: unique_by)
+ end
+
+ # Upserts (updates or inserts) multiple records into the database. This method constructs
+ # a single SQL INSERT statement and sends it straight to the database. It does not
+ # instantiate the involved models and it does not trigger Active Record callbacks or
+ # validations. However, values passed to #upsert_all will still go through Active Record's
+ # normal type casting and serialization.
+ #
+ # The +attributes+ parameter is an Array of Hashes. These Hashes describe the
+ # attributes on the objects that are to be created. All of the Hashes must have
+ # same keys.
+ #
+ # Returns an <tt>ActiveRecord::Result</tt>. The contents of the result depend on the
+ # value of <tt>:returning</tt> (see below).
+ #
+ # ==== Options
+ #
+ # [:returning]
+ # (Postgres-only) An array of attributes that should be returned for all successfully
+ # inserted records. For databases that support <tt>INSERT ... RETURNING</tt>, this will default
+ # to returning the primary keys of the successfully inserted records. Pass
+ # <tt>returning: %w[ id name ]</tt> to return the id and name of every successfully inserted
+ # record or pass <tt>returning: false</tt> to omit the clause.
+ #
+ # [:unique_by]
+ # (Postgres and SQLite only) In a table with more than one unique constraint or index,
+ # new records may be considered duplicates according to different criteria. For MySQL,
+ # an upsert will take place if a new record violates _any_ unique constraint. For
+ # Postgres and SQLite, new rows will replace existing rows when the new row has the
+ # same primary key as the existing row. In case of SQLite, an upsert operation causes
+ # an insert to behave as an update or a no-op if the insert would violate
+ # a uniqueness constraint. By defining <tt>:unique_by</tt>, you can supply
+ # a different unique constraint for matching new records to existing ones than the
+ # primary key.
+ #
+ # (For example, if you have a unique index on the ISBN column and use that as
+ # the <tt>:unique_by</tt>, a new record with the same ISBN as an existing record
+ # will replace the existing record but a new record with the same primary key
+ # as an existing record will raise <tt>ActiveRecord::RecordNotUnique</tt>.)
+ #
+ # Indexes can be identified by an array of columns:
+ #
+ # unique_by: { columns: %w[ isbn ] }
+ #
+ # Partial indexes can be identified by an array of columns and a <tt>:where</tt> condition:
+ #
+ # unique_by: { columns: %w[ isbn ], where: "published_on IS NOT NULL" }
+ #
+ # ==== Examples
+ #
+ # # Given a unique index on <tt>books.isbn</tt> and the following record:
+ # Book.create!(title: 'Rework', author: 'David', isbn: '1')
+ #
+ # # Insert multiple records, allowing new records with the same ISBN
+ # # as an existing record to overwrite the existing record.
+ # # ('Eloquent Ruby' will overwrite 'Rework' because its ISBN is duplicate)
+ # Book.upsert_all([
+ # { title: 'Eloquent Ruby', author: 'Russ', isbn: '1' },
+ # { title: 'Clean Code', author: 'Robert', isbn: '2' }
+ # ], unique_by: { columns: %w[ isbn ] })
+ #
+ def upsert_all(attributes, returning: nil, unique_by: nil)
+ InsertAll.new(self, attributes, on_duplicate: :update, returning: returning, unique_by: unique_by).execute
+ end
+
# Given an attributes hash, +instantiate+ returns a new instance of
# the appropriate class. Accepts only keys as strings.
#
diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb
index b3eeb60571..86021f0a80 100644
--- a/activerecord/lib/active_record/querying.rb
+++ b/activerecord/lib/active_record/querying.rb
@@ -2,18 +2,23 @@
module ActiveRecord
module Querying
- delegate :find, :take, :take!, :first, :first!, :last, :last!, :exists?, :any?, :many?, :none?, :one?, to: :all
- delegate :second, :second!, :third, :third!, :fourth, :fourth!, :fifth, :fifth!, :forty_two, :forty_two!, :third_to_last, :third_to_last!, :second_to_last, :second_to_last!, to: :all
- delegate :first_or_create, :first_or_create!, :first_or_initialize, to: :all
- delegate :find_or_create_by, :find_or_create_by!, :create_or_find_by, :create_or_find_by!, :find_or_initialize_by, to: :all
- delegate :find_by, :find_by!, to: :all
- delegate :destroy_all, :delete_all, :update_all, :destroy_by, :delete_by, to: :all
- delegate :find_each, :find_in_batches, :in_batches, to: :all
- delegate :select, :group, :order, :except, :reorder, :limit, :offset, :joins, :left_joins, :left_outer_joins, :or,
- :where, :rewhere, :preload, :eager_load, :includes, :from, :lock, :readonly, :extending,
- :having, :create_with, :distinct, :references, :none, :unscope, :merge, to: :all
- delegate :count, :average, :minimum, :maximum, :sum, :calculate, to: :all
- delegate :pluck, :pick, :ids, to: :all
+ QUERYING_METHODS = [
+ :find, :find_by, :find_by!, :take, :take!, :first, :first!, :last, :last!,
+ :second, :second!, :third, :third!, :fourth, :fourth!, :fifth, :fifth!,
+ :forty_two, :forty_two!, :third_to_last, :third_to_last!, :second_to_last, :second_to_last!,
+ :exists?, :any?, :many?, :none?, :one?,
+ :first_or_create, :first_or_create!, :first_or_initialize,
+ :find_or_create_by, :find_or_create_by!, :find_or_initialize_by,
+ :create_or_find_by, :create_or_find_by!,
+ :destroy_all, :delete_all, :update_all, :destroy_by, :delete_by,
+ :find_each, :find_in_batches, :in_batches,
+ :select, :reselect, :order, :reorder, :group, :limit, :offset, :joins, :left_joins, :left_outer_joins,
+ :where, :rewhere, :preload, :eager_load, :includes, :from, :lock, :readonly, :extending, :or,
+ :having, :create_with, :distinct, :references, :none, :unscope, :optimizer_hints, :merge, :except, :only,
+ :count, :average, :minimum, :maximum, :sum, :calculate,
+ :pluck, :pick, :ids
+ ].freeze # :nodoc:
+ delegate(*QUERYING_METHODS, to: :all)
# Executes a custom SQL query against your database and returns all the results. The results will
# be returned as an array, with the requested columns encapsulated as attributes of the model you call
diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake
index 8de06e8466..f021a8f6c4 100644
--- a/activerecord/lib/active_record/railties/databases.rake
+++ b/activerecord/lib/active_record/railties/databases.rake
@@ -66,6 +66,11 @@ db_namespace = namespace :db do
end
end
+ # desc "Truncates tables of each database for current environment"
+ task truncate_all: [:load_config, :check_protected_environments] do
+ ActiveRecord::Tasks::DatabaseTasks.truncate_all
+ end
+
# desc "Empty the database from DATABASE_URL or config/database.yml for the current RAILS_ENV (use db:purge:all to purge all databases in the config). Without RAILS_ENV it defaults to purging the development and test databases."
task purge: [:load_config, :check_protected_environments] do
ActiveRecord::Tasks::DatabaseTasks.purge_current
@@ -223,6 +228,11 @@ db_namespace = namespace :db do
ActiveRecord::Tasks::DatabaseTasks.load_seed
end
+ namespace :seed do
+ desc "Truncates tables of each database for current environment and loads the seeds"
+ task replant: [:load_config, :truncate_all, :seed]
+ end
+
namespace :fixtures do
desc "Loads fixtures into the current environment's database. Load specific fixtures using FIXTURES=x,y. Load from subdirectory in test/fixtures using FIXTURES_DIR=z. Specify an alternative path (eg. spec/fixtures) using FIXTURES_PATH=spec/fixtures."
task load: :load_config do
diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb
index f6b21eaa3a..6d954a2b2e 100644
--- a/activerecord/lib/active_record/relation.rb
+++ b/activerecord/lib/active_record/relation.rb
@@ -5,7 +5,7 @@ module ActiveRecord
class Relation
MULTI_VALUE_METHODS = [:includes, :eager_load, :preload, :select, :group,
:order, :joins, :left_outer_joins, :references,
- :extending, :unscope]
+ :extending, :unscope, :optimizer_hints]
SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :reordering,
:reverse_order, :distinct, :create_with, :skip_query_cache]
@@ -197,6 +197,10 @@ module ActiveRecord
# if a DELETE between those two statements is run by another client. But for most applications,
# that's a significantly less likely condition to hit.
# * It relies on exception handling to handle control flow, which may be marginally slower.
+ # * The primary key may auto-increment on each create, even if it fails. This can accelerate
+ # the problem of running out of integers, if the underlying table is still stuck on a primary
+ # key of type int (note: All Rails apps since 5.1+ have defaulted to bigint, which is not liable
+ # to this problem).
#
# This method will return a record if all given attributes are covered by unique constraints
# (unless the INSERT -> DELETE -> SELECT race condition is triggered), but if creation was attempted
@@ -338,6 +342,8 @@ module ActiveRecord
# trigger Active Record callbacks or validations. However, values passed to #update_all will still go through
# Active Record's normal type casting and serialization.
#
+ # Note: As Active Record callbacks are not triggered, this method will not automatically update +updated_at+/+updated_on+ columns.
+ #
# ==== Parameters
#
# * +updates+ - A string, array, or hash representing the SET part of an SQL statement.
@@ -372,6 +378,12 @@ module ActiveRecord
stmt.wheres = arel.constraints
if updates.is_a?(Hash)
+ if klass.locking_enabled? &&
+ !updates.key?(klass.locking_column) &&
+ !updates.key?(klass.locking_column.to_sym)
+ attr = arel_attribute(klass.locking_column)
+ updates[attr.name] = _increment_attribute(attr)
+ end
stmt.set _substitute_values(updates)
else
stmt.set Arel.sql(klass.sanitize_sql_for_assignment(updates, table.name))
@@ -394,10 +406,7 @@ module ActiveRecord
updates = {}
counters.each do |counter_name, value|
attr = arel_attribute(counter_name)
- bind = predicate_builder.build_bind_attribute(attr.name, value.abs)
- expr = table.coalesce(Arel::Nodes::UnqualifiedColumn.new(attr), 0)
- expr = value < 0 ? expr - bind : expr + bind
- updates[counter_name] = expr.expr
+ updates[attr.name] = _increment_attribute(attr, value)
end
if touch
@@ -409,10 +418,10 @@ module ActiveRecord
update_all updates
end
- # Touches all records in the current relation without instantiating records first with the updated_at/on attributes
+ # Touches all records in the current relation without instantiating records first with the +updated_at+/+updated_on+ attributes
# set to the current time or the time specified.
# This method can be passed attribute names and an optional time argument.
- # If attribute names are passed, they are updated along with updated_at/on attributes.
+ # If attribute names are passed, they are updated along with +updated_at+/+updated_on+ attributes.
# If no time argument is passed, the current time is used as default.
#
# === Examples
@@ -433,12 +442,7 @@ module ActiveRecord
# Person.where(name: 'David').touch_all
# # => "UPDATE \"people\" SET \"updated_at\" = '2018-01-04 22:55:23.132670' WHERE \"people\".\"name\" = 'David'"
def touch_all(*names, time: nil)
- if klass.locking_enabled?
- names << { time: time }
- update_counters(klass.locking_column => 1, touch: names)
- else
- update_all klass.touch_attributes_with_time(*names, time: time)
- end
+ update_all klass.touch_attributes_with_time(*names, time: time)
end
# Destroys the records by instantiating each
@@ -709,6 +713,13 @@ module ActiveRecord
end
end
+ def _increment_attribute(attribute, value = 1)
+ bind = predicate_builder.build_bind_attribute(attribute.name, value.abs)
+ expr = table.coalesce(Arel::Nodes::UnqualifiedColumn.new(attribute), 0)
+ expr = value < 0 ? expr - bind : expr + bind
+ expr.expr
+ end
+
def exec_queries(&block)
skip_query_cache_if_necessary do
@records =
diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb
index bdd3c540bb..4f9ddf302e 100644
--- a/activerecord/lib/active_record/relation/calculations.rb
+++ b/activerecord/lib/active_record/relation/calculations.rb
@@ -221,7 +221,6 @@ module ActiveRecord
end
private
-
def has_include?(column_name)
eager_loading? || (includes_values.present? && column_name && column_name != :all)
end
@@ -236,10 +235,12 @@ module ActiveRecord
if operation == "count"
column_name ||= select_for_count
if column_name == :all
- if distinct && (group_values.any? || select_values.empty? && order_values.empty?)
+ if !distinct
+ distinct = distinct_select?(select_for_count) if group_values.empty?
+ elsif group_values.any? || select_values.empty? && order_values.empty?
column_name = primary_key
end
- elsif column_name.is_a?(::String) && /\bDISTINCT[\s(]/i.match?(column_name)
+ elsif distinct_select?(column_name)
distinct = nil
end
end
@@ -251,6 +252,10 @@ module ActiveRecord
end
end
+ def distinct_select?(column_name)
+ column_name.is_a?(::String) && /\bDISTINCT[\s(]/i.match?(column_name)
+ end
+
def aggregate_column(column_name)
return column_name if Arel::Expressions === column_name
diff --git a/activerecord/lib/active_record/relation/delegation.rb b/activerecord/lib/active_record/relation/delegation.rb
index f7c3b3783f..7a53a9d1c7 100644
--- a/activerecord/lib/active_record/relation/delegation.rb
+++ b/activerecord/lib/active_record/relation/delegation.rb
@@ -1,5 +1,7 @@
# frozen_string_literal: true
+require "mutex_m"
+
module ActiveRecord
module Delegation # :nodoc:
module DelegateCache # :nodoc:
@@ -31,6 +33,10 @@ module ActiveRecord
super
end
+ def generate_relation_method(method)
+ generated_relation_methods.generate_method(method)
+ end
+
protected
def include_relation_methods(delegate)
superclass.include_relation_methods(delegate) unless base_class?
@@ -39,27 +45,32 @@ module ActiveRecord
private
def generated_relation_methods
- @generated_relation_methods ||= Module.new.tap do |mod|
- mod_name = "GeneratedRelationMethods"
- const_set mod_name, mod
- private_constant mod_name
- end
+ @generated_relation_methods ||= GeneratedRelationMethods.new
end
+ end
+
+ class GeneratedRelationMethods < Module # :nodoc:
+ include Mutex_m
+
+ def generate_method(method)
+ synchronize do
+ return if method_defined?(method)
- def generate_relation_method(method)
if /\A[a-zA-Z_]\w*[!?]?\z/.match?(method)
- generated_relation_methods.module_eval <<-RUBY, __FILE__, __LINE__ + 1
+ module_eval <<-RUBY, __FILE__, __LINE__ + 1
def #{method}(*args, &block)
scoping { klass.#{method}(*args, &block) }
end
RUBY
else
- generated_relation_methods.define_method(method) do |*args, &block|
+ define_method(method) do |*args, &block|
scoping { klass.public_send(method, *args, &block) }
end
end
end
+ end
end
+ private_constant :GeneratedRelationMethods
extend ActiveSupport::Concern
@@ -78,39 +89,17 @@ module ActiveRecord
module ClassSpecificRelation # :nodoc:
extend ActiveSupport::Concern
- included do
- @delegation_mutex = Mutex.new
- end
-
module ClassMethods # :nodoc:
def name
superclass.name
end
-
- def delegate_to_scoped_klass(method)
- @delegation_mutex.synchronize do
- return if method_defined?(method)
-
- if /\A[a-zA-Z_]\w*[!?]?\z/.match?(method)
- module_eval <<-RUBY, __FILE__, __LINE__ + 1
- def #{method}(*args, &block)
- scoping { @klass.#{method}(*args, &block) }
- end
- RUBY
- else
- define_method method do |*args, &block|
- scoping { @klass.public_send(method, *args, &block) }
- end
- end
- end
- end
end
private
def method_missing(method, *args, &block)
if @klass.respond_to?(method)
- self.class.delegate_to_scoped_klass(method)
+ @klass.generate_relation_method(method)
scoping { @klass.public_send(method, *args, &block) }
else
super
diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb
index 91cfc4e849..e2efd4aa0d 100644
--- a/activerecord/lib/active_record/relation/finder_methods.rb
+++ b/activerecord/lib/active_record/relation/finder_methods.rb
@@ -7,8 +7,8 @@ module ActiveRecord
ONE_AS_ONE = "1 AS one"
# Find by id - This can either be a specific id (1), a list of ids (1, 5, 6), or an array of ids ([5, 6, 10]).
- # If one or more records can not be found for the requested ids, then RecordNotFound will be raised. If the primary key
- # is an integer, find by id coerces its arguments using +to_i+.
+ # If one or more records cannot be found for the requested ids, then ActiveRecord::RecordNotFound will be raised.
+ # If the primary key is an integer, find by id coerces its arguments by using +to_i+.
#
# Person.find(1) # returns the object for ID = 1
# Person.find("1") # returns the object for ID = 1
diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb
index 5563dfb6c9..05ea0850d3 100644
--- a/activerecord/lib/active_record/relation/query_methods.rb
+++ b/activerecord/lib/active_record/relation/query_methods.rb
@@ -240,6 +240,27 @@ module ActiveRecord
self
end
+ # Allows you to change a previously set select statement.
+ #
+ # Post.select(:title, :body)
+ # # SELECT `posts`.`title`, `posts`.`body` FROM `posts`
+ #
+ # Post.select(:title, :body).reselect(:created_at)
+ # # SELECT `posts`.`created_at` FROM `posts`
+ #
+ # This is short-hand for <tt>unscope(:select).select(fields)</tt>.
+ # Note that we're unscoping the entire select statement.
+ def reselect(*args)
+ check_if_method_has_arguments!(:reselect, args)
+ spawn.reselect!(*args)
+ end
+
+ # Same as #reselect but operates on relation in-place instead of copying.
+ def reselect!(*args) # :nodoc:
+ self.select_values = args
+ self
+ end
+
# Allows to specify a group attribute:
#
# User.group(:name)
@@ -329,7 +350,7 @@ module ActiveRecord
VALID_UNSCOPING_VALUES = Set.new([:where, :select, :group, :order, :lock,
:limit, :offset, :joins, :left_outer_joins,
- :includes, :from, :readonly, :having])
+ :includes, :from, :readonly, :having, :optimizer_hints])
# Removes an unwanted relation that is already defined on a chain of relations.
# This is useful when passing around chains of relations and would like to
@@ -880,6 +901,29 @@ module ActiveRecord
self
end
+ # Specify optimizer hints to be used in the SELECT statement.
+ #
+ # Example (for MySQL):
+ #
+ # Topic.optimizer_hints("MAX_EXECUTION_TIME(50000)", "NO_INDEX_MERGE(topics)")
+ # # SELECT /*+ MAX_EXECUTION_TIME(50000) NO_INDEX_MERGE(topics) */ `topics`.* FROM `topics`
+ #
+ # Example (for PostgreSQL with pg_hint_plan):
+ #
+ # Topic.optimizer_hints("SeqScan(topics)", "Parallel(topics 8)")
+ # # SELECT /*+ SeqScan(topics) Parallel(topics 8) */ "topics".* FROM "topics"
+ def optimizer_hints(*args)
+ check_if_method_has_arguments!(:optimizer_hints, args)
+ spawn.optimizer_hints!(*args)
+ end
+
+ def optimizer_hints!(*args) # :nodoc:
+ args.flatten!
+
+ self.optimizer_hints_values += args
+ self
+ end
+
# Reverse the existing order clause on the relation.
#
# User.order('name ASC').reverse_order # generated SQL has 'ORDER BY name DESC'
@@ -956,6 +1000,7 @@ module ActiveRecord
build_select(arel)
+ arel.optimizer_hints(*optimizer_hints_values) unless optimizer_hints_values.empty?
arel.distinct(distinct_value)
arel.from(build_from) unless from_clause.empty?
arel.lock(lock_value) if lock_value
@@ -1068,14 +1113,19 @@ module ActiveRecord
def arel_column(field)
field = klass.attribute_alias(field) if klass.attribute_alias?(field)
+ from = from_clause.name || from_clause.value
- if klass.columns_hash.key?(field) && !from_clause.value
+ if klass.columns_hash.key?(field) && (!from || table_name_matches?(from))
arel_attribute(field)
else
yield
end
end
+ def table_name_matches?(from)
+ /(?:\A|(?<!FROM)\s)(?:\b#{table.name}\b|#{connection.quote_table_name(table.name)})(?!\.)/i.match?(from.to_s)
+ end
+
def reverse_sql_order(order_query)
if order_query.empty?
return [arel_attribute(primary_key).desc] if primary_key
@@ -1091,7 +1141,7 @@ module ActiveRecord
o.reverse
when String
if does_not_support_reverse?(o)
- raise IrreversibleOrderError, "Order #{o.inspect} can not be reversed automatically"
+ raise IrreversibleOrderError, "Order #{o.inspect} cannot be reversed automatically"
end
o.split(",").map! do |s|
s.strip!
diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb
index e6197752bc..750766714d 100644
--- a/activerecord/lib/active_record/sanitization.rb
+++ b/activerecord/lib/active_record/sanitization.rb
@@ -165,10 +165,11 @@ module ActiveRecord
def quote_bound_value(value, c = connection)
if value.respond_to?(:map) && !value.acts_like?(:string)
- if value.respond_to?(:empty?) && value.empty?
+ quoted = value.map { |v| c.quote(v) }
+ if quoted.empty?
c.quote(nil)
else
- value.map { |v| c.quote(v) }.join(",")
+ quoted.join(",")
end
else
c.quote(value)
diff --git a/activerecord/lib/active_record/scoping/default.rb b/activerecord/lib/active_record/scoping/default.rb
index 8c612df27a..87bcfd5181 100644
--- a/activerecord/lib/active_record/scoping/default.rb
+++ b/activerecord/lib/active_record/scoping/default.rb
@@ -86,8 +86,8 @@ module ActiveRecord
# # Should return a scope, you can call 'super' here etc.
# end
# end
- def default_scope(scope = nil) # :doc:
- scope = Proc.new if block_given?
+ def default_scope(scope = nil, &block) # :doc:
+ scope = block if block_given?
if scope.is_a?(Relation) || !scope.respond_to?(:call)
raise ArgumentError,
@@ -100,7 +100,7 @@ module ActiveRecord
self.default_scopes += [scope]
end
- def build_default_scope(base_rel = nil)
+ def build_default_scope(relation = relation())
return if abstract_class?
if default_scope_override.nil?
@@ -111,15 +111,14 @@ module ActiveRecord
# The user has defined their own default scope method, so call that
evaluate_default_scope do
if scope = default_scope
- (base_rel ||= relation).merge!(scope)
+ relation.merge!(scope)
end
end
elsif default_scopes.any?
- base_rel ||= relation
evaluate_default_scope do
- default_scopes.inject(base_rel) do |default_scope, scope|
+ default_scopes.inject(relation) do |default_scope, scope|
scope = scope.respond_to?(:to_proc) ? scope : scope.method(:call)
- default_scope.merge!(base_rel.instance_exec(&scope))
+ default_scope.instance_exec(&scope) || default_scope
end
end
end
diff --git a/activerecord/lib/active_record/statement_cache.rb b/activerecord/lib/active_record/statement_cache.rb
index 95984e7ada..93bce15230 100644
--- a/activerecord/lib/active_record/statement_cache.rb
+++ b/activerecord/lib/active_record/statement_cache.rb
@@ -113,8 +113,8 @@ module ActiveRecord
end
end
- def self.create(connection, block = Proc.new)
- relation = block.call Params.new
+ def self.create(connection, callable = nil, &block)
+ relation = (callable || block).call Params.new
query_builder, binds = connection.cacheable_query(self, relation.arel)
bind_map = BindMap.new(binds)
new(query_builder, bind_map, relation.klass)
diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb
index 27e401a756..155d2b0b98 100644
--- a/activerecord/lib/active_record/tasks/database_tasks.rb
+++ b/activerecord/lib/active_record/tasks/database_tasks.rb
@@ -142,7 +142,7 @@ module ActiveRecord
end
def for_each
- databases = Rails.application.config.database_configuration
+ databases = Rails.application.config.load_database_yaml
database_configs = ActiveRecord::DatabaseConfigurations.new(databases).configs_for(env_name: Rails.env)
# if this is a single database application we don't want tasks for each primary database
@@ -182,6 +182,25 @@ module ActiveRecord
}
end
+ def truncate_tables(configuration)
+ ActiveRecord::Base.connected_to(database: { truncation: configuration }) do
+ table_names = ActiveRecord::Base.connection.tables
+ table_names -= [
+ ActiveRecord::Base.schema_migrations_table_name,
+ ActiveRecord::Base.internal_metadata_table_name
+ ]
+
+ ActiveRecord::Base.connection.truncate_tables(*table_names)
+ end
+ end
+ private :truncate_tables
+
+ def truncate_all(environment = env)
+ ActiveRecord::Base.configurations.configs_for(env_name: environment).each do |db_config|
+ truncate_tables db_config.config
+ end
+ end
+
def migrate
check_target_version
diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb
index 88ae62c681..2c3a2fb797 100644
--- a/activerecord/lib/active_record/validations/uniqueness.rb
+++ b/activerecord/lib/active_record/validations/uniqueness.rb
@@ -25,7 +25,7 @@ module ActiveRecord
if finder_class.primary_key
relation = relation.where.not(finder_class.primary_key => record.id_in_database)
else
- raise UnknownPrimaryKey.new(finder_class, "Can not validate uniqueness for persisted record without primary key.")
+ raise UnknownPrimaryKey.new(finder_class, "Cannot validate uniqueness for persisted record without primary key.")
end
end
relation = scope_relation(record, relation)
@@ -60,10 +60,8 @@ module ActiveRecord
comparison = relation.bind_attribute(attribute, value) do |attr, bind|
return relation.none! if bind.unboundable?
- if bind.nil?
- attr.eq(bind)
- elsif !options.key?(:case_sensitive)
- klass.connection.default_uniqueness_comparison(attr, bind)
+ if !options.key?(:case_sensitive) || bind.nil?
+ klass.connection.default_uniqueness_comparison(attr, bind, klass)
elsif options[:case_sensitive]
klass.connection.case_sensitive_comparison(attr, bind)
else
diff --git a/activerecord/lib/arel/insert_manager.rb b/activerecord/lib/arel/insert_manager.rb
index c90fc33a48..cb31e3060b 100644
--- a/activerecord/lib/arel/insert_manager.rb
+++ b/activerecord/lib/arel/insert_manager.rb
@@ -33,13 +33,13 @@ module Arel # :nodoc: all
@ast.columns << column
values << value
end
- @ast.values = create_values values, @ast.columns
+ @ast.values = create_values(values)
end
self
end
- def create_values(values, columns)
- Nodes::Values.new values, columns
+ def create_values(values)
+ Nodes::ValuesList.new([values])
end
def create_values_list(rows)
diff --git a/activerecord/lib/arel/nodes.rb b/activerecord/lib/arel/nodes.rb
index 5af0e532e2..2f6dd9bc45 100644
--- a/activerecord/lib/arel/nodes.rb
+++ b/activerecord/lib/arel/nodes.rb
@@ -45,7 +45,6 @@ require "arel/nodes/and"
require "arel/nodes/function"
require "arel/nodes/count"
require "arel/nodes/extract"
-require "arel/nodes/values"
require "arel/nodes/values_list"
require "arel/nodes/named_function"
diff --git a/activerecord/lib/arel/nodes/select_core.rb b/activerecord/lib/arel/nodes/select_core.rb
index 73461ff683..6585f9b3ec 100644
--- a/activerecord/lib/arel/nodes/select_core.rb
+++ b/activerecord/lib/arel/nodes/select_core.rb
@@ -4,12 +4,13 @@ module Arel # :nodoc: all
module Nodes
class SelectCore < Arel::Nodes::Node
attr_accessor :projections, :wheres, :groups, :windows
- attr_accessor :havings, :source, :set_quantifier
+ attr_accessor :havings, :source, :set_quantifier, :optimizer_hints
def initialize
super()
@source = JoinSource.new nil
+ @optimizer_hints = nil
# https://ronsavage.github.io/SQL/sql-92.bnf.html#set%20quantifier
@set_quantifier = nil
@projections = []
@@ -42,7 +43,7 @@ module Arel # :nodoc: all
def hash
[
- @source, @set_quantifier, @projections,
+ @source, @set_quantifier, @projections, @optimizer_hints,
@wheres, @groups, @havings, @windows
].hash
end
@@ -51,6 +52,7 @@ module Arel # :nodoc: all
self.class == other.class &&
self.source == other.source &&
self.set_quantifier == other.set_quantifier &&
+ self.optimizer_hints == other.optimizer_hints &&
self.projections == other.projections &&
self.wheres == other.wheres &&
self.groups == other.groups &&
diff --git a/activerecord/lib/arel/nodes/unary.rb b/activerecord/lib/arel/nodes/unary.rb
index 00639304e4..6d1ac36b0e 100644
--- a/activerecord/lib/arel/nodes/unary.rb
+++ b/activerecord/lib/arel/nodes/unary.rb
@@ -35,6 +35,7 @@ module Arel # :nodoc: all
Not
Offset
On
+ OptimizerHints
Ordering
RollUp
}.each do |name|
diff --git a/activerecord/lib/arel/nodes/values.rb b/activerecord/lib/arel/nodes/values.rb
deleted file mode 100644
index 650248dc04..0000000000
--- a/activerecord/lib/arel/nodes/values.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-# frozen_string_literal: true
-
-module Arel # :nodoc: all
- module Nodes
- class Values < Arel::Nodes::Binary
- alias :expressions :left
- alias :expressions= :left=
- alias :columns :right
- alias :columns= :right=
-
- def initialize(exprs, columns = [])
- super
- end
- end
- end
-end
diff --git a/activerecord/lib/arel/nodes/values_list.rb b/activerecord/lib/arel/nodes/values_list.rb
index 27109848e4..1a9d9ebf01 100644
--- a/activerecord/lib/arel/nodes/values_list.rb
+++ b/activerecord/lib/arel/nodes/values_list.rb
@@ -2,23 +2,8 @@
module Arel # :nodoc: all
module Nodes
- class ValuesList < Node
- attr_reader :rows
-
- def initialize(rows)
- @rows = rows
- super()
- end
-
- def hash
- @rows.hash
- end
-
- def eql?(other)
- self.class == other.class &&
- self.rows == other.rows
- end
- alias :== :eql?
+ class ValuesList < Unary
+ alias :rows :expr
end
end
end
diff --git a/activerecord/lib/arel/select_manager.rb b/activerecord/lib/arel/select_manager.rb
index a2b2838a3d..32286b67f4 100644
--- a/activerecord/lib/arel/select_manager.rb
+++ b/activerecord/lib/arel/select_manager.rb
@@ -146,6 +146,13 @@ module Arel # :nodoc: all
@ctx.projections = projections
end
+ def optimizer_hints(*hints)
+ unless hints.empty?
+ @ctx.optimizer_hints = Arel::Nodes::OptimizerHints.new(hints)
+ end
+ self
+ end
+
def distinct(value = true)
if value
@ctx.set_quantifier = Arel::Nodes::Distinct.new
@@ -237,18 +244,6 @@ module Arel # :nodoc: all
@ctx.source
end
- class Row < Struct.new(:data) # :nodoc:
- def id
- data["id"]
- end
-
- def method_missing(name, *args)
- name = name.to_s
- return data[name] if data.key?(name)
- super
- end
- end
-
private
def collapse(exprs)
exprs = exprs.compact
diff --git a/activerecord/lib/arel/visitors/depth_first.rb b/activerecord/lib/arel/visitors/depth_first.rb
index 92d309453c..109afb7402 100644
--- a/activerecord/lib/arel/visitors/depth_first.rb
+++ b/activerecord/lib/arel/visitors/depth_first.rb
@@ -35,6 +35,8 @@ module Arel # :nodoc: all
alias :visit_Arel_Nodes_Ascending :unary
alias :visit_Arel_Nodes_Descending :unary
alias :visit_Arel_Nodes_UnqualifiedColumn :unary
+ alias :visit_Arel_Nodes_OptimizerHints :unary
+ alias :visit_Arel_Nodes_ValuesList :unary
def function(o)
visit o.expressions
@@ -102,7 +104,6 @@ module Arel # :nodoc: all
alias :visit_Arel_Nodes_Regexp :binary
alias :visit_Arel_Nodes_RightOuterJoin :binary
alias :visit_Arel_Nodes_TableAlias :binary
- alias :visit_Arel_Nodes_Values :binary
alias :visit_Arel_Nodes_When :binary
def visit_Arel_Nodes_StringJoin(o)
diff --git a/activerecord/lib/arel/visitors/dot.rb b/activerecord/lib/arel/visitors/dot.rb
index 6389c875cb..37803ce0c0 100644
--- a/activerecord/lib/arel/visitors/dot.rb
+++ b/activerecord/lib/arel/visitors/dot.rb
@@ -46,8 +46,8 @@ module Arel # :nodoc: all
visit_edge o, "distinct"
end
- def visit_Arel_Nodes_Values(o)
- visit_edge o, "expressions"
+ def visit_Arel_Nodes_ValuesList(o)
+ visit_edge o, "rows"
end
def visit_Arel_Nodes_StringJoin(o)
@@ -82,6 +82,7 @@ module Arel # :nodoc: all
alias :visit_Arel_Nodes_Offset :unary
alias :visit_Arel_Nodes_On :unary
alias :visit_Arel_Nodes_UnqualifiedColumn :unary
+ alias :visit_Arel_Nodes_OptimizerHints :unary
alias :visit_Arel_Nodes_Preceding :unary
alias :visit_Arel_Nodes_Following :unary
alias :visit_Arel_Nodes_Rows :unary
diff --git a/activerecord/lib/arel/visitors/ibm_db.rb b/activerecord/lib/arel/visitors/ibm_db.rb
index 73166054da..0ffc0725f7 100644
--- a/activerecord/lib/arel/visitors/ibm_db.rb
+++ b/activerecord/lib/arel/visitors/ibm_db.rb
@@ -4,6 +4,14 @@ module Arel # :nodoc: all
module Visitors
class IBM_DB < Arel::Visitors::ToSql
private
+ def visit_Arel_Nodes_SelectCore(o, collector)
+ collector = super
+ maybe_visit o.optimizer_hints, collector
+ end
+
+ def visit_Arel_Nodes_OptimizerHints(o, collector)
+ collector << "/* <OPTGUIDELINES>#{sanitize_as_sql_comment(o).join}</OPTGUIDELINES> */"
+ end
def visit_Arel_Nodes_Limit(o, collector)
collector << "FETCH FIRST "
@@ -16,6 +24,10 @@ module Arel # :nodoc: all
collector = visit [o.left, o.right, 0, 1], collector
collector << ")"
end
+
+ def collect_optimizer_hints(o, collector)
+ collector
+ end
end
end
end
diff --git a/activerecord/lib/arel/visitors/informix.rb b/activerecord/lib/arel/visitors/informix.rb
index 208fa15aef..cd43be8858 100644
--- a/activerecord/lib/arel/visitors/informix.rb
+++ b/activerecord/lib/arel/visitors/informix.rb
@@ -42,10 +42,15 @@ module Arel # :nodoc: all
collector
end
+ def visit_Arel_Nodes_OptimizerHints(o, collector)
+ collector << "/*+ #{sanitize_as_sql_comment(o).join(", ")} */"
+ end
+
def visit_Arel_Nodes_Offset(o, collector)
collector << "SKIP "
visit o.expr, collector
end
+
def visit_Arel_Nodes_Limit(o, collector)
collector << "FIRST "
visit o.expr, collector
diff --git a/activerecord/lib/arel/visitors/mssql.rb b/activerecord/lib/arel/visitors/mssql.rb
index fdd864b40d..85815baca2 100644
--- a/activerecord/lib/arel/visitors/mssql.rb
+++ b/activerecord/lib/arel/visitors/mssql.rb
@@ -76,6 +76,15 @@ module Arel # :nodoc: all
end
end
+ def visit_Arel_Nodes_SelectCore(o, collector)
+ collector = super
+ maybe_visit o.optimizer_hints, collector
+ end
+
+ def visit_Arel_Nodes_OptimizerHints(o, collector)
+ collector << "OPTION (#{sanitize_as_sql_comment(o).join(", ")})"
+ end
+
def get_offset_limit_clause(o)
first_row = o.offset ? o.offset.expr.to_i + 1 : 1
last_row = o.limit ? o.limit.expr.to_i - 1 + first_row : nil
@@ -103,6 +112,10 @@ module Arel # :nodoc: all
end
end
+ def collect_optimizer_hints(o, collector)
+ collector
+ end
+
def determine_order_by(orders, x)
if orders.any?
orders
diff --git a/activerecord/lib/arel/visitors/to_sql.rb b/activerecord/lib/arel/visitors/to_sql.rb
index d0dec63860..583f920290 100644
--- a/activerecord/lib/arel/visitors/to_sql.rb
+++ b/activerecord/lib/arel/visitors/to_sql.rb
@@ -159,7 +159,7 @@ module Arel # :nodoc: all
when Nodes::SqlLiteral, Nodes::BindParam
collector = visit(value, collector)
else
- collector << quote(value)
+ collector << quote(value).to_s
end
collector << COMMA unless k == row_len
end
@@ -169,25 +169,6 @@ module Arel # :nodoc: all
collector
end
- def visit_Arel_Nodes_Values(o, collector)
- collector << "VALUES ("
-
- len = o.expressions.length - 1
- o.expressions.each_with_index { |value, i|
- case value
- when Nodes::SqlLiteral, Nodes::BindParam
- collector = visit value, collector
- else
- collector << quote(value).to_s
- end
- unless i == len
- collector << COMMA
- end
- }
-
- collector << ")"
- end
-
def visit_Arel_Nodes_SelectStatement(o, collector)
if o.with
collector = visit o.with, collector
@@ -219,6 +200,7 @@ module Arel # :nodoc: all
def visit_Arel_Nodes_SelectCore(o, collector)
collector << "SELECT"
+ collector = collect_optimizer_hints(o, collector)
collector = maybe_visit o.set_quantifier, collector
collect_nodes_for o.projections, collector, SPACE
@@ -236,6 +218,10 @@ module Arel # :nodoc: all
collector
end
+ def visit_Arel_Nodes_OptimizerHints(o, collector)
+ collector << "/*+ #{sanitize_as_sql_comment(o).join(" ")} */"
+ end
+
def collect_nodes_for(nodes, collector, spacer, connector = COMMA)
unless nodes.empty?
collector << spacer
@@ -799,6 +785,16 @@ module Arel # :nodoc: all
@connection.quote_column_name(name)
end
+ def sanitize_as_sql_comment(o)
+ o.expr.map { |v|
+ v.gsub(%r{ (/ (?: | \g<1>) \*) \+? \s* | \s* (\* (?: | \g<2>) /) }x, "")
+ }
+ end
+
+ def collect_optimizer_hints(o, collector)
+ maybe_visit o.optimizer_hints, collector
+ end
+
def maybe_visit(thing, collector)
return collector unless thing
collector << " "