diff options
Diffstat (limited to 'activerecord')
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") |