diff options
Diffstat (limited to 'activerecord/lib')
23 files changed, 133 insertions, 53 deletions
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..bfd1c8402c 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -144,7 +144,11 @@ module ActiveRecord # Executes the truncate statement. def truncate(table_name, name = nil) - raise NotImplementedError + execute "TRUNCATE TABLE #{quote_table_name(table_name)}", name + end + + def truncate_tables(*table_names) # :nodoc: + table_names.each { |table_name| truncate(table_name) } end # Executes update +sql+ statement in the context of this connection using 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..5ceddf449c 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) 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..208934385f 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb @@ -143,6 +143,12 @@ module ActiveRecord end end + def truncate_tables(*table_names) # :nodoc: + unless table_names.empty? + execute "TRUNCATE TABLE #{table_names.map(&method(:quote_table_name)).join(", ")}" + end + end + # Begins a transaction. def begin_db_transaction execute "BEGIN" 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_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index 3004caf82d..8ee7e4c763 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -155,10 +155,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 @@ -279,6 +275,10 @@ module ActiveRecord end end + def truncate(table_name, name = nil) # :nodoc: + execute "DELETE FROM #{quote_table_name(table_name)}", name + end + def begin_db_transaction #:nodoc: log("begin transaction", nil) { @connection.transaction } end diff --git a/activerecord/lib/active_record/insert_all.rb b/activerecord/lib/active_record/insert_all.rb index 5baaea344b..3833cb2fcf 100644 --- a/activerecord/lib/active_record/insert_all.rb +++ b/activerecord/lib/active_record/insert_all.rb @@ -92,7 +92,15 @@ module ActiveRecord def values_list columns = connection.schema_cache.columns_hash(model.table_name) + + column_names = columns.keys.to_set keys = insert_all.keys.to_set + unknown_columns = keys - column_names + + unless unknown_columns.empty? + raise UnknownAttributeError.new(model.new, unknown_columns.first) + end + types = keys.map { |key| [ key, connection.lookup_cast_type_from_column(columns[key]) ] }.to_h values_list = insert_all.inserts.map do |attributes| 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..2603fac8b2 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -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..5a46003732 100644 --- a/activerecord/lib/active_record/tasks/database_tasks.rb +++ b/activerecord/lib/active_record/tasks/database_tasks.rb @@ -185,14 +185,17 @@ 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.disable_referential_integrity do + ActiveRecord::Base.connection.truncate_tables(*table_names) + end unless table_names.empty? 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/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/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..892d670c79 100644 --- a/activerecord/lib/arel/visitors/depth_first.rb +++ b/activerecord/lib/arel/visitors/depth_first.rb @@ -35,6 +35,7 @@ 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 def function(o) visit o.expressions diff --git a/activerecord/lib/arel/visitors/dot.rb b/activerecord/lib/arel/visitors/dot.rb index 6389c875cb..ffcbb7a7ab 100644 --- a/activerecord/lib/arel/visitors/dot.rb +++ b/activerecord/lib/arel/visitors/dot.rb @@ -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..7e3e265208 100644 --- a/activerecord/lib/arel/visitors/to_sql.rb +++ b/activerecord/lib/arel/visitors/to_sql.rb @@ -219,6 +219,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 +237,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 +804,14 @@ module Arel # :nodoc: all @connection.quote_column_name(name) end + def sanitize_as_sql_comment(o) + o.expr.map { |v| v.gsub(%r{ /\*\+?\s* | \s*\*/ }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 << " " |