diff options
Diffstat (limited to 'activerecord/lib/active_record/connection_adapters')
6 files changed, 277 insertions, 68 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 646a78622c..6178130c00 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -3,8 +3,16 @@ module ActiveRecord module DatabaseStatements # Returns an array of record hashes with the column names as keys and # column values as values. - def select_all(sql, name = nil) - select(sql, name) + def select_all(sql, name = nil, binds = []) + if supports_statement_cache? + select(sql, name, binds) + else + return select(sql, name) if binds.empty? + binds = binds.dup + select sql.gsub('?') { + quote(*binds.shift.reverse) + }, name + end end # Returns a record hash with the column names as keys and column values @@ -39,6 +47,12 @@ module ActiveRecord end undef_method :execute + # Executes +sql+ statement in the context of this connection using + # +binds+ as the bind substitutes. +name+ is logged along with + # the executed +sql+ statement. + def exec(sql, name = 'SQL', binds = []) + end + # Returns the last auto-generated ID from the affected table. def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) insert_sql(sql, name, pk, id_value, sequence_name) @@ -68,6 +82,12 @@ module ActiveRecord nil end + # Returns +true+ when the connection adapter supports prepared statement + # caching, otherwise returns +false+ + def supports_statement_cache? + false + end + # Runs the given block in a database transaction, and returns the result # of the block. # @@ -254,7 +274,7 @@ module ActiveRecord protected # Returns an array of record hashes with the column names as keys and # column values as values. - def select(sql, name = nil) + def select(sql, name = nil, binds = []) end undef_method :select diff --git a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb index 0ee61d0b6f..d555308485 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb @@ -1,5 +1,3 @@ -require 'active_support/core_ext/object/duplicable' - module ActiveRecord module ConnectionAdapters # :nodoc: module QueryCache @@ -49,32 +47,26 @@ module ActiveRecord @query_cache.clear end - def select_all(*args) + def select_all(sql, name = nil, binds = []) if @query_cache_enabled - cache_sql(args.first) { super } + cache_sql(sql, binds) { super } else super end end private - def cache_sql(sql) + def cache_sql(sql, binds) result = - if @query_cache.has_key?(sql) + if @query_cache[sql].key?(binds) ActiveSupport::Notifications.instrument("sql.active_record", :sql => sql, :name => "CACHE", :connection_id => self.object_id) - @query_cache[sql] + @query_cache[sql][binds] else - @query_cache[sql] = yield + @query_cache[sql][binds] = yield end - if Array === result - result.collect { |row| row.dup } - else - result.duplicable? ? result.dup : result - end - rescue TypeError - result + result.collect { |row| row.dup } end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index d8c92d0ad3..f3fba9a3a9 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -12,6 +12,7 @@ require 'active_record/connection_adapters/abstract/connection_pool' require 'active_record/connection_adapters/abstract/connection_specification' require 'active_record/connection_adapters/abstract/query_cache' require 'active_record/connection_adapters/abstract/database_limits' +require 'active_record/result' module ActiveRecord module ConnectionAdapters # :nodoc: @@ -40,7 +41,7 @@ module ActiveRecord @active = nil @connection, @logger = connection, logger @query_cache_enabled = false - @query_cache = {} + @query_cache = Hash.new { |h,sql| h[sql] = {} } @instrumenter = ActiveSupport::Notifications.instrumenter end @@ -97,6 +98,12 @@ module ActiveRecord quote_column_name(name) end + # Returns a bind substitution value given a +column+ and list of current + # +binds+ + def substitute_for(column, binds) + Arel.sql '?' + end + # REFERENTIAL INTEGRITY ==================================== # Override to turn off referential integrity while executing <tt>&block</tt>. @@ -135,6 +142,13 @@ module ActiveRecord # this should be overridden by concrete adapters end + ### + # Clear any caching the database adapter may be doing, for example + # clearing the prepared statement cache. This is database specific. + def clear_cache! + # this should be overridden by concrete adapters + end + # Returns true if its required to reload the connection between requests for development mode. # This is not the case for Ruby/MySQL and it's not necessary for any adapters except SQLite. def requires_reloading? diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index a4b336dfaf..7d47d06ae1 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -3,6 +3,28 @@ require 'active_support/core_ext/kernel/requires' require 'active_support/core_ext/object/blank' require 'set' +begin + require 'mysql' +rescue LoadError + raise "!!! Missing the mysql gem. Add it to your Gemfile: gem 'mysql'" +end + +unless defined?(Mysql::Result) && Mysql::Result.method_defined?(:each_hash) + raise "!!! Outdated mysql gem. Upgrade to 2.8.1 or later. In your Gemfile: gem 'mysql', '2.8.1'. Or use gem 'mysql2'" +end + +class Mysql + class Time + ### + # This monkey patch is for test_additional_columns_from_join_table + def to_date + Date.new(year, month, day) + end + end + class Stmt; include Enumerable end + class Result; include Enumerable end +end + module ActiveRecord class Base # Establishes a connection to the database that's used by all Active Record objects. @@ -15,18 +37,6 @@ module ActiveRecord password = config[:password].to_s database = config[:database] - unless defined? Mysql - begin - require 'mysql' - rescue LoadError - raise "!!! Missing the mysql2 gem. Add it to your Gemfile: gem 'mysql2'" - end - - unless defined?(Mysql::Result) && Mysql::Result.method_defined?(:each_hash) - raise "!!! Outdated mysql gem. Upgrade to 2.8.1 or later. In your Gemfile: gem 'mysql', '2.8.1'. Or use gem 'mysql2'" - end - end - mysql = Mysql.init mysql.ssl_set(config[:sslkey], config[:sslcert], config[:sslca], config[:sslcapath], config[:sslcipher]) if config[:sslca] || config[:sslkey] @@ -39,6 +49,30 @@ module ActiveRecord module ConnectionAdapters class MysqlColumn < Column #:nodoc: + class << self + def string_to_time(value) + return super unless Mysql::Time === value + new_time( + value.year, + value.month, + value.day, + value.hour, + value.minute, + value.second, + value.second_part) + end + + def string_to_dummy_time(v) + return super unless Mysql::Time === v + new_time(2000, 01, 01, v.hour, v.minute, v.second, v.second_part) + end + + def string_to_date(v) + return super unless Mysql::Time === v + new_date(v.year, v.month, v.day) + end + end + def extract_default(default) if sql_type =~ /blob/i || type == :text if default.blank? @@ -161,6 +195,7 @@ module ActiveRecord super(connection, logger) @connection_options, @config = connection_options, config @quoted_column_names, @quoted_table_names = {}, {} + @statements = {} connect end @@ -168,6 +203,12 @@ module ActiveRecord ADAPTER_NAME end + # Returns +true+ when the connection adapter supports prepared statement + # caching, otherwise returns +false+ + def supports_statement_cache? + true + end + def supports_migrations? #:nodoc: true end @@ -252,6 +293,7 @@ module ActiveRecord def reconnect! disconnect! + clear_cache! connect end @@ -272,14 +314,63 @@ module ActiveRecord def select_rows(sql, name = nil) @connection.query_with_result = true - result = execute(sql, name) - rows = [] - result.each { |row| rows << row } - result.free + rows = exec_without_stmt(sql, name).rows @connection.more_results && @connection.next_result # invoking stored procedures with CLIENT_MULTI_RESULTS requires this to tidy up else connection will be dropped rows end + def clear_cache! + @statements.values.each do |cache| + cache[:stmt].close + end + @statements.clear + end + + def exec(sql, name = 'SQL', binds = []) + log(sql, name) do + result = nil + + cache = {} + if binds.empty? + stmt = @connection.prepare(sql) + else + cache = @statements[sql] ||= { + :stmt => @connection.prepare(sql) + } + stmt = cache[:stmt] + end + + stmt.execute(*binds.map { |col, val| + col ? col.type_cast(val) : val + }) + if metadata = stmt.result_metadata + cols = cache[:cols] ||= metadata.fetch_fields.map { |field| + field.name + } + + metadata.free + result = ActiveRecord::Result.new(cols, stmt.to_a) + end + + stmt.free_result + stmt.close if binds.empty? + + result + end + end + + def exec_without_stmt(sql, name = 'SQL') # :nodoc: + # Some queries, like SHOW CREATE TABLE don't work through the prepared + # statement API. For those queries, we need to use this method. :'( + log(sql, name) do + result = @connection.query(sql) + cols = result.fetch_fields.map { |field| field.name } + rows = result.to_a + result.free + ActiveRecord::Result.new(cols, rows) + end + end + # Executes an SQL query and returns a MySQL::Result object. Note that you have to free # the Result object after you're done using it. def execute(sql, name = nil) #:nodoc: @@ -308,7 +399,7 @@ module ActiveRecord end def begin_db_transaction #:nodoc: - execute "BEGIN" + exec_without_stmt "BEGIN" rescue Exception # Transactions aren't supported end @@ -360,7 +451,8 @@ module ActiveRecord select_all(sql).map do |table| table.delete('Table_type') - select_one("SHOW CREATE TABLE #{quote_table_name(table.to_a.first.last)}")["Create Table"] + ";\n\n" + sql = "SHOW CREATE TABLE #{quote_table_name(table.to_a.first.last)}" + exec_without_stmt(sql).first['Create Table'] + ";\n\n" end.join("") end @@ -614,12 +706,9 @@ module ActiveRecord execute("SET SQL_AUTO_IS_NULL=0", :skip_logging) end - def select(sql, name = nil) + def select(sql, name = nil, binds = []) @connection.query_with_result = true - result = execute(sql, name) - rows = [] - result.each_hash { |row| rows << row } - result.free + rows = exec(sql, name, binds).to_a @connection.more_results && @connection.next_result # invoking stored procedures with CLIENT_MULTI_RESULTS requires this to tidy up else connection will be dropped rows end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 5949985e4d..58c0f85dc2 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -211,6 +211,12 @@ module ActiveRecord ADAPTER_NAME end + # Returns +true+ when the connection adapter supports prepared statement + # caching, otherwise returns +false+ + def supports_statement_cache? + true + end + # Initializes and connects a PostgreSQL adapter. def initialize(connection, logger, connection_parameters, config) super(connection, logger) @@ -220,11 +226,19 @@ module ActiveRecord @local_tz = nil @table_alias_length = nil @postgresql_version = nil + @statements = {} connect @local_tz = execute('SHOW TIME ZONE').first["TimeZone"] end + def clear_cache! + @statements.each_value do |value| + @connection.query "DEALLOCATE #{value}" + end + @statements.clear + end + # Is this connection alive and ready for queries? def active? if @connection.respond_to?(:status) @@ -242,6 +256,7 @@ module ActiveRecord # Close then reopen the connection. def reconnect! if @connection.respond_to?(:reset) + clear_cache! @connection.reset configure_connection else @@ -250,8 +265,14 @@ module ActiveRecord end end + def reset! + clear_cache! + super + end + # Close the connection. def disconnect! + clear_cache! @connection.close rescue nil end @@ -374,6 +395,12 @@ module ActiveRecord end end + # Set the authorized user for this session + def session_auth=(user) + clear_cache! + exec "SET SESSION AUTHORIZATION #{user}" + end + # REFERENTIAL INTEGRITY ==================================== def supports_disable_referential_integrity?() #:nodoc: @@ -501,6 +528,35 @@ module ActiveRecord end end + def substitute_for(column, current_values) + Arel.sql("$#{current_values.length + 1}") + end + + def exec(sql, name = 'SQL', binds = []) + return exec_no_cache(sql, name) if binds.empty? + + log(sql, name) do + unless @statements.key? sql + nextkey = "a#{@statements.length + 1}" + @connection.prepare nextkey, sql + @statements[sql] = nextkey + end + + key = @statements[sql] + + # Clear the queue + @connection.get_last_result + @connection.send_query_prepared(key, binds.map { |col, val| + col ? col.type_cast(val) : val + }) + @connection.block + result = @connection.get_last_result + ret = ActiveRecord::Result.new(result.fields, result_as_array(result)) + result.clear + return ret + end + end + # Executes an UPDATE query and returns the number of affected tuples. def update_sql(sql, name = nil) super.cmd_tuples @@ -920,6 +976,15 @@ module ActiveRecord end private + def exec_no_cache(sql, name) + log(sql, name) do + result = @connection.async_exec(sql) + ret = ActiveRecord::Result.new(result.fields, result_as_array(result)) + result.clear + ret + end + end + # The internal PostgreSQL identifier of the money data type. MONEY_COLUMN_TYPE_OID = 790 #:nodoc: # The internal PostgreSQL identifier of the BYTEA data type. @@ -974,11 +1039,8 @@ module ActiveRecord # Executes a SELECT query and returns the results, performing any data type # conversions that are required to be performed here instead of in PostgreSQLColumn. - def select(sql, name = nil) - fields, rows = select_raw(sql, name) - rows.map do |row| - Hash[fields.zip(row)] - end + def select(sql, name = nil, binds = []) + exec(sql, name, binds).to_a end def select_raw(sql, name = nil) diff --git a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb index 69bf2c0dcd..fc4d84a4f2 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb @@ -50,6 +50,7 @@ module ActiveRecord def initialize(connection, logger, config) super(connection, logger) + @statements = {} @config = config end @@ -61,6 +62,12 @@ module ActiveRecord sqlite_version >= '2.0.0' end + # Returns +true+ when the connection adapter supports prepared statement + # caching, otherwise returns +false+ + def supports_statement_cache? + true + end + def supports_migrations? #:nodoc: true end @@ -79,9 +86,14 @@ module ActiveRecord def disconnect! super + clear_cache! @connection.close rescue nil end + def clear_cache! + @statements.clear + end + def supports_count_distinct? #:nodoc: sqlite_version >= '3.2.6' end @@ -131,6 +143,29 @@ module ActiveRecord # DATABASE STATEMENTS ====================================== + def exec(sql, name = nil, binds = []) + log(sql, name) do + + # Don't cache statements without bind values + if binds.empty? + stmt = @connection.prepare(sql) + cols = stmt.columns + else + cache = @statements[sql] ||= { + :stmt => @connection.prepare(sql) + } + stmt = cache[:stmt] + cols = cache[:cols] ||= stmt.columns + stmt.reset! + stmt.bind_params binds.map { |col, val| + col ? col.type_cast(val) : val + } + end + + ActiveRecord::Result.new(cols, stmt.to_a) + end + end + def execute(sql, name = nil) #:nodoc: log(sql, name) { @connection.execute(sql) } end @@ -151,9 +186,7 @@ module ActiveRecord alias :create :insert_sql def select_rows(sql, name = nil) - execute(sql, name).map do |row| - (0...(row.size / 2)).map { |i| row[i] } - end + exec(sql, name).rows end def begin_db_transaction #:nodoc: @@ -177,7 +210,7 @@ module ActiveRecord WHERE type = 'table' AND NOT name = 'sqlite_sequence' SQL - execute(sql, name).map do |row| + exec(sql, name).map do |row| row['name'] end end @@ -189,12 +222,12 @@ module ActiveRecord end def indexes(table_name, name = nil) #:nodoc: - execute("PRAGMA index_list(#{quote_table_name(table_name)})", name).map do |row| + exec("PRAGMA index_list(#{quote_table_name(table_name)})", name).map do |row| IndexDefinition.new( table_name, row['name'], - row['unique'].to_i != 0, - execute("PRAGMA index_info('#{row['name']}')").map { |col| + row['unique'] != 0, + exec("PRAGMA index_info('#{row['name']}')").map { |col| col['name'] }) end @@ -202,17 +235,17 @@ module ActiveRecord def primary_key(table_name) #:nodoc: column = table_structure(table_name).find { |field| - field['pk'].to_i == 1 + field['pk'] == 1 } column && column['name'] end def remove_index!(table_name, index_name) #:nodoc: - execute "DROP INDEX #{quote_column_name(index_name)}" + exec "DROP INDEX #{quote_column_name(index_name)}" end def rename_table(name, new_name) - execute "ALTER TABLE #{quote_table_name(name)} RENAME TO #{quote_table_name(new_name)}" + exec "ALTER TABLE #{quote_table_name(name)} RENAME TO #{quote_table_name(new_name)}" end # See: http://www.sqlite.org/lang_altertable.html @@ -249,7 +282,7 @@ module ActiveRecord def change_column_null(table_name, column_name, null, default = nil) unless null || default.nil? - execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL") + exec("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL") end alter_table(table_name) do |definition| definition[column_name].null = null @@ -280,14 +313,13 @@ module ActiveRecord end protected - def select(sql, name = nil) #:nodoc: - execute(sql, name).map do |row| - record = {} - row.each do |key, value| - record[key.sub(/^"?\w+"?\./, '')] = value if key.is_a?(String) - end - record - end + def select(sql, name = nil, binds = []) #:nodoc: + result = exec(sql, name, binds) + columns = result.columns.map { |column| + column.sub(/^"?\w+"?\./, '') + } + + result.rows.map { |row| Hash[columns.zip(row)] } end def table_structure(table_name) @@ -367,11 +399,11 @@ module ActiveRecord quoted_columns = columns.map { |col| quote_column_name(col) } * ',' quoted_to = quote_table_name(to) - @connection.execute "SELECT * FROM #{quote_table_name(from)}" do |row| + exec("SELECT * FROM #{quote_table_name(from)}").each do |row| sql = "INSERT INTO #{quoted_to} (#{quoted_columns}) VALUES (" sql << columns.map {|col| quote row[column_mappings[col]]} * ', ' sql << ')' - @connection.execute sql + exec sql end end |