aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord
diff options
context:
space:
mode:
Diffstat (limited to 'activerecord')
-rw-r--r--activerecord/CHANGELOG.md25
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb116
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/quoting.rb14
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_adapter.rb5
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb42
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb51
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb4
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb18
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3/database_statements.rb38
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb18
-rw-r--r--activerecord/lib/active_record/database_configurations.rb2
-rw-r--r--activerecord/lib/active_record/insert_all.rb54
-rw-r--r--activerecord/lib/active_record/querying.rb2
-rw-r--r--activerecord/lib/active_record/relation.rb2
-rw-r--r--activerecord/lib/active_record/relation/query_methods.rb26
-rw-r--r--activerecord/lib/active_record/tasks/database_tasks.rb5
-rw-r--r--activerecord/lib/active_record/tasks/mysql_database_tasks.rb10
-rw-r--r--activerecord/lib/active_record/tasks/postgresql_database_tasks.rb12
-rw-r--r--activerecord/lib/active_record/tasks/sqlite_database_tasks.rb10
-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.rb7
-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
-rw-r--r--activerecord/test/cases/adapter_test.rb22
-rw-r--r--activerecord/test/cases/adapters/mysql2/connection_test.rb11
-rw-r--r--activerecord/test/cases/adapters/mysql2/optimizer_hints_test.rb40
-rw-r--r--activerecord/test/cases/adapters/postgresql/connection_test.rb8
-rw-r--r--activerecord/test/cases/adapters/postgresql/optimizer_hints_test.rb44
-rw-r--r--activerecord/test/cases/adapters/sqlite3/connection_test.rb18
-rw-r--r--activerecord/test/cases/arel/insert_manager_test.rb19
-rw-r--r--activerecord/test/cases/arel/visitors/depth_first_test.rb2
-rw-r--r--activerecord/test/cases/arel/visitors/dot_test.rb2
-rw-r--r--activerecord/test/cases/arel/visitors/to_sql_test.rb4
-rw-r--r--activerecord/test/cases/bind_parameter_test.rb2
-rw-r--r--activerecord/test/cases/fixtures_test.rb8
-rw-r--r--activerecord/test/cases/helper.rb1
-rw-r--r--activerecord/test/cases/insert_all_test.rb6
-rw-r--r--activerecord/test/cases/invertible_migration_test.rb2
47 files changed, 477 insertions, 296 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md
index 3d7508252a..148b3800a8 100644
--- a/activerecord/CHANGELOG.md
+++ b/activerecord/CHANGELOG.md
@@ -1,3 +1,28 @@
+* Support Optimizer Hints.
+
+ In most databases, there is a way to control the optimizer is by using optimizer hints,
+ which can be specified within individual statements.
+
+ 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"
+
+ See also:
+
+ * https://dev.mysql.com/doc/refman/8.0/en/optimizer-hints.html
+ * https://pghintplan.osdn.jp/pg_hint_plan.html
+ * https://docs.oracle.com/en/database/oracle/oracle-database/12.2/tgsql/influencing-the-optimizer.html
+ * https://docs.microsoft.com/en-us/sql/t-sql/queries/hints-transact-sql-query?view=sql-server-2017
+ * https://www.ibm.com/support/knowledgecenter/en/SSEPGG_11.1.0/com.ibm.db2.luw.admin.perf.doc/doc/c0070117.html
+
+ *Ryuta Kamizono*
+
* Fix query attribute method on user-defined attribute to be aware of typecasted value.
For example, the following code no longer return false as casted non-empty string:
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 3c872d6c1b..6aacbe5f88 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
@@ -142,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.
@@ -181,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.
#
@@ -341,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
@@ -416,14 +402,21 @@ module ActiveRecord
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
@@ -445,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
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_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
index 202187a047..7aad306d50 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
@@ -384,6 +384,11 @@ module ActiveRecord
false
end
+ # Does this adapter support optimizer hints?
+ def supports_optimizer_hints?
+ false
+ end
+
def supports_lazy_transactions?
false
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 a518b897a0..8ca2cfa9ed 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
@@ -103,6 +103,11 @@ 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
@@ -269,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)
@@ -511,12 +512,6 @@ module ActiveRecord
index.using == :btree || super
end
- def insert_fixtures_set(fixture_set, tables_to_delete = [])
- with_multi_statements do
- super { discard_remaining_results }
- end
- end
-
def build_insert_sql(insert) # :nodoc:
sql = +"INSERT #{insert.into} #{insert.values_list}"
@@ -538,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
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/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_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
index 672f11cfcb..29f764e8f4 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
@@ -259,10 +259,6 @@ module ActiveRecord
@use_insert_returning = @config.key?(:insert_returning) ? self.class.type_cast_config_to_boolean(@config[:insert_returning]) : true
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
@@ -351,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
@@ -381,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
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_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
index 3004caf82d..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"
@@ -58,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",
@@ -155,10 +157,6 @@ module ActiveRecord
@connection.close rescue nil
end
- def truncate(table_name, name = nil)
- execute "DELETE FROM #{quote_table_name(table_name)}", name
- end
-
def supports_index_sort_order?
true
end
@@ -386,18 +384,6 @@ 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" }
-
- fixture_set.each do |table_name, rows|
- rows.each { |row| insert_fixture(row, table_name) }
- end
- end
- end
- end
-
def build_insert_sql(insert) # :nodoc:
sql = +"INSERT #{insert.into} #{insert.values_list}"
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/insert_all.rb b/activerecord/lib/active_record/insert_all.rb
index 5baaea344b..98c98d61cd 100644
--- a/activerecord/lib/active_record/insert_all.rb
+++ b/activerecord/lib/active_record/insert_all.rb
@@ -2,12 +2,14 @@
module ActiveRecord
class InsertAll
- attr_reader :model, :connection, :inserts, :on_duplicate, :returning, :unique_by
+ 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, @on_duplicate, @returning, @unique_by = model, model.connection, inserts, on_duplicate, returning, unique_by
+ @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 == []
@@ -21,10 +23,6 @@ module ActiveRecord
connection.exec_query to_sql, "Bulk Insert"
end
- def keys
- inserts.first.keys.map(&:to_s)
- end
-
def updatable_columns
keys - readonly_columns - unique_by_columns
end
@@ -37,6 +35,17 @@ module ActiveRecord
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?
@@ -76,6 +85,12 @@ module ActiveRecord
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
@@ -91,21 +106,11 @@ module ActiveRecord
end
def values_list
- columns = connection.schema_cache.columns_hash(model.table_name)
- keys = insert_all.keys.to_set
- types = keys.map { |key| [ key, connection.lookup_cast_type_from_column(columns[key]) ] }.to_h
-
- values_list = insert_all.inserts.map do |attributes|
- attributes = attributes.stringify_keys
+ types = extract_types_from_columns_on(model.table_name, keys: insert_all.keys)
- unless attributes.keys.to_set == keys
- raise ArgumentError, "All objects being inserted must have the same keys"
- end
-
- keys.map do |key|
- bind = Relation::QueryAttribute.new(key, attributes[key], types[key])
- connection.with_yaml_fallback(bind.value_for_database)
- end
+ 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
@@ -133,6 +138,15 @@ module ActiveRecord
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
diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb
index 7a8d0cb663..86021f0a80 100644
--- a/activerecord/lib/active_record/querying.rb
+++ b/activerecord/lib/active_record/querying.rb
@@ -14,7 +14,7 @@ module ActiveRecord
: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, :merge, :except, :only,
+ :having, :create_with, :distinct, :references, :none, :unscope, :optimizer_hints, :merge, :except, :only,
:count, :average, :minimum, :maximum, :sum, :calculate,
:pluck, :pick, :ids
].freeze # :nodoc:
diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb
index ac37f73b76..37179774fa 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]
diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb
index 0f2cff4c9c..05ea0850d3 100644
--- a/activerecord/lib/active_record/relation/query_methods.rb
+++ b/activerecord/lib/active_record/relation/query_methods.rb
@@ -350,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
@@ -901,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'
@@ -977,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
diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb
index a8433fa0db..155d2b0b98 100644
--- a/activerecord/lib/active_record/tasks/database_tasks.rb
+++ b/activerecord/lib/active_record/tasks/database_tasks.rb
@@ -185,14 +185,15 @@ module ActiveRecord
def truncate_tables(configuration)
ActiveRecord::Base.connected_to(database: { truncation: configuration }) do
table_names = ActiveRecord::Base.connection.tables
- internal_table_names = [
+ table_names -= [
ActiveRecord::Base.schema_migrations_table_name,
ActiveRecord::Base.internal_metadata_table_name
]
- class_for_adapter(configuration["adapter"]).new(configuration).truncate_tables(*table_names.without(*internal_table_names))
+ 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|
diff --git a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb
index f2b4ead98d..1c1b29b5e1 100644
--- a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb
+++ b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb
@@ -31,16 +31,6 @@ module ActiveRecord
connection.recreate_database configuration["database"], creation_options
end
- def truncate_tables(*table_names)
- return if table_names.empty?
-
- ActiveRecord::Base.connection.disable_referential_integrity do
- table_names.each do |table_name|
- ActiveRecord::Base.connection.truncate(table_name)
- end
- end
- end
-
def charset
connection.charset
end
diff --git a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb
index dc368eb97d..8acb11f75f 100644
--- a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb
+++ b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb
@@ -48,18 +48,6 @@ module ActiveRecord
create true
end
- def truncate_tables(*table_names)
- return if table_names.empty?
-
- ActiveRecord::Base.connection.disable_referential_integrity do
- quoted_table_names = table_names.map do |table_name|
- ActiveRecord::Base.connection.quote_table_name(table_name)
- end
-
- ActiveRecord::Base.connection.execute "TRUNCATE TABLE #{quoted_table_names.join(", ")}"
- end
- end
-
def structure_dump(filename, extra_flags)
set_psql_env
diff --git a/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb b/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb
index cedbae6b7f..a82cea80ca 100644
--- a/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb
+++ b/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb
@@ -33,16 +33,6 @@ module ActiveRecord
create
end
- def truncate_tables(*table_names)
- return if table_names.empty?
-
- ActiveRecord::Base.connection.disable_referential_integrity do
- table_names.each do |table_name|
- ActiveRecord::Base.connection.truncate(table_name)
- end
- end
- end
-
def charset
connection.encoding
end
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 0da52ca91b..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
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 << " "
diff --git a/activerecord/test/cases/adapter_test.rb b/activerecord/test/cases/adapter_test.rb
index d90003b0ba..2b20d842e8 100644
--- a/activerecord/test/cases/adapter_test.rb
+++ b/activerecord/test/cases/adapter_test.rb
@@ -452,6 +452,8 @@ module ActiveRecord
class AdapterTestWithoutTransaction < ActiveRecord::TestCase
self.use_transactional_tests = false
+ fixtures :posts, :authors, :author_addresses
+
def setup
@connection = ActiveRecord::Base.connection
end
@@ -482,6 +484,26 @@ module ActiveRecord
end
end
+ def test_truncate
+ assert_operator @connection.query_value("SELECT COUNT(*) FROM posts"), :>, 0
+
+ @connection.truncate("posts")
+
+ assert_equal 0, @connection.query_value("SELECT COUNT(*) FROM posts")
+ end
+
+ def test_truncate_tables
+ assert_operator @connection.query_value("SELECT COUNT(*) FROM posts"), :>, 0
+ assert_operator @connection.query_value("SELECT COUNT(*) FROM authors"), :>, 0
+ assert_operator @connection.query_value("SELECT COUNT(*) FROM author_addresses"), :>, 0
+
+ @connection.truncate_tables("author_addresses", "authors", "posts")
+
+ assert_equal 0, @connection.query_value("SELECT COUNT(*) FROM posts")
+ assert_equal 0, @connection.query_value("SELECT COUNT(*) FROM authors")
+ assert_equal 0, @connection.query_value("SELECT COUNT(*) FROM author_addresses")
+ end
+
# test resetting sequences in odd tables in PostgreSQL
if ActiveRecord::Base.connection.respond_to?(:reset_pk_sequence!)
require "models/movie"
diff --git a/activerecord/test/cases/adapters/mysql2/connection_test.rb b/activerecord/test/cases/adapters/mysql2/connection_test.rb
index 3103589186..9c6566106a 100644
--- a/activerecord/test/cases/adapters/mysql2/connection_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/connection_test.rb
@@ -28,17 +28,6 @@ class Mysql2ConnectionTest < ActiveRecord::Mysql2TestCase
end
end
- def test_truncate
- rows = ActiveRecord::Base.connection.exec_query("select count(*) from comments")
- count = rows.first.values.first
- assert_operator count, :>, 0
-
- ActiveRecord::Base.connection.truncate("comments")
- rows = ActiveRecord::Base.connection.exec_query("select count(*) from comments")
- count = rows.first.values.first
- assert_equal 0, count
- end
-
def test_no_automatic_reconnection_after_timeout
assert_predicate @connection, :active?
@connection.update("set @@wait_timeout=1")
diff --git a/activerecord/test/cases/adapters/mysql2/optimizer_hints_test.rb b/activerecord/test/cases/adapters/mysql2/optimizer_hints_test.rb
new file mode 100644
index 0000000000..b9794c5710
--- /dev/null
+++ b/activerecord/test/cases/adapters/mysql2/optimizer_hints_test.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/post"
+
+if supports_optimizer_hints?
+ class Mysql2OptimzerHintsTest < ActiveRecord::Mysql2TestCase
+ fixtures :posts
+
+ def test_optimizer_hints
+ assert_sql(%r{\ASELECT /\*\+ NO_RANGE_OPTIMIZATION\(posts index_posts_on_author_id\) \*/}) do
+ posts = Post.optimizer_hints("NO_RANGE_OPTIMIZATION(posts index_posts_on_author_id)")
+ posts = posts.select(:id).where(author_id: [0, 1])
+ assert_includes posts.explain, "| index | index_posts_on_author_id | index_posts_on_author_id |"
+ end
+ end
+
+ def test_optimizer_hints_is_sanitized
+ assert_sql(%r{\ASELECT /\*\+ NO_RANGE_OPTIMIZATION\(posts index_posts_on_author_id\) \*/}) do
+ posts = Post.optimizer_hints("/*+ NO_RANGE_OPTIMIZATION(posts index_posts_on_author_id) */")
+ posts = posts.select(:id).where(author_id: [0, 1])
+ assert_includes posts.explain, "| index | index_posts_on_author_id | index_posts_on_author_id |"
+ end
+
+ assert_sql(%r{\ASELECT /\*\+ `posts`\.\*, \*/}) do
+ posts = Post.optimizer_hints("**// `posts`.*, //**")
+ posts = posts.select(:id).where(author_id: [0, 1])
+ assert_equal({ "id" => 1 }, posts.first.as_json)
+ end
+ end
+
+ def test_optimizer_hints_with_unscope
+ assert_sql(%r{\ASELECT `posts`\.`id`}) do
+ posts = Post.optimizer_hints("/*+ NO_RANGE_OPTIMIZATION(posts index_posts_on_author_id) */")
+ posts = posts.select(:id).where(author_id: [0, 1])
+ posts.unscope(:optimizer_hints).load
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/adapters/postgresql/connection_test.rb b/activerecord/test/cases/adapters/postgresql/connection_test.rb
index 40ab158c05..210758f462 100644
--- a/activerecord/test/cases/adapters/postgresql/connection_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/connection_test.rb
@@ -25,14 +25,6 @@ module ActiveRecord
super
end
- def test_truncate
- count = ActiveRecord::Base.connection.execute("select count(*) from comments").first["count"].to_i
- assert_operator count, :>, 0
- ActiveRecord::Base.connection.truncate("comments")
- count = ActiveRecord::Base.connection.execute("select count(*) from comments").first["count"].to_i
- assert_equal 0, count
- end
-
def test_encoding
assert_queries(1) do
assert_not_nil @connection.encoding
diff --git a/activerecord/test/cases/adapters/postgresql/optimizer_hints_test.rb b/activerecord/test/cases/adapters/postgresql/optimizer_hints_test.rb
new file mode 100644
index 0000000000..5e4bf232e1
--- /dev/null
+++ b/activerecord/test/cases/adapters/postgresql/optimizer_hints_test.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/post"
+
+if supports_optimizer_hints?
+ class PostgresqlOptimzerHintsTest < ActiveRecord::PostgreSQLTestCase
+ fixtures :posts
+
+ def setup
+ enable_extension!("pg_hint_plan", ActiveRecord::Base.connection)
+ end
+
+ def test_optimizer_hints
+ assert_sql(%r{\ASELECT /\*\+ SeqScan\(posts\) \*/}) do
+ posts = Post.optimizer_hints("SeqScan(posts)")
+ posts = posts.select(:id).where(author_id: [0, 1])
+ assert_includes posts.explain, "Seq Scan on posts"
+ end
+ end
+
+ def test_optimizer_hints_is_sanitized
+ assert_sql(%r{\ASELECT /\*\+ SeqScan\(posts\) \*/}) do
+ posts = Post.optimizer_hints("/*+ SeqScan(posts) */")
+ posts = posts.select(:id).where(author_id: [0, 1])
+ assert_includes posts.explain, "Seq Scan on posts"
+ end
+
+ assert_sql(%r{\ASELECT /\*\+ "posts"\.\*, \*/}) do
+ posts = Post.optimizer_hints("**// \"posts\".*, //**")
+ posts = posts.select(:id).where(author_id: [0, 1])
+ assert_equal({ "id" => 1 }, posts.first.as_json)
+ end
+ end
+
+ def test_optimizer_hints_with_unscope
+ assert_sql(%r{\ASELECT "posts"\."id"}) do
+ posts = Post.optimizer_hints("/*+ SeqScan(posts) */")
+ posts = posts.select(:id).where(author_id: [0, 1])
+ posts.unscope(:optimizer_hints).load
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/adapters/sqlite3/connection_test.rb b/activerecord/test/cases/adapters/sqlite3/connection_test.rb
deleted file mode 100644
index 3dabc8766a..0000000000
--- a/activerecord/test/cases/adapters/sqlite3/connection_test.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-# frozen_string_literal: true
-
-require "cases/helper"
-
-class SQLite3ConnectionTest < ActiveRecord::SQLite3TestCase
- fixtures :comments
-
- def test_truncate
- rows = ActiveRecord::Base.connection.exec_query("select count(*) from comments")
- count = rows.first.values.first
- assert_operator count, :>, 0
-
- ActiveRecord::Base.connection.truncate("comments")
- rows = ActiveRecord::Base.connection.exec_query("select count(*) from comments")
- count = rows.first.values.first
- assert_equal 0, count
- end
-end
diff --git a/activerecord/test/cases/arel/insert_manager_test.rb b/activerecord/test/cases/arel/insert_manager_test.rb
index 2376ad8d37..79b85742ee 100644
--- a/activerecord/test/cases/arel/insert_manager_test.rb
+++ b/activerecord/test/cases/arel/insert_manager_test.rb
@@ -11,19 +11,18 @@ module Arel
end
describe "insert" do
- it "can create a Values node" do
+ it "can create a ValuesList node" do
manager = Arel::InsertManager.new
- values = manager.create_values %w{ a b }, %w{ c d }
+ values = manager.create_values_list([%w{ a b }, %w{ c d }])
- assert_kind_of Arel::Nodes::Values, values
- assert_equal %w{ a b }, values.left
- assert_equal %w{ c d }, values.right
+ assert_kind_of Arel::Nodes::ValuesList, values
+ assert_equal [%w{ a b }, %w{ c d }], values.rows
end
it "allows sql literals" do
manager = Arel::InsertManager.new
manager.into Table.new(:users)
- manager.values = manager.create_values [Arel.sql("*")], %w{ a }
+ manager.values = manager.create_values([Arel.sql("*")])
manager.to_sql.must_be_like %{
INSERT INTO \"users\" VALUES (*)
}
@@ -186,9 +185,9 @@ module Arel
manager = Arel::InsertManager.new
manager.into table
- manager.values = Nodes::Values.new [1]
+ manager.values = Nodes::ValuesList.new([[1], [2]])
manager.to_sql.must_be_like %{
- INSERT INTO "users" VALUES (1)
+ INSERT INTO "users" VALUES (1), (2)
}
end
@@ -210,11 +209,11 @@ module Arel
manager = Arel::InsertManager.new
manager.into table
- manager.values = Nodes::Values.new [1, "aaron"]
+ manager.values = Nodes::ValuesList.new([[1, "aaron"], [2, "david"]])
manager.columns << table[:id]
manager.columns << table[:name]
manager.to_sql.must_be_like %{
- INSERT INTO "users" ("id", "name") VALUES (1, 'aaron')
+ INSERT INTO "users" ("id", "name") VALUES (1, 'aaron'), (2, 'david')
}
end
end
diff --git a/activerecord/test/cases/arel/visitors/depth_first_test.rb b/activerecord/test/cases/arel/visitors/depth_first_test.rb
index f94ad521d7..4a57608411 100644
--- a/activerecord/test/cases/arel/visitors/depth_first_test.rb
+++ b/activerecord/test/cases/arel/visitors/depth_first_test.rb
@@ -33,6 +33,7 @@ module Arel
Arel::Nodes::Ordering,
Arel::Nodes::StringJoin,
Arel::Nodes::UnqualifiedColumn,
+ Arel::Nodes::ValuesList,
Arel::Nodes::Limit,
Arel::Nodes::Else,
].each do |klass|
@@ -116,7 +117,6 @@ module Arel
Arel::Nodes::NotIn,
Arel::Nodes::Or,
Arel::Nodes::TableAlias,
- Arel::Nodes::Values,
Arel::Nodes::As,
Arel::Nodes::DeleteStatement,
Arel::Nodes::JoinSource,
diff --git a/activerecord/test/cases/arel/visitors/dot_test.rb b/activerecord/test/cases/arel/visitors/dot_test.rb
index 6b3c132f83..ade53c358e 100644
--- a/activerecord/test/cases/arel/visitors/dot_test.rb
+++ b/activerecord/test/cases/arel/visitors/dot_test.rb
@@ -37,6 +37,7 @@ module Arel
Arel::Nodes::Offset,
Arel::Nodes::Ordering,
Arel::Nodes::UnqualifiedColumn,
+ Arel::Nodes::ValuesList,
Arel::Nodes::Limit,
].each do |klass|
define_method("test_#{klass.name.gsub('::', '_')}") do
@@ -61,7 +62,6 @@ module Arel
Arel::Nodes::NotIn,
Arel::Nodes::Or,
Arel::Nodes::TableAlias,
- Arel::Nodes::Values,
Arel::Nodes::As,
Arel::Nodes::DeleteStatement,
Arel::Nodes::JoinSource,
diff --git a/activerecord/test/cases/arel/visitors/to_sql_test.rb b/activerecord/test/cases/arel/visitors/to_sql_test.rb
index 4bfa799a96..625e37f1c0 100644
--- a/activerecord/test/cases/arel/visitors/to_sql_test.rb
+++ b/activerecord/test/cases/arel/visitors/to_sql_test.rb
@@ -23,9 +23,9 @@ module Arel
sql.must_be_like "?"
end
- it "does not quote BindParams used as part of a Values" do
+ it "does not quote BindParams used as part of a ValuesList" do
bp = Nodes::BindParam.new(1)
- values = Nodes::Values.new([bp])
+ values = Nodes::ValuesList.new([[bp]])
sql = compile values
sql.must_be_like "VALUES (?)"
end
diff --git a/activerecord/test/cases/bind_parameter_test.rb b/activerecord/test/cases/bind_parameter_test.rb
index eb8b45431f..85685d1d00 100644
--- a/activerecord/test/cases/bind_parameter_test.rb
+++ b/activerecord/test/cases/bind_parameter_test.rb
@@ -184,7 +184,7 @@ if ActiveRecord::Base.connection.prepared_statements
name: "SQL",
sql: "select * from topics where id = ?",
binds: binds,
- type_casted_binds: @connection.type_casted_binds(binds)
+ type_casted_binds: @connection.send(:type_casted_binds, binds)
}
event = ActiveSupport::Notifications::Event.new(
diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb
index b4f28fbfd6..0cb868da6e 100644
--- a/activerecord/test/cases/fixtures_test.rb
+++ b/activerecord/test/cases/fixtures_test.rb
@@ -209,7 +209,7 @@ class FixturesTest < ActiveRecord::TestCase
conn = ActiveRecord::Base.connection
mysql_margin = 2
packet_size = 1024
- bytes_needed_to_have_a_1024_bytes_fixture = 858
+ bytes_needed_to_have_a_1024_bytes_fixture = 906
fixtures = {
"traffic_lights" => [
{ "location" => "US", "state" => ["NY"], "long_state" => ["a" * bytes_needed_to_have_a_1024_bytes_fixture] },
@@ -496,11 +496,7 @@ class FixturesTest < ActiveRecord::TestCase
ActiveRecord::FixtureSet.create_fixtures(FIXTURES_ROOT + "/naked/yml", "parrots")
end
- if current_adapter?(:SQLite3Adapter)
- assert_equal(%(table "parrots" has no column named "arrr".), e.message)
- else
- assert_equal(%(table "parrots" has no columns named "arrr", "foobar".), e.message)
- end
+ assert_equal(%(table "parrots" has no columns named "arrr", "foobar".), e.message)
end
def test_yaml_file_with_symbol_columns
diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb
index a9ec667eba..f95d082907 100644
--- a/activerecord/test/cases/helper.rb
+++ b/activerecord/test/cases/helper.rb
@@ -64,6 +64,7 @@ end
supports_insert_on_duplicate_skip?
supports_insert_on_duplicate_update?
supports_insert_conflict_target?
+ supports_optimizer_hints?
].each do |method_name|
define_method method_name do
ActiveRecord::Base.connection.public_send(method_name)
diff --git a/activerecord/test/cases/insert_all_test.rb b/activerecord/test/cases/insert_all_test.rb
index 63245719c0..a3e920969d 100644
--- a/activerecord/test/cases/insert_all_test.rb
+++ b/activerecord/test/cases/insert_all_test.rb
@@ -136,4 +136,10 @@ class InsertAllTest < ActiveRecord::TestCase
assert_equal ["Out of the Silent Planet", "Perelandra"], Book.where(isbn: "1974522598").order(:name).pluck(:name)
end
+
+ def test_insert_all_raises_on_unknown_attribute
+ assert_raise ActiveRecord::UnknownAttributeError do
+ Book.insert_all! [{ unknown_attribute: "Test" }]
+ end
+ end
end
diff --git a/activerecord/test/cases/invertible_migration_test.rb b/activerecord/test/cases/invertible_migration_test.rb
index 6cf17ac15d..d68cc40107 100644
--- a/activerecord/test/cases/invertible_migration_test.rb
+++ b/activerecord/test/cases/invertible_migration_test.rb
@@ -308,6 +308,8 @@ module ActiveRecord
migration2 = DisableExtension1.new
migration3 = DisableExtension2.new
+ assert_equal true, Horse.connection.extension_available?("hstore")
+
migration1.migrate(:up)
migration2.migrate(:up)
assert_equal true, Horse.connection.extension_enabled?("hstore")