diff options
Diffstat (limited to 'activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb')
-rw-r--r-- | activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb | 214 |
1 files changed, 179 insertions, 35 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 769f488469..fdc9ffa688 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord module ConnectionAdapters # :nodoc: module DatabaseStatements @@ -7,30 +9,43 @@ module ActiveRecord end # Converts an arel AST to SQL - def to_sql(arel, binds = []) - if arel.respond_to?(:ast) - collected = visitor.accept(arel.ast, collector) - collected.compile(binds, self).freeze + def to_sql(arel_or_sql_string, binds = []) + sql, _ = to_sql_and_binds(arel_or_sql_string, binds) + sql + end + + def to_sql_and_binds(arel_or_sql_string, binds = []) # :nodoc: + if arel_or_sql_string.respond_to?(:ast) + unless binds.empty? + raise "Passing bind parameters with an arel AST is forbidden. " \ + "The values must be stored on the AST directly" + end + sql, binds = visitor.compile(arel_or_sql_string.ast, collector) + [sql.freeze, binds || []] else - arel.dup.freeze + [arel_or_sql_string.dup.freeze, binds] end end + private :to_sql_and_binds # This is used in the StatementCache object. It returns an object that # can be used to query the database repeatedly. def cacheable_query(klass, arel) # :nodoc: - collected = visitor.accept(arel.ast, collector) if prepared_statements - klass.query(collected.value) + sql, binds = visitor.compile(arel.ast, collector) + query = klass.query(sql) else - klass.partial_query(collected.value) + collector = PartialQueryCollector.new + parts, binds = visitor.compile(arel.ast, collector) + query = klass.partial_query(parts) end + [query, binds] end # Returns an ActiveRecord::Result instance. def select_all(arel, name = nil, binds = [], preparable: nil) - arel, binds = binds_from_relation arel, binds - sql = to_sql(arel, binds) + arel = arel_from_relation(arel) + sql, binds = to_sql_and_binds(arel, binds) if !prepared_statements || (arel.is_a?(String) && preparable.nil?) preparable = false else @@ -51,9 +66,7 @@ module ActiveRecord # Returns a single value from a record def select_value(arel, name = nil, binds = []) - if result = select_rows(arel, name, binds).first - result.first - end + single_value_from_rows(select_rows(arel, name, binds)) end # Returns an array of the values of the first column in a select: @@ -68,6 +81,18 @@ module ActiveRecord select_all(arel, name, binds).rows end + def query_value(sql, name = nil) # :nodoc: + single_value_from_rows(query(sql, name)) + end + + def query_values(sql, name = nil) # :nodoc: + query(sql, name).map(&:first) + end + + def query(sql, name = nil) # :nodoc: + exec_query(sql, name).rows + end + # Executes the SQL statement in the context of this connection and returns # the raw result from the connection adapter. # Note: depending on your database connector, the result returned by this @@ -120,26 +145,30 @@ module ActiveRecord # If the next id was calculated in advance (as in Oracle), it should be # passed in as +id_value+. def insert(arel, name = nil, pk = nil, id_value = nil, sequence_name = nil, binds = []) - value = exec_insert(to_sql(arel, binds), name, binds, pk, sequence_name) + sql, binds = to_sql_and_binds(arel, binds) + value = exec_insert(sql, name, binds, pk, sequence_name) id_value || last_inserted_id(value) end alias create insert # Executes the update statement and returns the number of rows affected. def update(arel, name = nil, binds = []) - exec_update(to_sql(arel, binds), name, binds) + sql, binds = to_sql_and_binds(arel, binds) + exec_update(sql, name, binds) end # Executes the delete statement and returns the number of rows affected. def delete(arel, name = nil, binds = []) - exec_delete(to_sql(arel, binds), name, binds) + sql, binds = to_sql_and_binds(arel, binds) + exec_delete(sql, name, binds) end # Returns +true+ when the connection adapter supports prepared statement # caching, otherwise returns +false+ - def supports_statement_cache? - false + def supports_statement_cache? # :nodoc: + true end + deprecate :supports_statement_cache? # Runs the given block in a database transaction, and returns the result # of the block. @@ -152,7 +181,7 @@ module ActiveRecord # # In order to get around this problem, #transaction will emulate the effect # of nested transactions, by using savepoints: - # http://dev.mysql.com/doc/refman/5.7/en/savepoint.html + # https://dev.mysql.com/doc/refman/5.7/en/savepoint.html # Savepoints are supported by MySQL and PostgreSQL. SQLite3 version >= '3.6.8' # supports savepoints. # @@ -204,7 +233,7 @@ module ActiveRecord # You should consult the documentation for your database to understand the # semantics of these different levels: # - # * http://www.postgresql.org/docs/current/static/transaction-iso.html + # * https://www.postgresql.org/docs/current/static/transaction-iso.html # * https://dev.mysql.com/doc/refman/5.7/en/set-transaction.html # # An ActiveRecord::TransactionIsolationError will be raised if: @@ -230,7 +259,9 @@ module ActiveRecord attr_reader :transaction_manager #:nodoc: - delegate :within_new_transaction, :open_transactions, :current_transaction, :begin_transaction, :commit_transaction, :rollback_transaction, to: :transaction_manager + delegate :within_new_transaction, :open_transactions, :current_transaction, :begin_transaction, + :commit_transaction, :rollback_transaction, :materialize_transactions, + :disable_lazy_transactions!, :enable_lazy_transactions!, to: :transaction_manager def transaction_open? current_transaction.open? @@ -295,6 +326,9 @@ module ActiveRecord # Inserts the given fixture into the table. Overridden in adapters that require # something beyond a simple insert (eg. Oracle). + # Most of adapters should implement `insert_fixtures` that leverages bulk SQL insert. + # 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 @@ -307,19 +341,53 @@ module ActiveRecord raise Fixture::FixtureError, %(table "#{table_name}" has no column named #{name.inspect}.) end end - key_list = fixture.keys.map { |name| quote_column_name(name) } - value_list = binds.map(&:value_for_database).map do |value| - begin - quote(value) - rescue TypeError - quote(YAML.dump(value)) - 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 - execute "INSERT INTO #{quote_table_name(table_name)} (#{key_list.join(', ')}) VALUES (#{value_list.join(', ')})", "Fixture Insert" + manager = Arel::InsertManager.new + manager.into(table) + manager.insert(values) + execute manager.to_sql, "Fixture Insert" + end + + # Inserts a set of fixtures into the table. Overridden in adapters that require + # something beyond a simple insert (eg. Oracle). + def insert_fixtures(fixtures, table_name) + ActiveSupport::Deprecation.warn(<<-MSG.squish) + `insert_fixtures` is deprecated and will be removed in the next version of Rails. + Consider using `insert_fixtures_set` for performance improvement. + MSG + return if fixtures.empty? + + execute(build_fixture_sql(fixtures, table_name), "Fixtures 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}".dup } + 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? + end + end + end end - def empty_insert_statement_value + def empty_insert_statement_value(primary_key = nil) "DEFAULT VALUES" end @@ -348,6 +416,44 @@ module ActiveRecord alias join_to_delete join_to_update private + def default_insert_value(column) + Arel.sql("DEFAULT") + end + + def build_fixture_sql(fixtures, table_name) + columns = schema_cache.columns_hash(table_name) + + values = fixtures.map do |fixture| + fixture = fixture.stringify_keys + + unknown_columns = fixture.keys - columns.keys + if unknown_columns.any? + raise Fixture::FixtureError, %(table "#{table_name}" has no columns named #{unknown_columns.map(&:inspect).join(', ')}.) + end + + columns.map do |name, column| + if fixture.key?(name) + type = lookup_cast_type_from_column(column) + bind = Relation::QueryAttribute.new(name, fixture[name], type) + with_yaml_fallback(bind.value_for_database) + else + default_insert_value(column) + end + end + end + + 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) + + manager.to_sql + end + + def combine_multi_statements(total_sql) + total_sql.join(";\n") + end # Returns a subquery for the given key using the join information. def subquery_for(key, select) @@ -370,15 +476,53 @@ module ActiveRecord end def last_inserted_id(result) - row = result.rows.first + single_value_from_rows(result.rows) + end + + def single_value_from_rows(rows) + row = rows.first row && row.first end - def binds_from_relation(relation, binds) - if relation.is_a?(Relation) && binds.empty? - relation, binds = relation.arel, relation.bound_attributes + def arel_from_relation(relation) + if relation.is_a?(Relation) + relation.arel + else + relation + end + end + + # Fixture value is quoted by Arel, however scalar values + # are not quotable. In this case we want to convert + # the column value to YAML. + def with_yaml_fallback(value) + if value.is_a?(Hash) || value.is_a?(Array) + YAML.dump(value) + else + value + end + end + + class PartialQueryCollector + def initialize + @parts = [] + @binds = [] + end + + def <<(str) + @parts << str + self + end + + def add_bind(obj) + @binds << obj + @parts << Arel::Nodes::BindParam.new(1) + self + end + + def value + [@parts, @binds] end - [relation, binds] end end end |